update mutation observer handler
1. deep delete from adds set when node was dropped 2. remove node from dropped set when node was added again
This commit is contained in:
41
src/record/collection.ts
Normal file
41
src/record/collection.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Some utils to handle the mutation observer DOM records.
|
||||
* It should be more clear to extend the native data structure
|
||||
* like Set and Map, but currently Typescript does not support
|
||||
* that.
|
||||
*/
|
||||
|
||||
import { INode } from 'rrweb-snapshot';
|
||||
import { removedNodeMutation } from '../types';
|
||||
import { mirror } from '../utils';
|
||||
|
||||
export function deepDelete(addsSet: Set<Node>, n: Node) {
|
||||
addsSet.delete(n);
|
||||
n.childNodes.forEach(childN => deepDelete(addsSet, childN));
|
||||
}
|
||||
|
||||
export function isParentRemoved(
|
||||
removes: removedNodeMutation[],
|
||||
n: Node,
|
||||
): boolean {
|
||||
const { parentNode } = n;
|
||||
if (!parentNode) {
|
||||
return false;
|
||||
}
|
||||
const parentId = mirror.getId((parentNode as Node) as INode);
|
||||
if (removes.some(r => r.id === parentId)) {
|
||||
return true;
|
||||
}
|
||||
return isParentRemoved(removes, parentNode);
|
||||
}
|
||||
|
||||
export function isParentDropped(droppedSet: Set<Node>, n: Node): boolean {
|
||||
const { parentNode } = n;
|
||||
if (!parentNode) {
|
||||
return false;
|
||||
}
|
||||
if (droppedSet.has(parentNode)) {
|
||||
return true;
|
||||
}
|
||||
return isParentDropped(droppedSet, parentNode);
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
textCursor,
|
||||
attributeCursor,
|
||||
} from '../types';
|
||||
import { deepDelete, isParentRemoved, isParentDropped } from './collection';
|
||||
|
||||
/**
|
||||
* Mutation observer will merge several mutations into an array and pass
|
||||
@@ -49,16 +50,18 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
||||
const observer = new MutationObserver(mutations => {
|
||||
const texts: textCursor[] = [];
|
||||
const attributes: attributeCursor[] = [];
|
||||
let removes: removedNodeMutation[] = [];
|
||||
const removes: removedNodeMutation[] = [];
|
||||
const adds: addedNodeMutation[] = [];
|
||||
const dropped: Node[] = [];
|
||||
|
||||
const addsSet = new Set<Node>();
|
||||
const droppedSet = new Set<Node>();
|
||||
|
||||
const genAdds = (n: Node) => {
|
||||
if (isBlocked(n)) {
|
||||
return;
|
||||
}
|
||||
addsSet.add(n);
|
||||
droppedSet.delete(n);
|
||||
n.childNodes.forEach(childN => genAdds(childN));
|
||||
};
|
||||
mutations.forEach(mutation => {
|
||||
@@ -110,8 +113,8 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
||||
}
|
||||
// removed node has not been serialized yet, just remove it from the Set
|
||||
if (addsSet.has(n)) {
|
||||
addsSet.delete(n);
|
||||
dropped.push(n);
|
||||
deepDelete(addsSet, n);
|
||||
droppedSet.add(n);
|
||||
} else if (addsSet.has(target) && nodeId === -1) {
|
||||
/**
|
||||
* If target was newly added and removed child node was
|
||||
@@ -141,31 +144,8 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
||||
}
|
||||
});
|
||||
|
||||
const isDropped = (n: Node): boolean => {
|
||||
const { parentNode } = n;
|
||||
if (!parentNode) {
|
||||
return false;
|
||||
}
|
||||
if (dropped.some(d => d === parentNode)) {
|
||||
return true;
|
||||
}
|
||||
return isDropped(parentNode);
|
||||
};
|
||||
|
||||
const isRemoved = (n: Node): boolean => {
|
||||
const { parentNode } = n;
|
||||
if (!parentNode) {
|
||||
return false;
|
||||
}
|
||||
const parentId = mirror.getId((parentNode as Node) as INode);
|
||||
if (removes.some(r => r.id === parentId)) {
|
||||
return true;
|
||||
}
|
||||
return isRemoved(parentNode);
|
||||
};
|
||||
|
||||
Array.from(addsSet).forEach(n => {
|
||||
if (!isDropped(n) && !isRemoved(n)) {
|
||||
if (!isParentDropped(droppedSet, n) && !isParentRemoved(removes, n)) {
|
||||
adds.push({
|
||||
parentId: mirror.getId((n.parentNode as Node) as INode),
|
||||
previousId: !n.previousSibling
|
||||
@@ -177,7 +157,7 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
||||
node: serializeNodeWithId(n, document, mirror.map, true)!,
|
||||
});
|
||||
} else {
|
||||
dropped.push(n);
|
||||
droppedSet.add(n);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1603,6 +1603,274 @@ exports[`ignore 1`] = `
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`move-node 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\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 3
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 5
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"div\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 7
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"p\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 8
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 9
|
||||
}
|
||||
],
|
||||
\\"id\\": 6
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 10
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"span\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 12
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"i\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 14
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"b\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"1\\",
|
||||
\\"id\\": 16
|
||||
}
|
||||
],
|
||||
\\"id\\": 15
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 17
|
||||
}
|
||||
],
|
||||
\\"id\\": 13
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 18
|
||||
}
|
||||
],
|
||||
\\"id\\": 11
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\\\n \\",
|
||||
\\"id\\": 19
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"script\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
|
||||
\\"id\\": 21
|
||||
}
|
||||
],
|
||||
\\"id\\": 20
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
|
||||
\\"id\\": 22
|
||||
}
|
||||
],
|
||||
\\"id\\": 4
|
||||
}
|
||||
],
|
||||
\\"id\\": 2
|
||||
}
|
||||
],
|
||||
\\"id\\": 1
|
||||
},
|
||||
\\"initialOffset\\": {
|
||||
\\"left\\": 0,
|
||||
\\"top\\": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [
|
||||
{
|
||||
\\"parentId\\": 4,
|
||||
\\"id\\": 11
|
||||
}
|
||||
],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 6,
|
||||
\\"previousId\\": 9,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"span\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 23
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 23,
|
||||
\\"previousId\\": null,
|
||||
\\"nextId\\": 13,
|
||||
\\"node\\": {
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 24
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 23,
|
||||
\\"previousId\\": 24,
|
||||
\\"nextId\\": 18,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"i\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 25,
|
||||
\\"previousId\\": null,
|
||||
\\"nextId\\": 15,
|
||||
\\"node\\": {
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 26
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 25,
|
||||
\\"previousId\\": 26,
|
||||
\\"nextId\\": 17,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"b\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 27
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 27,
|
||||
\\"previousId\\": null,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"1\\",
|
||||
\\"id\\": 28
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 25,
|
||||
\\"previousId\\": 27,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 29
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 23,
|
||||
\\"previousId\\": 25,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 30
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`select2 1`] = `
|
||||
"[
|
||||
{
|
||||
|
||||
12
test/html/move-node.html
Normal file
12
test/html/move-node.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<html>
|
||||
<body>
|
||||
<div>
|
||||
<p></p>
|
||||
</div>
|
||||
<span>
|
||||
<i>
|
||||
<b>1</b>
|
||||
</i>
|
||||
</span>
|
||||
</body>
|
||||
</html>
|
||||
@@ -154,4 +154,22 @@ describe('record integration tests', () => {
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
assertSnapshot(snapshots, __filename, 'block');
|
||||
});
|
||||
|
||||
it('should record DOM node movement', async () => {
|
||||
const page: puppeteer.Page = await this.browser.newPage();
|
||||
await page.goto('about:blank');
|
||||
await page.setContent(getHtml.call(this, 'move-node.html'));
|
||||
|
||||
await page.evaluate(() => {
|
||||
const div = document.querySelector('div')!;
|
||||
const p = document.querySelector('p')!;
|
||||
const span = document.querySelector('span')!;
|
||||
document.body.removeChild(span);
|
||||
p.appendChild(span);
|
||||
p.removeChild(span);
|
||||
div.appendChild(span);
|
||||
});
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
assertSnapshot(snapshots, __filename, 'move-node');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user