Skip to content

Commit

Permalink
Support any remote selenium grid
Browse files Browse the repository at this point in the history
Merges #32 from @bicarbon8
  • Loading branch information
bicarbon8 authored Jun 14, 2023
1 parent 1e7a982 commit 4e0b212
Show file tree
Hide file tree
Showing 9 changed files with 460 additions and 94 deletions.
66 changes: 65 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,71 @@ To run the specs:
2. Run `npx jasmine-browser-runner`.
3. Visit <http://localhost:8888>.

## Saucelabs support
## Remote Grid support (Saucelabs, BrowserStack, etc.)

jasmine-browser-runner can run your Jasmine specs on a remote grid
provider like [Saucelabs](https://saucelabs.com/),
[BrowserStack](https://browserstack.com) or your own Selenium Grid.
To use a remote grid hub, set the `browser` object
in your config file as follows:

```json
// jasmine-browser.json
{
// ...
// BrowserStack
"browser": {
"name": "safari",
"useRemoteSeleniumGrid": true,
"remoteSeleniumGrid": {
"url": "https://hub-cloud.browserstack.com/wd/hub",
"bstack:options": {
"browserVersion": "13",
"os": "OS X",
"osVersion": "Snow Leopard",
"local": "true",
"localIdentifier": "tunnel ID",
"debug": "true",
"userName": "your BrowserStack username",
"accessKey": "your BrowserStack access key"
}
}
}
}
```
```json
// jasmine-browser.json
{
// ...
// Saucelabs
"browser": {
"name": "safari",
"useRemoteSeleniumGrid": true,
"remoteSeleniumGrid": {
"url": "https://ondemand.saucelabs.com/wd/hub",
"platformName": "macOS 12",
"sauce:options": {
"tunnel-identifier": "tunnel ID",
"userName": "your Saucelabs username",
"accessKey": "your Saucelabs access key"
}
}
}
}
```

When using a remote grid provider, all properties of the `browser` object are
optional except for `name` which will be passed as the `browserName` capability,
and `useRemoteSeleniumGrid` which must be set to a value of `true`. if a
`remoteSeleniumGrid` object is included, any values it contains, with the
exception of the `url` will be used as `capabilties` sent to the grid hub url.
if no value is specified for the `url` then a default of
`http://localhost:4445/wd/hub` is used.

## Saucelabs support (legacy)
> NOTE: the below configuration format only supports using Saucelabs in the US. if connecting from the EU, please use the above specifying a `url` value specific to your region (e.g. `https://ondemand.eu-central-1.saucelabs.com:443/wd/hub`) to avoid a connection error of `WebDriverError: This user is unauthorized to the region. Please try another region, or contact customer support.`
> WARNING: the below configuration format may be removed in favour of using the above in the future so it is advised that you migrate to the above
jasmine-browser-runner can run your Jasmine specs on [Saucelabs](https://saucelabs.com/).
To use Saucelabs, set `browser.name`, `browser.useSauce`, and `browser.sauce`
Expand Down
7 changes: 5 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,14 @@ module.exports = {

const reporters = await createReporters(options, deps);
const useSauce = options.browser && options.browser.useSauce;
const useRemote = options.browser && options.browser.useRemoteSeleniumGrid;
let portRequest;

if (useSauce) {
if (useSauce || useRemote) {
if (options.port) {
throw new Error("Can't specify a port when browser.useSauce is true");
throw new Error(
"Can't specify a port when browser.useSauce or browser.useRemoteSeleniumGrid is true"
);
}

portRequest = 5555;
Expand Down
56 changes: 39 additions & 17 deletions lib/webdriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ function buildWebdriver(browserInfo, webdriverBuilder) {

webdriverBuilder = webdriverBuilder || new webdriver.Builder();
const useSauce = typeof browserInfo === 'object' && browserInfo.useSauce;
const useRemote =
typeof browserInfo === 'object' && browserInfo.useRemoteSeleniumGrid;
let browserName;

if (typeof browserInfo === 'string') {
Expand All @@ -14,7 +16,7 @@ function buildWebdriver(browserInfo, webdriverBuilder) {

browserName = browserName || 'firefox';

if (!useSauce) {
if (!(useRemote || useSauce)) {
if (browserName === 'headlessChrome') {
const caps = webdriver.Capabilities.chrome();
caps.set('goog:chromeOptions', {
Expand Down Expand Up @@ -44,26 +46,46 @@ function buildWebdriver(browserInfo, webdriverBuilder) {
}
}

const sauce = browserInfo.sauce;
const capabilities = {
[Capability.BROWSER_NAME]: browserName,
build: sauce.build,
tags: sauce.tags,
};
let url;
let capabilities;
if (useRemote) {
const remote = browserInfo.remoteSeleniumGrid;
if (remote) {
url = remote.url;
capabilities = {
...remote,
[Capability.BROWSER_NAME]: browserName,
};
delete capabilities.url;
}
} else if (useSauce) {
// handle legacy `sauce` object
const sauce = browserInfo.sauce;
if (sauce) {
url = `http://${sauce.username}:${sauce.accessKey}@ondemand.saucelabs.com/wd/hub`;
capabilities = {
[Capability.BROWSER_NAME]: browserName,
build: sauce.build,
tags: sauce.tags,
};

capabilities[Capability.PLATFORM_NAME] = sauce.os;
capabilities[Capability.BROWSER_VERSION] = sauce.browserVersion;
capabilities['sauce:options'] = {
'tunnel-identifier': sauce.tunnelIdentifier,
};
capabilities[Capability.PLATFORM_NAME] = sauce.os;
capabilities[Capability.BROWSER_VERSION] = sauce.browserVersion;
capabilities['sauce:options'] = {
'tunnel-identifier': sauce.tunnelIdentifier,
};
}
}

if (!capabilities) {
capabilities = {
[Capability.BROWSER_NAME]: browserName,
};
}

return webdriverBuilder
.withCapabilities(capabilities)
.usingServer(
browserInfo.useSauce
? `http://${sauce.username}:${sauce.accessKey}@ondemand.saucelabs.com/wd/hub`
: 'http://@localhost:4445/wd/hub'
)
.usingServer(url || 'http://localhost:4445/wd/hub')
.build();
}

Expand Down
25 changes: 25 additions & 0 deletions spec/fixtures/remoteGridIntegration/jasmine-browser.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"srcDir": ".",
"srcFiles": [],
"specDir": ".",
"specFiles": [
"remoteGridParamsSpec.js"
],
"helpers": [],
"random": true,
"browser": {
"name": "<< JASMINE_BROWSER >>",
"useRemoteSeleniumGrid": true,
"remoteSeleniumGrid": {
"url": "https://ondemand.saucelabs.com/wd/hub",
"browserVersion": "<< SAUCE_BROWSER_VERSION >>",
"platformName": "<< SAUCE_OS >>",
"sauce:options": {
"tags": ["jasmine-browser"],
"tunnel-identifier": "<< SAUCE_TUNNEL_IDENTIFIER >>",
"username": "<< SAUCE_USERNAME >>",
"accessKey": "<< SAUCE_ACCESS_KEY >>"
}
}
}
}
6 changes: 6 additions & 0 deletions spec/fixtures/remoteGridIntegration/remoteGridParamsSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

describe('remote grid parameter handling', function() {
it('was run in the expected browser', function() {
expect(navigator.userAgent.toString()).toMatch(<< EXPECTED >>);
});
});
2 changes: 1 addition & 1 deletion spec/fixtures/sauceIntegration/jasmine-browser.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
"accessKey": "<< SAUCE_ACCESS_KEY >>"
}
}
}
}
2 changes: 1 addition & 1 deletion spec/indexSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('index', function() {
);

await expectAsync(promise).toBeRejectedWithError(
"Can't specify a port when browser.useSauce is true"
"Can't specify a port when browser.useSauce or browser.useRemoteSeleniumGrid is true"
);
expect(this.server.start).not.toHaveBeenCalled();
});
Expand Down
148 changes: 148 additions & 0 deletions spec/remoteGridIntegrationSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
const fs = require('fs');
const os = require('os');
const { exec } = require('child_process');

describe('remote grid parameter handling', function() {
// To reduce the amount of output that devs have to scroll past, pend a single
// spec and don't create the rest if Sauce isn't available.
if (
!(
process.env.USE_SAUCE &&
process.env.SAUCE_USERNAME &&
process.env.SAUCE_ACCESS_KEY
)
) {
it('passes params to Saucelabs correctly', function() {
pending(
"Can't run remote grid integration tests unless USE_SAUCE, SAUCE_USERNAME, and SAUCE_ACCESS_KEY are set"
);
});
return;
}

// These specs use browser+version+OS combos that are supported by Saucelabs.
// When more than one OS is supported, we use the older one to make sure that
// the OS parameter is actually being passed correctly. (Saucelabs generally
// defaults to the newest OS that has the requested browser version if the OS
// is not specified.)

createSpec('firefox', '', '', /Gecko\/[0-9]+ Firefox\/[0-9.]+$/);
createSpec(
'firefox',
'102',
'Windows 10',
/Windows NT 10.0;.* Firefox\/102\.0$/
);
createSpec('chrome', '', '', /\(KHTML, like Gecko\) Chrome\/[0-9]+[0-9.]+/);
createSpec(
'safari',
'15',
'macOS 12',
// Safari on 12.x reports the OS as 10_15_7
/Mac OS X 10_15.*Version\/15[0-9.]+ Safari/
);
createSpec(
'safari',
'16',
'macOS 12',
// Safari on 12.x reports the OS as 10_15_7
/Mac OS X 10_15.*Version\/16[0-9.]+ Safari/
);
createSpec(
'MicrosoftEdge',
'',
'Windows 10',
/Windows NT 10\.0.*Edg\/[0-9]+\.[0-9.]+$/
);

function createSpec(browser, version, sauceOS, expectedUserAgentRegex) {
const displayVersion = version
? `version ${version}`
: 'unspecified version';
const displayOS = sauceOS ? `OS ${sauceOS}` : 'unspecified OS';
it(
`passes browser ${browser}, ${displayVersion}, and ${displayOS} correctly`,
function(done) {
const suiteDir = createSuite();
const jasmineBrowserDir = process.cwd();
let timedOut = false;
let timerId;
console.log('remote grid test may take a minute or two');

const jasmineBrowserProcess = exec(
`"${jasmineBrowserDir}/bin/jasmine-browser-runner" runSpecs --config=jasmine-browser.json`,
{ cwd: suiteDir },
function(err, stdout, stderr) {
try {
if (timedOut) {
return;
}

clearTimeout(timerId);

if (!err) {
expect(stdout).toContain('1 spec, 0 failures');
done();
} else {
if (err.code !== 1 || stdout === '' || stderr !== '') {
// Some kind of unexpected failure happened. Include all the info
// that we have.
done.fail(
`Child suite failed with error:\n${err}\n\n` +
`stdout:\n${stdout}\n\n` +
`stderr:\n${stderr}`
);
} else {
// A normal suite failure. Just include the output.
done.fail(`Child suite failed with output:\n${stdout}`);
}
}
} catch (e) {
done.fail(e);
}
}
);

timerId = setTimeout(function() {
// Kill the child processs if we're about to time out, to free up
// the port.
timedOut = true;
jasmineBrowserProcess.kill();
}, 239 * 1000);
},
240 * 1000
);

function createSuite() {
const dir = fs.mkdtempSync(`${os.tmpdir()}/jasmine-browser-remote-grid-`);
processTemplate(
'spec/fixtures/remoteGridIntegration/jasmine-browser.json',
`${dir}/jasmine-browser.json`,
{
JASMINE_BROWSER: browser,
SAUCE_BROWSER_VERSION: version,
SAUCE_OS: sauceOS,
SAUCE_TUNNEL_IDENTIFIER: process.env.SAUCE_TUNNEL_IDENTIFIER || '',
SAUCE_USERNAME: process.env.SAUCE_USERNAME,
SAUCE_ACCESS_KEY: process.env.SAUCE_ACCESS_KEY,
}
);
processTemplate(
'spec/fixtures/remoteGridIntegration/remoteGridParamsSpec.js',
`${dir}/remoteGridParamsSpec.js`,
{ EXPECTED: expectedUserAgentRegex }
);
return dir;
}

function processTemplate(inPath, outPath, vars) {
const template = fs.readFileSync(inPath, { encoding: 'utf8' });

const output = Object.keys(vars).reduce(function(prev, k) {
return prev.replace(`<< ${k} >>`, vars[k]);
}, template);

fs.writeFileSync(outPath, output);
}
}
});
Loading

0 comments on commit 4e0b212

Please sign in to comment.