From 2718123d3036a58751d248ba6fcc96b0b2ad8938 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 22 Feb 2023 21:53:27 -0800 Subject: [PATCH] fix(snapshots): define dummy custom elements (#21131) For all custom elements defined in the page, we preserve their names and define them in the rendered snapshot. This makes things like `:defined` css pseudo work. Fixes #21030. --- .../trace/recorder/snapshotterInjected.ts | 13 ++++++++ packages/trace-viewer/src/snapshotRenderer.ts | 9 ++++++ tests/library/trace-viewer.spec.ts | 30 +++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts index 275c34cbd4..d7dce7aade 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts @@ -45,6 +45,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string) { const kScrollLeftAttribute = '__playwright_scroll_left_'; const kStyleSheetAttribute = '__playwright_style_sheet_'; const kTargetAttribute = '__playwright_target__'; + const kCustomElementsAttribute = '__playwright_custom_elements__'; // Symbols for our own info on Nodes/StyleSheets. const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_'); @@ -296,6 +297,8 @@ export function frameSnapshotStreamer(snapshotStreamer: string) { if (document.documentElement) findElementsToRestoreScrollPositionRecursively(document.documentElement); + const definedCustomElements = new Set(); + const visitNode = (node: Node | ShadowRoot): { equals: boolean, n: NodeSnapshot } | undefined => { const nodeType = node.nodeType; const nodeName = nodeType === Node.DOCUMENT_FRAGMENT_NODE ? 'template' : node.nodeName; @@ -385,6 +388,8 @@ export function frameSnapshotStreamer(snapshotStreamer: string) { if (nodeType === Node.ELEMENT_NODE) { const element = node as Element; + if (element.localName.includes('-') && window.customElements?.get(element.localName)) + definedCustomElements.add(element.localName); if (nodeName === 'INPUT' || nodeName === 'TEXTAREA') { const value = (element as HTMLInputElement).value; expectValue(kValueAttribute); @@ -453,6 +458,14 @@ export function frameSnapshotStreamer(snapshotStreamer: string) { attrs[name] = value; } + // Process custom elements before bailing out since they depend on JS, not the DOM. + if (nodeName === 'BODY' && definedCustomElements.size) { + const value = [...definedCustomElements].join(','); + expectValue(kCustomElementsAttribute); + expectValue(value); + attrs[kCustomElementsAttribute] = value; + } + // We can skip attributes comparison because nothing else has changed, // and mutation observer didn't tell us about the attributes. if (equals && data.attributesCached && !shadowDomNesting) diff --git a/packages/trace-viewer/src/snapshotRenderer.ts b/packages/trace-viewer/src/snapshotRenderer.ts index 041587488e..05995a4a21 100644 --- a/packages/trace-viewer/src/snapshotRenderer.ts +++ b/packages/trace-viewer/src/snapshotRenderer.ts @@ -229,6 +229,15 @@ function snapshotScript() { } } + { + const body = root.querySelector(`body[__playwright_custom_elements__]`); + if (body && window.customElements) { + const customElements = (body.getAttribute('__playwright_custom_elements__') || '').split(','); + for (const elementName of customElements) + window.customElements.define(elementName, class extends HTMLElement {}); + } + } + for (const element of root.querySelectorAll(`template[__playwright_shadow_root_]`)) { const template = element as HTMLTemplateElement; const shadowRoot = template.parentElement!.attachShadow({ mode: 'open' }); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 0a614e311d..0bd932ada8 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -526,6 +526,36 @@ test('should handle src=blob', async ({ page, server, runAndTrace, browserName } expect(size).toBe(10); }); +test('should register custom elements', async ({ page, server, runAndTrace }) => { + const traceViewer = await runAndTrace(async () => { + page.on('console', console.log); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + customElements.define('my-element', class extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({ mode: 'open' }); + const span = document.createElement('span'); + span.textContent = 'hello'; + shadow.appendChild(span); + shadow.appendChild(document.createElement('slot')); + } + }); + }); + await page.setContent(` + + world + `); + }); + + const frame = await traceViewer.snapshotFrame('page.setContent'); + await expect(frame.getByText('worldhello')).toBeVisible(); +}); + test('should highlight target elements', async ({ page, runAndTrace, browserName }) => { const traceViewer = await runAndTrace(async () => { await page.setContent(`