From 010c27410aec9e4e31804664717ffe8643f7a0be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsi?= Date: Tue, 18 Feb 2020 00:35:11 +0200 Subject: [PATCH 01/29] Docs: fix return type of launchPersistent (#1047) Docs fix: the return type of `browserType.launchPersistent` is [BrowserContext](https://github.com/microsoft/playwright/blob/master/src/server/browserType.ts#L46), not ` BrowserServer`. --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index f7a9dd7751..0a36c69d9b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3469,7 +3469,7 @@ const browser = await chromium.launch({ // Or 'firefox' or 'webkit'. - `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`. - `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`. - `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. -- returns: <[Promise]<[BrowserServer]>> Promise which resolves to the browser app instance. +- returns: <[Promise]<[BrowserContext]>> Promise which resolves to the browser app instance. Launches browser instance that uses persistent storage located at `userDataDir`. If `userDataDir` is not specified, temporary folder is created for the persistent storage. That folder is deleted when browser closes. From 8a7728da3d61de9577ec026064d6b9495f8da605 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 18 Feb 2020 08:56:20 -0800 Subject: [PATCH 02/29] docs: document LaunchOptions.dumpio (#1051) --- src/server/browserType.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/server/browserType.ts b/src/server/browserType.ts index 3c1c970c47..8c7cf207f5 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -34,6 +34,10 @@ export type LaunchOptions = BrowserArgOptions & { handleSIGTERM?: boolean, handleSIGHUP?: boolean, timeout?: number, + /** + * Whether to dump stdio of the browser, this is useful for example when + * diagnosing browser launch issues. + */ dumpio?: boolean, env?: {[key: string]: string} | undefined }; From f2b2d72693264130269cb3d7012aeb896c3c5abf Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 18 Feb 2020 09:15:47 -0800 Subject: [PATCH 03/29] fix(input): emit change events upon page.setInputFiles (#1028) --- src/dom.ts | 1 + test/input.spec.js | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/dom.ts b/src/dom.ts index ab2c3a05fa..b8f7ecc693 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -571,4 +571,5 @@ export const setFileInputFunction = async (element: HTMLInputElement, payloads: dt.items.add(file); element.files = dt.files; element.dispatchEvent(new Event('input', { 'bubbles': true })); + element.dispatchEvent(new Event('change', { 'bubbles': true })); }; diff --git a/test/input.spec.js b/test/input.spec.js index 4a78afa06a..6889632391 100644 --- a/test/input.spec.js +++ b/test/input.spec.js @@ -193,17 +193,19 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI path.relative(process.cwd(), __dirname + '/assets/pptr.png')).catch(e => error = e); expect(error).not.toBe(null); }); - it('should emit input change event', async({page, server}) => { + it('should emit input and change events', async({page, server}) => { const events = []; await page.exposeFunction('eventHandled', e => events.push(e)); await page.setContent(` `); await (await page.$('input')).setInputFiles(FILE_TO_UPLOAD); - expect(events.length).toBe(1); + expect(events.length).toBe(2); expect(events[0].type).toBe('input'); + expect(events[1].type).toBe('change'); }); }); From 1ee657823e2ef86e9b7e883180c28111a7f92ae1 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 18 Feb 2020 09:16:32 -0800 Subject: [PATCH 04/29] feat(viewport): update defaults to 1280x720, fix Firefox (#1038) --- docs/api.md | 4 ++-- src/browserContext.ts | 2 +- src/firefox/ffBrowser.ts | 24 +++++++++++++++++------- test/emulation.spec.js | 11 +++++++++-- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/api.md b/docs/api.md index 0a36c69d9b..41ce16ab69 100644 --- a/docs/api.md +++ b/docs/api.md @@ -184,7 +184,7 @@ Indicates that the browser is connected. - `options` <[Object]> - `ignoreHTTPSErrors` Whether to ignore HTTPS errors during navigation. Defaults to `false`. - `bypassCSP` Toggles bypassing page's Content-Security-Policy. - - `viewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. + - `viewport` Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `null` disables the default viewport. - `width` <[number]> page width in pixels. - `height` <[number]> page height in pixels. - `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as dpr). Defaults to `1`. @@ -217,7 +217,7 @@ Creates a new browser context. It won't share cookies/cache with other browser c - `options` <[Object]> - `ignoreHTTPSErrors` Whether to ignore HTTPS errors during navigation. Defaults to `false`. - `bypassCSP` Toggles bypassing page's Content-Security-Policy. - - `viewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. + - `viewport` Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `null` disables the default viewport. - `width` <[number]> page width in pixels. - `height` <[number]> page height in pixels. - `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as dpr). Defaults to `1`. diff --git a/src/browserContext.ts b/src/browserContext.ts index 39ac5859c5..346a9d92f3 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -63,7 +63,7 @@ export class BrowserContext extends platform.EventEmitter { this._timeoutSettings = new TimeoutSettings(); this._options = { ...options }; if (!this._options.viewport && this._options.viewport !== null) - this._options.viewport = { width: 800, height: 600 }; + this._options.viewport = { width: 1280, height: 720 }; if (this._options.viewport) this._options.viewport = { ...this._options.viewport }; if (this._options.geolocation) diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 578aeee12f..a684efd336 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -67,13 +67,23 @@ export class FFBrowser extends platform.EventEmitter implements Browser { } async newContext(options: BrowserContextOptions = {}): Promise { - const viewport = options.viewport ? { - viewportSize: { width: options.viewport.width, height: options.viewport.height }, - isMobile: !!options.viewport.isMobile, - deviceScaleFactor: options.viewport.deviceScaleFactor || 1, - hasTouch: !!options.viewport.isMobile, - } : undefined; - const {browserContextId} = await this._connection.send('Target.createBrowserContext', { + let viewport; + if (options.viewport) { + viewport = { + viewportSize: { width: options.viewport.width, height: options.viewport.height }, + isMobile: !!options.viewport.isMobile, + deviceScaleFactor: options.viewport.deviceScaleFactor || 1, + hasTouch: !!options.viewport.isMobile, + }; + } else if (options.viewport !== null) { + viewport = { + viewportSize: { width: 1280, height: 720 }, + isMobile: false, + deviceScaleFactor: 1, + hasTouch: false, + }; + } + const { browserContextId } = await this._connection.send('Target.createBrowserContext', { userAgent: options.userAgent, bypassCSP: options.bypassCSP, javaScriptDisabled: options.javaScriptEnabled === false ? true : undefined, diff --git a/test/emulation.spec.js b/test/emulation.spec.js index 9c50f56a0b..c7fea1b257 100644 --- a/test/emulation.spec.js +++ b/test/emulation.spec.js @@ -25,11 +25,18 @@ module.exports.describe = function({testRunner, expect, playwright, headless, FF const iPhone = playwright.devices['iPhone 6']; const iPhoneLandscape = playwright.devices['iPhone 6 landscape']; - describe('Page.viewport', function() { + describe('BrowserContext({viewport})', function() { it('should get the proper viewport size', async({page, server}) => { - expect(page.viewportSize()).toEqual({width: 800, height: 600}); + expect(page.viewportSize()).toEqual({width: 1280, height: 720}); + expect(await page.evaluate(() => window.innerWidth)).toBe(1280); + expect(await page.evaluate(() => window.innerHeight)).toBe(720); + }); + it('should set the proper viewport size', async({page, server}) => { + expect(page.viewportSize()).toEqual({width: 1280, height: 720}); await page.setViewportSize({width: 123, height: 456}); expect(page.viewportSize()).toEqual({width: 123, height: 456}); + expect(await page.evaluate(() => window.innerWidth)).toBe(123); + expect(await page.evaluate(() => window.innerHeight)).toBe(456); }); it('should support mobile emulation', async({newContext, server}) => { const context = await newContext({ viewport: iPhone.viewport }); From 1805acd5d501dafa44d764ae34f9f0706fc72581 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 18 Feb 2020 14:30:56 -0800 Subject: [PATCH 05/29] test: update animation click test (#1053) --- test/click.spec.js | 68 ++++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/test/click.spec.js b/test/click.spec.js index 2dd4e1cb32..ba7cdc9459 100644 --- a/test/click.spec.js +++ b/test/click.spec.js @@ -403,35 +403,51 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI await page.click('button'); expect(await page.evaluate('window.clicked')).toBe(true); }); - xit('should click on an animated button', async({page}) => { - const buttonSize = 50; + xit('should fail to click a button animated via CSS animations and setInterval', async({page}) => { + // This test has a setInterval that consistently animates a button. + // It checks that we detect the button to be continuously animating, and never try to click it. + // This test exposes two issues: + // - Chromium headless does not issue rafs between first and second animateLeft() calls. + // - Chromium and WebKit keep element bounds the same when for 2 frames when changing left to a new value. + const buttonSize = 10; const containerWidth = 500; - const transition = 500; + const transition = 100; await page.setContent(` - - -
- -
- - - + + +
+ +
+ + + `); - await page.click('button'); - expect(await page.evaluate('window.clicked')).toBe(1); - expect(await page.evaluate('document.querySelector("#button").style.left')).toBe(`${containerWidth - buttonSize}px`); - await new Promise(resolve => setTimeout(resolve, 500)); - await page.click('button'); - expect(await page.evaluate('window.clicked')).toBe(2); - expect(await page.evaluate('document.querySelector("#button").style.left')).toBe('0px'); + await page.evaluate(transition => { + window.setInterval(animateLeft, transition); + animateLeft(); + }, transition); + const error1 = await page.click('button', { timeout: 250 }).catch(e => e); + expect(await page.evaluate('window.clicked')).toBe(0); + expect(error1.message).toContain('timeout 250ms exceeded'); + const error2 = await page.click('button', { timeout: 250 }).catch(e => e); + expect(await page.evaluate('window.clicked')).toBe(0); + expect(error2.message).toContain('timeout 250ms exceeded'); }); }); From 223685ee7bb3c596343e3fc0f3f6bdd8c1ee3bce Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 18 Feb 2020 19:56:59 -0800 Subject: [PATCH 06/29] chore: strip out injected script from protocol logs (#1054) --- src/chromium/crConnection.ts | 9 ++++++++- src/firefox/ffConnection.ts | 13 ++++++++++--- src/webkit/wkConnection.ts | 13 ++++++++++--- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/chromium/crConnection.ts b/src/chromium/crConnection.ts index a3d703a054..e542ba87fd 100644 --- a/src/chromium/crConnection.ts +++ b/src/chromium/crConnection.ts @@ -60,7 +60,7 @@ export class CRConnection extends platform.EventEmitter { if (sessionId) message.sessionId = sessionId; const data = JSON.stringify(message); - this._debugProtocol('SEND ► ' + data); + this._debugProtocol('SEND ► ' + (rewriteInjectedScriptEvaluationLog(message) || data)); this._transport.send(data); return id; } @@ -192,3 +192,10 @@ function rewriteError(error: Error, message: string): Error { error.message = message; return error; } + +function rewriteInjectedScriptEvaluationLog(message: any): string | undefined { + // Injected script is very long and clutters protocol logs. + // To increase development velocity, we skip replace it with short description in the log. + if (message.method === 'Runtime.evaluate' && message.params && message.params.expression && message.params.expression.includes('src/injected/injected.ts')) + return `{"id":${message.id} [evaluate injected script]}`; +} diff --git a/src/firefox/ffConnection.ts b/src/firefox/ffConnection.ts index 09db9df2b7..1b3b83d10b 100644 --- a/src/firefox/ffConnection.ts +++ b/src/firefox/ffConnection.ts @@ -84,9 +84,9 @@ export class FFConnection extends platform.EventEmitter { } _rawSend(message: any) { - message = JSON.stringify(message); - this._debugProtocol('SEND ► ' + message); - this._transport.send(message); + const data = JSON.stringify(message); + this._debugProtocol('SEND ► ' + (rewriteInjectedScriptEvaluationLog(message) || data)); + this._transport.send(data); } async _onMessage(message: string) { @@ -226,3 +226,10 @@ function rewriteError(error: Error, message: string): Error { error.message = message; return error; } + +function rewriteInjectedScriptEvaluationLog(message: any): string | undefined { + // Injected script is very long and clutters protocol logs. + // To increase development velocity, we skip replace it with short description in the log. + if (message.method === 'Runtime.evaluate' && message.params && message.params.expression && message.params.expression.includes('src/injected/injected.ts')) + return `{"id":${message.id} [evaluate injected script]}`; +} diff --git a/src/webkit/wkConnection.ts b/src/webkit/wkConnection.ts index 2fc51a9624..7e7c5ec98b 100644 --- a/src/webkit/wkConnection.ts +++ b/src/webkit/wkConnection.ts @@ -53,9 +53,9 @@ export class WKConnection { } rawSend(message: any) { - message = JSON.stringify(message); - this._debugFunction('SEND ► ' + message); - this._transport.send(message); + const data = JSON.stringify(message); + this._debugFunction('SEND ► ' + (rewriteInjectedScriptEvaluationLog(message) || data)); + this._transport.send(data); } private _dispatchMessage(message: string) { @@ -177,3 +177,10 @@ export function rewriteError(error: Error, message: string): Error { export function isSwappedOutError(e: Error) { return e.message.includes('Target was swapped out.'); } + +function rewriteInjectedScriptEvaluationLog(message: any): string | undefined { + // Injected script is very long and clutters protocol logs. + // To increase development velocity, we skip replace it with short description in the log. + if (message.params && message.params.message && message.params.message.includes('Runtime.evaluate') && message.params.message.includes('src/injected/injected.ts')) + return `{"id":${message.id},"method":"${message.method}","params":{"message":[evaluate injected script],"targetId":"${message.params.targetId}"},"pageProxyId":${message.pageProxyId}}`; +} From 9f1edadbff88e77ced3453e13cf83662a4b99321 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 18 Feb 2020 19:58:04 -0800 Subject: [PATCH 07/29] fix(navigation): do not count random failures as navigation cancel (#1055) --- src/frames.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/frames.ts b/src/frames.ts index adc5b17dc2..fee3c2e0e6 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -428,9 +428,10 @@ export class Frame { let resolve: (error: Error|void) => void; const promise = new Promise(x => resolve = x); const watch = (documentId: string, error?: Error) => { - if (documentId !== expectedDocumentId) - return resolve(new Error('Navigation interrupted by another one')); - resolve(error); + if (documentId === expectedDocumentId) + resolve(error); + else if (!error) + resolve(new Error('Navigation interrupted by another one')); }; const dispose = () => this._documentWatchers.delete(watch); this._documentWatchers.add(watch); From 33824aa1d748a82f11f53b027018b5facf1d8cd3 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 19 Feb 2020 09:34:57 -0800 Subject: [PATCH 08/29] feat(click): waitForInteractable option, defaults to true (#934) (#1052) --- docs/api.md | 40 +++++++++-- src/dom.ts | 129 ++++++++++++++++++++++++---------- src/frames.ts | 16 ++--- src/injected/injected.ts | 14 +++- src/page.ts | 12 ++-- test/assets/input/button.html | 2 + test/click.spec.js | 90 ++++++++++++++++++++++++ test/elementhandle.spec.js | 2 +- 8 files changed, 242 insertions(+), 63 deletions(-) diff --git a/docs/api.md b/docs/api.md index 41ce16ab69..727be831ad 100644 --- a/docs/api.md +++ b/docs/api.md @@ -760,6 +760,7 @@ To disable authentication, pass `null`. - `selector` <[string]> A selector to search for checkbox or radio button to check. If there are multiple elements satisfying the selector, the first will be checked. - `options` <[Object]> - `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully checked. The Promise will be rejected if there is no element matching `selector`. @@ -779,6 +780,7 @@ Shortcut for [page.mainFrame().check(selector[, options])](#framecheckselector-o - y <[number]> - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully clicked. The Promise will be rejected if there is no element matching `selector`. @@ -835,6 +837,7 @@ Browser-specific Coverage implementation, only available for Chromium atm. See [ - y <[number]> - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the double click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully double clicked. The Promise will be rejected if there is no element matching `selector`. @@ -1119,6 +1122,7 @@ Shortcut for [page.mainFrame().goto(url, options)](#framegotourl-options) - y <[number]> - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the hover, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully hovered. Promise gets rejected if there's no element matching `selector`. @@ -1389,6 +1393,7 @@ Shortcut for [page.mainFrame().title()](#frametitle). - y <[number]> - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the triple click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully triple clicked. The Promise will be rejected if there is no element matching `selector`. @@ -1425,8 +1430,9 @@ Shortcut for [page.mainFrame().type(selector, text[, options])](#frametypeselect - `selector` <[string]> A selector to search for uncheckbox to check. If there are multiple elements satisfying the selector, the first will be checked. - `options` <[Object]> - `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. -- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully checked. The Promise will be rejected if there is no element matching `selector`. +- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully unchecked. The Promise will be rejected if there is no element matching `selector`. This method fetches an element with `selector`, if element is not already unchecked, it scrolls it into view if needed, and then uses [page.click](#pageclickselector-options) to click in the center of the element. If there's no element matching `selector`, the method throws an error. @@ -1795,6 +1801,7 @@ Adds a `` tag into the page with the desired url or a ` A selector to search for checkbox to check. If there are multiple elements satisfying the selector, the first will be checked. - `options` <[Object]> - `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully checked. The Promise will be rejected if there is no element matching `selector`. @@ -1815,6 +1822,7 @@ If there's no element matching `selector`, the method throws an error. - y <[number]> - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully clicked. The Promise will be rejected if there is no element matching `selector`. @@ -1845,6 +1853,7 @@ Gets the full HTML contents of the frame, including the doctype. - y <[number]> - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the double click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully double clicked. The Promise will be rejected if there is no element matching `selector`. @@ -1980,6 +1989,7 @@ console.log(frame === contentFrame); // -> true - y <[number]> - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the hover, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully hovered. Promise gets rejected if there's no element matching `selector`. @@ -2055,6 +2065,7 @@ frame.select('select#colors', { value: 'blue' }, { index: 2 }, 'red'); - y <[number]> - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the triple click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully triple clicked. The Promise will be rejected if there is no element matching `selector`. @@ -2087,8 +2098,9 @@ await frame.type('#mytextarea', 'World', {delay: 100}); // Types slower, like a - `selector` <[string]> A selector to search for uncheckbox to check. If there are multiple elements satisfying the selector, the first will be checked. - `options` <[Object]> - `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. -- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully checked. The Promise will be rejected if there is no element matching `selector`. +- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully unchecked. The Promise will be rejected if there is no element matching `selector`. This method fetches an element with `selector`, if element is not already unchecked, it scrolls it into view if needed, and then uses [frame.click](#frameclickselector-options) to click in the center of the element. If there's no element matching `selector`, the method throws an error. @@ -2263,7 +2275,7 @@ ElementHandle instances can be used as arguments in [`page.$eval()`](#pageevalse - [elementHandle.$$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args) - [elementHandle.$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args-1) - [elementHandle.boundingBox()](#elementhandleboundingbox) -- [elementHandle.check()](#elementhandlecheck) +- [elementHandle.check([options])](#elementhandlecheckoptions) - [elementHandle.click([options])](#elementhandleclickoptions) - [elementHandle.contentFrame()](#elementhandlecontentframe) - [elementHandle.dblclick([options])](#elementhandledblclickoptions) @@ -2279,7 +2291,7 @@ ElementHandle instances can be used as arguments in [`page.$eval()`](#pageevalse - [elementHandle.toString()](#elementhandletostring) - [elementHandle.tripleclick([options])](#elementhandletripleclickoptions) - [elementHandle.type(text[, options])](#elementhandletypetext-options) -- [elementHandle.uncheck()](#elementhandleuncheck) +- [elementHandle.uncheck([options])](#elementhandleuncheckoptions) - [elementHandle.visibleRatio()](#elementhandlevisibleratio) @@ -2352,7 +2364,10 @@ expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe('10'); This method returns the bounding box of the element (relative to the main frame), or `null` if the element is not visible. -#### elementHandle.check() +#### elementHandle.check([options]) +- `options` <[Object]> + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element is successfully checked. Promise gets rejected if the operation fails. If element is not already checked, it scrolls it into view if needed, and then uses [elementHandle.click](#elementhandleclickoptions) to click in the center of the element. @@ -2366,6 +2381,8 @@ If element is not already checked, it scrolls it into view if needed, and then u - x <[number]> - y <[number]> - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element is successfully clicked. Promise gets rejected if the element is detached from DOM. This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to click in the center of the element. @@ -2382,6 +2399,8 @@ If the element is detached from DOM, the method throws an error. - x <[number]> - y <[number]> - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the double click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element is successfully double clicked. Promise gets rejected if the element is detached from DOM. This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to click in the center of the element. @@ -2409,6 +2428,8 @@ Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus - x <[number]> - y <[number]> - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the hover, and then restores current modifiers back. If not specified, currently pressed modifiers are used. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element is successfully hovered. This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to hover over the center of the element. @@ -2494,6 +2515,8 @@ This method expects `elementHandle` to point to an [input element](https://devel - x <[number]> - y <[number]> - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the triple click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> Promise which resolves when the element is successfully triple clicked. Promise gets rejected if the element is detached from DOM. This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to click in the center of the element. @@ -2525,8 +2548,11 @@ await elementHandle.type('some text'); await elementHandle.press('Enter'); ``` -#### elementHandle.uncheck() -- returns: <[Promise]> Promise which resolves when the element is successfully checked. Promise gets rejected if the operation fails. +#### elementHandle.uncheck([options]) +- `options` <[Object]> + - `waitForInteractable` <[boolean]> Whether to wait for element to become static (not moving) and receive pointer events at the click point. Defaults to true. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]> Promise which resolves when the element is successfully unchecked. Promise gets rejected if the operation fails. If element is not already unchecked, it scrolls it into view if needed, and then uses [elementHandle.click](#elementhandleclickoptions) to click in the center of the element. diff --git a/src/dom.ts b/src/dom.ts index b8f7ecc693..54a6205916 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -25,6 +25,8 @@ import { Page } from './page'; import * as platform from './platform'; import { Selectors } from './selectors'; +export type WaitForInteractableOptions = types.TimeoutOptions & { waitForInteractable?: boolean }; + export class FrameExecutionContext extends js.ExecutionContext { readonly frame: frames.Frame; @@ -230,10 +232,15 @@ export class ElementHandle extends js.JSHandle { return point; } - async _performPointerAction(action: (point: types.Point) => Promise, options?: input.PointerActionOptions): Promise { + async _performPointerAction(action: (point: types.Point) => Promise, options?: input.PointerActionOptions & WaitForInteractableOptions): Promise { + const { waitForInteractable = true } = (options || {}); + if (waitForInteractable) + await this._waitForStablePosition(options); const relativePoint = options ? options.relativePoint : undefined; await this._scrollRectIntoViewIfNeeded(relativePoint ? { x: relativePoint.x, y: relativePoint.y, width: 0, height: 0 } : undefined); const point = relativePoint ? await this._relativePoint(relativePoint) : await this._clickablePoint(); + if (waitForInteractable) + await this._waitForHitTargetAt(point, options); let restoreModifiers: input.Modifier[] | undefined; if (options && options.modifiers) restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers); @@ -242,19 +249,19 @@ export class ElementHandle extends js.JSHandle { await this._page.keyboard._ensureModifiers(restoreModifiers); } - hover(options?: input.PointerActionOptions): Promise { + hover(options?: input.PointerActionOptions & WaitForInteractableOptions): Promise { return this._performPointerAction(point => this._page.mouse.move(point.x, point.y), options); } - click(options?: input.ClickOptions): Promise { + click(options?: input.ClickOptions & WaitForInteractableOptions): Promise { return this._performPointerAction(point => this._page.mouse.click(point.x, point.y, options), options); } - dblclick(options?: input.MultiClickOptions): Promise { + dblclick(options?: input.MultiClickOptions & WaitForInteractableOptions): Promise { return this._performPointerAction(point => this._page.mouse.dblclick(point.x, point.y, options), options); } - tripleclick(options?: input.MultiClickOptions): Promise { + tripleclick(options?: input.MultiClickOptions & WaitForInteractableOptions): Promise { return this._performPointerAction(point => this._page.mouse.tripleclick(point.x, point.y, options), options); } @@ -402,19 +409,20 @@ export class ElementHandle extends js.JSHandle { await this._page.keyboard.type(text, options); } - async press(key: string, options: { delay?: number; text?: string; } | undefined) { + async press(key: string, options?: { delay?: number, text?: string }) { await this.focus(); await this._page.keyboard.press(key, options); } - async check() { - await this._setChecked(true); + + async check(options?: WaitForInteractableOptions) { + await this._setChecked(true, options); } - async uncheck() { - await this._setChecked(false); + async uncheck(options?: WaitForInteractableOptions) { + await this._setChecked(false, options); } - private async _setChecked(state: boolean) { + private async _setChecked(state: boolean, options: WaitForInteractableOptions = {}) { const isCheckboxChecked = async (): Promise => { return this._evaluateInUtility((node: Node) => { if (node.nodeType !== Node.ELEMENT_NODE) @@ -442,7 +450,7 @@ export class ElementHandle extends js.JSHandle { if (await isCheckboxChecked() === state) return; - await this.click(); + await this.click(options); if (await isCheckboxChecked() !== state) throw new Error('Unable to click checkbox'); } @@ -497,6 +505,58 @@ export class ElementHandle extends js.JSHandle { return visibleRatio; }); } + + async _waitForStablePosition(options: types.TimeoutOptions = {}): Promise { + const context = await this._context.frame._utilityContext(); + const stablePromise = context.evaluate((injected: Injected, node: Node, timeout: number) => { + if (!node.isConnected) + throw new Error('Element is not attached to the DOM'); + const element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement; + if (!element) + throw new Error('Element is not attached to the DOM'); + + let lastRect: types.Rect | undefined; + let counter = 0; + return injected.poll('raf', undefined, timeout, () => { + // First raf happens in the same animation frame as evaluation, so it does not produce + // any client rect difference compared to synchronous call. We skip the synchronous call + // and only force layout during actual rafs as a small optimisation. + if (++counter === 1) + return false; + const clientRect = element.getBoundingClientRect(); + const rect = { x: clientRect.top, y: clientRect.left, width: clientRect.width, height: clientRect.height }; + const isStable = lastRect && rect.x === lastRect.x && rect.y === lastRect.y && rect.width === lastRect.width && rect.height === lastRect.height; + lastRect = rect; + return isStable; + }); + }, await context._injected(), this, options.timeout || 0); + await helper.waitWithTimeout(stablePromise, 'element to stop moving', options.timeout || 0); + } + + async _waitForHitTargetAt(point: types.Point, options: types.TimeoutOptions = {}): Promise { + const frame = await this.ownerFrame(); + if (frame && frame.parentFrame()) { + const element = await frame.frameElement(); + const box = await element.boundingBox(); + if (!box) + throw new Error('Element is not attached to the DOM'); + // Translate from viewport coordinates to frame coordinates. + point = { x: point.x - box.x, y: point.y - box.y }; + } + const context = await this._context.frame._utilityContext(); + const hitTargetPromise = context.evaluate((injected: Injected, node: Node, timeout: number, point: types.Point) => { + const element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement; + if (!element) + throw new Error('Element is not attached to the DOM'); + return injected.poll('raf', undefined, timeout, () => { + let hitElement = injected.utils.deepElementFromPoint(document, point.x, point.y); + while (hitElement && hitElement !== element) + hitElement = injected.utils.parentElementOrShadowHost(hitElement); + return hitElement === element; + }); + }, await context._injected(), this, options.timeout || 0, point); + await helper.waitWithTimeout(hitTargetPromise, 'element to receive mouse events', options.timeout || 0); + } } function normalizeSelector(selector: string): string { @@ -514,51 +574,44 @@ function normalizeSelector(selector: string): string { export type Task = (context: FrameExecutionContext) => Promise; -export function waitForFunctionTask(selector: string | undefined, pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]) { - const { polling = 'raf' } = options; +function assertPolling(polling: types.Polling) { if (helper.isString(polling)) assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling); else if (helper.isNumber(polling)) assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling); else throw new Error('Unknown polling options: ' + polling); +} + +export function waitForFunctionTask(selector: string | undefined, pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]): Task { + const { polling = 'raf' } = options; + assertPolling(polling); const predicateBody = helper.isString(pageFunction) ? 'return (' + pageFunction + ')' : 'return (' + pageFunction + ')(...args)'; if (selector !== undefined) selector = normalizeSelector(selector); return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, selector: string | undefined, predicateBody: string, polling: types.Polling, timeout: number, ...args) => { const innerPredicate = new Function('...args', predicateBody); - if (polling === 'raf') - return injected.pollRaf(selector, predicate, timeout); - if (polling === 'mutation') - return injected.pollMutation(selector, predicate, timeout); - return injected.pollInterval(selector, polling, predicate, timeout); - - function predicate(element: Element | undefined): any { + return injected.poll(polling, selector, timeout, (element: Element | undefined): any => { if (selector === undefined) return innerPredicate(...args); return innerPredicate(element, ...args); - } + }); }, await context._injected(), selector, predicateBody, polling, options.timeout || 0, ...args); } export function waitForSelectorTask(selector: string, visibility: types.Visibility, timeout: number): Task { - return async (context: FrameExecutionContext) => { - selector = normalizeSelector(selector); - return context.evaluateHandle((injected: Injected, selector: string, visibility: types.Visibility, timeout: number) => { - if (visibility !== 'any') - return injected.pollRaf(selector, predicate, timeout); - return injected.pollMutation(selector, predicate, timeout); - - function predicate(element: Element | undefined): Element | boolean { - if (!element) - return visibility === 'hidden'; - if (visibility === 'any') - return element; - return injected.isVisible(element) === (visibility === 'visible') ? element : false; - } - }, await context._injected(), selector, visibility, timeout); - }; + selector = normalizeSelector(selector); + return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, selector: string, visibility: types.Visibility, timeout: number) => { + const polling = visibility === 'any' ? 'mutation' : 'raf'; + return injected.poll(polling, selector, timeout, (element: Element | undefined): Element | boolean => { + if (!element) + return visibility === 'hidden'; + if (visibility === 'any') + return element; + return injected.isVisible(element) === (visibility === 'visible') ? element : false; + }); + }, await context._injected(), selector, visibility, timeout); } export const setFileInputFunction = async (element: HTMLInputElement, payloads: types.FilePayload[]) => { diff --git a/src/frames.ts b/src/frames.ts index fee3c2e0e6..167c2ff37f 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -781,19 +781,19 @@ export class Frame { return result!; } - async click(selector: string, options?: WaitForOptions & ClickOptions) { + async click(selector: string, options?: WaitForOptions & ClickOptions & dom.WaitForInteractableOptions) { const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); await handle.click(options); await handle.dispose(); } - async dblclick(selector: string, options?: WaitForOptions & MultiClickOptions) { + async dblclick(selector: string, options?: WaitForOptions & MultiClickOptions & dom.WaitForInteractableOptions) { const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); await handle.dblclick(options); await handle.dispose(); } - async tripleclick(selector: string, options?: WaitForOptions & MultiClickOptions) { + async tripleclick(selector: string, options?: WaitForOptions & MultiClickOptions & dom.WaitForInteractableOptions) { const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); await handle.tripleclick(options); await handle.dispose(); @@ -811,7 +811,7 @@ export class Frame { await handle.dispose(); } - async hover(selector: string, options?: WaitForOptions & PointerActionOptions) { + async hover(selector: string, options?: WaitForOptions & PointerActionOptions & dom.WaitForInteractableOptions) { const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); await handle.hover(options); await handle.dispose(); @@ -831,15 +831,15 @@ export class Frame { await handle.dispose(); } - async check(selector: string, options?: WaitForOptions) { + async check(selector: string, options?: WaitForOptions & dom.WaitForInteractableOptions) { const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); - await handle.check(); + await handle.check(options); await handle.dispose(); } - async uncheck(selector: string, options?: WaitForOptions) { + async uncheck(selector: string, options?: WaitForOptions & dom.WaitForInteractableOptions) { const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); - await handle.uncheck(); + await handle.uncheck(options); await handle.dispose(); } diff --git a/src/injected/injected.ts b/src/injected/injected.ts index 1afca5191d..46fd65ce70 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -145,7 +145,7 @@ class Injected { return !!(rect.top || rect.bottom || rect.width || rect.height); } - pollMutation(selector: string | undefined, predicate: Predicate, timeout: number): Promise { + private _pollMutation(selector: string | undefined, predicate: Predicate, timeout: number): Promise { let timedOut = false; if (timeout) setTimeout(() => timedOut = true, timeout); @@ -178,7 +178,7 @@ class Injected { return result; } - pollRaf(selector: string | undefined, predicate: Predicate, timeout: number): Promise { + private _pollRaf(selector: string | undefined, predicate: Predicate, timeout: number): Promise { let timedOut = false; if (timeout) setTimeout(() => timedOut = true, timeout); @@ -203,7 +203,7 @@ class Injected { return result; } - pollInterval(selector: string | undefined, pollInterval: number, predicate: Predicate, timeout: number): Promise { + private _pollInterval(selector: string | undefined, pollInterval: number, predicate: Predicate, timeout: number): Promise { let timedOut = false; if (timeout) setTimeout(() => timedOut = true, timeout); @@ -226,6 +226,14 @@ class Injected { onTimeout(); return result; } + + poll(polling: 'raf' | 'mutation' | number, selector: string | undefined, timeout: number, predicate: Predicate): Promise { + if (polling === 'raf') + return this._pollRaf(selector, predicate, timeout); + if (polling === 'mutation') + return this._pollMutation(selector, predicate, timeout); + return this._pollInterval(selector, polling, predicate, timeout); + } } export default Injected; diff --git a/src/page.ts b/src/page.ts index 162586d4fa..f0af0a318f 100644 --- a/src/page.ts +++ b/src/page.ts @@ -485,15 +485,15 @@ export class Page extends platform.EventEmitter { return this._closed; } - async click(selector: string, options?: frames.WaitForOptions & input.ClickOptions) { + async click(selector: string, options?: frames.WaitForOptions & input.ClickOptions & dom.WaitForInteractableOptions) { return this.mainFrame().click(selector, options); } - async dblclick(selector: string, options?: frames.WaitForOptions & input.MultiClickOptions) { + async dblclick(selector: string, options?: frames.WaitForOptions & input.MultiClickOptions & dom.WaitForInteractableOptions) { return this.mainFrame().dblclick(selector, options); } - async tripleclick(selector: string, options?: frames.WaitForOptions & input.MultiClickOptions) { + async tripleclick(selector: string, options?: frames.WaitForOptions & input.MultiClickOptions & dom.WaitForInteractableOptions) { return this.mainFrame().tripleclick(selector, options); } @@ -505,7 +505,7 @@ export class Page extends platform.EventEmitter { return this.mainFrame().focus(selector, options); } - async hover(selector: string, options?: frames.WaitForOptions & input.PointerActionOptions) { + async hover(selector: string, options?: frames.WaitForOptions & input.PointerActionOptions & dom.WaitForInteractableOptions) { return this.mainFrame().hover(selector, options); } @@ -517,11 +517,11 @@ export class Page extends platform.EventEmitter { return this.mainFrame().type(selector, text, options); } - async check(selector: string, options?: frames.WaitForOptions) { + async check(selector: string, options?: frames.WaitForOptions & dom.WaitForInteractableOptions) { return this.mainFrame().check(selector, options); } - async uncheck(selector: string, options?: frames.WaitForOptions) { + async uncheck(selector: string, options?: frames.WaitForOptions & dom.WaitForInteractableOptions) { return this.mainFrame().uncheck(selector, options); } diff --git a/test/assets/input/button.html b/test/assets/input/button.html index 164536e5e7..da1abb6e84 100644 --- a/test/assets/input/button.html +++ b/test/assets/input/button.html @@ -13,6 +13,8 @@ window.pageX = undefined; window.pageY = undefined; window.shiftKey = undefined; + window.pageX = undefined; + window.pageY = undefined; document.querySelector('button').addEventListener('click', e => { result = 'Clicked'; offsetX = e.offsetX; diff --git a/test/click.spec.js b/test/click.spec.js index ba7cdc9459..603be5ba45 100644 --- a/test/click.spec.js +++ b/test/click.spec.js @@ -378,6 +378,96 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI expect(await page.evaluate(() => pageY)).toBe(expected.y); }); + it('should wait for stable position', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.$eval('button', button => { + button.style.transition = 'margin 500ms linear 0s'; + button.style.marginLeft = '200px'; + button.style.borderWidth = '0'; + button.style.width = '200px'; + button.style.height = '20px'; + document.body.style.margin = '0'; + }); + await page.click('button'); + expect(await page.evaluate(() => window.result)).toBe('Clicked'); + expect(await page.evaluate(() => pageX)).toBe(300); + expect(await page.evaluate(() => pageY)).toBe(10); + }); + it('should timeout waiting for stable position', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await button.evaluate(button => { + button.style.transition = 'margin 5s linear 0s'; + button.style.marginLeft = '200px'; + }); + const error = await button.click({ timeout: 100 }).catch(e => e); + expect(error.message).toContain('timeout 100ms exceeded'); + }); + it('should wait for becoming hit target', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.$eval('button', button => { + button.style.borderWidth = '0'; + button.style.width = '200px'; + button.style.height = '20px'; + document.body.style.margin = '0'; + document.body.style.position = 'relative'; + const flyOver = document.createElement('div'); + flyOver.className = 'flyover'; + flyOver.style.position = 'absolute'; + flyOver.style.width = '400px'; + flyOver.style.height = '20px'; + flyOver.style.left = '-200px'; + flyOver.style.top = '0'; + flyOver.style.background = 'red'; + document.body.appendChild(flyOver); + }); + let clicked = false; + const clickPromise = page.click('button').then(() => clicked = true); + expect(clicked).toBe(false); + + await page.$eval('.flyover', flyOver => flyOver.style.left = '0'); + await page.evaluate(() => new Promise(requestAnimationFrame)); + await page.evaluate(() => new Promise(requestAnimationFrame)); + expect(clicked).toBe(false); + + await page.$eval('.flyover', flyOver => flyOver.style.left = '200px'); + await clickPromise; + expect(clicked).toBe(true); + expect(await page.evaluate(() => window.result)).toBe('Clicked'); + }); + it('should timeout waiting for hit target', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate(() => { + document.body.style.position = 'relative'; + const blocker = document.createElement('div'); + blocker.style.position = 'absolute'; + blocker.style.width = '400px'; + blocker.style.height = '20px'; + blocker.style.left = '0'; + blocker.style.top = '0'; + document.body.appendChild(blocker); + }); + const error = await button.click({ timeout: 100 }).catch(e => e); + expect(error.message).toContain('timeout 100ms exceeded'); + }); + it('should fail when obscured and not waiting for interactable', async({page, server}) => { + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate(() => { + document.body.style.position = 'relative'; + const blocker = document.createElement('div'); + blocker.style.position = 'absolute'; + blocker.style.width = '400px'; + blocker.style.height = '20px'; + blocker.style.left = '0'; + blocker.style.top = '0'; + document.body.appendChild(blocker); + }); + await button.click({ waitForInteractable: false }); + expect(await page.evaluate(() => window.result)).toBe('Was not clicked'); + }); + it('should update modifiers correctly', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); await page.click('button', { modifiers: ['Shift'] }); diff --git a/test/elementhandle.spec.js b/test/elementhandle.spec.js index c54e445b4b..67c96bcc10 100644 --- a/test/elementhandle.spec.js +++ b/test/elementhandle.spec.js @@ -228,7 +228,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) await page.evaluate(button => button.remove(), button); let error = null; await button.click().catch(err => error = err); - expect(error.message).toContain('Node is detached from document'); + expect(error.message).toContain('Element is not attached to the DOM'); }); it('should throw for hidden nodes', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); From 4be48a6b1b596629f3c47c9dc89924a559067802 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Wed, 19 Feb 2020 14:07:36 -0800 Subject: [PATCH 09/29] chore: disable DEBUGP on bots --- test/playwright.spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/playwright.spec.js b/test/playwright.spec.js index 05bbbc9f57..fcd6e1b86a 100644 --- a/test/playwright.spec.js +++ b/test/playwright.spec.js @@ -44,8 +44,7 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => { const headless = !!valueFromEnv('HEADLESS', true); const slowMo = valueFromEnv('SLOW_MO', 0); - const CI = valueFromEnv('CI', false); - const dumpProtocolOnFailure = CI || valueFromEnv('DEBUGP', false); + const dumpProtocolOnFailure = valueFromEnv('DEBUGP', false); function valueFromEnv(name, defaultValue) { if (!(name in process.env)) From 84ee297c4bbfaa2f3107c6821953194d210f1e12 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 19 Feb 2020 14:08:29 -0800 Subject: [PATCH 10/29] test: add a test for bounding box on partially visible element (#1011) --- test/elementhandle.spec.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/elementhandle.spec.js b/test/elementhandle.spec.js index 67c96bcc10..6769eecbd2 100644 --- a/test/elementhandle.spec.js +++ b/test/elementhandle.spec.js @@ -86,6 +86,34 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) expect(Math.round(box.width * 100)).toBe(200 * 100); expect(Math.round(box.height * 100)).toBe(20 * 100); }); + it('should work when inline box child is outside of viewport', async({page, server}) => { + await page.setContent(` + + woofdoggo + `); + const handle = await page.$('span'); + const box = await handle.boundingBox(); + const webBoundingBox = await handle.evaluate(e => { + const rect = e.getBoundingClientRect(); + return {x: rect.x, y: rect.y, width: rect.width, height: rect.height}; + }); + const round = box => ({ + x: Math.round(box.x * 100), + y: Math.round(box.y * 100), + width: Math.round(box.width * 100), + height: Math.round(box.height * 100), + }); + expect(round(box)).toEqual(round(webBoundingBox)); + }); }); describe('ElementHandle.contentFrame', function() { From 568c6cbb5493e0a614d202d9a170ba0115a150bc Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Wed, 19 Feb 2020 14:26:02 -0800 Subject: [PATCH 11/29] test(navigation): fix flaky networkidle tests (#1058) The network idle tests were waiting for requests to appear on the server, but not for playwright to be notified of the request via protocol. They also assumed performance.now would match up with setTimeout times. --- test/navigation.spec.js | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/test/navigation.spec.js b/test/navigation.spec.js index 6261829630..3c94b42b94 100644 --- a/test/navigation.spec.js +++ b/test/navigation.spec.js @@ -16,7 +16,6 @@ */ const utils = require('./utils'); -const { performance } = require('perf_hooks'); /** * @type {PageTestSuite} @@ -396,29 +395,39 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF expect(response.status()).toBe(200); }); + /** + * @param {import('../src/frames').Frame} frame + * @param {TestServer} server + * @param {'networkidle0'|'networkidle2'} signal + * @param {() => Promise} action + * @param {boolean} isSetContent + */ async function networkIdleTest(frame, server, signal, action, isSetContent) { - let lastResponseFinished; const finishResponse = response => { - lastResponseFinished = performance.now(); response.statusCode = 404; response.end(`File not found`); }; - + const waitForRequest = suffix => { + return Promise.all([ + server.waitForRequest(suffix), + frame._page.waitForRequest(server.PREFIX + suffix), + ]) + } let responses = {}; // Hold on to a bunch of requests without answering. server.setRoute('/fetch-request-a.js', (req, res) => responses.a = res); server.setRoute('/fetch-request-b.js', (req, res) => responses.b = res); server.setRoute('/fetch-request-c.js', (req, res) => responses.c = res); const initialFetchResourcesRequested = Promise.all([ - server.waitForRequest('/fetch-request-a.js'), - server.waitForRequest('/fetch-request-b.js'), - server.waitForRequest('/fetch-request-c.js'), + waitForRequest('/fetch-request-a.js'), + waitForRequest('/fetch-request-b.js'), + waitForRequest('/fetch-request-c.js') ]); let secondFetchResourceRequested; if (signal === 'networkidle0') { server.setRoute('/fetch-request-d.js', (req, res) => responses.d = res); - secondFetchResourceRequested = server.waitForRequest('/fetch-request-d.js'); + secondFetchResourceRequested = waitForRequest('/fetch-request-d.js'); } const waitForLoadPromise = isSetContent ? Promise.resolve() : frame.waitForNavigation({ waitUntil: 'load' }); @@ -442,10 +451,11 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF expect(responses.a).toBeTruthy(); expect(responses.b).toBeTruthy(); expect(responses.c).toBeTruthy(); - // Finishing first response should leave 2 requests alive and trigger networkidle2. - finishResponse(responses.a); - + let timer; + let timerTriggered = false; if (signal === 'networkidle0') { + // Finishing first response should leave 2 requests alive and trigger networkidle2. + finishResponse(responses.a); // Finishing two more responses should trigger the second round. finishResponse(responses.b); finishResponse(responses.c); @@ -454,11 +464,16 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF await secondFetchResourceRequested; expect(actionFinished).toBe(false); // Finishing the last response should trigger networkidle0. + timer = setTimeout(() => timerTriggered = true, 500); finishResponse(responses.d); + } else { + timer = setTimeout(() => timerTriggered = true, 500); + // Finishing first response should leave 2 requests alive and trigger networkidle2. + finishResponse(responses.a); } - const response = await actionPromise; - expect(performance.now() - lastResponseFinished).not.toBeLessThan(450); + clearTimeout(timer); + expect(timerTriggered).toBe(true); if (!isSetContent) expect(response.ok()).toBe(true); From 40164298a8190fd30fffcc96406782622531fef0 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 19 Feb 2020 16:08:25 -0800 Subject: [PATCH 12/29] api: remove ElementHandle.visibleRatio (#1069) --- docs/api.md | 8 +------- src/dom.ts | 19 ------------------- test/elementhandle.spec.js | 32 ++++++++------------------------ 3 files changed, 9 insertions(+), 50 deletions(-) diff --git a/docs/api.md b/docs/api.md index 727be831ad..c63a084fc2 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2292,7 +2292,6 @@ ElementHandle instances can be used as arguments in [`page.$eval()`](#pageevalse - [elementHandle.tripleclick([options])](#elementhandletripleclickoptions) - [elementHandle.type(text[, options])](#elementhandletypetext-options) - [elementHandle.uncheck([options])](#elementhandleuncheckoptions) -- [elementHandle.visibleRatio()](#elementhandlevisibleratio) - [jsHandle.asElement()](#jshandleaselement) @@ -2465,7 +2464,7 @@ If the element is detached from DOM, the method throws an error. #### elementHandle.scrollIntoViewIfNeeded() - returns: <[Promise]> Resolves after the element has been scrolled into view. -This method tries to scroll element into view, unless it is completely visible as defined by [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)'s ```ratio```. See also [elementHandle.visibleRatio()](#elementhandlevisibleratio). +This method tries to scroll element into view, unless it is completely visible as defined by [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)'s ```ratio```. Throws when ```elementHandle``` does not point to an element [connected](https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected) to a Document or a ShadowRoot. @@ -2556,11 +2555,6 @@ await elementHandle.press('Enter'); If element is not already unchecked, it scrolls it into view if needed, and then uses [elementHandle.click](#elementhandleclickoptions) to click in the center of the element. -#### elementHandle.visibleRatio() -- returns: <[Promise]<[number]>> Returns the visible ratio as defined by [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). - -Positive ratio means that some part of the element is visible in the current viewport. Ratio equal to one means that element is completely visible. - ### class: JSHandle JSHandle represents an in-page JavaScript object. JSHandles can be created with the [page.evaluateHandle](#pageevaluatehandlepagefunction-args) method. diff --git a/src/dom.ts b/src/dom.ts index 54a6205916..dccfc4f231 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -487,25 +487,6 @@ export class ElementHandle extends js.JSHandle { return result; } - visibleRatio(): Promise { - return this._evaluateInUtility(async (node: Node) => { - if (node.nodeType !== Node.ELEMENT_NODE) - throw new Error('Node is not of type HTMLElement'); - const element = node as Element; - const visibleRatio = await new Promise(resolve => { - const observer = new IntersectionObserver(entries => { - resolve(entries[0].intersectionRatio); - observer.disconnect(); - }); - observer.observe(element); - // Firefox doesn't call IntersectionObserver callback unless - // there are rafs. - requestAnimationFrame(() => {}); - }); - return visibleRatio; - }); - } - async _waitForStablePosition(options: types.TimeoutOptions = {}): Promise { const context = await this._context.frame._utilityContext(); const stablePromise = context.evaluate((injected: Injected, node: Node, timeout: number) => { diff --git a/test/elementhandle.spec.js b/test/elementhandle.spec.js index 6769eecbd2..b9b028ef70 100644 --- a/test/elementhandle.spec.js +++ b/test/elementhandle.spec.js @@ -296,36 +296,20 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) }); }); - describe('ElementHandle.visibleRatio', function() { - it('should work', async({page, server}) => { - await page.goto(server.PREFIX + '/offscreenbuttons.html'); - for (let i = 0; i < 11; ++i) { - const button = await page.$('#btn' + i); - const ratio = await button.visibleRatio(); - expect(Math.round(ratio * 10)).toBe(10 - i); - } - }); - it('should work when Node is removed', async({page, server}) => { - await page.goto(server.PREFIX + '/offscreenbuttons.html'); - await page.evaluate(() => delete window['Node']); - for (let i = 0; i < 11; ++i) { - const button = await page.$('#btn' + i); - const ratio = await button.visibleRatio(); - expect(Math.round(ratio * 10)).toBe(10 - i); - } - }); - }); - describe('ElementHandle.scrollIntoViewIfNeeded', function() { it.skip(FFOX)('should work', async({page, server}) => { await page.goto(server.PREFIX + '/offscreenbuttons.html'); for (let i = 0; i < 11; ++i) { const button = await page.$('#btn' + i); - const before = await button.visibleRatio(); - expect(Math.round(before * 10)).toBe(10 - i); + const before = await button.evaluate(button => { + return button.getBoundingClientRect().right - window.innerWidth; + }); + expect(before).toBe(10 * i); await button.scrollIntoViewIfNeeded(); - const after = await button.visibleRatio(); - expect(Math.round(after * 10)).toBe(10); + const after = await button.evaluate(button => { + return button.getBoundingClientRect().right - window.innerWidth; + }); + expect(after <= 0).toBe(true); await page.evaluate(() => window.scrollTo(0, 0)); } }); From 2fabaaf751b6cda2cd2a89b1516964e88ef723ee Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 19 Feb 2020 18:29:42 -0800 Subject: [PATCH 13/29] browser(webkit): force overlay scrollbars on mac, ignoring system setting (#1071) https://github.com/dgozman/webkit/commit/9ebd5359b7e78196a3cff6c9745ee3b3ec0fc6ab References #844. --- browser_patches/webkit/BUILD_NUMBER | 2 +- browser_patches/webkit/patches/bootstrap.diff | 101 +++++++++++++++++- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/browser_patches/webkit/BUILD_NUMBER b/browser_patches/webkit/BUILD_NUMBER index 691cbe1f97..d98057a110 100644 --- a/browser_patches/webkit/BUILD_NUMBER +++ b/browser_patches/webkit/BUILD_NUMBER @@ -1 +1 @@ -1151 +1152 diff --git a/browser_patches/webkit/patches/bootstrap.diff b/browser_patches/webkit/patches/bootstrap.diff index eb98b2391f..7b9ecf0e18 100644 --- a/browser_patches/webkit/patches/bootstrap.diff +++ b/browser_patches/webkit/patches/bootstrap.diff @@ -5652,6 +5652,46 @@ index 69a52e17a4a8932b5dfc5edfa72b922c18d43082..f859e07168077d10918f954cca699c4e WebProcess/InjectedBundle/API/glib/DOM/DOMObjectCache.cpp @no-unify WebProcess/InjectedBundle/API/glib/DOM/WebKitDOMDocument.cpp @no-unify WebProcess/InjectedBundle/API/glib/DOM/WebKitDOMElement.cpp @no-unify +diff --git a/Source/WebKit/UIProcess/API/APIProcessPoolConfiguration.cpp b/Source/WebKit/UIProcess/API/APIProcessPoolConfiguration.cpp +index d91e9532ec320ca84c9618f71ac0d0c421bea748..8f0ee1df58f2ec9cf2df0799d55e1e1b54d2fb67 100644 +--- a/Source/WebKit/UIProcess/API/APIProcessPoolConfiguration.cpp ++++ b/Source/WebKit/UIProcess/API/APIProcessPoolConfiguration.cpp +@@ -53,6 +53,9 @@ Ref ProcessPoolConfiguration::copy() + copy->m_ignoreSynchronousMessagingTimeoutsForTesting = this->m_ignoreSynchronousMessagingTimeoutsForTesting; + copy->m_attrStyleEnabled = this->m_attrStyleEnabled; + copy->m_overrideLanguages = this->m_overrideLanguages; ++#if PLATFORM(MAC) ++ copy->m_forceOverlayScrollbars = this->m_forceOverlayScrollbars; ++#endif + copy->m_alwaysRunsAtBackgroundPriority = this->m_alwaysRunsAtBackgroundPriority; + copy->m_shouldTakeUIBackgroundAssertion = this->m_shouldTakeUIBackgroundAssertion; + copy->m_shouldCaptureAudioInUIProcess = this->m_shouldCaptureAudioInUIProcess; +diff --git a/Source/WebKit/UIProcess/API/APIProcessPoolConfiguration.h b/Source/WebKit/UIProcess/API/APIProcessPoolConfiguration.h +index 8cf4d51e4b43ad1206accb596df79c8e10f6fb96..57b7fb31644564df157c83d9b19ac28b596c0af5 100644 +--- a/Source/WebKit/UIProcess/API/APIProcessPoolConfiguration.h ++++ b/Source/WebKit/UIProcess/API/APIProcessPoolConfiguration.h +@@ -98,6 +98,11 @@ public: + const Vector& overrideLanguages() const { return m_overrideLanguages; } + void setOverrideLanguages(Vector&& languages) { m_overrideLanguages = WTFMove(languages); } + ++#if PLATFORM(MAC) ++ bool forceOverlayScrollbars() const { return m_forceOverlayScrollbars; } ++ void setForceOverlayScrollbars(bool forceOverlayScrollbars) { m_forceOverlayScrollbars = forceOverlayScrollbars; } ++#endif ++ + bool alwaysRunsAtBackgroundPriority() const { return m_alwaysRunsAtBackgroundPriority; } + void setAlwaysRunsAtBackgroundPriority(bool alwaysRunsAtBackgroundPriority) { m_alwaysRunsAtBackgroundPriority = alwaysRunsAtBackgroundPriority; } + +@@ -158,6 +163,9 @@ private: + bool m_ignoreSynchronousMessagingTimeoutsForTesting { false }; + bool m_attrStyleEnabled { false }; + Vector m_overrideLanguages; ++#if PLATFORM(MAC) ++ bool m_forceOverlayScrollbars { false }; ++#endif + bool m_alwaysRunsAtBackgroundPriority { false }; + bool m_shouldTakeUIBackgroundAssertion { true }; + bool m_shouldCaptureAudioInUIProcess { false }; diff --git a/Source/WebKit/UIProcess/API/APIUIClient.h b/Source/WebKit/UIProcess/API/APIUIClient.h index 54513035b261bfd5e187ec945796198ed09d88b1..2d3200e4f6e90a16f248b255f75d2a7e17d895a5 100644 --- a/Source/WebKit/UIProcess/API/APIUIClient.h @@ -6006,6 +6046,39 @@ index 0000000000000000000000000000000000000000..f31834b80328fc5c182c64de53ff51e4 + +@implementation _WKBrowserContext +@end +diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.h b/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.h +index 238b6a20669190df8b15c5a8ae50400af0273839..bc09d61e28b31e1b2618f6bf8f378c00522f3fe9 100644 +--- a/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.h ++++ b/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.h +@@ -62,6 +62,7 @@ WK_CLASS_AVAILABLE(macos(10.10), ios(8.0)) + #endif + @property (nonatomic) pid_t presentingApplicationPID WK_API_AVAILABLE(macos(10.13), ios(11.0)); + @property (nonatomic) BOOL processSwapsOnNavigation WK_API_AVAILABLE(macos(10.14), ios(12.0)); ++@property (nonatomic) BOOL forceOverlayScrollbars WK_API_AVAILABLE(macos(10.14)); + @property (nonatomic) BOOL alwaysKeepAndReuseSwappedProcesses WK_API_AVAILABLE(macos(10.14), ios(12.0)); + @property (nonatomic) BOOL processSwapsOnWindowOpenWithOpener WK_API_AVAILABLE(macos(10.14), ios(12.0)); + @property (nonatomic) BOOL prewarmsProcessesAutomatically WK_API_AVAILABLE(macos(10.14.4), ios(12.2)); +diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.mm b/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.mm +index 6ec7c145fbbae9b6beb8fb5dc3e620f75fa20a32..6eaea7f858c2b475f7d127516ec736b46777a19f 100644 +--- a/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.mm ++++ b/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.mm +@@ -271,6 +271,16 @@ + return _processPoolConfiguration->processSwapsOnNavigation(); + } + ++- (void)setForceOverlayScrollbars:(BOOL)force ++{ ++ _processPoolConfiguration->setForceOverlayScrollbars(force); ++} ++ ++- (BOOL)forceOverlayScrollbars ++{ ++ return _processPoolConfiguration->forceOverlayScrollbars(); ++} ++ + - (void)setPrewarmsProcessesAutomatically:(BOOL)prewarms + { + _processPoolConfiguration->setIsAutomaticProcessWarmingEnabled(prewarms); diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKRemoteWebInspectorViewController.mm b/Source/WebKit/UIProcess/API/Cocoa/_WKRemoteWebInspectorViewController.mm index 2bdb8b9e7256f22096ebaa5d51959aa52389ca0a..2141d9d17497861a6c711f99306de10992a93ce8 100644 --- a/Source/WebKit/UIProcess/API/Cocoa/_WKRemoteWebInspectorViewController.mm @@ -7125,7 +7198,7 @@ index a3d0ded6df0566cb74493f98ec28bce0e8d893b3..a4d9a214407eb2c55c07a10b700abbb2 { auto delegate = m_uiDelegate.m_delegate.get(); diff --git a/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm b/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm -index f154a4e6fdfbe1dbcb23f07d329c7ae22bebd7f6..2a6ce63aa57d7a970cf5a8160e072d0dc9c24b82 100644 +index f154a4e6fdfbe1dbcb23f07d329c7ae22bebd7f6..fb98c957658dc47449b61ea32956f931694c8157 100644 --- a/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm +++ b/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm @@ -130,6 +130,13 @@ static void registerUserDefaultsIfNeeded() @@ -7142,6 +7215,15 @@ index f154a4e6fdfbe1dbcb23f07d329c7ae22bebd7f6..2a6ce63aa57d7a970cf5a8160e072d0d void WebProcessPool::updateProcessSuppressionState() { if (m_networkProcess) +@@ -311,7 +318,7 @@ void WebProcessPool::platformInitializeWebProcess(const WebProcessProxy& process + #if PLATFORM(MAC) + auto screenProperties = WebCore::collectScreenProperties(); + parameters.screenProperties = WTFMove(screenProperties); +- parameters.useOverlayScrollbars = ([NSScroller preferredScrollerStyle] == NSScrollerStyleOverlay); ++ parameters.useOverlayScrollbars = m_configuration->forceOverlayScrollbars() || ([NSScroller preferredScrollerStyle] == NSScrollerStyleOverlay); + #endif + + #if PLATFORM(IOS) @@ -385,6 +392,8 @@ void WebProcessPool::platformInitializeNetworkProcess(NetworkProcessCreationPara } } @@ -7151,6 +7233,17 @@ index f154a4e6fdfbe1dbcb23f07d329c7ae22bebd7f6..2a6ce63aa57d7a970cf5a8160e072d0d parameters.networkATSContext = adoptCF(_CFNetworkCopyATSContext()); parameters.shouldSuppressMemoryPressureHandler = [defaults boolForKey:WebKitSuppressMemoryPressureHandlerDefaultsKey]; +@@ -535,8 +544,8 @@ void WebProcessPool::registerNotificationObservers() + + #if ENABLE(WEBPROCESS_WINDOWSERVER_BLOCKING) + m_scrollerStyleNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSPreferredScrollerStyleDidChangeNotification object:nil queue:[NSOperationQueue currentQueue] usingBlock:^(NSNotification *notification) { +- auto scrollbarStyle = [NSScroller preferredScrollerStyle]; +- sendToAllProcesses(Messages::WebProcess::ScrollerStylePreferenceChanged(scrollbarStyle)); ++ bool useOverlayScrollbars = m_configuration->forceOverlayScrollbars() || ([NSScroller preferredScrollerStyle] == NSScrollerStyleOverlay); ++ sendToAllProcesses(Messages::WebProcess::ScrollerStylePreferenceChanged(useOverlayScrollbars)); + }]; + #endif + diff --git a/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp b/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp index 6bbd1cabd27ae2847648a8c2edcf9acfcd556ff5..38d101b9a96986e40f6e9f0261fa429a3d516e2e 100644 --- a/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp @@ -13928,10 +14021,10 @@ index 0000000000000000000000000000000000000000..41b5751cab626971cd12c5066218c6d1 +@end diff --git a/Tools/Playwright/mac/AppDelegate.m b/Tools/Playwright/mac/AppDelegate.m new file mode 100644 -index 0000000000000000000000000000000000000000..828d172381e12ea0455045fdc098dfa7726ae040 +index 0000000000000000000000000000000000000000..7fbd8c5426eb43b96a7c8e793a7f85810819053e --- /dev/null +++ b/Tools/Playwright/mac/AppDelegate.m -@@ -0,0 +1,488 @@ +@@ -0,0 +1,490 @@ +/* + * Copyright (C) 2010-2016 Apple Inc. All rights reserved. + * @@ -14120,6 +14213,7 @@ index 0000000000000000000000000000000000000000..828d172381e12ea0455045fdc098dfa7 + configuration.preferences._domTimersThrottlingEnabled = NO; + configuration.preferences._requestAnimationFrameEnabled = YES; + _WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease]; ++ processConfiguration.forceOverlayScrollbars = YES; + configuration.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration AndDataStore:configuration.websiteDataStore] autorelease]; + } + return configuration; @@ -14326,6 +14420,7 @@ index 0000000000000000000000000000000000000000..828d172381e12ea0455045fdc098dfa7 +{ + _WKBrowserContext *browserContext = [[_WKBrowserContext alloc] init]; + _WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease]; ++ processConfiguration.forceOverlayScrollbars = YES; + browserContext.dataStore = [WKWebsiteDataStore nonPersistentDataStore]; + browserContext.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease]; + [_browserContexts addObject:browserContext]; From 05a1e1c641b7ef68862a26694a6e986fa65214a2 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Thu, 20 Feb 2020 15:15:48 -0800 Subject: [PATCH 14/29] test: remove `newContext` and `newPage` test helpers (#1070) All tests should clean up their contexts themselves. --- test/browsercontext.spec.js | 113 ++++++++++++++++++---------- test/chromium/chromium.spec.js | 14 ++-- test/click.spec.js | 27 ++++--- test/cookies.spec.js | 26 ++++--- test/elementhandle.spec.js | 6 +- test/emulation.spec.js | 119 +++++++++++++++++++----------- test/features/permissions.spec.js | 5 +- test/geolocation.spec.js | 14 ++-- test/ignorehttpserrors.spec.js | 12 ++- test/interception.spec.js | 12 ++- test/mouse.spec.js | 5 +- test/playwright.spec.js | 23 ++---- test/popup.spec.js | 40 +++++----- test/screenshot.spec.js | 31 +++++--- test/types.d.ts | 4 +- 15 files changed, 275 insertions(+), 176 deletions(-) diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js index 5d24c3650f..b10d0cb41b 100644 --- a/test/browsercontext.spec.js +++ b/test/browsercontext.spec.js @@ -26,16 +26,16 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; describe('BrowserContext', function() { - it('should create new context', async function({browser, newContext}) { + it('should create new context', async function({browser}) { expect(browser.contexts().length).toBe(0); - const context = await newContext(); + const context = await browser.newContext(); expect(browser.contexts().length).toBe(1); expect(browser.contexts().indexOf(context) !== -1).toBe(true); await context.close(); expect(browser.contexts().length).toBe(0); }); - it('window.open should use parent tab context', async function({newContext, server}) { - const context = await newContext(); + it('window.open should use parent tab context', async function({browser, server}) { + const context = await browser.newContext(); const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); const [popupTarget] = await Promise.all([ @@ -43,11 +43,12 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE page.evaluate(url => window.open(url), server.EMPTY_PAGE) ]); expect(popupTarget.context()).toBe(context); + await context.close(); }); - it('should isolate localStorage and cookies', async function({browser, newContext, server}) { + it('should isolate localStorage and cookies', async function({browser, server}) { // Create two incognito contexts. - const context1 = await newContext(); - const context2 = await newContext(); + const context1 = await browser.newContext(); + const context2 = await browser.newContext(); expect((await context1.pages()).length).toBe(0); expect((await context2.pages()).length).toBe(0); @@ -88,69 +89,84 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE ]); expect(browser.contexts().length).toBe(0); }); - it('should propagate default viewport to the page', async({ newPage }) => { - const page = await newPage({ viewport: { width: 456, height: 789 } }); + it('should propagate default viewport to the page', async({ browser }) => { + const context = await browser.newContext({ viewport: { width: 456, height: 789 } }); + const page = await context.newPage(); expect(page.viewportSize().width).toBe(456); expect(page.viewportSize().height).toBe(789); expect(await page.evaluate('window.innerWidth')).toBe(456); expect(await page.evaluate('window.innerHeight')).toBe(789); + await context.close(); }); - it('should make a copy of default viewport', async({ newContext }) => { + it('should make a copy of default viewport', async({ browser }) => { const viewport = { width: 456, height: 789 }; - const context = await newContext({ viewport }); + const context = await browser.newContext({ viewport }); viewport.width = 567; const page = await context.newPage(); expect(page.viewportSize().width).toBe(456); expect(page.viewportSize().height).toBe(789); expect(await page.evaluate('window.innerWidth')).toBe(456); expect(await page.evaluate('window.innerHeight')).toBe(789); + await context.close(); }); }); describe('BrowserContext({userAgent})', function() { - it('should work', async({newPage, server}) => { + it('should work', async({browser, server}) => { { - const page = await newPage(); + const context = await browser.newContext(); + const page = await context.newPage(); expect(await page.evaluate(() => navigator.userAgent)).toContain('Mozilla'); + await context.close(); } { - const page = await newPage({ userAgent: 'foobar' }); + const context = await browser.newContext({ userAgent: 'foobar' }); + const page = await context.newPage(); const [request] = await Promise.all([ server.waitForRequest('/empty.html'), page.goto(server.EMPTY_PAGE), ]); expect(request.headers['user-agent']).toBe('foobar'); + await context.close(); } }); - it('should work for subframes', async({newPage, server}) => { + it('should work for subframes', async({browser, server}) => { { - const page = await newPage(); + const context = await browser.newContext(); + const page = await context.newPage(); expect(await page.evaluate(() => navigator.userAgent)).toContain('Mozilla'); + await context.close(); } { - const page = await newPage({ userAgent: 'foobar' }); + const context = await browser.newContext({ userAgent: 'foobar' }); + const page = await context.newPage(); const [request] = await Promise.all([ server.waitForRequest('/empty.html'), utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), ]); expect(request.headers['user-agent']).toBe('foobar'); + await context.close(); } }); - it('should emulate device user-agent', async({newPage, server}) => { + it('should emulate device user-agent', async({browser, server}) => { { - const page = await newPage(); + const context = await browser.newContext(); + const page = await context.newPage(); await page.goto(server.PREFIX + '/mobile.html'); expect(await page.evaluate(() => navigator.userAgent)).not.toContain('iPhone'); + await context.close(); } { - const page = await newPage({ userAgent: playwright.devices['iPhone 6'].userAgent }); + const context = await browser.newContext({ userAgent: playwright.devices['iPhone 6'].userAgent }); + const page = await context.newPage(); await page.goto(server.PREFIX + '/mobile.html'); expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone'); + await context.close(); } }); - it('should make a copy of default options', async({newContext, server}) => { + it('should make a copy of default options', async({browser, server}) => { const options = { userAgent: 'foobar' }; - const context = await newContext(options); + const context = await browser.newContext(options); options.userAgent = 'wrong'; const page = await context.newPage(); const [request] = await Promise.all([ @@ -158,50 +174,60 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE page.goto(server.EMPTY_PAGE), ]); expect(request.headers['user-agent']).toBe('foobar'); + await context.close(); }); }); describe('BrowserContext({bypassCSP})', function() { - it('should bypass CSP meta tag', async({newPage, server}) => { + it('should bypass CSP meta tag', async({browser, server}) => { // Make sure CSP prohibits addScriptTag. { - const page = await newPage(); + const context = await browser.newContext(); + const page = await context.newPage(); await page.goto(server.PREFIX + '/csp.html'); await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); expect(await page.evaluate(() => window.__injected)).toBe(undefined); + await context.close(); } // By-pass CSP and try one more time. { - const page = await newPage({ bypassCSP: true }); + const context = await browser.newContext({ bypassCSP: true }); + const page = await context.newPage(); await page.goto(server.PREFIX + '/csp.html'); await page.addScriptTag({content: 'window.__injected = 42;'}); expect(await page.evaluate(() => window.__injected)).toBe(42); + await context.close(); } }); - it('should bypass CSP header', async({newPage, server}) => { + it('should bypass CSP header', async({browser, server}) => { // Make sure CSP prohibits addScriptTag. server.setCSP('/empty.html', 'default-src "self"'); { - const page = await newPage(); + const context = await browser.newContext(); + const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); expect(await page.evaluate(() => window.__injected)).toBe(undefined); + await context.close(); } // By-pass CSP and try one more time. { - const page = await newPage({ bypassCSP: true }); + const context = await browser.newContext({ bypassCSP: true }); + const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); await page.addScriptTag({content: 'window.__injected = 42;'}); expect(await page.evaluate(() => window.__injected)).toBe(42); + await context.close(); } }); - it('should bypass after cross-process navigation', async({newPage, server}) => { - const page = await newPage({ bypassCSP: true }); + it('should bypass after cross-process navigation', async({browser, server}) => { + const context = await browser.newContext({ bypassCSP: true }); + const page = await context.newPage(); await page.goto(server.PREFIX + '/csp.html'); await page.addScriptTag({content: 'window.__injected = 42;'}); expect(await page.evaluate(() => window.__injected)).toBe(42); @@ -209,32 +235,38 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html'); await page.addScriptTag({content: 'window.__injected = 42;'}); expect(await page.evaluate(() => window.__injected)).toBe(42); + await context.close(); }); - it('should bypass CSP in iframes as well', async({newPage, server}) => { + it('should bypass CSP in iframes as well', async({browser, server}) => { // Make sure CSP prohibits addScriptTag in an iframe. { - const page = await newPage(); + const context = await browser.newContext(); + const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html'); await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); expect(await frame.evaluate(() => window.__injected)).toBe(undefined); + await context.close(); } // By-pass CSP and try one more time. { - const page = await newPage({ bypassCSP: true }); + const context = await browser.newContext({ bypassCSP: true }); + const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html'); await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); expect(await frame.evaluate(() => window.__injected)).toBe(42); + await context.close(); } }); }); describe('BrowserContext({javaScriptEnabled})', function() { - it('should work', async({newPage}) => { + it('should work', async({browser}) => { { - const page = await newPage({ javaScriptEnabled: false }); + const context = await browser.newContext({ javaScriptEnabled: false }); + const page = await context.newPage(); await page.goto('data:text/html, '); let error = null; await page.evaluate('something').catch(e => error = e); @@ -242,17 +274,22 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE expect(error.message).toContain('Can\'t find variable: something'); else expect(error.message).toContain('something is not defined'); + await context.close(); } { - const page = await newPage(); + const context = await browser.newContext(); + const page = await context.newPage(); await page.goto('data:text/html, '); expect(await page.evaluate('something')).toBe('forbidden'); + await context.close(); } }); - it('should be able to navigate after disabling javascript', async({newPage, server}) => { - const page = await newPage({ javaScriptEnabled: false }); + it('should be able to navigate after disabling javascript', async({browser, server}) => { + const context = await browser.newContext({ javaScriptEnabled: false }); + const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); + await context.close(); }); }); }; diff --git a/test/chromium/chromium.spec.js b/test/chromium/chromium.spec.js index 3234e8ef56..73e9342059 100644 --- a/test/chromium/chromium.spec.js +++ b/test/chromium/chromium.spec.js @@ -152,10 +152,10 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI expect(createdTarget.opener()).toBe(browser.pageTarget(page)); expect(browser.pageTarget(page).opener()).toBe(null); }); - it('should close all belonging targets once closing context', async function({browser, newContext}) { + it('should close all belonging targets once closing context', async function({browser}) { const targets = async (context) => (await browser.targets()).filter(t => t.type() === 'page' && t.context() === context); - const context = await newContext(); + const context = await browser.newContext(); await context.newPage(); expect((await targets(context)).length).toBe(1); expect((await context.pages()).length).toBe(1); @@ -166,8 +166,8 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI }); describe('Chromium.waitForTarget', () => { - it('should wait for a target', async function({browser, server, newContext}) { - const context = await newContext(); + it('should wait for a target', async function({server, browser}) { + const context = await browser.newContext(); let resolved = false; const targetPromise = browser.waitForTarget(target => target.context() === context && target.url() === server.EMPTY_PAGE); targetPromise.then(() => resolved = true); @@ -176,6 +176,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI await page.goto(server.EMPTY_PAGE); const target = await targetPromise; expect(await target.page()).toBe(page); + await context.close(); }); it('should timeout waiting for a non-existent target', async function({browser, context, server}) { const error = await browser.waitForTarget(target => target.context() === context && target.url() === server.EMPTY_PAGE, {timeout: 1}).catch(e => e); @@ -192,8 +193,8 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI expect(await target.page()).toBe(page); await page.context().close(); }); - it('should fire target events', async function({browser, newContext, server}) { - const context = await newContext(); + it('should fire target events', async function({browser, server}) { + const context = await browser.newContext(); const events = []; browser.on('targetcreated', target => events.push('CREATED: ' + target.url())); browser.on('targetchanged', target => events.push('CHANGED: ' + target.url())); @@ -206,6 +207,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI `CHANGED: ${server.EMPTY_PAGE}`, `DESTROYED: ${server.EMPTY_PAGE}` ]); + await context.close(); }); }); diff --git a/test/click.spec.js b/test/click.spec.js index 603be5ba45..0fc43e2ee2 100644 --- a/test/click.spec.js +++ b/test/click.spec.js @@ -59,13 +59,14 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI await page.click('span'); expect(await page.evaluate(() => window.CLICKED)).toBe(42); }); - it('should not throw UnhandledPromiseRejection when page closes', async({newContext, server}) => { - const context = await newContext(); + it('should not throw UnhandledPromiseRejection when page closes', async({browser, server}) => { + const context = await browser.newContext(); const page = await context.newPage(); await Promise.all([ page.close(), page.mouse.click(1, 2), ]).catch(e => {}); + await context.close(); }); it('should click the button after navigation ', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); @@ -81,14 +82,16 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI await page.click('button'); expect(await page.evaluate(() => result)).toBe('Clicked'); }); - it('should click with disabled javascript', async({newPage, server}) => { - const page = await newPage({ javaScriptEnabled: false }); + it('should click with disabled javascript', async({browser, server}) => { + const context = await browser.newContext({ javaScriptEnabled: false }); + const page = await context.newPage(); await page.goto(server.PREFIX + '/wrappedlink.html'); await Promise.all([ page.click('a'), page.waitForNavigation() ]); expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked'); + await context.close(); }); it('should click when one of inline box children is outside of viewport', async({page, server}) => { await page.setContent(` @@ -222,12 +225,13 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI expect(error.message).toBe('No node found for selector: button.does-not-exist'); }); // @see https://github.com/GoogleChrome/puppeteer/issues/161 - it('should not hang with touch-enabled viewports', async({server, newContext}) => { - const context = await newContext({ viewport: playwright.devices['iPhone 6'].viewport }); + it('should not hang with touch-enabled viewports', async({server, browser}) => { + const context = await browser.newContext({ viewport: playwright.devices['iPhone 6'].viewport }); const page = await context.newPage(); await page.mouse.down(); await page.mouse.move(100, 10); await page.mouse.up(); + await context.close(); }); it('should scroll and click the button', async({page, server}) => { await page.goto(server.PREFIX + '/input/scrollable.html'); @@ -298,8 +302,8 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI await frame.click('button'); expect(await frame.evaluate(() => window.result)).toBe('Clicked'); }); - it('should click the button with deviceScaleFactor set', async({newContext, server}) => { - const context = await newContext({ viewport: {width: 400, height: 400, deviceScaleFactor: 5} }); + it('should click the button with deviceScaleFactor set', async({browser, server}) => { + const context = await browser.newContext({ viewport: {width: 400, height: 400, deviceScaleFactor: 5} }); const page = await context.newPage(); expect(await page.evaluate(() => window.devicePixelRatio)).toBe(5); await page.setContent('
spacer
'); @@ -308,6 +312,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI const button = await frame.$('button'); await button.click(); expect(await frame.evaluate(() => window.result)).toBe('Clicked'); + await context.close(); }); it('should click the button with px border with relative point', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); @@ -357,8 +362,9 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI expect(await page.evaluate(() => offsetX)).toBe(WEBKIT ? 1900 + 8 : 1900); expect(await page.evaluate(() => offsetY)).toBe(WEBKIT ? 1910 + 8 : 1910); }); - it('should click the button with relative point with page scale', async({newPage, server}) => { - const page = await newPage({ viewport: { width: 400, height: 400, isMobile: true} }); + it('should click the button with relative point with page scale', async({browser, server}) => { + const context = await browser.newContext({ viewport: { width: 400, height: 400, isMobile: true} }); + const page = await context.newPage(); await page.goto(server.PREFIX + '/input/button.html'); await page.$eval('button', button => { button.style.borderWidth = '8px'; @@ -376,6 +382,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI } expect(await page.evaluate(() => pageX)).toBe(expected.x); expect(await page.evaluate(() => pageY)).toBe(expected.y); + await context.close(); }); it('should wait for stable position', async({page, server}) => { diff --git a/test/cookies.spec.js b/test/cookies.spec.js index 767c122c8f..4eb1ae6c81 100644 --- a/test/cookies.spec.js +++ b/test/cookies.spec.js @@ -168,8 +168,8 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows await page.goto(server.EMPTY_PAGE); expect(cookie).toBe('cookie=value'); }); - it('should isolate cookies in browser contexts', async({context, server, newContext}) => { - const anotherContext = await newContext(); + it('should isolate cookies in browser contexts', async({context, server, browser}) => { + const anotherContext = await browser.newContext(); await context.setCookies([{url: server.EMPTY_PAGE, name: 'isolatecookie', value: 'page1value'}]); await anotherContext.setCookies([{url: server.EMPTY_PAGE, name: 'isolatecookie', value: 'page2value'}]); @@ -181,8 +181,9 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows expect(cookies1[0].value).toBe('page1value'); expect(cookies2[0].name).toBe('isolatecookie'); expect(cookies2[0].value).toBe('page2value'); + await anotherContext.close(); }); - it('should isolate session cookies', async({context, server, newContext}) => { + it('should isolate session cookies', async({context, server, browser}) => { server.setRoute('/setcookie.html', (req, res) => { res.setHeader('Set-Cookie', 'session=value'); res.end(); @@ -199,14 +200,15 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows expect(cookies.map(c => c.value).join(',')).toBe('value'); } { - const context2 = await newContext(); + const context2 = await browser.newContext(); const page = await context2.newPage(); await page.goto(server.EMPTY_PAGE); const cookies = await context2.cookies(); expect(cookies[0] && cookies[0].name).toBe(undefined); + await context2.close(); } }); - it.skip(WEBKIT)('should isolate persistent cookies', async({context, server, newContext}) => { + it.skip(WEBKIT)('should isolate persistent cookies', async({context, server, browser}) => { server.setRoute('/setcookie.html', (req, res) => { res.setHeader('Set-Cookie', 'persistent=persistent-value; max-age=3600'); res.end(); @@ -215,7 +217,7 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows await page.goto(server.PREFIX + '/setcookie.html'); const context1 = context; - const context2 = await newContext(); + const context2 = await browser.newContext(); const [page1, page2] = await Promise.all([context1.newPage(), context2.newPage()]); await Promise.all([page1.goto(server.EMPTY_PAGE), page2.goto(server.EMPTY_PAGE)]); const [cookies1, cookies2] = await Promise.all([context1.cookies(), context2.cookies()]); @@ -223,8 +225,9 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows expect(cookies1[0].name).toBe('persistent'); expect(cookies1[0].value).toBe('persistent-value'); expect(cookies2.length).toBe(0); + await context2.close(); }); - it('should isolate send cookie header', async({server, context, newContext}) => { + it('should isolate send cookie header', async({server, context, browser}) => { let cookie = []; server.setRoute('/empty.html', (req, res) => { cookie = req.headers.cookie || ''; @@ -237,9 +240,11 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows expect(cookie).toBe('sendcookie=value'); } { - const page = await (await newContext()).newPage(); + const context = await browser.newContext(); + const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); expect(cookie).toBe(''); + await context.close(); } }); it('should isolate cookies between launches', async({server}) => { @@ -449,8 +454,8 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows await context.clearCookies(); expect(await page.evaluate('document.cookie')).toBe(''); }); - it('should isolate cookies when clearing', async({context, server, newContext}) => { - const anotherContext = await newContext(); + it('should isolate cookies when clearing', async({context, server, browser}) => { + const anotherContext = await browser.newContext(); await context.setCookies([{url: server.EMPTY_PAGE, name: 'page1cookie', value: 'page1value'}]); await anotherContext.setCookies([{url: server.EMPTY_PAGE, name: 'page2cookie', value: 'page2value'}]); @@ -464,6 +469,7 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows await anotherContext.clearCookies(); expect((await context.cookies()).length).toBe(0); expect((await anotherContext.cookies()).length).toBe(0); + await anotherContext.close(); }); }); }; diff --git a/test/elementhandle.spec.js b/test/elementhandle.spec.js index b9b028ef70..885fa30398 100644 --- a/test/elementhandle.spec.js +++ b/test/elementhandle.spec.js @@ -68,8 +68,9 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) }, element); expect(pwBoundingBox).toEqual(webBoundingBox); }); - it('should work with page scale', async({newPage, server}) => { - const page = await newPage({ viewport: { width: 400, height: 400, isMobile: true} }); + it('should work with page scale', async({browser, server}) => { + const context = await browser.newContext({ viewport: { width: 400, height: 400, isMobile: true} }); + const page = await context.newPage(); await page.goto(server.PREFIX + '/input/button.html'); const button = await page.$('button'); await button.evaluate(button => { @@ -85,6 +86,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) expect(Math.round(box.y * 100)).toBe(23 * 100); expect(Math.round(box.width * 100)).toBe(200 * 100); expect(Math.round(box.height * 100)).toBe(20 * 100); + await context.close(); }); it('should work when inline box child is outside of viewport', async({page, server}) => { await page.setContent(` diff --git a/test/emulation.spec.js b/test/emulation.spec.js index c7fea1b257..3f9140999d 100644 --- a/test/emulation.spec.js +++ b/test/emulation.spec.js @@ -38,13 +38,14 @@ module.exports.describe = function({testRunner, expect, playwright, headless, FF expect(await page.evaluate(() => window.innerWidth)).toBe(123); expect(await page.evaluate(() => window.innerHeight)).toBe(456); }); - it('should support mobile emulation', async({newContext, server}) => { - const context = await newContext({ viewport: iPhone.viewport }); + it('should support mobile emulation', async({browser, server}) => { + const context = await browser.newContext({ viewport: iPhone.viewport }); const page = await context.newPage(); await page.goto(server.PREFIX + '/mobile.html'); expect(await page.evaluate(() => window.innerWidth)).toBe(375); await page.setViewportSize({width: 400, height: 300}); expect(await page.evaluate(() => window.innerWidth)).toBe(400); + await context.close(); }); it('should not have touch by default', async({page, server}) => { await page.goto(server.PREFIX + '/mobile.html'); @@ -52,12 +53,13 @@ module.exports.describe = function({testRunner, expect, playwright, headless, FF await page.goto(server.PREFIX + '/detect-touch.html'); expect(await page.evaluate(() => document.body.textContent.trim())).toBe('NO'); }); - it('should support touch emulation', async({newContext, server}) => { - const context = await newContext({ viewport: iPhone.viewport }); + it('should support touch emulation', async({browser, server}) => { + const context = await browser.newContext({ viewport: iPhone.viewport }); const page = await context.newPage(); await page.goto(server.PREFIX + '/mobile.html'); expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true); expect(await page.evaluate(dispatchTouch)).toBe('Received touch'); + await context.close(); function dispatchTouch() { let fulfill; @@ -72,30 +74,34 @@ module.exports.describe = function({testRunner, expect, playwright, headless, FF return promise; } }); - it('should be detectable by Modernizr', async({newContext, server}) => { - const context = await newContext({ viewport: iPhone.viewport }); + it('should be detectable by Modernizr', async({browser, server}) => { + const context = await browser.newContext({ viewport: iPhone.viewport }); const page = await context.newPage(); await page.goto(server.PREFIX + '/detect-touch.html'); expect(await page.evaluate(() => document.body.textContent.trim())).toBe('YES'); + await context.close(); }); - it('should detect touch when applying viewport with touches', async({newContext, server}) => { - const context = await newContext({ viewport: { width: 800, height: 600, isMobile: true } }); + it('should detect touch when applying viewport with touches', async({browser, server}) => { + const context = await browser.newContext({ viewport: { width: 800, height: 600, isMobile: true } }); const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); await page.addScriptTag({url: server.PREFIX + '/modernizr.js'}); expect(await page.evaluate(() => Modernizr.touchevents)).toBe(true); + await context.close(); }); - it.skip(FFOX)('should support landscape emulation', async({newContext, server}) => { - const context1 = await newContext({ viewport: iPhone.viewport }); + it.skip(FFOX)('should support landscape emulation', async({browser, server}) => { + const context1 = await browser.newContext({ viewport: iPhone.viewport }); const page1 = await context1.newPage(); await page1.goto(server.PREFIX + '/mobile.html'); expect(await page1.evaluate(() => matchMedia('(orientation: landscape)').matches)).toBe(false); - const context2 = await newContext({ viewport: iPhoneLandscape.viewport }); + const context2 = await browser.newContext({ viewport: iPhoneLandscape.viewport }); const page2 = await context2.newPage(); expect(await page2.evaluate(() => matchMedia('(orientation: landscape)').matches)).toBe(true); + await context1.close(); + await context2.close(); }); - it.skip(FFOX || WEBKIT)('should fire orientationchange event', async({newContext, server}) => { - const context = await newContext({ viewport: { width: 300, height: 400, isMobile: true } }); + it.skip(FFOX || WEBKIT)('should fire orientationchange event', async({browser, server}) => { + const context = await browser.newContext({ viewport: { width: 300, height: 400, isMobile: true } }); const page = await context.newPage(); await page.goto(server.PREFIX + '/mobile.html'); await page.evaluate(() => { @@ -110,35 +116,42 @@ module.exports.describe = function({testRunner, expect, playwright, headless, FF const event2 = page.waitForEvent('console'); await page.setViewportSize({width: 300, height: 400}); expect((await event2).text()).toBe('2'); + await context.close(); }); - it.skip(FFOX)('default mobile viewports to 980 width', async({newContext, server}) => { - const context = await newContext({ viewport: {width: 320, height: 480, isMobile: true} }); + it.skip(FFOX)('default mobile viewports to 980 width', async({browser, server}) => { + const context = await browser.newContext({ viewport: {width: 320, height: 480, isMobile: true} }); const page = await context.newPage(); await page.goto(server.PREFIX + '/empty.html'); expect(await page.evaluate(() => window.innerWidth)).toBe(980); + await context.close(); }); - it.skip(FFOX)('respect meta viewport tag', async({newContext, server}) => { - const context = await newContext({ viewport: {width: 320, height: 480, isMobile: true} }); + it.skip(FFOX)('respect meta viewport tag', async({browser, server}) => { + const context = await browser.newContext({ viewport: {width: 320, height: 480, isMobile: true} }); const page = await context.newPage(); await page.goto(server.PREFIX + '/mobile.html'); expect(await page.evaluate(() => window.innerWidth)).toBe(320); + await context.close(); }); }); describe('Page.emulate', function() { - it('should work', async({newPage, server}) => { - const page = await newPage({ viewport: iPhone.viewport, userAgent: iPhone.userAgent }); + it('should work', async({browser, server}) => { + const context = await browser.newContext({viewport: iPhone.viewport, userAgent: iPhone.userAgent}); + const page = await context.newPage(); await page.goto(server.PREFIX + '/mobile.html'); expect(await page.evaluate(() => window.innerWidth)).toBe(375); expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone'); + await context.close(); }); - it('should support clicking', async({newPage, server}) => { - const page = await newPage({ viewport: iPhone.viewport, userAgent: iPhone.userAgent }); + it('should support clicking', async({browser, server}) => { + const context = await browser.newContext({ viewport: iPhone.viewport, userAgent: iPhone.userAgent }); + const page = await context.newPage(); await page.goto(server.PREFIX + '/input/button.html'); const button = await page.$('button'); await page.evaluate(button => button.style.marginTop = '200px', button); await button.click(); expect(await page.evaluate(() => result)).toBe('Clicked'); + await context.close(); }); }); @@ -201,72 +214,94 @@ module.exports.describe = function({testRunner, expect, playwright, headless, FF }); describe.skip(FFOX)('BrowserContext({timezoneId})', function() { - it('should work', async ({ newPage }) => { + it('should work', async ({ browser }) => { const func = () => new Date(1479579154987).toString(); { - const page = await newPage({ timezoneId: 'America/Jamaica' }); + const context = await browser.newContext({ timezoneId: 'America/Jamaica' }); + const page = await context.newPage(); expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)'); + await context.close(); } { - const page = await newPage({ timezoneId: 'Pacific/Honolulu' }); + const context = await browser.newContext({ timezoneId: 'Pacific/Honolulu' }); + const page = await context.newPage(); expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)'); + await context.close(); } { - const page = await newPage({ timezoneId: 'America/Buenos_Aires' }); + const context = await browser.newContext({ timezoneId: 'America/Buenos_Aires' }); + const page = await context.newPage(); expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)'); + await context.close(); } { - const page = await newPage({ timezoneId: 'Europe/Berlin' }); + const context = await browser.newContext({ timezoneId: 'Europe/Berlin' }); + const page = await context.newPage(); expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)'); + await context.close(); } }); - it('should throw for invalid timezone IDs', async({newPage}) => { - let error = null; - await newPage({ timezoneId: 'Foo/Bar' }).catch(e => error = e); - expect(error.message).toBe('Invalid timezone ID: Foo/Bar'); - await newPage({ timezoneId: 'Baz/Qux' }).catch(e => error = e); - expect(error.message).toBe('Invalid timezone ID: Baz/Qux'); + it('should throw for invalid timezone IDs when creating pages', async({browser}) => { + for (const timezoneId of ['Foo/Bar', 'Baz/Qux']) { + let error = null; + const context = await browser.newContext({ timezoneId }); + const page = await context.newPage().catch(e => error = e); + expect(error.message).toBe(`Invalid timezone ID: ${timezoneId}`); + await context.close(); + } }); }); describe.skip(CHROMIUM || FFOX)('BrowserContext({locale})', function() { - it('should affect accept-language header', async({newPage, server}) => { - const page = await newPage({ locale: 'fr-CH' }); + it('should affect accept-language header', async({browser, server}) => { + const context = await browser.newContext({ locale: 'fr-CH' }); + const page = await context.newPage(); const [request] = await Promise.all([ server.waitForRequest('/empty.html'), page.goto(server.EMPTY_PAGE), ]); expect(request.headers['accept-language'].substr(0, 5)).toBe('fr-CH'); + await context.close(); }); - it('should affect navigator.language', async({newPage, server}) => { - const page = await newPage({ locale: 'fr-CH' }); + it('should affect navigator.language', async({browser, server}) => { + const context = await browser.newContext({ locale: 'fr-CH' }); + const page = await context.newPage(); expect(await page.evaluate(() => navigator.language)).toBe('fr-CH'); + await context.close(); }); - it('should format number', async({newPage, server}) => { + it('should format number', async({browser, server}) => { { - const page = await newPage({ locale: 'en-US' }); + const context = await browser.newContext({ locale: 'en-US' }); + const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); expect(await page.evaluate(() => (1000000.50).toLocaleString())).toBe('1,000,000.5'); + await context.close(); } { - const page = await newPage({ locale: 'fr-CH' }); + const context = await browser.newContext({ locale: 'fr-CH' }); + const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); expect(await page.evaluate(() => (1000000.50).toLocaleString().replace(/\s/g, ' '))).toBe('1 000 000,5'); + await context.close(); } }); - it('should format date', async({newPage, server}) => { + it('should format date', async({browser, server}) => { { - const page = await newPage({ locale: 'en-US', timezoneId: 'America/Los_Angeles' }); + const context = await browser.newContext({ locale: 'en-US', timezoneId: 'America/Los_Angeles' }); + const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); const formatted = 'Sat Nov 19 2016 10:12:34 GMT-0800 (Pacific Standard Time)'; expect(await page.evaluate(() => new Date(1479579154987).toString())).toBe(formatted); + await context.close(); } { - const page = await newPage({ locale: 'de-DE', timezoneId: 'Europe/Berlin' }); + const context = await browser.newContext({ locale: 'de-DE', timezoneId: 'Europe/Berlin' }); + const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); expect(await page.evaluate(() => new Date(1479579154987).toString())).toBe( 'Sat Nov 19 2016 19:12:34 GMT+0100 (Mitteleuropäische Normalzeit)'); + await context.close(); } }); }); diff --git a/test/features/permissions.spec.js b/test/features/permissions.spec.js index 86d0148903..af38b1c09f 100644 --- a/test/features/permissions.spec.js +++ b/test/features/permissions.spec.js @@ -78,9 +78,9 @@ module.exports.describe = function({testRunner, expect, WEBKIT}) { await context.clearPermissions(); expect(await page.evaluate(() => window['events'])).toEqual(['prompt', 'denied', 'granted', 'prompt']); }); - it('should isolate permissions between browser contexs', async({page, server, context, newContext}) => { + it('should isolate permissions between browser contexs', async({page, server, context, browser}) => { await page.goto(server.EMPTY_PAGE); - const otherContext = await newContext(); + const otherContext = await browser.newContext(); const otherPage = await otherContext.newPage(); await otherPage.goto(server.EMPTY_PAGE); expect(await getPermission(page, 'geolocation')).toBe('prompt'); @@ -94,6 +94,7 @@ module.exports.describe = function({testRunner, expect, WEBKIT}) { await context.clearPermissions(); expect(await getPermission(page, 'geolocation')).toBe('prompt'); expect(await getPermission(otherPage, 'geolocation')).toBe('granted'); + await otherContext.close(); }); }); }; diff --git a/test/geolocation.spec.js b/test/geolocation.spec.js index 4e0a30ec93..4cc0f67ac5 100644 --- a/test/geolocation.spec.js +++ b/test/geolocation.spec.js @@ -54,27 +54,28 @@ module.exports.describe = function ({ testRunner, expect, FFOX, WEBKIT }) { } expect(error.message).toContain('Invalid latitude "undefined"'); }); - it('should not modify passed default options object', async({newContext}) => { + it('should not modify passed default options object', async({browser}) => { const geolocation = { longitude: 10, latitude: 10 }; const options = { geolocation }; - const context = await newContext(options); + const context = await browser.newContext(options); await context.setGeolocation({ longitude: 20, latitude: 20 }); expect(options.geolocation).toBe(geolocation); + await context.close(); }); - it('should throw with missing longitude in default options', async({newContext}) => { + it('should throw with missing longitude in default options', async({browser}) => { let error = null; try { - const context = await newContext({ geolocation: {latitude: 10} }); + const context = await browser.newContext({ geolocation: {latitude: 10} }); await context.close(); } catch (e) { error = e; } expect(error.message).toContain('Invalid longitude "undefined"'); }); - it('should use context options', async({newContext, server}) => { + it('should use context options', async({browser, server}) => { const options = { geolocation: { longitude: 10, latitude: 10 }, permissions: {} }; options.permissions[server.PREFIX] = ['geolocation']; - const context = await newContext(options); + const context = await browser.newContext(options); const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); @@ -85,6 +86,7 @@ module.exports.describe = function ({ testRunner, expect, FFOX, WEBKIT }) { latitude: 10, longitude: 10 }); + await context.close(); }); }); }; diff --git a/test/ignorehttpserrors.spec.js b/test/ignorehttpserrors.spec.js index b2457edf87..a240637d20 100644 --- a/test/ignorehttpserrors.spec.js +++ b/test/ignorehttpserrors.spec.js @@ -23,24 +23,28 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p const {it, fit, xit, dit} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; describe('ignoreHTTPSErrors', function() { - it('should work', async({newPage, httpsServer}) => { + it('should work', async({browser, httpsServer}) => { let error = null; - const page = await newPage({ ignoreHTTPSErrors: true }); + const context = await browser.newContext({ ignoreHTTPSErrors: true }); + const page = await context.newPage(); const response = await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e); expect(error).toBe(null); expect(response.ok()).toBe(true); + await context.close(); }); - it('should work with mixed content', async({newPage, server, httpsServer}) => { + it('should work with mixed content', async({browser, server, httpsServer}) => { httpsServer.setRoute('/mixedcontent.html', (req, res) => { res.end(``); }); - const page = await newPage({ ignoreHTTPSErrors: true }); + const context = await browser.newContext({ ignoreHTTPSErrors: true }); + const page = await context.newPage(); 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/GoogleChrome/puppeteer/issues/2709 expect(await page.frames()[0].evaluate('1 + 2')).toBe(3); expect(await page.frames()[1].evaluate('2 + 3')).toBe(5); + await context.close(); }); }); }; diff --git a/test/interception.spec.js b/test/interception.spec.js index 8e3e86b9cd..7bdf9ca815 100644 --- a/test/interception.spec.js +++ b/test/interception.spec.js @@ -325,9 +325,10 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p await request.continue().catch(e => error = e); expect(error).toBe(null); }); - it('should throw if interception is not enabled', async({newPage, server}) => { + it('should throw if interception is not enabled', async({browser, server}) => { let error = null; - const page = await newPage(); + const context = await browser.newContext(); + const page = await context.newPage(); page.on('request', async request => { try { await request.continue(); @@ -337,6 +338,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); await page.goto(server.EMPTY_PAGE); expect(error.message).toContain('Request Interception is not enabled'); + await context.close(); }); it('should intercept main resource during cross-process navigation', async({page, server}) => { await page.goto(server.EMPTY_PAGE); @@ -558,12 +560,14 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); describe('ignoreHTTPSErrors', function() { - it('should work with request interception', async({newPage, httpsServer}) => { - const page = await newPage({ ignoreHTTPSErrors: true, interceptNetwork: true }); + it('should work with request interception', async({browser, httpsServer}) => { + const context = await browser.newContext({ ignoreHTTPSErrors: true, interceptNetwork: true }); + const page = await context.newPage(); await page.route('**/*', request => request.continue()); const response = await page.goto(httpsServer.EMPTY_PAGE); expect(response.status()).toBe(200); + await context.close(); }); }); diff --git a/test/mouse.spec.js b/test/mouse.spec.js index de1ac8b3d3..4fc86adec6 100644 --- a/test/mouse.spec.js +++ b/test/mouse.spec.js @@ -133,8 +133,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT, ]); }); // @see https://crbug.com/929806 - it('should work with mobile viewports and cross process navigations', async({newContext, server}) => { - const context = await newContext({ viewport: {width: 360, height: 640, isMobile: true} }); + it('should work with mobile viewports and cross process navigations', async({browser, server}) => { + const context = await browser.newContext({ viewport: {width: 360, height: 640, isMobile: true} }); const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); await page.goto(server.CROSS_PROCESS_PREFIX + '/mobile.html'); @@ -147,6 +147,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT, await page.mouse.click(30, 40); expect(await page.evaluate('result')).toEqual({x: 30, y: 40}); + await context.close(); }); }); }; diff --git a/test/playwright.spec.js b/test/playwright.spec.js index fcd6e1b86a..2caacea451 100644 --- a/test/playwright.spec.js +++ b/test/playwright.spec.js @@ -112,7 +112,6 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => { }); beforeEach(async(state, test) => { - const contexts = []; const onLine = (line) => test.output += line + '\n'; if (dumpProtocolOnFailure) state.browser._setDebugFunction(onLine); @@ -125,7 +124,6 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => { } state.tearDown = async () => { - await Promise.all(contexts.map(c => c.close())); if (rl) { rl.removeListener('line', onLine); rl.close(); @@ -133,30 +131,25 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => { if (dumpProtocolOnFailure) state.browser._setDebugFunction(() => void 0); }; - - state.newContext = async (options) => { - const context = await state.browser.newContext(options); - contexts.push(context); - return context; - }; - - state.newPage = async (options) => { - const context = await state.newContext(options); - return await context.newPage(); - }; }); - afterEach(async state => { + afterEach(async (state, test) => { + if (state.browser.contexts().length !== 0) { + if (test.result === 'ok') + console.warn(`\nWARNING: test "${test.fullName}" (${test.location.fileName}:${test.location.lineNumber}) did not close all created contexts!\n`); + await Promise.all(state.browser.contexts().map(context => context.close())); + } await state.tearDown(); }); describe('Page', function() { beforeEach(async state => { - state.context = await state.newContext(); + state.context = await state.browser.newContext(); state.page = await state.context.newPage(); }); afterEach(async state => { + await state.context.close(); state.context = null; state.page = null; }); diff --git a/test/popup.spec.js b/test/popup.spec.js index be90356725..a19d956d65 100644 --- a/test/popup.spec.js +++ b/test/popup.spec.js @@ -20,8 +20,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; describe('window.open', function() { - it.skip(CHROMIUM || WEBKIT)('should inherit user agent from browser context', async function({newContext, server}) { - const context = await newContext({ + it.skip(CHROMIUM || WEBKIT)('should inherit user agent from browser context', async function({browser, server}) { + const context = await browser.newContext({ userAgent: 'hey' }); const page = await context.newPage(); @@ -36,8 +36,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE expect(userAgent).toBe('hey'); expect(request.headers['user-agent']).toBe('hey'); }); - it.skip(CHROMIUM)('should inherit touch support from browser context', async function({newContext, server}) { - const context = await newContext({ + it.skip(CHROMIUM)('should inherit touch support from browser context', async function({browser, server}) { + const context = await browser.newContext({ viewport: { width: 400, height: 500, isMobile: true } }); const page = await context.newPage(); @@ -49,8 +49,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE await context.close(); expect(hasTouch).toBe(true); }); - it.skip(CHROMIUM || WEBKIT)('should inherit viewport size from browser context', async function({newContext, server}) { - const context = await newContext({ + it.skip(CHROMIUM || WEBKIT)('should inherit viewport size from browser context', async function({browser, server}) { + const context = await browser.newContext({ viewport: { width: 400, height: 500, isMobile: true } }); const page = await context.newPage(); @@ -65,8 +65,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE }); describe('Page.Events.Popup', function() { - it('should work', async({newContext}) => { - const context = await newContext(); + it('should work', async({browser}) => { + const context = await browser.newContext(); const page = await context.newPage(); const [popup] = await Promise.all([ new Promise(x => page.once('popup', x)), @@ -76,8 +76,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE expect(await popup.evaluate(() => !!window.opener)).toBe(true); await context.close(); }); - it.skip(CHROMIUM)('should work with empty url', async({newContext}) => { - const context = await newContext(); + it.skip(CHROMIUM)('should work with empty url', async({browser}) => { + const context = await browser.newContext(); const page = await context.newPage(); const [popup] = await Promise.all([ new Promise(x => page.once('popup', x)), @@ -87,8 +87,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE expect(await popup.evaluate(() => !!window.opener)).toBe(true); await context.close(); }); - it('should work with noopener', async({newContext}) => { - const context = await newContext(); + it('should work with noopener', async({browser}) => { + const context = await browser.newContext(); const page = await context.newPage(); const [popup] = await Promise.all([ new Promise(x => page.once('popup', x)), @@ -98,8 +98,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE expect(await popup.evaluate(() => !!window.opener)).toBe(false); await context.close(); }); - it('should work with clicking target=_blank', async({newContext, server}) => { - const context = await newContext(); + it('should work with clicking target=_blank', async({browser, server}) => { + const context = await browser.newContext(); const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); await page.setContent('yo'); @@ -111,8 +111,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE expect(await popup.evaluate(() => !!window.opener)).toBe(true); await context.close(); }); - it('should work with fake-clicking target=_blank and rel=noopener', async({newContext, server}) => { - const context = await newContext(); + it('should work with fake-clicking target=_blank and rel=noopener', async({browser, server}) => { + const context = await browser.newContext(); const page = await context.newPage(); // TODO: FFOX sends events for "one-style.html" request to both pages. await page.goto(server.EMPTY_PAGE); @@ -127,8 +127,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE expect(await popup.evaluate(() => !!window.opener)).toBe(false); await context.close(); }); - it('should work with clicking target=_blank and rel=noopener', async({newContext, server}) => { - const context = await newContext(); + it('should work with clicking target=_blank and rel=noopener', async({browser, server}) => { + const context = await browser.newContext(); const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); await page.setContent('yo'); @@ -140,8 +140,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE expect(await popup.evaluate(() => !!window.opener)).toBe(false); await context.close(); }); - it('should not treat navigations as new popups', async({newContext, server}) => { - const context = await newContext(); + it('should not treat navigations as new popups', async({browser, server}) => { + const context = await browser.newContext(); const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); await page.setContent('yo'); diff --git a/test/screenshot.spec.js b/test/screenshot.spec.js index 6d6b64b255..2fcb2090ad 100644 --- a/test/screenshot.spec.js +++ b/test/screenshot.spec.js @@ -156,12 +156,13 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM, }); expect(Buffer.from(screenshot, 'base64')).toBeGolden('screenshot-sanity.png'); }); - it.skip(FFOX)('should work with a mobile viewport', async({newContext, server}) => { - const context = await newContext({viewport: { width: 320, height: 480, isMobile: true }}); + it.skip(FFOX)('should work with a mobile viewport', async({browser, server}) => { + const context = await browser.newContext({viewport: { width: 320, height: 480, isMobile: true }}); const page = await context.newPage(); await page.goto(server.PREFIX + '/overflow.html'); const screenshot = await page.screenshot(); expect(screenshot).toBeGolden('screenshot-mobile.png'); + await context.close(); }); it('should work for canvas', async({page, server}) => { await page.setViewportSize({width: 500, height: 500}); @@ -358,8 +359,9 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM, const screenshot = await elementHandle.screenshot(); expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png'); }); - it('should take screenshots when default viewport is null', async({server, newPage}) => { - const page = await newPage({ viewport: null }); + it('should take screenshots when default viewport is null', async({server, browser}) => { + const context = await browser.newContext({ viewport: null }); + const page = await context.newPage(); await page.goto(server.PREFIX + '/grid.html'); const sizeBefore = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight })); const screenshot = await page.screenshot(); @@ -368,9 +370,11 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM, const sizeAfter = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight })); expect(sizeBefore.width).toBe(sizeAfter.width); expect(sizeBefore.height).toBe(sizeAfter.height); + await context.close(); }); - it('should take fullPage screenshots when default viewport is null', async({server, newPage}) => { - const page = await newPage({ viewport: null }); + it('should take fullPage screenshots when default viewport is null', async({server, browser}) => { + const context = await browser.newContext({ viewport: null }); + const page = await context.newPage(); await page.goto(server.PREFIX + '/grid.html'); const sizeBefore = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight })); const screenshot = await page.screenshot({ @@ -381,9 +385,11 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM, const sizeAfter = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight })); expect(sizeBefore.width).toBe(sizeAfter.width); expect(sizeBefore.height).toBe(sizeAfter.height); + await context.close(); }); - it('should restore default viewport after fullPage screenshot', async({ newPage }) => { - const page = await newPage({ viewport: { width: 456, height: 789 } }); + it('should restore default viewport after fullPage screenshot', async({ browser }) => { + const context = await browser.newContext({ viewport: { width: 456, height: 789 } }); + const page = await context.newPage(); expect(page.viewportSize().width).toBe(456); expect(page.viewportSize().height).toBe(789); expect(await page.evaluate('window.innerWidth')).toBe(456); @@ -394,9 +400,11 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM, expect(page.viewportSize().height).toBe(789); expect(await page.evaluate('window.innerWidth')).toBe(456); expect(await page.evaluate('window.innerHeight')).toBe(789); + await context.close(); }); - it('should take element screenshot when default viewport is null and restore back', async({server, newPage}) => { - const page = await newPage({ viewport: null }); + it('should take element screenshot when default viewport is null and restore back', async({server, browser}) => { + const context = await browser.newContext({viewport: null}); + const page = await context.newPage({ viewport: null }); await page.setContent(`
oooo
+ + + +

Examples of DataTransfer: setData(), getData(), clearData()

+
+

+ Select this element, drag it to the Drop Zone and then release the selection to move the element.

+
+
Drop Zone
+ + diff --git a/test/mouse.spec.js b/test/mouse.spec.js index 4fc86adec6..28256de345 100644 --- a/test/mouse.spec.js +++ b/test/mouse.spec.js @@ -149,5 +149,15 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT, expect(await page.evaluate('result')).toEqual({x: 30, y: 40}); await context.close(); }); + describe('Drag and Drop', function() { + xit('should work', async({server, page}) => { + await page.goto(server.PREFIX + '/drag-n-drop.html'); + await page.hover('#source'); + await page.mouse.down(); + await page.hover('#target'); + await page.mouse.up(); + expect(await page.$eval('#target', target => target.contains(document.querySelector('#source')))).toBe(true, 'could not find source in target'); + }) + }); }); };