Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 21 additions & 14 deletions packages/playwright-core/src/server/chromium/crPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,8 +487,6 @@ class FrameSession {
startAutomaticVideoRecording(this._crPage._page);

let lifecycleEventsEnabled: Promise<any>;
if (!this._isMainFrame())
this._addRendererListeners();
this._addBrowserListeners();

// Buffer attachedToTarget events until we receive the frame tree.
Expand All @@ -500,18 +498,16 @@ class FrameSession {
const promises: Promise<any>[] = [
this._client.send('Page.enable'),
this._client.send('Page.getFrameTree').then(({ frameTree }) => {
if (this._isMainFrame()) {
this._handleFrameTree(frameTree);
this._addRendererListeners();
}
const localFrames: frames.Frame[] = [];
this._handleFrameTree(frameTree, localFrames);
this._addRendererListeners();

// Now that we have the frame tree, it is possible to insert oopif targets at the right place.
const attachedToTargetEvents = this._bufferedAttachedToTargetEvents || [];
this._bufferedAttachedToTargetEvents = undefined;
for (const event of attachedToTargetEvents)
this._onAttachedToTarget(event);

const localFrames = this._isMainFrame() ? this._page.frames() : [this._page.frameManager.frame(this._targetId)!];
for (const frame of localFrames) {
// Note: frames might be removed before we send these.
this._client._sendMayFail('Page.createIsolatedWorld', {
Expand Down Expand Up @@ -630,14 +626,24 @@ class FrameSession {
this._page.frameManager.frameLifecycleEvent(event.frameId, 'domcontentloaded');
}

_handleFrameTree(frameTree: Protocol.Page.FrameTree) {
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId || null);
this._onFrameNavigated(frameTree.frame, true);
if (!frameTree.childFrames)
_handleFrameTree(frameTree: Protocol.Page.FrameTree, collectedLocalFrames: frames.Frame[]) {
const isOopif = !this._isMainFrame() && frameTree.frame.id === this._targetId;
// Note: OOPIF frame has been already attached in the parent session, so skip it.
if (!isOopif)
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId || null);

const isInitialOopifDocument = isOopif && (frameTree.frame.url === '' || frameTree.frame.url === ':');
// Note: do not fake an empty document commit in a newly created OOPIF.
if (!isInitialOopifDocument)
this._onFrameNavigated(frameTree.frame, true);

const frame = this._page.frameManager.frame(frameTree.frame.id);
if (!frame)
return;

for (const child of frameTree.childFrames)
this._handleFrameTree(child);
collectedLocalFrames.push(frame);
for (const child of frameTree.childFrames || [])
this._handleFrameTree(child, collectedLocalFrames);
}

private _eventBelongsToStaleFrame(frameId: string) {
Expand Down Expand Up @@ -759,7 +765,8 @@ class FrameSession {
if (!frame && event.targetInfo.parentFrameId) {
// When connecting to an existing page with an iframe, there is an "iframe" target,
// but no local frame is reported in getFrameTree. We can create a remote frame here.
frame = this._page.frameManager.frameAttached(targetId, event.targetInfo.parentFrameId);
const parentFrame = this._page.frameManager.frame(event.targetInfo.parentFrameId) ?? this._page.mainFrame();
frame = this._page.frameManager.frameAttached(targetId, parentFrame._id);
}
if (!frame)
return; // Subtree may be already gone due to renderer/browser race.
Expand Down
74 changes: 74 additions & 0 deletions tests/library/chromium/oopif.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,25 @@ it('should intercept response body from oopif', async function({ page, browser,
expect(await response.text()).toBeTruthy();
});

it('should navigate a local frame to oopif with frame.goto', async ({ page, browser, server }) => {
await page.goto(server.PREFIX + '/frames/one-frame.html');
await assertOOPIFCount(browser, 0);
const frame = page.frames()[1];
expect(frame.url()).toBe(server.PREFIX + '/frames/frame.html');

const navigations: string[] = [];
page.on('framenavigated', f => navigations.push(f.url()));

const response = await frame.goto(server.CROSS_PROCESS_PREFIX + '/grid.html');
await assertOOPIFCount(browser, 1);
expect(response!.url()).toBe(server.CROSS_PROCESS_PREFIX + '/grid.html');
expect(response!.frame()).toBe(frame);
expect(frame.url()).toBe(server.CROSS_PROCESS_PREFIX + '/grid.html');
expect(await frame.evaluate(() => '' + location.href)).toBe(server.CROSS_PROCESS_PREFIX + '/grid.html');
expect(navigations).toEqual([server.CROSS_PROCESS_PREFIX + '/grid.html']);
expect(page.frames().length).toBe(2);
});

it('should allow to re-connect to OOPIFs with CDP when iframes were there already', async ({ browserType, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/36095' });
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/17656' });
Expand All @@ -417,6 +436,61 @@ it('should allow to re-connect to OOPIFs with CDP when iframes were there alread
await hostBrowser.close();
});

it('should report the whole frame tree when connecting over CDP to nested OOPIFs', async ({ browserType, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42955' });
it.skip(!!process.env.PWTEST_CHANNEL, 'Test default channel only');

// top > cross-site iframe (oopif) > same-process child iframe > sandboxed srcdoc iframe (oopif).
server.setRoute('/nested-top.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<h1 id=top>TOP</h1><iframe src="${server.CROSS_PROCESS_PREFIX}/nested-b.html"></iframe>`);
});
server.setRoute('/nested-b.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<iframe src="/nested-c.html"></iframe>`);
});
server.setRoute('/nested-c.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<iframe sandbox="allow-scripts" srcdoc="<p>D</p>"></iframe>`);
});

const cdpPort = 10123 + it.info().parallelIndex * 4;
const hostBrowser = await browserType.launch({
channel: 'chromium',
args: ['--remote-debugging-port=' + cdpPort, '--site-per-process', '--enable-features=IsolateSandboxedIframes'],
});
const hostPage = await hostBrowser.newPage();
await hostPage.goto(server.PREFIX + '/nested-top.html');

const browser = await browserType.connectOverCDP(`http://localhost:${cdpPort}`);
await assertOOPIFCount(browser, 2);
const page = browser.contexts()[0].pages()[0];

const expectedTree = [
server.PREFIX + '/nested-top.html',
' ' + server.CROSS_PROCESS_PREFIX + '/nested-b.html',
' ' + server.CROSS_PROCESS_PREFIX + '/nested-c.html',
' about:srcdoc',
];
const dumpFrames = async (frame: Frame, indent = ''): Promise<string[]> => {
expect(await frame.evaluate(() => '' + location.href)).toBe(frame.url());
const result = [indent + frame.url()];
for (const child of frame.childFrames())
result.push(...await dumpFrames(child, indent + ' '));
return result;
};
expect(await dumpFrames(page.mainFrame())).toEqual(expectedTree);
expect(page.frames().length).toBe(4);

expect(page.url()).toBe(server.PREFIX + '/nested-top.html');
expect(await page.evaluate(() => window.top === window)).toBe(true);
await expect(page.locator('#top')).toHaveText('TOP');
await expect(page.frameLocator('iframe').frameLocator('iframe').frameLocator('iframe').locator('p')).toHaveText('D');

await browser.close();
await hostBrowser.close();
});

async function assertOOPIFCount(browser: Browser, count: number) {
if (browser.browserType().name() !== 'chromium')
return;
Expand Down
Loading