mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-11 04:30:17 +03:00
chore: remove Frame dependecies on the chromium-specific things (#95)
This commit is contained in:
parent
c48b39345a
commit
35c27bfa45
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user