CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-testing-library--react-native

Simple and complete React Native testing utilities that encourage good testing practices

Pending
Overview
Eval results
Files

async-testing.mddocs/

Async Testing Utilities

Built-in utilities for handling asynchronous behavior, waiting for conditions, and testing time-dependent components in React Native applications.

Capabilities

WaitFor Utility

Wait for expectations to pass with configurable polling and timeout options.

/**
 * Wait for expectation to pass with polling
 * @param expectation - Function that should eventually not throw
 * @param options - Waiting configuration options
 * @returns Promise that resolves with expectation result
 */
function waitFor<T>(
  expectation: () => T,
  options?: WaitForOptions
): Promise<T>;

interface WaitForOptions {
  /** Timeout in milliseconds (default: from config.asyncUtilTimeout) */
  timeout?: number;
  
  /** Polling interval in milliseconds (default: 50) */
  interval?: number;
  
  /** Error object for stack traces */
  stackTraceError?: Error;
  
  /** Custom timeout error handler */
  onTimeout?: (error: Error) => Error;
}

Usage Examples:

import { render, screen, waitFor, fireEvent } from "@testing-library/react-native";

test("waiting for async state changes", async () => {
  const AsyncComponent = () => {
    const [loading, setLoading] = useState(true);
    const [data, setData] = useState(null);

    useEffect(() => {
      setTimeout(() => {
        setData("Loaded data");
        setLoading(false);
      }, 1000);
    }, []);

    return loading ? <Text>Loading...</Text> : <Text>{data}</Text>;
  };

  render(<AsyncComponent />);

  // Initially shows loading
  expect(screen.getByText("Loading...")).toBeOnTheScreen();

  // Wait for data to load
  await waitFor(() => {
    expect(screen.getByText("Loaded data")).toBeOnTheScreen();
  });

  // Loading should be gone
  expect(screen.queryByText("Loading...")).not.toBeOnTheScreen();
});

test("waiting with custom timeout", async () => {
  const SlowComponent = () => {
    const [message, setMessage] = useState("Initial");

    useEffect(() => {
      setTimeout(() => setMessage("Updated"), 2000);
    }, []);

    return <Text>{message}</Text>;
  };

  render(<SlowComponent />);

  // Wait with extended timeout
  await waitFor(
    () => {
      expect(screen.getByText("Updated")).toBeOnTheScreen();
    },
    { timeout: 3000, interval: 100 }
  );
});

test("waiting for user interactions to complete", async () => {
  const InteractiveComponent = () => {
    const [count, setCount] = useState(0);
    const [processing, setProcessing] = useState(false);

    const handlePress = async () => {
      setProcessing(true);
      // Simulate async operation
      await new Promise(resolve => setTimeout(resolve, 500));
      setCount(prev => prev + 1);
      setProcessing(false);
    };

    return (
      <View>
        <Text>Count: {count}</Text>
        <Text>Status: {processing ? "Processing" : "Ready"}</Text>
        <Pressable testID="increment" onPress={handlePress}>
          <Text>Increment</Text>
        </Pressable>
      </View>
    );
  };

  render(<InteractiveComponent />);

  const button = screen.getByTestId("increment");
  
  // Initial state
  expect(screen.getByText("Count: 0")).toBeOnTheScreen();
  expect(screen.getByText("Status: Ready")).toBeOnTheScreen();

  // Press button
  fireEvent.press(button);

  // Wait for processing to start
  await waitFor(() => {
    expect(screen.getByText("Status: Processing")).toBeOnTheScreen();
  });

  // Wait for processing to complete and count to update
  await waitFor(() => {
    expect(screen.getByText("Count: 1")).toBeOnTheScreen();
    expect(screen.getByText("Status: Ready")).toBeOnTheScreen();
  });
});

WaitFor with Custom Error Handling

Advanced waitFor usage with custom error handling and debugging.

/**
 * Custom timeout error handler
 * @param error - Original timeout error
 * @returns Modified error with additional context
 */
type TimeoutErrorHandler = (error: Error) => Error;

Usage Examples:

test("custom error handling", async () => {
  const FailingComponent = () => <Text>This will never change</Text>;

  render(<FailingComponent />);

  // Custom error with debugging info
  await expect(
    waitFor(
      () => {
        expect(screen.getByText("Non-existent")).toBeOnTheScreen();
      },
      {
        timeout: 1000,
        onTimeout: (error) => {
          const elements = screen.getAllByText(/./);
          const elementTexts = elements.map(el => 
            el.children.join(" ")
          );
          
          return new Error(
            `${error.message}\n\nAvailable elements:\n${elementTexts.join("\n")}`
          );
        }
      }
    )
  ).rejects.toThrow(/Available elements:/);
});

test("stack trace preservation", async () => {
  const stackTraceError = new Error();
  
  render(<Text>Static text</Text>);

  await expect(
    waitFor(
      () => {
        expect(screen.getByText("Dynamic text")).toBeOnTheScreen();
      },
      { 
        timeout: 100,
        stackTraceError // Preserves original call site in stack trace
      }
    )
  ).rejects.toThrow();
});

WaitForElementToBeRemoved

Wait for elements to be removed from the component tree.

/**
 * Wait for element(s) to be removed from the tree
 * @param callback - Function returning element(s) or element(s) directly
 * @param options - Waiting configuration options
 * @returns Promise that resolves when element(s) are removed
 */
function waitForElementToBeRemoved<T>(
  callback: (() => T) | T,
  options?: WaitForOptions
): Promise<void>;

Usage Examples:

test("waiting for element removal", async () => {
  const DismissibleComponent = () => {
    const [visible, setVisible] = useState(true);

    useEffect(() => {
      const timer = setTimeout(() => setVisible(false), 1000);
      return () => clearTimeout(timer);
    }, []);

    return visible ? (
      <View testID="dismissible">
        <Text>This will disappear</Text>
      </View>
    ) : null;
  };

  render(<DismissibleComponent />);

  // Element is initially present
  const element = screen.getByTestId("dismissible");
  expect(element).toBeOnTheScreen();

  // Wait for element to be removed
  await waitForElementToBeRemoved(element);

  // Element should no longer exist
  expect(screen.queryByTestId("dismissible")).not.toBeOnTheScreen();
});

test("waiting for multiple elements removal", async () => {
  const MultiDismissComponent = () => {
    const [items, setItems] = useState([1, 2, 3]);

    useEffect(() => {
      const timer = setTimeout(() => {
        setItems(prev => prev.filter(item => item !== 2));
      }, 500);
      return () => clearTimeout(timer);
    }, []);

    return (
      <View>
        {items.map(item => (
          <Text key={item} testID={`item-${item}`}>
            Item {item}
          </Text>
        ))}
      </View>
    );
  };

  render(<MultiDismissComponent />);

  // All items initially present
  expect(screen.getAllByText(/Item/)).toHaveLength(3);

  // Wait for specific item to be removed using callback
  await waitForElementToBeRemoved(() => 
    screen.getByTestId("item-2")
  );

  // Only items 1 and 3 should remain
  expect(screen.getAllByText(/Item/)).toHaveLength(2);
  expect(screen.queryByTestId("item-2")).not.toBeOnTheScreen();
});

test("removal with loading states", async () => {
  const LoadingComponent = () => {
    const [loading, setLoading] = useState(true);
    const [data, setData] = useState(null);

    const loadData = async () => {
      await new Promise(resolve => setTimeout(resolve, 800));
      setData("Loaded content");
      setLoading(false);
    };

    useEffect(() => {
      loadData();
    }, []);

    if (loading) {
      return (
        <View testID="loading-spinner">
          <Text>Loading...</Text>
        </View>
      );
    }

    return (
      <View testID="content">
        <Text>{data}</Text>
      </View>
    );
  };

  render(<LoadingComponent />);

  // Loading spinner is present
  const spinner = screen.getByTestId("loading-spinner");
  expect(spinner).toBeOnTheScreen();

  // Wait for loading spinner to be removed
  await waitForElementToBeRemoved(spinner);

  // Content should now be visible
  expect(screen.getByTestId("content")).toBeOnTheScreen();
  expect(screen.getByText("Loaded content")).toBeOnTheScreen();
});

FindBy Queries (Async Queries)

All query methods have findBy variants that automatically wait for elements to appear.

/**
 * Async versions of getBy queries that wait for elements to appear
 * These combine getBy queries with waitFor functionality
 */

// Text queries
function findByText(text: string | RegExp, options?: TextMatchOptions & WaitForOptions): Promise<ReactTestInstance>;
function findAllByText(text: string | RegExp, options?: TextMatchOptions & WaitForOptions): Promise<ReactTestInstance[]>;

// TestID queries
function findByTestId(testId: string | RegExp, options?: TestIdOptions & WaitForOptions): Promise<ReactTestInstance>;
function findAllByTestId(testId: string | RegExp, options?: TestIdOptions & WaitForOptions): Promise<ReactTestInstance[]>;

// Role queries
function findByRole(role: string, options?: RoleOptions & WaitForOptions): Promise<ReactTestInstance>;
function findAllByRole(role: string, options?: RoleOptions & WaitForOptions): Promise<ReactTestInstance[]>;

// Label text queries
function findByLabelText(text: string | RegExp, options?: LabelTextOptions & WaitForOptions): Promise<ReactTestInstance>;
function findAllByLabelText(text: string | RegExp, options?: LabelTextOptions & WaitForOptions): Promise<ReactTestInstance[]>;

// Hint text queries
function findByHintText(text: string | RegExp, options?: HintTextOptions & WaitForOptions): Promise<ReactTestInstance>;
function findAllByHintText(text: string | RegExp, options?: HintTextOptions & WaitForOptions): Promise<ReactTestInstance[]>;

// Placeholder text queries
function findByPlaceholderText(text: string | RegExp, options?: PlaceholderTextOptions & WaitForOptions): Promise<ReactTestInstance>;
function findAllByPlaceholderText(text: string | RegExp, options?: PlaceholderTextOptions & WaitForOptions): Promise<ReactTestInstance[]>;

// Display value queries
function findByDisplayValue(value: string | RegExp, options?: DisplayValueOptions & WaitForOptions): Promise<ReactTestInstance>;
function findAllByDisplayValue(value: string | RegExp, options?: DisplayValueOptions & WaitForOptions): Promise<ReactTestInstance[]>;

Usage Examples:

test("findBy queries for async elements", async () => {
  const AsyncContentComponent = () => {
    const [content, setContent] = useState(null);

    useEffect(() => {
      setTimeout(() => {
        setContent("Dynamic content loaded");
      }, 600);
    }, []);

    return (
      <View>
        {content ? (
          <Text testID="dynamic-content">{content}</Text>
        ) : (
          <Text>Loading content...</Text>
        )}
      </View>
    );
  };

  render(<AsyncContentComponent />);

  // Initially only loading text
  expect(screen.getByText("Loading content...")).toBeOnTheScreen();

  // Wait for dynamic content to appear
  const dynamicText = await screen.findByText("Dynamic content loaded");
  expect(dynamicText).toBeOnTheScreen();

  // Alternative using testID
  const dynamicElement = await screen.findByTestId("dynamic-content");
  expect(dynamicElement).toBeOnTheScreen();
});

test("findBy with custom wait options", async () => {
  const VerySlowComponent = () => {
    const [visible, setVisible] = useState(false);

    useEffect(() => {
      setTimeout(() => setVisible(true), 2500);
    }, []);

    return visible ? <Text>Finally loaded</Text> : <Text>Still loading</Text>;
  };

  render(<VerySlowComponent />);

  // Wait with extended timeout
  const slowText = await screen.findByText("Finally loaded", {
    timeout: 3000,
    interval: 100
  });
  
  expect(slowText).toBeOnTheScreen();
});

test("findAll for multiple async elements", async () => {
  const AsyncListComponent = () => {
    const [items, setItems] = useState([]);

    useEffect(() => {
      const timer = setTimeout(() => {
        setItems(["Item 1", "Item 2", "Item 3"]);
      }, 500);
      return () => clearTimeout(timer);
    }, []);

    return (
      <View>
        {items.map((item, index) => (
          <Text key={index} testID={`list-item-${index}`}>
            {item}
          </Text>
        ))}
      </View>
    );
  };

  render(<AsyncListComponent />);

  // Wait for all items to appear
  const items = await screen.findAllByText(/Item \d/);
  expect(items).toHaveLength(3);

  // Alternative using testID pattern
  const itemElements = await screen.findAllByTestId(/list-item-\d/);
  expect(itemElements).toHaveLength(3);
});

Act Utility

React act utility for properly wrapping state updates and effects in tests.

/**
 * React act utility for wrapping state updates
 * Ensures all updates are flushed before assertions
 * @param callback - Function containing state updates
 * @returns Promise that resolves when updates are complete
 */
function act<T>(callback: () => T): Promise<T>;
function act<T>(callback: () => Promise<T>): Promise<T>;

/**
 * Get current React act environment setting
 * @returns Current act environment state
 */
function getIsReactActEnvironment(): boolean;

/**
 * Set React act environment setting
 * @param isReactActEnvironment - Whether to use act environment
 */
function setReactActEnvironment(isReactActEnvironment: boolean): void;

Usage Examples:

import { render, screen, act, fireEvent } from "@testing-library/react-native";

test("using act for state updates", async () => {
  const StateComponent = () => {
    const [count, setCount] = useState(0);

    return (
      <View>
        <Text>Count: {count}</Text>
        <Pressable testID="increment" onPress={() => setCount(c => c + 1)}>
          <Text>+</Text>
        </Pressable>
      </View>
    );
  };

  render(<StateComponent />);

  const button = screen.getByTestId("increment");

  // Act is usually not needed with fireEvent as it's wrapped automatically
  fireEvent.press(button);
  expect(screen.getByText("Count: 1")).toBeOnTheScreen();

  // Manual act usage for direct state updates (rarely needed)
  await act(async () => {
    // Direct component state manipulation
    fireEvent.press(button);
    fireEvent.press(button);
  });

  expect(screen.getByText("Count: 3")).toBeOnTheScreen();
});

test("act environment configuration", () => {
  // Check current act environment
  const currentEnv = getIsReactActEnvironment();
  expect(typeof currentEnv).toBe("boolean");

  // Temporarily disable act warnings
  setReactActEnvironment(false);
  expect(getIsReactActEnvironment()).toBe(false);

  // Restore previous setting
  setReactActEnvironment(currentEnv);
  expect(getIsReactActEnvironment()).toBe(currentEnv);
});

FlushMicroTasks Utility

Utility for flushing pending microtasks in test environments.

/**
 * Flush all pending microtasks
 * Useful for ensuring all Promise.resolve() calls have completed
 * @returns Promise that resolves when all microtasks are flushed
 */
function flushMicroTasks(): Promise<void>;

Usage Examples:

import { render, screen, flushMicroTasks } from "@testing-library/react-native";

test("flushing microtasks", async () => {
  const MicroTaskComponent = () => {
    const [message, setMessage] = useState("Initial");

    useEffect(() => {
      Promise.resolve().then(() => {
        setMessage("Updated via microtask");
      });
    }, []);

    return <Text>{message}</Text>;
  };

  render(<MicroTaskComponent />);

  // Initially shows initial message
  expect(screen.getByText("Initial")).toBeOnTheScreen();

  // Flush microtasks to process Promise.resolve()
  await flushMicroTasks();

  // Message should now be updated
  expect(screen.getByText("Updated via microtask")).toBeOnTheScreen();
});

test("combining with act for complete updates", async () => {
  const ComplexAsyncComponent = () => {
    const [state, setState] = useState("start");

    useEffect(() => {
      // Microtask
      Promise.resolve().then(() => setState("microtask"));
      
      // Macrotask
      setTimeout(() => setState("macrotask"), 0);
    }, []);

    return <Text>{state}</Text>;
  };

  render(<ComplexAsyncComponent />);

  // Initial state
  expect(screen.getByText("start")).toBeOnTheScreen();

  // Flush microtasks first
  await flushMicroTasks();
  expect(screen.getByText("microtask")).toBeOnTheScreen();

  // Then wait for macrotask
  await waitFor(() => {
    expect(screen.getByText("macrotask")).toBeOnTheScreen();
  });
});

Configuration and Best Practices

Global configuration affecting async utilities behavior.

/**
 * Configuration affecting async utilities
 */
interface Config {
  /** Default timeout for waitFor and findBy queries (ms) */
  asyncUtilTimeout: number;
  
  /** Other config options... */
  defaultIncludeHiddenElements: boolean;
  concurrentRoot: boolean;
  defaultDebugOptions?: Partial<DebugOptions>;
}

Usage Examples:

import { configure, resetToDefaults } from "@testing-library/react-native";

test("configuring async timeouts", async () => {
  // Set longer timeout for slow tests
  configure({ asyncUtilTimeout: 5000 });

  const VerySlowComponent = () => {
    const [loaded, setLoaded] = useState(false);
    
    useEffect(() => {
      setTimeout(() => setLoaded(true), 4000);
    }, []);

    return loaded ? <Text>Finally ready</Text> : <Text>Loading...</Text>;
  };

  render(<VerySlowComponent />);

  // This will use the configured 5 second timeout
  await screen.findByText("Finally ready");

  // Reset to defaults
  resetToDefaults();
});

test("best practices for async testing", async () => {
  const BestPracticeComponent = () => {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        // Simulate API call
        await new Promise(resolve => setTimeout(resolve, 500));
        
        if (Math.random() > 0.8) {
          throw new Error("Random API error");
        }
        
        setData("Success data");
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    useEffect(() => {
      fetchData();
    }, []);

    if (loading) return <Text>Loading...</Text>;
    if (error) return <Text>Error: {error}</Text>;
    return <Text>Data: {data}</Text>;
  };

  render(<BestPracticeComponent />);

  // Wait for loading to complete
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."));

  // Check for either success or error state
  await waitFor(() => {
    const successElement = screen.queryByText(/Data:/);
    const errorElement = screen.queryByText(/Error:/);
    
    expect(successElement || errorElement).toBeTruthy();
  });
});

Install with Tessl CLI

npx tessl i tessl/npm-testing-library--react-native

docs

async-testing.md

configuration.md

hooks.md

index.md

interactions.md

matchers.md

queries.md

rendering.md

tile.json