/** * Copyright 2020 Google Inc. 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. */ import Protocol from 'devtools-protocol'; import expect from 'expect'; import * as fs from 'fs'; import * as path from 'path'; import rimraf from 'rimraf'; import sinon from 'sinon'; import {Browser, BrowserContext} from 'puppeteer-core/internal/api/Browser.js'; import {Page} from 'puppeteer-core/internal/api/Page.js'; import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js'; import { PuppeteerLaunchOptions, PuppeteerNode, } from 'puppeteer-core/internal/node/PuppeteerNode.js'; import puppeteer from 'puppeteer/lib/cjs/puppeteer/puppeteer.js'; import {TestServer} from '@pptr/testserver'; import {extendExpectWithToBeGolden} from './utils.js'; import * as Mocha from 'mocha'; const setupServer = async () => { const assetsPath = path.join(__dirname, '../assets'); const cachedPath = path.join(__dirname, '../assets', 'cached'); const server = await TestServer.create(assetsPath); const port = server.port; server.enableHTTPCache(cachedPath); server.PORT = port; server.PREFIX = `http://localhost:${port}`; server.CROSS_PROCESS_PREFIX = `http://127.0.0.1:${port}`; server.EMPTY_PAGE = `http://localhost:${port}/empty.html`; const httpsServer = await TestServer.createHTTPS(assetsPath); const httpsPort = httpsServer.port; httpsServer.enableHTTPCache(cachedPath); httpsServer.PORT = httpsPort; httpsServer.PREFIX = `https://localhost:${httpsPort}`; httpsServer.CROSS_PROCESS_PREFIX = `https://127.0.0.1:${httpsPort}`; httpsServer.EMPTY_PAGE = `https://localhost:${httpsPort}/empty.html`; return {server, httpsServer}; }; export const getTestState = (): PuppeteerTestState => { return state as PuppeteerTestState; }; const product = process.env['PRODUCT'] || process.env['PUPPETEER_PRODUCT'] || 'chrome'; const alternativeInstall = process.env['PUPPETEER_ALT_INSTALL'] || false; const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase(); const isHeadless = headless === 'true' || headless === 'chrome'; const isFirefox = product === 'firefox'; const isChrome = product === 'chrome'; const protocol = process.env['PUPPETEER_PROTOCOL'] || 'cdp'; let extraLaunchOptions = {}; try { extraLaunchOptions = JSON.parse(process.env['EXTRA_LAUNCH_OPTIONS'] || '{}'); } catch (error) { if (isErrorLike(error)) { console.warn( `Error parsing EXTRA_LAUNCH_OPTIONS: ${error.message}. Skipping.` ); } else { throw error; } } const defaultBrowserOptions = Object.assign( { handleSIGINT: true, executablePath: process.env['BINARY'], headless: headless === 'chrome' ? ('chrome' as const) : isHeadless, dumpio: !!process.env['DUMPIO'], protocol: protocol as 'cdp' | 'webDriverBiDi', }, extraLaunchOptions ); (async (): Promise => { if (defaultBrowserOptions.executablePath) { console.warn( `WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}` ); } else { // TODO(jackfranklin): declare updateRevision in some form for the Firefox // launcher. if (product === 'firefox') { // @ts-expect-error _updateRevision is defined on the FF launcher // but not the Chrome one. The types need tidying so that TS can infer that // properly and not error here. await puppeteer._launcher._updateRevision(); } const executablePath = puppeteer.executablePath(); if (!fs.existsSync(executablePath)) { throw new Error( `Browser is not downloaded at ${executablePath}. Run 'npm install' and try to re-run tests` ); } } })(); declare module 'expect/build/types' { interface Matchers { toBeGolden(x: string): R; } } const setupGoldenAssertions = (): void => { let suffix = product.toLowerCase(); if (suffix === 'chrome') { // TODO: to avoid moving golden folders. suffix = 'chromium'; } const GOLDEN_DIR = path.join(__dirname, `../golden-${suffix}`); const OUTPUT_DIR = path.join(__dirname, `../output-${suffix}`); if (fs.existsSync(OUTPUT_DIR)) { rimraf.sync(OUTPUT_DIR); } extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR); }; setupGoldenAssertions(); interface PuppeteerTestState { browser: Browser; context: BrowserContext; page: Page; puppeteer: PuppeteerNode; defaultBrowserOptions: PuppeteerLaunchOptions; server: TestServer; httpsServer: TestServer; isFirefox: boolean; isChrome: boolean; isHeadless: boolean; headless: string; puppeteerPath: string; } const state: Partial = {}; export const itOnlyRegularInstall = ( description: string, body: Mocha.AsyncFunc ): Mocha.Test => { if (alternativeInstall || process.env['BINARY']) { return it.skip(description, body); } else { return it(description, body); } }; if ( process.env['MOCHA_WORKER_ID'] === undefined || process.env['MOCHA_WORKER_ID'] === '0' ) { console.log( `Running unit tests with: -> product: ${product} -> binary: ${ defaultBrowserOptions.executablePath || path.relative(process.cwd(), puppeteer.executablePath()) } -> mode: ${ isHeadless ? headless === 'chrome' ? '--headless=chrome' : '--headless' : 'headful' }` ); } process.on('unhandledRejection', reason => { throw reason; }); export const setupTestBrowserHooks = (): void => { before(async () => { const browser = await puppeteer.launch(defaultBrowserOptions); state.browser = browser; }); after(async () => { await state.browser?.close(); state.browser = undefined; }); }; export const setupTestPageAndContextHooks = (): void => { beforeEach(async () => { state.context = await state.browser!.createIncognitoBrowserContext(); state.page = await state.context.newPage(); }); afterEach(async () => { await state.context?.close(); state.context = undefined; state.page = undefined; }); }; export const mochaHooks = { beforeAll: [ async (): Promise => { const {server, httpsServer} = await setupServer(); state.puppeteer = puppeteer; state.defaultBrowserOptions = defaultBrowserOptions; state.server = server; state.httpsServer = httpsServer; state.isFirefox = isFirefox; state.isChrome = isChrome; state.isHeadless = isHeadless; state.headless = headless; state.puppeteerPath = path.resolve( path.join(__dirname, '..', '..', 'packages', 'puppeteer') ); }, ], beforeEach: async (): Promise => { state.server!.reset(); state.httpsServer!.reset(); }, afterAll: [ async (): Promise => { await state.server!.stop(); state.server = undefined; await state.httpsServer!.stop(); state.httpsServer = undefined; }, ], afterEach: (): void => { sinon.restore(); }, }; export const expectCookieEquals = ( cookies: Protocol.Network.Cookie[], expectedCookies: Array> ): void => { const {isChrome} = getTestState(); if (!isChrome) { // Only keep standard properties when testing on a browser other than Chrome. expectedCookies = expectedCookies.map(cookie => { return { domain: cookie.domain, expires: cookie.expires, httpOnly: cookie.httpOnly, name: cookie.name, path: cookie.path, secure: cookie.secure, session: cookie.session, size: cookie.size, value: cookie.value, }; }); } expect(cookies.length).toBe(expectedCookies.length); for (let i = 0; i < cookies.length; i++) { expect(cookies[i]).toMatchObject(expectedCookies[i]!); } }; export const shortWaitForArrayToHaveAtLeastNElements = async ( data: unknown[], minLength: number, attempts = 3, timeout = 50 ): Promise => { for (let i = 0; i < attempts; i++) { if (data.length >= minLength) { break; } await new Promise(resolve => { return setTimeout(resolve, timeout); }); } }; export const createTimeout = ( n: number, value?: T ): Promise => { return new Promise(resolve => { setTimeout(() => { return resolve(value); }, n); }); };