From 178ec57f1bbc3d2a532a02a7fa93ba5c466756cc Mon Sep 17 00:00:00 2001 From: jaj1014 Date: Fri, 11 Oct 2024 10:59:12 -0400 Subject: [PATCH 1/2] Add replaceChromeGridTemplateAreas utility function to fix grid-template-area styles that are improperly improperly parsed by Chrome into rule.cssText and causing broken recordings when attempting to play back --- .changeset/clean-plants-look.md | 5 ++ packages/rrweb-snapshot/src/utils.ts | 46 ++++++++++- packages/rrweb-snapshot/test/utils.test.ts | 91 ++++++++++++++++++++++ 3 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 .changeset/clean-plants-look.md 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..3071c24d39 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..6b64b4a8a6 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,96 @@ 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; }'); From 2f4c2e7cd84a6d09fc708b70ab6575915c5c2bda Mon Sep 17 00:00:00 2001 From: jaj1014 Date: Wed, 23 Oct 2024 21:02:09 +0000 Subject: [PATCH 2/2] Apply formatting changes --- packages/rrweb-snapshot/src/utils.ts | 4 +- packages/rrweb-snapshot/test/utils.test.ts | 55 ++++++++++++---------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 3071c24d39..b0aae61ca4 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -188,11 +188,11 @@ export function replaceChromeGridTemplateAreas(rule: CSSStyleRule): string { 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.selectorText} { ${styleDeclarations.join('; ')}; }`; } return rule.cssText; diff --git a/packages/rrweb-snapshot/test/utils.test.ts b/packages/rrweb-snapshot/test/utils.test.ts index 6b64b4a8a6..ed94321063 100644 --- a/packages/rrweb-snapshot/test/utils.test.ts +++ b/packages/rrweb-snapshot/test/utils.test.ts @@ -272,25 +272,28 @@ describe('utils', () => { 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 cssText = + '#wrapper { display: grid; width: 100%; height: 100%; grid-template: minmax(2, 1fr); margin: 0px auto; }'; const mockCssRule = { cssText, selectorText: '#wrapper', style: { - getPropertyValue (prop) { + getPropertyValue(prop) { return { - 'grid-template-areas': '' - }[prop] - } - } - } as Partial as CSSStyleRule + '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 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, @@ -305,22 +308,22 @@ describe('utils', () => { getPropertyValue: (key: string): string => { switch (key) { case 'grid-template-areas': - return '"header header" "main main" "footer footer"' + 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': + case 'display': return 'grid'; - case'margin': - return '0px auto' + case 'margin': + return '0px auto'; default: - return '' + return ''; } }, - } as Record - } as Partial as CSSStyleRule - + } as Record, + } as Partial as CSSStyleRule; + const mockCssRule2 = { cssText: cssText2, selectorText: '.some-class', @@ -333,28 +336,28 @@ describe('utils', () => { 4: 'column-gap', getPropertyValue: (key: string): string => { switch (key) { - case'color': + case 'color': return 'purple'; case 'grid-template-areas': - return '"TopNav TopNav" "SideNav Content" "SideNav Content"' + 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' + case 'column-gap': + return '32px'; default: - return '' + return ''; } }, - } as Record - } as Partial as CSSStyleRule + } 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; }' + '#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; }' + '.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; }', ); }); });