Skip to content

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.

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 };
}
FieldPlatformDescription
web.urlWebBase URL Playwright navigates to on launch
android.packageNameAndroidAndroid application ID (e.g. com.example.app)
ios.bundleIdiOSiOS bundle identifier (e.g. com.example.app)
appPathAndroid/iOSPath to .app, .ipa, or .apk for auto-install
signing.teamIdiOSApple Development Team ID (required for physical devices)
signing.signingIdentityiOSCode signing identity (default: "Apple Development")
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?: string

Directory 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?: string[]

One or more reporter names. Available reporters:

NameOutput
consoleHuman-readable terminal output (default)
jsonStructured JSON events to stdout
junitJUnit XML — compatible with CI artifact ingestion
htmlSelf-contained HTML report with embedded screenshots
allureAllure-compatible result files

Default: ["console"]

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.

For spana test, CLI flags win over config values.

CLI flagConfig field
--platformplatforms
--reporterreporters
--retriesdefaults.retries
--driverexecution.mode
--appium-urlexecution.appium.serverUrl
--validate-configValidates config and exits
--shard / --bailCLI-only execution controls
--debug-on-failureCLI-only execution control

Sharding happens after tag/name/platform filtering so each shard gets a deterministic slice of the already-selected flows.

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;
}
OptionDefaultDescription
waitTimeout5000Maximum ms to wait for an element to appear
pollInterval200ms between hierarchy polls
settleTimeout300ms the element must remain stable before matching
retries2Number of retries on action failure (e.g. tap on stale element)

Controls screenshot and hierarchy capture on test completion.

artifacts?: {
outputDir?: string;
captureOnFailure?: boolean;
captureOnSuccess?: boolean;
captureSteps?: boolean;
screenshot?: boolean;
uiHierarchy?: boolean;
}
OptionDefaultDescription
outputDir".spana/artifacts"Directory to write captured artifacts
captureOnFailuretrueCapture on failed flows
captureOnSuccessfalseCapture on passed flows
captureStepsfalseCapture a screenshot after every step in the flow
screenshottrueInclude screenshot in capture
uiHierarchytrueInclude 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>;
}
HookWhen it runs
beforeAllOnce before all flows on a platform
beforeEachBefore each individual flow
afterEachAfter each individual flow (always runs, even on failure)
afterAllOnce after all flows on a platform

HookContext provides app, expect, platform, result (in afterEach), and summary (in afterAll).

HookOn failure
beforeAllAll flows on that platform are skipped and marked as failed
beforeEachThat flow is skipped and marked as failed
afterEachWarning logged, test result is not affected
afterAllWarning logged, test results are not affected
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}`);
}
},
},
});

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;
}
OptionDefaultDescription
tags[]Tags for filtering flows with --tag on the CLI
platformsOverride which platforms this flow runs on
timeoutPer-flow timeout in ms (overrides global defaults.waitTimeout)
autoLaunchtrueAutomatically launch the app before the flow starts
artifactsPer-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
},
);

Default launch options applied to every flow. Individual flows can override these via app.launch().

launchOptions?: LaunchOptions
export default defineConfig({
launchOptions: {
clearState: true, // fresh state every run
},
});
interface LaunchOptions {
clearState?: boolean;
clearKeychain?: boolean;
deepLink?: string;
launchArguments?: Record<string, unknown>;
}
OptionDefaultDescription
clearStatefalseClear app data/storage before launch
clearKeychainfalseClear the iOS keychain before launch (simulator only)
deepLinkOpen the app via a deep link URL
launchArgumentsKey-value pairs passed as launch arguments to the app
OptionWebAndroidiOS
clearStateClears cookies, localStorageadb shell pm clearResets app permissions
clearKeychainNo-opNo-op (warns)xcrun simctl keychain reset
launchArgumentsNo-opPassed as --es extras via am startNot yet supported (planned)
deepLinkNavigates to URLadb shell am start -d <url>xcrun simctl openurl / WDA

When autoLaunch is false, call app.launch() manually with options:

flow("onboarding", { autoLaunch: false }, async ({ app }) => {
await app.launch({ clearState: true, deepLink: "myapp://welcome" });
// ...
});