diff --git a/CHANGELOG.md b/CHANGELOG.md index 7222d4e400..d8eb2bd8e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +- Fixed: BrowserStack sessions honour `--provider-project`, `--provider-build`, and + `--provider-session-name`. The capability builder emitted the legacy JSON Wire keys `device`, + `os_version`, and `app` at the top level next to the W3C `bstack:options` block; the hub treats a + request carrying any legacy key as a legacy session, reads the labels from the legacy top-level + `project`/`build`/`name` (never set), and ignores `bstack:options`, so every session landed in + "Untitled Project" / "Untitled Build" with an empty name — even after #2495 carried the flags to + the provider (#2494). The builder now emits `appium:deviceName`, `appium:platformVersion`, and + `appium:app` and no legacy key, verified against live App Automate sessions. - Changed (sessions): the implicit session is now keyed by workspace **and platform**, so one checkout can drive iOS and Android without inventing a `--session` name for every command (#2580). An implicit session was addressed by `cwd::default`, one slot per checkout, and it stayed diff --git a/packages/provider-webdriver/src/browserstack.ts b/packages/provider-webdriver/src/browserstack.ts index 0a1dde4140..ca845ea93f 100644 --- a/packages/provider-webdriver/src/browserstack.ts +++ b/packages/provider-webdriver/src/browserstack.ts @@ -222,14 +222,23 @@ export function createBrowserStackUploadApp( }; } +/** + * Builds the W3C `alwaysMatch` capabilities for a BrowserStack App Automate session. + * + * Every key is either W3C-standard (`platformName`), `appium:`-prefixed, or inside + * `bstack:options`. The legacy JSON Wire keys (`device`, `os_version`, `app`, `project`, `build`, + * `name`) must not appear: when the hub sees any of them it treats the whole request as a legacy + * session and reads the labels from the legacy top-level keys instead of `bstack:options`, so the + * project/build/session names are silently dropped and the session lands in "Untitled Project". + */ export function buildBrowserStackCapabilities( options: BrowserStackCapabilitiesOptions, ): Record { const { 'bstack:options': configuredBstackOptions, ...configured } = options.configured ?? {}; return { - device: options.deviceName, - os_version: options.osVersion, - ...(options.app ? { app: options.app } : {}), + 'appium:deviceName': options.deviceName, + 'appium:platformVersion': options.osVersion, + ...(options.app ? { 'appium:app': options.app } : {}), ...configured, // Merged per key, never assigned: `configured` carrying its own `bstack:options` used to // replace the whole object and silently drop the session/build labels below. diff --git a/test/integration/provider-scenarios/cloud-webdriver-lease-http.test.ts b/test/integration/provider-scenarios/cloud-webdriver-lease-http.test.ts index 7dc5168d00..3dd8139dda 100644 --- a/test/integration/provider-scenarios/cloud-webdriver-lease-http.test.ts +++ b/test/integration/provider-scenarios/cloud-webdriver-lease-http.test.ts @@ -107,9 +107,8 @@ test('lease_allocate over HTTP prepares the BrowserStack session end to end', as assert.deepEqual(alwaysMatch(create), { platformName: 'Android', 'appium:deviceName': 'Google Pixel 8', - device: 'Google Pixel 8', - os_version: '14.0', - app: 'bs://preuploaded', + 'appium:platformVersion': '14.0', + 'appium:app': 'bs://preuploaded', 'bstack:options': { projectName: 'MyProject', buildName: 'Build-2026-09-11', diff --git a/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts b/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts index 991b4707f6..fdf18eabc7 100644 --- a/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts +++ b/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts @@ -131,8 +131,12 @@ test('BrowserStack facade nests device-feature capabilities inside bstack:option locale: 'Fr', networkProfile: '4g-lte-advanced-good', }); - // Vendor capabilities stay nested; only BrowserStack's legacy selectors sit at the top level. + // Vendor capabilities stay nested, and no legacy JSON Wire selector reaches the top level — + // the hub would otherwise treat the request as legacy and ignore bstack:options entirely. assert.equal(alwaysMatch?.deviceOrientation, undefined); + for (const legacyKey of ['device', 'os_version', 'app', 'project', 'build', 'name']) { + assert.equal(alwaysMatch?.[legacyKey], undefined, `legacy key ${legacyKey} leaked`); + } }); }, 15_000); @@ -390,9 +394,8 @@ function assertBrowserStackCalls( alwaysMatch: { platformName: 'Android', 'appium:deviceName': 'Google Pixel 8', - device: 'Google Pixel 8', - os_version: '14.0', - app: 'bs://preuploaded', + 'appium:platformVersion': '14.0', + 'appium:app': 'bs://preuploaded', 'bstack:options': { projectName: 'agent-device', buildName: `build-${lease.runId}`, diff --git a/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts b/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts index 3dd4633731..ca846cd0aa 100644 --- a/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts +++ b/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts @@ -480,9 +480,8 @@ function assertWebDriverCalls(calls: readonly CloudWebDriverHttpCall[], leaseId: alwaysMatch: { platformName: 'Android', 'appium:deviceName': 'Google Pixel 8', - device: 'Google Pixel 8', - os_version: '14.0', - app: 'bs://app-id', + 'appium:platformVersion': '14.0', + 'appium:app': 'bs://app-id', 'bstack:options': { projectName: 'agent-device', buildName: 'run-a',