refactor(firefox): migrate onto ExecutionContext events (#4064)

Juggler now has Runtime domain that emits Execution Context events
"ExecutionContextCreated" and "ExecutionContextDestroyed".
This commit is contained in:
Andrey Lushnikov 2019-02-24 23:07:24 -08:00 committed by GitHub
parent 56dafd7424
commit 4ecbd91e4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 81 additions and 23 deletions

View File

@ -15,10 +15,10 @@ class ExecutionContext {
async evaluateHandle(pageFunction, ...args) { async evaluateHandle(pageFunction, ...args) {
if (helper.isString(pageFunction)) { if (helper.isString(pageFunction)) {
const payload = await this._session.send('Page.evaluate', { const payload = await this._session.send('Runtime.evaluate', {
script: pageFunction, expression: pageFunction,
executionContextId: this._executionContextId, executionContextId: this._executionContextId,
}); }).catch(rewriteError);
return createHandle(this, payload.result, payload.exceptionDetails); return createHandle(this, payload.result, payload.exceptionDetails);
} }
if (typeof pageFunction !== 'function') if (typeof pageFunction !== 'function')
@ -59,10 +59,10 @@ class ExecutionContext {
return {unserializableValue: 'NaN'}; return {unserializableValue: 'NaN'};
return {value: arg}; return {value: arg};
}); });
let payload = null; let callFunctionPromise;
try { try {
payload = await this._session.send('Page.evaluate', { callFunctionPromise = this._session.send('Runtime.callFunction', {
functionText, functionDeclaration: functionText,
args, args,
executionContextId: this._executionContextId executionContextId: this._executionContextId
}); });
@ -71,7 +71,14 @@ class ExecutionContext {
err.message += ' Are you passing a nested JSHandle?'; err.message += ' Are you passing a nested JSHandle?';
throw err; throw err;
} }
const payload = await callFunctionPromise.catch(rewriteError);
return createHandle(this, payload.result, payload.exceptionDetails); return createHandle(this, payload.result, payload.exceptionDetails);
function rewriteError(error) {
if (error.message.includes('Failed to find execution context with id'))
throw new Error('Execution context was destroyed, most likely because of a navigation.');
throw error;
}
} }
frame() { frame() {

View File

@ -22,15 +22,41 @@ class FrameManager extends EventEmitter {
this._timeoutSettings = timeoutSettings; this._timeoutSettings = timeoutSettings;
this._mainFrame = null; this._mainFrame = null;
this._frames = new Map(); this._frames = new Map();
/** @type {!Map<string, !ExecutionContext>} */
this._contextIdToContext = new Map();
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)), helper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)),
helper.addEventListener(this._session, 'Page.frameAttached', this._onFrameAttached.bind(this)), helper.addEventListener(this._session, 'Page.frameAttached', this._onFrameAttached.bind(this)),
helper.addEventListener(this._session, 'Page.frameDetached', this._onFrameDetached.bind(this)), helper.addEventListener(this._session, 'Page.frameDetached', this._onFrameDetached.bind(this)),
helper.addEventListener(this._session, 'Page.navigationCommitted', this._onNavigationCommitted.bind(this)), helper.addEventListener(this._session, 'Page.navigationCommitted', this._onNavigationCommitted.bind(this)),
helper.addEventListener(this._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)), helper.addEventListener(this._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)),
helper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)),
helper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)),
]; ];
} }
executionContextById(executionContextId) {
return this._contextIdToContext.get(executionContextId) || null;
}
_onExecutionContextCreated({executionContextId, auxData}) {
const frameId = auxData ? auxData.frameId : null;
const frame = this._frames.get(frameId) || null;
const context = new ExecutionContext(this._session, frame, executionContextId);
if (frame)
frame._setContext(context);
this._contextIdToContext.set(executionContextId, context);
}
_onExecutionContextDestroyed({executionContextId}) {
const context = this._contextIdToContext.get(executionContextId);
if (!context)
return;
this._contextIdToContext.delete(executionContextId);
if (context._frame)
context._frame._setContext(null);
}
frame(frameId) { frame(frameId) {
return this._frames.get(frameId); return this._frames.get(frameId);
} }
@ -122,19 +148,37 @@ class Frame {
this._name = ''; this._name = '';
/** @type {!Set<!Frame>} */ /** @type {!Set<!Frame>} */
this._children = new Set(); this._children = new Set();
this._isDetached = false; this._detached = false;
this._firedEvents = new Set(); this._firedEvents = new Set();
/** @type {!Set<!WaitTask>} */ /** @type {!Set<!WaitTask>} */
this._waitTasks = new Set(); this._waitTasks = new Set();
this._documentPromise = null; this._documentPromise = null;
this._contextPromise;
this._contextResolveCallback = null;
this._setContext(null);
}
this._executionContext = new ExecutionContext(this._session, this, this._frameId); _setContext(context) {
if (context) {
this._contextResolveCallback.call(null, context);
this._contextResolveCallback = null;
for (const waitTask of this._waitTasks)
waitTask.rerun();
} else {
this._documentPromise = null;
this._contextPromise = new Promise(fulfill => {
this._contextResolveCallback = fulfill;
});
}
} }
async executionContext() { async executionContext() {
return this._executionContext; if (this._detached)
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
return this._contextPromise;
} }
/** /**
@ -267,7 +311,7 @@ class Frame {
_detach() { _detach() {
this._parentFrame._children.delete(this); this._parentFrame._children.delete(this);
this._parentFrame = null; this._parentFrame = null;
this._isDetached = true; this._detached = true;
for (const waitTask of this._waitTasks) for (const waitTask of this._waitTasks)
waitTask.terminate(new Error('waitForFunction failed: frame got detached.')); waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
} }
@ -438,7 +482,8 @@ class Frame {
} }
async evaluate(pageFunction, ...args) { async evaluate(pageFunction, ...args) {
return this._executionContext.evaluate(pageFunction, ...args); const context = await this.executionContext();
return context.evaluate(pageFunction, ...args);
} }
_document() { _document() {
@ -497,7 +542,8 @@ class Frame {
} }
async evaluateHandle(pageFunction, ...args) { async evaluateHandle(pageFunction, ...args) {
return this._executionContext.evaluateHandle(pageFunction, ...args); const context = await this.executionContext();
return context.evaluateHandle(pageFunction, ...args);
} }
/** /**
@ -636,7 +682,7 @@ class Frame {
} }
isDetached() { isDetached() {
return this._isDetached; return this._detached;
} }
childFrames() { childFrames() {

View File

@ -59,7 +59,7 @@ class JSHandle {
* @return {!Promise<Map<string, !JSHandle>>} * @return {!Promise<Map<string, !JSHandle>>}
*/ */
async getProperties() { async getProperties() {
const response = await this._session.send('Page.getObjectProperties', { const response = await this._session.send('Runtime.getObjectProperties', {
executionContextId: this._executionContextId, executionContextId: this._executionContextId,
objectId: this._objectId, objectId: this._objectId,
}); });
@ -85,10 +85,10 @@ class JSHandle {
async jsonValue() { async jsonValue() {
if (!this._objectId) if (!this._objectId)
return this._deserializeValue(this._protocolValue); return this._deserializeValue(this._protocolValue);
const simpleValue = await this._session.send('Page.evaluate', { const simpleValue = await this._session.send('Runtime.callFunction', {
executionContextId: this._executionContextId, executionContextId: this._executionContextId,
returnByValue: true, returnByValue: true,
functionText: (e => e).toString(), functionDeclaration: (e => e).toString(),
args: [this._protocolValue], args: [this._protocolValue],
}); });
return this._deserializeValue(simpleValue.result); return this._deserializeValue(simpleValue.result);
@ -105,9 +105,13 @@ class JSHandle {
if (!this._objectId) if (!this._objectId)
return; return;
this._disposed = true; this._disposed = true;
await this._session.send('Page.disposeObject', { await this._session.send('Runtime.disposeObject', {
executionContextId: this._executionContextId, executionContextId: this._executionContextId,
objectId: this._objectId, objectId: this._objectId,
}).catch(error => {
// Exceptions might happen in case of a page been navigated or closed.
// Swallow these since they are harmless and we don't leak anything in this case.
debugError(error);
}); });
} }
} }

View File

@ -27,6 +27,7 @@ class Page extends EventEmitter {
await Promise.all([ await Promise.all([
session.send('Page.enable'), session.send('Page.enable'),
session.send('Network.enable'), session.send('Network.enable'),
session.send('Runtime.enable'),
]); ]);
if (defaultViewport) if (defaultViewport)
@ -139,7 +140,7 @@ class Page extends EventEmitter {
else else
expression = helper.evaluationString(deliverErrorValue, name, seq, error); expression = helper.evaluationString(deliverErrorValue, name, seq, error);
} }
this._session.send('Page.evaluate', { script: expression, executionContextId: event.frameId }).catch(debugError); this._session.send('Runtime.evaluate', { expression, executionContextId: event.executionContextId }).catch(debugError);
/** /**
* @param {string} name * @param {string} name
@ -651,9 +652,9 @@ class Page extends EventEmitter {
_onClosed() { _onClosed() {
} }
_onConsole({type, args, frameId, location}) { _onConsole({type, args, executionContextId, location}) {
const frame = this._frameManager.frame(frameId); const context = this._frameManager.executionContextById(executionContextId);
this.emit(Events.Page.Console, new ConsoleMessage(type, args.map(arg => createHandle(frame._executionContext, arg)), location)); this.emit(Events.Page.Console, new ConsoleMessage(type, args.map(arg => createHandle(context, arg)), location));
} }
/** /**

View File

@ -9,7 +9,7 @@
"node": ">=8.9.4" "node": ">=8.9.4"
}, },
"puppeteer": { "puppeteer": {
"firefox_revision": "3ba79216e3c5ae4e85006047cdd93eac4197427d" "firefox_revision": "764023af0aa07d232984dec6bc81d9e904f25ddb"
}, },
"scripts": { "scripts": {
"install": "node install.js", "install": "node install.js",

View File

@ -209,7 +209,7 @@ module.exports.addTests = function({testRunner, expect}) {
const result = await page.evaluate(() => document.execCommand('copy')); const result = await page.evaluate(() => document.execCommand('copy'));
expect(result).toBe(true); expect(result).toBe(true);
}); });
it_fails_ffox('should throw a nice error after a navigation', async({page, server}) => { it('should throw a nice error after a navigation', async({page, server}) => {
const executionContext = await page.mainFrame().executionContext(); const executionContext = await page.mainFrame().executionContext();
await Promise.all([ await Promise.all([