Skip to content

Commit

Permalink
🐛 Serialize all CSSOM rules (#1051)
Browse files Browse the repository at this point in the history
Previously, we excluded serializing CSSOM rules that were already seemingly included in the
document. However, JavaScript can still manipulate this CSSOM without updating the document, which
prevents some CSSOM changes from being properly serialized. By removing this exclusion, existing
CSSOM style content will also be serialized, which will include any manipulated CSSOM.
  • Loading branch information
wwilsman authored Sep 1, 2022
1 parent 2174d78 commit 91ac9cc
Show file tree
Hide file tree
Showing 2 changed files with 13 additions and 31 deletions.
9 changes: 5 additions & 4 deletions packages/dom/src/serialize-cssom.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// Returns true if a stylesheet is a CSSOM-based stylesheet.
function isCSSOM(styleSheet) {
// no href, has a rulesheet, and isn't already in the DOM
return !styleSheet.href && styleSheet.cssRules &&
!styleSheet.ownerNode?.innerText?.trim().length;
// no href, has a rulesheet, and has an owner node
return !styleSheet.href && styleSheet.cssRules && styleSheet.ownerNode;
}

// Outputs in-memory CSSOM into their respective DOM nodes.
Expand All @@ -14,11 +13,13 @@ export function serializeCSSOM(dom, clone) {
let cloneOwnerNode = clone.querySelector(`[data-percy-element-id="${styleId}"]`);

style.type = 'text/css';
style.setAttribute('data-percy-element-id', styleId);
style.setAttribute('data-percy-cssom-serialized', 'true');
style.innerHTML = Array.from(styleSheet.cssRules)
.reduce((prev, cssRule) => prev + cssRule.cssText, '');
.map(cssRule => cssRule.cssText).join('\n');

cloneOwnerNode.parentNode.insertBefore(style, cloneOwnerNode.nextSibling);
cloneOwnerNode.remove();
}
}
}
Expand Down
35 changes: 8 additions & 27 deletions packages/dom/test/serialize-css.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,25 @@ import serializeDOM from '@percy/dom';

describe('serializeCSSOM', () => {
beforeEach(() => {
withExample('<div class="box"></div><style>div { display: inline-block; }</style>');
let link = '<link rel="stylesheet" href="data:text/css,.box { margin: 10px; }"/>';
let style = '<style>.box { display: inline-block; }</style>';

withExample(`<div class="box"></div>${link}${style}`);
withCSSOM('.box { height: 500px; width: 500px; background-color: green; }');
});

it('serializes CSSOM and does not mutate the orignal DOM', () => {
let $cssom = parseDOM(serializeDOM())('[data-percy-cssom-serialized]');

expect($cssom).toHaveSize(1);
// linked stylesheet is not included
expect($cssom).toHaveSize(2);
expect($cssom[0].innerHTML).toBe('.box { height: 500px; width: 500px; background-color: green; }');
expect($cssom[1].innerHTML).toBe('.box { display: inline-block; }');
expect(document.styleSheets[0]).toHaveProperty('ownerNode.innerText', '');
expect(document.querySelectorAll('[data-percy-cssom-serialized]')).toHaveSize(0);
});

it('does not serialize CSSOM that exists outside of memory', () => {
let $css = parseDOM(serializeDOM())('style');

expect($css).toHaveSize(3);
expect($css[1].innerHTML).toBe('.box { height: 500px; width: 500px; background-color: green; }');
expect($css[1].getAttribute('data-percy-cssom-serialized')).toBeDefined();
// style #2 (index 1) is the original injected style tag for `withCSSOM`
expect($css[2].innerHTML).toBe('div { display: inline-block; }');
expect($css[2].getAttribute('data-percy-cssom-serialized')).toBeNull();
});

it('does not break the CSSOM by adding new styles after serializng', () => {
it('does not break the CSSOM by adding new styles after serializing', () => {
let cssomSheet = document.styleSheets[0];

// serialize DOM
Expand All @@ -42,19 +36,6 @@ describe('serializeCSSOM', () => {
.toBe('.box { height: 200px; width: 200px; background-color: blue; }');
});

it('does not break the CSSOM with white space in the style tag', () => {
withCSSOM(
'.box { height: 500px; width: 500px; background-color: green; }',
$style => ($style.innerText = ' ')
);

let $ = parseDOM(serializeDOM());
let $cssom = $('[data-percy-cssom-serialized]');

expect($cssom).toHaveSize(1);
expect($cssom[0].innerHTML).toBe('.box { height: 500px; width: 500px; background-color: green; }');
});

it('does not serialize the CSSOM when JS is enabled', () => {
let $ = parseDOM(serializeDOM({ enableJavaScript: true }));
expect(document.styleSheets[0]).toHaveProperty('ownerNode.innerText', '');
Expand Down

0 comments on commit 91ac9cc

Please sign in to comment.