summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/test/src
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/test/src')
-rw-r--r--remote/test/puppeteer/test/src/accessibility.spec.ts567
-rw-r--r--remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts721
-rw-r--r--remote/test/puppeteer/test/src/autofill.spec.ts38
-rw-r--r--remote/test/puppeteer/test/src/browser.spec.ts81
-rw-r--r--remote/test/puppeteer/test/src/browsercontext.spec.ts368
-rw-r--r--remote/test/puppeteer/test/src/cdp/CDPSession.spec.ts147
-rw-r--r--remote/test/puppeteer/test/src/cdp/TargetManager.spec.ts96
-rw-r--r--remote/test/puppeteer/test/src/cdp/bfcache.spec.ts65
-rw-r--r--remote/test/puppeteer/test/src/cdp/devtools.spec.ts123
-rw-r--r--remote/test/puppeteer/test/src/cdp/extensions.spec.ts120
-rw-r--r--remote/test/puppeteer/test/src/cdp/prerender.spec.ts181
-rw-r--r--remote/test/puppeteer/test/src/cdp/queryObjects.spec.ts108
-rw-r--r--remote/test/puppeteer/test/src/chromiumonly.spec.ts168
-rw-r--r--remote/test/puppeteer/test/src/click.spec.ts478
-rw-r--r--remote/test/puppeteer/test/src/cookies.spec.ts557
-rw-r--r--remote/test/puppeteer/test/src/coverage.spec.ts343
-rw-r--r--remote/test/puppeteer/test/src/debugInfo.spec.ts36
-rw-r--r--remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts104
-rw-r--r--remote/test/puppeteer/test/src/device-request-prompt.spec.ts53
-rw-r--r--remote/test/puppeteer/test/src/dialog.spec.ts64
-rw-r--r--remote/test/puppeteer/test/src/diffstyle.css13
-rw-r--r--remote/test/puppeteer/test/src/drag-and-drop.spec.ts154
-rw-r--r--remote/test/puppeteer/test/src/elementhandle.spec.ts953
-rw-r--r--remote/test/puppeteer/test/src/emulation.spec.ts553
-rw-r--r--remote/test/puppeteer/test/src/evaluation.spec.ts607
-rw-r--r--remote/test/puppeteer/test/src/fixtures.spec.ts114
-rw-r--r--remote/test/puppeteer/test/src/frame.spec.ts297
-rw-r--r--remote/test/puppeteer/test/src/golden-utils.ts169
-rw-r--r--remote/test/puppeteer/test/src/headful.spec.ts91
-rw-r--r--remote/test/puppeteer/test/src/idle_override.spec.ts79
-rw-r--r--remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts128
-rw-r--r--remote/test/puppeteer/test/src/injected.spec.ts49
-rw-r--r--remote/test/puppeteer/test/src/input.spec.ts394
-rw-r--r--remote/test/puppeteer/test/src/jshandle.spec.ts373
-rw-r--r--remote/test/puppeteer/test/src/keyboard.spec.ts550
-rw-r--r--remote/test/puppeteer/test/src/launcher.spec.ts1025
-rw-r--r--remote/test/puppeteer/test/src/locator.spec.ts763
-rw-r--r--remote/test/puppeteer/test/src/mocha-utils.ts507
-rw-r--r--remote/test/puppeteer/test/src/mouse.spec.ts472
-rw-r--r--remote/test/puppeteer/test/src/navigation.spec.ts918
-rw-r--r--remote/test/puppeteer/test/src/network.spec.ts917
-rw-r--r--remote/test/puppeteer/test/src/oopif.spec.ts527
-rw-r--r--remote/test/puppeteer/test/src/page.spec.ts2287
-rw-r--r--remote/test/puppeteer/test/src/proxy.spec.ts236
-rw-r--r--remote/test/puppeteer/test/src/queryhandler.spec.ts653
-rw-r--r--remote/test/puppeteer/test/src/queryselector.spec.ts491
-rw-r--r--remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts969
-rw-r--r--remote/test/puppeteer/test/src/requestinterception.spec.ts920
-rw-r--r--remote/test/puppeteer/test/src/screencast.spec.ts99
-rw-r--r--remote/test/puppeteer/test/src/screenshot.spec.ts453
-rw-r--r--remote/test/puppeteer/test/src/stacktrace.spec.ts157
-rw-r--r--remote/test/puppeteer/test/src/target.spec.ts343
-rw-r--r--remote/test/puppeteer/test/src/touchscreen.spec.ts79
-rw-r--r--remote/test/puppeteer/test/src/tracing.spec.ts149
-rw-r--r--remote/test/puppeteer/test/src/utils.ts171
-rw-r--r--remote/test/puppeteer/test/src/waittask.spec.ts867
-rw-r--r--remote/test/puppeteer/test/src/worker.spec.ts109
57 files changed, 22054 insertions, 0 deletions
diff --git a/remote/test/puppeteer/test/src/accessibility.spec.ts b/remote/test/puppeteer/test/src/accessibility.spec.ts
new file mode 100644
index 0000000000..09e9c90b96
--- /dev/null
+++ b/remote/test/puppeteer/test/src/accessibility.spec.ts
@@ -0,0 +1,567 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+
+import expect from 'expect';
+import type {SerializedAXNode} from 'puppeteer-core/internal/cdp/Accessibility.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Accessibility', function () {
+ setupTestBrowserHooks();
+
+ it('should work', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <head>
+ <title>Accessibility Test</title>
+ </head>
+ <body>
+ <div>Hello World</div>
+ <h1>Inputs</h1>
+ <input placeholder="Empty input" autofocus />
+ <input placeholder="readonly input" readonly />
+ <input placeholder="disabled input" disabled />
+ <input aria-label="Input with whitespace" value=" " />
+ <input value="value only" />
+ <input aria-placeholder="placeholder" value="and a value" />
+ <div aria-hidden="true" id="desc">This is a description!</div>
+ <input aria-placeholder="placeholder" value="and a value" aria-describedby="desc" />
+ <select>
+ <option>First Option</option>
+ <option>Second Option</option>
+ </select>
+ </body>`);
+
+ await page.focus('[placeholder="Empty input"]');
+ const golden = isFirefox
+ ? {
+ role: 'document',
+ name: 'Accessibility Test',
+ children: [
+ {role: 'text leaf', name: 'Hello World'},
+ {role: 'heading', name: 'Inputs', level: 1},
+ {role: 'entry', name: 'Empty input', focused: true},
+ {role: 'entry', name: 'readonly input', readonly: true},
+ {role: 'entry', name: 'disabled input', disabled: true},
+ {role: 'entry', name: 'Input with whitespace', value: ' '},
+ {role: 'entry', name: '', value: 'value only'},
+ {role: 'entry', name: '', value: 'and a value'}, // firefox doesn't use aria-placeholder for the name
+ {
+ role: 'entry',
+ name: '',
+ value: 'and a value',
+ description: 'This is a description!',
+ }, // and here
+ {
+ role: 'combobox',
+ name: '',
+ value: 'First Option',
+ haspopup: true,
+ children: [
+ {
+ role: 'combobox option',
+ name: 'First Option',
+ selected: true,
+ },
+ {role: 'combobox option', name: 'Second Option'},
+ ],
+ },
+ ],
+ }
+ : {
+ role: 'RootWebArea',
+ name: 'Accessibility Test',
+ children: [
+ {role: 'StaticText', name: 'Hello World'},
+ {role: 'heading', name: 'Inputs', level: 1},
+ {role: 'textbox', name: 'Empty input', focused: true},
+ {role: 'textbox', name: 'readonly input', readonly: true},
+ {role: 'textbox', name: 'disabled input', disabled: true},
+ {role: 'textbox', name: 'Input with whitespace', value: ' '},
+ {role: 'textbox', name: '', value: 'value only'},
+ {role: 'textbox', name: 'placeholder', value: 'and a value'},
+ {
+ role: 'textbox',
+ name: 'placeholder',
+ value: 'and a value',
+ description: 'This is a description!',
+ },
+ {
+ role: 'combobox',
+ name: '',
+ value: 'First Option',
+ haspopup: 'menu',
+ children: [
+ {role: 'menuitem', name: 'First Option', selected: true},
+ {role: 'menuitem', name: 'Second Option'},
+ ],
+ },
+ ],
+ };
+ expect(await page.accessibility.snapshot()).toMatchObject(golden);
+ });
+ it('should report uninteresting nodes', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`<textarea>hi</textarea>`);
+ await page.focus('textarea');
+ const golden = isFirefox
+ ? {
+ role: 'entry',
+ name: '',
+ value: 'hi',
+ focused: true,
+ multiline: true,
+ children: [
+ {
+ role: 'text leaf',
+ name: 'hi',
+ },
+ ],
+ }
+ : {
+ role: 'textbox',
+ name: '',
+ value: 'hi',
+ focused: true,
+ multiline: true,
+ children: [
+ {
+ role: 'generic',
+ name: '',
+ children: [
+ {
+ role: 'StaticText',
+ name: 'hi',
+ },
+ ],
+ },
+ ],
+ };
+ expect(
+ findFocusedNode(
+ await page.accessibility.snapshot({interestingOnly: false})
+ )
+ ).toMatchObject(golden);
+ });
+ it('get snapshots while the tree is re-calculated', async () => {
+ // see https://github.com/puppeteer/puppeteer/issues/9404
+ const {page} = await getTestState();
+
+ await page.setContent(
+ `<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Accessible name + aria-expanded puppeteer bug</title>
+ <style>
+ [aria-expanded="false"] + * {
+ display: none;
+ }
+ </style>
+ </head>
+ <body>
+ <button hidden>Show</button>
+ <p>Some content</p>
+ <script>
+ const button = document.querySelector('button');
+ button.removeAttribute('hidden')
+ button.setAttribute('aria-expanded', 'false');
+ button.addEventListener('click', function() {
+ button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') !== 'true')
+ if (button.getAttribute('aria-expanded') == 'true') {
+ button.textContent = 'Hide'
+ } else {
+ button.textContent = 'Show'
+ }
+ })
+ </script>
+ </body>
+ </html>`
+ );
+ async function getAccessibleName(page: any, element: any) {
+ return (await page.accessibility.snapshot({root: element})).name;
+ }
+ using button = await page.$('button');
+ expect(await getAccessibleName(page, button)).toEqual('Show');
+ await button?.click();
+ await page.waitForSelector('aria/Hide');
+ });
+ it('roledescription', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div tabIndex=-1 aria-roledescription="foo">Hi</div>'
+ );
+ const snapshot = await page.accessibility.snapshot();
+ // See https://chromium-review.googlesource.com/c/chromium/src/+/3088862
+ assert(snapshot);
+ assert(snapshot.children);
+ assert(snapshot.children[0]);
+ expect(snapshot.children[0]!.roledescription).toBeUndefined();
+ });
+ it('orientation', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<a href="" role="slider" aria-orientation="vertical">11</a>'
+ );
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ assert(snapshot.children[0]);
+ expect(snapshot.children[0]!.orientation).toEqual('vertical');
+ });
+ it('autocomplete', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<input type="number" aria-autocomplete="list" />');
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ assert(snapshot.children[0]);
+ expect(snapshot.children[0]!.autocomplete).toEqual('list');
+ });
+ it('multiselectable', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div role="grid" tabIndex=-1 aria-multiselectable=true>hey</div>'
+ );
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ assert(snapshot.children[0]);
+ expect(snapshot.children[0]!.multiselectable).toEqual(true);
+ });
+ it('keyshortcuts', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div role="grid" tabIndex=-1 aria-keyshortcuts="foo">hey</div>'
+ );
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ assert(snapshot.children[0]);
+ expect(snapshot.children[0]!.keyshortcuts).toEqual('foo');
+ });
+ describe('filtering children of leaf nodes', function () {
+ it('should not report text nodes inside controls', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <div role="tablist">
+ <div role="tab" aria-selected="true"><b>Tab1</b></div>
+ <div role="tab">Tab2</div>
+ </div>`);
+ const golden = isFirefox
+ ? {
+ role: 'document',
+ name: '',
+ children: [
+ {
+ role: 'pagetab',
+ name: 'Tab1',
+ selected: true,
+ },
+ {
+ role: 'pagetab',
+ name: 'Tab2',
+ },
+ ],
+ }
+ : {
+ role: 'RootWebArea',
+ name: '',
+ children: [
+ {
+ role: 'tab',
+ name: 'Tab1',
+ selected: true,
+ },
+ {
+ role: 'tab',
+ name: 'Tab2',
+ },
+ ],
+ };
+ expect(await page.accessibility.snapshot()).toEqual(golden);
+ });
+ it('rich text editable fields should have children', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <div contenteditable="true">
+ Edit this image: <img src="fakeimage.png" alt="my fake image">
+ </div>`);
+ const golden = isFirefox
+ ? {
+ role: 'section',
+ name: '',
+ children: [
+ {
+ role: 'text leaf',
+ name: 'Edit this image:',
+ },
+ {
+ role: 'StaticText',
+ name: 'my fake image',
+ },
+ ],
+ }
+ : {
+ role: 'generic',
+ name: '',
+ value: 'Edit this image: ',
+ children: [
+ {
+ role: 'StaticText',
+ name: 'Edit this image: ',
+ },
+ {
+ role: 'image',
+ name: 'my fake image',
+ },
+ ],
+ };
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ expect(snapshot.children[0]).toMatchObject(golden);
+ });
+ it('rich text editable fields with role should have children', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <div contenteditable="true" role='textbox'>
+ Edit this image: <img src="fakeimage.png" alt="my fake image">
+ </div>`);
+ // Image node should not be exposed in contenteditable elements. See https://crbug.com/1324392.
+ const golden = isFirefox
+ ? {
+ role: 'entry',
+ name: '',
+ value: 'Edit this image: my fake image',
+ children: [
+ {
+ role: 'StaticText',
+ name: 'my fake image',
+ },
+ ],
+ }
+ : {
+ role: 'textbox',
+ name: '',
+ value: 'Edit this image: ',
+ multiline: true,
+ children: [
+ {
+ role: 'StaticText',
+ name: 'Edit this image: ',
+ },
+ ],
+ };
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ expect(snapshot.children[0]).toMatchObject(golden);
+ });
+
+ // Firefox does not support contenteditable="plaintext-only".
+ describe('plaintext contenteditable', function () {
+ it('plain text field with role should not have children', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`);
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ expect(snapshot.children[0]).toEqual({
+ role: 'textbox',
+ name: '',
+ value: 'Edit this image:',
+ multiline: true,
+ });
+ });
+ });
+ it('non editable textbox with role and tabIndex and label should not have children', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <div role="textbox" tabIndex=0 aria-checked="true" aria-label="my favorite textbox">
+ this is the inner content
+ <img alt="yo" src="fakeimg.png">
+ </div>`);
+ const golden = isFirefox
+ ? {
+ role: 'entry',
+ name: 'my favorite textbox',
+ value: 'this is the inner content yo',
+ }
+ : {
+ role: 'textbox',
+ name: 'my favorite textbox',
+ value: 'this is the inner content ',
+ };
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ expect(snapshot.children[0]).toEqual(golden);
+ });
+ it('checkbox with and tabIndex and label should not have children', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <div role="checkbox" tabIndex=0 aria-checked="true" aria-label="my favorite checkbox">
+ this is the inner content
+ <img alt="yo" src="fakeimg.png">
+ </div>`);
+ const golden = isFirefox
+ ? {
+ role: 'checkbutton',
+ name: 'my favorite checkbox',
+ checked: true,
+ }
+ : {
+ role: 'checkbox',
+ name: 'my favorite checkbox',
+ checked: true,
+ };
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ expect(snapshot.children[0]).toEqual(golden);
+ });
+ it('checkbox without label should not have children', async () => {
+ const {page, isFirefox} = await getTestState();
+
+ await page.setContent(`
+ <div role="checkbox" aria-checked="true">
+ this is the inner content
+ <img alt="yo" src="fakeimg.png">
+ </div>`);
+ const golden = isFirefox
+ ? {
+ role: 'checkbutton',
+ name: 'this is the inner content yo',
+ checked: true,
+ }
+ : {
+ role: 'checkbox',
+ name: 'this is the inner content yo',
+ checked: true,
+ };
+ const snapshot = await page.accessibility.snapshot();
+ assert(snapshot);
+ assert(snapshot.children);
+ expect(snapshot.children[0]).toEqual(golden);
+ });
+
+ describe('root option', function () {
+ it('should work a button', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<button>My Button</button>`);
+
+ using button = (await page.$('button'))!;
+ expect(await page.accessibility.snapshot({root: button})).toEqual({
+ role: 'button',
+ name: 'My Button',
+ });
+ });
+ it('should work an input', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input title="My Input" value="My Value">`);
+
+ using input = (await page.$('input'))!;
+ expect(await page.accessibility.snapshot({root: input})).toEqual({
+ role: 'textbox',
+ name: 'My Input',
+ value: 'My Value',
+ });
+ });
+ it('should work a menu', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <div role="menu" title="My Menu">
+ <div role="menuitem">First Item</div>
+ <div role="menuitem">Second Item</div>
+ <div role="menuitem">Third Item</div>
+ </div>
+ `);
+
+ using menu = (await page.$('div[role="menu"]'))!;
+ expect(await page.accessibility.snapshot({root: menu})).toEqual({
+ role: 'menu',
+ name: 'My Menu',
+ children: [
+ {role: 'menuitem', name: 'First Item'},
+ {role: 'menuitem', name: 'Second Item'},
+ {role: 'menuitem', name: 'Third Item'},
+ ],
+ orientation: 'vertical',
+ });
+ });
+ it('should return null when the element is no longer in DOM', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<button>My Button</button>`);
+ using button = (await page.$('button'))!;
+ await page.$eval('button', button => {
+ return button.remove();
+ });
+ expect(await page.accessibility.snapshot({root: button})).toEqual(null);
+ });
+ it('should support the interestingOnly option', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div><button>My Button</button></div>`);
+ using div = (await page.$('div'))!;
+ expect(await page.accessibility.snapshot({root: div})).toEqual(null);
+ expect(
+ await page.accessibility.snapshot({
+ root: div,
+ interestingOnly: false,
+ })
+ ).toMatchObject({
+ role: 'generic',
+ name: '',
+ children: [
+ {
+ role: 'button',
+ name: 'My Button',
+ children: [{role: 'StaticText', name: 'My Button'}],
+ },
+ ],
+ });
+ });
+ });
+ });
+
+ function findFocusedNode(
+ node: SerializedAXNode | null
+ ): SerializedAXNode | null {
+ if (node?.focused) {
+ return node;
+ }
+ for (const child of node?.children || []) {
+ const focusedChild = findFocusedNode(child);
+ if (focusedChild) {
+ return focusedChild;
+ }
+ }
+ return null;
+ }
+});
diff --git a/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts b/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts
new file mode 100644
index 0000000000..434d01426a
--- /dev/null
+++ b/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts
@@ -0,0 +1,721 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+
+import expect from 'expect';
+import {TimeoutError} from 'puppeteer';
+import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame, detachFrame} from './utils.js';
+
+describe('AriaQueryHandler', () => {
+ setupTestBrowserHooks();
+
+ describe('parseAriaSelector', () => {
+ it('should find button', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<button id="btn" role="button"> Submit button and some spaces </button>'
+ );
+ const expectFound = async (button: ElementHandle | null) => {
+ assert(button);
+ const id = await button.evaluate((button: Element) => {
+ return button.id;
+ });
+ expect(id).toBe('btn');
+ };
+ {
+ using button = await page.$(
+ 'aria/Submit button and some spaces[role="button"]'
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ "aria/Submit button and some spaces[role='button']"
+ );
+ await expectFound(button);
+ }
+ using button = await page.$(
+ 'aria/ Submit button and some spaces[role="button"]'
+ );
+ await expectFound(button);
+ {
+ using button = await page.$(
+ 'aria/Submit button and some spaces [role="button"]'
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ 'aria/Submit button and some spaces [ role = "button" ] '
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ 'aria/[role="button"]Submit button and some spaces'
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ 'aria/Submit button [role="button"]and some spaces'
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ 'aria/[name=" Submit button and some spaces"][role="button"]'
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ "aria/[name=' Submit button and some spaces'][role='button']"
+ );
+ await expectFound(button);
+ }
+ {
+ using button = await page.$(
+ 'aria/ignored[name="Submit button and some spaces"][role="button"]'
+ );
+ await expectFound(button);
+ await expect(page.$('aria/smth[smth="true"]')).rejects.toThrow(
+ 'Unknown aria attribute "smth" in selector'
+ );
+ }
+ });
+ });
+
+ describe('queryOne', () => {
+ it('should find button by role', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div id="div"><button id="btn" role="button">Submit</button></div>'
+ );
+ using button = (await page.$(
+ 'aria/[role="button"]'
+ )) as ElementHandle<HTMLButtonElement>;
+ const id = await button!.evaluate(button => {
+ return button.id;
+ });
+ expect(id).toBe('btn');
+ });
+
+ it('should find button by name and role', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div id="div"><button id="btn" role="button">Submit</button></div>'
+ );
+ using button = (await page.$(
+ 'aria/Submit[role="button"]'
+ )) as ElementHandle<HTMLButtonElement>;
+ const id = await button!.evaluate(button => {
+ return button.id;
+ });
+ expect(id).toBe('btn');
+ });
+
+ it('should find first matching element', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ `
+ <div role="menu" id="mnu1" aria-label="menu div"></div>
+ <div role="menu" id="mnu2" aria-label="menu div"></div>
+ `
+ );
+ using div = (await page.$(
+ 'aria/menu div'
+ )) as ElementHandle<HTMLDivElement>;
+ const id = await div!.evaluate(div => {
+ return div.id;
+ });
+ expect(id).toBe('mnu1');
+ });
+
+ it('should find by name', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ `
+ <div role="menu" id="mnu1" aria-label="menu-label1">menu div</div>
+ <div role="menu" id="mnu2" aria-label="menu-label2">menu div</div>
+ `
+ );
+ using menu = (await page.$(
+ 'aria/menu-label1'
+ )) as ElementHandle<HTMLDivElement>;
+ const id = await menu!.evaluate(div => {
+ return div.id;
+ });
+ expect(id).toBe('mnu1');
+ });
+
+ it('should find 2nd element by name', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ `
+ <div role="menu" id="mnu1" aria-label="menu-label1">menu div</div>
+ <div role="menu" id="mnu2" aria-label="menu-label2">menu div</div>
+ `
+ );
+ using menu = (await page.$(
+ 'aria/menu-label2'
+ )) as ElementHandle<HTMLDivElement>;
+ const id = await menu!.evaluate(div => {
+ return div.id;
+ });
+ expect(id).toBe('mnu2');
+ });
+ });
+
+ describe('queryAll', () => {
+ it('should find menu by name', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ `
+ <div role="menu" id="mnu1" aria-label="menu div"></div>
+ <div role="menu" id="mnu2" aria-label="menu div"></div>
+ `
+ );
+ const divs = (await page.$$('aria/menu div')) as Array<
+ ElementHandle<HTMLDivElement>
+ >;
+ const ids = await Promise.all(
+ divs.map(n => {
+ return n.evaluate(div => {
+ return div.id;
+ });
+ })
+ );
+ expect(ids.join(', ')).toBe('mnu1, mnu2');
+ });
+ });
+ describe('queryAllArray', () => {
+ it('$$eval should handle many elements', async function () {
+ this.timeout(40_000);
+
+ const {page} = await getTestState();
+ await page.setContent('');
+ await page.evaluate(
+ `
+ for (var i = 0; i <= 10000; i++) {
+ const button = document.createElement('button');
+ button.textContent = i;
+ document.body.appendChild(button);
+ }
+ `
+ );
+ const sum = await page.$$eval('aria/[role="button"]', buttons => {
+ return buttons.reduce((acc, button) => {
+ return acc + Number(button.textContent);
+ }, 0);
+ });
+ expect(sum).toBe(50005000);
+ });
+ });
+
+ describe('waitForSelector (aria)', function () {
+ const addElement = (tag: string) => {
+ return document.body.appendChild(document.createElement(tag));
+ };
+
+ it('should immediately resolve promise if node exists', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(addElement, 'button');
+ await page.waitForSelector('aria/[role="button"]');
+ });
+
+ it('should work for ElementHandle.waitForSelector', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ return (document.body.innerHTML = `<div><button>test</button></div>`);
+ });
+ using element = (await page.$('div'))!;
+ await element!.waitForSelector('aria/test');
+ });
+
+ it('should persist query handler bindings across reloads', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(addElement, 'button');
+ await page.waitForSelector('aria/[role="button"]');
+ await page.reload();
+ await page.evaluate(addElement, 'button');
+ await page.waitForSelector('aria/[role="button"]');
+ });
+
+ it('should persist query handler bindings across navigations', async () => {
+ const {page, server} = await getTestState();
+
+ // Reset page but make sure that execution context ids start with 1.
+ await page.goto('data:text/html,');
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(addElement, 'button');
+ await page.waitForSelector('aria/[role="button"]');
+
+ // Reset page but again make sure that execution context ids start with 1.
+ await page.goto('data:text/html,');
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(addElement, 'button');
+ await page.waitForSelector('aria/[role="button"]');
+ });
+
+ it('should work independently of `exposeFunction`', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.exposeFunction('ariaQuerySelector', (a: number, b: number) => {
+ return a + b;
+ });
+ await page.evaluate(addElement, 'button');
+ await page.waitForSelector('aria/[role="button"]');
+ const result = await page.evaluate('globalThis.ariaQuerySelector(2,8)');
+ expect(result).toBe(10);
+ });
+
+ it('should work with removed MutationObserver', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ // @ts-expect-error This is the point of the test.
+ return delete window.MutationObserver;
+ });
+ const [handle] = await Promise.all([
+ page.waitForSelector('aria/anything'),
+ page.setContent(`<h1>anything</h1>`),
+ ]);
+ assert(handle);
+ expect(
+ await page.evaluate(x => {
+ return x.textContent;
+ }, handle)
+ ).toBe('anything');
+ });
+
+ it('should resolve promise when node is added', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const frame = page.mainFrame();
+ const watchdog = frame.waitForSelector('aria/[role="heading"]');
+ await frame.evaluate(addElement, 'br');
+ await frame.evaluate(addElement, 'h1');
+ using elementHandle = (await watchdog)!;
+ const tagName = await (
+ await elementHandle.getProperty('tagName')
+ ).jsonValue();
+ expect(tagName).toBe('H1');
+ });
+
+ it('should work when node is added through innerHTML', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const watchdog = page.waitForSelector('aria/name');
+ await page.evaluate(addElement, 'span');
+ await page.evaluate(() => {
+ return (document.querySelector('span')!.innerHTML =
+ '<h3><div aria-label="name"></div></h3>');
+ });
+ await watchdog;
+ });
+
+ it('Page.waitForSelector is shortcut for main frame', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const otherFrame = page.frames()[1];
+ const watchdog = page.waitForSelector('aria/[role="button"]');
+ await otherFrame!.evaluate(addElement, 'button');
+ await page.evaluate(addElement, 'button');
+ using elementHandle = await watchdog;
+ expect(elementHandle!.frame).toBe(page.mainFrame());
+ });
+
+ it('should run in specified frame', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await attachFrame(page, 'frame2', server.EMPTY_PAGE);
+ const frame1 = page.frames()[1];
+ const frame2 = page.frames()[2];
+ const waitForSelectorPromise = frame2!.waitForSelector(
+ 'aria/[role="button"]'
+ );
+ await frame1!.evaluate(addElement, 'button');
+ await frame2!.evaluate(addElement, 'button');
+ using elementHandle = await waitForSelectorPromise;
+ expect(elementHandle!.frame).toBe(frame2);
+ });
+
+ it('should throw when frame is detached', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const frame = page.frames()[1];
+ let waitError!: Error;
+ const waitPromise = frame!
+ .waitForSelector('aria/does-not-exist')
+ .catch(error => {
+ return (waitError = error);
+ });
+ await detachFrame(page, 'frame1');
+ await waitPromise;
+ expect(waitError).toBeTruthy();
+ expect(waitError.message).toContain(
+ 'waitForFunction failed: frame got detached.'
+ );
+ });
+
+ it('should survive cross-process navigation', async () => {
+ const {page, server} = await getTestState();
+
+ let imgFound = false;
+ const waitForSelector = page
+ .waitForSelector('aria/[role="image"]')
+ .then(() => {
+ return (imgFound = true);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(imgFound).toBe(false);
+ await page.reload();
+ expect(imgFound).toBe(false);
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html');
+ await waitForSelector;
+ expect(imgFound).toBe(true);
+ });
+
+ it('should wait for visible', async () => {
+ const {page} = await getTestState();
+
+ let divFound = false;
+ const waitForSelector = page
+ .waitForSelector('aria/name', {visible: true})
+ .then(() => {
+ return (divFound = true);
+ });
+ await page.setContent(
+ `<div aria-label='name' style='display: none; visibility: hidden;'>1</div>`
+ );
+ expect(divFound).toBe(false);
+ await page.evaluate(() => {
+ return document.querySelector('div')!.style.removeProperty('display');
+ });
+ expect(divFound).toBe(false);
+ await page.evaluate(() => {
+ return document
+ .querySelector('div')!
+ .style.removeProperty('visibility');
+ });
+ expect(await waitForSelector).toBe(true);
+ expect(divFound).toBe(true);
+ });
+
+ it('should wait for visible recursively', async () => {
+ const {page} = await getTestState();
+
+ let divVisible = false;
+ const waitForSelector = page
+ .waitForSelector('aria/inner', {visible: true})
+ .then(() => {
+ return (divVisible = true);
+ })
+ .catch(() => {
+ return (divVisible = false);
+ });
+ await page.setContent(
+ `<div style='display: none; visibility: hidden;'><div aria-label="inner">hi</div></div>`
+ );
+ expect(divVisible).toBe(false);
+ await page.evaluate(() => {
+ return document.querySelector('div')!.style.removeProperty('display');
+ });
+ expect(divVisible).toBe(false);
+ await page.evaluate(() => {
+ return document
+ .querySelector('div')!
+ .style.removeProperty('visibility');
+ });
+ expect(await waitForSelector).toBe(true);
+ expect(divVisible).toBe(true);
+ });
+
+ it('hidden should wait for visibility: hidden', async () => {
+ const {page} = await getTestState();
+
+ let divHidden = false;
+ await page.setContent(
+ `<div role='button' style='display: block;'>text</div>`
+ );
+ const waitForSelector = page
+ .waitForSelector('aria/[role="button"]', {hidden: true})
+ .then(() => {
+ return (divHidden = true);
+ })
+ .catch(() => {
+ return (divHidden = false);
+ });
+ await page.waitForSelector('aria/[role="button"]'); // do a round trip
+ expect(divHidden).toBe(false);
+ await page.evaluate(() => {
+ return document
+ .querySelector('div')!
+ .style.setProperty('visibility', 'hidden');
+ });
+ expect(await waitForSelector).toBe(true);
+ expect(divHidden).toBe(true);
+ });
+
+ it('hidden should wait for display: none', async () => {
+ const {page} = await getTestState();
+
+ let divHidden = false;
+ await page.setContent(
+ `<div role='main' style='display: block;'>text</div>`
+ );
+ const waitForSelector = page
+ .waitForSelector('aria/[role="main"]', {hidden: true})
+ .then(() => {
+ return (divHidden = true);
+ })
+ .catch(() => {
+ return (divHidden = false);
+ });
+ await page.waitForSelector('aria/[role="main"]'); // do a round trip
+ expect(divHidden).toBe(false);
+ await page.evaluate(() => {
+ return document
+ .querySelector('div')!
+ .style.setProperty('display', 'none');
+ });
+ expect(await waitForSelector).toBe(true);
+ expect(divHidden).toBe(true);
+ });
+
+ it('hidden should wait for removal', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div role='main'>text</div>`);
+ let divRemoved = false;
+ const waitForSelector = page
+ .waitForSelector('aria/[role="main"]', {hidden: true})
+ .then(() => {
+ return (divRemoved = true);
+ })
+ .catch(() => {
+ return (divRemoved = false);
+ });
+ await page.waitForSelector('aria/[role="main"]'); // do a round trip
+ expect(divRemoved).toBe(false);
+ await page.evaluate(() => {
+ return document.querySelector('div')!.remove();
+ });
+ expect(await waitForSelector).toBe(true);
+ expect(divRemoved).toBe(true);
+ });
+
+ it('should return null if waiting to hide non-existing element', async () => {
+ const {page} = await getTestState();
+
+ using handle = await page.waitForSelector('aria/non-existing', {
+ hidden: true,
+ });
+ expect(handle).toBe(null);
+ });
+
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ const error = await page
+ .waitForSelector('aria/[role="button"]', {
+ timeout: 10,
+ })
+ .catch(error => {
+ return error;
+ });
+ expect(error.message).toContain(
+ 'Waiting for selector `[role="button"]` failed: Waiting failed: 10ms exceeded'
+ );
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+
+ it('should have an error message specifically for awaiting an element to be hidden', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div role='main'>text</div>`);
+ const promise = page.waitForSelector('aria/[role="main"]', {
+ hidden: true,
+ timeout: 10,
+ });
+ await expect(promise).rejects.toMatchObject({
+ message:
+ 'Waiting for selector `[role="main"]` failed: Waiting failed: 10ms exceeded',
+ });
+ });
+
+ it('should respond to node attribute mutation', async () => {
+ const {page} = await getTestState();
+
+ let divFound = false;
+ const waitForSelector = page
+ .waitForSelector('aria/zombo')
+ .then(() => {
+ return (divFound = true);
+ })
+ .catch(() => {
+ return (divFound = false);
+ });
+ await page.setContent(`<div aria-label='notZombo'></div>`);
+ expect(divFound).toBe(false);
+ await page.evaluate(() => {
+ return document
+ .querySelector('div')!
+ .setAttribute('aria-label', 'zombo');
+ });
+ expect(await waitForSelector).toBe(true);
+ });
+
+ it('should return the element handle', async () => {
+ const {page} = await getTestState();
+
+ const waitForSelector = page.waitForSelector('aria/zombo').catch(err => {
+ return err;
+ });
+ await page.setContent(`<div aria-label='zombo'>anything</div>`);
+ expect(
+ await page.evaluate(
+ x => {
+ return x?.textContent;
+ },
+ await waitForSelector
+ )
+ ).toBe('anything');
+ });
+
+ it('should have correct stack trace for timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.waitForSelector('aria/zombo', {timeout: 10}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error!.stack).toContain(
+ 'Waiting for selector `zombo` failed: Waiting failed: 10ms exceeded'
+ );
+ });
+ });
+
+ describe('queryOne (Chromium web test)', () => {
+ async function setupPage(): ReturnType<typeof getTestState> {
+ const state = await getTestState();
+ await state.page.setContent(
+ `
+ <h2 id="shown">title</h2>
+ <h2 id="hidden" aria-hidden="true">title</h2>
+ <div id="node1" aria-labeledby="node2"></div>
+ <div id="node2" aria-label="bar"></div>
+ <div id="node3" aria-label="foo"></div>
+ <div id="node4" class="container">
+ <div id="node5" role="button" aria-label="foo"></div>
+ <div id="node6" role="button" aria-label="foo"></div>
+ <!-- Accessible name not available when element is hidden -->
+ <div id="node7" hidden role="button" aria-label="foo"></div>
+ <div id="node8" role="button" aria-label="bar"></div>
+ </div>
+ <button id="node10">text content</button>
+ <h1 id="node11">text content</h1>
+ <!-- Accessible name not available when role is "presentation" -->
+ <h1 id="node12" role="presentation">text content</h1>
+ <!-- Elements inside shadow dom should be found -->
+ <script>
+ const div = document.createElement('div');
+ const shadowRoot = div.attachShadow({mode: 'open'});
+ const h1 = document.createElement('h1');
+ h1.textContent = 'text content';
+ h1.id = 'node13';
+ shadowRoot.appendChild(h1);
+ document.documentElement.appendChild(div);
+ </script>
+ <img id="node20" src="" alt="Accessible Name">
+ <input id="node21" type="submit" value="Accessible Name">
+ <label id="node22" for="node23">Accessible Name</label>
+ <!-- Accessible name for the <input> is "Accessible Name" -->
+ <input id="node23">
+ <div id="node24" title="Accessible Name"></div>
+ <div role="treeitem" id="node30">
+ <div role="treeitem" id="node31">
+ <div role="treeitem" id="node32">item1</div>
+ <div role="treeitem" id="node33">item2</div>
+ </div>
+ <div role="treeitem" id="node34">item3</div>
+ </div>
+ <!-- Accessible name for the <div> is "item1 item2 item3" -->
+ <div aria-describedby="node30"></div>
+ `
+ );
+ return state;
+ }
+ const getIds = async (elements: ElementHandle[]) => {
+ return await Promise.all(
+ elements.map(element => {
+ return element.evaluate((element: Element) => {
+ return element.id;
+ });
+ })
+ );
+ };
+ it('should find by name "foo"', async () => {
+ const {page} = await setupPage();
+ const found = await page.$$('aria/foo');
+ const ids = await getIds(found);
+ expect(ids).toEqual(['node3', 'node5', 'node6']);
+ });
+ it('should find by name "bar"', async () => {
+ const {page} = await setupPage();
+ const found = await page.$$('aria/bar');
+ const ids = await getIds(found);
+ expect(ids).toEqual(['node1', 'node2', 'node8']);
+ });
+ it('should find treeitem by name', async () => {
+ const {page} = await setupPage();
+ const found = await page.$$('aria/item1 item2 item3');
+ const ids = await getIds(found);
+ expect(ids).toEqual(['node30']);
+ });
+ it('should find by role "button"', async () => {
+ const {page} = await setupPage();
+ const found = (await page.$$('aria/[role="button"]')) as Array<
+ ElementHandle<HTMLButtonElement>
+ >;
+ const ids = await getIds(found);
+ expect(ids).toEqual([
+ 'node5',
+ 'node6',
+ 'node7',
+ 'node8',
+ 'node10',
+ 'node21',
+ ]);
+ });
+ it('should find by role "heading"', async () => {
+ const {page} = await setupPage();
+ const found = await page.$$('aria/[role="heading"]');
+ const ids = await getIds(found);
+ expect(ids).toEqual(['shown', 'hidden', 'node11', 'node13']);
+ });
+ it('should find both ignored and unignored', async () => {
+ const {page} = await setupPage();
+ const found = await page.$$('aria/title');
+ const ids = await getIds(found);
+ expect(ids).toEqual(['shown']);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/autofill.spec.ts b/remote/test/puppeteer/test/src/autofill.spec.ts
new file mode 100644
index 0000000000..a04e4b8e8b
--- /dev/null
+++ b/remote/test/puppeteer/test/src/autofill.spec.ts
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Autofill', function () {
+ setupTestBrowserHooks();
+ describe('ElementHandle.autofill', () => {
+ it('should fill out a credit card', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.PREFIX + '/credit-card.html');
+ using name = await page.waitForSelector('#name');
+ await name!.autofill({
+ creditCard: {
+ number: '4444444444444444',
+ name: 'John Smith',
+ expiryMonth: '01',
+ expiryYear: '2030',
+ cvc: '123',
+ },
+ });
+ expect(
+ await page.evaluate(() => {
+ const result = [];
+ for (const el of document.querySelectorAll('input')) {
+ result.push(el.value);
+ }
+ return result.join(',');
+ })
+ ).toBe('John Smith,4444444444444444,01,2030,Submit');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/browser.spec.ts b/remote/test/puppeteer/test/src/browser.spec.ts
new file mode 100644
index 0000000000..6f21af5d9a
--- /dev/null
+++ b/remote/test/puppeteer/test/src/browser.spec.ts
@@ -0,0 +1,81 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Browser specs', function () {
+ setupTestBrowserHooks();
+
+ describe('Browser.version', function () {
+ it('should return version', async () => {
+ const {browser} = await getTestState();
+
+ const version = await browser.version();
+ expect(version.length).toBeGreaterThan(0);
+ expect(version.toLowerCase()).atLeastOneToContain(['firefox', 'chrome']);
+ });
+ });
+
+ describe('Browser.userAgent', function () {
+ it('should include Browser engine', async () => {
+ const {browser, isChrome} = await getTestState();
+
+ const userAgent = await browser.userAgent();
+ expect(userAgent.length).toBeGreaterThan(0);
+ if (isChrome) {
+ expect(userAgent).toContain('WebKit');
+ } else {
+ expect(userAgent).toContain('Gecko');
+ }
+ });
+ });
+
+ describe('Browser.target', function () {
+ it('should return browser target', async () => {
+ const {browser} = await getTestState();
+
+ const target = browser.target();
+ expect(target.type()).toBe('browser');
+ });
+ });
+
+ describe('Browser.process', function () {
+ it('should return child_process instance', async () => {
+ const {browser} = await getTestState();
+
+ const process = await browser.process();
+ expect(process!.pid).toBeGreaterThan(0);
+ });
+ it('should not return child_process for remote browser', async () => {
+ const {browser, puppeteer} = await getTestState();
+
+ const browserWSEndpoint = browser.wsEndpoint();
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint,
+ protocol: browser.protocol,
+ });
+ expect(remoteBrowser.process()).toBe(null);
+ await remoteBrowser.disconnect();
+ });
+ });
+
+ describe('Browser.isConnected', () => {
+ it('should set the browser connected state', async () => {
+ const {browser, puppeteer} = await getTestState();
+
+ const browserWSEndpoint = browser.wsEndpoint();
+ const newBrowser = await puppeteer.connect({
+ browserWSEndpoint,
+ protocol: browser.protocol,
+ });
+ expect(newBrowser.isConnected()).toBe(true);
+ await newBrowser.disconnect();
+ expect(newBrowser.isConnected()).toBe(false);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/browsercontext.spec.ts b/remote/test/puppeteer/test/src/browsercontext.spec.ts
new file mode 100644
index 0000000000..9cbbda60a4
--- /dev/null
+++ b/remote/test/puppeteer/test/src/browsercontext.spec.ts
@@ -0,0 +1,368 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {TimeoutError} from 'puppeteer';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+
+describe('BrowserContext', function () {
+ setupTestBrowserHooks();
+
+ it('should have default context', async () => {
+ const {browser} = await getTestState({
+ skipContextCreation: true,
+ });
+ expect(browser.browserContexts()).toHaveLength(1);
+ const defaultContext = browser.browserContexts()[0]!;
+ expect(defaultContext!.isIncognito()).toBe(false);
+ let error!: Error;
+ await defaultContext!.close().catch(error_ => {
+ return (error = error_);
+ });
+ expect(browser.defaultBrowserContext()).toBe(defaultContext);
+ expect(error.message).toContain('cannot be closed');
+ });
+ it('should create new incognito context', async () => {
+ const {browser} = await getTestState({
+ skipContextCreation: true,
+ });
+
+ expect(browser.browserContexts()).toHaveLength(1);
+ const context = await browser.createIncognitoBrowserContext();
+ expect(context.isIncognito()).toBe(true);
+ expect(browser.browserContexts()).toHaveLength(2);
+ expect(browser.browserContexts().indexOf(context) !== -1).toBe(true);
+ await context.close();
+ expect(browser.browserContexts()).toHaveLength(1);
+ });
+ it('should close all belonging targets once closing context', async () => {
+ const {browser} = await getTestState({
+ skipContextCreation: true,
+ });
+
+ expect(await browser.pages()).toHaveLength(1);
+
+ const context = await browser.createIncognitoBrowserContext();
+ await context.newPage();
+ expect(await browser.pages()).toHaveLength(2);
+ expect(await context.pages()).toHaveLength(1);
+
+ await context.close();
+ expect(await browser.pages()).toHaveLength(1);
+ });
+ it('window.open should use parent tab context', async () => {
+ const {browser, server, page, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [popupTarget] = await Promise.all([
+ waitEvent(browser, 'targetcreated'),
+ page.evaluate(url => {
+ return window.open(url);
+ }, server.EMPTY_PAGE),
+ ]);
+ expect(popupTarget.browserContext()).toBe(context);
+ });
+ it('should fire target events', async () => {
+ const {server, context} = await getTestState();
+
+ const events: string[] = [];
+ context.on('targetcreated', target => {
+ events.push('CREATED: ' + target.url());
+ });
+ context.on('targetchanged', target => {
+ events.push('CHANGED: ' + target.url());
+ });
+ context.on('targetdestroyed', target => {
+ events.push('DESTROYED: ' + target.url());
+ });
+ const page = await context.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.close();
+ expect(events).toEqual([
+ 'CREATED: about:blank',
+ `CHANGED: ${server.EMPTY_PAGE}`,
+ `DESTROYED: ${server.EMPTY_PAGE}`,
+ ]);
+ });
+ it('should wait for a target', async () => {
+ const {server, context} = await getTestState();
+
+ let resolved = false;
+
+ const targetPromise = context.waitForTarget(target => {
+ return target.url() === server.EMPTY_PAGE;
+ });
+ targetPromise
+ .then(() => {
+ return (resolved = true);
+ })
+ .catch(error => {
+ resolved = true;
+ if (error instanceof TimeoutError) {
+ console.error(error);
+ } else {
+ throw error;
+ }
+ });
+ const page = await context.newPage();
+ expect(resolved).toBe(false);
+ await page.goto(server.EMPTY_PAGE);
+ try {
+ const target = await targetPromise;
+ expect(await target.page()).toBe(page);
+ } catch (error) {
+ if (error instanceof TimeoutError) {
+ console.error(error);
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should timeout waiting for a non-existent target', async () => {
+ const {browser, server} = await getTestState();
+
+ const context = await browser.createIncognitoBrowserContext();
+ const error = await context
+ .waitForTarget(
+ target => {
+ return target.url() === server.EMPTY_PAGE;
+ },
+ {
+ timeout: 1,
+ }
+ )
+ .catch(error_ => {
+ return error_;
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ await context.close();
+ });
+
+ it('should isolate localStorage and cookies', async () => {
+ const {browser, server} = await getTestState({
+ skipContextCreation: true,
+ });
+
+ // Create two incognito contexts.
+ const context1 = await browser.createIncognitoBrowserContext();
+ const context2 = await browser.createIncognitoBrowserContext();
+ expect(context1.targets()).toHaveLength(0);
+ expect(context2.targets()).toHaveLength(0);
+
+ // Create a page in first incognito context.
+ const page1 = await context1.newPage();
+ await page1.goto(server.EMPTY_PAGE);
+ await page1.evaluate(() => {
+ localStorage.setItem('name', 'page1');
+ document.cookie = 'name=page1';
+ });
+
+ expect(context1.targets()).toHaveLength(1);
+ expect(context2.targets()).toHaveLength(0);
+
+ // Create a page in second incognito context.
+ const page2 = await context2.newPage();
+ await page2.goto(server.EMPTY_PAGE);
+ await page2.evaluate(() => {
+ localStorage.setItem('name', 'page2');
+ document.cookie = 'name=page2';
+ });
+
+ expect(context1.targets()).toHaveLength(1);
+ expect(context1.targets()[0]).toBe(page1.target());
+ expect(context2.targets()).toHaveLength(1);
+ expect(context2.targets()[0]).toBe(page2.target());
+
+ // Make sure pages don't share localstorage or cookies.
+ expect(
+ await page1.evaluate(() => {
+ return localStorage.getItem('name');
+ })
+ ).toBe('page1');
+ expect(
+ await page1.evaluate(() => {
+ return document.cookie;
+ })
+ ).toBe('name=page1');
+ expect(
+ await page2.evaluate(() => {
+ return localStorage.getItem('name');
+ })
+ ).toBe('page2');
+ expect(
+ await page2.evaluate(() => {
+ return document.cookie;
+ })
+ ).toBe('name=page2');
+
+ // Cleanup contexts.
+ await Promise.all([context1.close(), context2.close()]);
+ expect(browser.browserContexts()).toHaveLength(1);
+ });
+
+ it('should work across sessions', async () => {
+ const {browser, puppeteer} = await getTestState({
+ skipContextCreation: true,
+ });
+
+ expect(browser.browserContexts()).toHaveLength(1);
+ const context = await browser.createIncognitoBrowserContext();
+ expect(browser.browserContexts()).toHaveLength(2);
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ const contexts = remoteBrowser.browserContexts();
+ expect(contexts).toHaveLength(2);
+ await remoteBrowser.disconnect();
+ await context.close();
+ });
+
+ it('should provide a context id', async () => {
+ const {browser} = await getTestState({
+ skipContextCreation: true,
+ });
+
+ expect(browser.browserContexts()).toHaveLength(1);
+ expect(browser.browserContexts()[0]!.id).toBeUndefined();
+
+ const context = await browser.createIncognitoBrowserContext();
+ expect(browser.browserContexts()).toHaveLength(2);
+ expect(browser.browserContexts()[1]!.id).toBeDefined();
+ await context.close();
+ });
+
+ describe('BrowserContext.overridePermissions', function () {
+ function getPermission(page: Page, name: PermissionName) {
+ return page.evaluate(name => {
+ return navigator.permissions.query({name}).then(result => {
+ return result.state;
+ });
+ }, name);
+ }
+
+ it('should be prompt by default', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(await getPermission(page, 'geolocation')).toBe('prompt');
+ });
+ it('should deny permission when not listed', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await context.overridePermissions(server.EMPTY_PAGE, []);
+ expect(await getPermission(page, 'geolocation')).toBe('denied');
+ });
+ it('should fail when bad permission is given', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let error!: Error;
+ await context
+ // @ts-expect-error purposeful bad input for test
+ .overridePermissions(server.EMPTY_PAGE, ['foo'])
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Unknown permission: foo');
+ });
+ it('should grant permission when listed', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']);
+ expect(await getPermission(page, 'geolocation')).toBe('granted');
+ });
+ it('should reset permissions', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']);
+ expect(await getPermission(page, 'geolocation')).toBe('granted');
+ await context.clearPermissionOverrides();
+ expect(await getPermission(page, 'geolocation')).toBe('prompt');
+ });
+ it('should trigger permission onchange', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ (globalThis as any).events = [];
+ return navigator.permissions
+ .query({name: 'geolocation'})
+ .then(function (result) {
+ (globalThis as any).events.push(result.state);
+ result.onchange = function () {
+ (globalThis as any).events.push(result.state);
+ };
+ });
+ });
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).events;
+ })
+ ).toEqual(['prompt']);
+ await context.overridePermissions(server.EMPTY_PAGE, []);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).events;
+ })
+ ).toEqual(['prompt', 'denied']);
+ await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).events;
+ })
+ ).toEqual(['prompt', 'denied', 'granted']);
+ await context.clearPermissionOverrides();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).events;
+ })
+ ).toEqual(['prompt', 'denied', 'granted', 'prompt']);
+ });
+ it('should isolate permissions between browser contexts', async () => {
+ const {page, server, context, browser} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const otherContext = await browser.createIncognitoBrowserContext();
+ const otherPage = await otherContext.newPage();
+ await otherPage.goto(server.EMPTY_PAGE);
+ expect(await getPermission(page, 'geolocation')).toBe('prompt');
+ expect(await getPermission(otherPage, 'geolocation')).toBe('prompt');
+
+ await context.overridePermissions(server.EMPTY_PAGE, []);
+ await otherContext.overridePermissions(server.EMPTY_PAGE, [
+ 'geolocation',
+ ]);
+ expect(await getPermission(page, 'geolocation')).toBe('denied');
+ expect(await getPermission(otherPage, 'geolocation')).toBe('granted');
+
+ await context.clearPermissionOverrides();
+ expect(await getPermission(page, 'geolocation')).toBe('prompt');
+ expect(await getPermission(otherPage, 'geolocation')).toBe('granted');
+
+ await otherContext.close();
+ });
+ it('should grant persistent-storage', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(await getPermission(page, 'persistent-storage')).not.toBe(
+ 'granted'
+ );
+ await context.overridePermissions(server.EMPTY_PAGE, [
+ 'persistent-storage',
+ ]);
+ expect(await getPermission(page, 'persistent-storage')).toBe('granted');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/CDPSession.spec.ts b/remote/test/puppeteer/test/src/cdp/CDPSession.spec.ts
new file mode 100644
index 0000000000..2000c0e435
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/CDPSession.spec.ts
@@ -0,0 +1,147 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import type {Target} from 'puppeteer-core/internal/api/Target.js';
+import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js';
+
+import {getTestState, setupTestBrowserHooks} from '../mocha-utils.js';
+import {waitEvent} from '../utils.js';
+
+describe('Target.createCDPSession', function () {
+ setupTestBrowserHooks();
+
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const client = await page.createCDPSession();
+
+ await Promise.all([
+ client.send('Runtime.enable'),
+ client.send('Runtime.evaluate', {expression: 'window.foo = "bar"'}),
+ ]);
+ const foo = await page.evaluate(() => {
+ return (globalThis as any).foo;
+ });
+ expect(foo).toBe('bar');
+ });
+
+ it('should not report created targets for custom CDP sessions', async () => {
+ const {browser} = await getTestState();
+ let called = 0;
+ const handler = async (target: Target) => {
+ called++;
+ if (called > 1) {
+ throw new Error('Too many targets created');
+ }
+ await target.createCDPSession();
+ };
+ browser.browserContexts()[0]!.on('targetcreated', handler);
+ await browser.newPage();
+ browser.browserContexts()[0]!.off('targetcreated', handler);
+ });
+
+ it('should send events', async () => {
+ const {page, server} = await getTestState();
+
+ const client = await page.createCDPSession();
+ await client.send('Network.enable');
+ const events: unknown[] = [];
+ client.on('Network.requestWillBeSent', event => {
+ events.push(event);
+ });
+ await Promise.all([
+ waitEvent(client, 'Network.requestWillBeSent'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(events).toHaveLength(1);
+ });
+ it('should enable and disable domains independently', async () => {
+ const {page} = await getTestState();
+
+ const client = await page.createCDPSession();
+ await client.send('Runtime.enable');
+ await client.send('Debugger.enable');
+ // JS coverage enables and then disables Debugger domain.
+ await page.coverage.startJSCoverage();
+ await page.coverage.stopJSCoverage();
+ // generate a script in page and wait for the event.
+ const [event] = await Promise.all([
+ waitEvent(client, 'Debugger.scriptParsed'),
+ page.evaluate('//# sourceURL=foo.js'),
+ ]);
+ // expect events to be dispatched.
+ expect(event.url).toBe('foo.js');
+ });
+ it('should be able to detach session', async () => {
+ const {page} = await getTestState();
+
+ const client = await page.createCDPSession();
+ await client.send('Runtime.enable');
+ const evalResponse = await client.send('Runtime.evaluate', {
+ expression: '1 + 2',
+ returnByValue: true,
+ });
+ expect(evalResponse.result.value).toBe(3);
+ await client.detach();
+ let error!: Error;
+ try {
+ await client.send('Runtime.evaluate', {
+ expression: '3 + 1',
+ returnByValue: true,
+ });
+ } catch (error_) {
+ if (isErrorLike(error_)) {
+ error = error_ as Error;
+ }
+ }
+ expect(error.message).toContain('Session closed.');
+ });
+ it('should throw nice errors', async () => {
+ const {page} = await getTestState();
+
+ const client = await page.createCDPSession();
+ const error = await theSourceOfTheProblems().catch(error => {
+ return error;
+ });
+ expect(error.stack).toContain('theSourceOfTheProblems');
+ expect(error.message).toContain('ThisCommand.DoesNotExist');
+
+ async function theSourceOfTheProblems() {
+ // @ts-expect-error This fails in TS as it knows that command does not
+ // exist but we want to have this tests for our users who consume in JS
+ // not TS.
+ await client.send('ThisCommand.DoesNotExist');
+ }
+ });
+
+ it('should respect custom timeout', async () => {
+ const {page} = await getTestState();
+
+ const client = await page.createCDPSession();
+ await expect(
+ client.send(
+ 'Runtime.evaluate',
+ {
+ expression: 'new Promise(resolve => {})',
+ awaitPromise: true,
+ },
+ {
+ timeout: 50,
+ }
+ )
+ ).rejects.toThrowError(
+ `Runtime.evaluate timed out. Increase the 'protocolTimeout' setting in launch/connect calls for a higher timeout if needed.`
+ );
+ });
+
+ it('should expose the underlying connection', async () => {
+ const {page} = await getTestState();
+
+ const client = await page.createCDPSession();
+ expect(client.connection()).toBeTruthy();
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/TargetManager.spec.ts b/remote/test/puppeteer/test/src/cdp/TargetManager.spec.ts
new file mode 100644
index 0000000000..d1f8992530
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/TargetManager.spec.ts
@@ -0,0 +1,96 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import type {CdpBrowser} from 'puppeteer-core/internal/cdp/Browser.js';
+
+import {getTestState, launch} from '../mocha-utils.js';
+import {attachFrame} from '../utils.js';
+
+describe('TargetManager', () => {
+ /* We use a special browser for this test as we need the --site-per-process flag */
+ let state: Awaited<ReturnType<typeof launch>> & {
+ browser: CdpBrowser;
+ };
+
+ beforeEach(async () => {
+ const {defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+ state = (await launch(
+ Object.assign({}, defaultBrowserOptions, {
+ args: (defaultBrowserOptions.args || []).concat([
+ '--site-per-process',
+ '--remote-debugging-port=21222',
+ '--host-rules=MAP * 127.0.0.1',
+ ]),
+ }),
+ {createPage: false}
+ )) as Awaited<ReturnType<typeof launch>> & {
+ browser: CdpBrowser;
+ };
+ });
+
+ afterEach(async () => {
+ await state.close();
+ });
+
+ // CDP-specific test.
+ it('should handle targets', async () => {
+ const {server, context, browser} = state;
+
+ const targetManager = browser._targetManager();
+ expect(targetManager.getAvailableTargets().size).toBe(3);
+
+ expect(await context.pages()).toHaveLength(0);
+ expect(targetManager.getAvailableTargets().size).toBe(3);
+
+ const page = await context.newPage();
+ expect(await context.pages()).toHaveLength(1);
+ expect(targetManager.getAvailableTargets().size).toBe(5);
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(await context.pages()).toHaveLength(1);
+ expect(targetManager.getAvailableTargets().size).toBe(5);
+
+ // attach a local iframe.
+ let framePromise = page.waitForFrame(frame => {
+ return frame.url().endsWith('/empty.html');
+ });
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await framePromise;
+ expect(await context.pages()).toHaveLength(1);
+ expect(targetManager.getAvailableTargets().size).toBe(5);
+ expect(page.frames()).toHaveLength(2);
+
+ // // attach a remote frame iframe.
+ framePromise = page.waitForFrame(frame => {
+ return frame.url() === server.CROSS_PROCESS_PREFIX + '/empty.html';
+ });
+ await attachFrame(
+ page,
+ 'frame2',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ await framePromise;
+ expect(await context.pages()).toHaveLength(1);
+ expect(targetManager.getAvailableTargets().size).toBe(6);
+ expect(page.frames()).toHaveLength(3);
+
+ framePromise = page.waitForFrame(frame => {
+ return frame.url() === server.CROSS_PROCESS_PREFIX + '/empty.html';
+ });
+ await attachFrame(
+ page,
+ 'frame3',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ await framePromise;
+ expect(await context.pages()).toHaveLength(1);
+ expect(targetManager.getAvailableTargets().size).toBe(7);
+ expect(page.frames()).toHaveLength(4);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/bfcache.spec.ts b/remote/test/puppeteer/test/src/cdp/bfcache.spec.ts
new file mode 100644
index 0000000000..211f93cd6b
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/bfcache.spec.ts
@@ -0,0 +1,65 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {PageEvent} from 'puppeteer-core';
+
+import {launch} from '../mocha-utils.js';
+import {waitEvent} from '../utils.js';
+
+describe('BFCache', function () {
+ it('can navigate to a BFCached page', async () => {
+ const {httpsServer, page, close} = await launch({
+ ignoreHTTPSErrors: true,
+ });
+
+ try {
+ page.setDefaultTimeout(3000);
+
+ await page.goto(httpsServer.PREFIX + '/cached/bfcache/index.html');
+
+ await Promise.all([page.waitForNavigation(), page.locator('a').click()]);
+
+ expect(page.url()).toContain('target.html');
+
+ await Promise.all([page.waitForNavigation(), page.goBack()]);
+
+ expect(
+ await page.evaluate(() => {
+ return document.body.innerText;
+ })
+ ).toBe('BFCachednext');
+ } finally {
+ await close();
+ }
+ });
+
+ it('can navigate to a BFCached page containing an OOPIF and a worker', async () => {
+ const {httpsServer, page, close} = await launch({
+ ignoreHTTPSErrors: true,
+ });
+ try {
+ page.setDefaultTimeout(3000);
+ const [worker1] = await Promise.all([
+ waitEvent(page, PageEvent.WorkerCreated),
+ page.goto(
+ httpsServer.PREFIX + '/cached/bfcache/worker-iframe-container.html'
+ ),
+ ]);
+ expect(await worker1.evaluate('1 + 1')).toBe(2);
+ await Promise.all([page.waitForNavigation(), page.locator('a').click()]);
+
+ const [worker2] = await Promise.all([
+ waitEvent(page, PageEvent.WorkerCreated),
+ page.waitForNavigation(),
+ page.goBack(),
+ ]);
+ expect(await worker2.evaluate('1 + 1')).toBe(2);
+ } finally {
+ await close();
+ }
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/devtools.spec.ts b/remote/test/puppeteer/test/src/cdp/devtools.spec.ts
new file mode 100644
index 0000000000..c158481af2
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/devtools.spec.ts
@@ -0,0 +1,123 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import type {PuppeteerLaunchOptions} from 'puppeteer-core/internal/node/PuppeteerNode.js';
+
+import {getTestState, launch} from '../mocha-utils.js';
+
+describe('DevTools', function () {
+ /* These tests fire up an actual browser so let's
+ * allow a higher timeout
+ */
+ this.timeout(20_000);
+
+ let launchOptions: PuppeteerLaunchOptions & {
+ devtools: boolean;
+ };
+ const browsers: Array<() => Promise<void>> = [];
+
+ beforeEach(async () => {
+ const {defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+ launchOptions = Object.assign({}, defaultBrowserOptions, {
+ devtools: true,
+ });
+ });
+
+ async function launchBrowser(options: typeof launchOptions) {
+ const {browser, close} = await launch(options, {createContext: false});
+ browsers.push(close);
+ return browser;
+ }
+
+ afterEach(async () => {
+ await Promise.all(
+ browsers.map((close, index) => {
+ delete browsers[index];
+ return close();
+ })
+ );
+ });
+
+ it('target.page() should return a DevTools page if custom isPageTarget is provided', async function () {
+ const {puppeteer} = await getTestState({skipLaunch: true});
+ const originalBrowser = await launchBrowser(launchOptions);
+
+ const browserWSEndpoint = originalBrowser.wsEndpoint();
+
+ const browser = await puppeteer.connect({
+ browserWSEndpoint,
+ _isPageTarget(target) {
+ return (
+ target.type() === 'other' && target.url().startsWith('devtools://')
+ );
+ },
+ });
+ const devtoolsPageTarget = await browser.waitForTarget(target => {
+ return target.type() === 'other';
+ });
+ const page = (await devtoolsPageTarget.page())!;
+ expect(
+ await page.evaluate(() => {
+ return 2 * 3;
+ })
+ ).toBe(6);
+ expect(await browser.pages()).toContainEqual(page);
+ });
+ it('target.page() should return a DevTools page if asPage is used', async function () {
+ const {puppeteer} = await getTestState({skipLaunch: true});
+ const originalBrowser = await launchBrowser(launchOptions);
+
+ const browserWSEndpoint = originalBrowser.wsEndpoint();
+
+ const browser = await puppeteer.connect({
+ browserWSEndpoint,
+ });
+ const devtoolsPageTarget = await browser.waitForTarget(target => {
+ return target.type() === 'other';
+ });
+ const page = (await devtoolsPageTarget.asPage())!;
+ expect(
+ await page.evaluate(() => {
+ return 2 * 3;
+ })
+ ).toBe(6);
+ expect(await browser.pages()).toContainEqual(page);
+ });
+ it('should open devtools when "devtools: true" option is given', async () => {
+ const browser = await launchBrowser(
+ Object.assign({devtools: true}, launchOptions)
+ );
+ const context = await browser.createIncognitoBrowserContext();
+ await Promise.all([
+ context.newPage(),
+ browser.waitForTarget((target: {url: () => string | string[]}) => {
+ return target.url().includes('devtools://');
+ }),
+ ]);
+ await browser.close();
+ });
+ it('should expose DevTools as a page', async () => {
+ const browser = await launchBrowser(
+ Object.assign({devtools: true}, launchOptions)
+ );
+ const context = await browser.createIncognitoBrowserContext();
+ const [target] = await Promise.all([
+ browser.waitForTarget((target: {url: () => string | string[]}) => {
+ return target.url().includes('devtools://');
+ }),
+ context.newPage(),
+ ]);
+ const page = await target.page();
+ await page!.waitForFunction(() => {
+ // @ts-expect-error wrong context.
+ return Boolean(DevToolsAPI);
+ });
+ await browser.close();
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/extensions.spec.ts b/remote/test/puppeteer/test/src/cdp/extensions.spec.ts
new file mode 100644
index 0000000000..6db9f931ad
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/extensions.spec.ts
@@ -0,0 +1,120 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import path from 'path';
+
+import expect from 'expect';
+import type {PuppeteerLaunchOptions} from 'puppeteer-core/internal/node/PuppeteerNode.js';
+
+import {getTestState, launch} from '../mocha-utils.js';
+
+const extensionPath = path.join(
+ __dirname,
+ '..',
+ '..',
+ 'assets',
+ 'simple-extension'
+);
+const serviceWorkerExtensionPath = path.join(
+ __dirname,
+ '..',
+ '..',
+ 'assets',
+ 'serviceworkers',
+ 'extension'
+);
+
+describe('extensions', function () {
+ /* These tests fire up an actual browser so let's
+ * allow a higher timeout
+ */
+ this.timeout(20_000);
+
+ let extensionOptions: PuppeteerLaunchOptions & {
+ args: string[];
+ };
+ const browsers: Array<() => Promise<void>> = [];
+
+ beforeEach(async () => {
+ const {defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+
+ extensionOptions = Object.assign({}, defaultBrowserOptions, {
+ args: [
+ `--disable-extensions-except=${extensionPath}`,
+ `--load-extension=${extensionPath}`,
+ ],
+ });
+ });
+
+ async function launchBrowser(options: typeof extensionOptions) {
+ const {browser, close} = await launch(options, {createContext: false});
+ browsers.push(close);
+ return browser;
+ }
+
+ afterEach(async () => {
+ await Promise.all(
+ browsers.map((close, index) => {
+ delete browsers[index];
+ return close();
+ })
+ );
+ });
+
+ it('background_page target type should be available', async () => {
+ const browserWithExtension = await launchBrowser(extensionOptions);
+ const page = await browserWithExtension.newPage();
+ const backgroundPageTarget = await browserWithExtension.waitForTarget(
+ target => {
+ return target.type() === 'background_page';
+ }
+ );
+ await page.close();
+ await browserWithExtension.close();
+ expect(backgroundPageTarget).toBeTruthy();
+ });
+
+ it('service_worker target type should be available', async () => {
+ const browserWithExtension = await launchBrowser({
+ args: [
+ `--disable-extensions-except=${serviceWorkerExtensionPath}`,
+ `--load-extension=${serviceWorkerExtensionPath}`,
+ ],
+ });
+ const page = await browserWithExtension.newPage();
+ const serviceWorkerTarget = await browserWithExtension.waitForTarget(
+ target => {
+ return target.type() === 'service_worker';
+ }
+ );
+ await page.close();
+ await browserWithExtension.close();
+ expect(serviceWorkerTarget).toBeTruthy();
+ });
+
+ it('target.page() should return a background_page', async function () {
+ const browserWithExtension = await launchBrowser(extensionOptions);
+ const backgroundPageTarget = await browserWithExtension.waitForTarget(
+ target => {
+ return target.type() === 'background_page';
+ }
+ );
+ const page = (await backgroundPageTarget.page())!;
+ expect(
+ await page.evaluate(() => {
+ return 2 * 3;
+ })
+ ).toBe(6);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).MAGIC;
+ })
+ ).toBe(42);
+ await browserWithExtension.close();
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/prerender.spec.ts b/remote/test/puppeteer/test/src/cdp/prerender.spec.ts
new file mode 100644
index 0000000000..4e0fb30da9
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/prerender.spec.ts
@@ -0,0 +1,181 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {statSync} from 'fs';
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from '../mocha-utils.js';
+import {getUniqueVideoFilePlaceholder} from '../utils.js';
+
+describe('Prerender', function () {
+ setupTestBrowserHooks();
+
+ it('can navigate to a prerendered page via input', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.PREFIX + '/prerender/index.html');
+
+ using button = await page.waitForSelector('button');
+ await button?.click();
+
+ using link = await page.waitForSelector('a');
+ await Promise.all([page.waitForNavigation(), link?.click()]);
+ expect(
+ await page.evaluate(() => {
+ return document.body.innerText;
+ })
+ ).toBe('target');
+ });
+
+ it('can navigate to a prerendered page via Puppeteer', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.PREFIX + '/prerender/index.html');
+
+ using button = await page.waitForSelector('button');
+ await button?.click();
+
+ await page.goto(server.PREFIX + '/prerender/target.html');
+ expect(
+ await page.evaluate(() => {
+ return document.body.innerText;
+ })
+ ).toBe('target');
+ });
+
+ describe('via frame', () => {
+ it('can navigate to a prerendered page via input', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.PREFIX + '/prerender/index.html');
+
+ using button = await page.waitForSelector('button');
+ await button?.click();
+
+ const mainFrame = page.mainFrame();
+ using link = await mainFrame.waitForSelector('a');
+ await Promise.all([mainFrame.waitForNavigation(), link?.click()]);
+ expect(mainFrame).toBe(page.mainFrame());
+ expect(
+ await mainFrame.evaluate(() => {
+ return document.body.innerText;
+ })
+ ).toBe('target');
+ expect(mainFrame).toBe(page.mainFrame());
+ });
+
+ it('can navigate to a prerendered page via Puppeteer', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.PREFIX + '/prerender/index.html');
+
+ using button = await page.waitForSelector('button');
+ await button?.click();
+
+ const mainFrame = page.mainFrame();
+ await mainFrame.goto(server.PREFIX + '/prerender/target.html');
+ expect(
+ await mainFrame.evaluate(() => {
+ return document.body.innerText;
+ })
+ ).toBe('target');
+ expect(mainFrame).toBe(page.mainFrame());
+ });
+ });
+
+ it('can screencast', async () => {
+ using file = getUniqueVideoFilePlaceholder();
+
+ const {page, server} = await getTestState();
+
+ const recorder = await page.screencast({
+ path: file.filename,
+ scale: 0.5,
+ crop: {width: 100, height: 100, x: 0, y: 0},
+ speed: 0.5,
+ });
+
+ await page.goto(server.PREFIX + '/prerender/index.html');
+
+ using button = await page.waitForSelector('button');
+ await button?.click();
+
+ using link = await page.locator('a').waitHandle();
+ await Promise.all([page.waitForNavigation(), link.click()]);
+ using input = await page.locator('input').waitHandle();
+ await input.type('ab', {delay: 100});
+
+ await recorder.stop();
+
+ expect(statSync(file.filename).size).toBeGreaterThan(0);
+ });
+
+ describe('with network requests', () => {
+ it('can receive requests from the prerendered page', async () => {
+ const {page, server} = await getTestState();
+
+ const urls: string[] = [];
+ page.on('request', request => {
+ urls.push(request.url());
+ });
+
+ await page.goto(server.PREFIX + '/prerender/index.html');
+ using button = await page.waitForSelector('button');
+ await button?.click();
+ const mainFrame = page.mainFrame();
+ using link = await mainFrame.waitForSelector('a');
+ await Promise.all([mainFrame.waitForNavigation(), link?.click()]);
+ expect(mainFrame).toBe(page.mainFrame());
+ expect(
+ await mainFrame.evaluate(() => {
+ return document.body.innerText;
+ })
+ ).toBe('target');
+ expect(mainFrame).toBe(page.mainFrame());
+ expect(
+ urls.find(url => {
+ return url.endsWith('prerender/target.html');
+ })
+ ).toBeTruthy();
+ expect(
+ urls.find(url => {
+ return url.includes('prerender/index.html');
+ })
+ ).toBeTruthy();
+ expect(
+ urls.find(url => {
+ return url.includes('prerender/target.html?fromPrerendered');
+ })
+ ).toBeTruthy();
+ });
+ });
+
+ describe('with emulation', () => {
+ it('can configure viewport for prerendered pages', async () => {
+ const {page, server} = await getTestState();
+ await page.setViewport({
+ width: 300,
+ height: 400,
+ });
+ await page.goto(server.PREFIX + '/prerender/index.html');
+ using button = await page.waitForSelector('button');
+ await button?.click();
+ using link = await page.waitForSelector('a');
+ await Promise.all([page.waitForNavigation(), link?.click()]);
+ const result = await page.evaluate(() => {
+ return {
+ width: document.documentElement.clientWidth,
+ height: document.documentElement.clientHeight,
+ dpr: window.devicePixelRatio,
+ };
+ });
+ expect({
+ width: result.width,
+ height: result.height,
+ }).toStrictEqual({
+ width: 300 * result.dpr,
+ height: 400 * result.dpr,
+ });
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cdp/queryObjects.spec.ts b/remote/test/puppeteer/test/src/cdp/queryObjects.spec.ts
new file mode 100644
index 0000000000..405303fb6b
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cdp/queryObjects.spec.ts
@@ -0,0 +1,108 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from '../mocha-utils.js';
+
+describe('page.queryObjects', function () {
+ setupTestBrowserHooks();
+
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ // Create a custom class
+ using classHandle = await page.evaluateHandle(() => {
+ return class CustomClass {};
+ });
+
+ // Create an instance.
+ await page.evaluate(CustomClass => {
+ // @ts-expect-error: Different context.
+ self.customClass = new CustomClass();
+ }, classHandle);
+
+ // Validate only one has been added.
+ using prototypeHandle = await page.evaluateHandle(CustomClass => {
+ return CustomClass.prototype;
+ }, classHandle);
+ using objectsHandle = await page.queryObjects(prototypeHandle);
+ await expect(
+ page.evaluate(objects => {
+ return objects.length;
+ }, objectsHandle)
+ ).resolves.toBe(1);
+
+ // Check that instances.
+ await expect(
+ page.evaluate(objects => {
+ // @ts-expect-error: Different context.
+ return objects[0] === self.customClass;
+ }, objectsHandle)
+ ).resolves.toBeTruthy();
+ });
+ it('should work for non-trivial page', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+
+ // Create a custom class
+ using classHandle = await page.evaluateHandle(() => {
+ return class CustomClass {};
+ });
+
+ // Create an instance.
+ await page.evaluate(CustomClass => {
+ // @ts-expect-error: Different context.
+ self.customClass = new CustomClass();
+ }, classHandle);
+
+ // Validate only one has been added.
+ using prototypeHandle = await page.evaluateHandle(CustomClass => {
+ return CustomClass.prototype;
+ }, classHandle);
+ using objectsHandle = await page.queryObjects(prototypeHandle);
+ await expect(
+ page.evaluate(objects => {
+ return objects.length;
+ }, objectsHandle)
+ ).resolves.toBe(1);
+
+ // Check that instances.
+ await expect(
+ page.evaluate(objects => {
+ // @ts-expect-error: Different context.
+ return objects[0] === self.customClass;
+ }, objectsHandle)
+ ).resolves.toBeTruthy();
+ });
+ it('should fail for disposed handles', async () => {
+ const {page} = await getTestState();
+
+ using prototypeHandle = await page.evaluateHandle(() => {
+ return HTMLBodyElement.prototype;
+ });
+ // We want to dispose early.
+ await prototypeHandle.dispose();
+ let error!: Error;
+ await page.queryObjects(prototypeHandle).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Prototype JSHandle is disposed!');
+ });
+ it('should fail primitive values as prototypes', async () => {
+ const {page} = await getTestState();
+
+ using prototypeHandle = await page.evaluateHandle(() => {
+ return 42;
+ });
+ let error!: Error;
+ await page.queryObjects(prototypeHandle).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe(
+ 'Prototype JSHandle must not be referencing primitive value'
+ );
+ });
+});
diff --git a/remote/test/puppeteer/test/src/chromiumonly.spec.ts b/remote/test/puppeteer/test/src/chromiumonly.spec.ts
new file mode 100644
index 0000000000..e0c41317aa
--- /dev/null
+++ b/remote/test/puppeteer/test/src/chromiumonly.spec.ts
@@ -0,0 +1,168 @@
+/**
+ * @license
+ * Copyright 2019 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import type {IncomingMessage} from 'http';
+
+import expect from 'expect';
+import {Deferred} from 'puppeteer-core/internal/util/Deferred.js';
+
+import {getTestState, setupTestBrowserHooks, launch} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+// TODO: rename this test suite to launch/connect test suite as it actually
+// works across browsers.
+describe('Chromium-Specific Launcher tests', function () {
+ describe('Puppeteer.launch |browserURL| option', function () {
+ it('should be able to connect using browserUrl, with and without trailing slash', async () => {
+ const {close, puppeteer} = await launch({
+ args: ['--remote-debugging-port=21222'],
+ });
+ try {
+ const browserURL = 'http://127.0.0.1:21222';
+
+ const browser1 = await puppeteer.connect({browserURL});
+ const page1 = await browser1.newPage();
+ expect(
+ await page1.evaluate(() => {
+ return 7 * 8;
+ })
+ ).toBe(56);
+ await browser1.disconnect();
+
+ const browser2 = await puppeteer.connect({
+ browserURL: browserURL + '/',
+ });
+ const page2 = await browser2.newPage();
+ expect(
+ await page2.evaluate(() => {
+ return 8 * 7;
+ })
+ ).toBe(56);
+ await browser2.disconnect();
+ } finally {
+ await close();
+ }
+ });
+ it('should throw when using both browserWSEndpoint and browserURL', async () => {
+ const {browser, close, puppeteer} = await launch({
+ args: ['--remote-debugging-port=21222'],
+ });
+ try {
+ const browserURL = 'http://127.0.0.1:21222';
+
+ let error!: Error;
+ await puppeteer
+ .connect({
+ browserURL,
+ browserWSEndpoint: browser.wsEndpoint(),
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain(
+ 'Exactly one of browserWSEndpoint, browserURL or transport'
+ );
+ } finally {
+ await close();
+ }
+ });
+ it('should throw when trying to connect to non-existing browser', async () => {
+ const {close, puppeteer} = await launch({
+ args: ['--remote-debugging-port=21222'],
+ });
+ try {
+ const browserURL = 'http://127.0.0.1:32333';
+
+ let error!: Error;
+ await puppeteer.connect({browserURL}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain(
+ 'Failed to fetch browser webSocket URL from'
+ );
+ } finally {
+ await close();
+ }
+ });
+ });
+
+ describe('Puppeteer.launch |pipe| option', function () {
+ it('should support the pipe option', async () => {
+ const {browser, close} = await launch({pipe: true}, {createPage: false});
+ try {
+ expect(await browser.pages()).toHaveLength(1);
+ expect(browser.wsEndpoint()).toBe('');
+ const page = await browser.newPage();
+ expect(await page.evaluate('11 * 11')).toBe(121);
+ await page.close();
+ } finally {
+ await close();
+ }
+ });
+ it('should support the pipe argument', async () => {
+ const {defaultBrowserOptions} = await getTestState({skipLaunch: true});
+ const options = Object.assign({}, defaultBrowserOptions);
+ options.args = ['--remote-debugging-pipe'].concat(options.args || []);
+ const {browser, close} = await launch(options);
+ try {
+ expect(browser.wsEndpoint()).toBe('');
+ const page = await browser.newPage();
+ expect(await page.evaluate('11 * 11')).toBe(121);
+ await page.close();
+ } finally {
+ await close();
+ }
+ });
+ it('should fire "disconnected" when closing with pipe', async function () {
+ const {browser, close} = await launch({pipe: true});
+ try {
+ const disconnectedEventPromise = waitEvent(browser, 'disconnected');
+ // Emulate user exiting browser.
+ browser.process()!.kill();
+ await Deferred.race([
+ disconnectedEventPromise,
+ Deferred.create({
+ message: `Failed in after Hook`,
+ timeout: this.timeout() - 1000,
+ }),
+ ]);
+ } finally {
+ await close();
+ }
+ });
+ });
+});
+
+describe('Chromium-Specific Page Tests', function () {
+ setupTestBrowserHooks();
+
+ it('Page.setRequestInterception should work with intervention headers', async () => {
+ const {server, page} = await getTestState();
+
+ server.setRoute('/intervention', (_req, res) => {
+ return res.end(`
+ <script>
+ document.write('<script src="${server.CROSS_PROCESS_PREFIX}/intervention.js">' + '</scr' + 'ipt>');
+ </script>
+ `);
+ });
+ server.setRedirect('/intervention.js', '/redirect.js');
+ let serverRequest: IncomingMessage | undefined;
+ server.setRoute('/redirect.js', (req, res) => {
+ serverRequest = req;
+ res.end('console.log(1);');
+ });
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ await page.goto(server.PREFIX + '/intervention');
+ // Check for feature URL substring rather than https://www.chromestatus.com to
+ // make it work with Edgium.
+ expect(serverRequest!.headers['intervention']).toContain(
+ 'feature/5718547946799104'
+ );
+ });
+});
diff --git a/remote/test/puppeteer/test/src/click.spec.ts b/remote/test/puppeteer/test/src/click.spec.ts
new file mode 100644
index 0000000000..cdc0e6c133
--- /dev/null
+++ b/remote/test/puppeteer/test/src/click.spec.ts
@@ -0,0 +1,478 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {KnownDevices} from 'puppeteer';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame} from './utils.js';
+
+describe('Page.click', function () {
+ setupTestBrowserHooks();
+
+ it('should click the button', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.click('button');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ it('should click svg', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <svg height="100" width="100">
+ <circle onclick="javascript:window.__CLICKED=42" cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
+ </svg>
+ `);
+ await page.click('circle');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__CLICKED;
+ })
+ ).toBe(42);
+ });
+ it('should click the button if window.Node is removed', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.evaluate(() => {
+ // @ts-expect-error Expected.
+ return delete window.Node;
+ });
+ await page.click('button');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4281
+ it('should click on a span with an inline element inside', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <style>
+ span::before {
+ content: 'q';
+ }
+ </style>
+ <span onclick='javascript:window.CLICKED=42'></span>
+ `);
+ await page.click('span');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).CLICKED;
+ })
+ ).toBe(42);
+ });
+ it('should not throw UnhandledPromiseRejection when page closes', async () => {
+ const {page} = await getTestState();
+
+ const newPage = await page.browser().newPage();
+ await Promise.all([newPage.close(), newPage.mouse.click(1, 2)]).catch(
+ () => {}
+ );
+ });
+ it('should click the button after navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.click('button');
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.click('button');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ it('should click with disabled javascript', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setJavaScriptEnabled(false);
+ await page.goto(server.PREFIX + '/wrappedlink.html');
+ await Promise.all([page.click('a'), page.waitForNavigation()]);
+ expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked');
+ });
+ it('should scroll and click with disabled javascript', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setJavaScriptEnabled(false);
+ await page.goto(server.PREFIX + '/wrappedlink.html');
+ using body = await page.waitForSelector('body');
+ await body!.evaluate(el => {
+ el.style.paddingTop = '3000px';
+ });
+ await Promise.all([page.click('a'), page.waitForNavigation()]);
+ expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked');
+ });
+ it('should click when one of inline box children is outside of viewport', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <style>
+ i {
+ position: absolute;
+ top: -1000px;
+ }
+ </style>
+ <span onclick='javascript:window.CLICKED = 42;'><i>woof</i><b>doggo</b></span>
+ `);
+ await page.click('span');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).CLICKED;
+ })
+ ).toBe(42);
+ });
+ it('should select the text by triple clicking', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.focus('textarea');
+ const text =
+ "This is the text that we are going to try to select. Let's see how it goes.";
+ await page.keyboard.type(text);
+ await page.evaluate(() => {
+ (window as any).clicks = [];
+ window.addEventListener('click', event => {
+ return (window as any).clicks.push(event.detail);
+ });
+ });
+ await page.click('textarea', {count: 3});
+ expect(
+ await page.evaluate(() => {
+ return (window as any).clicks;
+ })
+ ).toMatchObject({0: 1, 1: 2, 2: 3});
+ expect(
+ await page.evaluate(() => {
+ const textarea = document.querySelector('textarea');
+ return textarea!.value.substring(
+ textarea!.selectionStart,
+ textarea!.selectionEnd
+ );
+ })
+ ).toBe(text);
+ });
+ it('should click offscreen buttons', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/offscreenbuttons.html');
+ const messages: string[] = [];
+ page.on('console', msg => {
+ if (msg.type() === 'log') {
+ return messages.push(msg.text());
+ }
+ return;
+ });
+ for (let i = 0; i < 11; ++i) {
+ // We might've scrolled to click a button - reset to (0, 0).
+ await page.evaluate(() => {
+ return window.scrollTo(0, 0);
+ });
+ await page.click(`#btn${i}`);
+ }
+ expect(messages).toEqual([
+ 'button #0 clicked',
+ 'button #1 clicked',
+ 'button #2 clicked',
+ 'button #3 clicked',
+ 'button #4 clicked',
+ 'button #5 clicked',
+ 'button #6 clicked',
+ 'button #7 clicked',
+ 'button #8 clicked',
+ 'button #9 clicked',
+ 'button #10 clicked',
+ ]);
+ });
+
+ it('should click wrapped links', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/wrappedlink.html');
+ await page.click('a');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__clicked;
+ })
+ ).toBe(true);
+ });
+
+ it('should click on checkbox input and toggle', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/checkbox.html');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.check;
+ })
+ ).toBe(null);
+ await page.click('input#agree');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.check;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.events;
+ })
+ ).toEqual([
+ 'mouseover',
+ 'mouseenter',
+ 'mousemove',
+ 'mousedown',
+ 'mouseup',
+ 'click',
+ 'input',
+ 'change',
+ ]);
+ await page.click('input#agree');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.check;
+ })
+ ).toBe(false);
+ });
+
+ it('should click on checkbox label and toggle', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/checkbox.html');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.check;
+ })
+ ).toBe(null);
+ await page.click('label[for="agree"]');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.check;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.events;
+ })
+ ).toEqual(['click', 'input', 'change']);
+ await page.click('label[for="agree"]');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.check;
+ })
+ ).toBe(false);
+ });
+
+ it('should fail to click a missing button', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ let error!: Error;
+ await page.click('button.does-not-exist').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe(
+ 'No element found for selector: button.does-not-exist'
+ );
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/161
+ it('should not hang with touch-enabled viewports', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport(KnownDevices['iPhone 6'].viewport);
+ await page.mouse.down();
+ await page.mouse.move(100, 10);
+ await page.mouse.up();
+ });
+ it('should scroll and click the button', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.click('#button-5');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('#button-5')!.textContent;
+ })
+ ).toBe('clicked');
+ await page.click('#button-80');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('#button-80')!.textContent;
+ })
+ ).toBe('clicked');
+ });
+ it('should double click the button', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.evaluate(() => {
+ (globalThis as any).double = false;
+ const button = document.querySelector('button');
+ button!.addEventListener('dblclick', () => {
+ (globalThis as any).double = true;
+ });
+ });
+ using button = (await page.$('button'))!;
+ await button!.click({count: 2});
+ expect(await page.evaluate('double')).toBe(true);
+ expect(await page.evaluate('result')).toBe('Clicked');
+ });
+ it('should click a partially obscured button', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.evaluate(() => {
+ const button = document.querySelector('button');
+ button!.textContent = 'Some really long text that will go offscreen';
+ button!.style.position = 'absolute';
+ button!.style.left = '368px';
+ });
+ await page.click('button');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ it('should click a rotated button', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/rotatedButton.html');
+ await page.click('button');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ it('should fire contextmenu event on right click', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.click('#button-8', {button: 'right'});
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('#button-8')!.textContent;
+ })
+ ).toBe('context menu');
+ });
+ it('should fire aux event on middle click', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.click('#button-8', {button: 'middle'});
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('#button-8')!.textContent;
+ })
+ ).toBe('aux click');
+ });
+ it('should fire back click', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.click('#button-8', {button: 'back'});
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('#button-8')!.textContent;
+ })
+ ).toBe('back click');
+ });
+ it('should fire forward click', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.click('#button-8', {button: 'forward'});
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('#button-8')!.textContent;
+ })
+ ).toBe('forward click');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/206
+ it('should click links which cause navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setContent(`<a href="${server.EMPTY_PAGE}">empty.html</a>`);
+ // This await should not hang.
+ await page.click('a');
+ });
+ it('should click the button inside an iframe', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent('<div style="width:100px;height:100px">spacer</div>');
+ await attachFrame(
+ page,
+ 'button-test',
+ server.PREFIX + '/input/button.html'
+ );
+ const frame = page.frames()[1];
+ using button = await frame!.$('button');
+ await button!.click();
+ expect(
+ await frame!.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4110
+ it('should click the button with fixed position inside an iframe', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(
+ '<div style="width:100px;height:2000px">spacer</div>'
+ );
+ await attachFrame(
+ page,
+ 'button-test',
+ server.CROSS_PROCESS_PREFIX + '/input/button.html'
+ );
+ const frame = page.frames()[1];
+ await frame!.$eval('button', (button: Element) => {
+ return (button as HTMLElement).style.setProperty('position', 'fixed');
+ });
+ await frame!.click('button');
+ expect(
+ await frame!.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ it('should click the button with deviceScaleFactor set', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 400, height: 400, deviceScaleFactor: 5});
+ expect(
+ await page.evaluate(() => {
+ return window.devicePixelRatio;
+ })
+ ).toBe(5);
+ await page.setContent('<div style="width:100px;height:100px">spacer</div>');
+ await attachFrame(
+ page,
+ 'button-test',
+ server.PREFIX + '/input/button.html'
+ );
+ const frame = page.frames()[1];
+ using button = await frame!.$('button');
+ await button!.click();
+ expect(
+ await frame!.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+});
diff --git a/remote/test/puppeteer/test/src/cookies.spec.ts b/remote/test/puppeteer/test/src/cookies.spec.ts
new file mode 100644
index 0000000000..f232831b72
--- /dev/null
+++ b/remote/test/puppeteer/test/src/cookies.spec.ts
@@ -0,0 +1,557 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import expect from 'expect';
+
+import {
+ expectCookieEquals,
+ getTestState,
+ launch,
+ setupTestBrowserHooks,
+} from './mocha-utils.js';
+
+describe('Cookie specs', () => {
+ setupTestBrowserHooks();
+
+ describe('Page.cookies', function () {
+ it('should return no cookies in pristine browser context', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await expectCookieEquals(await page.cookies(), []);
+ });
+ it('should get a cookie', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ document.cookie = 'username=John Doe';
+ });
+
+ await expectCookieEquals(await page.cookies(), [
+ {
+ name: 'username',
+ value: 'John Doe',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 16,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+ });
+ it('should properly report httpOnly cookie', async () => {
+ const {page, server} = await getTestState();
+ server.setRoute('/empty.html', (_req, res) => {
+ res.setHeader('Set-Cookie', 'a=b; HttpOnly; Path=/');
+ res.end();
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const cookies = await page.cookies();
+ expect(cookies).toHaveLength(1);
+ expect(cookies[0]!.httpOnly).toBe(true);
+ });
+ it('should properly report "Strict" sameSite cookie', async () => {
+ const {page, server} = await getTestState();
+ server.setRoute('/empty.html', (_req, res) => {
+ res.setHeader('Set-Cookie', 'a=b; SameSite=Strict');
+ res.end();
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const cookies = await page.cookies();
+ expect(cookies).toHaveLength(1);
+ expect(cookies[0]!.sameSite).toBe('Strict');
+ });
+ it('should properly report "Lax" sameSite cookie', async () => {
+ const {page, server} = await getTestState();
+ server.setRoute('/empty.html', (_req, res) => {
+ res.setHeader('Set-Cookie', 'a=b; SameSite=Lax');
+ res.end();
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const cookies = await page.cookies();
+ expect(cookies).toHaveLength(1);
+ expect(cookies[0]!.sameSite).toBe('Lax');
+ });
+ it('should get multiple cookies', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ document.cookie = 'username=John Doe';
+ document.cookie = 'password=1234';
+ });
+ const cookies = await page.cookies();
+ cookies.sort((a, b) => {
+ return a.name.localeCompare(b.name);
+ });
+ await expectCookieEquals(cookies, [
+ {
+ name: 'password',
+ value: '1234',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 12,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourceScheme: 'NonSecure',
+ },
+ {
+ name: 'username',
+ value: 'John Doe',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 16,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+ });
+ it('should get cookies from multiple urls', async () => {
+ const {page} = await getTestState();
+ await page.setCookie(
+ {
+ url: 'https://foo.com',
+ name: 'doggo',
+ value: 'woofs',
+ },
+ {
+ url: 'https://bar.com',
+ name: 'catto',
+ value: 'purrs',
+ },
+ {
+ url: 'https://baz.com',
+ name: 'birdo',
+ value: 'tweets',
+ }
+ );
+ const cookies = await page.cookies('https://foo.com', 'https://baz.com');
+ cookies.sort((a, b) => {
+ return a.name.localeCompare(b.name);
+ });
+ await expectCookieEquals(cookies, [
+ {
+ name: 'birdo',
+ value: 'tweets',
+ domain: 'baz.com',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 11,
+ httpOnly: false,
+ secure: true,
+ session: true,
+ sourcePort: 443,
+ sourceScheme: 'Secure',
+ },
+ {
+ name: 'doggo',
+ value: 'woofs',
+ domain: 'foo.com',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 10,
+ httpOnly: false,
+ secure: true,
+ session: true,
+ sourcePort: 443,
+ sourceScheme: 'Secure',
+ },
+ ]);
+ });
+ });
+ describe('Page.setCookie', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({
+ name: 'password',
+ value: '123456',
+ });
+ expect(
+ await page.evaluate(() => {
+ return document.cookie;
+ })
+ ).toEqual('password=123456');
+ });
+ it('should isolate cookies in browser contexts', async () => {
+ const {page, server, browser} = await getTestState();
+
+ const anotherContext = await browser.createIncognitoBrowserContext();
+ const anotherPage = await anotherContext.newPage();
+
+ await page.goto(server.EMPTY_PAGE);
+ await anotherPage.goto(server.EMPTY_PAGE);
+
+ await page.setCookie({name: 'page1cookie', value: 'page1value'});
+ await anotherPage.setCookie({name: 'page2cookie', value: 'page2value'});
+
+ const cookies1 = await page.cookies();
+ const cookies2 = await anotherPage.cookies();
+ expect(cookies1).toHaveLength(1);
+ expect(cookies2).toHaveLength(1);
+ expect(cookies1[0]!.name).toBe('page1cookie');
+ expect(cookies1[0]!.value).toBe('page1value');
+ expect(cookies2[0]!.name).toBe('page2cookie');
+ expect(cookies2[0]!.value).toBe('page2value');
+ await anotherContext.close();
+ });
+ it('should set multiple cookies', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie(
+ {
+ name: 'password',
+ value: '123456',
+ },
+ {
+ name: 'foo',
+ value: 'bar',
+ }
+ );
+ const cookieStrings = await page.evaluate(() => {
+ const cookies = document.cookie.split(';');
+ return cookies
+ .map(cookie => {
+ return cookie.trim();
+ })
+ .sort();
+ });
+
+ expect(cookieStrings).toEqual(['foo=bar', 'password=123456']);
+ });
+ it('should have |expires| set to |-1| for session cookies', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({
+ name: 'password',
+ value: '123456',
+ });
+ const cookies = await page.cookies();
+ expect(cookies[0]!.session).toBe(true);
+ expect(cookies[0]!.expires).toBe(-1);
+ });
+ it('should set cookie with reasonable defaults', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({
+ name: 'password',
+ value: '123456',
+ });
+ const cookies = await page.cookies();
+ await expectCookieEquals(
+ cookies.sort((a, b) => {
+ return a.name.localeCompare(b.name);
+ }),
+ [
+ {
+ name: 'password',
+ value: '123456',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 14,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourcePort: 80,
+ sourceScheme: 'NonSecure',
+ },
+ ]
+ );
+ });
+ it('should set a cookie with a path', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/grid.html');
+ await page.setCookie({
+ name: 'gridcookie',
+ value: 'GRID',
+ path: '/grid.html',
+ });
+ await expectCookieEquals(await page.cookies(), [
+ {
+ name: 'gridcookie',
+ value: 'GRID',
+ domain: 'localhost',
+ path: '/grid.html',
+ sameParty: false,
+ expires: -1,
+ size: 14,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourcePort: 80,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+ expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID');
+ await page.goto(server.EMPTY_PAGE);
+ await expectCookieEquals(await page.cookies(), []);
+ expect(await page.evaluate('document.cookie')).toBe('');
+ await page.goto(server.PREFIX + '/grid.html');
+ expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID');
+ });
+ it('should not set a cookie on a blank page', async () => {
+ const {page} = await getTestState();
+
+ await page.goto('about:blank');
+ let error!: Error;
+ try {
+ await page.setCookie({name: 'example-cookie', value: 'best'});
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toContain(
+ 'At least one of the url and domain needs to be specified'
+ );
+ });
+ it('should not set a cookie with blank page URL', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ await page.goto(server.EMPTY_PAGE);
+ try {
+ await page.setCookie(
+ {name: 'example-cookie', value: 'best'},
+ {url: 'about:blank', name: 'example-cookie-blank', value: 'best'}
+ );
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toEqual(
+ `Blank page can not have cookie "example-cookie-blank"`
+ );
+ });
+ it('should not set a cookie on a data URL page', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.goto('data:,Hello%2C%20World!');
+ try {
+ await page.setCookie({name: 'example-cookie', value: 'best'});
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toContain(
+ 'At least one of the url and domain needs to be specified'
+ );
+ });
+ it('should default to setting secure cookie for HTTPS websites', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const SECURE_URL = 'https://example.com';
+ await page.setCookie({
+ url: SECURE_URL,
+ name: 'foo',
+ value: 'bar',
+ });
+ const [cookie] = await page.cookies(SECURE_URL);
+ expect(cookie!.secure).toBe(true);
+ });
+ it('should be able to set insecure cookie for HTTP website', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const HTTP_URL = 'http://example.com';
+ await page.setCookie({
+ url: HTTP_URL,
+ name: 'foo',
+ value: 'bar',
+ });
+ const [cookie] = await page.cookies(HTTP_URL);
+ expect(cookie!.secure).toBe(false);
+ });
+ it('should set a cookie on a different domain', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({
+ url: 'https://www.example.com',
+ name: 'example-cookie',
+ value: 'best',
+ });
+ expect(await page.evaluate('document.cookie')).toBe('');
+ await expectCookieEquals(await page.cookies(), []);
+ await expectCookieEquals(await page.cookies('https://www.example.com'), [
+ {
+ name: 'example-cookie',
+ value: 'best',
+ domain: 'www.example.com',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 18,
+ httpOnly: false,
+ secure: true,
+ session: true,
+ sourcePort: 443,
+ sourceScheme: 'Secure',
+ },
+ ]);
+ });
+ it('should set cookies from a frame', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/grid.html');
+ await page.setCookie({name: 'localhost-cookie', value: 'best'});
+ await page.evaluate(src => {
+ let fulfill!: () => void;
+ const promise = new Promise<void>(x => {
+ return (fulfill = x);
+ });
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.onload = fulfill;
+ iframe.src = src;
+ return promise;
+ }, server.CROSS_PROCESS_PREFIX);
+ await page.setCookie({
+ name: '127-cookie',
+ value: 'worst',
+ url: server.CROSS_PROCESS_PREFIX,
+ });
+ expect(await page.evaluate('document.cookie')).toBe(
+ 'localhost-cookie=best'
+ );
+ expect(await page.frames()[1]!.evaluate('document.cookie')).toBe('');
+
+ await expectCookieEquals(await page.cookies(), [
+ {
+ name: 'localhost-cookie',
+ value: 'best',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 20,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourcePort: 80,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+
+ await expectCookieEquals(
+ await page.cookies(server.CROSS_PROCESS_PREFIX),
+ [
+ {
+ name: '127-cookie',
+ value: 'worst',
+ domain: '127.0.0.1',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 15,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourcePort: 80,
+ sourceScheme: 'NonSecure',
+ },
+ ]
+ );
+ });
+ it('should set secure same-site cookies from a frame', async () => {
+ const {httpsServer, browser, close} = await launch({
+ ignoreHTTPSErrors: true,
+ });
+
+ try {
+ const page = await browser.newPage();
+ await page.goto(httpsServer.PREFIX + '/grid.html');
+ await page.evaluate(src => {
+ let fulfill!: () => void;
+ const promise = new Promise<void>(x => {
+ return (fulfill = x);
+ });
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.onload = fulfill;
+ iframe.src = src;
+ return promise;
+ }, httpsServer.CROSS_PROCESS_PREFIX);
+ await page.setCookie({
+ name: '127-same-site-cookie',
+ value: 'best',
+ url: httpsServer.CROSS_PROCESS_PREFIX,
+ sameSite: 'None',
+ });
+
+ expect(await page.frames()[1]!.evaluate('document.cookie')).toBe(
+ '127-same-site-cookie=best'
+ );
+ await expectCookieEquals(
+ await page.cookies(httpsServer.CROSS_PROCESS_PREFIX),
+ [
+ {
+ name: '127-same-site-cookie',
+ value: 'best',
+ domain: '127.0.0.1',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 24,
+ httpOnly: false,
+ sameSite: 'None',
+ secure: true,
+ session: true,
+ sourcePort: 443,
+ sourceScheme: 'Secure',
+ },
+ ]
+ );
+ } finally {
+ await close();
+ }
+ });
+ });
+
+ describe('Page.deleteCookie', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie(
+ {
+ name: 'cookie1',
+ value: '1',
+ },
+ {
+ name: 'cookie2',
+ value: '2',
+ },
+ {
+ name: 'cookie3',
+ value: '3',
+ }
+ );
+ expect(await page.evaluate('document.cookie')).toBe(
+ 'cookie1=1; cookie2=2; cookie3=3'
+ );
+ await page.deleteCookie({name: 'cookie2'});
+ expect(await page.evaluate('document.cookie')).toBe(
+ 'cookie1=1; cookie3=3'
+ );
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/coverage.spec.ts b/remote/test/puppeteer/test/src/coverage.spec.ts
new file mode 100644
index 0000000000..6a95db541c
--- /dev/null
+++ b/remote/test/puppeteer/test/src/coverage.spec.ts
@@ -0,0 +1,343 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Coverage specs', function () {
+ setupTestBrowserHooks();
+
+ describe('JSCoverage', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/simple.html', {
+ waitUntil: 'load',
+ });
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.url).toContain('/jscoverage/simple.html');
+ expect(coverage[0]!.ranges).toEqual([
+ {start: 0, end: 17},
+ {start: 35, end: 61},
+ ]);
+ });
+ it('should report sourceURLs', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/sourceurl.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.url).toBe('nicename.js');
+ });
+ it('should ignore eval() scripts by default', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/eval.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ });
+ it('should not ignore eval() scripts if reportAnonymousScripts is true', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage({reportAnonymousScripts: true});
+ await page.goto(server.PREFIX + '/jscoverage/eval.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(
+ coverage.find(entry => {
+ return entry.url.startsWith('debugger://');
+ })
+ ).not.toBe(null);
+ expect(coverage).toHaveLength(2);
+ });
+ it('should ignore pptr internal scripts if reportAnonymousScripts is true', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage({reportAnonymousScripts: true});
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate('console.log("foo")');
+ await page.evaluate(() => {
+ return console.log('bar');
+ });
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(0);
+ });
+ it('should report multiple scripts', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/multiple.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(2);
+ coverage.sort((a, b) => {
+ return a.url.localeCompare(b.url);
+ });
+ expect(coverage[0]!.url).toContain('/jscoverage/script1.js');
+ expect(coverage[1]!.url).toContain('/jscoverage/script2.js');
+ });
+ it('should report right ranges', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/ranges.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ const entry = coverage[0]!;
+ expect(entry.ranges).toHaveLength(2);
+ const range1 = entry.ranges[0]!;
+ expect(entry.text.substring(range1.start, range1.end)).toBe('\n');
+ const range2 = entry.ranges[1]!;
+ expect(entry.text.substring(range2.start, range2.end)).toBe(
+ `console.log('used!');if(true===false)`
+ );
+ });
+ it('should report right ranges for "per function" scope', async () => {
+ const {page, server} = await getTestState();
+
+ const coverageOptions = {
+ useBlockCoverage: false,
+ };
+
+ await page.coverage.startJSCoverage(coverageOptions);
+ await page.goto(server.PREFIX + '/jscoverage/ranges.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ const entry = coverage[0]!;
+ expect(entry.ranges).toHaveLength(2);
+ const range1 = entry.ranges[0]!;
+ expect(entry.text.substring(range1.start, range1.end)).toBe('\n');
+ const range2 = entry.ranges[1]!;
+ expect(entry.text.substring(range2.start, range2.end)).toBe(
+ `console.log('used!');if(true===false)console.log('unused!');`
+ );
+ });
+ it('should report scripts that have no coverage', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/unused.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ const entry = coverage[0]!;
+ expect(entry.url).toContain('unused.html');
+ expect(entry.ranges).toHaveLength(0);
+ });
+ it('should work with conditionals', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/involved.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(
+ JSON.stringify(coverage, null, 2).replace(/:\d{4,5}\//g, ':<PORT>/')
+ ).toBeGolden('jscoverage-involved.txt');
+ });
+ // @see https://crbug.com/990945
+ it.skip('should not hang when there is a debugger statement', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ debugger; // eslint-disable-line no-debugger
+ });
+ await page.coverage.stopJSCoverage();
+ });
+ describe('resetOnNavigation', function () {
+ it('should report scripts across navigations when disabled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage({resetOnNavigation: false});
+ await page.goto(server.PREFIX + '/jscoverage/multiple.html');
+ // TODO: navigating too fast might loose JS coverage data in the browser.
+ await page.waitForNetworkIdle();
+ await page.goto(server.EMPTY_PAGE);
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(2);
+ });
+
+ it('should NOT report scripts across navigations when enabled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage(); // Enabled by default.
+ await page.goto(server.PREFIX + '/jscoverage/multiple.html');
+ await page.goto(server.EMPTY_PAGE);
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(0);
+ });
+ });
+ describe('includeRawScriptCoverage', function () {
+ it('should not include rawScriptCoverage field when disabled', async () => {
+ const {page, server} = await getTestState();
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/simple.html', {
+ waitUntil: 'load',
+ });
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.rawScriptCoverage).toBeUndefined();
+ });
+ it('should include rawScriptCoverage field when enabled', async () => {
+ const {page, server} = await getTestState();
+ await page.coverage.startJSCoverage({
+ includeRawScriptCoverage: true,
+ });
+ await page.goto(server.PREFIX + '/jscoverage/simple.html', {
+ waitUntil: 'load',
+ });
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.rawScriptCoverage).toBeTruthy();
+ });
+ });
+ // @see https://crbug.com/990945
+ it.skip('should not hang when there is a debugger statement', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ debugger; // eslint-disable-line no-debugger
+ });
+ await page.coverage.stopJSCoverage();
+ });
+ });
+
+ describe('CSSCoverage', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/simple.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.url).toContain('/csscoverage/simple.html');
+ expect(coverage[0]!.ranges).toEqual([{start: 1, end: 22}]);
+ const range = coverage[0]!.ranges[0]!;
+ expect(coverage[0]!.text.substring(range.start, range.end)).toBe(
+ 'div { color: green; }'
+ );
+ });
+ it('should report sourceURLs', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/sourceurl.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.url).toBe('nicename.css');
+ });
+ it('should report multiple stylesheets', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/multiple.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(2);
+ coverage.sort((a, b) => {
+ return a.url.localeCompare(b.url);
+ });
+ expect(coverage[0]!.url).toContain('/csscoverage/stylesheet1.css');
+ expect(coverage[1]!.url).toContain('/csscoverage/stylesheet2.css');
+ });
+ it('should report stylesheets that have no coverage', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/unused.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.url).toBe('unused.css');
+ expect(coverage[0]!.ranges).toHaveLength(0);
+ });
+ it('should work with media queries', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/media.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.url).toContain('/csscoverage/media.html');
+ expect(coverage[0]!.ranges).toEqual([
+ {start: 8, end: 15},
+ {start: 17, end: 38},
+ ]);
+ });
+ it('should work with complicated usecases', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/involved.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(
+ JSON.stringify(coverage, null, 2).replace(/:\d{4,5}\//g, ':<PORT>/')
+ ).toBeGolden('csscoverage-involved.txt');
+ });
+ it('should work with empty stylesheets', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/empty.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(1);
+ expect(coverage[0]!.text).toEqual('');
+ });
+ it('should ignore injected stylesheets', async () => {
+ const {page} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.addStyleTag({content: 'body { margin: 10px;}'});
+ // trigger style recalc
+ const margin = await page.evaluate(() => {
+ return window.getComputedStyle(document.body).margin;
+ });
+ expect(margin).toBe('10px');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(0);
+ });
+ it('should work with a recently loaded stylesheet', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.evaluate(async url => {
+ document.body.textContent = 'hello, world';
+
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = url;
+ document.head.appendChild(link);
+ await new Promise(x => {
+ return (link.onload = x);
+ });
+ }, server.PREFIX + '/csscoverage/stylesheet1.css');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(1);
+ });
+ describe('resetOnNavigation', function () {
+ it('should report stylesheets across navigations', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage({resetOnNavigation: false});
+ await page.goto(server.PREFIX + '/csscoverage/multiple.html');
+ await page.goto(server.EMPTY_PAGE);
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(2);
+ });
+ it('should NOT report scripts across navigations', async () => {
+ const {page, server} = await getTestState();
+
+ await page.coverage.startCSSCoverage(); // Enabled by default.
+ await page.goto(server.PREFIX + '/csscoverage/multiple.html');
+ await page.goto(server.EMPTY_PAGE);
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage).toHaveLength(0);
+ });
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/debugInfo.spec.ts b/remote/test/puppeteer/test/src/debugInfo.spec.ts
new file mode 100644
index 0000000000..079107cab7
--- /dev/null
+++ b/remote/test/puppeteer/test/src/debugInfo.spec.ts
@@ -0,0 +1,36 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('DebugInfo', function () {
+ setupTestBrowserHooks();
+
+ describe('Browser.debugInfo', function () {
+ it('should work', async () => {
+ const {page, browser} = await getTestState();
+
+ const promise = page.evaluate(() => {
+ return new Promise(resolve => {
+ // @ts-expect-error another context
+ window.resolve = resolve;
+ });
+ });
+ try {
+ expect(browser.debugInfo.pendingProtocolErrors).toHaveLength(1);
+ } finally {
+ await page.evaluate(() => {
+ // @ts-expect-error another context
+ window.resolve();
+ });
+ }
+ await promise;
+ expect(browser.debugInfo.pendingProtocolErrors).toHaveLength(0);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts b/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts
new file mode 100644
index 0000000000..69a5a069af
--- /dev/null
+++ b/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts
@@ -0,0 +1,104 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import expect from 'expect';
+
+import {
+ expectCookieEquals,
+ getTestState,
+ setupTestBrowserHooks,
+} from './mocha-utils.js';
+
+describe('DefaultBrowserContext', function () {
+ setupTestBrowserHooks();
+
+ it('page.cookies() should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ document.cookie = 'username=John Doe';
+ });
+ await expectCookieEquals(await page.cookies(), [
+ {
+ name: 'username',
+ value: 'John Doe',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 16,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+ });
+ it('page.setCookie() should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({
+ name: 'username',
+ value: 'John Doe',
+ });
+ expect(
+ await page.evaluate(() => {
+ return document.cookie;
+ })
+ ).toBe('username=John Doe');
+ await expectCookieEquals(await page.cookies(), [
+ {
+ name: 'username',
+ value: 'John Doe',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 16,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourcePort: 80,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+ });
+ it('page.deleteCookie() should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie(
+ {
+ name: 'cookie1',
+ value: '1',
+ },
+ {
+ name: 'cookie2',
+ value: '2',
+ }
+ );
+ expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2');
+ await page.deleteCookie({name: 'cookie2'});
+ expect(await page.evaluate('document.cookie')).toBe('cookie1=1');
+ await expectCookieEquals(await page.cookies(), [
+ {
+ name: 'cookie1',
+ value: '1',
+ domain: 'localhost',
+ path: '/',
+ sameParty: false,
+ expires: -1,
+ size: 8,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ sourcePort: 80,
+ sourceScheme: 'NonSecure',
+ },
+ ]);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/device-request-prompt.spec.ts b/remote/test/puppeteer/test/src/device-request-prompt.spec.ts
new file mode 100644
index 0000000000..e6e2cdd65e
--- /dev/null
+++ b/remote/test/puppeteer/test/src/device-request-prompt.spec.ts
@@ -0,0 +1,53 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import expect from 'expect';
+import {TimeoutError} from 'puppeteer';
+
+import {launch} from './mocha-utils.js';
+
+describe('device request prompt', function () {
+ let state: Awaited<ReturnType<typeof launch>>;
+
+ before(async () => {
+ state = await launch(
+ {
+ args: ['--enable-features=WebBluetoothNewPermissionsBackend'],
+ ignoreHTTPSErrors: true,
+ },
+ {
+ after: 'all',
+ }
+ );
+ });
+
+ after(async () => {
+ await state.close();
+ });
+
+ beforeEach(async () => {
+ state.context = await state.browser.createIncognitoBrowserContext();
+ state.page = await state.context.newPage();
+ });
+
+ afterEach(async () => {
+ await state.context.close();
+ });
+
+ // Bug: #11072
+ it('does not crash', async function () {
+ this.timeout(1_000);
+
+ const {page, httpsServer} = state;
+
+ await page.goto(httpsServer.EMPTY_PAGE);
+
+ await expect(
+ page.waitForDevicePrompt({
+ timeout: 10,
+ })
+ ).rejects.toThrow(TimeoutError);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/dialog.spec.ts b/remote/test/puppeteer/test/src/dialog.spec.ts
new file mode 100644
index 0000000000..e137ccf517
--- /dev/null
+++ b/remote/test/puppeteer/test/src/dialog.spec.ts
@@ -0,0 +1,64 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import expect from 'expect';
+import sinon from 'sinon';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Page.Events.Dialog', function () {
+ setupTestBrowserHooks();
+
+ it('should fire', async () => {
+ const {page} = await getTestState();
+
+ const onDialog = sinon.stub().callsFake(dialog => {
+ dialog.accept();
+ });
+ page.on('dialog', onDialog);
+
+ await page.evaluate(() => {
+ return alert('yo');
+ });
+
+ expect(onDialog.callCount).toEqual(1);
+ const dialog = onDialog.firstCall.args[0]!;
+ expect(dialog.type()).toBe('alert');
+ expect(dialog.defaultValue()).toBe('');
+ expect(dialog.message()).toBe('yo');
+ });
+
+ it('should allow accepting prompts', async () => {
+ const {page} = await getTestState();
+
+ const onDialog = sinon.stub().callsFake(dialog => {
+ dialog.accept('answer!');
+ });
+ page.on('dialog', onDialog);
+
+ const result = await page.evaluate(() => {
+ return prompt('question?', 'yes.');
+ });
+
+ expect(onDialog.callCount).toEqual(1);
+ const dialog = onDialog.firstCall.args[0]!;
+ expect(dialog.type()).toBe('prompt');
+ expect(dialog.defaultValue()).toBe('yes.');
+ expect(dialog.message()).toBe('question?');
+
+ expect(result).toBe('answer!');
+ });
+ it('should dismiss the prompt', async () => {
+ const {page} = await getTestState();
+
+ page.on('dialog', dialog => {
+ void dialog.dismiss();
+ });
+ const result = await page.evaluate(() => {
+ return prompt('question?');
+ });
+ expect(result).toBe(null);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/diffstyle.css b/remote/test/puppeteer/test/src/diffstyle.css
new file mode 100644
index 0000000000..202e85f41a
--- /dev/null
+++ b/remote/test/puppeteer/test/src/diffstyle.css
@@ -0,0 +1,13 @@
+body {
+ font-family: monospace;
+ white-space: pre;
+}
+
+ins {
+ background-color: #9cffa0;
+ text-decoration: none;
+}
+
+del {
+ background-color: #ff9e9e;
+}
diff --git a/remote/test/puppeteer/test/src/drag-and-drop.spec.ts b/remote/test/puppeteer/test/src/drag-and-drop.spec.ts
new file mode 100644
index 0000000000..cfe18b55a4
--- /dev/null
+++ b/remote/test/puppeteer/test/src/drag-and-drop.spec.ts
@@ -0,0 +1,154 @@
+/**
+ * @license
+ * Copyright 2021 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+async function getDragState() {
+ const {page} = await getTestState({skipLaunch: true});
+ return parseInt(
+ await page.$eval('#drag-state', element => {
+ return element.innerHTML;
+ }),
+ 10
+ );
+}
+
+describe("Legacy Drag n' Drop", function () {
+ setupTestBrowserHooks();
+
+ it('should emit a dragIntercepted event when dragged', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+ expect(page.isDragInterceptionEnabled()).toBe(false);
+ await page.setDragInterception(true);
+ expect(page.isDragInterceptionEnabled()).toBe(true);
+ using draggable = (await page.$('#drag'))!;
+ const data = await draggable.drag({x: 1, y: 1});
+
+ assert(data instanceof Object);
+ expect(data.items).toHaveLength(1);
+ expect(await getDragState()).toBe(1);
+ });
+ it('should emit a dragEnter', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+ expect(page.isDragInterceptionEnabled()).toBe(false);
+ await page.setDragInterception(true);
+ expect(page.isDragInterceptionEnabled()).toBe(true);
+ using draggable = (await page.$('#drag'))!;
+ const data = await draggable.drag({x: 1, y: 1});
+ assert(data instanceof Object);
+ using dropzone = (await page.$('#drop'))!;
+ await dropzone.dragEnter(data);
+
+ expect(await getDragState()).toBe(12);
+ });
+ it('should emit a dragOver event', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+ expect(page.isDragInterceptionEnabled()).toBe(false);
+ await page.setDragInterception(true);
+ expect(page.isDragInterceptionEnabled()).toBe(true);
+ using draggable = (await page.$('#drag'))!;
+ const data = await draggable.drag({x: 1, y: 1});
+ assert(data instanceof Object);
+ using dropzone = (await page.$('#drop'))!;
+ await dropzone.dragEnter(data);
+ await dropzone.dragOver(data);
+
+ expect(await getDragState()).toBe(123);
+ });
+ it('can be dropped', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+ expect(page.isDragInterceptionEnabled()).toBe(false);
+ await page.setDragInterception(true);
+ expect(page.isDragInterceptionEnabled()).toBe(true);
+ using draggable = (await page.$('#drag'))!;
+ using dropzone = (await page.$('#drop'))!;
+ const data = await draggable.drag({x: 1, y: 1});
+ assert(data instanceof Object);
+ await dropzone.dragEnter(data);
+ await dropzone.dragOver(data);
+ await dropzone.drop(data);
+
+ expect(await getDragState()).toBe(12334);
+ });
+ it('can be dragged and dropped with a single function', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+ expect(page.isDragInterceptionEnabled()).toBe(false);
+ await page.setDragInterception(true);
+ expect(page.isDragInterceptionEnabled()).toBe(true);
+ using draggable = (await page.$('#drag'))!;
+ using dropzone = (await page.$('#drop'))!;
+ await draggable.dragAndDrop(dropzone);
+
+ expect(await getDragState()).toBe(12334);
+ });
+});
+
+describe("Drag n' Drop", () => {
+ setupTestBrowserHooks();
+
+ it('should drop', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+
+ using draggable = await page.$('#drag');
+ assert(draggable);
+ using dropzone = await page.$('#drop');
+ assert(dropzone);
+
+ await dropzone.drop(draggable);
+
+ expect(await getDragState()).toBe(1234);
+ });
+ it('should drop using mouse', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+
+ using draggable = await page.$('#drag');
+ assert(draggable);
+ using dropzone = await page.$('#drop');
+ assert(dropzone);
+
+ await draggable.hover();
+ await page.mouse.down();
+ await dropzone.hover();
+
+ expect(await getDragState()).toBe(123);
+
+ await page.mouse.up();
+ expect(await getDragState()).toBe(1234);
+ });
+ it('should drag and drop', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/drag-and-drop.html');
+
+ using draggable = await page.$('#drag');
+ assert(draggable);
+ using dropzone = await page.$('#drop');
+ assert(dropzone);
+
+ await draggable.drag(dropzone);
+ await dropzone.drop(draggable);
+
+ expect(await getDragState()).toBe(1234);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/elementhandle.spec.ts b/remote/test/puppeteer/test/src/elementhandle.spec.ts
new file mode 100644
index 0000000000..9aaf914224
--- /dev/null
+++ b/remote/test/puppeteer/test/src/elementhandle.spec.ts
@@ -0,0 +1,953 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {Puppeteer} from 'puppeteer';
+import {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js';
+import {
+ asyncDisposeSymbol,
+ disposeSymbol,
+} from 'puppeteer-core/internal/util/disposable.js';
+import sinon from 'sinon';
+
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ shortWaitForArrayToHaveAtLeastNElements,
+} from './mocha-utils.js';
+import {attachFrame} from './utils.js';
+
+describe('ElementHandle specs', function () {
+ setupTestBrowserHooks();
+
+ describe('ElementHandle.boundingBox', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ using elementHandle = (await page.$('.box:nth-of-type(13)'))!;
+ const box = await elementHandle.boundingBox();
+ expect(box).toEqual({x: 100, y: 50, width: 50, height: 50});
+ });
+ it('should handle nested frames', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ const nestedFrame = page.frames()[1]!.childFrames()[1]!;
+ using elementHandle = (await nestedFrame.$('div'))!;
+ const box = await elementHandle.boundingBox();
+ if (isChrome) {
+ expect(box).toEqual({x: 28, y: 182, width: 264, height: 18});
+ } else {
+ expect(box).toEqual({x: 28, y: 182, width: 254, height: 18});
+ }
+ });
+ it('should return null for invisible elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div style="display:none">hi</div>');
+ using element = (await page.$('div'))!;
+ expect(await element.boundingBox()).toBe(null);
+ });
+ it('should force a layout', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(
+ '<div style="width: 100px; height: 100px">hello</div>'
+ );
+ using elementHandle = (await page.$('div'))!;
+ await page.evaluate((element: HTMLElement) => {
+ return (element.style.height = '200px');
+ }, elementHandle);
+ const box = await elementHandle.boundingBox();
+ expect(box).toEqual({x: 8, y: 8, width: 100, height: 200});
+ });
+ it('should work with SVG nodes', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
+ <rect id="theRect" x="30" y="50" width="200" height="300"></rect>
+ </svg>
+ `);
+ using element = (await page.$(
+ '#therect'
+ )) as ElementHandle<SVGRectElement>;
+ const pptrBoundingBox = await element.boundingBox();
+ const webBoundingBox = await page.evaluate(e => {
+ const rect = e.getBoundingClientRect();
+ return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
+ }, element);
+ expect(pptrBoundingBox).toEqual(webBoundingBox);
+ });
+ });
+
+ describe('ElementHandle.boxModel', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/resetcss.html');
+
+ // Step 1: Add Frame and position it absolutely.
+ await attachFrame(page, 'frame1', server.PREFIX + '/resetcss.html');
+ await page.evaluate(() => {
+ const frame = document.querySelector<HTMLElement>('#frame1')!;
+ frame.style.position = 'absolute';
+ frame.style.left = '1px';
+ frame.style.top = '2px';
+ });
+
+ // Step 2: Add div and position it absolutely inside frame.
+ const frame = page.frames()[1]!;
+ using divHandle = (
+ await frame.evaluateHandle(() => {
+ const div = document.createElement('div');
+ document.body.appendChild(div);
+ div.style.boxSizing = 'border-box';
+ div.style.position = 'absolute';
+ div.style.borderLeft = '1px solid black';
+ div.style.paddingLeft = '2px';
+ div.style.marginLeft = '3px';
+ div.style.left = '4px';
+ div.style.top = '5px';
+ div.style.width = '6px';
+ div.style.height = '7px';
+ return div;
+ })
+ ).asElement()!;
+
+ // Step 3: query div's boxModel and assert box values.
+ const box = (await divHandle.boxModel())!;
+ expect(box.width).toBe(6);
+ expect(box.height).toBe(7);
+ expect(box.margin[0]).toEqual({
+ x: 1 + 4, // frame.left + div.left
+ y: 2 + 5,
+ });
+ expect(box.border[0]).toEqual({
+ x: 1 + 4 + 3, // frame.left + div.left + div.margin-left
+ y: 2 + 5,
+ });
+ expect(box.padding[0]).toEqual({
+ x: 1 + 4 + 3 + 1, // frame.left + div.left + div.marginLeft + div.borderLeft
+ y: 2 + 5,
+ });
+ expect(box.content[0]).toEqual({
+ x: 1 + 4 + 3 + 1 + 2, // frame.left + div.left + div.marginLeft + div.borderLeft + div.paddingLeft
+ y: 2 + 5,
+ });
+ });
+
+ it('should return null for invisible elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div style="display:none">hi</div>');
+ using element = (await page.$('div'))!;
+ expect(await element.boxModel()).toBe(null);
+ });
+ });
+
+ describe('ElementHandle.contentFrame', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ using elementHandle = (await page.$('#frame1'))!;
+ const frame = await elementHandle.contentFrame();
+ expect(frame).toBe(page.frames()[1]);
+ });
+ });
+
+ describe('ElementHandle.isVisible and ElementHandle.isHidden', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ await page.setContent('<div style="display: none">text</div>');
+ using element = (await page.waitForSelector('div'))!;
+ await expect(element.isVisible()).resolves.toBeFalsy();
+ await expect(element.isHidden()).resolves.toBeTruthy();
+ await element.evaluate(e => {
+ e.style.removeProperty('display');
+ });
+ await expect(element.isVisible()).resolves.toBeTruthy();
+ await expect(element.isHidden()).resolves.toBeFalsy();
+ });
+ });
+
+ describe('ElementHandle.click', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ using button = (await page.$('button'))!;
+ await button.click();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ it('should return Point data', async () => {
+ const {page} = await getTestState();
+
+ const clicks: Array<[x: number, y: number]> = [];
+
+ await page.exposeFunction('reportClick', (x: number, y: number): void => {
+ clicks.push([x, y]);
+ });
+
+ await page.evaluate(() => {
+ document.body.style.padding = '0';
+ document.body.style.margin = '0';
+ document.body.innerHTML = `
+ <div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div>
+ `;
+ document.body.addEventListener('click', e => {
+ (window as any).reportClick(e.clientX, e.clientY);
+ });
+ });
+
+ using divHandle = (await page.$('div'))!;
+ await divHandle.click();
+ await divHandle.click({
+ offset: {
+ x: 10,
+ y: 15,
+ },
+ });
+ await shortWaitForArrayToHaveAtLeastNElements(clicks, 2);
+ expect(clicks).toEqual([
+ [45 + 60, 45 + 30], // margin + middle point offset
+ [30 + 10, 30 + 15], // margin + offset
+ ]);
+ });
+ it('should work for Shadow DOM v1', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/shadow.html');
+ using buttonHandle = await page.evaluateHandle(() => {
+ // @ts-expect-error button is expected to be in the page's scope.
+ return button as HTMLButtonElement;
+ });
+ await buttonHandle.click();
+ expect(
+ await page.evaluate(() => {
+ // @ts-expect-error clicked is expected to be in the page's scope.
+ return clicked;
+ })
+ ).toBe(true);
+ });
+ it('should not work for TextNodes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ using buttonTextNode = await page.evaluateHandle(() => {
+ return document.querySelector('button')!.firstChild as HTMLElement;
+ });
+ let error!: Error;
+ await buttonTextNode.click().catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).atLeastOneToContain([
+ 'Node is not of type HTMLElement',
+ 'no such node',
+ ]);
+ });
+ it('should throw for detached nodes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ using button = (await page.$('button'))!;
+ await page.evaluate((button: HTMLElement) => {
+ return button.remove();
+ }, button);
+ let error!: Error;
+ await button.click().catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).atLeastOneToContain([
+ 'Node is detached from document',
+ 'no such node',
+ ]);
+ });
+ it('should throw for hidden nodes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ using button = (await page.$('button'))!;
+ await page.evaluate((button: HTMLElement) => {
+ return (button.style.display = 'none');
+ }, button);
+ const error = await button.click().catch(error_ => {
+ return error_;
+ });
+ expect(error.message).atLeastOneToContain([
+ 'Node is either not clickable or not an Element',
+ 'no such element',
+ ]);
+ });
+ it('should throw for recursively hidden nodes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ using button = (await page.$('button'))!;
+ await page.evaluate((button: HTMLElement) => {
+ return (button.parentElement!.style.display = 'none');
+ }, button);
+ const error = await button.click().catch(error_ => {
+ return error_;
+ });
+ expect(error.message).atLeastOneToContain([
+ 'Node is either not clickable or not an Element',
+ 'no such element',
+ ]);
+ });
+ it('should throw for <br> elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('hello<br>goodbye');
+ using br = (await page.$('br'))!;
+ const error = await br.click().catch(error_ => {
+ return error_;
+ });
+ expect(error.message).atLeastOneToContain([
+ 'Node is either not clickable or not an Element',
+ 'no such node',
+ ]);
+ });
+ });
+
+ describe('ElementHandle.clickablePoint', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ document.body.style.padding = '0';
+ document.body.style.margin = '0';
+ document.body.innerHTML = `
+ <div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div>
+ `;
+ });
+ await page.evaluate(async () => {
+ return await new Promise(resolve => {
+ return window.requestAnimationFrame(resolve);
+ });
+ });
+ using divHandle = (await page.$('div'))!;
+ expect(await divHandle.clickablePoint()).toEqual({
+ x: 45 + 60, // margin + middle point offset
+ y: 45 + 30, // margin + middle point offset
+ });
+ expect(
+ await divHandle.clickablePoint({
+ x: 10,
+ y: 15,
+ })
+ ).toEqual({
+ x: 30 + 10, // margin + offset
+ y: 30 + 15, // margin + offset
+ });
+ });
+
+ it('should not work if the click box is not visible', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<button style="width: 10px; height: 10px; position: absolute; left: -20px"></button>'
+ );
+ using handle = await page.locator('button').waitHandle();
+ await expect(handle.clickablePoint()).rejects.toBeInstanceOf(Error);
+
+ await page.setContent(
+ '<button style="width: 10px; height: 10px; position: absolute; right: -20px"></button>'
+ );
+ using handle2 = await page.locator('button').waitHandle();
+ await expect(handle2.clickablePoint()).rejects.toBeInstanceOf(Error);
+
+ await page.setContent(
+ '<button style="width: 10px; height: 10px; position: absolute; top: -20px"></button>'
+ );
+ using handle3 = await page.locator('button').waitHandle();
+ await expect(handle3.clickablePoint()).rejects.toBeInstanceOf(Error);
+
+ await page.setContent(
+ '<button style="width: 10px; height: 10px; position: absolute; bottom: -20px"></button>'
+ );
+ using handle4 = await page.locator('button').waitHandle();
+ await expect(handle4.clickablePoint()).rejects.toBeInstanceOf(Error);
+ });
+
+ it('should not work if the click box is not visible due to the iframe', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ `<iframe name='frame' style='position: absolute; left: -100px' srcdoc="<button style='width: 10px; height: 10px;'></button>"></iframe>`
+ );
+ const frame = await page.waitForFrame(frame => {
+ return frame.name() === 'frame';
+ });
+
+ using handle = await frame.locator('button').waitHandle();
+ await expect(handle.clickablePoint()).rejects.toBeInstanceOf(Error);
+
+ await page.setContent(
+ `<iframe name='frame2' style='position: absolute; top: -100px' srcdoc="<button style='width: 10px; height: 10px;'></button>"></iframe>`
+ );
+ const frame2 = await page.waitForFrame(frame => {
+ return frame.name() === 'frame2';
+ });
+
+ using handle2 = await frame2.locator('button').waitHandle();
+ await expect(handle2.clickablePoint()).rejects.toBeInstanceOf(Error);
+ });
+
+ it('should work for iframes', async () => {
+ const {page} = await getTestState();
+ await page.evaluate(() => {
+ document.body.style.padding = '10px';
+ document.body.style.margin = '10px';
+ document.body.innerHTML = `
+ <iframe style="border: none; margin: 0; padding: 0;" seamless sandbox srcdoc="<style>* { margin: 0; padding: 0;}</style><div style='cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;' />"></iframe>
+ `;
+ });
+ await page.evaluate(async () => {
+ return await new Promise(resolve => {
+ return window.requestAnimationFrame(resolve);
+ });
+ });
+ const frame = page.frames()[1]!;
+ using divHandle = (await frame.$('div'))!;
+ expect(await divHandle.clickablePoint()).toEqual({
+ x: 20 + 45 + 60, // iframe pos + margin + middle point offset
+ y: 20 + 45 + 30, // iframe pos + margin + middle point offset
+ });
+ expect(
+ await divHandle.clickablePoint({
+ x: 10,
+ y: 15,
+ })
+ ).toEqual({
+ x: 20 + 30 + 10, // iframe pos + margin + offset
+ y: 20 + 30 + 15, // iframe pos + margin + offset
+ });
+ });
+ });
+
+ describe('Element.waitForSelector', () => {
+ it('should wait correctly with waitForSelector on an element', async () => {
+ const {page} = await getTestState();
+ const waitFor = page.waitForSelector('.foo').catch(err => {
+ return err;
+ }) as Promise<ElementHandle<HTMLDivElement>>;
+ // Set the page content after the waitFor has been started.
+ await page.setContent(
+ '<div id="not-foo"></div><div class="bar">bar2</div><div class="foo">Foo1</div>'
+ );
+ using element = (await waitFor)!;
+ if (element instanceof Error) {
+ throw element;
+ }
+ expect(element).toBeDefined();
+
+ const innerWaitFor = element.waitForSelector('.bar').catch(err => {
+ return err;
+ }) as Promise<ElementHandle<HTMLDivElement>>;
+ await element.evaluate(el => {
+ el.innerHTML = '<div class="bar">bar1</div>';
+ });
+ using element2 = (await innerWaitFor)!;
+ if (element2 instanceof Error) {
+ throw element2;
+ }
+ expect(element2).toBeDefined();
+ expect(
+ await element2.evaluate(el => {
+ return (el as HTMLElement).innerText;
+ })
+ ).toStrictEqual('bar1');
+ });
+ });
+
+ describe('Element.waitForXPath', () => {
+ it('should wait correctly with waitForXPath on an element', async () => {
+ const {page} = await getTestState();
+ // Set the page content after the waitFor has been started.
+ await page.setContent(
+ `<div id=el1>
+ el1
+ <div id=el2>
+ el2
+ </div>
+ </div>
+ <div id=el3>
+ el3
+ </div>`
+ );
+
+ using el1 = (await page.waitForSelector(
+ '#el1'
+ )) as ElementHandle<HTMLDivElement>;
+
+ for (const path of ['//div', './/div']) {
+ using e = (await el1.waitForXPath(
+ path
+ )) as ElementHandle<HTMLDivElement>;
+ expect(
+ await e.evaluate(el => {
+ return el.id;
+ })
+ ).toStrictEqual('el2');
+ }
+ });
+ });
+
+ describe('ElementHandle.hover', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ using button = (await page.$('#button-6'))!;
+ await button.hover();
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('button:hover')!.id;
+ })
+ ).toBe('button-6');
+ });
+ });
+
+ describe('ElementHandle.isIntersectingViewport', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ async function getVisibilityForButton(selector: string) {
+ using button = (await page.$(selector))!;
+ return await button.isIntersectingViewport();
+ }
+
+ await page.goto(server.PREFIX + '/offscreenbuttons.html');
+ const buttonsPromises = [];
+ // Firefox seems slow when using `isIntersectingViewport`
+ // so we do all the tasks asynchronously
+ for (let i = 0; i < 11; ++i) {
+ buttonsPromises.push(getVisibilityForButton('#btn' + i));
+ }
+ const buttonVisibility = await Promise.all(buttonsPromises);
+ for (let i = 0; i < 11; ++i) {
+ // All but last button are visible.
+ const visible = i < 10;
+ expect(buttonVisibility[i]).toBe(visible);
+ }
+ });
+ it('should work with threshold', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/offscreenbuttons.html');
+ // a button almost cannot be seen
+ // sometimes we expect to return false by isIntersectingViewport1
+ using button = (await page.$('#btn11'))!;
+ expect(
+ await button.isIntersectingViewport({
+ threshold: 0.001,
+ })
+ ).toBe(false);
+ });
+ it('should work with threshold of 1', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/offscreenbuttons.html');
+ // a button almost cannot be seen
+ // sometimes we expect to return false by isIntersectingViewport1
+ using button = (await page.$('#btn0'))!;
+ expect(
+ await button.isIntersectingViewport({
+ threshold: 1,
+ })
+ ).toBe(true);
+ });
+ it('should work with svg elements', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/inline-svg.html');
+ const [visibleCircle, visibleSvg] = await Promise.all([
+ page.$('circle'),
+ page.$('svg'),
+ ]);
+
+ // Firefox seems slow when using `isIntersectingViewport`
+ // so we do all the tasks asynchronously
+ const [
+ circleThresholdOne,
+ circleThresholdZero,
+ svgThresholdOne,
+ svgThresholdZero,
+ ] = await Promise.all([
+ visibleCircle!.isIntersectingViewport({
+ threshold: 1,
+ }),
+ visibleCircle!.isIntersectingViewport({
+ threshold: 0,
+ }),
+ visibleSvg!.isIntersectingViewport({
+ threshold: 1,
+ }),
+ visibleSvg!.isIntersectingViewport({
+ threshold: 0,
+ }),
+ ]);
+
+ expect(circleThresholdOne).toBe(true);
+ expect(circleThresholdZero).toBe(true);
+ expect(svgThresholdOne).toBe(true);
+ expect(svgThresholdZero).toBe(true);
+
+ const [invisibleCircle, invisibleSvg] = await Promise.all([
+ page.$('div circle'),
+ page.$('div svg'),
+ ]);
+
+ // Firefox seems slow when using `isIntersectingViewport`
+ // so we do all the tasks asynchronously
+ const [
+ invisibleCircleThresholdOne,
+ invisibleCircleThresholdZero,
+ invisibleSvgThresholdOne,
+ invisibleSvgThresholdZero,
+ ] = await Promise.all([
+ invisibleCircle!.isIntersectingViewport({
+ threshold: 1,
+ }),
+ invisibleCircle!.isIntersectingViewport({
+ threshold: 0,
+ }),
+ invisibleSvg!.isIntersectingViewport({
+ threshold: 1,
+ }),
+ invisibleSvg!.isIntersectingViewport({
+ threshold: 0,
+ }),
+ ]);
+
+ expect(invisibleCircleThresholdOne).toBe(false);
+ expect(invisibleCircleThresholdZero).toBe(false);
+ expect(invisibleSvgThresholdOne).toBe(false);
+ expect(invisibleSvgThresholdZero).toBe(false);
+ });
+ });
+
+ describe('Custom queries', function () {
+ afterEach(() => {
+ Puppeteer.clearCustomQueryHandlers();
+ });
+ it('should register and unregister', async () => {
+ const {page} = await getTestState();
+ await page.setContent('<div id="not-foo"></div><div id="foo"></div>');
+
+ // Register.
+ Puppeteer.registerCustomQueryHandler('getById', {
+ queryOne: (_element, selector) => {
+ return document.querySelector(`[id="${selector}"]`);
+ },
+ });
+ using element = (await page.$(
+ 'getById/foo'
+ )) as ElementHandle<HTMLDivElement>;
+ expect(
+ await page.evaluate(element => {
+ return element.id;
+ }, element)
+ ).toBe('foo');
+ const handlerNamesAfterRegistering = Puppeteer.customQueryHandlerNames();
+ expect(handlerNamesAfterRegistering.includes('getById')).toBeTruthy();
+
+ // Unregister.
+ Puppeteer.unregisterCustomQueryHandler('getById');
+ try {
+ await page.$('getById/foo');
+ throw new Error('Custom query handler name not set - throw expected');
+ } catch (error) {
+ expect(error).not.toStrictEqual(
+ new Error('Custom query handler name not set - throw expected')
+ );
+ }
+ const handlerNamesAfterUnregistering =
+ Puppeteer.customQueryHandlerNames();
+ expect(handlerNamesAfterUnregistering.includes('getById')).toBeFalsy();
+ });
+ it('should throw with invalid query names', async () => {
+ try {
+ Puppeteer.registerCustomQueryHandler('1/2/3', {
+ queryOne: () => {
+ return document.querySelector('foo');
+ },
+ });
+ throw new Error(
+ 'Custom query handler name was invalid - throw expected'
+ );
+ } catch (error) {
+ expect(error).toStrictEqual(
+ new Error('Custom query handler names may only contain [a-zA-Z]')
+ );
+ }
+ });
+ it('should work for multiple elements', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>'
+ );
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryAll: (_element, selector) => {
+ return [...document.querySelectorAll(`.${selector}`)];
+ },
+ });
+ const elements = (await page.$$('getByClass/foo')) as Array<
+ ElementHandle<HTMLDivElement>
+ >;
+ const classNames = await Promise.all(
+ elements.map(async element => {
+ return await page.evaluate(element => {
+ return element.className;
+ }, element);
+ })
+ );
+
+ expect(classNames).toStrictEqual(['foo', 'foo baz']);
+ });
+ it('should eval correctly', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>'
+ );
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryAll: (_element, selector) => {
+ return [...document.querySelectorAll(`.${selector}`)];
+ },
+ });
+ const elements = await page.$$eval('getByClass/foo', divs => {
+ return divs.length;
+ });
+
+ expect(elements).toBe(2);
+ });
+ it('should wait correctly with waitForSelector', async () => {
+ const {page} = await getTestState();
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryOne: (element, selector) => {
+ return (element as Element).querySelector(`.${selector}`);
+ },
+ });
+ const waitFor = page.waitForSelector('getByClass/foo').catch(err => {
+ return err;
+ });
+
+ // Set the page content after the waitFor has been started.
+ await page.setContent(
+ '<div id="not-foo"></div><div class="foo">Foo1</div>'
+ );
+ const element = await waitFor;
+
+ if (element instanceof Error) {
+ throw element;
+ }
+
+ expect(element).toBeDefined();
+ });
+
+ it('should wait correctly with waitForSelector on an element', async () => {
+ const {page} = await getTestState();
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryOne: (element, selector) => {
+ return (element as Element).querySelector(`.${selector}`);
+ },
+ });
+ const waitFor = page.waitForSelector('getByClass/foo').catch(err => {
+ return err;
+ }) as Promise<ElementHandle<HTMLElement>>;
+
+ // Set the page content after the waitFor has been started.
+ await page.setContent(
+ '<div id="not-foo"></div><div class="bar">bar2</div><div class="foo">Foo1</div>'
+ );
+ using element = (await waitFor)!;
+ if (element instanceof Error) {
+ throw element;
+ }
+ expect(element).toBeDefined();
+
+ const innerWaitFor = element
+ .waitForSelector('getByClass/bar')
+ .catch(err => {
+ return err;
+ }) as Promise<ElementHandle<HTMLElement>>;
+
+ await element.evaluate(el => {
+ el.innerHTML = '<div class="bar">bar1</div>';
+ });
+
+ using element2 = (await innerWaitFor)!;
+ if (element2 instanceof Error) {
+ throw element2;
+ }
+ expect(element2).toBeDefined();
+ expect(
+ await element2.evaluate(el => {
+ return el.innerText;
+ })
+ ).toStrictEqual('bar1');
+ });
+
+ it('should wait correctly with waitFor', async () => {
+ /* page.waitFor is deprecated so we silence the warning to avoid test noise */
+ sinon.stub(console, 'warn').callsFake(() => {});
+ const {page} = await getTestState();
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryOne: (element, selector) => {
+ return (element as Element).querySelector(`.${selector}`);
+ },
+ });
+ const waitFor = page.waitForSelector('getByClass/foo').catch(err => {
+ return err;
+ });
+
+ // Set the page content after the waitFor has been started.
+ await page.setContent(
+ '<div id="not-foo"></div><div class="foo">Foo1</div>'
+ );
+ const element = await waitFor;
+
+ if (element instanceof Error) {
+ throw element;
+ }
+
+ expect(element).toBeDefined();
+ });
+ it('should work when both queryOne and queryAll are registered', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div id="not-foo"></div><div class="foo"><div id="nested-foo" class="foo"/></div><div class="foo baz">Foo2</div>'
+ );
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryOne: (element, selector) => {
+ return (element as Element).querySelector(`.${selector}`);
+ },
+ queryAll: (element, selector) => {
+ return [...(element as Element).querySelectorAll(`.${selector}`)];
+ },
+ });
+
+ using element = (await page.$('getByClass/foo'))!;
+ expect(element).toBeDefined();
+
+ const elements = await page.$$('getByClass/foo');
+ expect(elements).toHaveLength(3);
+ });
+ it('should eval when both queryOne and queryAll are registered', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div id="not-foo"></div><div class="foo">text</div><div class="foo baz">content</div>'
+ );
+ Puppeteer.registerCustomQueryHandler('getByClass', {
+ queryOne: (element, selector) => {
+ return (element as Element).querySelector(`.${selector}`);
+ },
+ queryAll: (element, selector) => {
+ return [...(element as Element).querySelectorAll(`.${selector}`)];
+ },
+ });
+
+ const txtContent = await page.$eval('getByClass/foo', div => {
+ return div.textContent;
+ });
+ expect(txtContent).toBe('text');
+
+ const txtContents = await page.$$eval('getByClass/foo', divs => {
+ return divs
+ .map(d => {
+ return d.textContent;
+ })
+ .join('');
+ });
+ expect(txtContents).toBe('textcontent');
+ });
+
+ it('should work with function shorthands', async () => {
+ const {page} = await getTestState();
+ await page.setContent('<div id="not-foo"></div><div id="foo"></div>');
+
+ Puppeteer.registerCustomQueryHandler('getById', {
+ // This is a function shorthand
+ queryOne(_element, selector) {
+ return document.querySelector(`[id="${selector}"]`);
+ },
+ });
+
+ using element = (await page.$(
+ 'getById/foo'
+ )) as ElementHandle<HTMLDivElement>;
+ expect(
+ await page.evaluate(element => {
+ return element.id;
+ }, element)
+ ).toBe('foo');
+ });
+ });
+
+ describe('ElementHandle.toElement', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ await page.setContent('<div class="foo">Foo1</div>');
+ using element = await page.$('.foo');
+ using div = await element?.toElement('div');
+ expect(div).toBeDefined();
+ });
+ });
+
+ describe('ElementHandle[Symbol.dispose]', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ using handle = await page.evaluateHandle('document');
+ const spy = sinon.spy(handle, disposeSymbol);
+ {
+ using _ = handle;
+ }
+ expect(handle).toBeInstanceOf(ElementHandle);
+ expect(spy.calledOnce).toBeTruthy();
+ expect(handle.disposed).toBeTruthy();
+ });
+ });
+
+ describe('ElementHandle[Symbol.asyncDispose]', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ using handle = await page.evaluateHandle('document');
+ const spy = sinon.spy(handle, asyncDisposeSymbol);
+ {
+ await using _ = handle;
+ }
+ expect(handle).toBeInstanceOf(ElementHandle);
+ expect(spy.calledOnce).toBeTruthy();
+ expect(handle.disposed).toBeTruthy();
+ });
+ });
+
+ describe('ElementHandle.move', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ using handle = await page.evaluateHandle('document');
+ const spy = sinon.spy(handle, disposeSymbol);
+ {
+ using _ = handle;
+ handle.move();
+ }
+ expect(handle).toBeInstanceOf(ElementHandle);
+ expect(spy.calledOnce).toBeTruthy();
+ expect(handle.disposed).toBeFalsy();
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/emulation.spec.ts b/remote/test/puppeteer/test/src/emulation.spec.ts
new file mode 100644
index 0000000000..823061c450
--- /dev/null
+++ b/remote/test/puppeteer/test/src/emulation.spec.ts
@@ -0,0 +1,553 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {KnownDevices, PredefinedNetworkConditions} from 'puppeteer';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+const iPhone = KnownDevices['iPhone 6'];
+const iPhoneLandscape = KnownDevices['iPhone 6 landscape'];
+
+describe('Emulation', () => {
+ setupTestBrowserHooks();
+
+ describe('Page.viewport', function () {
+ it('should get the proper viewport size', async () => {
+ const {page} = await getTestState();
+
+ expect(page.viewport()).toEqual({width: 800, height: 600});
+ await page.setViewport({width: 123, height: 456});
+ expect(page.viewport()).toEqual({width: 123, height: 456});
+ });
+ it('should support mobile emulation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ expect(
+ await page.evaluate(() => {
+ return window.innerWidth;
+ })
+ ).toBe(800);
+ await page.setViewport(iPhone.viewport);
+ expect(
+ await page.evaluate(() => {
+ return window.innerWidth;
+ })
+ ).toBe(375);
+ await page.setViewport({width: 400, height: 300});
+ expect(
+ await page.evaluate(() => {
+ return window.innerWidth;
+ })
+ ).toBe(400);
+ });
+ it('should support touch emulation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ expect(
+ await page.evaluate(() => {
+ return 'ontouchstart' in window;
+ })
+ ).toBe(false);
+ await page.setViewport(iPhone.viewport);
+ expect(
+ await page.evaluate(() => {
+ return 'ontouchstart' in window;
+ })
+ ).toBe(true);
+ expect(await page.evaluate(dispatchTouch)).toBe('Received touch');
+ await page.setViewport({width: 100, height: 100});
+ expect(
+ await page.evaluate(() => {
+ return 'ontouchstart' in window;
+ })
+ ).toBe(false);
+
+ function dispatchTouch() {
+ let fulfill!: (value: string) => void;
+ const promise = new Promise(x => {
+ fulfill = x;
+ });
+ window.ontouchstart = () => {
+ fulfill('Received touch');
+ };
+ window.dispatchEvent(new Event('touchstart'));
+
+ fulfill('Did not receive touch');
+
+ return promise;
+ }
+ });
+ it('should be detectable by Modernizr', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/detect-touch.html');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent!.trim();
+ })
+ ).toBe('NO');
+ await page.setViewport(iPhone.viewport);
+ await page.goto(server.PREFIX + '/detect-touch.html');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent!.trim();
+ })
+ ).toBe('YES');
+ });
+ it('should detect touch when applying viewport with touches', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 800, height: 600, hasTouch: true});
+ await page.addScriptTag({url: server.PREFIX + '/modernizr.js'});
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).Modernizr.touchevents;
+ })
+ ).toBe(true);
+ });
+ it('should support landscape emulation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ expect(
+ await page.evaluate(() => {
+ return screen.orientation.type;
+ })
+ ).toBe('portrait-primary');
+ await page.setViewport(iPhoneLandscape.viewport);
+ expect(
+ await page.evaluate(() => {
+ return screen.orientation.type;
+ })
+ ).toBe('landscape-primary');
+ await page.setViewport({width: 100, height: 100});
+ expect(
+ await page.evaluate(() => {
+ return screen.orientation.type;
+ })
+ ).toBe('portrait-primary');
+ });
+ it('should update media queries when resoltion changes', async () => {
+ const {page, server} = await getTestState();
+
+ async function getFontSize() {
+ return await page.evaluate(() => {
+ return parseInt(
+ window.getComputedStyle(document.querySelector('p')!).fontSize,
+ 10
+ );
+ });
+ }
+
+ for (const dpr of [1, 2, 3]) {
+ await page.setViewport({
+ width: 800,
+ height: 600,
+ deviceScaleFactor: dpr,
+ });
+
+ await page.goto(server.PREFIX + '/resolution.html');
+
+ await expect(getFontSize()).resolves.toEqual(dpr);
+
+ const screenshot = await page.screenshot({
+ fullPage: false,
+ });
+ expect(screenshot).toBeGolden(`device-pixel-ratio${dpr}.png`);
+ }
+ });
+ it('should load correct pictures when emulation dpr', async () => {
+ const {page, server} = await getTestState();
+
+ async function getCurrentSrc() {
+ return await page.evaluate(() => {
+ return document.querySelector('img')!.currentSrc;
+ });
+ }
+
+ for (const dpr of [1, 2, 3]) {
+ await page.setViewport({
+ width: 800,
+ height: 600,
+ deviceScaleFactor: dpr,
+ });
+
+ await page.goto(server.PREFIX + '/picture.html');
+
+ await expect(getCurrentSrc()).resolves.toMatch(
+ new RegExp(`logo-${dpr}x.png`)
+ );
+ }
+ });
+ });
+
+ describe('Page.emulate', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ await page.emulate(iPhone);
+ expect(
+ await page.evaluate(() => {
+ return window.innerWidth;
+ })
+ ).toBe(375);
+ expect(
+ await page.evaluate(() => {
+ return navigator.userAgent;
+ })
+ ).toContain('iPhone');
+ });
+ it('should support clicking', async () => {
+ const {page, server} = await getTestState();
+
+ await page.emulate(iPhone);
+ await page.goto(server.PREFIX + '/input/button.html');
+ using button = (await page.$('button'))!;
+ await page.evaluate((button: HTMLElement) => {
+ return (button.style.marginTop = '200px');
+ }, button);
+ await button.click();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe('Clicked');
+ });
+ });
+
+ describe('Page.emulateMediaType', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('screen').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('print').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaType('print');
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('screen').matches;
+ })
+ ).toBe(false);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('print').matches;
+ })
+ ).toBe(true);
+ await page.emulateMediaType();
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('screen').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('print').matches;
+ })
+ ).toBe(false);
+ });
+ it('should throw in case of bad argument', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.emulateMediaType('bad').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Unsupported media type: bad');
+ });
+ });
+
+ describe('Page.emulateMediaFeatures', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.emulateMediaFeatures([
+ {name: 'prefers-reduced-motion', value: 'reduce'},
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-reduced-motion: reduce)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-reduced-motion: no-preference)').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaFeatures([
+ {name: 'prefers-color-scheme', value: 'light'},
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-color-scheme: light)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-color-scheme: dark)').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaFeatures([
+ {name: 'prefers-color-scheme', value: 'dark'},
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-color-scheme: dark)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-color-scheme: light)').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaFeatures([
+ {name: 'prefers-reduced-motion', value: 'reduce'},
+ {name: 'prefers-color-scheme', value: 'light'},
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-reduced-motion: reduce)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-reduced-motion: no-preference)').matches;
+ })
+ ).toBe(false);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-color-scheme: light)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(prefers-color-scheme: dark)').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaFeatures([{name: 'color-gamut', value: 'srgb'}]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: p3)').matches;
+ })
+ ).toBe(false);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: srgb)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: rec2020)').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaFeatures([{name: 'color-gamut', value: 'p3'}]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: p3)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: srgb)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: rec2020)').matches;
+ })
+ ).toBe(false);
+ await page.emulateMediaFeatures([
+ {name: 'color-gamut', value: 'rec2020'},
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: p3)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: srgb)').matches;
+ })
+ ).toBe(true);
+ expect(
+ await page.evaluate(() => {
+ return matchMedia('(color-gamut: rec2020)').matches;
+ })
+ ).toBe(true);
+ });
+ it('should throw in case of bad argument', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .emulateMediaFeatures([{name: 'bad', value: ''}])
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Unsupported media feature: bad');
+ });
+ });
+
+ describe('Page.emulateTimezone', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ (globalThis as any).date = new Date(1479579154987);
+ });
+ await page.emulateTimezone('America/Jamaica');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).date.toString();
+ })
+ ).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)');
+
+ await page.emulateTimezone('Pacific/Honolulu');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).date.toString();
+ })
+ ).toBe(
+ 'Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)'
+ );
+
+ await page.emulateTimezone('America/Buenos_Aires');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).date.toString();
+ })
+ ).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)');
+
+ await page.emulateTimezone('Europe/Berlin');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).date.toString();
+ })
+ ).toBe(
+ 'Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)'
+ );
+ });
+
+ it('should throw for invalid timezone IDs', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.emulateTimezone('Foo/Bar').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Invalid timezone ID: Foo/Bar');
+ await page.emulateTimezone('Baz/Qux').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Invalid timezone ID: Baz/Qux');
+ });
+ });
+
+ describe('Page.emulateVisionDeficiency', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+
+ {
+ await page.emulateVisionDeficiency('none');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('screenshot-sanity.png');
+ }
+
+ {
+ await page.emulateVisionDeficiency('achromatopsia');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('vision-deficiency-achromatopsia.png');
+ }
+
+ {
+ await page.emulateVisionDeficiency('blurredVision');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('vision-deficiency-blurredVision.png');
+ }
+
+ {
+ await page.emulateVisionDeficiency('deuteranopia');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('vision-deficiency-deuteranopia.png');
+ }
+
+ {
+ await page.emulateVisionDeficiency('protanopia');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('vision-deficiency-protanopia.png');
+ }
+
+ {
+ await page.emulateVisionDeficiency('tritanopia');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('vision-deficiency-tritanopia.png');
+ }
+
+ {
+ await page.emulateVisionDeficiency('none');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('screenshot-sanity.png');
+ }
+ });
+
+ it('should throw for invalid vision deficiencies', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ // @ts-expect-error deliberately passing invalid deficiency
+ .emulateVisionDeficiency('invalid')
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe('Unsupported vision deficiency: invalid');
+ });
+ });
+
+ describe('Page.emulateNetworkConditions', function () {
+ it('should change navigator.connection.effectiveType', async () => {
+ const {page} = await getTestState();
+
+ const slow3G = PredefinedNetworkConditions['Slow 3G']!;
+ const fast3G = PredefinedNetworkConditions['Fast 3G']!;
+
+ expect(
+ await page.evaluate('window.navigator.connection.effectiveType')
+ ).toBe('4g');
+ await page.emulateNetworkConditions(fast3G);
+ expect(
+ await page.evaluate('window.navigator.connection.effectiveType')
+ ).toBe('3g');
+ await page.emulateNetworkConditions(slow3G);
+ expect(
+ await page.evaluate('window.navigator.connection.effectiveType')
+ ).toBe('2g');
+ await page.emulateNetworkConditions(null);
+ });
+ });
+
+ describe('Page.emulateCPUThrottling', function () {
+ it('should change the CPU throttling rate successfully', async () => {
+ const {page} = await getTestState();
+
+ await page.emulateCPUThrottling(100);
+ await page.emulateCPUThrottling(null);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/evaluation.spec.ts b/remote/test/puppeteer/test/src/evaluation.spec.ts
new file mode 100644
index 0000000000..3305b59cc2
--- /dev/null
+++ b/remote/test/puppeteer/test/src/evaluation.spec.ts
@@ -0,0 +1,607 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame} from './utils.js';
+
+describe('Evaluation specs', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.evaluate', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return 7 * 3;
+ });
+ expect(result).toBe(21);
+ });
+ it('should transfer BigInt', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate((a: bigint) => {
+ return a;
+ }, BigInt(42));
+ expect(result).toBe(BigInt(42));
+ });
+ it('should transfer NaN', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(a => {
+ return a;
+ }, NaN);
+ expect(Object.is(result, NaN)).toBe(true);
+ });
+ it('should transfer -0', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(a => {
+ return a;
+ }, -0);
+ expect(Object.is(result, -0)).toBe(true);
+ });
+ it('should transfer Infinity', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(a => {
+ return a;
+ }, Infinity);
+ expect(Object.is(result, Infinity)).toBe(true);
+ });
+ it('should transfer -Infinity', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(a => {
+ return a;
+ }, -Infinity);
+ expect(Object.is(result, -Infinity)).toBe(true);
+ });
+ it('should transfer arrays', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(
+ a => {
+ return a;
+ },
+ [1, 2, 3]
+ );
+ expect(result).toEqual([1, 2, 3]);
+ });
+ it('should transfer arrays as arrays, not objects', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(
+ a => {
+ return Array.isArray(a);
+ },
+ [1, 2, 3]
+ );
+ expect(result).toBe(true);
+ });
+ it('should modify global environment', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ return ((globalThis as any).globalVar = 123);
+ });
+ expect(await page.evaluate('globalVar')).toBe(123);
+ });
+ it('should evaluate in the page context', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/global-var.html');
+ expect(await page.evaluate('globalVar')).toBe(123);
+ });
+ it('should replace symbols with undefined', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return [Symbol('foo4'), 'foo'];
+ })
+ ).toEqual([undefined, 'foo']);
+ });
+ it('should work with function shorthands', async () => {
+ const {page} = await getTestState();
+
+ const a = {
+ sum(a: number, b: number) {
+ return a + b;
+ },
+
+ async mult(a: number, b: number) {
+ return a * b;
+ },
+ };
+ expect(await page.evaluate(a.sum, 1, 2)).toBe(3);
+ expect(await page.evaluate(a.mult, 2, 4)).toBe(8);
+ });
+ it('should work with unicode chars', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(
+ a => {
+ return a['中文字符'];
+ },
+ {
+ 中文字符: 42,
+ }
+ );
+ expect(result).toBe(42);
+ });
+ it('should throw when evaluation triggers reload', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .evaluate(() => {
+ location.reload();
+ return new Promise(() => {});
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Protocol error');
+ });
+ it('should await promise', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return Promise.resolve(8 * 7);
+ });
+ expect(result).toBe(56);
+ });
+ it('should work right after framenavigated', async () => {
+ const {page, server} = await getTestState();
+
+ let frameEvaluation = null;
+ page.on('framenavigated', async frame => {
+ frameEvaluation = frame.evaluate(() => {
+ return 6 * 7;
+ });
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(await frameEvaluation).toBe(42);
+ });
+ it('should work from-inside an exposed function', async () => {
+ const {page} = await getTestState();
+
+ // Setup inpage callback, which calls Page.evaluate
+ await page.exposeFunction(
+ 'callController',
+ async function (a: number, b: number) {
+ return await page.evaluate(
+ (a: number, b: number): number => {
+ return a * b;
+ },
+ a,
+ b
+ );
+ }
+ );
+ const result = await page.evaluate(async function () {
+ return (globalThis as any).callController(9, 3);
+ });
+ expect(result).toBe(27);
+ });
+ it('should reject promise with exception', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .evaluate(() => {
+ // @ts-expect-error we know the object doesn't exist
+ return notExistingObject.property;
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ expect(error.message).toContain('notExistingObject');
+ });
+ it('should support thrown strings as error messages', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .evaluate(() => {
+ throw 'qwerty';
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toEqual('qwerty');
+ });
+ it('should support thrown numbers as error messages', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .evaluate(() => {
+ throw 100500;
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toEqual(100500);
+ });
+ it('should return complex objects', async () => {
+ const {page} = await getTestState();
+
+ const object = {foo: 'bar!'};
+ const result = await page.evaluate(a => {
+ return a;
+ }, object);
+ expect(result).not.toBe(object);
+ expect(result).toEqual(object);
+ });
+ it('should return BigInt', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return BigInt(42);
+ });
+ expect(result).toBe(BigInt(42));
+ });
+ it('should return NaN', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return NaN;
+ });
+ expect(Object.is(result, NaN)).toBe(true);
+ });
+ it('should return -0', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return -0;
+ });
+ expect(Object.is(result, -0)).toBe(true);
+ });
+ it('should return Infinity', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return Infinity;
+ });
+ expect(Object.is(result, Infinity)).toBe(true);
+ });
+ it('should return -Infinity', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return -Infinity;
+ });
+ expect(Object.is(result, -Infinity)).toBe(true);
+ });
+ it('should accept "null" as one of multiple parameters', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(
+ (a, b) => {
+ return Object.is(a, null) && Object.is(b, 'foo');
+ },
+ null,
+ 'foo'
+ );
+ expect(result).toBe(true);
+ });
+ it('should properly serialize null fields', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return {a: undefined};
+ })
+ ).toEqual({});
+ });
+ it('should return undefined for non-serializable objects', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return window;
+ })
+ ).toBe(undefined);
+ });
+ it('should return promise as empty object', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ return {
+ promise: new Promise(resolve => {
+ setTimeout(resolve, 1000);
+ }),
+ };
+ });
+ expect(result).toEqual({
+ promise: {},
+ });
+ });
+ it('should work for circular object', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ const a: Record<string, unknown> = {
+ c: 5,
+ d: {
+ foo: 'bar',
+ },
+ };
+ const b = {a};
+ a['b'] = b;
+ return a;
+ });
+ expect(result).toMatchObject({
+ c: 5,
+ d: {
+ foo: 'bar',
+ },
+ b: {
+ a: undefined,
+ },
+ });
+ });
+ it('should accept a string', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate('1 + 2');
+ expect(result).toBe(3);
+ });
+ it('should accept a string with semi colons', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate('1 + 5;');
+ expect(result).toBe(6);
+ });
+ it('should accept a string with comments', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate('2 + 5;\n// do some math!');
+ expect(result).toBe(7);
+ });
+ it('should accept element handle as an argument', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>42</section>');
+ using element = (await page.$('section'))!;
+ const text = await page.evaluate(e => {
+ return e.textContent;
+ }, element);
+ expect(text).toBe('42');
+ });
+ it('should throw if underlying element was disposed', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>39</section>');
+ using element = (await page.$('section'))!;
+ expect(element).toBeTruthy();
+ // We want to dispose early.
+ await element.dispose();
+ let error!: Error;
+ await page
+ .evaluate((e: HTMLElement) => {
+ return e.textContent;
+ }, element)
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('JSHandle is disposed');
+ });
+ it('should throw if elementHandles are from other frames', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ using bodyHandle = await page.frames()[1]!.$('body');
+ let error!: Error;
+ await page
+ .evaluate(body => {
+ return body?.innerHTML;
+ }, bodyHandle)
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ expect(error.message).toContain(
+ 'JSHandles can be evaluated only in the context they were created'
+ );
+ });
+ it('should simulate a user gesture', async () => {
+ const {page} = await getTestState();
+
+ const result = await page.evaluate(() => {
+ document.body.appendChild(document.createTextNode('test'));
+ document.execCommand('selectAll');
+ return document.execCommand('copy');
+ });
+ expect(result).toBe(true);
+ });
+ it('should not throw an error when evaluation does a navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/one-style.html');
+ const onRequest = server.waitForRequest('/empty.html');
+ const result = await page.evaluate(() => {
+ (window as any).location = '/empty.html';
+ return [42];
+ });
+ expect(result).toEqual([42]);
+ await onRequest;
+ });
+ it('should transfer 100Mb of data from page to node.js', async function () {
+ this.timeout(25_000);
+ const {page} = await getTestState();
+
+ const a = await page.evaluate(() => {
+ return Array(100 * 1024 * 1024 + 1).join('a');
+ });
+ expect(a.length).toBe(100 * 1024 * 1024);
+ });
+ it('should throw error with detailed information on exception inside promise', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .evaluate(() => {
+ return new Promise(() => {
+ throw new Error('Error in promise');
+ });
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Error in promise');
+ });
+
+ it('should return properly serialize objects with unknown type fields', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ "<img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='>"
+ );
+
+ const result = await page.evaluate(async () => {
+ const image = document.querySelector('img')!;
+ const imageBitmap = await createImageBitmap(image);
+
+ return {
+ a: 'foo',
+ b: imageBitmap,
+ };
+ });
+
+ expect(result).toEqual({
+ a: 'foo',
+ b: undefined,
+ });
+ });
+ });
+
+ describe('Page.evaluateOnNewDocument', function () {
+ it('should evaluate before anything else on the page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.evaluateOnNewDocument(function () {
+ (globalThis as any).injected = 123;
+ });
+ await page.goto(server.PREFIX + '/tamperable.html');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe(123);
+ });
+ it('should work with CSP', async () => {
+ const {page, server} = await getTestState();
+
+ server.setCSP('/empty.html', 'script-src ' + server.PREFIX);
+ await page.evaluateOnNewDocument(function () {
+ (globalThis as any).injected = 123;
+ });
+ await page.goto(server.PREFIX + '/empty.html');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).injected;
+ })
+ ).toBe(123);
+
+ // Make sure CSP works.
+ await page.addScriptTag({content: 'window.e = 10;'}).catch(error => {
+ return void error;
+ });
+ expect(
+ await page.evaluate(() => {
+ return (window as any).e;
+ })
+ ).toBe(undefined);
+ });
+ });
+
+ describe('Page.removeScriptToEvaluateOnNewDocument', function () {
+ it('should remove new document script', async () => {
+ const {page, server} = await getTestState();
+
+ const {identifier} = await page.evaluateOnNewDocument(function () {
+ (globalThis as any).injected = 123;
+ });
+ await page.goto(server.PREFIX + '/tamperable.html');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result;
+ })
+ ).toBe(123);
+
+ await page.removeScriptToEvaluateOnNewDocument(identifier);
+ await page.reload();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result || null;
+ })
+ ).toBe(null);
+ });
+ });
+
+ describe('Frame.evaluate', function () {
+ it('should have different execution contexts', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(page.frames()).toHaveLength(2);
+ await page.frames()[0]!.evaluate(() => {
+ return ((globalThis as any).FOO = 'foo');
+ });
+ await page.frames()[1]!.evaluate(() => {
+ return ((globalThis as any).FOO = 'bar');
+ });
+ expect(
+ await page.frames()[0]!.evaluate(() => {
+ return (globalThis as any).FOO;
+ })
+ ).toBe('foo');
+ expect(
+ await page.frames()[1]!.evaluate(() => {
+ return (globalThis as any).FOO;
+ })
+ ).toBe('bar');
+ });
+ it('should have correct execution contexts', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame.html');
+ expect(page.frames()).toHaveLength(2);
+ expect(
+ await page.frames()[0]!.evaluate(() => {
+ return document.body.textContent!.trim();
+ })
+ ).toBe('');
+ expect(
+ await page.frames()[1]!.evaluate(() => {
+ return document.body.textContent!.trim();
+ })
+ ).toBe(`Hi, I'm frame`);
+ });
+ it('should execute after cross-site navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const mainFrame = page.mainFrame();
+ expect(
+ await mainFrame.evaluate(() => {
+ return window.location.href;
+ })
+ ).toContain('localhost');
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
+ expect(
+ await mainFrame.evaluate(() => {
+ return window.location.href;
+ })
+ ).toContain('127');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/fixtures.spec.ts b/remote/test/puppeteer/test/src/fixtures.spec.ts
new file mode 100644
index 0000000000..ca11e94cac
--- /dev/null
+++ b/remote/test/puppeteer/test/src/fixtures.spec.ts
@@ -0,0 +1,114 @@
+/**
+ * @license
+ * Copyright 2019 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {spawn, execSync} from 'child_process';
+import path from 'path';
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+
+describe('Fixtures', function () {
+ setupTestBrowserHooks();
+
+ it('dumpio option should work with pipe option', async () => {
+ const {defaultBrowserOptions, puppeteerPath, headless} =
+ await getTestState();
+ if (headless !== 'true') {
+ // This test only works in the old headless mode.
+ return;
+ }
+
+ let dumpioData = '';
+ const options = Object.assign({}, defaultBrowserOptions, {
+ pipe: true,
+ dumpio: true,
+ });
+ const res = spawn('node', [
+ path.join(__dirname, '../fixtures', 'dumpio.js'),
+ puppeteerPath,
+ JSON.stringify(options),
+ ]);
+ res.stderr.on('data', data => {
+ dumpioData += data.toString('utf8');
+ });
+ await new Promise(resolve => {
+ return res.on('close', resolve);
+ });
+ expect(dumpioData).toContain('message from dumpio');
+ });
+ it('should dump browser process stderr', async () => {
+ const {defaultBrowserOptions, puppeteerPath} = await getTestState();
+
+ let dumpioData = '';
+ const options = Object.assign({}, defaultBrowserOptions, {dumpio: true});
+ const res = spawn('node', [
+ path.join(__dirname, '../fixtures', 'dumpio.js'),
+ puppeteerPath,
+ JSON.stringify(options),
+ ]);
+ res.stderr.on('data', data => {
+ dumpioData += data.toString('utf8');
+ });
+ await new Promise(resolve => {
+ return res.on('close', resolve);
+ });
+ expect(dumpioData).toContain('DevTools listening on ws://');
+ });
+ it('should close the browser when the node process closes', async () => {
+ const {defaultBrowserOptions, puppeteerPath, puppeteer} =
+ await getTestState();
+
+ const options = Object.assign({}, defaultBrowserOptions, {
+ // Disable DUMPIO to cleanly read stdout.
+ dumpio: false,
+ });
+ const res = spawn('node', [
+ path.join(__dirname, '../fixtures', 'closeme.js'),
+ puppeteerPath,
+ JSON.stringify(options),
+ ]);
+ let killed = false;
+ function killProcess() {
+ if (killed) {
+ return;
+ }
+ if (process.platform === 'win32') {
+ execSync(`taskkill /pid ${res.pid} /T /F`);
+ } else {
+ process.kill(res.pid!);
+ }
+ killed = true;
+ }
+ try {
+ let wsEndPointCallback: (value: string) => void;
+ const wsEndPointPromise = new Promise<string>(x => {
+ wsEndPointCallback = x;
+ });
+ let output = '';
+ res.stdout.on('data', data => {
+ output += data;
+ if (output.indexOf('\n')) {
+ wsEndPointCallback(output.substring(0, output.indexOf('\n')));
+ }
+ });
+ const browser = await puppeteer.connect({
+ browserWSEndpoint: await wsEndPointPromise,
+ });
+ const promises = [
+ waitEvent(browser, 'disconnected'),
+ new Promise(resolve => {
+ res.on('close', resolve);
+ }),
+ ];
+ killProcess();
+ await Promise.all(promises);
+ } finally {
+ killProcess();
+ }
+ });
+});
diff --git a/remote/test/puppeteer/test/src/frame.spec.ts b/remote/test/puppeteer/test/src/frame.spec.ts
new file mode 100644
index 0000000000..3b2456821a
--- /dev/null
+++ b/remote/test/puppeteer/test/src/frame.spec.ts
@@ -0,0 +1,297 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js';
+import type {Frame} from 'puppeteer-core/internal/api/Frame.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {
+ attachFrame,
+ detachFrame,
+ dumpFrames,
+ navigateFrame,
+ waitEvent,
+} from './utils.js';
+
+describe('Frame specs', function () {
+ setupTestBrowserHooks();
+
+ describe('Frame.evaluateHandle', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const mainFrame = page.mainFrame();
+ using windowHandle = await mainFrame.evaluateHandle(() => {
+ return window;
+ });
+ expect(windowHandle).toBeTruthy();
+ });
+ });
+
+ describe('Frame.evaluate', function () {
+ it('should throw for detached frames', async () => {
+ const {page, server} = await getTestState();
+
+ const frame1 = (await attachFrame(page, 'frame1', server.EMPTY_PAGE))!;
+ await detachFrame(page, 'frame1');
+ let error: Error | undefined;
+ try {
+ await frame1.evaluate(() => {
+ return 7 * 8;
+ });
+ } catch (err) {
+ error = err as Error;
+ }
+ expect(error?.message).toContain('Attempted to use detached Frame');
+ });
+
+ it('allows readonly array to be an argument', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const mainFrame = page.mainFrame();
+
+ // This test checks if Frame.evaluate allows a readonly array to be an argument.
+ // See https://github.com/puppeteer/puppeteer/issues/6953.
+ const readonlyArray: readonly string[] = ['a', 'b', 'c'];
+ await mainFrame.evaluate(arr => {
+ return arr;
+ }, readonlyArray);
+ });
+ });
+
+ describe('Frame.page', function () {
+ it('should retrieve the page from a frame', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const mainFrame = page.mainFrame();
+ expect(mainFrame.page()).toEqual(page);
+ });
+ });
+
+ describe('Frame Management', function () {
+ it('should handle nested frames', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ expect(dumpFrames(page.mainFrame())).toEqual([
+ 'http://localhost:<PORT>/frames/nested-frames.html',
+ ' http://localhost:<PORT>/frames/two-frames.html (2frames)',
+ ' http://localhost:<PORT>/frames/frame.html (uno)',
+ ' http://localhost:<PORT>/frames/frame.html (dos)',
+ ' http://localhost:<PORT>/frames/frame.html (aframe)',
+ ]);
+ });
+ it('should send events when frames are manipulated dynamically', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ // validate frameattached events
+ const attachedFrames: Frame[] = [];
+ page.on('frameattached', frame => {
+ return attachedFrames.push(frame);
+ });
+ await attachFrame(page, 'frame1', './assets/frame.html');
+ expect(attachedFrames).toHaveLength(1);
+ expect(attachedFrames[0]!.url()).toContain('/assets/frame.html');
+
+ // validate framenavigated events
+ const navigatedFrames: Frame[] = [];
+ page.on('framenavigated', frame => {
+ return navigatedFrames.push(frame);
+ });
+ await navigateFrame(page, 'frame1', './empty.html');
+ expect(navigatedFrames).toHaveLength(1);
+ expect(navigatedFrames[0]!.url()).toBe(server.EMPTY_PAGE);
+
+ // validate framedetached events
+ const detachedFrames: Frame[] = [];
+ page.on('framedetached', frame => {
+ return detachedFrames.push(frame);
+ });
+ await detachFrame(page, 'frame1');
+ expect(detachedFrames).toHaveLength(1);
+ expect(detachedFrames[0]!.isDetached()).toBe(true);
+ });
+ it('should send "framenavigated" when navigating on anchor URLs', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await Promise.all([
+ page.goto(server.EMPTY_PAGE + '#foo'),
+ waitEvent(page, 'framenavigated'),
+ ]);
+ expect(page.url()).toBe(server.EMPTY_PAGE + '#foo');
+ });
+ it('should persist mainFrame on cross-process navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const mainFrame = page.mainFrame();
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
+ expect(page.mainFrame() === mainFrame).toBeTruthy();
+ });
+ it('should not send attach/detach events for main frame', async () => {
+ const {page, server} = await getTestState();
+
+ let hasEvents = false;
+ page.on('frameattached', () => {
+ return (hasEvents = true);
+ });
+ page.on('framedetached', () => {
+ return (hasEvents = true);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(hasEvents).toBe(false);
+ });
+ it('should detach child frames on navigation', async () => {
+ const {page, server} = await getTestState();
+
+ let attachedFrames: Frame[] = [];
+ let detachedFrames: Frame[] = [];
+ let navigatedFrames: Frame[] = [];
+ page.on('frameattached', frame => {
+ return attachedFrames.push(frame);
+ });
+ page.on('framedetached', frame => {
+ return detachedFrames.push(frame);
+ });
+ page.on('framenavigated', frame => {
+ return navigatedFrames.push(frame);
+ });
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+
+ expect(attachedFrames).toHaveLength(4);
+ expect(detachedFrames).toHaveLength(0);
+ expect(navigatedFrames).toHaveLength(5);
+
+ attachedFrames = [];
+ detachedFrames = [];
+ navigatedFrames = [];
+ await page.goto(server.EMPTY_PAGE);
+ expect(attachedFrames).toHaveLength(0);
+ expect(detachedFrames).toHaveLength(4);
+ expect(navigatedFrames).toHaveLength(1);
+ });
+ it('should support framesets', async () => {
+ const {page, server} = await getTestState();
+
+ let attachedFrames: Frame[] = [];
+ let detachedFrames: Frame[] = [];
+ let navigatedFrames: Frame[] = [];
+ page.on('frameattached', frame => {
+ return attachedFrames.push(frame);
+ });
+ page.on('framedetached', frame => {
+ return detachedFrames.push(frame);
+ });
+ page.on('framenavigated', frame => {
+ return navigatedFrames.push(frame);
+ });
+ await page.goto(server.PREFIX + '/frames/frameset.html');
+ expect(attachedFrames).toHaveLength(4);
+ expect(detachedFrames).toHaveLength(0);
+ expect(navigatedFrames).toHaveLength(5);
+
+ attachedFrames = [];
+ detachedFrames = [];
+ navigatedFrames = [];
+ await page.goto(server.EMPTY_PAGE);
+ expect(attachedFrames).toHaveLength(0);
+ expect(detachedFrames).toHaveLength(4);
+ expect(navigatedFrames).toHaveLength(1);
+ });
+ it('should report frame from-inside shadow DOM', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/shadow.html');
+ await page.evaluate(async (url: string) => {
+ const frame = document.createElement('iframe');
+ frame.src = url;
+ document.body.shadowRoot!.appendChild(frame);
+ await new Promise(x => {
+ return (frame.onload = x);
+ });
+ }, server.EMPTY_PAGE);
+ expect(page.frames()).toHaveLength(2);
+ expect(page.frames()[1]!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should report frame.name()', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'theFrameId', server.EMPTY_PAGE);
+ await page.evaluate((url: string) => {
+ const frame = document.createElement('iframe');
+ frame.name = 'theFrameName';
+ frame.src = url;
+ document.body.appendChild(frame);
+ return new Promise(x => {
+ return (frame.onload = x);
+ });
+ }, server.EMPTY_PAGE);
+ expect(page.frames()[0]!.name()).toBe('');
+ expect(page.frames()[1]!.name()).toBe('theFrameId');
+ expect(page.frames()[2]!.name()).toBe('theFrameName');
+ });
+ it('should report frame.parent()', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await attachFrame(page, 'frame2', server.EMPTY_PAGE);
+ expect(page.frames()[0]!.parentFrame()).toBe(null);
+ expect(page.frames()[1]!.parentFrame()).toBe(page.mainFrame());
+ expect(page.frames()[2]!.parentFrame()).toBe(page.mainFrame());
+ });
+ it('should report different frame instance when frame re-attaches', async () => {
+ const {page, server} = await getTestState();
+
+ const frame1 = await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ (globalThis as any).frame = document.querySelector('#frame1');
+ (globalThis as any).frame.remove();
+ });
+ expect(frame1!.isDetached()).toBe(true);
+ const [frame2] = await Promise.all([
+ waitEvent(page, 'frameattached'),
+ page.evaluate(() => {
+ return document.body.appendChild((globalThis as any).frame);
+ }),
+ ]);
+ expect(frame2.isDetached()).toBe(false);
+ expect(frame1).not.toBe(frame2);
+ });
+ it('should support url fragment', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame-url-fragment.html');
+
+ expect(page.frames()).toHaveLength(2);
+ expect(page.frames()[1]!.url()).toBe(
+ server.PREFIX + '/frames/frame.html?param=value#fragment'
+ );
+ });
+ it('should support lazy frames', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 1000, height: 1000});
+ await page.goto(server.PREFIX + '/frames/lazy-frame.html');
+
+ expect(
+ page.frames().map(frame => {
+ return frame._hasStartedLoading;
+ })
+ ).toEqual([true, true, false]);
+ });
+ });
+
+ describe('Frame.client', function () {
+ it('should return the client instance', async () => {
+ const {page} = await getTestState();
+ expect(page.mainFrame().client).toBeInstanceOf(CDPSession);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/golden-utils.ts b/remote/test/puppeteer/test/src/golden-utils.ts
new file mode 100644
index 0000000000..939f69c968
--- /dev/null
+++ b/remote/test/puppeteer/test/src/golden-utils.ts
@@ -0,0 +1,169 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'assert';
+import fs from 'fs';
+import path from 'path';
+
+import {diffLines} from 'diff';
+import jpeg from 'jpeg-js';
+import mime from 'mime';
+import pixelmatch from 'pixelmatch';
+import {PNG} from 'pngjs';
+
+interface DiffFile {
+ diff: string | Buffer;
+ ext?: string;
+}
+
+const GoldenComparators = new Map<
+ string,
+ (
+ actualBuffer: string | Buffer,
+ expectedBuffer: string | Buffer,
+ mimeType: string
+ ) => DiffFile | undefined
+>();
+
+const addSuffix = (
+ filePath: string,
+ suffix: string,
+ customExtension?: string
+): string => {
+ const dirname = path.dirname(filePath);
+ const ext = path.extname(filePath);
+ const name = path.basename(filePath, ext);
+ return path.join(dirname, name + suffix + (customExtension || ext));
+};
+
+const compareImages = (
+ actualBuffer: string | Buffer,
+ expectedBuffer: string | Buffer,
+ mimeType: string
+): DiffFile | undefined => {
+ assert(typeof actualBuffer !== 'string');
+ assert(typeof expectedBuffer !== 'string');
+
+ const actual =
+ mimeType === 'image/png'
+ ? PNG.sync.read(actualBuffer)
+ : jpeg.decode(actualBuffer);
+
+ const expected =
+ mimeType === 'image/png'
+ ? PNG.sync.read(expectedBuffer)
+ : jpeg.decode(expectedBuffer);
+ if (expected.width !== actual.width || expected.height !== actual.height) {
+ throw new Error(
+ `Sizes differ: expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px.`
+ );
+ }
+ const diff = new PNG({width: expected.width, height: expected.height});
+ const count = pixelmatch(
+ expected.data,
+ actual.data,
+ diff.data,
+ expected.width,
+ expected.height,
+ {threshold: 0.1}
+ );
+ return count > 0 ? {diff: PNG.sync.write(diff)} : undefined;
+};
+
+const compareText = (
+ actual: string | Buffer,
+ expectedBuffer: string | Buffer
+): DiffFile | undefined => {
+ assert(typeof actual === 'string');
+ const expected = expectedBuffer.toString('utf-8');
+ if (expected === actual) {
+ return;
+ }
+ const result = diffLines(expected, actual);
+ const html = result.reduce(
+ (text, change) => {
+ text += change.added
+ ? `<span class='ins'>${change.value}</span>`
+ : change.removed
+ ? `<span class='del'>${change.value}</span>`
+ : change.value;
+ return text;
+ },
+ `<link rel="stylesheet" href="file://${path.join(
+ __dirname,
+ 'diffstyle.css'
+ )}">`
+ );
+ return {
+ diff: html,
+ ext: '.html',
+ };
+};
+
+GoldenComparators.set('image/png', compareImages);
+GoldenComparators.set('image/jpeg', compareImages);
+GoldenComparators.set('text/plain', compareText);
+
+export const compare = (
+ goldenPath: string,
+ outputPath: string,
+ actual: string | Buffer,
+ goldenName: string
+): {pass: true} | {pass: false; message: string} => {
+ goldenPath = path.normalize(goldenPath);
+ outputPath = path.normalize(outputPath);
+ const expectedPath = path.join(goldenPath, goldenName);
+ const actualPath = path.join(outputPath, goldenName);
+
+ const messageSuffix = `Output is saved in "${path.basename(
+ outputPath + '" directory'
+ )}`;
+
+ if (!fs.existsSync(expectedPath)) {
+ ensureOutputDir();
+ fs.writeFileSync(actualPath, actual);
+ return {
+ pass: false,
+ message: `${goldenName} is missing in golden results. ${messageSuffix}`,
+ };
+ }
+ const expected = fs.readFileSync(expectedPath);
+ const mimeType = mime.getType(goldenName);
+ assert(mimeType);
+ const comparator = GoldenComparators.get(mimeType);
+ if (!comparator) {
+ return {
+ pass: false,
+ message: `Failed to find comparator with type ${mimeType}: ${goldenName}`,
+ };
+ }
+ const result = comparator(actual, expected, mimeType);
+ if (!result) {
+ return {pass: true};
+ }
+ ensureOutputDir();
+ if (goldenPath === outputPath) {
+ fs.writeFileSync(addSuffix(actualPath, '-actual'), actual);
+ } else {
+ fs.writeFileSync(actualPath, actual);
+ // Copy expected to the output/ folder for convenience.
+ fs.writeFileSync(addSuffix(actualPath, '-expected'), expected);
+ }
+ if (result) {
+ const diffPath = addSuffix(actualPath, '-diff', result.ext);
+ fs.writeFileSync(diffPath, result.diff);
+ }
+
+ return {
+ pass: false,
+ message: `${goldenName} mismatch! ${messageSuffix}`,
+ };
+
+ function ensureOutputDir() {
+ if (!fs.existsSync(outputPath)) {
+ fs.mkdirSync(outputPath);
+ }
+ }
+};
diff --git a/remote/test/puppeteer/test/src/headful.spec.ts b/remote/test/puppeteer/test/src/headful.spec.ts
new file mode 100644
index 0000000000..1e3248b4ff
--- /dev/null
+++ b/remote/test/puppeteer/test/src/headful.spec.ts
@@ -0,0 +1,91 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {mkdtemp} from 'fs/promises';
+import os from 'os';
+import path from 'path';
+
+import expect from 'expect';
+import type {PuppeteerLaunchOptions} from 'puppeteer-core/internal/node/PuppeteerNode.js';
+import {rmSync} from 'puppeteer-core/internal/node/util/fs.js';
+
+import {getTestState, isHeadless, launch} from './mocha-utils.js';
+
+const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
+
+(!isHeadless ? describe : describe.skip)('headful tests', function () {
+ /* These tests fire up an actual browser so let's
+ * allow a higher timeout
+ */
+ this.timeout(20_000);
+
+ let headfulOptions: PuppeteerLaunchOptions | undefined;
+ let headlessOptions: PuppeteerLaunchOptions & {headless: boolean};
+
+ const browsers: Array<() => Promise<void>> = [];
+
+ beforeEach(async () => {
+ const {defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+ headfulOptions = Object.assign({}, defaultBrowserOptions, {
+ headless: false,
+ });
+ headlessOptions = Object.assign({}, defaultBrowserOptions, {
+ headless: true,
+ });
+ });
+
+ async function launchBrowser(options: any) {
+ const {browser, close} = await launch(options, {createContext: false});
+ browsers.push(close);
+ return browser;
+ }
+
+ afterEach(async () => {
+ await Promise.all(
+ browsers.map((close, index) => {
+ delete browsers[index];
+ return close();
+ })
+ );
+ });
+
+ describe('HEADFUL', function () {
+ it('headless should be able to read cookies written by headful', async () => {
+ /* Needs investigation into why but this fails consistently on Windows CI. */
+ const {server} = await getTestState({skipLaunch: true});
+
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+ // Write a cookie in headful chrome
+ const headfulBrowser = await launchBrowser(
+ Object.assign({userDataDir}, headfulOptions)
+ );
+ const headfulPage = await headfulBrowser.newPage();
+ await headfulPage.goto(server.EMPTY_PAGE);
+ await headfulPage.evaluate(() => {
+ return (document.cookie =
+ 'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
+ });
+ await headfulBrowser.close();
+ // Read the cookie from headless chrome
+ const headlessBrowser = await launchBrowser(
+ Object.assign({userDataDir}, headlessOptions)
+ );
+ const headlessPage = await headlessBrowser.newPage();
+ await headlessPage.goto(server.EMPTY_PAGE);
+ const cookie = await headlessPage.evaluate(() => {
+ return document.cookie;
+ });
+ await headlessBrowser.close();
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ expect(cookie).toBe('foo=true');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/idle_override.spec.ts b/remote/test/puppeteer/test/src/idle_override.spec.ts
new file mode 100644
index 0000000000..cbcfd34640
--- /dev/null
+++ b/remote/test/puppeteer/test/src/idle_override.spec.ts
@@ -0,0 +1,79 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Emulate idle state', () => {
+ setupTestBrowserHooks();
+
+ async function getIdleState(page: Page) {
+ using stateElement = (await page.$('#state')) as ElementHandle<HTMLElement>;
+ return await page.evaluate(element => {
+ return element.innerText;
+ }, stateElement);
+ }
+
+ async function verifyState(page: Page, expectedState: string) {
+ const actualState = await getIdleState(page);
+ expect(actualState).toEqual(expectedState);
+ }
+
+ it('changing idle state emulation causes change of the IdleDetector state', async () => {
+ const {page, server, context} = await getTestState();
+ await context.overridePermissions(server.PREFIX + '/idle-detector.html', [
+ 'idle-detection',
+ ]);
+
+ await page.goto(server.PREFIX + '/idle-detector.html');
+
+ // Store initial state, as soon as it is not guaranteed to be `active, unlocked`.
+ const initialState = await getIdleState(page);
+
+ // Emulate Idle states and verify IdleDetector updates state accordingly.
+ await page.emulateIdleState({
+ isUserActive: false,
+ isScreenUnlocked: false,
+ });
+ await verifyState(page, 'Idle state: idle, locked.');
+
+ await page.emulateIdleState({
+ isUserActive: true,
+ isScreenUnlocked: false,
+ });
+ await verifyState(page, 'Idle state: active, locked.');
+
+ await page.emulateIdleState({
+ isUserActive: true,
+ isScreenUnlocked: true,
+ });
+ await verifyState(page, 'Idle state: active, unlocked.');
+
+ await page.emulateIdleState({
+ isUserActive: false,
+ isScreenUnlocked: true,
+ });
+ await verifyState(page, 'Idle state: idle, unlocked.');
+
+ // Remove Idle emulation and verify IdleDetector is in initial state.
+ await page.emulateIdleState();
+ await verifyState(page, initialState);
+
+ // Emulate idle state again after removing emulation.
+ await page.emulateIdleState({
+ isUserActive: false,
+ isScreenUnlocked: false,
+ });
+ await verifyState(page, 'Idle state: idle, locked.');
+
+ // Remove emulation second time.
+ await page.emulateIdleState();
+ await verifyState(page, initialState);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts b/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts
new file mode 100644
index 0000000000..8fb557cb88
--- /dev/null
+++ b/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts
@@ -0,0 +1,128 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {TLSSocket} from 'tls';
+
+import expect from 'expect';
+import type {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js';
+
+import {launch} from './mocha-utils.js';
+
+describe('ignoreHTTPSErrors', function () {
+ /* Note that this test creates its own browser rather than use
+ * the one provided by the test set-up as we need one
+ * with ignoreHTTPSErrors set to true
+ */
+ let state: Awaited<ReturnType<typeof launch>>;
+
+ before(async () => {
+ state = await launch(
+ {ignoreHTTPSErrors: true},
+ {
+ after: 'all',
+ }
+ );
+ });
+
+ after(async () => {
+ await state.close();
+ });
+
+ beforeEach(async () => {
+ state.context = await state.browser.createIncognitoBrowserContext();
+ state.page = await state.context.newPage();
+ });
+
+ afterEach(async () => {
+ await state.context.close();
+ });
+
+ describe('Response.securityDetails', function () {
+ it('should work', async () => {
+ const {httpsServer, page} = state;
+
+ const [serverRequest, response] = await Promise.all([
+ httpsServer.waitForRequest('/empty.html'),
+ page.goto(httpsServer.EMPTY_PAGE),
+ ]);
+ const securityDetails = response!.securityDetails()!;
+ expect(securityDetails.issuer()).toBe('puppeteer-tests');
+ const protocol = (serverRequest.socket as TLSSocket)
+ .getProtocol()!
+ .replace('v', ' ');
+ expect(securityDetails.protocol()).toBe(protocol);
+ expect(securityDetails.subjectName()).toBe('puppeteer-tests');
+ expect(securityDetails.validFrom()).toBe(1589357069);
+ expect(securityDetails.validTo()).toBe(1904717069);
+ expect(securityDetails.subjectAlternativeNames()).toEqual([
+ 'www.puppeteer-tests.test',
+ 'www.puppeteer-tests-1.test',
+ ]);
+ });
+ it('should be |null| for non-secure requests', async () => {
+ const {server, page} = state;
+
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.securityDetails()).toBe(null);
+ });
+ it('Network redirects should report SecurityDetails', async () => {
+ const {httpsServer, page} = state;
+
+ httpsServer.setRedirect('/plzredirect', '/empty.html');
+ const responses: HTTPResponse[] = [];
+ page.on('response', response => {
+ return responses.push(response);
+ });
+ const [serverRequest] = await Promise.all([
+ httpsServer.waitForRequest('/plzredirect'),
+ page.goto(httpsServer.PREFIX + '/plzredirect'),
+ ]);
+ expect(responses).toHaveLength(2);
+ expect(responses[0]!.status()).toBe(302);
+ const securityDetails = responses[0]!.securityDetails()!;
+ const protocol = (serverRequest.socket as TLSSocket)
+ .getProtocol()!
+ .replace('v', ' ');
+ expect(securityDetails.protocol()).toBe(protocol);
+ });
+ });
+
+ it('should work', async () => {
+ const {httpsServer, page} = state;
+
+ let error!: Error;
+ const response = await page.goto(httpsServer.EMPTY_PAGE).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeUndefined();
+ expect(response.ok()).toBe(true);
+ });
+ it('should work with request interception', async () => {
+ const {httpsServer, page} = state;
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ const response = (await page.goto(httpsServer.EMPTY_PAGE))!;
+ expect(response.status()).toBe(200);
+ });
+ it('should work with mixed content', async () => {
+ const {server, httpsServer, page} = state;
+
+ httpsServer.setRoute('/mixedcontent.html', (_req, res) => {
+ res.end(`<iframe src=${server.EMPTY_PAGE}></iframe>`);
+ });
+ await page.goto(httpsServer.PREFIX + '/mixedcontent.html', {
+ waitUntil: 'load',
+ });
+ expect(page.frames()).toHaveLength(2);
+ // Make sure blocked iframe has functional execution context
+ // @see https://github.com/puppeteer/puppeteer/issues/2709
+ expect(await page.frames()[0]!.evaluate('1 + 2')).toBe(3);
+ expect(await page.frames()[1]!.evaluate('2 + 3')).toBe(5);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/injected.spec.ts b/remote/test/puppeteer/test/src/injected.spec.ts
new file mode 100644
index 0000000000..5f3696d3f6
--- /dev/null
+++ b/remote/test/puppeteer/test/src/injected.spec.ts
@@ -0,0 +1,49 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {LazyArg} from 'puppeteer-core/internal/common/LazyArg.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('PuppeteerUtil tests', function () {
+ setupTestBrowserHooks();
+
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const world = page.mainFrame().isolatedRealm();
+ const value = await world.evaluate(
+ PuppeteerUtil => {
+ return typeof PuppeteerUtil === 'object';
+ },
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ })
+ );
+ expect(value).toBeTruthy();
+ });
+
+ describe('createFunction tests', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const world = page.mainFrame().isolatedRealm();
+ const value = await world.evaluate(
+ ({createFunction}, fnString) => {
+ return createFunction(fnString)(4);
+ },
+ LazyArg.create(context => {
+ return context.puppeteerUtil;
+ }),
+ (() => {
+ return 4;
+ }).toString()
+ );
+ expect(value).toBe(4);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/input.spec.ts b/remote/test/puppeteer/test/src/input.spec.ts
new file mode 100644
index 0000000000..7e4cae6709
--- /dev/null
+++ b/remote/test/puppeteer/test/src/input.spec.ts
@@ -0,0 +1,394 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import path from 'path';
+
+import expect from 'expect';
+import {TimeoutError} from 'puppeteer';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+
+const FILE_TO_UPLOAD = path.join(__dirname, '/../assets/file-to-upload.txt');
+
+describe('input tests', function () {
+ setupTestBrowserHooks();
+
+ describe('input', function () {
+ it('should upload the file', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/fileupload.html');
+ const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD);
+ using input = (await page.$('input'))!;
+ await page.evaluate((e: HTMLElement) => {
+ (globalThis as any)._inputEvents = [];
+ e.addEventListener('change', ev => {
+ return (globalThis as any)._inputEvents.push(ev.type);
+ });
+ e.addEventListener('input', ev => {
+ return (globalThis as any)._inputEvents.push(ev.type);
+ });
+ }, input);
+ await input.uploadFile(filePath);
+ expect(
+ await page.evaluate((e: HTMLInputElement) => {
+ return e.files![0]!.name;
+ }, input)
+ ).toBe('file-to-upload.txt');
+ expect(
+ await page.evaluate((e: HTMLInputElement) => {
+ return e.files![0]!.type;
+ }, input)
+ ).toBe('text/plain');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any)._inputEvents;
+ })
+ ).toEqual(['input', 'change']);
+ expect(
+ await page.evaluate((e: HTMLInputElement) => {
+ const reader = new FileReader();
+ const promise = new Promise(fulfill => {
+ return (reader.onload = fulfill);
+ });
+ reader.readAsText(e.files![0]!);
+ return promise.then(() => {
+ return reader.result;
+ });
+ }, input)
+ ).toBe('contents of the file');
+ });
+ });
+
+ describe('Page.waitForFileChooser', function () {
+ it('should work when file input is attached to DOM', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ expect(chooser).toBeTruthy();
+ });
+ it('should work when file input is not attached to DOM', async () => {
+ const {page} = await getTestState();
+
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.evaluate(() => {
+ const el = document.createElement('input');
+ el.type = 'file';
+ el.click();
+ }),
+ ]);
+ expect(chooser).toBeTruthy();
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.waitForFileChooser({timeout: 1}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should respect default timeout when there is no custom timeout', async () => {
+ const {page} = await getTestState();
+
+ page.setDefaultTimeout(1);
+ let error!: Error;
+ await page.waitForFileChooser().catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should prioritize exact timeout over default timeout', async () => {
+ const {page} = await getTestState();
+
+ page.setDefaultTimeout(0);
+ let error!: Error;
+ await page.waitForFileChooser({timeout: 1}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should work with no timeout', async () => {
+ const {page} = await getTestState();
+
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser({timeout: 0}),
+ page.evaluate(() => {
+ return setTimeout(() => {
+ const el = document.createElement('input');
+ el.type = 'file';
+ el.click();
+ }, 50);
+ }),
+ ]);
+ expect(chooser).toBeTruthy();
+ });
+ it('should return the same file chooser when there are many watchdogs simultaneously', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [fileChooser1, fileChooser2] = await Promise.all([
+ page.waitForFileChooser(),
+ page.waitForFileChooser(),
+ page.$eval('input', input => {
+ return (input as HTMLInputElement).click();
+ }),
+ ]);
+ expect(fileChooser1 === fileChooser2).toBe(true);
+ });
+ });
+
+ describe('FileChooser.accept', function () {
+ it('should accept single file', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ `<input type=file oninput='javascript:console.timeStamp()'>`
+ );
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ await Promise.all([
+ chooser.accept([FILE_TO_UPLOAD]),
+ waitEvent(page, 'metrics'),
+ ]);
+ expect(
+ await page.$eval('input', input => {
+ return (input as HTMLInputElement).files!.length;
+ })
+ ).toBe(1);
+ expect(
+ await page.$eval('input', input => {
+ return (input as HTMLInputElement).files![0]!.name;
+ })
+ ).toBe('file-to-upload.txt');
+ });
+ it('should be able to read selected file', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ void page.waitForFileChooser().then(chooser => {
+ return chooser.accept([FILE_TO_UPLOAD]);
+ });
+ expect(
+ await page.$eval('input', async picker => {
+ const pick = picker as HTMLInputElement;
+ pick.click();
+ await new Promise(x => {
+ return (pick.oninput = x);
+ });
+ const reader = new FileReader();
+ const promise = new Promise(fulfill => {
+ return (reader.onload = fulfill);
+ });
+ reader.readAsText(pick.files![0]!);
+ return await promise.then(() => {
+ return reader.result;
+ });
+ })
+ ).toBe('contents of the file');
+ });
+ it('should be able to reset selected files with empty file list', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ void page.waitForFileChooser().then(chooser => {
+ return chooser.accept([FILE_TO_UPLOAD]);
+ });
+ expect(
+ await page.$eval('input', async picker => {
+ const pick = picker as HTMLInputElement;
+ pick.click();
+ await new Promise(x => {
+ return (pick.oninput = x);
+ });
+ return pick.files!.length;
+ })
+ ).toBe(1);
+ void page.waitForFileChooser().then(chooser => {
+ return chooser.accept([]);
+ });
+ expect(
+ await page.$eval('input', async picker => {
+ const pick = picker as HTMLInputElement;
+ pick.click();
+ await new Promise(x => {
+ return (pick.oninput = x);
+ });
+ return pick.files!.length;
+ })
+ ).toBe(0);
+ });
+ it('should not accept multiple files for single-file input', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ let error!: Error;
+ await chooser
+ .accept([
+ path.relative(
+ process.cwd(),
+ __dirname + '/../assets/file-to-upload.txt'
+ ),
+ path.relative(process.cwd(), __dirname + '/../assets/pptr.png'),
+ ])
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).not.toBe(null);
+ });
+ it('should succeed even for non-existent files', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ let error!: Error;
+ await chooser.accept(['file-does-not-exist.txt']).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeUndefined();
+ });
+ it('should error on read of non-existent files', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ void page.waitForFileChooser().then(chooser => {
+ return chooser.accept(['file-does-not-exist.txt']);
+ });
+ expect(
+ await page.$eval('input', async picker => {
+ const pick = picker as HTMLInputElement;
+ pick.click();
+ await new Promise(x => {
+ return (pick.oninput = x);
+ });
+ const reader = new FileReader();
+ const promise = new Promise(fulfill => {
+ return (reader.onerror = fulfill);
+ });
+ reader.readAsText(pick.files![0]!);
+ return await promise.then(() => {
+ return false;
+ });
+ })
+ ).toBeFalsy();
+ });
+ it('should fail when accepting file chooser twice', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [fileChooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.$eval('input', input => {
+ return (input as HTMLInputElement).click();
+ }),
+ ]);
+ await fileChooser.accept([]);
+ let error!: Error;
+ await fileChooser.accept([]).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toBe(
+ 'Cannot accept FileChooser which is already handled!'
+ );
+ });
+ });
+
+ describe('FileChooser.cancel', function () {
+ it('should cancel dialog', async () => {
+ const {page} = await getTestState();
+
+ // Consider file chooser canceled if we can summon another one.
+ // There's no reliable way in WebPlatform to see that FileChooser was
+ // canceled.
+ await page.setContent(`<input type=file>`);
+ const [fileChooser1] = await Promise.all([
+ page.waitForFileChooser(),
+ page.$eval('input', input => {
+ return (input as HTMLInputElement).click();
+ }),
+ ]);
+ await fileChooser1.cancel();
+ // If this resolves, than we successfully canceled file chooser.
+ await Promise.all([
+ page.waitForFileChooser(),
+ page.$eval('input', input => {
+ return (input as HTMLInputElement).click();
+ }),
+ ]);
+ });
+ it('should fail when canceling file chooser twice', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [fileChooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.$eval('input', input => {
+ return (input as HTMLElement).click();
+ }),
+ ]);
+ await fileChooser.cancel();
+ let error!: Error;
+
+ try {
+ await fileChooser.cancel();
+ } catch (error_) {
+ error = error_ as Error;
+ }
+
+ expect(error.message).toBe(
+ 'Cannot cancel FileChooser which is already handled!'
+ );
+ });
+ });
+
+ describe('FileChooser.isMultiple', () => {
+ it('should work for single file pick', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ expect(chooser.isMultiple()).toBe(false);
+ });
+ it('should work for "multiple"', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input multiple type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ expect(chooser.isMultiple()).toBe(true);
+ });
+ it('should work for "webkitdirectory"', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<input multiple webkitdirectory type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ expect(chooser.isMultiple()).toBe(true);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/jshandle.spec.ts b/remote/test/puppeteer/test/src/jshandle.spec.ts
new file mode 100644
index 0000000000..28097811e4
--- /dev/null
+++ b/remote/test/puppeteer/test/src/jshandle.spec.ts
@@ -0,0 +1,373 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {JSHandle} from 'puppeteer-core/internal/api/JSHandle.js';
+import {
+ asyncDisposeSymbol,
+ disposeSymbol,
+} from 'puppeteer-core/internal/util/disposable.js';
+import sinon from 'sinon';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('JSHandle', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.evaluateHandle', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ using windowHandle = await page.evaluateHandle(() => {
+ return window;
+ });
+ expect(windowHandle).toBeTruthy();
+ });
+ it('should return the RemoteObject', async () => {
+ const {page} = await getTestState();
+
+ using windowHandle = await page.evaluateHandle(() => {
+ return window;
+ });
+ expect(windowHandle.remoteObject()).toBeTruthy();
+ });
+ it('should accept object handle as an argument', async () => {
+ const {page} = await getTestState();
+
+ using navigatorHandle = await page.evaluateHandle(() => {
+ return navigator;
+ });
+ const text = await page.evaluate(e => {
+ return e.userAgent;
+ }, navigatorHandle);
+ expect(text).toContain('Mozilla');
+ });
+ it('should accept object handle to primitive types', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return 5;
+ });
+ const isFive = await page.evaluate(e => {
+ return Object.is(e, 5);
+ }, aHandle);
+ expect(isFive).toBeTruthy();
+ });
+ it('should warn about recursive objects', async () => {
+ const {page} = await getTestState();
+
+ const test: {obj?: unknown} = {};
+ test.obj = test;
+ let error!: Error;
+ await page
+ .evaluateHandle(opts => {
+ return opts;
+ }, test)
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Recursive objects are not allowed.');
+ });
+ it('should accept object handle to unserializable value', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return Infinity;
+ });
+ expect(
+ await page.evaluate(e => {
+ return Object.is(e, Infinity);
+ }, aHandle)
+ ).toBe(true);
+ });
+ it('should use the same JS wrappers', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ (globalThis as any).FOO = 123;
+ return window;
+ });
+ expect(
+ await page.evaluate(e => {
+ return (e as any).FOO;
+ }, aHandle)
+ ).toBe(123);
+ });
+ });
+
+ describe('JSHandle.getProperty', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return {
+ one: 1,
+ two: 2,
+ three: 3,
+ };
+ });
+ using twoHandle = await aHandle.getProperty('two');
+ expect(await twoHandle.jsonValue()).toEqual(2);
+ });
+ });
+
+ describe('JSHandle.jsonValue', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return {foo: 'bar'};
+ });
+ const json = await aHandle.jsonValue();
+ expect(json).toEqual({foo: 'bar'});
+ });
+
+ it('works with jsonValues that are not objects', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return ['a', 'b'];
+ });
+ const json = await aHandle.jsonValue();
+ expect(json).toEqual(['a', 'b']);
+ });
+
+ it('works with jsonValues that are primitives', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return 'foo';
+ });
+ expect(await aHandle.jsonValue()).toEqual('foo');
+
+ using bHandle = await page.evaluateHandle(() => {
+ return undefined;
+ });
+ expect(await bHandle.jsonValue()).toEqual(undefined);
+ });
+
+ it('should work with dates', async () => {
+ const {page} = await getTestState();
+
+ using dateHandle = await page.evaluateHandle(() => {
+ return new Date('2017-09-26T00:00:00.000Z');
+ });
+ const date = await dateHandle.jsonValue();
+ expect(date).toBeInstanceOf(Date);
+ expect(date.toISOString()).toEqual('2017-09-26T00:00:00.000Z');
+ });
+ it('should not throw for circular objects', async () => {
+ const {page} = await getTestState();
+
+ using handle = await page.evaluateHandle(() => {
+ const t: {t?: unknown; g: number} = {g: 1};
+ t.t = t;
+ return t;
+ });
+ await handle.jsonValue();
+ });
+ });
+
+ describe('JSHandle.getProperties', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return {
+ foo: 'bar',
+ };
+ });
+ const properties = await aHandle.getProperties();
+ using foo = properties.get('foo')!;
+ expect(foo).toBeTruthy();
+ expect(await foo.jsonValue()).toBe('bar');
+ });
+ it('should return even non-own properties', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ class A {
+ a: string;
+ constructor() {
+ this.a = '1';
+ }
+ }
+ class B extends A {
+ b: string;
+ constructor() {
+ super();
+ this.b = '2';
+ }
+ }
+ return new B();
+ });
+ const properties = await aHandle.getProperties();
+ expect(await properties.get('a')!.jsonValue()).toBe('1');
+ expect(await properties.get('b')!.jsonValue()).toBe('2');
+ });
+ });
+
+ describe('JSHandle.asElement', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return document.body;
+ });
+ using element = aHandle.asElement();
+ expect(element).toBeTruthy();
+ });
+ it('should return null for non-elements', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return 2;
+ });
+ using element = aHandle.asElement();
+ expect(element).toBeFalsy();
+ });
+ it('should return ElementHandle for TextNodes', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>ee!</div>');
+ using aHandle = await page.evaluateHandle(() => {
+ return document.querySelector('div')!.firstChild;
+ });
+ using element = aHandle.asElement();
+ expect(element).toBeTruthy();
+ expect(
+ await page.evaluate(e => {
+ return e?.nodeType === Node.TEXT_NODE;
+ }, element)
+ );
+ });
+ });
+
+ describe('JSHandle.toString', function () {
+ it('should work for primitives', async () => {
+ const {page} = await getTestState();
+
+ using numberHandle = await page.evaluateHandle(() => {
+ return 2;
+ });
+ expect(numberHandle.toString()).toBe('JSHandle:2');
+ using stringHandle = await page.evaluateHandle(() => {
+ return 'a';
+ });
+ expect(stringHandle.toString()).toBe('JSHandle:a');
+ });
+ it('should work for complicated objects', async () => {
+ const {page} = await getTestState();
+
+ using aHandle = await page.evaluateHandle(() => {
+ return window;
+ });
+ expect(aHandle.toString()).atLeastOneToContain([
+ 'JSHandle@object',
+ 'JSHandle@window',
+ ]);
+ });
+ it('should work with different subtypes', async () => {
+ const {page} = await getTestState();
+
+ expect((await page.evaluateHandle('(function(){})')).toString()).toBe(
+ 'JSHandle@function'
+ );
+ expect((await page.evaluateHandle('12')).toString()).toBe('JSHandle:12');
+ expect((await page.evaluateHandle('true')).toString()).toBe(
+ 'JSHandle:true'
+ );
+ expect((await page.evaluateHandle('undefined')).toString()).toBe(
+ 'JSHandle:undefined'
+ );
+ expect((await page.evaluateHandle('"foo"')).toString()).toBe(
+ 'JSHandle:foo'
+ );
+ expect((await page.evaluateHandle('Symbol()')).toString()).toBe(
+ 'JSHandle@symbol'
+ );
+ expect((await page.evaluateHandle('new Map()')).toString()).toBe(
+ 'JSHandle@map'
+ );
+ expect((await page.evaluateHandle('new Set()')).toString()).toBe(
+ 'JSHandle@set'
+ );
+ expect((await page.evaluateHandle('[]')).toString()).toBe(
+ 'JSHandle@array'
+ );
+ expect((await page.evaluateHandle('null')).toString()).toBe(
+ 'JSHandle:null'
+ );
+ expect((await page.evaluateHandle('/foo/')).toString()).toBe(
+ 'JSHandle@regexp'
+ );
+ expect((await page.evaluateHandle('document.body')).toString()).toBe(
+ 'JSHandle@node'
+ );
+ expect((await page.evaluateHandle('new Date()')).toString()).toBe(
+ 'JSHandle@date'
+ );
+ expect((await page.evaluateHandle('new WeakMap()')).toString()).toBe(
+ 'JSHandle@weakmap'
+ );
+ expect((await page.evaluateHandle('new WeakSet()')).toString()).toBe(
+ 'JSHandle@weakset'
+ );
+ expect((await page.evaluateHandle('new Error()')).toString()).toBe(
+ 'JSHandle@error'
+ );
+ expect((await page.evaluateHandle('new Int32Array()')).toString()).toBe(
+ 'JSHandle@typedarray'
+ );
+ expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe(
+ 'JSHandle@proxy'
+ );
+ });
+ });
+
+ describe('JSHandle[Symbol.dispose]', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ using handle = await page.evaluateHandle('new Set()');
+ const spy = sinon.spy(handle, disposeSymbol);
+ {
+ using _ = handle;
+ }
+ expect(handle).toBeInstanceOf(JSHandle);
+ expect(spy.calledOnce).toBeTruthy();
+ expect(handle.disposed).toBeTruthy();
+ });
+ });
+
+ describe('JSHandle[Symbol.asyncDispose]', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ using handle = await page.evaluateHandle('new Set()');
+ const spy = sinon.spy(handle, asyncDisposeSymbol);
+ {
+ await using _ = handle;
+ }
+ expect(handle).toBeInstanceOf(JSHandle);
+ expect(spy.calledOnce).toBeTruthy();
+ expect(handle.disposed).toBeTruthy();
+ });
+ });
+
+ describe('JSHandle.move', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ using handle = await page.evaluateHandle('new Set()');
+ const spy = sinon.spy(handle, disposeSymbol);
+ {
+ using _ = handle;
+ handle.move();
+ }
+ expect(handle).toBeInstanceOf(JSHandle);
+ expect(spy.calledOnce).toBeTruthy();
+ expect(handle.disposed).toBeFalsy();
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/keyboard.spec.ts b/remote/test/puppeteer/test/src/keyboard.spec.ts
new file mode 100644
index 0000000000..9157465242
--- /dev/null
+++ b/remote/test/puppeteer/test/src/keyboard.spec.ts
@@ -0,0 +1,550 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import os from 'os';
+
+import expect from 'expect';
+import type {KeyInput} from 'puppeteer-core/internal/common/USKeyboardLayout.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame} from './utils.js';
+
+describe('Keyboard', function () {
+ setupTestBrowserHooks();
+
+ it('should type into a textarea', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ const textarea = document.createElement('textarea');
+ document.body.appendChild(textarea);
+ textarea.focus();
+ });
+ const text = 'Hello world. I am the text that was typed!';
+ await page.keyboard.type(text);
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe(text);
+ });
+ it('should move with the arrow keys', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.type('textarea', 'Hello World!');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('Hello World!');
+ for (const _ of 'World!') {
+ await page.keyboard.press('ArrowLeft');
+ }
+ await page.keyboard.type('inserted ');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('Hello inserted World!');
+ await page.keyboard.down('Shift');
+ for (const _ of 'inserted ') {
+ await page.keyboard.press('ArrowLeft');
+ }
+ await page.keyboard.up('Shift');
+ await page.keyboard.press('Backspace');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('Hello World!');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/1313
+ it('should trigger commands of keyboard shortcuts', async () => {
+ const {page, server} = await getTestState();
+ const cmdKey = os.platform() === 'darwin' ? 'Meta' : 'Control';
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.type('textarea', 'hello');
+
+ await page.keyboard.down(cmdKey);
+ await page.keyboard.press('a', {commands: ['SelectAll']});
+ await page.keyboard.up(cmdKey);
+
+ await page.keyboard.down(cmdKey);
+ await page.keyboard.down('c', {commands: ['Copy']});
+ await page.keyboard.up('c');
+ await page.keyboard.up(cmdKey);
+
+ await page.keyboard.down(cmdKey);
+ await page.keyboard.press('v', {commands: ['Paste']});
+ await page.keyboard.press('v', {commands: ['Paste']});
+ await page.keyboard.up(cmdKey);
+
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('hellohello');
+ });
+ it('should send a character with ElementHandle.press', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ using textarea = (await page.$('textarea'))!;
+ await textarea.press('a');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('a');
+
+ await page.evaluate(() => {
+ return window.addEventListener(
+ 'keydown',
+ e => {
+ return e.preventDefault();
+ },
+ true
+ );
+ });
+
+ await textarea.press('b');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('a');
+ });
+ it('ElementHandle.press should not support |text| option', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ using textarea = (await page.$('textarea'))!;
+ await textarea.press('a', {text: 'ё'});
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.value;
+ })
+ ).toBe('a');
+ });
+ it('should send a character with sendCharacter', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.focus('textarea');
+
+ await page.evaluate(() => {
+ (globalThis as any).inputCount = 0;
+ (globalThis as any).keyDownCount = 0;
+ window.addEventListener(
+ 'input',
+ () => {
+ (globalThis as any).inputCount += 1;
+ },
+ true
+ );
+ window.addEventListener(
+ 'keydown',
+ () => {
+ (globalThis as any).keyDownCount += 1;
+ },
+ true
+ );
+ });
+
+ await page.keyboard.sendCharacter('嗨');
+ expect(
+ await page.$eval('textarea', textarea => {
+ return {
+ value: textarea.value,
+ inputs: (globalThis as any).inputCount,
+ keyDowns: (globalThis as any).keyDownCount,
+ };
+ })
+ ).toMatchObject({value: '嗨', inputs: 1, keyDowns: 0});
+
+ await page.keyboard.sendCharacter('a');
+ expect(
+ await page.$eval('textarea', textarea => {
+ return {
+ value: textarea.value,
+ inputs: (globalThis as any).inputCount,
+ keyDowns: (globalThis as any).keyDownCount,
+ };
+ })
+ ).toMatchObject({value: '嗨a', inputs: 2, keyDowns: 0});
+ });
+ it('should send a character with sendCharacter in iframe', async () => {
+ this.timeout(2000);
+
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <iframe srcdoc="<iframe name='test' srcdoc='<textarea></textarea>'></iframe>"</iframe>
+ `);
+ const frame = await page.waitForFrame(frame => {
+ return frame.name() === 'test';
+ });
+ await frame.focus('textarea');
+
+ await frame.evaluate(() => {
+ (globalThis as any).inputCount = 0;
+ (globalThis as any).keyDownCount = 0;
+ window.addEventListener(
+ 'input',
+ () => {
+ (globalThis as any).inputCount += 1;
+ },
+ true
+ );
+ window.addEventListener(
+ 'keydown',
+ () => {
+ (globalThis as any).keyDownCount += 1;
+ },
+ true
+ );
+ });
+
+ await page.keyboard.sendCharacter('嗨');
+ expect(
+ await frame.$eval('textarea', textarea => {
+ return {
+ value: textarea.value,
+ inputs: (globalThis as any).inputCount,
+ keyDowns: (globalThis as any).keyDownCount,
+ };
+ })
+ ).toMatchObject({value: '嗨', inputs: 1, keyDowns: 0});
+
+ await page.keyboard.sendCharacter('a');
+ expect(
+ await frame.$eval('textarea', textarea => {
+ return {
+ value: textarea.value,
+ inputs: (globalThis as any).inputCount,
+ keyDowns: (globalThis as any).keyDownCount,
+ };
+ })
+ ).toMatchObject({value: '嗨a', inputs: 2, keyDowns: 0});
+ });
+ it('should report shiftKey', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/keyboard.html');
+ const keyboard = page.keyboard;
+ const codeForKey = new Set<KeyInput>(['Shift', 'Alt', 'Control']);
+ for (const modifierKey of codeForKey) {
+ await keyboard.down(modifierKey);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(`Keydown: ${modifierKey} ${modifierKey}Left [${modifierKey}]`);
+ await keyboard.down('!');
+ if (modifierKey === 'Shift') {
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(
+ `Keydown: ! Digit1 [${modifierKey}]\n` + `input: ! insertText false`
+ );
+ } else {
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(`Keydown: ! Digit1 [${modifierKey}]`);
+ }
+
+ await keyboard.up('!');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(`Keyup: ! Digit1 [${modifierKey}]`);
+ await keyboard.up(modifierKey);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(`Keyup: ${modifierKey} ${modifierKey}Left []`);
+ }
+ });
+ it('should report multiple modifiers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/keyboard.html');
+ const keyboard = page.keyboard;
+ await keyboard.down('Control');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe('Keydown: Control ControlLeft [Control]');
+ await keyboard.down('Alt');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe('Keydown: Alt AltLeft [Alt Control]');
+ await keyboard.down(';');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe('Keydown: ; Semicolon [Alt Control]');
+ await keyboard.up(';');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe('Keyup: ; Semicolon [Alt Control]');
+ await keyboard.up('Control');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe('Keyup: Control ControlLeft [Alt]');
+ await keyboard.up('Alt');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe('Keyup: Alt AltLeft []');
+ });
+ it('should send proper codes while typing', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/keyboard.html');
+ await page.keyboard.type('!');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(
+ [
+ 'Keydown: ! Digit1 []',
+ 'input: ! insertText false',
+ 'Keyup: ! Digit1 []',
+ ].join('\n')
+ );
+ await page.keyboard.type('^');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(
+ [
+ 'Keydown: ^ Digit6 []',
+ 'input: ^ insertText false',
+ 'Keyup: ^ Digit6 []',
+ ].join('\n')
+ );
+ });
+ it('should send proper codes while typing with shift', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/keyboard.html');
+ const keyboard = page.keyboard;
+ await keyboard.down('Shift');
+ await page.keyboard.type('~');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).getResult();
+ })
+ ).toBe(
+ [
+ 'Keydown: Shift ShiftLeft [Shift]',
+ 'Keydown: ~ Backquote [Shift]',
+ 'input: ~ insertText false',
+ 'Keyup: ~ Backquote [Shift]',
+ ].join('\n')
+ );
+ await keyboard.up('Shift');
+ });
+ it('should not type canceled events', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.focus('textarea');
+ await page.evaluate(() => {
+ window.addEventListener(
+ 'keydown',
+ event => {
+ event.stopPropagation();
+ event.stopImmediatePropagation();
+ if (event.key === 'l') {
+ event.preventDefault();
+ }
+ if (event.key === 'o') {
+ event.preventDefault();
+ }
+ },
+ false
+ );
+ });
+ await page.keyboard.type('Hello World!');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).textarea.value;
+ })
+ ).toBe('He Wrd!');
+ });
+ it('should specify repeat property', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.focus('textarea');
+ await page.evaluate(() => {
+ return document.querySelector('textarea')!.addEventListener(
+ 'keydown',
+ e => {
+ return ((globalThis as any).lastEvent = e);
+ },
+ true
+ );
+ });
+ await page.keyboard.down('a');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).lastEvent.repeat;
+ })
+ ).toBe(false);
+ await page.keyboard.press('a');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).lastEvent.repeat;
+ })
+ ).toBe(true);
+
+ await page.keyboard.down('b');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).lastEvent.repeat;
+ })
+ ).toBe(false);
+ await page.keyboard.down('b');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).lastEvent.repeat;
+ })
+ ).toBe(true);
+
+ await page.keyboard.up('a');
+ await page.keyboard.down('a');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).lastEvent.repeat;
+ })
+ ).toBe(false);
+ });
+ it('should type all kinds of characters', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.focus('textarea');
+ const text = 'This text goes onto two lines.\nThis character is 嗨.';
+ await page.keyboard.type(text);
+ expect(await page.evaluate('result')).toBe(text);
+ });
+ it('should specify location', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.evaluate(() => {
+ window.addEventListener(
+ 'keydown',
+ event => {
+ return ((globalThis as any).keyLocation = event.location);
+ },
+ true
+ );
+ });
+ using textarea = (await page.$('textarea'))!;
+
+ await textarea.press('Digit5');
+ expect(await page.evaluate('keyLocation')).toBe(0);
+
+ await textarea.press('ControlLeft');
+ expect(await page.evaluate('keyLocation')).toBe(1);
+
+ await textarea.press('ControlRight');
+ expect(await page.evaluate('keyLocation')).toBe(2);
+
+ await textarea.press('NumpadSubtract');
+ expect(await page.evaluate('keyLocation')).toBe(3);
+ });
+ it('should throw on unknown keys', async () => {
+ const {page} = await getTestState();
+
+ const error = await page.keyboard
+ // @ts-expect-error bad input
+ .press('NotARealKey')
+ .catch(error_ => {
+ return error_;
+ });
+ expect(error.message).toBe('Unknown key: "NotARealKey"');
+ });
+ it('should type emoji', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.type('textarea', '👹 Tokyo street Japan 🇯🇵');
+ expect(
+ await page.$eval('textarea', textarea => {
+ return textarea.value;
+ })
+ ).toBe('👹 Tokyo street Japan 🇯🇵');
+ });
+ it('should type emoji into an iframe', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(
+ page,
+ 'emoji-test',
+ server.PREFIX + '/input/textarea.html'
+ );
+ const frame = page.frames()[1]!;
+ using textarea = (await frame.$('textarea'))!;
+ await textarea.type('👹 Tokyo street Japan 🇯🇵');
+ expect(
+ await frame.$eval('textarea', textarea => {
+ return textarea.value;
+ })
+ ).toBe('👹 Tokyo street Japan 🇯🇵');
+ });
+ it('should press the meta key', async () => {
+ // This test only makes sense on macOS.
+ if (os.platform() !== 'darwin') {
+ return;
+ }
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ (globalThis as any).result = null;
+ document.addEventListener('keydown', event => {
+ (globalThis as any).result = [event.key, event.code, event.metaKey];
+ });
+ });
+ await page.keyboard.press('Meta');
+ // Have to do this because we lose a lot of type info when evaluating a
+ // string not a function. This is why functions are recommended rather than
+ // using strings (although we'll leave this test so we have coverage of both
+ // approaches.)
+ const [key, code, metaKey] = (await page.evaluate('result')) as [
+ string,
+ string,
+ boolean,
+ ];
+ expect(key).toBe('Meta');
+ expect(code).toBe('MetaLeft');
+ expect(metaKey).toBe(true);
+ });
+});
diff --git a/remote/test/puppeteer/test/src/launcher.spec.ts b/remote/test/puppeteer/test/src/launcher.spec.ts
new file mode 100644
index 0000000000..f31b22b1e5
--- /dev/null
+++ b/remote/test/puppeteer/test/src/launcher.spec.ts
@@ -0,0 +1,1025 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'assert';
+import fs from 'fs';
+import {mkdtemp, readFile, writeFile} from 'fs/promises';
+import os from 'os';
+import path from 'path';
+import type {TLSSocket} from 'tls';
+
+import expect from 'expect';
+import {TimeoutError} from 'puppeteer';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+import {rmSync} from 'puppeteer-core/internal/node/util/fs.js';
+import sinon from 'sinon';
+
+import {getTestState, isHeadless, launch} from './mocha-utils.js';
+import {dumpFrames, waitEvent} from './utils.js';
+
+const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
+const FIREFOX_TIMEOUT = 30_000;
+
+describe('Launcher specs', function () {
+ this.timeout(FIREFOX_TIMEOUT);
+
+ describe('Puppeteer', function () {
+ describe('Browser.disconnect', function () {
+ it('should reject navigation when browser closes', async () => {
+ const {browser, close, puppeteer, server} = await launch({});
+ server.setRoute('/one-style.css', () => {});
+ try {
+ const remote = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ const page = await remote.newPage();
+ const navigationPromise = page
+ .goto(server.PREFIX + '/one-style.html', {timeout: 60000})
+ .catch(error_ => {
+ return error_;
+ });
+ await server.waitForRequest('/one-style.css');
+ await remote.disconnect();
+ const error = await navigationPromise;
+ expect(
+ [
+ 'Navigating frame was detached',
+ 'Protocol error (Page.navigate): Target closed.',
+ ].includes(error.message)
+ ).toBeTruthy();
+ } finally {
+ await close();
+ }
+ });
+ it('should reject waitForSelector when browser closes', async () => {
+ const {browser, close, server, puppeteer} = await launch({});
+ server.setRoute('/empty.html', () => {});
+ try {
+ const remote = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ const page = await remote.newPage();
+ const watchdog = page
+ .waitForSelector('div', {timeout: 60000})
+ .catch(error_ => {
+ return error_;
+ });
+ await remote.disconnect();
+ const error = await watchdog;
+ expect(error.message).toContain('Session closed.');
+ } finally {
+ await close();
+ }
+ });
+ });
+ describe('Browser.close', function () {
+ it('should terminate network waiters', async () => {
+ const {browser, close, server, puppeteer} = await launch({});
+ try {
+ const remote = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ const newPage = await remote.newPage();
+ const results = await Promise.all([
+ newPage.waitForRequest(server.EMPTY_PAGE).catch(error => {
+ return error;
+ }),
+ newPage.waitForResponse(server.EMPTY_PAGE).catch(error => {
+ return error;
+ }),
+ browser.close(),
+ ]);
+ for (let i = 0; i < 2; i++) {
+ const message = results[i].message;
+ expect(message).atLeastOneToContain([
+ 'Target closed',
+ 'Page closed!',
+ ]);
+ expect(message).not.toContain('Timeout');
+ }
+ } finally {
+ await close();
+ }
+ });
+ });
+ describe('Puppeteer.launch', function () {
+ it('can launch and close the browser', async () => {
+ const {close} = await launch({});
+ await close();
+ });
+ it('should have default url when launching browser', async function () {
+ const {browser, close} = await launch({}, {createContext: false});
+ try {
+ const pages = (await browser.pages()).map(
+ (page: {url: () => any}) => {
+ return page.url();
+ }
+ );
+ expect(pages).toEqual(['about:blank']);
+ } finally {
+ await close();
+ }
+ });
+ it('should close browser with beforeunload page', async () => {
+ const {browser, server, close} = await launch(
+ {},
+ {createContext: false}
+ );
+ try {
+ const page = await browser.newPage();
+
+ await page.goto(server.PREFIX + '/beforeunload.html');
+ // We have to interact with a page so that 'beforeunload' handlers
+ // fire.
+ await page.click('body');
+ } finally {
+ await close();
+ }
+ });
+ it('should reject all promises when browser is closed', async () => {
+ const {page, close} = await launch({});
+ let error!: Error;
+ const neverResolves = page
+ .evaluate(() => {
+ return new Promise(() => {});
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ await close();
+ await neverResolves;
+ expect(error.message).toContain('Protocol error');
+ });
+ it('should reject if executable path is invalid', async () => {
+ let waitError!: Error;
+ await launch({
+ executablePath: 'random-invalid-path',
+ }).catch(error => {
+ return (waitError = error);
+ });
+ expect(waitError.message).toContain('Failed to launch');
+ });
+ it('userDataDir option', async () => {
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+ const {context, close} = await launch({userDataDir});
+ // Open a page to make sure its functional.
+ try {
+ await context.newPage();
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ } finally {
+ await close();
+ }
+
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ });
+ it('tmp profile should be cleaned up', async () => {
+ const {puppeteer} = await getTestState({skipLaunch: true});
+
+ // Set a custom test tmp dir so that we can validate that
+ // the profile dir is created and then cleaned up.
+ const testTmpDir = await fs.promises.mkdtemp(
+ path.join(os.tmpdir(), 'puppeteer_test_chrome_profile-')
+ );
+ const oldTmpDir = puppeteer.configuration.temporaryDirectory;
+ puppeteer.configuration.temporaryDirectory = testTmpDir;
+
+ // Path should be empty before starting the browser.
+ expect(fs.readdirSync(testTmpDir)).toHaveLength(0);
+ const {context, close} = await launch({});
+ try {
+ // One profile folder should have been created at this moment.
+ const profiles = fs.readdirSync(testTmpDir);
+ expect(profiles).toHaveLength(1);
+ expect(profiles[0]?.startsWith('puppeteer_dev_chrome_profile-')).toBe(
+ true
+ );
+
+ // Open a page to make sure its functional.
+ await context.newPage();
+ } finally {
+ await close();
+ }
+
+ // Profile should be deleted after closing the browser
+ expect(fs.readdirSync(testTmpDir)).toHaveLength(0);
+
+ // Restore env var
+ puppeteer.configuration.temporaryDirectory = oldTmpDir;
+ });
+ it('userDataDir option restores preferences', async () => {
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+
+ const prefsJSPath = path.join(userDataDir, 'prefs.js');
+ const prefsJSContent = 'user_pref("browser.warnOnQuit", true)';
+ await writeFile(prefsJSPath, prefsJSContent);
+
+ const {context, close} = await launch({userDataDir});
+ try {
+ // Open a page to make sure its functional.
+ await context.newPage();
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ await close();
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+
+ expect(await readFile(prefsJSPath, 'utf8')).toBe(prefsJSContent);
+ } finally {
+ await close();
+ }
+
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ });
+ it('userDataDir argument', async () => {
+ const {isChrome, defaultBrowserOptions: options} = await getTestState({
+ skipLaunch: true,
+ });
+
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+ if (isChrome) {
+ options.args = [
+ ...(options.args || []),
+ `--user-data-dir=${userDataDir}`,
+ ];
+ } else {
+ options.args = [...(options.args || []), '-profile', userDataDir];
+ }
+ const {close} = await launch(options);
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ await close();
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ });
+ it('userDataDir argument with non-existent dir', async () => {
+ const {isChrome, defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+ rmSync(userDataDir);
+ const options = Object.assign({}, defaultBrowserOptions);
+ if (isChrome) {
+ options.args = [
+ ...(defaultBrowserOptions.args || []),
+ `--user-data-dir=${userDataDir}`,
+ ];
+ } else {
+ options.args = [
+ ...(defaultBrowserOptions.args || []),
+ '-profile',
+ userDataDir,
+ ];
+ }
+ const {close} = await launch(options);
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ await close();
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ });
+ it('userDataDir option should restore state', async () => {
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+ const {server, browser, close} = await launch({userDataDir});
+ try {
+ const page = await browser.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ return (localStorage['hey'] = 'hello');
+ });
+ } finally {
+ await close();
+ }
+
+ const {browser: browser2, close: close2} = await launch({userDataDir});
+
+ try {
+ const page2 = await browser2.newPage();
+ await page2.goto(server.EMPTY_PAGE);
+ expect(
+ await page2.evaluate(() => {
+ return localStorage['hey'];
+ })
+ ).toBe('hello');
+ } finally {
+ await close2();
+ }
+
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ });
+ it('userDataDir option should restore cookies', async () => {
+ const userDataDir = await mkdtemp(TMP_FOLDER);
+ const {server, browser, close} = await launch({userDataDir});
+ try {
+ const page = await browser.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ return (document.cookie =
+ 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
+ });
+ } finally {
+ await close();
+ }
+
+ const {browser: browser2, close: close2} = await launch({userDataDir});
+ try {
+ const page2 = await browser2.newPage();
+ await page2.goto(server.EMPTY_PAGE);
+ expect(
+ await page2.evaluate(() => {
+ return document.cookie;
+ })
+ ).toBe('doSomethingOnlyOnce=true');
+ } finally {
+ await close2();
+ }
+
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ try {
+ rmSync(userDataDir);
+ } catch {}
+ });
+ it('should return the default arguments', async () => {
+ const {isChrome, isFirefox, puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+
+ if (isChrome) {
+ expect(puppeteer.defaultArgs()).toContain('--no-first-run');
+ expect(puppeteer.defaultArgs()).toContain('--headless');
+ expect(puppeteer.defaultArgs({headless: false})).not.toContain(
+ '--headless'
+ );
+ expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain(
+ `--user-data-dir=${path.resolve('foo')}`
+ );
+ } else if (isFirefox) {
+ expect(puppeteer.defaultArgs()).toContain('--headless');
+ expect(puppeteer.defaultArgs()).toContain('--no-remote');
+ if (os.platform() === 'darwin') {
+ expect(puppeteer.defaultArgs()).toContain('--foreground');
+ } else {
+ expect(puppeteer.defaultArgs()).not.toContain('--foreground');
+ }
+ expect(puppeteer.defaultArgs({headless: false})).not.toContain(
+ '--headless'
+ );
+ expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain(
+ '--profile'
+ );
+ expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain('foo');
+ } else {
+ expect(puppeteer.defaultArgs()).toContain('-headless');
+ expect(puppeteer.defaultArgs({headless: false})).not.toContain(
+ '-headless'
+ );
+ expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain(
+ '-profile'
+ );
+ expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain(
+ path.resolve('foo')
+ );
+ }
+ });
+ it('should report the correct product', async () => {
+ const {isChrome, isFirefox, puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+ if (isChrome) {
+ expect(puppeteer.product).toBe('chrome');
+ } else if (isFirefox) {
+ expect(puppeteer.product).toBe('firefox');
+ }
+ });
+ (!isHeadless ? it : it.skip)(
+ 'should work with no default arguments',
+ async () => {
+ const {context, close} = await launch({
+ ignoreDefaultArgs: true,
+ });
+ try {
+ const page = await context.newPage();
+ expect(await page.evaluate('11 * 11')).toBe(121);
+ await page.close();
+ } finally {
+ await close();
+ }
+ }
+ );
+ it('should filter out ignored default arguments in Chrome', async () => {
+ const {defaultBrowserOptions, puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+ // Make sure we launch with `--enable-automation` by default.
+ const defaultArgs = puppeteer.defaultArgs();
+ const {browser, close} = await launch(
+ Object.assign({}, defaultBrowserOptions, {
+ // Ignore first and third default argument.
+ ignoreDefaultArgs: [defaultArgs[0]!, defaultArgs[2]],
+ })
+ );
+ try {
+ const spawnargs = browser.process()!.spawnargs;
+ if (!spawnargs) {
+ throw new Error('spawnargs not present');
+ }
+ expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1);
+ expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1);
+ expect(spawnargs.indexOf(defaultArgs[2]!)).toBe(-1);
+ } finally {
+ await close();
+ }
+ });
+ it('should filter out ignored default argument in Firefox', async () => {
+ const {defaultBrowserOptions, puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+
+ const defaultArgs = puppeteer.defaultArgs();
+ const {browser, close} = await launch(
+ Object.assign({}, defaultBrowserOptions, {
+ // Only the first argument is fixed, others are optional.
+ ignoreDefaultArgs: [defaultArgs[0]!],
+ })
+ );
+ try {
+ const spawnargs = browser.process()!.spawnargs;
+ if (!spawnargs) {
+ throw new Error('spawnargs not present');
+ }
+ expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1);
+ expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1);
+ } finally {
+ await close();
+ }
+ });
+ it('should have default URL when launching browser', async function () {
+ const {browser, close} = await launch(
+ {},
+ {
+ createContext: false,
+ }
+ );
+ try {
+ const pages = (await browser.pages()).map(page => {
+ return page.url();
+ });
+ expect(pages).toEqual(['about:blank']);
+ } finally {
+ await close();
+ }
+ });
+ it('should have custom URL when launching browser', async () => {
+ const {server, defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+
+ const options = Object.assign({}, defaultBrowserOptions);
+ options.args = [server.EMPTY_PAGE].concat(options.args || []);
+ const {browser, close} = await launch(options, {
+ createContext: false,
+ });
+ try {
+ const pages = await browser.pages();
+ expect(pages).toHaveLength(1);
+ const page = pages[0]!;
+ if (page.url() !== server.EMPTY_PAGE) {
+ await page.waitForNavigation();
+ }
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ } finally {
+ await close();
+ }
+ });
+ it('should pass the timeout parameter to browser.waitForTarget', async () => {
+ const options = {
+ timeout: 1,
+ };
+ let error!: Error;
+ await launch(options).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should work with timeout = 0', async () => {
+ const {close} = await launch({
+ timeout: 0,
+ });
+ await close();
+ });
+ it('should set the default viewport', async () => {
+ const {context, close} = await launch({
+ defaultViewport: {
+ width: 456,
+ height: 789,
+ },
+ });
+
+ try {
+ const page = await context.newPage();
+ expect(await page.evaluate('window.innerWidth')).toBe(456);
+ expect(await page.evaluate('window.innerHeight')).toBe(789);
+ } finally {
+ await close();
+ }
+ });
+ it('should disable the default viewport', async () => {
+ const {context, close} = await launch({
+ defaultViewport: null,
+ });
+ try {
+ const page = await context.newPage();
+ expect(page.viewport()).toBe(null);
+ } finally {
+ await close();
+ }
+ });
+ it('should take fullPage screenshots when defaultViewport is null', async () => {
+ const {server, context, close} = await launch({
+ defaultViewport: null,
+ });
+ try {
+ const page = await context.newPage();
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ fullPage: true,
+ });
+ expect(screenshot).toBeInstanceOf(Buffer);
+ } finally {
+ await close();
+ }
+ });
+ it('should set the debugging port', async () => {
+ const {browser, close} = await launch({
+ defaultViewport: null,
+ debuggingPort: 9999,
+ });
+ try {
+ const url = new URL(browser.wsEndpoint());
+ expect(url.port).toBe('9999');
+ } finally {
+ await close();
+ }
+ });
+ it('should not allow setting debuggingPort and pipe', async () => {
+ const options = {
+ defaultViewport: null,
+ debuggingPort: 9999,
+ pipe: true,
+ };
+ let error!: Error;
+ await launch(options).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('either pipe or debugging port');
+ });
+ (!isHeadless ? it : it.skip)(
+ 'should launch Chrome properly with --no-startup-window and waitForInitialPage=false',
+ async () => {
+ const {defaultBrowserOptions} = await getTestState({
+ skipLaunch: true,
+ });
+ const options = {
+ waitForInitialPage: false,
+ // This is needed to prevent Puppeteer from adding an initial blank page.
+ // See also https://github.com/puppeteer/puppeteer/blob/ad6b736039436fcc5c0a262e5b575aa041427be3/src/node/Launcher.ts#L200
+ ignoreDefaultArgs: true,
+ ...defaultBrowserOptions,
+ args: ['--no-startup-window'],
+ };
+ const {browser, close} = await launch(options, {
+ createContext: false,
+ });
+ try {
+ const pages = await browser.pages();
+ expect(pages).toHaveLength(0);
+ } finally {
+ await close();
+ }
+ }
+ );
+ });
+
+ describe('Puppeteer.launch', function () {
+ it('should be able to launch Chrome', async () => {
+ const {browser, close} = await launch({product: 'chrome'});
+ try {
+ const userAgent = await browser.userAgent();
+ expect(userAgent).toContain('Chrome');
+ } finally {
+ await close();
+ }
+ });
+
+ it('should be able to launch Firefox', async function () {
+ this.timeout(FIREFOX_TIMEOUT);
+ const {browser, close} = await launch({product: 'firefox'});
+ try {
+ const userAgent = await browser.userAgent();
+ expect(userAgent).toContain('Firefox');
+ } finally {
+ await close();
+ }
+ });
+ });
+
+ describe('Puppeteer.connect', function () {
+ it('should be able to connect multiple times to the same browser', async () => {
+ const {puppeteer, browser, close} = await launch({});
+ try {
+ const otherBrowser = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ const page = await otherBrowser.newPage();
+ expect(
+ await page.evaluate(() => {
+ return 7 * 8;
+ })
+ ).toBe(56);
+ await otherBrowser.disconnect();
+
+ const secondPage = await browser.newPage();
+ expect(
+ await secondPage.evaluate(() => {
+ return 7 * 6;
+ })
+ ).toBe(42);
+ } finally {
+ await close();
+ }
+ });
+ it('should be able to close remote browser', async () => {
+ const {puppeteer, browser, close} = await launch({});
+ try {
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ await Promise.all([
+ waitEvent(browser, 'disconnected'),
+ remoteBrowser.close(),
+ ]);
+ } finally {
+ await close();
+ }
+ });
+ it('should be able to connect to a browser with no page targets', async () => {
+ const {puppeteer, browser, close} = await launch({});
+
+ try {
+ const pages = await browser.pages();
+ await Promise.all(
+ pages.map(page => {
+ return page.close();
+ })
+ );
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ protocol: browser.protocol,
+ });
+ await Promise.all([
+ waitEvent(browser, 'disconnected'),
+ remoteBrowser.close(),
+ ]);
+ } finally {
+ await close();
+ }
+ });
+ it('should support ignoreHTTPSErrors option', async () => {
+ const {puppeteer, httpsServer, browser, close} = await launch(
+ {},
+ {
+ createContext: false,
+ }
+ );
+
+ try {
+ const browserWSEndpoint = browser.wsEndpoint();
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint,
+ ignoreHTTPSErrors: true,
+ protocol: browser.protocol,
+ });
+ const page = await remoteBrowser.newPage();
+ const [serverRequest, response] = await Promise.all([
+ httpsServer.waitForRequest('/empty.html'),
+ page.goto(httpsServer.EMPTY_PAGE),
+ ]);
+ expect(response!.ok()).toBe(true);
+ expect(response!.securityDetails()).toBeTruthy();
+ const protocol = (serverRequest.socket as TLSSocket)
+ .getProtocol()!
+ .replace('v', ' ');
+ expect(response!.securityDetails()!.protocol()).toBe(protocol);
+ await page.close();
+ await remoteBrowser.close();
+ } finally {
+ await close();
+ }
+ });
+
+ it('should support targetFilter option in puppeteer.launch', async () => {
+ const {browser, close} = await launch(
+ {
+ targetFilter: target => {
+ return target.type() !== 'page';
+ },
+ waitForInitialPage: false,
+ },
+ {createContext: false}
+ );
+ try {
+ const targets = browser.targets();
+ expect(targets).toHaveLength(1);
+ expect(
+ targets.find(target => {
+ return target.type() === 'page';
+ })
+ ).toBeUndefined();
+ } finally {
+ await close();
+ }
+ });
+
+ // @see https://github.com/puppeteer/puppeteer/issues/4197
+ it('should support targetFilter option', async () => {
+ const {puppeteer, server, browser, close} = await launch(
+ {},
+ {
+ createContext: false,
+ }
+ );
+ try {
+ const browserWSEndpoint = browser.wsEndpoint();
+ const page1 = await browser.newPage();
+ await page1.goto(server.EMPTY_PAGE);
+
+ const page2 = await browser.newPage();
+ await page2.goto(server.EMPTY_PAGE + '?should-be-ignored');
+
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint,
+ targetFilter: target => {
+ return !target.url().includes('should-be-ignored');
+ },
+ protocol: browser.protocol,
+ });
+
+ const pages = await remoteBrowser.pages();
+
+ expect(
+ pages
+ .map((p: Page) => {
+ return p.url();
+ })
+ .sort()
+ ).toEqual(['about:blank', server.EMPTY_PAGE]);
+
+ await page2.close();
+ await page1.close();
+ await remoteBrowser.disconnect();
+ await browser.close();
+ } finally {
+ await close();
+ }
+ });
+ it('should be able to reconnect to a disconnected browser', async () => {
+ const {puppeteer, server, browser, close} = await launch({});
+ try {
+ const browserWSEndpoint = browser.wsEndpoint();
+ const page = await browser.newPage();
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ await browser.disconnect();
+
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint,
+ protocol: browser.protocol,
+ });
+ const pages = await remoteBrowser.pages();
+ const restoredPage = pages.find(page => {
+ return page.url() === server.PREFIX + '/frames/nested-frames.html';
+ })!;
+ expect(dumpFrames(restoredPage.mainFrame())).toEqual([
+ 'http://localhost:<PORT>/frames/nested-frames.html',
+ ' http://localhost:<PORT>/frames/two-frames.html (2frames)',
+ ' http://localhost:<PORT>/frames/frame.html (uno)',
+ ' http://localhost:<PORT>/frames/frame.html (dos)',
+ ' http://localhost:<PORT>/frames/frame.html (aframe)',
+ ]);
+ expect(
+ await restoredPage.evaluate(() => {
+ return 7 * 8;
+ })
+ ).toBe(56);
+ await remoteBrowser.close();
+ } finally {
+ await close();
+ }
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410
+ it('should be able to connect to the same page simultaneously', async () => {
+ const {puppeteer, browser: browserOne, close} = await launch({});
+
+ try {
+ const browserTwo = await puppeteer.connect({
+ browserWSEndpoint: browserOne.wsEndpoint(),
+ protocol: browserOne.protocol,
+ });
+ const [page1, page2] = await Promise.all([
+ new Promise<Page | null>(x => {
+ return browserOne.once('targetcreated', target => {
+ x(target.page());
+ });
+ }),
+ browserTwo.newPage(),
+ ]);
+ assert(page1);
+ expect(
+ await page1.evaluate(() => {
+ return 7 * 8;
+ })
+ ).toBe(56);
+ expect(
+ await page2.evaluate(() => {
+ return 7 * 6;
+ })
+ ).toBe(42);
+ } finally {
+ await close();
+ }
+ });
+ it('should be able to reconnect', async () => {
+ const {
+ puppeteer,
+ server,
+ browser: browserOne,
+ close,
+ } = await launch({});
+ try {
+ const browserWSEndpoint = browserOne.wsEndpoint();
+ const pageOne = await browserOne.newPage();
+ await pageOne.goto(server.EMPTY_PAGE);
+ await browserOne.disconnect();
+
+ const browserTwo = await puppeteer.connect({
+ browserWSEndpoint,
+ protocol: browserOne.protocol,
+ });
+ const pages = await browserTwo.pages();
+ const pageTwo = pages.find(page => {
+ return page.url() === server.EMPTY_PAGE;
+ })!;
+ await pageTwo.reload();
+ using _ = await pageTwo.waitForSelector('body', {
+ timeout: 10000,
+ });
+ await browserTwo.close();
+ } finally {
+ await close();
+ }
+ });
+ });
+ describe('Puppeteer.executablePath', function () {
+ it('should work', async () => {
+ const {puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+
+ const executablePath = puppeteer.executablePath();
+ expect(fs.existsSync(executablePath)).toBe(true);
+ expect(fs.realpathSync(executablePath)).toBe(executablePath);
+ });
+ it('returns executablePath for channel', async () => {
+ const {puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+
+ const executablePath = puppeteer.executablePath('chrome');
+ expect(executablePath).toBeTruthy();
+ });
+ describe('when executable path is configured', () => {
+ const sandbox = sinon.createSandbox();
+
+ beforeEach(async () => {
+ const {puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+ sandbox
+ .stub(puppeteer.configuration, 'executablePath')
+ .value('SOME_CUSTOM_EXECUTABLE');
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it('its value is used', async () => {
+ const {puppeteer} = await getTestState({
+ skipLaunch: true,
+ });
+ try {
+ puppeteer.executablePath();
+ } catch (error) {
+ expect((error as Error).message).toContain(
+ 'SOME_CUSTOM_EXECUTABLE'
+ );
+ }
+ });
+ });
+ });
+ });
+
+ describe('Browser target events', function () {
+ it('should work', async () => {
+ const {browser, server, close} = await launch({});
+
+ try {
+ const events: string[] = [];
+ browser.on('targetcreated', () => {
+ events.push('CREATED');
+ });
+ browser.on('targetchanged', () => {
+ events.push('CHANGED');
+ });
+ browser.on('targetdestroyed', () => {
+ events.push('DESTROYED');
+ });
+ const page = await browser.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.close();
+ expect(events).toEqual(['CREATED', 'CHANGED', 'DESTROYED']);
+ } finally {
+ await close();
+ }
+ });
+ });
+
+ describe('Browser.Events.disconnected', function () {
+ it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async () => {
+ const {puppeteer, browser, close} = await launch({});
+ try {
+ const browserWSEndpoint = browser.wsEndpoint();
+ const remoteBrowser1 = await puppeteer.connect({
+ browserWSEndpoint,
+ protocol: browser.protocol,
+ });
+ const remoteBrowser2 = await puppeteer.connect({
+ browserWSEndpoint,
+ protocol: browser.protocol,
+ });
+
+ let disconnectedOriginal = 0;
+ let disconnectedRemote1 = 0;
+ let disconnectedRemote2 = 0;
+ browser.on('disconnected', () => {
+ ++disconnectedOriginal;
+ });
+ remoteBrowser1.on('disconnected', () => {
+ ++disconnectedRemote1;
+ });
+ remoteBrowser2.on('disconnected', () => {
+ ++disconnectedRemote2;
+ });
+
+ await Promise.all([
+ waitEvent(remoteBrowser2, 'disconnected'),
+ remoteBrowser2.disconnect(),
+ ]);
+
+ expect(disconnectedOriginal).toBe(0);
+ expect(disconnectedRemote1).toBe(0);
+ expect(disconnectedRemote2).toBe(1);
+
+ await Promise.all([
+ waitEvent(remoteBrowser1, 'disconnected'),
+ waitEvent(browser, 'disconnected'),
+ browser.close(),
+ ]);
+
+ expect(disconnectedOriginal).toBe(1);
+ expect(disconnectedRemote1).toBe(1);
+ expect(disconnectedRemote2).toBe(1);
+ } finally {
+ await close();
+ }
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/locator.spec.ts b/remote/test/puppeteer/test/src/locator.spec.ts
new file mode 100644
index 0000000000..9b00cc2d7c
--- /dev/null
+++ b/remote/test/puppeteer/test/src/locator.spec.ts
@@ -0,0 +1,763 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {TimeoutError} from 'puppeteer-core';
+import {
+ Locator,
+ LocatorEvent,
+} from 'puppeteer-core/internal/api/locators/locators.js';
+import sinon from 'sinon';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Locator', function () {
+ setupTestBrowserHooks();
+
+ it('should work with a frame', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button onclick="this.innerText = 'clicked';">test</button>
+ `);
+ let willClick = false;
+ await page
+ .mainFrame()
+ .locator('button')
+ .on(LocatorEvent.Action, () => {
+ willClick = true;
+ })
+ .click();
+ using button = await page.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('clicked');
+ expect(willClick).toBe(true);
+ });
+
+ it('should work without preconditions', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button onclick="this.innerText = 'clicked';">test</button>
+ `);
+ let willClick = false;
+ await page
+ .locator('button')
+ .setEnsureElementIsInTheViewport(false)
+ .setTimeout(0)
+ .setVisibility(null)
+ .setWaitForEnabled(false)
+ .setWaitForStableBoundingBox(false)
+ .on(LocatorEvent.Action, () => {
+ willClick = true;
+ })
+ .click();
+ using button = await page.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('clicked');
+ expect(willClick).toBe(true);
+ });
+
+ describe('Locator.click', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button onclick="this.innerText = 'clicked';">test</button>
+ `);
+ let willClick = false;
+ await page
+ .locator('button')
+ .on(LocatorEvent.Action, () => {
+ willClick = true;
+ })
+ .click();
+ using button = await page.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('clicked');
+ expect(willClick).toBe(true);
+ });
+
+ it('should work for multiple selectors', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button onclick="this.innerText = 'clicked';">test</button>
+ `);
+ let clicked = false;
+ await page
+ .locator('::-p-text(test), ::-p-xpath(/button)')
+ .on(LocatorEvent.Action, () => {
+ clicked = true;
+ })
+ .click();
+ using button = await page.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('clicked');
+ expect(clicked).toBe(true);
+ });
+
+ it('should work if the element is out of viewport', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="margin-top: 600px;" onclick="this.innerText = 'clicked';">test</button>
+ `);
+ await page.locator('button').click();
+ using button = await page.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('clicked');
+ });
+
+ it('should work if the element becomes visible later', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
+ `);
+ using button = await page.$('button');
+ const result = page
+ .locator('button')
+ .click()
+ .catch(err => {
+ return err;
+ });
+ expect(
+ await button?.evaluate(el => {
+ return el.innerText;
+ })
+ ).toBe('test');
+ await button?.evaluate(el => {
+ el.style.display = 'block';
+ });
+ const maybeError = await result;
+ if (maybeError instanceof Error) {
+ throw maybeError;
+ }
+ expect(
+ await button?.evaluate(el => {
+ return el.innerText;
+ })
+ ).toBe('clicked');
+ });
+
+ it('should work if the element becomes enabled later', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button disabled onclick="this.innerText = 'clicked';">test</button>
+ `);
+ using button = await page.$('button');
+ const result = page.locator('button').click();
+ expect(
+ await button?.evaluate(el => {
+ return el.innerText;
+ })
+ ).toBe('test');
+ await button?.evaluate(el => {
+ el.disabled = false;
+ });
+ await result;
+ expect(
+ await button?.evaluate(el => {
+ return el.innerText;
+ })
+ ).toBe('clicked');
+ });
+
+ it('should work if multiple conditions are satisfied later', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="margin-top: 600px;" style="display: none;" disabled onclick="this.innerText = 'clicked';">test</button>
+ `);
+ using button = await page.$('button');
+ const result = page.locator('button').click();
+ expect(
+ await button?.evaluate(el => {
+ return el.innerText;
+ })
+ ).toBe('test');
+ await button?.evaluate(el => {
+ el.disabled = false;
+ el.style.display = 'block';
+ });
+ await result;
+ expect(
+ await button?.evaluate(el => {
+ return el.innerText;
+ })
+ ).toBe('clicked');
+ });
+
+ it('should time out', async () => {
+ const clock = sinon.useFakeTimers({
+ shouldClearNativeTimers: true,
+ shouldAdvanceTime: true,
+ });
+ try {
+ const {page} = await getTestState();
+
+ page.setDefaultTimeout(5000);
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
+ `);
+ const result = page.locator('button').click();
+ clock.tick(5100);
+ await expect(result).rejects.toEqual(
+ new TimeoutError('Timed out after waiting 5000ms')
+ );
+ } finally {
+ clock.restore();
+ }
+ });
+
+ it('should retry clicks on errors', async () => {
+ const {page} = await getTestState();
+ const clock = sinon.useFakeTimers({
+ shouldClearNativeTimers: true,
+ shouldAdvanceTime: true,
+ });
+ try {
+ page.setDefaultTimeout(5000);
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
+ `);
+ const result = page.locator('button').click();
+ clock.tick(5100);
+ await expect(result).rejects.toEqual(
+ new TimeoutError('Timed out after waiting 5000ms')
+ );
+ } finally {
+ clock.restore();
+ }
+ });
+
+ it('can be aborted', async () => {
+ const {page} = await getTestState();
+ const clock = sinon.useFakeTimers({
+ shouldClearNativeTimers: true,
+ shouldAdvanceTime: true,
+ });
+ try {
+ page.setDefaultTimeout(5000);
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
+ `);
+ const abortController = new AbortController();
+ const result = page.locator('button').click({
+ signal: abortController.signal,
+ });
+ clock.tick(2000);
+ abortController.abort();
+ await expect(result).rejects.toThrow(/aborted/);
+ } finally {
+ clock.restore();
+ }
+ });
+
+ it('should work with a OOPIF', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <iframe src="data:text/html,<button onclick=&quot;this.innerText = 'clicked';&quot;>test</button>"></iframe>
+ `);
+ const frame = await page.waitForFrame(frame => {
+ return frame.url().startsWith('data');
+ });
+ let willClick = false;
+ await frame
+ .locator('button')
+ .on(LocatorEvent.Action, () => {
+ willClick = true;
+ })
+ .click();
+ using button = await frame.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('clicked');
+ expect(willClick).toBe(true);
+ });
+ });
+
+ describe('Locator.hover', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button onmouseenter="this.innerText = 'hovered';">test</button>
+ `);
+ let hovered = false;
+ await page
+ .locator('button')
+ .on(LocatorEvent.Action, () => {
+ hovered = true;
+ })
+ .hover();
+ using button = await page.$('button');
+ const text = await button?.evaluate(el => {
+ return el.innerText;
+ });
+ expect(text).toBe('hovered');
+ expect(hovered).toBe(true);
+ });
+ });
+
+ describe('Locator.scroll', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <div style="height: 500px; width: 500px; overflow: scroll;">
+ <div style="height: 1000px; width: 1000px;">test</div>
+ </div>
+ `);
+ let scrolled = false;
+ await page
+ .locator('div')
+ .on(LocatorEvent.Action, () => {
+ scrolled = true;
+ })
+ .scroll({
+ scrollTop: 500,
+ scrollLeft: 500,
+ });
+ using scrollable = await page.$('div');
+ const scroll = await scrollable?.evaluate(el => {
+ return el.scrollTop + ' ' + el.scrollLeft;
+ });
+ expect(scroll).toBe('500 500');
+ expect(scrolled).toBe(true);
+ });
+ });
+
+ describe('Locator.fill', function () {
+ it('should work for textarea', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <textarea></textarea>
+ `);
+ let filled = false;
+ await page
+ .locator('textarea')
+ .on(LocatorEvent.Action, () => {
+ filled = true;
+ })
+ .fill('test');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('textarea')?.value === 'test';
+ })
+ ).toBe(true);
+ expect(filled).toBe(true);
+ });
+
+ it('should work for selects', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <select>
+ <option value="value1">Option 1</option>
+ <option value="value2">Option 2</option>
+ <select>
+ `);
+ let filled = false;
+ await page
+ .locator('select')
+ .on(LocatorEvent.Action, () => {
+ filled = true;
+ })
+ .fill('value2');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('select')?.value === 'value2';
+ })
+ ).toBe(true);
+ expect(filled).toBe(true);
+ });
+
+ it('should work for inputs', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`
+ <input>
+ `);
+ await page.locator('input').fill('test');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('input')?.value === 'test';
+ })
+ ).toBe(true);
+ });
+
+ it('should work if the input becomes enabled later', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`
+ <input disabled>
+ `);
+ using input = await page.$('input');
+ const result = page.locator('input').fill('test');
+ expect(
+ await input?.evaluate(el => {
+ return el.value;
+ })
+ ).toBe('');
+ await input?.evaluate(el => {
+ el.disabled = false;
+ });
+ await result;
+ expect(
+ await input?.evaluate(el => {
+ return el.value;
+ })
+ ).toBe('test');
+ });
+
+ it('should work for contenteditable', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`
+ <div contenteditable="true">
+ `);
+ await page.locator('div').fill('test');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('div')?.innerText === 'test';
+ })
+ ).toBe(true);
+ });
+
+ it('should work for pre-filled inputs', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`
+ <input value="te">
+ `);
+ await page.locator('input').fill('test');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('input')?.value === 'test';
+ })
+ ).toBe(true);
+ });
+
+ it('should override pre-filled inputs', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`
+ <input value="wrong prefix">
+ `);
+ await page.locator('input').fill('test');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('input')?.value === 'test';
+ })
+ ).toBe(true);
+ });
+
+ it('should work for non-text inputs', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`
+ <input type="color">
+ `);
+ await page.locator('input').fill('#333333');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('input')?.value === '#333333';
+ })
+ ).toBe(true);
+ });
+ });
+
+ describe('Locator.race', () => {
+ it('races multiple locators', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button onclick="window.count++;">test</button>
+ `);
+ await page.evaluate(() => {
+ // @ts-expect-error different context.
+ window.count = 0;
+ });
+ await Locator.race([
+ page.locator('button'),
+ page.locator('button'),
+ ]).click();
+ const count = await page.evaluate(() => {
+ // @ts-expect-error different context.
+ return globalThis.count;
+ });
+ expect(count).toBe(1);
+ });
+
+ it('can be aborted', async () => {
+ const {page} = await getTestState();
+ const clock = sinon.useFakeTimers({
+ shouldClearNativeTimers: true,
+ shouldAdvanceTime: true,
+ });
+ try {
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ <button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
+ `);
+ const abortController = new AbortController();
+ const result = Locator.race([
+ page.locator('button'),
+ page.locator('button'),
+ ])
+ .setTimeout(5000)
+ .click({
+ signal: abortController.signal,
+ });
+ clock.tick(2000);
+ abortController.abort();
+ await expect(result).rejects.toThrow(/aborted/);
+ } finally {
+ clock.restore();
+ }
+ });
+
+ it('should time out when all locators do not match', async () => {
+ const clock = sinon.useFakeTimers({
+ shouldClearNativeTimers: true,
+ shouldAdvanceTime: true,
+ });
+ try {
+ const {page} = await getTestState();
+ await page.setContent(`<button>test</button>`);
+ const result = Locator.race([
+ page.locator('not-found'),
+ page.locator('not-found'),
+ ])
+ .setTimeout(5000)
+ .click();
+ clock.tick(5100);
+ await expect(result).rejects.toEqual(
+ new TimeoutError('Timed out after waiting 5000ms')
+ );
+ } finally {
+ clock.restore();
+ }
+ });
+
+ it('should not time out when one of the locators matches', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`<button>test</button>`);
+ const result = Locator.race([
+ page.locator('not-found'),
+ page.locator('button'),
+ ]).click();
+ await expect(result).resolves.toEqual(undefined);
+ });
+ });
+
+ describe('Locator.prototype.map', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`<div>test</div>`);
+ await expect(
+ page
+ .locator('::-p-text(test)')
+ .map(element => {
+ return element.getAttribute('clickable');
+ })
+ .wait()
+ ).resolves.toEqual(null);
+ await page.evaluate(() => {
+ document.querySelector('div')?.setAttribute('clickable', 'true');
+ });
+ await expect(
+ page
+ .locator('::-p-text(test)')
+ .map(element => {
+ return element.getAttribute('clickable');
+ })
+ .wait()
+ ).resolves.toEqual('true');
+ });
+ it('should work with throws', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`<div>test</div>`);
+ const result = page
+ .locator('::-p-text(test)')
+ .map(element => {
+ const clickable = element.getAttribute('clickable');
+ if (!clickable) {
+ throw new Error('Missing `clickable` as an attribute');
+ }
+ return clickable;
+ })
+ .wait();
+ await page.evaluate(() => {
+ document.querySelector('div')?.setAttribute('clickable', 'true');
+ });
+ await expect(result).resolves.toEqual('true');
+ });
+ it('should work with expect', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`<div>test</div>`);
+ const result = page
+ .locator('::-p-text(test)')
+ .filter(element => {
+ return element.getAttribute('clickable') !== null;
+ })
+ .map(element => {
+ return element.getAttribute('clickable');
+ })
+ .wait();
+ await page.evaluate(() => {
+ document.querySelector('div')?.setAttribute('clickable', 'true');
+ });
+ await expect(result).resolves.toEqual('true');
+ });
+ });
+
+ describe('Locator.prototype.filter', () => {
+ it('should resolve as soon as the predicate matches', async () => {
+ const clock = sinon.useFakeTimers({
+ shouldClearNativeTimers: true,
+ shouldAdvanceTime: true,
+ });
+ try {
+ const {page} = await getTestState();
+ await page.setContent(`<div>test</div>`);
+ const result = page
+ .locator('::-p-text(test)')
+ .setTimeout(5000)
+ .filter(async element => {
+ return element.getAttribute('clickable') === 'true';
+ })
+ .filter(element => {
+ return element.getAttribute('clickable') === 'true';
+ })
+ .hover();
+ clock.tick(2000);
+ await page.evaluate(() => {
+ document.querySelector('div')?.setAttribute('clickable', 'true');
+ });
+ clock.restore();
+ await expect(result).resolves.toEqual(undefined);
+ } finally {
+ clock.restore();
+ }
+ });
+ });
+
+ describe('Locator.prototype.wait', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ void page.setContent(`
+ <script>
+ setTimeout(() => {
+ const element = document.createElement("div");
+ element.innerText = "test2"
+ document.body.append(element);
+ }, 50);
+ </script>
+ `);
+ // This shouldn't throw.
+ await page.locator('div').wait();
+ });
+ });
+
+ describe('Locator.prototype.waitHandle', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ void page.setContent(`
+ <script>
+ setTimeout(() => {
+ const element = document.createElement("div");
+ element.innerText = "test2"
+ document.body.append(element);
+ }, 50);
+ </script>
+ `);
+ await expect(page.locator('div').waitHandle()).resolves.toBeDefined();
+ });
+ });
+
+ describe('Locator.prototype.clone', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ const locator = page.locator('div');
+ const clone = locator.clone();
+ expect(locator).not.toStrictEqual(clone);
+ });
+ it('should work internally with delegated locators', async () => {
+ const {page} = await getTestState();
+ const locator = page.locator('div');
+ const delegatedLocators = [
+ locator.map(div => {
+ return div.textContent;
+ }),
+ locator.filter(div => {
+ return div.textContent?.length === 0;
+ }),
+ ];
+ for (let delegatedLocator of delegatedLocators) {
+ delegatedLocator = delegatedLocator.setTimeout(500);
+ expect(delegatedLocator.timeout).not.toStrictEqual(locator.timeout);
+ }
+ });
+ });
+
+ describe('FunctionLocator', () => {
+ it('should work', async () => {
+ const {page} = await getTestState();
+ const result = page
+ .locator(() => {
+ return new Promise<boolean>(resolve => {
+ return setTimeout(() => {
+ return resolve(true);
+ }, 100);
+ });
+ })
+ .wait();
+ await expect(result).resolves.toEqual(true);
+ });
+ it('should work with actions', async () => {
+ const {page} = await getTestState();
+ await page.setContent(`<div onclick="window.clicked = true">test</div>`);
+ await page
+ .locator(() => {
+ return document.getElementsByTagName('div')[0] as HTMLDivElement;
+ })
+ .click();
+ await expect(
+ page.evaluate(() => {
+ return (window as unknown as {clicked: boolean}).clicked;
+ })
+ ).resolves.toEqual(true);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/mocha-utils.ts b/remote/test/puppeteer/test/src/mocha-utils.ts
new file mode 100644
index 0000000000..3fff9c9930
--- /dev/null
+++ b/remote/test/puppeteer/test/src/mocha-utils.ts
@@ -0,0 +1,507 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+import {TestServer} from '@pptr/testserver';
+import type {Protocol} from 'devtools-protocol';
+import expect from 'expect';
+import type * as MochaBase from 'mocha';
+import puppeteer from 'puppeteer/lib/cjs/puppeteer/puppeteer.js';
+import type {Browser} from 'puppeteer-core/internal/api/Browser.js';
+import type {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+import type {
+ PuppeteerLaunchOptions,
+ PuppeteerNode,
+} from 'puppeteer-core/internal/node/PuppeteerNode.js';
+import {rmSync} from 'puppeteer-core/internal/node/util/fs.js';
+import {Deferred} from 'puppeteer-core/internal/util/Deferred.js';
+import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js';
+import sinon from 'sinon';
+
+import {extendExpectWithToBeGolden} from './utils.js';
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Mocha {
+ export interface SuiteFunction {
+ /**
+ * Use it if you want to capture debug logs for a specitic test suite in CI.
+ * This describe function enables capturing of debug logs and would print them
+ * only if a test fails to reduce the amount of output.
+ */
+ withDebugLogs: (
+ description: string,
+ body: (this: MochaBase.Suite) => void
+ ) => void;
+ }
+ export interface TestFunction {
+ /*
+ * Use to rerun the test and capture logs for the failed attempts
+ * that way we don't push all the logs making it easier to read.
+ */
+ deflake: (
+ repeats: number,
+ title: string,
+ fn: MochaBase.AsyncFunc
+ ) => void;
+ /*
+ * Use to rerun a single test and capture logs for the failed attempts
+ */
+ deflakeOnly: (
+ repeats: number,
+ title: string,
+ fn: MochaBase.AsyncFunc
+ ) => void;
+ }
+ }
+}
+
+const product =
+ process.env['PRODUCT'] || process.env['PUPPETEER_PRODUCT'] || 'chrome';
+
+const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase() as
+ | 'true'
+ | 'false'
+ | 'new';
+export const isHeadless = headless === 'true' || headless === 'new';
+const isFirefox = product === 'firefox';
+const isChrome = product === 'chrome';
+const protocol = (process.env['PUPPETEER_PROTOCOL'] || 'cdp') as
+ | 'cdp'
+ | 'webDriverBiDi';
+
+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 === 'new' ? ('new' as const) : isHeadless,
+ dumpio: !!process.env['DUMPIO'],
+ protocol,
+ },
+ extraLaunchOptions
+);
+
+if (defaultBrowserOptions.executablePath) {
+ console.warn(
+ `WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}`
+ );
+} else {
+ 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`
+ );
+ }
+}
+
+const processVariables: {
+ product: string;
+ headless: 'true' | 'false' | 'new';
+ isHeadless: boolean;
+ isFirefox: boolean;
+ isChrome: boolean;
+ protocol: 'cdp' | 'webDriverBiDi';
+ defaultBrowserOptions: PuppeteerLaunchOptions;
+} = {
+ product,
+ headless,
+ isHeadless,
+ isFirefox,
+ isChrome,
+ protocol,
+ defaultBrowserOptions,
+};
+
+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 setupTestBrowserHooks = (): void => {
+ before(async function () {
+ try {
+ if (!state.browser) {
+ state.browser = await puppeteer.launch({
+ ...processVariables.defaultBrowserOptions,
+ timeout: this.timeout() - 1_000,
+ });
+ }
+ } catch (error) {
+ console.error(error);
+ // Intentionally empty as `getTestState` will throw
+ // if browser is not found
+ }
+ });
+
+ after(() => {
+ if (typeof gc !== 'undefined') {
+ gc();
+ const memory = process.memoryUsage();
+ console.log('Memory stats:');
+ for (const key of Object.keys(memory)) {
+ console.log(
+ key,
+ // @ts-expect-error TS cannot the key type.
+ `${Math.round(((memory[key] / 1024 / 1024) * 100) / 100)} MB`
+ );
+ }
+ }
+ });
+};
+
+export const getTestState = async (
+ options: {
+ skipLaunch?: boolean;
+ skipContextCreation?: boolean;
+ } = {}
+): Promise<PuppeteerTestState> => {
+ const {skipLaunch = false, skipContextCreation = false} = options;
+
+ state.defaultBrowserOptions = JSON.parse(
+ JSON.stringify(processVariables.defaultBrowserOptions)
+ );
+
+ state.server?.reset();
+ state.httpsServer?.reset();
+
+ if (skipLaunch) {
+ return state as PuppeteerTestState;
+ }
+
+ if (!state.browser) {
+ throw new Error('Browser was not set-up in time!');
+ }
+
+ if (state.context) {
+ await state.context.close();
+ state.context = undefined;
+ state.page = undefined;
+ }
+
+ if (!skipContextCreation) {
+ state.context = await state.browser!.createIncognitoBrowserContext();
+ state.page = await state.context.newPage();
+ }
+ return state as PuppeteerTestState;
+};
+
+const setupGoldenAssertions = (): void => {
+ const suffix = processVariables.product.toLowerCase();
+ const GOLDEN_DIR = path.join(__dirname, `../golden-${suffix}`);
+ const OUTPUT_DIR = path.join(__dirname, `../output-${suffix}`);
+ if (fs.existsSync(OUTPUT_DIR)) {
+ rmSync(OUTPUT_DIR);
+ }
+ extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR);
+};
+
+setupGoldenAssertions();
+
+export interface PuppeteerTestState {
+ browser: Browser;
+ context: BrowserContext;
+ page: Page;
+ puppeteer: PuppeteerNode;
+ defaultBrowserOptions: PuppeteerLaunchOptions;
+ server: TestServer;
+ httpsServer: TestServer;
+ isFirefox: boolean;
+ isChrome: boolean;
+ isHeadless: boolean;
+ headless: 'true' | 'false' | 'new';
+ puppeteerPath: string;
+}
+const state: Partial<PuppeteerTestState> = {};
+
+if (
+ process.env['MOCHA_WORKER_ID'] === undefined ||
+ process.env['MOCHA_WORKER_ID'] === '0'
+) {
+ console.log(
+ `Running unit tests with:
+ -> product: ${processVariables.product}
+ -> binary: ${
+ processVariables.defaultBrowserOptions.executablePath ||
+ path.relative(process.cwd(), puppeteer.executablePath())
+ }
+ -> mode: ${
+ processVariables.isHeadless
+ ? processVariables.headless === 'new'
+ ? '--headless=new'
+ : '--headless'
+ : 'headful'
+ }`
+ );
+}
+
+const browserNotClosedError = new Error(
+ 'A manually launched browser was not closed!'
+);
+
+export const mochaHooks = {
+ async beforeAll(): Promise<void> {
+ async function setUpDefaultState() {
+ const {server, httpsServer} = await setupServer();
+
+ state.puppeteer = puppeteer;
+ state.server = server;
+ state.httpsServer = httpsServer;
+ state.isFirefox = processVariables.isFirefox;
+ state.isChrome = processVariables.isChrome;
+ state.isHeadless = processVariables.isHeadless;
+ state.headless = processVariables.headless;
+ state.puppeteerPath = path.resolve(
+ path.join(__dirname, '..', '..', 'packages', 'puppeteer')
+ );
+ }
+
+ try {
+ await Deferred.race([
+ setUpDefaultState(),
+ Deferred.create({
+ message: `Failed in after Hook`,
+ timeout: (this as any).timeout() - 1000,
+ }),
+ ]);
+ } catch {}
+ },
+
+ async afterAll(): Promise<void> {
+ (this as any).timeout(0);
+ const lastTestFile = (this as any)?.test?.parent?.suites?.[0]?.file
+ ?.split('/')
+ ?.at(-1);
+ try {
+ await Promise.all([
+ state.server?.stop(),
+ state.httpsServer?.stop(),
+ state.browser?.close(),
+ ]);
+ } catch (error) {
+ throw new Error(
+ `Closing defaults (HTTP TestServer, HTTPS TestServer, Browser ) failed in ${lastTestFile}}`
+ );
+ }
+ if (browserCleanupsAfterAll.length > 0) {
+ await closeLaunched(browserCleanupsAfterAll)();
+ throw new Error(`Browser was not closed in ${lastTestFile}`);
+ }
+ },
+
+ async afterEach(): Promise<void> {
+ if (browserCleanups.length > 0) {
+ (this as any).test.error(browserNotClosedError);
+ await Deferred.race([
+ closeLaunched(browserCleanups)(),
+ Deferred.create({
+ message: `Failed in after Hook`,
+ timeout: (this as any).timeout() - 1000,
+ }),
+ ]);
+ }
+ sinon.restore();
+ },
+};
+
+declare module 'expect' {
+ interface Matchers<R> {
+ atLeastOneToContain(expected: string[]): R;
+ }
+}
+
+expect.extend({
+ atLeastOneToContain: (actual: string, expected: string[]) => {
+ for (const test of expected) {
+ try {
+ expect(actual).toContain(test);
+ return {
+ pass: true,
+ message: () => {
+ return '';
+ },
+ };
+ } catch (err) {}
+ }
+
+ return {
+ pass: false,
+ message: () => {
+ return `"${actual}" didn't contain any of the strings ${JSON.stringify(
+ expected
+ )}`;
+ },
+ };
+ },
+});
+
+export const expectCookieEquals = async (
+ cookies: Protocol.Network.Cookie[],
+ expectedCookies: Array<Partial<Protocol.Network.Cookie>>
+): Promise<void> => {
+ if (!processVariables.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).toHaveLength(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<void> => {
+ for (let i = 0; i < attempts; i++) {
+ if (data.length >= minLength) {
+ break;
+ }
+ await new Promise(resolve => {
+ return setTimeout(resolve, timeout);
+ });
+ }
+};
+
+export const createTimeout = <T>(
+ n: number,
+ value?: T
+): Promise<T | undefined> => {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ return resolve(value);
+ }, n);
+ });
+};
+
+const browserCleanupsAfterAll: Array<() => Promise<void>> = [];
+const browserCleanups: Array<() => Promise<void>> = [];
+
+const closeLaunched = (storage: Array<() => Promise<void>>) => {
+ return async () => {
+ let cleanup = storage.pop();
+ try {
+ while (cleanup) {
+ await cleanup();
+ cleanup = storage.pop();
+ }
+ } catch (error) {
+ // If the browser was closed by other means, swallow the error
+ // and mark the browser as closed.
+ if ((error as Error)?.message.includes('Connection closed')) {
+ storage.splice(0, storage.length);
+ return;
+ }
+
+ throw error;
+ }
+ };
+};
+
+export const launch = async (
+ launchOptions: Readonly<PuppeteerLaunchOptions>,
+ options: {
+ after?: 'each' | 'all';
+ createContext?: boolean;
+ createPage?: boolean;
+ } = {}
+): Promise<
+ PuppeteerTestState & {
+ close: () => Promise<void>;
+ }
+> => {
+ const {after = 'each', createContext = true, createPage = true} = options;
+ const initState = await getTestState({
+ skipLaunch: true,
+ });
+ const cleanupStorage =
+ after === 'each' ? browserCleanups : browserCleanupsAfterAll;
+ try {
+ const browser = await puppeteer.launch({
+ ...initState.defaultBrowserOptions,
+ ...launchOptions,
+ });
+ cleanupStorage.push(() => {
+ return browser.close();
+ });
+
+ let context: BrowserContext;
+ let page: Page;
+ if (createContext) {
+ context = await browser.createIncognitoBrowserContext();
+ cleanupStorage.push(() => {
+ return context.close();
+ });
+
+ if (createPage) {
+ page = await context.newPage();
+ cleanupStorage.push(() => {
+ return page.close();
+ });
+ }
+ }
+
+ return {
+ ...initState,
+ browser,
+ context: context!,
+ page: page!,
+ close: closeLaunched(cleanupStorage),
+ };
+ } catch (error) {
+ await closeLaunched(cleanupStorage)();
+
+ throw error;
+ }
+};
diff --git a/remote/test/puppeteer/test/src/mouse.spec.ts b/remote/test/puppeteer/test/src/mouse.spec.ts
new file mode 100644
index 0000000000..69229eb147
--- /dev/null
+++ b/remote/test/puppeteer/test/src/mouse.spec.ts
@@ -0,0 +1,472 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import os from 'os';
+
+import expect from 'expect';
+import {MouseButton} from 'puppeteer-core/internal/api/Input.js';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+import type {KeyInput} from 'puppeteer-core/internal/common/USKeyboardLayout.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+interface ClickData {
+ type: string;
+ detail: number;
+ clientX: number;
+ clientY: number;
+ isTrusted: boolean;
+ button: number;
+ buttons: number;
+}
+
+interface Dimensions {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+}
+
+function dimensions(): Dimensions {
+ const rect = document.querySelector('textarea')!.getBoundingClientRect();
+ return {
+ x: rect.left,
+ y: rect.top,
+ width: rect.width,
+ height: rect.height,
+ };
+}
+
+describe('Mouse', function () {
+ setupTestBrowserHooks();
+
+ it('should click the document', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ (globalThis as any).clickPromise = new Promise(resolve => {
+ document.addEventListener('click', event => {
+ resolve({
+ type: event.type,
+ detail: event.detail,
+ clientX: event.clientX,
+ clientY: event.clientY,
+ isTrusted: event.isTrusted,
+ button: event.button,
+ });
+ });
+ });
+ });
+ await page.mouse.click(50, 60);
+ const event = await page.evaluate(() => {
+ return (globalThis as any).clickPromise;
+ });
+ expect(event.type).toBe('click');
+ expect(event.detail).toBe(1);
+ expect(event.clientX).toBe(50);
+ expect(event.clientY).toBe(60);
+ expect(event.isTrusted).toBe(true);
+ expect(event.button).toBe(0);
+ });
+ it('should resize the textarea', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ const {x, y, width, height} = await page.evaluate(dimensions);
+ const mouse = page.mouse;
+ await mouse.move(x + width - 4, y + height - 4);
+ await mouse.down();
+ await mouse.move(x + width + 100, y + height + 100);
+ await mouse.up();
+ const newDimensions = await page.evaluate(dimensions);
+ expect(newDimensions.width).toBe(Math.round(width + 104));
+ expect(newDimensions.height).toBe(Math.round(height + 104));
+ });
+ it('should select the text with mouse', async () => {
+ const {page, server} = await getTestState();
+
+ const text =
+ "This is the text that we are going to try to select. Let's see how it goes.";
+
+ await page.goto(`${server.PREFIX}/input/textarea.html`);
+ await page.focus('textarea');
+ await page.keyboard.type(text);
+ using handle = await page
+ .locator('textarea')
+ .filterHandle(async element => {
+ return await element.evaluate((element, text) => {
+ return element.value === text;
+ }, text);
+ })
+ .waitHandle();
+ const {x, y} = await page.evaluate(dimensions);
+ await page.mouse.move(x + 2, y + 2);
+ await page.mouse.down();
+ await page.mouse.move(100, 100);
+ await page.mouse.up();
+ expect(
+ await handle.evaluate(element => {
+ return element.value.substring(
+ element.selectionStart,
+ element.selectionEnd
+ );
+ })
+ ).toBe(text);
+ });
+ it('should trigger hover state', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.hover('#button-6');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('button:hover')!.id;
+ })
+ ).toBe('button-6');
+ await page.hover('#button-2');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('button:hover')!.id;
+ })
+ ).toBe('button-2');
+ await page.hover('#button-91');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('button:hover')!.id;
+ })
+ ).toBe('button-91');
+ });
+ it('should trigger hover state with removed window.Node', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.evaluate(() => {
+ // @ts-expect-error Expected.
+ return delete window.Node;
+ });
+ await page.hover('#button-6');
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('button:hover')!.id;
+ })
+ ).toBe('button-6');
+ });
+ it('should set modifier keys on click', async () => {
+ const {page, server, isFirefox} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.evaluate(() => {
+ return document.querySelector('#button-3')!.addEventListener(
+ 'mousedown',
+ e => {
+ return ((globalThis as any).lastEvent = e);
+ },
+ true
+ );
+ });
+ const modifiers = new Map<KeyInput, string>([
+ ['Shift', 'shiftKey'],
+ ['Control', 'ctrlKey'],
+ ['Alt', 'altKey'],
+ ['Meta', 'metaKey'],
+ ]);
+ // In Firefox, the Meta modifier only exists on Mac
+ if (isFirefox && os.platform() !== 'darwin') {
+ modifiers.delete('Meta');
+ }
+ for (const [modifier, key] of modifiers) {
+ await page.keyboard.down(modifier);
+ await page.click('#button-3');
+ if (
+ !(await page.evaluate((mod: string) => {
+ return (globalThis as any).lastEvent[mod];
+ }, key))
+ ) {
+ throw new Error(key + ' should be true');
+ }
+ await page.keyboard.up(modifier);
+ }
+ await page.click('#button-3');
+ for (const [modifier, key] of modifiers) {
+ if (
+ await page.evaluate((mod: string) => {
+ return (globalThis as any).lastEvent[mod];
+ }, key)
+ ) {
+ throw new Error(modifiers.get(modifier) + ' should be false');
+ }
+ }
+ });
+ it('should send mouse wheel events', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/wheel.html');
+ using elem = (await page.$('div'))!;
+ const boundingBoxBefore = (await elem.boundingBox())!;
+ expect(boundingBoxBefore).toMatchObject({
+ width: 115,
+ height: 115,
+ });
+
+ await page.mouse.move(
+ boundingBoxBefore.x + boundingBoxBefore.width / 2,
+ boundingBoxBefore.y + boundingBoxBefore.height / 2
+ );
+
+ await page.mouse.wheel({deltaY: -100});
+ const boundingBoxAfter = await elem.boundingBox();
+ expect(boundingBoxAfter).toMatchObject({
+ width: 230,
+ height: 230,
+ });
+ });
+ it('should tween mouse movement', async () => {
+ const {page} = await getTestState();
+
+ await page.mouse.move(100, 100);
+ await page.evaluate(() => {
+ (globalThis as any).result = [];
+ document.addEventListener('mousemove', event => {
+ (globalThis as any).result.push([event.clientX, event.clientY]);
+ });
+ });
+ await page.mouse.move(200, 300, {steps: 5});
+ expect(await page.evaluate('result')).toEqual([
+ [120, 140],
+ [140, 180],
+ [160, 220],
+ [180, 260],
+ [200, 300],
+ ]);
+ });
+ // @see https://crbug.com/929806
+ it('should work with mobile viewports and cross process navigations', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setViewport({width: 360, height: 640, isMobile: true});
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/mobile.html');
+ await page.evaluate(() => {
+ document.addEventListener('click', event => {
+ (globalThis as any).result = {x: event.clientX, y: event.clientY};
+ });
+ });
+
+ await page.mouse.click(30, 40);
+
+ expect(await page.evaluate('result')).toEqual({x: 30, y: 40});
+ });
+ it('should not throw if buttons are pressed twice', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.mouse.down();
+ await page.mouse.down();
+ });
+
+ interface AddMouseDataListenersOptions {
+ includeMove?: boolean;
+ }
+
+ const addMouseDataListeners = (
+ page: Page,
+ options: AddMouseDataListenersOptions = {}
+ ) => {
+ return page.evaluate(({includeMove}) => {
+ const clicks: ClickData[] = [];
+ const mouseEventListener = (event: MouseEvent) => {
+ clicks.push({
+ type: event.type,
+ detail: event.detail,
+ clientX: event.clientX,
+ clientY: event.clientY,
+ isTrusted: event.isTrusted,
+ button: event.button,
+ buttons: event.buttons,
+ });
+ };
+ document.addEventListener('mousedown', mouseEventListener);
+ if (includeMove) {
+ document.addEventListener('mousemove', mouseEventListener);
+ }
+ document.addEventListener('mouseup', mouseEventListener);
+ document.addEventListener('click', mouseEventListener);
+ document.addEventListener('auxclick', mouseEventListener);
+ (window as unknown as {clicks: ClickData[]}).clicks = clicks;
+ }, options);
+ };
+
+ it('should not throw if clicking in parallel', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await addMouseDataListeners(page);
+
+ await Promise.all([page.mouse.click(0, 5), page.mouse.click(6, 10)]);
+
+ const data = await page.evaluate(() => {
+ return (window as unknown as {clicks: ClickData[]}).clicks;
+ });
+ const commonAttrs = {
+ isTrusted: true,
+ detail: 1,
+ clientY: 5,
+ clientX: 0,
+ button: 0,
+ };
+ expect(data.splice(0, 3)).toMatchObject({
+ 0: {
+ type: 'mousedown',
+ buttons: 1,
+ ...commonAttrs,
+ },
+ 1: {
+ type: 'mouseup',
+ buttons: 0,
+ ...commonAttrs,
+ },
+ 2: {
+ type: 'click',
+ buttons: 0,
+ ...commonAttrs,
+ },
+ });
+ Object.assign(commonAttrs, {
+ clientX: 6,
+ clientY: 10,
+ });
+ expect(data).toMatchObject({
+ 0: {
+ type: 'mousedown',
+ buttons: 1,
+ ...commonAttrs,
+ },
+ 1: {
+ type: 'mouseup',
+ buttons: 0,
+ ...commonAttrs,
+ },
+ 2: {
+ type: 'click',
+ buttons: 0,
+ ...commonAttrs,
+ },
+ });
+ });
+
+ it('should reset properly', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.mouse.move(5, 5);
+ await Promise.all([
+ page.mouse.down({button: MouseButton.Left}),
+ page.mouse.down({button: MouseButton.Middle}),
+ page.mouse.down({button: MouseButton.Right}),
+ ]);
+
+ await addMouseDataListeners(page, {includeMove: true});
+ await page.mouse.reset();
+
+ const data = await page.evaluate(() => {
+ return (window as unknown as {clicks: ClickData[]}).clicks;
+ });
+ const commonAttrs = {
+ isTrusted: true,
+ clientY: 5,
+ clientX: 5,
+ };
+
+ expect(data.slice(0, 2)).toMatchObject([
+ {
+ ...commonAttrs,
+ button: 2,
+ buttons: 5,
+ detail: 1,
+ type: 'mouseup',
+ },
+ {
+ ...commonAttrs,
+ button: 2,
+ buttons: 5,
+ detail: 1,
+ type: 'auxclick',
+ },
+ ]);
+ // TODO(crbug/1485040): This should align with the firefox implementation.
+ if (isChrome) {
+ expect(data.slice(2)).toMatchObject([
+ {
+ ...commonAttrs,
+ button: 1,
+ buttons: 1,
+ detail: 0,
+ type: 'mouseup',
+ },
+ {
+ ...commonAttrs,
+ button: 0,
+ buttons: 0,
+ detail: 0,
+ type: 'mouseup',
+ },
+ ]);
+ return;
+ }
+ expect(data.slice(2)).toMatchObject([
+ {
+ ...commonAttrs,
+ button: 1,
+ buttons: 1,
+ detail: 1,
+ type: 'mouseup',
+ },
+ {
+ ...commonAttrs,
+ button: 1,
+ buttons: 1,
+ detail: 1,
+ type: 'auxclick',
+ },
+ {
+ ...commonAttrs,
+ button: 0,
+ buttons: 0,
+ detail: 1,
+ type: 'mouseup',
+ },
+ {
+ ...commonAttrs,
+ button: 0,
+ buttons: 0,
+ detail: 1,
+ type: 'click',
+ },
+ ]);
+ });
+
+ it('should evaluate before mouse event', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/input/button.html');
+
+ using button = await page.waitForSelector('button');
+
+ const point = await button!.clickablePoint();
+
+ const result = page.evaluate(() => {
+ return new Promise(resolve => {
+ document
+ .querySelector('button')
+ ?.addEventListener('click', resolve, {once: true});
+ });
+ });
+ await page.mouse.click(point?.x, point?.y);
+ await result;
+ });
+});
diff --git a/remote/test/puppeteer/test/src/navigation.spec.ts b/remote/test/puppeteer/test/src/navigation.spec.ts
new file mode 100644
index 0000000000..1f3a51f58a
--- /dev/null
+++ b/remote/test/puppeteer/test/src/navigation.spec.ts
@@ -0,0 +1,918 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ServerResponse} from 'http';
+
+import expect from 'expect';
+import {type Frame, TimeoutError} from 'puppeteer';
+import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js';
+import type {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js';
+import {Deferred} from 'puppeteer-core/internal/util/Deferred.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame, isFavicon, waitEvent} from './utils.js';
+
+describe('navigation', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.goto', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should work with anchor navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ await page.goto(server.EMPTY_PAGE + '#foo');
+ expect(page.url()).toBe(server.EMPTY_PAGE + '#foo');
+ await page.goto(server.EMPTY_PAGE + '#bar');
+ expect(page.url()).toBe(server.EMPTY_PAGE + '#bar');
+ });
+ it('should work with redirects', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/redirect/1.html', '/redirect/2.html');
+ server.setRedirect('/redirect/2.html', '/empty.html');
+ await page.goto(server.PREFIX + '/redirect/1.html');
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should navigate to about:blank', async () => {
+ const {page} = await getTestState();
+
+ const response = await page.goto('about:blank');
+ expect(response).toBe(null);
+ });
+ it('should return response when page changes its URL after load', async () => {
+ const {page, server} = await getTestState();
+
+ const response = await page.goto(server.PREFIX + '/historyapi.html');
+ expect(response!.status()).toBe(200);
+ });
+ it('should work with subframes return 204', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/frames/frame.html', (_req, res) => {
+ res.statusCode = 204;
+ res.end();
+ });
+ let error!: Error;
+ await page
+ .goto(server.PREFIX + '/frames/one-frame.html')
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeUndefined();
+ });
+ it('should fail when server returns 204', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ server.setRoute('/empty.html', (_req, res) => {
+ res.statusCode = 204;
+ res.end();
+ });
+ let error!: Error;
+ await page.goto(server.EMPTY_PAGE).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).not.toBe(null);
+ if (isChrome) {
+ expect(error.message).toContain('net::ERR_ABORTED');
+ } else {
+ expect(error.message).toContain('NS_BINDING_ABORTED');
+ }
+ });
+ it('should navigate to empty page with domcontentloaded', async () => {
+ const {page, server} = await getTestState();
+
+ const response = await page.goto(server.EMPTY_PAGE, {
+ waitUntil: 'domcontentloaded',
+ });
+ expect(response!.status()).toBe(200);
+ });
+ it('should work when page calls history API in beforeunload', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ window.addEventListener(
+ 'beforeunload',
+ () => {
+ return history.replaceState(null, 'initial', window.location.href);
+ },
+ false
+ );
+ });
+ const response = await page.goto(server.PREFIX + '/grid.html');
+ expect(response!.status()).toBe(200);
+ });
+ it('should navigate to empty page with networkidle0', async () => {
+ const {page, server} = await getTestState();
+
+ const response = await page.goto(server.EMPTY_PAGE, {
+ waitUntil: 'networkidle0',
+ });
+ expect(response!.status()).toBe(200);
+ });
+ it('should navigate to page with iframe and networkidle0', async () => {
+ const {page, server} = await getTestState();
+
+ const response = await page.goto(
+ server.PREFIX + '/frames/one-frame.html',
+ {
+ waitUntil: 'networkidle0',
+ }
+ );
+ expect(response!.status()).toBe(200);
+ });
+ it('should navigate to empty page with networkidle2', async () => {
+ const {page, server} = await getTestState();
+
+ const response = await page.goto(server.EMPTY_PAGE, {
+ waitUntil: 'networkidle2',
+ });
+ expect(response!.status()).toBe(200);
+ });
+ it('should fail when navigating to bad url', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.goto('asdfasdf').catch(error_ => {
+ return (error = error_);
+ });
+
+ expect(error.message).atLeastOneToContain([
+ 'Cannot navigate to invalid URL', // Firefox WebDriver BiDi.
+ 'invalid argument', // Others.
+ ]);
+ });
+
+ const EXPECTED_SSL_CERT_MESSAGE_REGEX =
+ /net::ERR_CERT_INVALID|net::ERR_CERT_AUTHORITY_INVALID/;
+
+ it('should fail when navigating to bad SSL', async () => {
+ const {page, httpsServer, isChrome} = await getTestState();
+
+ // Make sure that network events do not emit 'undefined'.
+ // @see https://crbug.com/750469
+ const requests: string[] = [];
+ page.on('request', () => {
+ return requests.push('request');
+ });
+ page.on('requestfinished', () => {
+ return requests.push('requestfinished');
+ });
+ page.on('requestfailed', () => {
+ return requests.push('requestfailed');
+ });
+
+ let error!: Error;
+ await page.goto(httpsServer.EMPTY_PAGE).catch(error_ => {
+ return (error = error_);
+ });
+ if (isChrome) {
+ expect(error.message).toMatch(EXPECTED_SSL_CERT_MESSAGE_REGEX);
+ } else {
+ expect(error.message).toContain('SSL_ERROR_UNKNOWN');
+ }
+
+ expect(requests).toHaveLength(2);
+ expect(requests[0]).toBe('request');
+ expect(requests[1]).toBe('requestfailed');
+ });
+ it('should fail when navigating to bad SSL after redirects', async () => {
+ const {page, server, httpsServer, isChrome} = await getTestState();
+
+ server.setRedirect('/redirect/1.html', '/redirect/2.html');
+ server.setRedirect('/redirect/2.html', '/empty.html');
+ let error!: Error;
+ await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(error_ => {
+ return (error = error_);
+ });
+ if (isChrome) {
+ expect(error.message).toMatch(EXPECTED_SSL_CERT_MESSAGE_REGEX);
+ } else {
+ expect(error.message).atLeastOneToContain([
+ 'MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT', // Firefox WebDriver BiDi.
+ 'SSL_ERROR_UNKNOWN ', // Others.
+ ]);
+ }
+ });
+ it('should fail when main resources failed to load', async () => {
+ const {page, isChrome} = await getTestState();
+
+ let error!: Error;
+ await page
+ .goto('http://localhost:44123/non-existing-url')
+ .catch(error_ => {
+ return (error = error_);
+ });
+ if (isChrome) {
+ expect(error.message).toContain('net::ERR_CONNECTION_REFUSED');
+ } else {
+ expect(error.message).toContain('NS_ERROR_CONNECTION_REFUSED');
+ }
+ });
+ it('should fail when exceeding maximum navigation timeout', async () => {
+ const {page, server} = await getTestState();
+
+ // Hang for request to the empty.html
+ server.setRoute('/empty.html', () => {});
+ let error!: Error;
+ await page
+ .goto(server.PREFIX + '/empty.html', {timeout: 1})
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should fail when exceeding default maximum navigation timeout', async () => {
+ const {page, server} = await getTestState();
+
+ // Hang for request to the empty.html
+ server.setRoute('/empty.html', () => {});
+ let error!: Error;
+ page.setDefaultNavigationTimeout(1);
+ await page.goto(server.PREFIX + '/empty.html').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should fail when exceeding default maximum timeout', async () => {
+ const {page, server} = await getTestState();
+
+ // Hang for request to the empty.html
+ server.setRoute('/empty.html', () => {});
+ let error!: Error;
+ page.setDefaultTimeout(1);
+ await page.goto(server.PREFIX + '/empty.html').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should prioritize default navigation timeout over default timeout', async () => {
+ const {page, server} = await getTestState();
+
+ // Hang for request to the empty.html
+ server.setRoute('/empty.html', () => {});
+ let error!: Error;
+ page.setDefaultTimeout(0);
+ page.setDefaultNavigationTimeout(1);
+ await page.goto(server.PREFIX + '/empty.html').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should disable timeout when its set to 0', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ let loaded = false;
+ page.once('load', () => {
+ loaded = true;
+ });
+ await page
+ .goto(server.PREFIX + '/grid.html', {timeout: 0, waitUntil: ['load']})
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeUndefined();
+ expect(loaded).toBe(true);
+ });
+ it('should work when navigating to valid url', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.ok()).toBe(true);
+ });
+ it('should work when navigating to data url', async () => {
+ const {page} = await getTestState();
+
+ const response = (await page.goto('data:text/html,hello'))!;
+ expect(response.ok()).toBe(true);
+ });
+ it('should work when navigating to 404', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.PREFIX + '/not-found'))!;
+ expect(response.ok()).toBe(false);
+ expect(response.status()).toBe(404);
+ });
+ it('should not throw an error for a 404 response with an empty body', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/404-error', (_, res) => {
+ res.statusCode = 404;
+ res.end();
+ });
+
+ const response = (await page.goto(server.PREFIX + '/404-error'))!;
+ expect(response.ok()).toBe(false);
+ expect(response.status()).toBe(404);
+ });
+ it('should not throw an error for a 500 response with an empty body', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/500-error', (_, res) => {
+ res.statusCode = 500;
+ res.end();
+ });
+
+ const response = (await page.goto(server.PREFIX + '/500-error'))!;
+ expect(response.ok()).toBe(false);
+ expect(response.status()).toBe(500);
+ });
+ it('should return last response in redirect chain', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/redirect/1.html', '/redirect/2.html');
+ server.setRedirect('/redirect/2.html', '/redirect/3.html');
+ server.setRedirect('/redirect/3.html', server.EMPTY_PAGE);
+ const response = (await page.goto(server.PREFIX + '/redirect/1.html'))!;
+ expect(response.ok()).toBe(true);
+ expect(response.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should wait for network idle to succeed navigation', async () => {
+ const {page, server} = await getTestState();
+
+ let responses: ServerResponse[] = [];
+ // Hold on to a bunch of requests without answering.
+ server.setRoute('/fetch-request-a.js', (_req, res) => {
+ return responses.push(res);
+ });
+ server.setRoute('/fetch-request-b.js', (_req, res) => {
+ return responses.push(res);
+ });
+ server.setRoute('/fetch-request-c.js', (_req, res) => {
+ return responses.push(res);
+ });
+ server.setRoute('/fetch-request-d.js', (_req, res) => {
+ return responses.push(res);
+ });
+ const initialFetchResourcesRequested = Promise.all([
+ server.waitForRequest('/fetch-request-a.js'),
+ server.waitForRequest('/fetch-request-b.js'),
+ server.waitForRequest('/fetch-request-c.js'),
+ ]).catch(() => {
+ // Ignore Error that arise from test server during hooks
+ });
+ const secondFetchResourceRequested = server
+ .waitForRequest('/fetch-request-d.js')
+ .catch(() => {
+ // Ignore Error that arise from test server during hooks
+ });
+
+ // Track when the navigation gets completed.
+ let navigationFinished = false;
+ let navigationError: Error | undefined;
+ // Navigate to a page which loads immediately and then does a bunch of
+ // requests via javascript's fetch method.
+ const navigationPromise = page
+ .goto(server.PREFIX + '/networkidle.html', {
+ waitUntil: 'networkidle0',
+ })
+ .then(response => {
+ navigationFinished = true;
+ return response;
+ })
+ .catch(error => {
+ navigationError = error;
+ return null;
+ });
+
+ let afterNavigationError: Error | undefined;
+ const afterNavigationPromise = (async () => {
+ // Wait for the page's 'load' event.
+ await waitEvent(page, 'load');
+ expect(navigationFinished).toBe(false);
+
+ // Wait for the initial three resources to be requested.
+ await initialFetchResourcesRequested;
+
+ // Expect navigation still to be not finished.
+ expect(navigationFinished).toBe(false);
+
+ // Respond to initial requests.
+ for (const response of responses) {
+ response.statusCode = 404;
+ response.end(`File not found`);
+ }
+
+ // Reset responses array
+ responses = [];
+
+ // Wait for the second round to be requested.
+ await secondFetchResourceRequested;
+ // Expect navigation still to be not finished.
+ expect(navigationFinished).toBe(false);
+
+ // Respond to requests.
+ for (const response of responses) {
+ response.statusCode = 404;
+ response.end(`File not found`);
+ }
+ })().catch(error => {
+ afterNavigationError = error;
+ });
+
+ await Promise.race([navigationPromise, afterNavigationPromise]);
+ if (navigationError) {
+ throw navigationError;
+ }
+ await Promise.all([navigationPromise, afterNavigationPromise]);
+ if (afterNavigationError) {
+ throw afterNavigationError;
+ }
+ // Expect navigation to succeed.
+ expect(navigationFinished).toBeTruthy();
+ expect((await navigationPromise)?.ok()).toBe(true);
+ });
+ it('should not leak listeners during navigation', async function () {
+ this.timeout(25_000);
+
+ const {page, server} = await getTestState();
+
+ let warning = null;
+ const warningHandler: NodeJS.WarningListener = w => {
+ return (warning = w);
+ };
+ process.on('warning', warningHandler);
+ for (let i = 0; i < 20; ++i) {
+ await page.goto(server.EMPTY_PAGE);
+ }
+ process.removeListener('warning', warningHandler);
+ expect(warning).toBe(null);
+ });
+ it('should not leak listeners during bad navigation', async function () {
+ this.timeout(25_000);
+
+ const {page} = await getTestState();
+
+ let warning = null;
+ const warningHandler: NodeJS.WarningListener = w => {
+ return (warning = w);
+ };
+ process.on('warning', warningHandler);
+ for (let i = 0; i < 20; ++i) {
+ await page.goto('asdf').catch(() => {
+ /* swallow navigation error */
+ });
+ }
+ process.removeListener('warning', warningHandler);
+ expect(warning).toBe(null);
+ });
+ it('should not leak listeners during navigation of 11 pages', async function () {
+ this.timeout(25_000);
+
+ const {context, server} = await getTestState();
+
+ let warning = null;
+ const warningHandler: NodeJS.WarningListener = w => {
+ return (warning = w);
+ };
+ process.on('warning', warningHandler);
+ await Promise.all(
+ [...Array(20)].map(async () => {
+ const page = await context.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.close();
+ })
+ );
+ process.removeListener('warning', warningHandler);
+ expect(warning).toBe(null);
+ });
+ it('should navigate to dataURL and fire dataURL requests', async () => {
+ const {page} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ const dataURL = 'data:text/html,<div>yo</div>';
+ const response = (await page.goto(dataURL))!;
+ expect(response.status()).toBe(200);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(dataURL);
+ });
+ it('should navigate to URL with hash and fire requests without hash', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!;
+ expect(response.status()).toBe(200);
+ expect(response.url()).toBe(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should work with self requesting page', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.PREFIX + '/self-request.html'))!;
+ expect(response.status()).toBe(200);
+ expect(response.url()).toContain('self-request.html');
+ });
+ it('should fail when navigating and show the url at the error message', async () => {
+ const {page, httpsServer} = await getTestState();
+
+ const url = httpsServer.PREFIX + '/redirect/1.html';
+ let error!: Error;
+ try {
+ await page.goto(url);
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toContain(url);
+ });
+ it('should send referer', async () => {
+ const {page, server} = await getTestState();
+
+ const requests = Promise.all([
+ server.waitForRequest('/grid.html'),
+ server.waitForRequest('/digits/1.png'),
+ page.goto(server.PREFIX + '/grid.html', {
+ referer: 'http://google.com/',
+ }),
+ ]).catch(() => {
+ return [];
+ });
+
+ const [request1, request2] = await requests;
+ expect(request1.headers['referer']).toBe('http://google.com/');
+ // Make sure subresources do not inherit referer.
+ expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html');
+ });
+
+ it('should send referer policy', async () => {
+ const {page, server} = await getTestState();
+
+ const [request1, request2] = await Promise.all([
+ server.waitForRequest('/grid.html'),
+ server.waitForRequest('/digits/1.png'),
+ page.goto(server.PREFIX + '/grid.html', {
+ referrerPolicy: 'no-referer',
+ }),
+ ]).catch(() => {
+ return [];
+ });
+ expect(request1.headers['referer']).toBeUndefined();
+ expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html');
+ });
+ });
+
+ describe('Page.waitForNavigation', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForNavigation(),
+ page.evaluate((url: string) => {
+ return (window.location.href = url);
+ }, server.PREFIX + '/grid.html'),
+ ]);
+ expect(response!.ok()).toBe(true);
+ expect(response!.url()).toContain('grid.html');
+ });
+ it('should work with both domcontentloaded and load', async () => {
+ const {page, server} = await getTestState();
+
+ let response!: ServerResponse;
+ server.setRoute('/one-style.css', (_req, res) => {
+ return (response = res);
+ });
+ let error: Error | undefined;
+ let bothFired = false;
+ const navigationPromise = page
+ .goto(server.PREFIX + '/one-style.html')
+ .catch(_error => {
+ return (error = _error);
+ });
+ const domContentLoadedPromise = page
+ .waitForNavigation({
+ waitUntil: 'domcontentloaded',
+ })
+ .catch(_error => {
+ return (error = _error);
+ });
+
+ const loadFiredPromise = page
+ .waitForNavigation({
+ waitUntil: 'load',
+ })
+ .then(() => {
+ return (bothFired = true);
+ })
+ .catch(_error => {
+ return (error = _error);
+ });
+
+ await server.waitForRequest('/one-style.css').catch(() => {});
+ await domContentLoadedPromise;
+ expect(bothFired).toBe(false);
+ response.end();
+ await loadFiredPromise;
+ await navigationPromise;
+ expect(error).toBeUndefined();
+ });
+ it('should work with clicking on anchor links', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(`<a href='#foobar'>foobar</a>`);
+ const [response] = await Promise.all([
+ page.waitForNavigation(),
+ page.click('a'),
+ ]);
+ expect(response).toBe(null);
+ expect(page.url()).toBe(server.EMPTY_PAGE + '#foobar');
+ });
+ it('should work with history.pushState()', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(`
+ <a onclick='javascript:pushState()'>SPA</a>
+ <script>
+ function pushState() { history.pushState({}, '', 'wow.html') }
+ </script>
+ `);
+ const [response] = await Promise.all([
+ page.waitForNavigation(),
+ page.click('a'),
+ ]);
+ expect(response).toBe(null);
+ expect(page.url()).toBe(server.PREFIX + '/wow.html');
+ });
+ it('should work with history.replaceState()', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(`
+ <a onclick='javascript:replaceState()'>SPA</a>
+ <script>
+ function replaceState() { history.replaceState({}, '', '/replaced.html') }
+ </script>
+ `);
+ const [response] = await Promise.all([
+ page.waitForNavigation(),
+ page.click('a'),
+ ]);
+ expect(response).toBe(null);
+ expect(page.url()).toBe(server.PREFIX + '/replaced.html');
+ });
+ it('should work with DOM history.back()/history.forward()', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(`
+ <a id=back onclick='javascript:goBack()'>back</a>
+ <a id=forward onclick='javascript:goForward()'>forward</a>
+ <script>
+ function goBack() { history.back(); }
+ function goForward() { history.forward(); }
+ history.pushState({}, '', '/first.html');
+ history.pushState({}, '', '/second.html');
+ </script>
+ `);
+ expect(page.url()).toBe(server.PREFIX + '/second.html');
+ const [backResponse] = await Promise.all([
+ page.waitForNavigation(),
+ page.click('a#back'),
+ ]);
+ expect(backResponse).toBe(null);
+ expect(page.url()).toBe(server.PREFIX + '/first.html');
+ const [forwardResponse] = await Promise.all([
+ page.waitForNavigation(),
+ page.click('a#forward'),
+ ]);
+ expect(forwardResponse).toBe(null);
+ expect(page.url()).toBe(server.PREFIX + '/second.html');
+ });
+ it('should work when subframe issues window.stop()', async function () {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/frames/style.css', () => {});
+ let frame: Frame | undefined;
+ const eventPromises = Deferred.race([
+ Promise.all([
+ waitEvent(page, 'frameattached').then(_frame => {
+ return (frame = _frame);
+ }),
+ waitEvent(page, 'framenavigated', f => {
+ return f === frame;
+ }),
+ ]),
+ Deferred.create({
+ message: `should work when subframe issues window.stop()`,
+ timeout: this.timeout() - 1000,
+ }),
+ ]);
+ const navigationPromise = page.goto(
+ server.PREFIX + '/frames/one-frame.html'
+ );
+ try {
+ await eventPromises;
+ } catch (error) {
+ navigationPromise.catch(() => {});
+ throw error;
+ }
+ await Promise.all([
+ frame!.evaluate(() => {
+ return window.stop();
+ }),
+ navigationPromise,
+ ]);
+ });
+ });
+
+ describe('Page.goBack', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.goto(server.PREFIX + '/grid.html');
+
+ let response = (await page.goBack())!;
+ expect(response.ok()).toBe(true);
+ expect(response.url()).toContain(server.EMPTY_PAGE);
+
+ response = (await page.goForward())!;
+ expect(response.ok()).toBe(true);
+ expect(response.url()).toContain('/grid.html');
+
+ response = (await page.goForward())!;
+ expect(response).toBe(null);
+ });
+ it('should work with HistoryAPI', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ history.pushState({}, '', '/first.html');
+ history.pushState({}, '', '/second.html');
+ });
+ expect(page.url()).toBe(server.PREFIX + '/second.html');
+
+ await page.goBack();
+ expect(page.url()).toBe(server.PREFIX + '/first.html');
+ await page.goBack();
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ await page.goForward();
+ expect(page.url()).toBe(server.PREFIX + '/first.html');
+ });
+ });
+
+ describe('Frame.goto', function () {
+ it('should navigate subframes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame.html');
+ expect(page.frames()[0]!.url()).toContain('/frames/one-frame.html');
+ expect(page.frames()[1]!.url()).toContain('/frames/frame.html');
+
+ const response = (await page.frames()[1]!.goto(server.EMPTY_PAGE))!;
+ expect(response.ok()).toBe(true);
+ expect(response.frame()).toBe(page.frames()[1]);
+ });
+ it('should reject when frame detaches', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame.html');
+
+ server.setRoute('/empty.html', () => {});
+ const navigationPromise = page
+ .frames()[1]!
+ .goto(server.EMPTY_PAGE)
+ .catch(error_ => {
+ return error_;
+ });
+ await server.waitForRequest('/empty.html').catch(() => {});
+
+ await page.$eval('iframe', frame => {
+ return frame.remove();
+ });
+ const error = await navigationPromise;
+ expect(error.message).atLeastOneToContain([
+ 'Navigating frame was detached',
+ 'Frame detached',
+ 'Error: NS_BINDING_ABORTED',
+ 'net::ERR_ABORTED',
+ ]);
+ });
+ it('should return matching responses', async () => {
+ const {page, server} = await getTestState();
+
+ // Disable cache: otherwise, the browser will cache similar requests.
+ await page.setCacheEnabled(false);
+ await page.goto(server.EMPTY_PAGE);
+ // Attach three frames.
+ const frames = await Promise.all([
+ attachFrame(page, 'frame1', server.EMPTY_PAGE),
+ attachFrame(page, 'frame2', server.EMPTY_PAGE),
+ attachFrame(page, 'frame3', server.EMPTY_PAGE),
+ ]);
+ // Navigate all frames to the same URL.
+ const serverResponses: ServerResponse[] = [];
+ server.setRoute('/one-style.html', (_req, res) => {
+ return serverResponses.push(res);
+ });
+ const navigations: Array<Promise<HTTPResponse | null>> = [];
+ for (let i = 0; i < 3; ++i) {
+ navigations.push(frames[i]!.goto(server.PREFIX + '/one-style.html'));
+ await server.waitForRequest('/one-style.html');
+ }
+ // Respond from server out-of-order.
+ const serverResponseTexts = ['AAA', 'BBB', 'CCC'];
+ try {
+ for (const i of [1, 2, 0]) {
+ const response = await getResponse(i);
+ expect(response.frame()).toBe(frames[i]);
+ expect(await response.text()).toBe(serverResponseTexts[i]);
+ }
+ } catch (error) {
+ await Promise.all([getResponse(0), getResponse(1), getResponse(2)]);
+ throw error;
+ }
+
+ async function getResponse(index: number) {
+ serverResponses[index]!.end(serverResponseTexts[index]);
+ return (await navigations[index])!;
+ }
+ });
+ });
+
+ describe('Frame.waitForNavigation', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame.html');
+ const frame = page.frames()[1]!;
+ const [response] = await Promise.all([
+ frame.waitForNavigation(),
+ frame.evaluate((url: string) => {
+ return (window.location.href = url);
+ }, server.PREFIX + '/grid.html'),
+ ]);
+ expect(response!.ok()).toBe(true);
+ expect(response!.url()).toContain('grid.html');
+ expect(response!.frame()).toBe(frame);
+ expect(page.url()).toContain('/frames/one-frame.html');
+ });
+ it('should fail when frame detaches', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame.html');
+ const frame = page.frames()[1]!;
+
+ server.setRoute('/empty.html', () => {});
+ let error!: Error;
+ const navigationPromise = frame.waitForNavigation().catch(error_ => {
+ return (error = error_);
+ });
+ await Promise.all([
+ server.waitForRequest('/empty.html'),
+ frame.evaluate(() => {
+ return ((window as any).location = '/empty.html');
+ }),
+ ]);
+ await page.$eval('iframe', frame => {
+ return frame.remove();
+ });
+ await navigationPromise;
+ expect(error.message).atLeastOneToContain([
+ 'Navigating frame was detached',
+ 'Frame detached',
+ ]);
+ });
+ });
+
+ describe('Page.reload', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ return ((globalThis as any)._foo = 10);
+ });
+ await page.reload();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any)._foo;
+ })
+ ).toBe(undefined);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/network.spec.ts b/remote/test/puppeteer/test/src/network.spec.ts
new file mode 100644
index 0000000000..c6f51a3412
--- /dev/null
+++ b/remote/test/puppeteer/test/src/network.spec.ts
@@ -0,0 +1,917 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import type {ServerResponse} from 'http';
+import path from 'path';
+
+import expect from 'expect';
+import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js';
+import type {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js';
+
+import {getTestState, launch, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame, isFavicon, waitEvent} from './utils.js';
+
+describe('network', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.Events.Request', function () {
+ it('should fire for navigation requests', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ });
+ it('should fire for iframes', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(requests).toHaveLength(2);
+ });
+ it('should fire for fetches', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ return fetch('/empty.html');
+ });
+ expect(requests).toHaveLength(2);
+ });
+ });
+ describe('Request.frame', function () {
+ it('should work for main frame navigation request', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.frame()).toBe(page.mainFrame());
+ });
+ it('should work for subframe navigation request', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.frame()).toBe(page.frames()[1]);
+ });
+ it('should work for fetch requests', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await page.evaluate(() => {
+ return fetch('/digits/1.png');
+ });
+ requests = requests.filter(request => {
+ return !request.url().includes('favicon');
+ });
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.frame()).toBe(page.mainFrame());
+ });
+ });
+
+ describe('Request.headers', function () {
+ it('should define Browser in user agent header', async () => {
+ const {page, server, isChrome} = await getTestState();
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ const userAgent = response.request().headers()['user-agent'];
+
+ if (isChrome) {
+ expect(userAgent).toContain('Chrome');
+ } else {
+ expect(userAgent).toContain('Firefox');
+ }
+ });
+ });
+
+ describe('Response.headers', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/empty.html', (_req, res) => {
+ res.setHeader('foo', 'bar');
+ res.end();
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.headers()['foo']).toBe('bar');
+ });
+ });
+
+ describe('Request.initiator', () => {
+ it('should return the initiator', async () => {
+ const {page, server} = await getTestState();
+
+ const initiators = new Map();
+ page.on('request', request => {
+ return initiators.set(
+ request.url().split('/').pop(),
+ request.initiator()
+ );
+ });
+ await page.goto(server.PREFIX + '/initiator.html');
+
+ expect(initiators.get('initiator.html').type).toBe('other');
+ expect(initiators.get('initiator.js').type).toBe('parser');
+ expect(initiators.get('initiator.js').url).toBe(
+ server.PREFIX + '/initiator.html'
+ );
+ expect(initiators.get('frame.html').type).toBe('parser');
+ expect(initiators.get('frame.html').url).toBe(
+ server.PREFIX + '/initiator.html'
+ );
+ expect(initiators.get('script.js').type).toBe('parser');
+ expect(initiators.get('script.js').url).toBe(
+ server.PREFIX + '/frames/frame.html'
+ );
+ expect(initiators.get('style.css').type).toBe('parser');
+ expect(initiators.get('style.css').url).toBe(
+ server.PREFIX + '/frames/frame.html'
+ );
+ expect(initiators.get('initiator.js').type).toBe('parser');
+ expect(initiators.get('injectedfile.js').type).toBe('script');
+ expect(initiators.get('injectedfile.js').stack.callFrames[0]!.url).toBe(
+ server.PREFIX + '/initiator.js'
+ );
+ expect(initiators.get('injectedstyle.css').type).toBe('script');
+ expect(initiators.get('injectedstyle.css').stack.callFrames[0]!.url).toBe(
+ server.PREFIX + '/initiator.js'
+ );
+ expect(initiators.get('initiator.js').url).toBe(
+ server.PREFIX + '/initiator.html'
+ );
+ });
+ });
+
+ describe('Response.fromCache', function () {
+ it('should return |false| for non-cached content', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.fromCache()).toBe(false);
+ });
+
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ const responses = new Map();
+ page.on('response', r => {
+ return (
+ !isFavicon(r.request()) && responses.set(r.url().split('/').pop(), r)
+ );
+ });
+
+ // Load and re-load to make sure it's cached.
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+ await page.reload();
+
+ expect(responses.size).toBe(2);
+ expect(responses.get('one-style.css').status()).toBe(200);
+ expect(responses.get('one-style.css').fromCache()).toBe(true);
+ expect(responses.get('one-style.html').status()).toBe(304);
+ expect(responses.get('one-style.html').fromCache()).toBe(false);
+ });
+ });
+
+ describe('Response.fromServiceWorker', function () {
+ it('should return |false| for non-service-worker content', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.fromServiceWorker()).toBe(false);
+ });
+
+ it('Response.fromServiceWorker', async () => {
+ const {page, server} = await getTestState();
+
+ const responses = new Map();
+ page.on('response', r => {
+ return !isFavicon(r) && responses.set(r.url().split('/').pop(), r);
+ });
+
+ // Load and re-load to make sure serviceworker is installed and running.
+ await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', {
+ waitUntil: 'networkidle2',
+ });
+ await page.evaluate(async () => {
+ return (globalThis as any).activationPromise;
+ });
+ await page.reload();
+
+ expect(responses.size).toBe(2);
+ expect(responses.get('sw.html').status()).toBe(200);
+ expect(responses.get('sw.html').fromServiceWorker()).toBe(true);
+ expect(responses.get('style.css').status()).toBe(200);
+ expect(responses.get('style.css').fromServiceWorker()).toBe(true);
+ });
+ });
+
+ describe('Request.postData', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ server.setRoute('/post', (_req, res) => {
+ return res.end();
+ });
+
+ const [request] = await Promise.all([
+ waitEvent<HTTPRequest>(page, 'request', r => {
+ return !isFavicon(r);
+ }),
+ page.evaluate(() => {
+ return fetch('./post', {
+ method: 'POST',
+ body: JSON.stringify({foo: 'bar'}),
+ });
+ }),
+ ]);
+
+ expect(request).toBeTruthy();
+ expect(request.postData()).toBe('{"foo":"bar"}');
+ });
+
+ it('should be |undefined| when there is no post data', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.request().postData()).toBe(undefined);
+ });
+
+ it('should work with blobs', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ server.setRoute('/post', (_req, res) => {
+ return res.end();
+ });
+
+ const [request] = await Promise.all([
+ waitEvent<HTTPRequest>(page, 'request', r => {
+ return !isFavicon(r);
+ }),
+ page.evaluate(() => {
+ return fetch('./post', {
+ method: 'POST',
+ body: new Blob([JSON.stringify({foo: 'bar'})], {
+ type: 'application/json',
+ }),
+ });
+ }),
+ ]);
+
+ expect(request).toBeTruthy();
+ expect(request.postData()).toBe(undefined);
+ expect(request.hasPostData()).toBe(true);
+ expect(await request.fetchPostData()).toBe('{"foo":"bar"}');
+ });
+ });
+
+ describe('Response.text', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.PREFIX + '/simple.json'))!;
+ const responseText = (await response.text()).trimEnd();
+ expect(responseText).toBe('{"foo": "bar"}');
+ });
+ it('should return uncompressed text', async () => {
+ const {page, server} = await getTestState();
+
+ server.enableGzip('/simple.json');
+ const response = (await page.goto(server.PREFIX + '/simple.json'))!;
+ expect(response.headers()['content-encoding']).toBe('gzip');
+ const responseText = (await response.text()).trimEnd();
+ expect(responseText).toBe('{"foo": "bar"}');
+ });
+ it('should throw when requesting body of redirected response', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/foo.html', '/empty.html');
+ const response = (await page.goto(server.PREFIX + '/foo.html'))!;
+ const redirectChain = response.request().redirectChain();
+ expect(redirectChain).toHaveLength(1);
+ const redirected = redirectChain[0]!.response()!;
+ expect(redirected.status()).toBe(302);
+ let error!: Error;
+ await redirected.text().catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain(
+ 'Response body is unavailable for redirect responses'
+ );
+ });
+ it('should wait until response completes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ // Setup server to trap request.
+ let serverResponse!: ServerResponse;
+ server.setRoute('/get', (_req, res) => {
+ serverResponse = res;
+ // In Firefox, |fetch| will be hanging until it receives |Content-Type| header
+ // from server.
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
+ res.write('hello ');
+ });
+ // Setup page to trap response.
+ let requestFinished = false;
+ page.on('requestfinished', r => {
+ return (requestFinished = requestFinished || r.url().includes('/get'));
+ });
+ // send request and wait for server response
+ const [pageResponse] = await Promise.all([
+ page.waitForResponse(r => {
+ return !isFavicon(r.request());
+ }),
+ page.evaluate(() => {
+ return fetch('./get', {method: 'GET'});
+ }),
+ server.waitForRequest('/get'),
+ ]);
+
+ expect(serverResponse).toBeTruthy();
+ expect(pageResponse).toBeTruthy();
+ expect(pageResponse.status()).toBe(200);
+ expect(requestFinished).toBe(false);
+
+ const responseText = pageResponse.text();
+ // Write part of the response and wait for it to be flushed.
+ await new Promise(x => {
+ return serverResponse.write('wor', x);
+ });
+ // Finish response.
+ await new Promise<void>(x => {
+ serverResponse.end('ld!', () => {
+ return x();
+ });
+ });
+ expect(await responseText).toBe('hello world!');
+ });
+ });
+
+ describe('Response.json', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.PREFIX + '/simple.json'))!;
+ expect(await response.json()).toEqual({foo: 'bar'});
+ });
+ });
+
+ describe('Response.buffer', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ const response = (await page.goto(server.PREFIX + '/pptr.png'))!;
+ const imageBuffer = fs.readFileSync(
+ path.join(__dirname, '../assets', 'pptr.png')
+ );
+ const responseBuffer = await response.buffer();
+ expect(responseBuffer.equals(imageBuffer)).toBe(true);
+ });
+ it('should work with compression', async () => {
+ const {page, server} = await getTestState();
+
+ server.enableGzip('/pptr.png');
+ const response = (await page.goto(server.PREFIX + '/pptr.png'))!;
+ const imageBuffer = fs.readFileSync(
+ path.join(__dirname, '../assets', 'pptr.png')
+ );
+ const responseBuffer = await response.buffer();
+ expect(responseBuffer.equals(imageBuffer)).toBe(true);
+ });
+ it('should throw if the response does not have a body', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/empty.html');
+
+ server.setRoute('/test.html', (_req, res) => {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Headers', 'x-ping');
+ res.end('Hello World');
+ });
+ const url = server.CROSS_PROCESS_PREFIX + '/test.html';
+ const responsePromise = waitEvent<HTTPResponse>(
+ page,
+ 'response',
+ response => {
+ // Get the preflight response.
+ return (
+ response.request().method() === 'OPTIONS' && response.url() === url
+ );
+ }
+ );
+
+ // Trigger a request with a preflight.
+ await page.evaluate(async src => {
+ const response = await fetch(src, {
+ method: 'POST',
+ headers: {'x-ping': 'pong'},
+ });
+ return response;
+ }, url);
+
+ const response = await responsePromise;
+ await expect(response.buffer()).rejects.toThrowError(
+ 'Could not load body for this request. This might happen if the request is a preflight request.'
+ );
+ });
+ });
+
+ describe('Response.statusText', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/cool', (_req, res) => {
+ res.writeHead(200, 'cool!');
+ res.end();
+ });
+ const response = (await page.goto(server.PREFIX + '/cool'))!;
+ expect(response.statusText()).toBe('cool!');
+ });
+
+ it('handles missing status text', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRoute('/nostatus', (_req, res) => {
+ res.writeHead(200, '');
+ res.end();
+ });
+ const response = (await page.goto(server.PREFIX + '/nostatus'))!;
+ expect(response.statusText()).toBe('');
+ });
+ });
+
+ describe('Response.timing', function () {
+ it('returns timing information', async () => {
+ const {page, server} = await getTestState();
+ const responses: HTTPResponse[] = [];
+ page.on('response', response => {
+ return responses.push(response);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(responses).toHaveLength(1);
+ expect(responses[0]!.timing()!.receiveHeadersEnd).toBeGreaterThan(0);
+ });
+ });
+
+ describe('Network Events', function () {
+ it('Page.Events.Request', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ return requests.push(request);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
+ expect(requests[0]!.resourceType()).toBe('document');
+ expect(requests[0]!.method()).toBe('GET');
+ expect(requests[0]!.response()).toBeTruthy();
+ expect(requests[0]!.frame() === page.mainFrame()).toBe(true);
+ expect(requests[0]!.frame()!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('Page.Events.RequestServedFromCache', async () => {
+ const {page, server} = await getTestState();
+
+ const cached: string[] = [];
+ page.on('requestservedfromcache', r => {
+ return cached.push(r.url().split('/').pop()!);
+ });
+
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+ expect(cached).toEqual([]);
+
+ await page.reload();
+ expect(cached).toEqual(['one-style.css']);
+ });
+ it('Page.Events.Response', async () => {
+ const {page, server} = await getTestState();
+
+ const responses: HTTPResponse[] = [];
+ page.on('response', response => {
+ return responses.push(response);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(responses).toHaveLength(1);
+ expect(responses[0]!.url()).toBe(server.EMPTY_PAGE);
+ expect(responses[0]!.status()).toBe(200);
+ expect(responses[0]!.ok()).toBe(true);
+ expect(responses[0]!.request()).toBeTruthy();
+ const remoteAddress = responses[0]!.remoteAddress();
+ // Either IPv6 or IPv4, depending on environment.
+ expect(
+ remoteAddress.ip!.includes('::1') || remoteAddress.ip === '127.0.0.1'
+ ).toBe(true);
+ expect(remoteAddress.port).toBe(server.PORT);
+ });
+
+ it('Page.Events.RequestFailed', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (request.url().endsWith('css')) {
+ void request.abort();
+ } else {
+ void request.continue();
+ }
+ });
+ const failedRequests: HTTPRequest[] = [];
+ page.on('requestfailed', request => {
+ return failedRequests.push(request);
+ });
+ await page.goto(server.PREFIX + '/one-style.html');
+ expect(failedRequests).toHaveLength(1);
+ expect(failedRequests[0]!.url()).toContain('one-style.css');
+ expect(failedRequests[0]!.response()).toBe(null);
+ expect(failedRequests[0]!.resourceType()).toBe('stylesheet');
+ if (isChrome) {
+ expect(failedRequests[0]!.failure()!.errorText).toBe('net::ERR_FAILED');
+ } else {
+ expect(failedRequests[0]!.failure()!.errorText).toBe(
+ 'NS_ERROR_FAILURE'
+ );
+ }
+ expect(failedRequests[0]!.frame()).toBeTruthy();
+ });
+ it('Page.Events.RequestFinished', async () => {
+ const {page, server} = await getTestState();
+
+ const requests: HTTPRequest[] = [];
+ page.on('requestfinished', request => {
+ return !isFavicon(request) && requests.push(request);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ const request = requests[0]!;
+ expect(request.url()).toBe(server.EMPTY_PAGE);
+ expect(request.response()).toBeTruthy();
+ expect(request.frame() === page.mainFrame()).toBe(true);
+ expect(request.frame()!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should fire events in proper order', async () => {
+ const {page, server} = await getTestState();
+
+ const events: string[] = [];
+ page.on('request', () => {
+ return events.push('request');
+ });
+ page.on('response', () => {
+ return events.push('response');
+ });
+ page.on('requestfinished', () => {
+ return events.push('requestfinished');
+ });
+ await page.goto(server.EMPTY_PAGE);
+ // Events can sneak in after the page has navigate
+ expect(events.slice(0, 3)).toEqual([
+ 'request',
+ 'response',
+ 'requestfinished',
+ ]);
+ });
+ it('should support redirects', async () => {
+ const {page, server} = await getTestState();
+
+ const events: string[] = [];
+ page.on('request', request => {
+ return events.push(`${request.method()} ${request.url()}`);
+ });
+ page.on('response', response => {
+ return events.push(`${response.status()} ${response.url()}`);
+ });
+ page.on('requestfinished', request => {
+ return events.push(`DONE ${request.url()}`);
+ });
+ page.on('requestfailed', request => {
+ return events.push(`FAIL ${request.url()}`);
+ });
+ server.setRedirect('/foo.html', '/empty.html');
+ const FOO_URL = server.PREFIX + '/foo.html';
+ const response = (await page.goto(FOO_URL))!;
+ expect(events).toEqual([
+ `GET ${FOO_URL}`,
+ `302 ${FOO_URL}`,
+ `DONE ${FOO_URL}`,
+ `GET ${server.EMPTY_PAGE}`,
+ `200 ${server.EMPTY_PAGE}`,
+ `DONE ${server.EMPTY_PAGE}`,
+ ]);
+
+ // Check redirect chain
+ const redirectChain = response.request().redirectChain();
+ expect(redirectChain).toHaveLength(1);
+ expect(redirectChain[0]!.url()).toContain('/foo.html');
+ expect(redirectChain[0]!.response()!.remoteAddress().port).toBe(
+ server.PORT
+ );
+ });
+ });
+
+ describe('Request.isNavigationRequest', () => {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ const requests = new Map();
+ page.on('request', request => {
+ return requests.set(request.url().split('/').pop(), request);
+ });
+ server.setRedirect('/rrredirect', '/frames/one-frame.html');
+ await page.goto(server.PREFIX + '/rrredirect');
+ expect(requests.get('rrredirect').isNavigationRequest()).toBe(true);
+ expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true);
+ expect(requests.get('frame.html').isNavigationRequest()).toBe(true);
+ expect(requests.get('script.js').isNavigationRequest()).toBe(false);
+ expect(requests.get('style.css').isNavigationRequest()).toBe(false);
+ });
+ it('should work with request interception', async () => {
+ const {page, server} = await getTestState();
+
+ const requests = new Map();
+ page.on('request', request => {
+ requests.set(request.url().split('/').pop(), request);
+ void request.continue();
+ });
+ await page.setRequestInterception(true);
+ server.setRedirect('/rrredirect', '/frames/one-frame.html');
+ await page.goto(server.PREFIX + '/rrredirect');
+ expect(requests.get('rrredirect').isNavigationRequest()).toBe(true);
+ expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true);
+ expect(requests.get('frame.html').isNavigationRequest()).toBe(true);
+ expect(requests.get('script.js').isNavigationRequest()).toBe(false);
+ expect(requests.get('style.css').isNavigationRequest()).toBe(false);
+ });
+ it('should work when navigating to image', async () => {
+ const {page, server} = await getTestState();
+
+ const [request] = await Promise.all([
+ waitEvent<HTTPRequest>(page, 'request'),
+ page.goto(server.PREFIX + '/pptr.png'),
+ ]);
+ expect(request.isNavigationRequest()).toBe(true);
+ });
+ });
+
+ describe('Page.setExtraHTTPHeaders', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({
+ foo: 'bar',
+ });
+ const [request] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(request.headers['foo']).toBe('bar');
+ });
+ it('should throw for non-string header values', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ try {
+ // @ts-expect-error purposeful bad input
+ await page.setExtraHTTPHeaders({foo: 1});
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toBe(
+ 'Expected value of header "foo" to be String, but "number" is found.'
+ );
+ });
+ });
+
+ describe('Page.authenticate', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ server.setAuth('/empty.html', 'user', 'pass');
+ let response;
+ try {
+ response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.status()).toBe(401);
+ } catch (error) {
+ // In headful, an error is thrown instead of 401.
+ if (
+ !(error as Error).message.startsWith(
+ 'net::ERR_INVALID_AUTH_CREDENTIALS'
+ )
+ ) {
+ throw error;
+ }
+ }
+ await page.authenticate({
+ username: 'user',
+ password: 'pass',
+ });
+ response = (await page.reload())!;
+ expect(response.status()).toBe(200);
+ });
+ it('should fail if wrong credentials', async () => {
+ const {page, server} = await getTestState();
+
+ // Use unique user/password since Chrome caches credentials per origin.
+ server.setAuth('/empty.html', 'user2', 'pass2');
+ await page.authenticate({
+ username: 'foo',
+ password: 'bar',
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.status()).toBe(401);
+ });
+ it('should allow disable authentication', async () => {
+ const {page, server} = await getTestState();
+
+ // Use unique user/password since Chrome caches credentials per origin.
+ server.setAuth('/empty.html', 'user3', 'pass3');
+ await page.authenticate({
+ username: 'user3',
+ password: 'pass3',
+ });
+ let response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.status()).toBe(200);
+ await page.authenticate({
+ username: '',
+ password: '',
+ });
+ // Navigate to a different origin to bust Chrome's credential caching.
+ try {
+ response = (await page.goto(
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ ))!;
+ expect(response.status()).toBe(401);
+ } catch (error) {
+ // In headful, an error is thrown instead of 401.
+ if (
+ !(error as Error).message.startsWith(
+ 'net::ERR_INVALID_AUTH_CREDENTIALS'
+ )
+ ) {
+ throw error;
+ }
+ }
+ });
+ it('should not disable caching', async () => {
+ const {page, server} = await getTestState();
+
+ // Use unique user/password since Chrome caches credentials per origin.
+ server.setAuth('/cached/one-style.css', 'user4', 'pass4');
+ server.setAuth('/cached/one-style.html', 'user4', 'pass4');
+ await page.authenticate({
+ username: 'user4',
+ password: 'pass4',
+ });
+
+ const responses = new Map();
+ page.on('response', r => {
+ return responses.set(r.url().split('/').pop(), r);
+ });
+
+ // Load and re-load to make sure it's cached.
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+ await page.reload();
+
+ expect(responses.get('one-style.css').status()).toBe(200);
+ expect(responses.get('one-style.css').fromCache()).toBe(true);
+ expect(responses.get('one-style.html').status()).toBe(304);
+ expect(responses.get('one-style.html').fromCache()).toBe(false);
+ });
+ });
+
+ describe('raw network headers', () => {
+ it('Same-origin set-cookie navigation', async () => {
+ const {page, server} = await getTestState();
+
+ const setCookieString = 'foo=bar';
+ server.setRoute('/empty.html', (_req, res) => {
+ res.setHeader('set-cookie', setCookieString);
+ res.end('hello world');
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.headers()['set-cookie']).toBe(setCookieString);
+ });
+
+ it('Same-origin set-cookie subresource', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+
+ const setCookieString = 'foo=bar';
+ server.setRoute('/foo', (_req, res) => {
+ res.setHeader('set-cookie', setCookieString);
+ res.end('hello world');
+ });
+
+ const [response] = await Promise.all([
+ waitEvent<HTTPResponse>(page, 'response', res => {
+ return !isFavicon(res);
+ }),
+ page.evaluate(() => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', '/foo');
+ xhr.send();
+ }),
+ ]);
+ expect(response.headers()['set-cookie']).toBe(setCookieString);
+ });
+
+ it('Cross-origin set-cookie', async () => {
+ const {page, httpsServer, close} = await launch({
+ ignoreHTTPSErrors: true,
+ });
+ try {
+ await page.goto(httpsServer.PREFIX + '/empty.html');
+
+ const setCookieString = 'hello=world';
+ httpsServer.setRoute('/setcookie.html', (_req, res) => {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('set-cookie', setCookieString);
+ res.end();
+ });
+ await page.goto(httpsServer.PREFIX + '/setcookie.html');
+ const url = httpsServer.CROSS_PROCESS_PREFIX + '/setcookie.html';
+ const [response] = await Promise.all([
+ waitEvent<HTTPResponse>(page, 'response', response => {
+ return response.url() === url;
+ }),
+ page.evaluate(src => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', src);
+ xhr.send();
+ }, url),
+ ]);
+ expect(response.headers()['set-cookie']).toBe(setCookieString);
+ } finally {
+ await close();
+ }
+ });
+ });
+
+ describe('Page.setBypassServiceWorker', () => {
+ it('bypass for network', async () => {
+ const {page, server} = await getTestState();
+
+ const responses = new Map();
+ page.on('response', r => {
+ return !isFavicon(r) && responses.set(r.url().split('/').pop(), r);
+ });
+
+ // Load and re-load to make sure serviceworker is installed and running.
+ await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', {
+ waitUntil: 'networkidle2',
+ });
+ await page.evaluate(async () => {
+ return (globalThis as any).activationPromise;
+ });
+ await page.reload({
+ waitUntil: 'networkidle2',
+ });
+
+ expect(page.isServiceWorkerBypassed()).toBe(false);
+ expect(responses.size).toBe(2);
+ expect(responses.get('sw.html').status()).toBe(200);
+ expect(responses.get('sw.html').fromServiceWorker()).toBe(true);
+ expect(responses.get('style.css').status()).toBe(200);
+ expect(responses.get('style.css').fromServiceWorker()).toBe(true);
+
+ await page.setBypassServiceWorker(true);
+ await page.reload({
+ waitUntil: 'networkidle2',
+ });
+
+ expect(page.isServiceWorkerBypassed()).toBe(true);
+ expect(responses.get('sw.html').status()).toBe(200);
+ expect(responses.get('sw.html').fromServiceWorker()).toBe(false);
+ expect(responses.get('style.css').status()).toBe(200);
+ expect(responses.get('style.css').fromServiceWorker()).toBe(false);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/oopif.spec.ts b/remote/test/puppeteer/test/src/oopif.spec.ts
new file mode 100644
index 0000000000..c024b76aba
--- /dev/null
+++ b/remote/test/puppeteer/test/src/oopif.spec.ts
@@ -0,0 +1,527 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import type {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js';
+import type {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js';
+import {CDPSessionEvent} from 'puppeteer-core/internal/api/CDPSession.js';
+import type {CdpTarget} from 'puppeteer-core/internal/cdp/Target.js';
+
+import {getTestState, launch} from './mocha-utils.js';
+import {attachFrame, detachFrame, navigateFrame} from './utils.js';
+
+describe('OOPIF', function () {
+ /* We use a special browser for this test as we need the --site-per-process flag */
+ let state: Awaited<ReturnType<typeof launch>>;
+
+ before(async () => {
+ const {defaultBrowserOptions} = await getTestState({skipLaunch: true});
+
+ state = await launch(
+ Object.assign({}, defaultBrowserOptions, {
+ args: (defaultBrowserOptions.args || []).concat([
+ '--site-per-process',
+ '--remote-debugging-port=21222',
+ '--host-rules=MAP * 127.0.0.1',
+ ]),
+ }),
+ {after: 'all'}
+ );
+ });
+
+ beforeEach(async () => {
+ state.context = await state.browser.createIncognitoBrowserContext();
+ state.page = await state.context.newPage();
+ });
+
+ afterEach(async () => {
+ await state.context.close();
+ });
+
+ after(async () => {
+ await state.close();
+ });
+
+ it('should treat OOP iframes and normal iframes the same', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return frame.url().endsWith('/empty.html');
+ });
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await attachFrame(
+ page,
+ 'frame2',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ await framePromise;
+ expect(page.mainFrame().childFrames()).toHaveLength(2);
+ });
+ it('should track navigations within OOP iframes', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ const frame = await framePromise;
+ expect(frame.url()).toContain('/empty.html');
+ await navigateFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/assets/frame.html'
+ );
+ expect(frame.url()).toContain('/assets/frame.html');
+ });
+ it('should support OOP iframes becoming normal iframes again', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+
+ const frame = await framePromise;
+ expect(frame.isOOPFrame()).toBe(false);
+ await navigateFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ expect(frame.isOOPFrame()).toBe(true);
+ await navigateFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(frame.isOOPFrame()).toBe(false);
+ expect(page.frames()).toHaveLength(2);
+ });
+ it('should support frames within OOP frames', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const frame1Promise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ const frame2Promise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 2;
+ });
+ await attachFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/frames/one-frame.html'
+ );
+
+ const [frame1, frame2] = await Promise.all([frame1Promise, frame2Promise]);
+
+ expect(
+ await frame1.evaluate(() => {
+ return document.location.href;
+ })
+ ).toMatch(/one-frame\.html$/);
+ expect(
+ await frame2.evaluate(() => {
+ return document.location.href;
+ })
+ ).toMatch(/frames\/frame\.html$/);
+ });
+ it('should support OOP iframes getting detached', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+
+ const frame = await framePromise;
+ expect(frame.isOOPFrame()).toBe(false);
+ await navigateFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ expect(frame.isOOPFrame()).toBe(true);
+ await detachFrame(page, 'frame1');
+ expect(page.frames()).toHaveLength(1);
+ });
+
+ it('should support wait for navigation for transitions from local to OOPIF', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+
+ const frame = await framePromise;
+ expect(frame.isOOPFrame()).toBe(false);
+ const nav = frame.waitForNavigation();
+ await navigateFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ await nav;
+ expect(frame.isOOPFrame()).toBe(true);
+ await detachFrame(page, 'frame1');
+ expect(page.frames()).toHaveLength(1);
+ });
+
+ it('should keep track of a frames OOP state', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ const frame = await framePromise;
+ expect(frame.url()).toContain('/empty.html');
+ await navigateFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(frame.url()).toBe(server.EMPTY_PAGE);
+ });
+
+ it('should support evaluating in oop iframes', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ const frame = await framePromise;
+ await frame.evaluate(() => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ _test = 'Test 123!';
+ });
+ const result = await frame.evaluate(() => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ return window._test;
+ });
+ expect(result).toBe('Test 123!');
+ });
+ it('should provide access to elements', async () => {
+ const {server, isHeadless, headless, page} = state;
+
+ if (!isHeadless || headless === 'new') {
+ // TODO: this test is partially blocked on crbug.com/1334119. Enable test once
+ // the upstream is fixed.
+ // TLDR: when we dispatch events to the frame the compositor might
+ // not be up-to-date yet resulting in a misclick (the iframe element
+ // becomes the event target instead of the content inside the iframe).
+ // The solution is to use InsertVisualCallback on the backend but that causes
+ // another issue that events cannot be dispatched to inactive tabs as the
+ // visual callback is never invoked.
+ // The old headless mode does not have this issue since it operates with
+ // special scheduling settings that keep even inactive tabs updating.
+ return;
+ }
+
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+
+ const frame = await framePromise;
+ await frame.evaluate(() => {
+ const button = document.createElement('button');
+ button.id = 'test-button';
+ button.innerText = 'click';
+ button.onclick = () => {
+ button.id = 'clicked';
+ };
+ document.body.appendChild(button);
+ });
+ await page.evaluate(() => {
+ document.body.style.border = '150px solid black';
+ document.body.style.margin = '250px';
+ document.body.style.padding = '50px';
+ });
+ await frame.waitForSelector('#test-button', {visible: true});
+ await frame.click('#test-button');
+ await frame.waitForSelector('#clicked');
+ });
+ it('should report oopif frames', async () => {
+ const {server, page, context} = state;
+
+ const frame = page.waitForFrame(frame => {
+ return frame.url().endsWith('/oopif.html');
+ });
+ await page.goto(server.PREFIX + '/dynamic-oopif.html');
+ await frame;
+ expect(oopifs(context)).toHaveLength(1);
+ expect(page.frames()).toHaveLength(2);
+ });
+
+ it('should wait for inner OOPIFs', async () => {
+ const {server, page, context} = state;
+ await page.goto(`http://mainframe:${server.PORT}/main-frame.html`);
+ const frame2 = await page.waitForFrame(frame => {
+ return frame.url().endsWith('inner-frame2.html');
+ });
+ expect(oopifs(context)).toHaveLength(2);
+ expect(
+ page.frames().filter(frame => {
+ return frame.isOOPFrame();
+ })
+ ).toHaveLength(2);
+ expect(
+ await frame2.evaluate(() => {
+ return document.querySelectorAll('button').length;
+ })
+ ).toStrictEqual(1);
+ });
+
+ it('should load oopif iframes with subresources and request interception', async () => {
+ const {server, page, context} = state;
+
+ const framePromise = page.waitForFrame(frame => {
+ return frame.url().endsWith('/oopif.html');
+ });
+ page.on('request', request => {
+ void request.continue();
+ });
+ await page.setRequestInterception(true);
+ const requestPromise = page.waitForRequest(request => {
+ return request.url().includes('requestFromOOPIF');
+ });
+ await page.goto(server.PREFIX + '/dynamic-oopif.html');
+ const frame = await framePromise;
+ const request = await requestPromise;
+ expect(oopifs(context)).toHaveLength(1);
+ expect(request.frame()).toBe(frame);
+ });
+
+ it('should support frames within OOP iframes', async () => {
+ const {server, page} = state;
+
+ const oopIframePromise = page.waitForFrame(frame => {
+ return frame.url().endsWith('/oopif.html');
+ });
+ await page.goto(server.PREFIX + '/dynamic-oopif.html');
+ const oopIframe = await oopIframePromise;
+ await attachFrame(
+ oopIframe,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+
+ const frame1 = oopIframe.childFrames()[0]!;
+ expect(frame1.url()).toMatch(/empty.html$/);
+ await navigateFrame(
+ oopIframe,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/oopif.html'
+ );
+ expect(frame1.url()).toMatch(/oopif.html$/);
+ await frame1.goto(
+ server.CROSS_PROCESS_PREFIX + '/oopif.html#navigate-within-document',
+ {waitUntil: 'load'}
+ );
+ expect(frame1.url()).toMatch(/oopif.html#navigate-within-document$/);
+ await detachFrame(oopIframe, 'frame1');
+ expect(oopIframe.childFrames()).toHaveLength(0);
+ });
+
+ it('clickablePoint, boundingBox, boxModel should work for elements inside OOPIFs', async () => {
+ const {server, page} = state;
+ await page.goto(server.EMPTY_PAGE);
+ const framePromise = page.waitForFrame(frame => {
+ return page.frames().indexOf(frame) === 1;
+ });
+ await attachFrame(
+ page,
+ 'frame1',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ const frame = await framePromise;
+ await page.evaluate(() => {
+ document.body.style.border = '50px solid black';
+ document.body.style.margin = '50px';
+ document.body.style.padding = '50px';
+ });
+ await frame.evaluate(() => {
+ const button = document.createElement('button');
+ button.id = 'test-button';
+ button.innerText = 'click';
+ document.body.appendChild(button);
+ });
+ using button = (await frame.waitForSelector('#test-button', {
+ visible: true,
+ }))!;
+ const result = await button.clickablePoint();
+ expect(result.x).toBeGreaterThan(150); // padding + margin + border left
+ expect(result.y).toBeGreaterThan(150); // padding + margin + border top
+ const resultBoxModel = (await button.boxModel())!;
+ for (const quad of [
+ resultBoxModel.content,
+ resultBoxModel.border,
+ resultBoxModel.margin,
+ resultBoxModel.padding,
+ ]) {
+ for (const part of quad) {
+ expect(part.x).toBeGreaterThan(150); // padding + margin + border left
+ expect(part.y).toBeGreaterThan(150); // padding + margin + border top
+ }
+ }
+ const resultBoundingBox = (await button.boundingBox())!;
+ expect(resultBoundingBox.x).toBeGreaterThan(150); // padding + margin + border left
+ expect(resultBoundingBox.y).toBeGreaterThan(150); // padding + margin + border top
+ });
+
+ it('should detect existing OOPIFs when Puppeteer connects to an existing page', async () => {
+ const {server, puppeteer, page, context} = state;
+
+ const frame = page.waitForFrame(frame => {
+ return frame.url().endsWith('/oopif.html');
+ });
+ await page.goto(server.PREFIX + '/dynamic-oopif.html');
+ await frame;
+ expect(oopifs(context)).toHaveLength(1);
+ expect(page.frames()).toHaveLength(2);
+
+ const browserURL = 'http://127.0.0.1:21222';
+ const browser1 = await puppeteer.connect({browserURL});
+ const target = await browser1.waitForTarget(target => {
+ return target.url().endsWith('dynamic-oopif.html');
+ });
+ await target.page();
+ await browser1.disconnect();
+ });
+
+ it('should support lazy OOP frames', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.PREFIX + '/lazy-oopif-frame.html');
+ await page.setViewport({width: 1000, height: 1000});
+
+ expect(
+ page.frames().map(frame => {
+ return frame._hasStartedLoading;
+ })
+ ).toEqual([true, true, false]);
+ });
+
+ describe('waitForFrame', () => {
+ it('should resolve immediately if the frame already exists', async () => {
+ const {server, page} = state;
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(
+ page,
+ 'frame2',
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+
+ await page.waitForFrame(frame => {
+ return frame.url().endsWith('/empty.html');
+ });
+ });
+ });
+
+ it('should report google.com frame', async () => {
+ const {server, page} = state;
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+ page.on('request', r => {
+ return r.respond({body: 'YO, GOOGLE.COM'});
+ });
+ await page.evaluate(() => {
+ const frame = document.createElement('iframe');
+ frame.setAttribute('src', 'https://google.com/');
+ document.body.appendChild(frame);
+ return new Promise(x => {
+ return (frame.onload = x);
+ });
+ });
+ await page.waitForSelector('iframe[src="https://google.com/"]');
+ const urls = page
+ .frames()
+ .map(frame => {
+ return frame.url();
+ })
+ .sort();
+ expect(urls).toEqual([server.EMPTY_PAGE, 'https://google.com/']);
+ });
+
+ it('should expose events within OOPIFs', async () => {
+ const {server, page} = state;
+
+ // Setup our session listeners to observe OOPIF activity.
+ const session = await page.target().createCDPSession();
+ const networkEvents: string[] = [];
+ const otherSessions: CDPSession[] = [];
+ await session.send('Target.setAutoAttach', {
+ autoAttach: true,
+ flatten: true,
+ waitForDebuggerOnStart: true,
+ });
+ session.on(CDPSessionEvent.SessionAttached, async session => {
+ otherSessions.push(session);
+
+ session.on('Network.requestWillBeSent', params => {
+ return networkEvents.push(params.request.url);
+ });
+ await session.send('Network.enable');
+ await session.send('Runtime.runIfWaitingForDebugger');
+ });
+
+ // Navigate to the empty page and add an OOPIF iframe with at least one request.
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(
+ (frameUrl: string) => {
+ const frame = document.createElement('iframe');
+ frame.setAttribute('src', frameUrl);
+ document.body.appendChild(frame);
+ return new Promise((x, y) => {
+ frame.onload = x;
+ frame.onerror = y;
+ });
+ },
+ server.PREFIX.replace('localhost', 'oopifdomain') + '/one-style.html'
+ );
+ await page.waitForSelector('iframe');
+
+ // Ensure we found the iframe session.
+ expect(otherSessions).toHaveLength(1);
+
+ // Resume the iframe and trigger another request.
+ const iframeSession = otherSessions[0]!;
+ await iframeSession.send('Runtime.evaluate', {
+ expression: `fetch('/fetch')`,
+ awaitPromise: true,
+ });
+
+ expect(networkEvents).toContain(`http://oopifdomain:${server.PORT}/fetch`);
+ });
+});
+
+function oopifs(context: BrowserContext) {
+ return context.targets().filter(target => {
+ return (target as CdpTarget)._getTargetInfo().type === 'iframe';
+ });
+}
diff --git a/remote/test/puppeteer/test/src/page.spec.ts b/remote/test/puppeteer/test/src/page.spec.ts
new file mode 100644
index 0000000000..79fc69ebbc
--- /dev/null
+++ b/remote/test/puppeteer/test/src/page.spec.ts
@@ -0,0 +1,2287 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'assert';
+import fs from 'fs';
+import type {ServerResponse} from 'http';
+import path from 'path';
+
+import expect from 'expect';
+import {KnownDevices, TimeoutError} from 'puppeteer';
+import {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js';
+import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js';
+import type {Metrics, Page} from 'puppeteer-core/internal/api/Page.js';
+import type {CdpPage} from 'puppeteer-core/internal/cdp/Page.js';
+import type {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js';
+import sinon from 'sinon';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {attachFrame, detachFrame, isFavicon, waitEvent} from './utils.js';
+
+describe('Page', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.close', function () {
+ it('should reject all promises when page is closed', async () => {
+ const {context} = await getTestState();
+
+ const newPage = await context.newPage();
+ let error!: Error;
+ await Promise.all([
+ newPage
+ .evaluate(() => {
+ return new Promise(() => {});
+ })
+ .catch(error_ => {
+ return (error = error_);
+ }),
+ newPage.close(),
+ ]);
+ expect(error.message).toContain('Protocol error');
+ });
+ it('should not be visible in browser.pages', async () => {
+ const {browser} = await getTestState();
+
+ const newPage = await browser.newPage();
+ expect(await browser.pages()).toContain(newPage);
+ await newPage.close();
+ expect(await browser.pages()).not.toContain(newPage);
+ });
+ it('should run beforeunload if asked for', async () => {
+ const {context, server, isChrome} = await getTestState();
+
+ const newPage = await context.newPage();
+ await newPage.goto(server.PREFIX + '/beforeunload.html');
+ // We have to interact with a page so that 'beforeunload' handlers
+ // fire.
+ await newPage.click('body');
+ const pageClosingPromise = newPage.close({runBeforeUnload: true});
+ const dialog = await waitEvent(newPage, 'dialog');
+ expect(dialog.type()).toBe('beforeunload');
+ expect(dialog.defaultValue()).toBe('');
+ if (isChrome) {
+ expect(dialog.message()).toBe('');
+ } else {
+ expect(dialog.message()).toBeTruthy();
+ }
+ await dialog.accept();
+ await pageClosingPromise;
+ });
+ it('should *not* run beforeunload by default', async () => {
+ const {context, server} = await getTestState();
+
+ const newPage = await context.newPage();
+ await newPage.goto(server.PREFIX + '/beforeunload.html');
+ // We have to interact with a page so that 'beforeunload' handlers
+ // fire.
+ await newPage.click('body');
+ await newPage.close();
+ });
+ it('should set the page close state', async () => {
+ const {context} = await getTestState();
+
+ const newPage = await context.newPage();
+ expect(newPage.isClosed()).toBe(false);
+ await newPage.close();
+ expect(newPage.isClosed()).toBe(true);
+ });
+ it('should terminate network waiters', async () => {
+ const {context, server} = await getTestState();
+
+ const newPage = await context.newPage();
+ const results = await Promise.all([
+ newPage.waitForRequest(server.EMPTY_PAGE).catch(error => {
+ return error;
+ }),
+ newPage.waitForResponse(server.EMPTY_PAGE).catch(error => {
+ return error;
+ }),
+ newPage.close(),
+ ]);
+ for (let i = 0; i < 2; i++) {
+ const message = results[i].message;
+ expect(message).atLeastOneToContain(['Target closed', 'Page closed!']);
+ expect(message).not.toContain('Timeout');
+ }
+ });
+ });
+
+ describe('Page.Events.Load', function () {
+ it('should fire when expected', async () => {
+ const {page} = await getTestState();
+
+ await Promise.all([waitEvent(page, 'load'), page.goto('about:blank')]);
+ });
+ });
+
+ describe('removing and adding event handlers', () => {
+ it('should correctly fire event handlers as they are added and then removed', async () => {
+ const {page, server} = await getTestState();
+
+ const handler = sinon.spy();
+ const onResponse = (response: {url: () => string}) => {
+ // Ignore default favicon requests.
+ if (!isFavicon(response)) {
+ handler();
+ }
+ };
+ page.on('response', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ expect(handler.callCount).toBe(1);
+ page.off('response', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ // Still one because we removed the handler.
+ expect(handler.callCount).toBe(1);
+ page.on('response', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ // Two now because we added the handler back.
+ expect(handler.callCount).toBe(2);
+ });
+
+ it('should correctly added and removed request events', async () => {
+ const {page, server} = await getTestState();
+
+ const handler = sinon.spy();
+ const onResponse = (response: {url: () => string}) => {
+ // Ignore default favicon requests.
+ if (!isFavicon(response)) {
+ handler();
+ }
+ };
+
+ page.on('request', onResponse);
+ page.on('request', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ expect(handler.callCount).toBe(2);
+ page.off('request', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ // Still one because we removed the handler.
+ expect(handler.callCount).toBe(3);
+ page.off('request', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ expect(handler.callCount).toBe(3);
+ page.on('request', onResponse);
+ await page.goto(server.EMPTY_PAGE);
+ // Two now because we added the handler back.
+ expect(handler.callCount).toBe(4);
+ });
+ });
+
+ describe('Page.Events.error', function () {
+ it('should throw when page crashes', async () => {
+ const {page, isChrome} = await getTestState();
+
+ let navigate: Promise<unknown>;
+ if (isChrome) {
+ navigate = page.goto('chrome://crash').catch(() => {});
+ } else {
+ navigate = page.goto('about:crashcontent').catch(() => {});
+ }
+ const [error] = await Promise.all([
+ waitEvent<Error>(page, 'error'),
+ navigate,
+ ]);
+ expect(error.message).toBe('Page crashed!');
+ });
+ });
+
+ describe('Page.Events.Popup', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const [popup] = await Promise.all([
+ waitEvent<Page>(page, 'popup'),
+ page.evaluate(() => {
+ return window.open('about:blank');
+ }),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ expect(
+ await popup.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(true);
+ });
+ it('should work with noopener', async () => {
+ const {page} = await getTestState();
+
+ const [popup] = await Promise.all([
+ waitEvent<Page>(page, 'popup'),
+ page.evaluate(() => {
+ return window.open('about:blank', undefined, 'noopener');
+ }),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ expect(
+ await popup.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ });
+ it('should work with clicking target=_blank and without rel=opener', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent('<a target=_blank href="/one-style.html">yo</a>');
+ const [popup] = await Promise.all([
+ waitEvent<Page>(page, 'popup'),
+ page.click('a'),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ expect(
+ await popup.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ });
+ it('should work with clicking target=_blank and with rel=opener', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(
+ '<a target=_blank rel=opener href="/one-style.html">yo</a>'
+ );
+ const [popup] = await Promise.all([
+ waitEvent<Page>(page, 'popup'),
+ page.click('a'),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ expect(
+ await popup.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(true);
+ });
+ it('should work with fake-clicking target=_blank and rel=noopener', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(
+ '<a target=_blank rel=noopener href="/one-style.html">yo</a>'
+ );
+ const [popup] = await Promise.all([
+ waitEvent<Page>(page, 'popup'),
+ page.$eval('a', a => {
+ return (a as HTMLAnchorElement).click();
+ }),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ expect(
+ await popup.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ });
+ it('should work with clicking target=_blank and rel=noopener', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent(
+ '<a target=_blank rel=noopener href="/one-style.html">yo</a>'
+ );
+ const [popup] = await Promise.all([
+ waitEvent<Page>(page, 'popup'),
+ page.click('a'),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ expect(
+ await popup.evaluate(() => {
+ return !!window.opener;
+ })
+ ).toBe(false);
+ });
+ });
+
+ describe('Page.setGeolocation', function () {
+ it('should work', async () => {
+ const {page, server, context} = await getTestState();
+
+ await context.overridePermissions(server.PREFIX, ['geolocation']);
+ await page.goto(server.EMPTY_PAGE);
+ await page.setGeolocation({longitude: 10, latitude: 10});
+ const geolocation = await page.evaluate(() => {
+ return new Promise(resolve => {
+ return navigator.geolocation.getCurrentPosition(position => {
+ resolve({
+ latitude: position.coords.latitude,
+ longitude: position.coords.longitude,
+ });
+ });
+ });
+ });
+ expect(geolocation).toEqual({
+ latitude: 10,
+ longitude: 10,
+ });
+ });
+ it('should throw when invalid longitude', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ try {
+ await page.setGeolocation({longitude: 200, latitude: 10});
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toContain('Invalid longitude "200"');
+ });
+ });
+
+ describe('Page.setOfflineMode', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setOfflineMode(true);
+ let error!: Error;
+ await page.goto(server.EMPTY_PAGE).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ await page.setOfflineMode(false);
+ const response = (await page.reload())!;
+ expect(response.status()).toBe(200);
+ });
+ it('should emulate navigator.onLine', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return window.navigator.onLine;
+ })
+ ).toBe(true);
+ await page.setOfflineMode(true);
+ expect(
+ await page.evaluate(() => {
+ return window.navigator.onLine;
+ })
+ ).toBe(false);
+ await page.setOfflineMode(false);
+ expect(
+ await page.evaluate(() => {
+ return window.navigator.onLine;
+ })
+ ).toBe(true);
+ });
+ });
+
+ describe('Page.Events.Console', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const [message] = await Promise.all([
+ waitEvent<ConsoleMessage>(page, 'console'),
+ page.evaluate(() => {
+ return console.log('hello', 5, {foo: 'bar'});
+ }),
+ ]);
+ expect(message.text()).toEqual('hello 5 JSHandle@object');
+ expect(message.type()).toEqual('log');
+ expect(message.args()).toHaveLength(3);
+ expect(message.location()).toEqual({
+ url: expect.any(String),
+ lineNumber: expect.any(Number),
+ columnNumber: expect.any(Number),
+ });
+
+ expect(await message.args()[0]!.jsonValue()).toEqual('hello');
+ expect(await message.args()[1]!.jsonValue()).toEqual(5);
+ expect(await message.args()[2]!.jsonValue()).toEqual({foo: 'bar'});
+ });
+ it('should work on script call right after navigation', async () => {
+ const {page} = await getTestState();
+
+ const [message] = await Promise.all([
+ waitEvent<ConsoleMessage>(page, 'console'),
+ page.goto(
+ // Firefox prints warn if <!DOCTYPE html> is not present
+ `data:text/html,<!DOCTYPE html><script>console.log('SOME_LOG_MESSAGE');</script>`
+ ),
+ ]);
+
+ expect(message.text()).toEqual('SOME_LOG_MESSAGE');
+ });
+ it('should work for different console API calls with logging functions', async () => {
+ const {page} = await getTestState();
+
+ const messages: ConsoleMessage[] = [];
+ page.on('console', msg => {
+ return messages.push(msg);
+ });
+ // All console events will be reported before `page.evaluate` is finished.
+ await page.evaluate(() => {
+ console.trace('calling console.trace');
+ console.dir('calling console.dir');
+ console.warn('calling console.warn');
+ console.error('calling console.error');
+ console.log(Promise.resolve('should not wait until resolved!'));
+ });
+ expect(
+ messages.map(msg => {
+ return msg.type();
+ })
+ ).toEqual(['trace', 'dir', 'warning', 'error', 'log']);
+ expect(
+ messages.map(msg => {
+ return msg.text();
+ })
+ ).toEqual([
+ 'calling console.trace',
+ 'calling console.dir',
+ 'calling console.warn',
+ 'calling console.error',
+ 'JSHandle@promise',
+ ]);
+ });
+ it('should work for different console API calls with timing functions', async () => {
+ const {page} = await getTestState();
+
+ const messages: any[] = [];
+ page.on('console', msg => {
+ return messages.push(msg);
+ });
+ // All console events will be reported before `page.evaluate` is finished.
+ await page.evaluate(() => {
+ // A pair of time/timeEnd generates only one Console API call.
+ console.time('calling console.time');
+ console.timeEnd('calling console.time');
+ });
+ expect(
+ messages.map(msg => {
+ return msg.type();
+ })
+ ).toEqual(['timeEnd']);
+ expect(messages[0]!.text()).toContain('calling console.time');
+ });
+ it('should not fail for window object', async () => {
+ const {page} = await getTestState();
+
+ const [message] = await Promise.all([
+ waitEvent<ConsoleMessage>(page, 'console'),
+ page.evaluate(() => {
+ return console.error(window);
+ }),
+ ]);
+ expect(message.text()).atLeastOneToContain([
+ 'JSHandle@object',
+ 'JSHandle@window',
+ ]);
+ });
+ it('should trigger correct Log', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.goto('about:blank');
+ const [message] = await Promise.all([
+ waitEvent(page, 'console'),
+ page.evaluate(async (url: string) => {
+ return await fetch(url).catch(() => {});
+ }, server.EMPTY_PAGE),
+ ]);
+ expect(message.text()).toContain('Access-Control-Allow-Origin');
+ if (isChrome) {
+ expect(message.type()).toEqual('error');
+ } else {
+ expect(message.type()).toEqual('warn');
+ }
+ });
+ it('should have location when fetch fails', async () => {
+ const {page, server} = await getTestState();
+
+ // The point of this test is to make sure that we report console messages from
+ // Log domain: https://vanilla.aslushnikov.com/?Log.entryAdded
+ await page.goto(server.EMPTY_PAGE);
+ const [message] = await Promise.all([
+ waitEvent(page, 'console'),
+ page.setContent(`<script>fetch('http://wat');</script>`),
+ ]);
+ expect(message.text()).toContain(`ERR_NAME_NOT_RESOLVED`);
+ expect(message.type()).toEqual('error');
+ expect(message.location()).toEqual({
+ url: 'http://wat/',
+ lineNumber: undefined,
+ });
+ });
+ it('should have location and stack trace for console API calls', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [message] = await Promise.all([
+ waitEvent(page, 'console'),
+ page.goto(server.PREFIX + '/consolelog.html'),
+ ]);
+ expect(message.text()).toBe('yellow');
+ expect(message.type()).toBe('log');
+ expect(message.location()).toEqual({
+ url: server.PREFIX + '/consolelog.html',
+ lineNumber: 8,
+ columnNumber: isChrome ? 16 : 8, // console.|log vs |console.log
+ });
+ expect(message.stackTrace()).toEqual([
+ {
+ url: server.PREFIX + '/consolelog.html',
+ lineNumber: 8,
+ columnNumber: isChrome ? 16 : 8, // console.|log vs |console.log
+ },
+ {
+ url: server.PREFIX + '/consolelog.html',
+ lineNumber: 11,
+ columnNumber: 8,
+ },
+ {
+ url: server.PREFIX + '/consolelog.html',
+ lineNumber: 13,
+ columnNumber: 6,
+ },
+ ]);
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/3865
+ it('should not throw when there are console messages in detached iframes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(async () => {
+ // 1. Create a popup that Puppeteer is not connected to.
+ const win = window.open(
+ window.location.href,
+ 'Title',
+ 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=780,height=200,top=0,left=0'
+ )!;
+ await new Promise(x => {
+ return (win.onload = x);
+ });
+ // 2. In this popup, create an iframe that console.logs a message.
+ win.document.body.innerHTML = `<iframe src='/consolelog.html'></iframe>`;
+ const frame = win.document.querySelector('iframe')!;
+ await new Promise(x => {
+ return (frame.onload = x);
+ });
+ // 3. After that, remove the iframe.
+ frame.remove();
+ });
+ const popupTarget = page
+ .browserContext()
+ .targets()
+ .find(target => {
+ return target !== page.target();
+ })!;
+ // 4. Connect to the popup and make sure it doesn't throw.
+ await popupTarget.page();
+ });
+ });
+
+ describe('Page.Events.DOMContentLoaded', function () {
+ it('should fire when expected', async () => {
+ const {page} = await getTestState();
+
+ const navigate = page.goto('about:blank');
+ await Promise.all([waitEvent(page, 'domcontentloaded'), navigate]);
+ });
+ });
+
+ describe('Page.metrics', function () {
+ it('should get metrics from a page', async () => {
+ const {page} = await getTestState();
+
+ await page.goto('about:blank');
+ const metrics = await page.metrics();
+ checkMetrics(metrics);
+ });
+ it('metrics event fired on console.timeStamp', async () => {
+ const {page} = await getTestState();
+
+ const metricsPromise = waitEvent<{metrics: Metrics; title: string}>(
+ page,
+ 'metrics'
+ );
+
+ await page.evaluate(() => {
+ return console.timeStamp('test42');
+ });
+ const metrics = await metricsPromise;
+ expect(metrics.title).toBe('test42');
+ checkMetrics(metrics.metrics);
+ });
+ function checkMetrics(metrics: Metrics) {
+ const metricsToCheck = new Set([
+ 'Timestamp',
+ 'Documents',
+ 'Frames',
+ 'JSEventListeners',
+ 'Nodes',
+ 'LayoutCount',
+ 'RecalcStyleCount',
+ 'LayoutDuration',
+ 'RecalcStyleDuration',
+ 'ScriptDuration',
+ 'TaskDuration',
+ 'JSHeapUsedSize',
+ 'JSHeapTotalSize',
+ ]);
+ for (const name in metrics) {
+ expect(metricsToCheck.has(name)).toBeTruthy();
+ expect(metrics[name as keyof Metrics]).toBeGreaterThanOrEqual(0);
+ metricsToCheck.delete(name);
+ }
+ expect(metricsToCheck.size).toBe(0);
+ }
+ });
+
+ describe('Page.waitForRequest', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ page.waitForRequest(server.PREFIX + '/digits/2.png'),
+ page.evaluate(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }),
+ ]);
+ expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should work with predicate', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ page.waitForRequest(request => {
+ return request.url() === server.PREFIX + '/digits/2.png';
+ }),
+ page.evaluate(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }),
+ ]);
+ expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should work with async predicate', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ page.waitForRequest(async request => {
+ return request.url() === server.PREFIX + '/digits/2.png';
+ }),
+ page.evaluate(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }),
+ ]);
+ expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .waitForRequest(
+ () => {
+ return false;
+ },
+ {timeout: 1}
+ )
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should respect default timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ page.setDefaultTimeout(1);
+ await page
+ .waitForRequest(() => {
+ return false;
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should work with no timeout', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ page.waitForRequest(server.PREFIX + '/digits/2.png', {timeout: 0}),
+ page.evaluate(() => {
+ return setTimeout(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }, 50);
+ }),
+ ]);
+ expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ });
+
+ describe('Page.waitForResponse', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForResponse(server.PREFIX + '/digits/2.png'),
+ page.evaluate(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }),
+ ]);
+ expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .waitForResponse(
+ () => {
+ return false;
+ },
+ {timeout: 1}
+ )
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should respect default timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ page.setDefaultTimeout(1);
+ await page
+ .waitForResponse(() => {
+ return false;
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should work with predicate', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForResponse(response => {
+ return response.url() === server.PREFIX + '/digits/2.png';
+ }),
+ page.evaluate(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }),
+ ]);
+ expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should work with async predicate', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForResponse(async response => {
+ return response.url() === server.PREFIX + '/digits/2.png';
+ }),
+ page.evaluate(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }),
+ ]);
+ expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should work with no timeout', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForResponse(server.PREFIX + '/digits/2.png', {timeout: 0}),
+ page.evaluate(() => {
+ return setTimeout(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }, 50);
+ }),
+ ]);
+ expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ });
+
+ describe('Page.waitForNetworkIdle', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ let res;
+ const [t1, t2] = await Promise.all([
+ page.waitForNetworkIdle().then(r => {
+ res = r;
+ return Date.now();
+ }),
+ page
+ .evaluate(async () => {
+ await Promise.all([fetch('/digits/1.png'), fetch('/digits/2.png')]);
+ await new Promise(resolve => {
+ return setTimeout(resolve, 200);
+ });
+ await fetch('/digits/3.png');
+ await new Promise(resolve => {
+ return setTimeout(resolve, 200);
+ });
+ await fetch('/digits/4.png');
+ })
+ .then(() => {
+ return Date.now();
+ }),
+ ]);
+ expect(res).toBe(undefined);
+ expect(t1).toBeGreaterThan(t2);
+ expect(t1 - t2).toBeGreaterThanOrEqual(400);
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+ let error!: Error;
+ await page.waitForNetworkIdle({timeout: 1}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should respect idleTime', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const [t1, t2] = await Promise.all([
+ page.waitForNetworkIdle({idleTime: 10}).then(() => {
+ return Date.now();
+ }),
+ page
+ .evaluate(() => {
+ return (async () => {
+ await Promise.all([
+ fetch('/digits/1.png'),
+ fetch('/digits/2.png'),
+ ]);
+ await new Promise(resolve => {
+ return setTimeout(resolve, 250);
+ });
+ })();
+ })
+ .then(() => {
+ return Date.now();
+ }),
+ ]);
+ expect(t2).toBeGreaterThan(t1);
+ });
+ it('should work with no timeout', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const [result] = await Promise.all([
+ page.waitForNetworkIdle({timeout: 0}),
+ page.evaluate(() => {
+ return setTimeout(() => {
+ void fetch('/digits/1.png');
+ void fetch('/digits/2.png');
+ void fetch('/digits/3.png');
+ }, 50);
+ }),
+ ]);
+ expect(result).toBe(undefined);
+ });
+ it('should work with aborted requests', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/abort-request.html');
+
+ using element = await page.$(`#abort`);
+ await element!.click();
+
+ let error = false;
+ await page.waitForNetworkIdle().catch(() => {
+ return (error = true);
+ });
+
+ expect(error).toBe(false);
+ });
+ it('should work with delayed response', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ let response!: ServerResponse;
+ server.setRoute('/fetch-request-b.js', (_req, res) => {
+ response = res;
+ });
+ const t0 = Date.now();
+ const [t1, t2] = await Promise.all([
+ page.waitForNetworkIdle({idleTime: 100}).then(() => {
+ return Date.now();
+ }),
+ new Promise<number>(res => {
+ setTimeout(() => {
+ response.end();
+ res(Date.now());
+ }, 300);
+ }),
+ page.evaluate(async () => {
+ await fetch('/fetch-request-b.js');
+ }),
+ ]);
+ expect(t1).toBeGreaterThan(t2);
+ // request finished + idle time.
+ expect(t1 - t0).toBeGreaterThan(400);
+ // request finished + idle time - request finished.
+ expect(t1 - t2).toBeGreaterThanOrEqual(100);
+ });
+ });
+
+ describe('Page.exposeFunction', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return a * b;
+ });
+ const result = await page.evaluate(async function () {
+ return (globalThis as any).compute(9, 4);
+ });
+ expect(result).toBe(36);
+ });
+ it('should throw exception in page context', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction('woof', () => {
+ throw new Error('WOOF WOOF');
+ });
+ const {message, stack} = await page.evaluate(async () => {
+ try {
+ return await (
+ globalThis as unknown as {woof(): Promise<never>}
+ ).woof();
+ } catch (error) {
+ return {
+ message: (error as Error).message,
+ stack: (error as Error).stack,
+ };
+ }
+ });
+ expect(message).toBe('WOOF WOOF');
+ expect(stack).toContain('page.spec.ts');
+ });
+ it('should support throwing "null"', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction('woof', function () {
+ throw null;
+ });
+ const thrown = await page.evaluate(async () => {
+ try {
+ await (globalThis as any).woof();
+ return;
+ } catch (error) {
+ return error;
+ }
+ });
+ expect(thrown).toBe(null);
+ });
+ it('should be callable from-inside evaluateOnNewDocument', async () => {
+ const {page} = await getTestState();
+
+ let called = false;
+ await page.exposeFunction('woof', function () {
+ called = true;
+ });
+ await page.evaluateOnNewDocument(() => {
+ return (globalThis as any).woof();
+ });
+ await page.reload();
+ expect(called).toBe(true);
+ });
+ it('should survive navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return a * b;
+ });
+
+ await page.goto(server.EMPTY_PAGE);
+ const result = await page.evaluate(async function () {
+ return (globalThis as any).compute(9, 4);
+ });
+ expect(result).toBe(36);
+ });
+ it('should await returned promise', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return Promise.resolve(a * b);
+ });
+
+ const result = await page.evaluate(async function () {
+ return (globalThis as any).compute(3, 5);
+ });
+ expect(result).toBe(15);
+ });
+ it('should await returned if called from function', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return Promise.resolve(a * b);
+ });
+
+ const result = await page.evaluate(async function () {
+ const result = await (globalThis as any).compute(3, 5);
+ return result;
+ });
+ expect(result).toBe(15);
+ });
+ it('should work on frames', async () => {
+ const {page, server} = await getTestState();
+
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return Promise.resolve(a * b);
+ });
+
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ const frame = page.frames()[1]!;
+ const result = await frame.evaluate(async function () {
+ return (globalThis as any).compute(3, 5);
+ });
+ expect(result).toBe(15);
+ });
+ it('should work with loading frames', async () => {
+ // Tries to reproduce the scenario from
+ // https://github.com/puppeteer/puppeteer/issues/8106
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ let saveRequest: (value: HTTPRequest | PromiseLike<HTTPRequest>) => void;
+ const iframeRequest = new Promise<HTTPRequest>(resolve => {
+ saveRequest = resolve;
+ });
+ page.on('request', async req => {
+ if (req.url().endsWith('/frames/frame.html')) {
+ saveRequest(req);
+ } else {
+ await req.continue();
+ }
+ });
+
+ let error: Error | undefined;
+ const navPromise = page
+ .goto(server.PREFIX + '/frames/one-frame.html', {
+ waitUntil: 'networkidle0',
+ })
+ .catch(err => {
+ error = err;
+ });
+ const req = await iframeRequest;
+ // Expose function while the frame is being loaded. Loading process is
+ // controlled by interception.
+ const exposePromise = page.exposeFunction(
+ 'compute',
+ function (a: number, b: number) {
+ return Promise.resolve(a * b);
+ }
+ );
+ await Promise.all([req.continue(), exposePromise]);
+ await navPromise;
+ expect(error).toBeUndefined();
+ const frame = page.frames()[1]!;
+ const result = await frame.evaluate(async function () {
+ return (globalThis as any).compute(3, 5);
+ });
+ expect(result).toBe(15);
+ });
+ it('should work on frames before navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return Promise.resolve(a * b);
+ });
+
+ const frame = page.frames()[1]!;
+ const result = await frame.evaluate(async function () {
+ return (globalThis as any).compute(3, 5);
+ });
+ expect(result).toBe(15);
+ });
+ it('should not throw when frames detach', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return Promise.resolve(a * b);
+ });
+ await detachFrame(page, 'frame1');
+
+ await expect(
+ page.evaluate(async function () {
+ return (globalThis as any).compute(3, 5);
+ })
+ ).resolves.toEqual(15);
+ });
+ it('should work with complex objects', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction(
+ 'complexObject',
+ function (a: {x: number}, b: {x: number}) {
+ return {x: a.x + b.x};
+ }
+ );
+ const result = await page.evaluate(async () => {
+ return (globalThis as any).complexObject({x: 5}, {x: 2});
+ });
+ expect(result.x).toBe(7);
+ });
+ it('should fallback to default export when passed a module object', async () => {
+ const {page, server} = await getTestState();
+ const moduleObject = {
+ default: function (a: number, b: number) {
+ return a * b;
+ },
+ };
+ await page.goto(server.EMPTY_PAGE);
+ await page.exposeFunction('compute', moduleObject);
+ const result = await page.evaluate(async function () {
+ return (globalThis as any).compute(9, 4);
+ });
+ expect(result).toBe(36);
+ });
+ });
+
+ describe('Page.removeExposedFunction', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.exposeFunction('compute', function (a: number, b: number) {
+ return a * b;
+ });
+ const result = await page.evaluate(async function () {
+ return (globalThis as any).compute(9, 4);
+ });
+ expect(result).toBe(36);
+ await page.removeExposedFunction('compute');
+
+ let error: Error | null = null;
+ await page
+ .evaluate(async function () {
+ return (globalThis as any).compute(9, 4);
+ })
+ .catch(_error => {
+ return (error = _error);
+ });
+ expect(error).toBeTruthy();
+ });
+ });
+
+ describe('Page.Events.PageError', function () {
+ it('should fire', async () => {
+ const {page, server} = await getTestState();
+
+ const [error] = await Promise.all([
+ waitEvent<Error>(page, 'pageerror', err => {
+ return err.message.includes('Fancy');
+ }),
+ page.goto(server.PREFIX + '/error.html'),
+ ]);
+ expect(error.message).toContain('Fancy');
+ expect(error.stack?.split('\n')[1]).toContain('error.html:13');
+ });
+ });
+
+ describe('Page.setUserAgent', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return navigator.userAgent;
+ })
+ ).toContain('Mozilla');
+ await page.setUserAgent('foobar');
+ const [request] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(request.headers['user-agent']).toBe('foobar');
+ });
+ it('should work for subframes', async () => {
+ const {page, server} = await getTestState();
+
+ expect(
+ await page.evaluate(() => {
+ return navigator.userAgent;
+ })
+ ).toContain('Mozilla');
+ await page.setUserAgent('foobar');
+ const [request] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ attachFrame(page, 'frame1', server.EMPTY_PAGE),
+ ]);
+ expect(request.headers['user-agent']).toBe('foobar');
+ });
+ it('should emulate device user-agent', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ expect(
+ await page.evaluate(() => {
+ return navigator.userAgent;
+ })
+ ).not.toContain('iPhone');
+ await page.setUserAgent(KnownDevices['iPhone 6'].userAgent);
+ expect(
+ await page.evaluate(() => {
+ return navigator.userAgent;
+ })
+ ).toContain('iPhone');
+ });
+ it('should work with additional userAgentMetdata', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setUserAgent('MockBrowser', {
+ architecture: 'Mock1',
+ mobile: false,
+ model: 'Mockbook',
+ platform: 'MockOS',
+ platformVersion: '3.1',
+ });
+ const [request] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(
+ await page.evaluate(() => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error: userAgentData not yet in TypeScript DOM API
+ return navigator.userAgentData.mobile;
+ })
+ ).toBe(false);
+
+ const uaData = await page.evaluate(() => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error: userAgentData not yet in TypeScript DOM API
+ return navigator.userAgentData.getHighEntropyValues([
+ 'architecture',
+ 'model',
+ 'platform',
+ 'platformVersion',
+ ]);
+ });
+ expect(uaData['architecture']).toBe('Mock1');
+ expect(uaData['model']).toBe('Mockbook');
+ expect(uaData['platform']).toBe('MockOS');
+ expect(uaData['platformVersion']).toBe('3.1');
+ expect(request.headers['user-agent']).toBe('MockBrowser');
+ });
+ });
+
+ describe('Page.setContent', function () {
+ const expectedOutput =
+ '<html><head></head><body><div>hello</div></body></html>';
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>hello</div>');
+ const result = await page.content();
+ expect(result).toBe(expectedOutput);
+ });
+ it('should work with doctype', async () => {
+ const {page} = await getTestState();
+
+ const doctype = '<!DOCTYPE html>';
+ await page.setContent(`${doctype}<div>hello</div>`);
+ const result = await page.content();
+ expect(result).toBe(`${doctype}${expectedOutput}`);
+ });
+ it('should work with HTML 4 doctype', async () => {
+ const {page} = await getTestState();
+
+ const doctype =
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" ' +
+ '"http://www.w3.org/TR/html4/strict.dtd">';
+ await page.setContent(`${doctype}<div>hello</div>`);
+ const result = await page.content();
+ expect(result).toBe(`${doctype}${expectedOutput}`);
+ });
+ it('should respect timeout', async () => {
+ const {page, server} = await getTestState();
+
+ const imgPath = '/img.png';
+ // stall for image
+ server.setRoute(imgPath, () => {});
+ let error!: Error;
+ await page
+ .setContent(`<img src="${server.PREFIX + imgPath}"></img>`, {
+ timeout: 1,
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should respect default navigation timeout', async () => {
+ const {page, server} = await getTestState();
+
+ page.setDefaultNavigationTimeout(1);
+ const imgPath = '/img.png';
+ // stall for image
+ server.setRoute(imgPath, () => {});
+ let error!: Error;
+ await page
+ .setContent(`<img src="${server.PREFIX + imgPath}"></img>`)
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ it('should await resources to load', async () => {
+ const {page, server} = await getTestState();
+
+ const imgPath = '/img.png';
+ let imgResponse!: ServerResponse;
+ server.setRoute(imgPath, (_req, res) => {
+ return (imgResponse = res);
+ });
+ let loaded = false;
+ const contentPromise = page
+ .setContent(`<img src="${server.PREFIX + imgPath}"></img>`)
+ .then(() => {
+ return (loaded = true);
+ });
+ await server.waitForRequest(imgPath);
+ expect(loaded).toBe(false);
+ imgResponse.end();
+ await contentPromise;
+ });
+ it('should work fast enough', async () => {
+ const {page} = await getTestState();
+
+ for (let i = 0; i < 20; ++i) {
+ await page.setContent('<div>yo</div>');
+ }
+ });
+ it('should work with tricky content', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>hello world</div>' + '\x7F');
+ expect(
+ await page.$eval('div', div => {
+ return div.textContent;
+ })
+ ).toBe('hello world');
+ });
+ it('should work with accents', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>aberración</div>');
+ expect(
+ await page.$eval('div', div => {
+ return div.textContent;
+ })
+ ).toBe('aberración');
+ });
+ it('should work with emojis', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>🐥</div>');
+ expect(
+ await page.$eval('div', div => {
+ return div.textContent;
+ })
+ ).toBe('🐥');
+ });
+ it('should work with newline', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>\n</div>');
+ expect(
+ await page.$eval('div', div => {
+ return div.textContent;
+ })
+ ).toBe('\n');
+ });
+ it('should work with comments outside HTML tag', async () => {
+ const {page} = await getTestState();
+
+ const comment = '<!-- Comment -->';
+ await page.setContent(`${comment}<div>hello</div>`);
+ const result = await page.content();
+ expect(result).toBe(`${comment}${expectedOutput}`);
+ });
+ });
+
+ describe('Page.setBypassCSP', function () {
+ it('should bypass CSP meta tag', async () => {
+ const {page, server} = await getTestState();
+
+ // Make sure CSP prohibits addScriptTag.
+ await page.goto(server.PREFIX + '/csp.html');
+ await page
+ .addScriptTag({content: 'window.__injected = 42;'})
+ .catch(error => {
+ return void error;
+ });
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(undefined);
+
+ // By-pass CSP and try one more time.
+ await page.setBypassCSP(true);
+ await page.reload();
+ await page.addScriptTag({content: 'window.__injected = 42;'});
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should bypass CSP header', async () => {
+ const {page, server} = await getTestState();
+
+ // Make sure CSP prohibits addScriptTag.
+ server.setCSP('/empty.html', 'default-src "self"');
+ await page.goto(server.EMPTY_PAGE);
+ await page
+ .addScriptTag({content: 'window.__injected = 42;'})
+ .catch(error => {
+ return void error;
+ });
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(undefined);
+
+ // By-pass CSP and try one more time.
+ await page.setBypassCSP(true);
+ await page.reload();
+ await page.addScriptTag({content: 'window.__injected = 42;'});
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should bypass after cross-process navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setBypassCSP(true);
+ await page.goto(server.PREFIX + '/csp.html');
+ await page.addScriptTag({content: 'window.__injected = 42;'});
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html');
+ await page.addScriptTag({content: 'window.__injected = 42;'});
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+ });
+ it('should bypass CSP in iframes as well', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ {
+ // Make sure CSP prohibits addScriptTag in an iframe.
+ const frame = (await attachFrame(
+ page,
+ 'frame1',
+ server.PREFIX + '/csp.html'
+ ))!;
+ await frame
+ .addScriptTag({content: 'window.__injected = 42;'})
+ .catch(error => {
+ return void error;
+ });
+ expect(
+ await frame.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(undefined);
+ }
+
+ // By-pass CSP and try one more time.
+ await page.setBypassCSP(true);
+ await page.reload();
+
+ {
+ const frame = (await attachFrame(
+ page,
+ 'frame1',
+ server.PREFIX + '/csp.html'
+ ))!;
+ await frame
+ .addScriptTag({content: 'window.__injected = 42;'})
+ .catch(error => {
+ return void error;
+ });
+ expect(
+ await frame.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+ }
+ });
+ });
+
+ describe('Page.addScriptTag', function () {
+ it('should throw an error if no options are provided', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ try {
+ // @ts-expect-error purposefully passing bad options
+ await page.addScriptTag('/injectedfile.js');
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toBe(
+ 'Exactly one of `url`, `path`, or `content` must be specified.'
+ );
+ });
+
+ it('should work with a url', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ using scriptHandle = await page.addScriptTag({url: '/injectedfile.js'});
+ expect(scriptHandle.asElement()).not.toBeNull();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should work with a url and type=module', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addScriptTag({url: '/es6/es6import.js', type: 'module'});
+ expect(
+ await page.evaluate(() => {
+ return (window as unknown as {__es6injected: number}).__es6injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should work with a path and type=module', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addScriptTag({
+ path: path.join(__dirname, '../assets/es6/es6pathimport.js'),
+ type: 'module',
+ });
+ await page.waitForFunction(() => {
+ return (window as unknown as {__es6injected: number}).__es6injected;
+ });
+ expect(
+ await page.evaluate(() => {
+ return (window as unknown as {__es6injected: number}).__es6injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should work with a content and type=module', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addScriptTag({
+ content: `import num from '/es6/es6module.js';window.__es6injected = num;`,
+ type: 'module',
+ });
+ await page.waitForFunction(() => {
+ return (window as unknown as {__es6injected: number}).__es6injected;
+ });
+ expect(
+ await page.evaluate(() => {
+ return (window as unknown as {__es6injected: number}).__es6injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should throw an error if loading from url fail', async () => {
+ const {page, server, isFirefox} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let error!: Error;
+ try {
+ await page.addScriptTag({url: '/nonexistfile.js'});
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ if (isFirefox) {
+ expect(error.message).toBeTruthy();
+ } else {
+ expect(error.message).toContain('Could not load script');
+ }
+ });
+
+ it('should work with a path', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ using scriptHandle = await page.addScriptTag({
+ path: path.join(__dirname, '../assets/injectedfile.js'),
+ });
+ expect(scriptHandle.asElement()).not.toBeNull();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(42);
+ });
+
+ it('should include sourcemap when path is provided', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addScriptTag({
+ path: path.join(__dirname, '../assets/injectedfile.js'),
+ });
+ const result = await page.evaluate(() => {
+ return (globalThis as any).__injectedError.stack;
+ });
+ expect(result).toContain(path.join('assets', 'injectedfile.js'));
+ });
+
+ it('should work with content', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ using scriptHandle = await page.addScriptTag({
+ content: 'window.__injected = 35;',
+ });
+ expect(scriptHandle.asElement()).not.toBeNull();
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).__injected;
+ })
+ ).toBe(35);
+ });
+
+ it('should add id when provided', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.addScriptTag({content: 'window.__injected = 1;', id: 'one'});
+ await page.addScriptTag({url: '/injectedfile.js', id: 'two'});
+ expect(await page.$('#one')).not.toBeNull();
+ expect(await page.$('#two')).not.toBeNull();
+ });
+
+ // @see https://github.com/puppeteer/puppeteer/issues/4840
+ it('should throw when added with content to the CSP page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/csp.html');
+ let error!: Error;
+ await page
+ .addScriptTag({content: 'window.__injected = 35;'})
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ });
+
+ it('should throw when added with URL to the CSP page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/csp.html');
+ let error!: Error;
+ await page
+ .addScriptTag({url: server.CROSS_PROCESS_PREFIX + '/injectedfile.js'})
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ });
+ });
+
+ describe('Page.addStyleTag', function () {
+ it('should throw an error if no options are provided', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ try {
+ // @ts-expect-error purposefully passing bad input
+ await page.addStyleTag('/injectedstyle.css');
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toBe(
+ 'Exactly one of `url`, `path`, or `content` must be specified.'
+ );
+ });
+
+ it('should work with a url', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ using styleHandle = await page.addStyleTag({url: '/injectedstyle.css'});
+ expect(styleHandle.asElement()).not.toBeNull();
+ expect(
+ await page.evaluate(
+ `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')`
+ )
+ ).toBe('rgb(255, 0, 0)');
+ });
+
+ it('should throw an error if loading from url fail', async () => {
+ const {page, server, isFirefox} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let error!: Error;
+ try {
+ await page.addStyleTag({url: '/nonexistfile.js'});
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ if (isFirefox) {
+ expect(error.message).toBeTruthy();
+ } else {
+ expect(error.message).toContain('Could not load style');
+ }
+ });
+
+ it('should work with a path', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ using styleHandle = await page.addStyleTag({
+ path: path.join(__dirname, '../assets/injectedstyle.css'),
+ });
+ expect(styleHandle.asElement()).not.toBeNull();
+ expect(
+ await page.evaluate(
+ `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')`
+ )
+ ).toBe('rgb(255, 0, 0)');
+ });
+
+ it('should include sourcemap when path is provided', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addStyleTag({
+ path: path.join(__dirname, '../assets/injectedstyle.css'),
+ });
+ using styleHandle = (await page.$('style'))!;
+ const styleContent = await page.evaluate((style: HTMLStyleElement) => {
+ return style.innerHTML;
+ }, styleHandle);
+ expect(styleContent).toContain(path.join('assets', 'injectedstyle.css'));
+ });
+
+ it('should work with content', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ using styleHandle = await page.addStyleTag({
+ content: 'body { background-color: green; }',
+ });
+ expect(styleHandle.asElement()).not.toBeNull();
+ expect(
+ await page.evaluate(
+ `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')`
+ )
+ ).toBe('rgb(0, 128, 0)');
+ });
+
+ it('should throw when added with content to the CSP page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/csp.html');
+ let error!: Error;
+ await page
+ .addStyleTag({content: 'body { background-color: green; }'})
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ });
+
+ it('should throw when added with URL to the CSP page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/csp.html');
+ let error!: Error;
+ await page
+ .addStyleTag({
+ url: server.CROSS_PROCESS_PREFIX + '/injectedstyle.css',
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ });
+ });
+
+ describe('Page.url', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ expect(page.url()).toBe('about:blank');
+ await page.goto(server.EMPTY_PAGE);
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ });
+ });
+
+ describe('Page.setJavaScriptEnabled', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setJavaScriptEnabled(false);
+ await page.goto(
+ 'data:text/html, <script>var something = "forbidden"</script>'
+ );
+ let error!: Error;
+ await page.evaluate('something').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('something is not defined');
+
+ await page.setJavaScriptEnabled(true);
+ await page.goto(
+ 'data:text/html, <script>var something = "forbidden"</script>'
+ );
+ expect(await page.evaluate('something')).toBe('forbidden');
+ });
+ });
+
+ describe('Page.setCacheEnabled', function () {
+ it('should enable or disable the cache based on the state passed', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+ const [cachedRequest] = await Promise.all([
+ server.waitForRequest('/cached/one-style.html'),
+ page.reload(),
+ ]);
+ // Rely on "if-modified-since" caching in our test server.
+ expect(cachedRequest.headers['if-modified-since']).not.toBe(undefined);
+
+ await page.setCacheEnabled(false);
+ const [nonCachedRequest] = await Promise.all([
+ server.waitForRequest('/cached/one-style.html'),
+ page.reload(),
+ ]);
+ expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined);
+ });
+ it('should stay disabled when toggling request interception on/off', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setCacheEnabled(false);
+ await page.setRequestInterception(true);
+ await page.setRequestInterception(false);
+
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+ const [nonCachedRequest] = await Promise.all([
+ server.waitForRequest('/cached/one-style.html'),
+ page.reload(),
+ ]);
+ expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined);
+ });
+ });
+
+ describe('Page.pdf', function () {
+ it('can print to PDF and save to file', async () => {
+ const {page, server} = await getTestState();
+
+ const outputFile = __dirname + '/../assets/output.pdf';
+ await page.goto(server.PREFIX + '/pdf.html');
+ await page.pdf({path: outputFile});
+ try {
+ expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0);
+ } finally {
+ fs.unlinkSync(outputFile);
+ }
+ });
+
+ it('can print to PDF with accessible', async () => {
+ const {page, server} = await getTestState();
+
+ const outputFile = __dirname + '/../assets/output.pdf';
+ const outputFileAccessible =
+ __dirname + '/../assets/output-accessible.pdf';
+ await page.goto(server.PREFIX + '/pdf.html');
+ await page.pdf({path: outputFile});
+ await page.pdf({path: outputFileAccessible, tagged: true});
+ try {
+ expect(
+ fs.readFileSync(outputFileAccessible).byteLength
+ ).toBeGreaterThan(fs.readFileSync(outputFile).byteLength);
+ } finally {
+ fs.unlinkSync(outputFileAccessible);
+ fs.unlinkSync(outputFile);
+ }
+ });
+
+ it('can print to PDF and stream the result', async () => {
+ const {page} = await getTestState();
+
+ const stream = await page.createPDFStream();
+ let size = 0;
+ for await (const chunk of stream) {
+ size += chunk.length;
+ }
+ expect(size).toBeGreaterThan(0);
+ });
+
+ it('should respect timeout', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/pdf.html');
+
+ const error = await page.pdf({timeout: 1}).catch(err => {
+ return err;
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ });
+
+ describe('Page.title', function () {
+ it('should return the page title', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/title.html');
+ expect(await page.title()).toBe('Woof-Woof');
+ });
+ });
+
+ describe('Page.select', function () {
+ it('should select single option', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('select', 'blue');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onInput;
+ })
+ ).toEqual(['blue']);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onChange;
+ })
+ ).toEqual(['blue']);
+ });
+ it('should select only first option', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('select', 'blue', 'green', 'red');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onInput;
+ })
+ ).toEqual(['blue']);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onChange;
+ })
+ ).toEqual(['blue']);
+ });
+ it('should not throw when select causes navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.$eval('select', select => {
+ return select.addEventListener('input', () => {
+ return ((window as any).location = '/empty.html');
+ });
+ });
+ await Promise.all([
+ page.select('select', 'blue'),
+ page.waitForNavigation(),
+ ]);
+ expect(page.url()).toContain('empty.html');
+ });
+ it('should select multiple options', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.evaluate(() => {
+ return (globalThis as any).makeMultiple();
+ });
+ await page.select('select', 'blue', 'green', 'red');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onInput;
+ })
+ ).toEqual(['blue', 'green', 'red']);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onChange;
+ })
+ ).toEqual(['blue', 'green', 'red']);
+ });
+ it('should respect event bubbling', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('select', 'blue');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onBubblingInput;
+ })
+ ).toEqual(['blue']);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onBubblingChange;
+ })
+ ).toEqual(['blue']);
+ });
+ it('should throw when element is not a <select>', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('body', '').catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain('Element is not a <select> element.');
+ });
+ it('should return [] on no matched values', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ const result = await page.select('select', '42', 'abc');
+ expect(result).toEqual([]);
+ });
+ it('should return an array of matched values', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.evaluate(() => {
+ return (globalThis as any).makeMultiple();
+ });
+ const result = await page.select('select', 'blue', 'black', 'magenta');
+ expect(
+ result.reduce((accumulator, current) => {
+ return ['blue', 'black', 'magenta'].includes(current) && accumulator;
+ }, true)
+ ).toEqual(true);
+ });
+ it('should return an array of one element when multiple is not set', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ const result = await page.select(
+ 'select',
+ '42',
+ 'blue',
+ 'black',
+ 'magenta'
+ );
+ expect(result).toHaveLength(1);
+ });
+ it('should return [] on no values', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ const result = await page.select('select');
+ expect(result).toEqual([]);
+ });
+ it('should deselect all options when passed no values for a multiple select', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.evaluate(() => {
+ return (globalThis as any).makeMultiple();
+ });
+ await page.select('select', 'blue', 'black', 'magenta');
+ await page.select('select');
+ expect(
+ await page.$eval('select', select => {
+ return Array.from((select as HTMLSelectElement).options).every(
+ option => {
+ return !option.selected;
+ }
+ );
+ })
+ ).toEqual(true);
+ });
+ it('should deselect all options when passed no values for a select without multiple', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('select', 'blue', 'black', 'magenta');
+ await page.select('select');
+ expect(
+ await page.$eval('select', select => {
+ return Array.from((select as HTMLSelectElement).options).filter(
+ option => {
+ return option.selected;
+ }
+ )[0]!.value;
+ })
+ ).toEqual('');
+ });
+ it('should throw if passed in non-strings', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<select><option value="12"/></select>');
+ let error!: Error;
+ try {
+ // @ts-expect-error purposefully passing bad input
+ await page.select('select', 12);
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error.message).toContain('Values must be strings');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/3327
+ it('should work when re-defining top-level Event class', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.evaluate(() => {
+ // @ts-expect-error Expected.
+ return (window.Event = undefined);
+ });
+ await page.select('select', 'blue');
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onInput;
+ })
+ ).toEqual(['blue']);
+ expect(
+ await page.evaluate(() => {
+ return (globalThis as any).result.onChange;
+ })
+ ).toEqual(['blue']);
+ });
+ });
+
+ describe('Page.Events.Close', function () {
+ it('should work with window.close', async () => {
+ const {page, context} = await getTestState();
+
+ const newPagePromise = new Promise<Page | null>(fulfill => {
+ return context.once('targetcreated', target => {
+ return fulfill(target.page());
+ });
+ });
+ assert(page);
+ await page.evaluate(() => {
+ return ((window as any)['newPage'] = window.open('about:blank'));
+ });
+ const newPage = await newPagePromise;
+ assert(newPage);
+ const closedPromise = waitEvent(newPage, 'close');
+ await page.evaluate(() => {
+ return (window as any)['newPage'].close();
+ });
+ await closedPromise;
+ });
+ it('should work with page.close', async () => {
+ const {context} = await getTestState();
+
+ const newPage = await context.newPage();
+ const closedPromise = waitEvent(newPage, 'close');
+ await newPage.close();
+ await closedPromise;
+ });
+ });
+
+ describe('Page.browser', function () {
+ it('should return the correct browser instance', async () => {
+ const {page, browser} = await getTestState();
+
+ expect(page.browser()).toBe(browser);
+ });
+ });
+
+ describe('Page.browserContext', function () {
+ it('should return the correct browser context instance', async () => {
+ const {page, context} = await getTestState();
+
+ expect(page.browserContext()).toBe(context);
+ });
+ });
+
+ describe('Page.client', function () {
+ it('should return the client instance', async () => {
+ const {page} = await getTestState();
+ expect((page as CdpPage)._client()).toBeInstanceOf(CDPSession);
+ });
+ });
+
+ describe('Page.bringToFront', function () {
+ it('should work', async () => {
+ const {browser} = await getTestState();
+ const page1 = await browser.newPage();
+ const page2 = await browser.newPage();
+
+ await page1.bringToFront();
+ expect(
+ await page1.evaluate(() => {
+ return document.visibilityState;
+ })
+ ).toBe('visible');
+ expect(
+ await page2.evaluate(() => {
+ return document.visibilityState;
+ })
+ ).toBe('hidden');
+
+ await page2.bringToFront();
+ expect(
+ await page1.evaluate(() => {
+ return document.visibilityState;
+ })
+ ).toBe('hidden');
+ expect(
+ await page2.evaluate(() => {
+ return document.visibilityState;
+ })
+ ).toBe('visible');
+
+ await page1.close();
+ await page2.close();
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/proxy.spec.ts b/remote/test/puppeteer/test/src/proxy.spec.ts
new file mode 100644
index 0000000000..07b73cdd0d
--- /dev/null
+++ b/remote/test/puppeteer/test/src/proxy.spec.ts
@@ -0,0 +1,236 @@
+/**
+ * @license
+ * Copyright 2021 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {IncomingMessage, Server, ServerResponse} from 'http';
+import http from 'http';
+import type {AddressInfo} from 'net';
+import os from 'os';
+
+import type {TestServer} from '@pptr/testserver';
+import expect from 'expect';
+
+import {getTestState, launch} from './mocha-utils.js';
+
+let HOSTNAME = os.hostname();
+
+// Hostname might not be always accessible in environments other than GitHub
+// Actions. Therefore, we try to find an external IPv4 address to be used as a
+// hostname in these tests.
+const networkInterfaces = os.networkInterfaces();
+for (const key of Object.keys(networkInterfaces)) {
+ const interfaces = networkInterfaces[key];
+ for (const net of interfaces || []) {
+ if (net.family === 'IPv4' && !net.internal) {
+ HOSTNAME = net.address;
+ break;
+ }
+ }
+}
+
+/**
+ * Requests to localhost do not get proxied by default. Create a URL using the hostname
+ * instead.
+ */
+function getEmptyPageUrl(server: TestServer): string {
+ const emptyPagePath = new URL(server.EMPTY_PAGE).pathname;
+
+ return `http://${HOSTNAME}:${server.PORT}${emptyPagePath}`;
+}
+
+describe('request proxy', () => {
+ let proxiedRequestUrls: string[];
+ let proxyServer: Server;
+ let proxyServerUrl: string;
+ const defaultArgs = [
+ // We disable this in tests so that proxy-related tests
+ // don't intercept queries from this service in headful.
+ '--disable-features=NetworkTimeServiceQuerying',
+ ];
+
+ beforeEach(() => {
+ proxiedRequestUrls = [];
+
+ proxyServer = http
+ .createServer(
+ (
+ originalRequest: IncomingMessage,
+ originalResponse: ServerResponse
+ ) => {
+ proxiedRequestUrls.push(originalRequest.url as string);
+
+ const proxyRequest = http.request(
+ originalRequest.url as string,
+ {
+ method: originalRequest.method,
+ headers: originalRequest.headers,
+ },
+ proxyResponse => {
+ originalResponse.writeHead(
+ proxyResponse.statusCode as number,
+ proxyResponse.headers
+ );
+ proxyResponse.pipe(originalResponse, {end: true});
+ }
+ );
+
+ originalRequest.pipe(proxyRequest, {end: true});
+ }
+ )
+ .listen();
+
+ proxyServerUrl = `http://${HOSTNAME}:${
+ (proxyServer.address() as AddressInfo).port
+ }`;
+ });
+
+ afterEach(async () => {
+ await new Promise((resolve, reject) => {
+ proxyServer.close(error => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(undefined);
+ }
+ });
+ });
+ });
+
+ it('should proxy requests when configured', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const emptyPageUrl = getEmptyPageUrl(server);
+ const {browser, close} = await launch({
+ args: [...defaultArgs, `--proxy-server=${proxyServerUrl}`],
+ });
+ try {
+ const page = await browser.newPage();
+ const response = (await page.goto(emptyPageUrl))!;
+
+ expect(response.ok()).toBe(true);
+ expect(proxiedRequestUrls).toEqual([emptyPageUrl]);
+ } finally {
+ await close();
+ }
+ });
+
+ it('should respect proxy bypass list', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const emptyPageUrl = getEmptyPageUrl(server);
+ const {browser, close} = await launch({
+ args: [
+ ...defaultArgs,
+ `--proxy-server=${proxyServerUrl}`,
+ `--proxy-bypass-list=${new URL(emptyPageUrl).host}`,
+ ],
+ });
+ try {
+ const page = await browser.newPage();
+ const response = (await page.goto(emptyPageUrl))!;
+
+ expect(response.ok()).toBe(true);
+ expect(proxiedRequestUrls).toEqual([]);
+ } finally {
+ await close();
+ }
+ });
+
+ describe('in incognito browser context', () => {
+ it('should proxy requests when configured at browser level', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const emptyPageUrl = getEmptyPageUrl(server);
+ const {browser, close} = await launch({
+ args: [...defaultArgs, `--proxy-server=${proxyServerUrl}`],
+ });
+ try {
+ const context = await browser.createIncognitoBrowserContext();
+ const page = await context.newPage();
+ const response = (await page.goto(emptyPageUrl))!;
+
+ expect(response.ok()).toBe(true);
+ expect(proxiedRequestUrls).toEqual([emptyPageUrl]);
+ } finally {
+ await close();
+ }
+ });
+
+ it('should respect proxy bypass list when configured at browser level', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const emptyPageUrl = getEmptyPageUrl(server);
+ const {browser, close} = await launch({
+ args: [
+ ...defaultArgs,
+ `--proxy-server=${proxyServerUrl}`,
+ `--proxy-bypass-list=${new URL(emptyPageUrl).host}`,
+ ],
+ });
+ try {
+ const context = await browser.createIncognitoBrowserContext();
+ const page = await context.newPage();
+ const response = (await page.goto(emptyPageUrl))!;
+
+ expect(response.ok()).toBe(true);
+ expect(proxiedRequestUrls).toEqual([]);
+ } finally {
+ await close();
+ }
+ });
+
+ /**
+ * See issues #7873, #7719, and #7698.
+ */
+ it('should proxy requests when configured at context level', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const emptyPageUrl = getEmptyPageUrl(server);
+ const {browser, close} = await launch({
+ args: defaultArgs,
+ });
+ try {
+ const context = await browser.createIncognitoBrowserContext({
+ proxyServer: proxyServerUrl,
+ });
+ const page = await context.newPage();
+ const response = (await page.goto(emptyPageUrl))!;
+
+ expect(response.ok()).toBe(true);
+ expect(proxiedRequestUrls).toEqual([emptyPageUrl]);
+ } finally {
+ await close();
+ }
+ });
+
+ it('should respect proxy bypass list when configured at context level', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const emptyPageUrl = getEmptyPageUrl(server);
+ const {browser, close} = await launch({
+ args: defaultArgs,
+ });
+ try {
+ const context = await browser.createIncognitoBrowserContext({
+ proxyServer: proxyServerUrl,
+ proxyBypassList: [new URL(emptyPageUrl).host],
+ });
+ const page = await context.newPage();
+ const response = (await page.goto(emptyPageUrl))!;
+
+ expect(response.ok()).toBe(true);
+ expect(proxiedRequestUrls).toEqual([]);
+ } finally {
+ await close();
+ }
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/queryhandler.spec.ts b/remote/test/puppeteer/test/src/queryhandler.spec.ts
new file mode 100644
index 0000000000..05f201a9be
--- /dev/null
+++ b/remote/test/puppeteer/test/src/queryhandler.spec.ts
@@ -0,0 +1,653 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'assert';
+
+import expect from 'expect';
+import {Puppeteer} from 'puppeteer-core';
+import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('Query handler tests', function () {
+ setupTestBrowserHooks();
+
+ describe('Pierce selectors', function () {
+ async function setUpPage(): ReturnType<typeof getTestState> {
+ const state = await getTestState();
+ await state.page.setContent(
+ `<script>
+ const div = document.createElement('div');
+ const shadowRoot = div.attachShadow({mode: 'open'});
+ const div1 = document.createElement('div');
+ div1.textContent = 'Hello';
+ div1.className = 'foo';
+ const div2 = document.createElement('div');
+ div2.textContent = 'World';
+ div2.className = 'foo';
+ shadowRoot.appendChild(div1);
+ shadowRoot.appendChild(div2);
+ document.documentElement.appendChild(div);
+ </script>`
+ );
+ return state;
+ }
+ it('should find first element in shadow', async () => {
+ const {page} = await setUpPage();
+ using div = (await page.$('pierce/.foo')) as ElementHandle<HTMLElement>;
+ const text = await div.evaluate(element => {
+ return element.textContent;
+ });
+ expect(text).toBe('Hello');
+ });
+ it('should find all elements in shadow', async () => {
+ const {page} = await setUpPage();
+ const divs = (await page.$$('pierce/.foo')) as Array<
+ ElementHandle<HTMLElement>
+ >;
+ const text = await Promise.all(
+ divs.map(div => {
+ return div.evaluate(element => {
+ return element.textContent;
+ });
+ })
+ );
+ expect(text.join(' ')).toBe('Hello World');
+ });
+ it('should find first child element', async () => {
+ const {page} = await setUpPage();
+ using parentElement = (await page.$('html > div'))!;
+ using childElement = (await parentElement.$(
+ 'pierce/div'
+ )) as ElementHandle<HTMLElement>;
+ const text = await childElement.evaluate(element => {
+ return element.textContent;
+ });
+ expect(text).toBe('Hello');
+ });
+ it('should find all child elements', async () => {
+ const {page} = await setUpPage();
+ using parentElement = (await page.$('html > div'))!;
+ const childElements = (await parentElement.$$('pierce/div')) as Array<
+ ElementHandle<HTMLElement>
+ >;
+ const text = await Promise.all(
+ childElements.map(div => {
+ return div.evaluate(element => {
+ return element.textContent;
+ });
+ })
+ );
+ expect(text.join(' ')).toBe('Hello World');
+ });
+ });
+
+ describe('Text selectors', function () {
+ describe('in Page', function () {
+ it('should query existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>test</section>');
+
+ expect(await page.$('text/test')).toBeTruthy();
+ expect(await page.$$('text/test')).toHaveLength(1);
+ });
+ it('should return empty array for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ expect(await page.$('text/test')).toBeFalsy();
+ expect(await page.$$('text/test')).toHaveLength(0);
+ });
+ it('should return first element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div id="1">a</div><div>a</div>');
+
+ using element = await page.$('text/a');
+ expect(
+ await element?.evaluate(e => {
+ return e.id;
+ })
+ ).toBe('1');
+ });
+ it('should return multiple elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>a</div><div>a</div>');
+
+ const elements = await page.$$('text/a');
+ expect(elements).toHaveLength(2);
+ });
+ it('should pierce shadow DOM', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ const div = document.createElement('div');
+ const shadow = div.attachShadow({mode: 'open'});
+ const diva = document.createElement('div');
+ shadow.append(diva);
+ const divb = document.createElement('div');
+ shadow.append(divb);
+ diva.innerHTML = 'a';
+ divb.innerHTML = 'b';
+ document.body.append(div);
+ });
+
+ using element = await page.$('text/a');
+ expect(
+ await element?.evaluate(e => {
+ return e.textContent;
+ })
+ ).toBe('a');
+ });
+ it('should query deeply nested text', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div><div>a</div><div>b</div></div>');
+
+ using element = await page.$('text/a');
+ expect(
+ await element?.evaluate(e => {
+ return e.textContent;
+ })
+ ).toBe('a');
+ });
+ it('should query inputs', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<input value="a">');
+
+ using element = (await page.$(
+ 'text/a'
+ )) as ElementHandle<HTMLInputElement>;
+ expect(
+ await element?.evaluate(e => {
+ return e.value;
+ })
+ ).toBe('a');
+ });
+ it('should not query radio', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<radio value="a">');
+
+ expect(await page.$('text/a')).toBeNull();
+ });
+ it('should query text spanning multiple elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div><span>a</span> <span>b</span><div>');
+
+ using element = await page.$('text/a b');
+ expect(
+ await element?.evaluate(e => {
+ return e.textContent;
+ })
+ ).toBe('a b');
+ });
+ it('should clear caches', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div id=target1>text</div><input id=target2 value=text><div id=target3>text</div>'
+ );
+ using div = (await page.$('#target1')) as ElementHandle<HTMLDivElement>;
+ using input = (await page.$(
+ '#target2'
+ )) as ElementHandle<HTMLInputElement>;
+
+ await div.evaluate(div => {
+ div.textContent = 'text';
+ });
+ expect(
+ await page.$eval(`text/text`, e => {
+ return e.id;
+ })
+ ).toBe('target1');
+ await div.evaluate(div => {
+ div.textContent = 'foo';
+ });
+ expect(
+ await page.$eval(`text/text`, e => {
+ return e.id;
+ })
+ ).toBe('target2');
+ await input.evaluate(input => {
+ input.value = '';
+ });
+ await input.type('foo');
+ expect(
+ await page.$eval(`text/text`, e => {
+ return e.id;
+ })
+ ).toBe('target3');
+
+ await div.evaluate(div => {
+ div.textContent = 'text';
+ });
+ await input.evaluate(input => {
+ input.value = '';
+ });
+ await input.type('text');
+ expect(
+ await page.$$eval(`text/text`, es => {
+ return es.length;
+ })
+ ).toBe(3);
+ await div.evaluate(div => {
+ div.textContent = 'foo';
+ });
+ expect(
+ await page.$$eval(`text/text`, es => {
+ return es.length;
+ })
+ ).toBe(2);
+ await input.evaluate(input => {
+ input.value = '';
+ });
+ await input.type('foo');
+ expect(
+ await page.$$eval(`text/text`, es => {
+ return es.length;
+ })
+ ).toBe(1);
+ });
+ });
+ describe('in ElementHandles', function () {
+ it('should query existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div class="a"><span>a</span></div>');
+
+ using elementHandle = (await page.$('div'))!;
+ expect(await elementHandle.$(`text/a`)).toBeTruthy();
+ expect(await elementHandle.$$(`text/a`)).toHaveLength(1);
+ });
+
+ it('should return null for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div class="a"></div>');
+
+ using elementHandle = (await page.$('div'))!;
+ expect(await elementHandle.$(`text/a`)).toBeFalsy();
+ expect(await elementHandle.$$(`text/a`)).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('XPath selectors', function () {
+ describe('in Page', function () {
+ it('should query existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>test</section>');
+
+ expect(await page.$('xpath/html/body/section')).toBeTruthy();
+ expect(await page.$$('xpath/html/body/section')).toHaveLength(1);
+ });
+ it('should return empty array for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.$('xpath/html/body/non-existing-element')
+ ).toBeFalsy();
+ expect(
+ await page.$$('xpath/html/body/non-existing-element')
+ ).toHaveLength(0);
+ });
+ it('should return first element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>a</div><div></div>');
+
+ using element = await page.$('xpath/html/body/div');
+ expect(
+ await element?.evaluate(e => {
+ return e.textContent === 'a';
+ })
+ ).toBeTruthy();
+ });
+ it('should return multiple elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div></div><div></div>');
+
+ const elements = await page.$$('xpath/html/body/div');
+ expect(elements).toHaveLength(2);
+ });
+ });
+ describe('in ElementHandles', function () {
+ it('should query existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div class="a">a<span></span></div>');
+
+ using elementHandle = (await page.$('div'))!;
+ expect(await elementHandle.$(`xpath/span`)).toBeTruthy();
+ expect(await elementHandle.$$(`xpath/span`)).toHaveLength(1);
+ });
+
+ it('should return null for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div class="a">a</div>');
+
+ using elementHandle = (await page.$('div'))!;
+ expect(await elementHandle.$(`xpath/span`)).toBeFalsy();
+ expect(await elementHandle.$$(`xpath/span`)).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('P selectors', () => {
+ beforeEach(async () => {
+ Puppeteer.clearCustomQueryHandlers();
+ });
+
+ it('should work with CSS selectors', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('div > button');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+
+ // Should parse more complex CSS selectors. Listing a few problematic
+ // cases from bug reports.
+ for (const selector of [
+ '.user_row[data-user-id="\\38 "]:not(.deactivated_user)',
+ `input[value='Search']:not([class='hidden'])`,
+ `[data-test-id^="test-"]:not([data-test-id^="test-foo"])`,
+ ]) {
+ await page.$$(selector);
+ }
+ });
+
+ it('should work with deep combinators', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ {
+ using element = await page.$('div >>>> div');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'c';
+ })
+ ).toBeTruthy();
+ }
+ {
+ const elements = await page.$$('div >>> div');
+ assert(elements[1], 'Could not find element');
+ expect(
+ await elements[1]?.evaluate(element => {
+ return element.id === 'd';
+ })
+ ).toBeTruthy();
+ }
+ {
+ const elements = await page.$$('#c >>>> div');
+ assert(elements[0], 'Could not find element');
+ expect(
+ await elements[0]?.evaluate(element => {
+ return element.id === 'd';
+ })
+ ).toBeTruthy();
+ }
+ {
+ const elements = await page.$$('#c >>> div');
+ assert(elements[0], 'Could not find element');
+ expect(
+ await elements[0]?.evaluate(element => {
+ return element.id === 'd';
+ })
+ ).toBeTruthy();
+ }
+ });
+
+ it('should work with text selectors', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('div ::-p-text(world)');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work ARIA selectors', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('div ::-p-aria(world)');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work for ARIA selectors in multiple isolated worlds', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.waitForSelector('::-p-aria(world)');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ // $ would add ARIA query handler to the main world.
+ await element.$('::-p-aria(world)');
+ using element2 = await page.waitForSelector('::-p-aria(world)');
+ assert(element2, 'Could not find element');
+ expect(
+ await element2.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work ARIA selectors with role', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('::-p-aria(world[role="button"])');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work ARIA selectors with name and role', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('::-p-aria([name="world"][role="button"])');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work XPath selectors', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('div ::-p-xpath(//button)');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work with custom selectors', async () => {
+ Puppeteer.registerCustomQueryHandler('div', {
+ queryOne() {
+ return document.querySelector('div');
+ },
+ });
+
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$('::-p-div');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'a';
+ })
+ ).toBeTruthy();
+ });
+
+ it('should work with custom selectors with args', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ Puppeteer.registerCustomQueryHandler('div', {
+ queryOne(_, selector) {
+ if (selector === 'true') {
+ return document.querySelector('div');
+ } else {
+ return document.querySelector('button');
+ }
+ },
+ });
+
+ {
+ using element = await page.$('::-p-div(true)');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'a';
+ })
+ ).toBeTruthy();
+ }
+ {
+ using element = await page.$('::-p-div("true")');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'a';
+ })
+ ).toBeTruthy();
+ }
+ {
+ using element = await page.$("::-p-div('true')");
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'a';
+ })
+ ).toBeTruthy();
+ }
+ {
+ using element = await page.$('::-p-div');
+ assert(element, 'Could not find element');
+ expect(
+ await element.evaluate(element => {
+ return element.id === 'b';
+ })
+ ).toBeTruthy();
+ }
+ });
+
+ it('should work with :hover', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using button = await page.$('div ::-p-text(world)');
+ assert(button, 'Could not find element');
+ await button.hover();
+
+ using button2 = await page.$('div ::-p-text(world):hover');
+ assert(button2, 'Could not find element');
+ const value = await button2.evaluate(span => {
+ return {textContent: span.textContent, tagName: span.tagName};
+ });
+ expect(value).toMatchObject({textContent: 'world', tagName: 'BUTTON'});
+ });
+
+ it('should work with selector lists', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ const elements = await page.$$('div, ::-p-text(world)');
+ expect(elements).toHaveLength(3);
+ });
+
+ const permute = <T>(inputs: T[]): T[][] => {
+ const results: T[][] = [];
+ for (let i = 0; i < inputs.length; ++i) {
+ const permutation = permute(
+ inputs.slice(0, i).concat(inputs.slice(i + 1))
+ );
+ const value = inputs[i] as T;
+ if (permutation.length === 0) {
+ results.push([value]);
+ continue;
+ }
+ for (const part of permutation) {
+ results.push([value].concat(part));
+ }
+ }
+ return results;
+ };
+
+ it('should match querySelector* ordering', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ for (const list of permute(['div', 'button', 'span'])) {
+ const elements = await page.$$(
+ list
+ .map(selector => {
+ return selector === 'button' ? '::-p-text(world)' : selector;
+ })
+ .join(',')
+ );
+ const actual = await Promise.all(
+ elements.map(element => {
+ return element.evaluate(element => {
+ return element.id;
+ });
+ })
+ );
+ expect(actual.join()).toStrictEqual('a,b,f,c');
+ }
+ });
+
+ it('should not have duplicate elements from selector lists', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ const elements = await page.$$('::-p-text(world), button');
+ expect(elements).toHaveLength(1);
+ });
+
+ it('should handle escapes', async () => {
+ const {server, page} = await getTestState();
+ await page.goto(`${server.PREFIX}/p-selectors.html`);
+ using element = await page.$(
+ ':scope >>> ::-p-text(My name is Jun \\(pronounced like "June"\\))'
+ );
+ expect(element).toBeTruthy();
+ using element2 = await page.$(
+ ':scope >>> ::-p-text("My name is Jun (pronounced like \\"June\\")")'
+ );
+ expect(element2).toBeTruthy();
+ using element3 = await page.$(
+ ':scope >>> ::-p-text(My name is Jun \\(pronounced like "June"\\)")'
+ );
+ expect(element3).toBeFalsy();
+ using element4 = await page.$(
+ ':scope >>> ::-p-text("My name is Jun \\(pronounced like "June"\\))'
+ );
+ expect(element4).toBeFalsy();
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/queryselector.spec.ts b/remote/test/puppeteer/test/src/queryselector.spec.ts
new file mode 100644
index 0000000000..7fd27f914f
--- /dev/null
+++ b/remote/test/puppeteer/test/src/queryselector.spec.ts
@@ -0,0 +1,491 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import expect from 'expect';
+import {Puppeteer} from 'puppeteer';
+import type {CustomQueryHandler} from 'puppeteer-core/internal/common/CustomQueryHandler.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('querySelector', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.$eval', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section id="testAttribute">43543</section>');
+ const idAttribute = await page.$eval('section', e => {
+ return e.id;
+ });
+ expect(idAttribute).toBe('testAttribute');
+ });
+ it('should accept arguments', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>hello</section>');
+ const text = await page.$eval(
+ 'section',
+ (e, suffix) => {
+ return e.textContent! + suffix;
+ },
+ ' world!'
+ );
+ expect(text).toBe('hello world!');
+ });
+ it('should accept ElementHandles as arguments', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>hello</section><div> world</div>');
+ using divHandle = (await page.$('div'))!;
+ const text = await page.$eval(
+ 'section',
+ (e, div) => {
+ return e.textContent! + (div as HTMLElement).textContent!;
+ },
+ divHandle
+ );
+ expect(text).toBe('hello world');
+ });
+ it('should throw error if no element is found', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .$eval('section', e => {
+ return e.id;
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error.message).toContain(
+ 'failed to find element matching selector "section"'
+ );
+ });
+ });
+
+ // The tests for $$eval are repeated later in this file in the test group 'QueryAll'.
+ // This is done to also test a query handler where QueryAll returns an Element[]
+ // as opposed to NodeListOf<Element>.
+ describe('Page.$$eval', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div>hello</div><div>beautiful</div><div>world!</div>'
+ );
+ const divsCount = await page.$$eval('div', divs => {
+ return divs.length;
+ });
+ expect(divsCount).toBe(3);
+ });
+ it('should accept extra arguments', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div>hello</div><div>beautiful</div><div>world!</div>'
+ );
+ const divsCountPlus5 = await page.$$eval(
+ 'div',
+ (divs, two, three) => {
+ return divs.length + (two as number) + (three as number);
+ },
+ 2,
+ 3
+ );
+ expect(divsCountPlus5).toBe(8);
+ });
+ it('should accept ElementHandles as arguments', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<section>2</section><section>2</section><section>1</section><div>3</div>'
+ );
+ using divHandle = (await page.$('div'))!;
+ const sum = await page.$$eval(
+ 'section',
+ (sections, div) => {
+ return (
+ sections.reduce((acc, section) => {
+ return acc + Number(section.textContent);
+ }, 0) + Number((div as HTMLElement).textContent)
+ );
+ },
+ divHandle
+ );
+ expect(sum).toBe(8);
+ });
+ it('should handle many elements', async function () {
+ this.timeout(25_000);
+
+ const {page} = await getTestState();
+ await page.evaluate(
+ `
+ for (var i = 0; i <= 1000; i++) {
+ const section = document.createElement('section');
+ section.textContent = i;
+ document.body.appendChild(section);
+ }
+ `
+ );
+ const sum = await page.$$eval('section', sections => {
+ return sections.reduce((acc, section) => {
+ return acc + Number(section.textContent);
+ }, 0);
+ });
+ expect(sum).toBe(500500);
+ });
+ });
+
+ describe('Page.$', function () {
+ it('should query existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>test</section>');
+ using element = (await page.$('section'))!;
+ expect(element).toBeTruthy();
+ });
+ it('should return null for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ using element = (await page.$('non-existing-element'))!;
+ expect(element).toBe(null);
+ });
+ });
+
+ describe('Page.$$', function () {
+ it('should query existing elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div>A</div><br/><div>B</div>');
+ const elements = await page.$$('div');
+ expect(elements).toHaveLength(2);
+ const promises = elements.map(element => {
+ return page.evaluate((e: HTMLElement) => {
+ return e.textContent;
+ }, element);
+ });
+ expect(await Promise.all(promises)).toEqual(['A', 'B']);
+ });
+ it('should return empty array if nothing is found', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const elements = await page.$$('div');
+ expect(elements).toHaveLength(0);
+ });
+ });
+
+ describe('Page.$x', function () {
+ it('should query existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<section>test</section>');
+ const elements = await page.$x('/html/body/section');
+ expect(elements[0]).toBeTruthy();
+ expect(elements).toHaveLength(1);
+ });
+ it('should return empty array for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ const element = await page.$x('/html/body/non-existing-element');
+ expect(element).toEqual([]);
+ });
+ it('should return multiple elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div></div><div></div>');
+ const elements = await page.$x('/html/body/div');
+ expect(elements).toHaveLength(2);
+ });
+ });
+
+ describe('ElementHandle.$', function () {
+ it('should query existing element', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/playground.html');
+ await page.setContent(
+ '<html><body><div class="second"><div class="inner">A</div></div></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ using second = (await html.$('.second'))!;
+ using inner = await second.$('.inner');
+ const content = await page.evaluate(e => {
+ return e?.textContent;
+ }, inner);
+ expect(content).toBe('A');
+ });
+
+ it('should return null for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><div class="second"><div class="inner">B</div></div></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ using second = await html.$('.third');
+ expect(second).toBe(null);
+ });
+ });
+ describe('ElementHandle.$eval', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><div class="tweet"><div class="like">100</div><div class="retweets">10</div></div></body></html>'
+ );
+ using tweet = (await page.$('.tweet'))!;
+ const content = await tweet.$eval('.like', node => {
+ return (node as HTMLElement).innerText;
+ });
+ expect(content).toBe('100');
+ });
+
+ it('should retrieve content from subtree', async () => {
+ const {page} = await getTestState();
+
+ const htmlContent =
+ '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a-child-div</div></div>';
+ await page.setContent(htmlContent);
+ using elementHandle = (await page.$('#myId'))!;
+ const content = await elementHandle.$eval('.a', node => {
+ return (node as HTMLElement).innerText;
+ });
+ expect(content).toBe('a-child-div');
+ });
+
+ it('should throw in case of missing selector', async () => {
+ const {page} = await getTestState();
+
+ const htmlContent =
+ '<div class="a">not-a-child-div</div><div id="myId"></div>';
+ await page.setContent(htmlContent);
+ using elementHandle = (await page.$('#myId'))!;
+ const errorMessage = await elementHandle
+ .$eval('.a', node => {
+ return (node as HTMLElement).innerText;
+ })
+ .catch(error => {
+ return error.message;
+ });
+ expect(errorMessage).toBe(
+ `Error: failed to find element matching selector ".a"`
+ );
+ });
+ });
+ describe('ElementHandle.$$eval', function () {
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><div class="tweet"><div class="like">100</div><div class="like">10</div></div></body></html>'
+ );
+ using tweet = (await page.$('.tweet'))!;
+ const content = await tweet.$$eval('.like', nodes => {
+ return (nodes as HTMLElement[]).map(n => {
+ return n.innerText;
+ });
+ });
+ expect(content).toEqual(['100', '10']);
+ });
+
+ it('should retrieve content from subtree', async () => {
+ const {page} = await getTestState();
+
+ const htmlContent =
+ '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a1-child-div</div><div class="a">a2-child-div</div></div>';
+ await page.setContent(htmlContent);
+ using elementHandle = (await page.$('#myId'))!;
+ const content = await elementHandle.$$eval('.a', nodes => {
+ return (nodes as HTMLElement[]).map(n => {
+ return n.innerText;
+ });
+ });
+ expect(content).toEqual(['a1-child-div', 'a2-child-div']);
+ });
+
+ it('should not throw in case of missing selector', async () => {
+ const {page} = await getTestState();
+
+ const htmlContent =
+ '<div class="a">not-a-child-div</div><div id="myId"></div>';
+ await page.setContent(htmlContent);
+ using elementHandle = (await page.$('#myId'))!;
+ const nodesLength = await elementHandle.$$eval('.a', nodes => {
+ return nodes.length;
+ });
+ expect(nodesLength).toBe(0);
+ });
+ });
+
+ describe('ElementHandle.$$', function () {
+ it('should query existing elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><div>A</div><br/><div>B</div></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ const elements = await html.$$('div');
+ expect(elements).toHaveLength(2);
+ const promises = elements.map(element => {
+ return page.evaluate((e: HTMLElement) => {
+ return e.textContent;
+ }, element);
+ });
+ expect(await Promise.all(promises)).toEqual(['A', 'B']);
+ });
+
+ it('should return empty array for non-existing elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><span>A</span><br/><span>B</span></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ const elements = await html.$$('div');
+ expect(elements).toHaveLength(0);
+ });
+ });
+
+ describe('ElementHandle.$x', function () {
+ it('should query existing element', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.PREFIX + '/playground.html');
+ await page.setContent(
+ '<html><body><div class="second"><div class="inner">A</div></div></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ const second = await html.$x(`./body/div[contains(@class, 'second')]`);
+ const inner = await second[0]!.$x(`./div[contains(@class, 'inner')]`);
+ const content = await page.evaluate(e => {
+ return e.textContent;
+ }, inner[0]!);
+ expect(content).toBe('A');
+ });
+
+ it('should return null for non-existing element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><div class="second"><div class="inner">B</div></div></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ const second = await html.$x(`/div[contains(@class, 'third')]`);
+ expect(second).toEqual([]);
+ });
+ });
+
+ // This is the same tests for `$$eval` and `$$` as above, but with a queryAll
+ // handler that returns an array instead of a list of nodes.
+ describe('QueryAll', function () {
+ const handler: CustomQueryHandler = {
+ queryAll: (element, selector) => {
+ return [...(element as Element).querySelectorAll(selector)];
+ },
+ };
+ before(() => {
+ Puppeteer.registerCustomQueryHandler('allArray', handler);
+ });
+
+ it('should have registered handler', async () => {
+ expect(
+ Puppeteer.customQueryHandlerNames().includes('allArray')
+ ).toBeTruthy();
+ });
+ it('$$ should query existing elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><div>A</div><br/><div>B</div></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ const elements = await html.$$('allArray/div');
+ expect(elements).toHaveLength(2);
+ const promises = elements.map(element => {
+ return page.evaluate(e => {
+ return e.textContent;
+ }, element);
+ });
+ expect(await Promise.all(promises)).toEqual(['A', 'B']);
+ });
+
+ it('$$ should return empty array for non-existing elements', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<html><body><span>A</span><br/><span>B</span></body></html>'
+ );
+ using html = (await page.$('html'))!;
+ const elements = await html.$$('allArray/div');
+ expect(elements).toHaveLength(0);
+ });
+ it('$$eval should work', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div>hello</div><div>beautiful</div><div>world!</div>'
+ );
+ const divsCount = await page.$$eval('allArray/div', divs => {
+ return divs.length;
+ });
+ expect(divsCount).toBe(3);
+ });
+ it('$$eval should accept extra arguments', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<div>hello</div><div>beautiful</div><div>world!</div>'
+ );
+ const divsCountPlus5 = await page.$$eval(
+ 'allArray/div',
+ (divs, two, three) => {
+ return divs.length + (two as number) + (three as number);
+ },
+ 2,
+ 3
+ );
+ expect(divsCountPlus5).toBe(8);
+ });
+ it('$$eval should accept ElementHandles as arguments', async () => {
+ const {page} = await getTestState();
+ await page.setContent(
+ '<section>2</section><section>2</section><section>1</section><div>3</div>'
+ );
+ using divHandle = (await page.$('div'))!;
+ const sum = await page.$$eval(
+ 'allArray/section',
+ (sections, div) => {
+ return (
+ sections.reduce((acc, section) => {
+ return acc + Number(section.textContent);
+ }, 0) + Number((div as HTMLElement).textContent)
+ );
+ },
+ divHandle
+ );
+ expect(sum).toBe(8);
+ });
+ it('$$eval should handle many elements', async function () {
+ this.timeout(25_000);
+
+ const {page} = await getTestState();
+ await page.evaluate(
+ `
+ for (var i = 0; i <= 1000; i++) {
+ const section = document.createElement('section');
+ section.textContent = i;
+ document.body.appendChild(section);
+ }
+ `
+ );
+ const sum = await page.$$eval('allArray/section', sections => {
+ return sections.reduce((acc, section) => {
+ return acc + Number(section.textContent);
+ }, 0);
+ });
+ expect(sum).toBe(500500);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts b/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts
new file mode 100644
index 0000000000..966554fd5d
--- /dev/null
+++ b/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts
@@ -0,0 +1,969 @@
+/**
+ * @license
+ * Copyright 2021 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+import expect from 'expect';
+import {
+ type ActionResult,
+ type HTTPRequest,
+ InterceptResolutionAction,
+} from 'puppeteer-core/internal/api/HTTPRequest.js';
+import type {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {isFavicon, waitEvent} from './utils.js';
+
+describe('cooperative request interception', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.setRequestInterception', function () {
+ const expectedActions: ActionResult[] = ['abort', 'continue', 'respond'];
+ while (expectedActions.length > 0) {
+ const expectedAction = expectedActions.pop();
+ it(`should cooperatively ${expectedAction} by priority`, async () => {
+ const {page, server} = await getTestState();
+
+ const actionResults: ActionResult[] = [];
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (request.url().endsWith('.css')) {
+ void request.continue(
+ {headers: {...request.headers(), xaction: 'continue'}},
+ expectedAction === 'continue' ? 1 : 0
+ );
+ } else {
+ void request.continue({}, 0);
+ }
+ });
+ page.on('request', request => {
+ if (request.url().endsWith('.css')) {
+ void request.respond(
+ {headers: {xaction: 'respond'}},
+ expectedAction === 'respond' ? 1 : 0
+ );
+ } else {
+ void request.continue({}, 0);
+ }
+ });
+ page.on('request', request => {
+ if (request.url().endsWith('.css')) {
+ void request.abort('aborted', expectedAction === 'abort' ? 1 : 0);
+ } else {
+ void request.continue({}, 0);
+ }
+ });
+ page.on('response', response => {
+ const {xaction} = response!.headers();
+ if (response!.url().endsWith('.css') && !!xaction) {
+ actionResults.push(xaction as ActionResult);
+ }
+ });
+ page.on('requestfailed', request => {
+ if (request.url().endsWith('.css')) {
+ actionResults.push('abort');
+ }
+ });
+
+ const response = (await (async () => {
+ if (expectedAction === 'continue') {
+ const [serverRequest, response] = await Promise.all([
+ server.waitForRequest('/one-style.css'),
+ page.goto(server.PREFIX + '/one-style.html'),
+ ]);
+ actionResults.push(
+ serverRequest.headers['xaction'] as ActionResult
+ );
+ return response;
+ } else {
+ return await page.goto(server.PREFIX + '/one-style.html');
+ }
+ })())!;
+
+ expect(actionResults).toHaveLength(1);
+ expect(actionResults[0]).toBe(expectedAction);
+ expect(response!.ok()).toBe(true);
+ });
+ }
+
+ it('should intercept', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (isFavicon(request)) {
+ void request.continue({}, 0);
+ return;
+ }
+ expect(request.url()).toContain('empty.html');
+ expect(request.headers()['user-agent']).toBeTruthy();
+ expect(request.method()).toBe('GET');
+ expect(request.postData()).toBe(undefined);
+ expect(request.isNavigationRequest()).toBe(true);
+ expect(request.resourceType()).toBe('document');
+ expect(request.frame() === page.mainFrame()).toBe(true);
+ expect(request.frame()!.url()).toBe('about:blank');
+ void request.continue({}, 0);
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response!.ok()).toBe(true);
+ expect(response!.remoteAddress().port).toBe(server.PORT);
+ });
+ // @see https://github.com/puppeteer/puppeteer/pull/3105
+ it('should work when POST is redirected with 302', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/rredirect', '/empty.html');
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ await page.setContent(`
+ <form action='/rredirect' method='post'>
+ <input type="hidden" id="foo" name="foo" value="FOOBAR">
+ </form>
+ `);
+ await Promise.all([
+ page.$eval('form', form => {
+ return (form as HTMLFormElement).submit();
+ }),
+ page.waitForNavigation(),
+ ]);
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/3973
+ it('should work when header manipulation headers with redirect', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/rrredirect', '/empty.html');
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const headers = Object.assign({}, request.headers(), {
+ foo: 'bar',
+ });
+ void request.continue({headers}, 0);
+
+ expect(request.continueRequestOverrides()).toEqual({headers});
+ });
+ // Make sure that the goto does not time out.
+ await page.goto(server.PREFIX + '/rrredirect');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4743
+ it('should be able to remove headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const headers = Object.assign({}, request.headers(), {
+ foo: 'bar',
+ origin: undefined, // remove "origin" header
+ });
+ void request.continue({headers}, 0);
+ });
+
+ const [serverRequest] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.PREFIX + '/empty.html'),
+ ]);
+
+ expect(serverRequest.headers.origin).toBe(undefined);
+ });
+ it('should contain referer header', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ if (!isFavicon(request)) {
+ requests.push(request);
+ }
+ void request.continue({}, 0);
+ });
+ await page.goto(server.PREFIX + '/one-style.html');
+ expect(requests[1]!.url()).toContain('/one-style.css');
+ expect(requests[1]!.headers()['referer']).toContain('/one-style.html');
+ });
+ it('should properly return navigation response when URL has cookies', async () => {
+ const {page, server} = await getTestState();
+
+ // Setup cookie.
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({name: 'foo', value: 'bar'});
+
+ // Setup request interception.
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ const response = await page.reload();
+ expect(response!.status()).toBe(200);
+ });
+ it('should stop intercepting', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.once('request', request => {
+ return request.continue({}, 0);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(false);
+ await page.goto(server.EMPTY_PAGE);
+ });
+ it('should show custom HTTP headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({
+ foo: 'bar',
+ });
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ expect(request.headers()['foo']).toBe('bar');
+ void request.continue({}, 0);
+ });
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response!.ok()).toBe(true);
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4337
+ it('should work with redirect inside sync XHR', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ server.setRedirect('/logo.png', '/pptr.png');
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ const status = await page.evaluate(async () => {
+ const request = new XMLHttpRequest();
+ request.open('GET', '/logo.png', false); // `false` makes the request synchronous
+ request.send(null);
+ return request.status;
+ });
+ expect(status).toBe(200);
+ });
+ it('should work with custom referer headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({referer: server.EMPTY_PAGE});
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
+ void request.continue({}, 0);
+ });
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response!.ok()).toBe(true);
+ });
+ it('should be abortable', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (request.url().endsWith('.css')) {
+ void request.abort('failed', 0);
+ } else {
+ void request.continue({}, 0);
+ }
+ });
+ let failedRequests = 0;
+ page.on('requestfailed', () => {
+ return ++failedRequests;
+ });
+ const response = await page.goto(server.PREFIX + '/one-style.html');
+ expect(response!.ok()).toBe(true);
+ expect(response!.request().failure()).toBe(null);
+ expect(failedRequests).toBe(1);
+ });
+ it('should be able to access the error reason', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.abort('failed', 0);
+ });
+ let abortReason = null;
+ page.on('request', request => {
+ abortReason = request.abortErrorReason();
+ void request.continue({}, 0);
+ });
+ await page.goto(server.EMPTY_PAGE).catch(() => {});
+ expect(abortReason).toBe('Failed');
+ });
+ it('should be abortable with custom error codes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.abort('internetdisconnected', 0);
+ });
+
+ const [failedRequest] = await Promise.all([
+ waitEvent<HTTPRequest>(page, 'requestfailed'),
+ page.goto(server.EMPTY_PAGE).catch(() => {}),
+ ]);
+ expect(failedRequest).toBeTruthy();
+ expect(failedRequest.failure()!.errorText).toBe(
+ 'net::ERR_INTERNET_DISCONNECTED'
+ );
+ });
+ it('should send referer', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({
+ referer: 'http://google.com/',
+ });
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ const [request] = await Promise.all([
+ server.waitForRequest('/grid.html'),
+ page.goto(server.PREFIX + '/grid.html'),
+ ]);
+ expect(request.headers['referer']).toBe('http://google.com/');
+ });
+ it('should fail navigation when aborting main resource', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.abort('failed', 0);
+ });
+ let error!: Error;
+ await page.goto(server.EMPTY_PAGE).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ if (isChrome) {
+ expect(error.message).toContain('net::ERR_FAILED');
+ } else {
+ expect(error.message).toContain('NS_ERROR_FAILURE');
+ }
+ });
+ it('should work with redirects', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ void request.continue({}, 0);
+ requests.push(request);
+ });
+ server.setRedirect(
+ '/non-existing-page.html',
+ '/non-existing-page-2.html'
+ );
+ server.setRedirect(
+ '/non-existing-page-2.html',
+ '/non-existing-page-3.html'
+ );
+ server.setRedirect(
+ '/non-existing-page-3.html',
+ '/non-existing-page-4.html'
+ );
+ server.setRedirect('/non-existing-page-4.html', '/empty.html');
+ const response = await page.goto(
+ server.PREFIX + '/non-existing-page.html'
+ );
+ expect(response!.status()).toBe(200);
+ expect(response!.url()).toContain('empty.html');
+ expect(requests).toHaveLength(5);
+ expect(requests[2]!.resourceType()).toBe('document');
+ // Check redirect chain
+ const redirectChain = response!.request().redirectChain();
+ expect(redirectChain).toHaveLength(4);
+ expect(redirectChain[0]!.url()).toContain('/non-existing-page.html');
+ expect(redirectChain[2]!.url()).toContain('/non-existing-page-3.html');
+ for (let i = 0; i < redirectChain.length; ++i) {
+ const request = redirectChain[i]!;
+ expect(request.isNavigationRequest()).toBe(true);
+ expect(request.redirectChain().indexOf(request)).toBe(i);
+ }
+ });
+ it('should work with redirects for subresources', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ void request.continue({}, 0);
+ if (!isFavicon(request)) {
+ requests.push(request);
+ }
+ });
+ server.setRedirect('/one-style.css', '/two-style.css');
+ server.setRedirect('/two-style.css', '/three-style.css');
+ server.setRedirect('/three-style.css', '/four-style.css');
+ server.setRoute('/four-style.css', (_req, res) => {
+ return res.end('body {box-sizing: border-box; }');
+ });
+
+ const response = await page.goto(server.PREFIX + '/one-style.html');
+ expect(response!.status()).toBe(200);
+ expect(response!.url()).toContain('one-style.html');
+ expect(requests).toHaveLength(5);
+ expect(requests[0]!.resourceType()).toBe('document');
+ expect(requests[1]!.resourceType()).toBe('stylesheet');
+ // Check redirect chain
+ const redirectChain = requests[1]!.redirectChain();
+ expect(redirectChain).toHaveLength(3);
+ expect(redirectChain[0]!.url()).toContain('/one-style.css');
+ expect(redirectChain[2]!.url()).toContain('/three-style.css');
+ });
+ it('should be able to abort redirects', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.setRequestInterception(true);
+ server.setRedirect('/non-existing.json', '/non-existing-2.json');
+ server.setRedirect('/non-existing-2.json', '/simple.html');
+ page.on('request', request => {
+ if (request.url().includes('non-existing-2')) {
+ void request.abort('failed', 0);
+ } else {
+ void request.continue({}, 0);
+ }
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const result = await page.evaluate(async () => {
+ try {
+ return await fetch('/non-existing.json');
+ } catch (error) {
+ return (error as Error).message;
+ }
+ });
+ if (isChrome) {
+ expect(result).toContain('Failed to fetch');
+ } else {
+ expect(result).toContain('NetworkError');
+ }
+ });
+ it('should work with equal requests', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let responseCount = 1;
+ server.setRoute('/zzz', (_req, res) => {
+ return res.end(responseCount++ * 11 + '');
+ });
+ await page.setRequestInterception(true);
+
+ let spinner = false;
+ // Cancel 2nd request.
+ page.on('request', request => {
+ if (isFavicon(request)) {
+ void request.continue({}, 0);
+ return;
+ }
+ void (spinner ? request.abort('failed', 0) : request.continue({}, 0));
+ spinner = !spinner;
+ });
+ const results = await page.evaluate(() => {
+ return Promise.all([
+ fetch('/zzz')
+ .then(response => {
+ return response!.text();
+ })
+ .catch(() => {
+ return 'FAILED';
+ }),
+ fetch('/zzz')
+ .then(response => {
+ return response!.text();
+ })
+ .catch(() => {
+ return 'FAILED';
+ }),
+ fetch('/zzz')
+ .then(response => {
+ return response!.text();
+ })
+ .catch(() => {
+ return 'FAILED';
+ }),
+ ]);
+ });
+ expect(results).toEqual(['11', 'FAILED', '22']);
+ });
+ it('should navigate to dataURL and fire dataURL requests', async () => {
+ const {page} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ requests.push(request);
+ void request.continue({}, 0);
+ });
+ const dataURL = 'data:text/html,<div>yo</div>';
+ const response = await page.goto(dataURL);
+ expect(response!.status()).toBe(200);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(dataURL);
+ });
+ it('should be able to fetch dataURL and fire dataURL requests', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ !isFavicon(request) && requests.push(request);
+ void request.continue({}, 0);
+ });
+ const dataURL = 'data:text/html,<div>yo</div>';
+ const text = await page.evaluate((url: string) => {
+ return fetch(url).then(r => {
+ return r.text();
+ });
+ }, dataURL);
+ expect(text).toBe('<div>yo</div>');
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(dataURL);
+ });
+ it('should navigate to URL with hash and fire requests without hash', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ requests.push(request);
+ void request.continue({}, 0);
+ });
+ const response = await page.goto(server.EMPTY_PAGE + '#hash');
+ expect(response!.status()).toBe(200);
+ expect(response!.url()).toBe(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should work with encoded server', async () => {
+ const {page, server} = await getTestState();
+
+ // The requestWillBeSent will report encoded URL, whereas interception will
+ // report URL as-is. @see crbug.com/759388
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ const response = await page.goto(
+ server.PREFIX + '/some nonexisting page'
+ );
+ expect(response!.status()).toBe(404);
+ });
+ it('should work with badly encoded server', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ server.setRoute('/malformed?rnd=%911', (_req, res) => {
+ return res.end();
+ });
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ const response = await page.goto(server.PREFIX + '/malformed?rnd=%911');
+ expect(response!.status()).toBe(200);
+ });
+ it('should work with encoded server - 2', async () => {
+ const {page, server} = await getTestState();
+
+ // The requestWillBeSent will report URL as-is, whereas interception will
+ // report encoded URL for stylesheet. @see crbug.com/759388
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ void request.continue({}, 0);
+ requests.push(request);
+ });
+ const response = await page.goto(
+ `data:text/html,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>`
+ );
+ expect(response!.status()).toBe(200);
+ expect(requests).toHaveLength(2);
+ expect(requests[1]!.response()!.status()).toBe(404);
+ });
+ it('should not throw "Invalid Interception Id" if the request was cancelled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setContent('<iframe></iframe>');
+ await page.setRequestInterception(true);
+ let request!: HTTPRequest;
+ page.on('request', async r => {
+ return (request = r);
+ });
+ void (page.$eval(
+ 'iframe',
+ (frame, url) => {
+ return ((frame as HTMLIFrameElement).src = url as string);
+ },
+ server.EMPTY_PAGE
+ ),
+ // Wait for request interception.
+ await waitEvent(page, 'request'));
+ // Delete frame to cause request to be canceled.
+ await page.$eval('iframe', frame => {
+ return frame.remove();
+ });
+ let error!: Error;
+ await request.continue({}, 0).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeUndefined();
+ });
+ it('should throw if interception is not enabled', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ page.on('request', async request => {
+ try {
+ await request.continue({}, 0);
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(error.message).toContain('Request Interception is not enabled');
+ });
+ it('should work with file URLs', async () => {
+ const {page} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const urls = new Set();
+ page.on('request', request => {
+ urls.add(request.url().split('/').pop());
+ void request.continue({}, 0);
+ });
+ await page.goto(
+ pathToFileURL(path.join(__dirname, '../assets', 'one-style.html'))
+ );
+ expect(urls.size).toBe(2);
+ expect(urls.has('one-style.html')).toBe(true);
+ expect(urls.has('one-style.css')).toBe(true);
+ });
+ it('should not cache if cache disabled', async () => {
+ const {page, server} = await getTestState();
+
+ // Load and re-load to make sure it's cached.
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+
+ await page.setRequestInterception(true);
+ await page.setCacheEnabled(false);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+
+ const cached: HTTPRequest[] = [];
+ page.on('requestservedfromcache', r => {
+ return cached.push(r);
+ });
+
+ await page.reload();
+ expect(cached).toHaveLength(0);
+ });
+ it('should cache if cache enabled', async () => {
+ const {page, server} = await getTestState();
+
+ // Load and re-load to make sure it's cached.
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+
+ await page.setRequestInterception(true);
+ await page.setCacheEnabled(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+
+ const cached: HTTPRequest[] = [];
+ page.on('requestservedfromcache', r => {
+ return cached.push(r);
+ });
+
+ await page.reload();
+ expect(cached).toHaveLength(1);
+ });
+ it('should load fonts if cache enabled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ await page.setCacheEnabled(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+
+ await page.goto(server.PREFIX + '/cached/one-style-font.html');
+ await page.waitForResponse(r => {
+ return r.url().endsWith('/one-style.woff');
+ });
+ });
+ });
+
+ describe('Request.continue', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue({}, 0);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ });
+ it('should amend HTTP headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const headers = Object.assign({}, request.headers());
+ headers['FOO'] = 'bar';
+ void request.continue({headers}, 0);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => {
+ return fetch('/sleep.zzz');
+ }),
+ ]);
+ expect(request.headers['foo']).toBe('bar');
+ });
+ it('should redirect in a way non-observable to page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const redirectURL = request.url().includes('/empty.html')
+ ? server.PREFIX + '/consolelog.html'
+ : undefined;
+ void request.continue({url: redirectURL}, 0);
+ });
+
+ const [consoleMessage] = await Promise.all([
+ waitEvent<ConsoleMessage>(page, 'console'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ expect(consoleMessage.text()).toBe('yellow');
+ });
+ it('should amend method', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue({method: 'POST'}, 0);
+ });
+ const [request] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => {
+ return fetch('/sleep.zzz');
+ }),
+ ]);
+ expect(request.method).toBe('POST');
+ });
+ it('should amend post data', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue({postData: 'doggo'}, 0);
+ });
+ const [serverRequest] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => {
+ return fetch('/sleep.zzz', {method: 'POST', body: 'birdy'});
+ }),
+ ]);
+ expect(await serverRequest.postBody).toBe('doggo');
+ });
+ it('should amend both post data and method on navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue({method: 'POST', postData: 'doggo'}, 0);
+ });
+ const [serverRequest] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(serverRequest.method).toBe('POST');
+ expect(await serverRequest.postBody).toBe('doggo');
+ });
+ });
+
+ describe('Request.respond', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond(
+ {
+ status: 201,
+ headers: {
+ foo: 'bar',
+ },
+ body: 'Yo, page!',
+ },
+ 0
+ );
+ });
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response!.status()).toBe(201);
+ expect(response!.headers()['foo']).toBe('bar');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent;
+ })
+ ).toBe('Yo, page!');
+ });
+ it('should be able to access the response', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond(
+ {
+ status: 200,
+ body: 'Yo, page!',
+ },
+ 0
+ );
+ });
+ let response = null;
+ page.on('request', request => {
+ response = request.responseForRequest();
+ void request.continue({}, 0);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(response).toEqual({status: 200, body: 'Yo, page!'});
+ });
+ it('should work with status code 422', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond(
+ {
+ status: 422,
+ body: 'Yo, page!',
+ },
+ 0
+ );
+ });
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response!.status()).toBe(422);
+ expect(response!.statusText()).toBe('Unprocessable Entity');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent;
+ })
+ ).toBe('Yo, page!');
+ });
+ it('should redirect', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (!request.url().includes('rrredirect')) {
+ void request.continue({}, 0);
+ return;
+ }
+ void request.respond(
+ {
+ status: 302,
+ headers: {
+ location: server.EMPTY_PAGE,
+ },
+ },
+ 0
+ );
+ });
+ const response = await page.goto(server.PREFIX + '/rrredirect');
+ expect(response!.request().redirectChain()).toHaveLength(1);
+ expect(response!.request().redirectChain()[0]!.url()).toBe(
+ server.PREFIX + '/rrredirect'
+ );
+ expect(response!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should allow mocking binary responses', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const imageBuffer = fs.readFileSync(
+ path.join(__dirname, '../assets', 'pptr.png')
+ );
+ void request.respond(
+ {
+ contentType: 'image/png',
+ body: imageBuffer,
+ },
+ 0
+ );
+ });
+ await page.evaluate(PREFIX => {
+ const img = document.createElement('img');
+ img.src = PREFIX + '/does-not-exist.png';
+ document.body.appendChild(img);
+ return new Promise(fulfill => {
+ return (img.onload = fulfill);
+ });
+ }, server.PREFIX);
+ using img = (await page.$('img'))!;
+ expect(await img.screenshot()).toBeGolden('mock-binary-response.png');
+ });
+ it('should stringify intercepted request response headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond(
+ {
+ status: 200,
+ headers: {
+ foo: true,
+ },
+ body: 'Yo, page!',
+ },
+ 0
+ );
+ });
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response!.status()).toBe(200);
+ const headers = response!.headers();
+ expect(headers['foo']).toBe('true');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent;
+ })
+ ).toBe('Yo, page!');
+ });
+ it('should indicate already-handled if an intercept has been handled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue();
+ });
+ page.on('request', request => {
+ expect(request.isInterceptResolutionHandled()).toBeTruthy();
+ });
+ page.on('request', request => {
+ const {action} = request.interceptResolutionState();
+ expect(action).toBe(InterceptResolutionAction.AlreadyHandled);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ });
+ });
+});
+
+function pathToFileURL(path: string): string {
+ let pathName = path.replace(/\\/g, '/');
+ // Windows drive letter must be prefixed with a slash.
+ if (!pathName.startsWith('/')) {
+ pathName = '/' + pathName;
+ }
+ return 'file://' + pathName;
+}
diff --git a/remote/test/puppeteer/test/src/requestinterception.spec.ts b/remote/test/puppeteer/test/src/requestinterception.spec.ts
new file mode 100644
index 0000000000..45827bb3cf
--- /dev/null
+++ b/remote/test/puppeteer/test/src/requestinterception.spec.ts
@@ -0,0 +1,920 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+import expect from 'expect';
+import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js';
+import type {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {isFavicon, waitEvent} from './utils.js';
+
+describe('request interception', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.setRequestInterception', function () {
+ it('should intercept', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (isFavicon(request)) {
+ void request.continue();
+ return;
+ }
+ expect(request.url()).toContain('empty.html');
+ expect(request.headers()['user-agent']).toBeTruthy();
+ expect(request.headers()['accept']).toBeTruthy();
+ expect(request.method()).toBe('GET');
+ expect(request.postData()).toBe(undefined);
+ expect(request.isNavigationRequest()).toBe(true);
+ expect(request.resourceType()).toBe('document');
+ expect(request.frame() === page.mainFrame()).toBe(true);
+ expect(request.frame()!.url()).toBe('about:blank');
+ void request.continue();
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.ok()).toBe(true);
+ expect(response.remoteAddress().port).toBe(server.PORT);
+ });
+ // @see https://github.com/puppeteer/puppeteer/pull/3105
+ it('should work when POST is redirected with 302', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/rredirect', '/empty.html');
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ await page.setContent(`
+ <form action='/rredirect' method='post'>
+ <input type="hidden" id="foo" name="foo" value="FOOBAR">
+ </form>
+ `);
+ await Promise.all([
+ page.$eval('form', form => {
+ return (form as HTMLFormElement).submit();
+ }),
+ page.waitForNavigation(),
+ ]);
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/3973
+ it('should work when header manipulation headers with redirect', async () => {
+ const {page, server} = await getTestState();
+
+ server.setRedirect('/rrredirect', '/empty.html');
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const headers = Object.assign({}, request.headers(), {
+ foo: 'bar',
+ });
+ void request.continue({headers});
+ });
+ await page.goto(server.PREFIX + '/rrredirect');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4743
+ it('should be able to remove headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const headers = Object.assign({}, request.headers(), {
+ foo: 'bar',
+ origin: undefined, // remove "origin" header
+ });
+ void request.continue({headers});
+ });
+
+ const [serverRequest] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.PREFIX + '/empty.html'),
+ ]);
+
+ expect(serverRequest.headers.origin).toBe(undefined);
+ });
+ it('should contain referer header', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ if (!isFavicon(request)) {
+ requests.push(request);
+ }
+ void request.continue();
+ });
+ await page.goto(server.PREFIX + '/one-style.html');
+ expect(requests[1]!.url()).toContain('/one-style.css');
+ expect(requests[1]!.headers()['referer']).toContain('/one-style.html');
+ });
+ it('should work with requests without networkId', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+
+ const cdp = await page.target().createCDPSession();
+ await cdp.send('DOM.enable');
+ const urls: string[] = [];
+ page.on('request', request => {
+ urls.push(request.url());
+ return request.continue();
+ });
+ // This causes network requests without networkId.
+ await cdp.send('CSS.enable');
+ expect(urls).toStrictEqual([server.EMPTY_PAGE]);
+ });
+ it('should properly return navigation response when URL has cookies', async () => {
+ const {page, server} = await getTestState();
+
+ // Setup cookie.
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({name: 'foo', value: 'bar'});
+
+ // Setup request interception.
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ const response = (await page.reload())!;
+ expect(response.status()).toBe(200);
+ });
+ it('should stop intercepting', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.once('request', request => {
+ return request.continue();
+ });
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(false);
+ await page.goto(server.EMPTY_PAGE);
+ });
+ it('should show custom HTTP headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({
+ foo: 'bar',
+ });
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ expect(request.headers()['foo']).toBe('bar');
+ void request.continue();
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.ok()).toBe(true);
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4337
+ it('should work with redirect inside sync XHR', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ server.setRedirect('/logo.png', '/pptr.png');
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ const status = await page.evaluate(async () => {
+ const request = new XMLHttpRequest();
+ request.open('GET', '/logo.png', false); // `false` makes the request synchronous
+ request.send(null);
+ return request.status;
+ });
+ expect(status).toBe(200);
+ });
+ it('should work with custom referer headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({referer: server.EMPTY_PAGE});
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
+ void request.continue();
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.ok()).toBe(true);
+ });
+ it('should be abortable', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (request.url().endsWith('.css')) {
+ void request.abort();
+ } else {
+ void request.continue();
+ }
+ });
+ let failedRequests = 0;
+ page.on('requestfailed', () => {
+ return ++failedRequests;
+ });
+ const response = (await page.goto(server.PREFIX + '/one-style.html'))!;
+ expect(response.ok()).toBe(true);
+ expect(response.request().failure()).toBe(null);
+ expect(failedRequests).toBe(1);
+ });
+ it('should be abortable with custom error codes', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.abort('internetdisconnected');
+ });
+ const [failedRequest] = await Promise.all([
+ waitEvent<HTTPRequest>(page, 'requestfailed'),
+ page.goto(server.EMPTY_PAGE).catch(() => {}),
+ ]);
+
+ expect(failedRequest).toBeTruthy();
+ expect(failedRequest.failure()!.errorText).toBe(
+ 'net::ERR_INTERNET_DISCONNECTED'
+ );
+ });
+ it('should send referer', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setExtraHTTPHeaders({
+ referer: 'http://google.com/',
+ });
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ const [request] = await Promise.all([
+ server.waitForRequest('/grid.html'),
+ page.goto(server.PREFIX + '/grid.html'),
+ ]);
+ expect(request.headers['referer']).toBe('http://google.com/');
+ });
+ it('should fail navigation when aborting main resource', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.abort();
+ });
+ let error!: Error;
+ await page.goto(server.EMPTY_PAGE).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ if (isChrome) {
+ expect(error.message).toContain('net::ERR_FAILED');
+ } else {
+ expect(error.message).toContain('NS_ERROR_FAILURE');
+ }
+ });
+ it('should work with redirects', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ void request.continue();
+ requests.push(request);
+ });
+ server.setRedirect(
+ '/non-existing-page.html',
+ '/non-existing-page-2.html'
+ );
+ server.setRedirect(
+ '/non-existing-page-2.html',
+ '/non-existing-page-3.html'
+ );
+ server.setRedirect(
+ '/non-existing-page-3.html',
+ '/non-existing-page-4.html'
+ );
+ server.setRedirect('/non-existing-page-4.html', '/empty.html');
+ const response = (await page.goto(
+ server.PREFIX + '/non-existing-page.html'
+ ))!;
+ expect(response.status()).toBe(200);
+ expect(response.url()).toContain('empty.html');
+ expect(requests).toHaveLength(5);
+ expect(requests[2]!.resourceType()).toBe('document');
+ // Check redirect chain
+ const redirectChain = response.request().redirectChain();
+ expect(redirectChain).toHaveLength(4);
+ expect(redirectChain[0]!.url()).toContain('/non-existing-page.html');
+ expect(redirectChain[2]!.url()).toContain('/non-existing-page-3.html');
+ for (let i = 0; i < redirectChain.length; ++i) {
+ const request = redirectChain[i]!;
+ expect(request.isNavigationRequest()).toBe(true);
+ expect(request.redirectChain().indexOf(request)).toBe(i);
+ }
+ });
+ it('should work with redirects for subresources', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ void request.continue();
+ if (!isFavicon(request)) {
+ requests.push(request);
+ }
+ });
+ server.setRedirect('/one-style.css', '/two-style.css');
+ server.setRedirect('/two-style.css', '/three-style.css');
+ server.setRedirect('/three-style.css', '/four-style.css');
+ server.setRoute('/four-style.css', (_req, res) => {
+ return res.end('body {box-sizing: border-box; }');
+ });
+
+ const response = (await page.goto(server.PREFIX + '/one-style.html'))!;
+ expect(response.status()).toBe(200);
+ expect(response.url()).toContain('one-style.html');
+ expect(requests).toHaveLength(5);
+ expect(requests[0]!.resourceType()).toBe('document');
+ expect(requests[1]!.resourceType()).toBe('stylesheet');
+ // Check redirect chain
+ const redirectChain = requests[1]!.redirectChain();
+ expect(redirectChain).toHaveLength(3);
+ expect(redirectChain[0]!.url()).toContain('/one-style.css');
+ expect(redirectChain[2]!.url()).toContain('/three-style.css');
+ });
+ it('should be able to abort redirects', async () => {
+ const {page, server, isChrome} = await getTestState();
+
+ await page.setRequestInterception(true);
+ server.setRedirect('/non-existing.json', '/non-existing-2.json');
+ server.setRedirect('/non-existing-2.json', '/simple.html');
+ page.on('request', request => {
+ if (request.url().includes('non-existing-2')) {
+ void request.abort();
+ } else {
+ void request.continue();
+ }
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const result = await page.evaluate(async () => {
+ try {
+ return await fetch('/non-existing.json');
+ } catch (error) {
+ return (error as Error).message;
+ }
+ });
+ if (isChrome) {
+ expect(result).toContain('Failed to fetch');
+ } else {
+ expect(result).toContain('NetworkError');
+ }
+ });
+ it('should work with equal requests', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let responseCount = 1;
+ server.setRoute('/zzz', (_req, res) => {
+ return res.end(responseCount++ * 11 + '');
+ });
+ await page.setRequestInterception(true);
+
+ let spinner = false;
+ // Cancel 2nd request.
+ page.on('request', request => {
+ if (isFavicon(request)) {
+ void request.continue();
+ return;
+ }
+ void (spinner ? request.abort() : request.continue());
+ spinner = !spinner;
+ });
+ const results = await page.evaluate(() => {
+ return Promise.all([
+ fetch('/zzz')
+ .then(response => {
+ return response.text();
+ })
+ .catch(() => {
+ return 'FAILED';
+ }),
+ fetch('/zzz')
+ .then(response => {
+ return response.text();
+ })
+ .catch(() => {
+ return 'FAILED';
+ }),
+ fetch('/zzz')
+ .then(response => {
+ return response.text();
+ })
+ .catch(() => {
+ return 'FAILED';
+ }),
+ ]);
+ });
+ expect(results).toEqual(['11', 'FAILED', '22']);
+ });
+ it('should navigate to dataURL and fire dataURL requests', async () => {
+ const {page} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ requests.push(request);
+ void request.continue();
+ });
+ const dataURL = 'data:text/html,<div>yo</div>';
+ const response = (await page.goto(dataURL))!;
+ expect(response.status()).toBe(200);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(dataURL);
+ });
+ it('should be able to fetch dataURL and fire dataURL requests', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ !isFavicon(request) && requests.push(request);
+ void request.continue();
+ });
+ const dataURL = 'data:text/html,<div>yo</div>';
+ const text = await page.evaluate((url: string) => {
+ return fetch(url).then(r => {
+ return r.text();
+ });
+ }, dataURL);
+ expect(text).toBe('<div>yo</div>');
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(dataURL);
+ });
+ it('should navigate to URL with hash and fire requests without hash', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ requests.push(request);
+ void request.continue();
+ });
+ const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!;
+ expect(response.status()).toBe(200);
+ expect(response.url()).toBe(server.EMPTY_PAGE);
+ expect(requests).toHaveLength(1);
+ expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should work with encoded server', async () => {
+ const {page, server} = await getTestState();
+
+ // The requestWillBeSent will report encoded URL, whereas interception will
+ // report URL as-is. @see crbug.com/759388
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ const response = (await page.goto(
+ server.PREFIX + '/some nonexisting page'
+ ))!;
+ expect(response.status()).toBe(404);
+ });
+ it('should work with badly encoded server', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ server.setRoute('/malformed?rnd=%911', (_req, res) => {
+ return res.end();
+ });
+ page.on('request', request => {
+ return request.continue();
+ });
+ const response = (await page.goto(
+ server.PREFIX + '/malformed?rnd=%911'
+ ))!;
+ expect(response.status()).toBe(200);
+ });
+ it('should work wit h encoded server - 2', async () => {
+ const {page, server} = await getTestState();
+
+ // The requestWillBeSent will report URL as-is, whereas interception will
+ // report encoded URL for stylesheet. @see crbug.com/759388
+ await page.setRequestInterception(true);
+ const requests: HTTPRequest[] = [];
+ page.on('request', request => {
+ void request.continue();
+ requests.push(request);
+ });
+ const response = (await page.goto(
+ `data:text/html,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>`
+ ))!;
+ expect(response.status()).toBe(200);
+ expect(requests).toHaveLength(2);
+ expect(requests[1]!.response()!.status()).toBe(404);
+ });
+ it('should not throw "Invalid Interception Id" if the request was cancelled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setContent('<iframe></iframe>');
+ await page.setRequestInterception(true);
+ let request!: HTTPRequest;
+ page.on('request', async r => {
+ return (request = r);
+ });
+ void (page.$eval(
+ 'iframe',
+ (frame, url) => {
+ return ((frame as HTMLIFrameElement).src = url as string);
+ },
+ server.EMPTY_PAGE
+ ),
+ // Wait for request interception.
+ await waitEvent(page, 'request'));
+ // Delete frame to cause request to be canceled.
+ await page.$eval('iframe', frame => {
+ return frame.remove();
+ });
+ let error!: Error;
+ await request.continue().catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeUndefined();
+ });
+ it('should throw if interception is not enabled', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ page.on('request', async request => {
+ try {
+ await request.continue();
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(error.message).toContain('Request Interception is not enabled');
+ });
+ it('should work with file URLs', async () => {
+ const {page} = await getTestState();
+
+ await page.setRequestInterception(true);
+ const urls = new Set();
+ page.on('request', request => {
+ urls.add(request.url().split('/').pop());
+ void request.continue();
+ });
+ await page.goto(
+ pathToFileURL(path.join(__dirname, '../assets', 'one-style.html'))
+ );
+ expect(urls.size).toBe(2);
+ expect(urls.has('one-style.html')).toBe(true);
+ expect(urls.has('one-style.css')).toBe(true);
+ });
+ it('should not cache if cache disabled', async () => {
+ const {page, server} = await getTestState();
+
+ // Load and re-load to make sure it's cached.
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+
+ await page.setRequestInterception(true);
+ await page.setCacheEnabled(false);
+ page.on('request', request => {
+ return request.continue();
+ });
+
+ const cached: HTTPRequest[] = [];
+ page.on('requestservedfromcache', r => {
+ return cached.push(r);
+ });
+
+ await page.reload();
+ expect(cached).toHaveLength(0);
+ });
+ it('should cache if cache enabled', async () => {
+ const {page, server} = await getTestState();
+
+ // Load and re-load to make sure it's cached.
+ await page.goto(server.PREFIX + '/cached/one-style.html');
+
+ await page.setRequestInterception(true);
+ await page.setCacheEnabled(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+
+ const cached: HTTPRequest[] = [];
+ page.on('requestservedfromcache', r => {
+ return cached.push(r);
+ });
+
+ await page.reload();
+ expect(cached).toHaveLength(1);
+ });
+ it('should load fonts if cache enabled', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ await page.setCacheEnabled(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+
+ const responsePromise = page.waitForResponse(r => {
+ return r.url().endsWith('/one-style.woff');
+ });
+ await page.goto(server.PREFIX + '/cached/one-style-font.html');
+ await responsePromise;
+ });
+ });
+
+ describe('Request.continue', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ return request.continue();
+ });
+ await page.goto(server.EMPTY_PAGE);
+ });
+ it('should amend HTTP headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const headers = Object.assign({}, request.headers());
+ headers['FOO'] = 'bar';
+ void request.continue({headers});
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => {
+ return fetch('/sleep.zzz');
+ }),
+ ]);
+ expect(request.headers['foo']).toBe('bar');
+ });
+ it('should redirect in a way non-observable to page', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const redirectURL = request.url().includes('/empty.html')
+ ? server.PREFIX + '/consolelog.html'
+ : undefined;
+ void request.continue({url: redirectURL});
+ });
+ const [consoleMessage] = await Promise.all([
+ waitEvent<ConsoleMessage>(page, 'console'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ expect(consoleMessage.text()).toBe('yellow');
+ });
+ it('should amend method', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue({method: 'POST'});
+ });
+ const [request] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => {
+ return fetch('/sleep.zzz');
+ }),
+ ]);
+ expect(request.method).toBe('POST');
+ });
+ it('should amend post data', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue({postData: 'doggo'});
+ });
+ const [serverRequest] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => {
+ return fetch('/sleep.zzz', {method: 'POST', body: 'birdy'});
+ }),
+ ]);
+ expect(await serverRequest.postBody).toBe('doggo');
+ });
+ it('should amend both post data and method on navigation', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.continue({method: 'POST', postData: 'doggo'});
+ });
+ const [serverRequest] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.EMPTY_PAGE),
+ ]);
+ expect(serverRequest.method).toBe('POST');
+ expect(await serverRequest.postBody).toBe('doggo');
+ });
+ it('should fail if the header value is invalid', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ await page.setRequestInterception(true);
+ page.on('request', async request => {
+ await request
+ .continue({
+ headers: {
+ 'X-Invalid-Header': 'a\nb',
+ },
+ })
+ .catch(error_ => {
+ error = error_ as Error;
+ });
+ await request.continue();
+ });
+ await page.goto(server.PREFIX + '/empty.html');
+ expect(error.message).toMatch(/Invalid header/);
+ });
+ });
+
+ describe('Request.respond', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond({
+ status: 201,
+ headers: {
+ foo: 'bar',
+ },
+ body: 'Yo, page!',
+ });
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.status()).toBe(201);
+ expect(response.headers()['foo']).toBe('bar');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent;
+ })
+ ).toBe('Yo, page!');
+ });
+ it('should work with status code 422', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond({
+ status: 422,
+ body: 'Yo, page!',
+ });
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.status()).toBe(422);
+ expect(response.statusText()).toBe('Unprocessable Entity');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent;
+ })
+ ).toBe('Yo, page!');
+ });
+ it('should redirect', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ if (!request.url().includes('rrredirect')) {
+ void request.continue();
+ return;
+ }
+ void request.respond({
+ status: 302,
+ headers: {
+ location: server.EMPTY_PAGE,
+ },
+ });
+ });
+ const response = (await page.goto(server.PREFIX + '/rrredirect'))!;
+ expect(response.request().redirectChain()).toHaveLength(1);
+ expect(response.request().redirectChain()[0]!.url()).toBe(
+ server.PREFIX + '/rrredirect'
+ );
+ expect(response.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should allow mocking multiple headers with same key', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond({
+ status: 200,
+ headers: {
+ foo: 'bar',
+ arr: ['1', '2'],
+ 'set-cookie': ['first=1', 'second=2'],
+ },
+ body: 'Hello world',
+ });
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ const cookies = await page.cookies();
+ const firstCookie = cookies.find(cookie => {
+ return cookie.name === 'first';
+ });
+ const secondCookie = cookies.find(cookie => {
+ return cookie.name === 'second';
+ });
+ expect(response.status()).toBe(200);
+ expect(response.headers()['foo']).toBe('bar');
+ expect(response.headers()['arr']).toBe('1\n2');
+ // request.respond() will not trigger Network.responseReceivedExtraInfo
+ // fail to get 'set-cookie' header from response
+ expect(firstCookie?.value).toBe('1');
+ expect(secondCookie?.value).toBe('2');
+ });
+ it('should allow mocking binary responses', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ const imageBuffer = fs.readFileSync(
+ path.join(__dirname, '../assets', 'pptr.png')
+ );
+ void request.respond({
+ contentType: 'image/png',
+ body: imageBuffer,
+ });
+ });
+ await page.evaluate(PREFIX => {
+ const img = document.createElement('img');
+ img.src = PREFIX + '/does-not-exist.png';
+ document.body.appendChild(img);
+ return new Promise(fulfill => {
+ return (img.onload = fulfill);
+ });
+ }, server.PREFIX);
+ using img = (await page.$('img'))!;
+ expect(await img.screenshot()).toBeGolden('mock-binary-response.png');
+ });
+ it('should stringify intercepted request response headers', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', request => {
+ void request.respond({
+ status: 200,
+ headers: {
+ foo: true,
+ },
+ body: 'Yo, page!',
+ });
+ });
+ const response = (await page.goto(server.EMPTY_PAGE))!;
+ expect(response.status()).toBe(200);
+ const headers = response.headers();
+ expect(headers['foo']).toBe('true');
+ expect(
+ await page.evaluate(() => {
+ return document.body.textContent;
+ })
+ ).toBe('Yo, page!');
+ });
+ it('should fail if the header value is invalid', async () => {
+ const {page, server} = await getTestState();
+
+ let error!: Error;
+ await page.setRequestInterception(true);
+ page.on('request', async request => {
+ await request
+ .respond({
+ headers: {
+ 'X-Invalid-Header': 'a\nb',
+ },
+ })
+ .catch(error_ => {
+ error = error_ as Error;
+ });
+ await request.respond({
+ status: 200,
+ body: 'Hello World',
+ });
+ });
+ await page.goto(server.PREFIX + '/empty.html');
+ expect(error.message).toMatch(/Invalid header/);
+ });
+ });
+});
+
+function pathToFileURL(path: string): string {
+ let pathName = path.replace(/\\/g, '/');
+ // Windows drive letter must be prefixed with a slash.
+ if (!pathName.startsWith('/')) {
+ pathName = '/' + pathName;
+ }
+ return 'file://' + pathName;
+}
diff --git a/remote/test/puppeteer/test/src/screencast.spec.ts b/remote/test/puppeteer/test/src/screencast.spec.ts
new file mode 100644
index 0000000000..b645f55da7
--- /dev/null
+++ b/remote/test/puppeteer/test/src/screencast.spec.ts
@@ -0,0 +1,99 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {statSync} from 'fs';
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {getUniqueVideoFilePlaceholder} from './utils.js';
+
+describe('Screencasts', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.screencast', function () {
+ it('should work', async () => {
+ using file = getUniqueVideoFilePlaceholder();
+
+ const {page} = await getTestState();
+
+ const recorder = await page.screencast({
+ path: file.filename,
+ scale: 0.5,
+ crop: {width: 100, height: 100, x: 0, y: 0},
+ speed: 0.5,
+ });
+
+ await page.goto('data:text/html,<input>');
+ using input = await page.locator('input').waitHandle();
+ await input.type('ab', {delay: 100});
+
+ await recorder.stop();
+
+ expect(statSync(file.filename).size).toBeGreaterThan(0);
+ });
+ it('should work concurrently', async () => {
+ using file1 = getUniqueVideoFilePlaceholder();
+ using file2 = getUniqueVideoFilePlaceholder();
+
+ const {page} = await getTestState();
+
+ const recorder = await page.screencast({path: file1.filename});
+ const recorder2 = await page.screencast({path: file2.filename});
+
+ await page.goto('data:text/html,<input>');
+ using input = await page.locator('input').waitHandle();
+
+ await input.type('ab', {delay: 100});
+ await recorder.stop();
+
+ await input.type('ab', {delay: 100});
+ await recorder2.stop();
+
+ // Since file2 spent about double the time of file1 recording, so file2
+ // should be around double the size of file1.
+ const ratio =
+ statSync(file2.filename).size / statSync(file1.filename).size;
+
+ // We use a range because we cannot be precise.
+ const DELTA = 1.3;
+ expect(ratio).toBeGreaterThan(2 - DELTA);
+ expect(ratio).toBeLessThan(2 + DELTA);
+ });
+ it('should validate options', async () => {
+ const {page} = await getTestState();
+
+ await expect(page.screencast({scale: 0})).rejects.toBeDefined();
+ await expect(page.screencast({scale: -1})).rejects.toBeDefined();
+
+ await expect(page.screencast({speed: 0})).rejects.toBeDefined();
+ await expect(page.screencast({speed: -1})).rejects.toBeDefined();
+
+ await expect(
+ page.screencast({crop: {x: 0, y: 0, height: 1, width: 0}})
+ ).rejects.toBeDefined();
+ await expect(
+ page.screencast({crop: {x: 0, y: 0, height: 0, width: 1}})
+ ).rejects.toBeDefined();
+ await expect(
+ page.screencast({crop: {x: -1, y: 0, height: 1, width: 1}})
+ ).rejects.toBeDefined();
+ await expect(
+ page.screencast({crop: {x: 0, y: -1, height: 1, width: 1}})
+ ).rejects.toBeDefined();
+ await expect(
+ page.screencast({crop: {x: 0, y: 0, height: 10000, width: 1}})
+ ).rejects.toBeDefined();
+ await expect(
+ page.screencast({crop: {x: 0, y: 0, height: 1, width: 10000}})
+ ).rejects.toBeDefined();
+
+ await expect(
+ page.screencast({ffmpegPath: 'non-existent-path'})
+ ).rejects.toBeDefined();
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/screenshot.spec.ts b/remote/test/puppeteer/test/src/screenshot.spec.ts
new file mode 100644
index 0000000000..ad53b60e95
--- /dev/null
+++ b/remote/test/puppeteer/test/src/screenshot.spec.ts
@@ -0,0 +1,453 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+
+import expect from 'expect';
+
+import {
+ getTestState,
+ isHeadless,
+ launch,
+ setupTestBrowserHooks,
+} from './mocha-utils.js';
+
+describe('Screenshots', function () {
+ setupTestBrowserHooks();
+
+ describe('Page.screenshot', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot();
+ expect(screenshot).toBeGolden('screenshot-sanity.png');
+ });
+ it('should clip rect', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ clip: {
+ x: 50,
+ y: 100,
+ width: 150,
+ height: 100,
+ },
+ });
+ expect(screenshot).toBeGolden('screenshot-clip-rect.png');
+ });
+ it('should get screenshot bigger than the viewport', async () => {
+ const {page, server} = await getTestState();
+ await page.setViewport({width: 50, height: 50});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ clip: {
+ x: 25,
+ y: 25,
+ width: 100,
+ height: 100,
+ },
+ });
+ expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
+ });
+ it('should clip clip bigger than the viewport without "captureBeyondViewport"', async () => {
+ const {page, server} = await getTestState();
+ await page.setViewport({width: 50, height: 50});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ captureBeyondViewport: false,
+ clip: {
+ x: 25,
+ y: 25,
+ width: 100,
+ height: 100,
+ },
+ });
+ expect(screenshot).toBeGolden('screenshot-offscreen-clip-2.png');
+ });
+ it('should run in parallel', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const promises = [];
+ for (let i = 0; i < 3; ++i) {
+ promises.push(
+ page.screenshot({
+ clip: {
+ x: 50 * i,
+ y: 0,
+ width: 50,
+ height: 50,
+ },
+ })
+ );
+ }
+ const screenshots = await Promise.all(promises);
+ expect(screenshots[1]).toBeGolden('grid-cell-1.png');
+ });
+ it('should take fullPage screenshots', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ fullPage: true,
+ });
+ expect(screenshot).toBeGolden('screenshot-grid-fullpage.png');
+ });
+ it('should take fullPage screenshots without captureBeyondViewport', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ fullPage: true,
+ captureBeyondViewport: false,
+ });
+ expect(screenshot).toBeGolden('screenshot-grid-fullpage-2.png');
+ expect(page.viewport()).toMatchObject({width: 500, height: 500});
+ });
+ it('should run in parallel in multiple pages', async () => {
+ const {server, context} = await getTestState();
+
+ const N = 2;
+ const pages = await Promise.all(
+ Array(N)
+ .fill(0)
+ .map(async () => {
+ const page = await context.newPage();
+ await page.goto(server.PREFIX + '/grid.html');
+ return page;
+ })
+ );
+ const promises = [];
+ for (let i = 0; i < N; ++i) {
+ promises.push(
+ pages[i]!.screenshot({
+ clip: {x: 50 * i, y: 0, width: 50, height: 50},
+ })
+ );
+ }
+ const screenshots = await Promise.all(promises);
+ for (let i = 0; i < N; ++i) {
+ expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`);
+ }
+ await Promise.all(
+ pages.map(page => {
+ return page.close();
+ })
+ );
+ });
+ it('should work with odd clip size on Retina displays', async () => {
+ const {page} = await getTestState();
+
+ // Make sure documentElement height is at least 11px.
+ await page.setContent(`<div style="width: 11px; height: 11px;">`);
+
+ const screenshot = await page.screenshot({
+ clip: {
+ x: 0,
+ y: 0,
+ width: 11,
+ height: 11,
+ },
+ });
+ expect(screenshot).toBeGolden('screenshot-clip-odd-size.png');
+ });
+ it('should return base64', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ encoding: 'base64',
+ });
+ expect(Buffer.from(screenshot, 'base64')).toBeGolden(
+ 'screenshot-sanity.png'
+ );
+ });
+ });
+
+ describe('ElementHandle.screenshot', function () {
+ it('should work', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ await page.evaluate(() => {
+ return window.scrollBy(50, 100);
+ });
+ using elementHandle = (await page.$('.box:nth-of-type(3)'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden('screenshot-element-bounding-box.png');
+ });
+ it('should work with a null viewport', async () => {
+ const {server} = await getTestState({
+ skipLaunch: true,
+ });
+ const {browser, close} = await launch({
+ defaultViewport: null,
+ });
+
+ try {
+ const page = await browser.newPage();
+ await page.goto(server.PREFIX + '/grid.html');
+ await page.evaluate(() => {
+ return window.scrollBy(50, 100);
+ });
+ using elementHandle = await page.$('.box:nth-of-type(3)');
+ assert(elementHandle);
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeTruthy();
+ } finally {
+ await close();
+ }
+ });
+ it('should take into account padding and border', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ something above
+ <style>div {
+ border: 2px solid blue;
+ background: green;
+ width: 50px;
+ height: 50px;
+ }
+ </style>
+ <div></div>
+ `);
+ using elementHandle = (await page.$('div'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden('screenshot-element-padding-border.png');
+ });
+ it('should capture full element when larger than viewport', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+
+ await page.setContent(`
+ something above
+ <style>
+ :root {
+ scrollbar-width: none;
+ }
+ div.to-screenshot {
+ border: 1px solid blue;
+ width: 600px;
+ height: 600px;
+ margin-left: 50px;
+ }
+ </style>
+ <div class="to-screenshot"></div>
+ `);
+ using elementHandle = (await page.$('div.to-screenshot'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden(
+ 'screenshot-element-larger-than-viewport.png'
+ );
+
+ expect(
+ await page.evaluate(() => {
+ return {
+ w: window.innerWidth,
+ h: window.innerHeight,
+ };
+ })
+ ).toEqual({w: 500, h: 500});
+ });
+ it('should scroll element into view', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`
+ something above
+ <style>div.above {
+ border: 2px solid blue;
+ background: red;
+ height: 1500px;
+ }
+ div.to-screenshot {
+ border: 2px solid blue;
+ background: green;
+ width: 50px;
+ height: 50px;
+ }
+ </style>
+ <div class="above"></div>
+ <div class="to-screenshot"></div>
+ `);
+ using elementHandle = (await page.$('div.to-screenshot'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden(
+ 'screenshot-element-scrolled-into-view.png'
+ );
+ });
+ it('should work with a rotated element', async () => {
+ const {page} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.setContent(`<div style="position:absolute;
+ top: 100px;
+ left: 100px;
+ width: 100px;
+ height: 100px;
+ background: green;
+ transform: rotateZ(200deg);">&nbsp;</div>`);
+ using elementHandle = (await page.$('div'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden('screenshot-element-rotate.png');
+ });
+ it('should fail to screenshot a detached element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<h1>remove this</h1>');
+ using elementHandle = (await page.$('h1'))!;
+ await page.evaluate((element: HTMLElement) => {
+ return element.remove();
+ }, elementHandle);
+ const screenshotError = await elementHandle.screenshot().catch(error => {
+ return error;
+ });
+ expect(screenshotError.message).toBe(
+ 'Node is either not visible or not an HTMLElement'
+ );
+ });
+ it('should not hang with zero width/height element', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div style="width: 50px; height: 0"></div>');
+ using div = (await page.$('div'))!;
+ const error = await div.screenshot().catch(error_ => {
+ return error_;
+ });
+ expect(error.message).toBe('Node has 0 height.');
+ });
+ it('should work for an element with fractional dimensions', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div style="width:48.51px;height:19.8px;border:1px solid black;"></div>'
+ );
+ using elementHandle = (await page.$('div'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden('screenshot-element-fractional.png');
+ });
+ it('should work for an element with an offset', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(
+ '<div style="position:absolute; top: 10.3px; left: 20.4px;width:50.3px;height:20.2px;border:1px solid black;"></div>'
+ );
+ using elementHandle = (await page.$('div'))!;
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png');
+ });
+ it('should work with webp', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 100, height: 100});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ type: 'webp',
+ });
+
+ expect(screenshot).toBeInstanceOf(Buffer);
+ });
+
+ it('should run in parallel in multiple pages', async () => {
+ const {browser, server} = await getTestState();
+
+ const context = await browser.createIncognitoBrowserContext();
+
+ const N = 2;
+ const pages = await Promise.all(
+ Array(N)
+ .fill(0)
+ .map(async () => {
+ const page = await context.newPage();
+ await page.goto(server.PREFIX + '/grid.html');
+ return page;
+ })
+ );
+ const promises = [];
+ for (let i = 0; i < N; ++i) {
+ promises.push(
+ pages[i]!.screenshot({
+ clip: {x: 50 * i, y: 0, width: 50, height: 50},
+ })
+ );
+ }
+ const screenshots = await Promise.all(promises);
+ for (let i = 0; i < N; ++i) {
+ expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`);
+ }
+ await Promise.all(
+ pages.map(page => {
+ return page.close();
+ })
+ );
+
+ await context.close();
+ });
+ });
+
+ describe('Cdp', () => {
+ it('should use scale for clip', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ clip: {
+ x: 50,
+ y: 100,
+ width: 150,
+ height: 100,
+ scale: 2,
+ },
+ });
+ expect(screenshot).toBeGolden('screenshot-clip-rect-scale2.png');
+ });
+ it('should allow transparency', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 100, height: 100});
+ await page.goto(server.EMPTY_PAGE);
+ const screenshot = await page.screenshot({omitBackground: true});
+ expect(screenshot).toBeGolden('transparent.png');
+ });
+ it('should render white background on jpeg file', async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 100, height: 100});
+ await page.goto(server.EMPTY_PAGE);
+ const screenshot = await page.screenshot({
+ omitBackground: true,
+ type: 'jpeg',
+ });
+ expect(screenshot).toBeGolden('white.jpg');
+ });
+ (!isHeadless ? it : it.skip)(
+ 'should work in "fromSurface: false" mode',
+ async () => {
+ const {page, server} = await getTestState();
+
+ await page.setViewport({width: 500, height: 500});
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ fromSurface: false,
+ });
+ expect(screenshot).toBeDefined(); // toBeGolden('screenshot-fromsurface-false.png');
+ }
+ );
+ });
+});
diff --git a/remote/test/puppeteer/test/src/stacktrace.spec.ts b/remote/test/puppeteer/test/src/stacktrace.spec.ts
new file mode 100644
index 0000000000..b36ee56661
--- /dev/null
+++ b/remote/test/puppeteer/test/src/stacktrace.spec.ts
@@ -0,0 +1,157 @@
+/**
+ * @license
+ * Copyright 2023 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert';
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+
+const FILENAME = __filename.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
+const parseStackTrace = (stack: string): string => {
+ stack = stack.replace(new RegExp(FILENAME, 'g'), '<filename>');
+ stack = stack.replace(/<filename>:(\d+):(\d+)/g, '<filename>:<line>:<col>');
+ stack = stack.replace(/<anonymous>:(\d+):(\d+)/g, '<anonymous>:<line>:<col>');
+ return stack;
+};
+
+describe('Stack trace', function () {
+ setupTestBrowserHooks();
+
+ it('should work', async () => {
+ const {page} = await getTestState();
+
+ const error = (await page
+ .evaluate(() => {
+ throw new Error('Test');
+ })
+ .catch((error: Error) => {
+ return error;
+ })) as Error;
+
+ expect(error.name).toEqual('Error');
+ expect(error.message).toEqual('Test');
+ assert(error.stack);
+ error.stack = error.stack.replace(new RegExp(FILENAME, 'g'), '<filename>');
+ expect(
+ parseStackTrace(error.stack).split('\n at ').slice(0, 2)
+ ).toMatchObject({
+ ...[
+ 'Error: Test',
+ 'evaluate (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ ],
+ });
+ });
+
+ it('should work with handles', async () => {
+ const {page} = await getTestState();
+
+ const error = (await page
+ .evaluateHandle(() => {
+ throw new Error('Test');
+ })
+ .catch((error: Error) => {
+ return error;
+ })) as Error;
+
+ expect(error.name).toEqual('Error');
+ expect(error.message).toEqual('Test');
+ assert(error.stack);
+ expect(
+ parseStackTrace(error.stack).split('\n at ').slice(0, 2)
+ ).toMatchObject({
+ ...[
+ 'Error: Test',
+ 'evaluateHandle (evaluateHandle at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ ],
+ });
+ });
+
+ it('should work with contiguous evaluation', async () => {
+ const {page} = await getTestState();
+
+ using thrower = await page.evaluateHandle(() => {
+ return () => {
+ throw new Error('Test');
+ };
+ });
+ const error = (await thrower
+ .evaluate(thrower => {
+ thrower();
+ })
+ .catch((error: Error) => {
+ return error;
+ })) as Error;
+
+ expect(error.name).toEqual('Error');
+ expect(error.message).toEqual('Test');
+ assert(error.stack);
+ expect(
+ parseStackTrace(error.stack).split('\n at ').slice(0, 3)
+ ).toMatchObject({
+ ...[
+ 'Error: Test',
+ 'evaluateHandle (evaluateHandle at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ 'evaluate (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ ],
+ });
+ });
+
+ it('should work with nested function calls', async () => {
+ const {page} = await getTestState();
+
+ const error = (await page
+ .evaluate(() => {
+ function a() {
+ throw new Error('Test');
+ }
+ function b() {
+ a();
+ }
+ function c() {
+ b();
+ }
+ function d() {
+ c();
+ }
+ d();
+ })
+ .catch((error: Error) => {
+ return error;
+ })) as Error;
+
+ expect(error.name).toEqual('Error');
+ expect(error.message).toEqual('Test');
+ assert(error.stack);
+ expect(
+ parseStackTrace(error.stack).split('\n at ').slice(0, 6)
+ ).toMatchObject({
+ ...[
+ 'Error: Test',
+ 'a (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ 'b (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ 'c (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ 'd (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ 'evaluate (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)',
+ ],
+ });
+ });
+
+ it('should work for none error objects', async () => {
+ const {page} = await getTestState();
+
+ const [error] = await Promise.all([
+ waitEvent<Error>(page, 'pageerror'),
+ page.evaluate(() => {
+ // This can happen when a 404 with HTML is returned
+ void Promise.reject(new Response());
+ }),
+ ]);
+
+ expect(error).toBeTruthy();
+ });
+});
diff --git a/remote/test/puppeteer/test/src/target.spec.ts b/remote/test/puppeteer/test/src/target.spec.ts
new file mode 100644
index 0000000000..28d17a4030
--- /dev/null
+++ b/remote/test/puppeteer/test/src/target.spec.ts
@@ -0,0 +1,343 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type {ServerResponse} from 'http';
+
+import expect from 'expect';
+import {type Target, TimeoutError} from 'puppeteer';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+
+describe('Target', function () {
+ setupTestBrowserHooks();
+
+ it('Browser.targets should return all of the targets', async () => {
+ const {browser} = await getTestState();
+
+ // The pages will be the testing page and the original newtab page
+ const targets = browser.targets();
+ expect(
+ targets.some(target => {
+ return target.type() === 'page' && target.url() === 'about:blank';
+ })
+ ).toBeTruthy();
+ expect(
+ targets.some(target => {
+ return target.type() === 'browser';
+ })
+ ).toBeTruthy();
+ });
+ it('Browser.pages should return all of the pages', async () => {
+ const {page, context} = await getTestState();
+
+ // The pages will be the testing page
+ const allPages = await context.pages();
+ expect(allPages).toHaveLength(1);
+ expect(allPages).toContain(page);
+ });
+ it('should contain browser target', async () => {
+ const {browser} = await getTestState();
+
+ const targets = browser.targets();
+ const browserTarget = targets.find(target => {
+ return target.type() === 'browser';
+ });
+ expect(browserTarget).toBeTruthy();
+ });
+ it('should be able to use the default page in the browser', async () => {
+ const {page, browser} = await getTestState();
+
+ // The pages will be the testing page and the original newtab page
+ const allPages = await browser.pages();
+ const originalPage = allPages.find(p => {
+ return p !== page;
+ })!;
+ expect(
+ await originalPage.evaluate(() => {
+ return ['Hello', 'world'].join(' ');
+ })
+ ).toBe('Hello world');
+ expect(await originalPage.$('body')).toBeTruthy();
+ });
+ it('should be able to use async waitForTarget', async () => {
+ const {page, server, context} = await getTestState();
+
+ const [otherPage] = await Promise.all([
+ context
+ .waitForTarget(
+ target => {
+ return target.page().then(page => {
+ return (
+ page!.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ });
+ },
+ {timeout: 3000}
+ )
+ .then(target => {
+ return target.page();
+ }),
+ page.evaluate((url: string) => {
+ return window.open(url);
+ }, server.CROSS_PROCESS_PREFIX + '/empty.html'),
+ ]);
+ expect(otherPage!.url()).toEqual(
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ );
+ expect(page).not.toBe(otherPage);
+ });
+ it('should report when a new page is created and closed', async () => {
+ const {page, server, context} = await getTestState();
+
+ const [otherPage] = await Promise.all([
+ context
+ .waitForTarget(
+ target => {
+ return target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html';
+ },
+ {timeout: 3000}
+ )
+ .then(target => {
+ return target.page();
+ }),
+ page.evaluate((url: string) => {
+ return window.open(url);
+ }, server.CROSS_PROCESS_PREFIX + '/empty.html'),
+ ]);
+ expect(otherPage!.url()).toContain(server.CROSS_PROCESS_PREFIX);
+ expect(
+ await otherPage!.evaluate(() => {
+ return ['Hello', 'world'].join(' ');
+ })
+ ).toBe('Hello world');
+ expect(await otherPage!.$('body')).toBeTruthy();
+
+ let allPages = await context.pages();
+ expect(allPages).toContain(page);
+ expect(allPages).toContain(otherPage);
+
+ const [closedTarget] = await Promise.all([
+ waitEvent<Target>(context, 'targetdestroyed'),
+ otherPage!.close(),
+ ]);
+ expect(await closedTarget.page()).toBe(otherPage);
+
+ allPages = (await Promise.all(
+ context.targets().map(target => {
+ return target.page();
+ })
+ )) as Page[];
+ expect(allPages).toContain(page);
+ expect(allPages).not.toContain(otherPage);
+ });
+ it('should report when a service worker is created and destroyed', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const createdTarget = waitEvent(context, 'targetcreated');
+
+ await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
+
+ expect((await createdTarget).type()).toBe('service_worker');
+ expect((await createdTarget).url()).toBe(
+ server.PREFIX + '/serviceworkers/empty/sw.js'
+ );
+
+ const destroyedTarget = waitEvent(context, 'targetdestroyed');
+ await page.evaluate(() => {
+ return (
+ globalThis as unknown as {
+ registrationPromise: Promise<{unregister: () => void}>;
+ }
+ ).registrationPromise.then((registration: any) => {
+ return registration.unregister();
+ });
+ });
+ expect(await destroyedTarget).toBe(await createdTarget);
+ });
+ it('should create a worker from a service worker', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
+
+ const target = await context.waitForTarget(
+ target => {
+ return target.type() === 'service_worker';
+ },
+ {timeout: 3000}
+ );
+ const worker = (await target.worker())!;
+
+ expect(
+ await worker.evaluate(() => {
+ return self.toString();
+ })
+ ).toBe('[object ServiceWorkerGlobalScope]');
+ });
+ it('should create a worker from a shared worker', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ new SharedWorker('data:text/javascript,console.log("hi")');
+ });
+ const target = await context.waitForTarget(
+ target => {
+ return target.type() === 'shared_worker';
+ },
+ {timeout: 3000}
+ );
+ const worker = (await target.worker())!;
+ expect(
+ await worker.evaluate(() => {
+ return self.toString();
+ })
+ ).toBe('[object SharedWorkerGlobalScope]');
+ });
+ it('should report when a target url changes', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let changedTarget = waitEvent(context, 'targetchanged');
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/');
+ expect((await changedTarget).url()).toBe(server.CROSS_PROCESS_PREFIX + '/');
+
+ changedTarget = waitEvent(context, 'targetchanged');
+ await page.goto(server.EMPTY_PAGE);
+ expect((await changedTarget).url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should not report uninitialized pages', async () => {
+ const {context} = await getTestState();
+
+ let targetChanged = false;
+ const listener = () => {
+ targetChanged = true;
+ };
+ context.on('targetchanged', listener);
+ const targetPromise = waitEvent<Target>(context, 'targetcreated');
+ const newPagePromise = context.newPage();
+ const target = await targetPromise;
+ expect(target.url()).toBe('about:blank');
+
+ const newPage = await newPagePromise;
+ const targetPromise2 = waitEvent<Target>(context, 'targetcreated');
+ const evaluatePromise = newPage.evaluate(() => {
+ return window.open('about:blank');
+ });
+ const target2 = await targetPromise2;
+ expect(target2.url()).toBe('about:blank');
+ await evaluatePromise;
+ await newPage.close();
+ expect(targetChanged).toBe(false);
+ context.off('targetchanged', listener);
+ });
+
+ it('should not crash while redirecting if original request was missed', async () => {
+ const {page, server, context} = await getTestState();
+
+ let serverResponse!: ServerResponse;
+ server.setRoute('/one-style.css', (_req, res) => {
+ return (serverResponse = res);
+ });
+ // Open a new page. Use window.open to connect to the page later.
+ await Promise.all([
+ page.evaluate((url: string) => {
+ return window.open(url);
+ }, server.PREFIX + '/one-style.html'),
+ server.waitForRequest('/one-style.css'),
+ ]);
+ // Connect to the opened page.
+ const target = await context.waitForTarget(
+ target => {
+ return target.url().includes('one-style.html');
+ },
+ {timeout: 3000}
+ );
+ const newPage = (await target.page())!;
+ const loadEvent = waitEvent(newPage, 'load');
+ // Issue a redirect.
+ serverResponse.writeHead(302, {location: '/injectedstyle.css'});
+ serverResponse.end();
+ // Wait for the new page to load.
+ await loadEvent;
+ // Cleanup.
+ await newPage.close();
+ });
+ it('should have an opener', async () => {
+ const {page, server, context} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [createdTarget] = await Promise.all([
+ waitEvent<Target>(context, 'targetcreated'),
+ page.goto(server.PREFIX + '/popup/window-open.html'),
+ ]);
+ expect((await createdTarget.page())!.url()).toBe(
+ server.PREFIX + '/popup/popup.html'
+ );
+ expect(createdTarget.opener()).toBe(page.target());
+ expect(page.target().opener()).toBeUndefined();
+ });
+
+ describe('Browser.waitForTarget', () => {
+ it('should wait for a target', async () => {
+ const {browser, server} = await getTestState();
+
+ let resolved = false;
+ const targetPromise = browser.waitForTarget(
+ target => {
+ return target.url() === server.EMPTY_PAGE;
+ },
+ {timeout: 3000}
+ );
+ targetPromise
+ .then(() => {
+ return (resolved = true);
+ })
+ .catch(error => {
+ resolved = true;
+ if (error instanceof TimeoutError) {
+ console.error(error);
+ } else {
+ throw error;
+ }
+ });
+ const page = await browser.newPage();
+ expect(resolved).toBe(false);
+ await page.goto(server.EMPTY_PAGE);
+ try {
+ const target = await targetPromise;
+ expect(await target.page()).toBe(page);
+ } catch (error) {
+ if (error instanceof TimeoutError) {
+ console.error(error);
+ } else {
+ throw error;
+ }
+ }
+ await page.close();
+ });
+ it('should timeout waiting for a non-existent target', async () => {
+ const {browser, server} = await getTestState();
+
+ let error!: Error;
+ await browser
+ .waitForTarget(
+ target => {
+ return target.url() === server.PREFIX + '/does-not-exist.html';
+ },
+ {
+ timeout: 1,
+ }
+ )
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/touchscreen.spec.ts b/remote/test/puppeteer/test/src/touchscreen.spec.ts
new file mode 100644
index 0000000000..28a18ec449
--- /dev/null
+++ b/remote/test/puppeteer/test/src/touchscreen.spec.ts
@@ -0,0 +1,79 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+declare const allEvents: Array<{type: string}>;
+
+describe('Touchscreen', () => {
+ setupTestBrowserHooks();
+
+ describe('Touchscreen.prototype.tap', () => {
+ it('should work', async () => {
+ const {page, server, isHeadless} = await getTestState();
+ await page.goto(server.PREFIX + '/input/touchscreen.html');
+
+ await page.tap('button');
+ expect(
+ (
+ await page.evaluate(() => {
+ return allEvents;
+ })
+ ).filter(({type}) => {
+ return type !== 'pointermove' || isHeadless;
+ })
+ ).toMatchObject([
+ {height: 1, type: 'pointerdown', width: 1, x: 5, y: 5},
+ {touches: [[5, 5, 0.5, 0.5]], type: 'touchstart'},
+ {height: 1, type: 'pointerup', width: 1, x: 5, y: 5},
+ {touches: [[5, 5, 0.5, 0.5]], type: 'touchend'},
+ {height: 1, type: 'click', width: 1, x: 5, y: 5},
+ ]);
+ });
+ });
+
+ describe('Touchscreen.prototype.touchMove', () => {
+ it('should work', async () => {
+ const {page, server, isHeadless} = await getTestState();
+ await page.goto(server.PREFIX + '/input/touchscreen.html');
+
+ await page.touchscreen.touchStart(0, 0);
+ await page.touchscreen.touchMove(10, 10);
+ await page.touchscreen.touchMove(15.5, 15);
+ await page.touchscreen.touchMove(20, 20.4);
+ await page.touchscreen.touchMove(40, 30);
+ await page.touchscreen.touchEnd();
+ expect(
+ (
+ await page.evaluate(() => {
+ return allEvents;
+ })
+ ).filter(({type}) => {
+ return type !== 'pointermove' || isHeadless;
+ })
+ ).toMatchObject(
+ [
+ {type: 'pointerdown', x: 0, y: 0, width: 1, height: 1},
+ {type: 'touchstart', touches: [[0, 0, 0.5, 0.5]]},
+ {type: 'pointermove', x: 10, y: 10, width: 1, height: 1},
+ {type: 'touchmove', touches: [[10, 10, 0.5, 0.5]]},
+ {type: 'pointermove', x: 16, y: 15, width: 1, height: 1},
+ {type: 'touchmove', touches: [[16, 15, 0.5, 0.5]]},
+ {type: 'pointermove', x: 20, y: 20, width: 1, height: 1},
+ {type: 'touchmove', touches: [[20, 20, 0.5, 0.5]]},
+ {type: 'pointermove', x: 40, y: 30, width: 1, height: 1},
+ {type: 'touchmove', touches: [[40, 30, 0.5, 0.5]]},
+ {type: 'pointerup', x: 40, y: 30, width: 1, height: 1},
+ {type: 'touchend', touches: [[40, 30, 0.5, 0.5]]},
+ ].filter(({type}) => {
+ return type !== 'pointermove' || isHeadless;
+ })
+ );
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/tracing.spec.ts b/remote/test/puppeteer/test/src/tracing.spec.ts
new file mode 100644
index 0000000000..2c0a5aff19
--- /dev/null
+++ b/remote/test/puppeteer/test/src/tracing.spec.ts
@@ -0,0 +1,149 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+import expect from 'expect';
+
+import {launch} from './mocha-utils.js';
+
+describe('Tracing', function () {
+ let outputFile!: string;
+ let testState: Awaited<ReturnType<typeof launch>>;
+
+ /* we manually manage the browser here as we want a new browser for each
+ * individual test, which isn't the default behaviour of getTestState()
+ */
+ beforeEach(async () => {
+ testState = await launch({});
+ outputFile = path.join(__dirname, 'trace.json');
+ });
+
+ afterEach(async () => {
+ await testState.close();
+ if (fs.existsSync(outputFile)) {
+ fs.unlinkSync(outputFile);
+ }
+ });
+
+ it('should output a trace', async () => {
+ const {server, page} = testState;
+ await page.tracing.start({screenshots: true, path: outputFile});
+ await page.goto(server.PREFIX + '/grid.html');
+ await page.tracing.stop();
+ expect(fs.existsSync(outputFile)).toBe(true);
+ });
+
+ it('should run with custom categories if provided', async () => {
+ const {page} = testState;
+ await page.tracing.start({
+ path: outputFile,
+ categories: ['-*', 'disabled-by-default-devtools.timeline.frame'],
+ });
+ await page.tracing.stop();
+
+ const traceJson = JSON.parse(
+ fs.readFileSync(outputFile, {encoding: 'utf8'})
+ );
+ const traceConfig = JSON.parse(traceJson.metadata['trace-config']);
+ expect(traceConfig.included_categories).toEqual([
+ 'disabled-by-default-devtools.timeline.frame',
+ ]);
+ expect(traceConfig.excluded_categories).toEqual(['*']);
+ expect(traceJson.traceEvents).not.toContainEqual(
+ expect.objectContaining({
+ cat: 'toplevel',
+ })
+ );
+ });
+
+ it('should run with default categories', async () => {
+ const {page} = testState;
+ await page.tracing.start({
+ path: outputFile,
+ });
+ await page.tracing.stop();
+
+ const traceJson = JSON.parse(
+ fs.readFileSync(outputFile, {encoding: 'utf8'})
+ );
+ expect(traceJson.traceEvents).toContainEqual(
+ expect.objectContaining({
+ cat: 'toplevel',
+ })
+ );
+ });
+ it('should throw if tracing on two pages', async () => {
+ const {page, browser} = testState;
+ await page.tracing.start({path: outputFile});
+ const newPage = await browser.newPage();
+ let error!: Error;
+ await newPage.tracing.start({path: outputFile}).catch(error_ => {
+ return (error = error_);
+ });
+ await newPage.close();
+ expect(error).toBeTruthy();
+ await page.tracing.stop();
+ });
+ it('should return a buffer', async () => {
+ const {page, server} = testState;
+
+ await page.tracing.start({screenshots: true, path: outputFile});
+ await page.goto(server.PREFIX + '/grid.html');
+ const trace = (await page.tracing.stop())!;
+ const buf = fs.readFileSync(outputFile);
+ expect(trace.toString()).toEqual(buf.toString());
+ });
+ it('should work without options', async () => {
+ const {page, server} = testState;
+
+ await page.tracing.start();
+ await page.goto(server.PREFIX + '/grid.html');
+ const trace = await page.tracing.stop();
+ expect(trace).toBeTruthy();
+ });
+
+ it('should return undefined in case of Buffer error', async () => {
+ const {page, server} = testState;
+
+ await page.tracing.start({screenshots: true});
+ await page.goto(server.PREFIX + '/grid.html');
+
+ const oldBufferConcat = Buffer.concat;
+ try {
+ Buffer.concat = () => {
+ throw new Error('error');
+ };
+ const trace = await page.tracing.stop();
+ expect(trace).toEqual(undefined);
+ } finally {
+ Buffer.concat = oldBufferConcat;
+ }
+ });
+
+ it('should support a buffer without a path', async () => {
+ const {page, server} = testState;
+
+ await page.tracing.start({screenshots: true});
+ await page.goto(server.PREFIX + '/grid.html');
+ const trace = (await page.tracing.stop())!;
+ expect(trace.toString()).toContain('screenshot');
+ });
+
+ it('should properly fail if readProtocolStream errors out', async () => {
+ const {page} = testState;
+ await page.tracing.start({path: __dirname});
+
+ let error!: Error;
+ try {
+ await page.tracing.stop();
+ } catch (error_) {
+ error = error_ as Error;
+ }
+ expect(error).toBeDefined();
+ });
+});
diff --git a/remote/test/puppeteer/test/src/utils.ts b/remote/test/puppeteer/test/src/utils.ts
new file mode 100644
index 0000000000..d1bad65a16
--- /dev/null
+++ b/remote/test/puppeteer/test/src/utils.ts
@@ -0,0 +1,171 @@
+/**
+ * @license
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {rm} from 'fs/promises';
+import {tmpdir} from 'os';
+import path from 'path';
+
+import expect from 'expect';
+import type {Frame} from 'puppeteer-core/internal/api/Frame.js';
+import type {Page} from 'puppeteer-core/internal/api/Page.js';
+import type {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js';
+import {Deferred} from 'puppeteer-core/internal/util/Deferred.js';
+
+import {compare} from './golden-utils.js';
+
+const PROJECT_ROOT = path.join(__dirname, '..', '..');
+
+declare module 'expect' {
+ interface Matchers<R> {
+ toBeGolden(pathOrBuffer: string | Buffer): R;
+ }
+}
+
+export const extendExpectWithToBeGolden = (
+ goldenDir: string,
+ outputDir: string
+): void => {
+ expect.extend({
+ toBeGolden: (testScreenshot: string | Buffer, goldenFilePath: string) => {
+ const result = compare(
+ goldenDir,
+ outputDir,
+ testScreenshot,
+ goldenFilePath
+ );
+
+ if (result.pass) {
+ return {
+ pass: true,
+ message: () => {
+ return '';
+ },
+ };
+ } else {
+ return {
+ pass: false,
+ message: () => {
+ return result.message;
+ },
+ };
+ }
+ },
+ });
+};
+
+export const projectRoot = (): string => {
+ return PROJECT_ROOT;
+};
+
+export const attachFrame = async (
+ pageOrFrame: Page | Frame,
+ frameId: string,
+ url: string
+): Promise<Frame | undefined> => {
+ using handle = await pageOrFrame.evaluateHandle(attachFrame, frameId, url);
+ return (await handle.asElement()?.contentFrame()) ?? undefined;
+
+ async function attachFrame(frameId: string, url: string) {
+ const frame = document.createElement('iframe');
+ frame.src = url;
+ frame.id = frameId;
+ document.body.appendChild(frame);
+ await new Promise(x => {
+ return (frame.onload = x);
+ });
+ return frame;
+ }
+};
+
+export const isFavicon = (request: {url: () => string | string[]}): boolean => {
+ return request.url().includes('favicon.ico');
+};
+
+export async function detachFrame(
+ pageOrFrame: Page | Frame,
+ frameId: string
+): Promise<void> {
+ await pageOrFrame.evaluate(detachFrame, frameId);
+
+ function detachFrame(frameId: string) {
+ const frame = document.getElementById(frameId) as HTMLIFrameElement;
+ frame.remove();
+ }
+}
+
+export async function navigateFrame(
+ pageOrFrame: Page | Frame,
+ frameId: string,
+ url: string
+): Promise<void> {
+ await pageOrFrame.evaluate(navigateFrame, frameId, url);
+
+ function navigateFrame(frameId: string, url: string) {
+ const frame = document.getElementById(frameId) as HTMLIFrameElement;
+ frame.src = url;
+ return new Promise(x => {
+ return (frame.onload = x);
+ });
+ }
+}
+
+export const dumpFrames = (frame: Frame, indentation?: string): string[] => {
+ indentation = indentation || '';
+ let description = frame.url().replace(/:\d{4,5}\//, ':<PORT>/');
+ if (frame.name()) {
+ description += ' (' + frame.name() + ')';
+ }
+ const result = [indentation + description];
+ for (const child of frame.childFrames()) {
+ result.push(...dumpFrames(child, ' ' + indentation));
+ }
+ return result;
+};
+
+export const waitEvent = async <T = any>(
+ emitter: EventEmitter<any>,
+ eventName: string,
+ predicate: (event: T) => boolean = () => {
+ return true;
+ }
+): Promise<T> => {
+ const deferred = Deferred.create<T>({
+ timeout: 5000,
+ message: `Waiting for ${eventName} event timed out.`,
+ });
+ const handler = (event: T) => {
+ if (!predicate(event)) {
+ return;
+ }
+ deferred.resolve(event);
+ };
+ emitter.on(eventName, handler);
+ try {
+ return await deferred.valueOrThrow();
+ } finally {
+ emitter.off(eventName, handler);
+ }
+};
+
+export interface FilePlaceholder {
+ filename: `${string}.webm`;
+ [Symbol.dispose](): void;
+}
+
+export function getUniqueVideoFilePlaceholder(): FilePlaceholder {
+ return {
+ filename: `${tmpdir()}/test-video-${Math.round(
+ Math.random() * 10000
+ )}.webm`,
+ [Symbol.dispose]() {
+ void rmIfExists(this.filename);
+ },
+ };
+}
+
+export function rmIfExists(file: string): Promise<void> {
+ return rm(file).catch(() => {});
+}
diff --git a/remote/test/puppeteer/test/src/waittask.spec.ts b/remote/test/puppeteer/test/src/waittask.spec.ts
new file mode 100644
index 0000000000..8ff52db16f
--- /dev/null
+++ b/remote/test/puppeteer/test/src/waittask.spec.ts
@@ -0,0 +1,867 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import {TimeoutError, ElementHandle} from 'puppeteer';
+import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js';
+
+import {
+ createTimeout,
+ getTestState,
+ setupTestBrowserHooks,
+} from './mocha-utils.js';
+import {attachFrame, detachFrame} from './utils.js';
+
+describe('waittask specs', function () {
+ setupTestBrowserHooks();
+
+ describe('Frame.waitForFunction', function () {
+ it('should accept a string', async () => {
+ const {page} = await getTestState();
+
+ const watchdog = page.waitForFunction('self.__FOO === 1');
+ await page.evaluate(() => {
+ return ((self as unknown as {__FOO: number}).__FOO = 1);
+ });
+ await watchdog;
+ });
+ it('should work when resolved right before execution context disposal', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluateOnNewDocument(() => {
+ return ((globalThis as any).__RELOADED = true);
+ });
+ await page.waitForFunction(() => {
+ if (!(globalThis as any).__RELOADED) {
+ window.location.reload();
+ return false;
+ }
+ return true;
+ });
+ });
+ it('should poll on interval', async () => {
+ const {page} = await getTestState();
+ const startTime = Date.now();
+ const polling = 100;
+ const watchdog = page.waitForFunction(
+ () => {
+ return (globalThis as any).__FOO === 'hit';
+ },
+ {polling}
+ );
+ await page.evaluate(() => {
+ setTimeout(() => {
+ (globalThis as any).__FOO = 'hit';
+ }, 50);
+ });
+ await watchdog;
+ expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
+ });
+ it('should poll on mutation', async () => {
+ const {page} = await getTestState();
+
+ let success = false;
+ const watchdog = page
+ .waitForFunction(
+ () => {
+ return (globalThis as any).__FOO === 'hit';
+ },
+ {
+ polling: 'mutation',
+ }
+ )
+ .then(() => {
+ return (success = true);
+ });
+ await page.evaluate(() => {
+ return ((globalThis as any).__FOO = 'hit');
+ });
+ expect(success).toBe(false);
+ await page.evaluate(() => {
+ return document.body.appendChild(document.createElement('div'));
+ });
+ await watchdog;
+ });
+ it('should poll on mutation async', async () => {
+ const {page} = await getTestState();
+
+ let success = false;
+ const watchdog = page
+ .waitForFunction(
+ async () => {
+ return (globalThis as any).__FOO === 'hit';
+ },
+ {
+ polling: 'mutation',
+ }
+ )
+ .then(() => {
+ return (success = true);
+ });
+ await page.evaluate(async () => {
+ return ((globalThis as any).__FOO = 'hit');
+ });
+ expect(success).toBe(false);
+ await page.evaluate(async () => {
+ return document.body.appendChild(document.createElement('div'));
+ });
+ await watchdog;
+ });
+ it('should poll on raf', async () => {
+ const {page} = await getTestState();
+
+ const watchdog = page.waitForFunction(
+ () => {
+ return (globalThis as any).__FOO === 'hit';
+ },
+ {
+ polling: 'raf',
+ }
+ );
+ await page.evaluate(() => {
+ return ((globalThis as any).__FOO = 'hit');
+ });
+ await watchdog;
+ });
+ it('should poll on raf async', async () => {
+ const {page} = await getTestState();
+
+ const watchdog = page.waitForFunction(
+ async () => {
+ return (globalThis as any).__FOO === 'hit';
+ },
+ {
+ polling: 'raf',
+ }
+ );
+ await page.evaluate(async () => {
+ return ((globalThis as any).__FOO = 'hit');
+ });
+ await watchdog;
+ });
+ it('should work with strict CSP policy', async () => {
+ const {page, server} = await getTestState();
+
+ server.setCSP('/empty.html', 'script-src ' + server.PREFIX);
+ await page.goto(server.EMPTY_PAGE);
+ let error!: Error;
+ await Promise.all([
+ page
+ .waitForFunction(
+ () => {
+ return (globalThis as any).__FOO === 'hit';
+ },
+ {
+ polling: 'raf',
+ }
+ )
+ .catch(error_ => {
+ return (error = error_);
+ }),
+ page.evaluate(() => {
+ return ((globalThis as any).__FOO = 'hit');
+ }),
+ ]);
+ expect(error).toBeUndefined();
+ });
+ it('should throw negative polling interval', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ try {
+ await page.waitForFunction(
+ () => {
+ return !!document.body;
+ },
+ {polling: -10}
+ );
+ } catch (error_) {
+ if (isErrorLike(error_)) {
+ error = error_ as Error;
+ }
+ }
+ expect(error?.message).toContain(
+ 'Cannot poll with non-positive interval'
+ );
+ });
+ it('should return the success value as a JSHandle', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await (
+ await page.waitForFunction(() => {
+ return 5;
+ })
+ ).jsonValue()
+ ).toBe(5);
+ });
+ it('should return the window as a success value', async () => {
+ const {page} = await getTestState();
+
+ expect(
+ await page.waitForFunction(() => {
+ return window;
+ })
+ ).toBeTruthy();
+ });
+ it('should accept ElementHandle arguments', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent('<div></div>');
+ using div = (await page.$('div'))!;
+ let resolved = false;
+ const waitForFunction = page
+ .waitForFunction(
+ element => {
+ return element.localName === 'div' && !element.parentElement;
+ },
+ {},
+ div
+ )
+ .then(() => {
+ return (resolved = true);
+ });
+ expect(resolved).toBe(false);
+ await page.evaluate((element: HTMLElement) => {
+ return element.remove();
+ }, div);
+ await waitForFunction;
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page
+ .waitForFunction(
+ () => {
+ return false;
+ },
+ {timeout: 10}
+ )
+ .catch(error_ => {
+ return (error = error_);
+ });
+
+ expect(error).toBeInstanceOf(TimeoutError);
+ expect(error?.message).toContain('Waiting failed: 10ms exceeded');
+ });
+ it('should respect default timeout', async () => {
+ const {page} = await getTestState();
+
+ page.setDefaultTimeout(1);
+ let error!: Error;
+ await page
+ .waitForFunction(() => {
+ return false;
+ })
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ expect(error?.message).toContain('Waiting failed: 1ms exceeded');
+ });
+ it('should disable timeout when its set to 0', async () => {
+ const {page} = await getTestState();
+
+ const watchdog = page.waitForFunction(
+ () => {
+ (globalThis as any).__counter =
+ ((globalThis as any).__counter || 0) + 1;
+ return (globalThis as any).__injected;
+ },
+ {timeout: 0, polling: 10}
+ );
+ await page.waitForFunction(() => {
+ return (globalThis as any).__counter > 10;
+ });
+ await page.evaluate(() => {
+ return ((globalThis as any).__injected = true);
+ });
+ await watchdog;
+ });
+ it('should survive cross-process navigation', async () => {
+ const {page, server} = await getTestState();
+
+ let fooFound = false;
+ const waitForFunction = page
+ .waitForFunction(() => {
+ return (globalThis as unknown as {__FOO: number}).__FOO === 1;
+ })
+ .then(() => {
+ return (fooFound = true);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(fooFound).toBe(false);
+ await page.reload();
+ expect(fooFound).toBe(false);
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html');
+ expect(fooFound).toBe(false);
+ await page.evaluate(() => {
+ return ((globalThis as any).__FOO = 1);
+ });
+ await waitForFunction;
+ expect(fooFound).toBe(true);
+ });
+ it('should survive navigations', async () => {
+ const {page, server} = await getTestState();
+
+ const watchdog = page.waitForFunction(() => {
+ return (globalThis as any).__done;
+ });
+ await page.goto(server.EMPTY_PAGE);
+ await page.goto(server.PREFIX + '/consolelog.html');
+ await page.evaluate(() => {
+ return ((globalThis as any).__done = true);
+ });
+ await watchdog;
+ });
+ it('should be cancellable', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const abortController = new AbortController();
+ const task = page.waitForFunction(
+ () => {
+ return (globalThis as any).__done;
+ },
+ {
+ signal: abortController.signal,
+ }
+ );
+ abortController.abort();
+ await expect(task).rejects.toThrow(/aborted/);
+ });
+ });
+
+ describe('Page.waitForTimeout', () => {
+ it('waits for the given timeout before resolving', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const startTime = Date.now();
+ await page.waitForTimeout(1000);
+ const endTime = Date.now();
+ /* In a perfect world endTime - startTime would be exactly 1000 but we
+ * expect some fluctuations and for it to be off by a little bit. So to
+ * avoid a flaky test we'll make sure it waited for roughly 1 second.
+ */
+ expect(endTime - startTime).toBeGreaterThan(700);
+ expect(endTime - startTime).toBeLessThan(1300);
+ });
+ });
+
+ describe('Frame.waitForTimeout', () => {
+ it('waits for the given timeout before resolving', async () => {
+ const {page, server} = await getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ const frame = page.mainFrame();
+ const startTime = Date.now();
+ await frame.waitForTimeout(1000);
+ const endTime = Date.now();
+ /* In a perfect world endTime - startTime would be exactly 1000 but we
+ * expect some fluctuations and for it to be off by a little bit. So to
+ * avoid a flaky test we'll make sure it waited for roughly 1 second
+ */
+ expect(endTime - startTime).toBeGreaterThan(700);
+ expect(endTime - startTime).toBeLessThan(1300);
+ });
+ });
+
+ describe('Frame.waitForSelector', function () {
+ const addElement = (tag: string) => {
+ return document.body.appendChild(document.createElement(tag));
+ };
+
+ it('should immediately resolve promise if node exists', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const frame = page.mainFrame();
+ await frame.waitForSelector('*');
+ await frame.evaluate(addElement, 'div');
+ await frame.waitForSelector('div');
+ });
+
+ it('should be cancellable', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const abortController = new AbortController();
+ const task = page.waitForSelector('wrong', {
+ signal: abortController.signal,
+ });
+ abortController.abort();
+ await expect(task).rejects.toThrow(/aborted/);
+ });
+
+ it('should work with removed MutationObserver', async () => {
+ const {page} = await getTestState();
+
+ await page.evaluate(() => {
+ // @ts-expect-error We want to remove it for the test.
+ return delete window.MutationObserver;
+ });
+ const [handle] = await Promise.all([
+ page.waitForSelector('.zombo'),
+ page.setContent(`<div class='zombo'>anything</div>`),
+ ]);
+ expect(
+ await page.evaluate(x => {
+ return x?.textContent;
+ }, handle)
+ ).toBe('anything');
+ });
+
+ it('should resolve promise when node is added', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const frame = page.mainFrame();
+ const watchdog = frame.waitForSelector('div');
+ await frame.evaluate(addElement, 'br');
+ await frame.evaluate(addElement, 'div');
+ using eHandle = (await watchdog)!;
+ const tagName = await (await eHandle.getProperty('tagName')).jsonValue();
+ expect(tagName).toBe('DIV');
+ });
+
+ it('should work when node is added through innerHTML', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const watchdog = page.waitForSelector('h3 div');
+ await page.evaluate(addElement, 'span');
+ await page.evaluate(() => {
+ return (document.querySelector('span')!.innerHTML =
+ '<h3><div></div></h3>');
+ });
+ await watchdog;
+ });
+
+ it('Page.waitForSelector is shortcut for main frame', async () => {
+ const {page, server} = await getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const otherFrame = page.frames()[1]!;
+ const watchdog = page.waitForSelector('div');
+ await otherFrame.evaluate(addElement, 'div');
+ await page.evaluate(addElement, 'div');
+ using eHandle = await watchdog;
+ expect(eHandle?.frame).toBe(page.mainFrame());
+ });
+
+ it('should run in specified frame', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await attachFrame(page, 'frame2', server.EMPTY_PAGE);
+ const frame1 = page.frames()[1]!;
+ const frame2 = page.frames()[2]!;
+ const waitForSelectorPromise = frame2.waitForSelector('div');
+ await frame1.evaluate(addElement, 'div');
+ await frame2.evaluate(addElement, 'div');
+ using eHandle = await waitForSelectorPromise;
+ expect(eHandle?.frame).toBe(frame2);
+ });
+
+ it('should throw when frame is detached', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const frame = page.frames()[1]!;
+ let waitError: Error | undefined;
+ const waitPromise = frame.waitForSelector('.box').catch(error => {
+ return (waitError = error);
+ });
+ await detachFrame(page, 'frame1');
+ await waitPromise;
+ expect(waitError).toBeTruthy();
+ expect(waitError?.message).toContain(
+ 'waitForFunction failed: frame got detached.'
+ );
+ });
+ it('should survive cross-process navigation', async () => {
+ const {page, server} = await getTestState();
+
+ let boxFound = false;
+ const waitForSelector = page.waitForSelector('.box').then(() => {
+ return (boxFound = true);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(boxFound).toBe(false);
+ await page.reload();
+ expect(boxFound).toBe(false);
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html');
+ await waitForSelector;
+ expect(boxFound).toBe(true);
+ });
+ it('should wait for element to be visible (display)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {visible: true});
+ await page.setContent('<div style="display: none">text</div>');
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ e.style.removeProperty('display');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be visible (visibility)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {visible: true});
+ await page.setContent('<div style="visibility: hidden">text</div>');
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ e.style.setProperty('visibility', 'collapse');
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ e.style.removeProperty('visibility');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be visible (bounding box)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {visible: true});
+ await page.setContent('<div style="width: 0">text</div>');
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ e.style.setProperty('height', '0');
+ e.style.removeProperty('width');
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ e.style.removeProperty('height');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be visible recursively', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div#inner', {
+ visible: true,
+ });
+ await page.setContent(
+ `<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`
+ );
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ return e.style.removeProperty('display');
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ return e.style.removeProperty('visibility');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be hidden (visibility)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {hidden: true});
+ await page.setContent(`<div style='display: block;'>text</div>`);
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ return e.style.setProperty('visibility', 'hidden');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be hidden (display)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {hidden: true});
+ await page.setContent(`<div style='display: block;'>text</div>`);
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ return e.style.setProperty('display', 'none');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be hidden (bounding box)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {hidden: true});
+ await page.setContent('<div>text</div>');
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40)])
+ ).resolves.toBeFalsy();
+ await element.evaluate(e => {
+ e.style.setProperty('height', '0');
+ });
+ await expect(promise).resolves.toBeTruthy();
+ });
+ it('should wait for element to be hidden (removal)', async () => {
+ const {page} = await getTestState();
+
+ const promise = page.waitForSelector('div', {hidden: true});
+ await page.setContent(`<div>text</div>`);
+ using element = await page.evaluateHandle(() => {
+ return document.getElementsByTagName('div')[0]!;
+ });
+ await expect(
+ Promise.race([promise, createTimeout(40, true)])
+ ).resolves.toBeTruthy();
+ await element.evaluate(e => {
+ e.remove();
+ });
+ await expect(promise).resolves.toBeFalsy();
+ });
+ it('should return null if waiting to hide non-existing element', async () => {
+ const {page} = await getTestState();
+
+ using handle = await page.waitForSelector('non-existing', {
+ hidden: true,
+ });
+ expect(handle).toBe(null);
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.waitForSelector('div', {timeout: 10}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ expect(error?.message).toContain(
+ 'Waiting for selector `div` failed: Waiting failed: 10ms exceeded'
+ );
+ });
+ it('should have an error message specifically for awaiting an element to be hidden', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div>text</div>`);
+ let error!: Error;
+ await page
+ .waitForSelector('div', {hidden: true, timeout: 10})
+ .catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeTruthy();
+ expect(error?.message).toContain(
+ 'Waiting for selector `div` failed: Waiting failed: 10ms exceeded'
+ );
+ });
+
+ it('should respond to node attribute mutation', async () => {
+ const {page} = await getTestState();
+
+ let divFound = false;
+ const waitForSelector = page.waitForSelector('.zombo').then(() => {
+ return (divFound = true);
+ });
+ await page.setContent(`<div class='notZombo'></div>`);
+ expect(divFound).toBe(false);
+ await page.evaluate(() => {
+ return (document.querySelector('div')!.className = 'zombo');
+ });
+ expect(await waitForSelector).toBe(true);
+ });
+ it('should return the element handle', async () => {
+ const {page} = await getTestState();
+
+ const waitForSelector = page.waitForSelector('.zombo');
+ await page.setContent(`<div class='zombo'>anything</div>`);
+ expect(
+ await page.evaluate(
+ x => {
+ return x?.textContent;
+ },
+ await waitForSelector
+ )
+ ).toBe('anything');
+ });
+ it('should have correct stack trace for timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.waitForSelector('.zombo', {timeout: 10}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error?.stack).toContain(
+ 'Waiting for selector `.zombo` failed: Waiting failed: 10ms exceeded'
+ );
+ // The extension is ts here as Mocha maps back via sourcemaps.
+ expect(error?.stack).toContain('WaitTask.ts');
+ });
+ });
+
+ describe('Frame.waitForXPath', function () {
+ const addElement = (tag: string) => {
+ return document.body.appendChild(document.createElement(tag));
+ };
+
+ it('should support some fancy xpath', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<p>red herring</p><p>hello world </p>`);
+ const waitForXPath = page.waitForXPath(
+ '//p[normalize-space(.)="hello world"]'
+ );
+ expect(
+ await page.evaluate(
+ x => {
+ return x?.textContent;
+ },
+ await waitForXPath
+ )
+ ).toBe('hello world ');
+ });
+ it('should respect timeout', async () => {
+ const {page} = await getTestState();
+
+ let error!: Error;
+ await page.waitForXPath('//div', {timeout: 10}).catch(error_ => {
+ return (error = error_);
+ });
+ expect(error).toBeInstanceOf(TimeoutError);
+ expect(error?.message).toContain('Waiting failed: 10ms exceeded');
+ });
+ it('should run in specified frame', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await attachFrame(page, 'frame2', server.EMPTY_PAGE);
+ const frame1 = page.frames()[1]!;
+ const frame2 = page.frames()[2]!;
+ const waitForXPathPromise = frame2.waitForXPath('//div');
+ await frame1.evaluate(addElement, 'div');
+ await frame2.evaluate(addElement, 'div');
+ using eHandle = await waitForXPathPromise;
+ expect(eHandle?.frame).toBe(frame2);
+ });
+ it('should throw when frame is detached', async () => {
+ const {page, server} = await getTestState();
+
+ await attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const frame = page.frames()[1]!;
+ let waitError: Error | undefined;
+ const waitPromise = frame
+ .waitForXPath('//*[@class="box"]')
+ .catch(error => {
+ return (waitError = error);
+ });
+ await detachFrame(page, 'frame1');
+ await waitPromise;
+ expect(waitError).toBeTruthy();
+ expect(waitError?.message).toContain(
+ 'waitForFunction failed: frame got detached.'
+ );
+ });
+ it('hidden should wait for display: none', async () => {
+ const {page} = await getTestState();
+
+ let divHidden = false;
+ await page.setContent(`<div style='display: block;'>text</div>`);
+ const waitForXPath = page
+ .waitForXPath('//div', {hidden: true})
+ .then(() => {
+ return (divHidden = true);
+ });
+ await page.waitForXPath('//div'); // do a round trip
+ expect(divHidden).toBe(false);
+ await page.evaluate(() => {
+ return document
+ .querySelector('div')
+ ?.style.setProperty('display', 'none');
+ });
+ expect(await waitForXPath).toBe(true);
+ expect(divHidden).toBe(true);
+ });
+ it('hidden should return null if the element is not found', async () => {
+ const {page} = await getTestState();
+
+ using waitForXPath = await page.waitForXPath('//div', {hidden: true});
+
+ expect(waitForXPath).toBe(null);
+ });
+ it('hidden should return an empty element handle if the element is found', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div style='display: none;'>text</div>`);
+
+ using waitForXPath = await page.waitForXPath('//div', {hidden: true});
+
+ expect(waitForXPath).toBeInstanceOf(ElementHandle);
+ });
+ it('should return the element handle', async () => {
+ const {page} = await getTestState();
+
+ const waitForXPath = page.waitForXPath('//*[@class="zombo"]');
+ await page.setContent(`<div class='zombo'>anything</div>`);
+ expect(
+ await page.evaluate(
+ x => {
+ return x?.textContent;
+ },
+ await waitForXPath
+ )
+ ).toBe('anything');
+ });
+ it('should allow you to select a text node', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div>some text</div>`);
+ using text = await page.waitForXPath('//div/text()');
+ expect(await (await text!.getProperty('nodeType')!).jsonValue()).toBe(
+ 3 /* Node.TEXT_NODE */
+ );
+ });
+ it('should allow you to select an element with single slash', async () => {
+ const {page} = await getTestState();
+
+ await page.setContent(`<div>some text</div>`);
+ const waitForXPath = page.waitForXPath('/html/body/div');
+ expect(
+ await page.evaluate(
+ x => {
+ return x?.textContent;
+ },
+ await waitForXPath
+ )
+ ).toBe('some text');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/src/worker.spec.ts b/remote/test/puppeteer/test/src/worker.spec.ts
new file mode 100644
index 0000000000..254ff4a514
--- /dev/null
+++ b/remote/test/puppeteer/test/src/worker.spec.ts
@@ -0,0 +1,109 @@
+/**
+ * @license
+ * Copyright 2020 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+import type {WebWorker} from 'puppeteer-core/internal/api/WebWorker.js';
+import type {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+import {waitEvent} from './utils.js';
+
+describe('Workers', function () {
+ setupTestBrowserHooks();
+
+ it('Page.workers', async () => {
+ const {page, server} = await getTestState();
+
+ await Promise.all([
+ waitEvent(page, 'workercreated'),
+ page.goto(server.PREFIX + '/worker/worker.html'),
+ ]);
+ const worker = page.workers()[0]!;
+ expect(worker?.url()).toContain('worker.js');
+
+ expect(
+ await worker?.evaluate(() => {
+ return (globalThis as any).workerFunction();
+ })
+ ).toBe('worker function result');
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(page.workers()).toHaveLength(0);
+ });
+ it('should emit created and destroyed events', async () => {
+ const {page} = await getTestState();
+
+ const workerCreatedPromise = waitEvent<WebWorker>(page, 'workercreated');
+ using workerObj = await page.evaluateHandle(() => {
+ return new Worker('data:text/javascript,1');
+ });
+ const worker = await workerCreatedPromise;
+ using workerThisObj = await worker.evaluateHandle(() => {
+ return this;
+ });
+ const workerDestroyedPromise = waitEvent(page, 'workerdestroyed');
+ await page.evaluate((workerObj: Worker) => {
+ return workerObj.terminate();
+ }, workerObj);
+ expect(await workerDestroyedPromise).toBe(worker);
+ const error = await workerThisObj.getProperty('self').catch(error => {
+ return error;
+ });
+ expect(error.message).toContain('Most likely the worker has been closed.');
+ });
+ it('should report console logs', async () => {
+ const {page} = await getTestState();
+
+ const [message] = await Promise.all([
+ waitEvent(page, 'console'),
+ page.evaluate(() => {
+ return new Worker(`data:text/javascript,console.log(1)`);
+ }),
+ ]);
+ expect(message.text()).toBe('1');
+ expect(message.location()).toEqual({
+ url: '',
+ lineNumber: 0,
+ columnNumber: 8,
+ });
+ });
+ it('should have JSHandles for console logs', async () => {
+ const {page} = await getTestState();
+
+ const logPromise = waitEvent<ConsoleMessage>(page, 'console');
+ await page.evaluate(() => {
+ return new Worker(`data:text/javascript,console.log(1,2,3,this)`);
+ });
+ const log = await logPromise;
+ expect(log.text()).toBe('1 2 3 JSHandle@object');
+ expect(log.args()).toHaveLength(4);
+ expect(await (await log.args()[3]!.getProperty('origin')).jsonValue()).toBe(
+ 'null'
+ );
+ });
+ it('should have an execution context', async () => {
+ const {page} = await getTestState();
+
+ const workerCreatedPromise = waitEvent<WebWorker>(page, 'workercreated');
+ await page.evaluate(() => {
+ return new Worker(`data:text/javascript,console.log(1)`);
+ });
+ const worker = await workerCreatedPromise;
+ expect(await worker.evaluate('1+1')).toBe(2);
+ });
+ it('should report errors', async () => {
+ const {page} = await getTestState();
+
+ const errorPromise = waitEvent<Error>(page, 'pageerror');
+ await page.evaluate(() => {
+ return new Worker(
+ `data:text/javascript, throw new Error('this is my error');`
+ );
+ });
+ const errorLog = await errorPromise;
+ expect(errorLog.message).toContain('this is my error');
+ });
+});