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 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).

### 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
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.

### 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
Type: `String`
Default: *One of the following environment variables*:
Expand Down Expand Up @@ -217,25 +214,21 @@ Required: `true`

Name of the browser.

### version
### browserVersion
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.

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

Name of platform to run browser on.

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

Accepted values: `'portrait' || 'landscape'`
### `sauce:options`

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

Expand Down
1 change: 0 additions & 1 deletion examples/karma.conf-ci.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ module.exports = function (config) {
testName: 'Karma and Sauce Labs demo',
recordScreenshots: false,
connectOptions: {
port: 5757,
logfile: 'sauce_connect.log'
},
public: 'public'
Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
],
"author": "Vojta Jina <vojta.jina@gmail.com>",
"dependencies": {
"sauce-connect-launcher": "^1.2.4",
"saucelabs": "^1.5.0",
"selenium-webdriver": "^4.0.0-alpha.1"
"global-agent": "^2.1.8",
"saucelabs": "^4.3.0",
"webdriverio": "^6.1.9"
},
"license": "MIT",
"devDependencies": {
Expand All @@ -49,7 +49,7 @@
"@semantic-release/git": "9.0.0",
"@semantic-release/npm": "7.0.4",
"@types/node": "^10.12.10",
"@types/selenium-webdriver": "^3.0.13",
"@types/global-agent": "^2.1.0",
"husky": "4.2.3",
"jasmine": "^3.3.0",
"karma": "^3.1.1",
Expand Down Expand Up @@ -88,6 +88,7 @@
"Parashuram <code@nparashuram.com>",
"Parashuram N <code@r.nparashuram.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 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
* when reporting the results to the Saucelabs web API.
*/
export interface SaucelabsBrowser {
export interface SaucelabsBrowser extends SauceBaseOption {
/** Saucelabs session id of this browser. */
sessionId: string;

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

/** Saucelabs access key that has been used to launch this browser. */
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. */
Expand Down
55 changes: 26 additions & 29 deletions src/launcher/launcher.ts
Original file line number 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 {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,
/* config.sauceLabs */ config,
/* SauceConnect */ sauceConnect,
Expand All @@ -16,28 +19,20 @@ export function SaucelabsLauncher(args,
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,
sauceApiProxy,
seleniumHostUrl,
seleniumCapabilities,
browserName,
username,
accessKey
browserName
} = 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.
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
// would expect, but we need to follow this approach unless we want to spend more work
// 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
// for it being ready. In case a tunnel is already active, this will just continue
// without establishing a new one.
await sauceConnect.establishTunnel(sauceConnectOptions);
await sauceConnect.establishTunnel(seleniumCapabilities, sauceConnectOptions);
} catch (error) {
log.error(error);

Expand All @@ -59,25 +54,28 @@ export function SaucelabsLauncher(args,
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 new Builder()
.withCapabilities(seleniumCapabilities)
.usingServer(`http://${username}:${accessKey}@${seleniumHostUrl}`)
.build();
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.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.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, accessKey, proxy: sauceApiProxy});

await driver.get(pageUrl);
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);

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

this.on('kill', async (doneFn: () => void) => {
this.on('kill', async (done: () => void) => {
try {
await Promise.all(connectedDrivers.map(driver => driver.quit()));
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)
Expand All @@ -98,9 +97,7 @@ export function SaucelabsLauncher(args,
log.error(e);
}

// Reset connected drivers in case the launcher will be reused.
connectedDrivers = [];

doneFn();
connectedDrivers.delete(this.id)
return process.nextTick(done);
})
}
38 changes: 20 additions & 18 deletions src/local-tunnel/sauceconnect.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {promisify} from 'util';

// This import lacks type definitions.
const launchSauceConnect = promisify(require('sauce-connect-launcher'));
import SaucelabsAPI, {SauceConnectInstance} from 'saucelabs';

/**
* 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) {
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.
let activeInstancePromise: Promise<any> = null;

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

this.establishTunnel = async (seleniumCapabilities, sauceConnectOptions: any) => {
// 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
// still starting.
Expand All @@ -26,21 +20,29 @@ export function SauceConnect(emitter, logger) {
}

// 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
// reset the state because Karma will exit completely.
emitter.on('exit', (doneFn: () => void) => {
emitter.on('exit', async (doneFn: () => void) => {
if (activeInstancePromise) {
log.debug('Shutting down Sauce Connect');

// Close the tunnel and notify Karma once the tunnel has been exited.
activeInstancePromise
.then(instance => instance.close(doneFn()))
.catch(() => doneFn())
} else {
doneFn();
// shut down Sauce Connect once all session have been terminated
try {
const tunnelInstance:SauceConnectInstance = await activeInstancePromise
await tunnelInstance.close()
} catch (err) {
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 Diff line number Diff line change
@@ -1,14 +1,23 @@
import {bootstrap} from 'global-agent'

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

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.
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.
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);
}

// 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 = {
build: config.build,
commandTimeout: config.commandTimeout || 300,
Expand All @@ -45,28 +48,34 @@ export function processConfig (config: any = {}, args: any = {}) {
};

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,
...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 = {
...capabilitiesFromConfig,
...config.options,
...args,
user: username,
key: accessKey,
region: config.region,
headless: config.headless,
logLevel: 'error',
capabilities: {
'sauce:options': capabilitiesFromConfig,
...args
},
...config.options
};

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

0 comments on commit 7e75e17

Please sign in to comment.