1/** 2 * Copyright 2020 Google Inc. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import { TestServer } from '../utils/testserver/index.js'; 18import * as path from 'path'; 19import * as fs from 'fs'; 20import * as os from 'os'; 21import sinon from 'sinon'; 22import puppeteer from '../lib/cjs/puppeteer/node.js'; 23import { 24 Browser, 25 BrowserContext, 26} from '../lib/cjs/puppeteer/common/Browser.js'; 27import { Page } from '../lib/cjs/puppeteer/common/Page.js'; 28import { PuppeteerNode } from '../lib/cjs/puppeteer/node/Puppeteer.js'; 29import utils from './utils.js'; 30import rimraf from 'rimraf'; 31import expect from 'expect'; 32 33import { trackCoverage } from './coverage-utils.js'; 34import Protocol from 'devtools-protocol'; 35 36const setupServer = async () => { 37 const assetsPath = path.join(__dirname, 'assets'); 38 const cachedPath = path.join(__dirname, 'assets', 'cached'); 39 40 const port = 8907; 41 const server = await TestServer.create(assetsPath, port); 42 server.enableHTTPCache(cachedPath); 43 server.PORT = port; 44 server.PREFIX = `http://localhost:${port}`; 45 server.CROSS_PROCESS_PREFIX = `http://127.0.0.1:${port}`; 46 server.EMPTY_PAGE = `http://localhost:${port}/empty.html`; 47 48 const httpsPort = port + 1; 49 const httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort); 50 httpsServer.enableHTTPCache(cachedPath); 51 httpsServer.PORT = httpsPort; 52 httpsServer.PREFIX = `https://localhost:${httpsPort}`; 53 httpsServer.CROSS_PROCESS_PREFIX = `https://127.0.0.1:${httpsPort}`; 54 httpsServer.EMPTY_PAGE = `https://localhost:${httpsPort}/empty.html`; 55 56 return { server, httpsServer }; 57}; 58 59export const getTestState = (): PuppeteerTestState => 60 state as PuppeteerTestState; 61 62const product = 63 process.env.PRODUCT || process.env.PUPPETEER_PRODUCT || 'Chromium'; 64 65const alternativeInstall = process.env.PUPPETEER_ALT_INSTALL || false; 66 67const isHeadless = 68 (process.env.HEADLESS || 'true').trim().toLowerCase() === 'true'; 69const isFirefox = product === 'firefox'; 70const isChrome = product === 'Chromium'; 71 72let extraLaunchOptions = {}; 73try { 74 extraLaunchOptions = JSON.parse(process.env.EXTRA_LAUNCH_OPTIONS || '{}'); 75} catch (error) { 76 console.warn( 77 `Error parsing EXTRA_LAUNCH_OPTIONS: ${error.message}. Skipping.` 78 ); 79} 80 81const defaultBrowserOptions = Object.assign( 82 { 83 handleSIGINT: true, 84 executablePath: process.env.BINARY, 85 headless: isHeadless, 86 dumpio: !!process.env.DUMPIO, 87 }, 88 extraLaunchOptions 89); 90 91(async (): Promise<void> => { 92 if (defaultBrowserOptions.executablePath) { 93 console.warn( 94 `WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}` 95 ); 96 } else { 97 // TODO(jackfranklin): declare updateRevision in some form for the Firefox 98 // launcher. 99 // @ts-expect-error _updateRevision is defined on the FF launcher 100 // but not the Chrome one. The types need tidying so that TS can infer that 101 // properly and not error here. 102 if (product === 'firefox') await puppeteer._launcher._updateRevision(); 103 const executablePath = puppeteer.executablePath(); 104 if (!fs.existsSync(executablePath)) 105 throw new Error( 106 `Browser is not downloaded at ${executablePath}. Run 'npm install' and try to re-run tests` 107 ); 108 } 109})(); 110 111declare module 'expect/build/types' { 112 interface Matchers<R> { 113 toBeGolden(x: string): R; 114 } 115} 116 117const setupGoldenAssertions = (): void => { 118 const suffix = product.toLowerCase(); 119 const GOLDEN_DIR = path.join(__dirname, 'golden-' + suffix); 120 const OUTPUT_DIR = path.join(__dirname, 'output-' + suffix); 121 if (fs.existsSync(OUTPUT_DIR)) rimraf.sync(OUTPUT_DIR); 122 utils.extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR); 123}; 124 125setupGoldenAssertions(); 126 127interface PuppeteerTestState { 128 browser: Browser; 129 context: BrowserContext; 130 page: Page; 131 puppeteer: PuppeteerNode; 132 defaultBrowserOptions: { 133 [x: string]: any; 134 }; 135 server: any; 136 httpsServer: any; 137 isFirefox: boolean; 138 isChrome: boolean; 139 isHeadless: boolean; 140 puppeteerPath: string; 141} 142const state: Partial<PuppeteerTestState> = {}; 143 144export const itFailsFirefox = ( 145 description: string, 146 body: Mocha.Func 147): Mocha.Test => { 148 if (isFirefox) return xit(description, body); 149 else return it(description, body); 150}; 151 152export const itChromeOnly = ( 153 description: string, 154 body: Mocha.Func 155): Mocha.Test => { 156 if (isChrome) return it(description, body); 157 else return xit(description, body); 158}; 159 160export const itOnlyRegularInstall = ( 161 description: string, 162 body: Mocha.Func 163): Mocha.Test => { 164 if (alternativeInstall || process.env.BINARY) return xit(description, body); 165 else return it(description, body); 166}; 167 168export const itFailsWindowsUntilDate = ( 169 date: Date, 170 description: string, 171 body: Mocha.Func 172): Mocha.Test => { 173 if (os.platform() === 'win32' && Date.now() < date.getTime()) { 174 // we are within the deferred time so skip the test 175 return xit(description, body); 176 } 177 178 return it(description, body); 179}; 180 181export const itFailsWindows = ( 182 description: string, 183 body: Mocha.Func 184): Mocha.Test => { 185 if (os.platform() === 'win32') { 186 return xit(description, body); 187 } 188 return it(description, body); 189}; 190 191export const describeFailsFirefox = ( 192 description: string, 193 body: (this: Mocha.Suite) => void 194): void | Mocha.Suite => { 195 if (isFirefox) return xdescribe(description, body); 196 else return describe(description, body); 197}; 198 199export const describeChromeOnly = ( 200 description: string, 201 body: (this: Mocha.Suite) => void 202): Mocha.Suite => { 203 if (isChrome) return describe(description, body); 204}; 205 206let coverageHooks = { 207 beforeAll: (): void => {}, 208 afterAll: (): void => {}, 209}; 210 211if (process.env.COVERAGE) { 212 coverageHooks = trackCoverage(); 213} 214 215console.log( 216 `Running unit tests with: 217 -> product: ${product} 218 -> binary: ${ 219 defaultBrowserOptions.executablePath || 220 path.relative(process.cwd(), puppeteer.executablePath()) 221 }` 222); 223 224export const setupTestBrowserHooks = (): void => { 225 before(async () => { 226 const browser = await puppeteer.launch(defaultBrowserOptions); 227 state.browser = browser; 228 }); 229 230 after(async () => { 231 await state.browser.close(); 232 state.browser = null; 233 }); 234}; 235 236export const setupTestPageAndContextHooks = (): void => { 237 beforeEach(async () => { 238 state.context = await state.browser.createIncognitoBrowserContext(); 239 state.page = await state.context.newPage(); 240 }); 241 242 afterEach(async () => { 243 await state.context.close(); 244 state.context = null; 245 state.page = null; 246 }); 247}; 248 249export const mochaHooks = { 250 beforeAll: [ 251 async (): Promise<void> => { 252 const { server, httpsServer } = await setupServer(); 253 254 state.puppeteer = puppeteer; 255 state.defaultBrowserOptions = defaultBrowserOptions; 256 state.server = server; 257 state.httpsServer = httpsServer; 258 state.isFirefox = isFirefox; 259 state.isChrome = isChrome; 260 state.isHeadless = isHeadless; 261 state.puppeteerPath = path.resolve(path.join(__dirname, '..')); 262 }, 263 coverageHooks.beforeAll, 264 ], 265 266 beforeEach: async (): Promise<void> => { 267 state.server.reset(); 268 state.httpsServer.reset(); 269 }, 270 271 afterAll: [ 272 async (): Promise<void> => { 273 await state.server.stop(); 274 state.server = null; 275 await state.httpsServer.stop(); 276 state.httpsServer = null; 277 }, 278 coverageHooks.afterAll, 279 ], 280 281 afterEach: (): void => { 282 sinon.restore(); 283 }, 284}; 285 286export const expectCookieEquals = ( 287 cookies: Protocol.Network.Cookie[], 288 expectedCookies: Array<Partial<Protocol.Network.Cookie>> 289): void => { 290 const { isChrome } = getTestState(); 291 if (!isChrome) { 292 // Only keep standard properties when testing on a browser other than Chrome. 293 expectedCookies = expectedCookies.map((cookie) => { 294 return { 295 domain: cookie.domain, 296 expires: cookie.expires, 297 httpOnly: cookie.httpOnly, 298 name: cookie.name, 299 path: cookie.path, 300 secure: cookie.secure, 301 session: cookie.session, 302 size: cookie.size, 303 value: cookie.value, 304 }; 305 }); 306 } 307 308 expect(cookies).toEqual(expectedCookies); 309}; 310