Skip to content

Test Runner

Tapsmith includes a built-in test runner with an API inspired by Jest and Playwright.

test(name: string, fn: (fixtures: TestFixtures) => Promise<void>): void

Section titled “test(name: string, fn: (fixtures: TestFixtures) => Promise<void>): void”

Register a test. The test function receives a fixtures object containing a device instance.

test("user can log in", async ({ device }) => {
await device.getByText("Sign In", { exact: true }).tap();
});

Run only this test (and other tests marked with .only). All other tests are skipped.

test.only("focused test", async ({ device }) => {
// Only this test will run
});

Skip this test.

test.skip("broken test", async ({ device }) => {
// This test will not run
});

Override configuration options for all tests in the current describe scope. Overrides cascade — inner describe blocks inherit and can further override outer ones.

describe("slow animations screen", () => {
test.use({ timeout: 60000 })
test("animation completes", async ({ device }) => {
// runs with 60s timeout instead of the default
})
})

Multiple calls in the same scope merge together:

describe("custom config", () => {
test.use({ timeout: 60000 })
test.use({ screenshot: "always" })
// equivalent to: test.use({ timeout: 60000, screenshot: "always" })
})

UseOptions

OptionTypeDescription
timeoutnumberAction/assertion timeout (ms)
screenshot'always' | 'only-on-failure' | 'never'Screenshot capture mode
retriesnumberRetry count for failed tests
traceTraceMode | Partial<TraceConfig>Trace recording configuration. See configuration.md for the full TraceConfig shape (includes network, networkHosts, networkIgnoreHosts, screenshots, etc.).
videoVideoMode | Partial<VideoConfig>Video recording configuration. See the Video recording section below.
appStatestringPath to saved app state archive to restore

The following device-shaping fields may only be set on a project’s use block (not via test.use()), since the device is bound to the worker before any test runs:

OptionTypeDescription
platform'android' | 'ios'Target platform for this project
devicestringExplicit device serial / iOS UDID
avdstringAndroid AVD name to launch
simulatorstringiOS simulator name or UDID
apkstringPath to Android APK under test
appstringPath to iOS .app bundle under test
packagestringAndroid package name / iOS bundle ID
activitystringOptional Android launcher activity
agentApkstringOverride path to the Android agent APK
agentTestApkstringOverride path to the Android agent test APK
iosXctestrunstringOverride path to the iOS .xctestrun file
deviceStrategy'prefer-connected' | 'avd-only'Device selection strategy (Android)
launchEmulatorsbooleanAuto-launch emulators (Android)
resetAppDeepLinkstringSoft-reset deep link between files
resetAppWaitMsnumberWait after the reset deep link

Reusable auth state — mirrors Playwright’s storageState:

// Setup: authenticate once and save state
test("authenticate", async ({ device }) => {
await device.launchApp("com.example.myapp");
// ... perform login flow ...
await device.saveAppState("com.example.myapp", "./auth-state.tar.gz");
});
// Tests: restore state instead of logging in
describe("authenticated tests", () => {
test.use({ appState: "./auth-state.tar.gz" });
test("shows profile", async ({ device }) => {
// Already logged in — no login flow needed
});
});

The fixtures object passed to every test function. Destructure the fields you need:

test("example", async ({ device, request, projectName, platform }) => {
// device — the primary interface for interacting with the mobile device
// request — HTTP client for API calls (seeding data, fetching tokens, etc.)
// projectName — current project name (when using multi-project config), or undefined
// platform — resolved platform: "android" or "ios"
});
FixtureTypeDescription
deviceDevicePrimary interface for interacting with the mobile device
requestAPIRequestContextHTTP client for API calls. See API Request Fixture.
projectNamestring | undefinedName of the current project (from projects config). undefined when no projects are configured.
platform'android' | 'ios'Resolved target platform for the current worker.

Create a new test function with additional custom fixtures. Follows the same pattern as Playwright’s test.extend().

Each fixture definition is a function that receives all other fixtures and a use callback. The fixture sets up its value, passes it to use(), and optionally cleans up after use() resolves.

import { test as base, expect } from "tapsmith";
// Define a custom fixture that seeds a todo item via the API before each test
const test = base.extend<{ todoId: string }>({
todoId: async ({ request }, use) => {
// Setup: create a todo item via the API
const res = await request.post("https://api.example.com/todos", {
data: { title: "Buy groceries", completed: false },
});
const { id } = await res.json() as { id: string };
// Provide the fixture value to the test
await use(id);
// Teardown: clean up after the test (runs even if the test fails)
await request.delete(`https://api.example.com/todos/${id}`);
},
});
test("can mark todo as complete", async ({ device, todoId, request }) => {
await device.getByText("Refresh").tap();
await device.getByText("Buy groceries").tap();
await expect(device.getByRole("checkbox")).toBeChecked();
// Verify the change persisted via the API
const res = await request.get(`https://api.example.com/todos/${todoId}`);
const todo = await res.json() as { completed: boolean };
expect(todo.completed).toBe(true);
});

For authentication, prefer setup projects with device.saveAppState() and test.use({ appState }) instead of custom fixtures. This mirrors Playwright’s storageState pattern — authenticate once, save to a file, and restore across all tests. See the auth state example above and the Authentication patterns section in the Writing Tests guide.

Fixture scopes:

By default, fixtures have test scope (created and torn down for each test). Use a tuple to specify worker scope (created once per worker, shared across tests):

const test = base.extend<{ apiToken: string }>({
apiToken: [async ({ request }, use) => {
const res = await request.post("https://api.example.com/auth/service-token", {
data: { clientId: process.env.API_CLIENT_ID },
});
const { token } = await res.json() as { token: string };
await use(token);
}, { scope: "worker" }],
});
ScopeLifecycleUse for
test (default)Created before each test, torn down afterPer-test data (seeded records, temp files)
workerCreated once when the worker starts, torn down when it endsExpensive setup (API tokens, database connections)

Custom fixtures can depend on other custom fixtures — they’re resolved in dependency order automatically.

describe(name: string, fn: () => void): void

Section titled “describe(name: string, fn: () => void): void”

Group tests into a suite.

describe("Login flow", () => {
test("valid credentials", async ({ device }) => { /* ... */ });
test("invalid credentials", async ({ device }) => { /* ... */ });
});

describe.only(name, fn) / describe.skip(name, fn)

Section titled “describe.only(name, fn) / describe.skip(name, fn)”

Focus or skip an entire suite.

beforeAll(fn: (fixtures) => void | Promise<void>): void

Section titled “beforeAll(fn: (fixtures) => void | Promise<void>): void”

Run a function once before all tests in the current suite. Receives { device, projectName }.

beforeAll(async ({ device }) => {
// One-time setup for the suite
await device.launchApp("com.example.myapp", { clearData: true });
});

afterAll(fn: (fixtures) => void | Promise<void>): void

Section titled “afterAll(fn: (fixtures) => void | Promise<void>): void”

Run a function once after all tests in the current suite. Receives { device, projectName }.

beforeEach(fn: (fixtures) => void | Promise<void>): void

Section titled “beforeEach(fn: (fixtures) => void | Promise<void>): void”

Run a function before each test in the current suite. Hooks are inherited by nested suites. Receives { device, projectName }.

beforeEach(async ({ device }) => {
await device.restartApp("com.example.myapp");
});

afterEach(fn: (fixtures) => void | Promise<void>): void

Section titled “afterEach(fn: (fixtures) => void | Promise<void>): void”

Run a function after each test in the current suite. Runs even if the test fails. Receives { device, projectName }.

Note: Hooks receive device and projectName only. The request fixture is test-scoped and only available inside test() callbacks. platform is not currently passed to hooks — if you need the platform in a hook, read it from the config or use projectName to infer it.