Skip to content

Commit b5a4594

Browse files
committed
Fix serialization and mutation of <textarea> elements taking account the duality that the value can be set in either the child node, or in the value _parameter_ (not attribute)
1 parent 8aea5b0 commit b5a4594

File tree

9 files changed

+438
-15
lines changed

9 files changed

+438
-15
lines changed

packages/rrweb-snapshot/src/snapshot.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,14 @@ function serializeElementNode(
670670
const value = (n as HTMLInputElement | HTMLTextAreaElement).value;
671671
const checked = (n as HTMLInputElement).checked;
672672
if (
673+
tagName === 'textarea' &&
674+
value &&
675+
n.childNodes.length === 1 &&
676+
n.childNodes[0].nodeType === n.TEXT_NODE &&
677+
(n.childNodes[0] as Text).data === value
678+
) {
679+
// value will be recorded via the childNode instead
680+
} else if (
673681
attributes.type !== 'radio' &&
674682
attributes.type !== 'checkbox' &&
675683
attributes.type !== 'submit' &&
@@ -1077,10 +1085,19 @@ export function serializeNodeWithId(
10771085
stylesheetLoadTimeout,
10781086
keepIframeSrcFn,
10791087
};
1080-
for (const childN of Array.from(n.childNodes)) {
1081-
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
1082-
if (serializedChildNode) {
1083-
serializedNode.childNodes.push(serializedChildNode);
1088+
1089+
if (
1090+
serializedNode.type === NodeType.Element &&
1091+
serializedNode.tagName === 'textarea' &&
1092+
serializedNode.attributes.value !== undefined
1093+
) {
1094+
// value parameter in DOM reflects the correct value, so ignore childNode
1095+
} else {
1096+
for (const childN of Array.from(n.childNodes)) {
1097+
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
1098+
if (serializedChildNode) {
1099+
serializedNode.childNodes.push(serializedChildNode);
1100+
}
10841101
}
10851102
}
10861103

packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ exports[`integration tests [html file]: form-fields.html 1`] = `
247247
</label>
248248
<label for=\\"textarea\\">
249249
<textarea name=\\"\\" id=\\"\\" cols=\\"30\\" rows=\\"10\\">1234</textarea>
250+
<textarea name=\\"\\" id=\\"\\" cols=\\"30\\" rows=\\"10\\">1234</textarea>
250251
</label>
251252
<label for=\\"select\\">
252253
<select name=\\"\\" id=\\"\\" value=\\"2\\">

packages/rrweb-snapshot/test/html/form-fields.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
</label>
2121
<label for="textarea">
2222
<textarea name="" id="" cols="30" rows="10"></textarea>
23+
<textarea name="" id="" cols="30" rows="10">-1</textarea>
2324
</label>
2425
<label for="select">
2526
<select name="" id="">
@@ -36,7 +37,8 @@
3637
document.querySelector('input[type="text"]').value = '1';
3738
document.querySelector('input[type="radio"]').checked = true;
3839
document.querySelector('input[type="checkbox"]').checked = true;
39-
document.querySelector('textarea').value = '1234';
40+
document.querySelector('textarea:empty').value = '1234';
41+
document.querySelector('textarea:not(:empty)').value = '1234';
4042
document.querySelector('select').value = '2';
4143
</script>
4244
</html>

packages/rrweb-snapshot/test/snapshot.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
serializeNodeWithId,
88
_isBlockedElement,
99
} from '../src/snapshot';
10-
import { serializedNodeWithId } from '../src/types';
10+
import { serializedNodeWithId, elementNode } from '../src/types';
1111
import { Mirror } from '../src/utils';
1212

1313
describe('absolute url to stylesheet', () => {
@@ -218,3 +218,41 @@ describe('scrollTop/scrollLeft', () => {
218218
});
219219
});
220220
});
221+
222+
describe('form', () => {
223+
const serializeNode = (node: Node): serializedNodeWithId | null => {
224+
return serializeNodeWithId(node, {
225+
doc: document,
226+
mirror: new Mirror(),
227+
blockClass: 'blockblock',
228+
blockSelector: null,
229+
maskTextClass: 'maskmask',
230+
maskTextSelector: null,
231+
skipChild: false,
232+
inlineStylesheet: true,
233+
maskTextFn: undefined,
234+
maskInputFn: undefined,
235+
slimDOMOptions: {},
236+
newlyAddedElement: false,
237+
});
238+
};
239+
240+
const render = (html: string): HTMLTextAreaElement => {
241+
document.write(html);
242+
return document.querySelector('textarea')!;
243+
};
244+
245+
it('should record textarea values once', () => {
246+
const el = render(`<textarea>Lorem ipsum</textarea>`);
247+
const sel = serializeNode(el) as elementNode;
248+
expect(sel?.attributes).toEqual({}); // shouldn't be stored in .value
249+
expect(sel).toMatchObject({
250+
childNodes: [
251+
{
252+
type: 3,
253+
textContent: 'Lorem ipsum',
254+
},
255+
],
256+
});
257+
});
258+
});

packages/rrweb/src/replay/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,6 +1564,8 @@ export class Replayer {
15641564
const childNodeArray = Array.isArray(parent.childNodes)
15651565
? parent.childNodes
15661566
: Array.from(parent.childNodes);
1567+
// This should be redundant now as we are either recording the value or the childNode, and not both
1568+
// keeping around for backwards compatibility with old bad double data, see
15671569

15681570
// https://github.com/rrweb-io/rrweb/issues/745
15691571
// parent is textarea, will only keep one child node as the value

0 commit comments

Comments
 (0)