Skip to content
34 changes: 13 additions & 21 deletions packages/isomorphic/trace/snapshotRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -163,6 +158,13 @@ export class SnapshotRenderer {
attrValue = rewriteURLForCustomProtocol(value);
result.push(' ', attrName, '="', escapeHTMLAttribute(attrValue), '"');
}
if (upperName === 'STYLE') {
Comment thread
yury-s marked this conversation as resolved.
// 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('></', nodeName, '>');
return;
}
result.push('>');
for (const child of children)
visit(child, snapshotIndex, nodeName, attrs);
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -665,19 +672,4 @@ function rewriteURLsInStyleSheetForCustomProtocol(text: string): string {
});
}

// url() inside a <style> tag can mess up with html parsing, so we encode some of them.
// As an example, the following url will close the </style> tag:
// url('data:image/svg+xml,<svg><defs><style>.a{fill:none}</style></defs><g class="a"></g></svg>')
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('</'))
return match.replace(url, encodeURI(url));
return match;
};
return text.replace(urlToEscapeRegex1, replacer).replace(urlToEscapeRegex2, replacer);
}

export const blankSnapshotUrl = 'data:text/html;base64,' + btoa(`<body></body><style>body { color-scheme: light dark; background: light-dark(white, #333) }</style>`);
21 changes: 21 additions & 0 deletions tests/library/trace-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}</style><img src=x onerror="window.__pwned = true">';
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 "</style>" broke out of the element, this <img> 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('<button>Hello</button>');
Expand Down
Loading