diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts index f3a69ea450..928c0b3b07 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts @@ -269,6 +269,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string) { const snapshotNumber = ++this._lastSnapshotNumber; let nodeCounter = 0; let shadowDomNesting = 0; + let headNesting = 0; // Ensure we are up to date. this._handleMutations(this._observer.takeRecords()); @@ -293,6 +294,10 @@ export function frameSnapshotStreamer(snapshotStreamer: string) { return; if (nodeName === 'META' && (node as HTMLMetaElement).httpEquiv.toLowerCase() === 'content-security-policy') return; + // Skip iframes which are inside document's head as they are not visisble. + // See https://github.com/microsoft/playwright/issues/12005. + if ((nodeName === 'IFRAME' || nodeName === 'FRAME') && headNesting) + return; const data = ensureCachedData(node); const values: any[] = []; @@ -392,12 +397,15 @@ export function frameSnapshotStreamer(snapshotStreamer: string) { result.push(value); } else { if (nodeName === 'HEAD') { + ++headNesting; // Insert fake first, to ensure all elements use the proper base uri. this._fakeBase.setAttribute('href', document.baseURI); visitChild(this._fakeBase); } for (let child = node.firstChild; child; child = child.nextSibling) visitChild(child); + if (nodeName === 'HEAD') + --headNesting; expectValue(kEndOfList); let documentOrShadowRoot = null; if (node.ownerDocument!.documentElement === node) diff --git a/tests/tracing.spec.ts b/tests/tracing.spec.ts index 5970529d03..0ee7ab2366 100644 --- a/tests/tracing.spec.ts +++ b/tests/tracing.spec.ts @@ -386,6 +386,27 @@ test('should not hang for clicks that open dialogs', async ({ context, page }) = await context.tracing.stop(); }); +test('should ignore iframes in head', async ({ context, page, server }, testInfo) => { + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => { + document.head.appendChild(document.createElement('iframe')); + // Add iframe in a shadow tree. + const div = document.createElement('div'); + document.head.appendChild(div); + const shadow = div.attachShadow({ mode: 'open' }); + shadow.appendChild(document.createElement('iframe')); + }); + + await context.tracing.start({ screenshots: true, snapshots: true }); + await page.click('button'); + await context.tracing.stopChunk({ path: testInfo.outputPath('trace.zip') }); + + const trace = await parseTrace(testInfo.outputPath('trace.zip')); + expect(trace.events.find(e => e.metadata?.apiName === 'page.click')).toBeTruthy(); + expect(trace.events.find(e => e.type === 'frame-snapshot')).toBeTruthy(); + expect(trace.events.find(e => e.type === 'frame-snapshot' && JSON.stringify(e.snapshot.html).includes('IFRAME'))).toBeFalsy(); +}); + test('should hide internal stack frames', async ({ context, page }, testInfo) => { await context.tracing.start({ screenshots: true, snapshots: true }); let evalPromise;