New Recipe
Universal E2E Testing with Detox and Playwright
Published on February 12th, 2025 by Joshua Yoes
View recipeLatest Ignite Release
View on GithubProven Recipes for your React Native apps
Starting from scratch doesn’t always make sense. That’s why we made the Ignite Cookbook for React Native – an easy way for developers to browse and share code snippets (or “recipes”) that actually work.
Spin Up Your App In Record Time
Stop reinventing the wheel on every project. Use the Ignite CLI to get your app started. Then, hop over to the Ignite Cookbook for React Native to browse for things like libraries in “cookie cutter” templates that work for almost any project. It’s the fastest way to get a React Native app off the ground.





Find Quality Code When You Need It
The popular forum sites are great for finding code until you realize it’s based on an old version of React Native. Ignite Cookbook is a place for recipes that work as of the time they’re published – meaning, it worked when it was posted. And if it ever goes out of date, we’ll make sure the community knows on what version it was last working.
npx ignite-cli@latest new PizzaApp cd PizzaApp export default defineConfig({ - testDir: "./tests/playwright", + testDir: "./e2e/tests", /* Run tests in files in parallel */ fullyParallel: true, e2e ├─ detox │ ├─ screens │ │ ├─ LoginScreen.ts │ │ └─ WelcomeScreen.ts │ ├─ entry.ts │ └─ setup.ts ├─ playwright │ ├─ screens │ │ ├─ LoginScreen.ts │ │ └─ WelcomeScreen.ts │ ├─ entry.ts │ └─ setup.ts ├─ tests │ └─ Login.test.ts ├─ screens.ts ├─ entry.ts └─ jest.config.js import { test } from "../entry"; test("Open up our app and use the default credentials to login and navigate to the demo screen", async ({ loadApp, loginScreen, welcomeScreen, }) => { await loadApp(); await loginScreen.login(); await welcomeScreen.launch(); }); import type { Fixtures } from "./screens"; /** Check for runtime globals that we are in a detox environment */ export const isDetoxTestEnv = () => // @ts-ignore typeof device !== "undefined"; /** Check for runtime globals that we are in a playwright environment */ export const isPlaywrightTestEnv = () => // @ts-ignore globalThis._playwrightInstance !== undefined; /** Our library-agnostic test function */ export type Test = ( name: string, fn: (fixtures: Fixtures) => Promise<void> ) => void; /** * This test function is a little funky, but it ensures that we don't accidentally * import playwright code into a detox environment, or vice versa. */ export const test: Test = (() => { const testEnvsLoaded = [isDetoxTestEnv(), isPlaywrightTestEnv()].filter( Boolean ).length; if (testEnvsLoaded !== 1) { throw new Error( `${testEnvsLoaded} test environments loaded. Only one is allowed. Check the isTestEnv functions to make sure they check for globals that are specific only to their test environment` ); } if (isDetoxTestEnv()) { return require("./detox/entry").test; } if (isPlaywrightTestEnv()) { return require("./playwright/entry").test; } throw new Error( "Unknown test environment. Check the isTestEnv functions to make sure they check for globals that are specific only to their test environment" ); })(); export interface ILoginScreen { login(): Promise<void>; } export interface IWelcomeScreen { launch(): Promise<void>; } /** A fixture of all the page object models we can use in our tests */ export type Fixtures = { loadApp: () => Promise<void>; loginScreen: ILoginScreen; welcomeScreen: IWelcomeScreen; // ... you can add more as needed }; import { expect, element, by } from "detox"; import type { ILoginScreen } from "../../screens"; export class DetoxLoginScreen implements ILoginScreen { async login() { await expect(element(by.text("Log In"))).toBeVisible(); await element(by.text("Tap to log in!")).tap(); } } import { expect, element, by } from "detox"; import type { IWelcomeScreen } from "../../screens"; export class DetoxWelcomeScreen implements IWelcomeScreen { async launch() { await expect( element(by.text("Your app, almost ready for launch!")) ).toBeVisible(); await element(by.text("Let's go!")).tap(); await expect( element(by.text("Components to jump start your project!")) ).toBeVisible(); } } import { device } from "detox"; import { resolveConfig } from "detox/internals"; import type { AppJSONConfig } from "@expo/config"; const appConfig: AppJSONConfig = require("../../app.json"); type Platform = ReturnType<typeof device.getPlatform>; export async function detoxLoadApp() { const config = await resolveConfig(); const platform = device.getPlatform(); const isDebugConfig = config.configurationName.split(".").at(-1) === "debug"; if (isDebugConfig) { return await openAppForDebugBuild(platform); } else { return await device.launchApp({ newInstance: true, }); } } async function openAppForDebugBuild(platform: Platform) { const deepLinkUrl = process.env.EXPO_USE_UPDATES ? // Testing latest published EAS update for the test_debug channel getDeepLinkUrl(getLatestUpdateUrl()) : // Local testing with packager getDeepLinkUrl(getDevLauncherPackagerUrl(platform)); if (platform === "ios") { await device.launchApp({ newInstance: true, }); await sleep(1000); await device.openURL({ url: deepLinkUrl, }); } else { await device.launchApp({ newInstance: true, url: deepLinkUrl, }); } await sleep(1000); } const getAppId = () => appConfig?.expo?.extra?.eas?.projectId ?? ""; const getAppSchema = () => appConfig?.expo?.scheme ?? ""; const getDeepLinkUrl = (url: string) => `exp+${getAppSchema()}://expo-development-client/?url=${encodeURIComponent( url )}`; const getDevLauncherPackagerUrl = (platform: Platform) => `http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1`; const getLatestUpdateUrl = () => `https://u.expo.dev/${getAppId()}?channel-name=test_debug&disableOnboarding=1`; const sleep = (t: number) => new Promise((res) => setTimeout(res, t)); import { DetoxWelcomeScreen } from "./screens/WelcomeScreen"; import { DetoxLoginScreen } from "./screens/LoginScreen"; import type { Fixtures } from "../screens"; import type { Test } from "../entry"; import { detoxLoadApp } from "./setup"; const fixtures: Fixtures = { loadApp: detoxLoadApp, loginScreen: new DetoxLoginScreen(), welcomeScreen: new DetoxWelcomeScreen(), }; export const test: Test = (name, fn) => globalThis.test(name, (done) => { fn(fixtures) .then(() => done()) .catch(done.fail); }); import { expect, Page } from "@playwright/test"; import type { ILoginScreen } from "../../screens"; export class PlaywrightLoginScreen implements ILoginScreen { constructor(private page: Page) {} async login() { await expect(this.page.locator("[data-testid='login-heading']")).toHaveText( "Log In" ); await this.page.locator("[data-testid='login-button']").click(); } } import { expect, Page } from "@playwright/test"; import type { IWelcomeScreen } from "../../screens"; export class PlaywrightWelcomeScreen implements IWelcomeScreen { constructor(private page: Page) {} async launch() { await expect( this.page.getByText("Your app, almost ready for launch!") ).toBeVisible(); await this.page.getByText("Let's go!").click(); await expect( this.page.getByText("Components to jump start your project!") ).toBeVisible(); } } import { test as base } from "@playwright/test"; import { playwrightLoadApp } from "./setup"; import { PlaywrightLoginScreen } from "./screens/LoginScreen"; import { PlaywrightWelcomeScreen } from "./screens/WelcomeScreen"; import type { Fixtures } from "../screens"; export const test = base.extend<Fixtures>({ loadApp: async ({ page }, use) => { await use(() => playwrightLoadApp(page)); }, loginScreen: async ({ page }, use) => { await use(new PlaywrightLoginScreen(page)); }, welcomeScreen: async ({ page }, use) => { await use(new PlaywrightWelcomeScreen(page)); }, }); import type { Page } from "@playwright/test"; export async function playwrightLoadApp(page: Page) { await page.goto("http://localhost:3000"); } { "scripts": { "detox:build:ios:debug": "detox build --c ios.sim.debug", "detox:test:debug": "detox test --configuration ios.sim.debug" } } yarn detox:build:ios:debug yarn start yarn detox:test:debug { "scripts": { "playwright:build": "yarn bundle:web && yarn serve:web", "playwright:test": "yarn playwright test" } } yarn playwright:build yarn playwright:test yarn playwright:test --ui module.exports = { preset: "jest-expo", setupFiles: ["<rootDir>/test/setup.ts"], + testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/e2e/"], } export default { - API_URL: "CHANGEME", + API_URL: "https://api.rss2json.com/v1/", }
Backed By A Community of React Native Experts
The Ignite Cookbook isn’t just a random group of code snippets. It’s a curated collection of usable code samples that the Infinite Red team’s used in their own React Native projects. Having worked with some of the biggest clients in the tech industry, we know a thing or two about keeping our code to a high standard. You can code confidently!








Freshly added to the cookbook
New
Universal E2E Testing with Detox and Playwright
Published on February 12th, 2025 by Joshua Yoes
Theming Ignite with Emotion.js
Published on October 2nd, 2024 by Mark Rickert
Theming Ignite with styled-components
Published on October 2nd, 2024 by Mark Rickert
Theming Ignite with Unistyles
Published on October 2nd, 2024 by Mark Rickert
View all recipes