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/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 new file mode 100644 index 0000000000..f057ead93c --- /dev/null +++ b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.html @@ -0,0 +1,12 @@ + + +
+ + + + + + \ 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 new file mode 100644 index 0000000000..6b24fd6036 --- /dev/null +++ b/packages/rrweb-snapshot/test/__snapshots__/cdata.svg.snap.json @@ -0,0 +1,88 @@ +{ + "type": 0, + "childNodes": [ + { + "type": 2, + "tagName": "html", + "attributes": {}, + "childNodes": [ + { + "type": 2, + "tagName": "head", + "attributes": {}, + "childNodes": [ + { + "type": 2, + "tagName": "style", + "attributes": { + "_cssText": ".Icon > span { color: blue; }" + }, + "childNodes": [ + { + "type": 3, + "textContent": "", + "id": 5 + } + ], + "id": 4 + } + ], + "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": 9 + } + ], + "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, + "id": 7 + } + ], + "id": 6 + } + ], + "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..3ecf1ca2e7 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -422,6 +422,55 @@ 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 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); +`); + 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}; + 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); + }); }); describe('iframe integration tests', function (this: ISuite) { 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 = {