chore: support Tab targets (#10148)
This commit is contained in:
parent
c4a4412920
commit
c4bad4a6da
@ -31,6 +31,10 @@ export enum TargetType {
|
|||||||
BROWSER = 'browser',
|
BROWSER = 'browser',
|
||||||
WEBVIEW = 'webview',
|
WEBVIEW = 'webview',
|
||||||
OTHER = 'other',
|
OTHER = 'other',
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
TAB = 'tab',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -141,6 +141,13 @@ export class Accessibility {
|
|||||||
this.#client = client;
|
this.#client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
updateClient(client: CDPSession): void {
|
||||||
|
this.#client = client;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Captures the current state of the accessibility tree.
|
* Captures the current state of the accessibility tree.
|
||||||
* The returned object represents the root accessible node of the page.
|
* The returned object represents the root accessible node of the page.
|
||||||
|
@ -442,7 +442,9 @@ export class CDPBrowser extends BrowserBase {
|
|||||||
url: 'about:blank',
|
url: 'about:blank',
|
||||||
browserContextId: contextId || undefined,
|
browserContextId: contextId || undefined,
|
||||||
});
|
});
|
||||||
const target = this.#targetManager.getAvailableTargets().get(targetId);
|
const target = (await this.waitForTarget(t => {
|
||||||
|
return (t as CDPTarget)._targetId === targetId;
|
||||||
|
})) as CDPTarget;
|
||||||
if (!target) {
|
if (!target) {
|
||||||
throw new Error(`Missing target for page (id = ${targetId})`);
|
throw new Error(`Missing target for page (id = ${targetId})`);
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,11 @@
|
|||||||
import {Protocol} from 'devtools-protocol';
|
import {Protocol} from 'devtools-protocol';
|
||||||
|
|
||||||
import {TargetFilterCallback} from '../api/Browser.js';
|
import {TargetFilterCallback} from '../api/Browser.js';
|
||||||
|
import {TargetType} from '../api/Target.js';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {Deferred} from '../util/Deferred.js';
|
import {Deferred} from '../util/Deferred.js';
|
||||||
|
|
||||||
import {CDPSession, Connection} from './Connection.js';
|
import {CDPSession, CDPSessionEmittedEvents, Connection} from './Connection.js';
|
||||||
import {EventEmitter} from './EventEmitter.js';
|
import {EventEmitter} from './EventEmitter.js';
|
||||||
import {InitializationStatus, CDPTarget} from './Target.js';
|
import {InitializationStatus, CDPTarget} from './Target.js';
|
||||||
import {
|
import {
|
||||||
@ -31,6 +32,17 @@ import {
|
|||||||
} from './TargetManager.js';
|
} from './TargetManager.js';
|
||||||
import {debugError} from './util.js';
|
import {debugError} from './util.js';
|
||||||
|
|
||||||
|
function isTargetExposed(target: CDPTarget): boolean {
|
||||||
|
return target.type() !== TargetType.TAB && !target._subtype();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPageTargetBecomingPrimary(
|
||||||
|
target: CDPTarget,
|
||||||
|
newTargetInfo: Protocol.Target.TargetInfo
|
||||||
|
): boolean {
|
||||||
|
return Boolean(target._subtype()) && !newTargetInfo.subtype;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ChromeTargetManager uses the CDP's auto-attach mechanism to intercept
|
* ChromeTargetManager uses the CDP's auto-attach mechanism to intercept
|
||||||
* new targets and allow the rest of Puppeteer to configure listeners while
|
* new targets and allow the rest of Puppeteer to configure listeners while
|
||||||
@ -86,6 +98,10 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
|||||||
#targetsIdsForInit = new Set<string>();
|
#targetsIdsForInit = new Set<string>();
|
||||||
#waitForInitiallyDiscoveredTargets = true;
|
#waitForInitiallyDiscoveredTargets = true;
|
||||||
|
|
||||||
|
// TODO: remove the flag once the testing/rollout is done.
|
||||||
|
#tabMode = false;
|
||||||
|
#discoveryFilter = this.#tabMode ? [{}] : [{type: 'tab', exclude: true}, {}];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
targetFactory: TargetFactory,
|
targetFactory: TargetFactory,
|
||||||
@ -107,7 +123,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
|||||||
this.#connection
|
this.#connection
|
||||||
.send('Target.setDiscoverTargets', {
|
.send('Target.setDiscoverTargets', {
|
||||||
discover: true,
|
discover: true,
|
||||||
filter: [{type: 'tab', exclude: true}, {}],
|
filter: this.#discoveryFilter,
|
||||||
})
|
})
|
||||||
.then(this.#storeExistingTargetsForInit)
|
.then(this.#storeExistingTargetsForInit)
|
||||||
.catch(debugError);
|
.catch(debugError);
|
||||||
@ -143,6 +159,15 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
|||||||
waitForDebuggerOnStart: true,
|
waitForDebuggerOnStart: true,
|
||||||
flatten: true,
|
flatten: true,
|
||||||
autoAttach: true,
|
autoAttach: true,
|
||||||
|
filter: this.#tabMode
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: 'page',
|
||||||
|
exclude: true,
|
||||||
|
},
|
||||||
|
...this.#discoveryFilter,
|
||||||
|
]
|
||||||
|
: this.#discoveryFilter,
|
||||||
});
|
});
|
||||||
this.#finishInitializationIfReady();
|
this.#finishInitializationIfReady();
|
||||||
await this.#initializeDeferred.valueOrThrow();
|
await this.#initializeDeferred.valueOrThrow();
|
||||||
@ -158,7 +183,13 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAvailableTargets(): Map<string, CDPTarget> {
|
getAvailableTargets(): Map<string, CDPTarget> {
|
||||||
return this.#attachedTargetsByTargetId;
|
const result = new Map<string, CDPTarget>();
|
||||||
|
for (const [id, target] of this.#attachedTargetsByTargetId.entries()) {
|
||||||
|
if (isTargetExposed(target)) {
|
||||||
|
result.set(id, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
addTargetInterceptor(
|
addTargetInterceptor(
|
||||||
@ -285,6 +316,18 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
|||||||
const wasInitialized =
|
const wasInitialized =
|
||||||
target._initializedDeferred.value() === InitializationStatus.SUCCESS;
|
target._initializedDeferred.value() === InitializationStatus.SUCCESS;
|
||||||
|
|
||||||
|
if (isPageTargetBecomingPrimary(target, event.targetInfo)) {
|
||||||
|
const target = this.#attachedTargetsByTargetId.get(
|
||||||
|
event.targetInfo.targetId
|
||||||
|
);
|
||||||
|
const session = target?._session();
|
||||||
|
assert(
|
||||||
|
session,
|
||||||
|
'Target that is being activated is missing a CDPSession.'
|
||||||
|
);
|
||||||
|
session.parentSession()?.emit(CDPSessionEmittedEvents.Swapped, session);
|
||||||
|
}
|
||||||
|
|
||||||
target._targetInfoChanged(event.targetInfo);
|
target._targetInfoChanged(event.targetInfo);
|
||||||
|
|
||||||
if (wasInitialized && previousURL !== target.url()) {
|
if (wasInitialized && previousURL !== target.url()) {
|
||||||
@ -350,7 +393,11 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
|||||||
|
|
||||||
const target = existingTarget
|
const target = existingTarget
|
||||||
? this.#attachedTargetsByTargetId.get(targetInfo.targetId)!
|
? this.#attachedTargetsByTargetId.get(targetInfo.targetId)!
|
||||||
: this.#targetFactory(targetInfo, session);
|
: this.#targetFactory(
|
||||||
|
targetInfo,
|
||||||
|
session,
|
||||||
|
parentSession instanceof CDPSession ? parentSession : undefined
|
||||||
|
);
|
||||||
|
|
||||||
if (this.#targetFilterCallback && !this.#targetFilterCallback(target)) {
|
if (this.#targetFilterCallback && !this.#targetFilterCallback(target)) {
|
||||||
this.#ignoredTargets.add(targetInfo.targetId);
|
this.#ignoredTargets.add(targetInfo.targetId);
|
||||||
@ -391,7 +438,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.#targetsIdsForInit.delete(target._targetId);
|
this.#targetsIdsForInit.delete(target._targetId);
|
||||||
if (!existingTarget) {
|
if (!existingTarget && isTargetExposed(target)) {
|
||||||
this.emit(TargetManagerEmittedEvents.TargetAvailable, target);
|
this.emit(TargetManagerEmittedEvents.TargetAvailable, target);
|
||||||
}
|
}
|
||||||
this.#finishInitializationIfReady();
|
this.#finishInitializationIfReady();
|
||||||
@ -403,6 +450,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
|||||||
waitForDebuggerOnStart: true,
|
waitForDebuggerOnStart: true,
|
||||||
flatten: true,
|
flatten: true,
|
||||||
autoAttach: true,
|
autoAttach: true,
|
||||||
|
filter: this.#discoveryFilter,
|
||||||
}),
|
}),
|
||||||
session.send('Runtime.runIfWaitingForDebugger'),
|
session.send('Runtime.runIfWaitingForDebugger'),
|
||||||
]).catch(debugError);
|
]).catch(debugError);
|
||||||
@ -428,6 +476,8 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.#attachedTargetsByTargetId.delete(target._targetId);
|
this.#attachedTargetsByTargetId.delete(target._targetId);
|
||||||
this.emit(TargetManagerEmittedEvents.TargetGone, target);
|
if (isTargetExposed(target)) {
|
||||||
|
this.emit(TargetManagerEmittedEvents.TargetGone, target);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import {ConnectionTransport} from './ConnectionTransport.js';
|
|||||||
import {debug} from './Debug.js';
|
import {debug} from './Debug.js';
|
||||||
import {TargetCloseError, ProtocolError} from './Errors.js';
|
import {TargetCloseError, ProtocolError} from './Errors.js';
|
||||||
import {EventEmitter} from './EventEmitter.js';
|
import {EventEmitter} from './EventEmitter.js';
|
||||||
|
import {CDPTarget} from './Target.js';
|
||||||
import {debugError} from './util.js';
|
import {debugError} from './util.js';
|
||||||
|
|
||||||
const debugProtocolSend = debug('puppeteer:protocol:SEND ►');
|
const debugProtocolSend = debug('puppeteer:protocol:SEND ►');
|
||||||
@ -429,6 +430,7 @@ export interface CDPSessionOnMessageObject {
|
|||||||
*/
|
*/
|
||||||
export const CDPSessionEmittedEvents = {
|
export const CDPSessionEmittedEvents = {
|
||||||
Disconnected: Symbol('CDPSession.Disconnected'),
|
Disconnected: Symbol('CDPSession.Disconnected'),
|
||||||
|
Swapped: Symbol('CDPSession.Swapped'),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -515,6 +517,7 @@ export class CDPSessionImpl extends CDPSession {
|
|||||||
#callbacks = new CallbackRegistry();
|
#callbacks = new CallbackRegistry();
|
||||||
#connection?: Connection;
|
#connection?: Connection;
|
||||||
#parentSessionId?: string;
|
#parentSessionId?: string;
|
||||||
|
#target?: CDPTarget;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
@ -532,6 +535,25 @@ export class CDPSessionImpl extends CDPSession {
|
|||||||
this.#parentSessionId = parentSessionId;
|
this.#parentSessionId = parentSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the CDPTarget associated with the session instance.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_setTarget(target: CDPTarget): void {
|
||||||
|
this.#target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the CDPTarget associated with the session instance.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_target(): CDPTarget {
|
||||||
|
assert(this.#target, 'Target must exist');
|
||||||
|
return this.#target;
|
||||||
|
}
|
||||||
|
|
||||||
override connection(): Connection | undefined {
|
override connection(): Connection | undefined {
|
||||||
return this.#connection;
|
return this.#connection;
|
||||||
}
|
}
|
||||||
|
@ -144,6 +144,14 @@ export class Coverage {
|
|||||||
this.#cssCoverage = new CSSCoverage(client);
|
this.#cssCoverage = new CSSCoverage(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
updateClient(client: CDPSession): void {
|
||||||
|
this.#jsCoverage.updateClient(client);
|
||||||
|
this.#cssCoverage.updateClient(client);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param options - Set of configurable options for coverage defaults to
|
* @param options - Set of configurable options for coverage defaults to
|
||||||
* `resetOnNavigation : true, reportAnonymousScripts : false,`
|
* `resetOnNavigation : true, reportAnonymousScripts : false,`
|
||||||
@ -212,6 +220,13 @@ export class JSCoverage {
|
|||||||
this.#client = client;
|
this.#client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
updateClient(client: CDPSession): void {
|
||||||
|
this.#client = client;
|
||||||
|
}
|
||||||
|
|
||||||
async start(
|
async start(
|
||||||
options: {
|
options: {
|
||||||
resetOnNavigation?: boolean;
|
resetOnNavigation?: boolean;
|
||||||
@ -342,6 +357,13 @@ export class CSSCoverage {
|
|||||||
this.#client = client;
|
this.#client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
updateClient(client: CDPSession): void {
|
||||||
|
this.#client = client;
|
||||||
|
}
|
||||||
|
|
||||||
async start(options: {resetOnNavigation?: boolean} = {}): Promise<void> {
|
async start(options: {resetOnNavigation?: boolean} = {}): Promise<void> {
|
||||||
assert(!this.#enabled, 'CSSCoverage is already enabled');
|
assert(!this.#enabled, 'CSSCoverage is already enabled');
|
||||||
const {resetOnNavigation = true} = options;
|
const {resetOnNavigation = true} = options;
|
||||||
|
@ -35,6 +35,10 @@ export class EmulationManager {
|
|||||||
this.#client = client;
|
this.#client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateClient(client: CDPSession): void {
|
||||||
|
this.#client = client;
|
||||||
|
}
|
||||||
|
|
||||||
get javascriptEnabled(): boolean {
|
get javascriptEnabled(): boolean {
|
||||||
return this.#javascriptEnabled;
|
return this.#javascriptEnabled;
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ export const FrameEmittedEvents = {
|
|||||||
LifecycleEvent: Symbol('Frame.LifecycleEvent'),
|
LifecycleEvent: Symbol('Frame.LifecycleEvent'),
|
||||||
FrameNavigatedWithinDocument: Symbol('Frame.FrameNavigatedWithinDocument'),
|
FrameNavigatedWithinDocument: Symbol('Frame.FrameNavigatedWithinDocument'),
|
||||||
FrameDetached: Symbol('Frame.FrameDetached'),
|
FrameDetached: Symbol('Frame.FrameDetached'),
|
||||||
|
FrameSwappedByActivation: Symbol('Frame.FrameSwappedByActivation'),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -82,14 +83,33 @@ export class Frame extends BaseFrame {
|
|||||||
this._loaderId = '';
|
this._loaderId = '';
|
||||||
|
|
||||||
this.updateClient(client);
|
this.updateClient(client);
|
||||||
|
|
||||||
|
this.on(FrameEmittedEvents.FrameSwappedByActivation, () => {
|
||||||
|
// Emulate loading process for swapped frames.
|
||||||
|
this._onLoadingStarted();
|
||||||
|
this._onLoadingStopped();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateClient(client: CDPSession): void {
|
/**
|
||||||
|
* Updates the frame ID with the new ID. This happens when the main frame is
|
||||||
|
* replaced by a different frame.
|
||||||
|
*/
|
||||||
|
updateId(id: string): void {
|
||||||
|
this._id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClient(client: CDPSession, keepWorlds = false): void {
|
||||||
this.#client = client;
|
this.#client = client;
|
||||||
this.worlds = {
|
if (!keepWorlds) {
|
||||||
[MAIN_WORLD]: new IsolatedWorld(this),
|
this.worlds = {
|
||||||
[PUPPETEER_WORLD]: new IsolatedWorld(this),
|
[MAIN_WORLD]: new IsolatedWorld(this),
|
||||||
};
|
[PUPPETEER_WORLD]: new IsolatedWorld(this),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.worlds[MAIN_WORLD].frameUpdated();
|
||||||
|
this.worlds[PUPPETEER_WORLD].frameUpdated();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override page(): Page {
|
override page(): Page {
|
||||||
|
@ -18,11 +18,13 @@ import {Protocol} from 'devtools-protocol';
|
|||||||
|
|
||||||
import {Page} from '../api/Page.js';
|
import {Page} from '../api/Page.js';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
|
import {Deferred} from '../util/Deferred.js';
|
||||||
import {isErrorLike} from '../util/ErrorLike.js';
|
import {isErrorLike} from '../util/ErrorLike.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CDPSession,
|
CDPSession,
|
||||||
CDPSessionEmittedEvents,
|
CDPSessionEmittedEvents,
|
||||||
|
CDPSessionImpl,
|
||||||
isTargetClosedError,
|
isTargetClosedError,
|
||||||
} from './Connection.js';
|
} from './Connection.js';
|
||||||
import {DeviceRequestPromptManager} from './DeviceRequestPrompt.js';
|
import {DeviceRequestPromptManager} from './DeviceRequestPrompt.js';
|
||||||
@ -59,6 +61,8 @@ export const FrameManagerEmittedEvents = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TIME_FOR_WAITING_FOR_SWAP = 100; // ms.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A frame manager manages the frames for a given {@link Page | page}.
|
* A frame manager manages the frames for a given {@link Page | page}.
|
||||||
*
|
*
|
||||||
@ -113,13 +117,71 @@ export class FrameManager extends EventEmitter {
|
|||||||
this.#timeoutSettings = timeoutSettings;
|
this.#timeoutSettings = timeoutSettings;
|
||||||
this.setupEventListeners(this.#client);
|
this.setupEventListeners(this.#client);
|
||||||
client.once(CDPSessionEmittedEvents.Disconnected, () => {
|
client.once(CDPSessionEmittedEvents.Disconnected, () => {
|
||||||
const mainFrame = this._frameTree.getMainFrame();
|
this.#onClientDisconnect().catch(debugError);
|
||||||
if (mainFrame) {
|
|
||||||
this.#removeFramesRecursively(mainFrame);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the frame's client is disconnected. We don't know if the
|
||||||
|
* disconnect means that the frame is removed or if it will be replaced by a
|
||||||
|
* new frame. Therefore, we wait for a swap event.
|
||||||
|
*/
|
||||||
|
async #onClientDisconnect() {
|
||||||
|
const mainFrame = this._frameTree.getMainFrame();
|
||||||
|
if (!mainFrame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const child of mainFrame.childFrames()) {
|
||||||
|
this.#removeFramesRecursively(child);
|
||||||
|
}
|
||||||
|
const swapped = Deferred.create<void>({
|
||||||
|
timeout: TIME_FOR_WAITING_FOR_SWAP,
|
||||||
|
message: 'Frame was not swapped',
|
||||||
|
});
|
||||||
|
mainFrame.once(FrameEmittedEvents.FrameSwappedByActivation, () => {
|
||||||
|
swapped.resolve();
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await swapped.valueOrThrow();
|
||||||
|
} catch (err) {
|
||||||
|
this.#removeFramesRecursively(mainFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the main frame is replaced by another main frame,
|
||||||
|
* we maintain the main frame object identity while updating
|
||||||
|
* its frame tree and ID.
|
||||||
|
*/
|
||||||
|
async swapFrameTree(client: CDPSession): Promise<void> {
|
||||||
|
this.#onExecutionContextsCleared(this.#client);
|
||||||
|
|
||||||
|
this.#client = client;
|
||||||
|
assert(
|
||||||
|
this.#client instanceof CDPSessionImpl,
|
||||||
|
'CDPSession is not an instance of CDPSessionImpl.'
|
||||||
|
);
|
||||||
|
const frame = this._frameTree.getMainFrame();
|
||||||
|
if (frame) {
|
||||||
|
this.#frameNavigatedReceived.add(this.#client._target()._targetId);
|
||||||
|
this._frameTree.removeFrame(frame);
|
||||||
|
frame.updateId(this.#client._target()._targetId);
|
||||||
|
frame.mainRealm().clearContext();
|
||||||
|
frame.isolatedRealm().clearContext();
|
||||||
|
this._frameTree.addFrame(frame);
|
||||||
|
frame.updateClient(client, true);
|
||||||
|
}
|
||||||
|
this.setupEventListeners(client);
|
||||||
|
client.once(CDPSessionEmittedEvents.Disconnected, () => {
|
||||||
|
this.#onClientDisconnect().catch(debugError);
|
||||||
|
});
|
||||||
|
await this.initialize(client);
|
||||||
|
await this.#networkManager.updateClient(client);
|
||||||
|
if (frame) {
|
||||||
|
frame.emit(FrameEmittedEvents.FrameSwappedByActivation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private setupEventListeners(session: CDPSession) {
|
private setupEventListeners(session: CDPSession) {
|
||||||
session.on('Page.frameAttached', event => {
|
session.on('Page.frameAttached', event => {
|
||||||
this.#onFrameAttached(session, event.frameId, event.parentFrameId);
|
this.#onFrameAttached(session, event.frameId, event.parentFrameId);
|
||||||
|
@ -59,6 +59,13 @@ export class CDPKeyboard extends Keyboard {
|
|||||||
this.#client = client;
|
this.#client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
updateClient(client: CDPSession): void {
|
||||||
|
this.#client = client;
|
||||||
|
}
|
||||||
|
|
||||||
override async down(
|
override async down(
|
||||||
key: KeyInput,
|
key: KeyInput,
|
||||||
options: Readonly<KeyDownOptions> = {
|
options: Readonly<KeyDownOptions> = {
|
||||||
@ -290,6 +297,13 @@ export class CDPMouse extends Mouse {
|
|||||||
this.#keyboard = keyboard;
|
this.#keyboard = keyboard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
updateClient(client: CDPSession): void {
|
||||||
|
this.#client = client;
|
||||||
|
}
|
||||||
|
|
||||||
#_state: Readonly<MouseState> = {
|
#_state: Readonly<MouseState> = {
|
||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
buttons: MouseButtonFlag.None,
|
buttons: MouseButtonFlag.None,
|
||||||
@ -571,6 +585,13 @@ export class CDPTouchscreen extends Touchscreen {
|
|||||||
this.#keyboard = keyboard;
|
this.#keyboard = keyboard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
updateClient(client: CDPSession): void {
|
||||||
|
this.#client = client;
|
||||||
|
}
|
||||||
|
|
||||||
override async tap(x: number, y: number): Promise<void> {
|
override async tap(x: number, y: number): Promise<void> {
|
||||||
await this.touchStart(x, y);
|
await this.touchStart(x, y);
|
||||||
await this.touchEnd();
|
await this.touchEnd();
|
||||||
|
@ -124,9 +124,11 @@ export class IsolatedWorld implements Realm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(frame: Frame) {
|
constructor(frame: Frame) {
|
||||||
// Keep own reference to client because it might differ from the FrameManager's
|
|
||||||
// client for OOP iframes.
|
|
||||||
this.#frame = frame;
|
this.#frame = frame;
|
||||||
|
this.frameUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
frameUpdated(): void {
|
||||||
this.#client.on('Runtime.bindingCalled', this.#onBindingCalled);
|
this.#client.on('Runtime.bindingCalled', this.#onBindingCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,6 +117,11 @@ export class LifecycleWatcher {
|
|||||||
FrameEmittedEvents.FrameSwapped,
|
FrameEmittedEvents.FrameSwapped,
|
||||||
this.#frameSwapped.bind(this)
|
this.#frameSwapped.bind(this)
|
||||||
),
|
),
|
||||||
|
addEventListener(
|
||||||
|
frame,
|
||||||
|
FrameEmittedEvents.FrameSwappedByActivation,
|
||||||
|
this.#frameSwapped.bind(this)
|
||||||
|
),
|
||||||
addEventListener(
|
addEventListener(
|
||||||
frame,
|
frame,
|
||||||
FrameEmittedEvents.FrameDetached,
|
FrameEmittedEvents.FrameDetached,
|
||||||
|
@ -21,7 +21,7 @@ import {createDebuggableDeferred} from '../util/DebuggableDeferred.js';
|
|||||||
import {Deferred} from '../util/Deferred.js';
|
import {Deferred} from '../util/Deferred.js';
|
||||||
|
|
||||||
import {CDPSession} from './Connection.js';
|
import {CDPSession} from './Connection.js';
|
||||||
import {EventEmitter} from './EventEmitter.js';
|
import {EventEmitter, Handler} from './EventEmitter.js';
|
||||||
import {FrameManager} from './FrameManager.js';
|
import {FrameManager} from './FrameManager.js';
|
||||||
import {HTTPRequest} from './HTTPRequest.js';
|
import {HTTPRequest} from './HTTPRequest.js';
|
||||||
import {HTTPResponse} from './HTTPResponse.js';
|
import {HTTPResponse} from './HTTPResponse.js';
|
||||||
@ -90,6 +90,23 @@ export class NetworkManager extends EventEmitter {
|
|||||||
};
|
};
|
||||||
#deferredInit?: Deferred<void>;
|
#deferredInit?: Deferred<void>;
|
||||||
|
|
||||||
|
#handlers = new Map<string, Handler<any>>([
|
||||||
|
['Fetch.requestPaused', this.#onRequestPaused.bind(this)],
|
||||||
|
['Fetch.authRequired', this.#onAuthRequired.bind(this)],
|
||||||
|
['Network.requestWillBeSent', this.#onRequestWillBeSent.bind(this)],
|
||||||
|
[
|
||||||
|
'Network.requestServedFromCache',
|
||||||
|
this.#onRequestServedFromCache.bind(this),
|
||||||
|
],
|
||||||
|
['Network.responseReceived', this.#onResponseReceived.bind(this)],
|
||||||
|
['Network.loadingFinished', this.#onLoadingFinished.bind(this)],
|
||||||
|
['Network.loadingFailed', this.#onLoadingFailed.bind(this)],
|
||||||
|
[
|
||||||
|
'Network.responseReceivedExtraInfo',
|
||||||
|
this.#onResponseReceivedExtraInfo.bind(this),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
client: CDPSession,
|
client: CDPSession,
|
||||||
ignoreHTTPSErrors: boolean,
|
ignoreHTTPSErrors: boolean,
|
||||||
@ -100,29 +117,18 @@ export class NetworkManager extends EventEmitter {
|
|||||||
this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
|
this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||||
this.#frameManager = frameManager;
|
this.#frameManager = frameManager;
|
||||||
|
|
||||||
this.#client.on('Fetch.requestPaused', this.#onRequestPaused.bind(this));
|
for (const [event, handler] of this.#handlers) {
|
||||||
this.#client.on('Fetch.authRequired', this.#onAuthRequired.bind(this));
|
this.#client.on(event, handler);
|
||||||
this.#client.on(
|
}
|
||||||
'Network.requestWillBeSent',
|
}
|
||||||
this.#onRequestWillBeSent.bind(this)
|
|
||||||
);
|
async updateClient(client: CDPSession): Promise<void> {
|
||||||
this.#client.on(
|
this.#client = client;
|
||||||
'Network.requestServedFromCache',
|
for (const [event, handler] of this.#handlers) {
|
||||||
this.#onRequestServedFromCache.bind(this)
|
this.#client.on(event, handler);
|
||||||
);
|
}
|
||||||
this.#client.on(
|
this.#deferredInit = undefined;
|
||||||
'Network.responseReceived',
|
await this.initialize();
|
||||||
this.#onResponseReceived.bind(this)
|
|
||||||
);
|
|
||||||
this.#client.on(
|
|
||||||
'Network.loadingFinished',
|
|
||||||
this.#onLoadingFinished.bind(this)
|
|
||||||
);
|
|
||||||
this.#client.on('Network.loadingFailed', this.#onLoadingFailed.bind(this));
|
|
||||||
this.#client.on(
|
|
||||||
'Network.responseReceivedExtraInfo',
|
|
||||||
this.#onResponseReceivedExtraInfo.bind(this)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,6 +46,7 @@ import {Binding} from './Binding.js';
|
|||||||
import {
|
import {
|
||||||
CDPSession,
|
CDPSession,
|
||||||
CDPSessionEmittedEvents,
|
CDPSessionEmittedEvents,
|
||||||
|
CDPSessionImpl,
|
||||||
isTargetClosedError,
|
isTargetClosedError,
|
||||||
} from './Connection.js';
|
} from './Connection.js';
|
||||||
import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js';
|
import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js';
|
||||||
@ -127,6 +128,7 @@ export class CDPPage extends Page {
|
|||||||
|
|
||||||
#closed = false;
|
#closed = false;
|
||||||
#client: CDPSession;
|
#client: CDPSession;
|
||||||
|
#tabSession: CDPSession | undefined;
|
||||||
#target: CDPTarget;
|
#target: CDPTarget;
|
||||||
#keyboard: CDPKeyboard;
|
#keyboard: CDPKeyboard;
|
||||||
#mouse: CDPMouse;
|
#mouse: CDPMouse;
|
||||||
@ -289,6 +291,7 @@ export class CDPPage extends Page {
|
|||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.#client = client;
|
this.#client = client;
|
||||||
|
this.#tabSession = client.parentSession();
|
||||||
this.#target = target;
|
this.#target = target;
|
||||||
this.#keyboard = new CDPKeyboard(client);
|
this.#keyboard = new CDPKeyboard(client);
|
||||||
this.#mouse = new CDPMouse(client, this.#keyboard);
|
this.#mouse = new CDPMouse(client, this.#keyboard);
|
||||||
@ -307,6 +310,25 @@ export class CDPPage extends Page {
|
|||||||
this.#viewport = null;
|
this.#viewport = null;
|
||||||
|
|
||||||
this.#setupEventListeners();
|
this.#setupEventListeners();
|
||||||
|
|
||||||
|
this.#tabSession?.on(CDPSessionEmittedEvents.Swapped, async newSession => {
|
||||||
|
this.#client = newSession;
|
||||||
|
assert(
|
||||||
|
this.#client instanceof CDPSessionImpl,
|
||||||
|
'CDPSession is not instance of CDPSessionImpl'
|
||||||
|
);
|
||||||
|
this.#target = this.#client._target();
|
||||||
|
assert(this.#target, 'Missing target on swap');
|
||||||
|
this.#keyboard.updateClient(newSession);
|
||||||
|
this.#mouse.updateClient(newSession);
|
||||||
|
this.#touchscreen.updateClient(newSession);
|
||||||
|
this.#accessibility.updateClient(newSession);
|
||||||
|
this.#emulationManager.updateClient(newSession);
|
||||||
|
this.#tracing.updateClient(newSession);
|
||||||
|
this.#coverage.updateClient(newSession);
|
||||||
|
await this.#frameManager.swapFrameTree(newSession);
|
||||||
|
this.#setupEventListeners();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#setupEventListeners() {
|
#setupEventListeners() {
|
||||||
|
@ -22,7 +22,7 @@ import {Page, PageEmittedEvents} from '../api/Page.js';
|
|||||||
import {Target, TargetType} from '../api/Target.js';
|
import {Target, TargetType} from '../api/Target.js';
|
||||||
import {Deferred} from '../util/Deferred.js';
|
import {Deferred} from '../util/Deferred.js';
|
||||||
|
|
||||||
import {CDPSession} from './Connection.js';
|
import {CDPSession, CDPSessionImpl} from './Connection.js';
|
||||||
import {CDPPage} from './Page.js';
|
import {CDPPage} from './Page.js';
|
||||||
import {Viewport} from './PuppeteerViewport.js';
|
import {Viewport} from './PuppeteerViewport.js';
|
||||||
import {TargetManager} from './TargetManager.js';
|
import {TargetManager} from './TargetManager.js';
|
||||||
@ -84,6 +84,16 @@ export class CDPTarget extends Target {
|
|||||||
this.#browserContext = browserContext;
|
this.#browserContext = browserContext;
|
||||||
this._targetId = targetInfo.targetId;
|
this._targetId = targetInfo.targetId;
|
||||||
this.#sessionFactory = sessionFactory;
|
this.#sessionFactory = sessionFactory;
|
||||||
|
if (this.#session && this.#session instanceof CDPSessionImpl) {
|
||||||
|
this.#session._setTarget(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_subtype(): string | undefined {
|
||||||
|
return this.#targetInfo.subtype;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,7 +119,10 @@ export class CDPTarget extends Target {
|
|||||||
if (!this.#sessionFactory) {
|
if (!this.#sessionFactory) {
|
||||||
throw new Error('sessionFactory is not initialized');
|
throw new Error('sessionFactory is not initialized');
|
||||||
}
|
}
|
||||||
return this.#sessionFactory(false);
|
return this.#sessionFactory(false).then(session => {
|
||||||
|
(session as CDPSessionImpl)._setTarget(this);
|
||||||
|
return session;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
override url(): string {
|
override url(): string {
|
||||||
@ -131,6 +144,8 @@ export class CDPTarget extends Target {
|
|||||||
return TargetType.BROWSER;
|
return TargetType.BROWSER;
|
||||||
case 'webview':
|
case 'webview':
|
||||||
return TargetType.WEBVIEW;
|
return TargetType.WEBVIEW;
|
||||||
|
case 'tab':
|
||||||
|
return TargetType.TAB;
|
||||||
default:
|
default:
|
||||||
return TargetType.OTHER;
|
return TargetType.OTHER;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,8 @@ import {CDPTarget} from './Target.js';
|
|||||||
*/
|
*/
|
||||||
export type TargetFactory = (
|
export type TargetFactory = (
|
||||||
targetInfo: Protocol.Target.TargetInfo,
|
targetInfo: Protocol.Target.TargetInfo,
|
||||||
session?: CDPSession
|
session?: CDPSession,
|
||||||
|
parentSession?: CDPSession
|
||||||
) => CDPTarget;
|
) => CDPTarget;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -57,6 +57,13 @@ export class Tracing {
|
|||||||
this.#client = client;
|
this.#client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
updateClient(client: CDPSession): void {
|
||||||
|
this.#client = client;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a trace for the current page.
|
* Starts a trace for the current page.
|
||||||
* @remarks
|
* @remarks
|
||||||
|
@ -178,7 +178,7 @@ export class ChromeLauncher extends ProductLauncher {
|
|||||||
'--disable-dev-shm-usage',
|
'--disable-dev-shm-usage',
|
||||||
'--disable-extensions',
|
'--disable-extensions',
|
||||||
// AcceptCHFrame disabled because of crbug.com/1348106.
|
// AcceptCHFrame disabled because of crbug.com/1348106.
|
||||||
'--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints',
|
'--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints,Prerender2',
|
||||||
'--disable-hang-monitor',
|
'--disable-hang-monitor',
|
||||||
'--disable-ipc-flooding-protection',
|
'--disable-ipc-flooding-protection',
|
||||||
'--disable-popup-blocking',
|
'--disable-popup-blocking',
|
||||||
|
21
test/assets/prerender/index.html
Normal file
21
test/assets/prerender/index.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<script>
|
||||||
|
function addRules() {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.type = 'speculationrules';
|
||||||
|
script.innerText = `
|
||||||
|
{
|
||||||
|
"prerender": [
|
||||||
|
{"source": "list", "urls": ["target.html"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.append(script);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button onclick="addRules()">add rules</button>
|
||||||
|
<a href="target.html">test</a>
|
||||||
|
</body>
|
4
test/assets/prerender/target.html
Normal file
4
test/assets/prerender/target.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head></head>
|
||||||
|
</head>
|
||||||
|
<body>target</body>
|
@ -82,7 +82,7 @@ describe('Launcher specs', function () {
|
|||||||
});
|
});
|
||||||
remote.disconnect();
|
remote.disconnect();
|
||||||
const error = await watchdog;
|
const error = await watchdog;
|
||||||
expect(error.message).toContain('frame got detached');
|
expect(error.message).toContain('Session closed.');
|
||||||
} finally {
|
} finally {
|
||||||
await close();
|
await close();
|
||||||
}
|
}
|
||||||
|
92
test/src/prerender.spec.ts
Normal file
92
test/src/prerender.spec.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2023 Google Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import expect from 'expect';
|
||||||
|
|
||||||
|
import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
|
||||||
|
|
||||||
|
describe('Prerender', function () {
|
||||||
|
setupTestBrowserHooks();
|
||||||
|
|
||||||
|
it('can navigate to a prerendered page via input', async () => {
|
||||||
|
const {page, server} = await getTestState();
|
||||||
|
await page.goto(server.PREFIX + '/prerender/index.html');
|
||||||
|
|
||||||
|
const button = await page.waitForSelector('button');
|
||||||
|
await button?.click();
|
||||||
|
|
||||||
|
const link = await page.waitForSelector('a');
|
||||||
|
await Promise.all([page.waitForNavigation(), link?.click()]);
|
||||||
|
expect(
|
||||||
|
await page.evaluate(() => {
|
||||||
|
return document.body.innerText;
|
||||||
|
})
|
||||||
|
).toBe('target');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can navigate to a prerendered page via Puppeteer', async () => {
|
||||||
|
const {page, server} = await getTestState();
|
||||||
|
await page.goto(server.PREFIX + '/prerender/index.html');
|
||||||
|
|
||||||
|
const button = await page.waitForSelector('button');
|
||||||
|
await button?.click();
|
||||||
|
|
||||||
|
await page.goto(server.PREFIX + '/prerender/target.html');
|
||||||
|
expect(
|
||||||
|
await page.evaluate(() => {
|
||||||
|
return document.body.innerText;
|
||||||
|
})
|
||||||
|
).toBe('target');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('via frame', () => {
|
||||||
|
it('can navigate to a prerendered page via input', async () => {
|
||||||
|
const {page, server} = await getTestState();
|
||||||
|
await page.goto(server.PREFIX + '/prerender/index.html');
|
||||||
|
|
||||||
|
const button = await page.waitForSelector('button');
|
||||||
|
await button?.click();
|
||||||
|
|
||||||
|
const mainFrame = page.mainFrame();
|
||||||
|
const link = await mainFrame.waitForSelector('a');
|
||||||
|
await Promise.all([mainFrame.waitForNavigation(), link?.click()]);
|
||||||
|
expect(mainFrame).toBe(page.mainFrame());
|
||||||
|
expect(
|
||||||
|
await mainFrame.evaluate(() => {
|
||||||
|
return document.body.innerText;
|
||||||
|
})
|
||||||
|
).toBe('target');
|
||||||
|
expect(mainFrame).toBe(page.mainFrame());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can navigate to a prerendered page via Puppeteer', async () => {
|
||||||
|
const {page, server} = await getTestState();
|
||||||
|
await page.goto(server.PREFIX + '/prerender/index.html');
|
||||||
|
|
||||||
|
const button = await page.waitForSelector('button');
|
||||||
|
await button?.click();
|
||||||
|
|
||||||
|
const mainFrame = page.mainFrame();
|
||||||
|
await mainFrame.goto(server.PREFIX + '/prerender/target.html');
|
||||||
|
expect(
|
||||||
|
await mainFrame.evaluate(() => {
|
||||||
|
return document.body.innerText;
|
||||||
|
})
|
||||||
|
).toBe('target');
|
||||||
|
expect(mainFrame).toBe(page.mainFrame());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user