-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathindex.js
379 lines (316 loc) · 9.68 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
'use strict';
const { spawn } = require('./cp');
require('@faltest/utils/src/require-before-webdriverio');
const { remote } = require('webdriverio');
const psList = require('ps-list');
const fkill = require('fkill');
const debug = require('./debug');
const log = require('./log');
const EventEmitter = require('events');
const yn = require('yn');
const {
defaults,
event: { emit },
} = require('@faltest/utils');
// const config = require('config');
// We aren't using `@wdio/cli` (wdio testrunner)
process.env.SUPPRESS_NO_CONFIG_WARNING = 'true';
const ChromeDriverName = 'chromedriver';
const FirefoxDriverName = 'geckodriver';
let port;
const webDriverRegex = /^(chromedriver(?:\.exe)?|geckodriver(?:\.exe)?)$/;
const browserNameRegex = (() => {
switch (process.platform) {
case 'linux': return /^(chrome|firefox)$/;
case 'darwin': return /^(Google Chrome|firefox-bin)$/;
case 'win32': return /^(chrome\.exe|firefox\.exe)$/;
default: throw new Error(`Platform "${process.platform}" not supported`);
}
})();
const browserCmdRegex = (() => {
let chrome = '--enable-automation';
let firefox = '-marionette';
return new RegExp(`(${chrome}|${firefox})`);
})();
function getDefaults() {
// let browser = config.get('browser') || defaults.browser;
// let headless = yn(config.get('headless'));
let browser = process.env.WEBDRIVER_BROWSER || defaults.browser;
let headless = yn(process.env.WEBDRIVER_HEADLESS);
let logLevel = debug.verbose.enabled ? 'trace' : debug.enabled ? 'warn' : 'silent';
return {
browser,
headless,
logLevel,
};
}
function browserSwitch(name, {
chrome,
firefox,
}) {
switch (name) {
case 'chrome': return chrome();
case 'firefox': return firefox();
default: throw new Error(`Browser "${name}" not implemented.`);
}
}
function driverSwitch(name, {
chrome,
firefox,
}) {
switch (name) {
case ChromeDriverName: return chrome();
case FirefoxDriverName: return firefox();
default: throw new Error(`Driver "${name}" not implemented.`);
}
}
let events = new EventEmitter();
async function killOrphans() {
await emit(events, 'kill-orphans-start');
let processes = await module.exports.psList();
let orphanedWebDrivers = processes.filter(({ name, pid, ppid }) => {
debug.verbose(`found process ${name} pid ${pid} ppid ${ppid}`);
return webDriverRegex.test(name);
});
let orphanedBrowsers = processes.filter(({ name, cmd, ppid }) => {
if (!browserNameRegex.test(name)) {
return;
}
let isAboutToBeOrphaned = orphanedWebDrivers.some(({ pid }) => pid === ppid);
if (isAboutToBeOrphaned) {
return true;
}
// This kills any other webdriver browsers that may be in use.
// Checking the ppid is inconsistent across OSes.
let isAlreadyOrphaned = browserCmdRegex.test(cmd);
if (isAlreadyOrphaned) {
return true;
}
});
async function kill(orphan) {
debug('killing orphan');
debug(`${orphan.cmd} ${orphan.pid}`);
try {
await module.exports.fkill(orphan.pid, {
force: process.platform === 'win32',
});
} catch (err) {
if (/(Process doesn't exist|No such process)/.test(err.message)) {
debug(err.message);
} else {
// Sometimes you get "Killing process 30602 failed"
// and there doesn't seem to be anything you can do about it?
// throw err;
debug(err.message);
}
}
}
for (let orphan of orphanedBrowsers.concat(orphanedWebDrivers)) {
await kill(orphan);
}
await emit(events, 'kill-orphans-end');
}
async function getNewPort(port) {
if (!port || port === '0') {
const { default: getPort } = await import('get-port');
port = (await getPort()).toString();
debug(`using random open port ${port}`);
} else {
debug(`using port ${port}`);
}
return port;
}
async function _getNewPort(_port) {
if (!_port) {
// _port = config.get('port');
_port = process.env.WEBDRIVER_PORT;
}
return port = await getNewPort(_port);
}
async function spawnWebDriver(name, args) {
await module.exports.spawn(name, ['--version']);
let webDriver = module.exports.spawn(name, args, {
stdio: ['ignore', 'pipe', 'ignore'],
});
async function waitForText(text) {
await new Promise(resolve => {
function wait(data) {
if (data.toString().includes(text)) {
resolve();
webDriver.stdout.removeListener('data', wait);
}
}
webDriver.stdout.on('data', wait);
});
}
await driverSwitch(name, {
async chrome() {
await waitForText('ChromeDriver was started successfully.');
},
async firefox() {
await waitForText('Listening on 127.0.0.1');
},
});
// There's a flaw with the logic in https://github.com/IndigoUnited/node-cross-spawn/issues/16.
// If you mark `shell: true`, then it skips validating that the file exists.
// Then when we force kill the process, it's error handling logic kicks in
// and says, "Oh the file doesn't exist? Then throw a ENOENT error."
if (process.platform === 'win32') {
let { emit } = webDriver;
webDriver.emit = function(eventName, exitCode) {
if (eventName === 'exit' && exitCode === 1) {
return true;
}
return emit.apply(webDriver, arguments);
};
}
// https://github.com/sindresorhus/execa/issues/173
delete webDriver.then;
delete webDriver.catch;
return webDriver;
}
function startWebDriver(options = {}) {
return log(async () => {
if (!yn(process.env.WEBDRIVER_DISABLE_CLEANUP)) {
await module.exports.killOrphans();
}
let { overrides = {} } = options;
let _browser = overrides.browser || getDefaults().browser;
await module.exports._getNewPort(overrides.port);
let driverName;
let driverArgs;
browserSwitch(_browser, {
chrome() {
driverName = ChromeDriverName;
driverArgs = [`--port=${port}`];
},
firefox() {
driverName = FirefoxDriverName;
driverArgs = ['--port', port];
},
});
let webDriver = await module.exports.spawnWebDriver(driverName, driverArgs);
if (!yn(process.env.WEBDRIVER_DISABLE_CLEANUP)) {
webDriver.once('exit', module.exports.killOrphans);
}
return webDriver;
});
}
function stopWebDriver(webDriver) {
return log(async () => {
if (!webDriver) {
return;
}
if (!yn(process.env.WEBDRIVER_DISABLE_CLEANUP)) {
webDriver.removeListener('exit', killOrphans);
}
webDriver.kill();
await new Promise(resolve => {
webDriver.once('exit', resolve);
});
});
}
async function getCapabilities({
customizeCapabilities = (browserName, capabilities) => capabilities,
overrides: {
browser: _browser = getDefaults().browser,
} = {},
}) {
let capabilities = {
browserName: _browser,
};
let headless = getDefaults().headless;
let browserCapabilities;
browserSwitch(_browser, {
chrome() {
let args = [];
if (headless) {
args.push('--headless');
}
browserCapabilities = {
args,
};
capabilities['goog:chromeOptions'] = browserCapabilities;
},
firefox() {
let args = [];
if (headless) {
args.push('-headless');
}
browserCapabilities = {
args,
};
capabilities['moz:firefoxOptions'] = browserCapabilities;
},
});
await customizeCapabilities(_browser, browserCapabilities);
return capabilities;
}
function startBrowser(options = {}) {
return log(async () => {
let { overrides = {} } = options;
let browser;
let connectionRetryCount = 150;
try {
// We should refrain from using the `baseUrl` option here
// because subdomain can change as you navigate your app
browser = await remote({
logLevel: overrides.logLevel || getDefaults().logLevel,
path: '/',
port: parseInt(port),
capabilities: await getCapabilities(options),
waitforTimeout: overrides.waitforTimeout !== undefined ? overrides.waitforTimeout : defaults.timeout,
// this is only needed for geckodriver because
// it takes a while to boot up and doesn't notify
// you via console output
// `connectionRetryTimeout` seems like it would be a better option,
// but the connections fail immediately and the timeout isn't even hit
connectionRetryCount,
});
} catch (err) {
debug(`It is possible \`connectionRetryCount=${connectionRetryCount}\` is not high enough.`);
throw err;
}
await resizeBrowser(browser, overrides.size);
return browser;
});
}
async function resizeBrowser(browser, size) {
let _size = process.env.WEBDRIVER_SIZE;
if (size !== undefined) {
_size = size;
}
if (_size) {
let [width, height] = _size.split(',').map(s => parseInt(s));
await browser.setWindowSize(width, height);
}
debug('browser.getWindowSize');
debug(await browser.getWindowSize());
}
function stopBrowser(browser) {
return log(async () => {
if (!browser) {
return;
}
if (browser.close) {
// We should probably remove the browser wrapper expectance
// as it violates separation of concerns.
await browser.close();
} else {
await browser.closeWindow();
}
});
}
module.exports.psList = psList;
module.exports.fkill = fkill;
module.exports.killOrphans = killOrphans;
module.exports.getNewPort = getNewPort;
module.exports.startWebDriver = startWebDriver;
module.exports.stopWebDriver = stopWebDriver;
module.exports.startBrowser = startBrowser;
module.exports.stopBrowser = stopBrowser;
module.exports.resizeBrowser = resizeBrowser;
module.exports.events = events;
module.exports._getNewPort = _getNewPort;
module.exports.spawnWebDriver = spawnWebDriver;
module.exports.spawn = spawn;