Skip to content

Commit 7e75e17

Browse files
fix: Update sauce lib (#207)
* fix(deps): update SauceLabs API binding * fix: start sauce connect via saucelabs package Co-authored-by: christian-bromann <[email protected]>
1 parent b1cf182 commit 7e75e17

File tree

9 files changed

+2363
-1075
lines changed

9 files changed

+2363
-1075
lines changed

README.md

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ Default: `process.env.SAUCE_ACCESS_KEY`
105105

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

108+
### region
109+
Type: `String`
110+
111+
Detect datacenter to run tests in. Can be either `eu` or `us`.
112+
113+
### headless
114+
Type: `Boolean`
115+
116+
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.
117+
108118
### proxy
109119
Type: `String`
110120

@@ -129,19 +139,6 @@ Default:
129139

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

132-
### connectLocationForSERelay
133-
Type: `String`
134-
default: `ondemand.saucelabs.com`
135-
136-
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.
137-
138-
### connectPortForSERelay
139-
Type: `Integer`
140-
Default: 80
141-
142-
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.
143-
144-
145142
### build
146143
Type: `String`
147144
Default: *One of the following environment variables*:
@@ -217,25 +214,21 @@ Required: `true`
217214

218215
Name of the browser.
219216

220-
### version
217+
### browserVersion
221218
Type: `String`
222-
Default: Latest browser version for all browsers except Chrome which defaults to `'27'`
219+
Default: Latest browser version for all browsers except Chrome
223220

224221
Version of the browser to use.
225222

226-
### platform
223+
### platformName
227224
Type: `String`
228225
Default: `'Linux'` for Firefox/Chrome, `'Windows 7'` for IE/Safari
229226

230227
Name of platform to run browser on.
231228

232-
### deviceOrientation
233-
Type: `String`
234-
Default: `'portrait'`
235-
236-
Accepted values: `'portrait' || 'landscape'`
229+
### `sauce:options`
237230

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

240233
## Behind the scenes
241234

examples/karma.conf-ci.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ module.exports = function (config) {
3838
testName: 'Karma and Sauce Labs demo',
3939
recordScreenshots: false,
4040
connectOptions: {
41-
port: 5757,
4241
logfile: 'sauce_connect.log'
4342
},
4443
public: 'public'

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@
3737
],
3838
"author": "Vojta Jina <[email protected]>",
3939
"dependencies": {
40-
"sauce-connect-launcher": "^1.2.4",
41-
"saucelabs": "^1.5.0",
42-
"selenium-webdriver": "^4.0.0-alpha.1"
40+
"global-agent": "^2.1.8",
41+
"saucelabs": "^4.3.0",
42+
"webdriverio": "^6.1.9"
4343
},
4444
"license": "MIT",
4545
"devDependencies": {
@@ -49,7 +49,7 @@
4949
"@semantic-release/git": "9.0.0",
5050
"@semantic-release/npm": "7.0.4",
5151
"@types/node": "^10.12.10",
52-
"@types/selenium-webdriver": "^3.0.13",
52+
"@types/global-agent": "^2.1.0",
5353
"husky": "4.2.3",
5454
"jasmine": "^3.3.0",
5555
"karma": "^3.1.1",
@@ -88,6 +88,7 @@
8888
"Parashuram <[email protected]>",
8989
"Parashuram N <[email protected]>",
9090
"Peter Johason <[email protected]>",
91-
"Paul Gschwendtner <[email protected]>"
91+
"Paul Gschwendtner <[email protected]>",
92+
"Christian Bromann <[email protected]>"
9293
]
9394
}

src/browser-info.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import {SauceLabsOptions} from 'saucelabs'
2+
3+
type SauceBaseOption = Pick<SauceLabsOptions, 'headless' | 'region'>
4+
15
/**
26
* This interface describes a browser that has been launched with Saucelabs. This is helpful
37
* when reporting the results to the Saucelabs web API.
48
*/
5-
export interface SaucelabsBrowser {
9+
export interface SaucelabsBrowser extends SauceBaseOption {
610
/** Saucelabs session id of this browser. */
711
sessionId: string;
812

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

1216
/** Saucelabs access key that has been used to launch this browser. */
1317
accessKey: string;
14-
15-
/** Proxy URL that will be used to make an API call to the Saucelabs API. */
16-
proxy: string;
1718
}
1819

1920
/** Type that describes the BrowserMap injection token. */

src/launcher/launcher.ts

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import {Builder, WebDriver} from 'selenium-webdriver';
1+
import {remote, BrowserObject} from 'webdriverio';
22
import {processConfig} from "../process-config";
33
import {BrowserMap} from "../browser-info";
44

5+
// Array of connected drivers. This is useful for quitting all connected drivers on kill.
6+
let connectedDrivers: Map<string, BrowserObject> = new Map();
7+
58
export function SaucelabsLauncher(args,
69
/* config.sauceLabs */ config,
710
/* SauceConnect */ sauceConnect,
@@ -16,28 +19,20 @@ export function SaucelabsLauncher(args,
1619
captureTimeoutLauncherDecorator(this);
1720
retryLauncherDecorator(this);
1821

22+
// initiate driver with null to not close the tunnel too early
23+
connectedDrivers.set(this.id, null)
24+
1925
const log = logger.create('SaucelabsLauncher');
2026
const {
2127
startConnect,
2228
sauceConnectOptions,
23-
sauceApiProxy,
24-
seleniumHostUrl,
2529
seleniumCapabilities,
26-
browserName,
27-
username,
28-
accessKey
30+
browserName
2931
} = processConfig(config, args);
3032

31-
// Array of connected drivers. This is useful for quitting all connected drivers on kill.
32-
let connectedDrivers: WebDriver[] = [];
33-
3433
// Setup Browser name that will be printed out by Karma.
3534
this.name = browserName + ' on SauceLabs';
3635

37-
const formatSauceError = (err) => {
38-
return err.message + '\n' + (err.data ? ' ' + err.data : '')
39-
}
40-
4136
// Listen for the start event from Karma. I know, the API is a bit different to how you
4237
// would expect, but we need to follow this approach unless we want to spend more work
4338
// improving type safety.
@@ -47,7 +42,7 @@ export function SaucelabsLauncher(args,
4742
// In case the "startConnect" option has been enabled, establish a tunnel and wait
4843
// for it being ready. In case a tunnel is already active, this will just continue
4944
// without establishing a new one.
50-
await sauceConnect.establishTunnel(sauceConnectOptions);
45+
await sauceConnect.establishTunnel(seleniumCapabilities, sauceConnectOptions);
5146
} catch (error) {
5247
log.error(error);
5348

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

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

71-
const sessionId = (await driver.getSession()).getId();
63+
const sessionId = driver.sessionId
7264

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

7668
// Store the information about the current session in the browserMap. This is necessary
7769
// because otherwise the Saucelabs reporter is not able to report results.
78-
browserMap.set(this.id, {sessionId, username, accessKey, proxy: sauceApiProxy});
79-
80-
await driver.get(pageUrl);
70+
browserMap.set(this.id, {
71+
sessionId,
72+
username: seleniumCapabilities.user,
73+
accessKey: seleniumCapabilities.key,
74+
region: seleniumCapabilities.region,
75+
headless: seleniumCapabilities.headless
76+
});
77+
78+
await driver.url(pageUrl);
8179
} catch (e) {
8280
log.error(e);
8381

@@ -86,9 +84,10 @@ export function SaucelabsLauncher(args,
8684
}
8785
});
8886

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

101-
// Reset connected drivers in case the launcher will be reused.
102-
connectedDrivers = [];
103-
104-
doneFn();
100+
connectedDrivers.delete(this.id)
101+
return process.nextTick(done);
105102
})
106103
}

src/local-tunnel/sauceconnect.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import {promisify} from 'util';
2-
3-
// This import lacks type definitions.
4-
const launchSauceConnect = promisify(require('sauce-connect-launcher'));
1+
import SaucelabsAPI, {SauceConnectInstance} from 'saucelabs';
52

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

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

17-
this.establishTunnel = async (connectOptions: any) => {
18-
// Redirect all logging output to Karma's logger.
19-
connectOptions.logger = log.debug.bind(log);
20-
14+
this.establishTunnel = async (seleniumCapabilities, sauceConnectOptions: any) => {
2115
// In case there is already a promise for a SauceConnect tunnel, we still need to return the
2216
// promise because we want to make sure that the launcher can wait in case the tunnel is
2317
// still starting.
@@ -26,21 +20,29 @@ export function SauceConnect(emitter, logger) {
2620
}
2721

2822
// Open a new SauceConnect tunnel.
29-
return activeInstancePromise = launchSauceConnect(connectOptions);
23+
const api = new SaucelabsAPI(seleniumCapabilities)
24+
return activeInstancePromise = api.startSauceConnect({
25+
// Redirect all logging output to Karma's logger.
26+
logger: log.debug.bind(log),
27+
...sauceConnectOptions
28+
});
3029
};
3130

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

38-
// Close the tunnel and notify Karma once the tunnel has been exited.
39-
activeInstancePromise
40-
.then(instance => instance.close(doneFn()))
41-
.catch(() => doneFn())
42-
} else {
43-
doneFn();
37+
// shut down Sauce Connect once all session have been terminated
38+
try {
39+
const tunnelInstance:SauceConnectInstance = await activeInstancePromise
40+
await tunnelInstance.close()
41+
} catch (err) {
42+
log.error(`Could not close Sauce Connect Tunnel. Failure message: ${err.stack}`);
43+
}
4444
}
45+
46+
doneFn();
4547
})
46-
}
48+
}

src/process-config.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1+
import {bootstrap} from 'global-agent'
2+
13
export function processConfig (config: any = {}, args: any = {}) {
24
const username = config.username || process.env.SAUCE_USERNAME;
35
const accessKey = config.accessKey || process.env.SAUCE_ACCESS_KEY;
46
const startConnect = config.startConnect !== false;
57

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

9-
// TODO: This option is very ambiguous because it technically only affects the reporter. Consider
10+
// TODO: This option is very ambiguous because it technically only affects the reporter. Consider
1011
// renaming in the future.
1112
const sauceApiProxy = args.proxy || config.proxy;
13+
if (sauceApiProxy) {
14+
const envVar = sauceApiProxy.startsWith('https') ? 'KARMA_HTTPS_PROXY' : 'KARMA_HTTP_PROXY'
15+
process.env[envVar] = sauceApiProxy
16+
bootstrap({
17+
environmentVariableNamespace: 'KARMA_',
18+
forceGlobalAgent: false
19+
})
20+
}
1221

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

25-
// Support passing a custom selenium location.
26-
// TODO: This should be just an URL that can be passed. Holding off to avoid breaking changes.
27-
if (config.connectLocationForSERelay) {
28-
seleniumHostUrl = `${config.connectLocationForSERelay}:${config.connectPortForSERelay || 80}`;
29-
}
30-
3134
const capabilitiesFromConfig = {
3235
build: config.build,
3336
commandTimeout: config.commandTimeout || 300,
@@ -45,28 +48,34 @@ export function processConfig (config: any = {}, args: any = {}) {
4548
};
4649

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

55+
// transform JWP capabilities into W3C capabilities for backward compatibility
56+
args.browserVersion = args.browserVersion || args.version || 'latest'
57+
args.platformName = args.platformName || args.platform || 'Windows 10'
58+
// delete JWP capabilities
59+
delete args.base
60+
delete args.version
61+
delete args.platform
5662
const seleniumCapabilities = {
57-
...capabilitiesFromConfig,
58-
...config.options,
59-
...args,
63+
user: username,
64+
key: accessKey,
65+
region: config.region,
66+
headless: config.headless,
67+
logLevel: 'error',
68+
capabilities: {
69+
'sauce:options': capabilitiesFromConfig,
70+
...args
71+
},
72+
...config.options
6073
};
6174

6275
return {
6376
startConnect,
6477
sauceConnectOptions,
65-
sauceApiProxy,
66-
seleniumHostUrl,
6778
seleniumCapabilities,
68-
browserName,
69-
username,
70-
accessKey,
79+
browserName
7180
}
7281
}

0 commit comments

Comments
 (0)