chore: remove Frame dependecies on the chromium-specific things (#95)

This commit is contained in:
Dmitry Gozman 2019-11-26 16:19:43 -08:00 committed by Pavel Feldman
parent c48b39345a
commit 35c27bfa45
5 changed files with 133 additions and 107 deletions

View File

@ -17,7 +17,7 @@
import { CDPSession } from './Connection';
import { Frame } from './Frame';
import { assert, helper } from '../helper';
import { helper } from '../helper';
import { valueFromRemoteObject, getExceptionMessage } from './protocolHelper';
import { createJSHandle, ElementHandle, JSHandle } from './JSHandle';
import { Protocol } from './protocol';

View File

@ -19,14 +19,11 @@ import * as types from '../types';
import * as fs from 'fs';
import { helper, assert } from '../helper';
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from '../input';
import { CDPSession } from './Connection';
import { ExecutionContext } from './ExecutionContext';
import { FrameManager } from './FrameManager';
import { ElementHandle, JSHandle, createJSHandle } from './JSHandle';
import { ElementHandle, JSHandle } from './JSHandle';
import { Response } from './NetworkManager';
import { Protocol } from './protocol';
import { LifecycleWatcher } from './LifecycleWatcher';
import { waitForSelectorOrXPath, WaitTaskParams, WaitTask } from '../waitTask';
import { TimeoutSettings } from '../TimeoutSettings';
const readFileAsync = helper.promisify(fs.readFile);
@ -38,25 +35,35 @@ type World = {
waitTasks: Set<WaitTask<JSHandle>>;
};
export type NavigateOptions = {
timeout?: number,
waitUntil?: string | string[],
};
export type GotoOptions = NavigateOptions & {
referer?: string,
};
export interface FrameDelegate {
timeoutSettings(): TimeoutSettings;
navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise<Response | null>;
waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise<Response | null>;
setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise<void>;
adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext): Promise<ElementHandle>;
}
export class Frame {
_id: string;
_frameManager: FrameManager;
private _client: CDPSession;
_delegate: FrameDelegate;
private _parentFrame: Frame;
private _url = '';
private _detached = false;
_loaderId = '';
_lifecycleEvents = new Set<string>();
_worlds = new Map<WorldType, World>();
private _worlds = new Map<WorldType, World>();
private _childFrames = new Set<Frame>();
private _name: string;
private _navigationURL: string;
constructor(frameManager: FrameManager, client: CDPSession, parentFrame: Frame | null, frameId: string) {
this._frameManager = frameManager;
this._client = client;
constructor(delegate: FrameDelegate, parentFrame: Frame | null) {
this._delegate = delegate;
this._parentFrame = parentFrame;
this._id = frameId;
this._worlds.set('main', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, waitTasks: new Set() });
this._worlds.set('utility', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, waitTasks: new Set() });
@ -67,15 +74,12 @@ export class Frame {
this._parentFrame._childFrames.add(this);
}
async goto(
url: string,
options: { referer?: string; timeout?: number; waitUntil?: string | string[]; } | undefined
): Promise<Response | null> {
return await this._frameManager.navigateFrame(this, url, options);
goto(url: string, options?: GotoOptions): Promise<Response | null> {
return this._delegate.navigateFrame(this, url, options);
}
async waitForNavigation(options: { timeout?: number; waitUntil?: string | string[]; } | undefined): Promise<Response | null> {
return await this._frameManager.waitForFrameNavigation(this, options);
waitForNavigation(options?: NavigateOptions): Promise<Response | null> {
return this._delegate.waitForFrameNavigation(this, options);
}
_mainContext(): Promise<ExecutionContext> {
@ -146,30 +150,8 @@ export class Frame {
});
}
async setContent(html: string, options: {
timeout?: number;
waitUntil?: string | string[];
} = {}) {
const {
waitUntil = ['load'],
timeout = this._frameManager._timeoutSettings.navigationTimeout(),
} = options;
const context = await this._utilityContext();
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
// lifecycle event. @see https://crrev.com/608658
await context.evaluate(html => {
document.open();
document.write(html);
document.close();
}, html);
const watcher = new LifecycleWatcher(this._frameManager, this, waitUntil, timeout);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise(),
watcher.lifecyclePromise(),
]);
watcher.dispose();
if (error)
throw error;
setContent(html: string, options?: NavigateOptions) {
return this._delegate.setFrameContent(this, html, options);
}
name(): string {
@ -404,7 +386,7 @@ export class Frame {
visible?: boolean;
hidden?: boolean;
timeout?: number; } | undefined): Promise<ElementHandle | null> {
const params = waitForSelectorOrXPath(selector, false /* isXPath */, { timeout: this._frameManager._timeoutSettings.timeout(), ...options });
const params = waitForSelectorOrXPath(selector, false /* isXPath */, { timeout: this._delegate.timeoutSettings().timeout(), ...options });
const handle = await this._scheduleWaitTask(params, this._worlds.get('utility'));
if (!handle.asElement()) {
await handle.dispose();
@ -418,7 +400,7 @@ export class Frame {
visible?: boolean;
hidden?: boolean;
timeout?: number; } | undefined): Promise<ElementHandle | null> {
const params = waitForSelectorOrXPath(xpath, true /* isXPath */, { timeout: this._frameManager._timeoutSettings.timeout(), ...options });
const params = waitForSelectorOrXPath(xpath, true /* isXPath */, { timeout: this._delegate.timeoutSettings().timeout(), ...options });
const handle = await this._scheduleWaitTask(params, this._worlds.get('utility'));
if (!handle.asElement()) {
await handle.dispose();
@ -434,7 +416,7 @@ export class Frame {
...args): Promise<JSHandle> {
const {
polling = 'raf',
timeout = this._frameManager._timeoutSettings.timeout(),
timeout = this._delegate.timeoutSettings().timeout(),
} = options;
const params: WaitTaskParams = {
predicateBody: pageFunction,
@ -451,28 +433,9 @@ export class Frame {
return context.evaluate(() => document.title);
}
_navigated(framePayload: Protocol.Page.Frame) {
this._name = framePayload.name;
// TODO(lushnikov): remove this once requestInterception has loaderId exposed.
this._navigationURL = framePayload.url;
this._url = framePayload.url;
}
_navigatedWithinDocument(url: string) {
_navigated(url: string, name: string) {
this._url = url;
}
_onLifecycleEvent(loaderId: string, name: string) {
if (name === 'init') {
this._loaderId = loaderId;
this._lifecycleEvents.clear();
}
this._lifecycleEvents.add(name);
}
_onLoadingStopped() {
this._lifecycleEvents.add('DOMContentLoaded');
this._lifecycleEvents.add('load');
this._name = name;
}
_detach() {
@ -527,12 +490,9 @@ export class Frame {
private async _adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext, dispose: boolean): Promise<ElementHandle> {
if (elementHandle.executionContext() === context)
return elementHandle;
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: elementHandle._remoteObject.objectId,
});
const result = await context._adoptBackendNodeId(nodeInfo.node.backendNodeId);
const handle = this._delegate.adoptElementHandle(elementHandle, context);
if (dispose)
await elementHandle.dispose();
return result;
return handle;
}
}

View File

@ -20,11 +20,12 @@ import { assert, debugError } from '../helper';
import { TimeoutSettings } from '../TimeoutSettings';
import { CDPSession } from './Connection';
import { EVALUATION_SCRIPT_URL, ExecutionContext } from './ExecutionContext';
import { Frame } from './Frame';
import { Frame, NavigateOptions, FrameDelegate } from './Frame';
import { LifecycleWatcher } from './LifecycleWatcher';
import { NetworkManager, Response } from './NetworkManager';
import { Page } from './Page';
import { Protocol } from './protocol';
import { ElementHandle, createJSHandle } from './JSHandle';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -36,7 +37,14 @@ export const FrameManagerEvents = {
FrameNavigatedWithinDocument: Symbol('Events.FrameManager.FrameNavigatedWithinDocument'),
};
export class FrameManager extends EventEmitter {
const frameDataSymbol = Symbol('frameData');
type FrameData = {
id: string,
loaderId: string,
lifecycleEvents: Set<string>,
};
export class FrameManager extends EventEmitter implements FrameDelegate {
_client: CDPSession;
private _page: Page;
private _networkManager: NetworkManager;
@ -81,6 +89,10 @@ export class FrameManager extends EventEmitter {
return this._networkManager;
}
_frameData(frame: Frame): FrameData {
return (frame as any)[frameDataSymbol];
}
async navigateFrame(
frame: Frame,
url: string,
@ -95,7 +107,7 @@ export class FrameManager extends EventEmitter {
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
let ensureNewDocumentNavigation = false;
let error = await Promise.race([
navigate(this._client, url, referer, frame._id),
navigate(this._client, url, referer, this._frameData(frame).id),
watcher.timeoutOrTerminationPromise(),
]);
if (!error) {
@ -141,11 +153,50 @@ export class FrameManager extends EventEmitter {
return watcher.navigationResponse();
}
async setFrameContent(frame: Frame, html: string, options: NavigateOptions = {}) {
const {
waitUntil = ['load'],
timeout = this._timeoutSettings.navigationTimeout(),
} = options;
const context = await frame._utilityContext();
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
// lifecycle event. @see https://crrev.com/608658
await context.evaluate(html => {
document.open();
document.write(html);
document.close();
}, html);
const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise(),
watcher.lifecyclePromise(),
]);
watcher.dispose();
if (error)
throw error;
}
timeoutSettings(): TimeoutSettings {
return this._timeoutSettings;
}
async adoptElementHandle(elementHandle: ElementHandle, context: ExecutionContext): Promise<ElementHandle> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: elementHandle._remoteObject.objectId,
});
return context._adoptBackendNodeId(nodeInfo.node.backendNodeId);
}
_onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {
const frame = this._frames.get(event.frameId);
if (!frame)
return;
frame._onLifecycleEvent(event.loaderId, event.name);
const data = this._frameData(frame);
if (event.name === 'init') {
data.loaderId = event.loaderId;
data.lifecycleEvents.clear();
}
data.lifecycleEvents.add(event.name);
this.emit(FrameManagerEvents.LifecycleEvent, frame);
}
@ -153,7 +204,9 @@ export class FrameManager extends EventEmitter {
const frame = this._frames.get(frameId);
if (!frame)
return;
frame._onLoadingStopped();
const data = this._frameData(frame);
data.lifecycleEvents.add('DOMContentLoaded');
data.lifecycleEvents.add('load');
this.emit(FrameManagerEvents.LifecycleEvent, frame);
}
@ -189,8 +242,14 @@ export class FrameManager extends EventEmitter {
return;
assert(parentFrameId);
const parentFrame = this._frames.get(parentFrameId);
const frame = new Frame(this, this._client, parentFrame, frameId);
this._frames.set(frame._id, frame);
const frame = new Frame(this, parentFrame);
const data: FrameData = {
id: frameId,
loaderId: '',
lifecycleEvents: new Set(),
};
frame[frameDataSymbol] = data;
this._frames.set(frameId, frame);
this.emit(FrameManagerEvents.FrameAttached, frame);
}
@ -209,18 +268,25 @@ export class FrameManager extends EventEmitter {
if (isMainFrame) {
if (frame) {
// Update frame id to retain frame identity on cross-process navigation.
this._frames.delete(frame._id);
frame._id = framePayload.id;
const data = this._frameData(frame);
this._frames.delete(data.id);
data.id = framePayload.id;
} else {
// Initial main frame navigation.
frame = new Frame(this, this._client, null, framePayload.id);
frame = new Frame(this, null);
const data: FrameData = {
id: framePayload.id,
loaderId: '',
lifecycleEvents: new Set(),
};
frame[frameDataSymbol] = data;
}
this._frames.set(framePayload.id, frame);
this._mainFrame = frame;
}
// Update frame payload.
frame._navigated(framePayload);
frame._navigated(framePayload.url, framePayload.name);
this.emit(FrameManagerEvents.FrameNavigated, frame);
}
@ -234,7 +300,7 @@ export class FrameManager extends EventEmitter {
worldName: name,
}),
await Promise.all(this.frames().map(frame => this._client.send('Page.createIsolatedWorld', {
frameId: frame._id,
frameId: this._frameData(frame).id,
grantUniveralAccess: true,
worldName: name,
}).catch(debugError))); // frames might be removed before we send this
@ -244,7 +310,7 @@ export class FrameManager extends EventEmitter {
const frame = this._frames.get(frameId);
if (!frame)
return;
frame._navigatedWithinDocument(url);
frame._navigated(url, frame.name());
this.emit(FrameManagerEvents.FrameNavigatedWithinDocument, frame);
this.emit(FrameManagerEvents.FrameNavigated, frame);
}
@ -294,7 +360,7 @@ export class FrameManager extends EventEmitter {
for (const child of frame.childFrames())
this._removeFramesRecursively(child);
frame._detach();
this._frames.delete(frame._id);
this._frames.delete(this._frameData(frame).id);
this.emit(FrameManagerEvents.FrameDetached, frame);
}
}

View File

@ -37,7 +37,7 @@ type Point = {
export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) {
const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._frameManager;
const frameManager = frame._delegate as FrameManager;
return new ElementHandle(context, context._client, remoteObject, frameManager.page(), frameManager);
}
return new JSHandle(context, context._client, remoteObject);

View File

@ -55,7 +55,7 @@ export class LifecycleWatcher {
this._frameManager = frameManager;
this._frame = frame;
this._initialLoaderId = frame._loaderId;
this._initialLoaderId = frameManager._frameData(frame).loaderId;
this._timeout = timeout;
this._eventListeners = [
helper.addEventListener(frameManager._client, CDPSessionEvents.Disconnected, () => this._terminate(new Error('Navigation failed because browser has disconnected!'))),
@ -138,20 +138,9 @@ export class LifecycleWatcher {
}
_checkLifecycleComplete() {
// We expect navigation to commit.
if (!checkLifecycle(this._frame, this._expectedLifecycle))
return;
this._lifecycleCallback();
if (this._frame._loaderId === this._initialLoaderId && !this._hasSameDocumentNavigation)
return;
if (this._hasSameDocumentNavigation)
this._sameDocumentNavigationCompleteCallback();
if (this._frame._loaderId !== this._initialLoaderId)
this._newDocumentNavigationCompleteCallback();
function checkLifecycle(frame: Frame, expectedLifecycle: string[]): boolean {
const checkLifecycle = (frame: Frame, expectedLifecycle: string[]): boolean => {
for (const event of expectedLifecycle) {
if (!frame._lifecycleEvents.has(event))
if (!this._frameManager._frameData(frame).lifecycleEvents.has(event))
return false;
}
for (const child of frame.childFrames()) {
@ -159,7 +148,18 @@ export class LifecycleWatcher {
return false;
}
return true;
}
};
// We expect navigation to commit.
if (!checkLifecycle(this._frame, this._expectedLifecycle))
return;
this._lifecycleCallback();
if (this._frameManager._frameData(this._frame).loaderId === this._initialLoaderId && !this._hasSameDocumentNavigation)
return;
if (this._hasSameDocumentNavigation)
this._sameDocumentNavigationCompleteCallback();
if (this._frameManager._frameData(this._frame).loaderId !== this._initialLoaderId)
this._newDocumentNavigationCompleteCallback();
}
dispose() {