Skip to content

Commit

Permalink
fix(page): use secondary DOMWorld to drive page.select() (#3809)
Browse files Browse the repository at this point in the history
This patch starts creating secondary DOMWorld for every connected
page and switches `page.select()` to run inside the secondary world.

Fix #3327.
  • Loading branch information
aslushnikov committed Jan 22, 2019
1 parent c09835f commit 678b8e8
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 56 deletions.
29 changes: 7 additions & 22 deletions lib/DOMWorld.js
Expand Up @@ -44,6 +44,13 @@ class DOMWorld {
this._detached = false;
}

/**
* @return {!Puppeteer.Frame}
*/
frame() {
return this._frame;
}

/**
* @param {?Puppeteer.ExecutionContext} context
*/
Expand Down Expand Up @@ -419,28 +426,6 @@ class DOMWorld {
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 {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
Expand Down
9 changes: 4 additions & 5 deletions lib/ExecutionContext.js
Expand Up @@ -24,20 +24,19 @@ class ExecutionContext {
/**
* @param {!Puppeteer.CDPSession} client
* @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._frame = frame;
this._world = world;
this._contextId = contextPayload.id;
this._isDefault = contextPayload.auxData ? !!contextPayload.auxData['isDefault'] : false;
}

/**
* @return {?Puppeteer.Frame}
*/
frame() {
return this._frame;
return this._world ? this._world.frame() : null;
}

/**
Expand Down
86 changes: 59 additions & 27 deletions lib/FrameManager.js
Expand Up @@ -17,10 +17,12 @@
const EventEmitter = require('events');
const {helper, assert} = require('./helper');
const {Events} = require('./Events');
const {ExecutionContext} = require('./ExecutionContext');
const {ExecutionContext, EVALUATION_SCRIPT_URL} = require('./ExecutionContext');
const {LifecycleWatcher} = require('./LifecycleWatcher');
const {DOMWorld} = require('./DOMWorld');

const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';

class FrameManager extends EventEmitter {
/**
* @param {!Puppeteer.CDPSession} client
Expand All @@ -38,6 +40,8 @@ class FrameManager extends EventEmitter {
this._frames = new Map();
/** @type {!Map<number, !ExecutionContext>} */
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.frameNavigated', event => this._onFrameNavigated(event.frame));
Expand All @@ -48,7 +52,6 @@ class FrameManager extends EventEmitter {
this._client.on('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId));
this._client.on('Runtime.executionContextsCleared', event => this._onExecutionContextsCleared());
this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event));

this._handleFrameTree(frameTree);
}

Expand Down Expand Up @@ -244,6 +247,28 @@ class FrameManager extends EventEmitter {
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} url
Expand All @@ -269,11 +294,20 @@ class FrameManager extends EventEmitter {
_onExecutionContextCreated(contextPayload) {
const frameId = contextPayload.auxData ? contextPayload.auxData.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} */
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);
if (frame)
frame._addExecutionContext(context);
}

/**
Expand All @@ -284,14 +318,14 @@ class FrameManager extends EventEmitter {
if (!context)
return;
this._contextIdToContext.delete(executionContextId);
if (context.frame())
context.frame()._removeExecutionContext(context);
if (context._world)
context._world._setContext(null);
}

_onExecutionContextsCleared() {
for (const context of this._contextIdToContext.values()) {
if (context.frame())
context.frame()._removeExecutionContext(context);
if (context._world)
context._world._setContext(null);
}
this._contextIdToContext.clear();
}
Expand Down Expand Up @@ -340,29 +374,14 @@ class Frame {
/** @type {!Set<string>} */
this._lifecycleEvents = new Set();
this._mainWorld = new DOMWorld(frameManager, this);
this._secondaryWorld = new DOMWorld(frameManager, this);

/** @type {!Set<!Frame>} */
this._childFrames = new Set();
if (this._parentFrame)
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 {!{referer?: string, timeout?: number, waitUntil?: string|!Array<string>}=} options
Expand Down Expand Up @@ -543,7 +562,7 @@ class Frame {
* @return {!Promise<!Array<string>>}
*/
select(selector, ...values){
return this._mainWorld.select(selector, ...values);
return this._secondaryWorld.select(selector, ...values);
}

/**
Expand All @@ -569,7 +588,19 @@ class Frame {
* @return {!Promise<!Puppeteer.JSHandle>}
*/
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)));
}

/**
Expand Down Expand Up @@ -643,6 +674,7 @@ class Frame {
_detach() {
this._detached = true;
this._mainWorld._detach();
this._secondaryWorld._detach();
if (this._parentFrame)
this._parentFrame._childFrames.delete(this);
this._parentFrame = null;
Expand Down
2 changes: 1 addition & 1 deletion lib/Page.js
Expand Up @@ -50,7 +50,7 @@ class Page extends EventEmitter {
client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false}),
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
client.send('Network.enable', {}),
client.send('Runtime.enable', {}),
client.send('Runtime.enable', {}).then(() => page._frameManager.ensureSecondaryDOMWorld()),
client.send('Security.enable', {}),
client.send('Performance.enable', {}),
client.send('Log.enable', {}),
Expand Down
2 changes: 2 additions & 0 deletions lib/externs.d.ts
Expand Up @@ -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 {Frame as RealFrame, FrameManager as RealFrameManager} from './FrameManager.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 { NetworkManager as RealNetworkManager, Request as RealRequest, Response as RealResponse } from './NetworkManager.js';
import * as child_process from 'child_process';
Expand All @@ -28,6 +29,7 @@ declare global {
export class NetworkManager extends RealNetworkManager {}
export class ElementHandle extends RealElementHandle {}
export class JSHandle extends RealJSHandle {}
export class DOMWorld extends RealDOMWorld {}
export class ExecutionContext extends RealExecutionContext {}
export class Page extends RealPage { }
export class Response extends RealResponse { }
Expand Down
2 changes: 1 addition & 1 deletion test/page.spec.js
Expand Up @@ -956,7 +956,7 @@ module.exports.addTests = function({testRunner, expect, headless}) {
expect(error.message).toContain('Values must be strings');
});
// @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.evaluate(() => window.Event = null);
await page.select('select', 'blue');
Expand Down

0 comments on commit 678b8e8

Please sign in to comment.