From b6966df9359d3eba4a51704ccaaf689c0c980fe7 Mon Sep 17 00:00:00 2001 From: "Sergey M." Date: Mon, 26 Aug 2024 17:17:34 +1000 Subject: [PATCH 1/3] Add Safari 18 to the opinionated Karma configuration --- node/src/karma_configuration.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/node/src/karma_configuration.ts b/node/src/karma_configuration.ts index 3923ffa..b96e808 100644 --- a/node/src/karma_configuration.ts +++ b/node/src/karma_configuration.ts @@ -209,12 +209,12 @@ const browserstackBrowsers = { Windows11_EdgeLatest: { platform: 'Windows', osVersion: '11', browserName: 'Edge', browserVersion: 'latest-beta', useHttps: true }, 'OSX10.14_Safari12': { platform: 'OS X', osVersion: 'Mojave', browserName: 'Safari', browserVersion: '12', useHttps: true }, OSX12_Safari15: { platform: 'OS X', osVersion: 'Monterey', browserName: 'Safari', browserVersion: '15', useHttps: false }, - OSX14_Safari17: { platform: 'OS X', osVersion: 'Sonoma', browserName: 'Safari', browserVersion: '17', useHttps: false }, - OSX14_ChromeLatest: { platform: 'OS X', osVersion: 'Sonoma', browserName: 'Chrome', browserVersion: 'latest-beta', useHttps: true }, - // OSX14_ChromeLatest_Incognito: { platform: 'OS X', osVersion: 'Sonoma', browserName: 'Chrome', browserVersion: 'latest-beta, ...chromeIncognitoCapabilities }, - OSX14_FirefoxLatest: { platform: 'OS X', osVersion: 'Sonoma', browserName: 'Firefox', browserVersion: 'latest-beta', useHttps: true }, - // OSX14_FirefoxLatest_Incognito: { platform: 'OS X', osVersion: 'Sonoma', browserName: 'Firefox', browserVersion: 'latest-beta, ...firefoxIncognitoCapabilities }, - OSX14_EdgeLatest: { platform: 'OS X', osVersion: 'Sonoma', browserName: 'Edge', browserVersion: 'latest-beta', useHttps: true }, + OSX15_Safari18: { platform: 'OS X', osVersion: 'Sequoia', browserName: 'Safari', browserVersion: '18', useHttps: false }, + OSX15_ChromeLatest: { platform: 'OS X', osVersion: 'Sequoia', browserName: 'Chrome', browserVersion: 'latest-beta', useHttps: true }, + // OSX15_ChromeLatest_Incognito: { platform: 'OS X', osVersion: 'Sequoia', browserName: 'Chrome', browserVersion: 'latest-beta, ...chromeIncognitoCapabilities }, + OSX15_FirefoxLatest: { platform: 'OS X', osVersion: 'Sequoia', browserName: 'Firefox', browserVersion: 'latest-beta', useHttps: true }, + // OSX15_FirefoxLatest_Incognito: { platform: 'OS X', osVersion: 'Sequoia', browserName: 'Firefox', browserVersion: 'latest-beta, ...firefoxIncognitoCapabilities }, + OSX15_EdgeLatest: { platform: 'OS X', osVersion: 'Sequoia', browserName: 'Edge', browserVersion: 'latest-beta', useHttps: true }, Android13_ChromeLatest: { platform: 'Android', osVersion: '13.0', browserName: 'Chrome', browserVersion: 'latest-beta', useHttps: true, flags: [BrowserFlags.MobileUserAgent] }, iOS12_Safari: { platform: 'iOS', osVersion: '12', browserName: 'Safari', useHttps: true, flags: [BrowserFlags.MobileUserAgent] }, iOS13_Safari: { platform: 'iOS', osVersion: '13', browserName: 'Safari', useHttps: true, flags: [BrowserFlags.MobileUserAgent] }, @@ -222,6 +222,7 @@ const browserstackBrowsers = { iOS15_Safari: { platform: 'iOS', osVersion: '15', browserName: 'Safari', useHttps: true, flags: [BrowserFlags.MobileUserAgent] }, iOS16_Safari: { platform: 'iOS', osVersion: '16', browserName: 'Safari', useHttps: true, flags: [BrowserFlags.MobileUserAgent] }, iOS17_Safari: { platform: 'iOS', osVersion: '17', browserName: 'Safari', useHttps: true, flags: [BrowserFlags.MobileUserAgent] }, + iOS18_Safari: { platform: 'iOS', osVersion: '18 Beta', browserName: 'Safari', useHttps: true, flags: [BrowserFlags.MobileUserAgent] }, } /* eslint-enable max-len */ From 5a13a9241efd0cc92c3c5ce0fad40e75c7f23802 Mon Sep 17 00:00:00 2001 From: "Sergey M." Date: Tue, 27 Aug 2024 00:06:16 +1000 Subject: [PATCH 2/3] Make it not necessary to add "beta" to the OS version --- node/readme.md | 10 ++-- node/src/browserstack_browsers.ts | 8 ++- node/src/browserstack_session_factory.ts | 4 +- node/src/karma_configuration.ts | 2 +- node/src/karma_plugin.ts | 1 + node/src/launcher.ts | 63 ++++++++++++++---------- 6 files changed, 54 insertions(+), 34 deletions(-) diff --git a/node/readme.md b/node/readme.md index dec315f..508e9d9 100644 --- a/node/readme.md +++ b/node/readme.md @@ -47,6 +47,7 @@ useHttps: true ``` _deviceType_ is used only on iOS and allows to choose from `iPhone` (default) and `iPad`. +You don't need to set a specific device name, the launcher chooses a device automatically. Same on Android. ```js Android11_ChromeLatest: { @@ -58,8 +59,6 @@ _deviceType_ is used only on iOS and allows to choose from `iPhone` (default) an }, ``` -You don't need to set a specific device name, the launcher chooses a device automatically. Same on Android. - _firefoxCapabilities_ an array of extra capabilities specifically for Firefox. ```js @@ -70,13 +69,16 @@ firefoxCapabilities: [ ], ``` +_osVersion_ selects the given OS version and also it's beta counterpart. For example, setting the OS version to `17` will choose either `17` or `17 Beta`. + ### Reporters There is a dedicated reporter that will mark successful tests as passed in BrowserStack. ```js - config.set({ - reporters: [...config.reporters, 'BrowserStack'], +config.set({ + reporters: [...config.reporters, 'BrowserStack'], +}) ``` ### BrowserStack specific settings diff --git a/node/src/browserstack_browsers.ts b/node/src/browserstack_browsers.ts index 0cbc1eb..3284740 100644 --- a/node/src/browserstack_browsers.ts +++ b/node/src/browserstack_browsers.ts @@ -54,7 +54,13 @@ export function makeBrowserStackBrowsers(browserStackCredentials: BrowserStackCr } function doesOSVersionMatch(browser: Browser, expectedOSVersion: string) { - return browser.os_version === expectedOSVersion + return ( + // Direct match + browser.os_version === expectedOSVersion || + // Beta version match + (browser.os_version.startsWith(expectedOSVersion) && + /^[ \-_]beta$/i.test(browser.os_version.slice(expectedOSVersion.length))) + ) } function doesDeviceTypeMatch(browser: Browser, expectedDeviceType: string) { diff --git a/node/src/browserstack_session_factory.ts b/node/src/browserstack_session_factory.ts index 88da90e..4d5de48 100644 --- a/node/src/browserstack_session_factory.ts +++ b/node/src/browserstack_session_factory.ts @@ -33,7 +33,7 @@ export class BrowserStackSessionFactory { public async createBrowser( browser: CustomLauncher, - deviceName: string | null, + deviceName: string | undefined, id: string, log: Logger, ): Promise { @@ -44,7 +44,7 @@ export class BrowserStackSessionFactory { this._build, id, this._project, - deviceName ?? undefined, + deviceName, browser.platform, this._idleTimeout, browser.osVersion, diff --git a/node/src/karma_configuration.ts b/node/src/karma_configuration.ts index b96e808..2cea10f 100644 --- a/node/src/karma_configuration.ts +++ b/node/src/karma_configuration.ts @@ -222,7 +222,7 @@ const browserstackBrowsers = { iOS15_Safari: { platform: 'iOS', osVersion: '15', browserName: 'Safari', useHttps: true, flags: [BrowserFlags.MobileUserAgent] }, iOS16_Safari: { platform: 'iOS', osVersion: '16', browserName: 'Safari', useHttps: true, flags: [BrowserFlags.MobileUserAgent] }, iOS17_Safari: { platform: 'iOS', osVersion: '17', browserName: 'Safari', useHttps: true, flags: [BrowserFlags.MobileUserAgent] }, - iOS18_Safari: { platform: 'iOS', osVersion: '18 Beta', browserName: 'Safari', useHttps: true, flags: [BrowserFlags.MobileUserAgent] }, + iOS18_Safari: { platform: 'iOS', osVersion: '18', browserName: 'Safari', useHttps: true, flags: [BrowserFlags.MobileUserAgent] }, } /* eslint-enable max-len */ diff --git a/node/src/karma_plugin.ts b/node/src/karma_plugin.ts index 3dcdaa3..d52e30b 100644 --- a/node/src/karma_plugin.ts +++ b/node/src/karma_plugin.ts @@ -34,6 +34,7 @@ declare module 'karma' { interface CustomLauncher { name?: string | undefined + /** Actually required, but left optional to avoid clashes with launcher types provided by other Karma plugins */ osVersion?: string | undefined deviceType?: 'iPhone' | 'iPad' | undefined browserVersion?: string | null | undefined diff --git a/node/src/launcher.ts b/node/src/launcher.ts index 329eac8..1031f83 100644 --- a/node/src/launcher.ts +++ b/node/src/launcher.ts @@ -28,7 +28,7 @@ export function BrowserStackLauncher( retryLauncherDecorator(this) const log = logger.create('Browserstack ' + this.id) const bsLocalManagerPromise = browserStackLocalManager.run(log) - const deviceNamesPromise = getDeviceNames(browserStackBrowsers, args, log) + const suitableDevicesPromise = getSuitableDevices(browserStackBrowsers, args, log) const captureTimeout = new CaptureTimeout(this, config, log) let startAttempt = 0 @@ -58,15 +58,19 @@ export function BrowserStackLauncher( this.on('start', async (pageUrl: string) => { try { - await bsLocalManagerPromise - const [deviceName] = await Promise.all([chooseDeviceName(), browserStackSessionsManager.ensureQueue(this, log)]) + const [{ name: deviceName, osVersion }] = await Promise.all([chooseDevice(), bsLocalManagerPromise]) + + // The queue should be checked right before creating a BrowserStack session to reduce the probability of a race + // condition where another Karma session also checks the queue in these events. + await browserStackSessionsManager.ensureQueue(this, log) log.debug(`creating browser with attributes: ${JSON.stringify(args)}`) log.debug(`attempt: ${startAttempt}`) - log.debug(`device name: ${deviceName}`) + log.debug(`device name override: ${deviceName}`) + log.debug(`OS version override: ${osVersion}`) startAttempt += 1 - browser = await browserStackSessionFactory.createBrowser(args, deviceName, this.id, log) + browser = await browserStackSessionFactory.createBrowser({ ...args, osVersion }, deviceName, this.id, log) captureTimeout.onStart() const sessionId = (await browser.getSession()).getId() log.debug(`WebDriver SessionId: ${sessionId}`) @@ -115,32 +119,38 @@ export function BrowserStackLauncher( done() }) - const chooseDeviceName = async () => { - const deviceNames = await deviceNamesPromise - if (!deviceNames) { - return null - } - - if (deviceNames.length === 0) { + const chooseDevice = async () => { + const devices = await suitableDevicesPromise + if (devices.length === 0) { throw new Error('No device available for the given configuration') } - const deviceName = deviceNames[startAttempt % deviceNames.length] - log.info(`Using ${deviceName} for the browser ${this.name}`) - return deviceName + const device = devices[startAttempt % devices.length] + if (device.name) { + log.info(`Using ${device.name} for the browser ${this.name}`) + } + return device } } +interface SuitableDevice { + name: string | undefined + osVersion: string | undefined +} + /** - * Returns the list of devices for the given launcher configuration. - * Returns `null` when the given configuration doesn't need a device name. + * Returns the list of devices suitable for the given launcher configuration. */ -async function getDeviceNames(browserStackBrowsers: BrowserStackBrowsers, args: CustomLauncher, log: Logger) { - let devices: browserstack.Browser[] | null = null +async function getSuitableDevices( + browserStackBrowsers: BrowserStackBrowsers, + args: CustomLauncher, + log: Logger, +): Promise { + let rawDevices: browserstack.Browser[] | null = null switch (args.platform) { case 'iOS': - devices = await browserStackBrowsers.getIOSDevices( + rawDevices = await browserStackBrowsers.getIOSDevices( args.osVersion ?? null, args.deviceType === 'iPad' ? 'ipad' : 'iphone', args.browserName?.toLowerCase() === 'chrome' ? 'chrome' : 'safari', @@ -149,7 +159,7 @@ async function getDeviceNames(browserStackBrowsers: BrowserStackBrowsers, args: ) break case 'Android': - devices = await browserStackBrowsers.getAndroidDevices( + rawDevices = await browserStackBrowsers.getAndroidDevices( args.osVersion ?? null, args.browserName?.toLowerCase() === 'samsung' ? 'samsung' : 'chrome', true, @@ -158,12 +168,13 @@ async function getDeviceNames(browserStackBrowsers: BrowserStackBrowsers, args: break } - const deviceNames = devices - ? devices.map((device) => device.device).filter((name): name is string => name !== null) - : null - log.debug(`device names for attributes ${JSON.stringify(args)}: ${JSON.stringify(deviceNames)}`) + const devices: SuitableDevice[] = rawDevices + ? rawDevices.map((device) => ({ name: device.device ?? undefined, osVersion: device.os_version })) + : [{ name: undefined, osVersion: args.osVersion }] + // todo: Debug level + log.info(`devices suitable for attributes ${JSON.stringify(args)}: ${JSON.stringify(devices)}`) - return deviceNames + return devices } function makeLauncherName(args: CustomLauncher) { From dead1327050046193c4148ccee24d61b43dcd7e5 Mon Sep 17 00:00:00 2001 From: "Sergey M." Date: Tue, 27 Aug 2024 00:14:57 +1000 Subject: [PATCH 3/3] Bump the Node package version, amendments --- node/package.json | 2 +- node/src/launcher.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/node/package.json b/node/package.json index 90b04f5..3c05f1d 100644 --- a/node/package.json +++ b/node/package.json @@ -1,7 +1,7 @@ { "name": "@fpjs-incubator/broyster", "description": "Test tools", - "version": "0.2.0", + "version": "0.2.1", "keywords": [ "test", "tools", diff --git a/node/src/launcher.ts b/node/src/launcher.ts index 1031f83..34a835a 100644 --- a/node/src/launcher.ts +++ b/node/src/launcher.ts @@ -66,7 +66,7 @@ export function BrowserStackLauncher( log.debug(`creating browser with attributes: ${JSON.stringify(args)}`) log.debug(`attempt: ${startAttempt}`) - log.debug(`device name override: ${deviceName}`) + log.debug(`device name: ${deviceName}`) log.debug(`OS version override: ${osVersion}`) startAttempt += 1 @@ -171,8 +171,7 @@ async function getSuitableDevices( const devices: SuitableDevice[] = rawDevices ? rawDevices.map((device) => ({ name: device.device ?? undefined, osVersion: device.os_version })) : [{ name: undefined, osVersion: args.osVersion }] - // todo: Debug level - log.info(`devices suitable for attributes ${JSON.stringify(args)}: ${JSON.stringify(devices)}`) + log.debug(`devices suitable for attributes ${JSON.stringify(args)}: ${JSON.stringify(devices)}`) return devices }