fix(chromium): use frameDetached reason (#4468)

This fixes the local -> remote frame swap when
Page.frameDetached arrives before Target.attachedToTarget.

Instead of error-prone logic we do currently, new CDP exposes
frame detach reason that we can use.
This commit is contained in:
Dmitry Gozman 2020-11-17 10:24:13 -08:00 committed by GitHub
parent c1a5cd51b1
commit 38fadcaded
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 64 additions and 47 deletions

View file

@ -43,8 +43,6 @@ import { VideoRecorder } from './videoRecorder';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
const swappingOutChildFrames = Symbol('swappingOutChildFrames');
export class CRPage implements PageDelegate { export class CRPage implements PageDelegate {
readonly _mainFrameSession: FrameSession; readonly _mainFrameSession: FrameSession;
readonly _sessions = new Map<Protocol.Target.TargetID, FrameSession>(); readonly _sessions = new Map<Protocol.Target.TargetID, FrameSession>();
@ -363,7 +361,7 @@ class FrameSession {
helper.addEventListener(this._client, 'Log.entryAdded', event => this._onLogEntryAdded(event)), helper.addEventListener(this._client, 'Log.entryAdded', event => this._onLogEntryAdded(event)),
helper.addEventListener(this._client, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)), helper.addEventListener(this._client, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)),
helper.addEventListener(this._client, 'Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)), helper.addEventListener(this._client, 'Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)),
helper.addEventListener(this._client, 'Page.frameDetached', event => this._onFrameDetached(event.frameId)), helper.addEventListener(this._client, 'Page.frameDetached', event => this._onFrameDetached(event.frameId, event.reason)),
helper.addEventListener(this._client, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)), helper.addEventListener(this._client, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)),
helper.addEventListener(this._client, 'Page.frameRequestedNavigation', event => this._onFrameRequestedNavigation(event)), helper.addEventListener(this._client, 'Page.frameRequestedNavigation', event => this._onFrameRequestedNavigation(event)),
helper.addEventListener(this._client, 'Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)), helper.addEventListener(this._client, 'Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)),
@ -551,35 +549,23 @@ class FrameSession {
this._page._frameManager.frameCommittedSameDocumentNavigation(frameId, url); this._page._frameManager.frameCommittedSameDocumentNavigation(frameId, url);
} }
private _takeParentForSwappingOutFrame(targetId: string) { _onFrameDetached(frameId: string, reason: 'remove' | 'swap') {
for (const frame of this._page.frames()) {
if (!(frame as any)[swappingOutChildFrames])
continue;
if ((frame as any)[swappingOutChildFrames].delete(targetId))
return frame._id;
}
return null;
}
private _frameMaybeSwappingOut(frameId: string) {
const frame = this._page._frameManager.frame(frameId);
if (!frame)
return;
const parent = frame.parentFrame() as any;
if (!parent)
return;
if (!parent[swappingOutChildFrames])
parent[swappingOutChildFrames] = new Set<string>();
parent[swappingOutChildFrames].add(frameId);
}
_onFrameDetached(frameId: string) {
if (this._crPage._sessions.has(frameId)) { if (this._crPage._sessions.has(frameId)) {
// This is a local -> remote frame transtion. // This is a local -> remote frame transtion, where
// We already got a new target and handled frame reattach - nothing to do here. // Page.frameDetached arrives after Target.attachedToTarget.
// We've already handled the new target and frame reattach - nothing to do here.
return; return;
} }
this._frameMaybeSwappingOut(frameId); if (reason === 'swap') {
// This is a local -> remote frame transtion, where
// Page.frameDetached arrives before Target.attachedToTarget.
// We should keep the frame in the tree, and it will be used for the new target.
const frame = this._page._frameManager.frame(frameId);
if (frame)
this._page._frameManager.removeChildFramesRecursively(frame);
return;
}
// Just a regular frame detach.
this._page._frameManager.frameDetached(frameId); this._page._frameManager.frameDetached(frameId);
} }
@ -615,16 +601,8 @@ class FrameSession {
if (event.targetInfo.type === 'iframe') { if (event.targetInfo.type === 'iframe') {
// Frame id equals target id. // Frame id equals target id.
const targetId = event.targetInfo.targetId; const targetId = event.targetInfo.targetId;
const frame = this._page._frameManager.frame(targetId); const frame = this._page._frameManager.frame(targetId)!;
if (frame) { this._page._frameManager.removeChildFramesRecursively(frame);
this._page._frameManager.removeChildFramesRecursively(frame);
} else {
// There is a race between Page.frameDetached and Target.attachedToTarget. If the frame
// has already been detached we look up its last parent frame.
const parentFrameId = this._takeParentForSwappingOutFrame(targetId);
assert(parentFrameId, 'Cannot find parent for iframe: ' + targetId);
this._page._frameManager.frameAttached(targetId, parentFrameId);
}
const frameSession = new FrameSession(this._crPage, session, targetId, this); const frameSession = new FrameSession(this._crPage, session, targetId, this);
this._crPage._sessions.set(targetId, frameSession); this._crPage._sessions.set(targetId, frameSession);
frameSession._initialize(false).catch(e => e); frameSession._initialize(false).catch(e => e);

View file

@ -1041,7 +1041,7 @@ Note that userVisibleOnly = true is the only currently supported type.
/** /**
* Browser command ids used by executeBrowserCommand. * Browser command ids used by executeBrowserCommand.
*/ */
export type BrowserCommandId = "openTabSearch"; export type BrowserCommandId = "openTabSearch"|"closeTabSearch";
/** /**
* Chrome histogram bucket. * Chrome histogram bucket.
*/ */
@ -6866,12 +6866,13 @@ passed by the developer (e.g. via "fetch") as understood by the backend.
export type ServiceWorkerResponseSource = "cache-storage"|"http-cache"|"fallback-code"|"network"; export type ServiceWorkerResponseSource = "cache-storage"|"http-cache"|"fallback-code"|"network";
/** /**
* Determines what type of Trust Token operation is executed and * Determines what type of Trust Token operation is executed and
depending on the type, some additional parameters. depending on the type, some additional parameters. The values
are specified in third_party/blink/renderer/core/fetch/trust_token.idl.
*/ */
export interface TrustTokenParams { export interface TrustTokenParams {
type: TrustTokenOperationType; type: TrustTokenOperationType;
/** /**
* Only set for "srr-token-redemption" type and determine whether * Only set for "token-redemption" type and determine whether
to request a fresh SRR or use a still valid cached SRR. to request a fresh SRR or use a still valid cached SRR.
*/ */
refreshPolicy: "UseCached"|"Refresh"; refreshPolicy: "UseCached"|"Refresh";
@ -7410,8 +7411,8 @@ https://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-
reportOnlyReportingEndpoint?: string; reportOnlyReportingEndpoint?: string;
} }
export interface SecurityIsolationStatus { export interface SecurityIsolationStatus {
coop: CrossOriginOpenerPolicyStatus; coop?: CrossOriginOpenerPolicyStatus;
coep: CrossOriginEmbedderPolicyStatus; coep?: CrossOriginEmbedderPolicyStatus;
} }
/** /**
* An object providing the result of a network resource load. * An object providing the result of a network resource load.
@ -8509,6 +8510,36 @@ continueInterceptedRequest call.
*/ */
gridBackgroundColor?: DOM.RGBA; gridBackgroundColor?: DOM.RGBA;
} }
/**
* Configuration data for the highlighting of Flex container elements.
*/
export interface FlexContainerHighlightConfig {
/**
* The style of the container border
*/
containerBorder?: LineStyle;
/**
* The style of the separator between lines
*/
lineSeparator?: LineStyle;
/**
* The style of the separator between items
*/
itemSeparator?: LineStyle;
}
/**
* Style information for drawing a line.
*/
export interface LineStyle {
/**
* The color of the line (default: transparent)
*/
color?: DOM.RGBA;
/**
* The line pattern (default: solid)
*/
pattern?: "dashed"|"dotted";
}
/** /**
* Configuration data for the highlighting of page elements. * Configuration data for the highlighting of page elements.
*/ */
@ -8573,6 +8604,10 @@ continueInterceptedRequest call.
* The grid layout highlight configuration (default: all transparent). * The grid layout highlight configuration (default: all transparent).
*/ */
gridHighlightConfig?: GridHighlightConfig; gridHighlightConfig?: GridHighlightConfig;
/**
* The flex container highlight configuration (default: all transparent).
*/
flexContainerHighlightConfig?: FlexContainerHighlightConfig;
} }
export type ColorFormat = "rgb"|"hsl"|"hex"; export type ColorFormat = "rgb"|"hsl"|"hex";
/** /**
@ -8997,6 +9032,7 @@ Backend then generates 'inspectNodeRequested' event upon element selection.
* Indicates whether the frame is cross-origin isolated and why it is the case. * Indicates whether the frame is cross-origin isolated and why it is the case.
*/ */
export type CrossOriginIsolatedContextType = "Isolated"|"NotIsolated"|"NotIsolatedFeatureDisabled"; export type CrossOriginIsolatedContextType = "Isolated"|"NotIsolated"|"NotIsolatedFeatureDisabled";
export type GatedAPIFeatures = "SharedArrayBuffers"|"SharedArrayBuffersTransferAllowed"|"PerformanceMeasureMemory"|"PerformanceProfile";
/** /**
* Information about the Frame on the page. * Information about the Frame on the page.
*/ */
@ -9056,6 +9092,10 @@ Example URLs: http://www.google.com/file.html -> "google.com"
* Indicates whether this is a cross origin isolated context. * Indicates whether this is a cross origin isolated context.
*/ */
crossOriginIsolatedContextType: CrossOriginIsolatedContextType; crossOriginIsolatedContextType: CrossOriginIsolatedContextType;
/**
* Indicated which gated APIs / features are available.
*/
gatedAPIFeatures: GatedAPIFeatures[];
} }
/** /**
* Information about the Resource on the page. * Information about the Resource on the page.
@ -9433,6 +9473,7 @@ Example URLs: http://www.google.com/file.html -> "google.com"
* Id of the frame that has been detached. * Id of the frame that has been detached.
*/ */
frameId: FrameId; frameId: FrameId;
reason: "remove"|"swap";
} }
/** /**
* Fired once navigation of the frame has completed. Frame is now associated with the new loader. * Fired once navigation of the frame has completed. Frame is now associated with the new loader.

View file

@ -16,10 +16,8 @@ async function generateProtocol(name, executablePath) {
async function generateChromiumProtocol(executablePath) { async function generateChromiumProtocol(executablePath) {
const outputPath = path.join(__dirname, '../../src/server/chromium/protocol.ts'); const outputPath = path.join(__dirname, '../../src/server/chromium/protocol.ts');
process.env.PLAYWRIGHT_CHROMIUM_DEBUG_PORT = '9339';
const playwright = require('../../index').chromium; const playwright = require('../../index').chromium;
const browser = await playwright.launch({ executablePath }); const browser = await playwright.launch({ executablePath, args: ['--remote-debugging-port=9339'] });
delete process.env.PLAYWRIGHT_CHROMIUM_DEBUG_PORT;
const page = await browser.newPage(); const page = await browser.newPage();
await page.goto(`http://localhost:9339/json/protocol`); await page.goto(`http://localhost:9339/json/protocol`);
const json = JSON.parse(await page.evaluate(() => document.documentElement.innerText)); const json = JSON.parse(await page.evaluate(() => document.documentElement.innerText));