Skip to content

Commit

Permalink
fix: Update sauce lib (#207)
Browse files Browse the repository at this point in the history
* fix(deps): update SauceLabs API binding

* fix: start sauce connect via saucelabs package

Co-authored-by: christian-bromann <mail@christian-bromann.com>
  • Loading branch information
wswebcreation and christian-bromann committed May 26, 2020
1 parent b1cf182 commit 7e75e17
Show file tree
Hide file tree
Showing 9 changed files with 2,363 additions and 1,075 deletions.
37 changes: 15 additions & 22 deletions README.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ Default: `process.env.SAUCE_ACCESS_KEY`


Your Sauce Labs access key which you will see on your [account page](https://saucelabs.com/account). Your Sauce Labs access key which you will see on your [account page](https://saucelabs.com/account).


### region
Type: `String`

Detect datacenter to run tests in. Can be either `eu` or `us`.

### headless
Type: `Boolean`

If set to `true` tests are being run on Sauce Labs headless platform on `us-east-1`. This option will be ignored if `region` is set.

### proxy ### proxy
Type: `String` Type: `String`


Expand All @@ -129,19 +139,6 @@ Default:


Options to send to Sauce Connect. Check [here](https://github.com/bermi/sauce-connect-launcher#advanced-usage) for all available options. Options to send to Sauce Connect. Check [here](https://github.com/bermi/sauce-connect-launcher#advanced-usage) for all available options.


### connectLocationForSERelay
Type: `String`
default: `ondemand.saucelabs.com`

If set, will attempt to connect to the specified host as a Selenium relay. This is intended to send Selenium commands through a Sauce Connect tunnel.

### connectPortForSERelay
Type: `Integer`
Default: 80

If set, will change the host used to connect to the Selenium server. This is intended to send Selenium commands through a Sauce Connect tunnel.


### build ### build
Type: `String` Type: `String`
Default: *One of the following environment variables*: Default: *One of the following environment variables*:
Expand Down Expand Up @@ -217,25 +214,21 @@ Required: `true`


Name of the browser. Name of the browser.


### version ### browserVersion
Type: `String` Type: `String`
Default: Latest browser version for all browsers except Chrome which defaults to `'27'` Default: Latest browser version for all browsers except Chrome


Version of the browser to use. Version of the browser to use.


### platform ### platformName
Type: `String` Type: `String`
Default: `'Linux'` for Firefox/Chrome, `'Windows 7'` for IE/Safari Default: `'Linux'` for Firefox/Chrome, `'Windows 7'` for IE/Safari


Name of platform to run browser on. Name of platform to run browser on.


### deviceOrientation ### `sauce:options`
Type: `String`
Default: `'portrait'`

Accepted values: `'portrait' || 'landscape'`


Set this string if your unit tests need to run on a particular mobile device orientation for Android Browser or iOS Safari. Specific Sauce Labs capability [options](https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options).


## Behind the scenes ## Behind the scenes


Expand Down
1 change: 0 additions & 1 deletion examples/karma.conf-ci.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ module.exports = function (config) {
testName: 'Karma and Sauce Labs demo', testName: 'Karma and Sauce Labs demo',
recordScreenshots: false, recordScreenshots: false,
connectOptions: { connectOptions: {
port: 5757,
logfile: 'sauce_connect.log' logfile: 'sauce_connect.log'
}, },
public: 'public' public: 'public'
Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
], ],
"author": "Vojta Jina <vojta.jina@gmail.com>", "author": "Vojta Jina <vojta.jina@gmail.com>",
"dependencies": { "dependencies": {
"sauce-connect-launcher": "^1.2.4", "global-agent": "^2.1.8",
"saucelabs": "^1.5.0", "saucelabs": "^4.3.0",
"selenium-webdriver": "^4.0.0-alpha.1" "webdriverio": "^6.1.9"
}, },
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
Expand All @@ -49,7 +49,7 @@
"@semantic-release/git": "9.0.0", "@semantic-release/git": "9.0.0",
"@semantic-release/npm": "7.0.4", "@semantic-release/npm": "7.0.4",
"@types/node": "^10.12.10", "@types/node": "^10.12.10",
"@types/selenium-webdriver": "^3.0.13", "@types/global-agent": "^2.1.0",
"husky": "4.2.3", "husky": "4.2.3",
"jasmine": "^3.3.0", "jasmine": "^3.3.0",
"karma": "^3.1.1", "karma": "^3.1.1",
Expand Down Expand Up @@ -88,6 +88,7 @@
"Parashuram <code@nparashuram.com>", "Parashuram <code@nparashuram.com>",
"Parashuram N <code@r.nparashuram.com>", "Parashuram N <code@r.nparashuram.com>",
"Peter Johason <peter@peterjohanson.com>", "Peter Johason <peter@peterjohanson.com>",
"Paul Gschwendtner <paulgschwendtner@gmail.com>" "Paul Gschwendtner <paulgschwendtner@gmail.com>",
"Christian Bromann <chrisian@saucelabs.com>"
] ]
} }
9 changes: 5 additions & 4 deletions src/browser-info.ts
Original file line number Original file line Diff line number Diff line change
@@ -1,8 +1,12 @@
import {SauceLabsOptions} from 'saucelabs'

type SauceBaseOption = Pick<SauceLabsOptions, 'headless' | 'region'>

/** /**
* This interface describes a browser that has been launched with Saucelabs. This is helpful * This interface describes a browser that has been launched with Saucelabs. This is helpful
* when reporting the results to the Saucelabs web API. * when reporting the results to the Saucelabs web API.
*/ */
export interface SaucelabsBrowser { export interface SaucelabsBrowser extends SauceBaseOption {
/** Saucelabs session id of this browser. */ /** Saucelabs session id of this browser. */
sessionId: string; sessionId: string;


Expand All @@ -11,9 +15,6 @@ export interface SaucelabsBrowser {


/** Saucelabs access key that has been used to launch this browser. */ /** Saucelabs access key that has been used to launch this browser. */
accessKey: string; accessKey: string;

/** Proxy URL that will be used to make an API call to the Saucelabs API. */
proxy: string;
} }


/** Type that describes the BrowserMap injection token. */ /** Type that describes the BrowserMap injection token. */
Expand Down
55 changes: 26 additions & 29 deletions src/launcher/launcher.ts
Original file line number Original file line Diff line number Diff line change
@@ -1,7 +1,10 @@
import {Builder, WebDriver} from 'selenium-webdriver'; import {remote, BrowserObject} from 'webdriverio';
import {processConfig} from "../process-config"; import {processConfig} from "../process-config";
import {BrowserMap} from "../browser-info"; import {BrowserMap} from "../browser-info";


// Array of connected drivers. This is useful for quitting all connected drivers on kill.
let connectedDrivers: Map<string, BrowserObject> = new Map();

export function SaucelabsLauncher(args, export function SaucelabsLauncher(args,
/* config.sauceLabs */ config, /* config.sauceLabs */ config,
/* SauceConnect */ sauceConnect, /* SauceConnect */ sauceConnect,
Expand All @@ -16,28 +19,20 @@ export function SaucelabsLauncher(args,
captureTimeoutLauncherDecorator(this); captureTimeoutLauncherDecorator(this);
retryLauncherDecorator(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 log = logger.create('SaucelabsLauncher');
const { const {
startConnect, startConnect,
sauceConnectOptions, sauceConnectOptions,
sauceApiProxy,
seleniumHostUrl,
seleniumCapabilities, seleniumCapabilities,
browserName, browserName
username,
accessKey
} = processConfig(config, args); } = processConfig(config, args);


// Array of connected drivers. This is useful for quitting all connected drivers on kill.
let connectedDrivers: WebDriver[] = [];

// Setup Browser name that will be printed out by Karma. // Setup Browser name that will be printed out by Karma.
this.name = browserName + ' on SauceLabs'; this.name = browserName + ' on SauceLabs';


const formatSauceError = (err) => {
return err.message + '\n' + (err.data ? ' ' + err.data : '')
}

// Listen for the start event from Karma. I know, the API is a bit different to how you // 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 // would expect, but we need to follow this approach unless we want to spend more work
// improving type safety. // improving type safety.
Expand All @@ -47,7 +42,7 @@ export function SaucelabsLauncher(args,
// In case the "startConnect" option has been enabled, establish a tunnel and wait // 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 // for it being ready. In case a tunnel is already active, this will just continue
// without establishing a new one. // without establishing a new one.
await sauceConnect.establishTunnel(sauceConnectOptions); await sauceConnect.establishTunnel(seleniumCapabilities, sauceConnectOptions);
} catch (error) { } catch (error) {
log.error(error); log.error(error);


Expand All @@ -59,25 +54,28 @@ export function SaucelabsLauncher(args,
try { try {
// See the following link for public API of the selenium server. // See the following link for public API of the selenium server.
// https://wiki.saucelabs.com/display/DOCS/Instant+Selenium+Node.js+Tests // https://wiki.saucelabs.com/display/DOCS/Instant+Selenium+Node.js+Tests
const driver = await new Builder() const driver = await remote(seleniumCapabilities);
.withCapabilities(seleniumCapabilities)
.usingServer(`http://${username}:${accessKey}@${seleniumHostUrl}`)
.build();


// Keep track of all connected drivers because it's possible that there are multiple // Keep track of all connected drivers because it's possible that there are multiple
// driver instances (e.g. when running with concurrency) // driver instances (e.g. when running with concurrency)
connectedDrivers.push(driver); connectedDrivers.set(this.id, driver);


const sessionId = (await driver.getSession()).getId(); const sessionId = driver.sessionId


log.info('%s session at https://saucelabs.com/tests/%s', browserName, sessionId); log.info('%s session at https://saucelabs.com/tests/%s', browserName, sessionId);
log.debug('Opening "%s" on the selenium client', pageUrl); log.debug('Opening "%s" on the selenium client', pageUrl);


// Store the information about the current session in the browserMap. This is necessary // Store the information about the current session in the browserMap. This is necessary
// because otherwise the Saucelabs reporter is not able to report results. // because otherwise the Saucelabs reporter is not able to report results.
browserMap.set(this.id, {sessionId, username, accessKey, proxy: sauceApiProxy}); browserMap.set(this.id, {

sessionId,
await driver.get(pageUrl); username: seleniumCapabilities.user,
accessKey: seleniumCapabilities.key,
region: seleniumCapabilities.region,
headless: seleniumCapabilities.headless
});

await driver.url(pageUrl);
} catch (e) { } catch (e) {
log.error(e); log.error(e);


Expand All @@ -86,9 +84,10 @@ export function SaucelabsLauncher(args,
} }
}); });


this.on('kill', async (doneFn: () => void) => { this.on('kill', async (done: () => void) => {
try { try {
await Promise.all(connectedDrivers.map(driver => driver.quit())); const driver = connectedDrivers.get(this.id);
await driver.deleteSession();
} catch (e) { } catch (e) {
// We need to ignore the exception here because we want to make sure that Karma is still // 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) // able to retry connecting if Saucelabs itself terminated the session (and not Karma)
Expand All @@ -98,9 +97,7 @@ export function SaucelabsLauncher(args,
log.error(e); log.error(e);
} }


// Reset connected drivers in case the launcher will be reused. connectedDrivers.delete(this.id)
connectedDrivers = []; return process.nextTick(done);

doneFn();
}) })
} }
38 changes: 20 additions & 18 deletions src/local-tunnel/sauceconnect.ts
Original file line number Original file line Diff line number Diff line change
@@ -1,7 +1,4 @@
import {promisify} from 'util'; import SaucelabsAPI, {SauceConnectInstance} from 'saucelabs';

// This import lacks type definitions.
const launchSauceConnect = promisify(require('sauce-connect-launcher'));


/** /**
* Service that can be used to create a SauceConnect tunnel automatically. This can be used * Service that can be used to create a SauceConnect tunnel automatically. This can be used
Expand All @@ -10,14 +7,11 @@ const launchSauceConnect = promisify(require('sauce-connect-launcher'));
export function SauceConnect(emitter, logger) { export function SauceConnect(emitter, logger) {
const log = logger.create('launcher.SauceConnect'); const log = logger.create('launcher.SauceConnect');


// Currently active tunnel instance. See: https://github.com/bermi/sauce-connect-launcher // Currently active tunnel instance. See: https://github.com/saucelabs/node-saucelabs
// for public API. // for public API.
let activeInstancePromise: Promise<any> = null; let activeInstancePromise: Promise<any> = null;


this.establishTunnel = async (connectOptions: any) => { this.establishTunnel = async (seleniumCapabilities, sauceConnectOptions: any) => {
// Redirect all logging output to Karma's logger.
connectOptions.logger = log.debug.bind(log);

// In case there is already a promise for a SauceConnect tunnel, we still need to return the // In case there is already a promise for a SauceConnect tunnel, we still need to return the
// promise because we want to make sure that the launcher can wait in case the tunnel is // promise because we want to make sure that the launcher can wait in case the tunnel is
// still starting. // still starting.
Expand All @@ -26,21 +20,29 @@ export function SauceConnect(emitter, logger) {
} }


// Open a new SauceConnect tunnel. // Open a new SauceConnect tunnel.
return activeInstancePromise = launchSauceConnect(connectOptions); const api = new SaucelabsAPI(seleniumCapabilities)
return activeInstancePromise = api.startSauceConnect({
// Redirect all logging output to Karma's logger.
logger: log.debug.bind(log),
...sauceConnectOptions
});
}; };


// Close the tunnel whenever Karma emits the "exit" event. In that case, we don't need to // Close the tunnel whenever Karma emits the "exit" event. In that case, we don't need to
// reset the state because Karma will exit completely. // reset the state because Karma will exit completely.
emitter.on('exit', (doneFn: () => void) => { emitter.on('exit', async (doneFn: () => void) => {
if (activeInstancePromise) { if (activeInstancePromise) {
log.debug('Shutting down Sauce Connect'); log.debug('Shutting down Sauce Connect');


// Close the tunnel and notify Karma once the tunnel has been exited. // shut down Sauce Connect once all session have been terminated
activeInstancePromise try {
.then(instance => instance.close(doneFn())) const tunnelInstance:SauceConnectInstance = await activeInstancePromise
.catch(() => doneFn()) await tunnelInstance.close()
} else { } catch (err) {
doneFn(); log.error(`Could not close Sauce Connect Tunnel. Failure message: ${err.stack}`);
}
} }

doneFn();
}) })
} }
49 changes: 29 additions & 20 deletions src/process-config.ts
Original file line number Original file line Diff line number Diff line change
@@ -1,14 +1,23 @@
import {bootstrap} from 'global-agent'

export function processConfig (config: any = {}, args: any = {}) { export function processConfig (config: any = {}, args: any = {}) {
const username = config.username || process.env.SAUCE_USERNAME; const username = config.username || process.env.SAUCE_USERNAME;
const accessKey = config.accessKey || process.env.SAUCE_ACCESS_KEY; const accessKey = config.accessKey || process.env.SAUCE_ACCESS_KEY;
const startConnect = config.startConnect !== false; const startConnect = config.startConnect !== false;


let tunnelIdentifier = args.tunnelIdentifier || config.tunnelIdentifier; let tunnelIdentifier = args.tunnelIdentifier || config.tunnelIdentifier;
let seleniumHostUrl = 'ondemand.saucelabs.com:80/wd/hub';


// TODO: This option is very ambiguous because it technically only affects the reporter. Consider // TODO: This option is very ambiguous because it technically only affects the reporter. Consider
// renaming in the future. // renaming in the future.
const sauceApiProxy = args.proxy || config.proxy; const sauceApiProxy = args.proxy || config.proxy;
if (sauceApiProxy) {
const envVar = sauceApiProxy.startsWith('https') ? 'KARMA_HTTPS_PROXY' : 'KARMA_HTTP_PROXY'
process.env[envVar] = sauceApiProxy
bootstrap({
environmentVariableNamespace: 'KARMA_',
forceGlobalAgent: false
})
}


// Browser name that will be printed out by Karma. // Browser name that will be printed out by Karma.
const browserName = args.browserName + const browserName = args.browserName +
Expand All @@ -22,12 +31,6 @@ export function processConfig (config: any = {}, args: any = {}) {
tunnelIdentifier = 'karma-sauce-' + Math.round(new Date().getTime() / 1000); tunnelIdentifier = 'karma-sauce-' + Math.round(new Date().getTime() / 1000);
} }


// Support passing a custom selenium location.
// TODO: This should be just an URL that can be passed. Holding off to avoid breaking changes.
if (config.connectLocationForSERelay) {
seleniumHostUrl = `${config.connectLocationForSERelay}:${config.connectPortForSERelay || 80}`;
}

const capabilitiesFromConfig = { const capabilitiesFromConfig = {
build: config.build, build: config.build,
commandTimeout: config.commandTimeout || 300, commandTimeout: config.commandTimeout || 300,
Expand All @@ -45,28 +48,34 @@ export function processConfig (config: any = {}, args: any = {}) {
}; };


const sauceConnectOptions = { const sauceConnectOptions = {
// By default, we just pass in the general Saucelabs credentials for establishing the
// SauceConnect tunnel. This makes it possible to use "startConnect" with no additional setup.
username: username,
accessKey: accessKey,
tunnelIdentifier: tunnelIdentifier, tunnelIdentifier: tunnelIdentifier,
...config.connectOptions, ...config.connectOptions,
}; };


// transform JWP capabilities into W3C capabilities for backward compatibility
args.browserVersion = args.browserVersion || args.version || 'latest'
args.platformName = args.platformName || args.platform || 'Windows 10'
// delete JWP capabilities
delete args.base
delete args.version
delete args.platform
const seleniumCapabilities = { const seleniumCapabilities = {
...capabilitiesFromConfig, user: username,
...config.options, key: accessKey,
...args, region: config.region,
headless: config.headless,
logLevel: 'error',
capabilities: {
'sauce:options': capabilitiesFromConfig,
...args
},
...config.options
}; };


return { return {
startConnect, startConnect,
sauceConnectOptions, sauceConnectOptions,
sauceApiProxy,
seleniumHostUrl,
seleniumCapabilities, seleniumCapabilities,
browserName, browserName
username,
accessKey,
} }
} }
Loading

0 comments on commit 7e75e17

Please sign in to comment.