Merge branch 'master' into ff-use-attach-to-bc

This commit is contained in:
Andrey Lushnikov 2020-02-11 18:43:25 -08:00 committed by GitHub
commit 61bc21dd7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 952 additions and 506 deletions

View file

@ -1 +1 @@
1025
1028

View file

@ -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?"

View file

@ -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

View file

@ -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);
});

View file

@ -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}) {

View file

@ -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);

View file

@ -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;

View file

@ -1 +1 @@
1141
1143

View file

@ -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<WebCore::Credential>());
+}
+
+void WebPageInspectorEmulationAgent::setActiveAndFocused(Inspector::ErrorString&, const bool* active)
+{
+ Optional<bool> 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<SetDeviceMetricsOverrideCallback>&&) 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<void (const String& error)>&&);
@ -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<API::Navigation> 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<WebCore::Credential>&& credentials) {
+void WebPageProxy::setAuthCredentialsForAutomation(Optional<WebCore::Credential>&& credentials)
+{
+ m_credentialsForAutomation = WTFMove(credentials);
+}
+
+void WebPageProxy::setPermissionsForAutomation(const HashMap<String, HashSet<String>>& permissions) {
+void WebPageProxy::setPermissionsForAutomation(const HashMap<String, HashSet<String>>& permissions)
+{
+ m_permissionsForAutomation = permissions;
+}
+
+void WebPageProxy::setActiveForAutomation(Optional<bool> active) {
+ m_activeForAutomation = active;
+ OptionSet<ActivityState::Flag> 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<ActivityState::Flag> 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<WebsitePoliciesData>&& websitePolicies, Ref<PolicyDecisionSender>&& 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<WebProcessProxy>&& p
@@ -4449,6 +4511,8 @@ void WebPageProxy::didFailProvisionalLoadForFrameShared(Ref<WebProcessProxy>&& 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<WebPageProxyIdentifier> 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<WebProcessProxy>&& 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<WebPageProxy> 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<GamepadData>& gamepadDatas, bool
@@ -7712,6 +7794,14 @@ void WebPageProxy::gamepadActivity(const Vector<GamepadData>& gamepadDatas, bool
void WebPageProxy::didReceiveAuthenticationChallengeProxy(Ref<AuthenticationChallengeProxy>&& 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<void(bool)> 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<PageLoadState::Observer>&&);
+ void setAuthCredentialsForAutomation(Optional<WebCore::Credential>&&);
+ void setPermissionsForAutomation(const HashMap<String, HashSet<String>>&);
+ void setActiveForAutomation(Optional<bool> active);
+ void logToStderr(const String& str);
+
void initializeWebPage();
void setDrawingArea(std::unique_ptr<DrawingAreaProxy>&&);
@@ -580,6 +587,7 @@ public:
@@ -580,6 +588,7 @@ public:
void closePage();
void addPlatformLoadParameters(LoadParameters&);
@ -9507,7 +9562,7 @@ index b3856a4f9e7859bf3e0b9957c95040a784e9b317..5d51d3e9e03b375a2cbd01f909e9b27c
RefPtr<API::Navigation> loadRequest(WebCore::ResourceRequest&&, WebCore::ShouldOpenExternalURLsPolicy = WebCore::ShouldOpenExternalURLsPolicy::ShouldAllowExternalSchemes, API::Object* userData = nullptr);
RefPtr<API::Navigation> loadFile(const String& fileURL, const String& resourceDirectoryURL, API::Object* userData = nullptr);
RefPtr<API::Navigation> 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<WebInspectorProxy> m_inspector;
@ -9515,12 +9570,13 @@ index b3856a4f9e7859bf3e0b9957c95040a784e9b317..5d51d3e9e03b375a2cbd01f909e9b27c
#if ENABLE(FULLSCREEN_API)
std::unique_ptr<WebFullScreenManagerProxy> m_fullScreenManager;
@@ -2707,6 +2716,8 @@ private:
@@ -2707,6 +2717,9 @@ private:
bool m_isLayerTreeFrozenDueToSwipeAnimation { false };
String m_overriddenMediaType;
+ Optional<WebCore::Credential> m_credentialsForAutomation;
+ HashMap<String, HashSet<String>> m_permissionsForAutomation;
+ Optional<bool> m_activeForAutomation;
#if PLATFORM(IOS_FAMILY) && ENABLE(DEVICE_ORIENTATION)
std::unique_ptr<WebDeviceOrientationUpdateProviderProxy> 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

View file

@ -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)
<!-- GEN:stop -->
#### 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();
```
<!-- GEN:toc -->
- [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)
<!-- GEN:stop -->
#### 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);
```
<!-- GEN:toc -->
- [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]<?[ElementHandle]>> 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]<?[ElementHandle]>> 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.
<!-- GEN:toc -->
- [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
<!-- GEN:toc -->
- [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'.
<!-- GEN:toc -->
- [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)
<!-- GEN:stop -->
#### 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)
<!-- GEN:stop -->
### 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)
<!-- GEN:stop -->
### Working with selectors

View file

@ -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;
}

View file

@ -8,7 +8,7 @@
},
"main": "index.js",
"playwright": {
"chromium_revision": "739261",
"chromium_revision": "740289",
"firefox_revision": "1028",
"webkit_revision": "1141"
},

View file

@ -21,11 +21,9 @@ import { Page } from './page';
export interface Browser extends platform.EventEmitterType {
newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
contexts(): BrowserContext[];
pages(): Promise<Page[]>;
newPage(options?: BrowserContextOptions): Promise<Page>;
isConnected(): boolean;
close(): Promise<void>;
_defaultContext: BrowserContext | undefined;
}
export type ConnectOptions = {
@ -33,14 +31,11 @@ export type ConnectOptions = {
wsEndpoint: string
};
export async function collectPages(browser: Browser): Promise<Page[]> {
const result: Promise<Page[]>[] = [];
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<Page> {
const context = await browser.newContext(options);
const page = await context.newPage();
page._ownedContext = context;
return page;
}
export type LaunchType = 'local' | 'server' | 'persistent';

View file

@ -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<Page[]>;
@ -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<Page> {
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 {

View file

@ -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<Page[]> {
return collectPages(this);
}
async newPage(options?: BrowserContextOptions): Promise<Page> {
const context = await this.newContext(options);
return context.newPage();
return createPageInNewContext(this, options);
}
async _targetCreated(event: Protocol.Target.targetCreatedPayload) {

View file

@ -484,6 +484,17 @@ export class CRPage implements PageDelegate {
return {x, y, width, height};
}
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<void> {
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<types.Quad[] | null> {
const result = await this._client.send('DOM.getContentQuads', {
objectId: toRemoteObject(handle).objectId

View file

@ -160,58 +160,12 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
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<void> {
await this._page._delegate.scrollRectIntoViewIfNeeded(this, rect);
}
private async _ensurePointerActionPoint(relativePoint?: types.Point): Promise<types.Point> {
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<types.Point> {
@ -253,7 +207,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return result;
}
private async _viewportPointAndScroll(relativePoint: types.Point): Promise<{point: types.Point, scrollX: number, scrollY: number}> {
private async _relativePoint(relativePoint: types.Point): Promise<types.Point> {
const [box, border] = await Promise.all([
this.boundingBox(),
this._evaluateInUtility((node: Node) => {
@ -273,23 +227,13 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
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<void>, options?: input.PointerActionOptions): Promise<void> {
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);

View file

@ -20,6 +20,10 @@ export const Events = {
Disconnected: 'disconnected'
},
BrowserContext: {
Close: 'close'
},
BrowserServer: {
Close: 'close',
},

View file

@ -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<Page[]> {
return collectPages(this);
}
async newPage(options?: BrowserContextOptions): Promise<Page> {
const context = await this.newContext(options);
return context.newPage();
return createPageInNewContext(this, options);
}
async _waitForTarget(predicate: (target: Target) => boolean, options: { timeout?: number; } = {}): Promise<Target> {

View file

@ -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<void> {
await this._session.send('Page.scrollIntoViewIfNeeded', {
frameId: handle._context.frame._id,
objectId: toRemoteObject(handle).objectId!,
rect,
});
}
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._session.send('Page.getContentQuads', {
frameId: handle._context.frame._id,

View file

@ -620,6 +620,10 @@ export class Frame {
return handle;
}
async $wait(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise<dom.ElementHandle<Element> | 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<string> {
const context = await this._utilityContext();
return context.evaluate(() => document.title);

View file

@ -69,6 +69,7 @@ export interface PageDelegate {
setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void>;
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle>;
scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<void>;
getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>;
pdf?: (options?: types.PDFOptions) => Promise<platform.BufferType>;
@ -113,6 +114,7 @@ export class Page extends platform.EventEmitter {
readonly pdf: ((options?: types.PDFOptions) => Promise<platform.BufferType>) | 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<dom.ElementHandle<Element> | 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()];
}

View file

@ -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<BrowserServer>;
launchPersistent(userDataDir: string, options?: LaunchOptions): Promise<BrowserContext>;
connect(options: ConnectOptions): Promise<Browser>;
downloadBrowserIfNeeded(progress?: OnProgressCallback): Promise<void>;
devices: types.Devices;
errors: { TimeoutError: typeof TimeoutError };
}

View file

@ -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',

View file

@ -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';
}

View file

@ -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<WKBrowser> {
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 };

View file

@ -27,7 +27,6 @@ export type Evaluate = <Args extends any[], R>(pageFunction: PageFunction<Args,
export type EvaluateHandle = <Args extends any[], R>(pageFunction: PageFunction<Args, R>, ...args: Boxed<Args>) => Promise<Handle<R>>;
export type $Eval = <Args extends any[], R>(selector: string, pageFunction: PageFunctionOn<Element, Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type $$Eval = <Args extends any[], R>(selector: string, pageFunction: PageFunctionOn<Element[], Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type $Wait = <Args extends any[], R>(selector: string, pageFunction: PageFunctionOn<Element | undefined, Args, R>, options?: WaitForFunctionOptions, ...args: Boxed<Args>) => Promise<Handle<R>>;
export type EvaluateOn<T> = <Args extends any[], R>(pageFunction: PageFunctionOn<T, Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type EvaluateHandleOn<T> = <Args extends any[], R>(pageFunction: PageFunctionOn<T, Args, R>, ...args: Boxed<Args>) => Promise<Handle<R>>;

View file

@ -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<Page[]> {
return collectPages(this);
}
async newPage(options?: BrowserContextOptions): Promise<Page> {
const context = await this.newContext(options);
return context.newPage();
return createPageInNewContext(this, options);
}
async _waitForFirstPageTarget(timeout: number): Promise<void> {

View file

@ -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<void> {
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<types.Quad[] | null> {
const result = await this._session.send('DOM.getContentQuads', {
objectId: toRemoteObject(handle).objectId!

View file

@ -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()');
});
});
};

View file

@ -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();

View file

@ -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();
});

View file

@ -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(`
<div style="border: 1px solid black; height: 500px; overflow: auto; width: 500px; scroll-behavior: smooth">
<button style="margin-top: 2000px" onClick="window.clicked = true">hi</button>
@ -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;

View file

@ -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');

View file

@ -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('<section>test</section>');

View file

@ -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() {

View file

@ -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();

View file

@ -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('<div></div>');
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('<div></div>');
let done = null;
const resultPromise = page.$wait('span', e => e).then(r => done = r);
expect(done).toBe(null);
await page.setContent('<section></section>');
expect(done).toBe(null);
await page.setContent('<span>text</span>');
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('<div></div>');
let done = null;
const resultPromise = page.$wait('div', e => !e).then(r => done = r);
expect(done).toBe(null);
await page.setContent('<section></section>');
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 = '<h3><div></div></h3>');
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('<section>test</section>');
const handle = await page.$wait('section');
expect(await handle.evaluate(e => e.textContent)).toBe('test');
});
});
describe('Frame.waitForSelector xpath', function() {

View file

@ -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;
});