feat: expose frame's execution contexts (#3048)
This patch exposes frame's execution contexts, making it possible to debug extension's content scripts. This is a resurrected #2812.
This commit is contained in:
parent
b84404c94e
commit
231a2be971
51
docs/api.md
51
docs/api.md
@ -58,6 +58,8 @@ Next Release: **Aug 9, 2018**
|
|||||||
* [event: 'dialog'](#event-dialog)
|
* [event: 'dialog'](#event-dialog)
|
||||||
* [event: 'domcontentloaded'](#event-domcontentloaded)
|
* [event: 'domcontentloaded'](#event-domcontentloaded)
|
||||||
* [event: 'error'](#event-error)
|
* [event: 'error'](#event-error)
|
||||||
|
* [event: 'executioncontextcreated'](#event-executioncontextcreated)
|
||||||
|
* [event: 'executioncontextdestroyed'](#event-executioncontextdestroyed)
|
||||||
* [event: 'frameattached'](#event-frameattached)
|
* [event: 'frameattached'](#event-frameattached)
|
||||||
* [event: 'framedetached'](#event-framedetached)
|
* [event: 'framedetached'](#event-framedetached)
|
||||||
* [event: 'framenavigated'](#event-framenavigated)
|
* [event: 'framenavigated'](#event-framenavigated)
|
||||||
@ -180,6 +182,7 @@ Next Release: **Aug 9, 2018**
|
|||||||
* [frame.evaluate(pageFunction, ...args)](#frameevaluatepagefunction-args)
|
* [frame.evaluate(pageFunction, ...args)](#frameevaluatepagefunction-args)
|
||||||
* [frame.evaluateHandle(pageFunction, ...args)](#frameevaluatehandlepagefunction-args)
|
* [frame.evaluateHandle(pageFunction, ...args)](#frameevaluatehandlepagefunction-args)
|
||||||
* [frame.executionContext()](#frameexecutioncontext)
|
* [frame.executionContext()](#frameexecutioncontext)
|
||||||
|
* [frame.executionContexts()](#frameexecutioncontexts)
|
||||||
* [frame.focus(selector)](#framefocusselector)
|
* [frame.focus(selector)](#framefocusselector)
|
||||||
* [frame.hover(selector)](#framehoverselector)
|
* [frame.hover(selector)](#framehoverselector)
|
||||||
* [frame.isDetached()](#frameisdetached)
|
* [frame.isDetached()](#frameisdetached)
|
||||||
@ -199,6 +202,8 @@ Next Release: **Aug 9, 2018**
|
|||||||
* [executionContext.evaluate(pageFunction, ...args)](#executioncontextevaluatepagefunction-args)
|
* [executionContext.evaluate(pageFunction, ...args)](#executioncontextevaluatepagefunction-args)
|
||||||
* [executionContext.evaluateHandle(pageFunction, ...args)](#executioncontextevaluatehandlepagefunction-args)
|
* [executionContext.evaluateHandle(pageFunction, ...args)](#executioncontextevaluatehandlepagefunction-args)
|
||||||
* [executionContext.frame()](#executioncontextframe)
|
* [executionContext.frame()](#executioncontextframe)
|
||||||
|
* [executionContext.isDefault()](#executioncontextisdefault)
|
||||||
|
* [executionContext.name()](#executioncontextname)
|
||||||
* [executionContext.queryObjects(prototypeHandle)](#executioncontextqueryobjectsprototypehandle)
|
* [executionContext.queryObjects(prototypeHandle)](#executioncontextqueryobjectsprototypehandle)
|
||||||
- [class: JSHandle](#class-jshandle)
|
- [class: JSHandle](#class-jshandle)
|
||||||
* [jsHandle.asElement()](#jshandleaselement)
|
* [jsHandle.asElement()](#jshandleaselement)
|
||||||
@ -770,6 +775,16 @@ Emitted when the page crashes.
|
|||||||
|
|
||||||
> **NOTE** `error` event has a special meaning in Node, see [error events](https://nodejs.org/api/events.html#events_error_events) for details.
|
> **NOTE** `error` event has a special meaning in Node, see [error events](https://nodejs.org/api/events.html#events_error_events) for details.
|
||||||
|
|
||||||
|
#### event: 'executioncontextcreated'
|
||||||
|
- <[ExecutionContext]>
|
||||||
|
|
||||||
|
Emitted whenever an execution context is created in one of the page's frames.
|
||||||
|
|
||||||
|
#### event: 'executioncontextdestroyed'
|
||||||
|
- <[ExecutionContext]>
|
||||||
|
|
||||||
|
Emitted whenever an execution context is removed from one of the page's frames.
|
||||||
|
|
||||||
#### event: 'frameattached'
|
#### event: 'frameattached'
|
||||||
- <[Frame]>
|
- <[Frame]>
|
||||||
|
|
||||||
@ -2200,7 +2215,14 @@ await resultHandle.dispose();
|
|||||||
|
|
||||||
|
|
||||||
#### frame.executionContext()
|
#### frame.executionContext()
|
||||||
- returns: <[Promise]<[ExecutionContext]>> Execution context associated with this frame.
|
- returns: <[Promise]<[ExecutionContext]>>
|
||||||
|
|
||||||
|
Returns promise that resolves to the frame's default execution context.
|
||||||
|
|
||||||
|
#### frame.executionContexts()
|
||||||
|
- returns: <[Array]<[ExecutionContext]>>
|
||||||
|
|
||||||
|
Returns all execution contexts associated with this frame.
|
||||||
|
|
||||||
#### frame.focus(selector)
|
#### frame.focus(selector)
|
||||||
- `selector` <[string]> A [selector] of an element to focus. If there are multiple elements satisfying the selector, the first will be focused.
|
- `selector` <[string]> A [selector] of an element to focus. If there are multiple elements satisfying the selector, the first will be focused.
|
||||||
@ -2398,9 +2420,16 @@ puppeteer.launch().then(async browser => {
|
|||||||
|
|
||||||
### class: ExecutionContext
|
### class: ExecutionContext
|
||||||
|
|
||||||
The class represents a context for JavaScript execution. Examples of JavaScript contexts are:
|
The class represents a context for JavaScript execution. Page might have many execution contexts:
|
||||||
- each [frame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) has a separate execution context
|
- each [frame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) has "default" execution context that is
|
||||||
- all kind of [workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) have their own contexts
|
always created after frame is attached to DOM. This context could be awaited with the [`frame.executionContext()`](#frameexecutioncontext) method.
|
||||||
|
- [extensions](https://developer.chrome.com/extensions)'s content scripts create additional execution contexts. These execution
|
||||||
|
contexts can be pulled with [`frame.executionContexts()`](#frameexecutioncontexts) method.
|
||||||
|
|
||||||
|
Execution context lifecycle could be observed using Page's ['executioncontextcreated'](#event-executioncontextcreated) and
|
||||||
|
['executioncontextdestroyed'](#event-executioncontextdestroyed) events.
|
||||||
|
|
||||||
|
Besides pages, execution contexts can be found in all kind of [workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API).
|
||||||
|
|
||||||
#### executionContext.evaluate(pageFunction, ...args)
|
#### executionContext.evaluate(pageFunction, ...args)
|
||||||
- `pageFunction` <[function]|[string]> Function to be evaluated in `executionContext`
|
- `pageFunction` <[function]|[string]> Function to be evaluated in `executionContext`
|
||||||
@ -2466,6 +2495,20 @@ await resultHandle.dispose();
|
|||||||
|
|
||||||
> **NOTE** Not every execution context is associated with a frame. For example, workers and extensions have execution contexts that are not associated with frames.
|
> **NOTE** Not every execution context is associated with a frame. For example, workers and extensions have execution contexts that are not associated with frames.
|
||||||
|
|
||||||
|
|
||||||
|
#### executionContext.isDefault()
|
||||||
|
- returns: <[boolean]>
|
||||||
|
|
||||||
|
Returns `true` if the execution context is a frame's main execution context; returns `false` otherwise.
|
||||||
|
|
||||||
|
|
||||||
|
#### executionContext.name()
|
||||||
|
- returns: <[string]>
|
||||||
|
|
||||||
|
Returns execution context name, if any. If this execution context is associated with Chrome Extension's content
|
||||||
|
script, this will return extension's name.
|
||||||
|
|
||||||
|
|
||||||
#### executionContext.queryObjects(prototypeHandle)
|
#### executionContext.queryObjects(prototypeHandle)
|
||||||
- `prototypeHandle` <[JSHandle]> A handle to the object prototype.
|
- `prototypeHandle` <[JSHandle]> A handle to the object prototype.
|
||||||
- returns: <[JSHandle]> A handle to an array of objects with this prototype
|
- returns: <[JSHandle]> A handle to an array of objects with this prototype
|
||||||
|
@ -30,9 +30,25 @@ class ExecutionContext {
|
|||||||
this._client = client;
|
this._client = client;
|
||||||
this._frame = frame;
|
this._frame = frame;
|
||||||
this._contextId = contextPayload.id;
|
this._contextId = contextPayload.id;
|
||||||
|
this._isDefault = contextPayload.auxData ? !!contextPayload.auxData['isDefault'] : false;
|
||||||
|
this._name = contextPayload.name;
|
||||||
this._objectHandleFactory = objectHandleFactory;
|
this._objectHandleFactory = objectHandleFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
name() {
|
||||||
|
return this._name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isDefault() {
|
||||||
|
return this._isDefault;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {?Puppeteer.Frame}
|
* @return {?Puppeteer.Frame}
|
||||||
*/
|
*/
|
||||||
|
@ -180,21 +180,14 @@ class FrameManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onExecutionContextCreated(contextPayload) {
|
_onExecutionContextCreated(contextPayload) {
|
||||||
const frameId = contextPayload.auxData && contextPayload.auxData.isDefault ? contextPayload.auxData.frameId : null;
|
const frameId = contextPayload.auxData ? contextPayload.auxData.frameId : null;
|
||||||
const frame = frameId ? this._frames.get(frameId) : null;
|
const frame = this._frames.get(frameId) || null;
|
||||||
/** @type {!ExecutionContext} */
|
/** @type {!ExecutionContext} */
|
||||||
const context = new ExecutionContext(this._client, contextPayload, obj => this.createJSHandle(context, obj), frame);
|
const context = new ExecutionContext(this._client, contextPayload, obj => this.createJSHandle(context, obj), frame);
|
||||||
this._contextIdToContext.set(contextPayload.id, context);
|
this._contextIdToContext.set(contextPayload.id, context);
|
||||||
if (frame)
|
if (frame)
|
||||||
frame._setDefaultContext(context);
|
frame._addExecutionContext(context);
|
||||||
}
|
this.emit(FrameManager.Events.ExecutionContextCreated, context);
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {!ExecutionContext} context
|
|
||||||
*/
|
|
||||||
_removeContext(context) {
|
|
||||||
if (context.frame())
|
|
||||||
context.frame()._setDefaultContext(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -205,13 +198,20 @@ class FrameManager extends EventEmitter {
|
|||||||
if (!context)
|
if (!context)
|
||||||
return;
|
return;
|
||||||
this._contextIdToContext.delete(executionContextId);
|
this._contextIdToContext.delete(executionContextId);
|
||||||
this._removeContext(context);
|
if (context.frame())
|
||||||
|
context.frame()._removeExecutionContext(context);
|
||||||
|
this.emit(FrameManager.Events.ExecutionContextDestroyed, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onExecutionContextsCleared() {
|
_onExecutionContextsCleared() {
|
||||||
for (const context of this._contextIdToContext.values())
|
const contexts = Array.from(this._contextIdToContext.values());
|
||||||
this._removeContext(context);
|
|
||||||
this._contextIdToContext.clear();
|
this._contextIdToContext.clear();
|
||||||
|
for (const context of contexts) {
|
||||||
|
if (context.frame())
|
||||||
|
context.frame()._removeExecutionContext(context);
|
||||||
|
}
|
||||||
|
for (const context of contexts)
|
||||||
|
this.emit(FrameManager.Events.ExecutionContextDestroyed, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -253,7 +253,9 @@ FrameManager.Events = {
|
|||||||
FrameNavigated: 'framenavigated',
|
FrameNavigated: 'framenavigated',
|
||||||
FrameDetached: 'framedetached',
|
FrameDetached: 'framedetached',
|
||||||
LifecycleEvent: 'lifecycleevent',
|
LifecycleEvent: 'lifecycleevent',
|
||||||
FrameNavigatedWithinDocument: 'framenavigatedwithindocument'
|
FrameNavigatedWithinDocument: 'framenavigatedwithindocument',
|
||||||
|
ExecutionContextCreated: 'executioncontextcreated',
|
||||||
|
ExecutionContextDestroyed: 'executioncontextdestroyed',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -278,6 +280,8 @@ class Frame {
|
|||||||
this._contextResolveCallback = null;
|
this._contextResolveCallback = null;
|
||||||
this._setDefaultContext(null);
|
this._setDefaultContext(null);
|
||||||
|
|
||||||
|
this._executionContexts = new Set();
|
||||||
|
|
||||||
/** @type {!Set<!WaitTask>} */
|
/** @type {!Set<!WaitTask>} */
|
||||||
this._waitTasks = new Set();
|
this._waitTasks = new Set();
|
||||||
this._loaderId = '';
|
this._loaderId = '';
|
||||||
@ -290,6 +294,24 @@ class Frame {
|
|||||||
this._parentFrame._childFrames.add(this);
|
this._parentFrame._childFrames.add(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!ExecutionContext} context
|
||||||
|
*/
|
||||||
|
_addExecutionContext(context) {
|
||||||
|
this._executionContexts.add(context);
|
||||||
|
if (context.isDefault())
|
||||||
|
this._setDefaultContext(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!ExecutionContext} context
|
||||||
|
*/
|
||||||
|
_removeExecutionContext(context) {
|
||||||
|
this._executionContexts.delete(context);
|
||||||
|
if (context.isDefault())
|
||||||
|
this._setDefaultContext(null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {?ExecutionContext} context
|
* @param {?ExecutionContext} context
|
||||||
*/
|
*/
|
||||||
@ -314,6 +336,13 @@ class Frame {
|
|||||||
return this._contextPromise;
|
return this._contextPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {!Array<!ExecutionContext>}
|
||||||
|
*/
|
||||||
|
executionContexts() {
|
||||||
|
return Array.from(this._executionContexts);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {function()|string} pageFunction
|
* @param {function()|string} pageFunction
|
||||||
* @param {!Array<*>} args
|
* @param {!Array<*>} args
|
||||||
|
@ -121,6 +121,8 @@ class Page extends EventEmitter {
|
|||||||
this._frameManager.on(FrameManager.Events.FrameAttached, event => this.emit(Page.Events.FrameAttached, event));
|
this._frameManager.on(FrameManager.Events.FrameAttached, event => this.emit(Page.Events.FrameAttached, event));
|
||||||
this._frameManager.on(FrameManager.Events.FrameDetached, event => this.emit(Page.Events.FrameDetached, event));
|
this._frameManager.on(FrameManager.Events.FrameDetached, event => this.emit(Page.Events.FrameDetached, event));
|
||||||
this._frameManager.on(FrameManager.Events.FrameNavigated, event => this.emit(Page.Events.FrameNavigated, event));
|
this._frameManager.on(FrameManager.Events.FrameNavigated, event => this.emit(Page.Events.FrameNavigated, event));
|
||||||
|
this._frameManager.on(FrameManager.Events.ExecutionContextCreated, event => this.emit(Page.Events.ExecutionContextCreated, event));
|
||||||
|
this._frameManager.on(FrameManager.Events.ExecutionContextDestroyed, event => this.emit(Page.Events.ExecutionContextDestroyed, event));
|
||||||
|
|
||||||
this._networkManager.on(NetworkManager.Events.Request, event => this.emit(Page.Events.Request, event));
|
this._networkManager.on(NetworkManager.Events.Request, event => this.emit(Page.Events.Request, event));
|
||||||
this._networkManager.on(NetworkManager.Events.Response, event => this.emit(Page.Events.Response, event));
|
this._networkManager.on(NetworkManager.Events.Response, event => this.emit(Page.Events.Response, event));
|
||||||
@ -1128,6 +1130,8 @@ Page.Events = {
|
|||||||
Metrics: 'metrics',
|
Metrics: 'metrics',
|
||||||
WorkerCreated: 'workercreated',
|
WorkerCreated: 'workercreated',
|
||||||
WorkerDestroyed: 'workerdestroyed',
|
WorkerDestroyed: 'workerdestroyed',
|
||||||
|
ExecutionContextCreated: 'executioncontextcreated',
|
||||||
|
ExecutionContextDestroyed: 'executioncontextdestroyed',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
3
test/assets/simple-extension/content-script.js
Normal file
3
test/assets/simple-extension/content-script.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
console.log('hey from the content-script');
|
||||||
|
self.thisIsTheContentScript = true;
|
||||||
|
|
@ -1,12 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "Simple extension",
|
"name": "Simple extension",
|
||||||
"version": "0.1",
|
"version": "0.1",
|
||||||
"app": {
|
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["index.js"]
|
"scripts": ["index.js"]
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"permissions": ["background"],
|
"content_scripts": [{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"css": [],
|
||||||
|
"js": ["content-script.js"]
|
||||||
|
}],
|
||||||
|
"permissions": ["background", "activeTab"],
|
||||||
"manifest_version": 2
|
"manifest_version": 2
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
const {waitEvent} = require('./utils.js');
|
||||||
|
|
||||||
const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
|
const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
|
||||||
|
|
||||||
@ -115,6 +116,27 @@ module.exports.addTests = function({testRunner, expect, PROJECT_ROOT, defaultBro
|
|||||||
]);
|
]);
|
||||||
await browser.close();
|
await browser.close();
|
||||||
});
|
});
|
||||||
|
it('should report content script execution contexts', async({server}) => {
|
||||||
|
const browserWithExtension = await puppeteer.launch(extensionOptions);
|
||||||
|
const page = await browserWithExtension.newPage();
|
||||||
|
const [extensionContext, msg] = await Promise.all([
|
||||||
|
waitEvent(page, 'executioncontextcreated', context => !context.isDefault()),
|
||||||
|
waitEvent(page, 'console'),
|
||||||
|
page.goto(server.EMPTY_PAGE)
|
||||||
|
]);
|
||||||
|
expect(msg.text()).toBe('hey from the content-script');
|
||||||
|
expect(page.mainFrame().executionContexts().length).toBe(2);
|
||||||
|
expect(extensionContext.frame()).toBe(page.mainFrame());
|
||||||
|
expect(extensionContext.name()).toBe('Simple extension');
|
||||||
|
expect(await extensionContext.evaluate('thisIsTheContentScript')).toBe(true);
|
||||||
|
|
||||||
|
const [destroyedContext] = await Promise.all([
|
||||||
|
waitEvent(page, 'executioncontextdestroyed', context => !context.isDefault()),
|
||||||
|
page.reload()
|
||||||
|
]);
|
||||||
|
expect(destroyedContext).toBe(extensionContext);
|
||||||
|
await browserWithExtension.close();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -77,6 +77,11 @@ module.exports.addTests = function({testRunner, expect, puppeteer, DeviceDescrip
|
|||||||
const result = await page.evaluate(() => 7 * 3);
|
const result = await page.evaluate(() => 7 * 3);
|
||||||
expect(result).toBe(21);
|
expect(result).toBe(21);
|
||||||
});
|
});
|
||||||
|
it('should have nice default execution context', async({page, server}) => {
|
||||||
|
const executionContext = await page.mainFrame().executionContext();
|
||||||
|
expect(executionContext.name()).toBe('');
|
||||||
|
expect(executionContext.isDefault()).toBe(true);
|
||||||
|
});
|
||||||
it('should throw when evaluation triggers reload', async({page, server}) => {
|
it('should throw when evaluation triggers reload', async({page, server}) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
|
@ -78,7 +78,14 @@ const utils = module.exports = {
|
|||||||
* @param {string} eventName
|
* @param {string} eventName
|
||||||
* @return {!Promise<!Object>}
|
* @return {!Promise<!Object>}
|
||||||
*/
|
*/
|
||||||
waitEvent: function(emitter, eventName) {
|
waitEvent: function(emitter, eventName, predicate = () => true) {
|
||||||
return new Promise(fulfill => emitter.once(eventName, fulfill));
|
return new Promise(fulfill => {
|
||||||
|
emitter.on(eventName, function listener(event) {
|
||||||
|
if (!predicate(event))
|
||||||
|
return;
|
||||||
|
emitter.removeListener(eventName, listener);
|
||||||
|
fulfill(event);
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user