feat(oop iframes)!: integrate OOP iframes with the frame manager (#7556)

This pull request to adds better support for OOP iframes (see #2548)

The current problem with OOP iframes is that they are moved to a different target. Because of this, the previous versions of Puppeteer pretty much ignored them.
This change extends the FrameManager to already take OOP iframes into account and hides the fact that those frames are actually in different targets.
Further work needs to be done to also make the NetworkManager aware of these and to make sure that settings like emulations etc. are also properly passed down to the new targets.
This commit is contained in:
Jan Scheffler 2021-10-28 11:25:49 +02:00 committed by GitHub
parent 0d6e688bf3
commit 4d9dc8c0e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 547 additions and 128 deletions

View File

@ -195,6 +195,7 @@
* [page.viewport()](#pageviewport) * [page.viewport()](#pageviewport)
* [page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#pagewaitforselectororfunctionortimeout-options-args) * [page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#pagewaitforselectororfunctionortimeout-options-args)
* [page.waitForFileChooser([options])](#pagewaitforfilechooseroptions) * [page.waitForFileChooser([options])](#pagewaitforfilechooseroptions)
* [page.waitForFrame(urlOrPredicate[, options])](#pagewaitforframeurlorpredicate-options)
* [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args) * [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args)
* [page.waitForNavigation([options])](#pagewaitfornavigationoptions) * [page.waitForNavigation([options])](#pagewaitfornavigationoptions)
* [page.waitForNetworkIdle([options])](#pagewaitfornetworkidleoptions) * [page.waitForNetworkIdle([options])](#pagewaitfornetworkidleoptions)
@ -269,6 +270,7 @@
* [frame.goto(url[, options])](#framegotourl-options) * [frame.goto(url[, options])](#framegotourl-options)
* [frame.hover(selector)](#framehoverselector) * [frame.hover(selector)](#framehoverselector)
* [frame.isDetached()](#frameisdetached) * [frame.isDetached()](#frameisdetached)
* [frame.isOOPFrame()](#frameisoopframe)
* [frame.name()](#framename) * [frame.name()](#framename)
* [frame.parentFrame()](#frameparentframe) * [frame.parentFrame()](#frameparentframe)
* [frame.select(selector, ...values)](#frameselectselector-values) * [frame.select(selector, ...values)](#frameselectselector-values)
@ -385,6 +387,7 @@
- [class: CDPSession](#class-cdpsession) - [class: CDPSession](#class-cdpsession)
* [cdpSession.connection()](#cdpsessionconnection) * [cdpSession.connection()](#cdpsessionconnection)
* [cdpSession.detach()](#cdpsessiondetach) * [cdpSession.detach()](#cdpsessiondetach)
* [cdpSession.id()](#cdpsessionid)
* [cdpSession.send(method[, ...paramArgs])](#cdpsessionsendmethod-paramargs) * [cdpSession.send(method[, ...paramArgs])](#cdpsessionsendmethod-paramargs)
- [class: Coverage](#class-coverage) - [class: Coverage](#class-coverage)
* [coverage.startCSSCoverage([options])](#coveragestartcsscoverageoptions) * [coverage.startCSSCoverage([options])](#coveragestartcsscoverageoptions)
@ -2769,6 +2772,19 @@ await fileChooser.accept(['/tmp/myfile.pdf']);
> **NOTE** “File picker” refers to the operating systems file selection UI that lets you browse to a folder and select file(s) to be shared with the web app. Its not the “Save file” dialog. > **NOTE** “File picker” refers to the operating systems file selection UI that lets you browse to a folder and select file(s) to be shared with the web app. Its not the “Save file” dialog.
#### page.waitForFrame(urlOrPredicate[, options])
- `urlOrPredicate` <[string]|[Function]> A URL or predicate to wait for.
- `options` <[Object]> Optional waiting parameters
- `timeout` <[number]> Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method.
- returns: <[Promise]<[Frame]>> Promise which resolves to the matched frame.
```js
const frame = await page.waitForFrame(async (frame) => {
return frame.name() === 'Test';
});
```
#### page.waitForFunction(pageFunction[, options[, ...args]]) #### page.waitForFunction(pageFunction[, options[, ...args]])
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context - `pageFunction` <[function]|[string]> Function to be evaluated in browser context
@ -3833,6 +3849,12 @@ If there's no element matching `selector`, the method throws an error.
Returns `true` if the frame has been detached, or `false` otherwise. Returns `true` if the frame has been detached, or `false` otherwise.
#### frame.isOOPFrame()
- returns: <[boolean]>
Returns `true` if the frame is an OOP frame, or `false` otherwise.
#### frame.name() #### frame.name()
- returns: <[string]> - returns: <[string]>
@ -5088,6 +5110,12 @@ Returns the underlying connection associated with the session. Can be used to ob
Detaches the cdpSession from the target. Once detached, the cdpSession object won't emit any events and can't be used Detaches the cdpSession from the target. Once detached, the cdpSession object won't emit any events and can't be used
to send messages. to send messages.
#### cdpSession.id()
- returns: <[string]>
Returns the session's id.
#### cdpSession.send(method[, ...paramArgs]) #### cdpSession.send(method[, ...paramArgs])
- `method` <[string]> protocol method name - `method` <[string]> protocol method name

View File

@ -350,6 +350,13 @@ export class CDPSession extends EventEmitter {
this._connection = null; this._connection = null;
this.emit(CDPSessionEmittedEvents.Disconnected); this.emit(CDPSessionEmittedEvents.Disconnected);
} }
/**
* @internal
*/
id(): string {
return this._sessionId;
}
} }
/** /**

View File

@ -37,6 +37,7 @@ import {
} from './EvalTypes.js'; } from './EvalTypes.js';
import { isNode } from '../environment.js'; import { isNode } from '../environment.js';
import { Protocol } from 'devtools-protocol'; import { Protocol } from 'devtools-protocol';
import { CDPSession } from './Connection.js';
// predicateQueryHandler and checkWaitForOptions are declared here so that // predicateQueryHandler and checkWaitForOptions are declared here so that
// TypeScript knows about them when used in the predicate function below. // TypeScript knows about them when used in the predicate function below.
@ -72,6 +73,7 @@ export interface PageBinding {
*/ */
export class DOMWorld { export class DOMWorld {
private _frameManager: FrameManager; private _frameManager: FrameManager;
private _client: CDPSession;
private _frame: Frame; private _frame: Frame;
private _timeoutSettings: TimeoutSettings; private _timeoutSettings: TimeoutSettings;
private _documentPromise?: Promise<ElementHandle> = null; private _documentPromise?: Promise<ElementHandle> = null;
@ -96,15 +98,19 @@ export class DOMWorld {
`${name}_${contextId}`; `${name}_${contextId}`;
constructor( constructor(
client: CDPSession,
frameManager: FrameManager, frameManager: FrameManager,
frame: Frame, frame: Frame,
timeoutSettings: TimeoutSettings timeoutSettings: TimeoutSettings
) { ) {
// Keep own reference to client because it might differ from the FrameManager's
// client for OOP iframes.
this._client = client;
this._frameManager = frameManager; this._frameManager = frameManager;
this._frame = frame; this._frame = frame;
this._timeoutSettings = timeoutSettings; this._timeoutSettings = timeoutSettings;
this._setContext(null); this._setContext(null);
frameManager._client.on('Runtime.bindingCalled', (event) => this._client.on('Runtime.bindingCalled', (event) =>
this._onBindingCalled(event) this._onBindingCalled(event)
); );
} }

View File

@ -14,11 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { debug } from '../common/Debug.js';
import { EventEmitter } from './EventEmitter.js'; import { EventEmitter } from './EventEmitter.js';
import { assert } from './assert.js'; import { assert } from './assert.js';
import { helper, debugError } from './helper.js'; import { helper } from './helper.js';
import { ExecutionContext, EVALUATION_SCRIPT_URL } from './ExecutionContext.js'; import { ExecutionContext, EVALUATION_SCRIPT_URL } from './ExecutionContext.js';
import { import {
LifecycleWatcher, LifecycleWatcher,
@ -27,7 +25,7 @@ import {
import { DOMWorld, WaitForSelectorOptions } from './DOMWorld.js'; import { DOMWorld, WaitForSelectorOptions } from './DOMWorld.js';
import { NetworkManager } from './NetworkManager.js'; import { NetworkManager } from './NetworkManager.js';
import { TimeoutSettings } from './TimeoutSettings.js'; import { TimeoutSettings } from './TimeoutSettings.js';
import { CDPSession } from './Connection.js'; import { Connection, CDPSession } from './Connection.js';
import { JSHandle, ElementHandle } from './JSHandle.js'; import { JSHandle, ElementHandle } from './JSHandle.js';
import { MouseButton } from './Input.js'; import { MouseButton } from './Input.js';
import { Page } from './Page.js'; import { Page } from './Page.js';
@ -72,9 +70,10 @@ export class FrameManager extends EventEmitter {
private _networkManager: NetworkManager; private _networkManager: NetworkManager;
_timeoutSettings: TimeoutSettings; _timeoutSettings: TimeoutSettings;
private _frames = new Map<string, Frame>(); private _frames = new Map<string, Frame>();
private _contextIdToContext = new Map<number, ExecutionContext>(); private _contextIdToContext = new Map<string, ExecutionContext>();
private _isolatedWorlds = new Set<string>(); private _isolatedWorlds = new Set<string>();
private _mainFrame: Frame; private _mainFrame: Frame;
private _disconnectPromise?: Promise<Error>;
constructor( constructor(
client: CDPSession, client: CDPSession,
@ -87,53 +86,81 @@ export class FrameManager extends EventEmitter {
this._page = page; this._page = page;
this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this); this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
this._timeoutSettings = timeoutSettings; this._timeoutSettings = timeoutSettings;
this._client.on('Page.frameAttached', (event) => this.setupEventListeners(this._client);
this._onFrameAttached(event.frameId, event.parentFrameId)
);
this._client.on('Page.frameNavigated', (event) =>
this._onFrameNavigated(event.frame)
);
this._client.on('Page.navigatedWithinDocument', (event) =>
this._onFrameNavigatedWithinDocument(event.frameId, event.url)
);
this._client.on('Page.frameDetached', (event) =>
this._onFrameDetached(event.frameId)
);
this._client.on('Page.frameStoppedLoading', (event) =>
this._onFrameStoppedLoading(event.frameId)
);
this._client.on('Runtime.executionContextCreated', (event) =>
this._onExecutionContextCreated(event.context)
);
this._client.on('Runtime.executionContextDestroyed', (event) =>
this._onExecutionContextDestroyed(event.executionContextId)
);
this._client.on('Runtime.executionContextsCleared', () =>
this._onExecutionContextsCleared()
);
this._client.on('Page.lifecycleEvent', (event) =>
this._onLifecycleEvent(event)
);
this._client.on('Target.attachedToTarget', async (event) =>
this._onFrameMoved(event)
);
} }
async initialize(): Promise<void> { private setupEventListeners(session: CDPSession) {
const result = await Promise.all([ session.on('Page.frameAttached', (event) => {
this._client.send('Page.enable'), this._onFrameAttached(session, event.frameId, event.parentFrameId);
this._client.send('Page.getFrameTree'), });
]); session.on('Page.frameNavigated', (event) => {
this._onFrameNavigated(event.frame);
});
session.on('Page.navigatedWithinDocument', (event) => {
this._onFrameNavigatedWithinDocument(event.frameId, event.url);
});
session.on(
'Page.frameDetached',
(event: Protocol.Page.FrameDetachedEvent) => {
this._onFrameDetached(
event.frameId,
event.reason as Protocol.Page.FrameDetachedEventReason
);
}
);
session.on('Page.frameStoppedLoading', (event) => {
this._onFrameStoppedLoading(event.frameId);
});
session.on('Runtime.executionContextCreated', (event) => {
this._onExecutionContextCreated(event.context, session);
});
session.on('Runtime.executionContextDestroyed', (event) => {
this._onExecutionContextDestroyed(event.executionContextId, session);
});
session.on('Runtime.executionContextsCleared', () => {
this._onExecutionContextsCleared(session);
});
session.on('Page.lifecycleEvent', (event) => {
this._onLifecycleEvent(event);
});
session.on('Target.attachedToTarget', async (event) => {
this._onAttachedToTarget(event);
});
session.on('Target.detachedFromTarget', async (event) => {
this._onDetachedFromTarget(event);
});
}
const { frameTree } = result[1]; async initialize(client: CDPSession = this._client): Promise<void> {
this._handleFrameTree(frameTree); try {
await Promise.all([ const result = await Promise.all([
this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }), client.send('Page.enable'),
this._client client.send('Page.getFrameTree'),
.send('Runtime.enable') ]);
.then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
this._networkManager.initialize(), const { frameTree } = result[1];
]); this._handleFrameTree(client, frameTree);
await Promise.all([
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
client
.send('Runtime.enable')
.then(() => this._ensureIsolatedWorld(client, UTILITY_WORLD_NAME)),
// TODO: Network manager is not aware of OOP iframes yet.
client === this._client
? this._networkManager.initialize()
: Promise.resolve(),
]);
} catch (error) {
// The target might have been closed before the initialization finished.
if (
error.message.includes('Target closed') ||
error.message.includes('Session closed')
) {
return;
}
throw error;
}
} }
networkManager(): NetworkManager { networkManager(): NetworkManager {
@ -219,18 +246,31 @@ export class FrameManager extends EventEmitter {
return watcher.navigationResponse(); return watcher.navigationResponse();
} }
private async _onFrameMoved(event: Protocol.Target.AttachedToTargetEvent) { private async _onAttachedToTarget(
event: Protocol.Target.AttachedToTargetEvent
) {
if (event.targetInfo.type !== 'iframe') { if (event.targetInfo.type !== 'iframe') {
return; return;
} }
// TODO(sadym): Remove debug message once proper OOPIF support is const frame = this._frames.get(event.targetInfo.targetId);
// implemented: https://github.com/puppeteer/puppeteer/issues/2548 const session = Connection.fromSession(this._client).session(
debug('puppeteer:frame')( event.sessionId
`The frame '${event.targetInfo.targetId}' moved to another session. ` +
`Out-of-process iframes (OOPIF) are not supported by Puppeteer yet. ` +
`https://github.com/puppeteer/puppeteer/issues/2548`
); );
frame._updateClient(session);
this.setupEventListeners(session);
await this.initialize(session);
}
private async _onDetachedFromTarget(
event: Protocol.Target.DetachedFromTargetEvent
) {
const frame = this._frames.get(event.targetId);
if (frame && frame.isOOPFrame()) {
// When an OOP iframe is removed from the page, it
// will only get a Target.detachedFromTarget event.
this._removeFramesRecursively(frame);
}
} }
_onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void { _onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
@ -247,13 +287,23 @@ export class FrameManager extends EventEmitter {
this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame); this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame);
} }
_handleFrameTree(frameTree: Protocol.Page.FrameTree): void { _handleFrameTree(
if (frameTree.frame.parentId) session: CDPSession,
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId); frameTree: Protocol.Page.FrameTree
): void {
if (frameTree.frame.parentId) {
this._onFrameAttached(
session,
frameTree.frame.id,
frameTree.frame.parentId
);
}
this._onFrameNavigated(frameTree.frame); this._onFrameNavigated(frameTree.frame);
if (!frameTree.childFrames) return; if (!frameTree.childFrames) return;
for (const child of frameTree.childFrames) this._handleFrameTree(child); for (const child of frameTree.childFrames) {
this._handleFrameTree(session, child);
}
} }
page(): Page { page(): Page {
@ -272,11 +322,24 @@ export class FrameManager extends EventEmitter {
return this._frames.get(frameId) || null; return this._frames.get(frameId) || null;
} }
_onFrameAttached(frameId: string, parentFrameId?: string): void { _onFrameAttached(
if (this._frames.has(frameId)) return; session: CDPSession,
frameId: string,
parentFrameId?: string
): void {
if (this._frames.has(frameId)) {
const frame = this._frames.get(frameId);
if (session && frame.isOOPFrame()) {
// If an OOP iframes becomes a normal iframe again
// it is first attached to the parent page before
// the target is removed.
frame._updateClient(session);
}
return;
}
assert(parentFrameId); assert(parentFrameId);
const parentFrame = this._frames.get(parentFrameId); const parentFrame = this._frames.get(parentFrameId);
const frame = new Frame(this, parentFrame, frameId); const frame = new Frame(this, parentFrame, frameId, session);
this._frames.set(frame._id, frame); this._frames.set(frame._id, frame);
this.emit(FrameManagerEmittedEvents.FrameAttached, frame); this.emit(FrameManagerEmittedEvents.FrameAttached, frame);
} }
@ -305,7 +368,7 @@ export class FrameManager extends EventEmitter {
frame._id = framePayload.id; frame._id = framePayload.id;
} else { } else {
// Initial main frame navigation. // Initial main frame navigation.
frame = new Frame(this, null, framePayload.id); frame = new Frame(this, null, framePayload.id, this._client);
} }
this._frames.set(framePayload.id, frame); this._frames.set(framePayload.id, frame);
this._mainFrame = frame; this._mainFrame = frame;
@ -317,24 +380,26 @@ export class FrameManager extends EventEmitter {
this.emit(FrameManagerEmittedEvents.FrameNavigated, frame); this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
} }
async _ensureIsolatedWorld(name: string): Promise<void> { async _ensureIsolatedWorld(session: CDPSession, name: string): Promise<void> {
if (this._isolatedWorlds.has(name)) return; const key = `${session.id()}:${name}`;
this._isolatedWorlds.add(name); if (this._isolatedWorlds.has(key)) return;
await this._client.send('Page.addScriptToEvaluateOnNewDocument', { this._isolatedWorlds.add(key);
await session.send('Page.addScriptToEvaluateOnNewDocument', {
source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`, source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`,
worldName: name, worldName: name,
}); });
// Frames might be removed before we send this. // Frames might be removed before we send this.
await Promise.all( await Promise.all(
this.frames().map((frame) => this.frames()
this._client .filter((frame) => frame._client === session)
.send('Page.createIsolatedWorld', { .map((frame) =>
session.send('Page.createIsolatedWorld', {
frameId: frame._id, frameId: frame._id,
worldName: name, worldName: name,
grantUniveralAccess: true, grantUniveralAccess: true,
}) })
.catch(debugError) )
)
); );
} }
@ -346,19 +411,31 @@ export class FrameManager extends EventEmitter {
this.emit(FrameManagerEmittedEvents.FrameNavigated, frame); this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
} }
_onFrameDetached(frameId: string): void { _onFrameDetached(
frameId: string,
reason: Protocol.Page.FrameDetachedEventReason
): void {
const frame = this._frames.get(frameId); const frame = this._frames.get(frameId);
if (frame) this._removeFramesRecursively(frame); if (reason === 'remove') {
// Only remove the frame if the reason for the detached event is
// an actual removement of the frame.
// For frames that become OOP iframes, the reason would be 'swap'.
if (frame) this._removeFramesRecursively(frame);
}
} }
_onExecutionContextCreated( _onExecutionContextCreated(
contextPayload: Protocol.Runtime.ExecutionContextDescription contextPayload: Protocol.Runtime.ExecutionContextDescription,
session: CDPSession
): void { ): void {
const auxData = contextPayload.auxData as { frameId?: string }; const auxData = contextPayload.auxData as { frameId?: string };
const frameId = auxData ? auxData.frameId : null; const frameId = auxData ? auxData.frameId : null;
const frame = this._frames.get(frameId) || null; const frame = this._frames.get(frameId) || null;
let world = null; let world = null;
if (frame) { if (frame) {
// Only care about execution contexts created for the current session.
if (frame._client !== session) return;
if (contextPayload.auxData && !!contextPayload.auxData['isDefault']) { if (contextPayload.auxData && !!contextPayload.auxData['isDefault']) {
world = frame._mainWorld; world = frame._mainWorld;
} else if ( } else if (
@ -371,27 +448,43 @@ export class FrameManager extends EventEmitter {
world = frame._secondaryWorld; world = frame._secondaryWorld;
} }
} }
const context = new ExecutionContext(this._client, contextPayload, world); const context = new ExecutionContext(
frame._client || this._client,
contextPayload,
world
);
if (world) world._setContext(context); if (world) world._setContext(context);
this._contextIdToContext.set(contextPayload.id, context); const key = `${session.id()}:${contextPayload.id}`;
this._contextIdToContext.set(key, context);
} }
private _onExecutionContextDestroyed(executionContextId: number): void { private _onExecutionContextDestroyed(
const context = this._contextIdToContext.get(executionContextId); executionContextId: number,
session: CDPSession
): void {
const key = `${session.id()}:${executionContextId}`;
const context = this._contextIdToContext.get(key);
if (!context) return; if (!context) return;
this._contextIdToContext.delete(executionContextId); this._contextIdToContext.delete(key);
if (context._world) context._world._setContext(null); if (context._world) context._world._setContext(null);
} }
private _onExecutionContextsCleared(): void { private _onExecutionContextsCleared(session: CDPSession): void {
for (const context of this._contextIdToContext.values()) { for (const [key, context] of this._contextIdToContext.entries()) {
// Make sure to only clear execution contexts that belong
// to the current session.
if (context._client !== session) continue;
if (context._world) context._world._setContext(null); if (context._world) context._world._setContext(null);
this._contextIdToContext.delete(key);
} }
this._contextIdToContext.clear();
} }
executionContextById(contextId: number): ExecutionContext { executionContextById(
const context = this._contextIdToContext.get(contextId); contextId: number,
session: CDPSession = this._client
): ExecutionContext {
const key = `${session.id()}:${contextId}`;
const context = this._contextIdToContext.get(key);
assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId); assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
return context; return context;
} }
@ -563,6 +656,10 @@ export class Frame {
* @internal * @internal
*/ */
_childFrames: Set<Frame>; _childFrames: Set<Frame>;
/**
* @internal
*/
_client: CDPSession;
/** /**
* @internal * @internal
@ -570,7 +667,8 @@ export class Frame {
constructor( constructor(
frameManager: FrameManager, frameManager: FrameManager,
parentFrame: Frame | null, parentFrame: Frame | null,
frameId: string frameId: string,
client: CDPSession
) { ) {
this._frameManager = frameManager; this._frameManager = frameManager;
this._parentFrame = parentFrame; this._parentFrame = parentFrame;
@ -579,19 +677,34 @@ export class Frame {
this._detached = false; this._detached = false;
this._loaderId = ''; this._loaderId = '';
this._mainWorld = new DOMWorld(
frameManager,
this,
frameManager._timeoutSettings
);
this._secondaryWorld = new DOMWorld(
frameManager,
this,
frameManager._timeoutSettings
);
this._childFrames = new Set(); this._childFrames = new Set();
if (this._parentFrame) this._parentFrame._childFrames.add(this); if (this._parentFrame) this._parentFrame._childFrames.add(this);
this._updateClient(client);
}
/**
* @internal
*/
_updateClient(client: CDPSession): void {
this._client = client;
this._mainWorld = new DOMWorld(
this._client,
this._frameManager,
this,
this._frameManager._timeoutSettings
);
this._secondaryWorld = new DOMWorld(
this._client,
this._frameManager,
this,
this._frameManager._timeoutSettings
);
}
isOOPFrame(): boolean {
return this._client !== this._frameManager._client;
} }
/** /**

View File

@ -489,34 +489,41 @@ export class Page extends EventEmitter {
this._screenshotTaskQueue = screenshotTaskQueue; this._screenshotTaskQueue = screenshotTaskQueue;
this._viewport = null; this._viewport = null;
client.on('Target.attachedToTarget', (event) => { client.on(
if ( 'Target.attachedToTarget',
event.targetInfo.type !== 'worker' && (event: Protocol.Target.AttachedToTargetEvent) => {
event.targetInfo.type !== 'iframe' if (
) { event.targetInfo.type !== 'worker' &&
// If we don't detach from service workers, they will never die. event.targetInfo.type !== 'iframe'
// We still want to attach to workers for emitting events. ) {
// We still want to attach to iframes so sessions may interact with them. // If we don't detach from service workers, they will never die.
// We detach from all other types out of an abundance of caution. // We still want to attach to workers for emitting events.
// See https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/devtools_agent_host_impl.cc?ss=chromium&q=f:devtools%20-f:out%20%22::kTypePage%5B%5D%22 // We still want to attach to iframes so sessions may interact with them.
// for the complete list of available types. // We detach from all other types out of an abundance of caution.
client // See https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/devtools_agent_host_impl.cc?ss=chromium&q=f:devtools%20-f:out%20%22::kTypePage%5B%5D%22
.send('Target.detachFromTarget', { // for the complete list of available types.
sessionId: event.sessionId, client
}) .send('Target.detachFromTarget', {
.catch(debugError); sessionId: event.sessionId,
return; })
.catch(debugError);
return;
}
if (event.targetInfo.type === 'worker') {
const session = Connection.fromSession(client).session(
event.sessionId
);
const worker = new WebWorker(
session,
event.targetInfo.url,
this._addConsoleMessage.bind(this),
this._handleException.bind(this)
);
this._workers.set(event.sessionId, worker);
this.emit(PageEmittedEvents.WorkerCreated, worker);
}
} }
const session = Connection.fromSession(client).session(event.sessionId); );
const worker = new WebWorker(
session,
event.targetInfo.url,
this._addConsoleMessage.bind(this),
this._handleException.bind(this)
);
this._workers.set(event.sessionId, worker);
this.emit(PageEmittedEvents.WorkerCreated, worker);
});
client.on('Target.detachedFromTarget', (event) => { client.on('Target.detachedFromTarget', (event) => {
const worker = this._workers.get(event.sessionId); const worker = this._workers.get(event.sessionId);
if (!worker) return; if (!worker) return;
@ -1532,7 +1539,8 @@ export class Page extends EventEmitter {
return; return;
} }
const context = this._frameManager.executionContextById( const context = this._frameManager.executionContextById(
event.executionContextId event.executionContextId,
this._client
); );
const values = event.args.map((arg) => createJSHandle(context, arg)); const values = event.args.map((arg) => createJSHandle(context, arg));
this._addConsoleMessage(event.type, values, event.stackTrace); this._addConsoleMessage(event.type, values, event.stackTrace);
@ -2000,6 +2008,55 @@ export class Page extends EventEmitter {
); );
} }
/**
* @param urlOrPredicate - A URL or predicate to wait for.
* @param options - Optional waiting parameters
* @returns Promise which resolves to the matched frame.
* @example
* ```js
* const frame = await page.waitForFrame(async (frame) => {
* return frame.name() === 'Test';
* });
* ```
* @remarks
* Optional Parameter have:
*
* - `timeout`: Maximum wait time in milliseconds, defaults to `30` seconds,
* pass `0` to disable the timeout. The default value can be changed by using
* the {@link Page.setDefaultTimeout} method.
*/
async waitForFrame(
urlOrPredicate: string | ((frame: Frame) => boolean | Promise<boolean>),
options: { timeout?: number } = {}
): Promise<Frame> {
const { timeout = this._timeoutSettings.timeout() } = options;
async function predicate(frame: Frame) {
if (helper.isString(urlOrPredicate))
return urlOrPredicate === frame.url();
if (typeof urlOrPredicate === 'function')
return !!(await urlOrPredicate(frame));
return false;
}
return Promise.race([
helper.waitForEvent(
this._frameManager,
FrameManagerEmittedEvents.FrameAttached,
predicate,
timeout,
this._sessionClosePromise()
),
helper.waitForEvent(
this._frameManager,
FrameManagerEmittedEvents.FrameNavigated,
predicate,
timeout,
this._sessionClosePromise()
),
]);
}
/** /**
* This method navigate to the previous page in history. * This method navigate to the previous page in history.
* @param options - Navigation parameters * @param options - Navigation parameters

View File

@ -3,7 +3,7 @@ window.addEventListener('DOMContentLoaded', () => {
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
const url = new URL(location.href); const url = new URL(location.href);
url.hostname = url.hostname === 'localhost' ? '127.0.0.1' : 'localhost'; url.hostname = url.hostname === 'localhost' ? '127.0.0.1' : 'localhost';
url.pathname = '/grid.html'; url.pathname = '/oopif.html';
iframe.src = url.toString(); iframe.src = url.toString();
document.body.appendChild(iframe); document.body.appendChild(iframe);
}, false); }, false);

2
test/assets/oopif.html Normal file
View File

@ -0,0 +1,2 @@
<a id="navigate-within-document" href="#nav">Navigate within document</a>
<a name="nav"></a>

View File

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import utils from './utils.js';
import expect from 'expect'; import expect from 'expect';
import { getTestState, describeChromeOnly } from './mocha-utils'; // eslint-disable-line import/extensions import { getTestState, describeChromeOnly } from './mocha-utils'; // eslint-disable-line import/extensions
@ -47,21 +48,226 @@ describeChromeOnly('OOPIF', function () {
await browser.close(); await browser.close();
browser = null; browser = null;
}); });
xit('should report oopif frames', async () => { it('should treat OOP iframes and normal iframes the same', async () => {
const { server } = getTestState(); const { server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const framePromise = page.waitForFrame((frame) =>
frame.url().endsWith('/empty.html')
);
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
await utils.attachFrame(
page,
'frame2',
server.CROSS_PROCESS_PREFIX + '/empty.html'
);
await framePromise;
expect(page.mainFrame().childFrames()).toHaveLength(2);
});
it('should track navigations within OOP iframes', async () => {
const { server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const framePromise = page.waitForFrame((frame) => {
return page.frames().indexOf(frame) === 1;
});
await utils.attachFrame(
page,
'frame1',
server.CROSS_PROCESS_PREFIX + '/empty.html'
);
const frame = await framePromise;
expect(frame.url()).toContain('/empty.html');
await utils.navigateFrame(
page,
'frame1',
server.CROSS_PROCESS_PREFIX + '/assets/frame.html'
);
expect(frame.url()).toContain('/assets/frame.html');
});
it('should support OOP iframes becoming normal iframes again', async () => {
const { server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const framePromise = page.waitForFrame((frame) => {
return page.frames().indexOf(frame) === 1;
});
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const frame = await framePromise;
expect(frame.isOOPFrame()).toBe(false);
await utils.navigateFrame(
page,
'frame1',
server.CROSS_PROCESS_PREFIX + '/empty.html'
);
expect(frame.isOOPFrame()).toBe(true);
await utils.navigateFrame(page, 'frame1', server.EMPTY_PAGE);
expect(frame.isOOPFrame()).toBe(false);
expect(page.frames()).toHaveLength(2);
});
it('should support frames within OOP frames', async () => {
const { server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const frame1Promise = page.waitForFrame((frame) => {
return page.frames().indexOf(frame) === 1;
});
const frame2Promise = page.waitForFrame((frame) => {
return page.frames().indexOf(frame) === 2;
});
await utils.attachFrame(
page,
'frame1',
server.CROSS_PROCESS_PREFIX + '/frames/one-frame.html'
);
const [frame1, frame2] = await Promise.all([frame1Promise, frame2Promise]);
expect(await frame1.evaluate(() => document.location.href)).toMatch(
/one-frame\.html$/
);
expect(await frame2.evaluate(() => document.location.href)).toMatch(
/frames\/frame\.html$/
);
});
it('should support OOP iframes getting detached', async () => {
const { server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const framePromise = page.waitForFrame((frame) => {
return page.frames().indexOf(frame) === 1;
});
await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const frame = await framePromise;
expect(frame.isOOPFrame()).toBe(false);
await utils.navigateFrame(
page,
'frame1',
server.CROSS_PROCESS_PREFIX + '/empty.html'
);
expect(frame.isOOPFrame()).toBe(true);
await utils.detachFrame(page, 'frame1');
expect(page.frames()).toHaveLength(1);
});
it('should keep track of a frames OOP state', async () => {
const { server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const framePromise = page.waitForFrame((frame) => {
return page.frames().indexOf(frame) === 1;
});
await utils.attachFrame(
page,
'frame1',
server.CROSS_PROCESS_PREFIX + '/empty.html'
);
const frame = await framePromise;
expect(frame.url()).toContain('/empty.html');
await utils.navigateFrame(page, 'frame1', server.EMPTY_PAGE);
expect(frame.url()).toBe(server.EMPTY_PAGE);
});
it('should support evaluating in oop iframes', async () => {
const { server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const framePromise = page.waitForFrame((frame) => {
return page.frames().indexOf(frame) === 1;
});
await utils.attachFrame(
page,
'frame1',
server.CROSS_PROCESS_PREFIX + '/empty.html'
);
const frame = await framePromise;
await frame.evaluate(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
_test = 'Test 123!';
});
const result = await frame.evaluate(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return window._test;
});
expect(result).toBe('Test 123!');
});
it('should provide access to elements', async () => {
const { server } = getTestState();
await page.goto(server.EMPTY_PAGE);
const framePromise = page.waitForFrame((frame) => {
return page.frames().indexOf(frame) === 1;
});
await utils.attachFrame(
page,
'frame1',
server.CROSS_PROCESS_PREFIX + '/empty.html'
);
const frame = await framePromise;
await frame.evaluate(() => {
const button = document.createElement('button');
button.id = 'test-button';
document.body.appendChild(button);
});
await frame.click('#test-button');
});
it('should report oopif frames', async () => {
const { server } = getTestState();
const frame = page.waitForFrame((frame) =>
frame.url().endsWith('/oopif.html')
);
await page.goto(server.PREFIX + '/dynamic-oopif.html'); await page.goto(server.PREFIX + '/dynamic-oopif.html');
await frame;
expect(oopifs(context).length).toBe(1); expect(oopifs(context).length).toBe(1);
expect(page.frames().length).toBe(2); expect(page.frames().length).toBe(2);
}); });
it('should load oopif iframes with subresources and request interception', async () => { it('should load oopif iframes with subresources and request interception', async () => {
const { server } = getTestState(); const { server } = getTestState();
const frame = page.waitForFrame((frame) =>
frame.url().endsWith('/oopif.html')
);
await page.setRequestInterception(true); await page.setRequestInterception(true);
page.on('request', (request) => request.continue()); page.on('request', (request) => request.continue());
await page.goto(server.PREFIX + '/dynamic-oopif.html'); await page.goto(server.PREFIX + '/dynamic-oopif.html');
await frame;
expect(oopifs(context).length).toBe(1); expect(oopifs(context).length).toBe(1);
}); });
it('should support frames within OOP iframes', async () => {
const { server } = getTestState();
const oopIframePromise = page.waitForFrame((frame) => {
return frame.url().endsWith('/oopif.html');
});
await page.goto(server.PREFIX + '/dynamic-oopif.html');
const oopIframe = await oopIframePromise;
await utils.attachFrame(
oopIframe,
'frame1',
server.CROSS_PROCESS_PREFIX + '/empty.html'
);
const frame1 = oopIframe.childFrames()[0];
expect(frame1.url()).toMatch(/empty.html$/);
await utils.navigateFrame(
oopIframe,
'frame1',
server.CROSS_PROCESS_PREFIX + '/oopif.html'
);
expect(frame1.url()).toMatch(/oopif.html$/);
await frame1.goto(
server.CROSS_PROCESS_PREFIX + '/oopif.html#navigate-within-document',
{ waitUntil: 'load' }
);
expect(frame1.url()).toMatch(/oopif.html#navigate-within-document$/);
await utils.detachFrame(oopIframe, 'frame1');
expect(oopIframe.childFrames()).toHaveLength(0);
});
}); });
/** /**