web-component-tester
Version:
web-component-tester makes testing your web components a breeze!
322 lines (315 loc) • 15.8 kB
JavaScript
;
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const bowerConfig = require("bower-config");
const cleankill = require("cleankill");
const fs = require("fs");
const _ = require("lodash");
const path = require("path");
const polyserve_1 = require("polyserve");
const resolve = require("resolve");
const semver = require("semver");
const send = require("send");
const serverDestroy = require("server-destroy");
const config_1 = require("./config");
// Template for generated indexes.
const INDEX_TEMPLATE = _.template(fs.readFileSync(path.resolve(__dirname, '../data/index.html'), { encoding: 'utf-8' }));
const DEFAULT_HEADERS = {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
};
function relativeFrom(fromPath, toPath) {
return path.relative(fromPath, toPath).replace(/\\/g, '/');
}
function resolveFrom(fromPath, moduleId) {
try {
return resolve.sync(moduleId, { basedir: fromPath, preserveSymlinks: true });
}
catch (e) {
return '';
}
}
/**
* The webserver module is a quasi-plugin. This ensures that it is hooked in a
* sane way (for other plugins), and just follows the same flow.
*
* It provides a static HTTP server for serving the desired tests and WCT's
* `browser.js`/`environment.js`.
*/
function webserver(wct) {
const options = wct.options;
wct.hook('configure', function () {
return __awaiter(this, void 0, void 0, function* () {
// For now, you should treat all these options as an implementation detail
// of WCT. They may be opened up for public configuration, but we need to
// spend some time rationalizing interactions with external webservers.
options.webserver = _.merge(options.webserver, {});
if (options.verbose) {
options.clientOptions.verbose = true;
}
// Hacky workaround for Firefox + Windows issue where FF screws up pathing.
// Bug: https://github.com/Polymer/web-component-tester/issues/194
options.suites = options.suites.map((cv) => cv.replace(/\\/g, '/'));
// The generated index needs the correct "browser.js" script. When using
// npm, the wct-browser-legacy package may be used, so we test for that
// package and will use its "browser.js" if present.
let browserScript = 'web-component-tester/browser.js';
const scripts = [], extraScripts = [];
const modules = [], extraModules = [];
if (options.npm) {
options.clientOptions = options.clientOptions || {};
options.clientOptions.environmentScripts =
options.clientOptions.environmentScripts || [];
browserScript = '';
const fromPath = path.resolve(options.root || process.cwd());
options.wctPackageName = options.wctPackageName ||
['wct-mocha', 'wct-browser-legacy', 'web-component-tester'].find((p) => !!resolveFrom(fromPath, p));
const npmPackageRootPath = path.dirname(resolveFrom(fromPath, options.wctPackageName + '/package.json'));
if (npmPackageRootPath) {
const wctPackageScriptName = ['web-component-tester', 'wct-browser-legacy'].includes(options.wctPackageName) ?
'browser.js' :
`${options.wctPackageName}.js`;
browserScript = `${npmPackageRootPath}/${wctPackageScriptName}`.slice(npmPackageRootPath.length - options.wctPackageName.length);
}
const packageName = config_1.getPackageName(options);
const isPackageScoped = packageName && packageName[0] === '@';
const rootNodeModules = path.resolve(path.join(options.root, 'node_modules'));
// WCT used to try to bundle a lot of packages for end-users, but
// because of `node_modules` layout, these need to actually be resolved
// from the package as installed, to ensure the desired version is
// loaded. Here we list the legacy packages and attempt to resolve them
// from the WCT package.
if (['web-component-tester', 'wct-browser-legacy'].includes(options.wctPackageName)) {
const legacyNpmSupportPackageScripts = [
'stacky/browser.js',
'async/lib/async.js',
'lodash/index.js',
'mocha/mocha.js',
'chai/chai.js',
'@polymer/sinonjs/sinon.js',
'sinon-chai/lib/sinon-chai.js',
'accessibility-developer-tools/dist/js/axs_testing.js',
'@polymer/test-fixture/test-fixture.js',
];
const resolvedLegacyNpmSupportPackageScripts = legacyNpmSupportPackageScripts
.map((script) => resolveFrom(npmPackageRootPath, script))
.filter((script) => script !== '');
options.clientOptions.environmentScripts.push(...resolvedLegacyNpmSupportPackageScripts.map((script) => relativeFrom(rootNodeModules, script)));
}
else {
// We need to load Mocha in the generated index.
const resolvedMochaScript = resolveFrom(npmPackageRootPath, 'mocha/mocha.js');
if (resolvedMochaScript) {
options.clientOptions.environmentScripts.push(relativeFrom(rootNodeModules, resolvedMochaScript));
}
}
if (browserScript && isPackageScoped) {
browserScript = `../${browserScript}`;
}
}
if (browserScript) {
scripts.push(`../${browserScript}`);
}
if (!options.npm) {
scripts.push('web-component-tester/data/a11ysuite.js');
}
options.webserver._generatedIndexContent =
INDEX_TEMPLATE(Object.assign({ scripts, extraScripts: [], modules }, options));
});
});
wct.hook('prepare', function () {
return __awaiter(this, void 0, void 0, function* () {
const wsOptions = options.webserver;
const additionalRoutes = new Map();
const packageName = config_1.getPackageName(options);
let componentDir;
// Check for client-side compatibility.
// Non-npm case.
if (!options.npm) {
componentDir = bowerConfig.read(options.root).directory;
const pathToLocalWct = path.join(options.root, componentDir, 'web-component-tester');
let version = undefined;
const mdFilenames = ['package.json', 'bower.json', '.bower.json'];
for (const mdFilename of mdFilenames) {
const pathToMetadata = path.join(pathToLocalWct, mdFilename);
try {
if (!version) {
version = require(pathToMetadata).version;
}
}
catch (e) {
// Handled below, where we check if we found a version.
}
}
if (!version) {
throw new Error(`
The web-component-tester Bower package is not installed as a dependency of this project (${packageName}).
Please run this command to install:
bower install --save-dev web-component-tester
Web Component Tester >=6.0 requires that support files needed in the browser are installed as part of the project's dependencies or dev-dependencies. This is to give projects greater control over the versions that are served, while also making Web Component Tester's behavior easier to understand.
Expected to find a ${mdFilenames.join(' or ')} at: ${pathToLocalWct}/
`);
}
const allowedRange = require(path.join(__dirname, '..', 'package.json'))['--private-wct--']['client-side-version-range'];
if (!semver.satisfies(version, allowedRange)) {
throw new Error(`
The web-component-tester Bower package installed is incompatible with the
wct node package you're using.
The test runner expects a version that satisfies ${allowedRange} but the
bower package you have installed is ${version}.
`);
}
let hasWarnedBrowserJs = false;
additionalRoutes.set('/browser.js', function (request, response) {
if (!hasWarnedBrowserJs) {
console.warn(`
WARNING:
Loading WCT's browser.js from /browser.js is deprecated.
Instead load it from ../web-component-tester/browser.js
(or with the absolute url /components/web-component-tester/browser.js)
`);
hasWarnedBrowserJs = true;
}
const browserJsPath = path.join(pathToLocalWct, 'browser.js');
send(request, browserJsPath).pipe(response);
});
}
const pathToGeneratedIndex = `/components/${packageName}/generated-index.html`;
additionalRoutes.set(pathToGeneratedIndex, (_request, response) => {
response.set(DEFAULT_HEADERS);
response.send(options.webserver._generatedIndexContent);
});
const appMapper = (app, options) => __awaiter(this, void 0, void 0, function* () {
// Using the define:webserver hook to provide a mapper function that
// allows user to substitute their own app for the generated polyserve
// app.
yield wct.emitHook('define:webserver', app, (substitution) => {
app = substitution;
}, options);
return app;
});
// Serve up project & dependencies via polyserve
const polyserveResult = yield polyserve_1.startServers({
root: options.root,
componentDir,
compile: options.compile,
hostname: options.webserver.hostname,
port: options.webserver.port,
headers: DEFAULT_HEADERS,
packageName,
additionalRoutes,
npm: !!options.npm,
moduleResolution: options.moduleResolution,
proxy: options.proxy,
}, appMapper);
let servers;
const onDestroyHandlers = [];
const registerServerTeardown = (serverInfo) => {
const destroyableServer = serverInfo.server;
serverDestroy(destroyableServer);
onDestroyHandlers.push(() => {
destroyableServer.destroy();
return new Promise((resolve) => serverInfo.server.on('close', () => resolve()));
});
};
if (polyserveResult.kind === 'mainline') {
servers = [polyserveResult];
registerServerTeardown(polyserveResult);
const address = polyserveResult.server.address();
if (typeof address !== 'string') {
wsOptions.port = address.port;
}
}
else if (polyserveResult.kind === 'MultipleServers') {
servers = [polyserveResult.mainline];
servers = servers.concat(polyserveResult.variants);
const address = polyserveResult.mainline.server.address();
if (typeof address !== 'string') {
wsOptions.port = address.port;
}
for (const server of polyserveResult.servers) {
registerServerTeardown(server);
}
}
else {
const never = polyserveResult;
throw new Error('Internal error: Got unknown response from polyserve.startServers: ' +
`${never}`);
}
wct._httpServers = servers.map((s) => s.server);
// At this point, we allow other plugins to hook and configure the
// webservers as they please.
for (const server of servers) {
yield wct.emitHook('prepare:webserver', server.app);
}
options.webserver._servers = servers.map((s) => {
const address = s.server.address();
const port = typeof address === 'string' ? '' : `:${address.port}`;
const hostname = s.options.hostname;
const url = `http://${hostname}${port}${pathToGeneratedIndex}`;
return { url, variant: s.kind === 'mainline' ? '' : s.variantName };
});
// TODO(rictic): re-enable this stuff. need to either move this code
// into polyserve or let the polyserve API expose this stuff.
// app.use('/httpbin', httpbin.httpbin);
// app.get('/favicon.ico', function(request, response) {
// response.end();
// });
// app.use(function(request, response, next) {
// wct.emit('log:warn', '404', chalk.magenta(request.method),
// request.url);
// next();
// });
function interruptHandler() {
return __awaiter(this, void 0, void 0, function* () {
// close the socket IO server directly if it is spun up
for (const io of (wct._socketIOServers || [])) {
// we will close the underlying server ourselves
io.httpServer = null;
io.close();
}
yield Promise.all(onDestroyHandlers.map((f) => f()));
});
}
cleankill.onInterrupt(() => {
return new Promise((resolve) => {
interruptHandler().then(() => resolve(), resolve);
});
});
});
});
}
exports.webserver = webserver;
function exists(path) {
try {
fs.statSync(path);
return true;
}
catch (_err) {
return false;
}
}
// HACK(rictic): remove this ES6-compat hack and export webserver itself
webserver['webserver'] = webserver;
module.exports = webserver;