diff --git a/.changeset/mobile-example-per-adapter.md b/.changeset/mobile-example-per-adapter.md new file mode 100644 index 00000000..b2bce3bd --- /dev/null +++ b/.changeset/mobile-example-per-adapter.md @@ -0,0 +1,18 @@ +--- +"@wdio/selenium-devtools": patch +"@wdio/nightwatch-devtools": patch +--- + +Add a mobile example per adapter — `pnpm demo:wdio:mobile`, `demo:selenium:mobile`, `demo:nightwatch:mobile`, `demo:python:mobile` — each honouring the same `DEVTOOLS_MODE=live|trace` switch the desktop demos already read. + +All four build the same capability bag — spelled per language, which is a duplication worth collapsing — so a difference in the dashboard between two adapters is a difference in the adapter rather than in the test. `examples/MOBILE.md` is the one place the prerequisites and switches are stated. + +The default target is the device's **own Clock app** (`com.google.android.deskclock`), which is what makes this landable at all: a native example previously needed an uploaded app and credentials in the environment, and that is the reason #354 closed without one. Nothing here needs an `.apk`. `APPIUM_APP` points it at a real app, and `DEVTOOLS_MOBILE=web` drives Chrome on the same device instead — worth running too, because a mobile browser session has a document and must keep every page-side call a native one skips. + +All four drive the same flow on the timer SETUP screen — clear the entry, key a duration on the keypad, read it back, correct it with backspace — and none of them starts a timer. A running timer survives the session and replaces that screen with its card, so a spec that starts one is re-runnable only if it also finishes. They avoid the preset chips for the same class of reason: those are recently-used-duration suggestions, absent on a freshly reset Clock, so a preset-based flow fails on any device without timer history. Verified on an Android 16 emulator with Clock 9.1, from a reset app; the Clock app updates independently of the Android version, so the resource-ids are re-read rather than assumed. + +Each one checks the toolchain before opening a session, through a shared `examples/mobile-preflight.cjs`, because none of the four frameworks reports a missing Appium in a way that names the cause: WDIO says "make sure browser driver is running", Nightwatch says it could not reach GeckoDriver, selenium-webdriver gives `ECONNREFUSED` and a stack trace. The check also distinguishes a missing Appium from a missing SDK from an emulator that is simply not started. + +Two framework details are worth recording, since both fail in ways that do not point at them. selenium-webdriver's `Builder.build()` throws unless `browserName` is a **string**, before it ever contacts the server, so a native session needs `browserName: ''` — which is the W3C signal for "no browser" anyway. And Nightwatch needs `selenium.use_appium`, not merely a `webdriver` block pointed at port 4723; without it it infers GeckoDriver, and even once connected it fills in `browserName: "firefox"` plus `moz:firefoxOptions` unless the capability is explicitly `null` — measured on the wire, and it made the adapters treat an app session as having a document. + +No adapter code changed: the dashboard already starts itself from `ensureBackendStarted()` and Python's `enable()`, so unlike the throwaway harnesses these replace, none of these examples needs a backend started by hand. diff --git a/.gitignore b/.gitignore index a36f454e..ad7b9c30 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,10 @@ __pycache__/ *.tgz examples/wdio/wdio-*.json examples/wdio/wdio-*.webm +examples/wdio/*.log examples/nightwatch/logs/ +examples/**/__pycache__/ +.DS_Store # Adapter-encoded screencasts (written next to the project root by default) selenium-video-*.webm @@ -48,10 +51,6 @@ packages/nightwatch-devtools/nightwatch-video-*.webm # trace output (mode: 'trace') trace-*.zip examples/**/trace-*/ -# ...but this one is a committed example, not output. The rule above exists for -# unpacked archives (`trace-/`), and a folder named for what it -# demonstrates collides with it. -!examples/selenium/python-test/trace-py-test/ # test results examples/**/test-results*/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3776e8f4..ed91928c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -190,11 +190,16 @@ The DOM-walking scripts run in the page via `browser.execute`, so — like `scri ### `examples/` -Per-framework demo projects used for manual verification. +Per-adapter demo projects used for manual verification. One directory per adapter, and inside it one directory per test runner, named for the runner itself: -- `examples/wdio/` — WebdriverIO, split into `cucumber/` and `mocha/` (shared page objects in `pageobjects/`). Run via `pnpm demo:wdio` (Cucumber), `pnpm demo:wdio:mocha`, or `pnpm demo:wdio:native` (Appium native app — needs a running Appium server and a device, see the README's Mobile testing section). -- `examples/nightwatch/` — Nightwatch (both vanilla and Cucumber). Run via `pnpm demo:nightwatch`. -- `examples/selenium/` — Selenium with subdirs for `mocha-test/`, `jest-test/`, `cucumber-test/`, `jasmine-test/`, `vitest-test/`. `pnpm demo:selenium` runs mocha; `pnpm --filter @wdio/selenium-devtools example:` runs the others. +- `examples/wdio/` — `mocha/` (including `native/`, an Appium app spec run by `pnpm demo:wdio:native`), `cucumber/`, `mobile/`, shared page objects in `pageobjects/`. Run via `pnpm demo:wdio` (Cucumber) or `pnpm demo:wdio:mocha`. +- `examples/nightwatch/` — `bdd/` (Nightwatch's `describe/it` interface), `cucumber/` and `mobile/`. Run via `pnpm demo:nightwatch`. +- `examples/selenium-js/` — `mocha/`, `jest/`, `cucumber/`, `mobile/`. `pnpm demo:selenium` runs Cucumber; `pnpm --filter @wdio/selenium-devtools example:` runs a specific one. +- `examples/selenium-py/` — the Python adapter: `scripts/` for the plain-script cases (no test runner) and `pytest/` for the pytest one. Run via `pnpm demo:python`, `:login` or `:pytest`. + +Every adapter also has a `mobile/` example driving the same Appium capability bag. A runner directory holds its own config and specs. `features/` inside a Cucumber directory is Cucumber's own convention, not ours. + +[`examples/README.md`](./examples/README.md) is the index: what each example demonstrates and how to run it. --- diff --git a/CLAUDE.md b/CLAUDE.md index 953f2050..67263801 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,6 +27,7 @@ Run from repo root unless noted. | `pnpm test:coverage` | Run vitest with v8 coverage. The thresholds in `vitest.config.ts` are aspirational, not a gate: that file states CI does not run this and the suite is currently below all four. CI runs `test`/`lint`/`test:ui`. | | `pnpm lint` | Lint all packages in parallel. Includes `eslint-plugin-security` for a subset of CodeQL findings; deeper taint-flow checks surface on the PR's CodeQL scan. | | `pnpm demo:wdio` / `pnpm demo:nightwatch` / `pnpm demo:selenium` | Run the per-framework example projects. Useful for manual verification of UI or runtime changes. | +| `pnpm demo:wdio:mobile` / `:selenium:mobile` / `:nightwatch:mobile` / `:python:mobile` | The same, against Appium. All four build the same capability bag and drive the Clock app that ships with every Android system image — starting a timer, pausing it, clearing it — so a native example needs no `.apk`. `DEVTOOLS_MOBILE_PLATFORM=ios` runs the iOS spec instead — all four adapters — which drives Settings because the simulator ships no Clock, from a separate spec per platform rather than a branch. The simulator is chosen by udid and defaults to whichever is already booted, and an unmatched `IOS_DEVICE_NAME` is refused: naming one that does not exist makes the XCUITest driver create and boot it, every run, rather than fail. That refusal and the booted-simulator preflight are **local** policy — `xcrun simctl` enumerates local simulators and nothing else — so `IOS_UDID` and a non-local `APPIUM_HOST` both bypass them, or a real device and a cloud grid would be refused a run they were correctly configured for. `DEVTOOLS_MOBILE=web` drives the device's own browser on both platforms — Chrome on Android, Safari on iOS, which the XCUITest driver serves without a chromedriver. `examples/MOBILE.md` holds the prerequisites and the `DEVTOOLS_MOBILE` / `APPIUM_APP` switches; `DEVTOOLS_MODE=trace` flips any demo to trace mode. | | `pnpm dev` | Run all packages in parallel dev mode. | `selenium-devtools` exposes per-runner variants of its example via `pnpm --filter @wdio/selenium-devtools example:mocha` / `:mocha:allure` / `:jest` / `:cucumber`. @@ -297,6 +298,7 @@ Documented divergences from the conventions above. They exist today as debt to b - **A preload dies with its session, and Nightwatch replaces sessions without saying so.** Registration used to run once at bringup, so a mid-run `browser.end()` left sessions 2..N with neither preload nor BiDi: measured 1 registration and 1 attach for a whole run, 4-6 `Collector missing … re-injecting` recoveries, and — because `bidiActive` stayed `true` from the dead session and gates the perf-log fallback off — **zero network capture after the rotation** (75 requests in session 1, 0 in session 2). `session-init.ts` `rearmCaptureForSession` re-arms both off the same command-hook detection the screencast rotation uses (`armedSessionId`, stamped before the first await so the command flood latches out), clearing `preloadRegistered`/`bidiActive` at detection so the fallbacks are open in the gap. After: 2 registrations, 2 attaches, 0-1 recoveries, 125 network entries (75 + 50), and the accumulated stream untouched — 21 action rows split 12/9 across the two sessions, identical to before. It deliberately does **not** rebuild the `SessionCapturer` — see the entry below, which made that the rule for every path rather than just this one. - The preload registers **before** the BiDi attach, sequentially: behind the attach it lost the race it exists to win (the network subscribe took 6.2 s while the triggering command navigated, so registration landed after its own document was born), and `Promise.all` races selenium-webdriver's unsynchronized `getBidi()` cache into two websockets of which `quit()` closes one. - Residual: detection is at command invocation and chromedriver serialises the handshake behind the in-flight command, so the **first navigation after a rotation** can still land before registration and falls back to `