Skip to content

Commit

Permalink
Implement the ability to bypass default flags.
Browse files Browse the repository at this point in the history
  • Loading branch information
samccone committed Jun 21, 2017
1 parent 1e6ec6f commit f8bb277
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 349 deletions.
34 changes: 24 additions & 10 deletions chrome-launcher/chrome-launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface Options {
handleSIGINT?: boolean;
chromePath?: string;
userDataDir?: string;
noDefaultFlags?: boolean;
}

export interface LaunchedChrome {
Expand All @@ -38,9 +39,14 @@ export interface LaunchedChrome {
kill: () => Promise<{}>;
}

export interface ModuleOverrides { fs?: typeof fs, rimraf?: typeof rimraf, }
export interface ModuleOverrides {
fs?: typeof fs;
rimraf?: typeof rimraf;
spawn?: typeof childProcess.spawn;
}

export async function launch(opts: Options = {}): Promise<LaunchedChrome> {
opts.noDefaultFlags = defaults(opts.noDefaultFlags, false);
opts.handleSIGINT = defaults(opts.handleSIGINT, true);

const instance = new Launcher(opts);
Expand Down Expand Up @@ -71,6 +77,7 @@ export class Launcher {
private chrome?: childProcess.ChildProcess;
private fs: typeof fs;
private rimraf: typeof rimraf;
private spawn: typeof childProcess.spawn;

userDataDir?: string;
port?: number;
Expand All @@ -79,6 +86,7 @@ export class Launcher {
constructor(private opts: Options = {}, moduleOverrides: ModuleOverrides = {}) {
this.fs = moduleOverrides.fs || fs;
this.rimraf = moduleOverrides.rimraf || rimraf;
this.spawn = moduleOverrides.spawn || spawn;

// choose the first one (default)
this.startingUrl = defaults(this.opts.startingUrl, 'about:blank');
Expand All @@ -88,18 +96,24 @@ export class Launcher {
}

private get flags() {
const flags = DEFAULT_FLAGS.concat([
`--remote-debugging-port=${this.port}`,
// Event if a user ignores default flags these flags are required to do a
// proper lifecycle cleanup as well as to drive chrome.
const requiredFlags = [
// Place Chrome profile in a custom location we'll rm -rf later
`--user-data-dir=${this.userDataDir}`
]);
`--remote-debugging-port=${this.port}`, `--user-data-dir=${this.userDataDir}`
];

if (this.opts.noDefaultFlags === true) {
return requiredFlags.concat([this.startingUrl]);
}

const flags = DEFAULT_FLAGS.concat(requiredFlags);

if (process.platform === 'linux') {
flags.push('--disable-setuid-sandbox');
}

flags.push(...this.chromeFlags);
flags.push(this.startingUrl);

return flags;
}
Expand Down Expand Up @@ -155,11 +169,11 @@ export class Launcher {
this.chromePath = installations[0];
}

this.pid = await this.spawn(this.chromePath);
this.pid = await this.spawnChromeProcess(this.chromePath);
return Promise.resolve();
}

private async spawn(execPath: string) {
private async spawnChromeProcess(execPath: string) {
// Typescript is losing track of the return type without the explict typing.
const spawnPromise: Promise<number> = new Promise(async (resolve) => {
if (this.chrome) {
Expand All @@ -176,7 +190,7 @@ export class Launcher {
this.port = await getRandomPort();
}

const chrome = spawn(
const chrome = this.spawn(
execPath, this.flags, {detached: true, stdio: ['ignore', this.outFile, this.errFile]});
this.chrome = chrome;

Expand Down Expand Up @@ -216,7 +230,7 @@ export class Launcher {
}

// resolves when debugger is ready, rejects after 10 polls
private waitUntilReady() {
waitUntilReady() {
const launcher = this;

return new Promise((resolve, reject) => {
Expand Down
2 changes: 2 additions & 0 deletions chrome-launcher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
},
"devDependencies": {
"@types/mocha": "^2.2.41",
"@types/sinon": "^2.3.1",
"clang-format": "^1.0.50",
"mocha": "^3.2.0",
"nyc": "^11.0.2",
"sinon": "^2.3.5",
"ts-node": "^3.0.4",
"typescript": "2.2.1"
},
Expand Down
78 changes: 55 additions & 23 deletions chrome-launcher/test/chrome-launcher-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,59 +6,95 @@
'use strict';

import {Launcher} from '../chrome-launcher';

import {spy, stub} from 'sinon';
import * as assert from 'assert';

const log = require('lighthouse-logger');
const fsMock = {
openSync: () => {},
closeSync: () => {}
closeSync: () => {},
writeFileSync: () => {}
};

describe('Launcher', () => {
it('accepts and uses a custom path', async () => {
beforeEach(() => {
log.setLevel('error');
let callCount = 0;
const rimrafMock = (_: string, done: () => void) => {
callCount++;
done();
};
});

afterEach(() => {
log.setLevel('');
});

it('sets no default flags', async () => {
const spawnStub = stub().returns({pid: 'some_pid'});

const chromeInstance = new Launcher(
{userDataDir: 'some_path', noDefaultFlags: true},
{fs: fsMock as any, rimraf: spy() as any, spawn: spawnStub as any});
stub(chromeInstance, 'waitUntilReady').returns(Promise.resolve());

chromeInstance.prepare();

try {
await chromeInstance.launch();
} catch (err) {
return Promise.reject(err);
}

assert.strictEqual(
(spawnStub.getCall(0).args[1] as string[]).indexOf('--disable-extensions') === -1, true);
});


it('sets default flags', async () => {
const spawnStub = stub().returns({pid: 'some_pid'});

const chromeInstance = new Launcher(
{userDataDir: 'some_path'},
{fs: fsMock as any, rimraf: spy() as any, spawn: spawnStub as any});
stub(chromeInstance, 'waitUntilReady').returns(Promise.resolve());

chromeInstance.prepare();

try {
await chromeInstance.launch();
} catch (err) {
return Promise.reject(err);
}

assert.strictEqual(
(spawnStub.getCall(0).args[1] as string[]).indexOf('--disable-extensions') !== -1, true);
});

it('accepts and uses a custom path', async () => {
const rimrafMock = spy();
const chromeInstance =
new Launcher({userDataDir: 'some_path'}, {fs: fsMock as any, rimraf: rimrafMock as any});

chromeInstance.prepare();

await chromeInstance.destroyTmp();
assert.strictEqual(callCount, 0);
assert.strictEqual(rimrafMock.callCount, 0);
});

it('cleans up the tmp dir after closing', async () => {
log.setLevel('error');
let callCount = 0;
const rimrafMock = (_: string, done: () => void) => {
callCount++;
done();
};
const rimrafMock = stub().callsFake((_, done) => done());

const chromeInstance = new Launcher({}, {fs: fsMock as any, rimraf: rimrafMock as any});

chromeInstance.prepare();
await chromeInstance.destroyTmp();
assert.strictEqual(callCount, 1);
assert.strictEqual(rimrafMock.callCount, 1);
});

it('does not delete created directory when custom path passed', () => {
log.setLevel('error');

const chromeInstance = new Launcher({userDataDir: 'some_path'}, {fs: fsMock as any});

chromeInstance.prepare();
assert.equal(chromeInstance.userDataDir, 'some_path');
});

it('defaults to genering a tmp dir when no data dir is passed', () => {
log.setLevel('error');
const chromeInstance = new Launcher({}, {fs: fsMock as any});
const originalMakeTmp = chromeInstance.makeTmpDir;
chromeInstance.makeTmpDir = () => 'tmp_dir'
Expand All @@ -70,21 +106,17 @@ describe('Launcher', () => {
});

it('doesn\'t fail when killed twice', async () => {
log.setLevel('error');
const chromeInstance = new Launcher();
await chromeInstance.launch();
log.setLevel();
await chromeInstance.kill();
await chromeInstance.kill();
});

it('doesn\'t launch multiple chrome processes', async () => {
log.setLevel('error');
const chromeInstance = new Launcher();
await chromeInstance.launch();
let pid = chromeInstance.pid!;
await chromeInstance.launch();
log.setLevel();
assert.strictEqual(pid, chromeInstance.pid);
await chromeInstance.kill();
});
Expand Down

0 comments on commit f8bb277

Please sign in to comment.