headless-test
Version:
Run Mocha tests in a headless browser
120 lines (101 loc) • 3.87 kB
JavaScript
/*
* Copyright 2018 Sam Thorogood. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
;
const puppeteer = require('puppeteer');
const util = require('util');
const runServer = require('./lib/server.js');
const buildHarness = require('./lib/harness.js');
function urlFromInput(raw) {
if (raw instanceof URL || typeof raw === 'string') {
return new URL(raw);
}
const addr = raw.address();
const addressPart = addr.family === 'IPv6' ? `[${addr.address}]` : addr.address;
return new URL(`http://${addressPart}:${addr.port}`);
}
/**
* @param {!http.Server|!https.Server|!URL|string} server
* @param {!Object=} options
*/
module.exports = async function(server, options) {
options = Object.assign({
args: [],
headless: true,
load: [],
virtual: [],
driver: {},
done: null,
// no implicit timeout
}, options);
const doneHandler = options.done;
delete options.done; // not serializable
const args = options.args.slice();
const cleanup = [];
// It's valid but weird to pass a null URL. Create a dummy server that doesn't point to anything.
if (server == null) {
server = await runServer();
cleanup.push(() => server.close());
}
// If we're passed a real http[s].Server, get its address. Otherwise, treat it as a URL.
const url = urlFromInput(server);
const testNonce = `__test=${Math.floor(Math.random() * 0xffffffff).toString(16)}`;
if (process.env.CI || process.env.TRAVIS) {
args.push('--no-sandbox', '--disable-setuid-sandbox');
}
const p = (async function runner() {
const browser = await puppeteer.launch({headless: options.headless, args});
cleanup.push(async () => {
await browser.disconnect();
await browser.close();
});
const pages = await browser.pages();
const page = pages[0] || await browser.newPage();
cleanup.push(() => page.close());
let resolveCompleted;
const completedPromise = new Promise((resolve, reject) => {
resolveCompleted = resolve;
page.on('pageerror', reject);
});
page.on('error', (err) => console.warn('got page err', err));
// Log all output but ensure it runs in the right order, as each message technically includes
// data we need to await (jsonValue below).
let consolePromise = Promise.resolve();
page.on('console', (msg) => {
consolePromise = consolePromise.then(async () => {
const args = await Promise.all(msg._args.map((arg) => arg.jsonValue()));
const out = util.format(...args);
process.stdout.write(out + '\n');
});
});
page.on('dialog', (dialog) => dialog.dismiss());
// We can't pass a function through to the evaluate function, as it's not Seralizable.
await page.exposeFunction('__headlessDone', resolveCompleted);
await page.evaluateOnNewDocument(require('./lib/preload.js'), options);
await page.setRequestInterception(true);
const handler = await buildHarness(url, testNonce);
page.on('request', handler);
await page.goto((new URL(`./${testNonce}`, url)).toString());
return completedPromise;
})();
try {
return await p; // we're an async function so this will still be a Promise
} finally {
doneHandler && await doneHandler();
while (cleanup.length !== 0) {
await cleanup.pop()();
}
}
};