UI Testing
React Native Harness provides powerful UI testing capabilities that allow you to test your React Native components visually through screenshot testing and element interaction.
Warning
UI testing is an opt-in feature that requires installing an additional package. The @react-native-harness/ui package is not included by default and must be explicitly installed to use these features.
For iOS and Android, this package contains native code and requires rebuilding your app after installation. This code is automatically excluded from release builds and is only available in debug builds.
For Web, this package uses standard DOM APIs for interactions and a Playwright-based bridge for capturing screenshots.
Installation
To enable UI testing in your project, install the UI testing package:
npm install --save-dev @react-native-harness/ui
yarn add --save-dev @react-native-harness/ui
pnpm add --save-dev @react-native-harness/ui
bun add --save-dev @react-native-harness/ui
deno add --save-dev npm:@react-native-harness/ui
On iOS, resolve native dependencies before rebuilding. For apps using React Native's SwiftPM integration, run npx react-native spm from the app directory. @react-native-harness/ui ships its own HarnessUI package manifest and autolinking metadata. For CocoaPods apps, run pod install in the iOS directory.
After installation, rebuild your app since this package contains native code that needs to be compiled and linked.
Screen API
The screen object provides methods to query and interact with UI elements in your React Native application. It uses accessibility properties and test IDs to locate elements.
Element queries
import { describe, test, render, expect } from 'react-native-harness';
import { screen } from '@react-native-harness/ui';
import { View, Text } from 'react-native';
describe('Element Queries', () => {
test('find elements by testID', async () => {
await render(
<View>
<Text testID="welcome-text">Welcome!</Text>
<Text testID="subtitle">This is a test</Text>
</View>
);
// These methods wait for the element and reject if it does not appear
const welcomeText = await screen.findByTestId('welcome-text');
const subtitle = await screen.findByTestId('subtitle');
// These methods return null if no element is found
const welcomeTextSafe = screen.queryByTestId('welcome-text');
const allSubtitles = screen.queryAllByTestId('subtitle');
});
test('find elements by accessibility label', async () => {
await render(
<View>
<Text accessibilityLabel="Main heading">Hello World</Text>
</View>
);
const heading = await screen.findByAccessibilityLabel('Main heading');
});
});
Available query methods
The find* methods are asynchronous: await them. They poll until the element appears (up to one second) and reject if it does not. The query* methods are synchronous and check once.
Screenshot testing
Screenshot testing allows you to capture and compare visual snapshots of your UI components. This is particularly useful for detecting unexpected visual changes in your components.
Basic screenshot testing
import { describe, test, render, expect } from 'react-native-harness';
import { screen } from '@react-native-harness/ui';
import { View, Text } from 'react-native';
describe('Screenshot Testing', () => {
test('should match visual snapshot', async () => {
await render(
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<View
style={{
width: 100,
height: 100,
backgroundColor: 'blue',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Text style={{ color: 'white' }}>Hello, world!</Text>
</View>
</View>
);
const screenshot = await screen.screenshot();
await expect(screenshot).toMatchImageSnapshot({ name: 'blue-square' });
});
});
Baselines are stored in __image_snapshots__/<runner>/ next to the test file. When a comparison fails, Harness saves <name>-diff.png and <name>-actual.png into __image_snapshots__/<runner>/__diff_output__/. You can add __diff_output__/ to your .gitignore to keep these files out of version control.
Screenshot options
The toMatchImageSnapshot matcher accepts various options to customize the comparison:
test('screenshot with custom options', async () => {
await render(/* your component */);
const screenshot = await screen.screenshot();
await expect(screenshot).toMatchImageSnapshot({
name: 'custom-screenshot',
threshold: 0.05, // More sensitive comparison (0-1)
failureThreshold: 0.01, // Minimum difference to trigger failure
failureThresholdType: 'percent', // 'pixel' or 'percent'
comparisonMethod: 'ssim', // 'pixelmatch' or 'ssim'
ssimThreshold: 0.95, // SSIM similarity threshold (0-1)
diffColor: [255, 0, 0], // RGB color for diff visualization
ignoreRegions: [
{ x: 10, y: 10, width: 50, height: 50 }, // Ignore specific regions
],
});
});
Screenshot options reference
Element-specific screenshots
You can capture screenshots of specific elements instead of the entire screen:
Warning
If you are capturing screenshots of elements that extend beyond the screen boundaries, you must disable view flattening in your configuration to ensure the entire element can be rendered.
This can be done by setting disableViewFlattening: true in your rn-harness.config.mjs file.
test('capture specific element', async () => {
await render(
<View>
<View testID="header" style={{ backgroundColor: 'red', padding: 20 }}>
<Text>Header</Text>
</View>
<View testID="content" style={{ backgroundColor: 'blue', padding: 20 }}>
<Text>Content</Text>
</View>
</View>
);
const headerElement = await screen.findByTestId('header');
const headerScreenshot = await screen.screenshot(headerElement);
await expect(headerScreenshot).toMatchImageSnapshot({
name: 'header-element',
});
});
Multiple screenshots in one test
test('multiple screenshots', async () => {
// Initial render
await render(<MyComponent color="blue" />);
const screenshot1 = await screen.screenshot();
await expect(screenshot1).toMatchImageSnapshot({ name: 'blue-state' });
// Update component
await render(<MyComponent color="red" />);
const screenshot2 = await screen.screenshot();
await expect(screenshot2).toMatchImageSnapshot({ name: 'red-state' });
});
User interaction testing
The userEvent API allows you to simulate user interactions with your components:
import { describe, test, render, expect, fn } from 'react-native-harness';
import { screen, userEvent } from '@react-native-harness/ui';
describe('User Interactions', () => {
test('button press', async () => {
const onPress = fn();
await render(
<TouchableOpacity testID="my-button" onPress={onPress}>
<Text>Press me</Text>
</TouchableOpacity>
);
const button = await screen.findByTestId('my-button');
await userEvent.press(button);
expect(onPress).toHaveBeenCalled();
});
test('text input', async () => {
const onChangeText = fn();
await render(
<TextInput testID="username-input" onChangeText={onChangeText} />
);
const input = await screen.findByTestId('username-input');
await userEvent.type(input, 'testuser');
// onChangeText fires for each character, so the last call has the full text
expect(onChangeText).toHaveBeenLastCalledWith('testuser');
});
});
## Web Support
React Native Harness supports UI testing on the web using Playwright. When running tests on the web platform, the interaction logic is handled by a Playwright bridge that simulates real user events at the browser level.
### Interaction logic
- **Pressing**: Simulates a real mouse click at the element's coordinates using Playwright's mouse API.
- **Typing**: Simulates real keyboard input using Playwright's keyboard API. This ensures that all browser events (keydown, keypress, input, keyup) are fired correctly and picked up by React Native Web.
- **Blurring**: Supports blurring the active element, with an option to trigger "Submit" (Enter key) before blurring.
### Configuration
To use UI testing on web, make sure you have the `@react-native-harness/platform-web` package installed and configured in your `rn-harness.config.mjs`.
```javascript
import { webPlatform, chromium } from '@react-native-harness/platform-web';
export default {
// ...
runners: [
webPlatform({
name: 'web',
browser: chromium('http://localhost:3000'),
}),
],
};
Troubleshooting
Anti-aliased text differences
Small differences in text rendering can cause false positives. Increase the threshold value for less sensitive comparison.
Dynamic content
For components with dynamic content (dates, random values), use ignoreRegions to exclude those areas from comparison.