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.
This commit is contained in:
Dmitry Gozman 2023-02-22 21:53:27 -08:00 committed by GitHub
parent 55c95a4463
commit 2718123d30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 52 additions and 0 deletions

View file

@ -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<string>();
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)

View file

@ -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' });

View file

@ -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(`
<style>
:not(:defined) {
visibility: hidden;
}
</style>
<MY-element>world</MY-element>
`);
});
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(`