<!-- Skal docs — markdown mirror of https://skal.run/docs/testing.html -->

# Testing

Three runtimes, one test story: fast unit tests in `bun test`, host tests in `flutter test`, and Maestro flows tapping the real app on a real device. Here's what to test where — with working examples.

## What to test where

| Layer | Runner | Speed | Reach for it when… |
| --- | --- | --- | --- |
| Unit | `bun test` | ms | logic, signals, store selectors — anything that isn't a widget |
| Host (Dart) | `flutter test` | seconds | you wrote a custom widget adapter or touched wire decoding |
| E2E | Maestro | ~30 s / flow | user journeys: launch, navigate, tap, type, assert — the whole stack |

Rule of thumb: **every bug gets a unit test; every user journey gets a flow.** The host layer is the framework's job — you only add Dart tests when you ship your own adapters.

## Unit tests (`bun test`)

Your app logic is plain JavaScript running on Solid primitives, so it tests as plain JavaScript: `*.test.js` files anywhere in the package run with `bun test` — signals, memos, derived state, reducers, formatting, store selectors. No device, no emulator, milliseconds per run:

```jsx
// src/cart.test.js
import { test, expect } from 'bun:test';
import { createRoot } from 'solid-js';
import { createCart } from './cart';

test('total reacts to quantity', () => {
  createRoot(() => {
    const cart = createCart();
    cart.add({ id: 1, price: 10 }, 3);
    expect(cart.total()).toBe(30);
  });
});
```

A headless component-render harness (mount JSX on an in-memory bridge, query by `testID`, assert emitted ops) is on the roadmap. Today, widget-level behavior is covered by Maestro flows below — they query the same `testID`s on the real app.

## Host tests (`flutter test`)

Only needed when you write custom Dart adapters. Put widget tests next to your adapter in `flutter-host/test/`; the framework's own decoder, wire format, and builders are covered upstream. `bun run test` runs them.

## E2E with Maestro

Flows are declarative YAML driving the **real app** through the platform accessibility tree — you write user journeys in neither of the app's two languages. The scaffold ships `.maestro/smoke.yaml`; add one file per journey:

```jsx
# .maestro/checkout.yaml
appId: com.example.myapp
---
- launchApp
- extendedWaitUntil:                 # covers async JS boot — always start with this
    visible: { id: "home-title" }
    timeout: 15000
- tapOn: { id: "product-3" }
- inputText: "2"
- tapOn: { id: "add-to-cart" }
- swipe: { direction: UP }
- tapOn: { id: "checkout" }
- assertVisible: { id: "order-confirmed" }
- takeScreenshot: checkout-done
```

```jsx
$ bun run test:e2e                 # all flows in .maestro/, attached device/emulator
$ maestro test .maestro/checkout.yaml   # one flow by file
[Passed] smoke (37s) · checkout (52s) — 2/2 flows
```

**Match by `id:`, not visible text.** Give every widget you assert or tap a `testID` — it becomes the platform accessibility identifier, survives copy changes, and is the same query your unit tests use. Skal widget labels live in the accessibility _description_, which Maestro's plain-string matcher doesn't read.

### Debugging a failing flow

-   `maestro hierarchy` — dump exactly what Maestro sees on the current screen. If your `testID` isn't in there, the widget isn't in the semantics tree (or you're on the wrong screen).
-   `maestro studio` — point-and-click flow recorder in the browser; great for finding selectors.
-   On failure, Maestro saves screenshots + logs under `~/.maestro/tests/` and prints the path.
-   Very long screens: extremely tall single `<ScrollView>`s can defer semantics for their top content — prefer `<ListView>` (virtualized) for long pages, or assert on a shorter screen.

### E2E in CI

Flows run anywhere Maestro and an emulator do. A minimal GitHub Actions job — an Android emulator on a runner, the app installed from the workflow's own build:

```jsx
# .github/workflows/e2e.yml (yours to add)
- uses: reactivecircus/android-emulator-runner@v2
  with:
    api-level: 34
    script: |
      bun run build
      (cd flutter-host && flutter build apk --debug)
      adb install flutter-host/build/app/outputs/flutter-apk/app-debug.apk
      maestro test .maestro/
```

Flows are the release gate: if a journey breaks, the merge waits — not your users.

[Previous← Hot reload & dev loop](tooling.html)
