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

Update sauce lib #207

Merged
merged 4 commits into from
May 26, 2020
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
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
}
}
Loading