1/**
2 * Copyright 2017 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 */
16import fs from 'fs';
17import os from 'os';
18import path from 'path';
19import sinon from 'sinon';
20import { promisify } from 'util';
21import Protocol from 'devtools-protocol';
22import {
23  getTestState,
24  itChromeOnly,
25  itFailsFirefox,
26  itOnlyRegularInstall,
27} from './mocha-utils'; // eslint-disable-line import/extensions
28import utils from './utils.js';
29import expect from 'expect';
30import rimraf from 'rimraf';
31import { Page } from '../lib/cjs/puppeteer/common/Page.js';
32
33const rmAsync = promisify(rimraf);
34const mkdtempAsync = promisify(fs.mkdtemp);
35const readFileAsync = promisify(fs.readFile);
36const statAsync = promisify(fs.stat);
37const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
38const FIREFOX_TIMEOUT = 30 * 1000;
39
40describe('Launcher specs', function () {
41  if (getTestState().isFirefox) this.timeout(FIREFOX_TIMEOUT);
42
43  describe('Puppeteer', function () {
44    describe('BrowserFetcher', function () {
45      it('should download and extract chrome linux binary', async () => {
46        const { server, puppeteer } = getTestState();
47
48        const downloadsFolder = await mkdtempAsync(TMP_FOLDER);
49        const browserFetcher = puppeteer.createBrowserFetcher({
50          platform: 'linux',
51          path: downloadsFolder,
52          host: server.PREFIX,
53        });
54        const expectedRevision = '123456';
55        let revisionInfo = browserFetcher.revisionInfo(expectedRevision);
56        server.setRoute(
57          revisionInfo.url.substring(server.PREFIX.length),
58          (req, res) => {
59            server.serveFile(req, res, '/chromium-linux.zip');
60          }
61        );
62
63        expect(revisionInfo.local).toBe(false);
64        expect(browserFetcher.platform()).toBe('linux');
65        expect(browserFetcher.product()).toBe('chrome');
66        expect(!!browserFetcher.host()).toBe(true);
67        expect(await browserFetcher.canDownload('100000')).toBe(false);
68        expect(await browserFetcher.canDownload(expectedRevision)).toBe(true);
69
70        revisionInfo = await browserFetcher.download(expectedRevision);
71        expect(revisionInfo.local).toBe(true);
72        expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe(
73          'LINUX BINARY\n'
74        );
75        const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755;
76        expect(
77          (await statAsync(revisionInfo.executablePath)).mode & 0o777
78        ).toBe(expectedPermissions);
79        expect(await browserFetcher.localRevisions()).toEqual([
80          expectedRevision,
81        ]);
82        await browserFetcher.remove(expectedRevision);
83        expect(await browserFetcher.localRevisions()).toEqual([]);
84        await rmAsync(downloadsFolder);
85      });
86      it('should download and extract firefox linux binary', async () => {
87        const { server, puppeteer } = getTestState();
88
89        const downloadsFolder = await mkdtempAsync(TMP_FOLDER);
90        const browserFetcher = puppeteer.createBrowserFetcher({
91          platform: 'linux',
92          path: downloadsFolder,
93          host: server.PREFIX,
94          product: 'firefox',
95        });
96        const expectedVersion = '75.0a1';
97        let revisionInfo = browserFetcher.revisionInfo(expectedVersion);
98        server.setRoute(
99          revisionInfo.url.substring(server.PREFIX.length),
100          (req, res) => {
101            server.serveFile(
102              req,
103              res,
104              `/firefox-${expectedVersion}.en-US.linux-x86_64.tar.bz2`
105            );
106          }
107        );
108
109        expect(revisionInfo.local).toBe(false);
110        expect(browserFetcher.platform()).toBe('linux');
111        expect(browserFetcher.product()).toBe('firefox');
112        expect(await browserFetcher.canDownload('100000')).toBe(false);
113        expect(await browserFetcher.canDownload(expectedVersion)).toBe(true);
114
115        revisionInfo = await browserFetcher.download(expectedVersion);
116        expect(revisionInfo.local).toBe(true);
117        expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe(
118          'FIREFOX LINUX BINARY\n'
119        );
120        const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755;
121        expect(
122          (await statAsync(revisionInfo.executablePath)).mode & 0o777
123        ).toBe(expectedPermissions);
124        expect(await browserFetcher.localRevisions()).toEqual([
125          expectedVersion,
126        ]);
127        await browserFetcher.remove(expectedVersion);
128        expect(await browserFetcher.localRevisions()).toEqual([]);
129        await rmAsync(downloadsFolder);
130      });
131    });
132
133    describe('Browser.disconnect', function () {
134      it('should reject navigation when browser closes', async () => {
135        const { server, puppeteer, defaultBrowserOptions } = getTestState();
136        server.setRoute('/one-style.css', () => {});
137        const browser = await puppeteer.launch(defaultBrowserOptions);
138        const remote = await puppeteer.connect({
139          browserWSEndpoint: browser.wsEndpoint(),
140        });
141        const page = await remote.newPage();
142        const navigationPromise = page
143          .goto(server.PREFIX + '/one-style.html', { timeout: 60000 })
144          .catch((error_) => error_);
145        await server.waitForRequest('/one-style.css');
146        remote.disconnect();
147        const error = await navigationPromise;
148        expect(error.message).toBe(
149          'Navigation failed because browser has disconnected!'
150        );
151        await browser.close();
152      });
153      it('should reject waitForSelector when browser closes', async () => {
154        const { server, puppeteer, defaultBrowserOptions } = getTestState();
155
156        server.setRoute('/empty.html', () => {});
157        const browser = await puppeteer.launch(defaultBrowserOptions);
158        const remote = await puppeteer.connect({
159          browserWSEndpoint: browser.wsEndpoint(),
160        });
161        const page = await remote.newPage();
162        const watchdog = page
163          .waitForSelector('div', { timeout: 60000 })
164          .catch((error_) => error_);
165        remote.disconnect();
166        const error = await watchdog;
167        expect(error.message).toContain('Protocol error');
168        await browser.close();
169      });
170    });
171    describe('Browser.close', function () {
172      it('should terminate network waiters', async () => {
173        const { server, puppeteer, defaultBrowserOptions } = getTestState();
174
175        const browser = await puppeteer.launch(defaultBrowserOptions);
176        const remote = await puppeteer.connect({
177          browserWSEndpoint: browser.wsEndpoint(),
178        });
179        const newPage = await remote.newPage();
180        const results = await Promise.all([
181          newPage.waitForRequest(server.EMPTY_PAGE).catch((error) => error),
182          newPage.waitForResponse(server.EMPTY_PAGE).catch((error) => error),
183          browser.close(),
184        ]);
185        for (let i = 0; i < 2; i++) {
186          const message = results[i].message;
187          expect(message).toContain('Target closed');
188          expect(message).not.toContain('Timeout');
189        }
190        await browser.close();
191      });
192    });
193    describe('Puppeteer.launch', function () {
194      it('should reject all promises when browser is closed', async () => {
195        const { defaultBrowserOptions, puppeteer } = getTestState();
196        const browser = await puppeteer.launch(defaultBrowserOptions);
197        const page = await browser.newPage();
198        let error = null;
199        const neverResolves = page
200          .evaluate(() => new Promise(() => {}))
201          .catch((error_) => (error = error_));
202        await browser.close();
203        await neverResolves;
204        expect(error.message).toContain('Protocol error');
205      });
206      it('should reject if executable path is invalid', async () => {
207        const { defaultBrowserOptions, puppeteer } = getTestState();
208
209        let waitError = null;
210        const options = Object.assign({}, defaultBrowserOptions, {
211          executablePath: 'random-invalid-path',
212        });
213        await puppeteer.launch(options).catch((error) => (waitError = error));
214        expect(waitError.message).toContain('Failed to launch');
215      });
216      it('userDataDir option', async () => {
217        const { defaultBrowserOptions, puppeteer } = getTestState();
218
219        const userDataDir = await mkdtempAsync(TMP_FOLDER);
220        const options = Object.assign({ userDataDir }, defaultBrowserOptions);
221        const browser = await puppeteer.launch(options);
222        // Open a page to make sure its functional.
223        await browser.newPage();
224        expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
225        await browser.close();
226        expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
227        // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
228        await rmAsync(userDataDir).catch(() => {});
229      });
230      it('userDataDir argument', async () => {
231        const { isChrome, puppeteer, defaultBrowserOptions } = getTestState();
232
233        const userDataDir = await mkdtempAsync(TMP_FOLDER);
234        const options = Object.assign({}, defaultBrowserOptions);
235        if (isChrome) {
236          options.args = [
237            ...(defaultBrowserOptions.args || []),
238            `--user-data-dir=${userDataDir}`,
239          ];
240        } else {
241          options.args = [
242            ...(defaultBrowserOptions.args || []),
243            `-profile`,
244            userDataDir,
245          ];
246        }
247        const browser = await puppeteer.launch(options);
248        expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
249        await browser.close();
250        expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
251        // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
252        await rmAsync(userDataDir).catch(() => {});
253      });
254      it('userDataDir option should restore state', async () => {
255        const { server, puppeteer, defaultBrowserOptions } = getTestState();
256
257        const userDataDir = await mkdtempAsync(TMP_FOLDER);
258        const options = Object.assign({ userDataDir }, defaultBrowserOptions);
259        const browser = await puppeteer.launch(options);
260        const page = await browser.newPage();
261        await page.goto(server.EMPTY_PAGE);
262        await page.evaluate(() => (localStorage.hey = 'hello'));
263        await browser.close();
264
265        const browser2 = await puppeteer.launch(options);
266        const page2 = await browser2.newPage();
267        await page2.goto(server.EMPTY_PAGE);
268        expect(await page2.evaluate(() => localStorage.hey)).toBe('hello');
269        await browser2.close();
270        // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
271        await rmAsync(userDataDir).catch(() => {});
272      });
273      // This mysteriously fails on Windows on AppVeyor. See
274      // https://github.com/puppeteer/puppeteer/issues/4111
275      xit('userDataDir option should restore cookies', async () => {
276        const { server, puppeteer, defaultBrowserOptions } = getTestState();
277
278        const userDataDir = await mkdtempAsync(TMP_FOLDER);
279        const options = Object.assign({ userDataDir }, defaultBrowserOptions);
280        const browser = await puppeteer.launch(options);
281        const page = await browser.newPage();
282        await page.goto(server.EMPTY_PAGE);
283        await page.evaluate(
284          () =>
285            (document.cookie =
286              'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT')
287        );
288        await browser.close();
289
290        const browser2 = await puppeteer.launch(options);
291        const page2 = await browser2.newPage();
292        await page2.goto(server.EMPTY_PAGE);
293        expect(await page2.evaluate(() => document.cookie)).toBe(
294          'doSomethingOnlyOnce=true'
295        );
296        await browser2.close();
297        // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
298        await rmAsync(userDataDir).catch(() => {});
299      });
300      it('should return the default arguments', async () => {
301        const { isChrome, isFirefox, puppeteer } = getTestState();
302
303        if (isChrome) {
304          expect(puppeteer.defaultArgs()).toContain('--no-first-run');
305          expect(puppeteer.defaultArgs()).toContain('--headless');
306          expect(puppeteer.defaultArgs({ headless: false })).not.toContain(
307            '--headless'
308          );
309          expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain(
310            `--user-data-dir=${path.resolve('foo')}`
311          );
312        } else if (isFirefox) {
313          expect(puppeteer.defaultArgs()).toContain('--headless');
314          expect(puppeteer.defaultArgs()).toContain('--no-remote');
315          expect(puppeteer.defaultArgs()).toContain('--foreground');
316          expect(puppeteer.defaultArgs({ headless: false })).not.toContain(
317            '--headless'
318          );
319          expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain(
320            '--profile'
321          );
322          expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain(
323            'foo'
324          );
325        } else {
326          expect(puppeteer.defaultArgs()).toContain('-headless');
327          expect(puppeteer.defaultArgs({ headless: false })).not.toContain(
328            '-headless'
329          );
330          expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain(
331            '-profile'
332          );
333          expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain(
334            path.resolve('foo')
335          );
336        }
337      });
338      it('should report the correct product', async () => {
339        const { isChrome, isFirefox, puppeteer } = getTestState();
340        if (isChrome) expect(puppeteer.product).toBe('chrome');
341        else if (isFirefox) expect(puppeteer.product).toBe('firefox');
342      });
343      it('should work with no default arguments', async () => {
344        const { defaultBrowserOptions, puppeteer } = getTestState();
345        const options = Object.assign({}, defaultBrowserOptions);
346        options.ignoreDefaultArgs = true;
347        const browser = await puppeteer.launch(options);
348        const page = await browser.newPage();
349        expect(await page.evaluate('11 * 11')).toBe(121);
350        await page.close();
351        await browser.close();
352      });
353      it('should filter out ignored default arguments', async () => {
354        const { defaultBrowserOptions, puppeteer } = getTestState();
355        // Make sure we launch with `--enable-automation` by default.
356        const defaultArgs = puppeteer.defaultArgs();
357        const browser = await puppeteer.launch(
358          Object.assign({}, defaultBrowserOptions, {
359            // Ignore first and third default argument.
360            ignoreDefaultArgs: [defaultArgs[0], defaultArgs[2]],
361          })
362        );
363        const spawnargs = browser.process().spawnargs;
364        if (!spawnargs) {
365          throw new Error('spawnargs not present');
366        }
367        expect(spawnargs.indexOf(defaultArgs[0])).toBe(-1);
368        expect(spawnargs.indexOf(defaultArgs[1])).not.toBe(-1);
369        expect(spawnargs.indexOf(defaultArgs[2])).toBe(-1);
370        await browser.close();
371      });
372      it('should have default URL when launching browser', async function () {
373        const { defaultBrowserOptions, puppeteer } = getTestState();
374        const browser = await puppeteer.launch(defaultBrowserOptions);
375        const pages = (await browser.pages()).map((page) => page.url());
376        expect(pages).toEqual(['about:blank']);
377        await browser.close();
378      });
379      it(
380        'should have custom URL when launching browser',
381        async () => {
382          const { server, puppeteer, defaultBrowserOptions } = getTestState();
383
384          const options = Object.assign({}, defaultBrowserOptions);
385          options.args = [server.EMPTY_PAGE].concat(options.args || []);
386          const browser = await puppeteer.launch(options);
387          const pages = await browser.pages();
388          expect(pages.length).toBe(1);
389          const page = pages[0];
390          if (page.url() !== server.EMPTY_PAGE) await page.waitForNavigation();
391          expect(page.url()).toBe(server.EMPTY_PAGE);
392          await browser.close();
393        }
394      );
395      it('should set the default viewport', async () => {
396        const { puppeteer, defaultBrowserOptions } = getTestState();
397        const options = Object.assign({}, defaultBrowserOptions, {
398          defaultViewport: {
399            width: 456,
400            height: 789,
401          },
402        });
403        const browser = await puppeteer.launch(options);
404        const page = await browser.newPage();
405        expect(await page.evaluate('window.innerWidth')).toBe(456);
406        expect(await page.evaluate('window.innerHeight')).toBe(789);
407        await browser.close();
408      });
409      it('should disable the default viewport', async () => {
410        const { puppeteer, defaultBrowserOptions } = getTestState();
411        const options = Object.assign({}, defaultBrowserOptions, {
412          defaultViewport: null,
413        });
414        const browser = await puppeteer.launch(options);
415        const page = await browser.newPage();
416        expect(page.viewport()).toBe(null);
417        await browser.close();
418      });
419      it('should take fullPage screenshots when defaultViewport is null', async () => {
420        const { server, puppeteer, defaultBrowserOptions } = getTestState();
421
422        const options = Object.assign({}, defaultBrowserOptions, {
423          defaultViewport: null,
424        });
425        const browser = await puppeteer.launch(options);
426        const page = await browser.newPage();
427        await page.goto(server.PREFIX + '/grid.html');
428        const screenshot = await page.screenshot({
429          fullPage: true,
430        });
431        expect(screenshot).toBeInstanceOf(Buffer);
432        await browser.close();
433      });
434      itChromeOnly(
435        'should launch Chrome properly with --no-startup-window and waitForInitialPage=false',
436        async () => {
437          const { defaultBrowserOptions, puppeteer } = getTestState();
438          const options = {
439            args: ['--no-startup-window'],
440            waitForInitialPage: false,
441            // This is needed to prevent Puppeteer from adding an initial blank page.
442            // See also https://github.com/puppeteer/puppeteer/blob/ad6b736039436fcc5c0a262e5b575aa041427be3/src/node/Launcher.ts#L200
443            ignoreDefaultArgs: true,
444            ...defaultBrowserOptions,
445          };
446          const browser = await puppeteer.launch(options);
447          const pages = await browser.pages();
448          expect(pages.length).toBe(0);
449          await browser.close();
450        }
451      );
452    });
453
454    describe('Puppeteer.launch', function () {
455      let productName;
456
457      before(async () => {
458        const { puppeteer } = getTestState();
459        productName = puppeteer._productName;
460      });
461
462      after(async () => {
463        const { puppeteer } = getTestState();
464        // @ts-expect-error launcher is a private property that users can't
465        // touch, but for testing purposes we need to reset it.
466        puppeteer._lazyLauncher = undefined;
467        puppeteer._productName = productName;
468      });
469
470      itOnlyRegularInstall('should be able to launch Chrome', async () => {
471        const { puppeteer } = getTestState();
472        const browser = await puppeteer.launch({ product: 'chrome' });
473        const userAgent = await browser.userAgent();
474        await browser.close();
475        expect(userAgent).toContain('Chrome');
476      });
477
478      it('falls back to launching chrome if there is an unknown product but logs a warning', async () => {
479        const { puppeteer } = getTestState();
480        const consoleStub = sinon.stub(console, 'warn');
481        // @ts-expect-error purposeful bad input
482        const browser = await puppeteer.launch({ product: 'SO_NOT_A_PRODUCT' });
483        const userAgent = await browser.userAgent();
484        await browser.close();
485        expect(userAgent).toContain('Chrome');
486        expect(consoleStub.callCount).toEqual(1);
487        expect(consoleStub.firstCall.args).toEqual([
488          'Warning: unknown product name SO_NOT_A_PRODUCT. Falling back to chrome.',
489        ]);
490      });
491
492      itOnlyRegularInstall(
493        'should be able to launch Firefox',
494        async function () {
495          this.timeout(FIREFOX_TIMEOUT);
496          const { puppeteer } = getTestState();
497          const browser = await puppeteer.launch({ product: 'firefox' });
498          const userAgent = await browser.userAgent();
499          await browser.close();
500          expect(userAgent).toContain('Firefox');
501        }
502      );
503    });
504
505    describe('Puppeteer.connect', function () {
506      it('should be able to connect multiple times to the same browser', async () => {
507        const { puppeteer, defaultBrowserOptions } = getTestState();
508
509        const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
510        const otherBrowser = await puppeteer.connect({
511          browserWSEndpoint: originalBrowser.wsEndpoint(),
512        });
513        const page = await otherBrowser.newPage();
514        expect(await page.evaluate(() => 7 * 8)).toBe(56);
515        otherBrowser.disconnect();
516
517        const secondPage = await originalBrowser.newPage();
518        expect(await secondPage.evaluate(() => 7 * 6)).toBe(42);
519        await originalBrowser.close();
520      });
521      it('should be able to close remote browser', async () => {
522        const { defaultBrowserOptions, puppeteer } = getTestState();
523
524        const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
525        const remoteBrowser = await puppeteer.connect({
526          browserWSEndpoint: originalBrowser.wsEndpoint(),
527        });
528        await Promise.all([
529          utils.waitEvent(originalBrowser, 'disconnected'),
530          remoteBrowser.close(),
531        ]);
532      });
533      it('should support ignoreHTTPSErrors option', async () => {
534        const { httpsServer, puppeteer, defaultBrowserOptions } =
535          getTestState();
536
537        const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
538        const browserWSEndpoint = originalBrowser.wsEndpoint();
539
540        const browser = await puppeteer.connect({
541          browserWSEndpoint,
542          ignoreHTTPSErrors: true,
543        });
544        const page = await browser.newPage();
545        let error = null;
546        const [serverRequest, response] = await Promise.all([
547          httpsServer.waitForRequest('/empty.html'),
548          page.goto(httpsServer.EMPTY_PAGE).catch((error_) => (error = error_)),
549        ]);
550        expect(error).toBe(null);
551        expect(response.ok()).toBe(true);
552        expect(response.securityDetails()).toBeTruthy();
553        const protocol = serverRequest.socket.getProtocol().replace('v', ' ');
554        expect(response.securityDetails().protocol()).toBe(protocol);
555        await page.close();
556        await browser.close();
557      });
558      it('should support targetFilter option', async () => {
559        const { server, puppeteer, defaultBrowserOptions } = getTestState();
560
561        const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
562        const browserWSEndpoint = originalBrowser.wsEndpoint();
563
564        const page1 = await originalBrowser.newPage();
565        await page1.goto(server.EMPTY_PAGE);
566
567        const page2 = await originalBrowser.newPage();
568        await page2.goto(server.EMPTY_PAGE + '?should-be-ignored');
569
570        const browser = await puppeteer.connect({
571          browserWSEndpoint,
572          targetFilter: (targetInfo: Protocol.Target.TargetInfo) =>
573            !targetInfo.url.includes('should-be-ignored'),
574        });
575
576        const pages = await browser.pages();
577
578        await page2.close();
579        await page1.close();
580        await browser.close();
581
582        expect(pages.map((p: Page) => p.url()).sort()).toEqual([
583          'about:blank',
584          server.EMPTY_PAGE,
585        ]);
586      });
587      it(
588        'should be able to reconnect to a disconnected browser',
589        async () => {
590          const { server, puppeteer, defaultBrowserOptions } = getTestState();
591
592          const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
593          const browserWSEndpoint = originalBrowser.wsEndpoint();
594          const page = await originalBrowser.newPage();
595          await page.goto(server.PREFIX + '/frames/nested-frames.html');
596          originalBrowser.disconnect();
597
598          const browser = await puppeteer.connect({ browserWSEndpoint });
599          const pages = await browser.pages();
600          const restoredPage = pages.find(
601            (page) =>
602              page.url() === server.PREFIX + '/frames/nested-frames.html'
603          );
604          expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([
605            'http://localhost:<PORT>/frames/nested-frames.html',
606            '    http://localhost:<PORT>/frames/two-frames.html (2frames)',
607            '        http://localhost:<PORT>/frames/frame.html (uno)',
608            '        http://localhost:<PORT>/frames/frame.html (dos)',
609            '    http://localhost:<PORT>/frames/frame.html (aframe)',
610          ]);
611          expect(await restoredPage.evaluate(() => 7 * 8)).toBe(56);
612          await browser.close();
613        }
614      );
615      // @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410
616      it(
617        'should be able to connect to the same page simultaneously',
618        async () => {
619          const { puppeteer } = getTestState();
620
621          const browserOne = await puppeteer.launch();
622          const browserTwo = await puppeteer.connect({
623            browserWSEndpoint: browserOne.wsEndpoint(),
624          });
625          const [page1, page2] = await Promise.all([
626            new Promise<Page>((x) =>
627              browserOne.once('targetcreated', (target) => x(target.page()))
628            ),
629            browserTwo.newPage(),
630          ]);
631          expect(await page1.evaluate(() => 7 * 8)).toBe(56);
632          expect(await page2.evaluate(() => 7 * 6)).toBe(42);
633          await browserOne.close();
634        }
635      );
636      it('should be able to reconnect', async () => {
637        const { puppeteer, server } = getTestState();
638        const browserOne = await puppeteer.launch();
639        const browserWSEndpoint = browserOne.wsEndpoint();
640        const pageOne = await browserOne.newPage();
641        await pageOne.goto(server.EMPTY_PAGE);
642        browserOne.disconnect();
643
644        const browserTwo = await puppeteer.connect({ browserWSEndpoint });
645        const pages = await browserTwo.pages();
646        const pageTwo = pages.find((page) => page.url() === server.EMPTY_PAGE);
647        await pageTwo.reload();
648        const bodyHandle = await pageTwo.waitForSelector('body', {
649          timeout: 10000,
650        });
651        await bodyHandle.dispose();
652        await browserTwo.close();
653      });
654    });
655    describe('Puppeteer.executablePath', function () {
656      itOnlyRegularInstall('should work', async () => {
657        const { puppeteer } = getTestState();
658
659        const executablePath = puppeteer.executablePath();
660        expect(fs.existsSync(executablePath)).toBe(true);
661        expect(fs.realpathSync(executablePath)).toBe(executablePath);
662      });
663    });
664  });
665
666  describe('Browser target events', function () {
667    it('should work', async () => {
668      const { server, puppeteer, defaultBrowserOptions } = getTestState();
669
670      const browser = await puppeteer.launch(defaultBrowserOptions);
671      const events = [];
672      browser.on('targetcreated', () => events.push('CREATED'));
673      browser.on('targetchanged', () => events.push('CHANGED'));
674      browser.on('targetdestroyed', () => events.push('DESTROYED'));
675      const page = await browser.newPage();
676      await page.goto(server.EMPTY_PAGE);
677      await page.close();
678      expect(events).toEqual(['CREATED', 'CHANGED', 'DESTROYED']);
679      await browser.close();
680    });
681  });
682
683  describe('Browser.Events.disconnected', function () {
684    it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async () => {
685      const { puppeteer, defaultBrowserOptions } = getTestState();
686      const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
687      const browserWSEndpoint = originalBrowser.wsEndpoint();
688      const remoteBrowser1 = await puppeteer.connect({ browserWSEndpoint });
689      const remoteBrowser2 = await puppeteer.connect({ browserWSEndpoint });
690
691      let disconnectedOriginal = 0;
692      let disconnectedRemote1 = 0;
693      let disconnectedRemote2 = 0;
694      originalBrowser.on('disconnected', () => ++disconnectedOriginal);
695      remoteBrowser1.on('disconnected', () => ++disconnectedRemote1);
696      remoteBrowser2.on('disconnected', () => ++disconnectedRemote2);
697
698      await Promise.all([
699        utils.waitEvent(remoteBrowser2, 'disconnected'),
700        remoteBrowser2.disconnect(),
701      ]);
702
703      expect(disconnectedOriginal).toBe(0);
704      expect(disconnectedRemote1).toBe(0);
705      expect(disconnectedRemote2).toBe(1);
706
707      await Promise.all([
708        utils.waitEvent(remoteBrowser1, 'disconnected'),
709        utils.waitEvent(originalBrowser, 'disconnected'),
710        originalBrowser.close(),
711      ]);
712
713      expect(disconnectedOriginal).toBe(1);
714      expect(disconnectedRemote1).toBe(1);
715      expect(disconnectedRemote2).toBe(1);
716    });
717  });
718});
719