/* * 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 */ 'use strict'; 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> = []; // 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'), ` `); fs.mkdirSync(path.join(tempDir, 'subdir')); fs.writeFileSync(path.join(tempDir, 'subdir/index.html'), ` `); 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); }); } }); }); });