Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1b43bac
refactor(examples): one directory per adapter, and land the mobile ones
vishnuv688 Sep 22, 2026
efc9199
docs: describe mobile capture as it behaves today
vishnuv688 Sep 22, 2026
2fb2200
fix(examples): repair nightwatch discovery, and stop advertising iOS
vishnuv688 Sep 22, 2026
f200592
fix(examples): refuse iOS in the Python example too, and correct the …
vishnuv688 Sep 22, 2026
3031c66
Merge remote-tracking branch 'origin/main' into fix/385-organise-exam…
vishnuv688 Sep 22, 2026
21e710e
fix(examples): clear leftover timers in the native Clock example
vishnuv688 Sep 22, 2026
d6896f7
fix(examples): bound timer cleanup by progress, and typecheck the mob…
vishnuv688 Sep 22, 2026
0bdeae8
fix(examples): drive the Clock keypad, and never start a timer
vishnuv688 Sep 22, 2026
d8256a5
fix(examples): attach to the booted simulator instead of creating one
vishnuv688 Sep 23, 2026
92df294
feat(examples): give every adapter an iOS mobile example
vishnuv688 Sep 23, 2026
5307e1f
docs: iOS is every adapter now, and say how the simulator is chosen
vishnuv688 Sep 23, 2026
816d308
fix(nightwatch): don't fetch a perf log the session never asked for
vishnuv688 Sep 23, 2026
4dfaa90
fix(examples): serve iOS web mode, and share one simulator resolver
vishnuv688 Sep 23, 2026
c8554f3
docs: iOS web mode, and how the simulator is chosen
vishnuv688 Sep 23, 2026
f2622ab
fix(examples): stop the local simulator rules from refusing remote de…
vishnuv688 Sep 24, 2026
6bd0f38
docs: the simulator rules are local policy, not universal
vishnuv688 Sep 24, 2026
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
18 changes: 18 additions & 0 deletions .changeset/mobile-example-per-adapter.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 3 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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-<hash>/`), and a folder named for what it
# demonstrates collides with it.
!examples/selenium/python-test/trace-py-test/

# test results
examples/**/test-results*/
Expand Down
13 changes: 9 additions & 4 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<runner>` 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:<runner>` 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.

---

Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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 `<script>` injection (1 recovery in 3 of 4 runs, 0 in the fourth). Nightwatch exposes no pre-test session hook to a plugin.
- **The perf-log fallback only runs when the session asked for a perf log** (`SessionCapturer.perfLogsRequested`, set from `goog:loggingPrefs.performance` at session init — the same reading that already warns "Network tab will be empty"). Without the capability the log cannot exist, so every per-command fetch is a round trip that can only fail, and Nightwatch's transport prints each failure: measured on an iOS Safari session, 21 error blocks in a two-test run that passed, 257 log lines and 12.3 s against 66 and 5.9 s once gated. A runtime latch cannot do this job — `await browser.getLog(...)` yields `undefined` in this Nightwatch version rather than the 500's body, so the refusal is invisible to the caller (the same quirk as `browser.elements()`). XCUITest is why it has to be the capability and not a "is this Chrome" test: it lists `performance` among its supported types and then serves none of it.
- Deliberately **not** gated on Nightwatch's `bidi` option. That option exists to avoid double-reporting console/network against the perf-log path; preload registration needs nothing but a session created with `webSocketUrl: true`, and gating DOM capture on an unrelated opt-in that defaults to `false` would leave the race in place for nearly every user. `ScriptManager.init` throws without that capability, so the helper self-degrades to the `<script>` path and returns false.
- The fallback path is kept and still works, but is strictly lower fidelity: measured with `bidi:false`, 0 rows without DOM but the per-action field-state mutations don't survive and a navigating click keeps the ~5 ms residual. Prefer `webSocketUrl: true`.
- **One `SessionCapturer` per RUN, not per WebDriver session (Nightwatch).** A replaced session used to tear the capturer down and rebuild it, discarding every command, console line, network entry and mutation accumulated so far — so `traceGranularity:'session'` on a **cucumber** run (Nightwatch quits the browser per scenario) wrote one zip holding only the LAST scenario: measured 8 action rows and 1 scenario group where the run had 17 and 2. Nothing forced the rebuild — the browser object is the capturer's only session-bound field (`session.ts` `setBrowser`, mirroring selenium's `setDriver`), and everything else is a run-long accumulator. A replacement is now a re-target plus the per-session bringup that already existed: metadata, `armReplacedSession`, `rotateScreencastForSession`. Measured after: **17 rows** (9+8), 3 groups nested feature→scenario, 4 DOM anchors, 0 of 17 rows replaying another document, 94 network entries (47+47); reproduced 2/2 runs.
Expand Down
Loading
Loading