From fba9caf06507e507173de5768282266e2266f83f Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Sun, 14 Sep 2025 15:24:39 +0100 Subject: [PATCH 1/4] Test demonstrating correct recording of CDATA section in embedded SVG --- .../test/__snapshots__/cdata.svg.snap.json | 58 +++++++++++++++++++ .../rrweb-snapshot/test/integration.test.ts | 22 +++++++ 2 files changed, 80 insertions(+) create mode 100644 packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json diff --git a/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json new file mode 100644 index 0000000000..b09ab90562 --- /dev/null +++ b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json @@ -0,0 +1,58 @@ +{ + "type": 0, + "childNodes": [ + { + "type": 2, + "tagName": "html", + "attributes": {}, + "childNodes": [ + { + "type": 2, + "tagName": "head", + "attributes": {}, + "childNodes": [], + "id": 3 + }, + { + "type": 2, + "tagName": "body", + "attributes": {}, + "childNodes": [ + { + "type": 2, + "tagName": "svg", + "attributes": { + "xmlns": "http://www.w3.org/2000/svg", + "version": "1.1" + }, + "childNodes": [ + { + "type": 2, + "tagName": "style", + "attributes": { + "_cssText": ".Icon > span { color: red; }" + }, + "childNodes": [ + { + "type": 4, + "textContent": "", + "id": 7 + } + ], + "isSVG": true, + "id": 6 + } + ], + "isSVG": true, + "id": 5 + } + ], + "id": 4 + } + ], + "id": 2 + } + ], + "compatMode": "BackCompat", + "id": 1 +} \ No newline at end of file diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index 1cc6acffea..9f7d7787c5 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -422,6 +422,28 @@ iframe.contentDocument.querySelector('center').clientHeight ); expect(snapshotResult).toMatchSnapshot(); }); + + it('correctly records CDATA section in SVG', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank', { + waitUntil: 'load', + }); + await page.evaluate(` +const defsSvg = (new window.DOMParser()).parseFromString( +'', 'image/svg+xml'); + document.body.appendChild(defsSvg.documentElement); +`); + await waitForRAF(page); // a small wait + const snapshotResult = JSON.stringify( + await page.evaluate(`${code}; + rrwebSnapshot.snapshot(document); + `), + null, + 2, + ); + const fname = `./__snapshots__/cdata.svg.snap.json`; + expect(snapshotResult).toMatchFileSnapshot(fname); + }); }); describe('iframe integration tests', function (this: ISuite) { From 3320edc86b5aa1baef50b4f18763f1890c2c9ef3 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Sun, 14 Sep 2025 16:05:02 +0100 Subject: [PATCH 2/4] Show that rebuilding fails when there's a CDATA type present, and provide a fix, also a fix that the style wasn't getting rebuilt --- .changeset/cdata-rebuild-html.md | 5 ++++ packages/rrweb-snapshot/src/rebuild.ts | 10 +++++-- .../test/__snapshots__/cdata.svg.snap.html | 10 +++++++ .../rrweb-snapshot/test/integration.test.ts | 26 ++++++++++++++++++- 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 .changeset/cdata-rebuild-html.md create mode 100644 packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html diff --git a/.changeset/cdata-rebuild-html.md b/.changeset/cdata-rebuild-html.md new file mode 100644 index 0000000000..4df7bcb564 --- /dev/null +++ b/.changeset/cdata-rebuild-html.md @@ -0,0 +1,5 @@ +--- +"rrweb-snapshot": patch +--- + +Fix that recording of CDATA sections were breaking rebuild diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 692da2d281..9b3cd00803 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -101,7 +101,7 @@ export function applyCssSplits( ): void { const childTextNodes = []; for (const scn of n.childNodes) { - if (scn.type === NodeType.Text) { + if (scn.type === NodeType.Text || scn.type === NodeType.CDATA) { childTextNodes.push(scn); } } @@ -432,7 +432,13 @@ function buildNode( } return doc.createTextNode(n.textContent); case NodeType.CDATA: - return doc.createCDATASection(n.textContent); + /* + https://developer.mozilla.org/en-US/docs/Web/API/Document/createCDATASection + expected: DOMException: Failed to execute 'createCDATASection' on 'Document': This operation is not supported for HTML documents. + "createTextNode() can often be used in its place" + */ + //return doc.createCDATASection(n.textContent); + return doc.createTextNode(n.textContent); case NodeType.Comment: return doc.createComment(n.textContent); default: diff --git a/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html new file mode 100644 index 0000000000..4de28a29b3 --- /dev/null +++ b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index 9f7d7787c5..0c36013e5f 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -434,15 +434,39 @@ const defsSvg = (new window.DOMParser()).parseFromString( document.body.appendChild(defsSvg.documentElement); `); await waitForRAF(page); // a small wait + const cdataType = await page.evaluate( + `document.querySelector('svg style').childNodes[0].nodeType`, + ); + assert(cdataType === 4); const snapshotResult = JSON.stringify( await page.evaluate(`${code}; - rrwebSnapshot.snapshot(document); + const snapshotResult = rrwebSnapshot.snapshot(document); + snapshotResult `), null, 2, ); const fname = `./__snapshots__/cdata.svg.snap.json`; expect(snapshotResult).toMatchFileSnapshot(fname); + + await waitForRAF(page); + const rebuildHtml = (await page.evaluate(` + const x = new XMLSerializer(); + const node = rrwebSnapshot.rebuild(JSON.parse('${snapshotResult.replace( + /\n/g, + '', + )}'), { doc: document }) + + let out = x.serializeToString(node); + if (document.querySelector('html').getAttribute('xmlns') !== 'http://www.w3.org/1999/xhtml') { + // this is just an artefact of serializeToString + out = out.replace(' xmlns=\"http://www.w3.org/1999/xhtml\"', ''); + } + out; // return +`)) as string; + + const fhname = `./__snapshots__/cdata.svg.snap.html`; + expect(rebuildHtml.replace(/>\n<')).toMatchFileSnapshot(fhname); }); }); From 2c67e534b691f3210552f12fc1f3ad0cb3d386b8 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Mon, 15 Sep 2025 10:21:55 +0100 Subject: [PATCH 3/4] Show that regular styles are also rendered with ` ` due to XMLSerializer --- .../test/__snapshots__/cdata.svg.snap.html | 1 + .../test/__snapshots__/cdata.svg.snap.json | 26 +++++++++++++++---- .../rrweb-snapshot/test/integration.test.ts | 3 +++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html index 4de28a29b3..2c27ab4c54 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html +++ b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html @@ -1,6 +1,7 @@ + diff --git a/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json index b09ab90562..0a5155e074 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json +++ b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json @@ -10,7 +10,23 @@ "type": 2, "tagName": "head", "attributes": {}, - "childNodes": [], + "childNodes": [ + { + "type": 2, + "tagName": "style", + "attributes": { + "_cssText": ".Icon > span { color: blue; }" + }, + "childNodes": [ + { + "type": 3, + "textContent": "", + "id": 5 + } + ], + "id": 4 + } + ], "id": 3 }, { @@ -36,18 +52,18 @@ { "type": 4, "textContent": "", - "id": 7 + "id": 9 } ], "isSVG": true, - "id": 6 + "id": 8 } ], "isSVG": true, - "id": 5 + "id": 7 } ], - "id": 4 + "id": 6 } ], "id": 2 diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index 0c36013e5f..b384e92e43 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -429,6 +429,9 @@ iframe.contentDocument.querySelector('center').clientHeight waitUntil: 'load', }); await page.evaluate(` +const regularStyle = document.createElement('style'); +regularStyle.innerText = '.Icon > span{ color: blue; }' +document.head.append(regularStyle); const defsSvg = (new window.DOMParser()).parseFromString( '', 'image/svg+xml'); document.body.appendChild(defsSvg.documentElement); From 18b68e520866cae7570815b0cc572e634e0468e4 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Mon, 15 Sep 2025 10:46:41 +0100 Subject: [PATCH 4/4] Don't ignore CDATA nodes, record their textContent; I don't believe any further escaping is needed --- packages/rrweb-snapshot/src/snapshot.ts | 34 +++++++------------ .../test/__snapshots__/cdata.svg.snap.html | 1 + .../test/__snapshots__/cdata.svg.snap.json | 14 ++++++++ .../rrweb-snapshot/test/integration.test.ts | 2 +- packages/types/src/index.ts | 2 +- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 8c29fa0d7f..483916b00a 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -465,18 +465,16 @@ function serializeNode( newlyAddedElement, rootId, }); - case n.TEXT_NODE: - return serializeTextNode(n as Text, { - doc, - needsMask, - maskTextFn, - rootId, - cssCaptured, - }); case n.CDATA_SECTION_NODE: + case n.TEXT_NODE: return { - type: NodeType.CDATA, - textContent: '', + type: n.nodeType === n.TEXT_NODE ? NodeType.Text : NodeType.CDATA, + textContent: serializeTextContent(n as Text, { + doc, + needsMask, + maskTextFn, + cssCaptured, + }), rootId, }; case n.COMMENT_NODE: @@ -496,17 +494,16 @@ function getRootId(doc: Document, mirror: Mirror): number | undefined { return docId === 1 ? undefined : docId; } -function serializeTextNode( - n: Text, +function serializeTextContent( + n: Text | CDATASection, options: { doc: Document; needsMask: boolean; maskTextFn: MaskTextFn | undefined; - rootId: number | undefined; cssCaptured?: boolean; }, -): serializedNode { - const { needsMask, maskTextFn, rootId, cssCaptured } = options; +): string { + const { needsMask, maskTextFn, cssCaptured } = options; // The parent node may not be a html element which has a tagName attribute. // So just let it be undefined which is ok in this use case. const parent = dom.parentNode(n); @@ -531,12 +528,7 @@ function serializeTextNode( ? maskTextFn(textContent, dom.parentElement(n)) : textContent.replace(/[\S]/g, '*'); } - - return { - type: NodeType.Text, - textContent: textContent || '', - rootId, - }; + return textContent || ''; } function serializeElementNode( diff --git a/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html index 2c27ab4c54..f057ead93c 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html +++ b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html @@ -6,6 +6,7 @@ +
</svg>& this is not markup<svg/>
\ No newline at end of file diff --git a/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json index 0a5155e074..6b24fd6036 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json +++ b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json @@ -57,6 +57,20 @@ ], "isSVG": true, "id": 8 + }, + { + "type": 2, + "tagName": "div", + "attributes": {}, + "childNodes": [ + { + "type": 4, + "textContent": "& this is not markup", + "id": 11 + } + ], + "isSVG": true, + "id": 10 } ], "isSVG": true, diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index b384e92e43..3ecf1ca2e7 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -433,7 +433,7 @@ const regularStyle = document.createElement('style'); regularStyle.innerText = '.Icon > span{ color: blue; }' document.head.append(regularStyle); const defsSvg = (new window.DOMParser()).parseFromString( -'', 'image/svg+xml'); +'
& this is not markup]]>
', 'image/svg+xml'); document.body.appendChild(defsSvg.documentElement); `); await waitForRAF(page); // a small wait diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index bba276e483..37644b2940 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -791,7 +791,7 @@ export type textNode = { export type cdataNode = { type: NodeType.CDATA; - textContent: ''; + textContent: string; }; export type commentNode = {