From 1439ada026a829618697105e1dfb194f101e9612 Mon Sep 17 00:00:00 2001 From: yashrao2607 <140533789+yashrao2607@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:15:47 +0530 Subject: [PATCH] test: add regression coverage for #1734 and #1692 css crashes Both issues report an uncaught CssSyntaxError taking down the replayer: #1734 from empty longhand declarations the browser's CSSOM produces for shorthand properties (e.g. `border-top-style: ;`), #1692 from a /* rr_split */ marker landing inside a quoted attribute selector. Verified against main: both are already handled by the combination of #1600 (splits are rejoined before being handed to postcss, so a split never has to parse on its own) and #1580 (postcss failures are caught and logged instead of thrown). Reproduced the exact "Unclosed string" error from #1734's stack trace directly against adaptCssForReplay and confirmed it degrades to a console.warn + original text instead of propagating. Adding these as permanent regressions, using the reporters' own snippets where possible, so the fix in #1600/#1580 doesn't silently regress and so #1734/#1692 can be closed with a paper trail. --- packages/rrweb-snapshot/test/css.test.ts | 45 +++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/rrweb-snapshot/test/css.test.ts b/packages/rrweb-snapshot/test/css.test.ts index cd7bdbb596..1256a0b4f9 100644 --- a/packages/rrweb-snapshot/test/css.test.ts +++ b/packages/rrweb-snapshot/test/css.test.ts @@ -6,7 +6,7 @@ import { mediaSelectorPlugin, pseudoClassPlugin } from '../src/css'; import postcss, { type AcceptedPlugin } from 'postcss'; import { JSDOM } from 'jsdom'; import { splitCssText, stringifyStylesheet } from './../src/utils'; -import { applyCssSplits } from './../src/rebuild'; +import { adaptCssForReplay, applyCssSplits, createCache } from './../src/rebuild'; import * as fs from 'fs'; import * as path from 'path'; import type { @@ -433,4 +433,47 @@ describe('applyCssSplits css rejoiner', function () { halfCssText + otherHalfCssText, ); }); + + it('rejoins a split that lands inside a quoted attribute selector without corrupting it (#1692)', () => { + // reduced from the CssSyntaxError: :25:1093: Unclosed bracket + // report in https://github.com/rrweb-io/rrweb/issues/1692, where the + // split point fell inside a `[class~="..."]` attribute selector + const firstHalf = + '.not-prose :where(blockquote strong):not(:where([class~="not-prose"], [class~="not-prose"'; + const secondHalf = '] *)) { color: inherit; }'; + const markedCssText = [firstHalf, secondHalf].join('/* rr_split */'); + expect(() => + applyCssSplits(sn, markedCssText, true, mockLastUnusedArg), + ).not.toThrow(); + expect( + (sn.childNodes[0] as textNode).textContent + + (sn.childNodes[1] as textNode).textContent, + ).toEqual(firstHalf + secondHalf); + }); +}); + +describe('adaptCssForReplay with malformed css (#1734)', function () { + it('does not throw on empty longhand declarations produced by the browser CSSOM', () => { + // captured from `style.sheet.rules[0].style.cssText` after setting a + // shorthand `border` alongside `border-color: var(...)`, per + // https://github.com/rrweb-io/rrweb/issues/1734 + const cssText = + '.cl {border-top-style: ; border-top-width: ; border-right-style: ; ' + + 'border-right-width: ; border-bottom-style: ; border-bottom-width: ; ' + + 'border-left-style: ; border-left-width: ; border-image-source: ; ' + + 'border-image-slice: ; border-image-width: ; border-image-outset: ; ' + + 'border-image-repeat: ; border-color: var(--border-color);}'; + const cache = createCache(); + let result = ''; + expect(() => { + result = adaptCssForReplay(cssText, cache); + }).not.toThrow(); + expect(result).toContain('border-color: var(--border-color)'); + }); + + it('falls back to the original text instead of throwing on genuinely unparseable css', () => { + const cssText = '.a { content: "unterminated }'; + const cache = createCache(); + expect(() => adaptCssForReplay(cssText, cache)).not.toThrow(); + }); });