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,
|
textCursor,
|
||||||
attributeCursor,
|
attributeCursor,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
import { deepDelete, isParentRemoved, isParentDropped } from './collection';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mutation observer will merge several mutations into an array and pass
|
* 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 observer = new MutationObserver(mutations => {
|
||||||
const texts: textCursor[] = [];
|
const texts: textCursor[] = [];
|
||||||
const attributes: attributeCursor[] = [];
|
const attributes: attributeCursor[] = [];
|
||||||
let removes: removedNodeMutation[] = [];
|
const removes: removedNodeMutation[] = [];
|
||||||
const adds: addedNodeMutation[] = [];
|
const adds: addedNodeMutation[] = [];
|
||||||
const dropped: Node[] = [];
|
|
||||||
|
|
||||||
const addsSet = new Set<Node>();
|
const addsSet = new Set<Node>();
|
||||||
|
const droppedSet = new Set<Node>();
|
||||||
|
|
||||||
const genAdds = (n: Node) => {
|
const genAdds = (n: Node) => {
|
||||||
if (isBlocked(n)) {
|
if (isBlocked(n)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addsSet.add(n);
|
addsSet.add(n);
|
||||||
|
droppedSet.delete(n);
|
||||||
n.childNodes.forEach(childN => genAdds(childN));
|
n.childNodes.forEach(childN => genAdds(childN));
|
||||||
};
|
};
|
||||||
mutations.forEach(mutation => {
|
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
|
// removed node has not been serialized yet, just remove it from the Set
|
||||||
if (addsSet.has(n)) {
|
if (addsSet.has(n)) {
|
||||||
addsSet.delete(n);
|
deepDelete(addsSet, n);
|
||||||
dropped.push(n);
|
droppedSet.add(n);
|
||||||
} else if (addsSet.has(target) && nodeId === -1) {
|
} else if (addsSet.has(target) && nodeId === -1) {
|
||||||
/**
|
/**
|
||||||
* If target was newly added and removed child node was
|
* 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 => {
|
Array.from(addsSet).forEach(n => {
|
||||||
if (!isDropped(n) && !isRemoved(n)) {
|
if (!isParentDropped(droppedSet, n) && !isParentRemoved(removes, n)) {
|
||||||
adds.push({
|
adds.push({
|
||||||
parentId: mirror.getId((n.parentNode as Node) as INode),
|
parentId: mirror.getId((n.parentNode as Node) as INode),
|
||||||
previousId: !n.previousSibling
|
previousId: !n.previousSibling
|
||||||
@@ -177,7 +157,7 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
|||||||
node: serializeNodeWithId(n, document, mirror.map, true)!,
|
node: serializeNodeWithId(n, document, mirror.map, true)!,
|
||||||
});
|
});
|
||||||
} else {
|
} 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`] = `
|
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');
|
const snapshots = await page.evaluate('window.snapshots');
|
||||||
assertSnapshot(snapshots, __filename, 'block');
|
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