End-to-end tests driving the example/ app with Appium 2
through WebdriverIO. Tests are written in TypeScript
and executed with Jest (to stay consistent with the rest of the repo).
The happy-path test mirrors the example app: it mints a local XHD Ed25519
key, creates a passkey against https://debug.liquidauth.com, and then
re-uses it for an assertion round-trip.
e2e/
├── driver/
│ ├── capabilities.ts # Android (UiAutomator2) + iOS (XCUITest) caps
│ ├── selectors.ts # testID + system-dialog helpers
│ └── session.ts # createDriver() — wraps webdriverio.remote()
├── tests/
│ └── passkey.test.ts # smoke + passkey create/use against debug.liquidauth.com
├── jest.config.ts
├── package.json
└── tsconfig.json
All app-level taps use React Native testIDs (exposed as
accessibility id on both platforms) via byTestId("..."). System
WebAuthn/biometric dialogs are auto-confirmed by confirmSystemPrompt().
The example app exposes a provider-status banner so the suite (and human testers) can confirm that this package is the user-selected system credential/autofill provider before exercising passkey flows — otherwise another installed password manager could silently serve the system WebAuthn prompts. The example exposes:
- A
testID="provider-status"label whose text readsactive,inactive, orunknown. - A
testID="open-provider-settings"CTA that deep-links to the OS settings (Android: credential provider picker; iOS: AutoFill settings).
The first suite step (surfaces the initial provider status) is a
smoke read only. The authoritative assertion runs after the passkey
creation step: once Android has routed a
BeginCreate/BeginGetCredentialRequest to our
PasskeyAutofillCredentialProviderService, the service stamps an MMKV
key that ReactNativePasskeyAutofill.isProviderActive() picks up and
the banner flips to active. This two-signal design sidesteps the
fact that Settings.Secure("credential_service") is @hide-restricted
on Android 12+ and cannot be read by a regular app.
If the passkey flow never reaches the stamp, enable the app manually on the device:
- Android 14+: Settings → Passwords, passkeys & accounts → Additional providers → Passkey AutoFill Example.
- iOS 17+: Settings → Passwords → Password Options → enable Passkey AutoFill Example.
The passkey create/use flow ultimately routes through either the
system's Single-Tap bottom sheet (when the provider attaches
BiometricPromptData + CryptoObject) or the fallback
CreatePasskeyActivity / GetPasskeyActivity screens. Both require a
biometric confirmation from the user. On emulators the suite automates
this by synthesising finger touches via adb (Android) or the
enrollBiometric extension (iOS).
-
Node.js 22+, pnpm 10+
-
Android: Android SDK, an AVD (API 34 recommended). Biometrics are automatically enrolled by the suite on first run.
-
iOS (macOS only): Xcode 16.1+, an iOS 17+ Simulator. Face ID is automatically enrolled by the suite.
-
Appium drivers. The driver packages are declared as regular
devDependenciesand installed bypnpm install, so they are version-locked alongside the rest of the workspace. Theappium:setup*scripts then register them with the Appium CLI from their localnode_modulespath (nonpmshell-out, no global~/.appiummutation required beyond Appium's own manifest):pnpm install # installs appium-uiautomator2-driver & appium-xcuitest-driver pnpm run appium:setup # register both drivers with Appium pnpm run appium:setup:android # Android only pnpm run appium:setup:ios # iOS only (macOS)
-
Install repo deps:
pnpm install
-
Build the example app for the target platform:
pnpm run e2e:build:android # produces example/android/app/build/outputs/apk/release/app-release.apk pnpm run e2e:build:ios # produces example/ios/build/Build/Products/Release-iphonesimulator/PasskeyAutofillExample.app
-
Boot an emulator/simulator, then run:
pnpm run e2e:android # or pnpm run e2e:iosThese scripts start Appium via
start-server-and-test, wait forhttp://127.0.0.1:4723/status, execute the Jest suite, and shut Appium down afterwards.
| Env var | Purpose |
|---|---|
E2E_PLATFORM |
android (default) or ios |
E2E_APP_PATH |
Absolute path to a prebuilt .apk / .app |
APPIUM_HOST, APPIUM_PORT |
Appium server (defaults 127.0.0.1:4723) |
ANDROID_PLATFORM_VERSION |
e.g. "14" |
ANDROID_DEVICE_NAME |
e.g. "Pixel_6_API_34" |
IOS_PLATFORM_VERSION |
e.g. "17.5" |
IOS_DEVICE_NAME |
e.g. "iPhone 15" |
WDIO_LOG_LEVEL |
error | warn (default) | info | debug |
The E2E workflow runs on
workflow_dispatch or when a PR is labelled e2e:
- Android job builds the release APK, installs the UiAutomator2 driver,
and runs the suite inside
reactivecircus/android-emulator-runner. - iOS job (currently disabled) builds the
.appfor the iPhone Simulator, installs the XCUITest driver, boots a simulator, and runs the suite.
Screenshots of the test run are uploaded as workflow artifacts for debugging.
-
Add a stable
testIDto the target React Native component. -
Import the helpers:
import { createDriver } from "../driver/session"; import { byTestId, tap, confirmSystemPrompt } from "../driver/selectors";
-
Drive the app with
tap(driver, byTestId("...")). UseconfirmSystemPrompt()for OS-level WebAuthn/biometric dialogs.