Repository navigation
Native mobile support is WDIO-only: no other adapter can detect a native session #372
Description
#345–#350 built native mobile capture, but the split between what of it is cross-adapter and what is not was never stated. The trace and player halves are cross-adapter; every adapter-side piece is WDIO-only, because the WDIO service is the only adapter that can tell a native session from a browser one.
Neither Selenium, Nightwatch nor the Python adapter claims native support today, so this is not a regression — it is an unimplemented case, and this issue is to close it.
Already cross-adapter — no work needed
Recorded here so nobody re-does it:
deviceoncontext-options(Native mobile capture carries no viewport, capabilities or device into the trace #345, A trace zip cannot say it was recorded on a device #346).deviceFromCapabilitiesruns inpackages/trace's exporter, on the capabilities every adapter already sends. Verified for Selenium, Nightwatch and Python.- Player-side framing — Player fits a portrait capture by the wrong axis — main pane clipped, filmstrip blank #344 (fit a domless capture by both axes), Player frames a phone capture as a desktop browser window #347 (device chrome instead of a browser window), Filmstrip thumbnails render blank for a portrait capture #348 (portrait filmstrip thumbnails), Player pane height is computed once and never adapts to the window #349 and the device-column layout. All
packages/app, all keyed offmetadata.device, all adapter-blind.
WDIO-only
isNativeMobile exists in exactly one file, packages/service/src/mobile.ts, and reads browser.isMobile / isAndroid / isIOS — runtime flags WDIO derives for its own browser object. Selenium's WebDriver, Nightwatch's browser and the Python driver have no equivalent.
Fourteen call sites in the service gate on it. Each one protects a page-side call that cannot work without a document:
| what it protects | site |
|---|---|
| the whole page drain | session.ts captureTrace |
| the drain and performance read after a page-transition command | session.ts:188 |
| the per-command screenshot | session.ts:165 |
| collector injection at bringup | index.ts:243 |
| the live-command drain | index.ts:674 |
reading the viewport with getWindowSize() rather than window.visualViewport |
session-metadata.ts:32 |
the per-action snapshot: skip the settle, and take the native path (getPageSource instead of runScript) |
action-snapshot.ts:76, :111 |
| the assertion screenshot | assertion-tracker.ts:122 |
The other adapters have the same-named methods and gate none of them:
- selenium-devtools:
session.tscaptureTrace/injectScript/takeScreenshot/captureBrowserLogs/reinjectIfNavigated,session-lifecycle.tsregisterPreload,helpers/commandPostActions.tscapturePerformance,action-snapshot.ts. - nightwatch-devtools:
session.tscaptureTrace/injectScript/anchorAfterNavigation/captureBrowserLogs/captureNetworkFromPerformanceLogs, and the fourhelpers/webdriverHttp.tsprobes. - selenium-devtools-py:
instrumentation.py_viewportreadswindow.innerWidth/Heightunconditionally;snapshot.py_DRAIN_SCRIPTlikewise.
Selenium reads platformName in one place — a boot log line in helpers/driverMetadata.ts. Nightwatch's isMobile/isAndroid/isIOS appear only as entries in INTERNAL_COMMANDS_TO_IGNORE, which is its own commands being filtered out of the action list, not detection. Python has no reference to platformName at all.
Consequences, measured
- A Selenium or Nightwatch native trace claims
viewport: 1280x720— the exporter's fallback — so the player frames a phone at desktop proportions.deviceis still correct, so the device layout selects; it is the geometry inside it that is wrong. - Either adapter would emit the failures Wasted Appium round trips: captureTrace runs on native sessions #350 just removed from the service:
Method is not implemented, once per drain. On the service that was five per run, at ERROR, in the user's output. - Python the same, at DEBUG.
The shared piece
A native-session predicate in packages/core — all three JS adapters may import core, and shared's isNativePlatform / deviceFromCapabilities already do the narrowing the exporter needs. It must read capabilities, not driver flags: capabilities are the one thing all four adapters have.
One constraint to settle before implementing, because it is not obvious: isNativePlatform(platformName) alone is the wrong predicate. WDIO's own isMobile deliberately returns false when automationName is gecko/safari/chrome/chromium (see @wdio/utils), i.e. it excludes mobile web. An Android Chrome session reports platformName: 'Android' and has a DOM — gating the drain on the platform alone would silently kill DOM capture for it, turning a fix into a worse bug than the one it fixes. The predicate has to reject a session that names a browser automation.
That is also why the app already splits the two questions, and should stay as it is: workbench.ts #deviceLayout gates on device alone (a mobile browser session is still a portrait viewport that wants the tall column, it just keeps its address bar inside it), while snapshot.ts #deviceCapture additionally requires no mutations and no metadata.url before it draws device chrome.
Scope
-
core: a native-session predicate reading capabilities, with the mobile-web exclusion above, unit-tested against real capability bags from both cases (local Appium, a device cloud, and Android Chrome). -
service:isNativeMobiledelegates to it. The runtime-flag read can stay as a fast path — those flags are what WDIO already computed — but the answer should come from one place. -
selenium-devtools: gate the sites listed above. Put the guard inside the function, not at the call sites — Wasted Appium round trips: captureTrace runs on native sessions #350's finding was that two of four call sites asked and two forgot. -
nightwatch-devtools: same. -
selenium-devtools-py: guard_viewportand the drain; read the window size off the driver on native. - every adapter reads the viewport the native way, so a native trace carries the device's real size rather than the 1280x720 fallback.
- a native example per adapter, or an explicit line in each README saying native is unsupported there.
Verification needs real devices or a device cloud; CI cannot cover it, so the PR should state what was run and on what.
Related
- Native mobile capture carries no viewport, capabilities or device into the trace #345, Wasted Appium round trips: captureTrace runs on native sessions #350 — the WDIO-side work this mirrors.
- Native mobile trace mode costs 40-60s per run in per-action probes #351 — per-action probe cost on a native session. Same code path; whatever lands here should be measured against it rather than in isolation.
Activity
vishnuv688 commentedon Sep 9, 2026
Two corrections to the plan above, found while implementing it.
The predicate belongs in
shared, notcore.packages/trace's exporter has three?? { width: 1280, height: 720 }viewport fallbacks and may import onlyshared, so putting it incorewould put it out of reach of a consumer that wants it. It landed inshared/src/device.tsbesidedeviceFromCapabilities, which already reads capabilities for the adjacent question.The mobile-web exclusion has a better discriminator than the one suggested. Copying WDIO's automationName exclusion list is not needed: whether the capabilities name a browser at all answers it directly and needs no list to keep current — mobile web must state a
browserName, a native app states none. Also, bothplatformNameandbrowserNamehave to be read one level into vendor options; WDIO's ownisMobilereadsbstack:options.browserName, so bags that carry them only there exist, and a shallow read calls such a session desktop.And one claim in the issue was wrong. It says the
devicefield is already cross-adapter, "verified for Selenium, Nightwatch and Python". Nightwatch and Python yes; Selenium no, and never has been.buildDriverMetadatacalledcapabilities.serialize?.(), but selenium-webdriver'sCapabilitieskeeps its data in a private Map and exposesserializeonly under a Symbol — so that call returnedundefined, the raw instance was published, and it JSON-serializes to{"map_":{}}. Measured against selenium-webdriver 4.46.0. Every Selenium trace has carried empty capabilities: nodevice, a guessed browser name, and an empty capabilities pane in the dashboard. The test stub that hid it had a string-keyedserialize()no real driver has. Fixed in the same change, since the new guards read that bag and were dead code without it.Scope note: the per-action snapshot turned out to be the densest page-script path of the lot — two injected scripts plus
urlandtitleon every action, unguarded in all three adapters — so it is included. Two items are deliberately not, and are now tracked separately: