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;