Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Safari 18 to the opinionated Karma configuration #78

Merged
merged 3 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion node/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@fpjs-incubator/broyster",
"description": "Test tools",
"version": "0.2.0",
"version": "0.2.1",
"keywords": [
"test",
"tools",
Expand Down
10 changes: 6 additions & 4 deletions node/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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
Expand All @@ -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
Expand Down
8 changes: 7 additions & 1 deletion node/src/browserstack_browsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions node/src/browserstack_session_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class BrowserStackSessionFactory {

public async createBrowser(
browser: CustomLauncher,
deviceName: string | null,
deviceName: string | undefined,
id: string,
log: Logger,
): Promise<WebDriver> {
Expand All @@ -44,7 +44,7 @@ export class BrowserStackSessionFactory {
this._build,
id,
this._project,
deviceName ?? undefined,
deviceName,
browser.platform,
this._idleTimeout,
browser.osVersion,
Expand Down
13 changes: 7 additions & 6 deletions node/src/karma_configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,19 +209,20 @@ 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] },
iOS14_Safari: { platform: 'iOS', osVersion: '14', browserName: 'Safari', useHttps: true, flags: [BrowserFlags.MobileUserAgent] },
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', browserName: 'Safari', useHttps: true, flags: [BrowserFlags.MobileUserAgent] },
}
/* eslint-enable max-len */

Expand Down
1 change: 1 addition & 0 deletions node/src/karma_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 35 additions & 25 deletions node/src/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(`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}`)
Expand Down Expand Up @@ -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<SuitableDevice[]> {
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',
Expand All @@ -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,
Expand All @@ -158,12 +168,12 @@ 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 }]
log.debug(`devices suitable for attributes ${JSON.stringify(args)}: ${JSON.stringify(devices)}`)

return deviceNames
return devices
}

function makeLauncherName(args: CustomLauncher) {
Expand Down