Skip to content

Commit

Permalink
fix: duplicate textContent for style element cause incremental style …
Browse files Browse the repository at this point in the history
…mutation invalid (rrweb-io#1417)

fix style element corner case
 - historically we have recorded duplicated css content in certain cases (demonstrated by the attached replayer test). This fix ensures that the replayer doesn't doubly add the content, which can cause problems when further mutations occur
---------
Review and further tests contributed by: Eoghan Murray <[email protected]>
  • Loading branch information
YunFeng0817 authored and jeffdnguyen committed Jul 29, 2024
1 parent aac006d commit 024dbc0
Show file tree
Hide file tree
Showing 7 changed files with 610 additions and 13 deletions.
6 changes: 6 additions & 0 deletions .changeset/cuddly-bikes-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"rrweb-snapshot": patch
"rrweb": patch
---

fix: duplicate textContent for style elements cause incremental style mutations to be invalid
3 changes: 2 additions & 1 deletion packages/rrweb-snapshot/src/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,9 @@ function buildNode(
value = adaptCssForReplay(value, cache);
}
if ((isTextarea || isRemoteOrDynamicCss) && typeof value === 'string') {
node.appendChild(doc.createTextNode(value));
// https://github.com/rrweb-io/rrweb/issues/112
// https://github.com/rrweb-io/rrweb/pull/1351
node.appendChild(doc.createTextNode(value));
n.childNodes = []; // value overrides childNodes
continue;
}
Expand Down
39 changes: 29 additions & 10 deletions packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1570,20 +1570,39 @@ export class Replayer {
if (
parentSn &&
parentSn.type === NodeType.Element &&
parentSn.tagName === 'textarea' &&
mutation.node.type === NodeType.Text
) {
const childNodeArray = Array.isArray(parent.childNodes)
const prospectiveSiblings = Array.isArray(parent.childNodes)
? parent.childNodes
: Array.from(parent.childNodes);
// This should be redundant now as we are either recording the value or the childNode, and not both
// keeping around for backwards compatibility with old bad double data, see

// https://github.com/rrweb-io/rrweb/issues/745
// parent is textarea, will only keep one child node as the value
for (const c of childNodeArray) {
if (c.nodeType === parent.TEXT_NODE) {
parent.removeChild(c as Node & RRNode);
if (parentSn.tagName === 'textarea') {
// This should be redundant now as we are either recording the value or the childNode, and not both
// keeping around for backwards compatibility with old bad double data, see

// https://github.com/rrweb-io/rrweb/issues/745
// parent is textarea, will only keep one child node as the value
for (const c of prospectiveSiblings) {
if (c.nodeType === parent.TEXT_NODE) {
parent.removeChild(c as Node & RRNode);
}
}
} else if (
parentSn.tagName === 'style' &&
prospectiveSiblings.length === 1
) {
// https://github.com/rrweb-io/rrweb/pull/1417
/**
* If both _cssText and textContent are present for a style element due to some existing bugs, the element was ending up with two child text nodes
* We need to remove the textNode created by _cssText as it doesn't have an id in the mirror, and thus cannot be further mutated.
*/
for (const cssText of prospectiveSiblings as (Node & RRNode)[]) {
if (
cssText.nodeType === parent.TEXT_NODE &&
!mirror.hasNode(cssText)
) {
target.textContent = cssText.textContent;
parent.removeChild(cssText);
}
}
}
} else if (parentSn?.type === NodeType.Document) {
Expand Down
218 changes: 218 additions & 0 deletions packages/rrweb/test/__snapshots__/integration.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16519,6 +16519,224 @@ exports[`record integration tests > should record shadow doms polyfilled by synt
]"
`;

exports[`record integration tests > should record style mutations and replay them correctly 1`] = `
"[
{
\\"type\\": 4,
\\"data\\": {
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"type\\": 2,
\\"data\\": {
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 1,
\\"name\\": \\"html\\",
\\"publicId\\": \\"\\",
\\"systemId\\": \\"\\",
\\"id\\": 2
},
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {
\\"lang\\": \\"en\\"
},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\t \\",
\\"id\\": 5
},
{
\\"type\\": 2,
\\"tagName\\": \\"style\\",
\\"attributes\\": {
\\"_cssText\\": \\"#one { color: rgb(255, 0, 0); }\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"#one { color: rgb(255, 0, 0); }\\",
\\"isStyle\\": true,
\\"id\\": 7
}
],
\\"id\\": 6
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 8
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 10
}
],
\\"id\\": 9
}
],
\\"id\\": 4
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 11
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\t \\",
\\"id\\": 13
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"id\\": \\"one\\"
},
\\"childNodes\\": [],
\\"id\\": 14
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 15
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"id\\": \\"two\\"
},
\\"childNodes\\": [],
\\"id\\": 16
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\t \\",
\\"id\\": 17
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 19
}
],
\\"id\\": 18
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\",
\\"id\\": 20
}
],
\\"id\\": 12
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 4,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"style\\",
\\"attributes\\": {
\\"_cssText\\": \\"#two { color: rgb(255, 0, 0); }\\"
},
\\"childNodes\\": [],
\\"id\\": 21
}
},
{
\\"parentId\\": 21,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"#two { color: rgb(255, 0, 0); }\\",
\\"isStyle\\": true,
\\"id\\": 22
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 13,
\\"id\\": 6,
\\"set\\": {
\\"property\\": \\"color\\",
\\"value\\": \\"rgb(255, 255, 0)\\"
},
\\"index\\": [
0
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 13,
\\"id\\": 21,
\\"set\\": {
\\"property\\": \\"color\\",
\\"value\\": \\"rgb(255, 255, 0)\\"
},
\\"index\\": [
0
]
}
}
]"
`;

exports[`record integration tests > should record webgl canvas mutations 1`] = `
"[
{
Expand Down
Loading

0 comments on commit 024dbc0

Please sign in to comment.