chore: support Tab targets (#10148)
This commit is contained in:
parent
c4a4412920
commit
c4bad4a6da
@ -31,6 +31,10 @@ export enum TargetType {
|
||||
BROWSER = 'browser',
|
||||
WEBVIEW = 'webview',
|
||||
OTHER = 'other',
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
TAB = 'tab',
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -141,6 +141,13 @@ export class Accessibility {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures the current state of the accessibility tree.
|
||||
* The returned object represents the root accessible node of the page.
|
||||
|
@ -442,7 +442,9 @@ export class CDPBrowser extends BrowserBase {
|
||||
url: 'about:blank',
|
||||
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) {
|
||||
throw new Error(`Missing target for page (id = ${targetId})`);
|
||||
}
|
||||
|
@ -17,10 +17,11 @@
|
||||
import {Protocol} from 'devtools-protocol';
|
||||
|
||||
import {TargetFilterCallback} from '../api/Browser.js';
|
||||
import {TargetType} from '../api/Target.js';
|
||||
import {assert} from '../util/assert.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 {InitializationStatus, CDPTarget} from './Target.js';
|
||||
import {
|
||||
@ -31,6 +32,17 @@ import {
|
||||
} from './TargetManager.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
|
||||
* 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>();
|
||||
#waitForInitiallyDiscoveredTargets = true;
|
||||
|
||||
// TODO: remove the flag once the testing/rollout is done.
|
||||
#tabMode = false;
|
||||
#discoveryFilter = this.#tabMode ? [{}] : [{type: 'tab', exclude: true}, {}];
|
||||
|
||||
constructor(
|
||||
connection: Connection,
|
||||
targetFactory: TargetFactory,
|
||||
@ -107,7 +123,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
||||
this.#connection
|
||||
.send('Target.setDiscoverTargets', {
|
||||
discover: true,
|
||||
filter: [{type: 'tab', exclude: true}, {}],
|
||||
filter: this.#discoveryFilter,
|
||||
})
|
||||
.then(this.#storeExistingTargetsForInit)
|
||||
.catch(debugError);
|
||||
@ -143,6 +159,15 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
||||
waitForDebuggerOnStart: true,
|
||||
flatten: true,
|
||||
autoAttach: true,
|
||||
filter: this.#tabMode
|
||||
? [
|
||||
{
|
||||
type: 'page',
|
||||
exclude: true,
|
||||
},
|
||||
...this.#discoveryFilter,
|
||||
]
|
||||
: this.#discoveryFilter,
|
||||
});
|
||||
this.#finishInitializationIfReady();
|
||||
await this.#initializeDeferred.valueOrThrow();
|
||||
@ -158,7 +183,13 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
||||
}
|
||||
|
||||
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(
|
||||
@ -285,6 +316,18 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
||||
const wasInitialized =
|
||||
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);
|
||||
|
||||
if (wasInitialized && previousURL !== target.url()) {
|
||||
@ -350,7 +393,11 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
||||
|
||||
const target = existingTarget
|
||||
? this.#attachedTargetsByTargetId.get(targetInfo.targetId)!
|
||||
: this.#targetFactory(targetInfo, session);
|
||||
: this.#targetFactory(
|
||||
targetInfo,
|
||||
session,
|
||||
parentSession instanceof CDPSession ? parentSession : undefined
|
||||
);
|
||||
|
||||
if (this.#targetFilterCallback && !this.#targetFilterCallback(target)) {
|
||||
this.#ignoredTargets.add(targetInfo.targetId);
|
||||
@ -391,7 +438,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
||||
}
|
||||
|
||||
this.#targetsIdsForInit.delete(target._targetId);
|
||||
if (!existingTarget) {
|
||||
if (!existingTarget && isTargetExposed(target)) {
|
||||
this.emit(TargetManagerEmittedEvents.TargetAvailable, target);
|
||||
}
|
||||
this.#finishInitializationIfReady();
|
||||
@ -403,6 +450,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
||||
waitForDebuggerOnStart: true,
|
||||
flatten: true,
|
||||
autoAttach: true,
|
||||
filter: this.#discoveryFilter,
|
||||
}),
|
||||
session.send('Runtime.runIfWaitingForDebugger'),
|
||||
]).catch(debugError);
|
||||
@ -428,6 +476,8 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
|
||||
}
|
||||
|
||||
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 {TargetCloseError, ProtocolError} from './Errors.js';
|
||||
import {EventEmitter} from './EventEmitter.js';
|
||||
import {CDPTarget} from './Target.js';
|
||||
import {debugError} from './util.js';
|
||||
|
||||
const debugProtocolSend = debug('puppeteer:protocol:SEND ►');
|
||||
@ -429,6 +430,7 @@ export interface CDPSessionOnMessageObject {
|
||||
*/
|
||||
export const CDPSessionEmittedEvents = {
|
||||
Disconnected: Symbol('CDPSession.Disconnected'),
|
||||
Swapped: Symbol('CDPSession.Swapped'),
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@ -515,6 +517,7 @@ export class CDPSessionImpl extends CDPSession {
|
||||
#callbacks = new CallbackRegistry();
|
||||
#connection?: Connection;
|
||||
#parentSessionId?: string;
|
||||
#target?: CDPTarget;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -532,6 +535,25 @@ export class CDPSessionImpl extends CDPSession {
|
||||
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 {
|
||||
return this.#connection;
|
||||
}
|
||||
|
@ -144,6 +144,14 @@ export class Coverage {
|
||||
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
|
||||
* `resetOnNavigation : true, reportAnonymousScripts : false,`
|
||||
@ -212,6 +220,13 @@ export class JSCoverage {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
async start(
|
||||
options: {
|
||||
resetOnNavigation?: boolean;
|
||||
@ -342,6 +357,13 @@ export class CSSCoverage {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
async start(options: {resetOnNavigation?: boolean} = {}): Promise<void> {
|
||||
assert(!this.#enabled, 'CSSCoverage is already enabled');
|
||||
const {resetOnNavigation = true} = options;
|
||||
|
@ -35,6 +35,10 @@ export class EmulationManager {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
get javascriptEnabled(): boolean {
|
||||
return this.#javascriptEnabled;
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ export const FrameEmittedEvents = {
|
||||
LifecycleEvent: Symbol('Frame.LifecycleEvent'),
|
||||
FrameNavigatedWithinDocument: Symbol('Frame.FrameNavigatedWithinDocument'),
|
||||
FrameDetached: Symbol('Frame.FrameDetached'),
|
||||
FrameSwappedByActivation: Symbol('Frame.FrameSwappedByActivation'),
|
||||
};
|
||||
|
||||
/**
|
||||
@ -82,14 +83,33 @@ export class Frame extends BaseFrame {
|
||||
this._loaderId = '';
|
||||
|
||||
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.worlds = {
|
||||
[MAIN_WORLD]: new IsolatedWorld(this),
|
||||
[PUPPETEER_WORLD]: new IsolatedWorld(this),
|
||||
};
|
||||
if (!keepWorlds) {
|
||||
this.worlds = {
|
||||
[MAIN_WORLD]: new IsolatedWorld(this),
|
||||
[PUPPETEER_WORLD]: new IsolatedWorld(this),
|
||||
};
|
||||
} else {
|
||||
this.worlds[MAIN_WORLD].frameUpdated();
|
||||
this.worlds[PUPPETEER_WORLD].frameUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
override page(): Page {
|
||||
|
@ -18,11 +18,13 @@ import {Protocol} from 'devtools-protocol';
|
||||
|
||||
import {Page} from '../api/Page.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
import {
|
||||
CDPSession,
|
||||
CDPSessionEmittedEvents,
|
||||
CDPSessionImpl,
|
||||
isTargetClosedError,
|
||||
} from './Connection.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}.
|
||||
*
|
||||
@ -113,13 +117,71 @@ export class FrameManager extends EventEmitter {
|
||||
this.#timeoutSettings = timeoutSettings;
|
||||
this.setupEventListeners(this.#client);
|
||||
client.once(CDPSessionEmittedEvents.Disconnected, () => {
|
||||
const mainFrame = this._frameTree.getMainFrame();
|
||||
if (mainFrame) {
|
||||
this.#removeFramesRecursively(mainFrame);
|
||||
}
|
||||
this.#onClientDisconnect().catch(debugError);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
session.on('Page.frameAttached', event => {
|
||||
this.#onFrameAttached(session, event.frameId, event.parentFrameId);
|
||||
|
@ -59,6 +59,13 @@ export class CDPKeyboard extends Keyboard {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
override async down(
|
||||
key: KeyInput,
|
||||
options: Readonly<KeyDownOptions> = {
|
||||
@ -290,6 +297,13 @@ export class CDPMouse extends Mouse {
|
||||
this.#keyboard = keyboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
#_state: Readonly<MouseState> = {
|
||||
position: {x: 0, y: 0},
|
||||
buttons: MouseButtonFlag.None,
|
||||
@ -571,6 +585,13 @@ export class CDPTouchscreen extends Touchscreen {
|
||||
this.#keyboard = keyboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
override async tap(x: number, y: number): Promise<void> {
|
||||
await this.touchStart(x, y);
|
||||
await this.touchEnd();
|
||||
|
@ -124,9 +124,11 @@ export class IsolatedWorld implements Realm {
|
||||
}
|
||||
|
||||
constructor(frame: Frame) {
|
||||
// Keep own reference to client because it might differ from the FrameManager's
|
||||
// client for OOP iframes.
|
||||
this.#frame = frame;
|
||||
this.frameUpdated();
|
||||
}
|
||||
|
||||
frameUpdated(): void {
|
||||
this.#client.on('Runtime.bindingCalled', this.#onBindingCalled);
|
||||
}
|
||||
|
||||
|
@ -117,6 +117,11 @@ export class LifecycleWatcher {
|
||||
FrameEmittedEvents.FrameSwapped,
|
||||
this.#frameSwapped.bind(this)
|
||||
),
|
||||
addEventListener(
|
||||
frame,
|
||||
FrameEmittedEvents.FrameSwappedByActivation,
|
||||
this.#frameSwapped.bind(this)
|
||||
),
|
||||
addEventListener(
|
||||
frame,
|
||||
FrameEmittedEvents.FrameDetached,
|
||||
|
@ -21,7 +21,7 @@ import {createDebuggableDeferred} from '../util/DebuggableDeferred.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
|
||||
import {CDPSession} from './Connection.js';
|
||||
import {EventEmitter} from './EventEmitter.js';
|
||||
import {EventEmitter, Handler} from './EventEmitter.js';
|
||||
import {FrameManager} from './FrameManager.js';
|
||||
import {HTTPRequest} from './HTTPRequest.js';
|
||||
import {HTTPResponse} from './HTTPResponse.js';
|
||||
@ -90,6 +90,23 @@ export class NetworkManager extends EventEmitter {
|
||||
};
|
||||
#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(
|
||||
client: CDPSession,
|
||||
ignoreHTTPSErrors: boolean,
|
||||
@ -100,29 +117,18 @@ export class NetworkManager extends EventEmitter {
|
||||
this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||
this.#frameManager = frameManager;
|
||||
|
||||
this.#client.on('Fetch.requestPaused', this.#onRequestPaused.bind(this));
|
||||
this.#client.on('Fetch.authRequired', this.#onAuthRequired.bind(this));
|
||||
this.#client.on(
|
||||
'Network.requestWillBeSent',
|
||||
this.#onRequestWillBeSent.bind(this)
|
||||
);
|
||||
this.#client.on(
|
||||
'Network.requestServedFromCache',
|
||||
this.#onRequestServedFromCache.bind(this)
|
||||
);
|
||||
this.#client.on(
|
||||
'Network.responseReceived',
|
||||
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)
|
||||
);
|
||||
for (const [event, handler] of this.#handlers) {
|
||||
this.#client.on(event, handler);
|
||||
}
|
||||
}
|
||||
|
||||
async updateClient(client: CDPSession): Promise<void> {
|
||||
this.#client = client;
|
||||
for (const [event, handler] of this.#handlers) {
|
||||
this.#client.on(event, handler);
|
||||
}
|
||||
this.#deferredInit = undefined;
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,6 +46,7 @@ import {Binding} from './Binding.js';
|
||||
import {
|
||||
CDPSession,
|
||||
CDPSessionEmittedEvents,
|
||||
CDPSessionImpl,
|
||||
isTargetClosedError,
|
||||
} from './Connection.js';
|
||||
import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js';
|
||||
@ -127,6 +128,7 @@ export class CDPPage extends Page {
|
||||
|
||||
#closed = false;
|
||||
#client: CDPSession;
|
||||
#tabSession: CDPSession | undefined;
|
||||
#target: CDPTarget;
|
||||
#keyboard: CDPKeyboard;
|
||||
#mouse: CDPMouse;
|
||||
@ -289,6 +291,7 @@ export class CDPPage extends Page {
|
||||
) {
|
||||
super();
|
||||
this.#client = client;
|
||||
this.#tabSession = client.parentSession();
|
||||
this.#target = target;
|
||||
this.#keyboard = new CDPKeyboard(client);
|
||||
this.#mouse = new CDPMouse(client, this.#keyboard);
|
||||
@ -307,6 +310,25 @@ export class CDPPage extends Page {
|
||||
this.#viewport = null;
|
||||
|
||||
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() {
|
||||
|
@ -22,7 +22,7 @@ import {Page, PageEmittedEvents} from '../api/Page.js';
|
||||
import {Target, TargetType} from '../api/Target.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
|
||||
import {CDPSession} from './Connection.js';
|
||||
import {CDPSession, CDPSessionImpl} from './Connection.js';
|
||||
import {CDPPage} from './Page.js';
|
||||
import {Viewport} from './PuppeteerViewport.js';
|
||||
import {TargetManager} from './TargetManager.js';
|
||||
@ -84,6 +84,16 @@ export class CDPTarget extends Target {
|
||||
this.#browserContext = browserContext;
|
||||
this._targetId = targetInfo.targetId;
|
||||
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) {
|
||||
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 {
|
||||
@ -131,6 +144,8 @@ export class CDPTarget extends Target {
|
||||
return TargetType.BROWSER;
|
||||
case 'webview':
|
||||
return TargetType.WEBVIEW;
|
||||
case 'tab':
|
||||
return TargetType.TAB;
|
||||
default:
|
||||
return TargetType.OTHER;
|
||||
}
|
||||
|
@ -25,7 +25,8 @@ import {CDPTarget} from './Target.js';
|
||||
*/
|
||||
export type TargetFactory = (
|
||||
targetInfo: Protocol.Target.TargetInfo,
|
||||
session?: CDPSession
|
||||
session?: CDPSession,
|
||||
parentSession?: CDPSession
|
||||
) => CDPTarget;
|
||||
|
||||
/**
|
||||
|
@ -57,6 +57,13 @@ export class Tracing {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a trace for the current page.
|
||||
* @remarks
|
||||
|
@ -178,7 +178,7 @@ export class ChromeLauncher extends ProductLauncher {
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-extensions',
|
||||
// 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-ipc-flooding-protection',
|
||||
'--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();
|
||||
const error = await watchdog;
|
||||
expect(error.message).toContain('frame got detached');
|
||||
expect(error.message).toContain('Session closed.');
|
||||
} finally {
|
||||
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