feat(cloud-webdriver): BrowserStack device-feature capabilities, and fix cloud orientation - #1544
Conversation
Adds the eight BrowserStack "device feature" session capabilities that had no representation in agent-device: deviceOrientation, geoLocation, timezone, language, locale, networkProfile, customNetwork, and resignApp. These are vendor capabilities, so they are emitted inside `bstack:options` rather than at the top level. BrowserStack's YAML config lists them unnested and its SDK relocates them; agent-device talks to the hub directly, so it nests them itself. A single spec table drives both the flag reader and the capability builder, so adding a capability is a table row rather than a branch in each. A structural test asserts every field owns exactly one row, since a field the table forgets would parse off the CLI, ride the profile, and then be silently dropped before the hub ever saw it. Rejects combinations the provider cannot act on unambiguously: an unknown orientation is caught at the flag boundary instead of being forwarded to a hub that accepts and then ignores it, --provider-no-resign-app is refused on Android, and a named network profile cannot be combined with a custom network shape. Also fixes a latent shallow-merge bug in buildBrowserStackCapabilities: a caller supplying its own `bstack:options` replaced the whole object and silently dropped the project, build, and session labels. It is now merged per key.
`setOrientation` on the cloud WebDriver path sent `mobile: rotate`, which is
not a driver command at all. UiAutomator2's own error enumerates its
extensions and `rotate` is absent from the list, so `agent-device orientation`
was a hard failure on every hosted provider.
It also forwarded agent-device's four-way rotation vocabulary verbatim
("landscape-left", "portrait-upside-down"), where the protocol accepts only
uppercase PORTRAIT/LANDSCAPE. Every other platform has a translation layer;
this path was the only one without one.
Now two transports, ordered by backend. `POST /rotation` takes exact four-way
degrees and leads on Android, since it is the only endpoint that can express
upside-down and left-versus-right. `POST /orientation` is two-way and leads on
XCUITest, which rejects `/rotation`. Each falls back to the other, because only
BrowserStack's UiAutomator2 is verified and a provider whose driver disagrees
should degrade rather than hard-fail.
Verified live against BrowserStack App Automate:
POST /rotation {"x":0,"y":0,"z":0} -> 200 {"value":"ROTATION_0"}
The rotation-to-surface-index mapping moves to contracts/device-rotation.ts and
the existing adb path now reads from it, so the local and hosted mappings
cannot drift apart.
Note this rotates the current display, not persistent device rotation, so an
activity that does not pin its own orientation may still need rotating once it
is in the foreground.
The capability was declared "partial" without the transport existing, and no
test covered setOrientation on the cloud path; only adb and the Apple runner
were covered. Both gaps are now closed.
|
Reviewed |
…ice-capabilities-and-orientation # Conflicts: # packages/provider-webdriver/src/browserstack-device-features.test.ts # packages/provider-webdriver/src/browserstack-device-features.ts # packages/provider-webdriver/src/webdriver-orientation.test.ts # packages/provider-webdriver/src/webdriver-orientation.ts # src/cli/commands/connection-runtime.ts # src/cli/connection/profile-fields.ts # src/cloud-webdriver/provider-definitions.ts # src/platforms/android/input-actions.ts # src/remote/remote-config-schema.ts # test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts
…wned flags Addresses review on callstack#1544. The orientation fallback caught every error, so a timeout, an auth rejection, a dead session or a provider 5xx on the first transport was swallowed and retried against the second. When that one also failed the caller got "rejected both endpoints" with the real cause discarded. Fallback is now keyed on structured unsupported-endpoint signals only — HTTP 404/405, or a W3C `unknown command` / `unknown method` code — matching the repo rule of keying on typed details rather than message text. Everything else rethrows unchanged. Device-feature capabilities are BrowserStack-owned, but the flags were accepted by any cloud provider, persisted into the generated profile, and then silently dropped at session creation. `connect aws-device-farm` now rejects them with a typed error naming each offending flag, raised before the provider's own required-argument checks so the caller is told what is unsupported rather than what else is missing. Ownership is modelled on the capability spec table, so a new capability inherits the guard without a second list to maintain. Adds provider-backed orientation scenarios driven through public daemon dispatch against the fake WebDriver provider: the four-way endpoint on the happy path, the documented collapse onto the two-way endpoint when the driver does not implement `/rotation`, and a provider 5xx that must surface without consulting the second transport. The fake server's route handling became a table in the process — it had grown to ten branches in one function.
|
Thank you so much for the contribution! How’s the experience? |
|
Re-reviewed
Exact-head iOS smoke is also red on a capture-stalled timeout, and BrowserStack iOS live evidence or an exact blocker is still absent. Rerun/classify CI and add iOS evidence after the code fixes; no |
…ership at the runtime boundary Addresses the second review pass on callstack#1544. The fallback classifier returned on any 404/405 before consulting the W3C error code, so an HTTP 404 carrying `invalid session id` was masked as a missing route and retried against the second transport. The structured code now takes precedence whenever the driver sent one; bare status is consulted only when no code exists. Two cases pin it: a 404 `invalid session id` and a 405 `timeout` must both surface rather than fall through. Provider ownership was enforced only in the CLI profile builder, which the typed client and hand-authored remote-config profiles bypass entirely — both reach session preparation without passing through `connect`, so the capabilities were accepted and then dropped. The check now lives on the capability-ownership module and runs inside AWS Device Farm's `prepareSession`, with the CLI builder calling the same helper instead of its own copy. Covered by a scenario that drives the runtime boundary directly and asserts the rejection happens before any provider session is created.
|
Re-reviewed at Remaining readiness work is evidence only: no exact-head CI checks are reported and merge state is currently unstable. Please rerun/attach the exact-head gates. Also provide BrowserStack iOS evidence for the iOS-first |
|
Got a real hosted-iOS session, so the blocker is retired — iOS evidence below, and it happens to validate the fallback classifier on a real device rather than only in unit tests. BrowserStack iOS evidenceDevice The iOS-first ordering behaves as designed. Every orientation call went to Portrait succeeded: Landscape was refused by the driver, because the app under test pins portrait: Surfaced to the caller as This is also the fallback classifier proving itself in the wild. That 500 is exactly the shape the earlier catch-everything implementation would have swallowed: it would have retried So the transport matrix is now covered on real devices both ways:
|
@thymikee Genuinely good, and the reason this PR exists — I hit both bugs driving a real app on BrowserStack. What worked:
Friction worth naming:
|
|
Re-reviewed unchanged head Remaining readiness work is CI only: no checks are reported for this exact head and merge state is unstable. Please rerun/attach the exact-head gates; once green, no further review work is requested. |
|
@thymikee Can you approve CI workflow please? |
Summary
Two related fixes to the hosted BrowserStack path:
orientationno longer fails on every cloud provider. The cloud WebDriver interactor sentmobile: rotate, which is not a driver command — UiAutomator2's own error enumerates its extensions androtateis absent — soagent-device orientationwas a hard failure against any hosted session.What changed
Device-feature capabilities
Adds
deviceOrientation,geoLocation,timezone,language,locale,networkProfile,customNetwork, andresignApp, each with a CLI flag and a short alias:agent-device connect browserstack \ --platform android --device "Google Pixel 8" \ --provider-os-version 14.0 --provider-app bs://app-id \ --provider-device-orientation portrait \ --provider-geo-location US \ --provider-timezone New_YorkThey are driven by a single spec table in
cloud-webdriver/browserstack-device-features.ts, so adding a capability is a spec row rather than a branch in the capability builder and another in the flag reader. A structural test asserts every field owns exactly one row, so a field cannot be parsed off the CLI, ride the profile, and then be silently dropped before the wire.Capabilities are emitted inside
bstack:options, which is where BrowserStack expects vendor capabilities on the W3C wire. Its YAML config lists them unnested and its SDK relocates them; agent-device talks to the hub directly, so it nests them itself.Three combinations are rejected up front with a typed error and a recovery hint, instead of being accepted by the hub and then ignored on a remote device:
portrait|landscape--provider-no-resign-appon a non-iOS sessionOrientation transport
setOrientationnow uses the WebDriver endpoints, ordered by backend:POST /rotationtakes exact four-way degrees, so it leads on Android — agent-device's rotation vocabulary is four-way and this is the only endpoint that can express upside-down and left-vs-right.POST /orientationis two-way. XCUITest rejects/rotation, so iOS leads with this and pays the cost of collapsing both landscape rotations onto one value.Each order keeps the other as a fallback, since only BrowserStack's UiAutomator2 is verified against a live device; a provider whose driver disagrees degrades rather than hard-failing.
The four-way mapping moved to a single table in
contracts/device-rotation.ts, and the existing adb path (resolveAndroidUserRotation) now reads from it, so the local and cloud rotation mappings cannot drift apart.Incidental fix
bstack:optionsis now merged per key rather than assigned. Previously a caller supplying its ownbstack:optionsreplaced the whole object, silently droppingprojectName,buildName, andsessionName. Latent before this change; load-bearing now that device features write into the same object.Testing
pnpm checkpasses: 533 files, 4689 tests.New coverage:
browserstack-device-features.test.ts— capability projection, the field/spec structural check, and each of the three rejected combinationswebdriver-orientation.test.ts— per-backend transport order, the four-way degree mapping, fallback on driver rejection, and the exhausted-both-endpoints errorcloud-webdriver-provider-adapters.test.tsthat device features land nested inbstack:optionsand not at the top level, and that a caller's own vendor options merge rather than clobberVerified live against BrowserStack (Google Pixel 8, Android 14). The capabilities appear in the session's recorded create-session payload, and the rotate is confirmed in the provider's Appium log:
Notes for reviewers
POST /rotationrotates the current display, not the device's persistent rotation. An activity that does not pin its own orientation — a Chrome Custom Tab hosting an OAuth page, for example — can still come up rotated and needs anotherorientationcall once it is foreground. The docs and the capability note both say so; the session capability alone does not cover it.settings put system user_rotationovermobile: shell) was written and then removed. As a last-resort fallback it could never fire in the case it existed for — that case is/rotationsucceeding but not persisting. If persistent rotation is wanted later it belongs as the preferred Android path, and providers commonly gatemobile: shelleven where the driver advertises it.orientationcapability stayspartial: four-way intent collapses to two-way on drivers that only accept/orientation.