Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .ffi-version
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Bump this when upgrading the FFI, then run `node scripts/install.mjs`.
# (`ffi/<version>` for releases, `prerelease/<sha>` for prereleases).
# See releases from https://github.com/webview-bundle/webview-bundles.
prerelease/7e38986
45 changes: 45 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: ci
on:
pull_request:
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
gradle-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # 4.0.1
- name: Install ffi
run: node scripts/install.mjs
env:
GITHUB_TOKEN: ${{ github.token }}
- run: ./gradlew :lib:assembleRelease :lib:lintRelease :testapp:assembleDebug
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # 4.0.1
- name: Install ffi
run: node scripts/install.mjs
env:
GITHUB_TOKEN: ${{ github.token }}
# KVM hardware acceleration for the Android emulator on Linux runners.
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- run: corepack enable
- run: yarn install --immutable
working-directory: e2e
# Boots an emulator, then runs `yarn test`; the harness reuses the running
# emulator and builds the APK via its vitest globalSetup.
- uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # 2.37.0
with:
api-level: 31
arch: x86_64
target: google_apis
disable-animations: true
working-directory: e2e
script: yarn test
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ variant (`lib-android`):
The Kotlin bindings are overwritten in place; `jniLibs` is replaced wholesale,
so an ABI dropped between releases does not linger.

### Pinned version

The FFI ref this checkout is built against is pinned in **`.ffi-version`**.

```sh
node scripts/install.mjs # installs the ref from .ffi-version
```

### Other invocations

```sh
# install a release (tag ffi/0.1.0)
node scripts/install.mjs 0.1.0
Expand All @@ -50,6 +60,15 @@ Tags follow the upstream convention: `ffi/<version>` for releases and
`prerelease/<sha>` for prereleases. Run `node scripts/install.mjs --help` for
all options.

## Test App & E2E

`testapp/` is a Android Application that serves a **real builtin `.wvb`**
to a `WebView`, and `e2e/` drives it with Appium.

```sh
cd e2e && yarn install && yarn test
```

## License

MIT License
4 changes: 4 additions & 0 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
.yarn/
*.log
.appium/
3 changes: 3 additions & 0 deletions e2e/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
nodeLinker: node-modules
enableScripts: true
npmMinimalAgeGate: 0
52 changes: 52 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# webview-bundle-android e2e

Appium-driven end-to-end tests for the `testapp` module, mirroring the iOS e2e.

## How it works

The `testapp` (`MainActivity`) is a minimal WebView host: it serves the builtin
`hacker-news` bundle over `https://hacker-news.wvb/` and loads it on launch. It
carries **no** test logic of its own. The scenarios live in the shared
`@wvb-playground/webview-hacker-news` package (the same suite the iOS e2e runs)
and are expressed against the platform-agnostic `WebviewDriver` from
`@wvb-playground/testing`.

The Appium session is switched into the testapp's **WEBVIEW** context, then each
exported `testCase` is run through `createAppiumDriver(driver, { baseURL:
"https://hacker-news.wvb" })`, which drives the live DOM (CSS selectors,
in-app navigation via `goto`).

`vitest` orchestrates it:

- **globalSetup** (`vitest.global-setup.ts`) builds the debug APK
(`./gradlew :testapp:assembleDebug`).
- **setup** (`vitest.setup.ts`) installs the `uiautomator2` Appium driver, boots
(or reuses) an emulator, starts an Appium server (with
`chromedriver_autodownload` enabled), opens a session that installs and
launches the testapp, and switches into its WEBVIEW context.
- **smoke.spec.ts** runs every `testCase` from
`@wvb-playground/webview-hacker-news/testing`.

## Prerequisites

- The Android SDK, with `platform-tools` (adb) and `emulator` available. Set
`ANDROID_HOME` (or `ANDROID_SDK_ROOT`) if it is not at the platform default.
- At least one AVD. The first one is used unless `ANDROID_AVD` is set; an
already-booted device/emulator is reused.
- Node + a package manager (the repo pins toolchain via `mise`).
- Appium is installed as a dev dependency; the `uiautomator2` driver is installed
into `~/.appium` on first run.

## Run

```sh
# from this directory
yarn install
yarn test
```

Environment knobs:

- `ANDROID_AVD` — AVD to boot when no device is already running.
- `WVB_E2E_HEADLESS=1` — boot the emulator with `-no-window` (default on `CI`).
- `WVB_E2E_KEEP=1` — leave a self-booted emulator running after the test.
101 changes: 101 additions & 0 deletions e2e/appium.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import os from "node:os";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { execa, type ResultPromise } from "execa";

export const APPIUM_PORT = 4723;

if (!process.env.APPIUM_HOME) {
process.env.APPIUM_HOME = path.join(os.homedir(), ".appium");
}

/** Ensures the given Appium drivers are installed into `APPIUM_HOME` (default `~/.appium`). */
export async function ensureAppiumDrivers(drivers: string[]): Promise<void> {
const installed = await listInstalledDrivers();
for (const driver of drivers) {
if (driver in installed) {
continue;
}
console.log(`[appium] installing driver: ${driver}`);
await execa("appium", ["driver", "install", driver], {
preferLocal: true,
stdout: "inherit",
stderr: "inherit",
timeout: 5 * 60_000,
});
}
}

async function listInstalledDrivers(): Promise<Record<string, unknown>> {
const { stdout } = await execa(
"appium",
["driver", "list", "--installed", "--json"],
{
preferLocal: true,
reject: false,
},
);
try {
return JSON.parse(stdout || "{}");
} catch {
return {};
}
}

export interface AppiumServer {
port: number;
stop: () => Promise<void>;
}

export async function startAppiumServer(
port = APPIUM_PORT,
): Promise<AppiumServer> {
console.log(`[appium] starting server on port ${port}`);
const proc: ResultPromise = execa(
"appium",
[
"--port",
String(port),
"--base-path",
"/",
"--log-level",
"error",
// Lets UiAutomator2 download a chromedriver matching the device's WebView
// when one isn't already cached, so the WEBVIEW context can be entered.
// Appium 3 requires the feature be namespaced by driver (or `*` for all).
"--allow-insecure",
"*:chromedriver_autodownload",
],
{ preferLocal: true, stdout: "inherit", stderr: "inherit" },
);
proc.catch(() => {});

await waitForAppiumServer(port, 60_000);

return {
port,
stop: async () => {
proc.kill("SIGTERM");
await proc.catch(() => {});
},
};
}

async function waitForAppiumServer(
port: number,
timeoutMs: number,
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const res = await fetch(`http://127.0.0.1:${port}/status`);
if (res.ok) {
return;
}
} catch {}
await delay(500);
}
throw new Error(
`Appium server did not become ready on port ${port} within ${timeoutMs}ms`,
);
}
Loading