How it works
Connect
Attaches to your running app via the Hermes CDP bridge that Metro already exposes. No simulator plugins, no separate proxy, no Appium server to spin up.
Find
Traverses the live React fiber tree to locate elements by testID, component name, text content, or accessibility attributes - the same tree React DevTools reads.
Interact
Calls React prop handlers directly - onPress, onChangeText, onValueChange. No native event dispatch, no coordinate math, no gesture simulation.
Features
Zero dependencies
No Appium, no WebDriver, no native test servers. The only runtime requirement is Node and a Hermes-powered Metro bundle.
iOS + Android
Works with iOS Simulators and Android emulators or physical devices. One test file, two platforms - switch with PLATFORM=android.
Full TypeScript
All APIs are typed end-to-end. Query results, element methods, config, and assertions - no any, no casting gymnastics.
Works with any Hermes app
Hermes has been the default React Native engine since RN 0.70. If your app runs on Hermes, Stowaway works out of the box.
One command to onboard
npx stowaway init scaffolds the entry point, a smoke test, and npm scripts - from zero to first run in under a minute.
JUnit XML + JSON output
Results land in test-results/ as JUnit XML and JSON. Plug straight into GitHub Actions, Bitrise, or Jenkins with no extra steps.
What a test looks like
Clean TypeScript. No config files. No driver boilerplate.
import { describe, it, expect } from 'stowaway';
import type { AppSession } from 'stowaway';
describe('Login', () => {
it('signs in and reaches the home screen', async (app: AppSession) => {
await (await app.find({ testID: 'input-email' })).typeText('user@example.com');
await (await app.find({ testID: 'input-password' })).typeText('secret');
await (await app.find({ testID: 'btn-sign-in' })).tap();
const welcome = await app.waitForElement('home-title');
expect(await welcome.text()).toBe('Welcome back');
});
it('shows an error on bad credentials', async (app: AppSession) => {
await app.mockNetwork(
{ method: 'POST', url: /\/api\/login/ },
{ status: 401, body: { error: 'Invalid credentials' } },
);
await (await app.find({ testID: 'btn-sign-in' })).tap();
const banner = await app.waitForElement('error-banner');
expect(await banner.text()).toContain('Invalid credentials');
});
});