Simple and complete React Native testing utilities that encourage good testing practices
—
Built-in utilities for handling asynchronous behavior, waiting for conditions, and testing time-dependent components in React Native applications.
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();
});
});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();
});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();
});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);
});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);
});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();
});
});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