polymer-cli
Version:
A commandline tool for Polymer projects
695 lines (637 loc) • 24.6 kB
text/typescript
/*
* Copyright (c) 2016 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
*/
;
import {assert} from 'chai';
import * as path from 'path';
import {run as runGenerator} from 'yeoman-test';
import {createApplicationGenerator} from '../../init/application/application';
import {runCommand} from './run-command';
import {createElementGenerator} from '../../init/element/element';
import {createGithubGenerator} from '../../init/github';
import * as puppeteer from 'puppeteer';
import {startServers, ServerOptions} from 'polyserve';
import {ProjectConfig, ProjectOptions} from 'polymer-project-config';
import * as tempMod from 'temp';
import * as fs from 'fs';
import {isNull} from 'util';
const debugging = !!process.env['DEBUG_CLI_TESTS'];
const temp = tempMod.track();
const disposables: Array<() => void | Promise<void>> = [];
// A zero privilege github token of a nonce account, used for quota.
const githubToken = '8d8622bf09bb1d85cb411b5e475a35e742a7ce35';
// TODO(https://github.com/Polymer/tools/issues/74): some tests time out on
// windows.
const isWindows = process.platform === 'win32';
const skipOnWindows = isWindows ? test.skip : test;
const binPath = path.join(__dirname, '../../../', 'bin', 'polymer.js');
// Serves the given directory with polyserve, returns a fully qualified
// url of the server.
async function serve(dirToServe: string, options: ServerOptions = {}) {
const startResult = await startServers({root: dirToServe, ...options});
if (startResult.kind === 'MultipleServers') {
for (const server of startResult.servers) {
server.server.close();
}
throw new Error(`Unexpected startResult`);
}
disposables.push(() => {
startResult.server.close();
});
const address = startResult.server.address();
if (typeof address === 'string') {
return `http://${address}`;
} else if (!isNull(address)) {
return `http://${address.address}:${address.port}`;
} else {
// How you gonna serve without an address? How is that even a server? If a
// server can't respond to requests, is it *really* a server?
throw new Error(`No address returned when starting server. ${startResult}`);
}
}
async function requestAnimationFrame(page: puppeteer.Page) {
// For the moment we don't type check in-browser expressions.
// tslint:disable-next-line: no-any
type Window = any;
await page.waitFor(function(this: Window) {
return new Promise((resolve) => {
this.requestAnimationFrame(resolve);
});
});
}
/**
* Like puppeteer's page.goto(), except it fails if any uncaught exceptions are
* thrown, and it waits a few rAFs after the load to be really sure the page is
* ready.
*/
async function gotoOrDie(page: puppeteer.Page, url: string) {
let error: Error|undefined;
const handler = (e: Error) => (error = error || e);
// Grab the first page error, if any.
page.on('pageerror', handler);
await page.goto(url);
if (error) {
throw new Error(`Error loading ${url} in Chrome: ${error}`);
}
for (let i = 0; i < 3; i++) {
await requestAnimationFrame(page);
}
if (error) {
throw new Error(`Error during rAFs after loading ${
url} in Chrome. Browser Error:\n${error}`);
}
page.removeListener('pageerror', handler);
}
suite('integration tests', function() {
// Extend timeout limit to 90 seconds for slower systems
this.timeout(4 * 60 * 1000);
suiteTeardown(async () => {
await Promise.all(disposables.map((d) => d()));
disposables.length = 0;
});
suite('init templates', () => {
skipOnWindows('test the Polymer 3.x element template', async () => {
const dir =
await runGenerator(createElementGenerator('polymer-3.x'))
.withPrompts({name: 'my-element'}) // Mock the prompt answers
.toPromise();
await runCommand(binPath, ['install'], {cwd: dir});
await runCommand(binPath, ['lint'], {cwd: dir});
await runCommand(binPath, ['test'], {cwd: dir});
});
skipOnWindows('test the Polymer 3.x application template', async () => {
const dir = await runGenerator(createApplicationGenerator('polymer-3.x'))
.withPrompts({name: 'my-app'}) // Mock the prompt answers
.toPromise();
await runCommand(binPath, ['install'], {cwd: dir});
await runCommand(binPath, ['lint'], {cwd: dir});
await runCommand(binPath, ['test'], {cwd: dir});
await runCommand(binPath, ['build'], {cwd: dir});
});
skipOnWindows('test the Polymer 1.x application template', async () => {
const dir = await runGenerator(createApplicationGenerator('polymer-1.x'))
.withPrompts({name: 'my-app'}) // Mock the prompt answers
.toPromise();
await runCommand(binPath, ['install'], {cwd: dir});
await runCommand(binPath, ['lint'], {cwd: dir});
await runCommand(binPath, ['test'], {cwd: dir});
await runCommand(binPath, ['build'], {cwd: dir});
});
skipOnWindows('test the Polymer 2.x application template', async () => {
const dir = await runGenerator(createApplicationGenerator('polymer-2.x'))
.withPrompts({name: 'my-app'}) // Mock the prompt answers
.toPromise();
await runCommand(binPath, ['install'], {cwd: dir});
await runCommand(binPath, ['lint'], {cwd: dir});
await runCommand(binPath, ['test'], {cwd: dir});
await runCommand(binPath, ['build'], {cwd: dir});
});
skipOnWindows('test the Polymer 2.x "element" template', async () => {
const dir =
await runGenerator(createElementGenerator('polymer-2.x'))
.withPrompts({name: 'my-element'}) // Mock the prompt answers
.toPromise();
await runCommand(binPath, ['install'], {cwd: dir});
await runCommand(binPath, ['lint'], {cwd: dir});
await runCommand(binPath, ['test'], {cwd: dir});
});
skipOnWindows('test the Polymer 1.x "element" template', async () => {
const dir =
await runGenerator(createElementGenerator('polymer-1.x'))
.withPrompts({name: 'my-element'}) // Mock the prompt answers
.toPromise();
await runCommand(binPath, ['install'], {cwd: dir});
await runCommand(binPath, ['lint'], {cwd: dir});
await runCommand(binPath, ['test'], {cwd: dir});
});
test('test the "shop" template', async () => {
const ShopGenerator = createGithubGenerator({
owner: 'Polymer',
repo: 'shop',
semverRange: '^2.0.0',
githubToken,
installDependencies: {
bower: true,
npm: false,
},
});
const dir = await runGenerator(ShopGenerator).toPromise();
await runCommand(binPath, ['install'], {cwd: dir});
// See: https://github.com/Polymer/shop/pull/114
// await runCommand(
// binPath, ['lint', '--rules=polymer-2-hybrid'],
// {cwd: dir})
// await runCommand(binPath, ['test'], {cwd: dir})
await runCommand(binPath, ['build'], {cwd: dir});
});
// TODO(justinfagnani): consider removing these integration tests
// or checking in the contents so that we're not subject to the
// other repo changing
test.skip('test the Polymer 1.x "starter-kit" template', async () => {
const PSKGenerator = createGithubGenerator({
owner: 'Polymer',
repo: 'polymer-starter-kit',
semverRange: '^2.0.0',
githubToken,
installDependencies: {
bower: true,
npm: false,
},
});
const dir = await runGenerator(PSKGenerator).toPromise();
await runCommand(binPath, ['install'], {cwd: dir});
await runCommand(binPath, ['lint', '--rules=polymer-2-hybrid'], {
cwd: dir,
});
// await runCommand(binPath, ['test'], {cwd: dir})
await runCommand(binPath, ['build'], {cwd: dir});
});
// TODO(justinfagnani): consider removing these integration tests
// or checking in the contents so that we're not subject to the
// other repo changing
test.skip('test the Polymer 2.x "starter-kit" template', async () => {
const PSKGenerator = createGithubGenerator({
owner: 'Polymer',
repo: 'polymer-starter-kit',
semverRange: '^3.0.0',
githubToken,
installDependencies: {
bower: true,
npm: false,
},
});
const dir = await runGenerator(PSKGenerator).toPromise();
await runCommand(binPath, ['install'], {cwd: dir});
await runCommand(binPath, ['lint', '--rules=polymer-2'], {cwd: dir});
// await runCommand(binPath, ['test'], {cwd: dir}));
await runCommand(binPath, ['build'], {cwd: dir});
});
});
// TODO(justinfagnani): consider removing these integration tests
// or checking in the contents so that we're not subject to the
// other repo changing
suite.skip('tools-sample-projects templates', () => {
let tspDir: string;
suiteSetup(async () => {
const TSPGenerator = createGithubGenerator({
owner: 'Polymer',
repo: 'tools-sample-projects',
githubToken,
});
tspDir = await runGenerator(TSPGenerator).toPromise();
});
test('test the "polymer-1-app" template', async () => {
const dir = path.join(tspDir, 'polymer-1-app');
await runCommand(binPath, ['install'], {cwd: dir});
await runCommand(binPath, ['lint'], {cwd: dir});
// await runCommand(binPath, ['test'], {cwd: dir});
await runCommand(binPath, ['build'], {cwd: dir});
});
test('test the "polymer-2-app" template', async () => {
const dir = path.join(tspDir, 'polymer-2-app');
await runCommand(binPath, ['install'], {cwd: dir});
await runCommand(binPath, ['lint'], {cwd: dir});
// await runCommand(binPath, ['test'], {cwd: dir})
await runCommand(binPath, ['build'], {cwd: dir});
});
});
});
suite('import.meta support', async () => {
let tempDir: string;
// Build options, copied from shop.
const options: ProjectOptions = {
entrypoint: 'index.html',
builds: [
{
name: 'esm-bundled',
browserCapabilities: ['es2015', 'modules'],
js: {minify: true},
css: {minify: true},
html: {minify: true},
bundle: true,
},
{
name: 'es6-bundled',
browserCapabilities: ['es2015'],
js: {minify: true, transformModulesToAmd: true},
css: {minify: true},
html: {minify: true},
bundle: true,
},
{
name: 'es5-bundled',
js: {compile: true, minify: true, transformModulesToAmd: true},
css: {minify: true},
html: {minify: true},
bundle: true,
},
],
moduleResolution: 'node',
npm: true,
};
suiteSetup(function() {
tempDir = temp.mkdirSync('-import-meta');
// An inline import.meta test fixture!
fs.writeFileSync(path.join(tempDir, 'index.html'), `
<script type="module">
import './subdir/foo.js';
window.indexHtmlUrl = import.meta.url;
</script>
`);
fs.mkdirSync(path.join(tempDir, 'subdir'));
fs.writeFileSync(path.join(tempDir, 'subdir/index.html'), `
<script type="module">
import './foo.js';
window.indexHtmlUrl = import.meta.url;
</script>
`);
fs.writeFileSync(path.join(tempDir, 'subdir', 'foo.js'), `
window.fooUrl = import.meta.url;
`);
fs.writeFileSync(
path.join(tempDir, 'package.json'),
JSON.stringify({name: 'import-meta-test'}));
fs.writeFileSync(
path.join(tempDir, 'polymer.json'), JSON.stringify(options));
});
teardown(async () => {
await Promise.all(disposables.map((d) => d()));
disposables.length = 0;
});
// The given url should be a fully qualified
const assertPageWorksCorrectly =
async (baseUrl: string, skipTestingSubdir: boolean = false) => {
const browser = await puppeteer.launch();
disposables.push(() => browser.close());
const page = await browser.newPage();
await gotoOrDie(page, `${baseUrl}/`);
assert.deepEqual(await page.evaluate(`window.indexHtmlUrl`), `${baseUrl}/`);
assert.deepEqual(
await page.evaluate('window.fooUrl'), `${baseUrl}/subdir/foo.js`);
await gotoOrDie(page, `${baseUrl}/index.html`);
assert.deepEqual(
await page.evaluate(`window.indexHtmlUrl`), `${baseUrl}/index.html`);
assert.deepEqual(
await page.evaluate('window.fooUrl'), `${baseUrl}/subdir/foo.js`);
if (!skipTestingSubdir) {
await gotoOrDie(page, `${baseUrl}/subdir/`);
assert.deepEqual(
await page.evaluate(`window.indexHtmlUrl`), `${baseUrl}/subdir/`);
assert.deepEqual(
await page.evaluate('window.fooUrl'), `${baseUrl}/subdir/foo.js`);
await gotoOrDie(page, `${baseUrl}/subdir/index.html`);
assert.deepEqual(
await page.evaluate(`window.indexHtmlUrl`),
`${baseUrl}/subdir/index.html`);
assert.deepEqual(
await page.evaluate('window.fooUrl'), `${baseUrl}/subdir/foo.js`);
}
return page;
};
test('import.meta works uncompiled in chrome', async function() {
const url = await serve(tempDir, {compile: 'never'});
const page = await assertPageWorksCorrectly(url);
await gotoOrDie(page, `${url}/`);
assert.include(
await page.content(),
'import.meta',
'expected import.meta to not be compiled out!');
});
let testName = 'import.meta works in chrome with polyserve es5 compilation';
test(testName, async function() {
const url = await serve(tempDir, {compile: 'always'});
const page = await assertPageWorksCorrectly(url);
await gotoOrDie(page, `${url}/`);
assert.notInclude(
await page.content(),
'import.meta',
'expected import.meta to be compiled out!');
});
suite('after building', () => {
suiteSetup(async function() {
this.timeout(20 * 1000);
await runCommand(binPath, ['build'], {cwd: tempDir});
});
for (const buildOption of options.builds!) {
const buildName = buildOption.name || 'default';
testName = `import.meta works in build configuration ${buildName}`;
test(testName, async function() {
const url = await serve(path.join(tempDir, 'build', buildName), {
compile: 'always',
});
const page = await assertPageWorksCorrectly(url, true);
await gotoOrDie(page, `${url}/`);
if (buildName !== 'esm-bundle') {
assert.notInclude(
await page.content(),
'import.meta',
'expected import.meta to be compiled out!');
} else {
assert.include(
await page.content(),
'import.meta',
'expected import.meta to not be compiled out!');
}
});
}
});
});
suite('polymer shop', function() {
this.timeout(60 * 1000);
// Given the URL of a server serving out Polymer shop, opens a Chrome tab
// and pokes around to test that Shop is working there.
async function assertThatShopWorks(baseUrl: string) {
let browser: puppeteer.Browser;
if (debugging) {
browser = await puppeteer.launch({headless: false, slowMo: 250});
} else {
// TODO(usergenic): For some unknown reason, tests failed in headless
// Chrome involving the `/cart` route for the Polymer/shop lit-element
// branch only. Remove the `{headless: false}` when this problem is
// fixed.
browser = await puppeteer.launch({headless: false});
}
disposables.push(() => browser.close());
const page = await browser.newPage();
page.on('pageerror', (e) => (error = error || e));
// Evaluate an expression as a string in the browser.
const evaluate = async (expression: string) => {
try {
return await page.evaluate(expression);
} catch (e) {
throw new Error(`Failed evaluating expression \`${
expression} in the browser. Error: ${e}`);
}
};
// Assert on an expression's result in the browser.
const assertTrueInPage = async (expression: string) => {
assert(
await evaluate(expression),
`Expected \`${expression}\` to evaluate to true in the browser`);
};
const waitFor =
async (name: string, expression: string, timeout?: number) => {
try {
await page.waitForFunction(expression, {timeout});
} catch (e) {
throw new Error(`Error waiting for ${name} in the browser`);
}
};
await gotoOrDie(page, `${baseUrl}/`);
assert.deepEqual(`${baseUrl}/`, page.url());
await waitFor(
'shop-app to be defined',
`this.customElements.get('shop-app') !== undefined`);
await waitFor(
'shop-app children to exist', `this.document.querySelector('shop-app')
.shadowRoot.querySelector('a[href="/cart"], shop-cart-button')`);
const isLitElement =
await evaluate(`!!this.document.querySelector('shop-app')
.shadowRoot.querySelector('shop-cart-button')`);
if (isLitElement) {
// Wait a few more rAFs for the button to definitely be there.
for (let i = 0; i < 10; i++) {
await requestAnimationFrame(page);
}
await page.waitForFunction(`!!(
document.querySelector('shop-app').shadowRoot
.querySelector('shop-cart-button').shadowRoot)`);
}
// The cart shouldn't be registered yet, because we've only loaded the
// main page.
await assertTrueInPage(`customElements.get('shop-cart') === undefined`);
// Click the shopping cart button.
await evaluate(`
(
// shop 3.0
document.querySelector('shop-app').shadowRoot
.querySelector('a[href="/cart"]')
||
// shop lit
document.querySelector('shop-app').shadowRoot
.querySelector('shop-cart-button').shadowRoot
.querySelector('a[href="/cart"]')
).click()`);
// The url changes immediately
assert.deepEqual(`${baseUrl}/cart`, page.url());
// We'll lazy load the code for shop-cart. We'll know that it worked
// when the element is registered. If this resolves, it loaded
// successfully!
await waitFor(
'shop-cart to be defined',
`this.customElements.get('shop-cart') !== undefined`,
3 * 60 * 1000);
}
let error: Error|undefined;
setup(async () => {
error = undefined;
});
teardown(async () => {
if (error !== undefined) {
throw new Error(
`Error encountered in browser page while testing: ${error}`);
}
await Promise.all(disposables.map((d) => d()));
disposables.length = 0;
});
suite('the 3.0 branch', () => {
let dir: string;
suiteSetup(async function() {
const debugDir = process.env['CLI_TEST_SHOP_3_DIR'];
if (debugDir != null) {
dir = debugDir;
} else {
// Cloning and installing can take a few minutes
this.timeout(4 * 60 * 1000);
const ShopGenerator = createGithubGenerator({
owner: 'Polymer',
repo: 'shop',
githubToken,
tag: 'v3.0.0',
installDependencies: {
bower: false,
npm: true,
},
});
dir = await runGenerator(ShopGenerator).toPromise();
await runCommand(binPath, ['install'], {cwd: dir});
}
});
test('serving sources with polyserve and `never` compile', async () => {
const baseUrl = await serve(dir, {
compile: 'never',
moduleResolution: 'node',
});
await assertThatShopWorks(baseUrl);
});
const testName = 'serving sources with polyserve and `always` compile';
test(testName, async function() {
// Compiling is a little slow.
this.timeout(30 * 1000);
const baseUrl = await serve(dir, {
compile: 'always',
moduleResolution: 'node',
});
await assertThatShopWorks(baseUrl);
});
test('serving sources with polyserve and `auto` compile', async () => {
const baseUrl = await serve(dir, {
compile: 'auto',
moduleResolution: 'node',
});
await assertThatShopWorks(baseUrl);
});
suite('when built with polymer build', () => {
const expectedBuildNames = [
'es5-bundled',
'es6-bundled',
'esm-bundled',
].sort();
suiteSetup(async function() {
// Building takes a few minutes.
this.timeout(10 * 60 * 1000);
await runCommand(binPath, ['lint'], { cwd: dir });
await runCommand(binPath, ['build'], { cwd: dir });
const config =
ProjectConfig.loadConfigFromFile(path.join(dir, 'polymer.json'));
if (config == null) {
throw new Error('Failed to load shop\'s polymer.json');
}
assert.deepEqual(
config.builds.map((b) => b.name || 'default').sort(),
expectedBuildNames);
});
for (const buildName of expectedBuildNames) {
test(`works in the ${buildName} configuration`, async () => {
const baseUrl = await serve(path.join(dir, 'build', buildName));
await assertThatShopWorks(baseUrl);
});
}
});
});
suite('the lit-element branch', function() {
let dir: string;
suiteSetup(async function() {
const debugDir = process.env['CLI_TEST_SHOP_LIT_DIR'];
if (debugDir != null) {
dir = debugDir;
} else {
// Cloning and installing can take a few minutes
this.timeout(4 * 60 * 1000);
const ShopGenerator = createGithubGenerator({
owner: 'Polymer',
repo: 'shop',
githubToken,
branch: 'lit-element',
installDependencies: {
bower: false,
npm: true,
},
});
dir = await runGenerator(ShopGenerator).toPromise();
await runCommand(binPath, ['install'], {cwd: dir});
}
});
test('serving sources with polyserve and `never` compile', async () => {
const baseUrl = await serve(dir, {
compile: 'never',
moduleResolution: 'node',
});
await assertThatShopWorks(baseUrl);
});
const testName = 'serving sources with polyserve and `always` compile';
test(testName, async function() {
// Compiling is a little slow.
this.timeout(30 * 1000);
const baseUrl = await serve(dir, {
compile: 'always',
moduleResolution: 'node',
});
await assertThatShopWorks(baseUrl);
});
test('serving sources with polyserve and `auto` compile', async () => {
const baseUrl = await serve(dir, {
compile: 'auto',
moduleResolution: 'node',
});
await assertThatShopWorks(baseUrl);
});
suite('when built with polymer build', () => {
const expectedBuildNames = [
'es5-bundled',
'es6-bundled',
'esm-bundled',
].sort();
suiteSetup(async function() {
// Building takes a few minutes.
this.timeout(10 * 60 * 1000);
await Promise.all([
// Does not lint clean at the moment.
// TODO: https://github.com/Polymer/tools/issues/274
// runCommand(binPath, ['lint', '--rules=polymer-3'], {cwd: dir}),
runCommand(binPath, ['build'], {cwd: dir}),
]);
const config =
ProjectConfig.loadConfigFromFile(path.join(dir, 'polymer.json'));
if (config == null) {
throw new Error('Failed to load shop\'s polymer.json');
}
assert.deepEqual(
config.builds.map((b) => b.name || 'default').sort(),
expectedBuildNames);
});
for (const buildName of expectedBuildNames) {
test(`works in the ${buildName} configuration`, async () => {
const baseUrl = await serve(path.join(dir, 'build', buildName));
await assertThatShopWorks(baseUrl);
});
}
});
});
});