Migrating from Detox
If you have an existing Detox suite, you already invested in selectors, assertions, and CI plumbing that work. klera is not a rebuild — adopt it incrementally, keep Detox running flow-by-flow, and retire the old suite once the new one has equivalent coverage.
This guide maps the patterns Detox tests use in practice to their klera equivalents, calls out the model-level differences worth understanding before you start, and ends with an honest “what doesn’t migrate yet” list.
What klera adds
Detox’s matchers, taps, types, swipes, and simulator control all have direct klera equivalents — the surface is familiar. The differences are what runs on top of that surface.
- Self-healing matcher. Detox’s
by.id/by.text/by.labelare exact matchers — when a screen redesign moves a button into a new container or renames atestID, the test fails until someone hand-fixes the selector. klera walks a strategy ladder (testID → accessibilityLabel → role+text → fuzzy text) and records drift events without rewriting the flow file. - Failed runs auto-classify. A failing klera flow produces a
verdict —
regression/drift/flake/data— plus a PM-readable narrative and a list of suspect commits. Detox surfaces the matcher failure; you triage from there. - Lower-friction surface. Zero-config adoption (
klera init), watch-mode authoring (klera run --watch), JSON Schema in the editor, generated CI configs (klera ci), OpenTelemetry-native runs.
Model differences worth understanding
Detox tests are imperative JavaScript that drive a device through function calls, run by a test runner (typically Jest). klera flows are declarative prose or YAML that an engine executes.
| Detox | klera | |
|---|---|---|
| Authoring surface | JS / TS test files | Prose (*.flow.md) or YAML (*.flow.yaml) |
| Execution model | Test runner orchestrates it blocks | Engine reads IR, dispatches step-by-step |
| Selector binding | by.id('email') resolves at call time, exact | Strategy ladder + drift recovery |
| Sync model | device.disableSynchronization() opt-out | waitForIdle step + automatic per-step settling |
| Output | Test runner report (Jest) | Versioned JSON + HTML render + JUnit + OTel spans |
| Failure dossier | Stack trace + screenshot | Boundary snapshots, frames, matcher trace, suspect commit |
The shift: “set up the device, write a sequence of actions, assert” becomes “declare the steps in order; the engine handles wait, retry, drift, capture.”
Pattern-by-pattern mapping
Each pair below shows a Detox fragment and its klera equivalent. The klera column uses the short-form sugar where the meaning is unambiguous; long form is always available.
Tapping
Detox
await element(by.id('submit')).tap();
await element(by.text('Sign In')).tap();Typing
Detox
await element(by.id('email')).typeText('user@example.com');Scrolling and swiping
Detox
await element(by.id('list')).scroll(200, 'down');
await element(by.id('card')).swipe('left', 'fast');Assertions
Detox
await expect(element(by.id('welcome'))).toBeVisible();
await expect(element(by.id('error'))).not.toBeVisible();
await expect(element(by.id('greeting'))).toHaveText('Welcome back');Waits and synchronisation
Detox synchronisation is opt-out — you disable it when it gets in the way and remember to re-enable it. klera flips this: per-step settling is automatic, and you opt in to gate-aware idle when you need a specific signal (animations done, network quiet, both).
Detox
// explicit timeout
await waitFor(element(by.id('toast'))).toBeVisible().withTimeout(5000);
// disable sync, manual sleep
await device.disableSynchronization();
await new Promise(r => setTimeout(r, 1000));
await device.enableSynchronization();The runtime tracks Animated / LayoutAnimation / rAF for animations and
wraps fetch / XHR / WebSocket for network; waitForIdle reports idle
the moment both gates are clear.
Device + lifecycle
Detox
await device.reloadReactNative();
await device.launchApp({ url: 'myapp://welcome' });
await device.setLocation(48.8566, 2.3522);
await device.matchFace();
await device.sendToHome();
await device.launchApp({ newInstance: false });Permissions
Detox grants permissions at launch via launchApp({ permissions }).
klera uses an explicit grantPermission step so the grant lives in
the flow, visible to the reader.
Detox
await device.launchApp({
permissions: { camera: 'YES', notifications: 'YES' },
});grantPermission is currently Android-only; iOS rejects with
capability_unsupported. iOS adopters use the existing system-dialog
auto-allow that ships with relaunch.
System UI
Detox
await element(by.label('OK')).atIndex(0).tap(); // dismiss alert
await element(by.id('submit')).tap();
await element(by.label('Cancel')).atIndex(0).tap(); // action sheetVisual snapshots
Detox’s screenshot diff requires the detox-snapshot-diff plugin and
manual baseline management. klera ships visual regression as a
first-class step:
- visualSnapshot:
id: onboarding-step-1
tolerancePercent: 1Baselines land at __baselines__/<flow>/<id>.png; mismatch produces a
diff PNG triplet (actual / baseline / diff) embedded in the HTML report.
Worked example
A 30-line Detox spec rewritten as prose plus the IR cache klera commits alongside.
Detox
// e2e/login.test.js
describe('Login', () => {
beforeAll(async () => {
await device.launchApp({
newInstance: true,
permissions: { notifications: 'YES' },
});
});
it('signs in with the seeded user', async () => {
await waitFor(element(by.id('email'))).toBeVisible().withTimeout(5000);
await element(by.id('email')).typeText('user@test.com');
await element(by.id('password')).typeText('correct horse');
await element(by.id('submit')).tap();
await expect(element(by.text('Welcome back'))).toBeVisible();
});
});klera prose
# Sign in with the seeded user
Relaunch the app and grant notification permissions. Wait for the email
field, type the seeded email and password, tap Sign In, and assert that
the welcome greeting is visible.That is the whole flow file (flows/login.flow.md). Save it and the
Metro plugin compiles the prose to
the IR cache below.
klera IR cache (committed plumbing)
# flows/login.flow.yaml — same flow, hand-authored.
name: Sign in with the seeded user
steps:
- relaunch: true
- grantPermission: notifications
- wait:
for: { testID: email }
timeoutMs: 5000
- type:
into: { testID: email }
value: user@test.com
- type:
into: { testID: password }
value: correct horse
- tap: { testID: submit }
- assert: { visible: Welcome back }Both forms run on the same executor. Hand-author YAML when you want a no-LLM authoring path; write prose when you want the planner to figure out the steps.
Incremental migration
Two patterns work well in practice. You do not need to commit to a big-bang rewrite.
Side-by-side runs
Run Detox and klera in parallel during the migration window. Both suites run on the same dev client; klera attaches to a fresh bridge port. CI scripts add a klera step alongside the existing Detox step:
detox test --configuration ios.sim.release # existing
klera run flows/login.flow.md \
--report reports/login.json # addedWhen klera coverage equals Detox coverage for a flow, drop the Detox test. The two engines do not conflict — Detox uses Earl Grey / XCUITest through the launchscreen; klera uses its own runtime + bridge.
Flow-by-flow rewrite
Pick the most-broken Detox test — the one rewritten most often, or the one that flakes most — and re-author it as a klera flow. Compare run-counts over a week. If klera wins on stability or debuggability, migrate the next flow. Repeat until the Detox suite is empty.
This recipe converges faster than wholesale rewrite because the most brittle Detox tests are typically the ones whose selectors moved most — exactly the cases klera’s matcher self-heals through.
Both engines share the simulator, not the runtime. Run them back-to-back rather than concurrently against the same simulator process — they will fight for control if they both think they own the app.
What does not migrate cleanly yet
An honest list, no marketing.
- Custom matchers and helpers. Extracted helper modules are imperative JS — you re-author the steps as a flow, not the JS. The klera equivalent is shared flows via fixture files for parameter data.
- Code-heavy assertions. Detox lets you call into JS to compute an
expected value mid-test. klera’s prose path handles many of these by
letting the planner derive the IR; for the rest,
assertJScovers most cases. - Network mocking. klera mocks at the IR level — straight port for response-shape mocks, not yet for request-recording. See network mocking.
- Element introspection.
getAttributes()returns a JS object you can inspect mid-test. klera’s matcher trace is per-step in the report; mid-flow introspection happens viaassertJS. - Multi-app flows. klera assumes one app per bridge today.
If your suite leans heavily on any of these, file an issue with a representative test.
Reference
- Quickstart —
klera initwalkthrough - Prose flows — the lead authoring surface
- IR reference — every step, every field
- Detox docs — incumbent reference