Adding firefox to test suite (#17195)

* Created general driver to accept an argument select the driver to run.

* Added environment variable for driver, changed package.json to use custim leadfoot, and made changes to browserdriver to use any driver.

* Made changes per PR.

* Fixed all nits.
This commit is contained in:
John Dorlus 2018-03-16 23:05:10 -04:00 committed by GitHub
parent dc0ce9e2b9
commit a31ec148f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 213 additions and 106 deletions

View file

@ -242,6 +242,7 @@
"event-stream": "3.3.2",
"expect.js": "0.3.1",
"faker": "1.1.0",
"geckodriver": "1.10.0",
"getopts": "2.0.0",
"grunt": "1.0.1",
"grunt-aws-s3": "0.14.5",
@ -269,7 +270,7 @@
"karma-junit-reporter": "1.2.0",
"karma-mocha": "1.3.0",
"karma-safari-launcher": "1.0.0",
"leadfoot": "1.7.1",
"leadfoot": "silne30/leadfoot#validation_for_response_in_ff58",
"license-checker": "^16.0.0",
"load-grunt-config": "0.19.2",
"makelogs": "^4.0.4",

View file

@ -94,6 +94,11 @@ export const schema = Joi.object().keys({
url: Joi.string().uri({ scheme: /https?/ }).default('http://localhost:9515')
}).default(),
firefoxdriver: Joi.object().keys({
url: Joi.string().uri({ scheme: /https?/ }).default('http://localhost:2828')
}).default(),
// definition of apps that work with `common.navigateToApp()`
apps: Joi.object().pattern(
ID_PATTERN,

View file

@ -1,21 +1,21 @@
import { EventEmitter } from 'events';
import { createLocalChromedriverApi } from './chromedriver_local_api';
import { createRemoteChromedriverApi } from './chromedriver_remote_api';
import { createLocalBrowserDriverApi } from './browser_driver_local_api';
import { createRemoteBrowserDriverApi } from './browser_driver_remote_api';
import { ping } from './ping';
const noop = () => {};
/**
* Api for interacting with a local or remote instance of Chromedriver
* Api for interacting with a local or remote instance of a browser
*
* @type {Object}
*/
export class ChromedriverApi extends EventEmitter {
static async factory(log, url) {
export class BrowserDriverApi extends EventEmitter {
static async factory(log, url, browserType) {
return (await ping(url))
? createRemoteChromedriverApi(log, url)
: createLocalChromedriverApi(log, url);
? createRemoteBrowserDriverApi(log, url)
: createLocalBrowserDriverApi(log, url, browserType);
}
constructor(options = {}) {
@ -25,19 +25,22 @@ export class ChromedriverApi extends EventEmitter {
url,
start = noop,
stop = noop,
requiredCapabilities,
} = options;
if (!url) {
throw new TypeError('url is a required parameter');
}
this._requiredCapabilities = requiredCapabilities;
this._url = url;
this._state = undefined;
this._callCustomStart = () => start(this);
this._callCustomStop = () => stop(this);
this._beforeStopFns = [];
}
getRequiredCapabilities() {
return this._requiredCapabilities;
}
getUrl() {
return this._url;
}
@ -52,7 +55,7 @@ export class ChromedriverApi extends EventEmitter {
async start() {
if (this._state !== undefined) {
throw new Error('Chromedriver can only be started once');
throw new Error('Driver can only be started once');
}
this._state = 'started';
@ -61,7 +64,7 @@ export class ChromedriverApi extends EventEmitter {
async stop() {
if (this._state !== 'started') {
throw new Error('Chromedriver can only be stopped after being started');
throw new Error('Driver can only be stopped after being started');
}
this._state = 'stopped';

View file

@ -0,0 +1,75 @@
import { spawn } from 'child_process';
import { parse as parseUrl } from 'url';
import treeKill from 'tree-kill';
import { delay, fromNode as fcb } from 'bluebird';
import { path as CHROMEDRIVER_EXEC } from 'chromedriver';
import { path as FIREFOXDRIVER_EXEC } from 'geckodriver';
import { ping } from './ping';
import { BrowserDriverApi } from './browser_driver_api';
const START_TIMEOUT = 15000;
const PING_INTERVAL = 500;
export function createLocalBrowserDriverApi(log, url, browser) {
let runningDriver = null;
const driverName = browser + 'driver';
switch (browser) {
case 'firefox':
runningDriver = FIREFOXDRIVER_EXEC;
break;
default:
runningDriver = CHROMEDRIVER_EXEC;
}
let proc = null;
return new BrowserDriverApi({
url,
requiredCapabilities: Object.create({ browserType: browser }),
async start(api) {
const { port } = parseUrl(url);
log.debug('Starting local ' + driverName + ' at port %d', port);
proc = spawn(runningDriver, [`--port=${port}`], {
stdio: ['ignore', 'pipe', 'pipe'],
});
proc.stdout.on('data', chunk => {
log.debug('[' + driverName + ':stdout]', chunk.toString('utf8').trim());
});
proc.stderr.on('data', chunk => {
log.debug('[' + driverName + ':stderr]', chunk.toString('utf8').trim());
});
proc.on('exit', (code) => {
if (!api.isStopped() || code > 0) {
api.emit('error', new Error(driverName + ` exited with code ${code}`));
}
});
const pingsStartedAt = Date.now();
while (true) {
log.debug('[' + driverName + ':ping] attempting to reach at %j', url);
if (await ping(url)) {
log.debug('[' + driverName + ':ping] success');
break;
} else {
log.debug('[' + driverName + ':ping] failure');
}
if ((Date.now() - pingsStartedAt) < START_TIMEOUT) {
log.debug('[' + driverName + ':ping] waiting for %d before next ping', PING_INTERVAL);
await delay(PING_INTERVAL);
continue;
}
throw new Error(driverName + ` did not start within the ${START_TIMEOUT}ms timeout`);
}
},
async stop() {
await fcb(cb => treeKill(proc.pid, undefined, cb));
}
});
}

View file

@ -0,0 +1,12 @@
import { BrowserDriverApi } from './browser_driver_api';
export function createRemoteBrowserDriverApi(log, url) {
return new BrowserDriverApi({
url,
start() {
log.info(`Reusing instance at %j`, url);
}
});
}

View file

@ -0,0 +1 @@
export { BrowserDriverApi } from './browser_driver_api';

View file

@ -1,64 +0,0 @@
import { spawn } from 'child_process';
import { parse as parseUrl } from 'url';
import treeKill from 'tree-kill';
import { delay, fromNode as fcb } from 'bluebird';
import { path as CHROMEDRIVER_EXEC } from 'chromedriver';
import { ping } from './ping';
import { ChromedriverApi } from './chromedriver_api';
const START_TIMEOUT = 15000;
const PING_INTERVAL = 500;
export function createLocalChromedriverApi(log, url) {
let proc = null;
return new ChromedriverApi({
url,
async start(api) {
const { port } = parseUrl(url);
log.info('Starting local chromedriver at port %d', port);
proc = spawn(CHROMEDRIVER_EXEC, [`--port=${port}`], {
stdio: ['ignore', 'pipe', 'pipe'],
});
proc.stdout.on('data', chunk => {
log.debug('[chromedriver:stdout]', chunk.toString('utf8').trim());
});
proc.stderr.on('data', chunk => {
log.debug('[chromedriver:stderr]', chunk.toString('utf8').trim());
});
proc.on('exit', (code) => {
if (!api.isStopped() || code > 0) {
api.emit('error', new Error(`Chromedriver exited with code ${code}`));
}
});
const pingsStartedAt = Date.now();
while (true) {
log.debug('[chromedriver:ping] attempting to reach chromedriver at %j', url);
if (await ping(url)) {
log.debug('[chromedriver:ping] success');
break;
} else {
log.debug('[chromedriver:ping] failure');
}
if ((Date.now() - pingsStartedAt) < START_TIMEOUT) {
log.debug('[chromedriver:ping] waiting for %d before next ping', PING_INTERVAL);
await delay(PING_INTERVAL);
continue;
}
throw new Error(`Chromedriver did not start within the ${START_TIMEOUT}ms timeout`);
}
},
async stop() {
await fcb(cb => treeKill(proc.pid, undefined, cb));
}
});
}

View file

@ -1,12 +0,0 @@
import { ChromedriverApi } from './chromedriver_api';
export function createRemoteChromedriverApi(log, url) {
return new ChromedriverApi({
url,
start() {
log.info(`Reusing instance at %j`, url);
}
});
}

View file

@ -1 +0,0 @@
export { ChromedriverApi } from './chromedriver_api';

View file

@ -8,15 +8,14 @@ const SECOND = 1000;
const MINUTE = 60 * SECOND;
let attemptCounter = 0;
async function attemptToCreateCommand(log, server, chromedriverApi) {
async function attemptToCreateCommand(log, server, driverApi) {
const attemptId = ++attemptCounter;
log.debug('[leadfoot:command] Creating session');
const session = await server.createSession({ browserName: 'chrome' });
const session = await server.createSession({}, driverApi.getRequiredCapabilities());
if (attemptId !== attemptCounter) return; // abort
log.debug('[leadfoot:command] Registerying session for teardown');
chromedriverApi.beforeStop(async () => session.quit());
driverApi.beforeStop(async () => session.quit());
if (attemptId !== attemptCounter) return; // abort
log.debug('[leadfoot:command] Completing session capabilities');
@ -29,7 +28,7 @@ async function attemptToCreateCommand(log, server, chromedriverApi) {
return { command: new Command(session) };
}
export async function initLeadfootCommand({ log, chromedriverApi }) {
export async function initLeadfootCommand({ log, browserDriverApi }) {
return await Promise.race([
(async () => {
await delay(2 * MINUTE);
@ -41,7 +40,7 @@ export async function initLeadfootCommand({ log, chromedriverApi }) {
// backend (chromedriver in this case). it helps with session management
// and all communication to the remote browser go through it, so we shim
// some of it's methods to enable very verbose logging.
const server = initVerboseRemoteLogging(log, new Server(chromedriverApi.getUrl()));
const server = initVerboseRemoteLogging(log, new Server(browserDriverApi.getUrl()));
// by default, calling server.createSession() automatically fixes the webdriver
// "capabilities" hash so that leadfoot knows the hoops it has to jump through
@ -59,7 +58,7 @@ export async function initLeadfootCommand({ log, chromedriverApi }) {
while (true) {
const command = await Promise.race([
delay(30 * SECOND),
attemptToCreateCommand(log, server, chromedriverApi)
attemptToCreateCommand(log, server, browserDriverApi)
]);
if (!command) {

View file

@ -1,18 +1,24 @@
import { initLeadfootCommand } from './leadfoot_command';
import { createRemoteInterceptors } from './interceptors';
import { ChromedriverApi } from './chromedriver_api';
import { BrowserDriverApi } from './browser_driver_api';
export async function RemoteProvider({ getService }) {
const lifecycle = getService('lifecycle');
const config = getService('config');
const log = getService('log');
const possibleBrowsers = ['chrome', 'firefox'];
const browserType = process.env.TEST_BROWSER_TYPE || 'chrome';
const chromedriverApi = await ChromedriverApi.factory(log, config.get('chromedriver.url'));
lifecycle.on('cleanup', async () => await chromedriverApi.stop());
if (!possibleBrowsers.includes(browserType)) {
throw new Error(`Unexpected TEST_BROWSER_TYPE "${browserType}". Valid options are ` + possibleBrowsers.join(','));
}
await chromedriverApi.start();
const browserDriverApi = await BrowserDriverApi.factory(log, config.get(browserType + 'driver.url'), browserType);
lifecycle.on('cleanup', async () => await browserDriverApi.stop());
const { command } = await initLeadfootCommand({ log, chromedriverApi });
await browserDriverApi.start();
const { command } = await initLeadfootCommand({ log, browserDriverApi: browserDriverApi });
const interceptors = createRemoteInterceptors(command);
log.info('Remote initialized');

View file

@ -246,6 +246,10 @@ address@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9"
adm-zip@0.4.7:
version "0.4.7"
resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.7.tgz#8606c2cbf1c426ce8c8ec00174447fd49b6eafc1"
after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
@ -1460,6 +1464,10 @@ bluebird@2.9.34:
version "2.9.34"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.9.34.tgz#2f7b4ec80216328a9fddebdf69c8d4942feff7d8"
bluebird@3.4.6:
version "3.4.6"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.6.tgz#01da8d821d87813d158967e743d5fe6c62cf8c0f"
bluebird@3.5.1, bluebird@^3.3.0:
version "3.5.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
@ -2025,6 +2033,10 @@ chokidar@^1.4.1, chokidar@^1.6.0, chokidar@^1.7.0:
optionalDependencies:
fsevents "^1.0.0"
chownr@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
chromedriver@2.33.2:
version "2.33.2"
resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-2.33.2.tgz#8fc779d54b6e45bef55d264a1eceed52427a9b49"
@ -2493,7 +2505,7 @@ create-ecdh@^4.0.0:
bn.js "^4.1.0"
elliptic "^6.0.0"
create-error-class@^3.0.0:
create-error-class@^3.0.0, create-error-class@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
dependencies:
@ -3277,6 +3289,12 @@ dtrace-provider@~0.6:
dependencies:
nan "^2.0.8"
duplexer2@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
dependencies:
readable-stream "^2.0.2"
duplexer3@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
@ -4516,6 +4534,15 @@ gauge@~2.7.3:
strip-ansi "^3.0.1"
wide-align "^1.1.0"
geckodriver@1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-1.10.0.tgz#73e2f785666521d0d3a9ddc9fd5a0a5e3bf47845"
dependencies:
adm-zip "0.4.7"
bluebird "3.4.6"
got "5.6.0"
tar "4.0.2"
generate-function@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74"
@ -4753,6 +4780,27 @@ good-squeeze@2.1.0:
hoek "2.x.x"
json-stringify-safe "5.0.x"
got@5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/got/-/got-5.6.0.tgz#bb1d7ee163b78082bbc8eb836f3f395004ea6fbf"
dependencies:
create-error-class "^3.0.1"
duplexer2 "^0.1.4"
is-plain-obj "^1.0.0"
is-redirect "^1.0.0"
is-retry-allowed "^1.0.0"
is-stream "^1.0.0"
lowercase-keys "^1.0.0"
node-status-codes "^1.0.0"
object-assign "^4.0.1"
parse-json "^2.1.0"
pinkie-promise "^2.0.0"
read-all-stream "^3.0.0"
readable-stream "^2.0.5"
timed-out "^2.0.0"
unzip-response "^1.0.0"
url-parse-lax "^1.0.0"
got@^3.2.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca"
@ -6818,9 +6866,9 @@ lcid@^1.0.0:
dependencies:
invert-kv "^1.0.0"
leadfoot@1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/leadfoot/-/leadfoot-1.7.1.tgz#7dae9961f01c9bec862b621ccece0826c8c64599"
leadfoot@silne30/leadfoot#validation_for_response_in_ff58:
version "1.7.4"
resolved "https://codeload.github.com/silne30/leadfoot/tar.gz/460c8ed67c08177adc9c79243ff045880f4cad09"
dependencies:
dojo "2.0.0-alpha.7"
jszip "2.5.0"
@ -7580,6 +7628,18 @@ minimost@^1.0.0:
camelcase-keys "^4.0.0"
minimist "^1.2.0"
minipass@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.1.tgz#5ada97538b1027b4cf7213432428578cb564011f"
dependencies:
yallist "^3.0.0"
minizlib@^1.0.4:
version "1.1.0"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
dependencies:
minipass "^2.2.1"
mkdirp@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12"
@ -7838,6 +7898,10 @@ node-pre-gyp@^0.6.39:
tar "^2.2.1"
tar-pack "^3.4.0"
node-status-codes@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f"
node-uuid@~1.4.0:
version "1.4.8"
resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907"
@ -8238,7 +8302,7 @@ parse-headers@^2.0.0:
for-each "^0.3.2"
trim "0.0.1"
parse-json@^2.2.0:
parse-json@^2.1.0, parse-json@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
dependencies:
@ -10598,6 +10662,16 @@ tar@2.2.0:
fstream "^1.0.2"
inherits "2"
tar@4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.0.2.tgz#e8e22bf3eec330e5c616d415a698395e294e8fad"
dependencies:
chownr "^1.0.1"
minipass "^2.2.1"
minizlib "^1.0.4"
mkdirp "^0.5.0"
yallist "^3.0.2"
tar@^2.2.1, "tar@~2.2.1 ":
version "2.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
@ -11038,6 +11112,10 @@ unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
unzip-response@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe"
unzip-response@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
@ -11822,6 +11900,10 @@ yallist@^2.0.0, yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
yallist@^3.0.0, yallist@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"
yargs-parser@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"