fix(snapshotter): support constructed CSSStyleSheet

Fixes #7085
This commit is contained in:
Joel Einbinder 2021-06-17 09:41:29 -07:00 committed by GitHub
parent 36c5395d2d
commit 10a82f862c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 99 additions and 2 deletions

View File

@ -124,7 +124,7 @@ function snapshotNodes(snapshot: FrameSnapshot): NodeSnapshot[] {
}
function snapshotScript() {
function applyPlaywrightAttributes(shadowAttribute: string, scrollTopAttribute: string, scrollLeftAttribute: string) {
function applyPlaywrightAttributes(shadowAttribute: string, scrollTopAttribute: string, scrollLeftAttribute: string, styleSheetAttribute: string) {
const scrollTops: Element[] = [];
const scrollLefts: Element[] = [];
@ -152,6 +152,17 @@ function snapshotScript() {
template.remove();
visit(shadowRoot);
}
if ('adoptedStyleSheets' in (root as any)) {
const adoptedSheets: CSSStyleSheet[] = [...(root as any).adoptedStyleSheets];
for (const element of root.querySelectorAll(`template[${styleSheetAttribute}]`)) {
const template = element as HTMLTemplateElement;
const sheet = new CSSStyleSheet();
(sheet as any).replaceSync(template.getAttribute(styleSheetAttribute));
adoptedSheets.push(sheet);
}
(root as any).adoptedStyleSheets = adoptedSheets;
}
};
visit(document);
@ -172,5 +183,6 @@ function snapshotScript() {
const kShadowAttribute = '__playwright_shadow_root_';
const kScrollTopAttribute = '__playwright_scroll_top_';
const kScrollLeftAttribute = '__playwright_scroll_left_';
return `\n(${applyPlaywrightAttributes.toString()})('${kShadowAttribute}', '${kScrollTopAttribute}', '${kScrollLeftAttribute}')`;
const kStyleSheetAttribute = '__playwright_style_sheet_';
return `\n(${applyPlaywrightAttributes.toString()})('${kShadowAttribute}', '${kScrollTopAttribute}', '${kScrollLeftAttribute}', '${kStyleSheetAttribute}')`;
}

View File

@ -39,6 +39,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string) {
const kShadowAttribute = '__playwright_shadow_root_';
const kScrollTopAttribute = '__playwright_scroll_top_';
const kScrollLeftAttribute = '__playwright_scroll_left_';
const kStyleSheetAttribute = '__playwright_style_sheet_';
// Symbols for our own info on Nodes/StyleSheets.
const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_');
@ -296,6 +297,15 @@ export function frameSnapshotStreamer(snapshotStreamer: string) {
}
};
const visitChildStyleSheet = (child: CSSStyleSheet) => {
const snapshot = visitStyleSheet(child);
if (snapshot) {
result.push(snapshot.n);
expectValue(child);
equals = equals && snapshot.equals;
}
};
if (nodeType === Node.DOCUMENT_FRAGMENT_NODE)
attrs[kShadowAttribute] = 'open';
@ -345,6 +355,15 @@ export function frameSnapshotStreamer(snapshotStreamer: string) {
}
for (let child = node.firstChild; child; child = child.nextSibling)
visitChild(child);
let documentOrShadowRoot = null;
if (node.ownerDocument!.documentElement === node)
documentOrShadowRoot = node.ownerDocument;
else if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE)
documentOrShadowRoot = node;
if (documentOrShadowRoot) {
for (const sheet of (documentOrShadowRoot as any).adoptedStyleSheets || [])
visitChildStyleSheet(sheet);
}
}
// Process iframe src attribute before bailing out since it depends on a symbol, not the DOM.
@ -397,6 +416,21 @@ export function frameSnapshotStreamer(snapshotStreamer: string) {
return checkAndReturn(result);
};
const visitStyleSheet = (sheet: CSSStyleSheet) => {
const data = ensureCachedData(sheet);
const oldCSSText = data.cssText;
const cssText = this._updateStyleElementStyleSheetTextIfNeeded(sheet) || '';
if (cssText === oldCSSText)
return { equals: true, n: [[ snapshotNumber - data.ref![0], data.ref![1] ]] };
data.ref = [snapshotNumber, nodeCounter++];
return {
equals: false,
n: ['template', {
[kStyleSheetAttribute]: cssText,
}]
};
};
let html: NodeSnapshot;
if (document.documentElement) {
const { n } = visitNode(document.documentElement)!;

View File

@ -75,6 +75,14 @@ it.describe('snapshots', () => {
expect(distillSnapshot(snapshot2)).toBe('<style>button { color: blue; }</style><BUTTON>Hello</BUTTON>');
});
it('should have a custom doctype', async ({page, server, toImpl, snapshotter}) => {
await page.goto(server.EMPTY_PAGE);
await page.setContent('<!DOCTYPE foo><body>hi</body>');
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot');
expect(distillSnapshot(snapshot)).toBe('<!DOCTYPE foo>hi');
});
it('should respect subresource CSSOM change', async ({ page, server, toImpl, snapshotter }) => {
await page.goto(server.EMPTY_PAGE);
await page.route('**/style.css', route => {
@ -166,6 +174,49 @@ it.describe('snapshots', () => {
expect(distillSnapshot(snapshot)).toBe('<BUTTON data="two">Hello</BUTTON>');
}
});
it('should contain adopted style sheets', async ({ page, toImpl, contextFactory, snapshotPort, snapshotter, browserName }) => {
it.skip(browserName !== 'chromium', 'Constructed stylesheets are only in Chromium.');
await page.setContent('<button>Hello</button>');
await page.evaluate(() => {
const sheet = new CSSStyleSheet();
sheet.addRule('button', 'color: red');
(document as any).adoptedStyleSheets = [sheet];
const div = document.createElement('div');
const root = div.attachShadow({
mode: 'open'
});
root.append('foo');
const sheet2 = new CSSStyleSheet();
sheet2.addRule(':host', 'color: blue');
(root as any).adoptedStyleSheets = [sheet2];
document.body.appendChild(div);
});
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
const previewContext = await contextFactory();
const previewPage = await previewContext.newPage();
previewPage.on('console', console.log);
await previewPage.goto(`http://localhost:${snapshotPort}/snapshot/`);
await previewPage.evaluate(snapshotId => {
(window as any).showSnapshot(snapshotId);
}, `${snapshot1.snapshot().pageId}?name=snapshot1`);
// wait for the render frame to load
while (previewPage.frames().length < 2)
await new Promise(f => previewPage.once('frameattached', f));
// wait for it to render
await previewPage.frames()[1].waitForSelector('button');
const buttonColor = await previewPage.frames()[1].$eval('button', button => {
return window.getComputedStyle(button).color;
});
expect(buttonColor).toBe('rgb(255, 0, 0)');
const divColor = await previewPage.frames()[1].$eval('div', div => {
return window.getComputedStyle(div).color;
});
expect(divColor).toBe('rgb(0, 0, 255)');
await previewContext.close();
});
});
function distillSnapshot(snapshot) {