Fix serialization and mutation of <textarea> elements (#1351)
* 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)
* Backwards compatibility: Bug fix and regression test for #112
- this is to fix up 'historical' recordings, as duplicate textarea content should no longer be being created at record time
- new test shows what the snapshot generated by previous versions of rrweb used to look like, hence 'bad'
- original 0efe23f04a fix either didn't work or no longer works due to childNodes being appended subsequent to this part of the code
- during review, we also verified that the `_cssText` case should still be handled okay, as there's currently no scenario where csstext is present with css child nodes of a <style>
* Masking: Fix that textarea values were being missed by the masking system if the value was recorded as a child node
- I didn't notice that form.html was used in other tests, so lucky that I noticed that those tests also should have the 'pre value' masked out
* Simplify by always storing the textarea value in the `.value` attribute (from it's DOM property) and not as a childNode. It should still be rebuilt as a childNode rather than a property
---------
Authored-by: eoghanmurray <eoghan@getthere.ie>
This commit is contained in:
6
.changeset/rare-adults-sneeze.md
Normal file
6
.changeset/rare-adults-sneeze.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'rrweb-snapshot': patch
|
||||||
|
'rrweb': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Don't double-record the values of <textarea>s when they already have some content prefilled #1301
|
||||||
@@ -5,7 +5,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "npm run prepack",
|
"prepare": "npm run prepack",
|
||||||
"prepack": "npm run bundle && npm run typings",
|
"prepack": "npm run bundle && npm run typings",
|
||||||
"test": "jest",
|
"retest": "jest",
|
||||||
|
"test": "yarn bundle && yarn retest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:update": "jest --updateSnapshot",
|
"test:update": "jest --updateSnapshot",
|
||||||
"bundle": "rollup --config",
|
"bundle": "rollup --config",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
tagMap,
|
tagMap,
|
||||||
elementNode,
|
elementNode,
|
||||||
BuildCache,
|
BuildCache,
|
||||||
attributes,
|
|
||||||
legacyAttributes,
|
legacyAttributes,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { isElement, Mirror, isNodeMetaEqual } from './utils';
|
import { isElement, Mirror, isNodeMetaEqual } from './utils';
|
||||||
@@ -200,14 +199,9 @@ function buildNode(
|
|||||||
value = addHoverClass(value, cache);
|
value = addHoverClass(value, cache);
|
||||||
}
|
}
|
||||||
if ((isTextarea || isRemoteOrDynamicCss) && typeof value === 'string') {
|
if ((isTextarea || isRemoteOrDynamicCss) && typeof value === 'string') {
|
||||||
const child = doc.createTextNode(value);
|
node.appendChild(doc.createTextNode(value));
|
||||||
// https://github.com/rrweb-io/rrweb/issues/112
|
// https://github.com/rrweb-io/rrweb/issues/112
|
||||||
for (const c of Array.from(node.childNodes)) {
|
n.childNodes = []; // value overrides childNodes
|
||||||
if (c.nodeType === node.TEXT_NODE) {
|
|
||||||
node.removeChild(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node.appendChild(child);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
MaskInputFn,
|
MaskInputFn,
|
||||||
KeepIframeSrcFn,
|
KeepIframeSrcFn,
|
||||||
ICanvas,
|
ICanvas,
|
||||||
|
elementNode,
|
||||||
serializedElementNodeWithId,
|
serializedElementNodeWithId,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {
|
import {
|
||||||
@@ -672,10 +673,9 @@ function serializeElementNode(
|
|||||||
attributes.type !== 'button' &&
|
attributes.type !== 'button' &&
|
||||||
value
|
value
|
||||||
) {
|
) {
|
||||||
const type = getInputType(n);
|
|
||||||
attributes.value = maskInputValue({
|
attributes.value = maskInputValue({
|
||||||
element: n,
|
element: n,
|
||||||
type,
|
type: getInputType(n),
|
||||||
tagName,
|
tagName,
|
||||||
value,
|
value,
|
||||||
maskInputOptions,
|
maskInputOptions,
|
||||||
@@ -1090,10 +1090,19 @@ export function serializeNodeWithId(
|
|||||||
stylesheetLoadTimeout,
|
stylesheetLoadTimeout,
|
||||||
keepIframeSrcFn,
|
keepIframeSrcFn,
|
||||||
};
|
};
|
||||||
for (const childN of Array.from(n.childNodes)) {
|
|
||||||
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
|
if (
|
||||||
if (serializedChildNode) {
|
serializedNode.type === NodeType.Element &&
|
||||||
serializedNode.childNodes.push(serializedChildNode);
|
serializedNode.tagName === 'textarea' &&
|
||||||
|
(serializedNode as elementNode).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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ exports[`integration tests [html file]: form-fields.html 1`] = `
|
|||||||
</label>
|
</label>
|
||||||
<label for=\\"textarea\\">
|
<label for=\\"textarea\\">
|
||||||
<textarea name=\\"\\" id=\\"\\" cols=\\"30\\" rows=\\"10\\">1234</textarea>
|
<textarea name=\\"\\" id=\\"\\" cols=\\"30\\" rows=\\"10\\">1234</textarea>
|
||||||
|
<textarea name=\\"\\" id=\\"\\" cols=\\"30\\" rows=\\"10\\">1234</textarea>
|
||||||
</label>
|
</label>
|
||||||
<label for=\\"select\\">
|
<label for=\\"select\\">
|
||||||
<select name=\\"\\" id=\\"\\" value=\\"2\\">
|
<select name=\\"\\" id=\\"\\" value=\\"2\\">
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label for="textarea">
|
<label for="textarea">
|
||||||
<textarea name="" id="" cols="30" rows="10"></textarea>
|
<textarea name="" id="" cols="30" rows="10"></textarea>
|
||||||
|
<textarea name="" id="" cols="30" rows="10">-1</textarea>
|
||||||
</label>
|
</label>
|
||||||
<label for="select">
|
<label for="select">
|
||||||
<select name="" id="">
|
<select name="" id="">
|
||||||
@@ -36,7 +37,8 @@
|
|||||||
document.querySelector('input[type="text"]').value = '1';
|
document.querySelector('input[type="text"]').value = '1';
|
||||||
document.querySelector('input[type="radio"]').checked = true;
|
document.querySelector('input[type="radio"]').checked = true;
|
||||||
document.querySelector('input[type="checkbox"]').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';
|
document.querySelector('select').value = '2';
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
serializeNodeWithId,
|
serializeNodeWithId,
|
||||||
_isBlockedElement,
|
_isBlockedElement,
|
||||||
} from '../src/snapshot';
|
} from '../src/snapshot';
|
||||||
import { serializedNodeWithId } from '../src/types';
|
import { serializedNodeWithId, elementNode } from '../src/types';
|
||||||
import { Mirror } from '../src/utils';
|
import { Mirror } from '../src/utils';
|
||||||
|
|
||||||
describe('absolute url to stylesheet', () => {
|
describe('absolute url to stylesheet', () => {
|
||||||
@@ -218,3 +218,42 @@ 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;
|
||||||
|
|
||||||
|
// we serialize according to where the DOM stores the value, not how
|
||||||
|
// the HTML stores it (this is so that maskInputValue can work over
|
||||||
|
// inputs/textareas/selects in a uniform way)
|
||||||
|
expect(sel).toMatchObject({
|
||||||
|
attributes: {
|
||||||
|
value: 'Lorem ipsum',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(sel?.childNodes).toEqual([]); // shouldn't be stored in childNodes while in transit
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"test": "yarn test:headless",
|
"test": "yarn test:headless",
|
||||||
"test:watch": "yarn test:headless -- --watch",
|
"test:watch": "yarn test:headless -- --watch",
|
||||||
"test:update": "yarn test:headless -- --updateSnapshot",
|
"test:update": "yarn test:headless -- --updateSnapshot",
|
||||||
|
"retest:update": "PUPPETEER_HEADLESS=true yarn retest -- --updateSnapshot",
|
||||||
"repl": "yarn bundle:browser && node scripts/repl.js",
|
"repl": "yarn bundle:browser && node scripts/repl.js",
|
||||||
"live-stream": "yarn bundle:browser && node scripts/stream.js",
|
"live-stream": "yarn bundle:browser && node scripts/stream.js",
|
||||||
"dev": "yarn bundle:browser --watch",
|
"dev": "yarn bundle:browser --watch",
|
||||||
|
|||||||
@@ -285,7 +285,11 @@ export default class MutationBuffer {
|
|||||||
return nextId;
|
return nextId;
|
||||||
};
|
};
|
||||||
const pushAdd = (n: Node) => {
|
const pushAdd = (n: Node) => {
|
||||||
if (!n.parentNode || !inDom(n)) {
|
if (
|
||||||
|
!n.parentNode ||
|
||||||
|
!inDom(n) ||
|
||||||
|
(n.parentNode as Element).tagName === 'TEXTAREA'
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parentId = isShadowRoot(n.parentNode)
|
const parentId = isShadowRoot(n.parentNode)
|
||||||
@@ -435,10 +439,17 @@ export default class MutationBuffer {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
texts: this.texts
|
texts: this.texts
|
||||||
.map((text) => ({
|
.map((text) => {
|
||||||
id: this.mirror.getId(text.node),
|
const n = text.node;
|
||||||
value: text.value,
|
if ((n.parentNode as Element).tagName === 'TEXTAREA') {
|
||||||
}))
|
// the node is being ignored as it isn't in the mirror, so shift mutation to attributes on parent textarea
|
||||||
|
this.genTextAreaValueMutation(n.parentNode as HTMLTextAreaElement);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: this.mirror.getId(n),
|
||||||
|
value: text.value,
|
||||||
|
};
|
||||||
|
})
|
||||||
// no need to include them on added elements, as they have just been serialized with up to date attribubtes
|
// no need to include them on added elements, as they have just been serialized with up to date attribubtes
|
||||||
.filter((text) => !addedIds.has(text.id))
|
.filter((text) => !addedIds.has(text.id))
|
||||||
// text mutation's id was not in the mirror map means the target node has been removed
|
// text mutation's id was not in the mirror map means the target node has been removed
|
||||||
@@ -497,6 +508,24 @@ export default class MutationBuffer {
|
|||||||
this.mutationCb(payload);
|
this.mutationCb(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private genTextAreaValueMutation = (textarea: HTMLTextAreaElement) => {
|
||||||
|
let item = this.attributeMap.get(textarea);
|
||||||
|
if (!item) {
|
||||||
|
item = {
|
||||||
|
node: textarea,
|
||||||
|
attributes: {},
|
||||||
|
styleDiff: {},
|
||||||
|
_unchangedStyles: {},
|
||||||
|
};
|
||||||
|
this.attributes.push(item);
|
||||||
|
this.attributeMap.set(textarea, item);
|
||||||
|
}
|
||||||
|
item.attributes.value = Array.from(
|
||||||
|
textarea.childNodes,
|
||||||
|
(cn) => cn.textContent || '',
|
||||||
|
).join('');
|
||||||
|
};
|
||||||
|
|
||||||
private processMutation = (m: mutationRecord) => {
|
private processMutation = (m: mutationRecord) => {
|
||||||
if (isIgnored(m.target, this.mirror)) {
|
if (isIgnored(m.target, this.mirror)) {
|
||||||
return;
|
return;
|
||||||
@@ -642,6 +671,12 @@ export default class MutationBuffer {
|
|||||||
if (isBlocked(m.target, this.blockClass, this.blockSelector, true))
|
if (isBlocked(m.target, this.blockClass, this.blockSelector, true))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if ((m.target as Element).tagName === 'TEXTAREA') {
|
||||||
|
// children would be ignored in genAdds as they aren't in the mirror
|
||||||
|
this.genTextAreaValueMutation(m.target as HTMLTextAreaElement);
|
||||||
|
return; // any removedNodes won't have been in mirror either
|
||||||
|
}
|
||||||
|
|
||||||
m.addedNodes.forEach((n) => this.genAdds(n, m.target));
|
m.addedNodes.forEach((n) => this.genAdds(n, m.target));
|
||||||
m.removedNodes.forEach((n) => {
|
m.removedNodes.forEach((n) => {
|
||||||
const nodeId = this.mirror.getId(n);
|
const nodeId = this.mirror.getId(n);
|
||||||
|
|||||||
@@ -1564,6 +1564,8 @@ export class Replayer {
|
|||||||
const childNodeArray = Array.isArray(parent.childNodes)
|
const childNodeArray = Array.isArray(parent.childNodes)
|
||||||
? parent.childNodes
|
? parent.childNodes
|
||||||
: Array.from(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
|
// https://github.com/rrweb-io/rrweb/issues/745
|
||||||
// parent is textarea, will only keep one child node as the value
|
// parent is textarea, will only keep one child node as the value
|
||||||
@@ -1761,10 +1763,24 @@ export class Replayer {
|
|||||||
// for safe
|
// for safe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(target as Element | RRElement).setAttribute(
|
if (attributeName === 'value' && target.nodeName === 'TEXTAREA') {
|
||||||
attributeName,
|
// this may or may not have an effect on the value property (which is what is displayed)
|
||||||
value,
|
// depending on whether the textarea has been modified by the user yet
|
||||||
);
|
// TODO: replaceChildNodes is not available in RRDom
|
||||||
|
const textarea = target as TNode;
|
||||||
|
textarea.childNodes.forEach((c) =>
|
||||||
|
textarea.removeChild(c as TNode),
|
||||||
|
);
|
||||||
|
const tn = target.ownerDocument?.createTextNode(value);
|
||||||
|
if (tn) {
|
||||||
|
textarea.appendChild(tn as TNode);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(target as Element | RRElement).setAttribute(
|
||||||
|
attributeName,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.warn(
|
this.warn(
|
||||||
'An error occurred may due to the checkout feature.',
|
'An error occurred may due to the checkout feature.',
|
||||||
|
|||||||
@@ -2268,8 +2268,22 @@ exports[`record integration tests can record form interactions 1`] = `
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 61
|
\\"id\\": 61
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"textarea\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"value\\": \\"pre value\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 63
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 18
|
\\"id\\": 18
|
||||||
@@ -2277,7 +2291,7 @@ exports[`record integration tests can record form interactions 1`] = `
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n \\",
|
\\"textContent\\": \\"\\\\n \\\\n \\",
|
||||||
\\"id\\": 62
|
\\"id\\": 64
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -2287,15 +2301,15 @@ exports[`record integration tests can record form interactions 1`] = `
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
||||||
\\"id\\": 64
|
\\"id\\": 66
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 63
|
\\"id\\": 65
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
||||||
\\"id\\": 65
|
\\"id\\": 67
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 16
|
\\"id\\": 16
|
||||||
@@ -3811,6 +3825,270 @@ exports[`record integration tests can record style changes compactly and preserv
|
|||||||
]"
|
]"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`record integration tests can record textarea mutations correctly 1`] = `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"type\\": 0,
|
||||||
|
\\"data\\": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 1,
|
||||||
|
\\"data\\": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"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 \\",
|
||||||
|
\\"id\\": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"meta\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"charset\\": \\"UTF-8\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"meta\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"name\\": \\"viewport\\",
|
||||||
|
\\"content\\": \\"width=device-width, initial-scale=1.0\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"title\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"Empty\\",
|
||||||
|
\\"id\\": 11
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 12
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"body\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"div\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"id\\": \\"one\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\\\n \\",
|
||||||
|
\\"id\\": 17
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"script\\",
|
||||||
|
\\"attributes\\": {},
|
||||||
|
\\"childNodes\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
||||||
|
\\"id\\": 19
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
||||||
|
\\"id\\": 20
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 14
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"id\\": 1
|
||||||
|
},
|
||||||
|
\\"initialOffset\\": {
|
||||||
|
\\"left\\": 0,
|
||||||
|
\\"top\\": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"data\\": {
|
||||||
|
\\"source\\": 0,
|
||||||
|
\\"texts\\": [],
|
||||||
|
\\"attributes\\": [],
|
||||||
|
\\"removes\\": [],
|
||||||
|
\\"adds\\": [
|
||||||
|
{
|
||||||
|
\\"parentId\\": 14,
|
||||||
|
\\"nextId\\": null,
|
||||||
|
\\"node\\": {
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"textarea\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"value\\": \\"pre value\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"data\\": {
|
||||||
|
\\"source\\": 0,
|
||||||
|
\\"texts\\": [],
|
||||||
|
\\"attributes\\": [
|
||||||
|
{
|
||||||
|
\\"id\\": 21,
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"value\\": \\"ok\\"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"removes\\": [],
|
||||||
|
\\"adds\\": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"data\\": {
|
||||||
|
\\"source\\": 0,
|
||||||
|
\\"texts\\": [],
|
||||||
|
\\"attributes\\": [
|
||||||
|
{
|
||||||
|
\\"id\\": 21,
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"value\\": \\"ok3\\"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"removes\\": [],
|
||||||
|
\\"adds\\": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"data\\": {
|
||||||
|
\\"source\\": 2,
|
||||||
|
\\"type\\": 5,
|
||||||
|
\\"id\\": 21
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"data\\": {
|
||||||
|
\\"source\\": 5,
|
||||||
|
\\"text\\": \\"1ok3\\",
|
||||||
|
\\"isChecked\\": false,
|
||||||
|
\\"id\\": 21
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"data\\": {
|
||||||
|
\\"source\\": 0,
|
||||||
|
\\"texts\\": [],
|
||||||
|
\\"attributes\\": [
|
||||||
|
{
|
||||||
|
\\"id\\": 21,
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"value\\": \\"ignore\\"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"removes\\": [],
|
||||||
|
\\"adds\\": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"data\\": {
|
||||||
|
\\"source\\": 5,
|
||||||
|
\\"text\\": \\"12ok3\\",
|
||||||
|
\\"isChecked\\": false,
|
||||||
|
\\"id\\": 21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`record integration tests can use maskInputOptions to configure which type of inputs should be masked 1`] = `
|
exports[`record integration tests can use maskInputOptions to configure which type of inputs should be masked 1`] = `
|
||||||
"[
|
"[
|
||||||
{
|
{
|
||||||
@@ -4238,8 +4516,22 @@ exports[`record integration tests can use maskInputOptions to configure which ty
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 61
|
\\"id\\": 61
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"textarea\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"value\\": \\"pre value\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 63
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 18
|
\\"id\\": 18
|
||||||
@@ -4247,7 +4539,7 @@ exports[`record integration tests can use maskInputOptions to configure which ty
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n \\",
|
\\"textContent\\": \\"\\\\n \\\\n \\",
|
||||||
\\"id\\": 62
|
\\"id\\": 64
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -4257,15 +4549,15 @@ exports[`record integration tests can use maskInputOptions to configure which ty
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
||||||
\\"id\\": 64
|
\\"id\\": 66
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 63
|
\\"id\\": 65
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
||||||
\\"id\\": 65
|
\\"id\\": 67
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 16
|
\\"id\\": 16
|
||||||
@@ -6230,8 +6522,22 @@ exports[`record integration tests should mask inputs via function call 1`] = `
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 61
|
\\"id\\": 61
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"textarea\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"value\\": \\"*********\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 63
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 18
|
\\"id\\": 18
|
||||||
@@ -6239,7 +6545,7 @@ exports[`record integration tests should mask inputs via function call 1`] = `
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n \\",
|
\\"textContent\\": \\"\\\\n \\\\n \\",
|
||||||
\\"id\\": 62
|
\\"id\\": 64
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -6249,15 +6555,15 @@ exports[`record integration tests should mask inputs via function call 1`] = `
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
||||||
\\"id\\": 64
|
\\"id\\": 66
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 63
|
\\"id\\": 65
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
||||||
\\"id\\": 65
|
\\"id\\": 67
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 16
|
\\"id\\": 16
|
||||||
@@ -9917,8 +10223,22 @@ exports[`record integration tests should not record input values if maskAllInput
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 61
|
\\"id\\": 61
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"textarea\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"value\\": \\"*********\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 63
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 18
|
\\"id\\": 18
|
||||||
@@ -9926,7 +10246,7 @@ exports[`record integration tests should not record input values if maskAllInput
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n \\",
|
\\"textContent\\": \\"\\\\n \\\\n \\",
|
||||||
\\"id\\": 62
|
\\"id\\": 64
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -9936,15 +10256,15 @@ exports[`record integration tests should not record input values if maskAllInput
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
||||||
\\"id\\": 64
|
\\"id\\": 66
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 63
|
\\"id\\": 65
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
||||||
\\"id\\": 65
|
\\"id\\": 67
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 16
|
\\"id\\": 16
|
||||||
@@ -13626,8 +13946,22 @@ exports[`record integration tests should record input userTriggered values if us
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"id\\": 61
|
\\"id\\": 61
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"textarea\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"value\\": \\"pre value\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"id\\": 62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"id\\": 63
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 18
|
\\"id\\": 18
|
||||||
@@ -13635,7 +13969,7 @@ exports[`record integration tests should record input userTriggered values if us
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n \\",
|
\\"textContent\\": \\"\\\\n \\\\n \\",
|
||||||
\\"id\\": 62
|
\\"id\\": 64
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 2,
|
\\"type\\": 2,
|
||||||
@@ -13645,15 +13979,15 @@ exports[`record integration tests should record input userTriggered values if us
|
|||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
||||||
\\"id\\": 64
|
\\"id\\": 66
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 63
|
\\"id\\": 65
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
||||||
\\"id\\": 65
|
\\"id\\": 67
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"id\\": 16
|
\\"id\\": 16
|
||||||
|
|||||||
81
packages/rrweb/test/events/bad-textarea.ts
Normal file
81
packages/rrweb/test/events/bad-textarea.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { EventType, IncrementalSource } from '@rrweb/types';
|
||||||
|
import type { eventWithTime } from '@rrweb/types';
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const events: eventWithTime[] = [
|
||||||
|
{
|
||||||
|
type: EventType.DomContentLoaded,
|
||||||
|
data: {},
|
||||||
|
timestamp: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: EventType.Load,
|
||||||
|
data: {},
|
||||||
|
timestamp: now + 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: EventType.Meta,
|
||||||
|
data: {
|
||||||
|
href: 'http://localhost',
|
||||||
|
width: 1000,
|
||||||
|
height: 800,
|
||||||
|
},
|
||||||
|
timestamp: now + 50,
|
||||||
|
},
|
||||||
|
// full snapshot:
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
node: {
|
||||||
|
id: 1,
|
||||||
|
type: 0,
|
||||||
|
childNodes: [
|
||||||
|
{ id: 2, name: 'html', type: 1, publicId: '', systemId: '' },
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
type: 2,
|
||||||
|
tagName: 'html',
|
||||||
|
attributes: { lang: 'en' },
|
||||||
|
childNodes: [
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
type: 2,
|
||||||
|
tagName: 'head',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
type: 2,
|
||||||
|
tagName: 'body',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
type: 2,
|
||||||
|
tagName: 'textarea',
|
||||||
|
attributes: {
|
||||||
|
value: 'this value is used for replay',
|
||||||
|
},
|
||||||
|
childNodes: [
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
textContent:
|
||||||
|
'this value is IGNORED for replay (but was present as a duplicte in legacy recordings)',
|
||||||
|
id: 7,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
initialOffset: { top: 0, left: 0 },
|
||||||
|
},
|
||||||
|
type: EventType.FullSnapshot,
|
||||||
|
timestamp: now + 50,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default events;
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
<label for="password">
|
<label for="password">
|
||||||
<input type="password" />
|
<input type="password" />
|
||||||
</label>
|
</label>
|
||||||
|
<textarea>pre value</textarea>
|
||||||
</form>
|
</form>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -105,6 +105,69 @@ describe('record integration tests', function (this: ISuite) {
|
|||||||
assertSnapshot(snapshots);
|
assertSnapshot(snapshots);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can record textarea mutations correctly', async () => {
|
||||||
|
const page: puppeteer.Page = await browser.newPage();
|
||||||
|
await page.goto('about:blank');
|
||||||
|
await page.setContent(getHtml.call(this, 'empty.html'));
|
||||||
|
|
||||||
|
await waitForRAF(page); // ensure mutations aren't included in fullsnapshot
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.innerText = 'pre value';
|
||||||
|
document.body.append(ta);
|
||||||
|
});
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const t = document.querySelector('textarea') as HTMLTextAreaElement;
|
||||||
|
t.innerText = 'ok'; // this mutation should be recorded
|
||||||
|
});
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const t = document.querySelector('textarea') as HTMLTextAreaElement;
|
||||||
|
(t.childNodes[0] as Text).appendData('3'); // this mutation is also valid
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.type('textarea', '1'); // types (inserts) at index 0, in front of existing text
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const t = document.querySelector('textarea') as HTMLTextAreaElement;
|
||||||
|
// user has typed so childNode content should now be ignored
|
||||||
|
(t.childNodes[0] as Text).data = 'igno';
|
||||||
|
(t.childNodes[0] as Text).appendData('re');
|
||||||
|
// this mutation is currently emitted, and shows up in snapshot
|
||||||
|
// but we will check that it doesn't have any effect on the value
|
||||||
|
// there is nothing explicit in rrweb which enforces this, but this test may protect against
|
||||||
|
// a future change where a mutation on a textarea incorrectly updates the .value
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.type('textarea', '2'); // cursor is at index 1
|
||||||
|
|
||||||
|
const snapshots = (await page.evaluate(
|
||||||
|
'window.snapshots',
|
||||||
|
)) as eventWithTime[];
|
||||||
|
assertSnapshot(snapshots);
|
||||||
|
|
||||||
|
// check after each mutation and text input
|
||||||
|
const replayTextareaValues = await page.evaluate(`
|
||||||
|
const { Replayer } = rrweb;
|
||||||
|
const replayer = new Replayer(window.snapshots);
|
||||||
|
const vals = [];
|
||||||
|
window.snapshots.filter((e)=>e.data.attributes || e.data.source === 5).forEach((e)=>{
|
||||||
|
replayer.pause((e.timestamp - window.snapshots[0].timestamp)+1);
|
||||||
|
let ts = replayer.iframe.contentDocument.querySelector('textarea');
|
||||||
|
vals.push((e.data.source === 0 ? 'Mutation' : 'User') + ':' + ts.value);
|
||||||
|
});
|
||||||
|
vals;
|
||||||
|
`);
|
||||||
|
expect(replayTextareaValues).toEqual([
|
||||||
|
'Mutation:pre value',
|
||||||
|
'Mutation:ok',
|
||||||
|
'Mutation:ok3',
|
||||||
|
'User:1ok3',
|
||||||
|
'Mutation:1ok3', // if this gets set to 'ignore', it's an error, as the 'user' has modified the textarea
|
||||||
|
'User:12ok3',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('can record childList mutations', async () => {
|
it('can record childList mutations', async () => {
|
||||||
const page: puppeteer.Page = await browser.newPage();
|
const page: puppeteer.Page = await browser.newPage();
|
||||||
await page.goto('about:blank');
|
await page.goto('about:blank');
|
||||||
|
|||||||
@@ -1586,9 +1586,25 @@ exports[`cross origin iframes form.html should map input events correctly 1`] =
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"rootId\\": 11,
|
\\"rootId\\": 11,
|
||||||
\\"id\\": 73
|
\\"id\\": 73
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"textarea\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"value\\": \\"pre value\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"rootId\\": 11,
|
||||||
|
\\"id\\": 74
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"rootId\\": 11,
|
||||||
|
\\"id\\": 75
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"rootId\\": 11,
|
\\"rootId\\": 11,
|
||||||
@@ -1598,7 +1614,7 @@ exports[`cross origin iframes form.html should map input events correctly 1`] =
|
|||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n\\\\n\\",
|
\\"textContent\\": \\"\\\\n \\\\n\\\\n\\",
|
||||||
\\"rootId\\": 11,
|
\\"rootId\\": 11,
|
||||||
\\"id\\": 74
|
\\"id\\": 76
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"rootId\\": 11,
|
\\"rootId\\": 11,
|
||||||
@@ -2583,9 +2599,25 @@ exports[`cross origin iframes form.html should map scroll events correctly 1`] =
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\",
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
\\"rootId\\": 11,
|
\\"rootId\\": 11,
|
||||||
\\"id\\": 73
|
\\"id\\": 73
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 2,
|
||||||
|
\\"tagName\\": \\"textarea\\",
|
||||||
|
\\"attributes\\": {
|
||||||
|
\\"value\\": \\"pre value\\"
|
||||||
|
},
|
||||||
|
\\"childNodes\\": [],
|
||||||
|
\\"rootId\\": 11,
|
||||||
|
\\"id\\": 74
|
||||||
|
},
|
||||||
|
{
|
||||||
|
\\"type\\": 3,
|
||||||
|
\\"textContent\\": \\"\\\\n \\",
|
||||||
|
\\"rootId\\": 11,
|
||||||
|
\\"id\\": 75
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"rootId\\": 11,
|
\\"rootId\\": 11,
|
||||||
@@ -2595,7 +2627,7 @@ exports[`cross origin iframes form.html should map scroll events correctly 1`] =
|
|||||||
\\"type\\": 3,
|
\\"type\\": 3,
|
||||||
\\"textContent\\": \\"\\\\n \\\\n\\\\n\\",
|
\\"textContent\\": \\"\\\\n \\\\n\\\\n\\",
|
||||||
\\"rootId\\": 11,
|
\\"rootId\\": 11,
|
||||||
\\"id\\": 74
|
\\"id\\": 76
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"rootId\\": 11,
|
\\"rootId\\": 11,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import inputEvents from './events/input';
|
|||||||
import iframeEvents from './events/iframe';
|
import iframeEvents from './events/iframe';
|
||||||
import selectionEvents from './events/selection';
|
import selectionEvents from './events/selection';
|
||||||
import shadowDomEvents from './events/shadow-dom';
|
import shadowDomEvents from './events/shadow-dom';
|
||||||
|
import textareaEvents from './events/bad-textarea';
|
||||||
import StyleSheetTextMutation from './events/style-sheet-text-mutation';
|
import StyleSheetTextMutation from './events/style-sheet-text-mutation';
|
||||||
import canvasInIframe from './events/canvas-in-iframe';
|
import canvasInIframe from './events/canvas-in-iframe';
|
||||||
import adoptedStyleSheet from './events/adopted-style-sheet';
|
import adoptedStyleSheet from './events/adopted-style-sheet';
|
||||||
@@ -1092,4 +1093,19 @@ describe('replayer', function () {
|
|||||||
// If the custom element is defined, the display value will be 'block'.
|
// If the custom element is defined, the display value will be 'block'.
|
||||||
expect(displayValue).toEqual('block');
|
expect(displayValue).toEqual('block');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can deal with legacy duplicate/conflicting values on textareas', async () => {
|
||||||
|
await page.evaluate(`events = ${JSON.stringify(textareaEvents)}`);
|
||||||
|
|
||||||
|
const displayValue = await page.evaluate(`
|
||||||
|
const { Replayer } = rrweb;
|
||||||
|
const replayer = new Replayer(events);
|
||||||
|
replayer.pause(100);
|
||||||
|
const textarea = replayer.iframe.contentDocument.querySelector('textarea');
|
||||||
|
textarea.value;
|
||||||
|
`);
|
||||||
|
// If the custom element is not defined, the display value will be 'none'.
|
||||||
|
// If the custom element is defined, the display value will be 'block'.
|
||||||
|
expect(displayValue).toEqual('this value is used for replay');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user