Testing
The mobile app's two test lanes - the jest-expo unit suite and the Maestro device flows - and how each one gates.
apps/mobile is the one tree in this repo that runs jest, not vitest: React Native components need the jest-expo preset and its native module mocks. Everything else in the repo stays on vitest.
Unit and component tests
| Fact | Value |
|---|---|
| Command | pnpm --filter mobile test |
| Location | apps/mobile/tests/** |
| Preset | jest-expo, with Testing Library for React Native |
| In CI | Runs inside the repo-wide test task, like every other workspace |
The suite follows the same zero-skip policy as the rest of the repo: gate on config with a plain if, never test.skip - see Writing tests.
apps/mobile/jest.config.js maps the packages jest cannot load on its own - the ESM-only ones, and the native modules that throw on first use. Add a mapping there when a new dependency fails at import rather than reaching for transformIgnorePatterns.
Dependency health
expo-doctor is a maintenance advisor you run by hand. It is in no test lane and no battery leg, because it grades the project against the newest npm patches of the installed SDK, so its verdict moves with every Expo release.
cd apps/mobile && npx expo-doctor| Finding | Meaning |
|---|---|
| SDK patch versions out of date | Upgrade when you choose to: npx expo install --check |
eas-cli installed locally | By design - the release workflow runs it through pnpm exec, pinned |
Device flows
| Fact | Value |
|---|---|
| Command | pnpm --filter mobile e2e:mobile:ci |
| Same run, simulator left open | pnpm --filter mobile e2e:mobile |
| Tool | Maestro, driving a real simulator or emulator |
| Location | apps/mobile/e2e/flows/** |
| Inventory | apps/mobile/e2e/tags.ts - the one list of flows and the tags each carries |
Tag gating
A flow carries zero or more tags, and your project enables zero or more. A flow runs only when every tag it carries is enabled, so a flow for a feature this build does not ship is never registered.
| Tag | Enabled when | Gates |
|---|---|---|
ai | the project ships the AI surface | chat, attachments, the thread list, automations |
org | the project is multi-tenant | the organization switch and the member invite |
projects | the project ships projects | the project switch |
push | the project ships notifications and mobile push | the notification tap, the deep link and the OS permission prompt |
purchases | the project ships store purchases | the paywall, a sandbox purchase, restore |
Untagged flows - sign-in, sign-up, the settings journeys, the offline banner - run in every project.
A run prints not run: <flow> — <reason> for every flow it skipped, then exits 0. A missing simulator, emulator or maestro binary is a reason, not a failure: the run never passes silently, and never reddens a project that cannot drive a device.
Selectors
Flows select by testID, never by copy, so a translation or a reword never breaks one. apps/mobile/e2e/README.md is the contract: it lists every id a flow may use, and a new screen adds its rows there before a flow can touch it.
Device flows need a built app, not just Metro. Compile the development build first - see Setup.
Metro's file map does not register a file created after it started, so a flow that reaches new code keeps running against the last good bundle. Restart Metro with --clear after adding a file.