diff --git a/browser_patches/firefox/BUILD_NUMBER b/browser_patches/firefox/BUILD_NUMBER index bec4b5cd08..1f3d8a7a1f 100644 --- a/browser_patches/firefox/BUILD_NUMBER +++ b/browser_patches/firefox/BUILD_NUMBER @@ -1 +1 @@ -1025 +1028 diff --git a/browser_patches/firefox/archive.sh b/browser_patches/firefox/archive.sh index e173a93721..1f652001b7 100755 --- a/browser_patches/firefox/archive.sh +++ b/browser_patches/firefox/archive.sh @@ -39,6 +39,7 @@ if [[ $OBJ_FOLDER == "" ]]; then fi ./mach package +node ../install-preferences.js $PWD/$OBJ_FOLDER/dist/firefox if ! [[ -d $OBJ_FOLDER/dist/firefox ]]; then echo "ERROR: cannot find $OBJ_FOLDER/dist/firefox folder in the checkout/. Did you build?" diff --git a/browser_patches/firefox/build.sh b/browser_patches/firefox/build.sh index a2cd370054..fe8a8793a9 100755 --- a/browser_patches/firefox/build.sh +++ b/browser_patches/firefox/build.sh @@ -37,3 +37,11 @@ else fi ./mach build + +OBJ_FOLDER=$(ls -1 | grep obj-) +if [[ "$(uname)" == "Darwin" ]]; then + node ../install-preferences.js $PWD/$OBJ_FOLDER/dist +else + node ../install-preferences.js $PWD/$OBJ_FOLDER/dist/bin +fi + diff --git a/browser_patches/firefox/install-preferences.js b/browser_patches/firefox/install-preferences.js new file mode 100644 index 0000000000..f00f526392 --- /dev/null +++ b/browser_patches/firefox/install-preferences.js @@ -0,0 +1,96 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const os = require('os'); +const fs = require('fs'); +const path = require('path'); +const util = require('util'); + +const writeFileAsync = util.promisify(fs.writeFile.bind(fs)); +const mkdirAsync = util.promisify(fs.mkdir.bind(fs)); + +// Install browser preferences after downloading and unpacking +// firefox instances. +// Based on: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Enterprise_deployment_before_60#Configuration +async function installFirefoxPreferences(distpath) { + let executablePath = ''; + if (os.platform() === 'linux') + executablePath = path.join(distpath, 'firefox'); + else if (os.platform() === 'darwin') + executablePath = path.join(distpath, 'Nightly.app', 'Contents', 'MacOS', 'firefox'); + else if (os.platform() === 'win32') + executablePath = path.join(distpath, 'firefox.exe'); + + const firefoxFolder = path.dirname(executablePath); + + let prefPath = ''; + let configPath = ''; + if (os.platform() === 'darwin') { + prefPath = path.join(firefoxFolder, '..', 'Resources', 'defaults', 'pref'); + configPath = path.join(firefoxFolder, '..', 'Resources'); + } else if (os.platform() === 'linux') { + if (!fs.existsSync(path.join(firefoxFolder, 'browser', 'defaults'))) + await mkdirAsync(path.join(firefoxFolder, 'browser', 'defaults')); + if (!fs.existsSync(path.join(firefoxFolder, 'browser', 'defaults', 'preferences'))) + await mkdirAsync(path.join(firefoxFolder, 'browser', 'defaults', 'preferences')); + prefPath = path.join(firefoxFolder, 'browser', 'defaults', 'preferences'); + configPath = firefoxFolder; + } else if (os.platform() === 'win32') { + prefPath = path.join(firefoxFolder, 'defaults', 'pref'); + configPath = firefoxFolder; + } else { + throw new Error('Unsupported platform: ' + os.platform()); + } + + await Promise.all([ + copyFile({ + from: path.join(__dirname, 'preferences', '00-playwright-prefs.js'), + to: path.join(prefPath, '00-playwright-prefs.js'), + }), + copyFile({ + from: path.join(__dirname, 'preferences', 'playwright.cfg'), + to: path.join(configPath, 'playwright.cfg'), + }), + ]); +} + +function copyFile({from, to}) { + const rd = fs.createReadStream(from); + const wr = fs.createWriteStream(to); + return new Promise(function(resolve, reject) { + rd.on('error', reject); + wr.on('error', reject); + wr.on('finish', resolve); + rd.pipe(wr); + }).catch(function(error) { + rd.destroy(); + wr.end(); + throw error; + }); +} + +if (process.argv.length !== 3) { + console.log('ERROR: expected a path to the directory with browser build'); + process.exit(1); + return; +} + +installFirefoxPreferences(process.argv[2]).catch(error => { + console.error('ERROR: failed to put preferences!'); + console.error(error); + process.exit(1); +}); diff --git a/browser_patches/firefox/patches/bootstrap.diff b/browser_patches/firefox/patches/bootstrap.diff index ab1de60659..448fed1d00 100644 --- a/browser_patches/firefox/patches/bootstrap.diff +++ b/browser_patches/firefox/patches/bootstrap.diff @@ -469,10 +469,10 @@ index 6dca2b78830edc1ddbd66264bd332853729dac71..fbe89c9682834e11b9d9219d9eb056ed diff --git a/testing/juggler/BrowserContextManager.js b/testing/juggler/BrowserContextManager.js new file mode 100644 -index 0000000000000000000000000000000000000000..a0a3799b6060692fa64f41411c0c276337d8f0c0 +index 0000000000000000000000000000000000000000..8f031b3f9afbb357a6bebc9938fca50a04d0421c --- /dev/null +++ b/testing/juggler/BrowserContextManager.js -@@ -0,0 +1,174 @@ +@@ -0,0 +1,180 @@ +"use strict"; + +const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm"); @@ -503,9 +503,8 @@ index 0000000000000000000000000000000000000000..a0a3799b6060692fa64f41411c0c2763 + } + + constructor() { -+ this._browserContextIdToUserContextId = new Map(); -+ this._userContextIdToBrowserContextId = new Map(); -+ this._principalsForBrowserContextId = new Map(); ++ this._browserContextIdToBrowserContext = new Map(); ++ this._userContextIdToBrowserContext = new Map(); + + // Cleanup containers from previous runs (if any) + for (const identity of ContextualIdentityService.getPublicIdentities()) { @@ -514,66 +513,75 @@ index 0000000000000000000000000000000000000000..a0a3799b6060692fa64f41411c0c2763 + ContextualIdentityService.closeContainerTabs(identity.userContextId); + } + } ++ ++ this._defaultContext = new BrowserContext(this, undefined, undefined); + } + -+ grantPermissions(browserContextId, origin, permissions) { -+ const attrs = browserContextId ? {userContextId: this.userContextId(browserContextId)} : {}; ++ createBrowserContext(options) { ++ return new BrowserContext(this, helper.generateId(), options); ++ } ++ ++ browserContextForId(browserContextId) { ++ return this._browserContextIdToBrowserContext.get(browserContextId); ++ } ++ ++ browserContextForUserContextId(userContextId) { ++ return this._userContextIdToBrowserContext.get(userContextId); ++ } ++ ++ getBrowserContexts() { ++ return Array.from(this._browserContextIdToBrowserContext.values()); ++ } ++} ++ ++class BrowserContext { ++ constructor(manager, browserContextId, options) { ++ this._manager = manager; ++ this.browserContextId = browserContextId; ++ this.userContextId = undefined; ++ if (browserContextId !== undefined) { ++ const identity = ContextualIdentityService.create(IDENTITY_NAME + browserContextId); ++ this.userContextId = identity.userContextId; ++ } ++ this._principals = []; ++ this._manager._browserContextIdToBrowserContext.set(this.browserContextId, this); ++ this._manager._userContextIdToBrowserContext.set(this.userContextId, this); ++ this.options = options || {}; ++ } ++ ++ destroy() { ++ if (this.userContextId !== undefined) { ++ ContextualIdentityService.remove(this.userContextId); ++ ContextualIdentityService.closeContainerTabs(this.userContextId); ++ } ++ this._manager._browserContextIdToBrowserContext.delete(this.browserContextId); ++ this._manager._userContextIdToBrowserContext.delete(this.userContextId); ++ } ++ ++ grantPermissions(origin, permissions) { ++ const attrs = {userContextId: this.userContextId}; + const principal = Services.scriptSecurityManager.createContentPrincipal(NetUtil.newURI(origin), attrs); -+ if (!this._principalsForBrowserContextId.has(browserContextId)) -+ this._principalsForBrowserContextId.set(browserContextId, []); -+ this._principalsForBrowserContextId.get(browserContextId).push(principal); ++ this._principals.push(principal); + for (const permission of ALL_PERMISSIONS) { + const action = permissions.includes(permission) ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION; + Services.perms.addFromPrincipal(principal, permission, action); + } + } + -+ resetPermissions(browserContextId) { -+ if (!this._principalsForBrowserContextId.has(browserContextId)) -+ return; -+ const principals = this._principalsForBrowserContextId.get(browserContextId); -+ for (const principal of principals) { ++ resetPermissions() { ++ for (const principal of this._principals) { + for (const permission of ALL_PERMISSIONS) + Services.perms.removeFromPrincipal(principal, permission); + } -+ this._principalsForBrowserContextId.delete(browserContextId); ++ this._principals = []; + } + -+ createBrowserContext() { -+ const browserContextId = helper.generateId(); -+ const identity = ContextualIdentityService.create(IDENTITY_NAME + browserContextId); -+ this._browserContextIdToUserContextId.set(browserContextId, identity.userContextId); -+ this._userContextIdToBrowserContextId.set(identity.userContextId, browserContextId); -+ return browserContextId; -+ } -+ -+ browserContextId(userContextId) { -+ return this._userContextIdToBrowserContextId.get(userContextId); -+ } -+ -+ userContextId(browserContextId) { -+ return this._browserContextIdToUserContextId.get(browserContextId); -+ } -+ -+ removeBrowserContext(browserContextId) { -+ const userContextId = this._browserContextIdToUserContextId.get(browserContextId); -+ ContextualIdentityService.remove(userContextId); -+ ContextualIdentityService.closeContainerTabs(userContextId); -+ this._browserContextIdToUserContextId.delete(browserContextId); -+ this._userContextIdToBrowserContextId.delete(userContextId); -+ } -+ -+ getBrowserContexts() { -+ return Array.from(this._browserContextIdToUserContextId.keys()); -+ } -+ -+ setCookies(browserContextId, cookies) { ++ setCookies(cookies) { + const protocolToSameSite = { + [undefined]: Ci.nsICookie.SAMESITE_NONE, + 'Lax': Ci.nsICookie.SAMESITE_LAX, + 'Strict': Ci.nsICookie.SAMESITE_STRICT, + }; -+ const userContextId = browserContextId ? this._browserContextIdToUserContextId.get(browserContextId) : undefined; + for (const cookie of cookies) { + const uri = cookie.url ? NetUtil.newURI(cookie.url) : null; + let domain = cookie.domain; @@ -599,19 +607,17 @@ index 0000000000000000000000000000000000000000..a0a3799b6060692fa64f41411c0c2763 + cookie.httpOnly || false, + cookie.expires === undefined || cookie.expires === -1 /* isSession */, + cookie.expires === undefined ? Date.now() + HUNDRED_YEARS : cookie.expires, -+ { userContextId } /* originAttributes */, ++ { userContextId: this.userContextId } /* originAttributes */, + protocolToSameSite[cookie.sameSite], + ); + } + } + -+ clearCookies(browserContextId) { -+ const userContextId = browserContextId ? this._browserContextIdToUserContextId.get(browserContextId) : undefined; -+ Services.cookies.removeCookiesWithOriginAttributes(JSON.stringify({ userContextId })); ++ clearCookies() { ++ Services.cookies.removeCookiesWithOriginAttributes(JSON.stringify({ userContextId: this.userContextId })); + } + -+ getCookies(browserContextId) { -+ const userContextId = browserContextId ? this._browserContextIdToUserContextId.get(browserContextId) : 0; ++ getCookies() { + const result = []; + const sameSiteToProtocol = { + [Ci.nsICookie.SAMESITE_NONE]: 'None', @@ -619,7 +625,7 @@ index 0000000000000000000000000000000000000000..a0a3799b6060692fa64f41411c0c2763 + [Ci.nsICookie.SAMESITE_STRICT]: 'Strict', + }; + for (let cookie of Services.cookies.cookies) { -+ if (cookie.originAttributes.userContextId !== userContextId) ++ if (cookie.originAttributes.userContextId !== (this.userContextId || 0)) + continue; + if (cookie.host === 'addons.mozilla.org') + continue; @@ -1444,10 +1450,10 @@ index 0000000000000000000000000000000000000000..66f61d432f9ad2f50931b780ec5ea0e3 +this.NetworkObserver = NetworkObserver; diff --git a/testing/juggler/TargetRegistry.js b/testing/juggler/TargetRegistry.js new file mode 100644 -index 0000000000000000000000000000000000000000..6231668027bb83bef2b3f839d44bcf043c5bb292 +index 0000000000000000000000000000000000000000..2b1a1bd0f931d82824a86ecbb46f86483177a4e0 --- /dev/null +++ b/testing/juggler/TargetRegistry.js -@@ -0,0 +1,196 @@ +@@ -0,0 +1,214 @@ +const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); @@ -1481,9 +1487,14 @@ index 0000000000000000000000000000000000000000..6231668027bb83bef2b3f839d44bcf04 + this._tabToTarget = new Map(); + + for (const tab of this._mainWindow.gBrowser.tabs) -+ this._ensureTargetForTab(tab); ++ this._createTargetForTab(tab); + this._mainWindow.gBrowser.tabContainer.addEventListener('TabOpen', event => { -+ this._ensureTargetForTab(event.target); ++ const target = this._createTargetForTab(event.target); ++ // If we come here, content will have juggler script from the start, ++ // and we should wait for initial navigation, unless the tab was window.open'ed. ++ target._waitForInitialNavigation = !event.target.linkedBrowser.hasContentOpener; ++ // For pages created before we attach to them, we don't wait for initial ++ // navigation (target._waitForInitialNavigation is false by default). + }); + this._mainWindow.gBrowser.tabContainer.addEventListener('TabClose', event => { + const tab = event.target; @@ -1498,26 +1509,14 @@ index 0000000000000000000000000000000000000000..6231668027bb83bef2b3f839d44bcf04 + } + + async newPage({browserContextId}) { ++ const browserContext = this._contextManager.browserContextForId(browserContextId); + const tab = this._mainWindow.gBrowser.addTab('about:blank', { -+ userContextId: this._contextManager.userContextId(browserContextId), ++ userContextId: browserContext.userContextId, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + this._mainWindow.gBrowser.selectedTab = tab; -+ // Await navigation to about:blank -+ await new Promise(resolve => { -+ const wpl = { -+ onLocationChange: function(aWebProgress, aRequest, aLocation) { -+ tab.linkedBrowser.removeProgressListener(wpl); -+ resolve(); -+ }, -+ QueryInterface: ChromeUtils.generateQI([ -+ Ci.nsIWebProgressListener, -+ Ci.nsISupportsWeakReference, -+ ]), -+ }; -+ tab.linkedBrowser.addProgressListener(wpl); -+ }); -+ const target = this._ensureTargetForTab(tab); ++ const target = this._tabToTarget.get(tab); ++ await target._contentReadyPromise; + return target.id(); + } + @@ -1550,30 +1549,31 @@ index 0000000000000000000000000000000000000000..6231668027bb83bef2b3f839d44bcf04 + return target._tab; + } + -+ _ensureTargetForTab(tab) { -+ if (this._tabToTarget.has(tab)) -+ return this._tabToTarget.get(tab); -+ const openerTarget = tab.openerTab ? this._ensureTargetForTab(tab.openerTab) : null; -+ const target = new PageTarget(this, tab, this._contextManager.browserContextId(tab.userContextId), openerTarget); ++ targetForId(targetId) { ++ return this._targets.get(targetId); ++ } + ++ _createTargetForTab(tab) { ++ if (this._tabToTarget.has(tab)) ++ throw new Error(`Internal error: two targets per tab`); ++ const openerTarget = tab.openerTab ? this._tabToTarget.get(tab.openerTab) : null; ++ const target = new PageTarget(this, tab, this._contextManager.browserContextForUserContextId(tab.userContextId), openerTarget); + this._targets.set(target.id(), target); + this._tabToTarget.set(tab, target); + this.emit(TargetRegistry.Events.TargetCreated, target.info()); ++ return target; + } +} + +class PageTarget { -+ constructor(registry, tab, browserContextId, opener) { ++ constructor(registry, tab, browserContext, opener) { + this._targetId = helper.generateId(); + this._registry = registry; + this._tab = tab; -+ this._browserContextId = browserContextId; ++ this._browserContext = browserContext; + this._openerId = opener ? opener.id() : undefined; + this._url = tab.linkedBrowser.currentURI.spec; + -+ // First navigation always happens to about:blank - do not report it. -+ this._skipNextNavigation = true; -+ + const navigationListener = { + QueryInterface: ChromeUtils.generateQI([ Ci.nsIWebProgressListener]), + onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation), @@ -1584,13 +1584,41 @@ index 0000000000000000000000000000000000000000..6231668027bb83bef2b3f839d44bcf04 + receiveMessage: () => this._onContentReady() + }), + ]; ++ ++ this._contentReadyPromise = new Promise(f => this._contentReadyCallback = f); ++ this._waitForInitialNavigation = false; ++ ++ if (browserContext && browserContext.options.viewport) ++ this.setViewportSize(browserContext.options.viewport.viewportSize); ++ } ++ ++ setViewportSize(viewportSize) { ++ if (viewportSize) { ++ const {width, height} = viewportSize; ++ this._tab.linkedBrowser.style.setProperty('min-width', width + 'px'); ++ this._tab.linkedBrowser.style.setProperty('min-height', height + 'px'); ++ this._tab.linkedBrowser.style.setProperty('max-width', width + 'px'); ++ this._tab.linkedBrowser.style.setProperty('max-height', height + 'px'); ++ } else { ++ this._tab.linkedBrowser.style.removeProperty('min-width'); ++ this._tab.linkedBrowser.style.removeProperty('min-height'); ++ this._tab.linkedBrowser.style.removeProperty('max-width'); ++ this._tab.linkedBrowser.style.removeProperty('max-height'); ++ } ++ const rect = this._tab.linkedBrowser.getBoundingClientRect(); ++ return { width: rect.width, height: rect.height }; + } + + _onContentReady() { -+ const attachInfo = []; -+ const data = { attachInfo, targetInfo: this.info() }; ++ const sessionIds = []; ++ const data = { sessionIds, targetInfo: this.info() }; + this._registry.emit(TargetRegistry.Events.PageTargetReady, data); -+ return attachInfo; ++ this._contentReadyCallback(); ++ return { ++ browserContextOptions: this._browserContext ? this._browserContext.options : {}, ++ waitForInitialNavigation: this._waitForInitialNavigation, ++ sessionIds ++ }; + } + + id() { @@ -1602,16 +1630,12 @@ index 0000000000000000000000000000000000000000..6231668027bb83bef2b3f839d44bcf04 + targetId: this.id(), + type: 'page', + url: this._url, -+ browserContextId: this._browserContextId, ++ browserContextId: this._browserContext ? this._browserContext.browserContextId : undefined, + openerId: this._openerId, + }; + } + + _onNavigated(aLocation) { -+ if (this._skipNextNavigation) { -+ this._skipNextNavigation = false; -+ return; -+ } + this._url = aLocation.spec; + this._registry.emit(TargetRegistry.Events.TargetChanged, this.info()); + } @@ -1792,10 +1816,10 @@ index 0000000000000000000000000000000000000000..268fbc361d8053182bb6c27f626e853d + diff --git a/testing/juggler/content/ContentSession.js b/testing/juggler/content/ContentSession.js new file mode 100644 -index 0000000000000000000000000000000000000000..2302be180eeee0cc686171cefb56f7ab2514648a +index 0000000000000000000000000000000000000000..3891da101e6906ae2a3888e256aefd03f724ab4b --- /dev/null +++ b/testing/juggler/content/ContentSession.js -@@ -0,0 +1,67 @@ +@@ -0,0 +1,68 @@ +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {RuntimeAgent} = ChromeUtils.import('chrome://juggler/content/content/RuntimeAgent.js'); +const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js'); @@ -1807,14 +1831,13 @@ index 0000000000000000000000000000000000000000..2302be180eeee0cc686171cefb56f7ab + * @param {string} sessionId + * @param {!ContentFrameMessageManager} messageManager + * @param {!FrameTree} frameTree -+ * @param {!ScrollbarManager} scrollbarManager + * @param {!NetworkMonitor} networkMonitor + */ -+ constructor(sessionId, messageManager, frameTree, scrollbarManager, networkMonitor) { ++ constructor(sessionId, messageManager, frameTree, networkMonitor) { + this._sessionId = sessionId; + this._messageManager = messageManager; + const runtimeAgent = new RuntimeAgent(this); -+ const pageAgent = new PageAgent(this, runtimeAgent, frameTree, scrollbarManager, networkMonitor); ++ const pageAgent = new PageAgent(this, runtimeAgent, frameTree, networkMonitor); + this._agents = { + Page: pageAgent, + Runtime: runtimeAgent, @@ -1822,6 +1845,8 @@ index 0000000000000000000000000000000000000000..2302be180eeee0cc686171cefb56f7ab + this._eventListeners = [ + helper.addMessageListener(messageManager, this._sessionId, this._onMessage.bind(this)), + ]; ++ runtimeAgent.enable(); ++ pageAgent.enable(); + } + + emitEvent(eventName, params) { @@ -1865,10 +1890,10 @@ index 0000000000000000000000000000000000000000..2302be180eeee0cc686171cefb56f7ab + diff --git a/testing/juggler/content/FrameTree.js b/testing/juggler/content/FrameTree.js new file mode 100644 -index 0000000000000000000000000000000000000000..f239981ae0d87581d9a1c25ca1ebe1730d20bfa0 +index 0000000000000000000000000000000000000000..dcebb7bbf6d0c9bb7a350443dfa2574bee5915ea --- /dev/null +++ b/testing/juggler/content/FrameTree.js -@@ -0,0 +1,242 @@ +@@ -0,0 +1,252 @@ +"use strict"; +const Ci = Components.interfaces; +const Cr = Components.results; @@ -1880,10 +1905,11 @@ index 0000000000000000000000000000000000000000..f239981ae0d87581d9a1c25ca1ebe173 +const helper = new Helper(); + +class FrameTree { -+ constructor(rootDocShell) { ++ constructor(rootDocShell, waitForInitialNavigation) { + EventEmitter.decorate(this); + this._docShellToFrame = new Map(); + this._frameIdToFrame = new Map(); ++ this._pageReady = !waitForInitialNavigation; + this._mainFrame = this._createFrame(rootDocShell); + const webProgress = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); @@ -1902,6 +1928,10 @@ index 0000000000000000000000000000000000000000..f239981ae0d87581d9a1c25ca1ebe173 + ]; + } + ++ isPageReady() { ++ return this._pageReady; ++ } ++ + frameForDocShell(docShell) { + return this._docShellToFrame.get(docShell) || null; + } @@ -1960,6 +1990,10 @@ index 0000000000000000000000000000000000000000..f239981ae0d87581d9a1c25ca1ebe173 + frame._lastCommittedNavigationId = navigationId; + frame._url = channel.URI.spec; + this.emit(FrameTree.Events.NavigationCommitted, frame); ++ if (frame === this._mainFrame && !this._pageReady) { ++ this._pageReady = true; ++ this.emit(FrameTree.Events.PageReady); ++ } + } else if (isStop && frame._pendingNavigationId && status) { + // Navigation is aborted. + const navigationId = frame._pendingNavigationId; @@ -2035,6 +2069,7 @@ index 0000000000000000000000000000000000000000..f239981ae0d87581d9a1c25ca1ebe173 + NavigationCommitted: 'navigationcommitted', + NavigationAborted: 'navigationaborted', + SameDocumentNavigation: 'samedocumentnavigation', ++ PageReady: 'pageready', +}; + +class Frame { @@ -2181,10 +2216,10 @@ index 0000000000000000000000000000000000000000..2508cce41565023b7fee9c7b85afe8ec + diff --git a/testing/juggler/content/PageAgent.js b/testing/juggler/content/PageAgent.js new file mode 100644 -index 0000000000000000000000000000000000000000..d592ad9355e7e74a1685acd9338b387a8aa1b032 +index 0000000000000000000000000000000000000000..e505911e81ef014f19a3a732f3c5f631f0bd1780 --- /dev/null +++ b/testing/juggler/content/PageAgent.js -@@ -0,0 +1,895 @@ +@@ -0,0 +1,875 @@ +"use strict"; +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const Ci = Components.interfaces; @@ -2343,12 +2378,11 @@ index 0000000000000000000000000000000000000000..d592ad9355e7e74a1685acd9338b387a +} + +class PageAgent { -+ constructor(session, runtimeAgent, frameTree, scrollbarManager, networkMonitor) { ++ constructor(session, runtimeAgent, frameTree, networkMonitor) { + this._session = session; + this._runtime = runtimeAgent; + this._frameTree = frameTree; + this._networkMonitor = networkMonitor; -+ this._scrollbarManager = scrollbarManager; + + this._frameData = new Map(); + this._scriptsToEvaluateOnNewDocument = new Map(); @@ -2399,14 +2433,6 @@ index 0000000000000000000000000000000000000000..d592ad9355e7e74a1685acd9338b387a + return this._networkMonitor.requestDetails(channelId); + } + -+ async setViewport({deviceScaleFactor, isMobile, hasTouch}) { -+ const docShell = this._frameTree.mainFrame().docShell(); -+ docShell.contentViewer.overrideDPPX = deviceScaleFactor || this._initialDPPX; -+ docShell.deviceSizeIsPageSize = isMobile; -+ docShell.touchEventsOverride = hasTouch ? Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED : Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_NONE; -+ this._scrollbarManager.setFloatingScrollbars(isMobile); -+ } -+ + async setEmulatedMedia({type, colorScheme}) { + const docShell = this._frameTree.mainFrame().docShell(); + const cv = docShell.contentViewer; @@ -2421,16 +2447,6 @@ index 0000000000000000000000000000000000000000..d592ad9355e7e74a1685acd9338b387a + } + } + -+ async setUserAgent({userAgent}) { -+ const docShell = this._frameTree.mainFrame().docShell(); -+ docShell.customUserAgent = userAgent; -+ } -+ -+ async setBypassCSP({enabled}) { -+ const docShell = this._frameTree.mainFrame().docShell(); -+ docShell.bypassCSPEnabled = enabled; -+ } -+ + addScriptToEvaluateOnNewDocument({script, worldName}) { + const scriptId = helper.generateId(); + this._scriptsToEvaluateOnNewDocument.set(scriptId, {script, worldName}); @@ -2454,11 +2470,6 @@ index 0000000000000000000000000000000000000000..d592ad9355e7e74a1685acd9338b387a + docShell.defaultLoadFlags = cacheDisabled ? disable : enable; + } + -+ setJavascriptEnabled({enabled}) { -+ const docShell = this._frameTree.mainFrame().docShell(); -+ docShell.allowJavascript = enabled; -+ } -+ + enable() { + if (this._enabled) + return; @@ -2486,11 +2497,15 @@ index 0000000000000000000000000000000000000000..d592ad9355e7e74a1685acd9338b387a + helper.on(this._frameTree, 'navigationcommitted', this._onNavigationCommitted.bind(this)), + helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)), + helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)), ++ helper.on(this._frameTree, 'pageready', () => this._session.emitEvent('Page.ready', {})), + ]; + + this._wdm.addListener(this._wdmListener); + for (const workerDebugger of this._wdm.getWorkerDebuggerEnumerator()) + this._onWorkerCreated(workerDebugger); ++ ++ if (this._frameTree.isPageReady()) ++ this._session.emitEvent('Page.ready', {}); + } + + setInterceptFileChooserDialog({enabled}) { @@ -3869,10 +3884,10 @@ index 0000000000000000000000000000000000000000..3a386425d3796d0a6786dea193b3402d + diff --git a/testing/juggler/content/main.js b/testing/juggler/content/main.js new file mode 100644 -index 0000000000000000000000000000000000000000..6a9f908676fc025b74ea585a0e4e9194f704d13f +index 0000000000000000000000000000000000000000..556f48d627401b8507b8bbec6dbf7ca797644baf --- /dev/null +++ b/testing/juggler/content/main.js -@@ -0,0 +1,56 @@ +@@ -0,0 +1,76 @@ +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {ContentSession} = ChromeUtils.import('chrome://juggler/content/content/ContentSession.js'); +const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js'); @@ -3880,17 +3895,15 @@ index 0000000000000000000000000000000000000000..6a9f908676fc025b74ea585a0e4e9194 +const {ScrollbarManager} = ChromeUtils.import('chrome://juggler/content/content/ScrollbarManager.js'); + +const sessions = new Map(); -+const frameTree = new FrameTree(docShell); -+const networkMonitor = new NetworkMonitor(docShell, frameTree); +const scrollbarManager = new ScrollbarManager(docShell); -+ ++let frameTree; ++let networkMonitor; +const helper = new Helper(); +const messageManager = this; ++let gListeners; + +function createContentSession(sessionId) { -+ const session = new ContentSession(sessionId, messageManager, frameTree, scrollbarManager, networkMonitor); -+ sessions.set(sessionId, session); -+ return session; ++ sessions.set(sessionId, new ContentSession(sessionId, messageManager, frameTree, networkMonitor)); +} + +function disposeContentSession(sessionId) { @@ -3901,34 +3914,56 @@ index 0000000000000000000000000000000000000000..6a9f908676fc025b74ea585a0e4e9194 + session.dispose(); +} + -+const gListeners = [ -+ helper.addMessageListener(messageManager, 'juggler:create-content-session', msg => { -+ const sessionId = msg.data; ++function initialize() { ++ let response = sendSyncMessage('juggler:content-ready', {})[0]; ++ if (!response) ++ response = { sessionIds: [], browserContextOptions: {}, waitForInitialNavigation: false }; ++ ++ const { sessionIds, browserContextOptions, waitForInitialNavigation } = response; ++ const { userAgent, bypassCSP, javaScriptDisabled, viewport} = browserContextOptions; ++ ++ if (userAgent !== undefined) ++ docShell.customUserAgent = userAgent; ++ if (bypassCSP !== undefined) ++ docShell.bypassCSPEnabled = bypassCSP; ++ if (javaScriptDisabled !== undefined) ++ docShell.allowJavascript = !javaScriptDisabled; ++ if (viewport !== undefined) { ++ docShell.contentViewer.overrideDPPX = viewport.deviceScaleFactor || this._initialDPPX; ++ docShell.deviceSizeIsPageSize = viewport.isMobile; ++ docShell.touchEventsOverride = viewport.hasTouch ? Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED : Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_NONE; ++ scrollbarManager.setFloatingScrollbars(viewport.isMobile); ++ } ++ ++ frameTree = new FrameTree(docShell, waitForInitialNavigation); ++ networkMonitor = new NetworkMonitor(docShell, frameTree); ++ for (const sessionId of sessionIds) + createContentSession(sessionId); -+ }), + -+ helper.addMessageListener(messageManager, 'juggler:dispose-content-session', msg => { -+ const sessionId = msg.data; -+ disposeContentSession(sessionId); -+ }), ++ gListeners = [ ++ helper.addMessageListener(messageManager, 'juggler:create-content-session', msg => { ++ const sessionId = msg.data; ++ createContentSession(sessionId); ++ }), + -+ helper.addEventListener(messageManager, 'unload', msg => { -+ helper.removeListeners(gListeners); -+ for (const session of sessions.values()) -+ session.dispose(); -+ sessions.clear(); -+ scrollbarManager.dispose(); -+ networkMonitor.dispose(); -+ frameTree.dispose(); -+ }), -+]; ++ helper.addMessageListener(messageManager, 'juggler:dispose-content-session', msg => { ++ const sessionId = msg.data; ++ disposeContentSession(sessionId); ++ }), + -+const [attachInfo] = sendSyncMessage('juggler:content-ready', {}); -+for (const { sessionId, messages } of attachInfo || []) { -+ const session = createContentSession(sessionId); -+ for (const message of messages) -+ session.handleMessage(message); ++ helper.addEventListener(messageManager, 'unload', msg => { ++ helper.removeListeners(gListeners); ++ for (const session of sessions.values()) ++ session.dispose(); ++ sessions.clear(); ++ scrollbarManager.dispose(); ++ networkMonitor.dispose(); ++ frameTree.dispose(); ++ }), ++ ]; +} ++ ++initialize(); diff --git a/testing/juggler/jar.mn b/testing/juggler/jar.mn new file mode 100644 index 0000000000000000000000000000000000000000..76377927a8c9af3cac3b028ff754491966d03ba3 @@ -4009,10 +4044,10 @@ index 0000000000000000000000000000000000000000..a2d3b79469566ca2edb7d864621f7085 +this.AccessibilityHandler = AccessibilityHandler; diff --git a/testing/juggler/protocol/BrowserHandler.js b/testing/juggler/protocol/BrowserHandler.js new file mode 100644 -index 0000000000000000000000000000000000000000..9bf14b3c4842d15508f67daa10f350475551a73e +index 0000000000000000000000000000000000000000..6b42032e8f6d39025f455300d376084826a781cc --- /dev/null +++ b/testing/juggler/protocol/BrowserHandler.js -@@ -0,0 +1,72 @@ +@@ -0,0 +1,73 @@ +"use strict"; + +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); @@ -4042,7 +4077,7 @@ index 0000000000000000000000000000000000000000..9bf14b3c4842d15508f67daa10f35047 + + async setIgnoreHTTPSErrors({enabled}) { + if (!enabled) { -+ allowAllCerts.disable() ++ allowAllCerts.disable() + Services.prefs.setBoolPref('security.mixed_content.block_active_content', true); + } else { + allowAllCerts.enable() @@ -4051,23 +4086,24 @@ index 0000000000000000000000000000000000000000..9bf14b3c4842d15508f67daa10f35047 + } + + grantPermissions({browserContextId, origin, permissions}) { -+ this._contextManager.grantPermissions(browserContextId, origin, permissions); ++ this._contextManager.browserContextForId(browserContextId).grantPermissions(origin, permissions); + } + + resetPermissions({browserContextId}) { -+ this._contextManager.resetPermissions(browserContextId); ++ this._contextManager.browserContextForId(browserContextId).resetPermissions(); + } + + setCookies({browserContextId, cookies}) { -+ this._contextManager.setCookies(browserContextId, cookies); ++ this._contextManager.browserContextForId(browserContextId).setCookies(cookies); + } + + clearCookies({browserContextId}) { -+ this._contextManager.clearCookies(browserContextId); ++ this._contextManager.browserContextForId(browserContextId).clearCookies(); + } + + getCookies({browserContextId}) { -+ return {cookies: this._contextManager.getCookies(browserContextId)}; ++ const cookies = this._contextManager.browserContextForId(browserContextId).getCookies(); ++ return {cookies}; + } + + async getInfo() { @@ -4087,10 +4123,10 @@ index 0000000000000000000000000000000000000000..9bf14b3c4842d15508f67daa10f35047 +this.BrowserHandler = BrowserHandler; diff --git a/testing/juggler/protocol/Dispatcher.js b/testing/juggler/protocol/Dispatcher.js new file mode 100644 -index 0000000000000000000000000000000000000000..835aa8b7d1c5a8e643691c4b89da77cd1c8b18c9 +index 0000000000000000000000000000000000000000..5c5a73b35cd178b51899ab3dd681d46b6c3e4770 --- /dev/null +++ b/testing/juggler/protocol/Dispatcher.js -@@ -0,0 +1,254 @@ +@@ -0,0 +1,265 @@ +const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); +const {protocol, checkScheme} = ChromeUtils.import("chrome://juggler/content/protocol/Protocol.js"); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); @@ -4123,7 +4159,7 @@ index 0000000000000000000000000000000000000000..835aa8b7d1c5a8e643691c4b89da77cd + ]; + } + -+ createSession(targetId) { ++ createSession(targetId, shouldConnect) { + const targetInfo = TargetRegistry.instance().targetInfo(targetId); + if (!targetInfo) + throw new Error(`Target "${targetId}" is not found`); @@ -4135,6 +4171,8 @@ index 0000000000000000000000000000000000000000..835aa8b7d1c5a8e643691c4b89da77cd + + const sessionId = helper.generateId(); + const contentSession = targetInfo.type === 'page' ? new ContentSession(this, sessionId, targetInfo) : null; ++ if (shouldConnect && contentSession) ++ contentSession.connect(); + const chromeSession = new ChromeSession(this, sessionId, contentSession, targetInfo); + targetSessions.set(sessionId, chromeSession); + this._sessions.set(sessionId, chromeSession); @@ -4234,6 +4272,12 @@ index 0000000000000000000000000000000000000000..835aa8b7d1c5a8e643691c4b89da77cd + if (protocol.domains[domainName].targets.includes(targetInfo.type)) + this._handlers[domainName] = new handlerFactory(this, contentSession); + } ++ const pageHandler = this._handlers['Page']; ++ if (pageHandler) ++ pageHandler.enable(); ++ const networkHandler = this._handlers['Network']; ++ if (networkHandler) ++ networkHandler.enable(); + } + + dispatcher() { @@ -4284,7 +4328,6 @@ index 0000000000000000000000000000000000000000..835aa8b7d1c5a8e643691c4b89da77cd + this._messageId = 0; + this._pendingMessages = new Map(); + this._sessionId = sessionId; -+ this._browser.messageManager.sendAsyncMessage('juggler:create-content-session', this._sessionId); + this._disposed = false; + this._eventListeners = [ + helper.addMessageListener(this._browser.messageManager, this._sessionId, { @@ -4293,6 +4336,10 @@ index 0000000000000000000000000000000000000000..835aa8b7d1c5a8e643691c4b89da77cd + ]; + } + ++ connect() { ++ this._browser.messageManager.sendAsyncMessage('juggler:create-content-session', this._sessionId); ++ } ++ + isDisposed() { + return this._disposed; + } @@ -4519,10 +4566,10 @@ index 0000000000000000000000000000000000000000..5d776ab6f28ccff44ef4663e8618ad9c +this.NetworkHandler = NetworkHandler; diff --git a/testing/juggler/protocol/PageHandler.js b/testing/juggler/protocol/PageHandler.js new file mode 100644 -index 0000000000000000000000000000000000000000..e9c5d94cf65b44d57bdb21ec892c3e325220a879 +index 0000000000000000000000000000000000000000..efb0fc1f3f7af37e101976cf8a682e09c223e59f --- /dev/null +++ b/testing/juggler/protocol/PageHandler.js -@@ -0,0 +1,285 @@ +@@ -0,0 +1,266 @@ +"use strict"; + +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); @@ -4540,6 +4587,7 @@ index 0000000000000000000000000000000000000000..e9c5d94cf65b44d57bdb21ec892c3e32 + constructor(chromeSession, contentSession) { + this._chromeSession = chromeSession; + this._contentSession = contentSession; ++ this._pageTarget = TargetRegistry.instance().targetForId(chromeSession.targetId()); + this._browser = TargetRegistry.instance().tabForTarget(chromeSession.targetId()).linkedBrowser; + this._dialogs = new Map(); + @@ -4568,38 +4616,18 @@ index 0000000000000000000000000000000000000000..e9c5d94cf65b44d57bdb21ec892c3e32 + }), + helper.addEventListener(this._browser, 'DOMModalDialogClosed', event => this._updateModalDialogs()), + ]; -+ await this._contentSession.send('Page.enable'); + } + + dispose() { + helper.removeListeners(this._eventListeners); + } + -+ async setViewport({viewport}) { -+ if (viewport) { -+ const {width, height} = viewport; -+ this._browser.style.setProperty('min-width', width + 'px'); -+ this._browser.style.setProperty('min-height', height + 'px'); -+ this._browser.style.setProperty('max-width', width + 'px'); -+ this._browser.style.setProperty('max-height', height + 'px'); -+ } else { -+ this._browser.style.removeProperty('min-width'); -+ this._browser.style.removeProperty('min-height'); -+ this._browser.style.removeProperty('max-width'); -+ this._browser.style.removeProperty('max-height'); -+ } -+ const dimensions = this._browser.getBoundingClientRect(); -+ await Promise.all([ -+ this._contentSession.send('Page.setViewport', { -+ deviceScaleFactor: viewport ? viewport.deviceScaleFactor : 0, -+ isMobile: viewport && viewport.isMobile, -+ hasTouch: viewport && viewport.hasTouch, -+ }), -+ this._contentSession.send('Page.awaitViewportDimensions', { -+ width: dimensions.width, -+ height: dimensions.height -+ }), -+ ]); ++ async setViewportSize({viewportSize}) { ++ const size = this._pageTarget.setViewportSize(viewportSize); ++ await this._contentSession.send('Page.awaitViewportDimensions', { ++ width: size.width, ++ height: size.height ++ }); + } + + _updateModalDialogs() { @@ -4959,10 +4987,10 @@ index 0000000000000000000000000000000000000000..78b6601b91d0b7fcda61114e6846aa07 +this.EXPORTED_SYMBOLS = ['t', 'checkScheme']; diff --git a/testing/juggler/protocol/Protocol.js b/testing/juggler/protocol/Protocol.js new file mode 100644 -index 0000000000000000000000000000000000000000..a59a7c218fdc3d2b3282bc5419eb4497a16559ea +index 0000000000000000000000000000000000000000..a0a96a87ff4a422deccae1045962690fa7941f25 --- /dev/null +++ b/testing/juggler/protocol/Protocol.js -@@ -0,0 +1,755 @@ +@@ -0,0 +1,746 @@ +const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js'); + +// Protocol-specific types. @@ -5016,13 +5044,16 @@ index 0000000000000000000000000000000000000000..a59a7c218fdc3d2b3282bc5419eb4497 + height: t.Number, +}; + -+pageTypes.Viewport = { ++pageTypes.Size = { + width: t.Number, + height: t.Number, ++}; ++ ++pageTypes.Viewport = { ++ viewportSize: pageTypes.Size, + deviceScaleFactor: t.Number, + isMobile: t.Boolean, + hasTouch: t.Boolean, -+ isLandscape: t.Boolean, +}; + +pageTypes.DOMQuad = { @@ -5218,6 +5249,9 @@ index 0000000000000000000000000000000000000000..a59a7c218fdc3d2b3282bc5419eb4497 + params: { + removeOnDetach: t.Optional(t.Boolean), + userAgent: t.Optional(t.String), ++ bypassCSP: t.Optional(t.Boolean), ++ javaScriptDisabled: t.Optional(t.Boolean), ++ viewport: t.Optional(pageTypes.Viewport), + }, + returns: { + browserContextId: t.String, @@ -5288,7 +5322,6 @@ index 0000000000000000000000000000000000000000..a59a7c218fdc3d2b3282bc5419eb4497 + }, + }, + methods: { -+ 'enable': {}, + 'setRequestInterception': { + params: { + enabled: t.Boolean, @@ -5359,9 +5392,6 @@ index 0000000000000000000000000000000000000000..a59a7c218fdc3d2b3282bc5419eb4497 + }, + }, + methods: { -+ 'enable': { -+ params: {}, -+ }, + 'evaluate': { + params: { + // Pass frameId here. @@ -5414,6 +5444,8 @@ index 0000000000000000000000000000000000000000..a59a7c218fdc3d2b3282bc5419eb4497 + + types: pageTypes, + events: { ++ 'ready': { ++ }, + 'eventFired': { + frameId: t.String, + name: t.Enum(['load', 'DOMContentLoaded']), @@ -5485,9 +5517,6 @@ index 0000000000000000000000000000000000000000..a59a7c218fdc3d2b3282bc5419eb4497 + }, + + methods: { -+ 'enable': { -+ params: {}, -+ }, + 'close': { + params: { + runBeforeUnload: t.Optional(t.Boolean), @@ -5505,9 +5534,9 @@ index 0000000000000000000000000000000000000000..a59a7c218fdc3d2b3282bc5419eb4497 + name: t.String, + }, + }, -+ 'setViewport': { ++ 'setViewportSize': { + params: { -+ viewport: t.Nullable(pageTypes.Viewport), ++ viewportSize: t.Nullable(pageTypes.Size), + }, + }, + 'setEmulatedMedia': { @@ -5516,21 +5545,11 @@ index 0000000000000000000000000000000000000000..a59a7c218fdc3d2b3282bc5419eb4497 + colorScheme: t.Optional(t.Enum(['dark', 'light', 'no-preference'])), + }, + }, -+ 'setBypassCSP': { -+ params: { -+ enabled: t.Boolean -+ } -+ }, + 'setCacheDisabled': { + params: { + cacheDisabled: t.Boolean, + }, + }, -+ 'setJavascriptEnabled': { -+ params: { -+ enabled: t.Boolean, -+ }, -+ }, + 'describeNode': { + params: { + frameId: t.String, @@ -5720,10 +5739,10 @@ index 0000000000000000000000000000000000000000..a59a7c218fdc3d2b3282bc5419eb4497 +this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme']; diff --git a/testing/juggler/protocol/RuntimeHandler.js b/testing/juggler/protocol/RuntimeHandler.js new file mode 100644 -index 0000000000000000000000000000000000000000..0026e8ff58ef6268f4c63783d0ff68ff355b1e72 +index 0000000000000000000000000000000000000000..089e66c617f114fcb32b3cea20abc6fb80e26a1e --- /dev/null +++ b/testing/juggler/protocol/RuntimeHandler.js -@@ -0,0 +1,41 @@ +@@ -0,0 +1,37 @@ +"use strict"; + +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); @@ -5740,10 +5759,6 @@ index 0000000000000000000000000000000000000000..0026e8ff58ef6268f4c63783d0ff68ff + this._contentSession = contentSession; + } + -+ async enable(options) { -+ return await this._contentSession.send('Runtime.enable', options); -+ } -+ + async evaluate(options) { + return await this._contentSession.send('Runtime.evaluate', options); + } @@ -5767,10 +5782,10 @@ index 0000000000000000000000000000000000000000..0026e8ff58ef6268f4c63783d0ff68ff +this.RuntimeHandler = RuntimeHandler; diff --git a/testing/juggler/protocol/TargetHandler.js b/testing/juggler/protocol/TargetHandler.js new file mode 100644 -index 0000000000000000000000000000000000000000..454fa4ebb9bda29bb957fa64a08ca92c33212f75 +index 0000000000000000000000000000000000000000..4795a4ddecdd016d6efbcde35aa7321af17cd7dc --- /dev/null +++ b/testing/juggler/protocol/TargetHandler.js -@@ -0,0 +1,104 @@ +@@ -0,0 +1,100 @@ +"use strict"; + +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); @@ -5789,34 +5804,34 @@ index 0000000000000000000000000000000000000000..454fa4ebb9bda29bb957fa64a08ca92c + this._targetRegistry = TargetRegistry.instance(); + this._enabled = false; + this._eventListeners = []; -+ this._createdBrowserContextOptions = new Map(); ++ this._createdBrowserContextIds = new Set(); + } + + async attachToTarget({targetId}) { + if (!this._enabled) + throw new Error('Target domain is not enabled'); -+ const sessionId = this._session.dispatcher().createSession(targetId); ++ const sessionId = this._session.dispatcher().createSession(targetId, true /* shouldConnect */); + return {sessionId}; + } + + async createBrowserContext(options) { + if (!this._enabled) + throw new Error('Target domain is not enabled'); -+ const browserContextId = this._contextManager.createBrowserContext(); -+ // TODO: introduce BrowserContext class, with options? -+ this._createdBrowserContextOptions.set(browserContextId, options); -+ return {browserContextId}; ++ const browserContext = this._contextManager.createBrowserContext(options); ++ this._createdBrowserContextIds.add(browserContext.browserContextId); ++ return {browserContextId: browserContext.browserContextId}; + } + + async removeBrowserContext({browserContextId}) { + if (!this._enabled) + throw new Error('Target domain is not enabled'); -+ this._createdBrowserContextOptions.delete(browserContextId); -+ this._contextManager.removeBrowserContext(browserContextId); ++ this._createdBrowserContextIds.delete(browserContextId); ++ this._contextManager.browserContextForId(browserContextId).destroy(); + } + + async getBrowserContexts() { -+ return {browserContextIds: this._contextManager.getBrowserContexts()}; ++ const browserContexts = this._contextManager.getBrowserContexts(); ++ return {browserContextIds: browserContexts.map(bc => bc.browserContextId)}; + } + + async enable() { @@ -5836,9 +5851,10 @@ index 0000000000000000000000000000000000000000..454fa4ebb9bda29bb957fa64a08ca92c + + dispose() { + helper.removeListeners(this._eventListeners); -+ for (const [browserContextId, options] of this._createdBrowserContextOptions) { -+ if (options.removeOnDetach) -+ this._contextManager.removeBrowserContext(browserContextId); ++ for (const browserContextId of this._createdBrowserContextIds) { ++ const browserContext = this._contextManager.browserContextForId(browserContextId); ++ if (browserContext.options.removeOnDetach) ++ browserContext.destroy(); + } + this._createdBrowserContextOptions.clear(); + } @@ -5855,16 +5871,11 @@ index 0000000000000000000000000000000000000000..454fa4ebb9bda29bb957fa64a08ca92c + this._session.emitEvent('Target.targetDestroyed', targetInfo); + } + -+ _onPageTargetReady({attachInfo, targetInfo}) { -+ const options = this._createdBrowserContextOptions.get(targetInfo.browserContextId); -+ if (!options) ++ _onPageTargetReady({sessionIds, targetInfo}) { ++ if (!this._createdBrowserContextIds.has(targetInfo.browserContextId)) + return; -+ const sessionId = this._session.dispatcher().createSession(targetInfo.targetId); -+ const messages = []; -+ // TODO: perhaps, we should just have a single message 'initBrowserContextOptions'. -+ if (options.userAgent !== undefined) -+ messages.push({ id: 0, methodName: 'Page.setUserAgent', params: { userAgent: options.userAgent } }); -+ attachInfo.push({ sessionId, messages }); ++ const sessionId = this._session.dispatcher().createSession(targetInfo.targetId, false /* shouldConnect */); ++ sessionIds.push(sessionId); + } + + async newPage({browserContextId}) { diff --git a/browser_patches/firefox/preferences/00-playwright-prefs.js b/browser_patches/firefox/preferences/00-playwright-prefs.js new file mode 100644 index 0000000000..1895d2d1b7 --- /dev/null +++ b/browser_patches/firefox/preferences/00-playwright-prefs.js @@ -0,0 +1,3 @@ +// Any comment. You must start the file with a single-line comment! +pref("general.config.filename", "playwright.cfg"); +pref("general.config.obscure_value", 0); diff --git a/browser_patches/firefox/preferences/playwright.cfg b/browser_patches/firefox/preferences/playwright.cfg new file mode 100644 index 0000000000..4e724051fd --- /dev/null +++ b/browser_patches/firefox/preferences/playwright.cfg @@ -0,0 +1,233 @@ +// Any comment. You must start the file with a comment! + +// Make sure Shield doesn't hit the network. +pref("app.normandy.api_url", ""); +pref("app.normandy.enabled", false); + +// Disable updater +pref("app.update.enabled", false); +// Disable Firefox old build background check +pref("app.update.checkInstallTime", false); +// Disable automatically upgrading Firefox +pref("app.update.disabledForTesting", true); + +// make absolutely sure it is really off +pref("app.update.auto", false); +pref("app.update.mode", 0); +pref("app.update.service.enabled", false); +// Dislabe newtabpage +pref("browser.startup.homepage", "about:blank"); +pref("browser.startup.page", 0); +pref("browser.newtabpage.enabled", false); +// Do not redirect user when a milstone upgrade of Firefox is detected +pref("browser.startup.homepage_override.mstone", "ignore"); + +// Disable topstories +pref("browser.newtabpage.activity-stream.feeds.section.topstories", false); +// DevTools JSONViewer sometimes fails to load dependencies with its require.js. +// This doesn't affect Puppeteer operations, but spams console with a lot of +// unpleasant errors. +// (bug 1424372) +pref("devtools.jsonview.enabled", false); + +// Prevent various error message on the console +pref("browser.contentblocking.features.standard", "-tp,tpPrivate,cookieBehavior0,-cm,-fp"); +pref("network.cookie.cookieBehavior", 0); + +// Increase the APZ content response timeout in tests to 1 minute. +// This is to accommodate the fact that test environments tends to be +// slower than production environments (with the b2g emulator being +// the slowest of them all), resulting in the production timeout value +// sometimes being exceeded and causing false-positive test failures. +// +// (bug 1176798, bug 1177018, bug 1210465) +pref("apz.content_response_timeout", 60000); + +// Allow creating files in content process - required for +// |Page.setFileInputFiles| protocol method. +pref("dom.file.createInChild", true); + +// Indicate that the download panel has been shown once so that +// whichever download test runs first doesn't show the popup +// inconsistently. +pref("browser.download.panel.shown", true); +// Background thumbnails in particular cause grief, and disabling +// thumbnails in general cannot hurt +pref("browser.pagethumbnails.capturing_disabled", true); +// Disable safebrowsing components. +pref("browser.safebrowsing.blockedURIs.enabled", false); +pref("browser.safebrowsing.downloads.enabled", false); +pref("browser.safebrowsing.passwords.enabled", false); +pref("browser.safebrowsing.malware.enabled", false); +pref("browser.safebrowsing.phishing.enabled", false); +// Disable updates to search engines. +pref("browser.search.update", false); +// Do not restore the last open set of tabs if the browser has crashed +pref("browser.sessionstore.resume_from_crash", false); +// Don't check for the default web browser during startup. +pref("browser.shell.checkDefaultBrowser", false); + +// Disable browser animations (tabs, fullscreen, sliding alerts) +pref("toolkit.cosmeticAnimations.enabled", false); + +// Close the window when the last tab gets closed +pref("browser.tabs.closeWindowWithLastTab", true); + +// Do not allow background tabs to be zombified on Android, otherwise for +// tests that open additional tabs, the test harness tab itself might get +// unloaded +pref("browser.tabs.disableBackgroundZombification", false); + +// Do not warn when closing all open tabs +pref("browser.tabs.warnOnClose", false); + +// Do not warn when closing all other open tabs +pref("browser.tabs.warnOnCloseOtherTabs", false); + +// Do not warn when multiple tabs will be opened +pref("browser.tabs.warnOnOpen", false); + +// Disable first run splash page on Windows 10 +pref("browser.usedOnWindows10.introURL", ""); + +// Disable the UI tour. +// +// Should be set in profile. +pref("browser.uitour.enabled", false); + +// Turn off search suggestions in the location bar so as not to trigger +// network connections. +pref("browser.urlbar.suggest.searches", false); + +// Do not warn on quitting Firefox +pref("browser.warnOnQuit", false); + +// Do not show datareporting policy notifications which can +// interfere with tests +pref("datareporting.healthreport.documentServerURI", "http://dummydummy.test/dummy/healthreport/"); +pref("datareporting.healthreport.about.reportUrl", "http://dummydummy.test/dummy/abouthealthreport/"); +pref("datareporting.healthreport.logging.consoleEnabled", false); +pref("datareporting.healthreport.service.enabled", false); +pref("datareporting.healthreport.service.firstRun", false); +pref("datareporting.healthreport.uploadEnabled", false); +pref("datareporting.policy.dataSubmissionEnabled", false); +pref("datareporting.policy.dataSubmissionPolicyAccepted", false); +pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true); + +// Automatically unload beforeunload alerts +pref("dom.disable_beforeunload", false); + +// Disable popup-blocker +pref("dom.disable_open_during_load", false); + +// Disable the ProcessHangMonitor +pref("dom.ipc.reportProcessHangs", false); +pref("hangmonitor.timeout", 0); + +// Disable slow script dialogues +pref("dom.max_chrome_script_run_time", 0); +pref("dom.max_script_run_time", 0); + +// Only load extensions from the application and user profile +// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION +pref("extensions.autoDisableScopes", 0); +pref("extensions.enabledScopes", 5); + +// Disable metadata caching for installed add-ons by default +pref("extensions.getAddons.cache.enabled", false); + +// Disable installing any distribution extensions or add-ons. +pref("extensions.installDistroAddons", false); + +// Turn off extension updates so they do not bother tests +pref("extensions.update.enabled", false); +pref("extensions.update.notifyUser", false); + +// Make sure opening about:addons will not hit the network +pref("extensions.webservice.discoverURL", "http://dummydummy.test/dummy/discoveryURL"); + +pref("extensions.screenshots.disabled", true); +pref("extensions.screenshots.upload-disabled", true); + +// Allow the application to have focus even it runs in the background +pref("focusmanager.testmode", true); + +// Disable useragent updates +pref("general.useragent.updates.enabled", false); + +// No ICC color correction. +// See https://developer.mozilla.org/en/docs/Mozilla/Firefox/Releases/3.5/ICC_color_correction_in_Firefox. +pref("gfx.color_management.mode", 0); +pref("gfx.color_management.rendering_intent", 3); + +// Always use network provider for geolocation tests so we bypass the +// macOS dialog raised by the corelocation provider +pref("geo.provider.testing", true); + +// Do not scan Wifi +pref("geo.wifi.scan", false); + +// Show chrome errors and warnings in the error console +pref("javascript.options.showInConsole", true); + +// Disable download and usage of OpenH264: and Widevine plugins +pref("media.gmp-manager.updateEnabled", false); + +// Do not prompt with long usernames or passwords in URLs +pref("network.http.phishy-userpass-length", 255); + +// Do not prompt for temporary redirects +pref("network.http.prompt-temp-redirect", false); + +// Disable speculative connections so they are not reported as leaking +// when they are hanging around +pref("network.http.speculative-parallel-limit", 0); + +// Do not automatically switch between offline and online +pref("network.manage-offline-status", false); + +// Make sure SNTP requests do not hit the network +pref("network.sntp.pools", "dummydummy.test"); + +// Disable Flash +pref("plugin.state.flash", 0); + +pref("privacy.trackingprotection.enabled", false); + +pref("security.certerrors.mitm.priming.enabled", false); + +// Local documents have access to all other local documents, +// including directory listings +pref("security.fileuri.strict_origin_policy", false); + +// Tests do not wait for the notification button security delay +pref("security.notification_enable_delay", 0); + +// Ensure blocklist updates do not hit the network +pref("services.settings.server", "http://dummydummy.test/dummy/blocklist/"); + +// Disable DocumentChannel. +// See https://github.com/microsoft/playwright/pull/451 +pref("browser.tabs.documentchannel", false); + +// Do not automatically fill sign-in forms with known usernames and +// passwords +pref("signon.autofillForms", false); + +// Disable password capture, so that tests that include forms are not +// influenced by the presence of the persistent doorhanger notification +pref("signon.rememberSignons", false); + +// Disable first-run welcome page +pref("startup.homepage_welcome_url", "about:blank"); +pref("startup.homepage_welcome_url.additional", ""); + +// Prevent starting into safe mode after application crashes +pref("toolkit.startup.max_resumed_crashes", -1); +lockPref("toolkit.crashreporter.enabled", false); + +pref("toolkit.telemetry.enabled", false); +pref("toolkit.telemetry.server", "https://dummydummy.test/dummy/telemetry"); + +// Disable crash reporter. +Components.classes["@mozilla.org/toolkit/crash-reporter;1"].getService(Components.interfaces.nsICrashReporter).submitReports = false; diff --git a/browser_patches/webkit/BUILD_NUMBER b/browser_patches/webkit/BUILD_NUMBER index e60c25e12e..e29bf83e37 100644 --- a/browser_patches/webkit/BUILD_NUMBER +++ b/browser_patches/webkit/BUILD_NUMBER @@ -1 +1 @@ -1141 +1143 diff --git a/browser_patches/webkit/patches/bootstrap.diff b/browser_patches/webkit/patches/bootstrap.diff index 2457a295bc..010770f8c5 100644 --- a/browser_patches/webkit/patches/bootstrap.diff +++ b/browser_patches/webkit/patches/bootstrap.diff @@ -731,10 +731,10 @@ index 0000000000000000000000000000000000000000..79edea03fed4e9be5da96e1275e182a4 +} diff --git a/Source/JavaScriptCore/inspector/protocol/Emulation.json b/Source/JavaScriptCore/inspector/protocol/Emulation.json new file mode 100644 -index 0000000000000000000000000000000000000000..bcf863e4bba3b99f66e75dabfc4d8c1289cc2b78 +index 0000000000000000000000000000000000000000..552e5dd60fa53fada79f8d6e333f52bc10a2bead --- /dev/null +++ b/Source/JavaScriptCore/inspector/protocol/Emulation.json -@@ -0,0 +1,32 @@ +@@ -0,0 +1,39 @@ +{ + "domain": "Emulation", + "availability": ["web"], @@ -764,6 +764,13 @@ index 0000000000000000000000000000000000000000..bcf863e4bba3b99f66e75dabfc4d8c12 + { "name": "username", "type": "string", "optional": true }, + { "name": "password", "type": "string", "optional": true } + ] ++ }, ++ { ++ "name": "setActiveAndFocused", ++ "description": "Makes page focused for test.", ++ "parameters": [ ++ { "name": "active", "type": "boolean", "optional": true } ++ ] + } + ] +} @@ -8723,10 +8730,10 @@ index 846a5aa27dfab3d274cffa4873861f2587d17fd8..cf0dc99f5601636c48abff09cd47ace4 diff --git a/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..78815bc17978a23ca811ff99039b4fe86e6fb4a0 +index 0000000000000000000000000000000000000000..466d5129363e8b3d2e7cfc10f2a1ac7ae77f634f --- /dev/null +++ b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp -@@ -0,0 +1,95 @@ +@@ -0,0 +1,103 @@ +/* + * Copyright (C) 2019 Microsoft Corporation. + * @@ -8821,13 +8828,21 @@ index 0000000000000000000000000000000000000000..78815bc17978a23ca811ff99039b4fe8 + m_page.setAuthCredentialsForAutomation(Optional()); +} + ++void WebPageInspectorEmulationAgent::setActiveAndFocused(Inspector::ErrorString&, const bool* active) ++{ ++ Optional value; ++ if (active) ++ value = *active; ++ m_page.setActiveForAutomation(value); ++} ++ +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h new file mode 100644 -index 0000000000000000000000000000000000000000..77dff2c191fee081773bc5705d80168c3898f496 +index 0000000000000000000000000000000000000000..43d827233df725fa8c85fc9ff4b20a1f716803e0 --- /dev/null +++ b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h -@@ -0,0 +1,66 @@ +@@ -0,0 +1,67 @@ +/* + * Copyright (C) 2019 Microsoft Corporation. + * @@ -8885,6 +8900,7 @@ index 0000000000000000000000000000000000000000..77dff2c191fee081773bc5705d80168c + void setDeviceMetricsOverride(int width, int height, double deviceScaleFactor, bool fixedlayout, Ref&&) override; + void setJavaScriptEnabled(Inspector::ErrorString&, bool enabled) override; + void setAuthCredentials(Inspector::ErrorString&, const String*, const String*) override; ++ void setActiveAndFocused(Inspector::ErrorString&, const bool*) override; + +private: + void platformSetSize(int width, int height, Function&&); @@ -9235,7 +9251,7 @@ index 0000000000000000000000000000000000000000..76290475097e756e3d932d22be4d8c79 + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp -index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e7772056b250ee5 100644 +index 059e904a16c6c4b91d6271e25d239b975191facd..df51560a3abc953030e7cb7602275c0a9ab72281 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.cpp +++ b/Source/WebKit/UIProcess/WebPageProxy.cpp @@ -905,6 +905,7 @@ void WebPageProxy::finishAttachingToWebProcess(ProcessLaunchReason reason) @@ -9268,18 +9284,30 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 RefPtr WebPageProxy::loadRequest(ResourceRequest&& request, ShouldOpenExternalURLsPolicy shouldOpenExternalURLsPolicy, API::Object* userData) { if (m_isClosed) -@@ -1680,6 +1696,19 @@ void WebPageProxy::setControlledByAutomation(bool controlled) +@@ -1680,6 +1696,31 @@ void WebPageProxy::setControlledByAutomation(bool controlled) m_process->processPool().sendToNetworkingProcess(Messages::NetworkProcess::SetSessionIsControlledByAutomation(m_websiteDataStore->sessionID(), m_controlledByAutomation)); } -+void WebPageProxy::setAuthCredentialsForAutomation(Optional&& credentials) { ++void WebPageProxy::setAuthCredentialsForAutomation(Optional&& credentials) ++{ + m_credentialsForAutomation = WTFMove(credentials); +} + -+void WebPageProxy::setPermissionsForAutomation(const HashMap>& permissions) { ++void WebPageProxy::setPermissionsForAutomation(const HashMap>& permissions) ++{ + m_permissionsForAutomation = permissions; +} + ++void WebPageProxy::setActiveForAutomation(Optional active) { ++ m_activeForAutomation = active; ++ OptionSet state; ++ state.add(ActivityState::IsFocused); ++ state.add(ActivityState::WindowIsActive); ++ state.add(ActivityState::IsVisible); ++ state.add(ActivityState::IsVisibleOrOccluded); ++ activityStateDidChange(state); ++} ++ +void WebPageProxy::logToStderr(const String& str) +{ + fprintf(stderr, "RENDERER: %s\n", str.utf8().data()); @@ -9288,7 +9316,33 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 void WebPageProxy::createInspectorTarget(const String& targetId, Inspector::InspectorTargetType type) { MESSAGE_CHECK(m_process, !targetId.isEmpty()); -@@ -2767,7 +2796,7 @@ static TrackingType mergeTrackingTypes(TrackingType a, TrackingType b) +@@ -1822,6 +1863,25 @@ void WebPageProxy::updateActivityState(OptionSet flagsToUpd + { + bool wasVisible = isViewVisible(); + m_activityState.remove(flagsToUpdate); ++ ++ ++ if (m_activeForAutomation) { ++ if (*m_activeForAutomation) { ++ if (flagsToUpdate & ActivityState::IsFocused) ++ m_activityState.add(ActivityState::IsFocused); ++ if (flagsToUpdate & ActivityState::WindowIsActive) ++ m_activityState.add(ActivityState::WindowIsActive); ++ if (flagsToUpdate & ActivityState::IsVisible) ++ m_activityState.add(ActivityState::IsVisible); ++ if (flagsToUpdate & ActivityState::IsVisibleOrOccluded) ++ m_activityState.add(ActivityState::IsVisibleOrOccluded); ++ } ++ flagsToUpdate.remove(ActivityState::IsFocused); ++ flagsToUpdate.remove(ActivityState::WindowIsActive); ++ flagsToUpdate.remove(ActivityState::IsVisible); ++ flagsToUpdate.remove(ActivityState::IsVisibleOrOccluded); ++ } ++ + if (flagsToUpdate & ActivityState::IsFocused && pageClient().isViewFocused()) + m_activityState.add(ActivityState::IsFocused); + if (flagsToUpdate & ActivityState::WindowIsActive && pageClient().isViewWindowActive()) +@@ -2767,7 +2827,7 @@ static TrackingType mergeTrackingTypes(TrackingType a, TrackingType b) void WebPageProxy::updateTouchEventTracking(const WebTouchEvent& touchStartEvent) { @@ -9297,7 +9351,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 const EventNames& names = eventNames(); for (auto& touchPoint : touchStartEvent.touchPoints()) { IntPoint location = touchPoint.location(); -@@ -2800,7 +2829,7 @@ void WebPageProxy::updateTouchEventTracking(const WebTouchEvent& touchStartEvent +@@ -2800,7 +2860,7 @@ void WebPageProxy::updateTouchEventTracking(const WebTouchEvent& touchStartEvent m_touchAndPointerEventTracking.touchStartTracking = TrackingType::Synchronous; m_touchAndPointerEventTracking.touchMoveTracking = TrackingType::Synchronous; m_touchAndPointerEventTracking.touchEndTracking = TrackingType::Synchronous; @@ -9306,7 +9360,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 } TrackingType WebPageProxy::touchEventTrackingType(const WebTouchEvent& touchStartEvent) const -@@ -3135,6 +3164,7 @@ void WebPageProxy::receivedNavigationPolicyDecision(PolicyAction policyAction, A +@@ -3135,6 +3195,7 @@ void WebPageProxy::receivedNavigationPolicyDecision(PolicyAction policyAction, A void WebPageProxy::receivedPolicyDecision(PolicyAction action, API::Navigation* navigation, Optional&& websitePolicies, Ref&& sender, WillContinueLoadInNewProcess willContinueLoadInNewProcess) { @@ -9314,7 +9368,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 if (!hasRunningProcess()) { sender->send(PolicyAction::Ignore, 0, DownloadID(), WTF::nullopt); return; -@@ -4228,6 +4258,7 @@ void WebPageProxy::didDestroyNavigation(uint64_t navigationID) +@@ -4228,6 +4289,7 @@ void WebPageProxy::didDestroyNavigation(uint64_t navigationID) // FIXME: Message check the navigationID. m_navigationState->didDestroyNavigation(navigationID); @@ -9322,7 +9376,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 } void WebPageProxy::didStartProvisionalLoadForFrame(FrameIdentifier frameID, uint64_t navigationID, URL&& url, URL&& unreachableURL, const UserData& userData) -@@ -4449,6 +4480,8 @@ void WebPageProxy::didFailProvisionalLoadForFrameShared(Ref&& p +@@ -4449,6 +4511,8 @@ void WebPageProxy::didFailProvisionalLoadForFrameShared(Ref&& p m_failingProvisionalLoadURL = { }; @@ -9331,7 +9385,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 // If the provisional page's load fails then we destroy the provisional page. if (m_provisionalPage && m_provisionalPage->mainFrame() == frame && willContinueLoading == WillContinueLoading::No) m_provisionalPage = nullptr; -@@ -4886,8 +4919,16 @@ void WebPageProxy::decidePolicyForNavigationActionAsync(FrameIdentifier frameID, +@@ -4886,8 +4950,16 @@ void WebPageProxy::decidePolicyForNavigationActionAsync(FrameIdentifier frameID, NavigationActionData&& navigationActionData, FrameInfoData&& frameInfoData, Optional originatingPageID, const WebCore::ResourceRequest& originalRequest, WebCore::ResourceRequest&& request, IPC::FormDataReference&& requestBody, WebCore::ResourceResponse&& redirectResponse, const UserData& userData, uint64_t listenerID) { @@ -9350,7 +9404,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 } void WebPageProxy::decidePolicyForNavigationActionAsyncShared(Ref&& process, PageIdentifier webPageID, FrameIdentifier frameID, WebCore::SecurityOriginData&& frameSecurityOrigin, -@@ -5459,6 +5500,8 @@ void WebPageProxy::runJavaScriptAlert(FrameIdentifier frameID, SecurityOriginDat +@@ -5459,6 +5531,8 @@ void WebPageProxy::runJavaScriptAlert(FrameIdentifier frameID, SecurityOriginDat if (auto* automationSession = process().processPool().automationSession()) automationSession->willShowJavaScriptDialog(*this); } @@ -9359,7 +9413,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 m_uiClient->runJavaScriptAlert(*this, message, frame, WTFMove(securityOrigin), WTFMove(reply)); } -@@ -5478,6 +5521,8 @@ void WebPageProxy::runJavaScriptConfirm(FrameIdentifier frameID, SecurityOriginD +@@ -5478,6 +5552,8 @@ void WebPageProxy::runJavaScriptConfirm(FrameIdentifier frameID, SecurityOriginD if (auto* automationSession = process().processPool().automationSession()) automationSession->willShowJavaScriptDialog(*this); } @@ -9368,7 +9422,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 m_uiClient->runJavaScriptConfirm(*this, message, frame, WTFMove(securityOrigin), WTFMove(reply)); } -@@ -5497,6 +5542,8 @@ void WebPageProxy::runJavaScriptPrompt(FrameIdentifier frameID, SecurityOriginDa +@@ -5497,6 +5573,8 @@ void WebPageProxy::runJavaScriptPrompt(FrameIdentifier frameID, SecurityOriginDa if (auto* automationSession = process().processPool().automationSession()) automationSession->willShowJavaScriptDialog(*this); } @@ -9377,7 +9431,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 m_uiClient->runJavaScriptPrompt(*this, message, defaultValue, frame, WTFMove(securityOrigin), WTFMove(reply)); } -@@ -5656,6 +5703,8 @@ void WebPageProxy::runBeforeUnloadConfirmPanel(FrameIdentifier frameID, Security +@@ -5656,6 +5734,8 @@ void WebPageProxy::runBeforeUnloadConfirmPanel(FrameIdentifier frameID, Security return; } } @@ -9386,7 +9440,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 // Since runBeforeUnloadConfirmPanel() can spin a nested run loop we need to turn off the responsiveness timer and the tryClose timer. m_process->stopResponsivenessTimer(); -@@ -6715,6 +6764,7 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) +@@ -6715,6 +6795,7 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) if (auto* automationSession = process().processPool().automationSession()) automationSession->mouseEventsFlushedForPage(*this); didFinishProcessingAllPendingMouseEvents(); @@ -9394,7 +9448,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 } break; -@@ -6741,7 +6791,6 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) +@@ -6741,7 +6822,6 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) case WebEvent::RawKeyDown: case WebEvent::Char: { LOG(KeyHandling, "WebPageProxy::didReceiveEvent: %s (queue empty %d)", webKeyboardEventTypeString(type), m_keyEventQueue.isEmpty()); @@ -9402,7 +9456,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 MESSAGE_CHECK(m_process, !m_keyEventQueue.isEmpty()); NativeWebKeyboardEvent event = m_keyEventQueue.takeFirst(); -@@ -6761,7 +6810,6 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) +@@ -6761,7 +6841,6 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) // The call to doneWithKeyEvent may close this WebPage. // Protect against this being destroyed. Ref protect(*this); @@ -9410,7 +9464,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 pageClient().doneWithKeyEvent(event, handled); if (!handled) m_uiClient->didNotHandleKeyEvent(this, event); -@@ -6770,6 +6818,7 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) +@@ -6770,6 +6849,7 @@ void WebPageProxy::didReceiveEvent(uint32_t opaqueType, bool handled) if (!canProcessMoreKeyEvents) { if (auto* automationSession = process().processPool().automationSession()) automationSession->keyboardEventsFlushedForPage(*this); @@ -9418,7 +9472,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 } break; } -@@ -7238,8 +7287,10 @@ static bool shouldReloadAfterProcessTermination(ProcessTerminationReason reason) +@@ -7238,8 +7318,10 @@ static bool shouldReloadAfterProcessTermination(ProcessTerminationReason reason) void WebPageProxy::dispatchProcessDidTerminate(ProcessTerminationReason reason) { RELEASE_LOG_IF_ALLOWED(Loading, "dispatchProcessDidTerminate: reason = %d", reason); @@ -9430,7 +9484,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 if (m_loaderClient) handledByClient = reason != ProcessTerminationReason::RequestedByClient && m_loaderClient->processDidCrash(*this); else -@@ -7712,6 +7763,14 @@ void WebPageProxy::gamepadActivity(const Vector& gamepadDatas, bool +@@ -7712,6 +7794,14 @@ void WebPageProxy::gamepadActivity(const Vector& gamepadDatas, bool void WebPageProxy::didReceiveAuthenticationChallengeProxy(Ref&& authenticationChallenge, NegotiatedLegacyTLS negotiatedLegacyTLS) { @@ -9445,7 +9499,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 if (negotiatedLegacyTLS == NegotiatedLegacyTLS::Yes) { m_navigationClient->shouldAllowLegacyTLS(*this, authenticationChallenge.get(), [this, protectedThis = makeRef(*this), authenticationChallenge = authenticationChallenge.copyRef()] (bool shouldAllowLegacyTLS) { if (shouldAllowLegacyTLS) -@@ -7794,7 +7853,8 @@ void WebPageProxy::requestGeolocationPermissionForFrame(uint64_t geolocationID, +@@ -7794,7 +7884,8 @@ void WebPageProxy::requestGeolocationPermissionForFrame(uint64_t geolocationID, MESSAGE_CHECK(m_process, securityOriginData); // FIXME: Geolocation should probably be using toString() as its string representation instead of databaseIdentifier(). @@ -9455,7 +9509,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 auto request = m_geolocationPermissionRequestManager.createRequest(geolocationID); Function completionHandler = [request = WTFMove(request)](bool allowed) { if (allowed) -@@ -7802,6 +7862,11 @@ void WebPageProxy::requestGeolocationPermissionForFrame(uint64_t geolocationID, +@@ -7802,6 +7893,11 @@ void WebPageProxy::requestGeolocationPermissionForFrame(uint64_t geolocationID, else request->deny(); }; @@ -9468,7 +9522,7 @@ index 059e904a16c6c4b91d6271e25d239b975191facd..0c5038d790e2c5913b1bb8344e777205 // FIXME: Once iOS migrates to the new WKUIDelegate SPI, clean this up // and make it one UIClient call that calls the completionHandler with false diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h -index b3856a4f9e7859bf3e0b9957c95040a784e9b317..5d51d3e9e03b375a2cbd01f909e9b27c3605f8e4 100644 +index b3856a4f9e7859bf3e0b9957c95040a784e9b317..2d154febc73de4877463e5099ffdc3002f6dfd2b 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.h +++ b/Source/WebKit/UIProcess/WebPageProxy.h @@ -35,6 +35,7 @@ @@ -9488,18 +9542,19 @@ index b3856a4f9e7859bf3e0b9957c95040a784e9b317..5d51d3e9e03b375a2cbd01f909e9b27c #if PLATFORM(IOS_FAMILY) void showInspectorIndication(); -@@ -555,6 +558,10 @@ public: +@@ -555,6 +558,11 @@ public: void setPageLoadStateObserver(std::unique_ptr&&); + void setAuthCredentialsForAutomation(Optional&&); + void setPermissionsForAutomation(const HashMap>&); ++ void setActiveForAutomation(Optional active); + void logToStderr(const String& str); + void initializeWebPage(); void setDrawingArea(std::unique_ptr&&); -@@ -580,6 +587,7 @@ public: +@@ -580,6 +588,7 @@ public: void closePage(); void addPlatformLoadParameters(LoadParameters&); @@ -9507,7 +9562,7 @@ index b3856a4f9e7859bf3e0b9957c95040a784e9b317..5d51d3e9e03b375a2cbd01f909e9b27c RefPtr loadRequest(WebCore::ResourceRequest&&, WebCore::ShouldOpenExternalURLsPolicy = WebCore::ShouldOpenExternalURLsPolicy::ShouldAllowExternalSchemes, API::Object* userData = nullptr); RefPtr loadFile(const String& fileURL, const String& resourceDirectoryURL, API::Object* userData = nullptr); RefPtr loadData(const IPC::DataReference&, const String& MIMEType, const String& encoding, const String& baseURL, API::Object* userData = nullptr, WebCore::ShouldOpenExternalURLsPolicy = WebCore::ShouldOpenExternalURLsPolicy::ShouldNotAllow); -@@ -2293,6 +2301,7 @@ private: +@@ -2293,6 +2302,7 @@ private: String m_overrideContentSecurityPolicy; RefPtr m_inspector; @@ -9515,12 +9570,13 @@ index b3856a4f9e7859bf3e0b9957c95040a784e9b317..5d51d3e9e03b375a2cbd01f909e9b27c #if ENABLE(FULLSCREEN_API) std::unique_ptr m_fullScreenManager; -@@ -2707,6 +2716,8 @@ private: +@@ -2707,6 +2717,9 @@ private: bool m_isLayerTreeFrozenDueToSwipeAnimation { false }; String m_overriddenMediaType; + Optional m_credentialsForAutomation; + HashMap> m_permissionsForAutomation; ++ Optional m_activeForAutomation; #if PLATFORM(IOS_FAMILY) && ENABLE(DEVICE_ORIENTATION) std::unique_ptr m_webDeviceOrientationUpdateProviderProxy; @@ -12529,7 +12585,7 @@ index f84e5cde0baaa08dab1fc77771e4a62a08a5df78..dac73d3d9db04c5d502432cea8adfa89 void navigateToHistory(UINT menuID); bool seedInitialDefaultPreferences(); diff --git a/Tools/MiniBrowser/win/WinMain.cpp b/Tools/MiniBrowser/win/WinMain.cpp -index b1d17e88de61a6f196830f62604e4174564506bd..0cc40ce5a5f0a3331275fcdaf7ea95cd0dc5031f 100644 +index b1d17e88de61a6f196830f62604e4174564506bd..2035067fef157cb23dc3a93f1a086719a32d7d38 100644 --- a/Tools/MiniBrowser/win/WinMain.cpp +++ b/Tools/MiniBrowser/win/WinMain.cpp @@ -32,6 +32,9 @@ @@ -12587,7 +12643,18 @@ index b1d17e88de61a6f196830f62604e4174564506bd..0cc40ce5a5f0a3331275fcdaf7ea95cd if (options.useFullDesktop) computeFullDesktopFrame(); -@@ -86,19 +115,35 @@ int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, +@@ -75,9 +104,7 @@ int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, + OleInitialize(nullptr); + + if (SetProcessDpiAwarenessContextPtr()) +- SetProcessDpiAwarenessContextPtr()(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); +- else +- ::SetProcessDPIAware(); ++ SetProcessDpiAwarenessContextPtr()(DPI_AWARENESS_CONTEXT_UNAWARE); + + #if !ENABLE(WEBKIT_LEGACY) + auto factory = WebKitBrowserWindow::create; +@@ -86,19 +113,35 @@ int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, #else auto factory = options.windowType == BrowserWindowType::WebKit ? WebKitBrowserWindow::create : WebKitLegacyBrowserWindow::create; #endif diff --git a/docs/api.md b/docs/api.md index b946c1790a..fb8e257c48 100644 --- a/docs/api.md +++ b/docs/api.md @@ -153,13 +153,12 @@ See [ChromiumBrowser], [FirefoxBrowser] and [WebKitBrowser] for browser-specific - [browser.isConnected()](#browserisconnected) - [browser.newContext(options)](#browsernewcontextoptions) - [browser.newPage([options])](#browsernewpageoptions) -- [browser.pages()](#browserpages) #### event: 'disconnected' Emitted when Browser gets disconnected from the browser application. This might happen because of one of the following: - Browser application is closed or crashed. -- The [`browser.disconnect`](#browserdisconnect) method was called. +- The [`browser.close`](#browserclose) method was called. #### browser.close() - returns: <[Promise]> @@ -233,12 +232,9 @@ Creates a new browser context. It won't share cookies/cache with other browser c - `permissions` <[Object]> A map from origin keys to permissions values. See [browserContext.setPermissions](#browsercontextsetpermissionsorigin-permissions) for more details. - returns: <[Promise]<[Page]>> -Creates a new page in a new browser context. +Creates a new page in a new browser context. Closing this page will close the context as well. -#### browser.pages() -- returns: <[Promise]<[Array]<[Page]>>> Promise which resolves to an array of all open pages. - -An array of all the pages inside all the browser contexts. +This is a convenience API that should only be used for the single-page scenarios and short snippets. Production code and testing frameworks should explicitly create [browser.newContext](#browsernewcontextoptions) followed by the [browserContext.newPage](#browsercontextnewpage) to control their exact life times. ### class: BrowserContext @@ -263,6 +259,7 @@ await context.close(); ``` +- [event: 'close'](#event-close) - [browserContext.clearCookies()](#browsercontextclearcookies) - [browserContext.clearPermissions()](#browsercontextclearpermissions) - [browserContext.close()](#browsercontextclose) @@ -274,6 +271,13 @@ await context.close(); - [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions) +#### event: 'close' + +Emitted when Browser context gets closed. This might happen because of one of the following: +- Browser context is closed. +- Browser application is closed or crashed. +- The [`browser.close`](#browserclose) method was called. + #### browserContext.clearCookies() - returns: <[Promise]> @@ -426,7 +430,7 @@ page.removeListener('request', logRequest); ``` -- [event: 'close'](#event-close) +- [event: 'close'](#event-close-1) - [event: 'console'](#event-console) - [event: 'dialog'](#event-dialog) - [event: 'domcontentloaded'](#event-domcontentloaded) @@ -448,7 +452,7 @@ page.removeListener('request', logRequest); - [page.$$(selector)](#pageselector-1) - [page.$$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args) - [page.$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args-1) -- [page.$wait(selector, pageFunction[, options[, ...args]])](#pagewaitselector-pagefunction-options-args) +- [page.$wait(selector[, options])](#pagewaitselector-options) - [page.accessibility](#pageaccessibility) - [page.addScriptTag(options)](#pageaddscripttagoptions) - [page.addStyleTag(options)](#pageaddstyletagoptions) @@ -679,23 +683,24 @@ const html = await page.$eval('.main-container', e => e.outerHTML); Shortcut for [page.mainFrame().$eval(selector, pageFunction)](#frameevalselector-pagefunction-args). -#### page.$wait(selector, pageFunction[, options[, ...args]]) -- `selector` <[string]> A selector to query page for -- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context -- `options` <[Object]> Optional waiting parameters - - `polling` <[number]|"raf"|"mutation"> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values: - - `'raf'` - to constantly execute `pageFunction` in `requestAnimationFrame` callback. This is the tightest polling mode which is suitable to observe styling changes. - - `'mutation'` - to execute `pageFunction` on every DOM mutation. - - `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method. -- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` -- returns: <[Promise]<[JSHandle]>> Promise which resolves to a JSHandle of the success value +#### page.$wait(selector[, options]) +- `selector` <[string]> A selector of an element to wait for +- `options` <[Object]> + - `visibility` <"visible"|"hidden"|"any"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`). Defaults to `any`. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method. +- returns: <[Promise]> Promise which resolves when element specified by selector string is added to DOM. Resolves to `null` if waiting for `hidden: true` and selector is not found in DOM. -This method runs `document.querySelector` within the page and passes it as the first argument to `pageFunction`. If there's no element matching `selector`, the method throws an error. +Wait for the `selector` to appear in page. If at the moment of calling +the method the `selector` already exists, the method will return +immediately. If the selector doesn't appear after the `timeout` milliseconds of waiting, the function will throw. -If `pageFunction` returns a [Promise], then `page.$wait` would wait for the promise to resolve and return its value. The function -is being called on the element periodically until either timeout expires or the function returns the truthy value. +This method works across navigations: +```js +const handle = await page.$wait(selector); +await handle.click(); +``` -Shortcut for [page.mainFrame().$wait(selector, pageFunction[, options[, ...args]])](#framewaitselector-pagefunction-options-args). +This is a shortcut to [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options). #### page.accessibility - returns: <[Accessibility]> @@ -1664,7 +1669,7 @@ An example of getting text from an iframe element: - [frame.$$(selector)](#frameselector-1) - [frame.$$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args) - [frame.$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args-1) -- [frame.$wait(selector, pageFunction[, options[, ...args]])](#framewaitselector-pagefunction-options-args) +- [frame.$wait(selector[, options])](#framewaitselector-options) - [frame.addScriptTag(options)](#frameaddscripttagoptions) - [frame.addStyleTag(options)](#frameaddstyletagoptions) - [frame.check(selector, [options])](#framecheckselector-options) @@ -1740,21 +1745,24 @@ const preloadHref = await frame.$eval('link[rel=preload]', el => el.href); const html = await frame.$eval('.main-container', e => e.outerHTML); ``` -#### frame.$wait(selector, pageFunction[, options[, ...args]]) -- `selector` <[string]> A selector to query page for -- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context -- `options` <[Object]> Optional waiting parameters - - `polling` <[number]|"raf"|"mutation"> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values: - - `'raf'` - to constantly execute `pageFunction` in `requestAnimationFrame` callback. This is the tightest polling mode which is suitable to observe styling changes. - - `'mutation'` - to execute `pageFunction` on every DOM mutation. - - `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method. -- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` -- returns: <[Promise]<[JSHandle]>> Promise which resolves to a JSHandle of the success value +#### frame.$wait(selector[, options]) +- `selector` <[string]> A selector of an element to wait for +- `options` <[Object]> + - `visibility` <"visible"|"hidden"|"any"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`). Defaults to `any`. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method. +- returns: <[Promise]> Promise which resolves when element specified by selector string is added to DOM. Resolves to `null` if waiting for `hidden: true` and selector is not found in DOM. -This method runs `document.querySelector` within the frame and passes it as the first argument to `pageFunction`. If there's no element matching `selector`, the method throws an error. +Wait for the `selector` to appear in page. If at the moment of calling +the method the `selector` already exists, the method will return +immediately. If the selector doesn't appear after the `timeout` milliseconds of waiting, the function will throw. -If `pageFunction` returns a [Promise], then `page.$wait` would wait for the promise to resolve and return its value. The function -is being called on the element periodically until either timeout expires or the function returns the truthy value. +This method works across navigations: +```js +const handle = await page.$wait(selector); +await handle.click(); +``` + +This is a shortcut to [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options). #### frame.addScriptTag(options) - `options` <[Object]> @@ -3168,7 +3176,7 @@ const { selectors, firefox } = require('playwright'); // Or 'chromium' or 'webk The [WebSocket] class represents websocket connections in the page. -- [event: 'close'](#event-close-1) +- [event: 'close'](#event-close-2) - [event: 'error'](#event-error) - [event: 'messageReceived'](#event-messagereceived) - [event: 'messageSent'](#event-messagesent) @@ -3423,7 +3431,7 @@ If the function passed to the `worker.evaluateHandle` returns a [Promise], then ### class: BrowserServer -- [event: 'close'](#event-close-2) +- [event: 'close'](#event-close-3) - [browserServer.close()](#browserserverclose) - [browserServer.kill()](#browserserverkill) - [browserServer.process()](#browserserverprocess) @@ -3470,6 +3478,7 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'. - [browserType.connect(options)](#browsertypeconnectoptions) - [browserType.devices](#browsertypedevices) +- [browserType.downloadBrowserIfNeeded([progress])](#browsertypedownloadbrowserifneededprogress) - [browserType.errors](#browsertypeerrors) - [browserType.executablePath()](#browsertypeexecutablepath) - [browserType.launch([options])](#browsertypelaunchoptions) @@ -3508,6 +3517,12 @@ const iPhone = webkit.devices['iPhone 6']; })(); ``` +#### browserType.downloadBrowserIfNeeded([progress]) +- `progress` <[function]> If download is initiated, this function is called with two parameters: `downloadedBytes` and `totalBytes`. +- returns: <[Promise]> promise that resolves when browser is successfully downloaded. + +Download browser binary if it is missing. + #### browserType.errors - returns: <[Object]> - `TimeoutError` <[function]> A class of [TimeoutError]. @@ -3652,7 +3667,6 @@ await browser.stopTracing(); - [browser.isConnected()](#browserisconnected) - [browser.newContext(options)](#browsernewcontextoptions) - [browser.newPage([options])](#browsernewpageoptions) -- [browser.pages()](#browserpages) #### event: 'targetchanged' @@ -3819,7 +3833,6 @@ Firefox browser instance does not expose Firefox-specific features. - [browser.isConnected()](#browserisconnected) - [browser.newContext(options)](#browsernewcontextoptions) - [browser.newPage([options])](#browsernewpageoptions) -- [browser.pages()](#browserpages) ### class: WebKitBrowser @@ -3835,7 +3848,6 @@ WebKit browser instance does not expose WebKit-specific features. - [browser.isConnected()](#browserisconnected) - [browser.newContext(options)](#browsernewcontextoptions) - [browser.newPage([options])](#browsernewpageoptions) -- [browser.pages()](#browserpages) ### Working with selectors diff --git a/download-browser.js b/download-browser.js index 5cbb6ba5ca..c2a72e755e 100644 --- a/download-browser.js +++ b/download-browser.js @@ -38,7 +38,7 @@ async function downloadBrowser(browser) { // Do nothing if the revision is already downloaded. if (revisionInfo.local) return revisionInfo; - await fetcher.download(revisionInfo.revision, onProgress); + await browserType.downloadBrowserIfNeeded(onProgress); logPolitely(`${browser} downloaded to ${revisionInfo.folderPath}`); return revisionInfo; } diff --git a/package.json b/package.json index e29bdecaeb..1483882b14 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "main": "index.js", "playwright": { - "chromium_revision": "739261", + "chromium_revision": "740289", "firefox_revision": "1028", "webkit_revision": "1141" }, diff --git a/src/browser.ts b/src/browser.ts index 905f5ee2f8..b5cca25331 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -21,11 +21,9 @@ import { Page } from './page'; export interface Browser extends platform.EventEmitterType { newContext(options?: BrowserContextOptions): Promise; contexts(): BrowserContext[]; - pages(): Promise; newPage(options?: BrowserContextOptions): Promise; isConnected(): boolean; close(): Promise; - _defaultContext: BrowserContext | undefined; } export type ConnectOptions = { @@ -33,14 +31,11 @@ export type ConnectOptions = { wsEndpoint: string }; -export async function collectPages(browser: Browser): Promise { - const result: Promise[] = []; - for (const browserContext of browser.contexts()) - result.push(browserContext.pages()); - const pages: Page[] = []; - for (const group of await Promise.all(result)) - pages.push(...group); - return pages; +export async function createPageInNewContext(browser: Browser, options?: BrowserContextOptions): Promise { + const context = await browser.newContext(options); + const page = await context.newPage(); + page._ownedContext = context; + return page; } export type LaunchType = 'local' | 'server' | 'persistent'; diff --git a/src/browserContext.ts b/src/browserContext.ts index 86753436ad..ca56d75520 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -19,6 +19,8 @@ import { Page } from './page'; import * as network from './network'; import * as types from './types'; import { helper } from './helper'; +import * as platform from './platform'; +import { Events } from './events'; export interface BrowserContextDelegate { pages(): Promise; @@ -47,12 +49,13 @@ export type BrowserContextOptions = { permissions?: { [key: string]: string[] }; }; -export class BrowserContext { +export class BrowserContext extends platform.EventEmitter { private readonly _delegate: BrowserContextDelegate; readonly _options: BrowserContextOptions; private _closed = false; constructor(delegate: BrowserContextDelegate, options: BrowserContextOptions) { + super(); this._delegate = delegate; this._options = { ...options }; if (!this._options.viewport && this._options.viewport !== null) @@ -79,6 +82,11 @@ export class BrowserContext { } async newPage(): Promise { + const pages = this._delegate.existingPages(); + for (const page of pages) { + if (page._ownedContext) + throw new Error('Please use browser.newContext() for multi-page scripts that share the context.'); + } return this._delegate.newPage(); } @@ -114,12 +122,20 @@ export class BrowserContext { return; await this._delegate.close(); this._closed = true; + this.emit(Events.BrowserContext.Close); } static validateOptions(options: BrowserContextOptions) { if (options.geolocation) verifyGeolocation(options.geolocation); } + + _browserClosed() { + this._closed = true; + for (const page of this._delegate.existingPages()) + page._didClose(); + this.emit(Events.BrowserContext.Close); + } } function verifyGeolocation(geolocation: types.Geolocation): types.Geolocation { diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index 5bc2987cb4..755b85525c 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -24,7 +24,7 @@ import { Page, Worker } from '../page'; import { CRTarget } from './crTarget'; import { Protocol } from './protocol'; import { CRPage } from './crPage'; -import { Browser, collectPages } from '../browser'; +import { Browser, createPageInNewContext } from '../browser'; import * as network from '../network'; import * as types from '../types'; import * as platform from '../platform'; @@ -56,7 +56,11 @@ export class CRBrowser extends platform.EventEmitter implements Browser { this._client = connection.rootSession; this._defaultContext = this._createBrowserContext(null, {}); - this._connection.on(ConnectionEvents.Disconnected, () => this.emit(CommonEvents.Browser.Disconnected)); + this._connection.on(ConnectionEvents.Disconnected, () => { + for (const context of this.contexts()) + context._browserClosed(); + this.emit(CommonEvents.Browser.Disconnected); + }); this._client.on('Target.targetCreated', this._targetCreated.bind(this)); this._client.on('Target.targetDestroyed', this._targetDestroyed.bind(this)); this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this)); @@ -164,13 +168,8 @@ export class CRBrowser extends platform.EventEmitter implements Browser { return Array.from(this._contexts.values()); } - async pages(): Promise { - return collectPages(this); - } - async newPage(options?: BrowserContextOptions): Promise { - const context = await this.newContext(options); - return context.newPage(); + return createPageInNewContext(this, options); } async _targetCreated(event: Protocol.Target.targetCreatedPayload) { diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index 49ea276fbc..63a1ba099c 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -484,6 +484,17 @@ export class CRPage implements PageDelegate { return {x, y, width, height}; } + async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise { + await this._client.send('DOM.scrollIntoViewIfNeeded', { + objectId: toRemoteObject(handle).objectId, + rect, + }).catch(e => { + if (e instanceof Error && e.message.includes('Node does not have a layout object')) + e.message = 'Node is either not visible or not an HTMLElement'; + throw e; + }); + } + async getContentQuads(handle: dom.ElementHandle): Promise { const result = await this._client.send('DOM.getContentQuads', { objectId: toRemoteObject(handle).objectId diff --git a/src/dom.ts b/src/dom.ts index 5a581ae945..ab2c3a05fa 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -160,58 +160,12 @@ export class ElementHandle extends js.JSHandle { return this._page._delegate.getContentFrame(this); } - async scrollIntoViewIfNeeded() { - const error = await this._evaluateInUtility(async (node: Node, pageJavascriptEnabled: boolean) => { - if (!node.isConnected) - return 'Node is detached from document'; - if (node.nodeType !== Node.ELEMENT_NODE) - return 'Node is not of type HTMLElement'; - const element = node as Element; - // force-scroll if page's javascript is disabled. - if (!pageJavascriptEnabled) { - // @ts-ignore because only Chromium still supports 'instant' - element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); - return false; - } - 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(() => {}); - }); - if (visibleRatio !== 1.0) { - // @ts-ignore because only Chromium still supports 'instant' - element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); - } - return false; - }, !!this._page.context()._options.javaScriptEnabled); - if (error) - throw new Error(error); + async _scrollRectIntoViewIfNeeded(rect?: types.Rect): Promise { + await this._page._delegate.scrollRectIntoViewIfNeeded(this, rect); } - private async _ensurePointerActionPoint(relativePoint?: types.Point): Promise { - await this.scrollIntoViewIfNeeded(); - if (!relativePoint) - return this._clickablePoint(); - let r = await this._viewportPointAndScroll(relativePoint); - if (r.scrollX || r.scrollY) { - const error = await this._evaluateInUtility((element, scrollX, scrollY) => { - if (!element.ownerDocument || !element.ownerDocument.defaultView) - return 'Node does not have a containing window'; - element.ownerDocument.defaultView.scrollBy(scrollX, scrollY); - return false; - }, r.scrollX, r.scrollY); - if (error) - throw new Error(error); - r = await this._viewportPointAndScroll(relativePoint); - if (r.scrollX || r.scrollY) - throw new Error('Failed to scroll relative point into viewport'); - } - return r.point; + async scrollIntoViewIfNeeded() { + await this._scrollRectIntoViewIfNeeded(); } private async _clickablePoint(): Promise { @@ -253,7 +207,7 @@ export class ElementHandle extends js.JSHandle { return result; } - private async _viewportPointAndScroll(relativePoint: types.Point): Promise<{point: types.Point, scrollX: number, scrollY: number}> { + private async _relativePoint(relativePoint: types.Point): Promise { const [box, border] = await Promise.all([ this.boundingBox(), this._evaluateInUtility((node: Node) => { @@ -273,23 +227,13 @@ export class ElementHandle extends js.JSHandle { point.x += border.x; point.y += border.y; } - const metrics = await this._page._delegate.layoutViewport(); - // Give 20 extra pixels to avoid any issues on viewport edge. - let scrollX = 0; - if (point.x < 20) - scrollX = point.x - 20; - if (point.x > metrics.width - 20) - scrollX = point.x - metrics.width + 20; - let scrollY = 0; - if (point.y < 20) - scrollY = point.y - 20; - if (point.y > metrics.height - 20) - scrollY = point.y - metrics.height + 20; - return { point, scrollX, scrollY }; + return point; } async _performPointerAction(action: (point: types.Point) => Promise, options?: input.PointerActionOptions): Promise { - const point = await this._ensurePointerActionPoint(options ? options.relativePoint : undefined); + 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(); let restoreModifiers: input.Modifier[] | undefined; if (options && options.modifiers) restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers); diff --git a/src/events.ts b/src/events.ts index a5d9f751ea..462b772e51 100644 --- a/src/events.ts +++ b/src/events.ts @@ -20,6 +20,10 @@ export const Events = { Disconnected: 'disconnected' }, + BrowserContext: { + Close: 'close' + }, + BrowserServer: { Close: 'close', }, diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 531e2fe50e..6fc992abb5 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Browser, collectPages } from '../browser'; +import { Browser, createPageInNewContext } from '../browser'; import { BrowserContext, BrowserContextOptions } from '../browserContext'; import { Events } from '../events'; import { assert, helper, RegisteredListener, debugError } from '../helper'; @@ -50,8 +50,11 @@ export class FFBrowser extends platform.EventEmitter implements Browser { this._defaultContext = this._createBrowserContext(null, {}); this._contexts = new Map(); - this._connection.on(ConnectionEvents.Disconnected, () => this.emit(Events.Browser.Disconnected)); - + this._connection.on(ConnectionEvents.Disconnected, () => { + for (const context of this.contexts()) + context._browserClosed(); + this.emit(Events.Browser.Disconnected); + }); this._eventListeners = [ helper.addEventListener(this._connection, 'Target.targetCreated', this._onTargetCreated.bind(this)), helper.addEventListener(this._connection, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)), @@ -90,13 +93,8 @@ export class FFBrowser extends platform.EventEmitter implements Browser { return Array.from(this._contexts.values()); } - async pages(): Promise { - return collectPages(this); - } - async newPage(options?: BrowserContextOptions): Promise { - const context = await this.newContext(options); - return context.newPage(); + return createPageInNewContext(this, options); } async _waitForTarget(predicate: (target: Target) => boolean, options: { timeout?: number; } = {}): Promise { diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index eb9a274c78..4780a528de 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -392,6 +392,14 @@ export class FFPage implements PageDelegate { return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; } + async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise { + await this._session.send('Page.scrollIntoViewIfNeeded', { + frameId: handle._context.frame._id, + objectId: toRemoteObject(handle).objectId!, + rect, + }); + } + async getContentQuads(handle: dom.ElementHandle): Promise { const result = await this._session.send('Page.getContentQuads', { frameId: handle._context.frame._id, diff --git a/src/frames.ts b/src/frames.ts index 39959e2162..eb5db37217 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -620,6 +620,10 @@ export class Frame { return handle; } + async $wait(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise | null> { + return this.waitForSelector(selector, options); + } + $eval: types.$Eval = async (selector, pageFunction, ...args) => { const context = await this._mainContext(); const elementHandle = await context._$(selector); @@ -938,12 +942,6 @@ export class Frame { return this._scheduleRerunnableTask(task, 'main', options.timeout); } - $wait: types.$Wait = async (selector, pageFunction, options, ...args) => { - options = { timeout: this._page._timeoutSettings.timeout(), ...(options || {}) }; - const task = dom.waitForFunctionTask(selector, pageFunction, options, ...args); - return this._scheduleRerunnableTask(task, 'main', options.timeout) as any; - } - async title(): Promise { const context = await this._utilityContext(); return context.evaluate(() => document.title); diff --git a/src/page.ts b/src/page.ts index 394eac87c6..3e899647f5 100644 --- a/src/page.ts +++ b/src/page.ts @@ -69,6 +69,7 @@ export interface PageDelegate { setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise; getBoundingBox(handle: dom.ElementHandle): Promise; getFrameElement(frame: frames.Frame): Promise; + scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise; getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>; pdf?: (options?: types.PDFOptions) => Promise; @@ -113,6 +114,7 @@ export class Page extends platform.EventEmitter { readonly pdf: ((options?: types.PDFOptions) => Promise) | undefined; readonly coverage: Coverage | undefined; readonly _requestHandlers: { url: types.URLMatch, handler: (request: network.Request) => void }[] = []; + _ownedContext: BrowserContext | undefined; constructor(delegate: PageDelegate, browserContext: BrowserContext) { super(); @@ -213,6 +215,10 @@ export class Page extends platform.EventEmitter { return this.mainFrame().waitForSelector(selector, options); } + async $wait(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise | null> { + return this.waitForSelector(selector, options); + } + evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => { return this.mainFrame().evaluateHandle(pageFunction, ...args as any); } @@ -471,6 +477,8 @@ export class Page extends platform.EventEmitter { await this._delegate.closePage(runBeforeUnload); if (!runBeforeUnload) await this._closedPromise; + if (this._ownedContext) + await this._ownedContext.close(); } isClosed(): boolean { @@ -525,10 +533,6 @@ export class Page extends platform.EventEmitter { return this.mainFrame().waitForFunction(pageFunction, options, ...args); } - $wait: types.$Wait = async (selector, pageFunction, options, ...args) => { - return this.mainFrame().$wait(selector, pageFunction, options, ...args as any); - } - workers(): Worker[] { return [...this._workers.values()]; } diff --git a/src/server/browserType.ts b/src/server/browserType.ts index b7a15387c2..3c1c970c47 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -19,6 +19,7 @@ import { TimeoutError } from '../errors'; import { Browser, ConnectOptions } from '../browser'; import { BrowserContext } from '../browserContext'; import { BrowserServer } from './browserServer'; +import { OnProgressCallback } from './browserFetcher'; export type BrowserArgOptions = { headless?: boolean, @@ -44,6 +45,7 @@ export interface BrowserType { launchServer(options?: LaunchOptions & { port?: number }): Promise; launchPersistent(userDataDir: string, options?: LaunchOptions): Promise; connect(options: ConnectOptions): Promise; + downloadBrowserIfNeeded(progress?: OnProgressCallback): Promise; devices: types.Devices; errors: { TimeoutError: typeof TimeoutError }; } diff --git a/src/server/chromium.ts b/src/server/chromium.ts index e987437360..833965f998 100644 --- a/src/server/chromium.ts +++ b/src/server/chromium.ts @@ -19,7 +19,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import * as util from 'util'; -import { BrowserFetcher, BrowserFetcherOptions } from '../server/browserFetcher'; +import { BrowserFetcher, OnProgressCallback, BrowserFetcherOptions } from '../server/browserFetcher'; import { DeviceDescriptors } from '../deviceDescriptors'; import * as types from '../types'; import { assert } from '../helper'; @@ -194,6 +194,15 @@ export class Chromium implements BrowserType { return chromeArguments; } + async downloadBrowserIfNeeded(onProgress?: OnProgressCallback) { + const fetcher = this._createBrowserFetcher(); + const revisionInfo = fetcher.revisionInfo(); + // Do nothing if the revision is already downloaded. + if (revisionInfo.local) + return; + await fetcher.download(revisionInfo.revision, onProgress); + } + _createBrowserFetcher(options: BrowserFetcherOptions = {}): BrowserFetcher { const downloadURLs = { linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip', diff --git a/src/server/firefox.ts b/src/server/firefox.ts index a1b6b1ba61..67c0c66810 100644 --- a/src/server/firefox.ts +++ b/src/server/firefox.ts @@ -16,7 +16,7 @@ */ import { FFBrowser } from '../firefox/ffBrowser'; -import { BrowserFetcher, BrowserFetcherOptions } from './browserFetcher'; +import { BrowserFetcher, OnProgressCallback, BrowserFetcherOptions } from './browserFetcher'; import { DeviceDescriptors } from '../deviceDescriptors'; import { launchProcess, waitForLine } from './processLauncher'; import * as types from '../types'; @@ -44,6 +44,15 @@ export class Firefox implements BrowserType { this._revision = preferredRevision; } + async downloadBrowserIfNeeded(onProgress?: OnProgressCallback) { + const fetcher = this._createBrowserFetcher(); + const revisionInfo = fetcher.revisionInfo(); + // Do nothing if the revision is already downloaded. + if (revisionInfo.local) + return; + await fetcher.download(revisionInfo.revision, onProgress); + } + name() { return 'firefox'; } diff --git a/src/server/webkit.ts b/src/server/webkit.ts index d201e3ec65..9a3297ef3c 100644 --- a/src/server/webkit.ts +++ b/src/server/webkit.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { BrowserFetcher, BrowserFetcherOptions } from './browserFetcher'; +import { BrowserFetcher, OnProgressCallback, BrowserFetcherOptions } from './browserFetcher'; import { DeviceDescriptors } from '../deviceDescriptors'; import { TimeoutError } from '../errors'; import * as types from '../types'; @@ -52,6 +52,15 @@ export class WebKit implements BrowserType { return 'webkit'; } + async downloadBrowserIfNeeded(onProgress?: OnProgressCallback) { + const fetcher = this._createBrowserFetcher(); + const revisionInfo = fetcher.revisionInfo(); + // Do nothing if the revision is already downloaded. + if (revisionInfo.local) + return; + await fetcher.download(revisionInfo.revision, onProgress); + } + async launch(options?: LaunchOptions & { slowMo?: number }): Promise { const { browserServer, transport } = await this._launchServer(options, 'local'); const browser = await WKBrowser.connect(transport!, options && options.slowMo); @@ -84,7 +93,6 @@ export class WebKit implements BrowserType { handleSIGINT = true, handleSIGTERM = true, handleSIGHUP = true, - timeout = 30000 } = options; let temporaryUserDataDir: string | null = null; @@ -136,7 +144,6 @@ export class WebKit implements BrowserType { }, }); - const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to WebKit!`); transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream); browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? await wrapTransportWithWebSocket(transport, port || 0) : null); return { browserServer, transport }; diff --git a/src/types.ts b/src/types.ts index 006d077c55..a4de23a5f1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,7 +27,6 @@ export type Evaluate = (pageFunction: PageFunction(pageFunction: PageFunction, ...args: Boxed) => Promise>; export type $Eval = (selector: string, pageFunction: PageFunctionOn, ...args: Boxed) => Promise; export type $$Eval = (selector: string, pageFunction: PageFunctionOn, ...args: Boxed) => Promise; -export type $Wait = (selector: string, pageFunction: PageFunctionOn, options?: WaitForFunctionOptions, ...args: Boxed) => Promise>; export type EvaluateOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise; export type EvaluateHandleOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise>; diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 659ff0ff76..c17ec2faf0 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Browser, collectPages } from '../browser'; +import { Browser, createPageInNewContext } from '../browser'; import { BrowserContext, BrowserContextOptions } from '../browserContext'; import { assert, helper, RegisteredListener } from '../helper'; import * as network from '../network'; @@ -66,6 +66,8 @@ export class WKBrowser extends platform.EventEmitter implements Browser { } _onDisconnect() { + for (const context of this.contexts()) + context._browserClosed(); for (const pageProxy of this._pageProxies.values()) pageProxy.dispose(); this._pageProxies.clear(); @@ -87,13 +89,8 @@ export class WKBrowser extends platform.EventEmitter implements Browser { return Array.from(this._contexts.values()); } - async pages(): Promise { - return collectPages(this); - } - async newPage(options?: BrowserContextOptions): Promise { - const context = await this.newContext(options); - return context.newPage(); + return createPageInNewContext(this, options); } async _waitForFirstPageTarget(timeout: number): Promise { diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index a75e7f5ba6..2b8ee55719 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -545,6 +545,17 @@ export class WKPage implements PageDelegate { return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; } + async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise { + await this._session.send('DOM.scrollIntoViewIfNeeded', { + objectId: toRemoteObject(handle).objectId!, + rect, + }).catch(e => { + if (e instanceof Error && e.message.includes('Node does not have a layout object')) + e.message = 'Node is either not visible or not an HTMLElement'; + throw e; + }); + } + async getContentQuads(handle: dom.ElementHandle): Promise { const result = await this._session.send('DOM.getContentQuads', { objectId: toRemoteObject(handle).objectId! diff --git a/test/browser.spec.js b/test/browser.spec.js index 4232dce3a8..f5e032ef8e 100644 --- a/test/browser.spec.js +++ b/test/browser.spec.js @@ -24,24 +24,26 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE const {it, fit, xit, dit} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; - describe('Browser', function() { + describe('Browser.newPage', function() { it('should create new page', async function({browser}) { - expect((await browser.pages()).length).toBe(0); const page1 = await browser.newPage(); - expect((await browser.pages()).length).toBe(1); expect(browser.contexts().length).toBe(1); const page2 = await browser.newPage(); - expect((await browser.pages()).length).toBe(2); expect(browser.contexts().length).toBe(2); - await page1.context().close(); - expect((await browser.pages()).length).toBe(1); + await page1.close(); expect(browser.contexts().length).toBe(1); - await page2.context().close(); - expect((await browser.pages()).length).toBe(0); + await page2.close(); expect(browser.contexts().length).toBe(0); }); + it('should throw upon second create new page', async function({browser}) { + const page = await browser.newPage(); + let error; + await page.context().newPage().catch(e => error = e); + await page.close(); + expect(error.message).toContain('Please use browser.newContext()'); + }); }); }; diff --git a/test/chromium/chromium.spec.js b/test/chromium/chromium.spec.js index 5f106e9e1f..6b169fe382 100644 --- a/test/chromium/chromium.spec.js +++ b/test/chromium/chromium.spec.js @@ -192,7 +192,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 page.close(); + await page.context().close(); }); it('should fire target events', async function({browser, newContext, server}) { const context = await newContext(); diff --git a/test/chromium/tracing.spec.js b/test/chromium/tracing.spec.js index fc9c16309e..d242af429b 100644 --- a/test/chromium/tracing.spec.js +++ b/test/chromium/tracing.spec.js @@ -58,7 +58,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p const newPage = await browser.newPage(); let error = null; await browser.startTracing(newPage, {path: outputFile}).catch(e => error = e); - await newPage.context().close(); + await newPage.close(); expect(error).toBeTruthy(); await browser.stopTracing(); }); diff --git a/test/click.spec.js b/test/click.spec.js index f60f5ac365..6b9e4f7f8c 100644 --- a/test/click.spec.js +++ b/test/click.spec.js @@ -286,6 +286,8 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI expect(await frame.evaluate(() => window.result)).toBe('Clicked'); }); // @see https://github.com/GoogleChrome/puppeteer/issues/4110 + // @see https://bugs.chromium.org/p/chromium/issues/detail?id=986390 + // @see https://chromium-review.googlesource.com/c/chromium/src/+/1742784 xit('should click the button with fixed position inside an iframe', async({page, server}) => { await page.goto(server.EMPTY_PAGE); await page.setViewportSize({width: 500, height: 500}); @@ -326,7 +328,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI expect(await page.evaluate(() => offsetX)).toBe(WEBKIT ? 12 * 2 + 20 : 20); expect(await page.evaluate(() => offsetY)).toBe(WEBKIT ? 12 * 2 + 10 : 10); }); - it('should click a very large button with relative point', async({page, server}) => { + it.skip(FFOX)('should click a very large button with relative point', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); await page.$eval('button', button => button.style.borderWidth = '8px'); await page.$eval('button', button => button.style.height = button.style.width = '2000px'); @@ -336,7 +338,7 @@ 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); }); - xit('should click a button in scrolling container with relative point', async({page, server}) => { + it.skip(FFOX)('should click a button in scrolling container with relative point', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); await page.$eval('button', button => { const container = document.createElement('div'); @@ -347,11 +349,13 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI container.appendChild(button); button.style.height = '2000px'; button.style.width = '2000px'; + button.style.borderWidth = '8px'; }); await page.click('button', { relativePoint: { x: 1900, y: 1910 } }); expect(await page.evaluate(() => window.result)).toBe('Clicked'); - expect(await page.evaluate(() => offsetX)).toBe(1900); - expect(await page.evaluate(() => offsetY)).toBe(1910); + // Safari reports border-relative offsetX/offsetY. + expect(await page.evaluate(() => offsetX)).toBe(WEBKIT ? 1900 + 8 : 1900); + expect(await page.evaluate(() => offsetY)).toBe(WEBKIT ? 1910 + 8 : 1910); }); it('should update modifiers correctly', async({page, server}) => { @@ -370,7 +374,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI await page.click('button'); expect(await page.evaluate(() => shiftKey)).toBe(false); }); - it.skip(CHROMIUM)('should click an offscreen element when scroll-behavior is smooth', async({page}) => { + it('should click an offscreen element when scroll-behavior is smooth', async({page}) => { await page.setContent(`
@@ -379,7 +383,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI await page.click('button'); expect(await page.evaluate('window.clicked')).toBe(true); }); - it.skip(true)('should click on an animated button', async({page}) => { + xit('should click on an animated button', async({page}) => { const buttonSize = 50; const containerWidth = 500; const transition = 500; diff --git a/test/elementhandle.spec.js b/test/elementhandle.spec.js index 65620ba44e..c83d95ad6d 100644 --- a/test/elementhandle.spec.js +++ b/test/elementhandle.spec.js @@ -68,6 +68,24 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) }, element); expect(pwBoundingBox).toEqual(webBoundingBox); }); + it.skip(WEBKIT)('should work with page scale', async({newPage, server}) => { + const page = await newPage({ viewport: { width: 400, height: 400, isMobile: true} }); + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await button.evaluate(button => { + document.body.style.margin = '0'; + button.style.borderWidth = '0'; + button.style.width = '200px'; + button.style.height = '20px'; + button.style.marginLeft = '17px'; + button.style.marginTop = '23px'; + }); + const box = await button.boundingBox(); + expect(Math.round(box.x * 100)).toBe(17 * 100); + 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); + }); }); describe('ElementHandle.contentFrame', function() { @@ -201,9 +219,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) it('should work for TextNodes', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); const buttonTextNode = await page.evaluateHandle(() => document.querySelector('button').firstChild); - let error = null; - await buttonTextNode.click().catch(err => error = err); - expect(error.message).toBe('Node is not of type HTMLElement'); + await buttonTextNode.click(); + expect(await page.evaluate(() => result)).toBe('Clicked'); }); it('should throw for detached nodes', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); @@ -211,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).toBe('Node is detached from document'); + expect(error.message).toContain('Node is detached from document'); }); it('should throw for hidden nodes', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); diff --git a/test/jshandle.spec.js b/test/jshandle.spec.js index 69b72e442d..4506eadd2f 100644 --- a/test/jshandle.spec.js +++ b/test/jshandle.spec.js @@ -160,7 +160,7 @@ module.exports.describe = function({testRunner, expect, CHROMIUM, FFOX, WEBKIT}) const aHandle = await page.evaluateHandle(() => document.querySelector('div').firstChild); const element = aHandle.asElement(); expect(element).toBeTruthy(); - expect(await page.evaluate(e => e.nodeType === HTMLElement.TEXT_NODE, element)); + expect(await page.evaluate(e => e.nodeType === HTMLElement.TEXT_NODE, element)).toBeTruthy(); }); it('should work with nullified Node', async({page, server}) => { await page.setContent('
test
'); diff --git a/test/launcher.spec.js b/test/launcher.spec.js index e9685f6ee7..08e87b7518 100644 --- a/test/launcher.spec.js +++ b/test/launcher.spec.js @@ -172,6 +172,19 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p expect(error.message).toContain('has been closed'); await browserServer.close(); }); + it('should emit close events on pages and contexts', async({server}) => { + const browserServer = await playwright.launchServer({...defaultBrowserOptions }); + const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); + const context = await remote.newContext(); + const page = await context.newPage(); + let pageClosed = false; + page.on('close', e => pageClosed = true); + await Promise.all([ + new Promise(f => context.on('close', f)), + browserServer.close() + ]); + expect(pageClosed).toBeTruthy(); + }); }); describe('Browser.close', function() { diff --git a/test/playwright.spec.js b/test/playwright.spec.js index 18f4920b57..63aedfebc1 100644 --- a/test/playwright.spec.js +++ b/test/playwright.spec.js @@ -118,6 +118,7 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => { state.tearDown = async () => { await Promise.all(contexts.map(c => c.close())); + expect((await state.browser.contexts()).length).toBe(0, `"${test.fullName}" leaked a context`); if (rl) { rl.removeListener('line', onLine); rl.close(); diff --git a/test/waittask.spec.js b/test/waittask.spec.js index 9bb1bead96..0f110840a7 100644 --- a/test/waittask.spec.js +++ b/test/waittask.spec.js @@ -208,39 +208,8 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO }); }); - describe('Frame.$wait', function() { - it('should accept arguments', async({page, server}) => { - await page.setContent('
'); - const result = await page.$wait('div', (e, foo, bar) => e.nodeName + foo + bar, {}, 'foo1', 'bar2'); - expect(await result.jsonValue()).toBe('DIVfoo1bar2'); - }); - it('should query selector constantly', async({page, server}) => { - await page.setContent('
'); - let done = null; - const resultPromise = page.$wait('span', e => e).then(r => done = r); - expect(done).toBe(null); - await page.setContent('
'); - expect(done).toBe(null); - await page.setContent('text'); - await resultPromise; - expect(done).not.toBe(null); - expect(await done.evaluate(e => e.textContent)).toBe('text'); - }); - it('should be able to wait for removal', async({page}) => { - await page.setContent('
'); - let done = null; - const resultPromise = page.$wait('div', e => !e).then(r => done = r); - expect(done).toBe(null); - await page.setContent('
'); - await resultPromise; - expect(done).not.toBe(null); - expect(await done.jsonValue()).toBe(true); - }); - }); - describe('Frame.waitForSelector', function() { const addElement = tag => document.body.appendChild(document.createElement(tag)); - it('should immediately resolve promise if node exists', async({page, server}) => { await page.goto(server.EMPTY_PAGE); const frame = page.mainFrame(); @@ -248,7 +217,6 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO await frame.evaluate(addElement, 'div'); await frame.waitForSelector('div'); }); - it('should work with removed MutationObserver', async({page, server}) => { await page.evaluate(() => delete window.MutationObserver); const [handle] = await Promise.all([ @@ -257,7 +225,6 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO ]); expect(await page.evaluate(x => x.textContent, handle)).toBe('anything'); }); - it('should resolve promise when node is added', async({page, server}) => { await page.goto(server.EMPTY_PAGE); const frame = page.mainFrame(); @@ -268,7 +235,6 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO const tagName = await eHandle.getProperty('tagName').then(e => e.jsonValue()); expect(tagName).toBe('DIV'); }); - it('should work when node is added through innerHTML', async({page, server}) => { await page.goto(server.EMPTY_PAGE); const watchdog = page.waitForSelector('h3 div'); @@ -276,7 +242,6 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO await page.evaluate(() => document.querySelector('span').innerHTML = '

'); await watchdog; }); - it('Page.$ waitFor is shortcut for main frame', async({page, server}) => { await page.goto(server.EMPTY_PAGE); await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); @@ -287,7 +252,6 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO const eHandle = await watchdog; expect(await eHandle.ownerFrame()).toBe(page.mainFrame()); }); - it('should run in specified frame', async({page, server}) => { await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); @@ -299,7 +263,6 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO const eHandle = await waitForSelectorPromise; expect(await eHandle.ownerFrame()).toBe(frame2); }); - it('should throw when frame is detached', async({page, server}) => { await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); const frame = page.frames()[1]; @@ -391,7 +354,6 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO expect(error).toBeTruthy(); expect(error.message).toContain('waiting for selector "[hidden] div" failed: timeout'); }); - it('should respond to node attribute mutation', async({page, server}) => { let divFound = false; const waitForSelector = page.waitForSelector('.zombo').then(() => divFound = true); @@ -441,6 +403,11 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO const tagName = await eHandle.getProperty('tagName').then(e => e.jsonValue()); expect(tagName).toBe('SPAN'); }); + it('$wait alias should work', async({page, server}) => { + await page.setContent('
test
'); + const handle = await page.$wait('section'); + expect(await handle.evaluate(e => e.textContent)).toBe('test'); + }); }); describe('Frame.waitForSelector xpath', function() { diff --git a/test/web.spec.js b/test/web.spec.js index 8e84de2d5e..d18d2b3cb7 100644 --- a/test/web.spec.js +++ b/test/web.spec.js @@ -46,7 +46,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p afterEach(async state => { await state.page.evaluate(() => teardown()); - await state.page.context().close(); + await state.page.close(); state.page = null; });