Configuration
Configuration lives in spana.config.ts at the project root. Pass the config object to defineConfig for type safety.
import { defineConfig } from "spana-test";
export default defineConfig({ // ...});Use --config ./path/to/spana.config.ts to specify a different location.
Run spana validate-config to validate the file without starting drivers or discovering flows.
Full example
Section titled “Full example”import { defineConfig } from "spana-test";
export default defineConfig({ apps: { web: { url: "http://localhost:3000" }, android: { packageName: "com.example.app", appPath: "./builds/app.apk" }, ios: { bundleId: "com.example.app", appPath: "./builds/App.app", signing: { teamId: "ABCDE12345" }, }, }, platforms: ["web", "android", "ios"], flowDir: "./flows", reporters: ["console", "json", "html"], defaults: { waitTimeout: 5000, pollInterval: 200, settleTimeout: 300, retries: 2, }, artifacts: { outputDir: ".spana/artifacts", captureOnFailure: true, captureOnSuccess: false, captureSteps: false, screenshot: true, uiHierarchy: true, }, hooks: { beforeAll: async ({ app }) => { /* global setup */ }, beforeEach: async ({ app }) => { /* reset state */ }, afterEach: async ({ app, result }) => { /* teardown */ }, afterAll: async ({ app, summary }) => { /* cleanup */ }, },});Defines the app targets for each platform.
apps: { web?: { url: string; appPath?: string }; android?: { packageName: string; appPath?: string }; ios?: { bundleId: string; appPath?: string; signing?: IOSSigningConfig };}| Field | Platform | Description |
|---|---|---|
web.url | Web | Base URL Playwright navigates to on launch |
android.packageName | Android | Android application ID (e.g. com.example.app) |
ios.bundleId | iOS | iOS bundle identifier (e.g. com.example.app) |
appPath | Android/iOS | Path to .app, .ipa, or .apk for auto-install |
signing.teamId | iOS | Apple Development Team ID (required for physical devices) |
signing.signingIdentity | iOS | Code signing identity (default: "Apple Development") |
platforms
Section titled “platforms”platforms?: Array<"web" | "android" | "ios">Which platforms to run tests on by default. Can be overridden per-flow with FlowConfig.platforms and at the CLI with --platform.
Default: ["web"]
flowDir
Section titled “flowDir”flowDir?: stringDirectory to discover flow files from. Accepts a glob or directory path.
Default: "./flows"
Relative paths are resolved from the directory that contains the config file, not from the current shell directory.
reporters
Section titled “reporters”reporters?: string[]One or more reporter names. Available reporters:
| Name | Output |
|---|---|
console | Human-readable terminal output (default) |
json | Structured JSON events to stdout |
junit | JUnit XML — compatible with CI artifact ingestion |
html | Self-contained HTML report with embedded screenshots |
allure | Allure-compatible result files |
Default: ["console"]
execution
Section titled “execution”Execution mode and remote Appium settings.
execution?: { mode?: "local" | "appium"; web?: { browser?: "chromium" | "firefox" | "webkit"; headless?: boolean; storageState?: string; }; appium?: { serverUrl?: string; capabilities?: Record<string, unknown>; capabilitiesFile?: string; reportToProvider?: boolean; browserstack?: { app?: { id?: string; path?: string; name?: string; customId?: string }; local?: { enabled?: boolean; binary?: string; identifier?: string; args?: string[] }; options?: Record<string, unknown>; }; saucelabs?: { app?: { id?: string; path?: string; name?: string }; connect?: { enabled?: boolean; binary?: string; tunnelName?: string; args?: string[] }; options?: Record<string, unknown>; }; };}Use execution.web to configure the local Playwright runtime for web flows. storageState is resolved relative to spana.config.ts, so you can preload a saved auth/session state file without hard-coding absolute paths.
Use mode: "appium" when running against BrowserStack, Sauce Labs, or another Appium-compatible grid.
Raw capabilities from execution.appium.capabilities, capabilitiesFile, and --caps-json remain the strongest override surface. The provider helper sections above fill in missing provider-specific fields and can manage BrowserStack Local / Sauce Connect lifecycle when enabled.
CLI precedence
Section titled “CLI precedence”For spana test, CLI flags win over config values.
| CLI flag | Config field |
|---|---|
--platform | platforms |
--reporter | reporters |
--retries | defaults.retries |
--driver | execution.mode |
--appium-url | execution.appium.serverUrl |
--validate-config | Validates config and exits |
--shard / --bail | CLI-only execution controls |
--debug-on-failure | CLI-only execution control |
Sharding happens after tag/name/platform filtering so each shard gets a deterministic slice of the already-selected flows.
defaults
Section titled “defaults”Timing and retry defaults applied to all auto-wait operations. Individual operations can override these with WaitOptions.
defaults?: { waitTimeout?: number; // ms pollInterval?: number; // ms settleTimeout?: number; // ms retries?: number;}| Option | Default | Description |
|---|---|---|
waitTimeout | 5000 | Maximum ms to wait for an element to appear |
pollInterval | 200 | ms between hierarchy polls |
settleTimeout | 300 | ms the element must remain stable before matching |
retries | 2 | Number of retries on action failure (e.g. tap on stale element) |
artifacts
Section titled “artifacts”Controls screenshot and hierarchy capture on test completion.
artifacts?: { outputDir?: string; captureOnFailure?: boolean; captureOnSuccess?: boolean; captureSteps?: boolean; screenshot?: boolean; uiHierarchy?: boolean;}| Option | Default | Description |
|---|---|---|
outputDir | ".spana/artifacts" | Directory to write captured artifacts |
captureOnFailure | true | Capture on failed flows |
captureOnSuccess | false | Capture on passed flows |
captureSteps | false | Capture a screenshot after every step in the flow |
screenshot | true | Include screenshot in capture |
uiHierarchy | true | Include UI hierarchy dump in capture |
Lifecycle hooks that run around flow execution. Each hook receives a HookContext.
hooks?: { beforeAll?: (ctx: HookContext) => Promise<void>; beforeEach?: (ctx: HookContext) => Promise<void>; afterEach?: (ctx: HookContext) => Promise<void>; afterAll?: (ctx: HookContext) => Promise<void>;}| Hook | When it runs |
|---|---|
beforeAll | Once before all flows on a platform |
beforeEach | Before each individual flow |
afterEach | After each individual flow (always runs, even on failure) |
afterAll | Once after all flows on a platform |
HookContext provides app, expect, platform, result (in afterEach), and summary (in afterAll).
Error handling
Section titled “Error handling”| Hook | On failure |
|---|---|
beforeAll | All flows on that platform are skipped and marked as failed |
beforeEach | That flow is skipped and marked as failed |
afterEach | Warning logged, test result is not affected |
afterAll | Warning logged, test results are not affected |
Example
Section titled “Example”export default defineConfig({ hooks: { beforeEach: async ({ app }) => { await app.launch({ clearState: true }); }, afterEach: async ({ app, result }) => { if (result?.status === "failed") { console.log(`Flow failed: ${result.name}`); } }, },});Per-flow configuration
Section titled “Per-flow configuration”The flow() function accepts an optional config object as its second argument, letting you override global settings for a single flow.
import { flow } from "spana-test";
flow( "checkout", { timeout: 30000, tags: ["smoke"], artifacts: { captureSteps: true } }, async ({ app }) => { // ... },);interface FlowConfig { tags?: string[]; platforms?: Array<"web" | "android" | "ios">; timeout?: number; autoLaunch?: boolean; artifacts?: ArtifactConfig;}| Option | Default | Description |
|---|---|---|
tags | [] | Tags for filtering flows with --tag on the CLI |
platforms | — | Override which platforms this flow runs on |
timeout | — | Per-flow timeout in ms (overrides global defaults.waitTimeout) |
autoLaunch | true | Automatically launch the app before the flow starts |
artifacts | — | Per-flow artifact overrides (same shape as global artifacts) |
The per-flow artifacts object is merged with the global artifacts config, so you only need to specify the fields you want to override. For example, enabling captureSteps on a single flow:
flow( "visual regression", { artifacts: { captureSteps: true, captureOnSuccess: true } }, async ({ app }) => { // Every step is captured, and the final state is saved even on success },);launchOptions
Section titled “launchOptions”Default launch options applied to every flow. Individual flows can override these via app.launch().
launchOptions?: LaunchOptionsexport default defineConfig({ launchOptions: { clearState: true, // fresh state every run },});LaunchOptions reference
Section titled “LaunchOptions reference”interface LaunchOptions { clearState?: boolean; clearKeychain?: boolean; deepLink?: string; launchArguments?: Record<string, unknown>;}| Option | Default | Description |
|---|---|---|
clearState | false | Clear app data/storage before launch |
clearKeychain | false | Clear the iOS keychain before launch (simulator only) |
deepLink | — | Open the app via a deep link URL |
launchArguments | — | Key-value pairs passed as launch arguments to the app |
Platform behavior
Section titled “Platform behavior”| Option | Web | Android | iOS |
|---|---|---|---|
clearState | Clears cookies, localStorage | adb shell pm clear | Resets app permissions |
clearKeychain | No-op | No-op (warns) | xcrun simctl keychain reset |
launchArguments | No-op | Passed as --es extras via am start | Not yet supported (planned) |
deepLink | Navigates to URL | adb shell am start -d <url> | xcrun simctl openurl / WDA |
Per-flow usage
Section titled “Per-flow usage”When autoLaunch is false, call app.launch() manually with options:
flow("onboarding", { autoLaunch: false }, async ({ app }) => { await app.launch({ clearState: true, deepLink: "myapp://welcome" }); // ...});