diff --git a/.changeset/clean-plants-look.md b/.changeset/clean-plants-look.md new file mode 100644 index 0000000000..dc5dab7f58 --- /dev/null +++ b/.changeset/clean-plants-look.md @@ -0,0 +1,5 @@ +--- +"rrweb-snapshot": patch +--- + +Fix issue with chrome improperly parsing grid-template-areas to grid-template shorthand. diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 862a3e5bf4..b0aae61ca4 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -148,10 +148,14 @@ export function stringifyRule(rule: CSSRule, sheetHref: string | null): string { return importStringified; } else { let ruleStringified = rule.cssText; - if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) { - // Safari does not escape selectors with : properly - // see https://bugs.webkit.org/show_bug.cgi?id=184604 - ruleStringified = fixSafariColons(ruleStringified); + if (isCSSStyleRule(rule)) { + ruleStringified = replaceChromeGridTemplateAreas(rule); + + if (rule.selectorText.includes(':')) { + // Safari does not escape selectors with : properly + // see https://bugs.webkit.org/show_bug.cgi?id=184604 + ruleStringified = fixSafariColons(ruleStringified); + } } if (sheetHref) { return absolutifyURLs(ruleStringified, sheetHref); @@ -160,6 +164,40 @@ export function stringifyRule(rule: CSSRule, sheetHref: string | null): string { } } +export function replaceChromeGridTemplateAreas(rule: CSSStyleRule): string { + // chrome does not correctly provide the grid-template-areas in the rule.cssText + // when it parses them to grid-template short-hand syntax + // e.g. https://bugs.chromium.org/p/chromium/issues/detail?id=1303968 + // so, we manually rebuild the cssText using rule.style when + // we find the cssText contains grid-template:, rule.style contains grid-template-areas, but + // cssText does not include grid-template-areas + const hasGridTemplateInCSSText = rule.cssText.includes('grid-template:'); + const hasGridTemplateAreaInStyleRules = + rule.style.getPropertyValue('grid-template-areas') !== ''; + const hasGridTemplateAreaInCSSText = rule.cssText.includes( + 'grid-template-areas:', + ); + + if ( + hasGridTemplateInCSSText && + hasGridTemplateAreaInStyleRules && + !hasGridTemplateAreaInCSSText + ) { + const styleDeclarations = []; + + for (let i = 0; i < rule.style.length; i++) { + const styleName = rule.style[i]; + const styleValue = rule.style.getPropertyValue(styleName); + + styleDeclarations.push(`${styleName}: ${styleValue}`); + } + + return `${rule.selectorText} { ${styleDeclarations.join('; ')}; }`; + } + + return rule.cssText; +} + export function fixSafariColons(cssStringified: string): string { // Replace e.g. [aa:bb] with [aa\\:bb] const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm; diff --git a/packages/rrweb-snapshot/test/utils.test.ts b/packages/rrweb-snapshot/test/utils.test.ts index c422223bed..ed94321063 100644 --- a/packages/rrweb-snapshot/test/utils.test.ts +++ b/packages/rrweb-snapshot/test/utils.test.ts @@ -6,6 +6,7 @@ import { NodeType, serializedNode } from '../src/types'; import { escapeImportStatement, extractFileExtension, + replaceChromeGridTemplateAreas, fixSafariColons, isNodeMetaEqual, } from '../src/utils'; @@ -268,6 +269,99 @@ describe('utils', () => { expect(out5).toEqual(`@import url("/foo.css;900;800\\"") layer;`); }); }); + + describe('replaceChromeGridTemplateAreas', () => { + it('does not alter corectly parsed grid template rules', () => { + const cssText = + '#wrapper { display: grid; width: 100%; height: 100%; grid-template: minmax(2, 1fr); margin: 0px auto; }'; + const mockCssRule = { + cssText, + selectorText: '#wrapper', + style: { + getPropertyValue(prop) { + return { + 'grid-template-areas': '', + }[prop]; + }, + }, + } as Partial as CSSStyleRule; + + expect(replaceChromeGridTemplateAreas(mockCssRule)).toEqual(cssText); + }); + + it('fixes incorrectly parsed grid template rules', () => { + const cssText1 = + '#wrapper { grid-template-areas: "header header" "main main" "footer footer"; grid-template-rows: minmax(2, 1fr); grid-template-columns: minmax(2, 1fr); display: grid; margin: 0px auto; }'; + const cssText2 = + '.some-class { color: purple; grid-template: "TopNav TopNav" 65px "SideNav Content" 52px "SideNav Content" / 255px auto; column-gap: 32px; }'; + + const mockCssRule1 = { + cssText: cssText1, + selectorText: '#wrapper', + style: { + length: 5, + 0: 'grid-template-areas', + 1: 'grid-template-rows', + 2: 'grid-template-columns', + 3: 'display', + 4: 'margin', + getPropertyValue: (key: string): string => { + switch (key) { + case 'grid-template-areas': + return '"header header" "main main" "footer footer"'; + case 'grid-template-rows': + return 'minmax(2, 1fr)'; + case 'grid-template-columns': + return 'minmax(2, 1fr)'; + case 'display': + return 'grid'; + case 'margin': + return '0px auto'; + default: + return ''; + } + }, + } as Record, + } as Partial as CSSStyleRule; + + const mockCssRule2 = { + cssText: cssText2, + selectorText: '.some-class', + style: { + length: 5, + 0: 'color', + 1: 'grid-template-areas', + 2: 'grid-template-rows', + 3: 'grid-template-columns', + 4: 'column-gap', + getPropertyValue: (key: string): string => { + switch (key) { + case 'color': + return 'purple'; + case 'grid-template-areas': + return '"TopNav TopNav" "SideNav Content" "SideNav Content"'; + case 'grid-template-rows': + return '65px 52px auto'; + case 'grid-template-columns': + return '255px auto'; + case 'column-gap': + return '32px'; + default: + return ''; + } + }, + } as Record, + } as Partial as CSSStyleRule; + + expect(replaceChromeGridTemplateAreas(mockCssRule1)).toEqual( + '#wrapper { grid-template-areas: "header header" "main main" "footer footer"; grid-template-rows: minmax(2, 1fr); grid-template-columns: minmax(2, 1fr); display: grid; margin: 0px auto; }', + ); + expect(replaceChromeGridTemplateAreas(mockCssRule2)).toEqual( + '.some-class { color: purple; grid-template-areas: "TopNav TopNav" "SideNav Content" "SideNav Content"; grid-template-rows: 65px 52px auto; grid-template-columns: 255px auto; column-gap: 32px; }', + ); + }); + }); + describe('fixSafariColons', () => { it('parses : in attribute selectors correctly', () => { const out1 = fixSafariColons('[data-foo] { color: red; }');