summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/test
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--remote/test/puppeteer/test-browser/connection.spec.js55
-rw-r--r--remote/test/puppeteer/test-browser/debug.spec.js65
-rw-r--r--remote/test/puppeteer/test-browser/helper.js27
-rw-r--r--remote/test/puppeteer/test/.eslintrc.js13
-rw-r--r--remote/test/puppeteer/test/CDPSession.spec.ts106
-rw-r--r--remote/test/puppeteer/test/EventEmitter.spec.ts170
-rw-r--r--remote/test/puppeteer/test/README.md88
-rw-r--r--remote/test/puppeteer/test/accessibility.spec.ts520
-rw-r--r--remote/test/puppeteer/test/ariaqueryhandler.spec.ts565
-rw-r--r--remote/test/puppeteer/test/assert-coverage-test.js25
-rw-r--r--remote/test/puppeteer/test/assets/beforeunload.html10
-rw-r--r--remote/test/puppeteer/test/assets/cached/one-style.css3
-rw-r--r--remote/test/puppeteer/test/assets/cached/one-style.html2
-rw-r--r--remote/test/puppeteer/test/assets/chromium-linux.zipbin0 -> 325 bytes
-rw-r--r--remote/test/puppeteer/test/assets/consolelog.html17
-rw-r--r--remote/test/puppeteer/test/assets/csp.html1
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttfbin0 -> 136940 bytes
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/OFL.txt95
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/involved.html26
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/media.html4
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/multiple.html8
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/simple.html6
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/sourceurl.html7
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css3
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css4
-rw-r--r--remote/test/puppeteer/test/assets/csscoverage/unused.html7
-rw-r--r--remote/test/puppeteer/test/assets/detect-touch.html12
-rw-r--r--remote/test/puppeteer/test/assets/digits/0.pngbin0 -> 434 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/1.pngbin0 -> 346 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/2.pngbin0 -> 413 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/3.pngbin0 -> 434 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/4.pngbin0 -> 403 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/5.pngbin0 -> 422 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/6.pngbin0 -> 445 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/7.pngbin0 -> 387 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/8.pngbin0 -> 447 bytes
-rw-r--r--remote/test/puppeteer/test/assets/digits/9.pngbin0 -> 437 bytes
-rw-r--r--remote/test/puppeteer/test/assets/dynamic-oopif.html10
-rw-r--r--remote/test/puppeteer/test/assets/empty.html0
-rw-r--r--remote/test/puppeteer/test/assets/error.html15
-rw-r--r--remote/test/puppeteer/test/assets/es6/.eslintrc5
-rw-r--r--remote/test/puppeteer/test/assets/es6/es6import.js2
-rw-r--r--remote/test/puppeteer/test/assets/es6/es6module.js1
-rw-r--r--remote/test/puppeteer/test/assets/es6/es6pathimport.js2
-rw-r--r--remote/test/puppeteer/test/assets/file-to-upload.txt1
-rw-r--r--remote/test/puppeteer/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2bin0 -> 211 bytes
-rw-r--r--remote/test/puppeteer/test/assets/frames/frame.html8
-rw-r--r--remote/test/puppeteer/test/assets/frames/frameset.html8
-rw-r--r--remote/test/puppeteer/test/assets/frames/nested-frames.html25
-rw-r--r--remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html1
-rw-r--r--remote/test/puppeteer/test/assets/frames/one-frame.html1
-rw-r--r--remote/test/puppeteer/test/assets/frames/script.js1
-rw-r--r--remote/test/puppeteer/test/assets/frames/style.css3
-rw-r--r--remote/test/puppeteer/test/assets/frames/two-frames.html13
-rw-r--r--remote/test/puppeteer/test/assets/global-var.html3
-rw-r--r--remote/test/puppeteer/test/assets/grid.html52
-rw-r--r--remote/test/puppeteer/test/assets/historyapi.html5
-rw-r--r--remote/test/puppeteer/test/assets/idle-detector.html23
-rw-r--r--remote/test/puppeteer/test/assets/injectedfile.js2
-rw-r--r--remote/test/puppeteer/test/assets/injectedstyle.css3
-rw-r--r--remote/test/puppeteer/test/assets/input/button.html16
-rw-r--r--remote/test/puppeteer/test/assets/input/checkbox.html42
-rw-r--r--remote/test/puppeteer/test/assets/input/fileupload.html9
-rw-r--r--remote/test/puppeteer/test/assets/input/keyboard.html42
-rw-r--r--remote/test/puppeteer/test/assets/input/mouse-helper.js74
-rw-r--r--remote/test/puppeteer/test/assets/input/rotatedButton.html21
-rw-r--r--remote/test/puppeteer/test/assets/input/scrollable.html23
-rw-r--r--remote/test/puppeteer/test/assets/input/select.html69
-rw-r--r--remote/test/puppeteer/test/assets/input/textarea.html15
-rw-r--r--remote/test/puppeteer/test/assets/input/touches.html35
-rw-r--r--remote/test/puppeteer/test/assets/input/wheel.html43
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/eval.html1
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/involved.html15
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/multiple.html2
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/ranges.html2
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/script1.js1
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/script2.js1
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/simple.html2
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/sourceurl.html4
-rw-r--r--remote/test/puppeteer/test/assets/jscoverage/unused.html1
-rw-r--r--remote/test/puppeteer/test/assets/mobile.html1
-rw-r--r--remote/test/puppeteer/test/assets/modernizr.js3
-rw-r--r--remote/test/puppeteer/test/assets/networkidle.html19
-rw-r--r--remote/test/puppeteer/test/assets/offscreenbuttons.html36
-rw-r--r--remote/test/puppeteer/test/assets/one-style.css3
-rw-r--r--remote/test/puppeteer/test/assets/one-style.html2
-rw-r--r--remote/test/puppeteer/test/assets/playground.html15
-rw-r--r--remote/test/puppeteer/test/assets/popup/popup.html9
-rw-r--r--remote/test/puppeteer/test/assets/popup/window-open.html11
-rw-r--r--remote/test/puppeteer/test/assets/pptr.pngbin0 -> 6138 bytes
-rw-r--r--remote/test/puppeteer/test/assets/resetcss.html50
-rw-r--r--remote/test/puppeteer/test/assets/self-request.html5
-rw-r--r--remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html3
-rw-r--r--remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js0
-rw-r--r--remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css3
-rw-r--r--remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html5
-rw-r--r--remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js7
-rw-r--r--remote/test/puppeteer/test/assets/shadow.html17
-rw-r--r--remote/test/puppeteer/test/assets/simple-extension/content-script.js2
-rw-r--r--remote/test/puppeteer/test/assets/simple-extension/index.js2
-rw-r--r--remote/test/puppeteer/test/assets/simple-extension/manifest.json14
-rw-r--r--remote/test/puppeteer/test/assets/simple.json1
-rw-r--r--remote/test/puppeteer/test/assets/tamperable.html3
-rw-r--r--remote/test/puppeteer/test/assets/title.html1
-rw-r--r--remote/test/puppeteer/test/assets/worker/worker.html14
-rw-r--r--remote/test/puppeteer/test/assets/worker/worker.js16
-rw-r--r--remote/test/puppeteer/test/assets/wrappedlink.html32
-rw-r--r--remote/test/puppeteer/test/browser.spec.ts81
-rw-r--r--remote/test/puppeteer/test/browsercontext.spec.ts206
-rw-r--r--remote/test/puppeteer/test/chromiumonly.spec.ts159
-rw-r--r--remote/test/puppeteer/test/click.spec.ts352
-rw-r--r--remote/test/puppeteer/test/cookies.spec.ts526
-rw-r--r--remote/test/puppeteer/test/coverage-utils.js164
-rw-r--r--remote/test/puppeteer/test/coverage.spec.ts280
-rw-r--r--remote/test/puppeteer/test/defaultbrowsercontext.spec.ts104
-rw-r--r--remote/test/puppeteer/test/dialog.spec.ts73
-rw-r--r--remote/test/puppeteer/test/diffstyle.css13
-rw-r--r--remote/test/puppeteer/test/elementhandle.spec.ts449
-rw-r--r--remote/test/puppeteer/test/emulation.spec.ts355
-rw-r--r--remote/test/puppeteer/test/evaluation.spec.ts474
-rw-r--r--remote/test/puppeteer/test/fixtures.spec.ts93
-rw-r--r--remote/test/puppeteer/test/fixtures/closeme.js5
-rw-r--r--remote/test/puppeteer/test/fixtures/dumpio.js8
-rw-r--r--remote/test/puppeteer/test/frame.spec.ts269
-rw-r--r--remote/test/puppeteer/test/golden-chromium/csscoverage-involved.txt16
-rw-r--r--remote/test/puppeteer/test/golden-chromium/grid-cell-0.pngbin0 -> 436 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/grid-cell-1.pngbin0 -> 276 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/grid-cell-2.pngbin0 -> 428 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/grid-cell-3.pngbin0 -> 448 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/jscoverage-involved.txt28
-rw-r--r--remote/test/puppeteer/test/golden-chromium/mock-binary-response.pngbin0 -> 6789 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/screenshot-clip-odd-size.pngbin0 -> 81 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/screenshot-clip-rect.pngbin0 -> 1962 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/screenshot-element-bounding-box.pngbin0 -> 461 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional-offset.pngbin0 -> 138 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional.pngbin0 -> 138 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/screenshot-element-larger-than-viewport.pngbin0 -> 2807 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/screenshot-element-padding-border.pngbin0 -> 168 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/screenshot-element-rotate.pngbin0 -> 2342 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/screenshot-element-scrolled-into-view.pngbin0 -> 168 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/screenshot-grid-fullpage.pngbin0 -> 74972 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/screenshot-offscreen-clip.pngbin0 -> 326 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/screenshot-sanity.pngbin0 -> 36252 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/transparent.pngbin0 -> 119 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/vision-deficiency-achromatopsia.pngbin0 -> 33569 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/vision-deficiency-blurredVision.pngbin0 -> 84544 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/vision-deficiency-deuteranopia.pngbin0 -> 37483 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/vision-deficiency-protanopia.pngbin0 -> 36282 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/vision-deficiency-tritanopia.pngbin0 -> 37282 bytes
-rw-r--r--remote/test/puppeteer/test/golden-chromium/white.jpgbin0 -> 357 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/grid-cell-0.pngbin0 -> 331 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/grid-cell-1.pngbin0 -> 201 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.pngbin0 -> 75 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.pngbin0 -> 1371 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.pngbin0 -> 311 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.pngbin0 -> 113 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.pngbin0 -> 109 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.pngbin0 -> 2797 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.pngbin0 -> 153 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.pngbin0 -> 1800 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.pngbin0 -> 153 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.pngbin0 -> 55662 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.pngbin0 -> 326 bytes
-rw-r--r--remote/test/puppeteer/test/golden-firefox/screenshot-sanity.pngbin0 -> 26146 bytes
-rw-r--r--remote/test/puppeteer/test/golden-utils.js160
-rw-r--r--remote/test/puppeteer/test/headful.spec.ts204
-rw-r--r--remote/test/puppeteer/test/idle_override.spec.ts93
-rw-r--r--remote/test/puppeteer/test/ignorehttpserrors.spec.ts133
-rw-r--r--remote/test/puppeteer/test/input.spec.ts337
-rw-r--r--remote/test/puppeteer/test/jshandle.spec.ts282
-rw-r--r--remote/test/puppeteer/test/keyboard.spec.ts407
-rw-r--r--remote/test/puppeteer/test/launcher.spec.ts654
-rw-r--r--remote/test/puppeteer/test/mocha-ts-require.js11
-rw-r--r--remote/test/puppeteer/test/mocha-utils.ts279
-rw-r--r--remote/test/puppeteer/test/mouse.spec.ts241
-rw-r--r--remote/test/puppeteer/test/navigation.spec.ts774
-rw-r--r--remote/test/puppeteer/test/network.spec.ts571
-rw-r--r--remote/test/puppeteer/test/oopif.spec.ts74
-rw-r--r--remote/test/puppeteer/test/page.spec.ts1720
-rw-r--r--remote/test/puppeteer/test/queryselector.spec.ts507
-rw-r--r--remote/test/puppeteer/test/requestinterception.spec.ts703
-rwxr-xr-xremote/test/puppeteer/test/run_static_server.js33
-rw-r--r--remote/test/puppeteer/test/screenshot.spec.ts323
-rw-r--r--remote/test/puppeteer/test/target.spec.ts294
-rw-r--r--remote/test/puppeteer/test/touchscreen.spec.ts48
-rw-r--r--remote/test/puppeteer/test/tracing.spec.ts133
-rw-r--r--remote/test/puppeteer/test/tsconfig.json7
-rw-r--r--remote/test/puppeteer/test/tsconfig.test.json6
-rw-r--r--remote/test/puppeteer/test/utils.js135
-rw-r--r--remote/test/puppeteer/test/waittask.spec.ts774
-rw-r--r--remote/test/puppeteer/test/worker.spec.ts126
191 files changed, 15399 insertions, 0 deletions
diff --git a/remote/test/puppeteer/test-browser/connection.spec.js b/remote/test/puppeteer/test-browser/connection.spec.js
new file mode 100644
index 0000000000..eef8fe3c21
--- /dev/null
+++ b/remote/test/puppeteer/test-browser/connection.spec.js
@@ -0,0 +1,55 @@
+/**
+ * Copyright 2020 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Connection } from '../lib/esm/puppeteer/common/Connection.js';
+import { BrowserWebSocketTransport } from '../lib/esm/puppeteer/common/BrowserWebSocketTransport.js';
+import puppeteer from '../lib/esm/puppeteer/web.js';
+import expect from '../node_modules/expect/build-es5/index.js';
+import { getWebSocketEndpoint } from './helper.js';
+
+describe('creating a Connection', () => {
+ it('can create a real connection to the backend and send messages', async () => {
+ const wsUrl = getWebSocketEndpoint();
+ const transport = await BrowserWebSocketTransport.create(wsUrl);
+
+ const connection = new Connection(wsUrl, transport);
+ const result = await connection.send('Browser.getVersion');
+ /* We can't expect exact results as the version of Chrome/CDP might change
+ * and we don't want flakey tests, so let's assert the structure, which is
+ * enough to confirm the result was recieved successfully.
+ */
+ expect(result).toEqual({
+ protocolVersion: expect.any(String),
+ jsVersion: expect.any(String),
+ revision: expect.any(String),
+ userAgent: expect.any(String),
+ product: expect.any(String),
+ });
+ });
+});
+
+describe('puppeteer.connect', () => {
+ it('can connect over websocket and make requests to the backend', async () => {
+ const wsUrl = getWebSocketEndpoint();
+ const browser = await puppeteer.connect({
+ browserWSEndpoint: wsUrl,
+ });
+
+ const version = await browser.version();
+ const versionLooksCorrect = /.+Chrome\/\d{2}/.test(version);
+ expect(version).toEqual(expect.any(String));
+ expect(versionLooksCorrect).toEqual(true);
+ });
+});
diff --git a/remote/test/puppeteer/test-browser/debug.spec.js b/remote/test/puppeteer/test-browser/debug.spec.js
new file mode 100644
index 0000000000..971d3a5346
--- /dev/null
+++ b/remote/test/puppeteer/test-browser/debug.spec.js
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2020 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { debug } from '../lib/esm/puppeteer/common/Debug.js';
+import expect from '../node_modules/expect/build-es5/index.js';
+
+describe('debug', () => {
+ let originalLog;
+ let logs;
+ beforeEach(() => {
+ originalLog = console.log;
+ logs = [];
+ console.log = (...args) => {
+ logs.push(args);
+ };
+ });
+
+ afterEach(() => {
+ console.log = originalLog;
+ });
+
+ it('should return a function', async () => {
+ expect(debug('foo')).toBeInstanceOf(Function);
+ });
+
+ it('does not log to the console if __PUPPETEER_DEBUG global is not set', async () => {
+ const debugFn = debug('foo');
+ debugFn('lorem', 'ipsum');
+
+ expect(logs.length).toEqual(0);
+ });
+
+ it('logs to the console if __PUPPETEER_DEBUG global is set to *', async () => {
+ globalThis.__PUPPETEER_DEBUG = '*';
+ const debugFn = debug('foo');
+ debugFn('lorem', 'ipsum');
+
+ expect(logs.length).toEqual(1);
+ expect(logs).toEqual([['foo:', 'lorem', 'ipsum']]);
+ });
+
+ it('logs only messages matching the __PUPPETEER_DEBUG prefix', async () => {
+ globalThis.__PUPPETEER_DEBUG = 'foo';
+ const debugFoo = debug('foo');
+ const debugBar = debug('bar');
+ debugFoo('a');
+ debugBar('b');
+
+ expect(logs.length).toEqual(1);
+ expect(logs).toEqual([['foo:', 'a']]);
+ });
+});
diff --git a/remote/test/puppeteer/test-browser/helper.js b/remote/test/puppeteer/test-browser/helper.js
new file mode 100644
index 0000000000..6cfbe934d5
--- /dev/null
+++ b/remote/test/puppeteer/test-browser/helper.js
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2020 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Returns the web socket endpoint for the backend of the browser the tests run
+ * in. Used to create connections to that browser in Puppeteer for unit tests.
+ *
+ * It's available on window.__ENV__ because setup code in
+ * web-test-runner.config.js puts it there. If you're changing this code (or
+ * that code), make sure the other is updated accordingly.
+ */
+export function getWebSocketEndpoint() {
+ return window.__ENV__.wsEndpoint;
+}
diff --git a/remote/test/puppeteer/test/.eslintrc.js b/remote/test/puppeteer/test/.eslintrc.js
new file mode 100644
index 0000000000..9d86da20e1
--- /dev/null
+++ b/remote/test/puppeteer/test/.eslintrc.js
@@ -0,0 +1,13 @@
+module.exports = {
+ rules: {
+ 'no-restricted-imports': [
+ 'error',
+ {
+ /** The mocha tests run on the compiled output in the /lib directory
+ * so we should avoid importing from src.
+ */
+ patterns: ['*src*'],
+ },
+ ],
+ },
+};
diff --git a/remote/test/puppeteer/test/CDPSession.spec.ts b/remote/test/puppeteer/test/CDPSession.spec.ts
new file mode 100644
index 0000000000..2ebf10fd96
--- /dev/null
+++ b/remote/test/puppeteer/test/CDPSession.spec.ts
@@ -0,0 +1,106 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { waitEvent } from './utils.js';
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+ describeChromeOnly,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describeChromeOnly('Target.createCDPSession', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ const client = await page.target().createCDPSession();
+
+ await Promise.all([
+ client.send('Runtime.enable'),
+ client.send('Runtime.evaluate', { expression: 'window.foo = "bar"' }),
+ ]);
+ const foo = await page.evaluate(() => globalThis.foo);
+ expect(foo).toBe('bar');
+ });
+ it('should send events', async () => {
+ const { page, server } = getTestState();
+
+ const client = await page.target().createCDPSession();
+ await client.send('Network.enable');
+ const events = [];
+ client.on('Network.requestWillBeSent', (event) => events.push(event));
+ await page.goto(server.EMPTY_PAGE);
+ expect(events.length).toBe(1);
+ });
+ it('should enable and disable domains independently', async () => {
+ const { page } = getTestState();
+
+ const client = await page.target().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 } = getTestState();
+
+ const client = await page.target().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 = null;
+ try {
+ await client.send('Runtime.evaluate', {
+ expression: '3 + 1',
+ returnByValue: true,
+ });
+ } catch (error_) {
+ error = error_;
+ }
+ expect(error.message).toContain('Session closed.');
+ });
+ it('should throw nice errors', async () => {
+ const { page } = getTestState();
+
+ const client = await page.target().createCDPSession();
+ const error = await theSourceOfTheProblems().catch((error) => 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');
+ }
+ });
+});
diff --git a/remote/test/puppeteer/test/EventEmitter.spec.ts b/remote/test/puppeteer/test/EventEmitter.spec.ts
new file mode 100644
index 0000000000..bf20e7fe8f
--- /dev/null
+++ b/remote/test/puppeteer/test/EventEmitter.spec.ts
@@ -0,0 +1,170 @@
+/**
+ * Copyright 2020 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { EventEmitter } from '../lib/cjs/puppeteer/common/EventEmitter.js';
+import sinon from 'sinon';
+import expect from 'expect';
+
+describe('EventEmitter', () => {
+ let emitter;
+
+ beforeEach(() => {
+ emitter = new EventEmitter();
+ });
+
+ describe('on', () => {
+ const onTests = (methodName: 'on' | 'addListener'): void => {
+ it(`${methodName}: adds an event listener that is fired when the event is emitted`, () => {
+ const listener = sinon.spy();
+ emitter[methodName]('foo', listener);
+ emitter.emit('foo');
+ expect(listener.callCount).toEqual(1);
+ });
+
+ it(`${methodName} sends the event data to the handler`, () => {
+ const listener = sinon.spy();
+ const data = {};
+ emitter[methodName]('foo', listener);
+ emitter.emit('foo', data);
+ expect(listener.callCount).toEqual(1);
+ expect(listener.firstCall.args[0]).toBe(data);
+ });
+
+ it(`${methodName}: supports chaining`, () => {
+ const listener = sinon.spy();
+ const returnValue = emitter[methodName]('foo', listener);
+ expect(returnValue).toBe(emitter);
+ });
+ };
+ onTests('on');
+ // we support addListener for legacy reasons
+ onTests('addListener');
+ });
+
+ describe('off', () => {
+ const offTests = (methodName: 'off' | 'removeListener'): void => {
+ it(`${methodName}: removes the listener so it is no longer called`, () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ emitter.emit('foo');
+ expect(listener.callCount).toEqual(1);
+ emitter.off('foo', listener);
+ emitter.emit('foo');
+ expect(listener.callCount).toEqual(1);
+ });
+
+ it(`${methodName}: supports chaining`, () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ const returnValue = emitter.off('foo', listener);
+ expect(returnValue).toBe(emitter);
+ });
+ };
+ offTests('off');
+ // we support removeListener for legacy reasons
+ offTests('removeListener');
+ });
+
+ describe('once', () => {
+ it('only calls the listener once and then removes it', () => {
+ const listener = sinon.spy();
+ emitter.once('foo', listener);
+ emitter.emit('foo');
+ expect(listener.callCount).toEqual(1);
+ emitter.emit('foo');
+ expect(listener.callCount).toEqual(1);
+ });
+
+ it('supports chaining', () => {
+ const listener = sinon.spy();
+ const returnValue = emitter.once('foo', listener);
+ expect(returnValue).toBe(emitter);
+ });
+ });
+
+ describe('emit', () => {
+ it('calls all the listeners for an event', () => {
+ const listener1 = sinon.spy();
+ const listener2 = sinon.spy();
+ const listener3 = sinon.spy();
+ emitter.on('foo', listener1).on('foo', listener2).on('bar', listener3);
+
+ emitter.emit('foo');
+
+ expect(listener1.callCount).toEqual(1);
+ expect(listener2.callCount).toEqual(1);
+ expect(listener3.callCount).toEqual(0);
+ });
+
+ it('passes data through to the listener', () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ const data = {};
+
+ emitter.emit('foo', data);
+ expect(listener.callCount).toEqual(1);
+ expect(listener.firstCall.args[0]).toBe(data);
+ });
+
+ it('returns true if the event has listeners', () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ expect(emitter.emit('foo')).toBe(true);
+ });
+
+ it('returns false if the event has listeners', () => {
+ const listener = sinon.spy();
+ emitter.on('foo', listener);
+ expect(emitter.emit('notFoo')).toBe(false);
+ });
+ });
+
+ describe('listenerCount', () => {
+ it('returns the number of listeners for the given event', () => {
+ emitter.on('foo', () => {});
+ emitter.on('foo', () => {});
+ emitter.on('bar', () => {});
+ expect(emitter.listenerCount('foo')).toEqual(2);
+ expect(emitter.listenerCount('bar')).toEqual(1);
+ expect(emitter.listenerCount('noListeners')).toEqual(0);
+ });
+ });
+
+ describe('removeAllListeners', () => {
+ it('removes every listener from all events by default', () => {
+ emitter.on('foo', () => {}).on('bar', () => {});
+
+ emitter.removeAllListeners();
+ expect(emitter.emit('foo')).toBe(false);
+ expect(emitter.emit('bar')).toBe(false);
+ });
+
+ it('returns the emitter for chaining', () => {
+ expect(emitter.removeAllListeners()).toBe(emitter);
+ });
+
+ it('can filter to remove only listeners for a given event name', () => {
+ emitter
+ .on('foo', () => {})
+ .on('bar', () => {})
+ .on('bar', () => {});
+
+ emitter.removeAllListeners('bar');
+ expect(emitter.emit('foo')).toBe(true);
+ expect(emitter.emit('bar')).toBe(false);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/README.md b/remote/test/puppeteer/test/README.md
new file mode 100644
index 0000000000..304e3e0b0a
--- /dev/null
+++ b/remote/test/puppeteer/test/README.md
@@ -0,0 +1,88 @@
+# Puppeteer unit tests
+
+Unit tests in Puppeteer are written using [Mocha] as the test runner and [Expect] as the assertions library.
+
+## Test state
+
+
+We have some common setup that runs before each test and is defined in `mocha-utils.js`.
+
+You can use the `getTestState` function to read state. It exposes the following that you can use in your tests. These will be reset/tidied between tests automatically for you:
+
+* `puppeteer`: an instance of the Puppeteer library. This is exactly what you'd get if you ran `require('puppeteer')`.
+* `puppeteerPath`: the path to the root source file for Puppeteer.
+* `defaultBrowserOptions`: the default options the Puppeteer browser is launched from in test mode, so tests can use them and override if required.
+* `server`: a dummy test server instance (see `utils/testserver` for more).
+* `httpsServer`: a dummy test server HTTPS instance (see `utils/testserver` for more).
+* `isFirefox`: true if running in Firefox.
+* `isChrome`: true if running Chromium.
+* `isHeadless`: true if the test is in headless mode.
+
+If your test needs a browser instance, you can use the `setupTestBrowserHooks()` function which will automatically configure a browser that will be cleaned between each test suite run. You access this via `getTestState()`.
+
+If your test needs a Puppeteer page and context, you can use the `setupTestPageAndContextHooks()` function which will configure these. You can access `page` and `context` from `getTestState()` once you have done this.
+
+The best place to look is an existing test to see how they use the helpers.
+
+## Skipping tests in specific conditions
+
+Tests that are not expected to pass in Firefox can be skipped. You can skip an individual test by using `itFailsFirefox` rather than `it`. Similarly you can skip a describe block with `describeFailsFirefox`.
+
+There is also `describeChromeOnly` and `itChromeOnly` which will only execute the test if running in Chromium. Note that this is different from `describeFailsFirefox`: the goal is to get any `FailsFirefox` calls passing in Firefox, whereas `describeChromeOnly` should be used to test behaviour that will only ever apply in Chromium.
+
+There are also tests that assume a normal install flow, with browser binaries ending up in `.local-<browser>`, for example. Such tests are skipped with
+`itOnlyRegularInstall` which checks `BINARY` and `PUPPETEER_ALT_INSTALL` environment variables.
+
+[Mocha]: https://mochajs.org/
+[Expect]: https://www.npmjs.com/package/expect
+
+## Running tests
+
+Despite being named 'unit', these are integration tests, making sure public API methods and events work as expected.
+
+- To run all tests:
+
+```bash
+npm run unit
+```
+
+- __Important__: don't forget to first run TypeScript if you're testing local changes:
+
+```bash
+npm run tsc && npm run unit
+```
+
+- To run a specific test, substitute the `it` with `it.only`:
+
+```js
+ ...
+ it.only('should work', async function() {
+ const {server, page} = getTestState();
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response.ok).toBe(true);
+ });
+```
+
+- To disable a specific test, substitute the `it` with `xit` (mnemonic rule: '*cross it*'):
+
+```js
+ ...
+ // Using "xit" to skip specific test
+ xit('should work', async function({server, page}) {
+ const {server, page} = getTestState();
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response.ok).toBe(true);
+ });
+```
+
+- To run tests in non-headless mode:
+
+```bash
+HEADLESS=false npm run unit
+```
+
+- To run tests with custom browser executable:
+
+```bash
+BINARY=<path-to-executable> npm run unit
+```
diff --git a/remote/test/puppeteer/test/accessibility.spec.ts b/remote/test/puppeteer/test/accessibility.spec.ts
new file mode 100644
index 0000000000..e941c683a4
--- /dev/null
+++ b/remote/test/puppeteer/test/accessibility.spec.ts
@@ -0,0 +1,520 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+ describeFailsFirefox,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describeFailsFirefox('Accessibility', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ it('should work', async () => {
+ const { page, isFirefox } = 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: 'WebArea',
+ name: 'Accessibility Test',
+ children: [
+ { role: 'text', 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',
+ children: [
+ { role: 'menuitem', name: 'First Option', selected: true },
+ { role: 'menuitem', name: 'Second Option' },
+ ],
+ },
+ ],
+ };
+ expect(await page.accessibility.snapshot()).toEqual(golden);
+ });
+ it('should report uninteresting nodes', async () => {
+ const { page, isFirefox } = 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: 'text',
+ name: 'hi',
+ },
+ ],
+ },
+ ],
+ };
+ expect(
+ findFocusedNode(
+ await page.accessibility.snapshot({ interestingOnly: false })
+ )
+ ).toEqual(golden);
+ });
+ it('roledescription', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<div tabIndex=-1 aria-roledescription="foo">Hi</div>'
+ );
+ const snapshot = await page.accessibility.snapshot();
+ expect(snapshot.children[0].roledescription).toEqual('foo');
+ });
+ it('orientation', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<a href="" role="slider" aria-orientation="vertical">11</a>'
+ );
+ const snapshot = await page.accessibility.snapshot();
+ expect(snapshot.children[0].orientation).toEqual('vertical');
+ });
+ it('autocomplete', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<input type="number" aria-autocomplete="list" />');
+ const snapshot = await page.accessibility.snapshot();
+ expect(snapshot.children[0].autocomplete).toEqual('list');
+ });
+ it('multiselectable', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<div role="grid" tabIndex=-1 aria-multiselectable=true>hey</div>'
+ );
+ const snapshot = await page.accessibility.snapshot();
+ expect(snapshot.children[0].multiselectable).toEqual(true);
+ });
+ it('keyshortcuts', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<div role="grid" tabIndex=-1 aria-keyshortcuts="foo">hey</div>'
+ );
+ const snapshot = await page.accessibility.snapshot();
+ 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 } = 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: 'WebArea',
+ 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 } = 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: 'text',
+ name: 'my fake image',
+ },
+ ],
+ }
+ : {
+ role: 'generic',
+ name: '',
+ value: 'Edit this image: ',
+ children: [
+ {
+ role: 'text',
+ name: 'Edit this image:',
+ },
+ {
+ role: 'img',
+ name: 'my fake image',
+ },
+ ],
+ };
+ const snapshot = await page.accessibility.snapshot();
+ expect(snapshot.children[0]).toEqual(golden);
+ });
+ it('rich text editable fields with role should have children', async () => {
+ const { page, isFirefox } = getTestState();
+
+ await page.setContent(`
+ <div contenteditable="true" role='textbox'>
+ Edit this image: <img src="fakeimage.png" alt="my fake image">
+ </div>`);
+ const golden = isFirefox
+ ? {
+ role: 'entry',
+ name: '',
+ value: 'Edit this image: my fake image',
+ children: [
+ {
+ role: 'text',
+ name: 'my fake image',
+ },
+ ],
+ }
+ : {
+ role: 'textbox',
+ name: '',
+ value: 'Edit this image: ',
+ children: [
+ {
+ role: 'text',
+ name: 'Edit this image:',
+ },
+ {
+ role: 'img',
+ name: 'my fake image',
+ },
+ ],
+ };
+ const snapshot = await page.accessibility.snapshot();
+ expect(snapshot.children[0]).toEqual(golden);
+ });
+
+ // Firefox does not support contenteditable="plaintext-only".
+ describeFailsFirefox('plaintext contenteditable', function () {
+ it('plain text field with role should not have children', async () => {
+ const { page } = 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();
+ expect(snapshot.children[0]).toEqual({
+ role: 'textbox',
+ name: '',
+ value: 'Edit this image:',
+ });
+ });
+ it('plain text field without role should not have content', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`
+ <div contenteditable="plaintext-only">Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`);
+ const snapshot = await page.accessibility.snapshot();
+ expect(snapshot.children[0]).toEqual({
+ role: 'generic',
+ name: '',
+ });
+ });
+ it('plain text field with tabindex and without role should not have content', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`
+ <div contenteditable="plaintext-only" tabIndex=0>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`);
+ const snapshot = await page.accessibility.snapshot();
+ expect(snapshot.children[0]).toEqual({
+ role: 'generic',
+ name: '',
+ });
+ });
+ });
+ it('non editable textbox with role and tabIndex and label should not have children', async () => {
+ const { page, isFirefox } = 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();
+ expect(snapshot.children[0]).toEqual(golden);
+ });
+ it('checkbox with and tabIndex and label should not have children', async () => {
+ const { page, isFirefox } = 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();
+ expect(snapshot.children[0]).toEqual(golden);
+ });
+ it('checkbox without label should not have children', async () => {
+ const { page, isFirefox } = 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();
+ expect(snapshot.children[0]).toEqual(golden);
+ });
+
+ describe('root option', function () {
+ it('should work a button', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`<button>My Button</button>`);
+
+ const 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 } = getTestState();
+
+ await page.setContent(`<input title="My Input" value="My Value">`);
+
+ const 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 } = 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>
+ `);
+
+ const 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' },
+ ],
+ });
+ });
+ it('should return null when the element is no longer in DOM', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`<button>My Button</button>`);
+ const button = await page.$('button');
+ await page.$eval('button', (button) => button.remove());
+ expect(await page.accessibility.snapshot({ root: button })).toEqual(
+ null
+ );
+ });
+ it('should support the interestingOnly option', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`<div><button>My Button</button></div>`);
+ const div = await page.$('div');
+ expect(await page.accessibility.snapshot({ root: div })).toEqual(null);
+ expect(
+ await page.accessibility.snapshot({
+ root: div,
+ interestingOnly: false,
+ })
+ ).toEqual({
+ role: 'generic',
+ name: '',
+ children: [
+ {
+ role: 'button',
+ name: 'My Button',
+ children: [{ role: 'text', name: 'My Button' }],
+ },
+ ],
+ });
+ });
+ });
+ });
+ function findFocusedNode(node) {
+ 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/ariaqueryhandler.spec.ts b/remote/test/puppeteer/test/ariaqueryhandler.spec.ts
new file mode 100644
index 0000000000..83f78fd611
--- /dev/null
+++ b/remote/test/puppeteer/test/ariaqueryhandler.spec.ts
@@ -0,0 +1,565 @@
+/**
+ * Copyright 2020 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+ describeChromeOnly,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+import { ElementHandle } from '../lib/cjs/puppeteer/common/JSHandle.js';
+import utils from './utils.js';
+
+describeChromeOnly('AriaQueryHandler', () => {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ describe('parseAriaSelector', () => {
+ beforeEach(async () => {
+ const { page } = getTestState();
+ await page.setContent(
+ '<button id="btn" role="button"> Submit button and some spaces </button>'
+ );
+ });
+ it('should find button', async () => {
+ const { page } = getTestState();
+ const expectFound = async (button: ElementHandle) => {
+ const id = await button.evaluate((button: Element) => button.id);
+ expect(id).toBe('btn');
+ };
+ let button = await page.$(
+ 'aria/Submit button and some spaces[role="button"]'
+ );
+ await expectFound(button);
+ button = await page.$(
+ 'aria/ Submit button and some spaces[role="button"]'
+ );
+ await expectFound(button);
+ button = await page.$(
+ 'aria/Submit button and some spaces [role="button"]'
+ );
+ await expectFound(button);
+ button = await page.$(
+ 'aria/Submit button and some spaces [ role = "button" ] '
+ );
+ await expectFound(button);
+ button = await page.$(
+ 'aria/[role="button"]Submit button and some spaces'
+ );
+ await expectFound(button);
+ button = await page.$(
+ 'aria/Submit button [role="button"]and some spaces'
+ );
+ await expectFound(button);
+ button = await page.$(
+ 'aria/[name=" Submit button and some spaces"][role="button"]'
+ );
+ await expectFound(button);
+ button = await page.$(
+ 'aria/ignored[name="Submit button and some spaces"][role="button"]'
+ );
+ await expectFound(button);
+ });
+ });
+
+ describe('queryOne', () => {
+ it('should find button by role', async () => {
+ const { page } = getTestState();
+ await page.setContent(
+ '<div id="div"><button id="btn" role="button">Submit</button></div>'
+ );
+ const button = await page.$('aria/[role="button"]');
+ const id = await button.evaluate((button: Element) => button.id);
+ expect(id).toBe('btn');
+ });
+
+ it('should find button by name and role', async () => {
+ const { page } = getTestState();
+ await page.setContent(
+ '<div id="div"><button id="btn" role="button">Submit</button></div>'
+ );
+ const button = await page.$('aria/Submit[role="button"]');
+ const id = await button.evaluate((button: Element) => button.id);
+ expect(id).toBe('btn');
+ });
+
+ it('should find first matching element', async () => {
+ const { page } = 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 div = await page.$('aria/menu div');
+ const id = await div.evaluate((div: Element) => div.id);
+ expect(id).toBe('mnu1');
+ });
+
+ it('should find by name', async () => {
+ const { page } = 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>
+ `
+ );
+ const menu = await page.$('aria/menu-label1');
+ const id = await menu.evaluate((div: Element) => div.id);
+ expect(id).toBe('mnu1');
+ });
+
+ it('should find by name', async () => {
+ const { page } = 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>
+ `
+ );
+ const menu = await page.$('aria/menu-label2');
+ const id = await menu.evaluate((div: Element) => div.id);
+ expect(id).toBe('mnu2');
+ });
+ });
+
+ describe('queryAll', () => {
+ it('should find menu by name', async () => {
+ const { page } = 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');
+ const ids = await Promise.all(
+ divs.map((n) => n.evaluate((div: Element) => div.id))
+ );
+ expect(ids.join(', ')).toBe('mnu1, mnu2');
+ });
+ });
+ describe('queryAllArray', () => {
+ it('$$eval should handle many elements', async () => {
+ const { page } = 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) =>
+ buttons.reduce((acc, button) => acc + Number(button.textContent), 0)
+ );
+ expect(sum).toBe(50005000);
+ });
+ });
+
+ describe('waitForSelector (aria)', function () {
+ const addElement = (tag) =>
+ document.body.appendChild(document.createElement(tag));
+
+ it('should immediately resolve promise if node exists', async () => {
+ const { page, server } = getTestState();
+ 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 } = getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.exposeFunction('ariaQuerySelector', (a, b) => 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 } = getTestState();
+
+ await page.evaluate(() => delete window.MutationObserver);
+ const [handle] = await Promise.all([
+ page.waitForSelector('aria/anything'),
+ page.setContent(`<h1>anything</h1>`),
+ ]);
+ expect(
+ await page.evaluate((x: HTMLElement) => x.textContent, handle)
+ ).toBe('anything');
+ });
+
+ it('should resolve promise when node is added', async () => {
+ const { page, server } = 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');
+ const elementHandle = await watchdog;
+ const tagName = await elementHandle
+ .getProperty('tagName')
+ .then((element) => element.jsonValue());
+ expect(tagName).toBe('H1');
+ });
+
+ it('should work when node is added through innerHTML', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const watchdog = page.waitForSelector('aria/name');
+ await page.evaluate(addElement, 'span');
+ await page.evaluate(
+ () =>
+ (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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await utils.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');
+ const elementHandle = await watchdog;
+ expect(elementHandle.executionContext().frame()).toBe(page.mainFrame());
+ });
+
+ it('should run in specified frame', async () => {
+ const { page, server } = getTestState();
+
+ await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await utils.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');
+ const elementHandle = await waitForSelectorPromise;
+ expect(elementHandle.executionContext().frame()).toBe(frame2);
+ });
+
+ it('should throw when frame is detached', async () => {
+ const { page, server } = getTestState();
+
+ await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const frame = page.frames()[1];
+ let waitError = null;
+ const waitPromise = frame
+ .waitForSelector('aria/does-not-exist')
+ .catch((error) => (waitError = error));
+ await utils.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 } = getTestState();
+
+ let imgFound = false;
+ const waitForSelector = page
+ .waitForSelector('aria/[role="img"]')
+ .then(() => (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 } = getTestState();
+
+ let divFound = false;
+ const waitForSelector = page
+ .waitForSelector('aria/name', { visible: true })
+ .then(() => (divFound = true));
+ await page.setContent(
+ `<div aria-label='name' style='display: none; visibility: hidden;'>1</div>`
+ );
+ expect(divFound).toBe(false);
+ await page.evaluate(() =>
+ document.querySelector('div').style.removeProperty('display')
+ );
+ expect(divFound).toBe(false);
+ await page.evaluate(() =>
+ document.querySelector('div').style.removeProperty('visibility')
+ );
+ expect(await waitForSelector).toBe(true);
+ expect(divFound).toBe(true);
+ });
+
+ it('should wait for visible recursively', async () => {
+ const { page } = getTestState();
+
+ let divVisible = false;
+ const waitForSelector = page
+ .waitForSelector('aria/inner', { visible: true })
+ .then(() => (divVisible = true));
+ await page.setContent(
+ `<div style='display: none; visibility: hidden;'><div aria-label="inner">hi</div></div>`
+ );
+ expect(divVisible).toBe(false);
+ await page.evaluate(() =>
+ document.querySelector('div').style.removeProperty('display')
+ );
+ expect(divVisible).toBe(false);
+ await page.evaluate(() =>
+ 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 } = getTestState();
+
+ let divHidden = false;
+ await page.setContent(
+ `<div role='button' style='display: block;'></div>`
+ );
+ const waitForSelector = page
+ .waitForSelector('aria/[role="button"]', { hidden: true })
+ .then(() => (divHidden = true));
+ await page.waitForSelector('aria/[role="button"]'); // do a round trip
+ expect(divHidden).toBe(false);
+ await page.evaluate(() =>
+ 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 } = getTestState();
+
+ let divHidden = false;
+ await page.setContent(`<div role='main' style='display: block;'></div>`);
+ const waitForSelector = page
+ .waitForSelector('aria/[role="main"]', { hidden: true })
+ .then(() => (divHidden = true));
+ await page.waitForSelector('aria/[role="main"]'); // do a round trip
+ expect(divHidden).toBe(false);
+ await page.evaluate(() =>
+ 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 } = getTestState();
+
+ await page.setContent(`<div role='main'></div>`);
+ let divRemoved = false;
+ const waitForSelector = page
+ .waitForSelector('aria/[role="main"]', { hidden: true })
+ .then(() => (divRemoved = true));
+ await page.waitForSelector('aria/[role="main"]'); // do a round trip
+ expect(divRemoved).toBe(false);
+ await page.evaluate(() => 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 } = getTestState();
+
+ const handle = await page.waitForSelector('aria/non-existing', {
+ hidden: true,
+ });
+ expect(handle).toBe(null);
+ });
+
+ it('should respect timeout', async () => {
+ const { page, puppeteer } = getTestState();
+
+ let error = null;
+ await page
+ .waitForSelector('aria/[role="button"]', { timeout: 10 })
+ .catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ expect(error.message).toContain(
+ 'waiting for selector `[role="button"]` failed: timeout'
+ );
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+
+ it('should have an error message specifically for awaiting an element to be hidden', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`<div role='main'></div>`);
+ let error = null;
+ await page
+ .waitForSelector('aria/[role="main"]', { hidden: true, timeout: 10 })
+ .catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ expect(error.message).toContain(
+ 'waiting for selector `[role="main"]` to be hidden failed: timeout'
+ );
+ });
+
+ it('should respond to node attribute mutation', async () => {
+ const { page } = getTestState();
+
+ let divFound = false;
+ const waitForSelector = page
+ .waitForSelector('aria/zombo')
+ .then(() => (divFound = true));
+ await page.setContent(`<div aria-label='notZombo'></div>`);
+ expect(divFound).toBe(false);
+ await page.evaluate(() =>
+ document.querySelector('div').setAttribute('aria-label', 'zombo')
+ );
+ expect(await waitForSelector).toBe(true);
+ });
+
+ it('should return the element handle', async () => {
+ const { page } = getTestState();
+
+ const waitForSelector = page.waitForSelector('aria/zombo');
+ await page.setContent(`<div aria-label='zombo'>anything</div>`);
+ expect(
+ await page.evaluate(
+ (x: HTMLElement) => x.textContent,
+ await waitForSelector
+ )
+ ).toBe('anything');
+ });
+
+ it('should have correct stack trace for timeout', async () => {
+ const { page } = getTestState();
+
+ let error;
+ await page
+ .waitForSelector('aria/zombo', { timeout: 10 })
+ .catch((error_) => (error = error_));
+ expect(error.stack).toContain('waiting for selector `zombo` failed');
+ });
+ });
+
+ describe('queryOne (Chromium web test)', async () => {
+ beforeEach(async () => {
+ const { page } = getTestState();
+ await 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>
+ `
+ );
+ });
+ const getIds = async (elements: ElementHandle[]) =>
+ Promise.all(
+ elements.map((element) =>
+ element.evaluate((element: Element) => element.id)
+ )
+ );
+ it('should find by name "foo"', async () => {
+ const { page } = getTestState();
+ 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 } = getTestState();
+ 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 } = getTestState();
+ 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 } = getTestState();
+ const found = await page.$$('aria/[role="button"]');
+ const ids = await getIds(found);
+ expect(ids).toEqual(['node5', 'node6', 'node8', 'node10', 'node21']);
+ });
+ it('should find by role "heading"', async () => {
+ const { page } = getTestState();
+ 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 } = getTestState();
+ const found = await page.$$('aria/title');
+ const ids = await getIds(found);
+ expect(ids).toEqual(['shown', 'hidden']);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/assert-coverage-test.js b/remote/test/puppeteer/test/assert-coverage-test.js
new file mode 100644
index 0000000000..6e26a1121a
--- /dev/null
+++ b/remote/test/puppeteer/test/assert-coverage-test.js
@@ -0,0 +1,25 @@
+const { describe, it } = require('mocha');
+const { getCoverageResults } = require('./coverage-utils');
+const expect = require('expect');
+
+describe('API coverage test', () => {
+ it('calls every method', () => {
+ if (!process.env.COVERAGE) return;
+
+ const coverageMap = getCoverageResults();
+ const missingMethods = [];
+ for (const method of coverageMap.keys()) {
+ if (!coverageMap.get(method)) missingMethods.push(method);
+ }
+ if (missingMethods.length) {
+ console.error(
+ '\nCoverage check failed: not all API methods called. See above output for list of missing methods.'
+ );
+ console.error(missingMethods.join('\n'));
+ }
+
+ // We know this will fail because we checked above
+ // but we need the actual test to fail.
+ expect(missingMethods.length).toEqual(0);
+ });
+});
diff --git a/remote/test/puppeteer/test/assets/beforeunload.html b/remote/test/puppeteer/test/assets/beforeunload.html
new file mode 100644
index 0000000000..3cef6763f3
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/beforeunload.html
@@ -0,0 +1,10 @@
+<div>beforeunload demo.</div>
+<script>
+window.addEventListener('beforeunload', event => {
+ // Chrome way.
+ event.returnValue = 'Leave?';
+ // Firefox way.
+ event.preventDefault();
+});
+</script>
+
diff --git a/remote/test/puppeteer/test/assets/cached/one-style.css b/remote/test/puppeteer/test/assets/cached/one-style.css
new file mode 100644
index 0000000000..04e7110b41
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/cached/one-style.css
@@ -0,0 +1,3 @@
+body {
+ background-color: pink;
+}
diff --git a/remote/test/puppeteer/test/assets/cached/one-style.html b/remote/test/puppeteer/test/assets/cached/one-style.html
new file mode 100644
index 0000000000..4760f2b9f7
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/cached/one-style.html
@@ -0,0 +1,2 @@
+<link rel='stylesheet' href='./one-style.css'>
+<div>hello, world!</div>
diff --git a/remote/test/puppeteer/test/assets/chromium-linux.zip b/remote/test/puppeteer/test/assets/chromium-linux.zip
new file mode 100644
index 0000000000..9c00ec080d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/chromium-linux.zip
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/consolelog.html b/remote/test/puppeteer/test/assets/consolelog.html
new file mode 100644
index 0000000000..4a27803aa9
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/consolelog.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>console.log test</title>
+ </head>
+ <body>
+ <script>
+ function foo() {
+ console.log('yellow')
+ }
+ function bar() {
+ foo();
+ }
+ bar();
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/csp.html b/remote/test/puppeteer/test/assets/csp.html
new file mode 100644
index 0000000000..34fc1fc1a5
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csp.html
@@ -0,0 +1 @@
+<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
diff --git a/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf b/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf
new file mode 100644
index 0000000000..4b208624e8
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/csscoverage/OFL.txt b/remote/test/puppeteer/test/assets/csscoverage/OFL.txt
new file mode 100644
index 0000000000..a9b3c8b34e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/OFL.txt
@@ -0,0 +1,95 @@
+Copyright (c) 2011, Edgar Tolentino and Pablo Impallari (www.impallari.com|impallari@gmail.com),
+Copyright (c) 2011, Igino Marini. (www.ikern.com|mail@iginomarini.com),
+with Reserved Font Names "Dosis".
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/remote/test/puppeteer/test/assets/csscoverage/involved.html b/remote/test/puppeteer/test/assets/csscoverage/involved.html
new file mode 100644
index 0000000000..bcd9845b93
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/involved.html
@@ -0,0 +1,26 @@
+<style>
+@charset "utf-8";
+@namespace svg url(http://www.w3.org/2000/svg);
+@font-face {
+ font-family: "Example Font";
+ src: url("./Dosis-Regular.ttf");
+}
+
+#fluffy {
+ border: 1px solid black;
+ z-index: 1;
+ /* -webkit-disabled-property: rgb(1, 2, 3) */
+ -lol-cats: "dogs" /* non-existing property */
+}
+
+@media (min-width: 1px) {
+ span {
+ -webkit-border-radius: 10px;
+ font-family: "Example Font";
+ animation: 1s identifier;
+ }
+}
+</style>
+<div id="fluffy">woof!</div>
+<span>fancy text</span>
+
diff --git a/remote/test/puppeteer/test/assets/csscoverage/media.html b/remote/test/puppeteer/test/assets/csscoverage/media.html
new file mode 100644
index 0000000000..bfb89f8f75
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/media.html
@@ -0,0 +1,4 @@
+<style>
+@media screen { div { color: green; } } </style>
+<div>hello, world</div>
+
diff --git a/remote/test/puppeteer/test/assets/csscoverage/multiple.html b/remote/test/puppeteer/test/assets/csscoverage/multiple.html
new file mode 100644
index 0000000000..0fd97e962a
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/multiple.html
@@ -0,0 +1,8 @@
+<link rel="stylesheet" href="stylesheet1.css">
+<link rel="stylesheet" href="stylesheet2.css">
+<script>
+window.addEventListener('DOMContentLoaded', () => {
+ // Force stylesheets to load.
+ console.log(window.getComputedStyle(document.body).color);
+}, false);
+</script>
diff --git a/remote/test/puppeteer/test/assets/csscoverage/simple.html b/remote/test/puppeteer/test/assets/csscoverage/simple.html
new file mode 100644
index 0000000000..3beae21829
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/simple.html
@@ -0,0 +1,6 @@
+<style>
+div { color: green; }
+a { color: blue; }
+</style>
+<div>hello, world</div>
+
diff --git a/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html b/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html
new file mode 100644
index 0000000000..df4e9c276c
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html
@@ -0,0 +1,7 @@
+<style>
+body {
+ padding: 10px;
+}
+/*# sourceURL=nicename.css */
+</style>
+
diff --git a/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css b/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css
new file mode 100644
index 0000000000..60f1eab971
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css
@@ -0,0 +1,3 @@
+body {
+ color: red;
+}
diff --git a/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css b/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css
new file mode 100644
index 0000000000..a87defb098
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css
@@ -0,0 +1,4 @@
+html {
+ margin: 0;
+ padding: 0;
+}
diff --git a/remote/test/puppeteer/test/assets/csscoverage/unused.html b/remote/test/puppeteer/test/assets/csscoverage/unused.html
new file mode 100644
index 0000000000..5b8186a3bf
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/csscoverage/unused.html
@@ -0,0 +1,7 @@
+<style>
+@media screen {
+ a { color: green; }
+}
+/*# sourceURL=unused.css */
+</style>
+
diff --git a/remote/test/puppeteer/test/assets/detect-touch.html b/remote/test/puppeteer/test/assets/detect-touch.html
new file mode 100644
index 0000000000..80a4123fbd
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/detect-touch.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Detect Touch Test</title>
+ <script src='modernizr.js'></script>
+ </head>
+ <body style="font-size:30vmin">
+ <script>
+ document.body.textContent = Modernizr.touchevents ? 'YES' : 'NO';
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/digits/0.png b/remote/test/puppeteer/test/assets/digits/0.png
new file mode 100644
index 0000000000..ac3c4768ed
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/0.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/1.png b/remote/test/puppeteer/test/assets/digits/1.png
new file mode 100644
index 0000000000..6768222729
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/1.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/2.png b/remote/test/puppeteer/test/assets/digits/2.png
new file mode 100644
index 0000000000..b1daa4735d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/2.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/3.png b/remote/test/puppeteer/test/assets/digits/3.png
new file mode 100644
index 0000000000..6eca99b21b
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/3.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/4.png b/remote/test/puppeteer/test/assets/digits/4.png
new file mode 100644
index 0000000000..a721071e2c
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/4.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/5.png b/remote/test/puppeteer/test/assets/digits/5.png
new file mode 100644
index 0000000000..15cb19932a
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/5.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/6.png b/remote/test/puppeteer/test/assets/digits/6.png
new file mode 100644
index 0000000000..639f38439d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/6.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/7.png b/remote/test/puppeteer/test/assets/digits/7.png
new file mode 100644
index 0000000000..5c1150b005
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/7.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/8.png b/remote/test/puppeteer/test/assets/digits/8.png
new file mode 100644
index 0000000000..abb8b48b0b
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/8.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/digits/9.png b/remote/test/puppeteer/test/assets/digits/9.png
new file mode 100644
index 0000000000..6a40a21c6f
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/digits/9.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/dynamic-oopif.html b/remote/test/puppeteer/test/assets/dynamic-oopif.html
new file mode 100644
index 0000000000..f00c741dfb
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/dynamic-oopif.html
@@ -0,0 +1,10 @@
+<script>
+window.addEventListener('DOMContentLoaded', () => {
+ const iframe = document.createElement('iframe');
+ const url = new URL(location.href);
+ url.hostname = url.hostname === 'localhost' ? '127.0.0.1' : 'localhost';
+ url.pathname = '/grid.html';
+ iframe.src = url.toString();
+ document.body.appendChild(iframe);
+}, false);
+</script>
diff --git a/remote/test/puppeteer/test/assets/empty.html b/remote/test/puppeteer/test/assets/empty.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/empty.html
diff --git a/remote/test/puppeteer/test/assets/error.html b/remote/test/puppeteer/test/assets/error.html
new file mode 100644
index 0000000000..130400c006
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/error.html
@@ -0,0 +1,15 @@
+<script>
+a();
+
+function a() {
+ b();
+}
+
+function b() {
+ c();
+}
+
+function c() {
+ throw new Error('Fancy error!');
+}
+</script>
diff --git a/remote/test/puppeteer/test/assets/es6/.eslintrc b/remote/test/puppeteer/test/assets/es6/.eslintrc
new file mode 100644
index 0000000000..1903e176f5
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/es6/.eslintrc
@@ -0,0 +1,5 @@
+{
+ "parserOptions": {
+ "sourceType": "module"
+ }
+} \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/es6/es6import.js b/remote/test/puppeteer/test/assets/es6/es6import.js
new file mode 100644
index 0000000000..9aac2d4d64
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/es6/es6import.js
@@ -0,0 +1,2 @@
+import num from './es6module.js';
+window.__es6injected = num;
diff --git a/remote/test/puppeteer/test/assets/es6/es6module.js b/remote/test/puppeteer/test/assets/es6/es6module.js
new file mode 100644
index 0000000000..7a4e8a723a
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/es6/es6module.js
@@ -0,0 +1 @@
+export default 42;
diff --git a/remote/test/puppeteer/test/assets/es6/es6pathimport.js b/remote/test/puppeteer/test/assets/es6/es6pathimport.js
new file mode 100644
index 0000000000..eb17a9a3d1
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/es6/es6pathimport.js
@@ -0,0 +1,2 @@
+import num from './es6/es6module.js';
+window.__es6injected = num;
diff --git a/remote/test/puppeteer/test/assets/file-to-upload.txt b/remote/test/puppeteer/test/assets/file-to-upload.txt
new file mode 100644
index 0000000000..b4ad118489
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/file-to-upload.txt
@@ -0,0 +1 @@
+contents of the file \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 b/remote/test/puppeteer/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2
new file mode 100644
index 0000000000..be6d188027
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/frames/frame.html b/remote/test/puppeteer/test/assets/frames/frame.html
new file mode 100644
index 0000000000..8f20d2da9f
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/frame.html
@@ -0,0 +1,8 @@
+<link rel='stylesheet' href='./style.css'>
+<script src='./script.js' type='text/javascript'></script>
+<style>
+div {
+ line-height: 18px;
+}
+</style>
+<div>Hi, I'm frame</div>
diff --git a/remote/test/puppeteer/test/assets/frames/frameset.html b/remote/test/puppeteer/test/assets/frames/frameset.html
new file mode 100644
index 0000000000..4d56f88839
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/frameset.html
@@ -0,0 +1,8 @@
+<frameset>
+ <frameset>
+ <frame src='./frame.html'></frame>
+ <frame src='about:blank'></frame>
+ </frameset>
+ <frame src='/empty.html'></frame>
+ <frame></frame>
+</frameset>
diff --git a/remote/test/puppeteer/test/assets/frames/nested-frames.html b/remote/test/puppeteer/test/assets/frames/nested-frames.html
new file mode 100644
index 0000000000..de1987586f
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/nested-frames.html
@@ -0,0 +1,25 @@
+<style>
+body {
+ display: flex;
+}
+
+body iframe {
+ flex-grow: 1;
+ flex-shrink: 1;
+}
+::-webkit-scrollbar{
+ display: none;
+}
+</style>
+<script>
+async function attachFrame(frameId, url) {
+ var frame = document.createElement('iframe');
+ frame.src = url;
+ frame.id = frameId;
+ document.body.appendChild(frame);
+ await new Promise(x => frame.onload = x);
+ return 'kazakh';
+}
+</script>
+<iframe src='./two-frames.html' name='2frames'></iframe>
+<iframe src='./frame.html' name='aframe'></iframe>
diff --git a/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html b/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html
new file mode 100644
index 0000000000..d1462641ff
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html
@@ -0,0 +1 @@
+<iframe src='./frame.html?param=value#fragment'></iframe>
diff --git a/remote/test/puppeteer/test/assets/frames/one-frame.html b/remote/test/puppeteer/test/assets/frames/one-frame.html
new file mode 100644
index 0000000000..e941d795a2
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/one-frame.html
@@ -0,0 +1 @@
+<iframe src='./frame.html'></iframe>
diff --git a/remote/test/puppeteer/test/assets/frames/script.js b/remote/test/puppeteer/test/assets/frames/script.js
new file mode 100644
index 0000000000..be22256d16
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/script.js
@@ -0,0 +1 @@
+console.log('Cheers!');
diff --git a/remote/test/puppeteer/test/assets/frames/style.css b/remote/test/puppeteer/test/assets/frames/style.css
new file mode 100644
index 0000000000..5b5436e874
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/style.css
@@ -0,0 +1,3 @@
+div {
+ color: blue;
+}
diff --git a/remote/test/puppeteer/test/assets/frames/two-frames.html b/remote/test/puppeteer/test/assets/frames/two-frames.html
new file mode 100644
index 0000000000..b2ee853eda
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/frames/two-frames.html
@@ -0,0 +1,13 @@
+<style>
+body {
+ display: flex;
+ flex-direction: column;
+}
+
+body iframe {
+ flex-grow: 1;
+ flex-shrink: 1;
+}
+</style>
+<iframe src='./frame.html' name='uno'></iframe>
+<iframe src='./frame.html' name='dos'></iframe>
diff --git a/remote/test/puppeteer/test/assets/global-var.html b/remote/test/puppeteer/test/assets/global-var.html
new file mode 100644
index 0000000000..b6be975038
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/global-var.html
@@ -0,0 +1,3 @@
+<script>
+var globalVar = 123;
+</script> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/grid.html b/remote/test/puppeteer/test/assets/grid.html
new file mode 100644
index 0000000000..0bdbb1220e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/grid.html
@@ -0,0 +1,52 @@
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+ function generatePalette(amount) {
+ var result = [];
+ var hueStep = 360 / amount;
+ for (var i = 0; i < amount; ++i)
+ result.push('hsl(' + (hueStep * i) + ', 100%, 90%)');
+ return result;
+ }
+
+ var palette = generatePalette(100);
+ for (var i = 0; i < 200; ++i) {
+ var box = document.createElement('div');
+ box.classList.add('box');
+ box.style.setProperty('background-color', palette[i % palette.length]);
+ var x = i;
+ do {
+ var digit = x % 10;
+ x = (x / 10)|0;
+ var img = document.createElement('img');
+ img.src = `./digits/${digit}.png`;
+ box.insertBefore(img, box.firstChild);
+ } while (x);
+ document.body.appendChild(box);
+ }
+});
+</script>
+
+<style>
+
+body {
+ margin: 0;
+ padding: 0;
+}
+
+.box {
+ font-family: arial;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0;
+ padding: 0;
+ width: 50px;
+ height: 50px;
+ box-sizing: border-box;
+ border: 1px solid darkgray;
+}
+
+::-webkit-scrollbar {
+ display: none;
+}
+</style>
diff --git a/remote/test/puppeteer/test/assets/historyapi.html b/remote/test/puppeteer/test/assets/historyapi.html
new file mode 100644
index 0000000000..bacaf9e9a0
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/historyapi.html
@@ -0,0 +1,5 @@
+<script>
+window.addEventListener('DOMContentLoaded', () => {
+ history.pushState({}, '', '#1');
+});
+</script>
diff --git a/remote/test/puppeteer/test/assets/idle-detector.html b/remote/test/puppeteer/test/assets/idle-detector.html
new file mode 100644
index 0000000000..83b496c03d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/idle-detector.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<div id="state"></div>
+<script>
+ const elState = document.querySelector('#state');
+ function setState(msg) {
+ elState.textContent = msg;
+ }
+ async function main() {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const idleDetector = new IdleDetector({
+ threshold: 60000,
+ signal,
+ });
+ idleDetector.addEventListener('change', () => {
+ const userState = idleDetector.userState;
+ const screenState = idleDetector.screenState;
+ setState(`Idle state: ${userState}, ${screenState}.`);
+ });
+ idleDetector.start();
+ }
+ main();
+</script>
diff --git a/remote/test/puppeteer/test/assets/injectedfile.js b/remote/test/puppeteer/test/assets/injectedfile.js
new file mode 100644
index 0000000000..c211b62c16
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/injectedfile.js
@@ -0,0 +1,2 @@
+window.__injected = 42;
+window.__injectedError = new Error('hi');
diff --git a/remote/test/puppeteer/test/assets/injectedstyle.css b/remote/test/puppeteer/test/assets/injectedstyle.css
new file mode 100644
index 0000000000..aa1634c255
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/injectedstyle.css
@@ -0,0 +1,3 @@
+body {
+ background-color: red;
+}
diff --git a/remote/test/puppeteer/test/assets/input/button.html b/remote/test/puppeteer/test/assets/input/button.html
new file mode 100644
index 0000000000..d4c6e13fd2
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/button.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Button test</title>
+ </head>
+ <body>
+ <script src="mouse-helper.js"></script>
+ <button onclick="clicked();">Click target</button>
+ <script>
+ window.result = 'Was not clicked';
+ function clicked() {
+ result = 'Clicked';
+ }
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/input/checkbox.html b/remote/test/puppeteer/test/assets/input/checkbox.html
new file mode 100644
index 0000000000..ca56762e2b
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/checkbox.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Selection Test</title>
+ </head>
+ <body>
+ <label for="agree">Remember Me</label>
+ <input id="agree" type="checkbox">
+ <script>
+ window.result = {
+ check: null,
+ events: [],
+ };
+
+ let checkbox = document.querySelector('input');
+
+ const events = [
+ 'change',
+ 'click',
+ 'dblclick',
+ 'input',
+ 'mousedown',
+ 'mouseenter',
+ 'mouseleave',
+ 'mousemove',
+ 'mouseout',
+ 'mouseover',
+ 'mouseup',
+ ];
+
+ for (let event of events) {
+ checkbox.addEventListener(event, () => {
+ if (['change', 'click', 'dblclick', 'input'].includes(event) === true) {
+ result.check = checkbox.checked;
+ }
+
+ result.events.push(event);
+ }, false);
+ }
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/input/fileupload.html b/remote/test/puppeteer/test/assets/input/fileupload.html
new file mode 100644
index 0000000000..55fd7c5006
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/fileupload.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>File upload test</title>
+ </head>
+ <body>
+ <input type="file">
+ </body>
+</html> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/input/keyboard.html b/remote/test/puppeteer/test/assets/input/keyboard.html
new file mode 100644
index 0000000000..fd962c7518
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/keyboard.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Keyboard test</title>
+ </head>
+ <body>
+ <textarea></textarea>
+ <script>
+ window.result = "";
+ let textarea = document.querySelector('textarea');
+ textarea.focus();
+ textarea.addEventListener('keydown', event => {
+ log('Keydown:', event.key, event.code, event.which, modifiers(event));
+ });
+ textarea.addEventListener('keypress', event => {
+ log('Keypress:', event.key, event.code, event.which, event.charCode, modifiers(event));
+ });
+ textarea.addEventListener('keyup', event => {
+ log('Keyup:', event.key, event.code, event.which, modifiers(event));
+ });
+ function modifiers(event) {
+ let m = [];
+ if (event.altKey)
+ m.push('Alt')
+ if (event.ctrlKey)
+ m.push('Control');
+ if (event.shiftKey)
+ m.push('Shift')
+ return '[' + m.join(' ') + ']';
+ }
+ function log(...args) {
+ console.log.apply(console, args);
+ result += args.join(' ') + '\n';
+ }
+ function getResult() {
+ let temp = result.trim();
+ result = "";
+ return temp;
+ }
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/input/mouse-helper.js b/remote/test/puppeteer/test/assets/input/mouse-helper.js
new file mode 100644
index 0000000000..4f2824dceb
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/mouse-helper.js
@@ -0,0 +1,74 @@
+// This injects a box into the page that moves with the mouse;
+// Useful for debugging
+(function () {
+ const box = document.createElement('div');
+ box.classList.add('mouse-helper');
+ const styleElement = document.createElement('style');
+ styleElement.innerHTML = `
+ .mouse-helper {
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 20px;
+ height: 20px;
+ background: rgba(0,0,0,.4);
+ border: 1px solid white;
+ border-radius: 10px;
+ margin-left: -10px;
+ margin-top: -10px;
+ transition: background .2s, border-radius .2s, border-color .2s;
+ }
+ .mouse-helper.button-1 {
+ transition: none;
+ background: rgba(0,0,0,0.9);
+ }
+ .mouse-helper.button-2 {
+ transition: none;
+ border-color: rgba(0,0,255,0.9);
+ }
+ .mouse-helper.button-3 {
+ transition: none;
+ border-radius: 4px;
+ }
+ .mouse-helper.button-4 {
+ transition: none;
+ border-color: rgba(255,0,0,0.9);
+ }
+ .mouse-helper.button-5 {
+ transition: none;
+ border-color: rgba(0,255,0,0.9);
+ }
+ `;
+ document.head.appendChild(styleElement);
+ document.body.appendChild(box);
+ document.addEventListener(
+ 'mousemove',
+ (event) => {
+ box.style.left = event.pageX + 'px';
+ box.style.top = event.pageY + 'px';
+ updateButtons(event.buttons);
+ },
+ true
+ );
+ document.addEventListener(
+ 'mousedown',
+ (event) => {
+ updateButtons(event.buttons);
+ box.classList.add('button-' + event.which);
+ },
+ true
+ );
+ document.addEventListener(
+ 'mouseup',
+ (event) => {
+ updateButtons(event.buttons);
+ box.classList.remove('button-' + event.which);
+ },
+ true
+ );
+ function updateButtons(buttons) {
+ for (let i = 0; i < 5; i++)
+ box.classList.toggle('button-' + i, buttons & (1 << i));
+ }
+})();
diff --git a/remote/test/puppeteer/test/assets/input/rotatedButton.html b/remote/test/puppeteer/test/assets/input/rotatedButton.html
new file mode 100644
index 0000000000..1bce66cf5e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/rotatedButton.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Rotated button test</title>
+ </head>
+ <body>
+ <script src="mouse-helper.js"></script>
+ <button onclick="clicked();">Click target</button>
+ <style>
+ button {
+ transform: rotateY(180deg);
+ }
+ </style>
+ <script>
+ window.result = 'Was not clicked';
+ function clicked() {
+ result = 'Clicked';
+ }
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/input/scrollable.html b/remote/test/puppeteer/test/assets/input/scrollable.html
new file mode 100644
index 0000000000..885d3739d5
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/scrollable.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Scrollable test</title>
+ </head>
+ <body>
+ <script src='mouse-helper.js'></script>
+ <script>
+ for (let i = 0; i < 100; i++) {
+ let button = document.createElement('button');
+ button.textContent = i + ': not clicked';
+ button.id = 'button-' + i;
+ button.onclick = () => button.textContent = 'clicked';
+ button.oncontextmenu = event => {
+ event.preventDefault();
+ button.textContent = 'context menu';
+ }
+ document.body.appendChild(button);
+ document.body.appendChild(document.createElement('br'));
+ }
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/input/select.html b/remote/test/puppeteer/test/assets/input/select.html
new file mode 100644
index 0000000000..879a537a76
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/select.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Selection Test</title>
+ </head>
+ <body>
+ <select>
+ <option value="black">Black</option>
+ <option value="blue">Blue</option>
+ <option value="brown">Brown</option>
+ <option value="cyan">Cyan</option>
+ <option value="gray">Gray</option>
+ <option value="green">Green</option>
+ <option value="indigo">Indigo</option>
+ <option value="magenta">Magenta</option>
+ <option value="orange">Orange</option>
+ <option value="pink">Pink</option>
+ <option value="purple">Purple</option>
+ <option value="red">Red</option>
+ <option value="violet">Violet</option>
+ <option value="white">White</option>
+ <option value="yellow">Yellow</option>
+ </select>
+ <script>
+ window.result = {
+ onInput: null,
+ onChange: null,
+ onBubblingChange: null,
+ onBubblingInput: null,
+ };
+
+ let select = document.querySelector('select');
+
+ function makeEmpty() {
+ for (let i = select.options.length - 1; i >= 0; --i) {
+ select.remove(i);
+ }
+ }
+
+ function makeMultiple() {
+ select.setAttribute('multiple', true);
+ }
+
+ select.addEventListener('input', () => {
+ result.onInput = Array.from(select.querySelectorAll('option:checked')).map((option) => {
+ return option.value;
+ });
+ }, false);
+
+ select.addEventListener('change', () => {
+ result.onChange = Array.from(select.querySelectorAll('option:checked')).map((option) => {
+ return option.value;
+ });
+ }, false);
+
+ document.body.addEventListener('input', () => {
+ result.onBubblingInput = Array.from(select.querySelectorAll('option:checked')).map((option) => {
+ return option.value;
+ });
+ }, false);
+
+ document.body.addEventListener('change', () => {
+ result.onBubblingChange = Array.from(select.querySelectorAll('option:checked')).map((option) => {
+ return option.value;
+ });
+ }, false);
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/input/textarea.html b/remote/test/puppeteer/test/assets/input/textarea.html
new file mode 100644
index 0000000000..6d77f3106d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/textarea.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Textarea test</title>
+ </head>
+ <body>
+ <textarea></textarea>
+ <script src='mouse-helper.js'></script>
+ <script>
+ globalThis.result = '';
+ globalThis.textarea = document.querySelector('textarea');
+ textarea.addEventListener('input', () => result = textarea.value, false);
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/input/touches.html b/remote/test/puppeteer/test/assets/input/touches.html
new file mode 100644
index 0000000000..4392cfacbd
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/touches.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Touch test</title>
+ </head>
+ <body>
+ <script src="mouse-helper.js"></script>
+ <button onclick="clicked();">Click target</button>
+ <script>
+ window.result = [];
+ const button = document.querySelector('button');
+ button.style.height = '200px';
+ button.style.width = '200px';
+ button.focus();
+ button.addEventListener('touchstart', event => {
+ log('Touchstart:', ...Array.from(event.changedTouches).map(touch => touch.identifier));
+ });
+ button.addEventListener('touchend', event => {
+ log('Touchend:', ...Array.from(event.changedTouches).map(touch => touch.identifier));
+ });
+ button.addEventListener('touchmove', event => {
+ log('Touchmove:', ...Array.from(event.changedTouches).map(touch => touch.identifier));
+ });
+ function log(...args) {
+ console.log.apply(console, args);
+ result.push(args.join(' '));
+ }
+ function getResult() {
+ let temp = result;
+ result = [];
+ return temp;
+ }
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/input/wheel.html b/remote/test/puppeteer/test/assets/input/wheel.html
new file mode 100644
index 0000000000..3d093a993e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/input/wheel.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ body {
+ min-height: 100vh;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ div {
+ width: 105px;
+ height: 105px;
+ background: #cdf;
+ padding: 5px;
+ }
+ </style>
+ <title>Element: wheel event - Scaling_an_element_via_the_wheel - code sample</title>
+ </head>
+ <body>
+ <div>Scale me with your mouse wheel.</div>
+ <script>
+ function zoom(event) {
+ event.preventDefault();
+
+ scale += event.deltaY * -0.01;
+
+ // Restrict scale
+ scale = Math.min(Math.max(.125, scale), 4);
+
+ // Apply scale transform
+ el.style.transform = `scale(${scale})`;
+ }
+
+ let scale = 1;
+ const el = document.querySelector('div');
+ el.onwheel = zoom;
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/eval.html b/remote/test/puppeteer/test/assets/jscoverage/eval.html
new file mode 100644
index 0000000000..838ae28763
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/eval.html
@@ -0,0 +1 @@
+<script>eval('console.log("foo")')</script>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/involved.html b/remote/test/puppeteer/test/assets/jscoverage/involved.html
new file mode 100644
index 0000000000..889c86bed5
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/involved.html
@@ -0,0 +1,15 @@
+<script>
+function foo() {
+ if (1 > 2)
+ console.log(1);
+ if (1 < 2)
+ console.log(2);
+ let x = 1 > 2 ? 'foo' : 'bar';
+ let y = 1 < 2 ? 'foo' : 'bar';
+ let z = () => {};
+ let q = () => {};
+ q();
+}
+
+foo();
+</script>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/multiple.html b/remote/test/puppeteer/test/assets/jscoverage/multiple.html
new file mode 100644
index 0000000000..bdef59885b
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/multiple.html
@@ -0,0 +1,2 @@
+<script src='script1.js'></script>
+<script src='script2.js'></script>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/ranges.html b/remote/test/puppeteer/test/assets/jscoverage/ranges.html
new file mode 100644
index 0000000000..a537a7da6a
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/ranges.html
@@ -0,0 +1,2 @@
+<script>
+function unused(){}console.log('used!');</script>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/script1.js b/remote/test/puppeteer/test/assets/jscoverage/script1.js
new file mode 100644
index 0000000000..3bd241b50e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/script1.js
@@ -0,0 +1 @@
+console.log(3);
diff --git a/remote/test/puppeteer/test/assets/jscoverage/script2.js b/remote/test/puppeteer/test/assets/jscoverage/script2.js
new file mode 100644
index 0000000000..3bd241b50e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/script2.js
@@ -0,0 +1 @@
+console.log(3);
diff --git a/remote/test/puppeteer/test/assets/jscoverage/simple.html b/remote/test/puppeteer/test/assets/jscoverage/simple.html
new file mode 100644
index 0000000000..49eeeea6ae
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/simple.html
@@ -0,0 +1,2 @@
+<script>
+function foo() {function bar() { } console.log(1); } foo(); </script>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html b/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html
new file mode 100644
index 0000000000..e477750320
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html
@@ -0,0 +1,4 @@
+<script>
+console.log(1);
+//# sourceURL=nicename.js
+</script>
diff --git a/remote/test/puppeteer/test/assets/jscoverage/unused.html b/remote/test/puppeteer/test/assets/jscoverage/unused.html
new file mode 100644
index 0000000000..59c4a5a70b
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/jscoverage/unused.html
@@ -0,0 +1 @@
+<script>function foo() { }</script>
diff --git a/remote/test/puppeteer/test/assets/mobile.html b/remote/test/puppeteer/test/assets/mobile.html
new file mode 100644
index 0000000000..8e94b2fe29
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/mobile.html
@@ -0,0 +1 @@
+<meta name = "viewport" content = "initial-scale = 1, user-scalable = no">
diff --git a/remote/test/puppeteer/test/assets/modernizr.js b/remote/test/puppeteer/test/assets/modernizr.js
new file mode 100644
index 0000000000..7991a4ec40
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/modernizr.js
@@ -0,0 +1,3 @@
+/*! modernizr 3.5.0 (Custom Build) | MIT *
+* https://modernizr.com/download/?-touchevents-setclasses !*/
+!function(e,n,t){function o(e,n){return typeof e===n}function s(){var e,n,t,s,a,i,r;for(var l in c)if(c.hasOwnProperty(l)){if(e=[],n=c[l],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;t<n.options.aliases.length;t++)e.push(n.options.aliases[t].toLowerCase());for(s=o(n.fn,"function")?n.fn():n.fn,a=0;a<e.length;a++)i=e[a],r=i.split("."),1===r.length?Modernizr[r[0]]=s:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=s),f.push((s?"":"no-")+r.join("-"))}}function a(e){var n=u.className,t=Modernizr._config.classPrefix||"";if(p&&(n=n.baseVal),Modernizr._config.enableJSClass){var o=new RegExp("(^|\\s)"+t+"no-js(\\s|$)");n=n.replace(o,"$1"+t+"js$2")}Modernizr._config.enableClasses&&(n+=" "+t+e.join(" "+t),p?u.className.baseVal=n:u.className=n)}function i(){return"function"!=typeof n.createElement?n.createElement(arguments[0]):p?n.createElementNS.call(n,"http://www.w3.org/2000/svg",arguments[0]):n.createElement.apply(n,arguments)}function r(){var e=n.body;return e||(e=i(p?"svg":"body"),e.fake=!0),e}function l(e,t,o,s){var a,l,f,c,d="modernizr",p=i("div"),h=r();if(parseInt(o,10))for(;o--;)f=i("div"),f.id=s?s[o]:d+(o+1),p.appendChild(f);return a=i("style"),a.type="text/css",a.id="s"+d,(h.fake?h:p).appendChild(a),h.appendChild(p),a.styleSheet?a.styleSheet.cssText=e:a.appendChild(n.createTextNode(e)),p.id=d,h.fake&&(h.style.background="",h.style.overflow="hidden",c=u.style.overflow,u.style.overflow="hidden",u.appendChild(h)),l=t(p,e),h.fake?(h.parentNode.removeChild(h),u.style.overflow=c,u.offsetHeight):p.parentNode.removeChild(p),!!l}var f=[],c=[],d={_version:"3.5.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,n){var t=this;setTimeout(function(){n(t[e])},0)},addTest:function(e,n,t){c.push({name:e,fn:n,options:t})},addAsyncTest:function(e){c.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=d,Modernizr=new Modernizr;var u=n.documentElement,p="svg"===u.nodeName.toLowerCase(),h=d._config.usePrefixes?" -webkit- -moz- -o- -ms- ".split(" "):["",""];d._prefixes=h;var m=d.testStyles=l;Modernizr.addTest("touchevents",function(){var t;if("ontouchstart"in e||e.DocumentTouch&&n instanceof DocumentTouch)t=!0;else{var o=["@media (",h.join("touch-enabled),("),"heartz",")","{#modernizr{top:9px;position:absolute}}"].join("");m(o,function(e){t=9===e.offsetTop})}return t}),s(),a(f),delete d.addTest,delete d.addAsyncTest;for(var v=0;v<Modernizr._q.length;v++)Modernizr._q[v]();e.Modernizr=Modernizr}(window,document);
diff --git a/remote/test/puppeteer/test/assets/networkidle.html b/remote/test/puppeteer/test/assets/networkidle.html
new file mode 100644
index 0000000000..910ae1736d
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/networkidle.html
@@ -0,0 +1,19 @@
+<script>
+ async function sleep(delay) {
+ return new Promise(resolve => setTimeout(resolve, delay));
+ }
+
+ async function main() {
+ const roundOne = Promise.all([
+ fetch('fetch-request-a.js'),
+ fetch('fetch-request-b.js'),
+ fetch('fetch-request-c.js'),
+ ]);
+
+ await roundOne;
+ await sleep(50);
+ await fetch('fetch-request-d.js');
+ }
+
+ main();
+</script>
diff --git a/remote/test/puppeteer/test/assets/offscreenbuttons.html b/remote/test/puppeteer/test/assets/offscreenbuttons.html
new file mode 100644
index 0000000000..d45e2a4129
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/offscreenbuttons.html
@@ -0,0 +1,36 @@
+<style>
+ button {
+ position: absolute;
+ width: 100px;
+ height: 20px;
+ }
+
+ #btn0 { right: 0px; top: 0; }
+ #btn1 { right: -10px; top: 25px; }
+ #btn2 { right: -20px; top: 50px; }
+ #btn3 { right: -30px; top: 75px; }
+ #btn4 { right: -40px; top: 100px; }
+ #btn5 { right: -50px; top: 125px; }
+ #btn6 { right: -60px; top: 150px; }
+ #btn7 { right: -70px; top: 175px; }
+ #btn8 { right: -80px; top: 200px; }
+ #btn9 { right: -90px; top: 225px; }
+ #btn10 { right: -100px; top: 250px; }
+</style>
+<button id=btn0>0</button>
+<button id=btn1>1</button>
+<button id=btn2>2</button>
+<button id=btn3>3</button>
+<button id=btn4>4</button>
+<button id=btn5>5</button>
+<button id=btn6>6</button>
+<button id=btn7>7</button>
+<button id=btn8>8</button>
+<button id=btn9>9</button>
+<button id=btn10>10</button>
+<script>
+window.addEventListener('DOMContentLoaded', () => {
+ for (const button of Array.from(document.querySelectorAll('button')))
+ button.addEventListener('click', () => console.log('button #' + button.textContent + ' clicked'), false);
+}, false);
+</script>
diff --git a/remote/test/puppeteer/test/assets/one-style.css b/remote/test/puppeteer/test/assets/one-style.css
new file mode 100644
index 0000000000..7b26410d8a
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/one-style.css
@@ -0,0 +1,3 @@
+body {
+ background-color: pink;
+}
diff --git a/remote/test/puppeteer/test/assets/one-style.html b/remote/test/puppeteer/test/assets/one-style.html
new file mode 100644
index 0000000000..4760f2b9f7
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/one-style.html
@@ -0,0 +1,2 @@
+<link rel='stylesheet' href='./one-style.css'>
+<div>hello, world!</div>
diff --git a/remote/test/puppeteer/test/assets/playground.html b/remote/test/puppeteer/test/assets/playground.html
new file mode 100644
index 0000000000..828cfb1c70
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/playground.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Playground</title>
+ </head>
+ <body>
+ <button>A button</button>
+ <textarea>A text area</textarea>
+ <div id="first">First div</div>
+ <div id="second">
+ Second div
+ <span class="inner">Inner span</span>
+ </div>
+ </body>
+</html> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/popup/popup.html b/remote/test/puppeteer/test/assets/popup/popup.html
new file mode 100644
index 0000000000..b855162c25
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/popup/popup.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Popup</title>
+ </head>
+ <body>
+ I am a popup
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/popup/window-open.html b/remote/test/puppeteer/test/assets/popup/window-open.html
new file mode 100644
index 0000000000..d138be1d22
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/popup/window-open.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Popup test</title>
+ </head>
+ <body>
+ <script>
+ window.open('./popup.html');
+ </script>
+ </body>
+</html>
diff --git a/remote/test/puppeteer/test/assets/pptr.png b/remote/test/puppeteer/test/assets/pptr.png
new file mode 100644
index 0000000000..65d87c68e6
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/pptr.png
Binary files differ
diff --git a/remote/test/puppeteer/test/assets/resetcss.html b/remote/test/puppeteer/test/assets/resetcss.html
new file mode 100644
index 0000000000..e4e04b1f8a
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/resetcss.html
@@ -0,0 +1,50 @@
+<style>
+/* http://meyerweb.com/eric/tools/css/reset/
+ v2.0 | 20110126
+ License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+body {
+ line-height: 1;
+}
+ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+</style>
diff --git a/remote/test/puppeteer/test/assets/self-request.html b/remote/test/puppeteer/test/assets/self-request.html
new file mode 100644
index 0000000000..88aff620ff
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/self-request.html
@@ -0,0 +1,5 @@
+<script>
+var req = new XMLHttpRequest();
+req.open('GET', '/self-request.html');
+req.send(null);
+</script>
diff --git a/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html
new file mode 100644
index 0000000000..bef85d985b
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html
@@ -0,0 +1,3 @@
+<script>
+ window.registrationPromise = navigator.serviceWorker.register('sw.js');
+</script>
diff --git a/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js
diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css b/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css
new file mode 100644
index 0000000000..7b26410d8a
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css
@@ -0,0 +1,3 @@
+body {
+ background-color: pink;
+}
diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html
new file mode 100644
index 0000000000..a9d28acb09
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html
@@ -0,0 +1,5 @@
+<link rel="stylesheet" href="./style.css">
+<script>
+ window.registrationPromise = navigator.serviceWorker.register('sw.js');
+ window.activationPromise = new Promise(resolve => navigator.serviceWorker.oncontrollerchange = resolve);
+</script>
diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js
new file mode 100644
index 0000000000..21381484b6
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js
@@ -0,0 +1,7 @@
+self.addEventListener('fetch', (event) => {
+ event.respondWith(fetch(event.request));
+});
+
+self.addEventListener('activate', (event) => {
+ event.waitUntil(clients.claim());
+});
diff --git a/remote/test/puppeteer/test/assets/shadow.html b/remote/test/puppeteer/test/assets/shadow.html
new file mode 100644
index 0000000000..3796ca768c
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/shadow.html
@@ -0,0 +1,17 @@
+<script>
+
+let h1 = null;
+window.button = null;
+window.clicked = false;
+
+window.addEventListener('DOMContentLoaded', () => {
+ const shadowRoot = document.body.attachShadow({mode: 'open'});
+ h1 = document.createElement('h1');
+ h1.textContent = 'Hellow Shadow DOM v1';
+ button = document.createElement('button');
+ button.textContent = 'Click';
+ button.addEventListener('click', () => clicked = true);
+ shadowRoot.appendChild(h1);
+ shadowRoot.appendChild(button);
+});
+</script>
diff --git a/remote/test/puppeteer/test/assets/simple-extension/content-script.js b/remote/test/puppeteer/test/assets/simple-extension/content-script.js
new file mode 100644
index 0000000000..0fd83b90f1
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/simple-extension/content-script.js
@@ -0,0 +1,2 @@
+console.log('hey from the content-script');
+self.thisIsTheContentScript = true;
diff --git a/remote/test/puppeteer/test/assets/simple-extension/index.js b/remote/test/puppeteer/test/assets/simple-extension/index.js
new file mode 100644
index 0000000000..a0bb3f4eae
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/simple-extension/index.js
@@ -0,0 +1,2 @@
+// Mock script for background extension
+window.MAGIC = 42;
diff --git a/remote/test/puppeteer/test/assets/simple-extension/manifest.json b/remote/test/puppeteer/test/assets/simple-extension/manifest.json
new file mode 100644
index 0000000000..da2cd082ed
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/simple-extension/manifest.json
@@ -0,0 +1,14 @@
+{
+ "name": "Simple extension",
+ "version": "0.1",
+ "background": {
+ "scripts": ["index.js"]
+ },
+ "content_scripts": [{
+ "matches": ["<all_urls>"],
+ "css": [],
+ "js": ["content-script.js"]
+ }],
+ "permissions": ["background", "activeTab"],
+ "manifest_version": 2
+}
diff --git a/remote/test/puppeteer/test/assets/simple.json b/remote/test/puppeteer/test/assets/simple.json
new file mode 100644
index 0000000000..6d95903051
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/simple.json
@@ -0,0 +1 @@
+{"foo": "bar"}
diff --git a/remote/test/puppeteer/test/assets/tamperable.html b/remote/test/puppeteer/test/assets/tamperable.html
new file mode 100644
index 0000000000..d027e97038
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/tamperable.html
@@ -0,0 +1,3 @@
+<script>
+ window.result = window.injected;
+</script> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/title.html b/remote/test/puppeteer/test/assets/title.html
new file mode 100644
index 0000000000..88a86ce412
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/title.html
@@ -0,0 +1 @@
+<title>Woof-Woof</title>
diff --git a/remote/test/puppeteer/test/assets/worker/worker.html b/remote/test/puppeteer/test/assets/worker/worker.html
new file mode 100644
index 0000000000..7de2d9fd9e
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/worker/worker.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Worker test</title>
+ </head>
+ <body>
+ <script>
+ var worker = new Worker('worker.js');
+ worker.onmessage = function(message) {
+ console.log(message.data);
+ };
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/remote/test/puppeteer/test/assets/worker/worker.js b/remote/test/puppeteer/test/assets/worker/worker.js
new file mode 100644
index 0000000000..0626f13e58
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/worker/worker.js
@@ -0,0 +1,16 @@
+console.log('hello from the worker');
+
+function workerFunction() {
+ return 'worker function result';
+}
+
+self.addEventListener('message', (event) => {
+ console.log('got this data: ' + event.data);
+});
+
+(async function () {
+ while (true) {
+ self.postMessage(workerFunction.toString());
+ await new Promise((x) => setTimeout(x, 100));
+ }
+})();
diff --git a/remote/test/puppeteer/test/assets/wrappedlink.html b/remote/test/puppeteer/test/assets/wrappedlink.html
new file mode 100644
index 0000000000..429b6e9156
--- /dev/null
+++ b/remote/test/puppeteer/test/assets/wrappedlink.html
@@ -0,0 +1,32 @@
+<style>
+:root {
+ font-family: monospace;
+}
+
+body {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+div {
+ width: 10ch;
+ word-wrap: break-word;
+ border: 1px solid blue;
+ transform: rotate(33deg);
+ line-height: 8ch;
+ padding: 2ch;
+}
+
+a {
+ margin-left: 7ch;
+}
+</style>
+<div>
+ <a href='#clicked'>123321</a>
+</div>
+<script>
+ document.querySelector('a').addEventListener('click', () => {
+ window.__clicked = true;
+ });
+</script>
diff --git a/remote/test/puppeteer/test/browser.spec.ts b/remote/test/puppeteer/test/browser.spec.ts
new file mode 100644
index 0000000000..0d06e7f60e
--- /dev/null
+++ b/remote/test/puppeteer/test/browser.spec.ts
@@ -0,0 +1,81 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import { getTestState, setupTestBrowserHooks } from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('Browser specs', function () {
+ setupTestBrowserHooks();
+
+ describe('Browser.version', function () {
+ it('should return whether we are in headless', async () => {
+ const { browser, isHeadless } = getTestState();
+
+ const version = await browser.version();
+ expect(version.length).toBeGreaterThan(0);
+ expect(version.startsWith('Headless')).toBe(isHeadless);
+ });
+ });
+
+ describe('Browser.userAgent', function () {
+ it('should include WebKit', async () => {
+ const { browser, isChrome } = 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 } = getTestState();
+
+ const target = browser.target();
+ expect(target.type()).toBe('browser');
+ });
+ });
+
+ describe('Browser.process', function () {
+ it('should return child_process instance', async () => {
+ const { browser } = getTestState();
+
+ const process = await browser.process();
+ expect(process.pid).toBeGreaterThan(0);
+ });
+ it('should not return child_process for remote browser', async () => {
+ const { browser, puppeteer } = getTestState();
+
+ const browserWSEndpoint = browser.wsEndpoint();
+ const remoteBrowser = await puppeteer.connect({ browserWSEndpoint });
+ expect(remoteBrowser.process()).toBe(null);
+ remoteBrowser.disconnect();
+ });
+ });
+
+ describe('Browser.isConnected', () => {
+ it('should set the browser connected state', async () => {
+ const { browser, puppeteer } = getTestState();
+
+ const browserWSEndpoint = browser.wsEndpoint();
+ const newBrowser = await puppeteer.connect({ browserWSEndpoint });
+ expect(newBrowser.isConnected()).toBe(true);
+ newBrowser.disconnect();
+ expect(newBrowser.isConnected()).toBe(false);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/browsercontext.spec.ts b/remote/test/puppeteer/test/browsercontext.spec.ts
new file mode 100644
index 0000000000..dd2be4b673
--- /dev/null
+++ b/remote/test/puppeteer/test/browsercontext.spec.ts
@@ -0,0 +1,206 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+import utils from './utils.js';
+
+describe('BrowserContext', function () {
+ setupTestBrowserHooks();
+ it('should have default context', async () => {
+ const { browser } = getTestState();
+ expect(browser.browserContexts().length).toEqual(1);
+ const defaultContext = browser.browserContexts()[0];
+ expect(defaultContext.isIncognito()).toBe(false);
+ let error = null;
+ await defaultContext.close().catch((error_) => (error = error_));
+ expect(browser.defaultBrowserContext()).toBe(defaultContext);
+ expect(error.message).toContain('cannot be closed');
+ });
+ it('should create new incognito context', async () => {
+ const { browser } = getTestState();
+
+ expect(browser.browserContexts().length).toBe(1);
+ const context = await browser.createIncognitoBrowserContext();
+ expect(context.isIncognito()).toBe(true);
+ expect(browser.browserContexts().length).toBe(2);
+ expect(browser.browserContexts().indexOf(context) !== -1).toBe(true);
+ await context.close();
+ expect(browser.browserContexts().length).toBe(1);
+ });
+ it('should close all belonging targets once closing context', async () => {
+ const { browser } = getTestState();
+
+ expect((await browser.pages()).length).toBe(1);
+
+ const context = await browser.createIncognitoBrowserContext();
+ await context.newPage();
+ expect((await browser.pages()).length).toBe(2);
+ expect((await context.pages()).length).toBe(1);
+
+ await context.close();
+ expect((await browser.pages()).length).toBe(1);
+ });
+ it('window.open should use parent tab context', async () => {
+ const { browser, server } = getTestState();
+
+ const context = await browser.createIncognitoBrowserContext();
+ const page = await context.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ const [popupTarget] = await Promise.all([
+ utils.waitEvent(browser, 'targetcreated'),
+ page.evaluate<(url: string) => void>(
+ (url) => window.open(url),
+ server.EMPTY_PAGE
+ ),
+ ]);
+ expect(popupTarget.browserContext()).toBe(context);
+ await context.close();
+ });
+ it('should fire target events', async () => {
+ const { browser, server } = getTestState();
+
+ const context = await browser.createIncognitoBrowserContext();
+ const events = [];
+ 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}`,
+ ]);
+ await context.close();
+ });
+ it('should wait for a target', async () => {
+ const { browser, puppeteer, server } = getTestState();
+
+ const context = await browser.createIncognitoBrowserContext();
+ let resolved = false;
+
+ const targetPromise = context.waitForTarget(
+ (target) => target.url() === server.EMPTY_PAGE
+ );
+ targetPromise
+ .then(() => (resolved = true))
+ .catch((error) => {
+ resolved = true;
+ if (error instanceof puppeteer.errors.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 puppeteer.errors.TimeoutError) {
+ console.error(error);
+ } else throw error;
+ }
+ await context.close();
+ });
+
+ it('should timeout waiting for a non-existent target', async () => {
+ const { browser, puppeteer, server } = getTestState();
+
+ const context = await browser.createIncognitoBrowserContext();
+ const error = await context
+ .waitForTarget((target) => target.url() === server.EMPTY_PAGE, {
+ timeout: 1,
+ })
+ .catch((error_) => error_);
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ await context.close();
+ });
+
+ it('should isolate localStorage and cookies', async () => {
+ const { browser, server } = getTestState();
+
+ // Create two incognito contexts.
+ const context1 = await browser.createIncognitoBrowserContext();
+ const context2 = await browser.createIncognitoBrowserContext();
+ expect(context1.targets().length).toBe(0);
+ expect(context2.targets().length).toBe(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().length).toBe(1);
+ expect(context2.targets().length).toBe(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().length).toBe(1);
+ expect(context1.targets()[0]).toBe(page1.target());
+ expect(context2.targets().length).toBe(1);
+ expect(context2.targets()[0]).toBe(page2.target());
+
+ // Make sure pages don't share localstorage or cookies.
+ expect(await page1.evaluate(() => localStorage.getItem('name'))).toBe(
+ 'page1'
+ );
+ expect(await page1.evaluate(() => document.cookie)).toBe('name=page1');
+ expect(await page2.evaluate(() => localStorage.getItem('name'))).toBe(
+ 'page2'
+ );
+ expect(await page2.evaluate(() => document.cookie)).toBe('name=page2');
+
+ // Cleanup contexts.
+ await Promise.all([context1.close(), context2.close()]);
+ expect(browser.browserContexts().length).toBe(1);
+ });
+
+ it('should work across sessions', async () => {
+ const { browser, puppeteer } = getTestState();
+
+ expect(browser.browserContexts().length).toBe(1);
+ const context = await browser.createIncognitoBrowserContext();
+ expect(browser.browserContexts().length).toBe(2);
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ });
+ const contexts = remoteBrowser.browserContexts();
+ expect(contexts.length).toBe(2);
+ remoteBrowser.disconnect();
+ await context.close();
+ });
+});
diff --git a/remote/test/puppeteer/test/chromiumonly.spec.ts b/remote/test/puppeteer/test/chromiumonly.spec.ts
new file mode 100644
index 0000000000..7b64a70fd3
--- /dev/null
+++ b/remote/test/puppeteer/test/chromiumonly.spec.ts
@@ -0,0 +1,159 @@
+/**
+ * Copyright 2019 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+ describeChromeOnly,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describeChromeOnly('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 { defaultBrowserOptions, puppeteer } = getTestState();
+
+ const originalBrowser = await puppeteer.launch(
+ Object.assign({}, defaultBrowserOptions, {
+ args: ['--remote-debugging-port=21222'],
+ })
+ );
+ const browserURL = 'http://127.0.0.1:21222';
+
+ const browser1 = await puppeteer.connect({ browserURL });
+ const page1 = await browser1.newPage();
+ expect(await page1.evaluate(() => 7 * 8)).toBe(56);
+ browser1.disconnect();
+
+ const browser2 = await puppeteer.connect({
+ browserURL: browserURL + '/',
+ });
+ const page2 = await browser2.newPage();
+ expect(await page2.evaluate(() => 8 * 7)).toBe(56);
+ browser2.disconnect();
+ originalBrowser.close();
+ });
+ it('should throw when using both browserWSEndpoint and browserURL', async () => {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+
+ const originalBrowser = await puppeteer.launch(
+ Object.assign({}, defaultBrowserOptions, {
+ args: ['--remote-debugging-port=21222'],
+ })
+ );
+ const browserURL = 'http://127.0.0.1:21222';
+
+ let error = null;
+ await puppeteer
+ .connect({
+ browserURL,
+ browserWSEndpoint: originalBrowser.wsEndpoint(),
+ })
+ .catch((error_) => (error = error_));
+ expect(error.message).toContain(
+ 'Exactly one of browserWSEndpoint, browserURL or transport'
+ );
+
+ originalBrowser.close();
+ });
+ it('should throw when trying to connect to non-existing browser', async () => {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+
+ const originalBrowser = await puppeteer.launch(
+ Object.assign({}, defaultBrowserOptions, {
+ args: ['--remote-debugging-port=21222'],
+ })
+ );
+ const browserURL = 'http://127.0.0.1:32333';
+
+ let error = null;
+ await puppeteer
+ .connect({ browserURL })
+ .catch((error_) => (error = error_));
+ expect(error.message).toContain(
+ 'Failed to fetch browser webSocket URL from'
+ );
+ originalBrowser.close();
+ });
+ });
+
+ describe('Puppeteer.launch |pipe| option', function () {
+ it('should support the pipe option', async () => {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+ const options = Object.assign({ pipe: true }, defaultBrowserOptions);
+ const browser = await puppeteer.launch(options);
+ expect((await browser.pages()).length).toBe(1);
+ expect(browser.wsEndpoint()).toBe('');
+ const page = await browser.newPage();
+ expect(await page.evaluate('11 * 11')).toBe(121);
+ await page.close();
+ await browser.close();
+ });
+ it('should support the pipe argument', async () => {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+ const options = Object.assign({}, defaultBrowserOptions);
+ options.args = ['--remote-debugging-pipe'].concat(options.args || []);
+ const browser = await puppeteer.launch(options);
+ expect(browser.wsEndpoint()).toBe('');
+ const page = await browser.newPage();
+ expect(await page.evaluate('11 * 11')).toBe(121);
+ await page.close();
+ await browser.close();
+ });
+ it('should fire "disconnected" when closing with pipe', async () => {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+ const options = Object.assign({ pipe: true }, defaultBrowserOptions);
+ const browser = await puppeteer.launch(options);
+ const disconnectedEventPromise = new Promise((resolve) =>
+ browser.once('disconnected', resolve)
+ );
+ // Emulate user exiting browser.
+ browser.process().kill();
+ await disconnectedEventPromise;
+ });
+ });
+});
+
+describeChromeOnly('Chromium-Specific Page Tests', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+ it('Page.setRequestInterception should work with intervention headers', async () => {
+ const { server, page } = getTestState();
+
+ server.setRoute('/intervention', (req, res) =>
+ res.end(`
+ <script>
+ document.write('<script src="${server.CROSS_PROCESS_PREFIX}/intervention.js">' + '</scr' + 'ipt>');
+ </script>
+ `)
+ );
+ server.setRedirect('/intervention.js', '/redirect.js');
+ let serverRequest = null;
+ server.setRoute('/redirect.js', (req, res) => {
+ serverRequest = req;
+ res.end('console.log(1);');
+ });
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => 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/click.spec.ts b/remote/test/puppeteer/test/click.spec.ts
new file mode 100644
index 0000000000..48d4b64409
--- /dev/null
+++ b/remote/test/puppeteer/test/click.spec.ts
@@ -0,0 +1,352 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestPageAndContextHooks,
+ setupTestBrowserHooks,
+ itFailsFirefox,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+import utils from './utils.js';
+
+describe('Page.click', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+ it('should click the button', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.click('button');
+ expect(await page.evaluate(() => globalThis.result)).toBe('Clicked');
+ });
+ it('should click svg', async () => {
+ const { page } = 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(() => globalThis.__CLICKED)).toBe(42);
+ });
+ it(
+ 'should click the button if window.Node is removed',
+ async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.evaluate(() => delete window.Node);
+ await page.click('button');
+ expect(await page.evaluate(() => globalThis.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 } = 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(() => globalThis.CLICKED)).toBe(42);
+ });
+ it('should not throw UnhandledPromiseRejection when page closes', async () => {
+ const { page } = 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 } = 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(() => globalThis.result)).toBe('Clicked');
+ });
+ it('should click with disabled javascript', async () => {
+ const { page, server } = 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 click when one of inline box children is outside of viewport', async () => {
+ const { page } = 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(() => globalThis.CLICKED)).toBe(42);
+ });
+ it('should select the text by triple clicking', async () => {
+ const { page, server } = 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.click('textarea');
+ await page.click('textarea', { clickCount: 2 });
+ await page.click('textarea', { clickCount: 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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/offscreenbuttons.html');
+ const messages = [];
+ page.on('console', (msg) => messages.push(msg.text()));
+ for (let i = 0; i < 11; ++i) {
+ // We might've scrolled to click a button - reset to (0, 0).
+ await page.evaluate(() => 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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/wrappedlink.html');
+ await page.click('a');
+ expect(await page.evaluate(() => globalThis.__clicked)).toBe(true);
+ });
+
+ it('should click on checkbox input and toggle', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/checkbox.html');
+ expect(await page.evaluate(() => globalThis.result.check)).toBe(null);
+ await page.click('input#agree');
+ expect(await page.evaluate(() => globalThis.result.check)).toBe(true);
+ expect(await page.evaluate(() => globalThis.result.events)).toEqual([
+ 'mouseover',
+ 'mouseenter',
+ 'mousemove',
+ 'mousedown',
+ 'mouseup',
+ 'click',
+ 'input',
+ 'change',
+ ]);
+ await page.click('input#agree');
+ expect(await page.evaluate(() => globalThis.result.check)).toBe(false);
+ });
+
+ it('should click on checkbox label and toggle', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/checkbox.html');
+ expect(await page.evaluate(() => globalThis.result.check)).toBe(null);
+ await page.click('label[for="agree"]');
+ expect(await page.evaluate(() => globalThis.result.check)).toBe(true);
+ expect(await page.evaluate(() => globalThis.result.events)).toEqual([
+ 'click',
+ 'input',
+ 'change',
+ ]);
+ await page.click('label[for="agree"]');
+ expect(await page.evaluate(() => globalThis.result.check)).toBe(false);
+ });
+
+ it('should fail to click a missing button', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ let error = null;
+ await page
+ .click('button.does-not-exist')
+ .catch((error_) => (error = error_));
+ expect(error.message).toBe(
+ 'No node 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, puppeteer } = getTestState();
+
+ await page.setViewport(puppeteer.devices['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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.click('#button-5');
+ expect(
+ await page.evaluate(() => document.querySelector('#button-5').textContent)
+ ).toBe('clicked');
+ await page.click('#button-80');
+ expect(
+ await page.evaluate(
+ () => document.querySelector('#button-80').textContent
+ )
+ ).toBe('clicked');
+ });
+ it('should double click the button', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.evaluate(() => {
+ globalThis.double = false;
+ const button = document.querySelector('button');
+ button.addEventListener('dblclick', () => {
+ globalThis.double = true;
+ });
+ });
+ const button = await page.$('button');
+ await button.click({ clickCount: 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 } = 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(() => globalThis.result)).toBe('Clicked');
+ });
+ it('should click a rotated button', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/rotatedButton.html');
+ await page.click('button');
+ expect(await page.evaluate(() => globalThis.result)).toBe('Clicked');
+ });
+ it('should fire contextmenu event on right click', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.click('#button-8', { button: 'right' });
+ expect(
+ await page.evaluate(() => document.querySelector('#button-8').textContent)
+ ).toBe('context menu');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/206
+ it('should click links which cause navigation', async () => {
+ const { page, server } = 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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent('<div style="width:100px;height:100px">spacer</div>');
+ await utils.attachFrame(
+ page,
+ 'button-test',
+ server.PREFIX + '/input/button.html'
+ );
+ const frame = page.frames()[1];
+ const button = await frame.$('button');
+ await button.click();
+ expect(await frame.evaluate(() => globalThis.result)).toBe('Clicked');
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/4110
+ xit('should click the button with fixed position inside an iframe', async () => {
+ const { page, server } = 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 utils.attachFrame(
+ page,
+ 'button-test',
+ server.CROSS_PROCESS_PREFIX + '/input/button.html'
+ );
+ const frame = page.frames()[1];
+ await frame.$eval('button', (button: HTMLElement) =>
+ button.style.setProperty('position', 'fixed')
+ );
+ await frame.click('button');
+ expect(await frame.evaluate(() => globalThis.result)).toBe('Clicked');
+ });
+ it(
+ 'should click the button with deviceScaleFactor set',
+ async () => {
+ const { page, server } = getTestState();
+
+ await page.setViewport({ width: 400, height: 400, deviceScaleFactor: 5 });
+ expect(await page.evaluate(() => window.devicePixelRatio)).toBe(5);
+ await page.setContent(
+ '<div style="width:100px;height:100px">spacer</div>'
+ );
+ await utils.attachFrame(
+ page,
+ 'button-test',
+ server.PREFIX + '/input/button.html'
+ );
+ const frame = page.frames()[1];
+ const button = await frame.$('button');
+ await button.click();
+ expect(await frame.evaluate(() => globalThis.result)).toBe('Clicked');
+ }
+ );
+});
diff --git a/remote/test/puppeteer/test/cookies.spec.ts b/remote/test/puppeteer/test/cookies.spec.ts
new file mode 100644
index 0000000000..6d3d9a8cab
--- /dev/null
+++ b/remote/test/puppeteer/test/cookies.spec.ts
@@ -0,0 +1,526 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+ itFailsFirefox,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('Cookie specs', () => {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ describe('Page.cookies', function () {
+ it('should return no cookies in pristine browser context', async () => {
+ const { page, server } = getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ expect(await page.cookies()).toEqual([]);
+ });
+ it('should get a cookie', async () => {
+ const { page, server } = getTestState();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ document.cookie = 'username=John Doe';
+ });
+ expect(await page.cookies()).toEqual([
+ {
+ name: 'username',
+ value: 'John Doe',
+ domain: 'localhost',
+ path: '/',
+ expires: -1,
+ size: 16,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ },
+ ]);
+ });
+ it('should properly report httpOnly cookie', async () => {
+ const { page, server } = 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.length).toBe(1);
+ expect(cookies[0].httpOnly).toBe(true);
+ });
+ it('should properly report "Strict" sameSite cookie', async () => {
+ const { page, server } = 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.length).toBe(1);
+ expect(cookies[0].sameSite).toBe('Strict');
+ });
+ it('should properly report "Lax" sameSite cookie', async () => {
+ const { page, server } = 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.length).toBe(1);
+ expect(cookies[0].sameSite).toBe('Lax');
+ });
+ it('should get multiple cookies', async () => {
+ const { page, server } = 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) => a.name.localeCompare(b.name));
+ expect(cookies).toEqual([
+ {
+ name: 'password',
+ value: '1234',
+ domain: 'localhost',
+ path: '/',
+ expires: -1,
+ size: 12,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ },
+ {
+ name: 'username',
+ value: 'John Doe',
+ domain: 'localhost',
+ path: '/',
+ expires: -1,
+ size: 16,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ },
+ ]);
+ });
+ it('should get cookies from multiple urls', async () => {
+ const { page } = 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) => a.name.localeCompare(b.name));
+ expect(cookies).toEqual([
+ {
+ name: 'birdo',
+ value: 'tweets',
+ domain: 'baz.com',
+ path: '/',
+ expires: -1,
+ size: 11,
+ httpOnly: false,
+ secure: true,
+ session: true,
+ },
+ {
+ name: 'doggo',
+ value: 'woofs',
+ domain: 'foo.com',
+ path: '/',
+ expires: -1,
+ size: 10,
+ httpOnly: false,
+ secure: true,
+ session: true,
+ },
+ ]);
+ });
+ });
+ describe('Page.setCookie', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({
+ name: 'password',
+ value: '123456',
+ });
+ expect(await page.evaluate(() => document.cookie)).toEqual(
+ 'password=123456'
+ );
+ });
+ it('should isolate cookies in browser contexts', async () => {
+ const { page, server, browser } = 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.length).toBe(1);
+ expect(cookies2.length).toBe(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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie(
+ {
+ name: 'password',
+ value: '123456',
+ },
+ {
+ name: 'foo',
+ value: 'bar',
+ }
+ );
+ expect(
+ await page.evaluate(() => {
+ const cookies = document.cookie.split(';');
+ return cookies.map((cookie) => cookie.trim()).sort();
+ })
+ ).toEqual(['foo=bar', 'password=123456']);
+ });
+ it('should have |expires| set to |-1| for session cookies', async () => {
+ const { page, server } = 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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({
+ name: 'password',
+ value: '123456',
+ });
+ const cookies = await page.cookies();
+ expect(cookies.sort((a, b) => a.name.localeCompare(b.name))).toEqual([
+ {
+ name: 'password',
+ value: '123456',
+ domain: 'localhost',
+ path: '/',
+ expires: -1,
+ size: 14,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ },
+ ]);
+ });
+ it('should set a cookie with a path', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/grid.html');
+ await page.setCookie({
+ name: 'gridcookie',
+ value: 'GRID',
+ path: '/grid.html',
+ });
+ expect(await page.cookies()).toEqual([
+ {
+ name: 'gridcookie',
+ value: 'GRID',
+ domain: 'localhost',
+ path: '/grid.html',
+ expires: -1,
+ size: 14,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ },
+ ]);
+ expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID');
+ await page.goto(server.EMPTY_PAGE);
+ expect(await page.cookies()).toEqual([]);
+ 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 } = getTestState();
+
+ await page.goto('about:blank');
+ let error = null;
+ try {
+ await page.setCookie({ name: 'example-cookie', value: 'best' });
+ } catch (error_) {
+ error = 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 } = getTestState();
+
+ let error = null;
+ 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_;
+ }
+ 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 } = getTestState();
+
+ let error = null;
+ await page.goto('data:,Hello%2C%20World!');
+ try {
+ await page.setCookie({ name: 'example-cookie', value: 'best' });
+ } catch (error_) {
+ error = 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 } = 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 unsecure cookie for HTTP website', async () => {
+ const { page, server } = 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 } = 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('');
+ expect(await page.cookies()).toEqual([]);
+ expect(await page.cookies('https://www.example.com')).toEqual([
+ {
+ name: 'example-cookie',
+ value: 'best',
+ domain: 'www.example.com',
+ path: '/',
+ expires: -1,
+ size: 18,
+ httpOnly: false,
+ secure: true,
+ session: true,
+ },
+ ]);
+ });
+ it('should set cookies from a frame', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/grid.html');
+ await page.setCookie({ name: 'localhost-cookie', value: 'best' });
+ await page.evaluate<(src: string) => Promise<void>>((src) => {
+ let fulfill;
+ const promise = new Promise<void>((x) => (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('');
+
+ expect(await page.cookies()).toEqual([
+ {
+ name: 'localhost-cookie',
+ value: 'best',
+ domain: 'localhost',
+ path: '/',
+ expires: -1,
+ size: 20,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ },
+ ]);
+
+ expect(await page.cookies(server.CROSS_PROCESS_PREFIX)).toEqual([
+ {
+ name: '127-cookie',
+ value: 'worst',
+ domain: '127.0.0.1',
+ path: '/',
+ expires: -1,
+ size: 15,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ },
+ ]);
+ });
+ it(
+ 'should set secure same-site cookies from a frame',
+ async () => {
+ const {
+ httpsServer,
+ puppeteer,
+ defaultBrowserOptions,
+ } = getTestState();
+
+ const browser = await puppeteer.launch({
+ ...defaultBrowserOptions,
+ ignoreHTTPSErrors: true,
+ });
+
+ const page = await browser.newPage();
+
+ try {
+ await page.goto(httpsServer.PREFIX + '/grid.html');
+ await page.evaluate<(src: string) => Promise<void>>((src) => {
+ let fulfill;
+ const promise = new Promise<void>((x) => (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'
+ );
+ expect(await page.cookies(httpsServer.CROSS_PROCESS_PREFIX)).toEqual([
+ {
+ name: '127-same-site-cookie',
+ value: 'best',
+ domain: '127.0.0.1',
+ path: '/',
+ expires: -1,
+ size: 24,
+ httpOnly: false,
+ sameSite: 'None',
+ secure: true,
+ session: true,
+ },
+ ]);
+ } finally {
+ await page.close();
+ await browser.close();
+ }
+ }
+ );
+ });
+
+ describe('Page.deleteCookie', function () {
+ it('should work', async () => {
+ const { page, server } = 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/coverage-utils.js b/remote/test/puppeteer/test/coverage-utils.js
new file mode 100644
index 0000000000..c23e507f9d
--- /dev/null
+++ b/remote/test/puppeteer/test/coverage-utils.js
@@ -0,0 +1,164 @@
+/**
+ * Copyright 2020 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// TODO (@jackfranklin): convert this to TypeScript and enable type-checking
+// @ts-nocheck
+
+/* We want to ensure that all of Puppeteer's public API is tested via our unit
+ * tests but we can't use a tool like Istanbul because the way it instruments
+ * code unfortunately breaks in Puppeteer where some of that code is then being
+ * executed in a browser context.
+ *
+ * So instead we maintain this coverage code which does the following:
+ * * takes every public method that we expect to be tested
+ * * replaces it with a method that calls the original but also updates a Map of calls
+ * * in an after() test callback it asserts that every public method was called.
+ *
+ * We run this when COVERAGE=1.
+ */
+
+const path = require('path');
+const fs = require('fs');
+
+/**
+ * This object is also used by DocLint to know which classes to check are
+ * documented. It's a pretty hacky solution but DocLint is going away soon as
+ * part of the TSDoc migration.
+ */
+const MODULES_TO_CHECK_FOR_COVERAGE = {
+ Accessibility: '../lib/cjs/puppeteer/common/Accessibility',
+ Browser: '../lib/cjs/puppeteer/common/Browser',
+ BrowserContext: '../lib/cjs/puppeteer/common/Browser',
+ BrowserFetcher: '../lib/cjs/puppeteer/node/BrowserFetcher',
+ CDPSession: '../lib/cjs/puppeteer/common/Connection',
+ ConsoleMessage: '../lib/cjs/puppeteer/common/ConsoleMessage',
+ Coverage: '../lib/cjs/puppeteer/common/Coverage',
+ Dialog: '../lib/cjs/puppeteer/common/Dialog',
+ ElementHandle: '../lib/cjs/puppeteer/common/JSHandle',
+ ExecutionContext: '../lib/cjs/puppeteer/common/ExecutionContext',
+ EventEmitter: '../lib/cjs/puppeteer/common/EventEmitter',
+ FileChooser: '../lib/cjs/puppeteer/common/FileChooser',
+ Frame: '../lib/cjs/puppeteer/common/FrameManager',
+ JSHandle: '../lib/cjs/puppeteer/common/JSHandle',
+ Keyboard: '../lib/cjs/puppeteer/common/Input',
+ Mouse: '../lib/cjs/puppeteer/common/Input',
+ Page: '../lib/cjs/puppeteer/common/Page',
+ Puppeteer: '../lib/cjs/puppeteer/common/Puppeteer',
+ PuppeteerNode: '../lib/cjs/puppeteer/node/Puppeteer',
+ HTTPRequest: '../lib/cjs/puppeteer/common/HTTPRequest',
+ HTTPResponse: '../lib/cjs/puppeteer/common/HTTPResponse',
+ SecurityDetails: '../lib/cjs/puppeteer/common/SecurityDetails',
+ Target: '../lib/cjs/puppeteer/common/Target',
+ TimeoutError: '../lib/cjs/puppeteer/common/Errors',
+ Touchscreen: '../lib/cjs/puppeteer/common/Input',
+ Tracing: '../lib/cjs/puppeteer/common/Tracing',
+ WebWorker: '../lib/cjs/puppeteer/common/WebWorker',
+};
+
+function traceAPICoverage(apiCoverage, className, modulePath) {
+ const loadedModule = require(modulePath);
+ const classType = loadedModule[className];
+
+ if (!classType || !classType.prototype) {
+ console.error(
+ `Coverage error: could not find class for ${className}. Is src/api.ts up to date?`
+ );
+ process.exit(1);
+ }
+ for (const methodName of Reflect.ownKeys(classType.prototype)) {
+ const method = Reflect.get(classType.prototype, methodName);
+ if (
+ methodName === 'constructor' ||
+ typeof methodName !== 'string' ||
+ methodName.startsWith('_') ||
+ typeof method !== 'function'
+ )
+ continue;
+ apiCoverage.set(`${className}.${methodName}`, false);
+ Reflect.set(classType.prototype, methodName, function (...args) {
+ apiCoverage.set(`${className}.${methodName}`, true);
+ return method.call(this, ...args);
+ });
+ }
+
+ /**
+ * If classes emit events, those events are exposed via an object in the same
+ * module named XEmittedEvents, where X is the name of the class. For example,
+ * the Page module exposes PageEmittedEvents.
+ */
+ const eventsName = `${className}EmittedEvents`;
+ if (loadedModule[eventsName]) {
+ for (const event of Object.values(loadedModule[eventsName])) {
+ if (typeof event !== 'symbol')
+ apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false);
+ }
+ const method = Reflect.get(classType.prototype, 'emit');
+ Reflect.set(classType.prototype, 'emit', function (event, ...args) {
+ if (typeof event !== 'symbol' && this.listenerCount(event))
+ apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true);
+ return method.call(this, event, ...args);
+ });
+ }
+}
+
+const coverageLocation = path.join(__dirname, 'coverage.json');
+
+const clearOldCoverage = () => {
+ try {
+ fs.unlinkSync(coverageLocation);
+ } catch (error) {
+ // do nothing, the file didn't exist
+ }
+};
+const writeCoverage = (coverage) => {
+ fs.writeFileSync(coverageLocation, JSON.stringify([...coverage.entries()]));
+};
+
+const getCoverageResults = () => {
+ let contents;
+ try {
+ contents = fs.readFileSync(coverageLocation, { encoding: 'utf8' });
+ } catch (error) {
+ console.error('Warning: coverage file does not exist or is not readable.');
+ }
+
+ const coverageMap = new Map(JSON.parse(contents));
+ return coverageMap;
+};
+
+const trackCoverage = () => {
+ clearOldCoverage();
+ const coverageMap = new Map();
+
+ return {
+ beforeAll: () => {
+ for (const [className, moduleFilePath] of Object.entries(
+ MODULES_TO_CHECK_FOR_COVERAGE
+ )) {
+ traceAPICoverage(coverageMap, className, moduleFilePath);
+ }
+ },
+ afterAll: () => {
+ writeCoverage(coverageMap);
+ },
+ };
+};
+
+module.exports = {
+ trackCoverage,
+ getCoverageResults,
+ MODULES_TO_CHECK_FOR_COVERAGE,
+};
diff --git a/remote/test/puppeteer/test/coverage.spec.ts b/remote/test/puppeteer/test/coverage.spec.ts
new file mode 100644
index 0000000000..d3aa3bf225
--- /dev/null
+++ b/remote/test/puppeteer/test/coverage.spec.ts
@@ -0,0 +1,280 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestPageAndContextHooks,
+ setupTestBrowserHooks,
+ describeChromeOnly,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('Coverage specs', function () {
+ describeChromeOnly('JSCoverage', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ it('should work', async () => {
+ const { page, server } = getTestState();
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/simple.html', {
+ waitUntil: 'networkidle0',
+ });
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage.length).toBe(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 } = getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/sourceurl.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage.length).toBe(1);
+ expect(coverage[0].url).toBe('nicename.js');
+ });
+ it('should ignore eval() scripts by default', async () => {
+ const { page, server } = getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/eval.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage.length).toBe(1);
+ });
+ it("shouldn't ignore eval() scripts if reportAnonymousScripts is true", async () => {
+ const { page, server } = 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) => entry.url.startsWith('debugger://'))
+ ).not.toBe(null);
+ expect(coverage.length).toBe(2);
+ });
+ it('should ignore pptr internal scripts if reportAnonymousScripts is true', async () => {
+ const { page, server } = getTestState();
+
+ await page.coverage.startJSCoverage({ reportAnonymousScripts: true });
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate('console.log("foo")');
+ await page.evaluate(() => console.log('bar'));
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage.length).toBe(0);
+ });
+ it('should report multiple scripts', async () => {
+ const { page, server } = getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/multiple.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage.length).toBe(2);
+ coverage.sort((a, b) => 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 } = getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/ranges.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage.length).toBe(1);
+ const entry = coverage[0];
+ expect(entry.ranges.length).toBe(1);
+ const range = entry.ranges[0];
+ expect(entry.text.substring(range.start, range.end)).toBe(
+ `console.log('used!');`
+ );
+ });
+ it('should report scripts that have no coverage', async () => {
+ const { page, server } = getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.PREFIX + '/jscoverage/unused.html');
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage.length).toBe(1);
+ const entry = coverage[0];
+ expect(entry.url).toContain('unused.html');
+ expect(entry.ranges.length).toBe(0);
+ });
+ it('should work with conditionals', async () => {
+ const { page, server } = 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}\//g, ':<PORT>/')
+ ).toBeGolden('jscoverage-involved.txt');
+ });
+ describe('resetOnNavigation', function () {
+ it('should report scripts across navigations when disabled', async () => {
+ const { page, server } = getTestState();
+
+ await page.coverage.startJSCoverage({ resetOnNavigation: false });
+ await page.goto(server.PREFIX + '/jscoverage/multiple.html');
+ await page.goto(server.EMPTY_PAGE);
+ const coverage = await page.coverage.stopJSCoverage();
+ expect(coverage.length).toBe(2);
+ });
+
+ it('should NOT report scripts across navigations when enabled', async () => {
+ const { page, server } = 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.length).toBe(0);
+ });
+ });
+ // @see https://crbug.com/990945
+ xit('should not hang when there is a debugger statement', async () => {
+ const { page, server } = getTestState();
+
+ await page.coverage.startJSCoverage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ debugger; // eslint-disable-line no-debugger
+ });
+ await page.coverage.stopJSCoverage();
+ });
+ });
+
+ describeChromeOnly('CSSCoverage', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/simple.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage.length).toBe(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 } = getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/sourceurl.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage.length).toBe(1);
+ expect(coverage[0].url).toBe('nicename.css');
+ });
+ it('should report multiple stylesheets', async () => {
+ const { page, server } = getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/multiple.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage.length).toBe(2);
+ coverage.sort((a, b) => 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 } = getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/unused.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage.length).toBe(1);
+ expect(coverage[0].url).toBe('unused.css');
+ expect(coverage[0].ranges.length).toBe(0);
+ });
+ it('should work with media queries', async () => {
+ const { page, server } = getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.goto(server.PREFIX + '/csscoverage/media.html');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage.length).toBe(1);
+ expect(coverage[0].url).toContain('/csscoverage/media.html');
+ expect(coverage[0].ranges).toEqual([{ start: 17, end: 38 }]);
+ });
+ it('should work with complicated usecases', async () => {
+ const { page, server } = 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}\//g, ':<PORT>/')
+ ).toBeGolden('csscoverage-involved.txt');
+ });
+ it('should ignore injected stylesheets', async () => {
+ const { page } = getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.addStyleTag({ content: 'body { margin: 10px;}' });
+ // trigger style recalc
+ const margin = await page.evaluate(
+ () => window.getComputedStyle(document.body).margin
+ );
+ expect(margin).toBe('10px');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage.length).toBe(0);
+ });
+ describe('resetOnNavigation', function () {
+ it('should report stylesheets across navigations', async () => {
+ const { page, server } = 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.length).toBe(2);
+ });
+ it('should NOT report scripts across navigations', async () => {
+ const { page, server } = 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.length).toBe(0);
+ });
+ });
+ it('should work with a recently loaded stylesheet', async () => {
+ const { page, server } = getTestState();
+
+ await page.coverage.startCSSCoverage();
+ await page.evaluate<(url: string) => Promise<void>>(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) => (link.onload = x));
+ }, server.PREFIX + '/csscoverage/stylesheet1.css');
+ const coverage = await page.coverage.stopCSSCoverage();
+ expect(coverage.length).toBe(1);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/defaultbrowsercontext.spec.ts b/remote/test/puppeteer/test/defaultbrowsercontext.spec.ts
new file mode 100644
index 0000000000..23521ae762
--- /dev/null
+++ b/remote/test/puppeteer/test/defaultbrowsercontext.spec.ts
@@ -0,0 +1,104 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+ itFailsFirefox,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('DefaultBrowserContext', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+ it('page.cookies() should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ document.cookie = 'username=John Doe';
+ });
+ expect(await page.cookies()).toEqual([
+ {
+ name: 'username',
+ value: 'John Doe',
+ domain: 'localhost',
+ path: '/',
+ expires: -1,
+ size: 16,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ },
+ ]);
+ });
+ it('page.setCookie() should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setCookie({
+ name: 'username',
+ value: 'John Doe',
+ });
+ expect(await page.evaluate(() => document.cookie)).toBe(
+ 'username=John Doe'
+ );
+ expect(await page.cookies()).toEqual([
+ {
+ name: 'username',
+ value: 'John Doe',
+ domain: 'localhost',
+ path: '/',
+ expires: -1,
+ size: 16,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ },
+ ]);
+ });
+ it('page.deleteCookie() should work', async () => {
+ const { page, server } = 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');
+ expect(await page.cookies()).toEqual([
+ {
+ name: 'cookie1',
+ value: '1',
+ domain: 'localhost',
+ path: '/',
+ expires: -1,
+ size: 8,
+ httpOnly: false,
+ secure: false,
+ session: true,
+ },
+ ]);
+ });
+});
diff --git a/remote/test/puppeteer/test/dialog.spec.ts b/remote/test/puppeteer/test/dialog.spec.ts
new file mode 100644
index 0000000000..0064020fff
--- /dev/null
+++ b/remote/test/puppeteer/test/dialog.spec.ts
@@ -0,0 +1,73 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import expect from 'expect';
+import sinon from 'sinon';
+
+import {
+ getTestState,
+ setupTestPageAndContextHooks,
+ setupTestBrowserHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('Page.Events.Dialog', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ it('should fire', async () => {
+ const { page } = getTestState();
+
+ const onDialog = sinon.stub().callsFake((dialog) => {
+ dialog.accept();
+ });
+ page.on('dialog', onDialog);
+
+ await page.evaluate(() => 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 } = getTestState();
+
+ const onDialog = sinon.stub().callsFake((dialog) => {
+ dialog.accept('answer!');
+ });
+ page.on('dialog', onDialog);
+
+ const result = await page.evaluate(() => 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 } = getTestState();
+
+ page.on('dialog', (dialog) => {
+ dialog.dismiss();
+ });
+ const result = await page.evaluate(() => prompt('question?'));
+ expect(result).toBe(null);
+ });
+});
diff --git a/remote/test/puppeteer/test/diffstyle.css b/remote/test/puppeteer/test/diffstyle.css
new file mode 100644
index 0000000000..c58f0e90a6
--- /dev/null
+++ b/remote/test/puppeteer/test/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/elementhandle.spec.ts b/remote/test/puppeteer/test/elementhandle.spec.ts
new file mode 100644
index 0000000000..617ff8e0ec
--- /dev/null
+++ b/remote/test/puppeteer/test/elementhandle.spec.ts
@@ -0,0 +1,449 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import sinon from 'sinon';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+import utils from './utils.js';
+import { ElementHandle } from '../lib/cjs/puppeteer/common/JSHandle.js';
+
+describe('ElementHandle specs', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ describe('ElementHandle.boundingBox', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.setViewport({ width: 500, height: 500 });
+ await page.goto(server.PREFIX + '/grid.html');
+ const 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 } = getTestState();
+
+ await page.setViewport({ width: 500, height: 500 });
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ const nestedFrame = page.frames()[1].childFrames()[1];
+ const 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 } = getTestState();
+
+ await page.setContent('<div style="display:none">hi</div>');
+ const element = await page.$('div');
+ expect(await element.boundingBox()).toBe(null);
+ });
+ it('should force a layout', async () => {
+ const { page } = getTestState();
+
+ await page.setViewport({ width: 500, height: 500 });
+ await page.setContent(
+ '<div style="width: 100px; height: 100px">hello</div>'
+ );
+ const elementHandle = await page.$('div');
+ await page.evaluate<(element: HTMLElement) => void>(
+ (element) => (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 } = 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>
+ `);
+ const element = await page.$('#therect');
+ const pptrBoundingBox = await element.boundingBox();
+ const webBoundingBox = await page.evaluate((e: HTMLElement) => {
+ 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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/resetcss.html');
+
+ // Step 1: Add Frame and position it absolutely.
+ await utils.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];
+ const 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 + dif.paddingLeft
+ y: 2 + 5,
+ });
+ });
+
+ it('should return null for invisible elements', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<div style="display:none">hi</div>');
+ const element = await page.$('div');
+ expect(await element.boxModel()).toBe(null);
+ });
+ });
+
+ describe('ElementHandle.contentFrame', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const elementHandle = await page.$('#frame1');
+ const frame = await elementHandle.contentFrame();
+ expect(frame).toBe(page.frames()[1]);
+ });
+ });
+
+ describe('ElementHandle.click', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ const button = await page.$('button');
+ await button.click();
+ expect(await page.evaluate(() => globalThis.result)).toBe('Clicked');
+ });
+ it('should work for Shadow DOM v1', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/shadow.html');
+ const buttonHandle = await page.evaluateHandle<ElementHandle>(
+ // @ts-expect-error button is expected to be in the page's scope.
+ () => button
+ );
+ await buttonHandle.click();
+ expect(
+ await page.evaluate(
+ // @ts-expect-error clicked is expected to be in the page's scope.
+ () => clicked
+ )
+ ).toBe(true);
+ });
+ it('should work for TextNodes', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ const buttonTextNode = await page.evaluateHandle<ElementHandle>(
+ () => document.querySelector('button').firstChild
+ );
+ let error = null;
+ await buttonTextNode.click().catch((error_) => (error = error_));
+ expect(error.message).toBe('Node is not of type HTMLElement');
+ });
+ it('should throw for detached nodes', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ const button = await page.$('button');
+ await page.evaluate((button: HTMLElement) => button.remove(), button);
+ let error = null;
+ await button.click().catch((error_) => (error = error_));
+ expect(error.message).toBe('Node is detached from document');
+ });
+ it('should throw for hidden nodes', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ const button = await page.$('button');
+ await page.evaluate(
+ (button: HTMLElement) => (button.style.display = 'none'),
+ button
+ );
+ const error = await button.click().catch((error_) => error_);
+ expect(error.message).toBe(
+ 'Node is either not visible or not an HTMLElement'
+ );
+ });
+ it('should throw for recursively hidden nodes', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/button.html');
+ const button = await page.$('button');
+ await page.evaluate(
+ (button: HTMLElement) => (button.parentElement.style.display = 'none'),
+ button
+ );
+ const error = await button.click().catch((error_) => error_);
+ expect(error.message).toBe(
+ 'Node is either not visible or not an HTMLElement'
+ );
+ });
+ it('should throw for <br> elements', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('hello<br>goodbye');
+ const br = await page.$('br');
+ const error = await br.click().catch((error_) => error_);
+ expect(error.message).toBe(
+ 'Node is either not visible or not an HTMLElement'
+ );
+ });
+ });
+
+ describe('ElementHandle.hover', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ const button = await page.$('#button-6');
+ await button.hover();
+ expect(
+ await page.evaluate(() => document.querySelector('button:hover').id)
+ ).toBe('button-6');
+ });
+ });
+
+ describe('ElementHandle.isIntersectingViewport', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/offscreenbuttons.html');
+ for (let i = 0; i < 11; ++i) {
+ const button = await page.$('#btn' + i);
+ // All but last button are visible.
+ const visible = i < 10;
+ expect(await button.isIntersectingViewport()).toBe(visible);
+ }
+ });
+ });
+
+ describe('Custom queries', function () {
+ this.afterEach(() => {
+ const { puppeteer } = getTestState();
+ puppeteer.clearCustomQueryHandlers();
+ });
+ it('should register and unregister', async () => {
+ const { page, puppeteer } = getTestState();
+ await page.setContent('<div id="not-foo"></div><div id="foo"></div>');
+
+ // Register.
+ puppeteer.registerCustomQueryHandler('getById', {
+ queryOne: (element, selector) =>
+ document.querySelector(`[id="${selector}"]`),
+ });
+ const element = await page.$('getById/foo');
+ expect(
+ await page.evaluate<(element: HTMLElement) => string>(
+ (element) => 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).toStrictEqual(
+ new Error(
+ 'Query set to use "getById", but no query handler of that name was found'
+ )
+ );
+ }
+ const handlerNamesAfterUnregistering = puppeteer.customQueryHandlerNames();
+ expect(handlerNamesAfterUnregistering.includes('getById')).toBeFalsy();
+ });
+ it('should throw with invalid query names', () => {
+ try {
+ const { puppeteer } = getTestState();
+ puppeteer.registerCustomQueryHandler('1/2/3', {
+ queryOne: () => 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, puppeteer } = 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) =>
+ document.querySelectorAll(`.${selector}`),
+ });
+ const elements = await page.$$('getByClass/foo');
+ const classNames = await Promise.all(
+ elements.map(
+ async (element) =>
+ await page.evaluate<(element: HTMLElement) => string>(
+ (element) => element.className,
+ element
+ )
+ )
+ );
+
+ expect(classNames).toStrictEqual(['foo', 'foo baz']);
+ });
+ it('should eval correctly', async () => {
+ const { page, puppeteer } = 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) =>
+ document.querySelectorAll(`.${selector}`),
+ });
+ const elements = await page.$$eval(
+ 'getByClass/foo',
+ (divs) => divs.length
+ );
+
+ expect(elements).toBe(2);
+ });
+ it('should wait correctly with waitForSelector', async () => {
+ const { page, puppeteer } = getTestState();
+ puppeteer.registerCustomQueryHandler('getByClass', {
+ queryOne: (element, selector) => element.querySelector(`.${selector}`),
+ });
+ const waitFor = page.waitForSelector('getByClass/foo');
+
+ // 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;
+
+ expect(element).toBeDefined();
+ });
+
+ 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, puppeteer } = getTestState();
+ puppeteer.registerCustomQueryHandler('getByClass', {
+ queryOne: (element, selector) => element.querySelector(`.${selector}`),
+ });
+ const waitFor = page.waitFor('getByClass/foo');
+
+ // 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;
+
+ expect(element).toBeDefined();
+ });
+ it('should work when both queryOne and queryAll are registered', async () => {
+ const { page, puppeteer } = 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) => element.querySelector(`.${selector}`),
+ queryAll: (element, selector) =>
+ element.querySelectorAll(`.${selector}`),
+ });
+
+ const element = await page.$('getByClass/foo');
+ expect(element).toBeDefined();
+
+ const elements = await page.$$('getByClass/foo');
+ expect(elements.length).toBe(3);
+ });
+ it('should eval when both queryOne and queryAll are registered', async () => {
+ const { page, puppeteer } = 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) => element.querySelector(`.${selector}`),
+ queryAll: (element, selector) =>
+ element.querySelectorAll(`.${selector}`),
+ });
+
+ const txtContent = await page.$eval(
+ 'getByClass/foo',
+ (div) => div.textContent
+ );
+ expect(txtContent).toBe('text');
+
+ const txtContents = await page.$$eval('getByClass/foo', (divs) =>
+ divs.map((d) => d.textContent).join('')
+ );
+ expect(txtContents).toBe('textcontent');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/emulation.spec.ts b/remote/test/puppeteer/test/emulation.spec.ts
new file mode 100644
index 0000000000..f594127a63
--- /dev/null
+++ b/remote/test/puppeteer/test/emulation.spec.ts
@@ -0,0 +1,355 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('Emulation', () => {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+ let iPhone;
+ let iPhoneLandscape;
+
+ before(() => {
+ const { puppeteer } = getTestState();
+ iPhone = puppeteer.devices['iPhone 6'];
+ iPhoneLandscape = puppeteer.devices['iPhone 6 landscape'];
+ });
+
+ describe('Page.viewport', function () {
+ it('should get the proper viewport size', async () => {
+ const { page } = 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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ expect(await page.evaluate(() => window.innerWidth)).toBe(800);
+ await page.setViewport(iPhone.viewport);
+ expect(await page.evaluate(() => window.innerWidth)).toBe(375);
+ await page.setViewport({ width: 400, height: 300 });
+ expect(await page.evaluate(() => window.innerWidth)).toBe(400);
+ });
+ it('should support touch emulation', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false);
+ await page.setViewport(iPhone.viewport);
+ expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true);
+ expect(await page.evaluate(dispatchTouch)).toBe('Received touch');
+ await page.setViewport({ width: 100, height: 100 });
+ expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false);
+
+ function dispatchTouch() {
+ let fulfill;
+ 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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/detect-touch.html');
+ expect(await page.evaluate(() => document.body.textContent.trim())).toBe(
+ 'NO'
+ );
+ await page.setViewport(iPhone.viewport);
+ await page.goto(server.PREFIX + '/detect-touch.html');
+ expect(await page.evaluate(() => document.body.textContent.trim())).toBe(
+ 'YES'
+ );
+ });
+ it('should detect touch when applying viewport with touches', async () => {
+ const { page, server } = getTestState();
+
+ await page.setViewport({ width: 800, height: 600, hasTouch: true });
+ await page.addScriptTag({ url: server.PREFIX + '/modernizr.js' });
+ expect(await page.evaluate(() => globalThis.Modernizr.touchevents)).toBe(
+ true
+ );
+ });
+ it('should support landscape emulation', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ expect(await page.evaluate(() => screen.orientation.type)).toBe(
+ 'portrait-primary'
+ );
+ await page.setViewport(iPhoneLandscape.viewport);
+ expect(await page.evaluate(() => screen.orientation.type)).toBe(
+ 'landscape-primary'
+ );
+ await page.setViewport({ width: 100, height: 100 });
+ expect(await page.evaluate(() => screen.orientation.type)).toBe(
+ 'portrait-primary'
+ );
+ });
+ });
+
+ describe('Page.emulate', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ await page.emulate(iPhone);
+ expect(await page.evaluate(() => window.innerWidth)).toBe(375);
+ expect(await page.evaluate(() => navigator.userAgent)).toContain(
+ 'iPhone'
+ );
+ });
+ it('should support clicking', async () => {
+ const { page, server } = getTestState();
+
+ await page.emulate(iPhone);
+ await page.goto(server.PREFIX + '/input/button.html');
+ const button = await page.$('button');
+ await page.evaluate(
+ (button: HTMLElement) => (button.style.marginTop = '200px'),
+ button
+ );
+ await button.click();
+ expect(await page.evaluate(() => globalThis.result)).toBe('Clicked');
+ });
+ });
+
+ describe('Page.emulateMediaType', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(
+ true
+ );
+ expect(await page.evaluate(() => matchMedia('print').matches)).toBe(
+ false
+ );
+ await page.emulateMediaType('print');
+ expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(
+ false
+ );
+ expect(await page.evaluate(() => matchMedia('print').matches)).toBe(true);
+ await page.emulateMediaType(null);
+ expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(
+ true
+ );
+ expect(await page.evaluate(() => matchMedia('print').matches)).toBe(
+ false
+ );
+ });
+ it('should throw in case of bad argument', async () => {
+ const { page } = getTestState();
+
+ let error = null;
+ await page.emulateMediaType('bad').catch((error_) => (error = error_));
+ expect(error.message).toBe('Unsupported media type: bad');
+ });
+ });
+
+ describe('Page.emulateMediaFeatures', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ await page.emulateMediaFeatures([
+ { name: 'prefers-reduced-motion', value: 'reduce' },
+ ]);
+ expect(
+ await page.evaluate(
+ () => matchMedia('(prefers-reduced-motion: reduce)').matches
+ )
+ ).toBe(true);
+ expect(
+ await page.evaluate(
+ () => matchMedia('(prefers-reduced-motion: no-preference)').matches
+ )
+ ).toBe(false);
+ await page.emulateMediaFeatures([
+ { name: 'prefers-color-scheme', value: 'light' },
+ ]);
+ expect(
+ await page.evaluate(
+ () => matchMedia('(prefers-color-scheme: light)').matches
+ )
+ ).toBe(true);
+ expect(
+ await page.evaluate(
+ () => matchMedia('(prefers-color-scheme: dark)').matches
+ )
+ ).toBe(false);
+ await page.emulateMediaFeatures([
+ { name: 'prefers-color-scheme', value: 'dark' },
+ ]);
+ expect(
+ await page.evaluate(
+ () => matchMedia('(prefers-color-scheme: dark)').matches
+ )
+ ).toBe(true);
+ expect(
+ await page.evaluate(
+ () => 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(
+ () => matchMedia('(prefers-reduced-motion: reduce)').matches
+ )
+ ).toBe(true);
+ expect(
+ await page.evaluate(
+ () => matchMedia('(prefers-reduced-motion: no-preference)').matches
+ )
+ ).toBe(false);
+ expect(
+ await page.evaluate(
+ () => matchMedia('(prefers-color-scheme: light)').matches
+ )
+ ).toBe(true);
+ expect(
+ await page.evaluate(
+ () => matchMedia('(prefers-color-scheme: dark)').matches
+ )
+ ).toBe(false);
+ });
+ it('should throw in case of bad argument', async () => {
+ const { page } = getTestState();
+
+ let error = null;
+ await page
+ .emulateMediaFeatures([{ name: 'bad', value: '' }])
+ .catch((error_) => (error = error_));
+ expect(error.message).toBe('Unsupported media feature: bad');
+ });
+ });
+
+ describe('Page.emulateTimezone', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ await page.evaluate(() => {
+ globalThis.date = new Date(1479579154987);
+ });
+ await page.emulateTimezone('America/Jamaica');
+ expect(await page.evaluate(() => globalThis.date.toString())).toBe(
+ 'Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)'
+ );
+
+ await page.emulateTimezone('Pacific/Honolulu');
+ expect(await page.evaluate(() => globalThis.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(() => globalThis.date.toString())).toBe(
+ 'Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)'
+ );
+
+ await page.emulateTimezone('Europe/Berlin');
+ expect(await page.evaluate(() => globalThis.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 } = getTestState();
+
+ let error = null;
+ await page.emulateTimezone('Foo/Bar').catch((error_) => (error = error_));
+ expect(error.message).toBe('Invalid timezone ID: Foo/Bar');
+ await page.emulateTimezone('Baz/Qux').catch((error_) => (error = error_));
+ expect(error.message).toBe('Invalid timezone ID: Baz/Qux');
+ });
+ });
+
+ describe('Page.emulateVisionDeficiency', function () {
+ it('should work', async () => {
+ const { page, server } = 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 } = getTestState();
+
+ let error = null;
+ await page
+ // @ts-expect-error deliberately passign invalid deficiency
+ .emulateVisionDeficiency('invalid')
+ .catch((error_) => (error = error_));
+ expect(error.message).toBe('Unsupported vision deficiency: invalid');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/evaluation.spec.ts b/remote/test/puppeteer/test/evaluation.spec.ts
new file mode 100644
index 0000000000..3e9423728a
--- /dev/null
+++ b/remote/test/puppeteer/test/evaluation.spec.ts
@@ -0,0 +1,474 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import utils from './utils.js';
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+const bigint = typeof BigInt !== 'undefined';
+
+describe('Evaluation specs', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ describe('Page.evaluate', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate(() => 7 * 3);
+ expect(result).toBe(21);
+ });
+ (bigint ? it : xit)('should transfer BigInt', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate((a: BigInt) => a, BigInt(42));
+ expect(result).toBe(BigInt(42));
+ });
+ it('should transfer NaN', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate((a) => a, NaN);
+ expect(Object.is(result, NaN)).toBe(true);
+ });
+ it('should transfer -0', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate((a) => a, -0);
+ expect(Object.is(result, -0)).toBe(true);
+ });
+ it('should transfer Infinity', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate((a) => a, Infinity);
+ expect(Object.is(result, Infinity)).toBe(true);
+ });
+ it('should transfer -Infinity', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate((a) => a, -Infinity);
+ expect(Object.is(result, -Infinity)).toBe(true);
+ });
+ it('should transfer arrays', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate((a) => a, [1, 2, 3]);
+ expect(result).toEqual([1, 2, 3]);
+ });
+ it('should transfer arrays as arrays, not objects', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate((a) => Array.isArray(a), [1, 2, 3]);
+ expect(result).toBe(true);
+ });
+ it('should modify global environment', async () => {
+ const { page } = getTestState();
+
+ await page.evaluate(() => (globalThis.globalVar = 123));
+ expect(await page.evaluate('globalVar')).toBe(123);
+ });
+ it('should evaluate in the page context', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/global-var.html');
+ expect(await page.evaluate('globalVar')).toBe(123);
+ });
+ it(
+ 'should return undefined for objects with symbols',
+ async () => {
+ const { page } = getTestState();
+
+ expect(await page.evaluate(() => [Symbol('foo4')])).toBe(undefined);
+ }
+ );
+ it('should work with function shorthands', async () => {
+ const { page } = getTestState();
+
+ const a = {
+ sum(a, b) {
+ return a + b;
+ },
+
+ async mult(a, b) {
+ 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 } = getTestState();
+
+ const result = await page.evaluate((a) => a['中文字符'], {
+ 中文字符: 42,
+ });
+ expect(result).toBe(42);
+ });
+ it('should throw when evaluation triggers reload', async () => {
+ const { page } = getTestState();
+
+ let error = null;
+ await page
+ .evaluate(() => {
+ location.reload();
+ return new Promise(() => {});
+ })
+ .catch((error_) => (error = error_));
+ expect(error.message).toContain('Protocol error');
+ });
+ it('should await promise', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate(() => Promise.resolve(8 * 7));
+ expect(result).toBe(56);
+ });
+ it('should work right after framenavigated', async () => {
+ const { page, server } = getTestState();
+
+ let frameEvaluation = null;
+ page.on('framenavigated', async (frame) => {
+ frameEvaluation = frame.evaluate(() => 6 * 7);
+ });
+ await page.goto(server.EMPTY_PAGE);
+ expect(await frameEvaluation).toBe(42);
+ });
+ it('should work from-inside an exposed function', async () => {
+ const { page } = getTestState();
+
+ // Setup inpage callback, which calls Page.evaluate
+ await page.exposeFunction('callController', async function (a, b) {
+ return await page.evaluate<(a: number, b: number) => number>(
+ (a, b) => a * b,
+ a,
+ b
+ );
+ });
+ const result = await page.evaluate(async function () {
+ return await globalThis.callController(9, 3);
+ });
+ expect(result).toBe(27);
+ });
+ it('should reject promise with exception', async () => {
+ const { page } = getTestState();
+
+ let error = null;
+ await page
+ // @ts-expect-error we know the object doesn't exist
+ .evaluate(() => notExistingObject.property)
+ .catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ expect(error.message).toContain('notExistingObject');
+ });
+ it('should support thrown strings as error messages', async () => {
+ const { page } = getTestState();
+
+ let error = null;
+ await page
+ .evaluate(() => {
+ throw 'qwerty';
+ })
+ .catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ expect(error.message).toContain('qwerty');
+ });
+ it('should support thrown numbers as error messages', async () => {
+ const { page } = getTestState();
+
+ let error = null;
+ await page
+ .evaluate(() => {
+ throw 100500;
+ })
+ .catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ expect(error.message).toContain('100500');
+ });
+ it('should return complex objects', async () => {
+ const { page } = getTestState();
+
+ const object = { foo: 'bar!' };
+ const result = await page.evaluate((a) => a, object);
+ expect(result).not.toBe(object);
+ expect(result).toEqual(object);
+ });
+ (bigint ? it : xit)('should return BigInt', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate(() => BigInt(42));
+ expect(result).toBe(BigInt(42));
+ });
+ it('should return NaN', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate(() => NaN);
+ expect(Object.is(result, NaN)).toBe(true);
+ });
+ it('should return -0', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate(() => -0);
+ expect(Object.is(result, -0)).toBe(true);
+ });
+ it('should return Infinity', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate(() => Infinity);
+ expect(Object.is(result, Infinity)).toBe(true);
+ });
+ it('should return -Infinity', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate(() => -Infinity);
+ expect(Object.is(result, -Infinity)).toBe(true);
+ });
+ it('should accept "undefined" as one of multiple parameters', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate(
+ (a, b) => Object.is(a, undefined) && Object.is(b, 'foo'),
+ undefined,
+ 'foo'
+ );
+ expect(result).toBe(true);
+ });
+ it('should properly serialize null fields', async () => {
+ const { page } = getTestState();
+
+ expect(await page.evaluate(() => ({ a: undefined }))).toEqual({});
+ });
+ it(
+ 'should return undefined for non-serializable objects',
+ async () => {
+ const { page } = getTestState();
+
+ expect(await page.evaluate(() => window)).toBe(undefined);
+ }
+ );
+ it('should fail for circular object', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate(() => {
+ const a: { [x: string]: any } = {};
+ const b = { a };
+ a.b = b;
+ return a;
+ });
+ expect(result).toBe(undefined);
+ });
+ it('should be able to throw a tricky error', async () => {
+ const { page } = getTestState();
+
+ const windowHandle = await page.evaluateHandle(() => window);
+ const errorText = await windowHandle
+ .jsonValue()
+ .catch((error_) => error_.message);
+ const error = await page
+ .evaluate<(errorText: string) => Error>((errorText) => {
+ throw new Error(errorText);
+ }, errorText)
+ .catch((error_) => error_);
+ expect(error.message).toContain(errorText);
+ });
+ it('should accept a string', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate('1 + 2');
+ expect(result).toBe(3);
+ });
+ it('should accept a string with semi colons', async () => {
+ const { page } = getTestState();
+
+ const result = await page.evaluate('1 + 5;');
+ expect(result).toBe(6);
+ });
+ it('should accept a string with comments', async () => {
+ const { page } = 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 } = getTestState();
+
+ await page.setContent('<section>42</section>');
+ const element = await page.$('section');
+ const text = await page.evaluate<(e: HTMLElement) => string>(
+ (e) => e.textContent,
+ element
+ );
+ expect(text).toBe('42');
+ });
+ it('should throw if underlying element was disposed', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<section>39</section>');
+ const element = await page.$('section');
+ expect(element).toBeTruthy();
+ await element.dispose();
+ let error = null;
+ await page
+ .evaluate((e: HTMLElement) => e.textContent, element)
+ .catch((error_) => (error = error_));
+ expect(error.message).toContain('JSHandle is disposed');
+ });
+ it(
+ 'should throw if elementHandles are from other frames',
+ async () => {
+ const { page, server } = getTestState();
+
+ await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const bodyHandle = await page.frames()[1].$('body');
+ let error = null;
+ await page
+ .evaluate((body: HTMLElement) => body.innerHTML, bodyHandle)
+ .catch((error_) => (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 } = 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 throw a nice error after a navigation', async () => {
+ const { page } = getTestState();
+
+ const executionContext = await page.mainFrame().executionContext();
+
+ await Promise.all([
+ page.waitForNavigation(),
+ executionContext.evaluate(() => window.location.reload()),
+ ]);
+ const error = await executionContext
+ .evaluate(() => null)
+ .catch((error_) => error_);
+ expect((error as Error).message).toContain('navigation');
+ });
+ it(
+ 'should not throw an error when evaluation does a navigation',
+ async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/one-style.html');
+ const result = await page.evaluate(() => {
+ (window as any).location = '/empty.html';
+ return [42];
+ });
+ expect(result).toEqual([42]);
+ }
+ );
+ it('should transfer 100Mb of data from page to node.js', async function () {
+ const { page } = getTestState();
+
+ const a = await page.evaluate<() => string>(() =>
+ 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 } = getTestState();
+
+ let error = null;
+ await page
+ .evaluate(
+ () =>
+ new Promise(() => {
+ throw new Error('Error in promise');
+ })
+ )
+ .catch((error_) => (error = error_));
+ expect(error.message).toContain('Error in promise');
+ });
+ });
+
+ describe('Page.evaluateOnNewDocument', function () {
+ it('should evaluate before anything else on the page', async () => {
+ const { page, server } = getTestState();
+
+ await page.evaluateOnNewDocument(function () {
+ globalThis.injected = 123;
+ });
+ await page.goto(server.PREFIX + '/tamperable.html');
+ expect(await page.evaluate(() => globalThis.result)).toBe(123);
+ });
+ it('should work with CSP', async () => {
+ const { page, server } = getTestState();
+
+ server.setCSP('/empty.html', 'script-src ' + server.PREFIX);
+ await page.evaluateOnNewDocument(function () {
+ globalThis.injected = 123;
+ });
+ await page.goto(server.PREFIX + '/empty.html');
+ expect(await page.evaluate(() => globalThis.injected)).toBe(123);
+
+ // Make sure CSP works.
+ await page
+ .addScriptTag({ content: 'window.e = 10;' })
+ .catch((error) => void error);
+ expect(await page.evaluate(() => (window as any).e)).toBe(undefined);
+ });
+ });
+
+ describe('Frame.evaluate', function () {
+ it('should have different execution contexts', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(page.frames().length).toBe(2);
+ await page.frames()[0].evaluate(() => (globalThis.FOO = 'foo'));
+ await page.frames()[1].evaluate(() => (globalThis.FOO = 'bar'));
+ expect(await page.frames()[0].evaluate(() => globalThis.FOO)).toBe('foo');
+ expect(await page.frames()[1].evaluate(() => globalThis.FOO)).toBe('bar');
+ });
+ it('should have correct execution contexts', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame.html');
+ expect(page.frames().length).toBe(2);
+ expect(
+ await page.frames()[0].evaluate(() => document.body.textContent.trim())
+ ).toBe('');
+ expect(
+ await page.frames()[1].evaluate(() => document.body.textContent.trim())
+ ).toBe(`Hi, I'm frame`);
+ });
+ it('should execute after cross-site navigation', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const mainFrame = page.mainFrame();
+ expect(await mainFrame.evaluate(() => window.location.href)).toContain(
+ 'localhost'
+ );
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
+ expect(await mainFrame.evaluate(() => window.location.href)).toContain(
+ '127'
+ );
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/fixtures.spec.ts b/remote/test/puppeteer/test/fixtures.spec.ts
new file mode 100644
index 0000000000..8eca362071
--- /dev/null
+++ b/remote/test/puppeteer/test/fixtures.spec.ts
@@ -0,0 +1,93 @@
+/**
+ * Copyright 2019 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable @typescript-eslint/no-var-requires */
+
+import expect from 'expect';
+import { getTestState, itChromeOnly } from './mocha-utils'; // eslint-disable-line import/extensions
+
+import path from 'path';
+
+describe('Fixtures', function () {
+ itChromeOnly('dumpio option should work with pipe option ', async () => {
+ const { defaultBrowserOptions, puppeteerPath } = getTestState();
+
+ let dumpioData = '';
+ const { spawn } = require('child_process');
+ 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) => res.on('close', resolve));
+ expect(dumpioData).toContain('message from dumpio');
+ });
+ it('should dump browser process stderr', async () => {
+ const { defaultBrowserOptions, puppeteerPath } = getTestState();
+
+ let dumpioData = '';
+ const { spawn } = require('child_process');
+ 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) => 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 } = getTestState();
+
+ const { spawn, execSync } = require('child_process');
+ 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 wsEndPointCallback;
+ 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 = [
+ new Promise((resolve) => browser.once('disconnected', resolve)),
+ new Promise((resolve) => res.on('close', resolve)),
+ ];
+ if (process.platform === 'win32')
+ execSync(`taskkill /pid ${res.pid} /T /F`);
+ else process.kill(res.pid);
+ await Promise.all(promises);
+ });
+});
diff --git a/remote/test/puppeteer/test/fixtures/closeme.js b/remote/test/puppeteer/test/fixtures/closeme.js
new file mode 100644
index 0000000000..dbe798f70d
--- /dev/null
+++ b/remote/test/puppeteer/test/fixtures/closeme.js
@@ -0,0 +1,5 @@
+(async () => {
+ const [, , puppeteerRoot, options] = process.argv;
+ const browser = await require(puppeteerRoot).launch(JSON.parse(options));
+ console.log(browser.wsEndpoint());
+})();
diff --git a/remote/test/puppeteer/test/fixtures/dumpio.js b/remote/test/puppeteer/test/fixtures/dumpio.js
new file mode 100644
index 0000000000..40b9714f6c
--- /dev/null
+++ b/remote/test/puppeteer/test/fixtures/dumpio.js
@@ -0,0 +1,8 @@
+(async () => {
+ const [, , puppeteerRoot, options] = process.argv;
+ const browser = await require(puppeteerRoot).launch(JSON.parse(options));
+ const page = await browser.newPage();
+ await page.evaluate(() => console.error('message from dumpio'));
+ await page.close();
+ await browser.close();
+})();
diff --git a/remote/test/puppeteer/test/frame.spec.ts b/remote/test/puppeteer/test/frame.spec.ts
new file mode 100644
index 0000000000..6f226c12ea
--- /dev/null
+++ b/remote/test/puppeteer/test/frame.spec.ts
@@ -0,0 +1,269 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import utils from './utils.js';
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('Frame specs', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ describe('Frame.executionContext', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(page.frames().length).toBe(2);
+ const [frame1, frame2] = page.frames();
+ const context1 = await frame1.executionContext();
+ const context2 = await frame2.executionContext();
+ expect(context1).toBeTruthy();
+ expect(context2).toBeTruthy();
+ expect(context1 !== context2).toBeTruthy();
+ expect(context1.frame()).toBe(frame1);
+ expect(context2.frame()).toBe(frame2);
+
+ await Promise.all([
+ context1.evaluate(() => (globalThis.a = 1)),
+ context2.evaluate(() => (globalThis.a = 2)),
+ ]);
+ const [a1, a2] = await Promise.all([
+ context1.evaluate(() => globalThis.a),
+ context2.evaluate(() => globalThis.a),
+ ]);
+ expect(a1).toBe(1);
+ expect(a2).toBe(2);
+ });
+ });
+
+ describe('Frame.evaluateHandle', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const mainFrame = page.mainFrame();
+ const windowHandle = await mainFrame.evaluateHandle(() => window);
+ expect(windowHandle).toBeTruthy();
+ });
+ });
+
+ describe('Frame.evaluate', function () {
+ it('should throw for detached frames', async () => {
+ const { page, server } = getTestState();
+
+ const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await utils.detachFrame(page, 'frame1');
+ let error = null;
+ await frame1.evaluate(() => 7 * 8).catch((error_) => (error = error_));
+ expect(error.message).toContain(
+ 'Execution context is not available in detached frame'
+ );
+ });
+ });
+
+ describe('Frame Management', function () {
+ it('should handle nested frames', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ expect(utils.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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ // validate frameattached events
+ const attachedFrames = [];
+ page.on('frameattached', (frame) => attachedFrames.push(frame));
+ await utils.attachFrame(page, 'frame1', './assets/frame.html');
+ expect(attachedFrames.length).toBe(1);
+ expect(attachedFrames[0].url()).toContain('/assets/frame.html');
+
+ // validate framenavigated events
+ const navigatedFrames = [];
+ page.on('framenavigated', (frame) => navigatedFrames.push(frame));
+ await utils.navigateFrame(page, 'frame1', './empty.html');
+ expect(navigatedFrames.length).toBe(1);
+ expect(navigatedFrames[0].url()).toBe(server.EMPTY_PAGE);
+
+ // validate framedetached events
+ const detachedFrames = [];
+ page.on('framedetached', (frame) => detachedFrames.push(frame));
+ await utils.detachFrame(page, 'frame1');
+ expect(detachedFrames.length).toBe(1);
+ expect(detachedFrames[0].isDetached()).toBe(true);
+ }
+ );
+ it(
+ 'should send "framenavigated" when navigating on anchor URLs',
+ async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await Promise.all([
+ page.goto(server.EMPTY_PAGE + '#foo'),
+ utils.waitEvent(page, 'framenavigated'),
+ ]);
+ expect(page.url()).toBe(server.EMPTY_PAGE + '#foo');
+ }
+ );
+ it('should persist mainFrame on cross-process navigation', async () => {
+ const { page, server } = 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 } = getTestState();
+
+ let hasEvents = false;
+ page.on('frameattached', () => (hasEvents = true));
+ page.on('framedetached', () => (hasEvents = true));
+ await page.goto(server.EMPTY_PAGE);
+ expect(hasEvents).toBe(false);
+ });
+ it('should detach child frames on navigation', async () => {
+ const { page, server } = getTestState();
+
+ let attachedFrames = [];
+ let detachedFrames = [];
+ let navigatedFrames = [];
+ page.on('frameattached', (frame) => attachedFrames.push(frame));
+ page.on('framedetached', (frame) => detachedFrames.push(frame));
+ page.on('framenavigated', (frame) => navigatedFrames.push(frame));
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ expect(attachedFrames.length).toBe(4);
+ expect(detachedFrames.length).toBe(0);
+ expect(navigatedFrames.length).toBe(5);
+
+ attachedFrames = [];
+ detachedFrames = [];
+ navigatedFrames = [];
+ await page.goto(server.EMPTY_PAGE);
+ expect(attachedFrames.length).toBe(0);
+ expect(detachedFrames.length).toBe(4);
+ expect(navigatedFrames.length).toBe(1);
+ });
+ it('should support framesets', async () => {
+ const { page, server } = getTestState();
+
+ let attachedFrames = [];
+ let detachedFrames = [];
+ let navigatedFrames = [];
+ page.on('frameattached', (frame) => attachedFrames.push(frame));
+ page.on('framedetached', (frame) => detachedFrames.push(frame));
+ page.on('framenavigated', (frame) => navigatedFrames.push(frame));
+ await page.goto(server.PREFIX + '/frames/frameset.html');
+ expect(attachedFrames.length).toBe(4);
+ expect(detachedFrames.length).toBe(0);
+ expect(navigatedFrames.length).toBe(5);
+
+ attachedFrames = [];
+ detachedFrames = [];
+ navigatedFrames = [];
+ await page.goto(server.EMPTY_PAGE);
+ expect(attachedFrames.length).toBe(0);
+ expect(detachedFrames.length).toBe(4);
+ expect(navigatedFrames.length).toBe(1);
+ });
+ it('should report frame from-inside shadow DOM', async () => {
+ const { page, server } = 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) => (frame.onload = x));
+ }, server.EMPTY_PAGE);
+ expect(page.frames().length).toBe(2);
+ expect(page.frames()[1].url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should report frame.name()', async () => {
+ const { page, server } = getTestState();
+
+ await utils.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) => (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 } = getTestState();
+
+ await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await utils.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 } = getTestState();
+
+ const frame1 = await utils.attachFrame(
+ page,
+ 'frame1',
+ server.EMPTY_PAGE
+ );
+ await page.evaluate(() => {
+ globalThis.frame = document.querySelector('#frame1');
+ globalThis.frame.remove();
+ });
+ expect(frame1.isDetached()).toBe(true);
+ const [frame2] = await Promise.all([
+ utils.waitEvent(page, 'frameattached'),
+ page.evaluate(() => document.body.appendChild(globalThis.frame)),
+ ]);
+ expect(frame2.isDetached()).toBe(false);
+ expect(frame1).not.toBe(frame2);
+ }
+ );
+ it('should support url fragment', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame-url-fragment.html');
+
+ expect(page.frames().length).toBe(2);
+ expect(page.frames()[1].url()).toBe(
+ server.PREFIX + '/frames/frame.html?param=value#fragment'
+ );
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/golden-chromium/csscoverage-involved.txt b/remote/test/puppeteer/test/golden-chromium/csscoverage-involved.txt
new file mode 100644
index 0000000000..9b851d0bd3
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/csscoverage-involved.txt
@@ -0,0 +1,16 @@
+[
+ {
+ "url": "http://localhost:<PORT>/csscoverage/involved.html",
+ "ranges": [
+ {
+ "start": 149,
+ "end": 297
+ },
+ {
+ "start": 327,
+ "end": 433
+ }
+ ],
+ "text": "\n@charset \"utf-8\";\n@namespace svg url(http://www.w3.org/2000/svg);\n@font-face {\n font-family: \"Example Font\";\n src: url(\"./Dosis-Regular.ttf\");\n}\n\n#fluffy {\n border: 1px solid black;\n z-index: 1;\n /* -webkit-disabled-property: rgb(1, 2, 3) */\n -lol-cats: \"dogs\" /* non-existing property */\n}\n\n@media (min-width: 1px) {\n span {\n -webkit-border-radius: 10px;\n font-family: \"Example Font\";\n animation: 1s identifier;\n }\n}\n"
+ }
+] \ No newline at end of file
diff --git a/remote/test/puppeteer/test/golden-chromium/grid-cell-0.png b/remote/test/puppeteer/test/golden-chromium/grid-cell-0.png
new file mode 100644
index 0000000000..ff282e989b
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/grid-cell-0.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/grid-cell-1.png b/remote/test/puppeteer/test/golden-chromium/grid-cell-1.png
new file mode 100644
index 0000000000..91a1cb8510
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/grid-cell-1.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/grid-cell-2.png b/remote/test/puppeteer/test/golden-chromium/grid-cell-2.png
new file mode 100644
index 0000000000..7b01753b6a
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/grid-cell-2.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/grid-cell-3.png b/remote/test/puppeteer/test/golden-chromium/grid-cell-3.png
new file mode 100644
index 0000000000..b9b8b2922b
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/grid-cell-3.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/jscoverage-involved.txt b/remote/test/puppeteer/test/golden-chromium/jscoverage-involved.txt
new file mode 100644
index 0000000000..6f28e1580e
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/jscoverage-involved.txt
@@ -0,0 +1,28 @@
+[
+ {
+ "url": "http://localhost:<PORT>/jscoverage/involved.html",
+ "ranges": [
+ {
+ "start": 0,
+ "end": 35
+ },
+ {
+ "start": 50,
+ "end": 100
+ },
+ {
+ "start": 107,
+ "end": 141
+ },
+ {
+ "start": 148,
+ "end": 160
+ },
+ {
+ "start": 168,
+ "end": 207
+ }
+ ],
+ "text": "\nfunction foo() {\n if (1 > 2)\n console.log(1);\n if (1 < 2)\n console.log(2);\n let x = 1 > 2 ? 'foo' : 'bar';\n let y = 1 < 2 ? 'foo' : 'bar';\n let z = () => {};\n let q = () => {};\n q();\n}\n\nfoo();\n"
+ }
+] \ No newline at end of file
diff --git a/remote/test/puppeteer/test/golden-chromium/mock-binary-response.png b/remote/test/puppeteer/test/golden-chromium/mock-binary-response.png
new file mode 100644
index 0000000000..8595e0598e
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/mock-binary-response.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-clip-odd-size.png b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-odd-size.png
new file mode 100644
index 0000000000..b010d1f87f
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-odd-size.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-clip-rect.png b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-rect.png
new file mode 100644
index 0000000000..ac23b7de50
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-rect.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-bounding-box.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-bounding-box.png
new file mode 100644
index 0000000000..32e05bf05b
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-bounding-box.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional-offset.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional-offset.png
new file mode 100644
index 0000000000..cc8669d598
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional-offset.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional.png
new file mode 100644
index 0000000000..35c53377f9
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-larger-than-viewport.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-larger-than-viewport.png
new file mode 100644
index 0000000000..5fcdb92355
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-larger-than-viewport.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-padding-border.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-padding-border.png
new file mode 100644
index 0000000000..917dd48188
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-padding-border.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-rotate.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-rotate.png
new file mode 100644
index 0000000000..52e2a0f6d3
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-rotate.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-scrolled-into-view.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-scrolled-into-view.png
new file mode 100644
index 0000000000..917dd48188
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-scrolled-into-view.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-grid-fullpage.png b/remote/test/puppeteer/test/golden-chromium/screenshot-grid-fullpage.png
new file mode 100644
index 0000000000..d6d38217f7
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/screenshot-grid-fullpage.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-offscreen-clip.png b/remote/test/puppeteer/test/golden-chromium/screenshot-offscreen-clip.png
new file mode 100644
index 0000000000..31a0935cda
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/screenshot-offscreen-clip.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-sanity.png b/remote/test/puppeteer/test/golden-chromium/screenshot-sanity.png
new file mode 100644
index 0000000000..ecab61fe17
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/screenshot-sanity.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/transparent.png b/remote/test/puppeteer/test/golden-chromium/transparent.png
new file mode 100644
index 0000000000..1cf45d8688
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/transparent.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-achromatopsia.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-achromatopsia.png
new file mode 100644
index 0000000000..4d74aac44c
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-achromatopsia.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-blurredVision.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-blurredVision.png
new file mode 100644
index 0000000000..d89858ef53
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-blurredVision.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-deuteranopia.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-deuteranopia.png
new file mode 100644
index 0000000000..79b4b0fa1b
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-deuteranopia.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-protanopia.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-protanopia.png
new file mode 100644
index 0000000000..bede7c1ed0
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-protanopia.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-tritanopia.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-tritanopia.png
new file mode 100644
index 0000000000..d5f6bbec2e
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-tritanopia.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-chromium/white.jpg b/remote/test/puppeteer/test/golden-chromium/white.jpg
new file mode 100644
index 0000000000..fb9070def3
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-chromium/white.jpg
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png b/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png
new file mode 100644
index 0000000000..4677bdbc4f
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png b/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png
new file mode 100644
index 0000000000..532dc8db65
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png
new file mode 100644
index 0000000000..8e86dc9017
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png
new file mode 100644
index 0000000000..7a74457869
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png
new file mode 100644
index 0000000000..f4e059c300
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png
new file mode 100644
index 0000000000..f554b1d62c
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png
new file mode 100644
index 0000000000..d1431bd91d
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png
new file mode 100644
index 0000000000..6d28cddcea
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png
new file mode 100644
index 0000000000..2b72c7528b
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png
new file mode 100644
index 0000000000..0a78fb1ae7
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png
new file mode 100644
index 0000000000..2b72c7528b
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png
new file mode 100644
index 0000000000..ac47ec83b1
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png
new file mode 100644
index 0000000000..31a0935cda
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png b/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png
new file mode 100644
index 0000000000..07890a04b3
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png
Binary files differ
diff --git a/remote/test/puppeteer/test/golden-utils.js b/remote/test/puppeteer/test/golden-utils.js
new file mode 100644
index 0000000000..f820afe6bf
--- /dev/null
+++ b/remote/test/puppeteer/test/golden-utils.js
@@ -0,0 +1,160 @@
+// @ts-nocheck
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const path = require('path');
+const fs = require('fs');
+const Diff = require('text-diff');
+const mime = require('mime');
+const PNG = require('pngjs').PNG;
+const jpeg = require('jpeg-js');
+const pixelmatch = require('pixelmatch');
+
+module.exports = { compare };
+
+const GoldenComparators = {
+ 'image/png': compareImages,
+ 'image/jpeg': compareImages,
+ 'text/plain': compareText,
+};
+
+/**
+ * @param {?Object} actualBuffer
+ * @param {!Buffer} expectedBuffer
+ * @param {!string} mimeType
+ * @returns {?{diff: (!Object:undefined), errorMessage: (string|undefined)}}
+ */
+function compareImages(actualBuffer, expectedBuffer, mimeType) {
+ if (!actualBuffer || !(actualBuffer instanceof Buffer))
+ return { errorMessage: 'Actual result should be Buffer.' };
+
+ 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) {
+ return {
+ errorMessage: `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) } : null;
+}
+
+/**
+ * @param {?Object} actual
+ * @param {!Buffer} expectedBuffer
+ * @returns {?{diff: (!Object:undefined), errorMessage: (string|undefined)}}
+ */
+function compareText(actual, expectedBuffer) {
+ if (typeof actual !== 'string')
+ return { errorMessage: 'Actual result should be string' };
+ const expected = expectedBuffer.toString('utf-8');
+ if (expected === actual) return null;
+ const diff = new Diff();
+ const result = diff.main(expected, actual);
+ diff.cleanupSemantic(result);
+ let html = diff.prettyHtml(result);
+ const diffStylePath = path.join(__dirname, 'diffstyle.css');
+ html = `<link rel="stylesheet" href="file://${diffStylePath}">` + html;
+ return {
+ diff: html,
+ diffExtension: '.html',
+ };
+}
+
+/**
+ * @param {?Object} actual
+ * @param {string} goldenName
+ * @returns {!{pass: boolean, message: (undefined|string)}}
+ */
+function compare(goldenPath, outputPath, actual, goldenName) {
+ 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);
+ const comparator = GoldenComparators[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.diff) {
+ const diffPath = addSuffix(actualPath, '-diff', result.diffExtension);
+ fs.writeFileSync(diffPath, result.diff);
+ }
+
+ let message = goldenName + ' mismatch!';
+ if (result.errorMessage) message += ' ' + result.errorMessage;
+ return {
+ pass: false,
+ message: message + ' ' + messageSuffix,
+ };
+
+ function ensureOutputDir() {
+ if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath);
+ }
+}
+
+/**
+ * @param {string} filePath
+ * @param {string} suffix
+ * @param {string=} customExtension
+ * @returns {string}
+ */
+function addSuffix(filePath, suffix, customExtension) {
+ const dirname = path.dirname(filePath);
+ const ext = path.extname(filePath);
+ const name = path.basename(filePath, ext);
+ return path.join(dirname, name + suffix + (customExtension || ext));
+}
diff --git a/remote/test/puppeteer/test/headful.spec.ts b/remote/test/puppeteer/test/headful.spec.ts
new file mode 100644
index 0000000000..119823eb50
--- /dev/null
+++ b/remote/test/puppeteer/test/headful.spec.ts
@@ -0,0 +1,204 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import path from 'path';
+import os from 'os';
+import fs from 'fs';
+import { promisify } from 'util';
+import expect from 'expect';
+import {
+ getTestState,
+ describeChromeOnly,
+ itFailsWindows,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+import rimraf from 'rimraf';
+
+const rmAsync = promisify(rimraf);
+const mkdtempAsync = promisify(fs.mkdtemp);
+
+const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
+
+const extensionPath = path.join(__dirname, 'assets', 'simple-extension');
+
+describeChromeOnly('headful tests', function () {
+ /* These tests fire up an actual browser so let's
+ * allow a higher timeout
+ */
+ this.timeout(20 * 1000);
+
+ let headfulOptions;
+ let headlessOptions;
+ let extensionOptions;
+
+ beforeEach(() => {
+ const { defaultBrowserOptions } = getTestState();
+ headfulOptions = Object.assign({}, defaultBrowserOptions, {
+ headless: false,
+ });
+ headlessOptions = Object.assign({}, defaultBrowserOptions, {
+ headless: true,
+ });
+
+ extensionOptions = Object.assign({}, defaultBrowserOptions, {
+ headless: false,
+ args: [
+ `--disable-extensions-except=${extensionPath}`,
+ `--load-extension=${extensionPath}`,
+ ],
+ });
+ });
+
+ describe('HEADFUL', function () {
+ it('background_page target type should be available', async () => {
+ const { puppeteer } = getTestState();
+ const browserWithExtension = await puppeteer.launch(extensionOptions);
+ const page = await browserWithExtension.newPage();
+ const backgroundPageTarget = await browserWithExtension.waitForTarget(
+ (target) => target.type() === 'background_page'
+ );
+ await page.close();
+ await browserWithExtension.close();
+ expect(backgroundPageTarget).toBeTruthy();
+ });
+ it('target.page() should return a background_page', async function () {
+ const { puppeteer } = getTestState();
+ const browserWithExtension = await puppeteer.launch(extensionOptions);
+ const backgroundPageTarget = await browserWithExtension.waitForTarget(
+ (target) => target.type() === 'background_page'
+ );
+ const page = await backgroundPageTarget.page();
+ expect(await page.evaluate(() => 2 * 3)).toBe(6);
+ expect(await page.evaluate(() => globalThis.MAGIC)).toBe(42);
+ await browserWithExtension.close();
+ });
+ it('should have default url when launching browser', async function () {
+ const { puppeteer } = getTestState();
+ const browser = await puppeteer.launch(extensionOptions);
+ const pages = (await browser.pages()).map((page) => page.url());
+ expect(pages).toEqual(['about:blank']);
+ await browser.close();
+ });
+ itFailsWindows(
+ 'headless should be able to read cookies written by headful',
+ async () => {
+ /* Needs investigation into why but this fails consistently on Windows CI. */
+ const { server, puppeteer } = getTestState();
+
+ const userDataDir = await mkdtempAsync(TMP_FOLDER);
+ // Write a cookie in headful chrome
+ const headfulBrowser = await puppeteer.launch(
+ Object.assign({ userDataDir }, headfulOptions)
+ );
+ const headfulPage = await headfulBrowser.newPage();
+ await headfulPage.goto(server.EMPTY_PAGE);
+ await headfulPage.evaluate(
+ () =>
+ (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 puppeteer.launch(
+ Object.assign({ userDataDir }, headlessOptions)
+ );
+ const headlessPage = await headlessBrowser.newPage();
+ await headlessPage.goto(server.EMPTY_PAGE);
+ const cookie = await headlessPage.evaluate(() => document.cookie);
+ await headlessBrowser.close();
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ await rmAsync(userDataDir).catch(() => {});
+ expect(cookie).toBe('foo=true');
+ }
+ );
+ // TODO: Support OOOPIF. @see https://github.com/puppeteer/puppeteer/issues/2548
+ xit('OOPIF: should report google.com frame', async () => {
+ const { server, puppeteer } = getTestState();
+
+ // https://google.com is isolated by default in Chromium embedder.
+ const browser = await puppeteer.launch(headfulOptions);
+ const page = await browser.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+ page.on('request', (r) => 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) => (frame.onload = x));
+ });
+ await page.waitForSelector('iframe[src="https://google.com/"]');
+ const urls = page
+ .frames()
+ .map((frame) => frame.url())
+ .sort();
+ expect(urls).toEqual([server.EMPTY_PAGE, 'https://google.com/']);
+ await browser.close();
+ });
+ it('should close browser with beforeunload page', async () => {
+ const { server, puppeteer } = getTestState();
+
+ const browser = await puppeteer.launch(headfulOptions);
+ 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');
+ await browser.close();
+ });
+ it('should open devtools when "devtools: true" option is given', async () => {
+ const { puppeteer } = getTestState();
+
+ const browser = await puppeteer.launch(
+ Object.assign({ devtools: true }, headfulOptions)
+ );
+ const context = await browser.createIncognitoBrowserContext();
+ await Promise.all([
+ context.newPage(),
+ context.waitForTarget((target) => target.url().includes('devtools://')),
+ ]);
+ await browser.close();
+ });
+ });
+
+ describe('Page.bringToFront', function () {
+ it('should work', async () => {
+ const { puppeteer } = getTestState();
+ const browser = await puppeteer.launch(headfulOptions);
+ const page1 = await browser.newPage();
+ const page2 = await browser.newPage();
+
+ await page1.bringToFront();
+ expect(await page1.evaluate(() => document.visibilityState)).toBe(
+ 'visible'
+ );
+ expect(await page2.evaluate(() => document.visibilityState)).toBe(
+ 'hidden'
+ );
+
+ await page2.bringToFront();
+ expect(await page1.evaluate(() => document.visibilityState)).toBe(
+ 'hidden'
+ );
+ expect(await page2.evaluate(() => document.visibilityState)).toBe(
+ 'visible'
+ );
+
+ await page1.close();
+ await page2.close();
+ await browser.close();
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/idle_override.spec.ts b/remote/test/puppeteer/test/idle_override.spec.ts
new file mode 100644
index 0000000000..b6a927e48a
--- /dev/null
+++ b/remote/test/puppeteer/test/idle_override.spec.ts
@@ -0,0 +1,93 @@
+/**
+ * Copyright 2020 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('Emulate idle state', () => {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ async function getIdleState() {
+ const { page } = getTestState();
+
+ const stateElement = await page.$('#state');
+ return await page.evaluate((element: HTMLElement) => {
+ return element.innerText;
+ }, stateElement);
+ }
+
+ async function verifyState(expectedState: string) {
+ const actualState = await getIdleState();
+ expect(actualState).toEqual(expectedState);
+ }
+
+ it('changing idle state emulation causes change of the IdleDetector state', async () => {
+ const { page, server, context } = 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();
+
+ // Emulate Idle states and verify IdleDetector updates state accordingly.
+ await page.emulateIdleState({
+ isUserActive: false,
+ isScreenUnlocked: false,
+ });
+ await verifyState('Idle state: idle, locked.');
+
+ await page.emulateIdleState({
+ isUserActive: true,
+ isScreenUnlocked: false,
+ });
+ await verifyState('Idle state: active, locked.');
+
+ await page.emulateIdleState({
+ isUserActive: true,
+ isScreenUnlocked: true,
+ });
+ await verifyState('Idle state: active, unlocked.');
+
+ await page.emulateIdleState({
+ isUserActive: false,
+ isScreenUnlocked: true,
+ });
+ await verifyState('Idle state: idle, unlocked.');
+
+ // Remove Idle emulation and verify IdleDetector is in initial state.
+ await page.emulateIdleState();
+ await verifyState(initialState);
+
+ // Emulate idle state again after removing emulation.
+ await page.emulateIdleState({
+ isUserActive: false,
+ isScreenUnlocked: false,
+ });
+ await verifyState('Idle state: idle, locked.');
+
+ // Remove emulation second time.
+ await page.emulateIdleState();
+ await verifyState(initialState);
+ });
+});
diff --git a/remote/test/puppeteer/test/ignorehttpserrors.spec.ts b/remote/test/puppeteer/test/ignorehttpserrors.spec.ts
new file mode 100644
index 0000000000..81252af298
--- /dev/null
+++ b/remote/test/puppeteer/test/ignorehttpserrors.spec.ts
@@ -0,0 +1,133 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import {
+ getTestState,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+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 browser;
+ let context;
+ let page;
+
+ before(async () => {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+ const options = Object.assign(
+ { ignoreHTTPSErrors: true },
+ defaultBrowserOptions
+ );
+ browser = await puppeteer.launch(options);
+ });
+
+ after(async () => {
+ await browser.close();
+ browser = null;
+ });
+
+ beforeEach(async () => {
+ context = await browser.createIncognitoBrowserContext();
+ page = await context.newPage();
+ });
+
+ afterEach(async () => {
+ await context.close();
+ context = null;
+ page = null;
+ });
+
+ describe('Response.securityDetails', function () {
+ it('should work', async () => {
+ const { httpsServer } = getTestState();
+
+ 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.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 } = getTestState();
+
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response.securityDetails()).toBe(null);
+ });
+ it('Network redirects should report SecurityDetails', async () => {
+ const { httpsServer } = getTestState();
+
+ httpsServer.setRedirect('/plzredirect', '/empty.html');
+ const responses = [];
+ page.on('response', (response) => responses.push(response));
+ const [serverRequest] = await Promise.all([
+ httpsServer.waitForRequest('/plzredirect'),
+ page.goto(httpsServer.PREFIX + '/plzredirect'),
+ ]);
+ expect(responses.length).toBe(2);
+ expect(responses[0].status()).toBe(302);
+ const securityDetails = responses[0].securityDetails();
+ const protocol = serverRequest.socket.getProtocol().replace('v', ' ');
+ expect(securityDetails.protocol()).toBe(protocol);
+ });
+ });
+
+ it('should work', async () => {
+ const { httpsServer } = getTestState();
+
+ let error = null;
+ const response = await page
+ .goto(httpsServer.EMPTY_PAGE)
+ .catch((error_) => (error = error_));
+ expect(error).toBe(null);
+ expect(response.ok()).toBe(true);
+ });
+ it('should work with request interception', async () => {
+ const { httpsServer } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => 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 } = getTestState();
+
+ 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().length).toBe(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/input.spec.ts b/remote/test/puppeteer/test/input.spec.ts
new file mode 100644
index 0000000000..d87aa1375f
--- /dev/null
+++ b/remote/test/puppeteer/test/input.spec.ts
@@ -0,0 +1,337 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import path from 'path';
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+ describeFailsFirefox,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+const FILE_TO_UPLOAD = path.join(__dirname, '/assets/file-to-upload.txt');
+
+describe('input tests', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ describeFailsFirefox('input', function () {
+ it('should upload the file', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/fileupload.html');
+ const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD);
+ const input = await page.$('input');
+ await page.evaluate((e: HTMLElement) => {
+ globalThis._inputEvents = [];
+ e.addEventListener('change', (ev) =>
+ globalThis._inputEvents.push(ev.type)
+ );
+ e.addEventListener('input', (ev) =>
+ globalThis._inputEvents.push(ev.type)
+ );
+ }, input);
+ await input.uploadFile(filePath);
+ expect(
+ await page.evaluate((e: HTMLInputElement) => e.files[0].name, input)
+ ).toBe('file-to-upload.txt');
+ expect(
+ await page.evaluate((e: HTMLInputElement) => e.files[0].type, input)
+ ).toBe('text/plain');
+ expect(await page.evaluate(() => globalThis._inputEvents)).toEqual([
+ 'input',
+ 'change',
+ ]);
+ expect(
+ await page.evaluate((e: HTMLInputElement) => {
+ const reader = new FileReader();
+ const promise = new Promise((fulfill) => (reader.onload = fulfill));
+ reader.readAsText(e.files[0]);
+ return promise.then(() => reader.result);
+ }, input)
+ ).toBe('contents of the file');
+ });
+ });
+
+ describeFailsFirefox('Page.waitForFileChooser', function () {
+ it('should work when file input is attached to DOM', async () => {
+ const { page } = 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 } = 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, puppeteer } = getTestState();
+
+ let error = null;
+ await page
+ .waitForFileChooser({ timeout: 1 })
+ .catch((error_) => (error = error_));
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should respect default timeout when there is no custom timeout', async () => {
+ const { page, puppeteer } = getTestState();
+
+ page.setDefaultTimeout(1);
+ let error = null;
+ await page.waitForFileChooser().catch((error_) => (error = error_));
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should prioritize exact timeout over default timeout', async () => {
+ const { page, puppeteer } = getTestState();
+
+ page.setDefaultTimeout(0);
+ let error = null;
+ await page
+ .waitForFileChooser({ timeout: 1 })
+ .catch((error_) => (error = error_));
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should work with no timeout', async () => {
+ const { page } = getTestState();
+
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser({ timeout: 0 }),
+ page.evaluate(() =>
+ 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 } = getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [fileChooser1, fileChooser2] = await Promise.all([
+ page.waitForFileChooser(),
+ page.waitForFileChooser(),
+ page.$eval('input', (input: HTMLInputElement) => input.click()),
+ ]);
+ expect(fileChooser1 === fileChooser2).toBe(true);
+ });
+ });
+
+ describeFailsFirefox('FileChooser.accept', function () {
+ it('should accept single file', async () => {
+ const { page } = 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]),
+ new Promise((x) => page.once('metrics', x)),
+ ]);
+ expect(
+ await page.$eval(
+ 'input',
+ (input: HTMLInputElement) => input.files.length
+ )
+ ).toBe(1);
+ expect(
+ await page.$eval(
+ 'input',
+ (input: HTMLInputElement) => input.files[0].name
+ )
+ ).toBe('file-to-upload.txt');
+ });
+ it('should be able to read selected file', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`<input type=file>`);
+ page
+ .waitForFileChooser()
+ .then((chooser) => chooser.accept([FILE_TO_UPLOAD]));
+ expect(
+ await page.$eval('input', async (picker: HTMLInputElement) => {
+ picker.click();
+ await new Promise((x) => (picker.oninput = x));
+ const reader = new FileReader();
+ const promise = new Promise((fulfill) => (reader.onload = fulfill));
+ reader.readAsText(picker.files[0]);
+ return promise.then(() => reader.result);
+ })
+ ).toBe('contents of the file');
+ });
+ it('should be able to reset selected files with empty file list', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`<input type=file>`);
+ page
+ .waitForFileChooser()
+ .then((chooser) => chooser.accept([FILE_TO_UPLOAD]));
+ expect(
+ await page.$eval('input', async (picker: HTMLInputElement) => {
+ picker.click();
+ await new Promise((x) => (picker.oninput = x));
+ return picker.files.length;
+ })
+ ).toBe(1);
+ page.waitForFileChooser().then((chooser) => chooser.accept([]));
+ expect(
+ await page.$eval('input', async (picker: HTMLInputElement) => {
+ picker.click();
+ await new Promise((x) => (picker.oninput = x));
+ return picker.files.length;
+ })
+ ).toBe(0);
+ });
+ it('should not accept multiple files for single-file input', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ let error = null;
+ await chooser
+ .accept([
+ path.relative(
+ process.cwd(),
+ __dirname + '/assets/file-to-upload.txt'
+ ),
+ path.relative(process.cwd(), __dirname + '/assets/pptr.png'),
+ ])
+ .catch((error_) => (error = error_));
+ expect(error).not.toBe(null);
+ });
+ it('should fail for non-existent files', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [chooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.click('input'),
+ ]);
+ let error = null;
+ await chooser
+ .accept(['file-does-not-exist.txt'])
+ .catch((error_) => (error = error_));
+ expect(error).not.toBe(null);
+ });
+ it('should fail when accepting file chooser twice', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [fileChooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.$eval('input', (input: HTMLInputElement) => input.click()),
+ ]);
+ await fileChooser.accept([]);
+ let error = null;
+ await fileChooser.accept([]).catch((error_) => (error = error_));
+ expect(error.message).toBe(
+ 'Cannot accept FileChooser which is already handled!'
+ );
+ });
+ });
+
+ describeFailsFirefox('FileChooser.cancel', function () {
+ it('should cancel dialog', async () => {
+ const { page } = 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: HTMLInputElement) => input.click()),
+ ]);
+ await fileChooser1.cancel();
+ // If this resolves, than we successfully canceled file chooser.
+ await Promise.all([
+ page.waitForFileChooser(),
+ page.$eval('input', (input: HTMLInputElement) => input.click()),
+ ]);
+ });
+ it('should fail when canceling file chooser twice', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`<input type=file>`);
+ const [fileChooser] = await Promise.all([
+ page.waitForFileChooser(),
+ page.$eval('input', (input: HTMLInputElement) => input.click()),
+ ]);
+ await fileChooser.cancel();
+ let error = null;
+ await fileChooser.cancel().catch((error_) => (error = error_));
+ expect(error.message).toBe(
+ 'Cannot cancel FileChooser which is already handled!'
+ );
+ });
+ });
+
+ describeFailsFirefox('FileChooser.isMultiple', () => {
+ it('should work for single file pick', async () => {
+ const { page } = 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 } = 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 } = 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/jshandle.spec.ts b/remote/test/puppeteer/test/jshandle.spec.ts
new file mode 100644
index 0000000000..35b0f8edbe
--- /dev/null
+++ b/remote/test/puppeteer/test/jshandle.spec.ts
@@ -0,0 +1,282 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('JSHandle', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ describe('Page.evaluateHandle', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ const windowHandle = await page.evaluateHandle(() => window);
+ expect(windowHandle).toBeTruthy();
+ });
+ it('should accept object handle as an argument', async () => {
+ const { page } = getTestState();
+
+ const navigatorHandle = await page.evaluateHandle(() => navigator);
+ const text = await page.evaluate(
+ (e: Navigator) => e.userAgent,
+ navigatorHandle
+ );
+ expect(text).toContain('Mozilla');
+ });
+ it('should accept object handle to primitive types', async () => {
+ const { page } = getTestState();
+
+ const aHandle = await page.evaluateHandle(() => 5);
+ const isFive = await page.evaluate((e) => Object.is(e, 5), aHandle);
+ expect(isFive).toBeTruthy();
+ });
+ it('should warn on nested object handles', async () => {
+ const { page } = getTestState();
+
+ const aHandle = await page.evaluateHandle(() => document.body);
+ let error = null;
+ await page
+ // @ts-expect-error we are deliberately passing a bad type here (nested object)
+ .evaluateHandle((opts) => opts.elem.querySelector('p'), {
+ elem: aHandle,
+ })
+ .catch((error_) => (error = error_));
+ expect(error.message).toContain('Are you passing a nested JSHandle?');
+ });
+ it('should accept object handle to unserializable value', async () => {
+ const { page } = getTestState();
+
+ const aHandle = await page.evaluateHandle(() => Infinity);
+ expect(await page.evaluate((e) => Object.is(e, Infinity), aHandle)).toBe(
+ true
+ );
+ });
+ it('should use the same JS wrappers', async () => {
+ const { page } = getTestState();
+
+ const aHandle = await page.evaluateHandle(() => {
+ globalThis.FOO = 123;
+ return window;
+ });
+ expect(await page.evaluate((e: { FOO: number }) => e.FOO, aHandle)).toBe(
+ 123
+ );
+ });
+ it('should work with primitives', async () => {
+ const { page } = getTestState();
+
+ const aHandle = await page.evaluateHandle(() => {
+ globalThis.FOO = 123;
+ return window;
+ });
+ expect(await page.evaluate((e: { FOO: number }) => e.FOO, aHandle)).toBe(
+ 123
+ );
+ });
+ });
+
+ describe('JSHandle.getProperty', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ const aHandle = await page.evaluateHandle(() => ({
+ one: 1,
+ two: 2,
+ three: 3,
+ }));
+ const twoHandle = await aHandle.getProperty('two');
+ expect(await twoHandle.jsonValue()).toEqual(2);
+ });
+ });
+
+ describe('JSHandle.jsonValue', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ const aHandle = await page.evaluateHandle(() => ({ foo: 'bar' }));
+ const json = await aHandle.jsonValue();
+ expect(json).toEqual({ foo: 'bar' });
+ });
+ it('should not work with dates', async () => {
+ const { page } = getTestState();
+
+ const dateHandle = await page.evaluateHandle(
+ () => new Date('2017-09-26T00:00:00.000Z')
+ );
+ const json = await dateHandle.jsonValue();
+ expect(json).toEqual({});
+ });
+ it('should throw for circular objects', async () => {
+ const { page, isChrome } = getTestState();
+
+ const windowHandle = await page.evaluateHandle('window');
+ let error = null;
+ await windowHandle.jsonValue().catch((error_) => (error = error_));
+ if (isChrome)
+ expect(error.message).toContain('Object reference chain is too long');
+ else expect(error.message).toContain('Object is not serializable');
+ });
+ });
+
+ describe('JSHandle.getProperties', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ const aHandle = await page.evaluateHandle(() => ({
+ foo: 'bar',
+ }));
+ const properties = await aHandle.getProperties();
+ const foo = properties.get('foo');
+ expect(foo).toBeTruthy();
+ expect(await foo.jsonValue()).toBe('bar');
+ });
+ it('should return even non-own properties', async () => {
+ const { page } = getTestState();
+
+ const 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 } = getTestState();
+
+ const aHandle = await page.evaluateHandle(() => document.body);
+ const element = aHandle.asElement();
+ expect(element).toBeTruthy();
+ });
+ it('should return null for non-elements', async () => {
+ const { page } = getTestState();
+
+ const aHandle = await page.evaluateHandle(() => 2);
+ const element = aHandle.asElement();
+ expect(element).toBeFalsy();
+ });
+ it('should return ElementHandle for TextNodes', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<div>ee!</div>');
+ const aHandle = await page.evaluateHandle(
+ () => document.querySelector('div').firstChild
+ );
+ const element = aHandle.asElement();
+ expect(element).toBeTruthy();
+ expect(
+ await page.evaluate(
+ (e: HTMLElement) => e.nodeType === Node.TEXT_NODE,
+ element
+ )
+ );
+ });
+ });
+
+ describe('JSHandle.toString', function () {
+ it('should work for primitives', async () => {
+ const { page } = getTestState();
+
+ const numberHandle = await page.evaluateHandle(() => 2);
+ expect(numberHandle.toString()).toBe('JSHandle:2');
+ const stringHandle = await page.evaluateHandle(() => 'a');
+ expect(stringHandle.toString()).toBe('JSHandle:a');
+ });
+ it('should work for complicated objects', async () => {
+ const { page } = getTestState();
+
+ const aHandle = await page.evaluateHandle(() => window);
+ expect(aHandle.toString()).toBe('JSHandle@object');
+ });
+ it('should work with different subtypes', async () => {
+ const { page } = 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'
+ );
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/keyboard.spec.ts b/remote/test/puppeteer/test/keyboard.spec.ts
new file mode 100644
index 0000000000..c6d28dac8e
--- /dev/null
+++ b/remote/test/puppeteer/test/keyboard.spec.ts
@@ -0,0 +1,407 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import utils from './utils.js';
+import os from 'os';
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+import { KeyInput } from '../lib/cjs/puppeteer/common/USKeyboardLayout.js';
+
+describe('Keyboard', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ it('should type into a textarea', async () => {
+ const { page } = 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(() => document.querySelector('textarea').value)
+ ).toBe(text);
+ });
+ it('should press the metaKey', async () => {
+ const { page, isFirefox } = getTestState();
+
+ await page.evaluate(() => {
+ (window as any).keyPromise = new Promise((resolve) =>
+ document.addEventListener('keydown', (event) => resolve(event.key))
+ );
+ });
+ await page.keyboard.press('Meta');
+ expect(await page.evaluate('keyPromise')).toBe(
+ isFirefox && os.platform() !== 'darwin' ? 'OS' : 'Meta'
+ );
+ });
+ it('should move with the arrow keys', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.type('textarea', 'Hello World!');
+ expect(
+ await page.evaluate(() => document.querySelector('textarea').value)
+ ).toBe('Hello World!');
+ for (let i = 0; i < 'World!'.length; i++) page.keyboard.press('ArrowLeft');
+ await page.keyboard.type('inserted ');
+ expect(
+ await page.evaluate(() => document.querySelector('textarea').value)
+ ).toBe('Hello inserted World!');
+ page.keyboard.down('Shift');
+ for (let i = 0; i < 'inserted '.length; i++)
+ page.keyboard.press('ArrowLeft');
+ page.keyboard.up('Shift');
+ await page.keyboard.press('Backspace');
+ expect(
+ await page.evaluate(() => document.querySelector('textarea').value)
+ ).toBe('Hello World!');
+ });
+ it('should send a character with ElementHandle.press', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ const textarea = await page.$('textarea');
+ await textarea.press('a');
+ expect(
+ await page.evaluate(() => document.querySelector('textarea').value)
+ ).toBe('a');
+
+ await page.evaluate(() =>
+ window.addEventListener('keydown', (e) => e.preventDefault(), true)
+ );
+
+ await textarea.press('b');
+ expect(
+ await page.evaluate(() => document.querySelector('textarea').value)
+ ).toBe('a');
+ });
+ it(
+ 'ElementHandle.press should support |text| option',
+ async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ const textarea = await page.$('textarea');
+ await textarea.press('a', { text: 'ё' });
+ expect(
+ await page.evaluate(() => document.querySelector('textarea').value)
+ ).toBe('ё');
+ }
+ );
+ it('should send a character with sendCharacter', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.focus('textarea');
+ await page.keyboard.sendCharacter('嗨');
+ expect(
+ await page.evaluate(() => document.querySelector('textarea').value)
+ ).toBe('嗨');
+ await page.evaluate(() =>
+ window.addEventListener('keydown', (e) => e.preventDefault(), true)
+ );
+ await page.keyboard.sendCharacter('a');
+ expect(
+ await page.evaluate(() => document.querySelector('textarea').value)
+ ).toBe('嗨a');
+ });
+ it('should report shiftKey', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/keyboard.html');
+ const keyboard = page.keyboard;
+ const codeForKey = new Map<KeyInput, number>([
+ ['Shift', 16],
+ ['Alt', 18],
+ ['Control', 17],
+ ]);
+ for (const [modifierKey, modifierCode] of codeForKey) {
+ await keyboard.down(modifierKey);
+ expect(await page.evaluate(() => globalThis.getResult())).toBe(
+ 'Keydown: ' +
+ modifierKey +
+ ' ' +
+ modifierKey +
+ 'Left ' +
+ modifierCode +
+ ' [' +
+ modifierKey +
+ ']'
+ );
+ await keyboard.down('!');
+ // Shift+! will generate a keypress
+ if (modifierKey === 'Shift')
+ expect(await page.evaluate(() => globalThis.getResult())).toBe(
+ 'Keydown: ! Digit1 49 [' +
+ modifierKey +
+ ']\nKeypress: ! Digit1 33 33 [' +
+ modifierKey +
+ ']'
+ );
+ else
+ expect(await page.evaluate(() => globalThis.getResult())).toBe(
+ 'Keydown: ! Digit1 49 [' + modifierKey + ']'
+ );
+
+ await keyboard.up('!');
+ expect(await page.evaluate(() => globalThis.getResult())).toBe(
+ 'Keyup: ! Digit1 49 [' + modifierKey + ']'
+ );
+ await keyboard.up(modifierKey);
+ expect(await page.evaluate(() => globalThis.getResult())).toBe(
+ 'Keyup: ' +
+ modifierKey +
+ ' ' +
+ modifierKey +
+ 'Left ' +
+ modifierCode +
+ ' []'
+ );
+ }
+ });
+ it('should report multiple modifiers', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/keyboard.html');
+ const keyboard = page.keyboard;
+ await keyboard.down('Control');
+ expect(await page.evaluate(() => globalThis.getResult())).toBe(
+ 'Keydown: Control ControlLeft 17 [Control]'
+ );
+ await keyboard.down('Alt');
+ expect(await page.evaluate(() => globalThis.getResult())).toBe(
+ 'Keydown: Alt AltLeft 18 [Alt Control]'
+ );
+ await keyboard.down(';');
+ expect(await page.evaluate(() => globalThis.getResult())).toBe(
+ 'Keydown: ; Semicolon 186 [Alt Control]'
+ );
+ await keyboard.up(';');
+ expect(await page.evaluate(() => globalThis.getResult())).toBe(
+ 'Keyup: ; Semicolon 186 [Alt Control]'
+ );
+ await keyboard.up('Control');
+ expect(await page.evaluate(() => globalThis.getResult())).toBe(
+ 'Keyup: Control ControlLeft 17 [Alt]'
+ );
+ await keyboard.up('Alt');
+ expect(await page.evaluate(() => globalThis.getResult())).toBe(
+ 'Keyup: Alt AltLeft 18 []'
+ );
+ });
+ it('should send proper codes while typing', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/keyboard.html');
+ await page.keyboard.type('!');
+ expect(await page.evaluate(() => globalThis.getResult())).toBe(
+ [
+ 'Keydown: ! Digit1 49 []',
+ 'Keypress: ! Digit1 33 33 []',
+ 'Keyup: ! Digit1 49 []',
+ ].join('\n')
+ );
+ await page.keyboard.type('^');
+ expect(await page.evaluate(() => globalThis.getResult())).toBe(
+ [
+ 'Keydown: ^ Digit6 54 []',
+ 'Keypress: ^ Digit6 94 94 []',
+ 'Keyup: ^ Digit6 54 []',
+ ].join('\n')
+ );
+ });
+ it('should send proper codes while typing with shift', async () => {
+ const { page, server } = 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(() => globalThis.getResult())).toBe(
+ [
+ 'Keydown: Shift ShiftLeft 16 [Shift]',
+ 'Keydown: ~ Backquote 192 [Shift]', // 192 is ` keyCode
+ 'Keypress: ~ Backquote 126 126 [Shift]', // 126 is ~ charCode
+ 'Keyup: ~ Backquote 192 [Shift]',
+ ].join('\n')
+ );
+ await keyboard.up('Shift');
+ });
+ it('should not type canceled events', async () => {
+ const { page, server } = 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(() => globalThis.textarea.value)).toBe(
+ 'He Wrd!'
+ );
+ });
+ it('should specify repeat property', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.focus('textarea');
+ await page.evaluate(() =>
+ document
+ .querySelector('textarea')
+ .addEventListener('keydown', (e) => (globalThis.lastEvent = e), true)
+ );
+ await page.keyboard.down('a');
+ expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(false);
+ await page.keyboard.press('a');
+ expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(true);
+
+ await page.keyboard.down('b');
+ expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(false);
+ await page.keyboard.down('b');
+ expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(true);
+
+ await page.keyboard.up('a');
+ await page.keyboard.down('a');
+ expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(false);
+ });
+ it('should type all kinds of characters', async () => {
+ const { page, server } = 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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.evaluate(() => {
+ window.addEventListener(
+ 'keydown',
+ (event) => (globalThis.keyLocation = event.location),
+ true
+ );
+ });
+ const 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 } = getTestState();
+
+ let error = await page.keyboard
+ // @ts-expect-error bad input
+ .press('NotARealKey')
+ .catch((error_) => error_);
+ expect(error.message).toBe('Unknown key: "NotARealKey"');
+
+ // @ts-expect-error bad input
+ error = await page.keyboard.press('ё').catch((error_) => error_);
+ expect(error && error.message).toBe('Unknown key: "ё"');
+
+ // @ts-expect-error bad input
+ error = await page.keyboard.press('😊').catch((error_) => error_);
+ expect(error && error.message).toBe('Unknown key: "😊"');
+ });
+ it('should type emoji', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ await page.type('textarea', '👹 Tokyo street Japan 🇯🇵');
+ expect(
+ await page.$eval(
+ 'textarea',
+ (textarea: HTMLInputElement) => textarea.value
+ )
+ ).toBe('👹 Tokyo street Japan 🇯🇵');
+ });
+ it('should type emoji into an iframe', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await utils.attachFrame(
+ page,
+ 'emoji-test',
+ server.PREFIX + '/input/textarea.html'
+ );
+ const frame = page.frames()[1];
+ const textarea = await frame.$('textarea');
+ await textarea.type('👹 Tokyo street Japan 🇯🇵');
+ expect(
+ await frame.$eval(
+ 'textarea',
+ (textarea: HTMLInputElement) => textarea.value
+ )
+ ).toBe('👹 Tokyo street Japan 🇯🇵');
+ });
+ it('should press the meta key', async () => {
+ const { page, isFirefox } = getTestState();
+
+ await page.evaluate(() => {
+ globalThis.result = null;
+ document.addEventListener('keydown', (event) => {
+ globalThis.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
+ ];
+ if (isFirefox && os.platform() !== 'darwin') expect(key).toBe('OS');
+ else expect(key).toBe('Meta');
+
+ if (isFirefox) expect(code).toBe('OSLeft');
+ else expect(code).toBe('MetaLeft');
+
+ if (isFirefox && os.platform() !== 'darwin') expect(metaKey).toBe(false);
+ else expect(metaKey).toBe(true);
+ });
+});
diff --git a/remote/test/puppeteer/test/launcher.spec.ts b/remote/test/puppeteer/test/launcher.spec.ts
new file mode 100644
index 0000000000..4c9c2b6e61
--- /dev/null
+++ b/remote/test/puppeteer/test/launcher.spec.ts
@@ -0,0 +1,654 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+import sinon from 'sinon';
+import { promisify } from 'util';
+import {
+ getTestState,
+ itOnlyRegularInstall,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+import utils from './utils.js';
+import expect from 'expect';
+import rimraf from 'rimraf';
+import { Page } from '../lib/cjs/puppeteer/common/Page.js';
+
+const rmAsync = promisify(rimraf);
+const mkdtempAsync = promisify(fs.mkdtemp);
+const readFileAsync = promisify(fs.readFile);
+const statAsync = promisify(fs.stat);
+const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
+const FIREFOX_TIMEOUT = 30 * 1000;
+
+describe('Launcher specs', function () {
+ if (getTestState().isFirefox) this.timeout(FIREFOX_TIMEOUT);
+
+ describe('Puppeteer', function () {
+ describe('BrowserFetcher', function () {
+ it('should download and extract chrome linux binary', async () => {
+ const { server, puppeteer } = getTestState();
+
+ const downloadsFolder = await mkdtempAsync(TMP_FOLDER);
+ const browserFetcher = puppeteer.createBrowserFetcher({
+ platform: 'linux',
+ path: downloadsFolder,
+ host: server.PREFIX,
+ });
+ const expectedRevision = '123456';
+ let revisionInfo = browserFetcher.revisionInfo(expectedRevision);
+ server.setRoute(
+ revisionInfo.url.substring(server.PREFIX.length),
+ (req, res) => {
+ server.serveFile(req, res, '/chromium-linux.zip');
+ }
+ );
+
+ expect(revisionInfo.local).toBe(false);
+ expect(browserFetcher.platform()).toBe('linux');
+ expect(browserFetcher.product()).toBe('chrome');
+ expect(!!browserFetcher.host()).toBe(true);
+ expect(await browserFetcher.canDownload('100000')).toBe(false);
+ expect(await browserFetcher.canDownload(expectedRevision)).toBe(true);
+
+ revisionInfo = await browserFetcher.download(expectedRevision);
+ expect(revisionInfo.local).toBe(true);
+ expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe(
+ 'LINUX BINARY\n'
+ );
+ const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755;
+ expect(
+ (await statAsync(revisionInfo.executablePath)).mode & 0o777
+ ).toBe(expectedPermissions);
+ expect(await browserFetcher.localRevisions()).toEqual([
+ expectedRevision,
+ ]);
+ await browserFetcher.remove(expectedRevision);
+ expect(await browserFetcher.localRevisions()).toEqual([]);
+ await rmAsync(downloadsFolder);
+ });
+ it('should download and extract firefox linux binary', async () => {
+ const { server, puppeteer } = getTestState();
+
+ const downloadsFolder = await mkdtempAsync(TMP_FOLDER);
+ const browserFetcher = puppeteer.createBrowserFetcher({
+ platform: 'linux',
+ path: downloadsFolder,
+ host: server.PREFIX,
+ product: 'firefox',
+ });
+ const expectedVersion = '75.0a1';
+ let revisionInfo = browserFetcher.revisionInfo(expectedVersion);
+ server.setRoute(
+ revisionInfo.url.substring(server.PREFIX.length),
+ (req, res) => {
+ server.serveFile(
+ req,
+ res,
+ `/firefox-${expectedVersion}.en-US.linux-x86_64.tar.bz2`
+ );
+ }
+ );
+
+ expect(revisionInfo.local).toBe(false);
+ expect(browserFetcher.platform()).toBe('linux');
+ expect(browserFetcher.product()).toBe('firefox');
+ expect(await browserFetcher.canDownload('100000')).toBe(false);
+ expect(await browserFetcher.canDownload(expectedVersion)).toBe(true);
+
+ revisionInfo = await browserFetcher.download(expectedVersion);
+ expect(revisionInfo.local).toBe(true);
+ expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe(
+ 'FIREFOX LINUX BINARY\n'
+ );
+ const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755;
+ expect(
+ (await statAsync(revisionInfo.executablePath)).mode & 0o777
+ ).toBe(expectedPermissions);
+ expect(await browserFetcher.localRevisions()).toEqual([
+ expectedVersion,
+ ]);
+ await browserFetcher.remove(expectedVersion);
+ expect(await browserFetcher.localRevisions()).toEqual([]);
+ await rmAsync(downloadsFolder);
+ });
+ });
+
+ describe('Browser.disconnect', function () {
+ it('should reject navigation when browser closes', async () => {
+ const { server, puppeteer, defaultBrowserOptions } = getTestState();
+ server.setRoute('/one-style.css', () => {});
+ const browser = await puppeteer.launch(defaultBrowserOptions);
+ const remote = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ });
+ const page = await remote.newPage();
+ const navigationPromise = page
+ .goto(server.PREFIX + '/one-style.html', { timeout: 60000 })
+ .catch((error_) => error_);
+ await server.waitForRequest('/one-style.css');
+ remote.disconnect();
+ const error = await navigationPromise;
+ expect(error.message).toBe(
+ 'Navigation failed because browser has disconnected!'
+ );
+ await browser.close();
+ });
+ it('should reject waitForSelector when browser closes', async () => {
+ const { server, puppeteer, defaultBrowserOptions } = getTestState();
+
+ server.setRoute('/empty.html', () => {});
+ const browser = await puppeteer.launch(defaultBrowserOptions);
+ const remote = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ });
+ const page = await remote.newPage();
+ const watchdog = page
+ .waitForSelector('div', { timeout: 60000 })
+ .catch((error_) => error_);
+ remote.disconnect();
+ const error = await watchdog;
+ expect(error.message).toContain('Protocol error');
+ await browser.close();
+ });
+ });
+ describe('Browser.close', function () {
+ it('should terminate network waiters', async () => {
+ const { server, puppeteer, defaultBrowserOptions } = getTestState();
+
+ const browser = await puppeteer.launch(defaultBrowserOptions);
+ const remote = await puppeteer.connect({
+ browserWSEndpoint: browser.wsEndpoint(),
+ });
+ const newPage = await remote.newPage();
+ const results = await Promise.all([
+ newPage.waitForRequest(server.EMPTY_PAGE).catch((error) => error),
+ newPage.waitForResponse(server.EMPTY_PAGE).catch((error) => error),
+ browser.close(),
+ ]);
+ for (let i = 0; i < 2; i++) {
+ const message = results[i].message;
+ expect(message).toContain('Target closed');
+ expect(message).not.toContain('Timeout');
+ }
+ await browser.close();
+ });
+ });
+ describe('Puppeteer.launch', function () {
+ it('should reject all promises when browser is closed', async () => {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+ const browser = await puppeteer.launch(defaultBrowserOptions);
+ const page = await browser.newPage();
+ let error = null;
+ const neverResolves = page
+ .evaluate(() => new Promise(() => {}))
+ .catch((error_) => (error = error_));
+ await browser.close();
+ await neverResolves;
+ expect(error.message).toContain('Protocol error');
+ });
+ it('should reject if executable path is invalid', async () => {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+
+ let waitError = null;
+ const options = Object.assign({}, defaultBrowserOptions, {
+ executablePath: 'random-invalid-path',
+ });
+ await puppeteer.launch(options).catch((error) => (waitError = error));
+ expect(waitError.message).toContain('Failed to launch');
+ });
+ it('userDataDir option', async () => {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+
+ const userDataDir = await mkdtempAsync(TMP_FOLDER);
+ const options = Object.assign({ userDataDir }, defaultBrowserOptions);
+ const browser = await puppeteer.launch(options);
+ // Open a page to make sure its functional.
+ await browser.newPage();
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ await browser.close();
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ await rmAsync(userDataDir).catch(() => {});
+ });
+ it('userDataDir argument', async () => {
+ const { isChrome, puppeteer, defaultBrowserOptions } = getTestState();
+
+ const userDataDir = await mkdtempAsync(TMP_FOLDER);
+ const options = Object.assign({}, defaultBrowserOptions);
+ if (isChrome) {
+ options.args = [
+ ...(defaultBrowserOptions.args || []),
+ `--user-data-dir=${userDataDir}`,
+ ];
+ } else {
+ options.args = [
+ ...(defaultBrowserOptions.args || []),
+ `-profile`,
+ userDataDir,
+ ];
+ }
+ const browser = await puppeteer.launch(options);
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ await browser.close();
+ expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ await rmAsync(userDataDir).catch(() => {});
+ });
+ it('userDataDir option should restore state', async () => {
+ const { server, puppeteer, defaultBrowserOptions } = getTestState();
+
+ const userDataDir = await mkdtempAsync(TMP_FOLDER);
+ const options = Object.assign({ userDataDir }, defaultBrowserOptions);
+ const browser = await puppeteer.launch(options);
+ const page = await browser.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => (localStorage.hey = 'hello'));
+ await browser.close();
+
+ const browser2 = await puppeteer.launch(options);
+ const page2 = await browser2.newPage();
+ await page2.goto(server.EMPTY_PAGE);
+ expect(await page2.evaluate(() => localStorage.hey)).toBe('hello');
+ await browser2.close();
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ await rmAsync(userDataDir).catch(() => {});
+ });
+ // This mysteriously fails on Windows on AppVeyor. See
+ // https://github.com/puppeteer/puppeteer/issues/4111
+ xit('userDataDir option should restore cookies', async () => {
+ const { server, puppeteer, defaultBrowserOptions } = getTestState();
+
+ const userDataDir = await mkdtempAsync(TMP_FOLDER);
+ const options = Object.assign({ userDataDir }, defaultBrowserOptions);
+ const browser = await puppeteer.launch(options);
+ const page = await browser.newPage();
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(
+ () =>
+ (document.cookie =
+ 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT')
+ );
+ await browser.close();
+
+ const browser2 = await puppeteer.launch(options);
+ const page2 = await browser2.newPage();
+ await page2.goto(server.EMPTY_PAGE);
+ expect(await page2.evaluate(() => document.cookie)).toBe(
+ 'doSomethingOnlyOnce=true'
+ );
+ await browser2.close();
+ // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
+ await rmAsync(userDataDir).catch(() => {});
+ });
+ it('should return the default arguments', async () => {
+ const { isChrome, isFirefox, puppeteer } = getTestState();
+
+ 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');
+ expect(puppeteer.defaultArgs()).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 } = getTestState();
+ if (isChrome) expect(puppeteer.product).toBe('chrome');
+ else if (isFirefox) expect(puppeteer.product).toBe('firefox');
+ });
+ it('should work with no default arguments', async () => {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+ const options = Object.assign({}, defaultBrowserOptions);
+ options.ignoreDefaultArgs = true;
+ const browser = await puppeteer.launch(options);
+ const page = await browser.newPage();
+ expect(await page.evaluate('11 * 11')).toBe(121);
+ await page.close();
+ await browser.close();
+ });
+ it('should filter out ignored default arguments', async () => {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+ // Make sure we launch with `--enable-automation` by default.
+ const defaultArgs = puppeteer.defaultArgs();
+ const browser = await puppeteer.launch(
+ Object.assign({}, defaultBrowserOptions, {
+ // Ignore first and third default argument.
+ ignoreDefaultArgs: [defaultArgs[0], defaultArgs[2]],
+ })
+ );
+ 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);
+ await browser.close();
+ });
+ it('should have default URL when launching browser', async function () {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+ const browser = await puppeteer.launch(defaultBrowserOptions);
+ const pages = (await browser.pages()).map((page) => page.url());
+ expect(pages).toEqual(['about:blank']);
+ await browser.close();
+ });
+ it(
+ 'should have custom URL when launching browser',
+ async () => {
+ const { server, puppeteer, defaultBrowserOptions } = getTestState();
+
+ const options = Object.assign({}, defaultBrowserOptions);
+ options.args = [server.EMPTY_PAGE].concat(options.args || []);
+ const browser = await puppeteer.launch(options);
+ const pages = await browser.pages();
+ expect(pages.length).toBe(1);
+ const page = pages[0];
+ if (page.url() !== server.EMPTY_PAGE) await page.waitForNavigation();
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ await browser.close();
+ }
+ );
+ it('should set the default viewport', async () => {
+ const { puppeteer, defaultBrowserOptions } = getTestState();
+ const options = Object.assign({}, defaultBrowserOptions, {
+ defaultViewport: {
+ width: 456,
+ height: 789,
+ },
+ });
+ const browser = await puppeteer.launch(options);
+ const page = await browser.newPage();
+ expect(await page.evaluate('window.innerWidth')).toBe(456);
+ expect(await page.evaluate('window.innerHeight')).toBe(789);
+ await browser.close();
+ });
+ it('should disable the default viewport', async () => {
+ const { puppeteer, defaultBrowserOptions } = getTestState();
+ const options = Object.assign({}, defaultBrowserOptions, {
+ defaultViewport: null,
+ });
+ const browser = await puppeteer.launch(options);
+ const page = await browser.newPage();
+ expect(page.viewport()).toBe(null);
+ await browser.close();
+ });
+ it('should take fullPage screenshots when defaultViewport is null', async () => {
+ const { server, puppeteer, defaultBrowserOptions } = getTestState();
+
+ const options = Object.assign({}, defaultBrowserOptions, {
+ defaultViewport: null,
+ });
+ const browser = await puppeteer.launch(options);
+ const page = await browser.newPage();
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ fullPage: true,
+ });
+ expect(screenshot).toBeInstanceOf(Buffer);
+ await browser.close();
+ });
+ });
+
+ describe('Puppeteer.launch', function () {
+ let productName;
+
+ before(async () => {
+ const { puppeteer } = getTestState();
+ productName = puppeteer._productName;
+ });
+
+ after(async () => {
+ const { puppeteer } = getTestState();
+ // @ts-expect-error launcher is a private property that users can't
+ // touch, but for testing purposes we need to reset it.
+ puppeteer._lazyLauncher = undefined;
+ puppeteer._productName = productName;
+ });
+
+ itOnlyRegularInstall('should be able to launch Chrome', async () => {
+ const { puppeteer } = getTestState();
+ const browser = await puppeteer.launch({ product: 'chrome' });
+ const userAgent = await browser.userAgent();
+ await browser.close();
+ expect(userAgent).toContain('Chrome');
+ });
+
+ it('falls back to launching chrome if there is an unknown product but logs a warning', async () => {
+ const { puppeteer } = getTestState();
+ const consoleStub = sinon.stub(console, 'warn');
+ // @ts-expect-error purposeful bad input
+ const browser = await puppeteer.launch({ product: 'SO_NOT_A_PRODUCT' });
+ const userAgent = await browser.userAgent();
+ await browser.close();
+ expect(userAgent).toContain('Chrome');
+ expect(consoleStub.callCount).toEqual(1);
+ expect(consoleStub.firstCall.args).toEqual([
+ 'Warning: unknown product name SO_NOT_A_PRODUCT. Falling back to chrome.',
+ ]);
+ });
+
+ /* We think there's a bug in the FF Windows launcher, or some
+ * combo of that plus it running on CI, but it's hard to track down.
+ * See comment here: https://github.com/puppeteer/puppeteer/issues/5673#issuecomment-670141377.
+ */
+ itOnlyRegularInstall('should be able to launch Firefox', async function () {
+ this.timeout(FIREFOX_TIMEOUT);
+ const { puppeteer } = getTestState();
+ const browser = await puppeteer.launch({ product: 'firefox' });
+ const userAgent = await browser.userAgent();
+ await browser.close();
+ expect(userAgent).toContain('Firefox');
+ });
+ });
+
+ describe('Puppeteer.connect', function () {
+ it('should be able to connect multiple times to the same browser', async () => {
+ const { puppeteer, defaultBrowserOptions } = getTestState();
+
+ const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
+ const otherBrowser = await puppeteer.connect({
+ browserWSEndpoint: originalBrowser.wsEndpoint(),
+ });
+ const page = await otherBrowser.newPage();
+ expect(await page.evaluate(() => 7 * 8)).toBe(56);
+ otherBrowser.disconnect();
+
+ const secondPage = await originalBrowser.newPage();
+ expect(await secondPage.evaluate(() => 7 * 6)).toBe(42);
+ await originalBrowser.close();
+ });
+ it('should be able to close remote browser', async () => {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+
+ const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
+ const remoteBrowser = await puppeteer.connect({
+ browserWSEndpoint: originalBrowser.wsEndpoint(),
+ });
+ await Promise.all([
+ utils.waitEvent(originalBrowser, 'disconnected'),
+ remoteBrowser.close(),
+ ]);
+ });
+ it('should support ignoreHTTPSErrors option', async () => {
+ const {
+ httpsServer,
+ puppeteer,
+ defaultBrowserOptions,
+ } = getTestState();
+
+ const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
+ const browserWSEndpoint = originalBrowser.wsEndpoint();
+
+ const browser = await puppeteer.connect({
+ browserWSEndpoint,
+ ignoreHTTPSErrors: true,
+ });
+ const page = await browser.newPage();
+ let error = null;
+ const [serverRequest, response] = await Promise.all([
+ httpsServer.waitForRequest('/empty.html'),
+ page.goto(httpsServer.EMPTY_PAGE).catch((error_) => (error = error_)),
+ ]);
+ expect(error).toBe(null);
+ expect(response.ok()).toBe(true);
+ expect(response.securityDetails()).toBeTruthy();
+ const protocol = serverRequest.socket.getProtocol().replace('v', ' ');
+ expect(response.securityDetails().protocol()).toBe(protocol);
+ await page.close();
+ await browser.close();
+ });
+ it(
+ 'should be able to reconnect to a disconnected browser',
+ async () => {
+ const { server, puppeteer, defaultBrowserOptions } = getTestState();
+
+ const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
+ const browserWSEndpoint = originalBrowser.wsEndpoint();
+ const page = await originalBrowser.newPage();
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ originalBrowser.disconnect();
+
+ const browser = await puppeteer.connect({ browserWSEndpoint });
+ const pages = await browser.pages();
+ const restoredPage = pages.find(
+ (page) =>
+ page.url() === server.PREFIX + '/frames/nested-frames.html'
+ );
+ expect(utils.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(() => 7 * 8)).toBe(56);
+ await browser.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 } = getTestState();
+
+ const browserOne = await puppeteer.launch();
+ const browserTwo = await puppeteer.connect({
+ browserWSEndpoint: browserOne.wsEndpoint(),
+ });
+ const [page1, page2] = await Promise.all([
+ new Promise<Page>((x) =>
+ browserOne.once('targetcreated', (target) => x(target.page()))
+ ),
+ browserTwo.newPage(),
+ ]);
+ expect(await page1.evaluate(() => 7 * 8)).toBe(56);
+ expect(await page2.evaluate(() => 7 * 6)).toBe(42);
+ await browserOne.close();
+ }
+ );
+ });
+ describe('Puppeteer.executablePath', function () {
+ itOnlyRegularInstall('should work', async () => {
+ const { puppeteer } = getTestState();
+
+ const executablePath = puppeteer.executablePath();
+ expect(fs.existsSync(executablePath)).toBe(true);
+ expect(fs.realpathSync(executablePath)).toBe(executablePath);
+ });
+ });
+ });
+
+ describe('Browser target events', function () {
+ it('should work', async () => {
+ const { server, puppeteer, defaultBrowserOptions } = getTestState();
+
+ const browser = await puppeteer.launch(defaultBrowserOptions);
+ const events = [];
+ 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']);
+ await browser.close();
+ });
+ });
+
+ describe('Browser.Events.disconnected', function () {
+ it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async () => {
+ const { puppeteer, defaultBrowserOptions } = getTestState();
+ const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
+ const browserWSEndpoint = originalBrowser.wsEndpoint();
+ const remoteBrowser1 = await puppeteer.connect({ browserWSEndpoint });
+ const remoteBrowser2 = await puppeteer.connect({ browserWSEndpoint });
+
+ let disconnectedOriginal = 0;
+ let disconnectedRemote1 = 0;
+ let disconnectedRemote2 = 0;
+ originalBrowser.on('disconnected', () => ++disconnectedOriginal);
+ remoteBrowser1.on('disconnected', () => ++disconnectedRemote1);
+ remoteBrowser2.on('disconnected', () => ++disconnectedRemote2);
+
+ await Promise.all([
+ utils.waitEvent(remoteBrowser2, 'disconnected'),
+ remoteBrowser2.disconnect(),
+ ]);
+
+ expect(disconnectedOriginal).toBe(0);
+ expect(disconnectedRemote1).toBe(0);
+ expect(disconnectedRemote2).toBe(1);
+
+ await Promise.all([
+ utils.waitEvent(remoteBrowser1, 'disconnected'),
+ utils.waitEvent(originalBrowser, 'disconnected'),
+ originalBrowser.close(),
+ ]);
+
+ expect(disconnectedOriginal).toBe(1);
+ expect(disconnectedRemote1).toBe(1);
+ expect(disconnectedRemote2).toBe(1);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/mocha-ts-require.js b/remote/test/puppeteer/test/mocha-ts-require.js
new file mode 100644
index 0000000000..a0ac64fa62
--- /dev/null
+++ b/remote/test/puppeteer/test/mocha-ts-require.js
@@ -0,0 +1,11 @@
+const path = require('path');
+
+require('ts-node').register({
+ /**
+ * We ignore the lib/ directory because that's already been TypeScript
+ * compiled and checked. So we don't want to check it again as part of running
+ * the unit tests.
+ */
+ ignore: ['lib/*', 'node_modules'],
+ project: path.join(__dirname, 'tsconfig.test.json'),
+});
diff --git a/remote/test/puppeteer/test/mocha-utils.ts b/remote/test/puppeteer/test/mocha-utils.ts
new file mode 100644
index 0000000000..d13c82701c
--- /dev/null
+++ b/remote/test/puppeteer/test/mocha-utils.ts
@@ -0,0 +1,279 @@
+/**
+ * Copyright 2020 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { TestServer } from '../utils/testserver/index.js';
+import * as path from 'path';
+import * as fs from 'fs';
+import * as os from 'os';
+import sinon from 'sinon';
+import puppeteer from '../lib/cjs/puppeteer/node.js';
+import {
+ Browser,
+ BrowserContext,
+} from '../lib/cjs/puppeteer/common/Browser.js';
+import { Page } from '../lib/cjs/puppeteer/common/Page.js';
+import { PuppeteerNode } from '../lib/cjs/puppeteer/node/Puppeteer.js';
+import utils from './utils.js';
+import rimraf from 'rimraf';
+
+import { trackCoverage } from './coverage-utils.js';
+
+const setupServer = async () => {
+ const assetsPath = path.join(__dirname, 'assets');
+ const cachedPath = path.join(__dirname, 'assets', 'cached');
+
+ const port = 8907;
+ const server = await TestServer.create(assetsPath, 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 httpsPort = port + 1;
+ const httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort);
+ 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 getTestState = (): PuppeteerTestState =>
+ state as PuppeteerTestState;
+
+const product =
+ process.env.PRODUCT || process.env.PUPPETEER_PRODUCT || 'Chromium';
+
+const alternativeInstall = process.env.PUPPETEER_ALT_INSTALL || false;
+
+const isHeadless =
+ (process.env.HEADLESS || 'true').trim().toLowerCase() === 'true';
+const isFirefox = product === 'firefox';
+const isChrome = product === 'Chromium';
+
+let extraLaunchOptions = {};
+try {
+ extraLaunchOptions = JSON.parse(process.env.EXTRA_LAUNCH_OPTIONS || '{}');
+} catch (error) {
+ console.warn(
+ `Error parsing EXTRA_LAUNCH_OPTIONS: ${error.message}. Skipping.`
+ );
+}
+
+const defaultBrowserOptions = Object.assign(
+ {
+ handleSIGINT: true,
+ executablePath: process.env.BINARY,
+ headless: isHeadless,
+ dumpio: !!process.env.DUMPIO,
+ },
+ extraLaunchOptions
+);
+
+(async (): Promise<void> => {
+ if (defaultBrowserOptions.executablePath) {
+ console.warn(
+ `WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}`
+ );
+ } else {
+ // TODO(jackfranklin): declare updateRevision in some form for the Firefox
+ // launcher.
+ // @ts-expect-error _updateRevision is defined on the FF launcher
+ // but not the Chrome one. The types need tidying so that TS can infer that
+ // properly and not error here.
+ if (product === 'firefox') await puppeteer._launcher._updateRevision();
+ 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`
+ );
+ }
+})();
+
+declare module 'expect/build/types' {
+ interface Matchers<R> {
+ toBeGolden(x: string): R;
+ }
+}
+
+const setupGoldenAssertions = (): void => {
+ const suffix = product.toLowerCase();
+ const GOLDEN_DIR = path.join(__dirname, 'golden-' + suffix);
+ const OUTPUT_DIR = path.join(__dirname, 'output-' + suffix);
+ if (fs.existsSync(OUTPUT_DIR)) rimraf.sync(OUTPUT_DIR);
+ utils.extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR);
+};
+
+setupGoldenAssertions();
+
+interface PuppeteerTestState {
+ browser: Browser;
+ context: BrowserContext;
+ page: Page;
+ puppeteer: PuppeteerNode;
+ defaultBrowserOptions: {
+ [x: string]: any;
+ };
+ server: any;
+ httpsServer: any;
+ isFirefox: boolean;
+ isChrome: boolean;
+ isHeadless: boolean;
+ puppeteerPath: string;
+}
+const state: Partial<PuppeteerTestState> = {};
+
+export const itFailsFirefox = (
+ description: string,
+ body: Mocha.Func
+): Mocha.Test => {
+ if (isFirefox) return xit(description, body);
+ else return it(description, body);
+};
+
+export const itChromeOnly = (
+ description: string,
+ body: Mocha.Func
+): Mocha.Test => {
+ if (isChrome) return it(description, body);
+ else return xit(description, body);
+};
+
+export const itOnlyRegularInstall = (
+ description: string,
+ body: Mocha.Func
+): Mocha.Test => {
+ if (alternativeInstall || process.env.BINARY) return xit(description, body);
+ else return it(description, body);
+};
+
+export const itFailsWindowsUntilDate = (
+ date: Date,
+ description: string,
+ body: Mocha.Func
+): Mocha.Test => {
+ if (os.platform() === 'win32' && Date.now() < date.getTime()) {
+ // we are within the deferred time so skip the test
+ return xit(description, body);
+ }
+
+ return it(description, body);
+};
+
+export const itFailsWindows = (description: string, body: Mocha.Func) => {
+ if (os.platform() === 'win32') {
+ return xit(description, body);
+ }
+ return it(description, body);
+};
+
+export const describeFailsFirefox = (
+ description: string,
+ body: (this: Mocha.Suite) => void
+): void | Mocha.Suite => {
+ if (isFirefox) return xdescribe(description, body);
+ else return describe(description, body);
+};
+
+export const describeChromeOnly = (
+ description: string,
+ body: (this: Mocha.Suite) => void
+): Mocha.Suite => {
+ if (isChrome) return describe(description, body);
+};
+
+let coverageHooks = {
+ beforeAll: (): void => {},
+ afterAll: (): void => {},
+};
+
+if (process.env.COVERAGE) {
+ coverageHooks = trackCoverage();
+}
+
+console.log(
+ `Running unit tests with:
+ -> product: ${product}
+ -> binary: ${
+ defaultBrowserOptions.executablePath ||
+ path.relative(process.cwd(), puppeteer.executablePath())
+ }`
+);
+
+export const setupTestBrowserHooks = () => {
+ before(async () => {
+ const browser = await puppeteer.launch(defaultBrowserOptions);
+ state.browser = browser;
+ });
+
+ after(async () => {
+ await state.browser.close();
+ state.browser = null;
+ });
+};
+
+export const setupTestPageAndContextHooks = () => {
+ beforeEach(async () => {
+ state.context = await state.browser.createIncognitoBrowserContext();
+ state.page = await state.context.newPage();
+ });
+
+ afterEach(async () => {
+ await state.context.close();
+ state.context = null;
+ state.page = null;
+ });
+};
+
+export const mochaHooks = {
+ beforeAll: [
+ async () => {
+ const { server, httpsServer } = await setupServer();
+
+ state.puppeteer = puppeteer;
+ state.defaultBrowserOptions = defaultBrowserOptions;
+ state.server = server;
+ state.httpsServer = httpsServer;
+ state.isFirefox = isFirefox;
+ state.isChrome = isChrome;
+ state.isHeadless = isHeadless;
+ state.puppeteerPath = path.resolve(path.join(__dirname, '..'));
+ },
+ coverageHooks.beforeAll,
+ ],
+
+ beforeEach: async () => {
+ state.server.reset();
+ state.httpsServer.reset();
+ },
+
+ afterAll: [
+ async () => {
+ await state.server.stop();
+ state.server = null;
+ await state.httpsServer.stop();
+ state.httpsServer = null;
+ },
+ coverageHooks.afterAll,
+ ],
+
+ afterEach: () => {
+ sinon.restore();
+ },
+};
diff --git a/remote/test/puppeteer/test/mouse.spec.ts b/remote/test/puppeteer/test/mouse.spec.ts
new file mode 100644
index 0000000000..fadd9b9b95
--- /dev/null
+++ b/remote/test/puppeteer/test/mouse.spec.ts
@@ -0,0 +1,241 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import os from 'os';
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+import { KeyInput } from '../lib/cjs/puppeteer/common/USKeyboardLayout.js';
+
+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();
+ setupTestPageAndContextHooks();
+ it('should click the document', async () => {
+ const { page } = getTestState();
+
+ await page.evaluate(() => {
+ globalThis.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<() => MouseEvent>(
+ () => globalThis.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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/textarea.html');
+ const { x, y, width, height } = await page.evaluate<() => Dimensions>(
+ 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>(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 } = 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);
+ // Firefox needs an extra frame here after typing or it will fail to set the scrollTop
+ await page.evaluate(() => new Promise(requestAnimationFrame));
+ await page.evaluate(
+ () => (document.querySelector('textarea').scrollTop = 0)
+ );
+ 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 page.evaluate(() => {
+ const textarea = document.querySelector('textarea');
+ return textarea.value.substring(
+ textarea.selectionStart,
+ textarea.selectionEnd
+ );
+ })
+ ).toBe(text);
+ });
+ it('should trigger hover state', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.hover('#button-6');
+ expect(
+ await page.evaluate(() => document.querySelector('button:hover').id)
+ ).toBe('button-6');
+ await page.hover('#button-2');
+ expect(
+ await page.evaluate(() => document.querySelector('button:hover').id)
+ ).toBe('button-2');
+ await page.hover('#button-91');
+ expect(
+ await page.evaluate(() => document.querySelector('button:hover').id)
+ ).toBe('button-91');
+ });
+ it(
+ 'should trigger hover state with removed window.Node',
+ async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.evaluate(() => delete window.Node);
+ await page.hover('#button-6');
+ expect(
+ await page.evaluate(() => document.querySelector('button:hover').id)
+ ).toBe('button-6');
+ }
+ );
+ it('should set modifier keys on click', async () => {
+ const { page, server, isFirefox } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/scrollable.html');
+ await page.evaluate(() =>
+ document
+ .querySelector('#button-3')
+ .addEventListener('mousedown', (e) => (globalThis.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') delete modifiers['Meta'];
+ for (const [modifier, key] of modifiers) {
+ await page.keyboard.down(modifier);
+ await page.click('#button-3');
+ if (
+ !(await page.evaluate((mod: string) => globalThis.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) => globalThis.lastEvent[mod], key))
+ throw new Error(modifiers[modifier] + ' should be false');
+ }
+ });
+ it('should send mouse wheel events', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/wheel.html');
+ const 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 } = getTestState();
+
+ await page.mouse.move(100, 100);
+ await page.evaluate(() => {
+ globalThis.result = [];
+ document.addEventListener('mousemove', (event) => {
+ globalThis.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 } = 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.result = { x: event.clientX, y: event.clientY };
+ });
+ });
+
+ await page.mouse.click(30, 40);
+
+ expect(await page.evaluate('result')).toEqual({ x: 30, y: 40 });
+ }
+ );
+});
diff --git a/remote/test/puppeteer/test/navigation.spec.ts b/remote/test/puppeteer/test/navigation.spec.ts
new file mode 100644
index 0000000000..205d98a0b0
--- /dev/null
+++ b/remote/test/puppeteer/test/navigation.spec.ts
@@ -0,0 +1,774 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import utils from './utils.js';
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+import os from 'os';
+
+describe('navigation', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+ describe('Page.goto', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(page.url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should work with anchor navigation', async () => {
+ const { page, server } = 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 } = 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 } = 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 } = 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 } = getTestState();
+
+ server.setRoute('/frames/frame.html', (req, res) => {
+ res.statusCode = 204;
+ res.end();
+ });
+ let error = null;
+ await page
+ .goto(server.PREFIX + '/frames/one-frame.html')
+ .catch((error_) => (error = error_));
+ expect(error).toBe(null);
+ });
+ it('should fail when server returns 204', async () => {
+ const { page, server, isChrome } = getTestState();
+
+ server.setRoute('/empty.html', (req, res) => {
+ res.statusCode = 204;
+ res.end();
+ });
+ let error = null;
+ await page.goto(server.EMPTY_PAGE).catch((error_) => (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 } = 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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ window.addEventListener(
+ 'beforeunload',
+ () => 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 } = getTestState();
+
+ const response = await page.goto(server.EMPTY_PAGE, {
+ waitUntil: 'networkidle0',
+ });
+ expect(response.status()).toBe(200);
+ }
+ );
+ it(
+ 'should navigate to empty page with networkidle2',
+ async () => {
+ const { page, server } = 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, isChrome } = getTestState();
+
+ let error = null;
+ await page.goto('asdfasdf').catch((error_) => (error = error_));
+ if (isChrome)
+ expect(error.message).toContain('Cannot navigate to invalid URL');
+ else expect(error.message).toContain('Invalid url');
+ });
+
+ /* If you are running this on pre-Catalina versions of macOS this will fail locally.
+ /* Mac OSX Catalina outputs a different message than other platforms.
+ * See https://support.google.com/chrome/thread/18125056?hl=en for details.
+ * If you're running pre-Catalina Mac OSX this test will fail locally.
+ */
+ const EXPECTED_SSL_CERT_MESSAGE =
+ os.platform() === 'darwin'
+ ? 'net::ERR_CERT_INVALID'
+ : 'net::ERR_CERT_AUTHORITY_INVALID';
+
+ it('should fail when navigating to bad SSL', async () => {
+ const { page, httpsServer, isChrome } = getTestState();
+
+ // Make sure that network events do not emit 'undefined'.
+ // @see https://crbug.com/750469
+ const requests = [];
+ page.on('request', () => requests.push('request'));
+ page.on('requestfinished', () => requests.push('requestfinished'));
+ page.on('requestfailed', () => requests.push('requestfailed'));
+
+ let error = null;
+ await page
+ .goto(httpsServer.EMPTY_PAGE)
+ .catch((error_) => (error = error_));
+ if (isChrome) expect(error.message).toContain(EXPECTED_SSL_CERT_MESSAGE);
+ else expect(error.message).toContain('SSL_ERROR_UNKNOWN');
+
+ expect(requests.length).toBe(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 } = getTestState();
+
+ server.setRedirect('/redirect/1.html', '/redirect/2.html');
+ server.setRedirect('/redirect/2.html', '/empty.html');
+ let error = null;
+ await page
+ .goto(httpsServer.PREFIX + '/redirect/1.html')
+ .catch((error_) => (error = error_));
+ if (isChrome) expect(error.message).toContain(EXPECTED_SSL_CERT_MESSAGE);
+ else expect(error.message).toContain('SSL_ERROR_UNKNOWN');
+ });
+ it('should throw if networkidle is passed as an option', async () => {
+ const { page, server } = getTestState();
+
+ let error = null;
+ await page
+ // @ts-expect-error purposefully passing an old option
+ .goto(server.EMPTY_PAGE, { waitUntil: 'networkidle' })
+ .catch((error_) => (error = error_));
+ expect(error.message).toContain(
+ '"networkidle" option is no longer supported'
+ );
+ });
+ it('should fail when main resources failed to load', async () => {
+ const { page, isChrome } = getTestState();
+
+ let error = null;
+ await page
+ .goto('http://localhost:44123/non-existing-url')
+ .catch((error_) => (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, puppeteer } = getTestState();
+
+ // Hang for request to the empty.html
+ server.setRoute('/empty.html', () => {});
+ let error = null;
+ await page
+ .goto(server.PREFIX + '/empty.html', { timeout: 1 })
+ .catch((error_) => (error = error_));
+ expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should fail when exceeding default maximum navigation timeout', async () => {
+ const { page, server, puppeteer } = getTestState();
+
+ // Hang for request to the empty.html
+ server.setRoute('/empty.html', () => {});
+ let error = null;
+ page.setDefaultNavigationTimeout(1);
+ await page
+ .goto(server.PREFIX + '/empty.html')
+ .catch((error_) => (error = error_));
+ expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should fail when exceeding default maximum timeout', async () => {
+ const { page, server, puppeteer } = getTestState();
+
+ // Hang for request to the empty.html
+ server.setRoute('/empty.html', () => {});
+ let error = null;
+ page.setDefaultTimeout(1);
+ await page
+ .goto(server.PREFIX + '/empty.html')
+ .catch((error_) => (error = error_));
+ expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should prioritize default navigation timeout over default timeout', async () => {
+ const { page, server, puppeteer } = getTestState();
+
+ // Hang for request to the empty.html
+ server.setRoute('/empty.html', () => {});
+ let error = null;
+ page.setDefaultTimeout(0);
+ page.setDefaultNavigationTimeout(1);
+ await page
+ .goto(server.PREFIX + '/empty.html')
+ .catch((error_) => (error = error_));
+ expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should disable timeout when its set to 0', async () => {
+ const { page, server } = getTestState();
+
+ let error = null;
+ let loaded = false;
+ page.once('load', () => (loaded = true));
+ await page
+ .goto(server.PREFIX + '/grid.html', { timeout: 0, waitUntil: ['load'] })
+ .catch((error_) => (error = error_));
+ expect(error).toBe(null);
+ expect(loaded).toBe(true);
+ });
+ it('should work when navigating to valid url', async () => {
+ const { page, server } = 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 } = 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 } = getTestState();
+
+ const response = await page.goto(server.PREFIX + '/not-found');
+ expect(response.ok()).toBe(false);
+ expect(response.status()).toBe(404);
+ });
+ it('should return last response in redirect chain', async () => {
+ const { page, server } = 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 } = getTestState();
+
+ let responses = [];
+ // Hold on to a bunch of requests without answering.
+ server.setRoute('/fetch-request-a.js', (req, res) =>
+ responses.push(res)
+ );
+ server.setRoute('/fetch-request-b.js', (req, res) =>
+ responses.push(res)
+ );
+ server.setRoute('/fetch-request-c.js', (req, res) =>
+ responses.push(res)
+ );
+ server.setRoute('/fetch-request-d.js', (req, res) =>
+ 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'),
+ ]);
+ const secondFetchResourceRequested = server.waitForRequest(
+ '/fetch-request-d.js'
+ );
+
+ // 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',
+ }
+ );
+ // Track when the navigation gets completed.
+ let navigationFinished = false;
+ navigationPromise.then(() => (navigationFinished = true));
+
+ // Wait for the page's 'load' event.
+ await new Promise((fulfill) => page.once('load', fulfill));
+ 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`);
+ }
+
+ const response = await navigationPromise;
+ // Expect navigation to succeed.
+ expect(response.ok()).toBe(true);
+ }
+ );
+ it('should not leak listeners during navigation', async () => {
+ const { page, server } = getTestState();
+
+ let warning = null;
+ const warningHandler = (w) => (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 () => {
+ const { page } = getTestState();
+
+ let warning = null;
+ const warningHandler = (w) => (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 () => {
+ const { context, server } = getTestState();
+
+ let warning = null;
+ const warningHandler = (w) => (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 } = getTestState();
+
+ const requests = [];
+ page.on(
+ 'request',
+ (request) => !utils.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.length).toBe(1);
+ expect(requests[0].url()).toBe(dataURL);
+ }
+ );
+ it(
+ 'should navigate to URL with hash and fire requests without hash',
+ async () => {
+ const { page, server } = getTestState();
+
+ const requests = [];
+ page.on(
+ 'request',
+ (request) => !utils.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.length).toBe(1);
+ expect(requests[0].url()).toBe(server.EMPTY_PAGE);
+ }
+ );
+ it('should work with self requesting page', async () => {
+ const { page, server } = 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 } = getTestState();
+
+ const url = httpsServer.PREFIX + '/redirect/1.html';
+ let error = null;
+ try {
+ await page.goto(url);
+ } catch (error_) {
+ error = error_;
+ }
+ expect(error.message).toContain(url);
+ });
+ it('should send referer', async () => {
+ const { page, server } = getTestState();
+
+ const [request1, request2] = await Promise.all([
+ server.waitForRequest('/grid.html'),
+ server.waitForRequest('/digits/1.png'),
+ page.goto(server.PREFIX + '/grid.html', {
+ referer: 'http://google.com/',
+ }),
+ ]);
+ expect(request1.headers['referer']).toBe('http://google.com/');
+ // Make sure subresources do not inherit referer.
+ expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html');
+ });
+ });
+
+ describe('Page.waitForNavigation', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForNavigation(),
+ page.evaluate(
+ (url: string) => (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 } = getTestState();
+
+ let response = null;
+ server.setRoute('/one-style.css', (req, res) => (response = res));
+ const navigationPromise = page.goto(server.PREFIX + '/one-style.html');
+ const domContentLoadedPromise = page.waitForNavigation({
+ waitUntil: 'domcontentloaded',
+ });
+
+ let bothFired = false;
+ const bothFiredPromise = page
+ .waitForNavigation({
+ waitUntil: ['load', 'domcontentloaded'],
+ })
+ .then(() => (bothFired = true));
+
+ await server.waitForRequest('/one-style.css');
+ await domContentLoadedPromise;
+ expect(bothFired).toBe(false);
+ response.end();
+ await bothFiredPromise;
+ await navigationPromise;
+ });
+ it('should work with clicking on anchor links', async () => {
+ const { page, server } = 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 } = 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 } = 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 } = 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 () => {
+ const { page, server } = getTestState();
+
+ server.setRoute('/frames/style.css', () => {});
+ const navigationPromise = page.goto(
+ server.PREFIX + '/frames/one-frame.html'
+ );
+ const frame = await utils.waitEvent(page, 'frameattached');
+ await new Promise((fulfill) => {
+ page.on('framenavigated', (f) => {
+ if (f === frame) fulfill();
+ });
+ });
+ await Promise.all([
+ frame.evaluate(() => window.stop()),
+ navigationPromise,
+ ]);
+ }
+ );
+ });
+
+ describe('Page.goBack', function () {
+ it('should work', async () => {
+ const { page, server } = 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 } = 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 } = 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 } = 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_) => error_);
+ await server.waitForRequest('/empty.html');
+
+ await page.$eval('iframe', (frame) => frame.remove());
+ const error = await navigationPromise;
+ expect(error.message).toBe('Navigating frame was detached');
+ });
+ it('should return matching responses', async () => {
+ const { page, server } = getTestState();
+
+ // Disable cache: otherwise, chromium will cache similar requests.
+ await page.setCacheEnabled(false);
+ await page.goto(server.EMPTY_PAGE);
+ // Attach three frames.
+ const frames = await Promise.all([
+ utils.attachFrame(page, 'frame1', server.EMPTY_PAGE),
+ utils.attachFrame(page, 'frame2', server.EMPTY_PAGE),
+ utils.attachFrame(page, 'frame3', server.EMPTY_PAGE),
+ ]);
+ // Navigate all frames to the same URL.
+ const serverResponses = [];
+ server.setRoute('/one-style.html', (req, res) =>
+ serverResponses.push(res)
+ );
+ const navigations = [];
+ 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'];
+ for (const i of [1, 2, 0]) {
+ serverResponses[i].end(serverResponseTexts[i]);
+ const response = await navigations[i];
+ expect(response.frame()).toBe(frames[i]);
+ expect(await response.text()).toBe(serverResponseTexts[i]);
+ }
+ });
+ });
+
+ describe('Frame.waitForNavigation', function () {
+ it('should work', async () => {
+ const { page, server } = 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) => (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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/frames/one-frame.html');
+ const frame = page.frames()[1];
+
+ server.setRoute('/empty.html', () => {});
+ let error = null;
+ const navigationPromise = frame
+ .waitForNavigation()
+ .catch((error_) => (error = error_));
+ await Promise.all([
+ server.waitForRequest('/empty.html'),
+ frame.evaluate(() => ((window as any).location = '/empty.html')),
+ ]);
+ await page.$eval('iframe', (frame) => frame.remove());
+ await navigationPromise;
+ expect(error.message).toBe('Navigating frame was detached');
+ });
+ });
+
+ describe('Page.reload', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => (globalThis._foo = 10));
+ await page.reload();
+ expect(await page.evaluate(() => globalThis._foo)).toBe(undefined);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/network.spec.ts b/remote/test/puppeteer/test/network.spec.ts
new file mode 100644
index 0000000000..63b2dfb948
--- /dev/null
+++ b/remote/test/puppeteer/test/network.spec.ts
@@ -0,0 +1,571 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import fs from 'fs';
+import path from 'path';
+import utils from './utils.js';
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('network', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ describe('Page.Events.Request', function () {
+ it('should fire for navigation requests', async () => {
+ const { page, server } = getTestState();
+
+ const requests = [];
+ page.on(
+ 'request',
+ (request) => !utils.isFavicon(request) && requests.push(request)
+ );
+ await page.goto(server.EMPTY_PAGE);
+ expect(requests.length).toBe(1);
+ });
+ it('should fire for iframes', async () => {
+ const { page, server } = getTestState();
+
+ const requests = [];
+ page.on(
+ 'request',
+ (request) => !utils.isFavicon(request) && requests.push(request)
+ );
+ await page.goto(server.EMPTY_PAGE);
+ await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(requests.length).toBe(2);
+ });
+ it('should fire for fetches', async () => {
+ const { page, server } = getTestState();
+
+ const requests = [];
+ page.on(
+ 'request',
+ (request) => !utils.isFavicon(request) && requests.push(request)
+ );
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => fetch('/empty.html'));
+ expect(requests.length).toBe(2);
+ });
+ });
+
+ describe('Request.frame', function () {
+ it('should work for main frame navigation request', async () => {
+ const { page, server } = getTestState();
+
+ const requests = [];
+ page.on(
+ 'request',
+ (request) => !utils.isFavicon(request) && requests.push(request)
+ );
+ await page.goto(server.EMPTY_PAGE);
+ expect(requests.length).toBe(1);
+ expect(requests[0].frame()).toBe(page.mainFrame());
+ });
+ it('should work for subframe navigation request', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const requests = [];
+ page.on(
+ 'request',
+ (request) => !utils.isFavicon(request) && requests.push(request)
+ );
+ await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ expect(requests.length).toBe(1);
+ expect(requests[0].frame()).toBe(page.frames()[1]);
+ });
+ it('should work for fetch requests', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let requests = [];
+ page.on(
+ 'request',
+ (request) => !utils.isFavicon(request) && requests.push(request)
+ );
+ await page.evaluate(() => fetch('/digits/1.png'));
+ requests = requests.filter(
+ (request) => !request.url().includes('favicon')
+ );
+ expect(requests.length).toBe(1);
+ expect(requests[0].frame()).toBe(page.mainFrame());
+ });
+ });
+
+ describe('Request.headers', function () {
+ it('should work', async () => {
+ const { page, server, isChrome } = getTestState();
+
+ const response = await page.goto(server.EMPTY_PAGE);
+ if (isChrome)
+ expect(response.request().headers()['user-agent']).toContain('Chrome');
+ else
+ expect(response.request().headers()['user-agent']).toContain('Firefox');
+ });
+ });
+
+ describe('Response.headers', function () {
+ it('should work', async () => {
+ const { page, server } = 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('Response.fromCache', function () {
+ it('should return |false| for non-cached content', async () => {
+ const { page, server } = getTestState();
+
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response.fromCache()).toBe(false);
+ });
+
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ const responses = new Map();
+ page.on(
+ 'response',
+ (r) =>
+ !utils.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 } = getTestState();
+
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response.fromServiceWorker()).toBe(false);
+ });
+
+ it('Response.fromServiceWorker', async () => {
+ const { page, server } = getTestState();
+
+ const responses = new Map();
+ page.on('response', (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 () => await globalThis.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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ server.setRoute('/post', (req, res) => res.end());
+ let request = null;
+ page.on('request', (r) => (request = r));
+ await page.evaluate(() =>
+ 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 } = getTestState();
+
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response.request().postData()).toBe(undefined);
+ });
+ });
+
+ describe('Response.text', function () {
+ it('should work', async () => {
+ const { page, server } = 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 } = 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 } = getTestState();
+
+ server.setRedirect('/foo.html', '/empty.html');
+ const response = await page.goto(server.PREFIX + '/foo.html');
+ const redirectChain = response.request().redirectChain();
+ expect(redirectChain.length).toBe(1);
+ const redirected = redirectChain[0].response();
+ expect(redirected.status()).toBe(302);
+ let error = null;
+ await redirected.text().catch((error_) => (error = error_));
+ expect(error.message).toContain(
+ 'Response body is unavailable for redirect responses'
+ );
+ });
+ it('should wait until response completes', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ // Setup server to trap request.
+ let serverResponse = null;
+ 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) => (requestFinished = requestFinished || r.url().includes('/get'))
+ );
+ // send request and wait for server response
+ const [pageResponse] = await Promise.all([
+ page.waitForResponse((r) => !utils.isFavicon(r.request())),
+ page.evaluate(() => 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) => serverResponse.write('wor', x));
+ // Finish response.
+ await new Promise((x) => serverResponse.end('ld!', x));
+ expect(await responseText).toBe('hello world!');
+ });
+ });
+
+ describe('Response.json', function () {
+ it('should work', async () => {
+ const { page, server } = 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 } = 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 } = 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);
+ });
+ });
+
+ describe('Response.statusText', function () {
+ it('should work', async () => {
+ const { page, server } = 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!');
+ });
+ });
+
+ describe('Network Events', function () {
+ it('Page.Events.Request', async () => {
+ const { page, server } = getTestState();
+
+ const requests = [];
+ page.on('request', (request) => requests.push(request));
+ await page.goto(server.EMPTY_PAGE);
+ expect(requests.length).toBe(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.Response', async () => {
+ const { page, server } = getTestState();
+
+ const responses = [];
+ page.on('response', (response) => responses.push(response));
+ await page.goto(server.EMPTY_PAGE);
+ expect(responses.length).toBe(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 } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ if (request.url().endsWith('css')) request.abort();
+ else request.continue();
+ });
+ const failedRequests = [];
+ page.on('requestfailed', (request) => failedRequests.push(request));
+ await page.goto(server.PREFIX + '/one-style.html');
+ expect(failedRequests.length).toBe(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 } = getTestState();
+
+ const requests = [];
+ page.on('requestfinished', (request) => requests.push(request));
+ await page.goto(server.EMPTY_PAGE);
+ expect(requests.length).toBe(1);
+ expect(requests[0].url()).toBe(server.EMPTY_PAGE);
+ expect(requests[0].response()).toBeTruthy();
+ expect(requests[0].frame() === page.mainFrame()).toBe(true);
+ expect(requests[0].frame().url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should fire events in proper order', async () => {
+ const { page, server } = getTestState();
+
+ const events = [];
+ page.on('request', () => events.push('request'));
+ page.on('response', () => events.push('response'));
+ page.on('requestfinished', () => events.push('requestfinished'));
+ await page.goto(server.EMPTY_PAGE);
+ expect(events).toEqual(['request', 'response', 'requestfinished']);
+ });
+ it('should support redirects', async () => {
+ const { page, server } = getTestState();
+
+ const events = [];
+ page.on('request', (request) =>
+ events.push(`${request.method()} ${request.url()}`)
+ );
+ page.on('response', (response) =>
+ events.push(`${response.status()} ${response.url()}`)
+ );
+ page.on('requestfinished', (request) =>
+ events.push(`DONE ${request.url()}`)
+ );
+ page.on('requestfailed', (request) =>
+ 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.length).toBe(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 } = getTestState();
+
+ const requests = new Map();
+ page.on('request', (request) =>
+ 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 } = getTestState();
+
+ const requests = new Map();
+ page.on('request', (request) => {
+ requests.set(request.url().split('/').pop(), request);
+ 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 } = getTestState();
+
+ const requests = [];
+ page.on('request', (request) => requests.push(request));
+ await page.goto(server.PREFIX + '/pptr.png');
+ expect(requests[0].isNavigationRequest()).toBe(true);
+ });
+ });
+
+ describe('Page.setExtraHTTPHeaders', function () {
+ it('should work', async () => {
+ const { page, server } = 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 } = getTestState();
+
+ let error = null;
+ try {
+ // @ts-expect-error purposeful bad input
+ await page.setExtraHTTPHeaders({ foo: 1 });
+ } catch (error_) {
+ error = 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 } = getTestState();
+
+ server.setAuth('/empty.html', 'user', 'pass');
+ let response = await page.goto(server.EMPTY_PAGE);
+ expect(response.status()).toBe(401);
+ 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 } = 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 } = 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(null);
+ // Navigate to a different origin to bust Chrome's credential caching.
+ response = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
+ expect(response.status()).toBe(401);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/oopif.spec.ts b/remote/test/puppeteer/test/oopif.spec.ts
new file mode 100644
index 0000000000..845429a69f
--- /dev/null
+++ b/remote/test/puppeteer/test/oopif.spec.ts
@@ -0,0 +1,74 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import { getTestState, describeChromeOnly } from './mocha-utils'; // eslint-disable-line import/extensions
+
+describeChromeOnly('OOPIF', function () {
+ /* We use a special browser for this test as we need the --site-per-process flag */
+ let browser;
+ let context;
+ let page;
+
+ before(async () => {
+ const { puppeteer, defaultBrowserOptions } = getTestState();
+ browser = await puppeteer.launch(
+ Object.assign({}, defaultBrowserOptions, {
+ args: (defaultBrowserOptions.args || []).concat(['--site-per-process']),
+ })
+ );
+ });
+
+ beforeEach(async () => {
+ context = await browser.createIncognitoBrowserContext();
+ page = await context.newPage();
+ });
+
+ afterEach(async () => {
+ await context.close();
+ page = null;
+ context = null;
+ });
+
+ after(async () => {
+ await browser.close();
+ browser = null;
+ });
+ xit('should report oopif frames', async () => {
+ const { server } = getTestState();
+
+ await page.goto(server.PREFIX + '/dynamic-oopif.html');
+ expect(oopifs(context).length).toBe(1);
+ expect(page.frames().length).toBe(2);
+ });
+ it('should load oopif iframes with subresources and request interception', async () => {
+ const { server } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => request.continue());
+ await page.goto(server.PREFIX + '/dynamic-oopif.html');
+ expect(oopifs(context).length).toBe(1);
+ });
+});
+
+/**
+ * @param {!BrowserContext} context
+ */
+function oopifs(context) {
+ return context
+ .targets()
+ .filter((target) => target._targetInfo.type === 'iframe');
+}
diff --git a/remote/test/puppeteer/test/page.spec.ts b/remote/test/puppeteer/test/page.spec.ts
new file mode 100644
index 0000000000..512c26921e
--- /dev/null
+++ b/remote/test/puppeteer/test/page.spec.ts
@@ -0,0 +1,1720 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import fs from 'fs';
+import path from 'path';
+import utils from './utils.js';
+const { waitEvent } = utils;
+import expect from 'expect';
+import sinon from 'sinon';
+import {
+ getTestState,
+ itFailsFirefox,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+import { Page, Metrics } from '../lib/cjs/puppeteer/common/Page.js';
+import { JSHandle } from '../lib/cjs/puppeteer/common/JSHandle.js';
+
+describe('Page', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+ describe('Page.close', function () {
+ it('should reject all promises when page is closed', async () => {
+ const { context } = getTestState();
+
+ const newPage = await context.newPage();
+ let error = null;
+ await Promise.all([
+ newPage
+ .evaluate(() => new Promise(() => {}))
+ .catch((error_) => (error = error_)),
+ newPage.close(),
+ ]);
+ expect(error.message).toContain('Protocol error');
+ });
+ it('should not be visible in browser.pages', async () => {
+ const { browser } = 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 } = 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()).toBe(
+ 'This page is asking you to confirm that you want to leave - data you have entered may not be saved.'
+ );
+ await dialog.accept();
+ await pageClosingPromise;
+ });
+ it('should *not* run beforeunload by default', async () => {
+ const { context, server } = 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 } = 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 } = getTestState();
+
+ const newPage = await context.newPage();
+ const results = await Promise.all([
+ newPage.waitForRequest(server.EMPTY_PAGE).catch((error) => error),
+ newPage.waitForResponse(server.EMPTY_PAGE).catch((error) => error),
+ newPage.close(),
+ ]);
+ for (let i = 0; i < 2; i++) {
+ const message = results[i].message;
+ expect(message).toContain('Target closed');
+ expect(message).not.toContain('Timeout');
+ }
+ });
+ });
+
+ describe('Page.Events.Load', function () {
+ it('should fire when expected', async () => {
+ const { page } = getTestState();
+
+ await Promise.all([
+ page.goto('about:blank'),
+ utils.waitEvent(page, 'load'),
+ ]);
+ });
+ });
+
+ // This test fails on Firefox on CI consistently but cannot be replicated
+ // locally. Skipping for now to unblock the Mitt release and given FF support
+ // isn't fully done yet but raising an issue to ask the FF folks to have a
+ // look at this.
+ describe('removing and adding event handlers', () => {
+ it('should correctly fire event handlers as they are added and then removed', async () => {
+ const { page, server } = getTestState();
+
+ const handler = sinon.spy();
+ page.on('response', handler);
+ await page.goto(server.EMPTY_PAGE);
+ expect(handler.callCount).toBe(1);
+ page.off('response', handler);
+ await page.goto(server.EMPTY_PAGE);
+ // Still one because we removed the handler.
+ expect(handler.callCount).toBe(1);
+ page.on('response', handler);
+ await page.goto(server.EMPTY_PAGE);
+ // Two now because we added the handler back.
+ expect(handler.callCount).toBe(2);
+ });
+ });
+
+ describe('Page.Events.error', function () {
+ it('should throw when page crashes', async () => {
+ const { page } = getTestState();
+
+ let error = null;
+ page.on('error', (err) => (error = err));
+ page.goto('chrome://crash').catch(() => {});
+ await waitEvent(page, 'error');
+ expect(error.message).toBe('Page crashed!');
+ });
+ });
+
+ describe('Page.Events.Popup', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ const [popup] = await Promise.all([
+ new Promise<Page>((x) => page.once('popup', x)),
+ page.evaluate(() => window.open('about:blank')),
+ ]);
+ expect(await page.evaluate(() => !!window.opener)).toBe(false);
+ expect(await popup.evaluate(() => !!window.opener)).toBe(true);
+ });
+ it('should work with noopener', async () => {
+ const { page } = getTestState();
+
+ const [popup] = await Promise.all([
+ new Promise<Page>((x) => page.once('popup', x)),
+ page.evaluate(() => window.open('about:blank', null, 'noopener')),
+ ]);
+ expect(await page.evaluate(() => !!window.opener)).toBe(false);
+ expect(await popup.evaluate(() => !!window.opener)).toBe(false);
+ });
+ it('should work with clicking target=_blank', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setContent('<a target=_blank href="/one-style.html">yo</a>');
+ const [popup] = await Promise.all([
+ new Promise<Page>((x) => page.once('popup', x)),
+ page.click('a'),
+ ]);
+ expect(await page.evaluate(() => !!window.opener)).toBe(false);
+ expect(await popup.evaluate(() => !!window.opener)).toBe(true);
+ });
+ it('should work with fake-clicking target=_blank and rel=noopener', async () => {
+ const { page, server } = 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([
+ new Promise<Page>((x) => page.once('popup', x)),
+ page.$eval('a', (a: HTMLAnchorElement) => a.click()),
+ ]);
+ expect(await page.evaluate(() => !!window.opener)).toBe(false);
+ expect(await popup.evaluate(() => !!window.opener)).toBe(false);
+ });
+ it('should work with clicking target=_blank and rel=noopener', async () => {
+ const { page, server } = 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([
+ new Promise<Page>((x) => page.once('popup', x)),
+ page.click('a'),
+ ]);
+ expect(await page.evaluate(() => !!window.opener)).toBe(false);
+ expect(await popup.evaluate(() => !!window.opener)).toBe(false);
+ });
+ });
+
+ describe('BrowserContext.overridePermissions', function () {
+ function getPermission(page, name) {
+ return page.evaluate(
+ (name) =>
+ navigator.permissions.query({ name }).then((result) => result.state),
+ name
+ );
+ }
+
+ it('should be prompt by default', async () => {
+ const { page, server } = 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 } = 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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let error = null;
+ await context
+ .overridePermissions(server.EMPTY_PAGE, ['foo'])
+ .catch((error_) => (error = error_));
+ expect(error.message).toBe('Unknown permission: foo');
+ });
+ it('should grant permission when listed', async () => {
+ const { page, server, context } = 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 } = 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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ globalThis.events = [];
+ return navigator.permissions
+ .query({ name: 'geolocation' })
+ .then(function (result) {
+ globalThis.events.push(result.state);
+ result.onchange = function () {
+ globalThis.events.push(result.state);
+ };
+ });
+ });
+ expect(await page.evaluate(() => globalThis.events)).toEqual(['prompt']);
+ await context.overridePermissions(server.EMPTY_PAGE, []);
+ expect(await page.evaluate(() => globalThis.events)).toEqual([
+ 'prompt',
+ 'denied',
+ ]);
+ await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']);
+ expect(await page.evaluate(() => globalThis.events)).toEqual([
+ 'prompt',
+ 'denied',
+ 'granted',
+ ]);
+ await context.clearPermissionOverrides();
+ expect(await page.evaluate(() => globalThis.events)).toEqual([
+ 'prompt',
+ 'denied',
+ 'granted',
+ 'prompt',
+ ]);
+ });
+ it(
+ 'should isolate permissions between browser contexs',
+ async () => {
+ const { page, server, context, browser } = 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();
+ }
+ );
+ });
+
+ describe('Page.setGeolocation', function () {
+ it('should work', async () => {
+ const { page, server, context } = 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(
+ () =>
+ new Promise((resolve) =>
+ 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 } = getTestState();
+
+ let error = null;
+ try {
+ await page.setGeolocation({ longitude: 200, latitude: 10 });
+ } catch (error_) {
+ error = error_;
+ }
+ expect(error.message).toContain('Invalid longitude "200"');
+ });
+ });
+
+ describe('Page.setOfflineMode', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.setOfflineMode(true);
+ let error = null;
+ await page.goto(server.EMPTY_PAGE).catch((error_) => (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 } = getTestState();
+
+ expect(await page.evaluate(() => window.navigator.onLine)).toBe(true);
+ await page.setOfflineMode(true);
+ expect(await page.evaluate(() => window.navigator.onLine)).toBe(false);
+ await page.setOfflineMode(false);
+ expect(await page.evaluate(() => window.navigator.onLine)).toBe(true);
+ });
+ });
+
+ describe('ExecutionContext.queryObjects', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ // Instantiate an object
+ await page.evaluate(() => (globalThis.set = new Set(['hello', 'world'])));
+ const prototypeHandle = await page.evaluateHandle(() => Set.prototype);
+ const objectsHandle = await page.queryObjects(prototypeHandle);
+ const count = await page.evaluate(
+ (objects: JSHandle[]) => objects.length,
+ objectsHandle
+ );
+ expect(count).toBe(1);
+ const values = await page.evaluate(
+ (objects) => Array.from(objects[0].values()),
+ objectsHandle
+ );
+ expect(values).toEqual(['hello', 'world']);
+ });
+ it('should work for non-blank page', async () => {
+ const { page, server } = getTestState();
+
+ // Instantiate an object
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => (globalThis.set = new Set(['hello', 'world'])));
+ const prototypeHandle = await page.evaluateHandle(() => Set.prototype);
+ const objectsHandle = await page.queryObjects(prototypeHandle);
+ const count = await page.evaluate(
+ (objects: JSHandle[]) => objects.length,
+ objectsHandle
+ );
+ expect(count).toBe(1);
+ });
+ it('should fail for disposed handles', async () => {
+ const { page } = getTestState();
+
+ const prototypeHandle = await page.evaluateHandle(
+ () => HTMLBodyElement.prototype
+ );
+ await prototypeHandle.dispose();
+ let error = null;
+ await page
+ .queryObjects(prototypeHandle)
+ .catch((error_) => (error = error_));
+ expect(error.message).toBe('Prototype JSHandle is disposed!');
+ });
+ it('should fail primitive values as prototypes', async () => {
+ const { page } = getTestState();
+
+ const prototypeHandle = await page.evaluateHandle(() => 42);
+ let error = null;
+ await page
+ .queryObjects(prototypeHandle)
+ .catch((error_) => (error = error_));
+ expect(error.message).toBe(
+ 'Prototype JSHandle must not be referencing primitive value'
+ );
+ });
+ });
+
+ describe('Page.Events.Console', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ let message = null;
+ page.once('console', (m) => (message = m));
+ await Promise.all([
+ page.evaluate(() => console.log('hello', 5, { foo: 'bar' })),
+ waitEvent(page, 'console'),
+ ]);
+ 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 for different console API calls', async () => {
+ const { page } = getTestState();
+
+ const messages = [];
+ page.on('console', (msg) => 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');
+ 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) => msg.type())).toEqual([
+ 'timeEnd',
+ 'trace',
+ 'dir',
+ 'warning',
+ 'error',
+ 'log',
+ ]);
+ expect(messages[0].text()).toContain('calling console.time');
+ expect(messages.slice(1).map((msg) => msg.text())).toEqual([
+ 'calling console.trace',
+ 'calling console.dir',
+ 'calling console.warn',
+ 'calling console.error',
+ 'JSHandle@promise',
+ ]);
+ });
+ it('should not fail for window object', async () => {
+ const { page } = getTestState();
+
+ let message = null;
+ page.once('console', (msg) => (message = msg));
+ await Promise.all([
+ page.evaluate(() => console.error(window)),
+ waitEvent(page, 'console'),
+ ]);
+ expect(message.text()).toBe('JSHandle@object');
+ });
+ it('should trigger correct Log', async () => {
+ const { page, server, isChrome } = getTestState();
+
+ await page.goto('about:blank');
+ const [message] = await Promise.all([
+ waitEvent(page, 'console'),
+ page.evaluate(
+ async (url: string) => 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 } = 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 } = 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 } = 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) => (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) => (frame.onload = x));
+ // 3. After that, remove the iframe.
+ frame.remove();
+ });
+ const popupTarget = page
+ .browserContext()
+ .targets()
+ .find((target) => 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 } = getTestState();
+
+ page.goto('about:blank');
+ await waitEvent(page, 'domcontentloaded');
+ });
+ });
+
+ describe('Page.metrics', function () {
+ it('should get metrics from a page', async () => {
+ const { page } = getTestState();
+
+ await page.goto('about:blank');
+ const metrics = await page.metrics();
+ checkMetrics(metrics);
+ });
+ it('metrics event fired on console.timeStamp', async () => {
+ const { page } = getTestState();
+
+ const metricsPromise = new Promise<{ metrics: Metrics; title: string }>(
+ (fulfill) => page.once('metrics', fulfill)
+ );
+ await page.evaluate(() => console.timeStamp('test42'));
+ const metrics = await metricsPromise;
+ expect(metrics.title).toBe('test42');
+ checkMetrics(metrics.metrics);
+ });
+ function checkMetrics(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]).toBeGreaterThanOrEqual(0);
+ metricsToCheck.delete(name);
+ }
+ expect(metricsToCheck.size).toBe(0);
+ }
+ });
+
+ describe('Page.waitForRequest', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ page.waitForRequest(server.PREFIX + '/digits/2.png'),
+ page.evaluate(() => {
+ fetch('/digits/1.png');
+ fetch('/digits/2.png');
+ fetch('/digits/3.png');
+ }),
+ ]);
+ expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should work with predicate', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ page.waitForRequest(
+ (request) => request.url() === server.PREFIX + '/digits/2.png'
+ ),
+ page.evaluate(() => {
+ fetch('/digits/1.png');
+ fetch('/digits/2.png');
+ fetch('/digits/3.png');
+ }),
+ ]);
+ expect(request.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should respect timeout', async () => {
+ const { page, puppeteer } = getTestState();
+
+ let error = null;
+ await page
+ .waitForRequest(() => false, { timeout: 1 })
+ .catch((error_) => (error = error_));
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should respect default timeout', async () => {
+ const { page, puppeteer } = getTestState();
+
+ let error = null;
+ page.setDefaultTimeout(1);
+ await page
+ .waitForRequest(() => false)
+ .catch((error_) => (error = error_));
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should work with no timeout', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ page.waitForRequest(server.PREFIX + '/digits/2.png', { timeout: 0 }),
+ page.evaluate(() =>
+ setTimeout(() => {
+ fetch('/digits/1.png');
+ fetch('/digits/2.png');
+ 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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForResponse(server.PREFIX + '/digits/2.png'),
+ page.evaluate(() => {
+ fetch('/digits/1.png');
+ fetch('/digits/2.png');
+ fetch('/digits/3.png');
+ }),
+ ]);
+ expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should respect timeout', async () => {
+ const { page, puppeteer } = getTestState();
+
+ let error = null;
+ await page
+ .waitForResponse(() => false, { timeout: 1 })
+ .catch((error_) => (error = error_));
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should respect default timeout', async () => {
+ const { page, puppeteer } = getTestState();
+
+ let error = null;
+ page.setDefaultTimeout(1);
+ await page
+ .waitForResponse(() => false)
+ .catch((error_) => (error = error_));
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should work with predicate', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForResponse(
+ (response) => response.url() === server.PREFIX + '/digits/2.png'
+ ),
+ page.evaluate(() => {
+ fetch('/digits/1.png');
+ fetch('/digits/2.png');
+ fetch('/digits/3.png');
+ }),
+ ]);
+ expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ it('should work with no timeout', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [response] = await Promise.all([
+ page.waitForResponse(server.PREFIX + '/digits/2.png', { timeout: 0 }),
+ page.evaluate(() =>
+ setTimeout(() => {
+ fetch('/digits/1.png');
+ fetch('/digits/2.png');
+ fetch('/digits/3.png');
+ }, 50)
+ ),
+ ]);
+ expect(response.url()).toBe(server.PREFIX + '/digits/2.png');
+ });
+ });
+
+ describe('Page.exposeFunction', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ await page.exposeFunction('compute', function (a, b) {
+ return a * b;
+ });
+ const result = await page.evaluate(async function () {
+ return await globalThis.compute(9, 4);
+ });
+ expect(result).toBe(36);
+ });
+ it('should throw exception in page context', async () => {
+ const { page } = getTestState();
+
+ await page.exposeFunction('woof', function () {
+ throw new Error('WOOF WOOF');
+ });
+ const { message, stack } = await page.evaluate(async () => {
+ try {
+ await globalThis.woof();
+ } catch (error) {
+ return { message: error.message, stack: error.stack };
+ }
+ });
+ expect(message).toBe('WOOF WOOF');
+ expect(stack).toContain(__filename);
+ });
+ it('should support throwing "null"', async () => {
+ const { page } = getTestState();
+
+ await page.exposeFunction('woof', function () {
+ throw null;
+ });
+ const thrown = await page.evaluate(async () => {
+ try {
+ await globalThis.woof();
+ } catch (error) {
+ return error;
+ }
+ });
+ expect(thrown).toBe(null);
+ });
+ it('should be callable from-inside evaluateOnNewDocument', async () => {
+ const { page } = getTestState();
+
+ let called = false;
+ await page.exposeFunction('woof', function () {
+ called = true;
+ });
+ await page.evaluateOnNewDocument(() => globalThis.woof());
+ await page.reload();
+ expect(called).toBe(true);
+ });
+ it('should survive navigation', async () => {
+ const { page, server } = getTestState();
+
+ await page.exposeFunction('compute', function (a, b) {
+ return a * b;
+ });
+
+ await page.goto(server.EMPTY_PAGE);
+ const result = await page.evaluate(async function () {
+ return await globalThis.compute(9, 4);
+ });
+ expect(result).toBe(36);
+ });
+ it('should await returned promise', async () => {
+ const { page } = getTestState();
+
+ await page.exposeFunction('compute', function (a, b) {
+ return Promise.resolve(a * b);
+ });
+
+ const result = await page.evaluate(async function () {
+ return await globalThis.compute(3, 5);
+ });
+ expect(result).toBe(15);
+ });
+ it('should work on frames', async () => {
+ const { page, server } = getTestState();
+
+ await page.exposeFunction('compute', function (a, b) {
+ 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 await globalThis.compute(3, 5);
+ });
+ expect(result).toBe(15);
+ });
+ it('should work on frames before navigation', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ await page.exposeFunction('compute', function (a, b) {
+ return Promise.resolve(a * b);
+ });
+
+ const frame = page.frames()[1];
+ const result = await frame.evaluate(async function () {
+ return await globalThis.compute(3, 5);
+ });
+ expect(result).toBe(15);
+ });
+ it('should work with complex objects', async () => {
+ const { page } = getTestState();
+
+ await page.exposeFunction('complexObject', function (a, b) {
+ return { x: a.x + b.x };
+ });
+ const result = await page.evaluate<() => Promise<{ x: number }>>(
+ async () => globalThis.complexObject({ x: 5 }, { x: 2 })
+ );
+ expect(result.x).toBe(7);
+ });
+ });
+
+ describe('Page.Events.PageError', function () {
+ it('should fire', async () => {
+ const { page, server } = getTestState();
+
+ let error = null;
+ page.once('pageerror', (e) => (error = e));
+ await Promise.all([
+ page.goto(server.PREFIX + '/error.html'),
+ waitEvent(page, 'pageerror'),
+ ]);
+ expect(error.message).toContain('Fancy');
+ });
+ });
+
+ describe('Page.setUserAgent', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ expect(await page.evaluate(() => 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 } = getTestState();
+
+ expect(await page.evaluate(() => navigator.userAgent)).toContain(
+ 'Mozilla'
+ );
+ await page.setUserAgent('foobar');
+ const [request] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ utils.attachFrame(page, 'frame1', server.EMPTY_PAGE),
+ ]);
+ expect(request.headers['user-agent']).toBe('foobar');
+ });
+ it('should emulate device user-agent', async () => {
+ const { page, server, puppeteer } = getTestState();
+
+ await page.goto(server.PREFIX + '/mobile.html');
+ expect(await page.evaluate(() => navigator.userAgent)).not.toContain(
+ 'iPhone'
+ );
+ await page.setUserAgent(puppeteer.devices['iPhone 6'].userAgent);
+ expect(await page.evaluate(() => navigator.userAgent)).toContain(
+ 'iPhone'
+ );
+ });
+ });
+
+ describe('Page.setContent', function () {
+ const expectedOutput =
+ '<html><head></head><body><div>hello</div></body></html>';
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<div>hello</div>');
+ const result = await page.content();
+ expect(result).toBe(expectedOutput);
+ });
+ it('should work with doctype', async () => {
+ const { page } = 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 } = 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, puppeteer } = getTestState();
+
+ const imgPath = '/img.png';
+ // stall for image
+ server.setRoute(imgPath, () => {});
+ let error = null;
+ await page
+ .setContent(`<img src="${server.PREFIX + imgPath}"></img>`, {
+ timeout: 1,
+ })
+ .catch((error_) => (error = error_));
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should respect default navigation timeout', async () => {
+ const { page, server, puppeteer } = getTestState();
+
+ page.setDefaultNavigationTimeout(1);
+ const imgPath = '/img.png';
+ // stall for image
+ server.setRoute(imgPath, () => {});
+ let error = null;
+ await page
+ .setContent(`<img src="${server.PREFIX + imgPath}"></img>`)
+ .catch((error_) => (error = error_));
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should await resources to load', async () => {
+ const { page, server } = getTestState();
+
+ const imgPath = '/img.png';
+ let imgResponse = null;
+ server.setRoute(imgPath, (req, res) => (imgResponse = res));
+ let loaded = false;
+ const contentPromise = page
+ .setContent(`<img src="${server.PREFIX + imgPath}"></img>`)
+ .then(() => (loaded = true));
+ await server.waitForRequest(imgPath);
+ expect(loaded).toBe(false);
+ imgResponse.end();
+ await contentPromise;
+ });
+ it('should work fast enough', async () => {
+ const { page } = getTestState();
+
+ for (let i = 0; i < 20; ++i) await page.setContent('<div>yo</div>');
+ });
+ it('should work with tricky content', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<div>hello world</div>' + '\x7F');
+ expect(await page.$eval('div', (div) => div.textContent)).toBe(
+ 'hello world'
+ );
+ });
+ it('should work with accents', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<div>aberración</div>');
+ expect(await page.$eval('div', (div) => div.textContent)).toBe(
+ 'aberración'
+ );
+ });
+ it('should work with emojis', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<div>🐥</div>');
+ expect(await page.$eval('div', (div) => div.textContent)).toBe('🐥');
+ });
+ it('should work with newline', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<div>\n</div>');
+ expect(await page.$eval('div', (div) => div.textContent)).toBe('\n');
+ });
+ });
+
+ describe('Page.setBypassCSP', function () {
+ it('should bypass CSP meta tag', async () => {
+ const { page, server } = getTestState();
+
+ // Make sure CSP prohibits addScriptTag.
+ await page.goto(server.PREFIX + '/csp.html');
+ await page
+ .addScriptTag({ content: 'window.__injected = 42;' })
+ .catch((error) => void error);
+ expect(await page.evaluate(() => globalThis.__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(() => globalThis.__injected)).toBe(42);
+ });
+
+ it('should bypass CSP header', async () => {
+ const { page, server } = 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) => void error);
+ expect(await page.evaluate(() => globalThis.__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(() => globalThis.__injected)).toBe(42);
+ });
+
+ it('should bypass after cross-process navigation', async () => {
+ const { page, server } = getTestState();
+
+ await page.setBypassCSP(true);
+ await page.goto(server.PREFIX + '/csp.html');
+ await page.addScriptTag({ content: 'window.__injected = 42;' });
+ expect(await page.evaluate(() => globalThis.__injected)).toBe(42);
+
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html');
+ await page.addScriptTag({ content: 'window.__injected = 42;' });
+ expect(await page.evaluate(() => globalThis.__injected)).toBe(42);
+ });
+ it('should bypass CSP in iframes as well', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ {
+ // Make sure CSP prohibits addScriptTag in an iframe.
+ const frame = await utils.attachFrame(
+ page,
+ 'frame1',
+ server.PREFIX + '/csp.html'
+ );
+ await frame
+ .addScriptTag({ content: 'window.__injected = 42;' })
+ .catch((error) => void error);
+ expect(await frame.evaluate(() => globalThis.__injected)).toBe(
+ undefined
+ );
+ }
+
+ // By-pass CSP and try one more time.
+ await page.setBypassCSP(true);
+ await page.reload();
+
+ {
+ const frame = await utils.attachFrame(
+ page,
+ 'frame1',
+ server.PREFIX + '/csp.html'
+ );
+ await frame
+ .addScriptTag({ content: 'window.__injected = 42;' })
+ .catch((error) => void error);
+ expect(await frame.evaluate(() => globalThis.__injected)).toBe(42);
+ }
+ });
+ });
+
+ describe('Page.addScriptTag', function () {
+ it('should throw an error if no options are provided', async () => {
+ const { page } = getTestState();
+
+ let error = null;
+ try {
+ // @ts-expect-error purposefully passing bad options
+ await page.addScriptTag('/injectedfile.js');
+ } catch (error_) {
+ error = error_;
+ }
+ expect(error.message).toBe(
+ 'Provide an object with a `url`, `path` or `content` property'
+ );
+ });
+
+ it('should work with a url', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const scriptHandle = await page.addScriptTag({ url: '/injectedfile.js' });
+ expect(scriptHandle.asElement()).not.toBeNull();
+ expect(await page.evaluate(() => globalThis.__injected)).toBe(42);
+ });
+
+ it('should work with a url and type=module', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addScriptTag({ url: '/es6/es6import.js', type: 'module' });
+ expect(await page.evaluate(() => globalThis.__es6injected)).toBe(42);
+ });
+
+ it('should work with a path and type=module', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addScriptTag({
+ path: path.join(__dirname, 'assets/es6/es6pathimport.js'),
+ type: 'module',
+ });
+ await page.waitForFunction('window.__es6injected');
+ expect(await page.evaluate(() => globalThis.__es6injected)).toBe(42);
+ });
+
+ it('should work with a content and type=module', async () => {
+ const { page, server } = 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('window.__es6injected');
+ expect(await page.evaluate(() => globalThis.__es6injected)).toBe(42);
+ });
+
+ it('should throw an error if loading from url fail', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let error = null;
+ try {
+ await page.addScriptTag({ url: '/nonexistfile.js' });
+ } catch (error_) {
+ error = error_;
+ }
+ expect(error.message).toBe('Loading script from /nonexistfile.js failed');
+ });
+
+ it('should work with a path', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const scriptHandle = await page.addScriptTag({
+ path: path.join(__dirname, 'assets/injectedfile.js'),
+ });
+ expect(scriptHandle.asElement()).not.toBeNull();
+ expect(await page.evaluate(() => globalThis.__injected)).toBe(42);
+ });
+
+ it('should include sourcemap when path is provided', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addScriptTag({
+ path: path.join(__dirname, 'assets/injectedfile.js'),
+ });
+ const result = await page.evaluate(
+ () => globalThis.__injectedError.stack
+ );
+ expect(result).toContain(path.join('assets', 'injectedfile.js'));
+ });
+
+ it('should work with content', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const scriptHandle = await page.addScriptTag({
+ content: 'window.__injected = 35;',
+ });
+ expect(scriptHandle.asElement()).not.toBeNull();
+ expect(await page.evaluate(() => globalThis.__injected)).toBe(35);
+ });
+
+ // @see https://github.com/puppeteer/puppeteer/issues/4840
+ xit('should throw when added with content to the CSP page', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/csp.html');
+ let error = null;
+ await page
+ .addScriptTag({ content: 'window.__injected = 35;' })
+ .catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ });
+
+ it('should throw when added with URL to the CSP page', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/csp.html');
+ let error = null;
+ await page
+ .addScriptTag({ url: server.CROSS_PROCESS_PREFIX + '/injectedfile.js' })
+ .catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ });
+ });
+
+ describe('Page.addStyleTag', function () {
+ it('should throw an error if no options are provided', async () => {
+ const { page } = getTestState();
+
+ let error = null;
+ try {
+ // @ts-expect-error purposefully passing bad input
+ await page.addStyleTag('/injectedstyle.css');
+ } catch (error_) {
+ error = error_;
+ }
+ expect(error.message).toBe(
+ 'Provide an object with a `url`, `path` or `content` property'
+ );
+ });
+
+ it('should work with a url', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const 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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let error = null;
+ try {
+ await page.addStyleTag({ url: '/nonexistfile.js' });
+ } catch (error_) {
+ error = error_;
+ }
+ expect(error.message).toBe('Loading style from /nonexistfile.js failed');
+ });
+
+ it('should work with a path', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const 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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.addStyleTag({
+ path: path.join(__dirname, 'assets/injectedstyle.css'),
+ });
+ const styleHandle = await page.$('style');
+ const styleContent = await page.evaluate(
+ (style: HTMLStyleElement) => style.innerHTML,
+ styleHandle
+ );
+ expect(styleContent).toContain(path.join('assets', 'injectedstyle.css'));
+ });
+
+ it('should work with content', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const 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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/csp.html');
+ let error = null;
+ await page
+ .addStyleTag({ content: 'body { background-color: green; }' })
+ .catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ }
+ );
+
+ it('should throw when added with URL to the CSP page', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/csp.html');
+ let error = null;
+ await page
+ .addStyleTag({
+ url: server.CROSS_PROCESS_PREFIX + '/injectedstyle.css',
+ })
+ .catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ });
+ });
+
+ describe('Page.url', function () {
+ it('should work', async () => {
+ const { page, server } = 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 } = getTestState();
+
+ await page.setJavaScriptEnabled(false);
+ await page.goto(
+ 'data:text/html, <script>var something = "forbidden"</script>'
+ );
+ let error = null;
+ await page.evaluate('something').catch((error_) => (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 } = 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 } = 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('printing to PDF', function () {
+ it('can print to PDF and save to file', async () => {
+ // Printing to pdf is currently only supported in headless
+ const { isHeadless, page } = getTestState();
+
+ if (!isHeadless) return;
+
+ const outputFile = __dirname + '/assets/output.pdf';
+ await page.pdf({ path: outputFile });
+ expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0);
+ fs.unlinkSync(outputFile);
+ });
+ });
+
+ describe('Page.title', function () {
+ it('should return the page title', async () => {
+ const { page, server } = 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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('select', 'blue');
+ expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([
+ 'blue',
+ ]);
+ expect(await page.evaluate(() => globalThis.result.onChange)).toEqual([
+ 'blue',
+ ]);
+ });
+ it('should select only first option', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('select', 'blue', 'green', 'red');
+ expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([
+ 'blue',
+ ]);
+ expect(await page.evaluate(() => globalThis.result.onChange)).toEqual([
+ 'blue',
+ ]);
+ });
+ it('should not throw when select causes navigation', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.$eval('select', (select) =>
+ select.addEventListener(
+ 'input',
+ () => ((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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.evaluate(() => globalThis.makeMultiple());
+ await page.select('select', 'blue', 'green', 'red');
+ expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([
+ 'blue',
+ 'green',
+ 'red',
+ ]);
+ expect(await page.evaluate(() => globalThis.result.onChange)).toEqual([
+ 'blue',
+ 'green',
+ 'red',
+ ]);
+ });
+ it('should respect event bubbling', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('select', 'blue');
+ expect(
+ await page.evaluate(() => globalThis.result.onBubblingInput)
+ ).toEqual(['blue']);
+ expect(
+ await page.evaluate(() => globalThis.result.onBubblingChange)
+ ).toEqual(['blue']);
+ });
+ it('should throw when element is not a <select>', async () => {
+ const { page, server } = getTestState();
+
+ let error = null;
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.select('body', '').catch((error_) => (error = error_));
+ expect(error.message).toContain('Element is not a <select> element.');
+ });
+ it('should return [] on no matched values', async () => {
+ const { page, server } = 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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.evaluate(() => globalThis.makeMultiple());
+ const result = await page.select('select', 'blue', 'black', 'magenta');
+ expect(
+ result.reduce(
+ (accumulator, current) =>
+ ['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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ const result = await page.select(
+ 'select',
+ '42',
+ 'blue',
+ 'black',
+ 'magenta'
+ );
+ expect(result.length).toEqual(1);
+ });
+ it('should return [] on no values', async () => {
+ const { page, server } = 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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.evaluate(() => globalThis.makeMultiple());
+ await page.select('select', 'blue', 'black', 'magenta');
+ await page.select('select');
+ expect(
+ await page.$eval('select', (select: HTMLSelectElement) =>
+ Array.from(select.options).every(
+ (option: HTMLOptionElement) => !option.selected
+ )
+ )
+ ).toEqual(true);
+ });
+ it('should deselect all options when passed no values for a select without multiple', async () => {
+ const { page, server } = 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: HTMLSelectElement) =>
+ Array.from(select.options).every(
+ (option: HTMLOptionElement) => !option.selected
+ )
+ )
+ ).toEqual(true);
+ });
+ it('should throw if passed in non-strings', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<select><option value="12"/></select>');
+ let error = null;
+ try {
+ // @ts-expect-error purposefully passing bad input
+ await page.select('select', 12);
+ } catch (error_) {
+ error = 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 } = getTestState();
+
+ await page.goto(server.PREFIX + '/input/select.html');
+ await page.evaluate(() => (window.Event = null));
+ await page.select('select', 'blue');
+ expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([
+ 'blue',
+ ]);
+ expect(await page.evaluate(() => globalThis.result.onChange)).toEqual([
+ 'blue',
+ ]);
+ }
+ );
+ });
+
+ describe('Page.Events.Close', function () {
+ itFailsFirefox('should work with window.close', async () => {
+ const { page, context } = getTestState();
+
+ const newPagePromise = new Promise<Page>((fulfill) =>
+ context.once('targetcreated', (target) => fulfill(target.page()))
+ );
+ await page.evaluate(
+ () => (window['newPage'] = window.open('about:blank'))
+ );
+ const newPage = await newPagePromise;
+ const closedPromise = new Promise((x) => newPage.on('close', x));
+ await page.evaluate(() => window['newPage'].close());
+ await closedPromise;
+ });
+ it('should work with page.close', async () => {
+ const { context } = getTestState();
+
+ const newPage = await context.newPage();
+ const closedPromise = new Promise((x) => newPage.on('close', x));
+ await newPage.close();
+ await closedPromise;
+ });
+ });
+
+ describe('Page.browser', function () {
+ it('should return the correct browser instance', async () => {
+ const { page, browser } = getTestState();
+
+ expect(page.browser()).toBe(browser);
+ });
+ });
+
+ describe('Page.browserContext', function () {
+ it('should return the correct browser instance', async () => {
+ const { page, context } = getTestState();
+
+ expect(page.browserContext()).toBe(context);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/queryselector.spec.ts b/remote/test/puppeteer/test/queryselector.spec.ts
new file mode 100644
index 0000000000..7a147ddd01
--- /dev/null
+++ b/remote/test/puppeteer/test/queryselector.spec.ts
@@ -0,0 +1,507 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+import { CustomQueryHandler } from '../lib/cjs/puppeteer/common/QueryHandler.js';
+
+describe('querySelector', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+ describe('Page.$eval', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<section id="testAttribute">43543</section>');
+ const idAttribute = await page.$eval('section', (e) => e.id);
+ expect(idAttribute).toBe('testAttribute');
+ });
+ it('should accept arguments', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<section>hello</section>');
+ const text = await page.$eval(
+ 'section',
+ (e, suffix) => e.textContent + suffix,
+ ' world!'
+ );
+ expect(text).toBe('hello world!');
+ });
+ it('should accept ElementHandles as arguments', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<section>hello</section><div> world</div>');
+ const divHandle = await page.$('div');
+ const text = await page.$eval(
+ 'section',
+ (e, div: HTMLElement) => e.textContent + div.textContent,
+ divHandle
+ );
+ expect(text).toBe('hello world');
+ });
+ it('should throw error if no element is found', async () => {
+ const { page } = getTestState();
+
+ let error = null;
+ await page
+ .$eval('section', (e) => e.id)
+ .catch((error_) => (error = error_));
+ expect(error.message).toContain(
+ 'failed to find element matching selector "section"'
+ );
+ });
+ });
+
+ describe('pierceHandler', function () {
+ beforeEach(async () => {
+ const { page } = getTestState();
+ await 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>`
+ );
+ });
+ it('should find first element in shadow', async () => {
+ const { page } = getTestState();
+ const div = await page.$('pierce/.foo');
+ const text = await div.evaluate(
+ (element: Element) => element.textContent
+ );
+ expect(text).toBe('Hello');
+ });
+ it('should find all elements in shadow', async () => {
+ const { page } = getTestState();
+ const divs = await page.$$('pierce/.foo');
+ const text = await Promise.all(
+ divs.map((div) =>
+ div.evaluate((element: Element) => element.textContent)
+ )
+ );
+ expect(text.join(' ')).toBe('Hello World');
+ });
+ });
+
+ // 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 } = getTestState();
+
+ await page.setContent(
+ '<div>hello</div><div>beautiful</div><div>world!</div>'
+ );
+ const divsCount = await page.$$eval('div', (divs) => divs.length);
+ expect(divsCount).toBe(3);
+ });
+ it('should accept extra arguments', async () => {
+ const { page } = getTestState();
+ await page.setContent(
+ '<div>hello</div><div>beautiful</div><div>world!</div>'
+ );
+ const divsCountPlus5 = await page.$$eval(
+ 'div',
+ (divs, two: number, three: number) => divs.length + two + three,
+ 2,
+ 3
+ );
+ expect(divsCountPlus5).toBe(8);
+ });
+ it('should accept ElementHandles as arguments', async () => {
+ const { page } = getTestState();
+ await page.setContent(
+ '<section>2</section><section>2</section><section>1</section><div>3</div>'
+ );
+ const divHandle = await page.$('div');
+ const sum = await page.$$eval(
+ 'section',
+ (sections, div: HTMLElement) =>
+ sections.reduce(
+ (acc, section) => acc + Number(section.textContent),
+ 0
+ ) + Number(div.textContent),
+ divHandle
+ );
+ expect(sum).toBe(8);
+ });
+ it('should handle many elements', async () => {
+ const { page } = 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) =>
+ sections.reduce((acc, section) => acc + Number(section.textContent), 0)
+ );
+ expect(sum).toBe(500500);
+ });
+ });
+
+ describe('Page.$', function () {
+ it('should query existing element', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<section>test</section>');
+ const element = await page.$('section');
+ expect(element).toBeTruthy();
+ });
+ it('should return null for non-existing element', async () => {
+ const { page } = getTestState();
+
+ const element = await page.$('non-existing-element');
+ expect(element).toBe(null);
+ });
+ });
+
+ describe('Page.$$', function () {
+ it('should query existing elements', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<div>A</div><br/><div>B</div>');
+ const elements = await page.$$('div');
+ expect(elements.length).toBe(2);
+ const promises = elements.map((element) =>
+ page.evaluate((e: HTMLElement) => e.textContent, element)
+ );
+ expect(await Promise.all(promises)).toEqual(['A', 'B']);
+ });
+ it('should return empty array if nothing is found', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const elements = await page.$$('div');
+ expect(elements.length).toBe(0);
+ });
+ });
+
+ describe('Path.$x', function () {
+ it('should query existing element', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<section>test</section>');
+ const elements = await page.$x('/html/body/section');
+ expect(elements[0]).toBeTruthy();
+ expect(elements.length).toBe(1);
+ });
+ it('should return empty array for non-existing element', async () => {
+ const { page } = getTestState();
+
+ const element = await page.$x('/html/body/non-existing-element');
+ expect(element).toEqual([]);
+ });
+ it('should return multiple elements', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<div></div><div></div>');
+ const elements = await page.$x('/html/body/div');
+ expect(elements.length).toBe(2);
+ });
+ });
+
+ describe('ElementHandle.$', function () {
+ it('should query existing element', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/playground.html');
+ await page.setContent(
+ '<html><body><div class="second"><div class="inner">A</div></div></body></html>'
+ );
+ const html = await page.$('html');
+ const second = await html.$('.second');
+ const inner = await second.$('.inner');
+ const content = await page.evaluate(
+ (e: HTMLElement) => e.textContent,
+ inner
+ );
+ expect(content).toBe('A');
+ });
+
+ it('should return null for non-existing element', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<html><body><div class="second"><div class="inner">B</div></div></body></html>'
+ );
+ const html = await page.$('html');
+ const second = await html.$('.third');
+ expect(second).toBe(null);
+ });
+ });
+ describe('ElementHandle.$eval', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<html><body><div class="tweet"><div class="like">100</div><div class="retweets">10</div></div></body></html>'
+ );
+ const tweet = await page.$('.tweet');
+ const content = await tweet.$eval(
+ '.like',
+ (node: HTMLElement) => node.innerText
+ );
+ expect(content).toBe('100');
+ });
+
+ it('should retrieve content from subtree', async () => {
+ const { page } = 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);
+ const elementHandle = await page.$('#myId');
+ const content = await elementHandle.$eval(
+ '.a',
+ (node: HTMLElement) => node.innerText
+ );
+ expect(content).toBe('a-child-div');
+ });
+
+ it('should throw in case of missing selector', async () => {
+ const { page } = getTestState();
+
+ const htmlContent =
+ '<div class="a">not-a-child-div</div><div id="myId"></div>';
+ await page.setContent(htmlContent);
+ const elementHandle = await page.$('#myId');
+ const errorMessage = await elementHandle
+ .$eval('.a', (node: HTMLElement) => node.innerText)
+ .catch((error) => error.message);
+ expect(errorMessage).toBe(
+ `Error: failed to find element matching selector ".a"`
+ );
+ });
+ });
+ describe('ElementHandle.$$eval', function () {
+ it('should work', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<html><body><div class="tweet"><div class="like">100</div><div class="like">10</div></div></body></html>'
+ );
+ const tweet = await page.$('.tweet');
+ const content = await tweet.$$eval('.like', (nodes: HTMLElement[]) =>
+ nodes.map((n) => n.innerText)
+ );
+ expect(content).toEqual(['100', '10']);
+ });
+
+ it('should retrieve content from subtree', async () => {
+ const { page } = 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);
+ const elementHandle = await page.$('#myId');
+ const content = await elementHandle.$$eval('.a', (nodes: HTMLElement[]) =>
+ nodes.map((n) => n.innerText)
+ );
+ expect(content).toEqual(['a1-child-div', 'a2-child-div']);
+ });
+
+ it('should not throw in case of missing selector', async () => {
+ const { page } = getTestState();
+
+ const htmlContent =
+ '<div class="a">not-a-child-div</div><div id="myId"></div>';
+ await page.setContent(htmlContent);
+ const elementHandle = await page.$('#myId');
+ const nodesLength = await elementHandle.$$eval(
+ '.a',
+ (nodes) => nodes.length
+ );
+ expect(nodesLength).toBe(0);
+ });
+ });
+
+ describe('ElementHandle.$$', function () {
+ it('should query existing elements', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<html><body><div>A</div><br/><div>B</div></body></html>'
+ );
+ const html = await page.$('html');
+ const elements = await html.$$('div');
+ expect(elements.length).toBe(2);
+ const promises = elements.map((element) =>
+ page.evaluate((e: HTMLElement) => e.textContent, element)
+ );
+ expect(await Promise.all(promises)).toEqual(['A', 'B']);
+ });
+
+ it('should return empty array for non-existing elements', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<html><body><span>A</span><br/><span>B</span></body></html>'
+ );
+ const html = await page.$('html');
+ const elements = await html.$$('div');
+ expect(elements.length).toBe(0);
+ });
+ });
+
+ describe('ElementHandle.$x', function () {
+ it('should query existing element', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.PREFIX + '/playground.html');
+ await page.setContent(
+ '<html><body><div class="second"><div class="inner">A</div></div></body></html>'
+ );
+ const 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: HTMLElement) => e.textContent,
+ inner[0]
+ );
+ expect(content).toBe('A');
+ });
+
+ it('should return null for non-existing element', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<html><body><div class="second"><div class="inner">B</div></div></body></html>'
+ );
+ const 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: Element, selector: string) =>
+ Array.from(element.querySelectorAll(selector)),
+ };
+ before(() => {
+ const { puppeteer } = getTestState();
+ puppeteer.registerCustomQueryHandler('allArray', handler);
+ });
+
+ it('should have registered handler', async () => {
+ const { puppeteer } = getTestState();
+ expect(
+ puppeteer.customQueryHandlerNames().includes('allArray')
+ ).toBeTruthy();
+ });
+ it('$$ should query existing elements', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<html><body><div>A</div><br/><div>B</div></body></html>'
+ );
+ const html = await page.$('html');
+ const elements = await html.$$('allArray/div');
+ expect(elements.length).toBe(2);
+ const promises = elements.map((element) =>
+ page.evaluate((e: HTMLElement) => e.textContent, element)
+ );
+ expect(await Promise.all(promises)).toEqual(['A', 'B']);
+ });
+
+ it('$$ should return empty array for non-existing elements', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<html><body><span>A</span><br/><span>B</span></body></html>'
+ );
+ const html = await page.$('html');
+ const elements = await html.$$('allArray/div');
+ expect(elements.length).toBe(0);
+ });
+ it('$$eval should work', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<div>hello</div><div>beautiful</div><div>world!</div>'
+ );
+ const divsCount = await page.$$eval(
+ 'allArray/div',
+ (divs) => divs.length
+ );
+ expect(divsCount).toBe(3);
+ });
+ it('$$eval should accept extra arguments', async () => {
+ const { page } = getTestState();
+ await page.setContent(
+ '<div>hello</div><div>beautiful</div><div>world!</div>'
+ );
+ const divsCountPlus5 = await page.$$eval(
+ 'allArray/div',
+ (divs, two: number, three: number) => divs.length + two + three,
+ 2,
+ 3
+ );
+ expect(divsCountPlus5).toBe(8);
+ });
+ it('$$eval should accept ElementHandles as arguments', async () => {
+ const { page } = getTestState();
+ await page.setContent(
+ '<section>2</section><section>2</section><section>1</section><div>3</div>'
+ );
+ const divHandle = await page.$('div');
+ const sum = await page.$$eval(
+ 'allArray/section',
+ (sections, div: HTMLElement) =>
+ sections.reduce(
+ (acc, section) => acc + Number(section.textContent),
+ 0
+ ) + Number(div.textContent),
+ divHandle
+ );
+ expect(sum).toBe(8);
+ });
+ it('$$eval should handle many elements', async () => {
+ const { page } = 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) =>
+ sections.reduce((acc, section) => acc + Number(section.textContent), 0)
+ );
+ expect(sum).toBe(500500);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/requestinterception.spec.ts b/remote/test/puppeteer/test/requestinterception.spec.ts
new file mode 100644
index 0000000000..462eb714c7
--- /dev/null
+++ b/remote/test/puppeteer/test/requestinterception.spec.ts
@@ -0,0 +1,703 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import fs from 'fs';
+import path from 'path';
+import utils from './utils.js';
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('request interception', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+ describe('Page.setRequestInterception', function () {
+ it('should intercept', async () => {
+ const { page, server } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ if (utils.isFavicon(request)) {
+ request.continue();
+ 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');
+ request.continue();
+ });
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response.ok()).toBe(true);
+ expect(response.remoteAddress().port).toBe(server.PORT);
+ });
+ it('should work when POST is redirected with 302', async () => {
+ const { page, server } = getTestState();
+
+ server.setRedirect('/rredirect', '/empty.html');
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+ page.on('request', (request) => 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: HTMLFormElement) => form.submit()),
+ page.waitForNavigation(),
+ ]);
+ });
+ // @see https://github.com/puppeteer/puppeteer/issues/3973
+ it('should work when header manipulation headers with redirect', async () => {
+ const { page, server } = getTestState();
+
+ server.setRedirect('/rrredirect', '/empty.html');
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ const headers = Object.assign({}, request.headers(), {
+ foo: 'bar',
+ });
+ 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 } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ const headers = Object.assign({}, request.headers(), {
+ foo: 'bar',
+ origin: undefined, // remove "origin" header
+ });
+ 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 } = getTestState();
+
+ await page.setRequestInterception(true);
+ const requests = [];
+ page.on('request', (request) => {
+ if (!utils.isFavicon(request)) requests.push(request);
+ 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 properly return navigation response when URL has cookies', async () => {
+ const { page, server } = 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) => request.continue());
+ const response = await page.reload();
+ expect(response.status()).toBe(200);
+ });
+ it('should stop intercepting', async () => {
+ const { page, server } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.once('request', (request) => 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 } = getTestState();
+
+ await page.setExtraHTTPHeaders({
+ foo: 'bar',
+ });
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ expect(request.headers()['foo']).toBe('bar');
+ 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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ server.setRedirect('/logo.png', '/pptr.png');
+ await page.setRequestInterception(true);
+ page.on('request', (request) => 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 } = getTestState();
+
+ await page.setExtraHTTPHeaders({ referer: server.EMPTY_PAGE });
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
+ request.continue();
+ });
+ const response = await page.goto(server.EMPTY_PAGE);
+ expect(response.ok()).toBe(true);
+ });
+ it('should be abortable', async () => {
+ const { page, server } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ if (request.url().endsWith('.css')) request.abort();
+ else request.continue();
+ });
+ let failedRequests = 0;
+ page.on('requestfailed', () => ++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 } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ request.abort('internetdisconnected');
+ });
+ let failedRequest = null;
+ page.on('requestfailed', (request) => (failedRequest = request));
+ await 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 } = getTestState();
+
+ await page.setExtraHTTPHeaders({
+ referer: 'http://google.com/',
+ });
+ await page.setRequestInterception(true);
+ page.on('request', (request) => 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 } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => request.abort());
+ let error = null;
+ await page.goto(server.EMPTY_PAGE).catch((error_) => (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 } = getTestState();
+
+ await page.setRequestInterception(true);
+ const requests = [];
+ page.on('request', (request) => {
+ 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.length).toBe(5);
+ expect(requests[2].resourceType()).toBe('document');
+ // Check redirect chain
+ const redirectChain = response.request().redirectChain();
+ expect(redirectChain.length).toBe(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 } = getTestState();
+
+ await page.setRequestInterception(true);
+ const requests = [];
+ page.on('request', (request) => {
+ request.continue();
+ if (!utils.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) =>
+ 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.length).toBe(5);
+ expect(requests[0].resourceType()).toBe('document');
+ expect(requests[1].resourceType()).toBe('stylesheet');
+ // Check redirect chain
+ const redirectChain = requests[1].redirectChain();
+ expect(redirectChain.length).toBe(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 } = 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')) request.abort();
+ else request.continue();
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const result = await page.evaluate(async () => {
+ try {
+ await fetch('/non-existing.json');
+ } catch (error) {
+ return 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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let responseCount = 1;
+ server.setRoute('/zzz', (req, res) => res.end(responseCount++ * 11 + ''));
+ await page.setRequestInterception(true);
+
+ let spinner = false;
+ // Cancel 2nd request.
+ page.on('request', (request) => {
+ if (utils.isFavicon(request)) {
+ request.continue();
+ return;
+ }
+ spinner ? request.abort() : request.continue();
+ spinner = !spinner;
+ });
+ const results = await page.evaluate(() =>
+ Promise.all([
+ fetch('/zzz')
+ .then((response) => response.text())
+ .catch(() => 'FAILED'),
+ fetch('/zzz')
+ .then((response) => response.text())
+ .catch(() => 'FAILED'),
+ fetch('/zzz')
+ .then((response) => response.text())
+ .catch(() => 'FAILED'),
+ ])
+ );
+ expect(results).toEqual(['11', 'FAILED', '22']);
+ });
+ it('should navigate to dataURL and fire dataURL requests', async () => {
+ const { page } = getTestState();
+
+ await page.setRequestInterception(true);
+ const requests = [];
+ page.on('request', (request) => {
+ requests.push(request);
+ request.continue();
+ });
+ const dataURL = 'data:text/html,<div>yo</div>';
+ const response = await page.goto(dataURL);
+ expect(response.status()).toBe(200);
+ expect(requests.length).toBe(1);
+ expect(requests[0].url()).toBe(dataURL);
+ });
+ it('should be able to fetch dataURL and fire dataURL requests', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.setRequestInterception(true);
+ const requests = [];
+ page.on('request', (request) => {
+ requests.push(request);
+ request.continue();
+ });
+ const dataURL = 'data:text/html,<div>yo</div>';
+ const text = await page.evaluate(
+ (url: string) => fetch(url).then((r) => r.text()),
+ dataURL
+ );
+ expect(text).toBe('<div>yo</div>');
+ expect(requests.length).toBe(1);
+ expect(requests[0].url()).toBe(dataURL);
+ });
+ it('should navigate to URL with hash and fire requests without hash', async () => {
+ const { page, server } = getTestState();
+
+ await page.setRequestInterception(true);
+ const requests = [];
+ page.on('request', (request) => {
+ requests.push(request);
+ 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.length).toBe(1);
+ expect(requests[0].url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should work with encoded server', async () => {
+ const { page, server } = 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) => 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 } = getTestState();
+
+ await page.setRequestInterception(true);
+ server.setRoute('/malformed?rnd=%911', (req, res) => res.end());
+ page.on('request', (request) => request.continue());
+ 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 } = 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 = [];
+ page.on('request', (request) => {
+ 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.length).toBe(2);
+ expect(requests[1].response().status()).toBe(404);
+ });
+ it('should not throw "Invalid Interception Id" if the request was cancelled', async () => {
+ const { page, server } = getTestState();
+
+ await page.setContent('<iframe></iframe>');
+ await page.setRequestInterception(true);
+ let request = null;
+ page.on('request', async (r) => (request = r));
+ page.$eval(
+ 'iframe',
+ (frame: HTMLIFrameElement, url: string) => (frame.src = url),
+ server.EMPTY_PAGE
+ ),
+ // Wait for request interception.
+ await utils.waitEvent(page, 'request');
+ // Delete frame to cause request to be canceled.
+ await page.$eval('iframe', (frame) => frame.remove());
+ let error = null;
+ await request.continue().catch((error_) => (error = error_));
+ expect(error).toBe(null);
+ });
+ it('should throw if interception is not enabled', async () => {
+ const { page, server } = getTestState();
+
+ let error = null;
+ page.on('request', async (request) => {
+ try {
+ await request.continue();
+ } catch (error_) {
+ error = 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 } = getTestState();
+
+ await page.setRequestInterception(true);
+ const urls = new Set();
+ page.on('request', (request) => {
+ urls.add(request.url().split('/').pop());
+ 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);
+ });
+ });
+
+ describe('Request.continue', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => request.continue());
+ await page.goto(server.EMPTY_PAGE);
+ });
+ it('should amend HTTP headers', async () => {
+ const { page, server } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ const headers = Object.assign({}, request.headers());
+ headers['FOO'] = 'bar';
+ request.continue({ headers });
+ });
+ await page.goto(server.EMPTY_PAGE);
+ const [request] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => fetch('/sleep.zzz')),
+ ]);
+ expect(request.headers['foo']).toBe('bar');
+ });
+ it('should redirect in a way non-observable to page', async () => {
+ const { page, server } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ const redirectURL = request.url().includes('/empty.html')
+ ? server.PREFIX + '/consolelog.html'
+ : undefined;
+ request.continue({ url: redirectURL });
+ });
+ let consoleMessage = null;
+ page.on('console', (msg) => (consoleMessage = msg));
+ await 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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ request.continue({ method: 'POST' });
+ });
+ const [request] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() => fetch('/sleep.zzz')),
+ ]);
+ expect(request.method).toBe('POST');
+ });
+ it('should amend post data', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ request.continue({ postData: 'doggo' });
+ });
+ const [serverRequest] = await Promise.all([
+ server.waitForRequest('/sleep.zzz'),
+ page.evaluate(() =>
+ 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 } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ 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');
+ });
+ });
+
+ describe('Request.respond', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ 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(() => document.body.textContent)).toBe(
+ 'Yo, page!'
+ );
+ });
+ it('should work with status code 422', async () => {
+ const { page, server } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ 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(() => document.body.textContent)).toBe(
+ 'Yo, page!'
+ );
+ });
+ it('should redirect', async () => {
+ const { page, server } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ if (!request.url().includes('rrredirect')) {
+ request.continue();
+ return;
+ }
+ request.respond({
+ status: 302,
+ headers: {
+ location: server.EMPTY_PAGE,
+ },
+ });
+ });
+ const response = await page.goto(server.PREFIX + '/rrredirect');
+ expect(response.request().redirectChain().length).toBe(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 } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ const imageBuffer = fs.readFileSync(
+ path.join(__dirname, 'assets', 'pptr.png')
+ );
+ 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) => (img.onload = fulfill));
+ }, server.PREFIX);
+ const img = await page.$('img');
+ expect(await img.screenshot()).toBeGolden('mock-binary-response.png');
+ });
+ it('should stringify intercepted request response headers', async () => {
+ const { page, server } = getTestState();
+
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ 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(() => document.body.textContent)).toBe(
+ 'Yo, page!'
+ );
+ });
+ });
+});
+
+/**
+ * @param {string} path
+ * @returns {string}
+ */
+function pathToFileURL(path) {
+ 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/run_static_server.js b/remote/test/puppeteer/test/run_static_server.js
new file mode 100755
index 0000000000..6779e8816a
--- /dev/null
+++ b/remote/test/puppeteer/test/run_static_server.js
@@ -0,0 +1,33 @@
+#!/usr/bin/env node
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const path = require('path');
+const { TestServer } = require('../utils/testserver/');
+
+const port = 8907;
+const httpsPort = 8908;
+const assetsPath = path.join(__dirname, 'assets');
+const cachedPath = path.join(__dirname, 'assets', 'cached');
+
+Promise.all([
+ TestServer.create(assetsPath, port),
+ TestServer.createHTTPS(assetsPath, httpsPort),
+]).then(([server, httpsServer]) => {
+ server.enableHTTPCache(cachedPath);
+ httpsServer.enableHTTPCache(cachedPath);
+ console.log(`HTTP: server is running on http://localhost:${port}`);
+ console.log(`HTTPS: server is running on https://localhost:${httpsPort}`);
+});
diff --git a/remote/test/puppeteer/test/screenshot.spec.ts b/remote/test/puppeteer/test/screenshot.spec.ts
new file mode 100644
index 0000000000..de33b9c94f
--- /dev/null
+++ b/remote/test/puppeteer/test/screenshot.spec.ts
@@ -0,0 +1,323 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('Screenshots', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ describe('Page.screenshot', function () {
+ it('should work', async () => {
+ const { page, server } = 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 } = 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 clip elements to the viewport', async () => {
+ const { page, server } = getTestState();
+
+ await page.setViewport({ width: 500, height: 500 });
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ clip: {
+ x: 50,
+ y: 600,
+ width: 100,
+ height: 100,
+ },
+ });
+ expect(screenshot).toBeGolden('screenshot-offscreen-clip.png');
+ });
+ it('should run in parallel', async () => {
+ const { page, server } = 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 } = 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 run in parallel in multiple pages', async () => {
+ const { server, context } = 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) => page.close()));
+ });
+ it('should allow transparency', async () => {
+ const { page, server } = 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 } = 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');
+ });
+ it('should work with odd clip size on Retina displays', async () => {
+ const { page } = getTestState();
+
+ 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 } = getTestState();
+
+ await page.setViewport({ width: 500, height: 500 });
+ await page.goto(server.PREFIX + '/grid.html');
+ const screenshot = await page.screenshot({
+ encoding: 'base64',
+ });
+ // TODO (@jackfranklin): improve the screenshot types.
+ // - if we pass encoding: 'base64', it returns a string
+ // - else it returns a buffer.
+ // If we can fix that we can avoid this "as string" here.
+ expect(Buffer.from(screenshot as string, 'base64')).toBeGolden(
+ 'screenshot-sanity.png'
+ );
+ });
+ });
+
+ describe('ElementHandle.screenshot', function () {
+ it('should work', async () => {
+ const { page, server } = getTestState();
+
+ await page.setViewport({ width: 500, height: 500 });
+ await page.goto(server.PREFIX + '/grid.html');
+ await page.evaluate(() => window.scrollBy(50, 100));
+ const elementHandle = await page.$('.box:nth-of-type(3)');
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden('screenshot-element-bounding-box.png');
+ });
+ it('should take into account padding and border', async () => {
+ const { page } = 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>
+ `);
+ const 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 } = getTestState();
+
+ await page.setViewport({ width: 500, height: 500 });
+
+ await page.setContent(`
+ something above
+ <style>
+ div.to-screenshot {
+ border: 1px solid blue;
+ width: 600px;
+ height: 600px;
+ margin-left: 50px;
+ }
+ ::-webkit-scrollbar{
+ display: none;
+ }
+ </style>
+ <div class="to-screenshot"></div>
+ `);
+ const elementHandle = await page.$('div.to-screenshot');
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden(
+ 'screenshot-element-larger-than-viewport.png'
+ );
+
+ expect(
+ await page.evaluate(() => ({
+ w: window.innerWidth,
+ h: window.innerHeight,
+ }))
+ ).toEqual({ w: 500, h: 500 });
+ });
+ it('should scroll element into view', async () => {
+ const { page } = 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>
+ `);
+ const 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 } = 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>`);
+ const 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 } = getTestState();
+
+ await page.setContent('<h1>remove this</h1>');
+ const elementHandle = await page.$('h1');
+ await page.evaluate(
+ (element: HTMLElement) => element.remove(),
+ elementHandle
+ );
+ const screenshotError = await elementHandle
+ .screenshot()
+ .catch((error) => 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 } = getTestState();
+
+ await page.setContent('<div style="width: 50px; height: 0"></div>');
+ const div = await page.$('div');
+ const error = await div.screenshot().catch((error_) => error_);
+ expect(error.message).toBe('Node has 0 height.');
+ });
+ it('should work for an element with fractional dimensions', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(
+ '<div style="width:48.51px;height:19.8px;border:1px solid black;"></div>'
+ );
+ const 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 } = 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>'
+ );
+ const elementHandle = await page.$('div');
+ const screenshot = await elementHandle.screenshot();
+ expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/target.spec.ts b/remote/test/puppeteer/test/target.spec.ts
new file mode 100644
index 0000000000..72cfe1d835
--- /dev/null
+++ b/remote/test/puppeteer/test/target.spec.ts
@@ -0,0 +1,294 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import utils from './utils.js';
+const { waitEvent } = utils;
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+import { Target } from '../lib/cjs/puppeteer/common/Target.js';
+
+describe('Target', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ it('Browser.targets should return all of the targets', async () => {
+ const { browser } = getTestState();
+
+ // The pages will be the testing page and the original newtab page
+ const targets = browser.targets();
+ expect(
+ targets.some(
+ (target) => target.type() === 'page' && target.url() === 'about:blank'
+ )
+ ).toBeTruthy();
+ expect(targets.some((target) => target.type() === 'browser')).toBeTruthy();
+ });
+ it('Browser.pages should return all of the pages', async () => {
+ const { page, context } = getTestState();
+
+ // The pages will be the testing page
+ const allPages = await context.pages();
+ expect(allPages.length).toBe(1);
+ expect(allPages).toContain(page);
+ expect(allPages[0]).not.toBe(allPages[1]);
+ });
+ it('should contain browser target', async () => {
+ const { browser } = getTestState();
+
+ const targets = browser.targets();
+ const browserTarget = targets.find((target) => target.type() === 'browser');
+ expect(browserTarget).toBeTruthy();
+ });
+ it('should be able to use the default page in the browser', async () => {
+ const { page, browser } = getTestState();
+
+ // The pages will be the testing page and the original newtab page
+ const allPages = await browser.pages();
+ const originalPage = allPages.find((p) => p !== page);
+ expect(
+ await originalPage.evaluate(() => ['Hello', 'world'].join(' '))
+ ).toBe('Hello world');
+ expect(await originalPage.$('body')).toBeTruthy();
+ });
+ it(
+ 'should report when a new page is created and closed',
+ async () => {
+ const { page, server, context } = getTestState();
+
+ const [otherPage] = await Promise.all([
+ context
+ .waitForTarget(
+ (target) =>
+ target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'
+ )
+ .then((target) => target.page()),
+ page.evaluate(
+ (url: string) => window.open(url),
+ server.CROSS_PROCESS_PREFIX + '/empty.html'
+ ),
+ ]);
+ expect(otherPage.url()).toContain(server.CROSS_PROCESS_PREFIX);
+ expect(await otherPage.evaluate(() => ['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 closePagePromise = new Promise((fulfill) =>
+ context.once('targetdestroyed', (target) => fulfill(target.page()))
+ );
+ await otherPage.close();
+ expect(await closePagePromise).toBe(otherPage);
+
+ allPages = await Promise.all(
+ context.targets().map((target) => target.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 } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const createdTarget = new Promise<Target>((fulfill) =>
+ context.once('targetcreated', (target) => fulfill(target))
+ );
+
+ 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 = new Promise((fulfill) =>
+ context.once('targetdestroyed', (target) => fulfill(target))
+ );
+ await page.evaluate(() =>
+ globalThis.registrationPromise.then((registration) =>
+ registration.unregister()
+ )
+ );
+ expect(await destroyedTarget).toBe(await createdTarget);
+ }
+ );
+ it('should create a worker from a service worker', async () => {
+ const { page, server, context } = getTestState();
+
+ await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
+
+ const target = await context.waitForTarget(
+ (target) => target.type() === 'service_worker'
+ );
+ const worker = await target.worker();
+ expect(await worker.evaluate(() => self.toString())).toBe(
+ '[object ServiceWorkerGlobalScope]'
+ );
+ });
+ it('should create a worker from a shared worker', async () => {
+ const { page, server, context } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await page.evaluate(() => {
+ new SharedWorker('data:text/javascript,console.log("hi")');
+ });
+ const target = await context.waitForTarget(
+ (target) => target.type() === 'shared_worker'
+ );
+ const worker = await target.worker();
+ expect(await worker.evaluate(() => self.toString())).toBe(
+ '[object SharedWorkerGlobalScope]'
+ );
+ });
+ it('should report when a target url changes', async () => {
+ const { page, server, context } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ let changedTarget = new Promise<Target>((fulfill) =>
+ context.once('targetchanged', (target) => fulfill(target))
+ );
+ await page.goto(server.CROSS_PROCESS_PREFIX + '/');
+ expect((await changedTarget).url()).toBe(server.CROSS_PROCESS_PREFIX + '/');
+
+ changedTarget = new Promise((fulfill) =>
+ context.once('targetchanged', (target) => fulfill(target))
+ );
+ await page.goto(server.EMPTY_PAGE);
+ expect((await changedTarget).url()).toBe(server.EMPTY_PAGE);
+ });
+ it('should not report uninitialized pages', async () => {
+ const { context } = getTestState();
+
+ let targetChanged = false;
+ const listener = () => (targetChanged = true);
+ context.on('targetchanged', listener);
+ const targetPromise = new Promise<Target>((fulfill) =>
+ context.once('targetcreated', (target) => fulfill(target))
+ );
+ const newPagePromise = context.newPage();
+ const target = await targetPromise;
+ expect(target.url()).toBe('about:blank');
+
+ const newPage = await newPagePromise;
+ const targetPromise2 = new Promise<Target>((fulfill) =>
+ context.once('targetcreated', (target) => fulfill(target))
+ );
+ const evaluatePromise = newPage.evaluate(() => window.open('about:blank'));
+ const target2 = await targetPromise2;
+ expect(target2.url()).toBe('about:blank');
+ await evaluatePromise;
+ await newPage.close();
+ expect(targetChanged).toBe(false);
+ context.removeListener('targetchanged', listener);
+ });
+ it(
+ 'should not crash while redirecting if original request was missed',
+ async () => {
+ const { page, server, context } = getTestState();
+
+ let serverResponse = null;
+ server.setRoute('/one-style.css', (req, res) => (serverResponse = res));
+ // Open a new page. Use window.open to connect to the page later.
+ await Promise.all([
+ page.evaluate(
+ (url: string) => window.open(url),
+ server.PREFIX + '/one-style.html'
+ ),
+ server.waitForRequest('/one-style.css'),
+ ]);
+ // Connect to the opened page.
+ const target = await context.waitForTarget((target) =>
+ target.url().includes('one-style.html')
+ );
+ const newPage = await target.page();
+ // Issue a redirect.
+ serverResponse.writeHead(302, { location: '/injectedstyle.css' });
+ serverResponse.end();
+ // Wait for the new page to load.
+ await waitEvent(newPage, 'load');
+ // Cleanup.
+ await newPage.close();
+ }
+ );
+ it('should have an opener', async () => {
+ const { page, server, context } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const [createdTarget] = await Promise.all([
+ new Promise<Target>((fulfill) =>
+ context.once('targetcreated', (target) => fulfill(target))
+ ),
+ 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()).toBe(null);
+ });
+
+ describe('Browser.waitForTarget', () => {
+ it('should wait for a target', async () => {
+ const { browser, puppeteer, server } = getTestState();
+
+ let resolved = false;
+ const targetPromise = browser.waitForTarget(
+ (target) => target.url() === server.EMPTY_PAGE
+ );
+ targetPromise
+ .then(() => (resolved = true))
+ .catch((error) => {
+ resolved = true;
+ if (error instanceof puppeteer.errors.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 puppeteer.errors.TimeoutError) {
+ console.error(error);
+ } else throw error;
+ }
+ await page.close();
+ });
+ it('should timeout waiting for a non-existent target', async () => {
+ const { browser, server, puppeteer } = getTestState();
+
+ let error = null;
+ await browser
+ .waitForTarget((target) => target.url() === server.EMPTY_PAGE, {
+ timeout: 1,
+ })
+ .catch((error_) => (error = error_));
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/touchscreen.spec.ts b/remote/test/puppeteer/test/touchscreen.spec.ts
new file mode 100644
index 0000000000..b7fc67bfa9
--- /dev/null
+++ b/remote/test/puppeteer/test/touchscreen.spec.ts
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('Touchscreen', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ it('should tap the button', async () => {
+ const { puppeteer, page, server } = getTestState();
+ const iPhone = puppeteer.devices['iPhone 6'];
+ await page.emulate(iPhone);
+ await page.goto(server.PREFIX + '/input/button.html');
+ await page.tap('button');
+ expect(await page.evaluate(() => globalThis.result)).toBe('Clicked');
+ });
+ it('should report touches', async () => {
+ const { puppeteer, page, server } = getTestState();
+ const iPhone = puppeteer.devices['iPhone 6'];
+ await page.emulate(iPhone);
+ await page.goto(server.PREFIX + '/input/touches.html');
+ const button = await page.$('button');
+ await button.tap();
+ expect(await page.evaluate(() => globalThis.getResult())).toEqual([
+ 'Touchstart: 0',
+ 'Touchend: 0',
+ ]);
+ });
+});
diff --git a/remote/test/puppeteer/test/tracing.spec.ts b/remote/test/puppeteer/test/tracing.spec.ts
new file mode 100644
index 0000000000..5e06f12b4c
--- /dev/null
+++ b/remote/test/puppeteer/test/tracing.spec.ts
@@ -0,0 +1,133 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import fs from 'fs';
+import path from 'path';
+import expect from 'expect';
+import { getTestState, describeChromeOnly } from './mocha-utils'; // eslint-disable-line import/extensions
+
+describeChromeOnly('Tracing', function () {
+ let outputFile;
+ let browser;
+ let page;
+
+ /* 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 () => {
+ const { defaultBrowserOptions, puppeteer } = getTestState();
+ browser = await puppeteer.launch(defaultBrowserOptions);
+ page = await browser.newPage();
+ outputFile = path.join(__dirname, 'assets', 'trace.json');
+ });
+
+ afterEach(async () => {
+ await browser.close();
+ browser = null;
+ page = null;
+ if (fs.existsSync(outputFile)) {
+ fs.unlinkSync(outputFile);
+ outputFile = null;
+ }
+ });
+ it('should output a trace', async () => {
+ const { server } = getTestState();
+
+ 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 () => {
+ await page.tracing.start({
+ path: outputFile,
+ categories: ['disabled-by-default-v8.cpu_profiler.hires'],
+ });
+ await page.tracing.stop();
+
+ const traceJson = JSON.parse(
+ fs.readFileSync(outputFile, { encoding: 'utf8' })
+ );
+ expect(traceJson.metadata['trace-config']).toContain(
+ 'disabled-by-default-v8.cpu_profiler.hires'
+ );
+ });
+ it('should throw if tracing on two pages', async () => {
+ await page.tracing.start({ path: outputFile });
+ const newPage = await browser.newPage();
+ let error = null;
+ await newPage.tracing
+ .start({ path: outputFile })
+ .catch((error_) => (error = error_));
+ await newPage.close();
+ expect(error).toBeTruthy();
+ await page.tracing.stop();
+ });
+ it('should return a buffer', async () => {
+ const { server } = getTestState();
+
+ 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 { server } = getTestState();
+
+ await page.tracing.start();
+ await page.goto(server.PREFIX + '/grid.html');
+ const trace = await page.tracing.stop();
+ expect(trace).toBeTruthy();
+ });
+
+ it('should return null in case of Buffer error', async () => {
+ const { server } = getTestState();
+
+ await page.tracing.start({ screenshots: true });
+ await page.goto(server.PREFIX + '/grid.html');
+ const oldBufferConcat = Buffer.concat;
+ Buffer.concat = () => {
+ throw 'error';
+ };
+ const trace = await page.tracing.stop();
+ expect(trace).toEqual(null);
+ Buffer.concat = oldBufferConcat;
+ });
+
+ it('should support a buffer without a path', async () => {
+ const { server } = getTestState();
+
+ 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 () => {
+ await page.tracing.start({ path: __dirname });
+
+ let error: Error = null;
+ try {
+ await page.tracing.stop();
+ } catch (error_) {
+ error = error_;
+ }
+ expect(error).toBeDefined();
+ });
+});
diff --git a/remote/test/puppeteer/test/tsconfig.json b/remote/test/puppeteer/test/tsconfig.json
new file mode 100644
index 0000000000..8b1f1e866c
--- /dev/null
+++ b/remote/test/puppeteer/test/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": ["*.ts", "*.js"]
+}
diff --git a/remote/test/puppeteer/test/tsconfig.test.json b/remote/test/puppeteer/test/tsconfig.test.json
new file mode 100644
index 0000000000..3432441200
--- /dev/null
+++ b/remote/test/puppeteer/test/tsconfig.test.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "CommonJS"
+ }
+}
diff --git a/remote/test/puppeteer/test/utils.js b/remote/test/puppeteer/test/utils.js
new file mode 100644
index 0000000000..935b44d98e
--- /dev/null
+++ b/remote/test/puppeteer/test/utils.js
@@ -0,0 +1,135 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// TODO (@jackfranklin): convert to TS and enable type checking.
+
+// @ts-nocheck
+const fs = require('fs');
+const path = require('path');
+const expect = require('expect');
+const GoldenUtils = require('./golden-utils');
+const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json'))
+ ? path.join(__dirname, '..')
+ : path.join(__dirname, '..', '..');
+
+const utils = (module.exports = {
+ extendExpectWithToBeGolden: function (goldenDir, outputDir) {
+ expect.extend({
+ toBeGolden: (testScreenshot, goldenFilePath) => {
+ const result = GoldenUtils.compare(
+ goldenDir,
+ outputDir,
+ testScreenshot,
+ goldenFilePath
+ );
+
+ return {
+ message: () => result.message,
+ pass: result.pass,
+ };
+ },
+ });
+ },
+
+ /**
+ * @returns {string}
+ */
+ projectRoot: function () {
+ return PROJECT_ROOT;
+ },
+
+ /**
+ * @param {!Page} page
+ * @param {string} frameId
+ * @param {string} url
+ * @returns {!Frame}
+ */
+ attachFrame: async function (page, frameId, url) {
+ const handle = await page.evaluateHandle(attachFrame, frameId, url);
+ return await handle.asElement().contentFrame();
+
+ async function attachFrame(frameId, url) {
+ const frame = document.createElement('iframe');
+ frame.src = url;
+ frame.id = frameId;
+ document.body.appendChild(frame);
+ await new Promise((x) => (frame.onload = x));
+ return frame;
+ }
+ },
+
+ isFavicon: function (request) {
+ return request.url().includes('favicon.ico');
+ },
+
+ /**
+ * @param {!Page} page
+ * @param {string} frameId
+ */
+ detachFrame: async function (page, frameId) {
+ await page.evaluate(detachFrame, frameId);
+
+ function detachFrame(frameId) {
+ const frame = document.getElementById(frameId);
+ frame.remove();
+ }
+ },
+
+ /**
+ * @param {!Page} page
+ * @param {string} frameId
+ * @param {string} url
+ */
+ navigateFrame: async function (page, frameId, url) {
+ await page.evaluate(navigateFrame, frameId, url);
+
+ function navigateFrame(frameId, url) {
+ const frame = document.getElementById(frameId);
+ frame.src = url;
+ return new Promise((x) => (frame.onload = x));
+ }
+ },
+
+ /**
+ * @param {!Frame} frame
+ * @param {string=} indentation
+ * @returns {Array<string>}
+ */
+ dumpFrames: function (frame, indentation) {
+ indentation = indentation || '';
+ let description = frame.url().replace(/:\d{4}\//, ':<PORT>/');
+ if (frame.name()) description += ' (' + frame.name() + ')';
+ const result = [indentation + description];
+ for (const child of frame.childFrames())
+ result.push(...utils.dumpFrames(child, ' ' + indentation));
+ return result;
+ },
+
+ /**
+ * @param {!EventEmitter} emitter
+ * @param {string} eventName
+ * @returns {!Promise<!Object>}
+ */
+ waitEvent: function (emitter, eventName, predicate = () => true) {
+ return new Promise((fulfill) => {
+ emitter.on(eventName, function listener(event) {
+ if (!predicate(event)) return;
+ emitter.removeListener(eventName, listener);
+ fulfill(event);
+ });
+ });
+ },
+});
diff --git a/remote/test/puppeteer/test/waittask.spec.ts b/remote/test/puppeteer/test/waittask.spec.ts
new file mode 100644
index 0000000000..c65b682674
--- /dev/null
+++ b/remote/test/puppeteer/test/waittask.spec.ts
@@ -0,0 +1,774 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import utils from './utils.js';
+import sinon from 'sinon';
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+
+describe('waittask specs', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+
+ describe('Page.waitFor', function () {
+ /* This method is deprecated but we don't want the warnings showing up in
+ * tests. Until we remove this method we still want to ensure we don't break
+ * it.
+ */
+ beforeEach(() => sinon.stub(console, 'warn').callsFake(() => {}));
+
+ it('should wait for selector', async () => {
+ const { page, server } = getTestState();
+
+ let found = false;
+ const waitFor = page.waitFor('div').then(() => (found = true));
+ await page.goto(server.EMPTY_PAGE);
+ expect(found).toBe(false);
+ await page.goto(server.PREFIX + '/grid.html');
+ await waitFor;
+ expect(found).toBe(true);
+ });
+
+ it('should wait for an xpath', async () => {
+ const { page, server } = getTestState();
+
+ let found = false;
+ const waitFor = page.waitFor('//div').then(() => (found = true));
+ await page.goto(server.EMPTY_PAGE);
+ expect(found).toBe(false);
+ await page.goto(server.PREFIX + '/grid.html');
+ await waitFor;
+ expect(found).toBe(true);
+ });
+ it('should not allow you to select an element with single slash xpath', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`<div>some text</div>`);
+ let error = null;
+ await page.waitFor('/html/body/div').catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ });
+ it('should timeout', async () => {
+ const { page } = getTestState();
+
+ const startTime = Date.now();
+ const timeout = 42;
+ await page.waitFor(timeout);
+ expect(Date.now() - startTime).not.toBeLessThan(timeout / 2);
+ });
+ it('should work with multiline body', async () => {
+ const { page } = getTestState();
+
+ const result = await page.waitForFunction(`
+ (() => true)()
+ `);
+ expect(await result.jsonValue()).toBe(true);
+ });
+ it('should wait for predicate', async () => {
+ const { page } = getTestState();
+
+ await Promise.all([
+ page.waitFor(() => window.innerWidth < 100),
+ page.setViewport({ width: 10, height: 10 }),
+ ]);
+ });
+ it('should throw when unknown type', async () => {
+ const { page } = getTestState();
+
+ let error = null;
+ // @ts-expect-error purposefully passing bad type for test
+ await page.waitFor({ foo: 'bar' }).catch((error_) => (error = error_));
+ expect(error.message).toContain('Unsupported target type');
+ });
+ it('should wait for predicate with arguments', async () => {
+ const { page } = getTestState();
+
+ await page.waitFor((arg1, arg2) => arg1 !== arg2, {}, 1, 2);
+ });
+
+ it('should log a deprecation warning', async () => {
+ const { page } = getTestState();
+
+ await page.waitFor(() => true);
+
+ const consoleWarnStub = console.warn as sinon.SinonSpy;
+
+ expect(consoleWarnStub.calledOnce).toBe(true);
+ expect(
+ consoleWarnStub.firstCall.calledWith(
+ 'waitFor is deprecated and will be removed in a future release. See https://github.com/puppeteer/puppeteer/issues/6214 for details and how to migrate your code.'
+ )
+ ).toBe(true);
+ expect((console.warn as sinon.SinonSpy).calledOnce).toBe(true);
+ });
+ });
+
+ describe('Frame.waitForFunction', function () {
+ it('should accept a string', async () => {
+ const { page } = getTestState();
+
+ const watchdog = page.waitForFunction('window.__FOO === 1');
+ await page.evaluate(() => (globalThis.__FOO = 1));
+ await watchdog;
+ });
+ it('should work when resolved right before execution context disposal', async () => {
+ const { page } = getTestState();
+
+ await page.evaluateOnNewDocument(() => (globalThis.__RELOADED = true));
+ await page.waitForFunction(() => {
+ if (!globalThis.__RELOADED) window.location.reload();
+ return true;
+ });
+ });
+ it('should poll on interval', async () => {
+ const { page } = getTestState();
+
+ let success = false;
+ const startTime = Date.now();
+ const polling = 100;
+ const watchdog = page
+ .waitForFunction(() => globalThis.__FOO === 'hit', { polling })
+ .then(() => (success = true));
+ await page.evaluate(() => (globalThis.__FOO = 'hit'));
+ expect(success).toBe(false);
+ await page.evaluate(() =>
+ document.body.appendChild(document.createElement('div'))
+ );
+ await watchdog;
+ expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
+ });
+ it('should poll on interval async', async () => {
+ const { page } = getTestState();
+ let success = false;
+ const startTime = Date.now();
+ const polling = 100;
+ const watchdog = page
+ .waitForFunction(async () => globalThis.__FOO === 'hit', { polling })
+ .then(() => (success = true));
+ await page.evaluate(async () => (globalThis.__FOO = 'hit'));
+ expect(success).toBe(false);
+ await page.evaluate(async () =>
+ document.body.appendChild(document.createElement('div'))
+ );
+ await watchdog;
+ expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
+ });
+ it('should poll on mutation', async () => {
+ const { page } = getTestState();
+
+ let success = false;
+ const watchdog = page
+ .waitForFunction(() => globalThis.__FOO === 'hit', {
+ polling: 'mutation',
+ })
+ .then(() => (success = true));
+ await page.evaluate(() => (globalThis.__FOO = 'hit'));
+ expect(success).toBe(false);
+ await page.evaluate(() =>
+ document.body.appendChild(document.createElement('div'))
+ );
+ await watchdog;
+ });
+ it('should poll on mutation async', async () => {
+ const { page } = getTestState();
+
+ let success = false;
+ const watchdog = page
+ .waitForFunction(async () => globalThis.__FOO === 'hit', {
+ polling: 'mutation',
+ })
+ .then(() => (success = true));
+ await page.evaluate(async () => (globalThis.__FOO = 'hit'));
+ expect(success).toBe(false);
+ await page.evaluate(async () =>
+ document.body.appendChild(document.createElement('div'))
+ );
+ await watchdog;
+ });
+ it('should poll on raf', async () => {
+ const { page } = getTestState();
+
+ const watchdog = page.waitForFunction(() => globalThis.__FOO === 'hit', {
+ polling: 'raf',
+ });
+ await page.evaluate(() => (globalThis.__FOO = 'hit'));
+ await watchdog;
+ });
+ it('should poll on raf async', async () => {
+ const { page } = getTestState();
+
+ const watchdog = page.waitForFunction(
+ async () => globalThis.__FOO === 'hit',
+ {
+ polling: 'raf',
+ }
+ );
+ await page.evaluate(async () => (globalThis.__FOO = 'hit'));
+ await watchdog;
+ });
+ it('should work with strict CSP policy', async () => {
+ const { page, server } = getTestState();
+
+ server.setCSP('/empty.html', 'script-src ' + server.PREFIX);
+ await page.goto(server.EMPTY_PAGE);
+ let error = null;
+ await Promise.all([
+ page
+ .waitForFunction(() => globalThis.__FOO === 'hit', { polling: 'raf' })
+ .catch((error_) => (error = error_)),
+ page.evaluate(() => (globalThis.__FOO = 'hit')),
+ ]);
+ expect(error).toBe(null);
+ });
+ it('should throw on bad polling value', async () => {
+ const { page } = getTestState();
+
+ let error = null;
+ try {
+ await page.waitForFunction(() => !!document.body, {
+ polling: 'unknown',
+ });
+ } catch (error_) {
+ error = error_;
+ }
+ expect(error).toBeTruthy();
+ expect(error.message).toContain('polling');
+ });
+ it('should throw negative polling interval', async () => {
+ const { page } = getTestState();
+
+ let error = null;
+ try {
+ await page.waitForFunction(() => !!document.body, { polling: -10 });
+ } catch (error_) {
+ error = error_;
+ }
+ expect(error).toBeTruthy();
+ expect(error.message).toContain('Cannot poll with non-positive interval');
+ });
+ it('should return the success value as a JSHandle', async () => {
+ const { page } = getTestState();
+
+ expect(await (await page.waitForFunction(() => 5)).jsonValue()).toBe(5);
+ });
+ it('should return the window as a success value', async () => {
+ const { page } = getTestState();
+
+ expect(await page.waitForFunction(() => window)).toBeTruthy();
+ });
+ it('should accept ElementHandle arguments', async () => {
+ const { page } = getTestState();
+
+ await page.setContent('<div></div>');
+ const div = await page.$('div');
+ let resolved = false;
+ const waitForFunction = page
+ .waitForFunction((element) => !element.parentElement, {}, div)
+ .then(() => (resolved = true));
+ expect(resolved).toBe(false);
+ await page.evaluate((element: HTMLElement) => element.remove(), div);
+ await waitForFunction;
+ });
+ it('should respect timeout', async () => {
+ const { page, puppeteer } = getTestState();
+
+ let error = null;
+ await page
+ .waitForFunction('false', { timeout: 10 })
+ .catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ expect(error.message).toContain('waiting for function failed: timeout');
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should respect default timeout', async () => {
+ const { page, puppeteer } = getTestState();
+
+ page.setDefaultTimeout(1);
+ let error = null;
+ await page.waitForFunction('false').catch((error_) => (error = error_));
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ expect(error.message).toContain('waiting for function failed: timeout');
+ });
+ it('should disable timeout when its set to 0', async () => {
+ const { page } = getTestState();
+
+ const watchdog = page.waitForFunction(
+ () => {
+ globalThis.__counter = (globalThis.__counter || 0) + 1;
+ return globalThis.__injected;
+ },
+ { timeout: 0, polling: 10 }
+ );
+ await page.waitForFunction(() => globalThis.__counter > 10);
+ await page.evaluate(() => (globalThis.__injected = true));
+ await watchdog;
+ });
+ it('should survive cross-process navigation', async () => {
+ const { page, server } = getTestState();
+
+ let fooFound = false;
+ const waitForFunction = page
+ .waitForFunction('globalThis.__FOO === 1')
+ .then(() => (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(() => (globalThis.__FOO = 1));
+ await waitForFunction;
+ expect(fooFound).toBe(true);
+ });
+ it('should survive navigations', async () => {
+ const { page, server } = getTestState();
+
+ const watchdog = page.waitForFunction(() => globalThis.__done);
+ await page.goto(server.EMPTY_PAGE);
+ await page.goto(server.PREFIX + '/consolelog.html');
+ await page.evaluate(() => (globalThis.__done = true));
+ await watchdog;
+ });
+ });
+
+ describe('Page.waitForTimeout', () => {
+ it('waits for the given timeout before resolving', async () => {
+ const { page, server } = 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 by
+ * ensuring 900 < endTime - startTime < 1100
+ */
+ expect(endTime - startTime).toBeGreaterThan(900);
+ expect(endTime - startTime).toBeLessThan(1100);
+ });
+ });
+
+ describe('Frame.waitForTimeout', () => {
+ it('waits for the given timeout before resolving', async () => {
+ const { page, server } = 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 by
+ * ensuring 900 < endTime - startTime < 1100
+ */
+ expect(endTime - startTime).toBeGreaterThan(900);
+ expect(endTime - startTime).toBeLessThan(1100);
+ });
+ });
+
+ describe('Frame.waitForSelector', function () {
+ const addElement = (tag) =>
+ document.body.appendChild(document.createElement(tag));
+
+ it('should immediately resolve promise if node exists', async () => {
+ const { page, server } = 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 work with removed MutationObserver', async () => {
+ const { page } = getTestState();
+
+ await page.evaluate(() => delete window.MutationObserver);
+ const [handle] = await Promise.all([
+ page.waitForSelector('.zombo'),
+ page.setContent(`<div class='zombo'>anything</div>`),
+ ]);
+ expect(
+ await page.evaluate((x: HTMLElement) => x.textContent, handle)
+ ).toBe('anything');
+ });
+
+ it('should resolve promise when node is added', async () => {
+ const { page, server } = 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');
+ const eHandle = await watchdog;
+ const tagName = await eHandle
+ .getProperty('tagName')
+ .then((e) => e.jsonValue());
+ expect(tagName).toBe('DIV');
+ });
+
+ it('should work when node is added through innerHTML', async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ const watchdog = page.waitForSelector('h3 div');
+ await page.evaluate(addElement, 'span');
+ await page.evaluate(
+ () =>
+ (document.querySelector('span').innerHTML = '<h3><div></div></h3>')
+ );
+ await watchdog;
+ });
+
+ it(
+ 'Page.waitForSelector is shortcut for main frame',
+ async () => {
+ const { page, server } = getTestState();
+
+ await page.goto(server.EMPTY_PAGE);
+ await utils.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');
+ const eHandle = await watchdog;
+ expect(eHandle.executionContext().frame()).toBe(page.mainFrame());
+ }
+ );
+
+ it('should run in specified frame', async () => {
+ const { page, server } = getTestState();
+
+ await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await utils.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');
+ const eHandle = await waitForSelectorPromise;
+ expect(eHandle.executionContext().frame()).toBe(frame2);
+ });
+
+ it('should throw when frame is detached', async () => {
+ const { page, server } = getTestState();
+
+ await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const frame = page.frames()[1];
+ let waitError = null;
+ const waitPromise = frame
+ .waitForSelector('.box')
+ .catch((error) => (waitError = error));
+ await utils.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 } = getTestState();
+
+ let boxFound = false;
+ const waitForSelector = page
+ .waitForSelector('.box')
+ .then(() => (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 visible', async () => {
+ const { page } = getTestState();
+
+ let divFound = false;
+ const waitForSelector = page
+ .waitForSelector('div', { visible: true })
+ .then(() => (divFound = true));
+ await page.setContent(
+ `<div style='display: none; visibility: hidden;'>1</div>`
+ );
+ expect(divFound).toBe(false);
+ await page.evaluate(() =>
+ document.querySelector('div').style.removeProperty('display')
+ );
+ expect(divFound).toBe(false);
+ await page.evaluate(() =>
+ document.querySelector('div').style.removeProperty('visibility')
+ );
+ expect(await waitForSelector).toBe(true);
+ expect(divFound).toBe(true);
+ });
+ it('should wait for visible recursively', async () => {
+ const { page } = getTestState();
+
+ let divVisible = false;
+ const waitForSelector = page
+ .waitForSelector('div#inner', { visible: true })
+ .then(() => (divVisible = true));
+ await page.setContent(
+ `<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`
+ );
+ expect(divVisible).toBe(false);
+ await page.evaluate(() =>
+ document.querySelector('div').style.removeProperty('display')
+ );
+ expect(divVisible).toBe(false);
+ await page.evaluate(() =>
+ 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 } = getTestState();
+
+ let divHidden = false;
+ await page.setContent(`<div style='display: block;'></div>`);
+ const waitForSelector = page
+ .waitForSelector('div', { hidden: true })
+ .then(() => (divHidden = true));
+ await page.waitForSelector('div'); // do a round trip
+ expect(divHidden).toBe(false);
+ await page.evaluate(() =>
+ 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 } = getTestState();
+
+ let divHidden = false;
+ await page.setContent(`<div style='display: block;'></div>`);
+ const waitForSelector = page
+ .waitForSelector('div', { hidden: true })
+ .then(() => (divHidden = true));
+ await page.waitForSelector('div'); // do a round trip
+ expect(divHidden).toBe(false);
+ await page.evaluate(() =>
+ 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 } = getTestState();
+
+ await page.setContent(`<div></div>`);
+ let divRemoved = false;
+ const waitForSelector = page
+ .waitForSelector('div', { hidden: true })
+ .then(() => (divRemoved = true));
+ await page.waitForSelector('div'); // do a round trip
+ expect(divRemoved).toBe(false);
+ await page.evaluate(() => 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 } = getTestState();
+
+ const handle = await page.waitForSelector('non-existing', {
+ hidden: true,
+ });
+ expect(handle).toBe(null);
+ });
+ it('should respect timeout', async () => {
+ const { page, puppeteer } = getTestState();
+
+ let error = null;
+ await page
+ .waitForSelector('div', { timeout: 10 })
+ .catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ expect(error.message).toContain(
+ 'waiting for selector `div` failed: timeout'
+ );
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should have an error message specifically for awaiting an element to be hidden', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`<div></div>`);
+ let error = null;
+ await page
+ .waitForSelector('div', { hidden: true, timeout: 10 })
+ .catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ expect(error.message).toContain(
+ 'waiting for selector `div` to be hidden failed: timeout'
+ );
+ });
+
+ it('should respond to node attribute mutation', async () => {
+ const { page } = getTestState();
+
+ let divFound = false;
+ const waitForSelector = page
+ .waitForSelector('.zombo')
+ .then(() => (divFound = true));
+ await page.setContent(`<div class='notZombo'></div>`);
+ expect(divFound).toBe(false);
+ await page.evaluate(
+ () => (document.querySelector('div').className = 'zombo')
+ );
+ expect(await waitForSelector).toBe(true);
+ });
+ it('should return the element handle', async () => {
+ const { page } = getTestState();
+
+ const waitForSelector = page.waitForSelector('.zombo');
+ await page.setContent(`<div class='zombo'>anything</div>`);
+ expect(
+ await page.evaluate(
+ (x: HTMLElement) => x.textContent,
+ await waitForSelector
+ )
+ ).toBe('anything');
+ });
+ it('should have correct stack trace for timeout', async () => {
+ const { page } = getTestState();
+
+ let error;
+ await page
+ .waitForSelector('.zombo', { timeout: 10 })
+ .catch((error_) => (error = error_));
+ expect(error.stack).toContain('waiting for selector `.zombo` failed');
+ // The extension is ts here as Mocha maps back via sourcemaps.
+ expect(error.stack).toContain('waittask.spec.ts');
+ });
+ });
+
+ describe('Frame.waitForXPath', function () {
+ const addElement = (tag) =>
+ document.body.appendChild(document.createElement(tag));
+
+ it('should support some fancy xpath', async () => {
+ const { page } = 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: HTMLElement) => x.textContent,
+ await waitForXPath
+ )
+ ).toBe('hello world ');
+ });
+ it('should respect timeout', async () => {
+ const { page, puppeteer } = getTestState();
+
+ let error = null;
+ await page
+ .waitForXPath('//div', { timeout: 10 })
+ .catch((error_) => (error = error_));
+ expect(error).toBeTruthy();
+ expect(error.message).toContain(
+ 'waiting for XPath `//div` failed: timeout'
+ );
+ expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
+ });
+ it('should run in specified frame', async () => {
+ const { page, server } = getTestState();
+
+ await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ await utils.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');
+ const eHandle = await waitForXPathPromise;
+ expect(eHandle.executionContext().frame()).toBe(frame2);
+ });
+ it('should throw when frame is detached', async () => {
+ const { page, server } = getTestState();
+
+ await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
+ const frame = page.frames()[1];
+ let waitError = null;
+ const waitPromise = frame
+ .waitForXPath('//*[@class="box"]')
+ .catch((error) => (waitError = error));
+ await utils.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 } = getTestState();
+
+ let divHidden = false;
+ await page.setContent(`<div style='display: block;'></div>`);
+ const waitForXPath = page
+ .waitForXPath('//div', { hidden: true })
+ .then(() => (divHidden = true));
+ await page.waitForXPath('//div'); // do a round trip
+ expect(divHidden).toBe(false);
+ await page.evaluate(() =>
+ document.querySelector('div').style.setProperty('display', 'none')
+ );
+ expect(await waitForXPath).toBe(true);
+ expect(divHidden).toBe(true);
+ });
+ it('should return the element handle', async () => {
+ const { page } = getTestState();
+
+ const waitForXPath = page.waitForXPath('//*[@class="zombo"]');
+ await page.setContent(`<div class='zombo'>anything</div>`);
+ expect(
+ await page.evaluate(
+ (x: HTMLElement) => x.textContent,
+ await waitForXPath
+ )
+ ).toBe('anything');
+ });
+ it('should allow you to select a text node', async () => {
+ const { page } = getTestState();
+
+ await page.setContent(`<div>some text</div>`);
+ const 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 } = getTestState();
+
+ await page.setContent(`<div>some text</div>`);
+ const waitForXPath = page.waitForXPath('/html/body/div');
+ expect(
+ await page.evaluate(
+ (x: HTMLElement) => x.textContent,
+ await waitForXPath
+ )
+ ).toBe('some text');
+ });
+ });
+});
diff --git a/remote/test/puppeteer/test/worker.spec.ts b/remote/test/puppeteer/test/worker.spec.ts
new file mode 100644
index 0000000000..2c5827361b
--- /dev/null
+++ b/remote/test/puppeteer/test/worker.spec.ts
@@ -0,0 +1,126 @@
+/**
+ * Copyright 2020 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import expect from 'expect';
+import {
+ getTestState,
+ setupTestBrowserHooks,
+ setupTestPageAndContextHooks,
+ describeFailsFirefox,
+} from './mocha-utils'; // eslint-disable-line import/extensions
+import utils from './utils.js';
+import { WebWorker } from '../lib/cjs/puppeteer/common/WebWorker.js';
+import { ConsoleMessage } from '../lib/cjs/puppeteer/common/ConsoleMessage.js';
+const { waitEvent } = utils;
+
+describeFailsFirefox('Workers', function () {
+ setupTestBrowserHooks();
+ setupTestPageAndContextHooks();
+ it('Page.workers', async () => {
+ const { page, server } = getTestState();
+
+ await Promise.all([
+ new Promise((x) => page.once('workercreated', x)),
+ page.goto(server.PREFIX + '/worker/worker.html'),
+ ]);
+ const worker = page.workers()[0];
+ expect(worker.url()).toContain('worker.js');
+
+ expect(await worker.evaluate(() => globalThis.workerFunction())).toBe(
+ 'worker function result'
+ );
+
+ await page.goto(server.EMPTY_PAGE);
+ expect(page.workers().length).toBe(0);
+ });
+ it('should emit created and destroyed events', async () => {
+ const { page } = getTestState();
+
+ const workerCreatedPromise = new Promise<WebWorker>((x) =>
+ page.once('workercreated', x)
+ );
+ const workerObj = await page.evaluateHandle(
+ () => new Worker('data:text/javascript,1')
+ );
+ const worker = await workerCreatedPromise;
+ const workerThisObj = await worker.evaluateHandle(() => this);
+ const workerDestroyedPromise = new Promise((x) =>
+ page.once('workerdestroyed', x)
+ );
+ await page.evaluate(
+ (workerObj: Worker) => workerObj.terminate(),
+ workerObj
+ );
+ expect(await workerDestroyedPromise).toBe(worker);
+ const error = await workerThisObj
+ .getProperty('self')
+ .catch((error) => error);
+ expect(error.message).toContain('Most likely the worker has been closed.');
+ });
+ it('should report console logs', async () => {
+ const { page } = getTestState();
+
+ const [message] = await Promise.all([
+ waitEvent(page, 'console'),
+ page.evaluate(() => new Worker(`data:text/javascript,console.log(1)`)),
+ ]);
+ expect(message.text()).toBe('1');
+ expect(message.location()).toEqual({
+ url: 'data:text/javascript,console.log(1)',
+ lineNumber: 0,
+ columnNumber: 8,
+ });
+ });
+ it('should have JSHandles for console logs', async () => {
+ const { page } = getTestState();
+
+ const logPromise = new Promise<ConsoleMessage>((x) =>
+ page.on('console', x)
+ );
+ await page.evaluate(
+ () => 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().length).toBe(4);
+ expect(await (await log.args()[3].getProperty('origin')).jsonValue()).toBe(
+ 'null'
+ );
+ });
+ it('should have an execution context', async () => {
+ const { page } = getTestState();
+
+ const workerCreatedPromise = new Promise<WebWorker>((x) =>
+ page.once('workercreated', x)
+ );
+ await page.evaluate(
+ () => new Worker(`data:text/javascript,console.log(1)`)
+ );
+ const worker = await workerCreatedPromise;
+ expect(await (await worker.executionContext()).evaluate('1+1')).toBe(2);
+ });
+ it('should report errors', async () => {
+ const { page } = getTestState();
+
+ const errorPromise = new Promise<Error>((x) => page.on('pageerror', x));
+ await page.evaluate(
+ () =>
+ new Worker(`data:text/javascript, throw new Error('this is my error');`)
+ );
+ const errorLog = await errorPromise;
+ expect(errorLog.message).toContain('this is my error');
+ });
+});