Skip to content

Best Practices ​

This guide covers recommended patterns and practices for using Acquiescence effectively.

General Principles ​

1. Always Check Before Acting ​

Never assume an element is ready for interaction. Always verify its state first:

typescript
// ❌ Bad: Assuming element is ready
button.click();

// ✅ Good: Verify before acting
const result = await inspector.queryElementStates(button, ['visible', 'enabled']);
if (result.status === 'success') {
  button.click();
}

2. Use Appropriate Timeouts ​

Choose timeouts based on expected behavior:

typescript
// Fast UI updates: 1-2 seconds
await inspector.waitForInteractionReady(quickButton, 'click', 2000);

// Slow network requests: 5-10 seconds
await inspector.waitForInteractionReady(loadingButton, 'click', 10000);

// Complex page loads: 15-30 seconds
await inspector.waitForInteractionReady(slowPage, 'click', 30000);

3. Reuse Inspector Instances ​

Create one inspector and reuse it throughout your application:

typescript
// ✅ Good: Single instance
class TestRunner {
  private inspector = new ElementStateInspector();
  
  async clickButton(selector: string) {
    const button = document.querySelector(selector);
    await this.inspector.waitForInteractionReady(button, 'click', 5000);
  }
}

// ❌ Avoid: Creating multiple instances
async function clickButton(selector: string) {
  const inspector = new ElementStateInspector(); // Creates new caches each time
  // ...
}

4. Handle Errors Gracefully ​

Always wrap interaction waits in try-catch blocks:

typescript
async function safeClick(element: Element) {
  try {
    const hitPoint = await inspector.waitForInteractionReady(
      element,
      'click',
      5000
    );
    element.click();
    return true;
  } catch (error) {
    console.error('Failed to click element:', error.message);
    return false;
  }
}

Element Selection ​

Use Specific Selectors ​

Prefer specific selectors that uniquely identify elements:

typescript
// ❌ Ambiguous
const button = document.querySelector('button');

// ✅ Specific
const button = document.querySelector('#submit-form-button');
const button = document.querySelector('[data-testid="submit-button"]');

Handle Dynamic Elements ​

For elements that might not exist yet:

typescript
async function waitForElement(
  selector: string,
  timeout: number = 5000
): Promise<Element> {
  const start = Date.now();
  
  while (Date.now() - start < timeout) {
    const element = document.querySelector(selector);
    if (element) return element;
    
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  
  throw new Error(`Element ${selector} not found within timeout`);
}

// Usage
const button = await waitForElement('#dynamic-button');
await inspector.waitForInteractionReady(button, 'click', 5000);

State Checking ​

Check Only Necessary States ​

Don't check more states than you need:

typescript
// ❌ Unnecessary checks
await inspector.queryElementStates(
  staticElement,
  ['visible', 'enabled', 'stable', 'inview']
);

// ✅ Only what's needed
await inspector.queryElementStates(staticElement, ['visible', 'enabled']);

Order States by Likelihood of Failure ​

Put states most likely to fail first (except stable, which is always checked first):

typescript
// Check visibility before enabled state
await inspector.queryElementStates(element, ['visible', 'enabled', 'stable']);

Use Helper Methods When Appropriate ​

For simple synchronous checks, use helpers:

typescript
// ❌ Overkill for simple check
const result = await inspector.queryElementState(element, 'visible');
if (result.matches) { /* ... */ }

// ✅ Use helper for synchronous check
if (inspector.isElementVisible(element)) {
  // ...
}

Interaction Patterns ​

Wait Once, Act Immediately ​

Don't wait multiple times for the same element:

typescript
// ❌ Wasteful: Multiple waits
await inspector.waitForInteractionReady(button, 'click', 5000);
const rect = await inspector.getElementInViewPortRect(button);
button.click();

// ✅ Wait once, then act
const hitPoint = await inspector.waitForInteractionReady(button, 'click', 5000);
button.click();

Batch Element Checks ​

Check multiple elements in parallel:

typescript
// ❌ Sequential checks
for (const button of buttons) {
  await inspector.waitForInteractionReady(button, 'click', 5000);
}

// ✅ Parallel checks
await Promise.all(
  buttons.map(button =>
    inspector.waitForInteractionReady(button, 'click', 5000)
  )
);

Verify After State Changes ​

After DOM mutations, re-verify element states:

typescript
// Trigger state change
await toggleButton.click();

// Wait for target to reflect the change
await inspector.waitForInteractionReady(targetElement, 'click', 2000);

Performance Optimization ​

Minimize State Queries ​

Cache results when possible:

typescript
class ElementCache {
  private states = new Map<Element, { timestamp: number, visible: boolean }>();
  
  async isVisible(element: Element): Promise<boolean> {
    const cached = this.states.get(element);
    
    // Cache valid for 100ms
    if (cached && Date.now() - cached.timestamp < 100) {
      return cached.visible;
    }
    
    const result = await inspector.queryElementState(element, 'visible');
    this.states.set(element, {
      timestamp: Date.now(),
      visible: result.matches
    });
    
    return result.matches;
  }
}

Use Appropriate Interaction Types ​

Choose the interaction type that matches your needs:

typescript
// ❌ Checks editability unnecessarily
await inspector.waitForInteractionReady(button, 'type', 5000);

// ✅ Use correct interaction type
await inspector.waitForInteractionReady(button, 'click', 5000);

Avoid Unnecessary Stability Checks ​

Skip stability for known-static elements:

typescript
// Static page elements don't need stability checks
const result = await inspector.queryElementStates(
  staticNavLink,
  ['visible', 'enabled'] // No 'stable'
);

Testing Best Practices ​

Create Helper Functions ​

Build reusable testing utilities:

typescript
class TestHelpers {
  constructor(private inspector: ElementStateInspector) {}
  
  async clickWhenReady(selector: string, timeout = 5000) {
    const element = document.querySelector(selector);
    if (!element) throw new Error(`Element not found: ${selector}`);
    
    await this.inspector.waitForInteractionReady(element, 'click', timeout);
    element.click();
  }
  
  async typeWhenReady(selector: string, text: string, timeout = 5000) {
    const element = document.querySelector(selector);
    if (!element) throw new Error(`Element not found: ${selector}`);
    
    await this.inspector.waitForInteractionReady(element, 'type', timeout);
    (element as HTMLInputElement).value = text;
    element.dispatchEvent(new Event('input', { bubbles: true }));
  }
  
  async isVisible(selector: string): Promise<boolean> {
    const element = document.querySelector(selector);
    return element ? this.inspector.isElementVisible(element) : false;
  }
}

Use Descriptive Error Messages ​

Provide context in error messages:

typescript
async function clickButton(name: string) {
  const button = document.querySelector(`[aria-label="${name}"]`);
  
  if (!button) {
    throw new Error(`Button "${name}" not found in DOM`);
  }
  
  try {
    await inspector.waitForInteractionReady(button, 'click', 5000);
    button.click();
  } catch (error) {
    throw new Error(
      `Failed to click "${name}" button: ${error.message}`
    );
  }
}

Wait for Side Effects ​

After interactions, wait for expected side effects:

typescript
async function submitForm() {
  const submitButton = document.querySelector('#submit');
  
  // Click submit
  await inspector.waitForInteractionReady(submitButton, 'click', 5000);
  submitButton.click();
  
  // Wait for success message to appear
  const successMsg = await waitForElement('.success-message', 5000);
  
  // Verify success message is visible
  const result = await inspector.queryElementState(successMsg, 'visible');
  if (!result.matches) {
    throw new Error('Success message not visible after form submission');
  }
}

Common Pitfalls ​

1. Not Checking Element Connection ​

Always verify elements are still in the DOM. waitForInteractionReady() polls a removed element until the timeout, so check the element when the wait fails:

typescript
const element = document.querySelector('#my-element');

// Element might be removed here...

try {
  await inspector.waitForInteractionReady(element, 'click', 5000);
} catch (error) {
  if (!element.isConnected) {
    console.error('Element was removed from DOM');
  }
}

2. Ignoring Shadow DOM ​

Remember to traverse into shadow roots:

typescript
// Find element in shadow DOM
const host = document.querySelector('my-component');
const button = host?.shadowRoot?.querySelector('button');

if (button) {
  await inspector.waitForInteractionReady(button, 'click', 5000);
}

// Or search the document and every open shadow root in it
const scopes = [document, ...inspector.findOpenShadowRoots([document])];

3. Not Handling Async Nature ​

Remember that most methods are async:

typescript
// ❌ Forgot await
inspector.waitForInteractionReady(button, 'click', 5000);
button.click(); // Clicks immediately, before element is ready!

// ✅ Properly awaited
await inspector.waitForInteractionReady(button, 'click', 5000);
button.click();

4. Using Wrong State for Interaction ​

Match states to your actual interaction:

typescript
// ❌ Checking 'editable' for a button click
await inspector.queryElementStates(button, ['visible', 'editable']);

// ✅ Checking appropriate states
await inspector.queryElementStates(button, ['visible', 'enabled']);

Debugging Tips ​

Log State Check Results ​

typescript
async function debugElementState(element: Element, states: ElementState[]) {
  console.log('Checking element:', element);
  
  for (const state of states) {
    if (state !== 'stable') {
      const result = await inspector.queryElementState(element, state);
      console.log(`  ${state}:`, result.matches ? '✓' : '✗', 
                  `(received: ${result.received})`);
    }
  }
  
  // Check stable separately
  if (states.includes('stable')) {
    const result = await inspector.queryElementStates(element, ['stable']);
    console.log(`  stable:`, result.status === 'success' ? '✓' : '✗');
  }
}

Capture Screenshots on Failure ​

typescript
async function clickWithScreenshot(element: Element) {
  try {
    await inspector.waitForInteractionReady(element, 'click', 5000);
    element.click();
  } catch (error) {
    // Capture state for debugging
    console.error('Click failed:', {
      visible: inspector.isElementVisible(element),
      disabled: inspector.isElementDisabled(element),
      inViewport: await inspector.isElementInViewPort(element),
      error: error.message
    });
    throw error;
  }
}

Next Steps ​

Released under the Apache License 2.0.