Skip to content

Commit

Permalink
fix(snapshotter): support constructed CSSStyleSheet
Browse files Browse the repository at this point in the history
Fixes #7085
  • Loading branch information
JoelEinbinder committed Jun 17, 2021
1 parent 36c5395 commit 10a82f8
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 2 deletions.
16 changes: 14 additions & 2 deletions src/server/snapshot/snapshotRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];

Expand Down Expand Up @@ -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);

Expand All @@ -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}')`;
}
34 changes: 34 additions & 0 deletions src/server/snapshot/snapshotterInjected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_');
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)!;
Expand Down
51 changes: 51 additions & 0 deletions tests/snapshotter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 10a82f8

Please sign in to comment.