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:
Yanzhen Yu
2019-02-03 23:07:35 +08:00
parent a69bf87f7f
commit 406e7a8d39
5 changed files with 348 additions and 29 deletions

41
src/record/collection.ts Normal file
View 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);
}

View File

@@ -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);
}
});

View File

@@ -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
View File

@@ -0,0 +1,12 @@
<html>
<body>
<div>
<p></p>
</div>
<span>
<i>
<b>1</b>
</i>
</span>
</body>
</html>

View File

@@ -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');
});
});