spana-test
v0.2.0
Published
TypeScript-native E2E testing for React Native + Web
Downloads
446
Maintainers
Readme
spana
TypeScript-native E2E testing for React Native + Web.
Documentation | GitHub | npm | E2E Test Report
Features
- Pure TypeScript flows, plus optional Gherkin / BDD
.featuresupport - Cross-platform execution across web, Android, and iOS from the same suite
- Local web via Playwright, local Android via UiAutomator2, local iOS via WebDriverAgent, plus Appium cloud mode
- Smart waits, retries, relative selectors, and per-flow stability defaults
- Reporters: console, JSON, JUnit XML, HTML, and Allure
- Artifacts on failure or success: screenshots, UI hierarchy dumps, per-step capture, and web HAR / diagnostics
- App auto-install with
appPath, iOS device signing, and BrowserStack / Sauce helper services - CLI tooling for teams and agents:
validate,validate-config,init,studio,selectors,hierarchy, anddevices - Execution controls for targeted and CI runs:
--device,--shard,--bail,--debug-on-failure - Browser helpers for network/auth state, uploads/downloads, tabs/HAR diagnostics, plus WebView / hybrid context APIs where the driver supports them
Quick Start
npm install spana-test
# or
bun add spana-testCreate spana.config.ts:
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/MyApp.app",
},
},
platforms: ["web", "android"],
reporters: ["console", "html"],
artifacts: {
outputDir: ".spana/artifacts",
captureOnFailure: true,
},
});Create flows/login.flow.ts:
import { flow } from "spana-test";
export default flow("user can log in", async ({ app, expect }) => {
await app.tap({ testID: "email-input" });
await app.inputText("[email protected]");
await app.dismissKeyboard();
await app.tap({ testID: "password-input" });
await app.inputText("secret");
await app.dismissKeyboard();
await app.tap({ testID: "login-button" });
await expect({ testID: "home-screen" }).toBeVisible();
});Run:
spana validate-config
spana validate ./flows
spana testCapability Matrix
| Capability | Web | Local Android / iOS | Appium cloud |
| ---------------------------------------------------- | --- | -------------------------------- | ------------------------------------------ |
| TypeScript .flow.ts suites | Yes | Yes | Yes |
| Gherkin .feature suites | Yes | Yes | Yes |
| Smart waits, retries, relative selectors | Yes | Yes | Yes |
| HTML / JUnit / Allure reports and artifacts | Yes | Yes | Yes |
| App install / app reference handling | n/a | Yes, via appPath | Yes, via capabilities and provider helpers |
| Browser helpers (mockNetwork, cookies, downloads, tabs, HAR`) | Yes | No | No |
| WebView context APIs | n/a | Android yes, iOS via Appium mode | Yes |
Writing Flows
Basic API
import { flow } from "spana-test";
export default flow(
"checkout flow",
{
tags: ["smoke", "payments"],
platforms: ["android", "ios"],
timeout: 60_000,
defaults: {
waitForIdleTimeout: 250,
typingDelay: 20,
},
},
async ({ app, expect }) => {
await app.tap({ text: "Sign In" });
await app.inputText("[email protected]");
await app.dismissKeyboard();
await app.scrollUntilVisible({ testID: "order-summary" });
await app.doubleTap({ testID: "promo-card" });
await app.longPress({ testID: "options-trigger" });
await app.scroll("down");
await expect({ testID: "welcome" }).toBeVisible();
await expect({ text: "Welcome" }).toBeVisible();
},
);Use scrollUntilVisible() for off-screen targets instead of hand-written scroll loops. Use dismissKeyboard() for a platform-aware keyboard close path, and backUntilVisible() when you want system back navigation to stop on a known screen. Tap-like actions also prefer the nearest actionable container for nested label-inside-button layouts.
FlowConfig options
| Option | Type | Default | Description |
| ------------ | ---------------- | -------------- | -------------------------------------------- |
| tags | string[] | - | Tags for --tag filtering |
| platforms | Platform[] | all | Restrict a flow to specific platforms |
| timeout | number | config default | Flow timeout in ms |
| autoLaunch | boolean | true | Launch app before the flow starts |
| when | WhenCondition | - | Conditionally run by platform or env |
| artifacts | ArtifactConfig | config default | Per-flow artifact overrides |
| defaults | FlowDefaults | config default | Per-flow wait / typing / stability overrides |
BDD parity
Spana can compile .feature files into the same runtime as .flow.ts files. Keep Gherkin scenarios for high-value readable coverage, and use step definitions for reuse:
Feature: Login
Scenario: Signed-out user logs in
Given I am on the login screen
When I type valid credentials
Then I should see the home screenCLI Commands
| Command | Description |
| ------------------------------ | ------------------------------------------------- |
| spana test [path] | Run test flows (default: ./flows) |
| spana hierarchy | Dump full element hierarchy as JSON |
| spana selectors | List actionable elements with suggested selectors |
| spana validate [path] | Validate flow files without a device connection |
| spana validate-config [path] | Validate spana.config.ts without running flows |
| spana studio | Launch Spana Studio |
| spana init | Scaffold a new Spana project |
| spana devices | List connected devices across all platforms |
| spana version | Show version |
test options
spana test flows/login.flow.ts # run a single file
spana test --platform android,ios # target platforms
spana test --device emulator-5554 # target a specific local device
spana test --tag smoke --grep "log in" # filter flows
spana test --reporter html,allure # choose reporters
spana test --retries 2 --shard 1/3 --bail 5 # CI-friendly execution
spana test --debug-on-failure # open REPL on the first failure
spana test --driver appium --appium-url $BROWSERSTACK_URL --caps ./caps/android.json --platform android
spana test --config ./spana.config.ts # explicit config pathinspection and debugging
spana hierarchy --platform android --pretty
spana selectors --platform ios
spana validate-config
spana studio --no-openConfiguration
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/MyApp.app",
signing: { teamId: "ABCDE12345" },
},
},
execution: {
web: {
browser: "chromium",
headless: true,
storageState: "./auth/web-user.json",
},
appium: {
serverUrl: process.env.BROWSERSTACK_URL,
capabilitiesFile: "./caps/browserstack-android.json",
reportToProvider: true,
browserstack: {
local: { enabled: true },
},
},
},
platforms: ["web", "android", "ios"],
flowDir: "./flows",
reporters: ["console", "json", "html", "allure"],
defaults: {
waitTimeout: 5000,
pollInterval: 200,
settleTimeout: 300,
retries: 2,
waitForIdleTimeout: 250,
typingDelay: 20,
initialPollInterval: 50,
hierarchyCacheTtl: 100,
retryDelay: 0,
},
launchOptions: {
clearState: false,
},
artifacts: {
outputDir: ".spana/artifacts",
captureOnFailure: true,
captureOnSuccess: false,
captureSteps: false,
screenshot: true,
uiHierarchy: true,
},
hooks: {
beforeAll: async ({ app }) => {
/* setup */
},
beforeEach: async ({ app }) => {
/* reset state */
},
afterEach: async ({ app, result }) => {
/* teardown */
},
afterAll: async ({ app, summary }) => {
/* cleanup */
},
},
});appPath lets Spana install the app automatically for local Android and iOS runs. execution.appium.browserstack and execution.appium.saucelabs can also manage uploaded app references plus BrowserStack Local / Sauce Connect lifecycles for cloud runs.
Browser runtime helpers (web)
import { flow } from "spana-test";
export default flow("web dashboard", async ({ app, platform }) => {
if (platform !== "web") return;
await app.loadAuthState("./auth/web-user.json");
await app.mockNetwork("**/api/dashboard", {
json: { widgets: ["revenue", "alerts"] },
});
await app.blockNetwork("**/analytics/**");
await app.setNetworkConditions({ offline: false, latencyMs: 120 });
await app.evaluate(() => console.info("dashboard hydrated"));
const download = app.downloadFile("./tmp/dashboard-report.json");
await app.tap({ testID: "download-report-button" });
await download;
await app.uploadFile({ testID: "avatar-upload-input" }, "./fixtures/avatar.png");
const newTabId = await app.newTab("http://localhost:3000/settings");
await app.switchToTab(1);
console.log(await app.getTabIds(), newTabId);
console.log(await app.getConsoleLogs());
console.log(await app.getJSErrors());
console.log(await app.getHAR());
await app.saveCookies("./tmp/cookies.json");
});mockNetwork, blockNetwork, clearNetworkMocks, setNetworkConditions, saveCookies, loadCookies, saveAuthState, loadAuthState, downloadFile, uploadFile, newTab, switchToTab, closeTab, getTabIds, getConsoleLogs, getJSErrors, and getHAR are web-only helpers backed by local Playwright runs. downloadFile() waits for the next browser download and saves it locally; uploadFile() supports simple testID / text / accessibilityLabel selectors. Latency and throughput throttling require Chromium. When artifact capture is enabled, web failures also include captured console logs, JavaScript errors, and HAR output in spana-output/ and the HTML report. Set execution.web.verboseLogging = true when you want Playwright runtime events echoed to stdout while debugging.
Hybrid / WebView helpers
import { flow } from "spana-test";
export default flow("hybrid checkout", async ({ app, platform }) => {
if (platform === "web") return;
const contexts = await app.getContexts();
await app.switchToWebView();
await app.switchToNativeApp();
console.log(contexts);
});Context APIs depend on driver support. Appium mode is the best path for iOS WebView automation.
Selectors
| Selector | Example | Notes |
| -------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
| testID | { testID: "login-btn" } | Preferred - maps to accessibilityIdentifier (iOS), resource-id (Android), and data-testid (web) |
| text | { text: "Sign In" } | Visible label text, partial match supported |
| accessibilityLabel | { accessibilityLabel: "Close" } | OS accessibility label |
| point | { point: { x: 100, y: 200 } } | Absolute coordinate tap, use as a last resort |
Selectors can be combined. When multiple fields are set, all must match.
Relative selectors
For actions that accept an extended selector, you can locate an element relative to another one:
await app.tap({
selector: { testID: "confirm-button" },
below: { text: "Delete account" },
});Supported relations: below, above, leftOf, rightOf, and childOf.
Tooling and Debugging
Spana ships a few built-in workflows that are easy to miss from the minimal quick start:
spana validate-configcatches config issues before a suite startsspana initscaffolds a starter projectspana deviceslists local execution targetsspana studiolaunches a browser UI for inspection and test runs--debug-on-failuredrops into an interactive REPL with boundappand driver context--device,--shard, and--bailhelp with targeted local runs and CI fan-out
Agent Integration
spana is designed for AI agent workflows:
spana selectors --platform androidreturns JSON with element details and suggested selectorsspana hierarchy --platform web --prettydumps the accessibility tree as structured JSONspana validateexits non-zero on invalid flows, which works well as a preflight step--reporter jsonemits structured JSON events to stdout for downstream analysis
Example agent loop:
# 1. discover what's on screen
spana selectors --platform web | jq '.[] | select(.testID != null)'
# 2. run a specific flow with JSON output
spana test flows/login.flow.ts --reporter json 2>&1 | jq '.results'Cloud Testing
Spana supports running the same TypeScript flows on cloud device farms via Appium mode.
# BrowserStack
spana test --driver appium --appium-url $BROWSERSTACK_URL --caps ./caps/browserstack-android.json --platform android
# Sauce Labs
spana test --driver appium --appium-url $SAUCE_URL --caps ./caps/saucelabs-android.json --platform androidOr configure it in spana.config.ts:
import { defineConfig } from "spana-test";
export default defineConfig({
execution: {
mode: "appium",
appium: {
serverUrl: process.env.BROWSERSTACK_URL,
capabilitiesFile: "./caps/browserstack-android.json",
reportToProvider: true,
browserstack: {
local: { enabled: true },
},
},
},
apps: {
android: {
packageName: "com.example.myapp",
appPath: "./builds/app.apk",
},
},
});Spana ships first-class helper services for BrowserStack and Sauce Labs:
- managed app upload and app reference resolution
- BrowserStack Local lifecycle management
- Sauce Connect lifecycle management
- provider result reporting
Generic Appium hubs still work even without provider-specific helpers.
Guides:
Architecture
spana uses a layered architecture: CLI -> TestRunner -> PlatformOrchestrator -> SmartLayer -> RawDriver.
Raw drivers are thin HTTP clients. All selector matching, auto-wait, retry, and element resolution lives in the TypeScript smart layer instead of companion binaries.
See ARCHITECTURE.md for details.
Platforms
| Platform | Driver | Companion binary |
| ------------- | --------------------------- | -------------------------------------------------------- |
| Web / RN Web | Playwright (CDP) | None - Playwright is a dev dependency |
| Android | UiAutomator2 HTTP client | Appium UiAutomator2 server APK (bundled) |
| iOS Simulator | WebDriverAgent HTTP client | WDA XCTest bundle (bundled unsigned) |
| iOS Device | Same WDA bundle | Re-signed with codesign; requires iproxy |
| Appium cloud | Appium 2 / 3 compatible hub | BrowserStack, Sauce Labs, or another W3C-compatible grid |
License
Apache-2.0
