diff --git a/packages/isomorphic/trace/snapshotRenderer.ts b/packages/isomorphic/trace/snapshotRenderer.ts index 2c9284987015d..433ee074c70bd 100644 --- a/packages/isomorphic/trace/snapshotRenderer.ts +++ b/packages/isomorphic/trace/snapshotRenderer.ts @@ -79,12 +79,7 @@ export class SnapshotRenderer { const visit = (n: NodeSnapshot, snapshotIndex: number, parentTag: string | undefined, parentAttrs: [string, string][] | undefined) => { // Text node. if (typeof n === 'string') { - // Best-effort Electron support: rewrite custom protocol in url() links in stylesheets. - // Old snapshotter was sending lower-case. - if (parentTag === 'STYLE' || parentTag === 'style') - result.push(escapeURLsInStyleSheet(rewriteURLsInStyleSheetForCustomProtocol(n))); - else - result.push(escapeHTML(n)); + result.push(escapeHTML(n)); return; } @@ -163,6 +158,13 @@ export class SnapshotRenderer { attrValue = rewriteURLForCustomProtocol(value); result.push(' ', attrName, '="', escapeHTMLAttribute(attrValue), '"'); } + if (upperName === 'STYLE') { + // Style has always exactly one child which is a text node. + const styleContent = typeof children[0] === 'string' ? children[0] : ''; + result.push(' ', '__playwright_style_content__', '="', escapeHTMLAttribute(rewriteURLsInStyleSheetForCustomProtocol(styleContent)), '"'); + result.push('>'); + return; + } result.push('>'); for (const child of children) visit(child, snapshotIndex, nodeName, attrs); @@ -334,6 +336,11 @@ function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefine for (const e of root.querySelectorAll(`[__playwright_scroll_left_]`)) scrollLefts.push(e); + for (const element of root.querySelectorAll(`style[__playwright_style_content__]`)) { + element.textContent = element.getAttribute('__playwright_style_content__'); + element.removeAttribute('__playwright_style_content__'); + } + for (const element of root.querySelectorAll(`[__playwright_value_]`)) { const inputElement = element as HTMLInputElement | HTMLTextAreaElement; if (inputElement.type !== 'file') @@ -665,19 +672,4 @@ function rewriteURLsInStyleSheetForCustomProtocol(text: string): string { }); } -// url() inside a tag: -// url('data:image/svg+xml,') -const urlToEscapeRegex1 = /url\(\s*'([^']*)'\s*\)/ig; -const urlToEscapeRegex2 = /url\(\s*"([^"]*)"\s*\)/ig; -function escapeURLsInStyleSheet(text: string): string { - const replacer = (match: string, url: string) => { - // Conservatively encode only urls with a closing tag. - if (url.includes('`); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index fcc9323add279..d784c98c46cf1 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -869,6 +869,27 @@ test('should capture data-url svg iframe', async ({ page, server, runAndTrace }) expect(content).toContain(`d="M16.5 3c-1.74 0-3.41.81-4.5 2.09C10.91 3.81 9.24 3 7.5 3 4.42 3 2 5.42 2 8.5c0 3.78 3.4 6.86 8.55 11.54L12 21.35l1.45-1.32C18.6 15.36 22 12.28 22 8.5 22 5.42 19.58 3 16.5 3zm-4.4 15.55l-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05z"`); }); +test('should not let style text break out of the style element', async ({ page, runAndTrace, server }) => { + const traceViewer = await runAndTrace(async () => { + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + const style = document.createElement('style'); + style.textContent = 'div{color:rgb(1, 2, 3)}'; + const div = document.createElement('div'); + div.textContent = 'hello'; + document.body.append(style, div); + }); + await page.locator('body').click(); + }); + + const frame = await traceViewer.snapshotFrame('Click'); + // If the "" broke out of the element, this would be a real node. + await expect(frame.locator('img')).toHaveCount(0); + // The stylesheet is preserved verbatim and still applies. + await expect(frame.locator('div')).toHaveCSS('color', 'rgb(1, 2, 3)'); + expect(await frame.locator('body style').evaluate(el => el.textContent)).toContain('onerror='); +}); + test('should contain adopted style sheets', async ({ page, runAndTrace, browserName }) => { const traceViewer = await runAndTrace(async () => { await page.setContent('');