diff --git a/.changeset/stale-llamas-divide.md b/.changeset/stale-llamas-divide.md new file mode 100644 index 0000000000..a5e4b38c85 --- /dev/null +++ b/.changeset/stale-llamas-divide.md @@ -0,0 +1,5 @@ +--- +'rrweb-snapshot': patch +--- + +adds a correction for when chrome passes an incorrect grid-template-area cssText during snapshot diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 95444c18b3..d1e49baa5d 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -1,16 +1,16 @@ import { + documentNode, + documentTypeNode, + elementNode, idNodeMap, + IMirror, MaskInputFn, MaskInputOptions, nodeMetaMap, - IMirror, - serializedNodeWithId, - serializedNode, NodeType, - documentNode, - documentTypeNode, + serializedNode, + serializedNodeWithId, textNode, - elementNode, } from './types'; export function isElement(n: Node): n is Element { @@ -106,8 +106,48 @@ export function stringifyStylesheet(s: CSSStyleSheet): string | null { } } +function replaceChromeGridTemplateAreas(rule: CSSStyleRule): string { + const hasGridTemplateInCSSText = rule.cssText.includes('grid-template:'); + const hasGridTemplateAreaInStyleRules = + rule.style.getPropertyValue('grid-template-areas') !== ''; + const hasGridTemplateAreaInCSSText = rule.cssText.includes( + 'grid-template-areas:', + ); + if ( + isCSSStyleRule(rule) && + hasGridTemplateInCSSText && + hasGridTemplateAreaInStyleRules && + !hasGridTemplateAreaInCSSText + ) { + // chrome does not correctly provide the grid template areas in the rules cssText + // e.g. https://bugs.chromium.org/p/chromium/issues/detail?id=1303968 + // we remove the grid-template rule from the text... so everything from grid-template: to the next semicolon + // and then add each grid-template-x rule into the css text because Chrome isn't doing this correctly + const parts = rule.cssText + .split(';') + .filter((s) => !s.includes('grid-template:')) + .map((s) => s.trim()); + + const gridStyles: string[] = []; + + for (let i = 0; i < rule.style.length; i++) { + const styleName = rule.style[i]; + if (styleName.startsWith('grid-template')) { + gridStyles.push( + `${styleName}: ${rule.style.getPropertyValue(styleName)}`, + ); + } + } + parts.splice(parts.length - 1, 0, gridStyles.join('; ')); + return parts.join('; '); + } + return rule.cssText; +} + export function stringifyRule(rule: CSSRule): string { let importStringified; + let gridTemplateFixed; + if (isCSSImportRule(rule)) { try { importStringified = @@ -125,7 +165,11 @@ export function stringifyRule(rule: CSSRule): string { return fixSafariColons(rule.cssText); } - return importStringified || rule.cssText; + if (isCSSStyleRule(rule)) { + gridTemplateFixed = replaceChromeGridTemplateAreas(rule); + } + + return importStringified || gridTemplateFixed || rule.cssText; } export function fixSafariColons(cssStringified: string): string { diff --git a/packages/rrweb-snapshot/test/css.test.ts b/packages/rrweb-snapshot/test/css.test.ts index 2818386071..8bad9e76b4 100644 --- a/packages/rrweb-snapshot/test/css.test.ts +++ b/packages/rrweb-snapshot/test/css.test.ts @@ -1,5 +1,9 @@ import { parse, Rule, Media } from '../src/css'; -import { fixSafariColons, escapeImportStatement } from './../src/utils'; +import { + fixSafariColons, + escapeImportStatement, + stringifyRule, +} from './../src/utils'; describe('css parser', () => { it('should save the filename and source', () => { @@ -119,6 +123,52 @@ describe('css parser', () => { expect(out3).toEqual('[data-aa\\:other] { color: red; }'); }); + it('does not alter correctly parsed grid template rules', () => { + const cssText = + '#wrapper { display: grid; width: 100%; height: 100%; grid-template: repeat(2, 1fr); margin: 0px auto; }'; + const stringified = stringifyRule({ + cssText: cssText, + } as Partial as CSSStyleRule); + expect(stringified).toEqual(cssText); + }); + + it('fixes incorrectly parsed grid template rules', () => { + const cssText = + '#wrapper { display: grid; grid-template: "header header" max-content / repeat(2, 1fr); margin: 0px auto; }'; + // to avoid using JSDom we can fake as much of the CSSStyleDeclaration as we need + const cssStyleDeclaration: Record = { + length: 3, + 0: 'grid-template-areas', + 1: 'grid-template-rows', + 2: 'grid-template-columns', + items: (i: number): string => { + return [cssStyleDeclaration[i]].toString(); + }, + getPropertyValue: (key: string): string => { + if (key === 'grid-template-areas') { + return '"header header" "main main" "footer footer"'; + } + if (key === 'grid-template-rows') { + return 'repeat(2, 1fr)'; + } + if (key === 'grid-template-columns') { + return 'repeat(2, 1fr)'; + } + return ''; + }, + }; + + const stringified = stringifyRule({ + cssText: cssText, + selectorText: '#wrapper', + style: cssStyleDeclaration as unknown as CSSStyleDeclaration, + } as Partial as CSSStyleRule); + + expect(stringified).toEqual( + '#wrapper { display: grid; margin: 0px auto; grid-template-areas: "header header" "main main" "footer footer"; grid-template-rows: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr); }', + ); + }); + it('parses imports with quotes correctly', () => { const out1 = escapeImportStatement({ cssText: `@import url("/foo.css;900;800"");`,