From 517b60a7dd5a96a40e8523ac5d7b1390bc48fdf1 Mon Sep 17 00:00:00 2001 From: Wim Selles Date: Sun, 25 Oct 2020 17:08:40 +0100 Subject: [PATCH] feat: add test annotation --- src/browser-info.ts | 4 + src/launcher/launcher.ts | 173 ++++++++++++++++++++------------------- src/reporter/reporter.ts | 13 +++ 3 files changed, 104 insertions(+), 86 deletions(-) diff --git a/src/browser-info.ts b/src/browser-info.ts index 1ebba4a..ab4576d 100644 --- a/src/browser-info.ts +++ b/src/browser-info.ts @@ -1,4 +1,5 @@ import {SauceLabsOptions} from 'saucelabs' +import {BrowserObject} from "webdriverio"; type SauceBaseOption = Pick @@ -15,6 +16,9 @@ export interface SaucelabsBrowser extends SauceBaseOption { /** Saucelabs access key that has been used to launch this browser. */ accessKey: string; + + /** Saucelabs driver instance to communicate with this browser. */ + driver: BrowserObject } /** Type that describes the BrowserMap injection token. */ diff --git a/src/launcher/launcher.ts b/src/launcher/launcher.ts index 02e3cde..c97b393 100644 --- a/src/launcher/launcher.ts +++ b/src/launcher/launcher.ts @@ -14,90 +14,91 @@ export function SaucelabsLauncher(args, captureTimeoutLauncherDecorator, retryLauncherDecorator) { - // Apply base class mixins. This would be nice to have typed, but this is a low-priority now. - baseLauncherDecorator(this); - captureTimeoutLauncherDecorator(this); - retryLauncherDecorator(this); - - // initiate driver with null to not close the tunnel too early - connectedDrivers.set(this.id, null) - - const log = logger.create('SaucelabsLauncher'); - const { - startConnect, - sauceConnectOptions, - seleniumCapabilities, - browserName - } = processConfig(config, args); - - // Setup Browser name that will be printed out by Karma. - this.name = browserName + ' on SauceLabs'; - - // Listen for the start event from Karma. I know, the API is a bit different to how you - // would expect, but we need to follow this approach unless we want to spend more work - // improving type safety. - this.on('start', async (pageUrl: string) => { - if (startConnect) { - try { - // In case the "startConnect" option has been enabled, establish a tunnel and wait - // for it being ready. In case a tunnel is already active, this will just continue - // without establishing a new one. - await sauceConnect.establishTunnel(seleniumCapabilities, sauceConnectOptions); - } catch (error) { - log.error(error); - - this._done('failure'); - return; - } - } - - try { - // See the following link for public API of the selenium server. - // https://wiki.saucelabs.com/display/DOCS/Instant+Selenium+Node.js+Tests - const driver = await remote(seleniumCapabilities); - - // Keep track of all connected drivers because it's possible that there are multiple - // driver instances (e.g. when running with concurrency) - connectedDrivers.set(this.id, driver); - - const sessionId = driver.sessionId - - log.info('%s session at https://saucelabs.com/tests/%s', browserName, sessionId); - log.debug('Opening "%s" on the selenium client', pageUrl); - - // Store the information about the current session in the browserMap. This is necessary - // because otherwise the Saucelabs reporter is not able to report results. - browserMap.set(this.id, { - sessionId, - username: seleniumCapabilities.user, - accessKey: seleniumCapabilities.key, - region: seleniumCapabilities.region, - headless: seleniumCapabilities.headless - }); - - await driver.url(pageUrl); - } catch (e) { - log.error(e); - - // Notify karma about the failure. - this._done('failure'); - } - }); - - this.on('kill', async (done: () => void) => { - try { - const driver = connectedDrivers.get(this.id); - await driver.deleteSession(); - } catch (e) { - // We need to ignore the exception here because we want to make sure that Karma is still - // able to retry connecting if Saucelabs itself terminated the session (and not Karma) - // For example if the "idleTimeout" is exceeded and Saucelabs errored the session. See: - // https://wiki.saucelabs.com/display/DOCS/Test+Didn%27t+See+a+New+Command+for+90+Seconds - log.error('Could not quit the Saucelabs selenium connection. Failure message:'); - log.error(e); - } - - connectedDrivers.delete(this.id) - return process.nextTick(done); - }) + // Apply base class mixins. This would be nice to have typed, but this is a low-priority now. + baseLauncherDecorator(this); + captureTimeoutLauncherDecorator(this); + retryLauncherDecorator(this); + + // initiate driver with null to not close the tunnel too early + connectedDrivers.set(this.id, null) + + const log = logger.create('SaucelabsLauncher'); + const { + startConnect, + sauceConnectOptions, + seleniumCapabilities, + browserName + } = processConfig(config, args); + + // Setup Browser name that will be printed out by Karma. + this.name = browserName + ' on SauceLabs'; + + // Listen for the start event from Karma. I know, the API is a bit different to how you + // would expect, but we need to follow this approach unless we want to spend more work + // improving type safety. + this.on('start', async (pageUrl: string) => { + if (startConnect) { + try { + // In case the "startConnect" option has been enabled, establish a tunnel and wait + // for it being ready. In case a tunnel is already active, this will just continue + // without establishing a new one. + await sauceConnect.establishTunnel(seleniumCapabilities, sauceConnectOptions); + } catch (error) { + log.error(error); + + this._done('failure'); + return; + } + } + + try { + // See the following link for public API of the selenium server. + // https://wiki.saucelabs.com/display/DOCS/Instant+Selenium+Node.js+Tests + const driver = await remote(seleniumCapabilities); + + // Keep track of all connected drivers because it's possible that there are multiple + // driver instances (e.g. when running with concurrency) + connectedDrivers.set(this.id, driver); + + const sessionId = driver.sessionId + + log.info('%s session at https://saucelabs.com/tests/%s', browserName, sessionId); + log.debug('Opening "%s" on the selenium client', pageUrl); + + // Store the information about the current session in the browserMap. This is necessary + // because otherwise the Saucelabs reporter is not able to report results. + browserMap.set(this.id, { + sessionId, + username: seleniumCapabilities.user, + accessKey: seleniumCapabilities.key, + region: seleniumCapabilities.region, + headless: seleniumCapabilities.headless, + driver, + }); + + await driver.url(pageUrl); + } catch (e) { + log.error(e); + + // Notify karma about the failure. + this._done('failure'); + } + }); + + this.on('kill', async (done: () => void) => { + try { + const driver = connectedDrivers.get(this.id); + await driver.deleteSession(); + } catch (e) { + // We need to ignore the exception here because we want to make sure that Karma is still + // able to retry connecting if Saucelabs itself terminated the session (and not Karma) + // For example if the "idleTimeout" is exceeded and Saucelabs errored the session. See: + // https://wiki.saucelabs.com/display/DOCS/Test+Didn%27t+See+a+New+Command+for+90+Seconds + log.error('Could not quit the Saucelabs selenium connection. Failure message:'); + log.error(e); + } + + connectedDrivers.delete(this.id) + return process.nextTick(done); + }) } diff --git a/src/reporter/reporter.ts b/src/reporter/reporter.ts index 2b9515c..9cf53f9 100644 --- a/src/reporter/reporter.ts +++ b/src/reporter/reporter.ts @@ -9,6 +9,19 @@ export function SaucelabsReporter(logger, browserMap: BrowserMap) { const log = logger.create('reporter.sauce'); let pendingUpdates: Promise[] = []; + // This fires when a single test is executed and will update the run in sauce labs with an annotation + // of the test including the status of the test + this.onSpecComplete = function(browser, result) { + const driver = browserMap.get(browser.id).driver + const status = result.success ? '✅' : '❌' + + pendingUpdates.push(driver.execute(`sauce:context=${status}: ${result.fullName}`)) + + if(!result.success && result.log.length > 0){ + pendingUpdates.push(driver.execute(`sauce:context=${result.log[0]}`)) + } + } + // This fires whenever any browser completes. This is when we want to report results // to the Saucelabs API, so that people can create coverage banners for their project. this.onBrowserComplete = function (browser) {