Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions packages/dom/src/clone-dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,22 +79,21 @@ export function cloneNodeAndShadow(ctx) {

// Handle <style> tag specifically for media queries
if (node.nodeName === 'STYLE' && !enableJavaScript) {
let cssText = node.textContent?.trim() || '';
if (!cssText && node.sheet) {
let ownText = node.textContent?.trim() || '';
if (!ownText && node.sheet) {
try {
const cssRules = node.sheet.cssRules;
if (cssRules && cssRules.length > 0) {
cssText = Array.from(cssRules).map(rule => rule.cssText).join('\n');
let cssText = Array.from(cssRules).map(rule => rule.cssText).join('\n');
if (cssText) {
clone.textContent = cssText;
clone.setAttribute('data-percy-cssom-serialized', 'true');
}
}
} catch (_) {
// ignore errors
}
}

if (cssText) {
clone.textContent = cssText;
clone.setAttribute('data-percy-cssom-serialized', 'true');
}
}

// We apply any element transformations here to avoid another treeWalk
Expand Down
3 changes: 1 addition & 2 deletions packages/dom/src/serialize-cssom.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ function styleSheetsMatch(sheetA, sheetB) {
// Only treat as mismatch when the live sheet has MORE rules than the
// clone — that signals rules were added via CSSOM (insertRule/adopted)
// and must be re-serialized. When lenA <= lenB, the clone already
// contains all of the live rules (often duplicated by clone-dom's
// <style> textContent handling); trust the clone's source text so that
// contains all of the live rules; trust the clone's source text so that
// CSS shorthand semantics (e.g. `all: initial; border-radius: var(...)`)
// survive — `cssRule.cssText` expansion appends logical longhands like
// `border-end-end-radius: initial` AFTER the shorthand, which silently
Expand Down
47 changes: 25 additions & 22 deletions packages/dom/test/serialize-css.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ describe('serializeCSSOM', () => {
it(`${platform}: serializes CSSOM and does not mutate the orignal DOM`, () => {
let $cssom = parseDOM(serializeDOM(), platform)('[data-percy-cssom-serialized]');

// linked and unmodified stylesheets are not included
expect($cssom).toHaveSize(3);
// linked, unmodified and plain authored stylesheets are not included
expect($cssom).toHaveSize(2);
expect($cssom[0].innerHTML).toBe('.box { height: 500px; }');
expect($cssom[1].innerHTML).toBe('.box { width: 1000px; }');

Expand Down Expand Up @@ -83,7 +83,7 @@ describe('serializeCSSOM', () => {
cssomSheet.deleteRule(0); // Remove all rules to make it empty
const serialized = serializeDOM();
let $cssom = parseDOM(serialized, platform)('[data-percy-cssom-serialized]');
expect($cssom).toHaveSize(2); // should skip the empty stylesheet
expect($cssom).toHaveSize(1); // should skip the empty stylesheet
});

it(`${platform}: preserves media queries inside CSSOM`, () => {
Expand Down Expand Up @@ -463,23 +463,14 @@ describe('serializeCSSOM', () => {

describe('regression: cloned <style> text-node duplication', () => {
// serializeCSSOM decides whether to re-serialize each in-memory CSSOM
// stylesheet via styleSheetsMatch(liveSheet, cloneSheet). Cloned <style>
// elements in real snapshots can end up with the original text node
// appended twice (clone-dom assigns clone.textContent = ... and the
// subsequent generic walkTree re-clones the original text node). The
// parsed cloneSheet then has 2x the rules of liveSheet.
//
// Strict length equality (`lenA !== lenB`) treats that as a mismatch
// and forces re-serialization via Array.from(liveSheet.cssRules).map(
// r => r.cssText), which is NOT semantically identity-preserving:
// Chromium expands `all: initial` into hundreds of longhands and
// emits logical-property longhands (border-end-end-radius: initial,
// ...) AFTER the shorthand it sat next to in source, silently
// overriding `border-radius: var(--x)` with 0.
//
// styleSheetsMatch must therefore tolerate lenB > lenA when every
// live rule still appears in the clone (re-serialization is only
// required when the live sheet has rules the clone is missing).
// stylesheet via styleSheetsMatch(liveSheet, cloneSheet). It must
// tolerate lenB > lenA when every live rule still appears in the clone:
// re-serialization via Array.from(liveSheet.cssRules).map(r => r.cssText)
// is NOT semantically identity-preserving — Chromium expands
// `all: initial` into hundreds of longhands and emits logical-property
// longhands (border-end-end-radius: initial, ...) AFTER the shorthand
// it sat next to in source, silently overriding
// `border-radius: var(--x)` with 0.

it('skips re-serialization when clone <style> text duplicates the live rules — preserves `all: initial; border-radius: var(--x)` semantics', () => {
withExample('<div class="box"></div>', { withShadow: false });
Expand All @@ -492,8 +483,7 @@ describe('serializeCSSOM', () => {
const clone = document.createDocumentFragment();
const cloneOwner = document.createElement('style');
cloneOwner.setAttribute('data-percy-element-id', 'dup-regression');
// Duplicate the source rule — what clone-dom currently produces.
// styleSheetFromNode will parse this into a sheet with 2 rules,
// styleSheetFromNode parses this into a sheet with 2 rules,
// while the live sheet has 1.
cloneOwner.textContent = liveRuleText + '\n' + liveRuleText;
clone.appendChild(cloneOwner);
Expand Down Expand Up @@ -549,5 +539,18 @@ describe('serializeCSSOM', () => {
expect(reserialized).not.toBeNull();
expect(reserialized.textContent).toContain('.live-only');
});

it('does not duplicate authored <style> text, preserving `background: var(--x)` shorthands', () => {
withExample('<style id="var-shorthand">.box { all: initial; background: var(--x); background-clip: text; }</style>', { withShadow: false });

let liveStyle = document.getElementById('var-shorthand');
let liveText = liveStyle.textContent;

let $ = parseDOM(serializeDOM(), 'plain');
let clonedStyle = $('#var-shorthand')[0];

expect(clonedStyle.innerHTML).toBe(liveText);
expect(clonedStyle.getAttribute('data-percy-cssom-serialized')).toBeNull();
});
});
});
Loading