chore: inherit FrameExecutionContext from ExecutionContext (#234)

This commit is contained in:
Dmitry Gozman 2019-12-12 21:11:52 -08:00 committed by Pavel Feldman
parent 5822de844a
commit 97c50c22ab
7 changed files with 183 additions and 189 deletions

View file

@ -350,16 +350,17 @@ export class FrameManager extends EventEmitter implements PageDelegate {
const frame = this._frames.get(frameId) || null;
if (contextPayload.auxData && contextPayload.auxData.type === 'isolated')
this._isolatedWorlds.add(contextPayload.name);
const context = new js.ExecutionContext(new ExecutionContextDelegate(this._client, contextPayload));
if (frame)
context._domWorld = new dom.DOMWorld(context, frame);
const delegate = new ExecutionContextDelegate(this._client, contextPayload);
if (frame) {
const context = new dom.FrameExecutionContext(delegate, frame);
if (contextPayload.auxData && !!contextPayload.auxData.isDefault)
frame._contextCreated('main', context);
else if (contextPayload.name === UTILITY_WORLD_NAME)
frame._contextCreated('utility', context);
this._contextIdToContext.set(contextPayload.id, context);
} else {
this._contextIdToContext.set(contextPayload.id, new js.ExecutionContext(delegate));
}
this._contextIdToContext.set(contextPayload.id, context);
}
_onExecutionContextDestroyed(executionContextId: number) {
@ -368,7 +369,7 @@ export class FrameManager extends EventEmitter implements PageDelegate {
return;
this._contextIdToContext.delete(executionContextId);
if (context.frame())
context.frame()._contextDestroyed(context);
context.frame()._contextDestroyed(context as dom.FrameExecutionContext);
}
_onExecutionContextsCleared() {
@ -452,8 +453,8 @@ export class FrameManager extends EventEmitter implements PageDelegate {
async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) {
const frame = this.frame(event.frameId);
const utilityWorld = await frame._utilityDOMWorld();
const handle = await this.adoptBackendNodeId(event.backendNodeId, utilityWorld);
const utilityContext = await frame._utilityContext();
const handle = await this.adoptBackendNodeId(event.backendNodeId, utilityContext);
this._page._onFileChooserOpened(handle);
}
@ -617,21 +618,21 @@ export class FrameManager extends EventEmitter implements PageDelegate {
await handle.evaluate(input.setFileInputFunction, files);
}
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.DOMWorld): Promise<dom.ElementHandle<T>> {
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: toRemoteObject(handle).objectId,
});
return this.adoptBackendNodeId(nodeInfo.node.backendNodeId, to) as Promise<dom.ElementHandle<T>>;
}
async adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.DOMWorld): Promise<dom.ElementHandle> {
async adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.FrameExecutionContext): Promise<dom.ElementHandle> {
const result = await this._client.send('DOM.resolveNode', {
backendNodeId,
executionContextId: (to.context._delegate as ExecutionContextDelegate)._contextId,
executionContextId: (to._delegate as ExecutionContextDelegate)._contextId,
}).catch(debugError);
if (!result)
throw new Error('Unable to adopt element handle from a different document');
return to.context._createHandle(result.object).asElement()!;
return to._createHandle(result.object).asElement()!;
}
}

View file

@ -11,28 +11,32 @@ import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSourc
import * as zsSelectorEngineSource from './generated/zsSelectorEngineSource';
import { assert, helper, debugError } from './helper';
import Injected from './injected/injected';
import { Page } from './page';
type ScopedSelector = types.Selector & { scope?: ElementHandle };
type ResolvedSelector = { scope?: ElementHandle, selector: string, visible?: boolean, disposeScope?: boolean };
export class DOMWorld {
readonly context: js.ExecutionContext;
readonly frame: frames.Frame;
export class FrameExecutionContext extends js.ExecutionContext {
private readonly _frame: frames.Frame;
private _injectedPromise?: Promise<js.JSHandle>;
constructor(context: js.ExecutionContext, frame: frames.Frame) {
this.context = context;
this.frame = frame;
constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame) {
super(delegate);
this._frame = frame;
}
createHandle(remoteObject: any): ElementHandle | null {
if (this.frame._page._delegate.isElementHandle(remoteObject))
return new ElementHandle(this.context, remoteObject);
return null;
frame(): frames.Frame | null {
return this._frame;
}
injected(): Promise<js.JSHandle> {
_createHandle(remoteObject: any): js.JSHandle | null {
if (this._frame._page._delegate.isElementHandle(remoteObject))
return new ElementHandle(this, remoteObject);
return super._createHandle(remoteObject);
}
_injected(): Promise<js.JSHandle> {
if (!this._injectedPromise) {
const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source, zsSelectorEngineSource.source];
const source = `
@ -40,36 +44,36 @@ export class DOMWorld {
${engineSources.join(',\n')}
])
`;
this._injectedPromise = this.context.evaluateHandle(source);
this._injectedPromise = this.evaluateHandle(source);
}
return this._injectedPromise;
}
async adoptElementHandle<T extends Node>(handle: ElementHandle<T>): Promise<ElementHandle<T>> {
assert(handle.executionContext() !== this.context, 'Should not adopt to the same context');
return this.frame._page._delegate.adoptElementHandle(handle, this);
async _adoptElementHandle<T extends Node>(handle: ElementHandle<T>): Promise<ElementHandle<T>> {
assert(handle.executionContext() !== this, 'Should not adopt to the same context');
return this._frame._page._delegate.adoptElementHandle(handle, this);
}
async resolveSelector(selector: string | ScopedSelector): Promise<ResolvedSelector> {
async _resolveSelector(selector: string | ScopedSelector): Promise<ResolvedSelector> {
if (helper.isString(selector))
return { selector: normalizeSelector(selector) };
if (selector.scope && selector.scope.executionContext() !== this.context) {
const scope = await this.adoptElementHandle(selector.scope);
if (selector.scope && selector.scope.executionContext() !== this) {
const scope = await this._adoptElementHandle(selector.scope);
return { scope, selector: normalizeSelector(selector.selector), disposeScope: true, visible: selector.visible };
}
return { scope: selector.scope, selector: normalizeSelector(selector.selector), visible: selector.visible };
}
async $(selector: string | ScopedSelector): Promise<ElementHandle<Element> | null> {
const resolved = await this.resolveSelector(selector);
const handle = await this.context.evaluateHandle(
async _$(selector: string | ScopedSelector): Promise<ElementHandle<Element> | null> {
const resolved = await this._resolveSelector(selector);
const handle = await this.evaluateHandle(
(injected: Injected, selector: string, scope?: Node, visible?: boolean) => {
const element = injected.querySelector(selector, scope || document);
if (visible === undefined || !element)
return element;
return injected.isVisible(element) === visible ? element : undefined;
},
await this.injected(), resolved.selector, resolved.scope, resolved.visible
await this._injected(), resolved.selector, resolved.scope, resolved.visible
);
if (resolved.disposeScope)
await resolved.scope.dispose();
@ -78,16 +82,16 @@ export class DOMWorld {
return handle.asElement();
}
async $$(selector: string | ScopedSelector): Promise<ElementHandle<Element>[]> {
const resolved = await this.resolveSelector(selector);
const arrayHandle = await this.context.evaluateHandle(
async _$$(selector: string | ScopedSelector): Promise<ElementHandle<Element>[]> {
const resolved = await this._resolveSelector(selector);
const arrayHandle = await this.evaluateHandle(
(injected: Injected, selector: string, scope?: Node, visible?: boolean) => {
const elements = injected.querySelectorAll(selector, scope || document);
if (visible !== undefined)
return elements.filter(element => injected.isVisible(element) === visible);
return elements;
},
await this.injected(), resolved.selector, resolved.scope, resolved.visible
await this._injected(), resolved.selector, resolved.scope, resolved.visible
);
if (resolved.disposeScope)
await resolved.scope.dispose();
@ -104,8 +108,8 @@ export class DOMWorld {
return result;
}
$eval: types.$Eval<string | ScopedSelector> = async (selector, pageFunction, ...args) => {
const elementHandle = await this.$(selector);
_$eval: types.$Eval<string | ScopedSelector> = async (selector, pageFunction, ...args) => {
const elementHandle = await this._$(selector);
if (!elementHandle)
throw new Error(`Error: failed to find element matching selector "${types.selectorToString(selector)}"`);
const result = await elementHandle.evaluate(pageFunction, ...args as any);
@ -113,16 +117,16 @@ export class DOMWorld {
return result;
}
$$eval: types.$$Eval<string | ScopedSelector> = async (selector, pageFunction, ...args) => {
const resolved = await this.resolveSelector(selector);
const arrayHandle = await this.context.evaluateHandle(
_$$eval: types.$$Eval<string | ScopedSelector> = async (selector, pageFunction, ...args) => {
const resolved = await this._resolveSelector(selector);
const arrayHandle = await this.evaluateHandle(
(injected: Injected, selector: string, scope?: Node, visible?: boolean) => {
const elements = injected.querySelectorAll(selector, scope || document);
if (visible !== undefined)
return elements.filter(element => injected.isVisible(element) === visible);
return elements;
},
await this.injected(), resolved.selector, resolved.scope, resolved.visible
await this._injected(), resolved.selector, resolved.scope, resolved.visible
);
const result = await arrayHandle.evaluate(pageFunction, ...args as any);
await arrayHandle.dispose();
@ -131,12 +135,12 @@ export class DOMWorld {
}
export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
readonly _world: DOMWorld;
readonly _context: FrameExecutionContext;
readonly _page: Page;
constructor(context: js.ExecutionContext, remoteObject: any) {
constructor(context: FrameExecutionContext, remoteObject: any) {
super(context, remoteObject);
assert(context._domWorld, 'Element handle should have a dom world');
this._world = context._domWorld;
this._page = context.frame()._page;
}
asElement(): ElementHandle<T> | null {
@ -144,7 +148,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async contentFrame(): Promise<frames.Frame | null> {
return this._world.frame._page._delegate.getContentFrame(this);
return this._page._delegate.getContentFrame(this);
}
async _scrollIntoViewIfNeeded() {
@ -175,7 +179,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
}
return false;
}, !!this._world.frame._page._state.javascriptEnabled);
}, !!this._page._state.javascriptEnabled);
if (error)
throw new Error(error);
}
@ -222,8 +226,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
};
const [quads, metrics] = await Promise.all([
this._world.frame._page._delegate.getContentQuads(this),
this._world.frame._page._delegate.layoutViewport(),
this._page._delegate.getContentQuads(this),
this._page._delegate.layoutViewport(),
]);
if (!quads || !quads.length)
throw new Error('Node is either not visible or not an HTMLElement');
@ -260,7 +264,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
point.x += border.x;
point.y += border.y;
}
const metrics = await this._world.frame._page._delegate.layoutViewport();
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)
@ -279,26 +283,26 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
const point = await this._ensurePointerActionPoint(options ? options.relativePoint : undefined);
let restoreModifiers: input.Modifier[] | undefined;
if (options && options.modifiers)
restoreModifiers = await this._world.frame._page.keyboard._ensureModifiers(options.modifiers);
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
await action(point);
if (restoreModifiers)
await this._world.frame._page.keyboard._ensureModifiers(restoreModifiers);
await this._page.keyboard._ensureModifiers(restoreModifiers);
}
hover(options?: input.PointerActionOptions): Promise<void> {
return this._performPointerAction(point => this._world.frame._page.mouse.move(point.x, point.y), options);
return this._performPointerAction(point => this._page.mouse.move(point.x, point.y), options);
}
click(options?: input.ClickOptions): Promise<void> {
return this._performPointerAction(point => this._world.frame._page.mouse.click(point.x, point.y, options), options);
return this._performPointerAction(point => this._page.mouse.click(point.x, point.y, options), options);
}
dblclick(options?: input.MultiClickOptions): Promise<void> {
return this._performPointerAction(point => this._world.frame._page.mouse.dblclick(point.x, point.y, options), options);
return this._performPointerAction(point => this._page.mouse.dblclick(point.x, point.y, options), options);
}
tripleclick(options?: input.MultiClickOptions): Promise<void> {
return this._performPointerAction(point => this._world.frame._page.mouse.tripleclick(point.x, point.y, options), options);
return this._performPointerAction(point => this._page.mouse.tripleclick(point.x, point.y, options), options);
}
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
@ -321,7 +325,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
const error = await this.evaluate(input.fillFunction);
if (error)
throw new Error(error);
await this._world.frame._page.keyboard.sendCharacters(value);
await this._page.keyboard.sendCharacters(value);
}
async setInputFiles(...files: (string|input.FilePayload)[]) {
@ -332,7 +336,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return input.multiple;
});
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
await this._world.frame._page._delegate.setInputFiles(this, await input.loadFiles(files));
await this._page._delegate.setInputFiles(this, await input.loadFiles(files));
}
async focus() {
@ -348,20 +352,20 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async type(text: string, options: { delay: (number | undefined); } | undefined) {
await this.focus();
await this._world.frame._page.keyboard.type(text, options);
await this._page.keyboard.type(text, options);
}
async press(key: string, options: { delay?: number; text?: string; } | undefined) {
await this.focus();
await this._world.frame._page.keyboard.press(key, options);
await this._page.keyboard.press(key, options);
}
async boundingBox(): Promise<types.Rect | null> {
return this._world.frame._page._delegate.getBoundingBox(this);
return this._page._delegate.getBoundingBox(this);
}
async screenshot(options?: types.ElementScreenshotOptions): Promise<string | Buffer> {
return this._world.frame._page._screenshotter.screenshotElement(this, options);
return this._page._screenshotter.screenshotElement(this, options);
}
private _scopedSelector(selector: string | types.Selector): string | ScopedSelector {
@ -372,23 +376,23 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
$(selector: string | types.Selector): Promise<ElementHandle | null> {
return this._world.$(this._scopedSelector(selector));
return this._context._$(this._scopedSelector(selector));
}
$$(selector: string | types.Selector): Promise<ElementHandle<Element>[]> {
return this._world.$$(this._scopedSelector(selector));
return this._context._$$(this._scopedSelector(selector));
}
$eval: types.$Eval<string | types.Selector> = (selector, pageFunction, ...args) => {
return this._world.$eval(this._scopedSelector(selector), pageFunction, ...args as any);
return this._context._$eval(this._scopedSelector(selector), pageFunction, ...args as any);
}
$$eval: types.$$Eval<string | types.Selector> = (selector, pageFunction, ...args) => {
return this._world.$$eval(this._scopedSelector(selector), pageFunction, ...args as any);
return this._context._$$eval(this._scopedSelector(selector), pageFunction, ...args as any);
}
$x(expression: string): Promise<ElementHandle<Element>[]> {
return this._world.$$({ scope: this, selector: 'xpath=' + expression });
return this._context._$$({ scope: this, selector: 'xpath=' + expression });
}
isIntersectingViewport(): Promise<boolean> {
@ -422,7 +426,7 @@ function normalizeSelector(selector: string): string {
return 'css=' + selector;
}
export type Task = (domWorld: DOMWorld) => Promise<js.JSHandle>;
export type Task = (context: FrameExecutionContext) => Promise<js.JSHandle>;
export function waitForFunctionTask(pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]) {
const { polling = 'raf' } = options;
@ -434,20 +438,20 @@ export function waitForFunctionTask(pageFunction: Function | string, options: ty
throw new Error('Unknown polling options: ' + polling);
const predicateBody = helper.isString(pageFunction) ? 'return (' + pageFunction + ')' : 'return (' + pageFunction + ')(...args)';
return async (domWorld: DOMWorld) => domWorld.context.evaluateHandle((injected: Injected, predicateBody: string, polling: types.Polling, timeout: number, ...args) => {
return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, predicateBody: string, polling: types.Polling, timeout: number, ...args) => {
const predicate = new Function('...args', predicateBody);
if (polling === 'raf')
return injected.pollRaf(predicate, timeout, ...args);
if (polling === 'mutation')
return injected.pollMutation(predicate, timeout, ...args);
return injected.pollInterval(polling, predicate, timeout, ...args);
}, await domWorld.injected(), predicateBody, polling, options.timeout, ...args);
}, await context._injected(), predicateBody, polling, options.timeout, ...args);
}
export function waitForSelectorTask(selector: string | types.Selector, timeout: number): Task {
return async (domWorld: DOMWorld) => {
const resolved = await domWorld.resolveSelector(selector);
return domWorld.context.evaluateHandle((injected: Injected, selector: string, scope: Node | undefined, visible: boolean | undefined, timeout: number) => {
return async (context: FrameExecutionContext) => {
const resolved = await context._resolveSelector(selector);
return context.evaluateHandle((injected: Injected, selector: string, scope: Node | undefined, visible: boolean | undefined, timeout: number) => {
if (visible !== undefined)
return injected.pollRaf(predicate, timeout);
return injected.pollMutation(predicate, timeout);
@ -460,6 +464,6 @@ export function waitForSelectorTask(selector: string | types.Selector, timeout:
return element;
return injected.isVisible(element) === visible ? element : false;
}
}, await domWorld.injected(), resolved.selector, resolved.scope, resolved.visible, timeout);
}, await context._injected(), resolved.selector, resolved.scope, resolved.visible, timeout);
};
}

View file

@ -109,13 +109,15 @@ export class FrameManager extends EventEmitter implements PageDelegate {
_onExecutionContextCreated({executionContextId, auxData}) {
const frameId = auxData ? auxData.frameId : null;
const frame = this._frames.get(frameId) || null;
const context = new js.ExecutionContext(new ExecutionContextDelegate(this._session, executionContextId));
const delegate = new ExecutionContextDelegate(this._session, executionContextId);
if (frame) {
context._domWorld = new dom.DOMWorld(context, frame);
const context = new dom.FrameExecutionContext(delegate, frame);
frame._contextCreated('main', context);
frame._contextCreated('utility', context);
this._contextIdToContext.set(executionContextId, context);
} else {
this._contextIdToContext.set(executionContextId, new js.ExecutionContext(delegate));
}
this._contextIdToContext.set(executionContextId, context);
}
_onExecutionContextDestroyed({executionContextId}) {
@ -124,7 +126,7 @@ export class FrameManager extends EventEmitter implements PageDelegate {
return;
this._contextIdToContext.delete(executionContextId);
if (context.frame())
context.frame()._contextDestroyed(context);
context.frame()._contextDestroyed(context as dom.FrameExecutionContext);
}
_frameData(frame: frames.Frame): FrameData {
@ -440,7 +442,7 @@ export class FrameManager extends EventEmitter implements PageDelegate {
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
const { frameId } = await this._session.send('Page.contentFrame', {
frameId: this._frameData(handle._world.frame).frameId,
frameId: this._frameData(handle._context.frame()).frameId,
objectId: toRemoteObject(handle).objectId,
});
if (!frameId)
@ -473,7 +475,7 @@ export class FrameManager extends EventEmitter implements PageDelegate {
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._session.send('Page.getContentQuads', {
frameId: this._frameData(handle._world.frame).frameId,
frameId: this._frameData(handle._context.frame()).frameId,
objectId: toRemoteObject(handle).objectId,
}).catch(debugError);
if (!result)
@ -489,7 +491,7 @@ export class FrameManager extends EventEmitter implements PageDelegate {
await handle.evaluate(input.setFileInputFunction, files);
}
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.DOMWorld): Promise<dom.ElementHandle<T>> {
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
assert(false, 'Multiple isolated worlds are not implemented');
return handle;
}

View file

@ -29,11 +29,11 @@ import { ConsoleMessage } from './console';
const readFileAsync = helper.promisify(fs.readFile);
type WorldType = 'main' | 'utility';
type World = {
contextPromise: Promise<js.ExecutionContext>;
contextResolveCallback: (c: js.ExecutionContext) => void;
context: js.ExecutionContext | null;
type ContextType = 'main' | 'utility';
type ContextData = {
contextPromise: Promise<dom.FrameExecutionContext>;
contextResolveCallback: (c: dom.FrameExecutionContext) => void;
context: dom.FrameExecutionContext | null;
rerunnableTasks: Set<RerunnableTask>;
};
@ -55,7 +55,7 @@ export class Frame {
private _parentFrame: Frame;
private _url = '';
private _detached = false;
private _worlds = new Map<WorldType, World>();
private _contextData = new Map<ContextType, ContextData>();
private _childFrames = new Set<Frame>();
private _name: string;
@ -65,8 +65,8 @@ export class Frame {
this._page = page;
this._parentFrame = parentFrame;
this._worlds.set('main', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
this._worlds.set('utility', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
this._contextData.set('main', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
this._contextData.set('utility', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
this._setContext('main', null);
this._setContext('utility', null);
@ -82,33 +82,19 @@ export class Frame {
return this._page._delegate.waitForFrameNavigation(this, options);
}
_mainContext(): Promise<js.ExecutionContext> {
_mainContext(): Promise<dom.FrameExecutionContext> {
if (this._detached)
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
return this._worlds.get('main').contextPromise;
return this._contextData.get('main').contextPromise;
}
async _mainDOMWorld(): Promise<dom.DOMWorld> {
const context = await this._mainContext();
if (!context._domWorld)
throw new Error(`Execution Context does not belong to frame`);
return context._domWorld;
}
_utilityContext(): Promise<js.ExecutionContext> {
_utilityContext(): Promise<dom.FrameExecutionContext> {
if (this._detached)
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
return this._worlds.get('utility').contextPromise;
return this._contextData.get('utility').contextPromise;
}
async _utilityDOMWorld(): Promise<dom.DOMWorld> {
const context = await this._utilityContext();
if (!context._domWorld)
throw new Error(`Execution Context does not belong to frame`);
return context._domWorld;
}
executionContext(): Promise<js.ExecutionContext> {
executionContext(): Promise<dom.FrameExecutionContext> {
return this._mainContext();
}
@ -123,28 +109,28 @@ export class Frame {
}
async $(selector: string | types.Selector): Promise<dom.ElementHandle<Element> | null> {
const domWorld = await this._mainDOMWorld();
return domWorld.$(types.clearSelector(selector));
const context = await this._mainContext();
return context._$(types.clearSelector(selector));
}
async $x(expression: string): Promise<dom.ElementHandle<Element>[]> {
const domWorld = await this._mainDOMWorld();
return domWorld.$$('xpath=' + expression);
const context = await this._mainContext();
return context._$$('xpath=' + expression);
}
$eval: types.$Eval = async (selector, pageFunction, ...args) => {
const domWorld = await this._mainDOMWorld();
return domWorld.$eval(selector, pageFunction, ...args as any);
const context = await this._mainContext();
return context._$eval(selector, pageFunction, ...args as any);
}
$$eval: types.$$Eval = async (selector, pageFunction, ...args) => {
const domWorld = await this._mainDOMWorld();
return domWorld.$$eval(selector, pageFunction, ...args as any);
const context = await this._mainContext();
return context._$$eval(selector, pageFunction, ...args as any);
}
async $$(selector: string | types.Selector): Promise<dom.ElementHandle<Element>[]> {
const domWorld = await this._mainDOMWorld();
return domWorld.$$(types.clearSelector(selector));
const context = await this._mainContext();
return context._$$(types.clearSelector(selector));
}
async content(): Promise<string> {
@ -319,61 +305,61 @@ export class Frame {
}
async click(selector: string | types.Selector, options?: ClickOptions) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(types.clearSelector(selector));
const context = await this._utilityContext();
const handle = await context._$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.click(options);
await handle.dispose();
}
async dblclick(selector: string | types.Selector, options?: MultiClickOptions) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(types.clearSelector(selector));
const context = await this._utilityContext();
const handle = await context._$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.dblclick(options);
await handle.dispose();
}
async tripleclick(selector: string | types.Selector, options?: MultiClickOptions) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(types.clearSelector(selector));
const context = await this._utilityContext();
const handle = await context._$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.tripleclick(options);
await handle.dispose();
}
async fill(selector: string | types.Selector, value: string) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(types.clearSelector(selector));
const context = await this._utilityContext();
const handle = await context._$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.fill(value);
await handle.dispose();
}
async focus(selector: string | types.Selector) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(types.clearSelector(selector));
const context = await this._utilityContext();
const handle = await context._$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.focus();
await handle.dispose();
}
async hover(selector: string | types.Selector, options?: PointerActionOptions) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(types.clearSelector(selector));
const context = await this._utilityContext();
const handle = await context._$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.hover(options);
await handle.dispose();
}
async select(selector: string | types.Selector, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise<string[]> {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(types.clearSelector(selector));
const context = await this._utilityContext();
const handle = await context._$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
const toDispose: Promise<dom.ElementHandle>[] = [];
const adoptedValues = await Promise.all(values.map(async value => {
if (value instanceof dom.ElementHandle && value.executionContext() !== domWorld.context) {
const adopted = domWorld.adoptElementHandle(value);
if (value instanceof dom.ElementHandle && value.executionContext() !== context) {
const adopted = context._adoptElementHandle(value);
toDispose.push(adopted);
return adopted;
}
@ -386,8 +372,8 @@ export class Frame {
}
async type(selector: string | types.Selector, text: string, options: { delay: (number | undefined); } | undefined) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(types.clearSelector(selector));
const context = await this._utilityContext();
const handle = await context._$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.type(text, options);
await handle.dispose();
@ -411,10 +397,10 @@ export class Frame {
await handle.dispose();
return null;
}
const mainDOMWorld = await this._mainDOMWorld();
if (handle.executionContext() === mainDOMWorld.context)
const maincontext = await this._mainContext();
if (handle.executionContext() === maincontext)
return handle.asElement();
const adopted = await mainDOMWorld.adoptElementHandle(handle.asElement());
const adopted = await maincontext._adoptElementHandle(handle.asElement());
await handle.dispose();
return adopted;
}
@ -465,8 +451,8 @@ export class Frame {
_onDetached() {
this._detached = true;
for (const world of this._worlds.values()) {
for (const rerunnableTask of world.rerunnableTasks)
for (const data of this._contextData.values()) {
for (const rerunnableTask of data.rerunnableTasks)
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
}
if (this._parentFrame)
@ -476,51 +462,50 @@ export class Frame {
watcher._onFrameDetached(this);
}
private _scheduleRerunnableTask(task: dom.Task, worldType: WorldType, timeout?: number, title?: string): Promise<js.JSHandle> {
const world = this._worlds.get(worldType);
const rerunnableTask = new RerunnableTask(world, task, timeout, title);
world.rerunnableTasks.add(rerunnableTask);
if (world.context)
rerunnableTask.rerun(world.context._domWorld);
private _scheduleRerunnableTask(task: dom.Task, contextType: ContextType, timeout?: number, title?: string): Promise<js.JSHandle> {
const data = this._contextData.get(contextType);
const rerunnableTask = new RerunnableTask(data, task, timeout, title);
data.rerunnableTasks.add(rerunnableTask);
if (data.context)
rerunnableTask.rerun(data.context);
return rerunnableTask.promise;
}
private _setContext(worldType: WorldType, context: js.ExecutionContext | null) {
const world = this._worlds.get(worldType);
world.context = context;
private _setContext(contextType: ContextType, context: dom.FrameExecutionContext | null) {
const data = this._contextData.get(contextType);
data.context = context;
if (context) {
assert(context._domWorld, 'Frame context must have a dom world');
world.contextResolveCallback.call(null, context);
for (const rerunnableTask of world.rerunnableTasks)
rerunnableTask.rerun(context._domWorld);
data.contextResolveCallback.call(null, context);
for (const rerunnableTask of data.rerunnableTasks)
rerunnableTask.rerun(context);
} else {
world.contextPromise = new Promise(fulfill => {
world.contextResolveCallback = fulfill;
data.contextPromise = new Promise(fulfill => {
data.contextResolveCallback = fulfill;
});
}
}
_contextCreated(worldType: WorldType, context: js.ExecutionContext) {
const world = this._worlds.get(worldType);
_contextCreated(contextType: ContextType, context: dom.FrameExecutionContext) {
const data = this._contextData.get(contextType);
// In case of multiple sessions to the same target, there's a race between
// connections so we might end up creating multiple isolated worlds.
// We can use either.
if (world.context)
this._setContext(worldType, null);
this._setContext(worldType, context);
if (data.context)
this._setContext(contextType, null);
this._setContext(contextType, context);
}
_contextDestroyed(context: js.ExecutionContext) {
for (const [worldType, world] of this._worlds) {
if (world.context === context)
this._setContext(worldType, null);
_contextDestroyed(context: dom.FrameExecutionContext) {
for (const [contextType, data] of this._contextData) {
if (data.context === context)
this._setContext(contextType, null);
}
}
}
class RerunnableTask {
readonly promise: Promise<js.JSHandle>;
private _world: World;
private _contextData: ContextData;
private _task: dom.Task;
private _runCount: number;
private _resolve: (result: js.JSHandle) => void;
@ -528,8 +513,8 @@ class RerunnableTask {
private _timeoutTimer: NodeJS.Timer;
private _terminated: boolean;
constructor(world: World, task: dom.Task, timeout?: number, title?: string) {
this._world = world;
constructor(data: ContextData, task: dom.Task, timeout?: number, title?: string) {
this._contextData = data;
this._task = task;
this._runCount = 0;
this.promise = new Promise<js.JSHandle>((resolve, reject) => {
@ -550,12 +535,12 @@ class RerunnableTask {
this._doCleanup();
}
async rerun(domWorld: dom.DOMWorld) {
async rerun(context: dom.FrameExecutionContext) {
const runCount = ++this._runCount;
let success: js.JSHandle | null = null;
let error = null;
try {
success = await this._task(domWorld);
success = await this._task(context);
} catch (e) {
error = e;
}
@ -569,7 +554,7 @@ class RerunnableTask {
// Ignore timeouts in pageScript - we track timeouts ourselves.
// If execution context has been already destroyed, `context.evaluate` will
// throw an error - ignore this predicate run altogether.
if (!error && await domWorld.context.evaluate(s => !s, success).catch(e => true)) {
if (!error && await context.evaluate(s => !s, success).catch(e => true)) {
await success.dispose();
return;
}
@ -594,7 +579,7 @@ class RerunnableTask {
_doCleanup() {
clearTimeout(this._timeoutTimer);
this._world.rerunnableTasks.delete(this);
this._contextData.rerunnableTasks.delete(this);
}
}

View file

@ -15,14 +15,13 @@ export interface ExecutionContextDelegate {
export class ExecutionContext {
readonly _delegate: ExecutionContextDelegate;
_domWorld?: dom.DOMWorld;
constructor(delegate: ExecutionContextDelegate) {
this._delegate = delegate;
}
frame(): frames.Frame | null {
return this._domWorld ? this._domWorld.frame : null;
return null;
}
evaluate: types.Evaluate = (pageFunction, ...args) => {
@ -34,7 +33,7 @@ export class ExecutionContext {
}
_createHandle(remoteObject: any): JSHandle {
return (this._domWorld && this._domWorld.createHandle(remoteObject)) || new JSHandle(this, remoteObject);
return new JSHandle(this, remoteObject);
}
}

View file

@ -64,7 +64,7 @@ export interface PageDelegate {
resetViewport(oldSize: types.Size): Promise<void>;
isElementHandle(remoteObject: any): boolean;
adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.DOMWorld): Promise<dom.ElementHandle<T>>;
adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>>;
getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null>;
getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null>;
layoutViewport(): Promise<{ width: number, height: number }>;
@ -198,10 +198,10 @@ export class Page extends EventEmitter {
}
async _createSelector(name: string, handle: dom.ElementHandle<Element>): Promise<string> {
const mainWorld = await this.mainFrame()._mainDOMWorld();
return mainWorld.context.evaluate((injected: Injected, target: Element, name: string) => {
const mainContext = await this.mainFrame()._mainContext();
return mainContext.evaluate((injected: Injected, target: Element, name: string) => {
return injected.engines.get(name).create(document.documentElement, target);
}, await mainWorld.injected(), handle, name);
}, await mainContext._injected(), handle, name);
}
evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => {

View file

@ -146,7 +146,8 @@ export class FrameManager extends EventEmitter implements PageDelegate {
disconnectFromTarget() {
for (const context of this._contextIdToContext.values()) {
(context._delegate as ExecutionContextDelegate)._dispose();
context.frame()._contextDestroyed(context);
if (context.frame())
context.frame()._contextDestroyed(context as dom.FrameExecutionContext);
}
// this._mainFrame = null;
}
@ -250,7 +251,7 @@ export class FrameManager extends EventEmitter implements PageDelegate {
const delegate = context._delegate as ExecutionContextDelegate;
delegate._dispose();
this._contextIdToContext.delete(delegate._contextId);
frame._contextDestroyed(context);
frame._contextDestroyed(context as dom.FrameExecutionContext);
}
}
@ -289,15 +290,17 @@ export class FrameManager extends EventEmitter implements PageDelegate {
const frame = this._frames.get(frameId) || null;
if (!frame)
return;
const context = new js.ExecutionContext(new ExecutionContextDelegate(this._session, contextPayload));
const delegate = new ExecutionContextDelegate(this._session, contextPayload);
if (frame) {
context._domWorld = new dom.DOMWorld(context, frame);
const context = new dom.FrameExecutionContext(delegate, frame);
if (contextPayload.isPageContext)
frame._contextCreated('main', context);
else if (contextPayload.name === UTILITY_WORLD_NAME)
frame._contextCreated('utility', context);
this._contextIdToContext.set(contextPayload.id, context);
} else {
this._contextIdToContext.set(contextPayload.id, new js.ExecutionContext(delegate));
}
this._contextIdToContext.set(contextPayload.id, context);
}
executionContextById(contextId: number): js.ExecutionContext {
@ -593,12 +596,12 @@ export class FrameManager extends EventEmitter implements PageDelegate {
await this._session.send('DOM.setInputFiles', { objectId, files });
}
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.DOMWorld): Promise<dom.ElementHandle<T>> {
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
const result = await this._session.send('DOM.resolveNode', {
objectId: toRemoteObject(handle).objectId,
executionContextId: (to.context._delegate as ExecutionContextDelegate)._contextId
executionContextId: (to._delegate as ExecutionContextDelegate)._contextId
});
return to.context._createHandle(result.object) as dom.ElementHandle<T>;
return to._createHandle(result.object) as dom.ElementHandle<T>;
}
}