CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-detox

Gray box end-to-end testing and automation framework for mobile applications with advanced synchronization capabilities

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

synchronization.mddocs/

Synchronization

Advanced waiting and synchronization utilities for handling asynchronous operations, ensuring test stability, and managing timing-sensitive scenarios in mobile app testing.

Capabilities

WaitFor API

Primary waiting mechanism for elements to reach expected states before proceeding with test execution.

/**
 * Create waiting context for element state changes
 * @param element - Element to wait for
 * @returns WaitFor interface with assertion methods
 */
function waitFor(element: NativeElement): Expect<WaitFor>;

interface WaitFor {
  /**
   * Set maximum wait timeout
   * @param millis - Timeout in milliseconds
   * @returns Promise that resolves when condition is met or rejects on timeout
   */
  withTimeout(millis: number): Promise<void>;
  
  /**
   * Perform actions while waiting for condition
   * @param matcher - Matcher for element to interact with during wait
   * @returns Action interface for wait-time interactions
   */
  whileElement(matcher: NativeMatcher): NativeElementWaitableActions & WaitFor;
}

interface NativeElementWaitableActions {
  /**
   * Scroll element while waiting for condition
   * @param pixels - Distance to scroll in pixels
   * @param direction - Scroll direction
   * @param startPositionX - Starting X position (0-1, NaN for auto)
   * @param startPositionY - Starting Y position (0-1, NaN for auto)
   */
  scroll(
    pixels: number,
    direction: Direction,
    startPositionX?: number,
    startPositionY?: number
  ): Promise<void>;
  
  /**
   * Tap element while waiting for condition
   * @param point - Optional coordinates within element
   */
  tap(point?: Point2D): Promise<void>;
  
  /**
   * Long press element while waiting for condition
   * @param point - Optional coordinates within element
   * @param duration - Optional press duration in milliseconds
   */
  longPress(point?: Point2D, duration?: number): Promise<void>;
  
  /**
   * Multiple tap element while waiting for condition
   * @param times - Number of taps to perform
   */
  multiTap(times: number): Promise<void>;
  
  /**
   * Type text into element while waiting for condition
   * @param text - Text to type
   */
  typeText(text: string): Promise<void>;
  
  /**
   * Replace text in element while waiting for condition
   * @param text - Replacement text
   */
  replaceText(text: string): Promise<void>;
  
  /**
   * Clear text from element while waiting for condition
   */
  clearText(): Promise<void>;
  
  /**
   * Tap backspace key while waiting for condition
   */
  tapBackspaceKey(): Promise<void>;
  
  /**
   * Tap return key while waiting for condition
   */
  tapReturnKey(): Promise<void>;
  
  /**
   * Scroll element to edge while waiting for condition
   * @param edge - Target edge to scroll to
   * @param startPositionX - Starting X position (0-1, NaN for auto)
   * @param startPositionY - Starting Y position (0-1, NaN for auto)
   */
  scrollTo(
    edge: Direction,
    startPositionX?: number,
    startPositionY?: number
  ): Promise<void>;
  
  /**
   * Swipe element while waiting for condition
   * @param direction - Swipe direction
   * @param speed - Swipe speed
   * @param percentage - Screen percentage to swipe (0-1)
   * @param normalizedStartingPointX - Starting X position (0-1)
   * @param normalizedStartingPointY - Starting Y position (0-1)
   */
  swipe(
    direction: Direction,
    speed?: Speed,
    percentage?: number,
    normalizedStartingPointX?: number,
    normalizedStartingPointY?: number
  ): Promise<void>;
  
  /**
   * Set picker column value while waiting for condition (iOS only)
   * @param column - Column index
   * @param value - Target value string
   */
  setColumnToValue(column: number, value: string): Promise<void>;
  
  /**
   * Set date picker date while waiting for condition
   * @param dateString - Date in specified format
   * @param dateFormat - Format specification
   */
  setDatePickerDate(dateString: string, dateFormat: string): Promise<void>;
  
  /**
   * Perform accessibility action while waiting for condition
   * @param actionName - Name of accessibility action
   */
  performAccessibilityAction(actionName: string): Promise<void>;
  
  /**
   * Pinch element while waiting for condition (iOS only)
   * @param scale - Scale factor
   * @param speed - Pinch speed
   * @param angle - Rotation angle in radians
   */
  pinch(scale: number, speed?: Speed, angle?: number): Promise<void>;
}

Usage Examples:

// Basic waiting with timeout
await waitFor(element(by.id('loading_complete')))
  .toBeVisible()
  .withTimeout(10000);

// Wait for element to disappear
await waitFor(element(by.id('loading_spinner')))
  .not.toBeVisible()
  .withTimeout(5000);

// Wait for element existence
await waitFor(element(by.id('dynamic_content')))
  .toExist()
  .withTimeout(15000);

// Wait for specific text to appear
await waitFor(element(by.id('status_message')))
  .toHaveText('Process Complete')
  .withTimeout(30000);

Action-Based Waiting

Perform actions while waiting for conditions to be met, useful for infinite scroll or progressive loading scenarios.

/**
 * Perform scrolling actions while waiting for target condition
 */
interface NativeElementWaitableActions {
  scroll(
    pixels: number,
    direction: Direction,
    startPositionX?: number,
    startPositionY?: number
  ): Promise<void>;
}

Usage Examples:

// Scroll down while waiting for element to appear
await waitFor(element(by.text('Load More Button')))
  .toBeVisible()
  .whileElement(by.id('content_list'))
  .scroll(50, 'down');

// Scroll horizontally while looking for specific item
await waitFor(element(by.id('target_item')))
  .toBeVisible()
  .whileElement(by.id('horizontal_scroll'))
  .scroll(100, 'right')
  .withTimeout(20000);

// Multiple scroll actions
await waitFor(element(by.text('End of List')))
  .toBeVisible()
  .whileElement(by.id('product_list'))
  .scroll(200, 'down')
  .withTimeout(60000);

// Tap while waiting for condition
await waitFor(element(by.id('submit_button')))
  .toBeVisible()
  .whileElement(by.id('refresh_button'))
  .tap()
  .withTimeout(10000);

// Swipe while waiting
await waitFor(element(by.text('Target Item')))
  .toBeVisible()
  .whileElement(by.id('carousel'))
  .swipe('left', 'fast', 0.8)
  .withTimeout(15000);

// Type text while waiting for condition
await waitFor(element(by.id('search_results')))
  .toBeVisible()
  .whileElement(by.id('search_input'))
  .typeText('query text')
  .withTimeout(8000);

Automatic Synchronization Control

Control Detox's automatic synchronization system that monitors app state and waits for idle conditions.

/**
 * Disable automatic synchronization with app
 * WARNING: May require manual sleep() calls - use with caution
 */
async function disableSynchronization(): Promise<void>;

/**
 * Re-enable automatic synchronization with app
 * IMPORTANT: Only call when app can become idle again
 */
async function enableSynchronization(): Promise<void>;

/**
 * Configure URL patterns to ignore during synchronization
 * @param urls - Array of URL patterns (regex strings) to blacklist
 */
async function setURLBlacklist(urls: string[]): Promise<void>;

Usage Examples:

// Handle continuous animations or timers
await device.disableSynchronization();
try {
  // Perform actions in area with continuous activity
  await element(by.id('animated_element')).tap();
  await new Promise(resolve => setTimeout(resolve, 2000)); // Manual wait
  await expect(element(by.id('result'))).toBeVisible();
} finally {
  // Always re-enable synchronization
  await device.enableSynchronization();
}

// Ignore specific network endpoints
await device.setURLBlacklist([
  '.*analytics\\..*',
  '.*tracking\\..*',
  '.*127\\.0\\.0\\.1.*'
]);

Wait Conditions

All standard assertion methods can be used with waitFor for flexible waiting conditions.

/**
 * Available wait conditions (same as assertion methods)
 */
interface Expect<WaitFor> {
  toBeVisible(pct?: number): WaitFor;
  toExist(): WaitFor;
  toBeFocused(): WaitFor;
  toHaveText(text: string): WaitFor;
  toHaveLabel(label: string): WaitFor;
  toHaveId(id: string): WaitFor;
  toHaveValue(value: any): WaitFor;
  toHaveToggleValue(value: boolean): WaitFor;
  not: Expect<WaitFor>;
}

Usage Examples:

// Wait for visibility with specific percentage
await waitFor(element(by.id('partial_modal')))
  .toBeVisible(50)
  .withTimeout(8000);

// Wait for focus state
await waitFor(element(by.id('auto_focus_input')))
  .toBeFocused()
  .withTimeout(3000);

// Wait for specific value
await waitFor(element(by.id('counter_display')))
  .toHaveValue('100')
  .withTimeout(15000);

// Wait for toggle state change
await waitFor(element(by.id('network_status')))
  .toHaveToggleValue(true)
  .withTimeout(10000);

// Wait for text update
await waitFor(element(by.id('status_label')))
  .toHaveText('Sync Complete')
  .withTimeout(20000);

Timeout Management

Configure appropriate timeouts based on expected operation duration and system performance.

Timeout Guidelines:

// Quick UI updates (1-3 seconds)
await waitFor(element(by.id('button_press_feedback')))
  .toBeVisible()
  .withTimeout(3000);

// Standard network operations (5-10 seconds)
await waitFor(element(by.id('api_response_data')))
  .toBeVisible()
  .withTimeout(10000);

// Complex operations or slow networks (15-30 seconds)
await waitFor(element(by.id('file_upload_complete')))
  .toBeVisible()
  .withTimeout(30000);

// Background processing (30+ seconds)
await waitFor(element(by.id('batch_processing_done')))
  .toBeVisible()
  .withTimeout(60000);

Error Handling and Debugging

Handle timeout errors and provide meaningful debugging information.

// Timeout error handling
try {
  await waitFor(element(by.id('expected_element')))
    .toBeVisible()
    .withTimeout(5000);
} catch (error) {
  console.log('Element failed to appear:', error.message);
  
  // Take screenshot for debugging
  const screenshot = await device.takeScreenshot('timeout_failure');
  console.log('Screenshot saved:', screenshot);
  
  // Check if element exists but not visible
  try {
    await expect(element(by.id('expected_element'))).toExist();
    console.log('Element exists but not visible');
  } catch {
    console.log('Element does not exist in hierarchy');
  }
}

// Gradual timeout strategy
async function waitWithFallback(elementMatcher, primaryTimeout = 10000, fallbackTimeout = 30000) {
  try {
    await waitFor(element(elementMatcher))
      .toBeVisible()
      .withTimeout(primaryTimeout);
  } catch (error) {
    console.log('Primary timeout reached, trying longer wait...');
    await waitFor(element(elementMatcher))
      .toBeVisible()
      .withTimeout(fallbackTimeout);
  }
}

Advanced Synchronization Patterns

Common patterns for complex synchronization scenarios.

Progressive Loading

// Handle progressive content loading
async function waitForAllContent() {
  let previousCount = 0;
  let stableCount = 0;
  
  while (stableCount < 3) { // Wait for 3 stable counts
    await waitFor(element(by.id('content_list')))
      .toBeVisible()
      .withTimeout(5000);
    
    const items = await element(by.type('ListItem')).getAttributes();
    const currentCount = items.elements ? items.elements.length : 1;
    
    if (currentCount === previousCount) {
      stableCount++;
    } else {
      stableCount = 0;
      previousCount = currentCount;
    }
    
    if (stableCount === 0) {
      // Trigger more loading
      await element(by.id('content_list')).scroll(200, 'down');
    }
    
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
}

State Transitions

// Wait for multi-state transitions
async function waitForCompleteStateTransition() {
  // Wait for loading to start
  await waitFor(element(by.id('loading_indicator')))
    .toBeVisible()
    .withTimeout(5000);
  
  // Wait for loading to complete
  await waitFor(element(by.id('loading_indicator')))
    .not.toBeVisible()
    .withTimeout(30000);
  
  // Wait for results to appear
  await waitFor(element(by.id('results_container')))
    .toBeVisible()
    .withTimeout(10000);
}

Conditional Waiting

// Wait for one of multiple possible outcomes
async function waitForAnyOutcome(timeoutMs = 15000) {
  const outcomes = [
    { matcher: by.id('success_message'), type: 'success' },
    { matcher: by.id('error_message'), type: 'error' },
    { matcher: by.id('warning_message'), type: 'warning' }
  ];
  
  const promises = outcomes.map(outcome => 
    waitFor(element(outcome.matcher))
      .toBeVisible()
      .withTimeout(timeoutMs)
      .then(() => outcome.type)
      .catch(() => null)
  );
  
  const results = await Promise.allSettled(promises);
  const successfulResult = results.find(result => 
    result.status === 'fulfilled' && result.value !== null
  );
  
  if (successfulResult) {
    return successfulResult.value;
  } else {
    throw new Error('None of the expected outcomes appeared within timeout');
  }
}

Best Practices

Timeout Selection

// Use appropriate timeouts for different scenarios
const TIMEOUTS = {
  IMMEDIATE: 1000,     // UI feedback, button press response
  SHORT: 3000,         // Simple animations, transitions
  STANDARD: 10000,     // Network requests, data loading
  LONG: 30000,         // File uploads, complex processing
  VERY_LONG: 60000     // Background sync, batch operations
};

await waitFor(element(by.id('spinner')))
  .not.toBeVisible()
  .withTimeout(TIMEOUTS.STANDARD);

Always Use Timeouts

// Always specify timeout - never wait indefinitely
await waitFor(element(by.id('element')))
  .toBeVisible()
  .withTimeout(10000); // Required!

// This will do nothing without withTimeout():
// waitFor(element(by.id('element'))).toBeVisible(); // ❌ Wrong

Prefer waitFor Over Manual Delays

// Good: Wait for actual condition
await waitFor(element(by.id('data_loaded')))
  .toBeVisible()
  .withTimeout(15000);

// Bad: Arbitrary sleep
await new Promise(resolve => setTimeout(resolve, 5000)); // ❌ Unreliable

Types

interface WaitFor {
  withTimeout(millis: number): Promise<void>;
  whileElement(matcher: NativeMatcher): NativeElementWaitableActions & WaitFor;
}

interface NativeElementWaitableActions {
  scroll(pixels: number, direction: Direction, startPositionX?: number, startPositionY?: number): Promise<void>;
  tap(point?: Point2D): Promise<void>;
  longPress(point?: Point2D, duration?: number): Promise<void>;
  multiTap(times: number): Promise<void>;
  typeText(text: string): Promise<void>;
  replaceText(text: string): Promise<void>;
  clearText(): Promise<void>;
  tapBackspaceKey(): Promise<void>;
  tapReturnKey(): Promise<void>;
  scrollTo(edge: Direction, startPositionX?: number, startPositionY?: number): Promise<void>;
  swipe(direction: Direction, speed?: Speed, percentage?: number, normalizedStartingPointX?: number, normalizedStartingPointY?: number): Promise<void>;
  setColumnToValue(column: number, value: string): Promise<void>;
  setDatePickerDate(dateString: string, dateFormat: string): Promise<void>;
  performAccessibilityAction(actionName: string): Promise<void>;
  pinch(scale: number, speed?: Speed, angle?: number): Promise<void>;
}

type Direction = 'left' | 'right' | 'top' | 'bottom' | 'up' | 'down';

Install with Tessl CLI

npx tessl i tessl/npm-detox

docs

assertions.md

configuration.md

device-management.md

element-interaction.md

element-selection.md

index.md

synchronization.md

web-view-testing.md

tile.json