Skip to content

Commit

Permalink
Fix serialization and mutation of <textarea> elements taking account …
Browse files Browse the repository at this point in the history
…the duality that the value can be set in either the child node, or in the value _parameter_ (not attribute)
  • Loading branch information
eoghanmurray committed Nov 9, 2023
1 parent 8aea5b0 commit b5a4594
Show file tree
Hide file tree
Showing 9 changed files with 438 additions and 15 deletions.
25 changes: 21 additions & 4 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,14 @@ function serializeElementNode(
const value = (n as HTMLInputElement | HTMLTextAreaElement).value;
const checked = (n as HTMLInputElement).checked;
if (
tagName === 'textarea' &&
value &&
n.childNodes.length === 1 &&
n.childNodes[0].nodeType === n.TEXT_NODE &&
(n.childNodes[0] as Text).data === value
) {
// value will be recorded via the childNode instead
} else if (
attributes.type !== 'radio' &&
attributes.type !== 'checkbox' &&
attributes.type !== 'submit' &&
Expand Down Expand Up @@ -1077,10 +1085,19 @@ export function serializeNodeWithId(
stylesheetLoadTimeout,
keepIframeSrcFn,
};
for (const childN of Array.from(n.childNodes)) {
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
if (serializedChildNode) {
serializedNode.childNodes.push(serializedChildNode);

if (
serializedNode.type === NodeType.Element &&
serializedNode.tagName === 'textarea' &&
serializedNode.attributes.value !== undefined
) {
// value parameter in DOM reflects the correct value, so ignore childNode
} else {
for (const childN of Array.from(n.childNodes)) {
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
if (serializedChildNode) {
serializedNode.childNodes.push(serializedChildNode);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ exports[`integration tests [html file]: form-fields.html 1`] = `
</label>
<label for=\\"textarea\\">
<textarea name=\\"\\" id=\\"\\" cols=\\"30\\" rows=\\"10\\">1234</textarea>
<textarea name=\\"\\" id=\\"\\" cols=\\"30\\" rows=\\"10\\">1234</textarea>
</label>
<label for=\\"select\\">
<select name=\\"\\" id=\\"\\" value=\\"2\\">
Expand Down
4 changes: 3 additions & 1 deletion packages/rrweb-snapshot/test/html/form-fields.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
</label>
<label for="textarea">
<textarea name="" id="" cols="30" rows="10"></textarea>
<textarea name="" id="" cols="30" rows="10">-1</textarea>
</label>
<label for="select">
<select name="" id="">
Expand All @@ -36,7 +37,8 @@
document.querySelector('input[type="text"]').value = '1';
document.querySelector('input[type="radio"]').checked = true;
document.querySelector('input[type="checkbox"]').checked = true;
document.querySelector('textarea').value = '1234';
document.querySelector('textarea:empty').value = '1234';
document.querySelector('textarea:not(:empty)').value = '1234';
document.querySelector('select').value = '2';
</script>
</html>
40 changes: 39 additions & 1 deletion packages/rrweb-snapshot/test/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
serializeNodeWithId,
_isBlockedElement,
} from '../src/snapshot';
import { serializedNodeWithId } from '../src/types';
import { serializedNodeWithId, elementNode } from '../src/types';
import { Mirror } from '../src/utils';

describe('absolute url to stylesheet', () => {
Expand Down Expand Up @@ -218,3 +218,41 @@ describe('scrollTop/scrollLeft', () => {
});
});
});

describe('form', () => {
const serializeNode = (node: Node): serializedNodeWithId | null => {
return serializeNodeWithId(node, {
doc: document,
mirror: new Mirror(),
blockClass: 'blockblock',
blockSelector: null,
maskTextClass: 'maskmask',
maskTextSelector: null,
skipChild: false,
inlineStylesheet: true,
maskTextFn: undefined,
maskInputFn: undefined,
slimDOMOptions: {},
newlyAddedElement: false,
});
};

const render = (html: string): HTMLTextAreaElement => {
document.write(html);
return document.querySelector('textarea')!;
};

it('should record textarea values once', () => {
const el = render(`<textarea>Lorem ipsum</textarea>`);
const sel = serializeNode(el) as elementNode;
expect(sel?.attributes).toEqual({}); // shouldn't be stored in .value
expect(sel).toMatchObject({
childNodes: [
{
type: 3,
textContent: 'Lorem ipsum',
},
],
});
});
});
2 changes: 2 additions & 0 deletions packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1564,6 +1564,8 @@ export class Replayer {
const childNodeArray = 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
Expand Down
Loading

0 comments on commit b5a4594

Please sign in to comment.