Skip to content

fix: tell a native session from a browser one in every adapter - #375

Open
vishnuv688 wants to merge 11 commits into
mainfrom
fix/372-native-detection-every-adapter
Open

fix: tell a native session from a browser one in every adapter#375
vishnuv688 wants to merge 11 commits into
mainfrom
fix/372-native-detection-every-adapter

Conversation

@vishnuv688

@vishnuv688 vishnuv688 commented Sep 11, 2026

Copy link
Copy Markdown
Member

What & why

Every adapter can now tell a native session from a browser one. Until now only the WDIO service could: the predicate read browser.isMobile/isAndroid/isIOS, which are WDIO runtime flags that Selenium's WebDriver, Nightwatch's browser and the Python driver do not have. So Selenium and Nightwatch ran their DOM drain, their collector injection and their page-script probes against a native app anyway — the same wasted round trips and Method is not implemented errors the service stopped emitting in #371 — and the Python adapter read window.innerWidth on a session with no window.

The fact now has one reader, isNativeAppSession in shared, which asks the capabilities every adapter already publishes rather than a driver flag. It keys on whether the session named a browser, because a device alone does not answer the question — an Appium session driving Chrome or Safari runs on a phone and has a real page — and it reads both platformName and browserName one level into vendor options, since a device cloud commonly states them only inside its own bag.

Gated per adapter: Selenium's captureTrace, injectScript, reinjectIfNavigated and its performance read (whose 500 ms settle was being spent to reach a document that does not exist); Nightwatch's captureTrace, injectScript, anchorAfterNavigation and its own performance read; Python's collector, its performance read and its viewport, which now measures the device window rather than asking a page that isn't there.

The densest of these is the per-action snapshot, and all three adapters were paying it: two injected scripts plus url and title, on every action. Those four now drop out on a native session while the screenshot — the one probe a native app does serve — is still taken, so the trace keeps its per-action frames. Screenshots and manage().logs() are deliberately left alone: Appium serves both, and logcat arrives through the second, so gating them would lose data rather than save a failed call.

Two pre-existing bugs fell out of the work, both from one root: Selenium published its capabilities as selenium-webdriver's Capabilities instance, whose data lives in a private Map with serialize exposed only under a Symbol — so the string-keyed serialize?.() the adapter called returned undefined and the instance reached the dashboard as {"map_":{}}. Every Selenium trace therefore carried no device and a guessed browser name, and the capabilities pane was empty. It is now flattened through the class's own keys()/get(), which is also what makes the new guards work at all, since they read that bag. Reading the device out of vendor options fixes the same field for a cloud session, which previously read as desktop and reached the player framed as a browser window rather than a phone.

The player's mobile layout was also still WDIO-only in live mode: it gates on metadata.device, and only the service derived one before sending. The app now derives it from the capabilities when the adapter sent none, at the single ingestion point every live message passes through. A device the adapter did send wins. Trace mode was already correct — the exporter derives it on the way into the zip.

Advances #372. Not closing it: two checklist items remain, both below.

Type of change

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Polish (an improvement to an existing feature)
  • Breaking change (existing behavior changes for users)
  • Documentation
  • Internal (build, CI, dependencies, tooling)

Packages touched

  • shared (types and contracts)
  • core (framework-agnostic capture/reporting)
  • elements (published element/snapshot API — @wdio/elements)
  • service (WebdriverIO adapter)
  • nightwatch-devtools (Nightwatch adapter)
  • selenium-devtools (Selenium adapter)
  • selenium-devtools-py (Selenium Python adapter)
  • backend (server)
  • app (UI)
  • script (page-injected runtime)
  • trace (Trace mode)

Notes for reviewers

Why the shared logic is in shared and not core. #372 proposed core, and this lands in shared instead. The predicate is a pure read over a capability bag — no driver, no framework hook, no capture session — and shared already owns the neighbouring narrowing (isNativePlatform, deviceFromCapabilities) that the trace exporter calls. Putting it in core would have split one question across two packages and put it out of reach of anything that may not import core. It stays framework-agnostic either way; shared is the layer more consumers can reach.

Why the guards read metadata rather than the driver. SessionCapturerBase exposes it as isNativeAppSession, resolved from the metadata the adapter has already set. That indirection is not decoration: Selenium's own getCapabilities() is async, and a guard cannot await it at the point it has to decide.

Why the guards are inside the guarded methods, not at the call sites. This is #350's finding applied: two of four call sites asked and two forgot. Selenium's drain has three call sites and Nightwatch's has four.

Known gaps, both tracked, neither introduced here:

A native session also still gets no accessibility tree: deriving one from page source is a capture feature the service has and the other three do not.

Verification

CI cannot cover any of this — it needs a device. pnpm lint, pnpm build, pnpm test (2361 tests) and pnpm test:ui are green on CI; the native paths were exercised locally against a real Android emulator through a per-adapter Appium example, driving the device's own Settings app so no .apk or credentials are involved.

The mobile examples themselves are not in this PR — they are #372's remaining checklist item and land separately. That is the second reason this PR does not close #372.

One finding from that exercise is worth flagging because it is a live blocker rather than a gap in this change: a mobile web Appium session deadlocks with the service attached (#374) — beforeCommand awaits page-side calls from inside the hook wrapping the command being issued, Appium serialises commands per session, so the wrapped command never reaches the browser. Measured at 6 m 13 s of timeouts against 1.6 s with the service removed. Native app sessions are unaffected, precisely because isNativeAppSession makes those same calls no-ops — which is the change in this PR.

Screenshots / recordings

n/a — no UI change beyond live-mode device derivation, which selects the existing mobile layout rather than adding one.

@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

The PR is not yet safe to merge because hybrid Appium sessions still lose DOM capture after switching from a native context into a webview.

Findings

  1. P1 Hybrid contexts lose DOM capture

Summary

  • Skips document-dependent collection for native sessions while preserving screenshot capture.
  • Derives device metadata for live sessions and normalizes Selenium capabilities.
  • Supports collector-bundle lookup from both built and source-resolved package entries.
  • Adds cross-adapter tests for native and mobile-browser sessions.
  • The previously reported hybrid-context limitation remains explicitly documented but unresolved.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  CAPS[Session capabilities] --> DETECT[Shared native-session detection]
  DETECT -->|Native app| NATIVE[Skip DOM and page-script probes]
  DETECT -->|Browser session| WEB[Inject and drain page collector]
  NATIVE --> SHOT[Retain screenshots]
  WEB --> DOM[Capture DOM and action snapshots]
  CAPS --> DEVICE[Derive device metadata]
  DEVICE --> APP[Live player layout]
Loading

Reviews (2) · Last reviewed commit: "style(shared): satisfy prettier in the a..."

Comment on lines +164 to +167
if (!deviceFromCapabilities(caps) && !namesAnAutomation(caps)) {
return false
}
return !deepCapString(caps, 'browserName')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Hybrid contexts lose DOM capture

Hybrid Appium sessions are classified for their entire lifetime from startup capabilities, even though their active context can switch between native and webview. When a native-capability session enters a webview, this predicate remains true, so the new guards in the Selenium, Nightwatch, and Python adapters keep skipping collector injection, DOM drains, performance reads, and element snapshots. The webview portion of the trace therefore has no DOM evidence. Document availability needs to follow the current runtime context for hybrid sessions rather than remain fixed from capabilities.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real, and deliberately so — the predicate's docstring states this residual, and it is carried over from the service's isNativeAppSession already on main (#371), not introduced here.

The behaviour change for Selenium/Nightwatch/Python is accurate though, and worth stating plainly: those three gated nothing before, so a hybrid session's webview portion was captured by accident and now is not.

Not fixing it in this PR. Following the runtime context means caching a getContext() read and invalidating it on context switches across core and three adapters — outside #372's scope, and unverifiable without a hybrid app on a real device. The cheap-direction argument also cuts the other way: treating a document-less session as web costs the service's 8 s-timeout probe poll per action, which is #351.

Tracked in #376.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native mobile support is WDIO-only: no other adapter can detect a native session

1 participant