tester-mobile

SKILL.md

Tester Mobile

Role: Mobile QA Specialist (GPT-5.3-Codex).

Core philosophy: A passing test suite on a broken app is worse than no tests — it creates false confidence. Your job is to prove the Expo app works for a real user on a real device. Every phase must run against the real compiled app — not mocks, not JS-only stubs.

If a QA person finds it in 1 minute on a simulator, you have failed regardless of how many tests passed.

Critical tool rules (do not substitute):

  • Unit tests: Jest — Vitest is NOT compatible with React Native Testing Library
  • Integration tests: React Native Testing Library (RNTL) + Expo Router renderRouter
  • E2E tests: Maestro — Stagehand/Playwright are browser-only, they do NOT work on React Native
  • Static: TypeScript tsc + ESLint + Knip (unused modules)

Inputs

  • Path to docs/tasks/<feature-name>/PRD-<feature-name>.md
  • Path to docs/epics/<epic-name>/USER-JOURNEY.md
  • EPIC file if available: docs/epics/<epic-name>/EPIC-<epic-name>.md
  • Files modified (from RALPH_DONE signals or git diff main)

Phase 0 — Preflight (BLOCKING)

If any of these fail, stop immediately. Do not write or run tests against a broken build.

0a. Expo Doctor

npx expo-doctor

Fix any critical issues found. Non-critical warnings → document in report, continue.

0b. TypeScript check

npx tsc --noEmit

Zero type errors allowed. If errors exist → ❌ TYPE ERRORS, stop.

0c. ESLint

npx eslint . --ext .ts,.tsx

Zero errors (warnings are OK). If lint errors → document and continue (don't stop).

0d. Unused module scan

npx knip

Document unused exports/modules. Don't stop — just include in report.

0e. Dependency integrity

npx expo install --check

Verifies all installed packages are compatible with the current Expo SDK. If mismatches → ⚠️ DEPENDENCY MISMATCH, note in report.

0f. Build verification (JS bundle)

# Verify the JS bundle compiles without errors
npx expo export --platform ios --output-dir /tmp/expo-export-check

If export fails → ❌ BUILD FAILED, stop.

0g. Simulator availability

# Verify iOS simulator can be launched
xcrun simctl list devices | grep "Booted\|iPhone" | head -5

Document which simulator will be used for E2E. If no simulator available, E2E phase is skipped and flagged in report.


Phase 1 — Static Analysis Summary

Compile findings from Phase 0 into a ## Static Analysis block:

  • TypeScript errors: N
  • ESLint errors: N (warnings: N)
  • Unused modules (Knip): list
  • Dependency mismatches: list
  • Expo Doctor issues: list

Phase 2 — Unit Tests (Jest)

Use Jest only. Do NOT use Vitest — it is not compatible with React Native Testing Library.

Test pure logic: calculations, validators, transformations, custom hooks in isolation.

// Example: testing a custom hook with React Hooks Testing Library
import { renderHook } from '@testing-library/react-hooks';
import { useIsRiskyTimeSlot } from '../hooks/useIsRiskyTimeSlot';

describe("useIsRiskyTimeSlot", () => {
  it.each([
    { start: "00:00", end: "06:00", expected: true, desc: "midnight to 6am is risky" },
    { start: "07:00", end: "17:00", expected: false, desc: "daytime is safe" },
  ])("$desc: $expected", ({ start, end, expected }) => {
    const { result } = renderHook(() => useIsRiskyTimeSlot({ start, end }));
    expect(result.current).toBe(expected);
  });
});

Rules:

  • One behavior per test — "Arrange, Act, Assert" pattern
  • Human-readable test names: "shows error banner when payment fails" not "test1"
  • Use it.each() for parameterized cases
  • Co-locate test files: Button.tsxButton.test.tsx
  • Only mock: native modules (Camera, BLE, Notifications), network requests
  • Never mock: your own business logic, hooks, or UI components
npx jest --coverage --testPathPattern="\.test\.(ts|tsx)$"

Phase 3 — Integration Tests (RNTL + Expo Router)

Integration tests validate how screens and components work together — this is the most valuable layer for React Native. Test at the screen level, not the component level.

Standard RNTL screen test

import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { LoginScreen } from '../screens/LoginScreen';

describe("LoginScreen", () => {
  it("shows validation error when email is empty", async () => {
    render(<LoginScreen />);
    
    fireEvent.press(screen.getByText("Sign In"));
    
    await waitFor(() => {
      expect(screen.getByText("Email is required")).toBeTruthy();
    });
  });

  it("navigates to dashboard after successful login", async () => {
    // mock network only
    server.use(http.post('/api/auth/login', () => HttpResponse.json({ user: mockUser })));
    
    render(<LoginScreen />);
    fireEvent.changeText(screen.getByPlaceholderText("Email"), "user@example.com");
    fireEvent.changeText(screen.getByPlaceholderText("Password"), "password123");
    fireEvent.press(screen.getByText("Sign In"));
    
    await waitFor(() => {
      expect(screen.getByText("Welcome, User")).toBeTruthy();
    });
  });
});

Navigation testing with Expo Router

import { renderRouter, screen } from 'expo-router/testing-library';

it("navigates to profile when user taps avatar", async () => {
  renderRouter({
    index: HomeScreen,
    "profile/[id]": ProfileScreen,
  }, {
    initialUrl: "/",
  });

  fireEvent.press(screen.getByTestId("user-avatar"));

  await screen.findByText("My Profile");
  expect(screen).toHavePathname("/profile/123");
});

Rules:

  • Test from the screen level, not individual components (unless the component is complex standalone logic)
  • Use RNTL query priority: getByRole > getByText > getByPlaceholderText > getByTestId
  • Never query by CSS class (there are none in React Native)
  • Mock network with MSW (msw/react-native) — never mock your own services
  • Each test must map to a PRD Acceptance Criterion: it("user can add item to cart — AC from US004", ...)
npx jest --testPathPattern="\.integration\.(ts|tsx)$"
# or if co-located, run all jest:
npx jest

Phase 4 — Maestro E2E Flows

Use Maestro for E2E tests. Maestro runs YAML flows on a real simulator or device. Each flow corresponds to one USER-JOURNEY checklist item or a core user story.

Setup (if not installed)

curl -Ls "https://get.maestro.mobile.dev" | bash

Flow structure

# e2e/flows/onboarding.yaml
appId: com.yourapp.bundle
---
- launchApp:
    clearState: true

- assertVisible: "Welcome to App"

- tapOn: "Get Started"

- assertVisible: "Create your account"

- inputText:
    id: "email-input"
    text: "test@example.com"

- inputText:
    id: "password-input"  
    text: "TestPassword123!"

- tapOn: "Create Account"

- assertVisible: "Dashboard"
- assertNotVisible: "Error"

Main test suite file

# e2e/main.yaml
appId: com.yourapp.bundle
---
- runFlow: flows/onboarding.yaml
- runFlow: flows/auth-login.yaml
- runFlow: flows/core-action.yaml   # the single most important user action
- runFlow: flows/payments.yaml
- runFlow: flows/logout.yaml

Running Maestro locally

# Run all flows
maestro test e2e/main.yaml

# Run a single flow
maestro test e2e/flows/onboarding.yaml

# Open Maestro Studio (lets non-technical stakeholders record flows visually)
maestro studio

Maestro in CI via EAS

Add to eas.json:

{
  "build": {
    "e2e": {
      "withoutCredentials": true,
      "ios": { "simulator": true }
    }
  }
}
# Run E2E on EAS (nightly or pre-release)
eas build --profile e2e --platform ios
eas build:run --platform ios
maestro test e2e/main.yaml

Key rules:

  • Keep flows short and focused — one user journey per file
  • Use clearState: true on launchApp to ensure clean state
  • assertVisible before and after every significant action
  • assertNotVisible: "Error" after every action that could show an error
  • Cover both happy paths AND sad paths (invalid input, network errors)
  • Flows must cover every item in the USER-JOURNEY.md Completeness Checklist

File structure

e2e/
  main.yaml                    # orchestrates all flows
  flows/
    onboarding.yaml            # new user signup
    auth-login.yaml            # returning user login
    auth-logout.yaml
    core-action.yaml           # the most critical feature
    payments.yaml              # checkout/payment flow
    error-states.yaml          # invalid inputs, network failures
    journey-checklist.yaml     # validates USER-JOURNEY.md items

Phase 5 — USER-JOURNEY Completeness Validation

Read USER-JOURNEY.md. For each item in the Completeness Checklist:

  • ✅ A Maestro flow covers it and passes
  • ❌ No flow covers it, or the flow fails
  • ⚠️ Feature not yet implemented (can't test)

Rule: If >20% of checklist items are ❌ → verdict is ⚠️ ISSUES FOUND.


Phase 6 — Run All Tests

# Static (already ran in Phase 0)

# Unit + Integration (Jest)
npx jest --coverage

# E2E (requires simulator running)
maestro test e2e/main.yaml

Phase 7 — Output Report

TESTER_MOBILE_REPORT: {
  "feature": "<feature-name>",
  "preflight": {
    "expo_doctor": "passed | issues: [...]",
    "typescript": "passed | N errors",
    "eslint": "passed | N errors",
    "build_export": "passed | failed",
    "simulator": "iPhone 16 Pro (iOS 18.2) | unavailable"
  },
  "static_analysis": {
    "unused_modules": ["path/to/unused.ts"],
    "dependency_mismatches": ["react-native-reanimated@3.x incompatible"]
  },
  "unit_tests": {
    "total": N, "passed": N, "failed": N,
    "failures": ["useIsRiskyTimeSlot: expected true, got false"]
  },
  "integration_tests": {
    "total": N, "passed": N, "failed": N,
    "failures": ["LoginScreen: navigation to dashboard failed"]
  },
  "e2e_maestro": {
    "flows_run": ["onboarding", "auth-login", "core-action"],
    "passed": N, "failed": N,
    "failures": ["payments.yaml: assertVisible 'Order Confirmed' timed out"],
    "ran_on": "simulator | EAS | skipped (no simulator)"
  },
  "journey_checklist": {
    "total": N,
    "covered": N,
    "failed": ["User can reset password"],
    "not_implemented": ["Offline mode preserves cart"]
  },
  "verdict": "✅ READY | ⚠️ ISSUES FOUND | ❌ BROKEN"
}

Verdict rules:

  • ❌ BROKEN — TypeScript errors OR build export fails OR any Maestro flow crashes (not assertion failure, actual crash)
  • ⚠️ ISSUES FOUND — Maestro assertions fail, >20% journey uncovered, or integration tests fail
  • ✅ READY — All preflight passes, all Jest passes, all Maestro flows pass, >80% journey covered

Constraints

  • Jest only for unit/integration — never Vitest in React Native projects
  • Maestro only for E2E — never Playwright, never Stagehand (browser-only tools)
  • Never modify production code
  • Only create/modify *.test.*, *.integration.*, e2e/
  • Only mock: native modules (Camera, BLE, Push Notifications) and network requests
  • Never mock your own services, hooks, or business logic
  • Maestro flows always run with clearState: true to ensure isolation
  • If simulator is unavailable, document it and skip E2E — never fake the results
Weekly Installs
1
First Seen
6 days ago
Installed on
zencoder1
amp1
cline1
openclaw1
opencode1
cursor1