Skip to content

Commit 06aee79

Browse files
committed
feat: add waitForInspectableTarget option (fix GoogleChrome#145)
1 parent c4c72c6 commit 06aee79

File tree

5 files changed

+186
-2
lines changed

5 files changed

+186
-2
lines changed

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ npm install chrome-launcher
7777
// Default: 50
7878
maxConnectionRetries: number;
7979

80+
// (optional) Interval in ms, which defines whether and how long an inspectable target should be awaited.
81+
// `0` means that list of inspectable targets will not be requested and awaited.
82+
// Default: 0
83+
waitForInspectableTarget: number;
84+
8085
// (optional) A dict of environmental key value pairs to pass to the spawned chrome process.
8186
envVars: {[key: string]: string};
8287
};

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"is-wsl": "^2.1.0",
2626
"lighthouse-logger": "^1.0.0",
2727
"mkdirp": "0.5.1",
28+
"phin": "^3.4.1",
29+
"povtor": "^1.1.0",
2830
"rimraf": "^2.6.1"
2931
},
3032
"version": "0.13.0",

src/chrome-launcher.ts

+40-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import * as childProcess from 'child_process';
99
import * as fs from 'fs';
1010
import * as net from 'net';
11+
import * as phin from 'phin';
12+
import {retry} from 'povtor';
1113
import * as rimraf from 'rimraf';
1214
import * as chromeFinder from './chrome-finder';
1315
import {getRandomPort} from './random-port';
@@ -40,6 +42,7 @@ export interface Options {
4042
ignoreDefaultFlags?: boolean;
4143
connectionPollInterval?: number;
4244
maxConnectionRetries?: number;
45+
waitForInspectableTarget?: number;
4346
envVars?: {[key: string]: string|undefined};
4447
}
4548

@@ -112,6 +115,7 @@ class Launcher {
112115
private requestedPort?: number;
113116
private connectionPollInterval: number;
114117
private maxConnectionRetries: number;
118+
private waitForInspectableTarget: number;
115119
private fs: typeof fs;
116120
private rimraf: RimrafModule;
117121
private spawn: typeof childProcess.spawn;
@@ -122,6 +126,7 @@ class Launcher {
122126
userDataDir?: string;
123127
port?: number;
124128
pid?: number;
129+
getTargetRetryTimeout: number = 500;
125130

126131
constructor(private opts: Options = {}, moduleOverrides: ModuleOverrides = {}) {
127132
this.fs = moduleOverrides.fs || fs;
@@ -138,6 +143,7 @@ class Launcher {
138143
this.ignoreDefaultFlags = defaults(this.opts.ignoreDefaultFlags, false);
139144
this.connectionPollInterval = defaults(this.opts.connectionPollInterval, 500);
140145
this.maxConnectionRetries = defaults(this.opts.maxConnectionRetries, 50);
146+
this.waitForInspectableTarget = defaults(this.opts.waitForInspectableTarget, 0);
141147
this.envVars = defaults(opts.envVars, Object.assign({}, process.env));
142148

143149
if (typeof this.opts.userDataDir === 'boolean') {
@@ -278,8 +284,26 @@ class Launcher {
278284
}
279285
}
280286

287+
getTargetList() {
288+
return phin({
289+
url: `http://127.0.0.1:${this.port}/json/list`,
290+
parse: 'json'
291+
});
292+
}
293+
294+
waitForTarget() {
295+
return retry({
296+
action: this.getTargetList,
297+
actionContext: this,
298+
retryOnError: true,
299+
retryTest: (response: phin.IResponse) => !response || !Array.isArray(response.body) || !response.body.length,
300+
retryTimeout: this.getTargetRetryTimeout,
301+
timeLimit: this.waitForInspectableTarget
302+
}).promise;
303+
}
304+
281305
// resolves if ready, rejects otherwise
282-
private isDebuggerReady(): Promise<{}> {
306+
isDebuggerReady(): Promise<{}> {
283307
return new Promise((resolve, reject) => {
284308
const client = net.createConnection(this.port!);
285309
client.once('error', err => {
@@ -312,7 +336,21 @@ class Launcher {
312336
launcher.isDebuggerReady()
313337
.then(() => {
314338
log.log('ChromeLauncher', waitStatus + `${log.greenify(log.tick)}`);
315-
resolve();
339+
if (launcher.waitForInspectableTarget > 0) {
340+
log.log('ChromeLauncher', 'Waiting for an inspectable target...');
341+
launcher.waitForTarget()
342+
.then((response: phin.IResponse) => {
343+
log.log('ChromeLauncher', 'Received target list: %O', response.body);
344+
resolve(response.body);
345+
})
346+
.catch((reason: unknown) => {
347+
log.error('ChromeLauncher', `Cannot get target list. Reason: ${reason}`);
348+
reject(reason);
349+
});
350+
}
351+
else {
352+
resolve();
353+
}
316354
})
317355
.catch(err => {
318356
if (retries > launcher.maxConnectionRetries) {

test/chrome-launcher-test.ts

+122
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,126 @@ describe('Launcher', () => {
183183
const chromeInstance = new Launcher({chromePath: ''});
184184
chromeInstance.launch().catch(() => done());
185185
});
186+
187+
describe('waitForTarget method', () => {
188+
function getChromeInstance(targetList: unknown, waitForInspectableTarget?: number) {
189+
const chromeInstance = new Launcher({waitForInspectableTarget: typeof waitForInspectableTarget === 'number' ? waitForInspectableTarget : 100});
190+
const getTargetListStub = stub(chromeInstance, 'getTargetList').returns(Promise.resolve(targetList));
191+
192+
return {
193+
chromeInstance,
194+
getTargetListStub
195+
};
196+
}
197+
198+
it('returns promise that is resolved with the same value as promise returned by getTargetList method', () => {
199+
const response = {
200+
body: ['test', 'list']
201+
};
202+
const {chromeInstance, getTargetListStub} = getChromeInstance(response);
203+
204+
return chromeInstance.waitForTarget().then((result) => {
205+
assert.ok(getTargetListStub.calledOnce);
206+
assert.strictEqual(result, response);
207+
});
208+
});
209+
210+
it('returns promise that is resolved with the same value as promise returned by getTargetList method after interval specified in waitForInspectableTarget option', () => {
211+
const response = {
212+
body: []
213+
};
214+
const waitTime = 90;
215+
const {chromeInstance, getTargetListStub} = getChromeInstance(response, waitTime);
216+
chromeInstance.getTargetRetryTimeout = 50;
217+
const startTime = new Date().getTime();
218+
219+
return chromeInstance.waitForTarget().then((result) => {
220+
assert.ok(getTargetListStub.callCount === 3);
221+
assert.ok(new Date().getTime() - startTime > waitTime);
222+
assert.strictEqual(result, response);
223+
});
224+
});
225+
226+
it('returns promise that is rejected with the same value as promise returned by getTargetList method after interval specified in waitForInspectableTarget option', () => {
227+
const reason = 'No target';
228+
const waitTime = 100;
229+
const {chromeInstance, getTargetListStub} = getChromeInstance(Promise.reject(reason), waitTime);
230+
chromeInstance.getTargetRetryTimeout = 40;
231+
const startTime = new Date().getTime();
232+
233+
return chromeInstance.waitForTarget().catch((result) => {
234+
assert.ok(getTargetListStub.callCount === 4);
235+
assert.ok(new Date().getTime() - startTime > waitTime);
236+
assert.strictEqual(result, reason);
237+
});
238+
});
239+
});
240+
241+
describe('waitForInspectableTarget option', () => {
242+
function getChromeInstance(options?: Options) {
243+
const chromeInstance = new Launcher(options);
244+
stub(chromeInstance, 'isDebuggerReady').returns(Promise.resolve());
245+
246+
return chromeInstance;
247+
}
248+
249+
it('waitUntilReady does not call waitForTarget method when the option is not set', () => {
250+
const chromeInstance = getChromeInstance();
251+
const waitForTargetSpy = spy(chromeInstance, 'waitForTarget');
252+
253+
return chromeInstance.waitUntilReady().then(() => {
254+
assert.ok(waitForTargetSpy.notCalled);
255+
});
256+
});
257+
258+
it('waitUntilReady does not call waitForTarget method when 0 is set for the option', () => {
259+
const chromeInstance = getChromeInstance({waitForInspectableTarget: 0});
260+
const waitForTargetSpy = spy(chromeInstance, 'waitForTarget');
261+
262+
return chromeInstance.waitUntilReady().then(() => {
263+
assert.ok(waitForTargetSpy.notCalled);
264+
});
265+
});
266+
267+
it('waitUntilReady does not call waitForTarget method when negative value is set for the option', () => {
268+
const chromeInstance = getChromeInstance({waitForInspectableTarget: -1});
269+
const waitForTargetSpy = spy(chromeInstance, 'waitForTarget');
270+
271+
return chromeInstance.waitUntilReady().then(() => {
272+
assert.ok(waitForTargetSpy.notCalled);
273+
});
274+
});
275+
276+
it('waitUntilReady calls waitForTarget method when the option is set', () => {
277+
const chromeInstance = getChromeInstance({waitForInspectableTarget: 1000});
278+
const response = {
279+
body: [{
280+
description: '',
281+
devtoolsFrontendUrl: '/devtools/inspector.html?ws=127.0.0.1:54321/devtools/page/1C2C62A45591F2DECB9CC50E7C3B1FA5',
282+
id: '1C2C62A45591F2DECB9CC50E7C3B1FA5',
283+
title: '',
284+
type: 'page',
285+
url: 'about:blank',
286+
webSocketDebuggerUrl: 'ws://127.0.0.1:54321/devtools/page/1C2C62A45591F2DECB9CC50E7C3B1FA5'
287+
}]
288+
};
289+
const waitForTargetStub = stub(chromeInstance, 'waitForTarget').returns(Promise.resolve(response));
290+
291+
return chromeInstance.waitUntilReady().then((result) => {
292+
assert.ok(waitForTargetStub.calledOnce);
293+
assert.deepEqual(result, response.body);
294+
});
295+
});
296+
297+
it('waitUntilReady rejects when waitForTarget method returns rejected promise', () => {
298+
const chromeInstance = getChromeInstance({waitForInspectableTarget: 1});
299+
const reason = 'No targets';
300+
const waitForTargetStub = stub(chromeInstance, 'waitForTarget').returns(Promise.reject(reason));
301+
302+
return chromeInstance.waitUntilReady().catch((result) => {
303+
assert.ok(waitForTargetStub.calledOnce);
304+
assert.strictEqual(result, reason);
305+
});
306+
});
307+
});
186308
});

yarn.lock

+17
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ camelcase@^5.0.0:
8787
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
8888
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
8989

90+
centra@^2.2.1:
91+
version "2.4.0"
92+
resolved "https://registry.yarnpkg.com/centra/-/centra-2.4.0.tgz#53846f97db27705e9f90c46e0f824f6eb697e2d1"
93+
integrity sha512-AWmF3EHNe/noJHviynZOrdnUuQzT5AMgl9nJPXGvnzGXrI2ZvNDrEcdqskc4EtQwt2Q1IggXb0OXy7zZ1Xvvew==
94+
9095
chalk@^2.0.1:
9196
version "2.4.2"
9297
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@@ -518,6 +523,18 @@ path-to-regexp@^1.7.0:
518523
dependencies:
519524
isarray "0.0.1"
520525

526+
phin@^3.4.1:
527+
version "3.4.1"
528+
resolved "https://registry.yarnpkg.com/phin/-/phin-3.4.1.tgz#b023d14fa86fc6e4b40b6d0dfd5fe478c9c1bbb8"
529+
integrity sha512-NkBCNRPxeyrgaPlWx4DHTAdca3s2LkvIBiiG6RoSbykcOtW/pA/7rUP/67FPIinvbo7h+HENST/vJ17LdRNUdw==
530+
dependencies:
531+
centra "^2.2.1"
532+
533+
povtor@^1.1.0:
534+
version "1.1.0"
535+
resolved "https://registry.yarnpkg.com/povtor/-/povtor-1.1.0.tgz#bebe6618c0bcd0df55bd9f6dd2bebebb4d15c5a5"
536+
integrity sha512-gUhd8L9iC4rSipLzx3mCInjusheig56wDrQLiwi5DH5FuumXJE0fEtvZNuheDqjXgMxARLoCz2erqOaa6Trgiw==
537+
521538
require-directory@^2.1.1:
522539
version "2.1.1"
523540
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"

0 commit comments

Comments
 (0)