# Testing

> Headless Vitest and Node-native suites, plus real-device Detox journeys across adapters.

An app built with SymbioteNative is tested the same way as any other React Native
app — SymbioteNative doesn't ask for a different tool. Two layers: headless
component/integration tests, and real-device end-to-end journeys.

## Headless tests — Vitest and Node

Component and integration tests run against a fake Fabric slot — no simulator,
device, or native build. Production-critical ESM/CommonJS tools use Node's
built-in test runner so they exercise the exact module format shipped and run
without a Vite transform:

```bash
pnpm test          # both layers
pnpm test:vitest   # TypeScript component/integration suites
pnpm test:node     # native .test.mjs/.test.cjs tooling suites
```

Vitest tests live next to what they exercise (`Component.test.ts(x)` beside
`Component.ts(x)`) across `core/`, `adapters/`, and `packages/`. The Node
runner discovers every `*.test.mjs` and `*.test.cjs` under `scripts/` and
`packages/`, excluding generated build and dependency trees. Adding such a
suite therefore places it under the same root `pnpm test` command CI runs.

## End-to-end journeys — Detox

Real-device (or simulator) tests drive an actual build of the app:

```bash
pnpm -C examples/react e2e:build:ios && pnpm -C examples/react e2e:test:ios
pnpm -C examples/react e2e:build:android && pnpm -C examples/react e2e:test:android
```

The `e2e:*` scripts live in each example's own `package.json`
(`examples/react`, `examples/vue-sfc`, `examples/vue-tsx`, `examples/angular`,
`examples/svelte`), not the repo root.

<Aside type="tip" title="One spec file, shared across React, Vue, and Svelte">
  Detox attaches to the stock React Native host **below** the layer
  SymbioteNative replaces — it drives the real native views through Fabric's own
  accessibility tree, the same way it would for a plain React Native app. The
  same `canary-journeys` e2e spec runs unmodified across `examples/react`,
  `examples/vue-sfc`, `examples/vue-tsx`, and `examples/svelte`, as long as each
  exposes the same `testID`s. A component that mounts but behaves wrong under
  one adapter — say, a native-driven `Animated` view that renders but never
  actually animates — fails the same assertion the same way regardless of which
  framework rendered it. `examples/angular` has its own Detox e2e harness today
  but doesn't yet share that exact spec file. `examples/solid` has no Detox e2e
  harness yet at all.
</Aside>

## A gotcha specific to perpetual animations

Detox's default synchronization waits for the app to go idle before running
the next step — but an app with a perpetual native `Animated.loop` (a
heartbeat pulse, a spinner) never reports idle, so `device.launchApp()`
hangs. Disable synchronization for that launch:

```ts
await device.launchApp({
  newInstance: true,
  launchArgs: { detoxEnableSynchronization: 0 },
});
```

You then drive individual assertions with explicit `waitFor(...)` calls
instead of relying on Detox's automatic idle detection.
