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