| const express = require('express'); |
| const fs = require('fs'); |
| const path = require('path'); |
| const puppeteer = require('puppeteer'); |
| const webpack = require('webpack'); |
| const webpackDevMiddleware = require('webpack-dev-middleware'); |
| |
| /** |
| * This function allows tests to catch document-level events in a Puppeteer |
| * page. |
| * |
| * It takes a Puppeteer page and a list of event names, and adds event listeners |
| * to the page's document for the given events. It must be called before the |
| * page is loaded with e.g. page.goto() for it to work. |
| * |
| * The returned function takes an event name in eventNames and returns a promise |
| * that will resolve to the corresponding Event object's "detail" field when the |
| * event is caught. Multiple promises for the same event will be resolved in the |
| * order that they were created, i.e. one caught event resolves the oldest |
| * pending promise. |
| * |
| * @param {Object} page A Puppeteer page. |
| * @param {Array<string>} eventNames Event names to listen to. |
| * @return {Promise<Function>} Event promise builder function. |
| */ |
| exports.addEventListenersToPuppeteerPage = async (page, eventNames) => { |
| // Maps event names to FIFO queues of promise resolver functions. |
| const resolverFnQueues = {}; |
| eventNames.forEach((eventName) => resolverFnQueues[eventName] = []); |
| |
| // Use an unlikely prefix to reduce chances of name collision. |
| await page.exposeFunction('__pptr_onEvent', (eventName, eventDetail) => { |
| const resolverFn = resolverFnQueues[eventName].shift(); // Dequeue. |
| if (resolverFn) { // Undefined if queue length was 0. |
| resolverFn(eventDetail); |
| } |
| }); |
| |
| // Add an event listener for each one of the given events. |
| await eventNames.forEach(async (name) => { |
| await page.evaluateOnNewDocument((name) => { |
| document.addEventListener(name, (event) => { |
| window.__pptr_onEvent(name, event.detail); |
| }); |
| }, name); |
| }); |
| |
| // The returned function takes an event name and returns a promise that will |
| // resolve to the event details when the event is caught. |
| return (eventName) => { |
| if (resolverFnQueues[eventName] === undefined) { |
| // Fail if the event wasn't included in eventNames. |
| throw new Error(`no event listener for "${eventName}"`); |
| } |
| return new Promise( |
| // Enqueue resolver function at the end of the queue. |
| (resolve) => resolverFnQueues[eventName].push(resolve), |
| ); |
| }; |
| }; |
| |
| /** |
| * Returns true if running from within a Docker container, or false otherwise. |
| * @return {boolean} |
| */ |
| exports.inDocker = () => fs.existsSync('/.dockerenv'); |
| |
| /** |
| * Launches a Puppeteer browser with the right platform-specific arguments. |
| * @return {Promise} |
| */ |
| exports.launchBrowser = () => puppeteer.launch( |
| // See |
| // https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-in-docker. |
| exports.inDocker() |
| ? { args: ['--disable-dev-shm-usage', '--no-sandbox'] } |
| : {}, |
| ); |
| |
| /** |
| * Returns the output directory where tests should e.g. save screenshots. |
| * Screenshots saved in this directory will be uploaded to Gold. |
| * @return {string} |
| */ |
| exports.outputDir = () => (exports.inDocker() |
| ? '/out' |
| : path.join(__dirname, 'output')); // Resolves to //puppeteer-tests/output for local development. |
| |
| /** |
| * This function sets up the before(Each) and after(Each) hooks required for |
| * test suites that take screenshots of demo pages. |
| * |
| * Test cases can access the demo page server's base URL and a Puppeteer page |
| * ready to be used via the return value's baseUrl and page objects, respectively. |
| * |
| * This function assumes that each test case uses exactly one Puppeteer page |
| * (that's why it doesn't expose the Browser instance to tests). The page is set |
| * up with a cookie (name: "puppeteer", value: "true") to give demo pages a |
| * means to detect whether they are running within Puppeteer or not. |
| * |
| * Call this function at the beginning of a Mocha describe() block. |
| * |
| * @param {string} pathToWebpackConfigJs Path to the webpack.config.js file. |
| */ |
| exports.setUpPuppeteerAndDemoPageServer = (pathToWebpackConfigJs) => { |
| let browser; |
| let stopDemoPageServer; |
| const testBed = { |
| page: null, |
| baseUrl: null, |
| }; |
| |
| before(async () => { |
| let baseUrl; |
| ({ baseUrl, stopDemoPageServer } = await exports.startDemoPageServer(pathToWebpackConfigJs)); |
| testBed.baseUrl = baseUrl; // Make baseUrl available to tests. |
| browser = await exports.launchBrowser(); |
| }); |
| |
| after(async () => { |
| await browser.close(); |
| await stopDemoPageServer(); |
| }); |
| |
| beforeEach(async () => { |
| testBed.page = await browser.newPage(); // Make page available to tests. |
| // Tell demo pages this is a Puppeteer test. Demo pages should not fake RPC |
| // latency, render animations or exhibit any other non-deterministic |
| // behavior that could result in differences in the screenshots uploaded to |
| // Gold. |
| await testBed.page.setCookie( |
| { url: testBed.baseUrl, name: 'puppeteer', value: 'true' }, |
| ); |
| }); |
| |
| afterEach(async () => { |
| await testBed.page.close(); |
| }); |
| |
| return testBed; |
| }; |
| |
| /** |
| * Starts a web server that serves custom element demo pages. Equivalent to |
| * running "npx webpack-dev-server" on the terminal. |
| * |
| * Demo pages can be accessed at the returned baseUrl. For example, page |
| * my-component-sk-demo.html is found at `${baseUrl}/dist/my-component-sk.html`. |
| * |
| * This function should be called once at the beginning of any test suite that |
| * requires custom element demo pages. The returned function stopDemoPageServer |
| * should be called at the end of the test suite. |
| * |
| * @param {string} pathToWebpackConfigJs Path to the webpack.config.js file. |
| * @return {Promise<{baseUrl: string, stopDemoPageServer: function}>} |
| */ |
| exports.startDemoPageServer = async (pathToWebpackConfigJs) => { |
| // Load Webpack configuration. |
| const webpackConfigJs = require(pathToWebpackConfigJs); |
| const configuration = webpackConfigJs(null, {}); |
| |
| // See https://webpack.js.org/configuration/mode/. |
| configuration.mode = 'development'; |
| |
| // Quiet down the CleanWebpackPlugin. |
| // TODO(lovisolo): Move this change to the Pulito repo. |
| configuration |
| .plugins |
| .filter((p) => p.constructor.name === 'CleanWebpackPlugin') |
| .forEach((p) => p.options.verbose = false); |
| |
| // This is equivalent to running "npx webpack-dev-server" on the terminal. |
| const middleware = webpackDevMiddleware(webpack(configuration), { |
| logLevel: 'warn', // Do not print summary on startup. |
| }); |
| await new Promise((resolve) => middleware.waitUntilValid(resolve)); |
| |
| // Start an HTTP server on a random, unused port. Serve the above middleware. |
| const app = express(); |
| app.use(configuration.output.publicPath, middleware); // Serve on /dist. |
| let server; |
| await new Promise((resolve) => { server = app.listen(0, resolve); }); |
| |
| return { |
| // Base URL for the demo page server. |
| baseUrl: `http://localhost:${server.address().port}`, |
| |
| // Call this function to shut down the HTTP server after tests are finished. |
| stopDemoPageServer: async () => { |
| await Promise.all([ |
| new Promise((resolve) => middleware.close(resolve)), |
| new Promise((resolve) => server.close(resolve)), |
| ]); |
| }, |
| }; |
| }; |
| |
| /** |
| * Takes a screenshot and saves it to the tests output directory to be uploaded |
| * to Gold. |
| * |
| * The screenshot will be saved as <appName>_<testName>.png. Using the |
| * application name as a prefix prevents name collisions between different apps |
| * and increases consistency among test names. |
| * |
| * @param {Object} handle Puppeteer Page or ElementHandle instance. |
| * @param {string} appName Application name, e.g. 'gold'. |
| * @param {string} testName Test name, e.g. 'my-component-sk_mouse-over'. |
| * @return {Promise} |
| */ |
| exports.takeScreenshot = (handle, appName, testName) => handle.screenshot({ |
| path: path.join(exports.outputDir(), `${appName}_${testName}.png`), |
| }); |