| name | detox |
| description | [Applies to: **/*.{js,jsx}] This guide provides definitive, opinionated best practices for writing reliable, maintainable, and performant end-to-end tests with Detox in React Native applications. |
| source | cursor_mdc |
detox Best Practices
Detox is the definitive choice for E2E testing in React Native. These rules ensure your tests are fast, stable, and easy to maintain, leveraging modern JavaScript and Detox's gray-box capabilities.
1. Test Organization with Page Object Model (POM)
Always structure your tests using the Page Object Model. This abstracts UI interactions and selectors, making tests readable and resilient to UI changes.
โ BAD: Direct selectors and repeated logic
describe('Login Flow', () => {
it('should log in successfully', async () => {
await element(by.id('emailInput')).typeText('test@example.com');
await element(by.id('passwordInput')).typeText('password123');
await element(by.id('loginButton')).tap();
await expect(element(by.id('homeScreen'))).toBeVisible();
});
});
โ
GOOD: Page Object Model
class LoginPage {
constructor() {
this.emailInput = element(by.id('emailInput'));
this.passwordInput = element(by.id('passwordInput'));
this.loginButton = element(by.id('loginButton'));
}
async login(email, password) {
await this.emailInput.typeText(email);
await this.passwordInput.typeText(password);
await this.loginButton.tap();
}
async isVisible() {
await expect(this.emailInput).toBeVisible();
}
}
export default new LoginPage();
import LoginPage from ;
;
(, {
( () => {
device.();
});
(, () => {
.(, );
.();
});
});
2. Stable Selectors are Paramount
Prioritize by.id for all interactive elements. Fallback to by.text only when by.id is not feasible (e.g., dynamic content). Avoid fragile selectors like by.type or by.label if a more stable alternative exists.
โ BAD: Fragile selector
await element(by.type('RCTTextView').withAncestor(by.id('welcomeMessage'))).toBeVisible();
โ
GOOD: Stable selector with testID
await expect(element(by.id('welcomeMessageText'))).toBeVisible();
3. Explicit Waits and Assertions
Never use arbitrary sleep() calls. Detox's gray-box synchronization handles most async operations, but for complex UI states or specific data loads, use waitFor with expect conditions.
โ BAD: Arbitrary sleep
await element(by.id('submitButton')).tap();
await sleep(2000);
await expect(element(by.id('successMessage'))).toBeVisible();
โ
GOOD: Explicit waitFor and expect
await element(by.id('submitButton')).tap();
await waitFor(element(by.id('successMessage')))
.toBeVisible()
.withTimeout(5000);
4. Reset App State Before Each Test
Ensure test isolation by resetting the app state before every it block. device.reloadReactNative() is the standard for a quick, clean slate. For a full app re-installation, use device.launchApp({ delete: true }) in beforeAll.
describe('User Profile', () => {
beforeEach(async () => {
await device.reloadReactNative();
await LoginPage.login('existing@example.com', 'password123');
});
it('should display user details', async () => {
});
});
5. Embrace async/await Everywhere
All Detox interactions and assertions are asynchronous. Use async/await consistently for clear, sequential test logic.
โ BAD: Mixing Promises and async/await inconsistently
it('should navigate', () => {
element(by.id('navButton')).tap().then(() => {
return expect(element(by.id('nextScreen'))).toBeVisible();
});
});
โ
GOOD: Consistent async/await
it('should navigate', async () => {
await element(by.id('navButton')).tap();
await expect(element(by.id('nextScreen'))).toBeVisible();
});
6. Mocking Strategies for External Dependencies
For true E2E tests, mock external APIs or services to control test data and eliminate flakiness from network instability. Use tools like msw (Mock Service Worker) for client-side mocking or nock for Node.js-based API mocks.
import { setupServer } from 'msw/node';
import { rest } from 'msw';
const server = setupServer(
rest.post('https://api.example.com/login', (req, res, ctx) => {
return res(ctx.json({ token: 'mock-token', user: { id: '123' } }));
}),
rest.get('https://api.example.com/profile', (req, res, ctx) => {
return res(ctx.json({ name: 'Test User', email: 'test@example.com' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('User Profile', () => {
beforeEach(async () => {
await device.reloadReactNative();
.(, );
});
(, () => {
((by.())).();
((by.())).();
});
});