fix(page): use secondary DOMWorld to drive page.select() (#3809)
This patch starts creating secondary DOMWorld for every connected page and switches `page.select()` to run inside the secondary world. Fix #3327.
This commit is contained in:
parent
c09835fd70
commit
678b8e85ad
@ -44,6 +44,13 @@ class DOMWorld {
|
|||||||
this._detached = false;
|
this._detached = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {!Puppeteer.Frame}
|
||||||
|
*/
|
||||||
|
frame() {
|
||||||
|
return this._frame;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {?Puppeteer.ExecutionContext} context
|
* @param {?Puppeteer.ExecutionContext} context
|
||||||
*/
|
*/
|
||||||
@ -419,28 +426,6 @@ class DOMWorld {
|
|||||||
await handle.dispose();
|
await handle.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {(string|number|Function)} selectorOrFunctionOrTimeout
|
|
||||||
* @param {!Object=} options
|
|
||||||
* @param {!Array<*>} args
|
|
||||||
* @return {!Promise<!Puppeteer.JSHandle>}
|
|
||||||
*/
|
|
||||||
waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
|
|
||||||
const xPathPattern = '//';
|
|
||||||
|
|
||||||
if (helper.isString(selectorOrFunctionOrTimeout)) {
|
|
||||||
const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
|
|
||||||
if (string.startsWith(xPathPattern))
|
|
||||||
return this.waitForXPath(string, options);
|
|
||||||
return this.waitForSelector(string, options);
|
|
||||||
}
|
|
||||||
if (helper.isNumber(selectorOrFunctionOrTimeout))
|
|
||||||
return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout)));
|
|
||||||
if (typeof selectorOrFunctionOrTimeout === 'function')
|
|
||||||
return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
|
|
||||||
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} selector
|
* @param {string} selector
|
||||||
* @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
|
* @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
|
||||||
|
@ -24,20 +24,19 @@ class ExecutionContext {
|
|||||||
/**
|
/**
|
||||||
* @param {!Puppeteer.CDPSession} client
|
* @param {!Puppeteer.CDPSession} client
|
||||||
* @param {!Protocol.Runtime.ExecutionContextDescription} contextPayload
|
* @param {!Protocol.Runtime.ExecutionContextDescription} contextPayload
|
||||||
* @param {?Puppeteer.Frame} frame
|
* @param {?Puppeteer.DOMWorld} world
|
||||||
*/
|
*/
|
||||||
constructor(client, contextPayload, frame) {
|
constructor(client, contextPayload, world) {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
this._frame = frame;
|
this._world = world;
|
||||||
this._contextId = contextPayload.id;
|
this._contextId = contextPayload.id;
|
||||||
this._isDefault = contextPayload.auxData ? !!contextPayload.auxData['isDefault'] : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {?Puppeteer.Frame}
|
* @return {?Puppeteer.Frame}
|
||||||
*/
|
*/
|
||||||
frame() {
|
frame() {
|
||||||
return this._frame;
|
return this._world ? this._world.frame() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,10 +17,12 @@
|
|||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const {helper, assert} = require('./helper');
|
const {helper, assert} = require('./helper');
|
||||||
const {Events} = require('./Events');
|
const {Events} = require('./Events');
|
||||||
const {ExecutionContext} = require('./ExecutionContext');
|
const {ExecutionContext, EVALUATION_SCRIPT_URL} = require('./ExecutionContext');
|
||||||
const {LifecycleWatcher} = require('./LifecycleWatcher');
|
const {LifecycleWatcher} = require('./LifecycleWatcher');
|
||||||
const {DOMWorld} = require('./DOMWorld');
|
const {DOMWorld} = require('./DOMWorld');
|
||||||
|
|
||||||
|
const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';
|
||||||
|
|
||||||
class FrameManager extends EventEmitter {
|
class FrameManager extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* @param {!Puppeteer.CDPSession} client
|
* @param {!Puppeteer.CDPSession} client
|
||||||
@ -38,6 +40,8 @@ class FrameManager extends EventEmitter {
|
|||||||
this._frames = new Map();
|
this._frames = new Map();
|
||||||
/** @type {!Map<number, !ExecutionContext>} */
|
/** @type {!Map<number, !ExecutionContext>} */
|
||||||
this._contextIdToContext = new Map();
|
this._contextIdToContext = new Map();
|
||||||
|
/** @type {!Set<string>} */
|
||||||
|
this._isolatedWorlds = new Set();
|
||||||
|
|
||||||
this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId));
|
this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId));
|
||||||
this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame));
|
this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame));
|
||||||
@ -48,7 +52,6 @@ class FrameManager extends EventEmitter {
|
|||||||
this._client.on('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId));
|
this._client.on('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId));
|
||||||
this._client.on('Runtime.executionContextsCleared', event => this._onExecutionContextsCleared());
|
this._client.on('Runtime.executionContextsCleared', event => this._onExecutionContextsCleared());
|
||||||
this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event));
|
this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event));
|
||||||
|
|
||||||
this._handleFrameTree(frameTree);
|
this._handleFrameTree(frameTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,6 +247,28 @@ class FrameManager extends EventEmitter {
|
|||||||
this.emit(Events.FrameManager.FrameNavigated, frame);
|
this.emit(Events.FrameManager.FrameNavigated, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async ensureSecondaryDOMWorld() {
|
||||||
|
await this._ensureIsolatedWorld(UTILITY_WORLD_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
async _ensureIsolatedWorld(name) {
|
||||||
|
if (this._isolatedWorlds.has(name))
|
||||||
|
return;
|
||||||
|
this._isolatedWorlds.add(name);
|
||||||
|
await this._client.send('Page.addScriptToEvaluateOnNewDocument', {
|
||||||
|
source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`,
|
||||||
|
worldName: name,
|
||||||
|
}),
|
||||||
|
await Promise.all(this.frames().map(frame => this._client.send('Page.createIsolatedWorld', {
|
||||||
|
frameId: frame._id,
|
||||||
|
grantUniveralAccess: true,
|
||||||
|
worldName: name,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} frameId
|
* @param {string} frameId
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
@ -269,11 +294,20 @@ class FrameManager extends EventEmitter {
|
|||||||
_onExecutionContextCreated(contextPayload) {
|
_onExecutionContextCreated(contextPayload) {
|
||||||
const frameId = contextPayload.auxData ? contextPayload.auxData.frameId : null;
|
const frameId = contextPayload.auxData ? contextPayload.auxData.frameId : null;
|
||||||
const frame = this._frames.get(frameId) || null;
|
const frame = this._frames.get(frameId) || null;
|
||||||
|
let world = null;
|
||||||
|
if (frame) {
|
||||||
|
if (contextPayload.auxData && !!contextPayload.auxData['isDefault'])
|
||||||
|
world = frame._mainWorld;
|
||||||
|
else if (contextPayload.name === UTILITY_WORLD_NAME)
|
||||||
|
world = frame._secondaryWorld;
|
||||||
|
}
|
||||||
|
if (contextPayload.auxData && contextPayload.auxData['type'] === 'isolated')
|
||||||
|
this._isolatedWorlds.add(contextPayload.name);
|
||||||
/** @type {!ExecutionContext} */
|
/** @type {!ExecutionContext} */
|
||||||
const context = new ExecutionContext(this._client, contextPayload, frame);
|
const context = new ExecutionContext(this._client, contextPayload, world);
|
||||||
|
if (world)
|
||||||
|
world._setContext(context);
|
||||||
this._contextIdToContext.set(contextPayload.id, context);
|
this._contextIdToContext.set(contextPayload.id, context);
|
||||||
if (frame)
|
|
||||||
frame._addExecutionContext(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -284,14 +318,14 @@ class FrameManager extends EventEmitter {
|
|||||||
if (!context)
|
if (!context)
|
||||||
return;
|
return;
|
||||||
this._contextIdToContext.delete(executionContextId);
|
this._contextIdToContext.delete(executionContextId);
|
||||||
if (context.frame())
|
if (context._world)
|
||||||
context.frame()._removeExecutionContext(context);
|
context._world._setContext(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onExecutionContextsCleared() {
|
_onExecutionContextsCleared() {
|
||||||
for (const context of this._contextIdToContext.values()) {
|
for (const context of this._contextIdToContext.values()) {
|
||||||
if (context.frame())
|
if (context._world)
|
||||||
context.frame()._removeExecutionContext(context);
|
context._world._setContext(null);
|
||||||
}
|
}
|
||||||
this._contextIdToContext.clear();
|
this._contextIdToContext.clear();
|
||||||
}
|
}
|
||||||
@ -340,6 +374,7 @@ class Frame {
|
|||||||
/** @type {!Set<string>} */
|
/** @type {!Set<string>} */
|
||||||
this._lifecycleEvents = new Set();
|
this._lifecycleEvents = new Set();
|
||||||
this._mainWorld = new DOMWorld(frameManager, this);
|
this._mainWorld = new DOMWorld(frameManager, this);
|
||||||
|
this._secondaryWorld = new DOMWorld(frameManager, this);
|
||||||
|
|
||||||
/** @type {!Set<!Frame>} */
|
/** @type {!Set<!Frame>} */
|
||||||
this._childFrames = new Set();
|
this._childFrames = new Set();
|
||||||
@ -347,22 +382,6 @@ class Frame {
|
|||||||
this._parentFrame._childFrames.add(this);
|
this._parentFrame._childFrames.add(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {!ExecutionContext} context
|
|
||||||
*/
|
|
||||||
_addExecutionContext(context) {
|
|
||||||
if (context._isDefault)
|
|
||||||
this._mainWorld._setContext(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {!ExecutionContext} context
|
|
||||||
*/
|
|
||||||
_removeExecutionContext(context) {
|
|
||||||
if (context._isDefault)
|
|
||||||
this._mainWorld._setContext(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
* @param {!{referer?: string, timeout?: number, waitUntil?: string|!Array<string>}=} options
|
* @param {!{referer?: string, timeout?: number, waitUntil?: string|!Array<string>}=} options
|
||||||
@ -543,7 +562,7 @@ class Frame {
|
|||||||
* @return {!Promise<!Array<string>>}
|
* @return {!Promise<!Array<string>>}
|
||||||
*/
|
*/
|
||||||
select(selector, ...values){
|
select(selector, ...values){
|
||||||
return this._mainWorld.select(selector, ...values);
|
return this._secondaryWorld.select(selector, ...values);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -569,7 +588,19 @@ class Frame {
|
|||||||
* @return {!Promise<!Puppeteer.JSHandle>}
|
* @return {!Promise<!Puppeteer.JSHandle>}
|
||||||
*/
|
*/
|
||||||
waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
|
waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
|
||||||
return this._mainWorld.waitFor(selectorOrFunctionOrTimeout, options, ...args);
|
const xPathPattern = '//';
|
||||||
|
|
||||||
|
if (helper.isString(selectorOrFunctionOrTimeout)) {
|
||||||
|
const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
|
||||||
|
if (string.startsWith(xPathPattern))
|
||||||
|
return this.waitForXPath(string, options);
|
||||||
|
return this.waitForSelector(string, options);
|
||||||
|
}
|
||||||
|
if (helper.isNumber(selectorOrFunctionOrTimeout))
|
||||||
|
return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout)));
|
||||||
|
if (typeof selectorOrFunctionOrTimeout === 'function')
|
||||||
|
return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
|
||||||
|
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -643,6 +674,7 @@ class Frame {
|
|||||||
_detach() {
|
_detach() {
|
||||||
this._detached = true;
|
this._detached = true;
|
||||||
this._mainWorld._detach();
|
this._mainWorld._detach();
|
||||||
|
this._secondaryWorld._detach();
|
||||||
if (this._parentFrame)
|
if (this._parentFrame)
|
||||||
this._parentFrame._childFrames.delete(this);
|
this._parentFrame._childFrames.delete(this);
|
||||||
this._parentFrame = null;
|
this._parentFrame = null;
|
||||||
|
@ -50,7 +50,7 @@ class Page extends EventEmitter {
|
|||||||
client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false}),
|
client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false}),
|
||||||
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
|
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
|
||||||
client.send('Network.enable', {}),
|
client.send('Network.enable', {}),
|
||||||
client.send('Runtime.enable', {}),
|
client.send('Runtime.enable', {}).then(() => page._frameManager.ensureSecondaryDOMWorld()),
|
||||||
client.send('Security.enable', {}),
|
client.send('Security.enable', {}),
|
||||||
client.send('Performance.enable', {}),
|
client.send('Performance.enable', {}),
|
||||||
client.send('Log.enable', {}),
|
client.send('Log.enable', {}),
|
||||||
|
2
lib/externs.d.ts
vendored
2
lib/externs.d.ts
vendored
@ -6,6 +6,7 @@ import {TaskQueue as RealTaskQueue} from './TaskQueue.js';
|
|||||||
import {Mouse as RealMouse, Keyboard as RealKeyboard, Touchscreen as RealTouchscreen} from './Input.js';
|
import {Mouse as RealMouse, Keyboard as RealKeyboard, Touchscreen as RealTouchscreen} from './Input.js';
|
||||||
import {Frame as RealFrame, FrameManager as RealFrameManager} from './FrameManager.js';
|
import {Frame as RealFrame, FrameManager as RealFrameManager} from './FrameManager.js';
|
||||||
import {JSHandle as RealJSHandle, ElementHandle as RealElementHandle} from './JSHandle.js';
|
import {JSHandle as RealJSHandle, ElementHandle as RealElementHandle} from './JSHandle.js';
|
||||||
|
import {DOMWorld as RealDOMWorld} from './DOMWorld.js';
|
||||||
import {ExecutionContext as RealExecutionContext} from './ExecutionContext.js';
|
import {ExecutionContext as RealExecutionContext} from './ExecutionContext.js';
|
||||||
import { NetworkManager as RealNetworkManager, Request as RealRequest, Response as RealResponse } from './NetworkManager.js';
|
import { NetworkManager as RealNetworkManager, Request as RealRequest, Response as RealResponse } from './NetworkManager.js';
|
||||||
import * as child_process from 'child_process';
|
import * as child_process from 'child_process';
|
||||||
@ -28,6 +29,7 @@ declare global {
|
|||||||
export class NetworkManager extends RealNetworkManager {}
|
export class NetworkManager extends RealNetworkManager {}
|
||||||
export class ElementHandle extends RealElementHandle {}
|
export class ElementHandle extends RealElementHandle {}
|
||||||
export class JSHandle extends RealJSHandle {}
|
export class JSHandle extends RealJSHandle {}
|
||||||
|
export class DOMWorld extends RealDOMWorld {}
|
||||||
export class ExecutionContext extends RealExecutionContext {}
|
export class ExecutionContext extends RealExecutionContext {}
|
||||||
export class Page extends RealPage { }
|
export class Page extends RealPage { }
|
||||||
export class Response extends RealResponse { }
|
export class Response extends RealResponse { }
|
||||||
|
@ -956,7 +956,7 @@ module.exports.addTests = function({testRunner, expect, headless}) {
|
|||||||
expect(error.message).toContain('Values must be strings');
|
expect(error.message).toContain('Values must be strings');
|
||||||
});
|
});
|
||||||
// @see https://github.com/GoogleChrome/puppeteer/issues/3327
|
// @see https://github.com/GoogleChrome/puppeteer/issues/3327
|
||||||
xit('should work when re-defining top-level Event class', async({page, server}) => {
|
it('should work when re-defining top-level Event class', async({page, server}) => {
|
||||||
await page.goto(server.PREFIX + '/input/select.html');
|
await page.goto(server.PREFIX + '/input/select.html');
|
||||||
await page.evaluate(() => window.Event = null);
|
await page.evaluate(() => window.Event = null);
|
||||||
await page.select('select', 'blue');
|
await page.select('select', 'blue');
|
||||||
|
Loading…
Reference in New Issue
Block a user