1/**
2 * Copyright 2018 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 path from 'path';
18import os from 'os';
19import fs from 'fs';
20import { promisify } from 'util';
21import expect from 'expect';
22import {
23  getTestState,
24  describeChromeOnly,
25  itFailsWindows,
26} from './mocha-utils'; // eslint-disable-line import/extensions
27import rimraf from 'rimraf';
28
29const rmAsync = promisify(rimraf);
30const mkdtempAsync = promisify(fs.mkdtemp);
31
32const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
33
34const extensionPath = path.join(__dirname, 'assets', 'simple-extension');
35
36describeChromeOnly('headful tests', function () {
37  /* These tests fire up an actual browser so let's
38   * allow a higher timeout
39   */
40  this.timeout(20 * 1000);
41
42  let headfulOptions;
43  let headlessOptions;
44  let extensionOptions;
45  let forcedOopifOptions;
46  const browsers = [];
47
48  beforeEach(() => {
49    const { server, defaultBrowserOptions } = getTestState();
50    headfulOptions = Object.assign({}, defaultBrowserOptions, {
51      headless: false,
52    });
53    headlessOptions = Object.assign({}, defaultBrowserOptions, {
54      headless: true,
55    });
56
57    extensionOptions = Object.assign({}, defaultBrowserOptions, {
58      headless: false,
59      args: [
60        `--disable-extensions-except=${extensionPath}`,
61        `--load-extension=${extensionPath}`,
62      ],
63    });
64
65    forcedOopifOptions = Object.assign({}, defaultBrowserOptions, {
66      headless: false,
67      devtools: true,
68      args: [
69        `--host-rules=MAP oopifdomain 127.0.0.1`,
70        `--isolate-origins=${server.PREFIX.replace(
71          'localhost',
72          'oopifdomain'
73        )}`,
74      ],
75    });
76  });
77
78  async function launchBrowser(puppeteer, options) {
79    const browser = await puppeteer.launch(options);
80    browsers.push(browser);
81    return browser;
82  }
83
84  afterEach(() => {
85    for (const i in browsers) {
86      const browser = browsers[i];
87      if (browser.isConnected()) {
88        browser.close();
89      }
90      delete browsers[i];
91    }
92  });
93
94  describe('HEADFUL', function () {
95    it('background_page target type should be available', async () => {
96      const { puppeteer } = getTestState();
97      const browserWithExtension = await launchBrowser(
98        puppeteer,
99        extensionOptions
100      );
101      const page = await browserWithExtension.newPage();
102      const backgroundPageTarget = await browserWithExtension.waitForTarget(
103        (target) => target.type() === 'background_page'
104      );
105      await page.close();
106      await browserWithExtension.close();
107      expect(backgroundPageTarget).toBeTruthy();
108    });
109    it('target.page() should return a background_page', async function () {
110      const { puppeteer } = getTestState();
111      const browserWithExtension = await launchBrowser(
112        puppeteer,
113        extensionOptions
114      );
115      const backgroundPageTarget = await browserWithExtension.waitForTarget(
116        (target) => target.type() === 'background_page'
117      );
118      const page = await backgroundPageTarget.page();
119      expect(await page.evaluate(() => 2 * 3)).toBe(6);
120      expect(await page.evaluate(() => globalThis.MAGIC)).toBe(42);
121      await browserWithExtension.close();
122    });
123    it('should have default url when launching browser', async function () {
124      const { puppeteer } = getTestState();
125      const browser = await launchBrowser(puppeteer, extensionOptions);
126      const pages = (await browser.pages()).map((page) => page.url());
127      expect(pages).toEqual(['about:blank']);
128      await browser.close();
129    });
130    itFailsWindows(
131      'headless should be able to read cookies written by headful',
132      async () => {
133        /* Needs investigation into why but this fails consistently on Windows CI. */
134        const { server, puppeteer } = getTestState();
135
136        const userDataDir = await mkdtempAsync(TMP_FOLDER);
137        // Write a cookie in headful chrome
138        const headfulBrowser = await launchBrowser(
139          puppeteer,
140          Object.assign({ userDataDir }, headfulOptions)
141        );
142        const headfulPage = await headfulBrowser.newPage();
143        await headfulPage.goto(server.EMPTY_PAGE);
144        await headfulPage.evaluate(
145          () =>
146            (document.cookie =
147              'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT')
148        );
149        await headfulBrowser.close();
150        // Read the cookie from headless chrome
151        const headlessBrowser = await launchBrowser(
152          puppeteer,
153          Object.assign({ userDataDir }, headlessOptions)
154        );
155        const headlessPage = await headlessBrowser.newPage();
156        await headlessPage.goto(server.EMPTY_PAGE);
157        const cookie = await headlessPage.evaluate(() => document.cookie);
158        await headlessBrowser.close();
159        // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
160        await rmAsync(userDataDir).catch(() => {});
161        expect(cookie).toBe('foo=true');
162      }
163    );
164    // TODO: Support OOOPIF. @see https://github.com/puppeteer/puppeteer/issues/2548
165    xit('OOPIF: should report google.com frame', async () => {
166      const { server, puppeteer } = getTestState();
167
168      // https://google.com is isolated by default in Chromium embedder.
169      const browser = await launchBrowser(puppeteer, headfulOptions);
170      const page = await browser.newPage();
171      await page.goto(server.EMPTY_PAGE);
172      await page.setRequestInterception(true);
173      page.on('request', (r) => r.respond({ body: 'YO, GOOGLE.COM' }));
174      await page.evaluate(() => {
175        const frame = document.createElement('iframe');
176        frame.setAttribute('src', 'https://google.com/');
177        document.body.appendChild(frame);
178        return new Promise((x) => (frame.onload = x));
179      });
180      await page.waitForSelector('iframe[src="https://google.com/"]');
181      const urls = page
182        .frames()
183        .map((frame) => frame.url())
184        .sort();
185      expect(urls).toEqual([server.EMPTY_PAGE, 'https://google.com/']);
186      await browser.close();
187    });
188    it('OOPIF: should expose events within OOPIFs', async () => {
189      const { server, puppeteer } = getTestState();
190
191      const browser = await launchBrowser(puppeteer, forcedOopifOptions);
192      const page = await browser.newPage();
193
194      // Setup our session listeners to observe OOPIF activity.
195      const session = await page.target().createCDPSession();
196      const networkEvents = [];
197      const otherSessions = [];
198      await session.send('Target.setAutoAttach', {
199        autoAttach: true,
200        flatten: true,
201        waitForDebuggerOnStart: true,
202      });
203      session.on('sessionattached', async (session) => {
204        otherSessions.push(session);
205
206        session.on('Network.requestWillBeSent', (params) =>
207          networkEvents.push(params)
208        );
209        await session.send('Network.enable');
210        await session.send('Runtime.runIfWaitingForDebugger');
211      });
212
213      // Navigate to the empty page and add an OOPIF iframe with at least one request.
214      await page.goto(server.EMPTY_PAGE);
215      await page.evaluate((frameUrl) => {
216        const frame = document.createElement('iframe');
217        frame.setAttribute('src', frameUrl);
218        document.body.appendChild(frame);
219        return new Promise((x, y) => {
220          frame.onload = x;
221          frame.onerror = y;
222        });
223      }, server.PREFIX.replace('localhost', 'oopifdomain') + '/one-style.html');
224      await page.waitForSelector('iframe');
225
226      // Ensure we found the iframe session.
227      expect(otherSessions).toHaveLength(1);
228
229      // Resume the iframe and trigger another request.
230      const iframeSession = otherSessions[0];
231      await iframeSession.send('Runtime.evaluate', {
232        expression: `fetch('/fetch')`,
233        awaitPromise: true,
234      });
235      await browser.close();
236
237      const requests = networkEvents.map((event) => event.request.url);
238      expect(requests).toContain(`http://oopifdomain:${server.PORT}/fetch`);
239    });
240    it('should close browser with beforeunload page', async () => {
241      const { server, puppeteer } = getTestState();
242
243      const browser = await launchBrowser(puppeteer, headfulOptions);
244      const page = await browser.newPage();
245      await page.goto(server.PREFIX + '/beforeunload.html');
246      // We have to interact with a page so that 'beforeunload' handlers
247      // fire.
248      await page.click('body');
249      await browser.close();
250    });
251    it('should open devtools when "devtools: true" option is given', async () => {
252      const { puppeteer } = getTestState();
253
254      const browser = await launchBrowser(
255        puppeteer,
256        Object.assign({ devtools: true }, headfulOptions)
257      );
258      const context = await browser.createIncognitoBrowserContext();
259      await Promise.all([
260        context.newPage(),
261        browser.waitForTarget((target) => target.url().includes('devtools://')),
262      ]);
263      await browser.close();
264    });
265  });
266
267  describe('Page.bringToFront', function () {
268    it('should work', async () => {
269      const { puppeteer } = getTestState();
270      const browser = await launchBrowser(puppeteer, headfulOptions);
271      const page1 = await browser.newPage();
272      const page2 = await browser.newPage();
273
274      await page1.bringToFront();
275      expect(await page1.evaluate(() => document.visibilityState)).toBe(
276        'visible'
277      );
278      expect(await page2.evaluate(() => document.visibilityState)).toBe(
279        'hidden'
280      );
281
282      await page2.bringToFront();
283      expect(await page1.evaluate(() => document.visibilityState)).toBe(
284        'hidden'
285      );
286      expect(await page2.evaluate(() => document.visibilityState)).toBe(
287        'visible'
288      );
289
290      await page1.close();
291      await page2.close();
292      await browser.close();
293    });
294  });
295
296  describe('Page.screenshot', function () {
297    it('should run in parallel in multiple pages', async () => {
298      const { server, puppeteer } = getTestState();
299      const browser = await puppeteer.launch(headfulOptions);
300      const context = await browser.createIncognitoBrowserContext();
301
302      const N = 2;
303      const pages = await Promise.all(
304        Array(N)
305          .fill(0)
306          .map(async () => {
307            const page = await context.newPage();
308            await page.goto(server.PREFIX + '/grid.html');
309            return page;
310          })
311      );
312      const promises = [];
313      for (let i = 0; i < N; ++i)
314        promises.push(
315          pages[i].screenshot({
316            clip: { x: 50 * i, y: 0, width: 50, height: 50 },
317          })
318        );
319      const screenshots = await Promise.all(promises);
320      for (let i = 0; i < N; ++i)
321        expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`);
322      await Promise.all(pages.map((page) => page.close()));
323
324      await browser.close();
325    });
326  });
327});
328