Upgrade the DOM mutation observer

This is an important patch contains some crtical bug fixes for
the DOM mutation observer.
Previously the observer did not handle complex DOM movement very
well. So in this patch we optimized this by distinguishing moved
node better and added a resolving queue to avoid the error caused
by ordering.
This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent 79981a6a44
commit eaf339ed79
6 changed files with 522 additions and 149 deletions

View File

@@ -29,13 +29,13 @@ export function isParentRemoved(
return isParentRemoved(removes, parentNode);
}
export function isParentDropped(droppedSet: Set<Node>, n: Node): boolean {
export function isAncestorInSet(set: Set<Node>, n: Node): boolean {
const { parentNode } = n;
if (!parentNode) {
return false;
}
if (droppedSet.has(parentNode)) {
if (set.has(parentNode)) {
return true;
}
return isParentDropped(droppedSet, parentNode);
return isAncestorInSet(set, parentNode);
}

View File

@@ -28,7 +28,12 @@ import {
attributeCursor,
blockClass,
} from '../types';
import { deepDelete, isParentRemoved, isParentDropped } from './collection';
import { deepDelete, isParentRemoved, isAncestorInSet } from './collection';
const moveKey = (id: number, parentId: number) => `${id}@${parentId}`;
function isINode(n: Node | INode): n is INode {
return '__sn' in n;
}
/**
* Mutation observer will merge several mutations into an array and pass
@@ -55,20 +60,35 @@ function initMutationObserver(
const observer = new MutationObserver(mutations => {
const texts: textCursor[] = [];
const attributes: attributeCursor[] = [];
const removes: removedNodeMutation[] = [];
let removes: removedNodeMutation[] = [];
const adds: addedNodeMutation[] = [];
const addsSet = new Set<Node>();
const addedSet = new Set<Node>();
const movedSet = new Set<Node>();
const droppedSet = new Set<Node>();
const genAdds = (n: Node) => {
const movedMap: Record<string, true> = {};
const genAdds = (n: Node | INode, target?: Node | INode) => {
if (isBlocked(n, blockClass)) {
return;
}
addsSet.add(n);
droppedSet.delete(n);
if (isINode(n)) {
movedSet.add(n);
let targetId: number | null = null;
if (target && isINode(target)) {
targetId = target.__sn.id;
}
if (targetId) {
movedMap[moveKey(n.__sn.id, targetId)] = true;
}
} else {
addedSet.add(n);
droppedSet.delete(n);
}
n.childNodes.forEach(childN => genAdds(childN));
};
mutations.forEach(mutation => {
const {
type,
@@ -109,7 +129,7 @@ function initMutationObserver(
break;
}
case 'childList': {
addedNodes.forEach(n => genAdds(n));
addedNodes.forEach(n => genAdds(n, target));
removedNodes.forEach(n => {
const nodeId = mirror.getId(n as INode);
const parentId = mirror.getId(target as INode);
@@ -117,14 +137,15 @@ function initMutationObserver(
return;
}
// removed node has not been serialized yet, just remove it from the Set
if (addsSet.has(n)) {
deepDelete(addsSet, n);
if (addedSet.has(n)) {
deepDelete(addedSet, n);
droppedSet.add(n);
} else if (addsSet.has(target) && nodeId === -1) {
} else if (addedSet.has(target) && nodeId === -1) {
/**
* If target was newly added and removed child node was
* not serialized, it means the child node has been removed
* before callback fired, so we can ignore it.
* before callback fired, so we can ignore it because
* newly added node will be serialized without child nodes.
* TODO: verify this
*/
} else if (isAncestorRemoved(target as INode)) {
@@ -134,6 +155,8 @@ function initMutationObserver(
* the node is also removed which we do not need to track
* and replay.
*/
} else if (movedSet.has(n) && movedMap[moveKey(nodeId, parentId)]) {
deepDelete(movedSet, n);
} else {
removes.push({
parentId,
@@ -149,23 +172,63 @@ function initMutationObserver(
}
});
Array.from(addsSet).forEach(n => {
if (!isParentDropped(droppedSet, n) && !isParentRemoved(removes, n)) {
adds.push({
parentId: mirror.getId((n.parentNode as Node) as INode),
previousId: !n.previousSibling
? n.previousSibling
: mirror.getId(n.previousSibling as INode),
nextId: !n.nextSibling
? n.nextSibling
: mirror.getId((n.nextSibling as unknown) as INode),
node: serializeNodeWithId(n, document, mirror.map, blockClass, true)!,
});
/**
* Sometimes child node may be pushed before its newly added
* parent, so we init a queue to store these nodes.
*/
const addQueue: Node[] = [];
const pushAdd = (n: Node) => {
const parentId = mirror.getId((n.parentNode as Node) as INode);
if (parentId === -1) {
return addQueue.push(n);
}
adds.push({
parentId,
previousId: !n.previousSibling
? n.previousSibling
: mirror.getId(n.previousSibling as INode),
nextId: !n.nextSibling
? n.nextSibling
: mirror.getId((n.nextSibling as unknown) as INode),
node: serializeNodeWithId(
n,
document,
mirror.map,
blockClass,
true,
inlineStylesheet,
)!,
});
};
Array.from(movedSet).forEach(pushAdd);
Array.from(addedSet).forEach(n => {
if (!isAncestorInSet(droppedSet, n) && !isParentRemoved(removes, n)) {
pushAdd(n);
} else if (isAncestorInSet(movedSet, n)) {
pushAdd(n);
} else {
droppedSet.add(n);
}
});
while (addQueue.length) {
if (
addQueue.every(
n => mirror.getId((n.parentNode as Node) as INode) === -1,
)
) {
/**
* If all nodes in queue could not find a serialized parent,
* it may be a bug or corner case. We need to escape the
* dead while loop at once.
*/
break;
}
pushAdd(addQueue.shift()!);
}
const payload = {
texts: texts
.map(text => ({

View File

@@ -353,17 +353,19 @@ export class Replayer {
});
const missingNodeMap: missingNodeMap = { ...this.missingNodeRetryMap };
d.adds.forEach(mutation => {
const queue: addedNodeMutation[] = [];
const appendNode = (mutation: addedNodeMutation) => {
const parent = mirror.getNode(mutation.parentId);
if (!parent) {
return queue.push(mutation);
}
const target = buildNodeWithSN(
mutation.node,
this.iframe.contentDocument!,
mirror.map,
true,
) as Node;
const parent = mirror.getNode(mutation.parentId);
if (!parent) {
return this.warnNodeNotFound(d, mutation.parentId);
}
let previous: Node | null = null;
let next: Node | null = null;
if (mutation.previousId) {
@@ -396,7 +398,20 @@ export class Replayer {
if (mutation.previousId || mutation.nextId) {
this.resolveMissingNode(missingNodeMap, parent, target, mutation);
}
};
d.adds.forEach(mutation => {
appendNode(mutation);
});
while (queue.length) {
if (queue.every(m => !Boolean(mirror.getNode(m.parentId)))) {
return queue.forEach(m => this.warnNodeNotFound(d, m.node.id));
}
const mutation = queue.shift()!;
appendNode(mutation);
}
if (Object.keys(missingNodeMap).length) {
Object.assign(this.missingNodeRetryMap, missingNodeMap);
}

View File

@@ -1613,7 +1613,7 @@ exports[`ignore 1`] = `
]"
`;
exports[`move-node 1`] = `
exports[`move-node-1 1`] = `
"[
{
\\"type\\": 0,
@@ -1798,81 +1798,361 @@ exports[`move-node 1`] = `
\\"tagName\\": \\"span\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 23
\\"id\\": 11
}
},
{
\\"parentId\\": 23,
\\"parentId\\": 11,
\\"previousId\\": null,
\\"nextId\\": 13,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 24
\\"id\\": 12
}
},
{
\\"parentId\\": 23,
\\"previousId\\": 24,
\\"parentId\\": 11,
\\"previousId\\": 12,
\\"nextId\\": 18,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"i\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 25
\\"id\\": 13
}
},
{
\\"parentId\\": 25,
\\"parentId\\": 13,
\\"previousId\\": null,
\\"nextId\\": 15,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 26
\\"id\\": 14
}
},
{
\\"parentId\\": 25,
\\"previousId\\": 26,
\\"parentId\\": 13,
\\"previousId\\": 14,
\\"nextId\\": 17,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"b\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 27
\\"id\\": 15
}
},
{
\\"parentId\\": 27,
\\"parentId\\": 15,
\\"previousId\\": null,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"1\\",
\\"id\\": 28
\\"id\\": 16
}
},
{
\\"parentId\\": 25,
\\"previousId\\": 27,
\\"parentId\\": 13,
\\"previousId\\": 15,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 29
\\"id\\": 17
}
},
{
\\"parentId\\": 23,
\\"previousId\\": 25,
\\"parentId\\": 11,
\\"previousId\\": 13,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 30
\\"id\\": 18
}
}
]
}
}
]"
`;
exports[`move-node-2 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\\": 11,
\\"previousId\\": null,
\\"nextId\\": 13,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 12
}
},
{
\\"parentId\\": 11,
\\"previousId\\": 12,
\\"nextId\\": 18,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"i\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 13
}
},
{
\\"parentId\\": 13,
\\"previousId\\": null,
\\"nextId\\": 15,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 14
}
},
{
\\"parentId\\": 13,
\\"previousId\\": 14,
\\"nextId\\": 17,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"b\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 15
}
},
{
\\"parentId\\": 15,
\\"previousId\\": null,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"1\\",
\\"id\\": 16
}
},
{
\\"parentId\\": 13,
\\"previousId\\": 15,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 17
}
},
{
\\"parentId\\": 11,
\\"previousId\\": 13,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 18
}
},
{
\\"parentId\\": 4,
\\"previousId\\": 22,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 23
}
},
{
\\"parentId\\": 23,
\\"previousId\\": null,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"span\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 11
}
}
]
@@ -2432,20 +2712,20 @@ exports[`select2 1`] = `
}
},
{
\\"id\\": 75,
\\"id\\": 36,
\\"attributes\\": {
\\"id\\": \\"select2-drop\\",
\\"class\\": \\"select2-drop select2-display-none select2-with-searchbox select2-drop-active\\"
}
},
{
\\"id\\": 93,
\\"id\\": 75,
\\"attributes\\": {
\\"style\\": \\"\\"
}
},
{
\\"id\\": 81,
\\"id\\": 42,
\\"attributes\\": {
\\"class\\": \\"select2-input select2-focused\\",
\\"aria-activedescendant\\": \\"select2-result-label-2\\"
@@ -2458,7 +2738,7 @@ exports[`select2 1`] = `
}
},
{
\\"id\\": 85,
\\"id\\": 67,
\\"attributes\\": {
\\"class\\": \\"select2-results-dept-0 select2-result select2-result-selectable select2-highlighted\\"
}
@@ -2488,22 +2768,22 @@ exports[`select2 1`] = `
\\"tabindex\\": \\"-1\\"
},
\\"childNodes\\": [],
\\"id\\": 67
\\"id\\": 26
}
},
{
\\"parentId\\": 67,
\\"parentId\\": 26,
\\"previousId\\": null,
\\"nextId\\": 28,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\" \\",
\\"id\\": 68
\\"id\\": 27
}
},
{
\\"parentId\\": 67,
\\"previousId\\": 68,
\\"parentId\\": 26,
\\"previousId\\": 27,
\\"nextId\\": 30,
\\"node\\": {
\\"type\\": 2,
@@ -2513,22 +2793,22 @@ exports[`select2 1`] = `
\\"id\\": \\"select2-chosen-1\\"
},
\\"childNodes\\": [],
\\"id\\": 69
\\"id\\": 28
}
},
{
\\"parentId\\": 69,
\\"parentId\\": 28,
\\"previousId\\": null,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"A\\",
\\"id\\": 70
\\"id\\": 29
}
},
{
\\"parentId\\": 67,
\\"previousId\\": 69,
\\"parentId\\": 26,
\\"previousId\\": 28,
\\"nextId\\": 31,
\\"node\\": {
\\"type\\": 2,
@@ -2537,22 +2817,22 @@ exports[`select2 1`] = `
\\"class\\": \\"select2-search-choice-close\\"
},
\\"childNodes\\": [],
\\"id\\": 71
\\"id\\": 30
}
},
{
\\"parentId\\": 67,
\\"previousId\\": 71,
\\"parentId\\": 26,
\\"previousId\\": 30,
\\"nextId\\": 32,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\" \\",
\\"id\\": 72
\\"id\\": 31
}
},
{
\\"parentId\\": 67,
\\"previousId\\": 72,
\\"parentId\\": 26,
\\"previousId\\": 31,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
@@ -2562,11 +2842,11 @@ exports[`select2 1`] = `
\\"role\\": \\"presentation\\"
},
\\"childNodes\\": [],
\\"id\\": 73
\\"id\\": 32
}
},
{
\\"parentId\\": 73,
\\"parentId\\": 32,
\\"previousId\\": null,
\\"nextId\\": null,
\\"node\\": {
@@ -2576,7 +2856,7 @@ exports[`select2 1`] = `
\\"role\\": \\"presentation\\"
},
\\"childNodes\\": [],
\\"id\\": 74
\\"id\\": 33
}
},
{
@@ -2591,22 +2871,22 @@ exports[`select2 1`] = `
\\"id\\": \\"select2-drop\\"
},
\\"childNodes\\": [],
\\"id\\": 75
\\"id\\": 36
}
},
{
\\"parentId\\": 75,
\\"parentId\\": 36,
\\"previousId\\": null,
\\"nextId\\": 38,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\" \\",
\\"id\\": 76
\\"id\\": 37
}
},
{
\\"parentId\\": 75,
\\"previousId\\": 76,
\\"parentId\\": 36,
\\"previousId\\": 37,
\\"nextId\\": 44,
\\"node\\": {
\\"type\\": 2,
@@ -2615,22 +2895,22 @@ exports[`select2 1`] = `
\\"class\\": \\"select2-search\\"
},
\\"childNodes\\": [],
\\"id\\": 77
\\"id\\": 38
}
},
{
\\"parentId\\": 77,
\\"parentId\\": 38,
\\"previousId\\": null,
\\"nextId\\": 40,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\" \\",
\\"id\\": 78
\\"id\\": 39
}
},
{
\\"parentId\\": 77,
\\"previousId\\": 78,
\\"parentId\\": 38,
\\"previousId\\": 39,
\\"nextId\\": 41,
\\"node\\": {
\\"type\\": 2,
@@ -2640,22 +2920,22 @@ exports[`select2 1`] = `
\\"class\\": \\"select2-offscreen\\"
},
\\"childNodes\\": [],
\\"id\\": 79
\\"id\\": 40
}
},
{
\\"parentId\\": 77,
\\"previousId\\": 79,
\\"parentId\\": 38,
\\"previousId\\": 40,
\\"nextId\\": 42,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\" \\",
\\"id\\": 80
\\"id\\": 41
}
},
{
\\"parentId\\": 77,
\\"previousId\\": 80,
\\"parentId\\": 38,
\\"previousId\\": 41,
\\"nextId\\": 43,
\\"node\\": {
\\"type\\": 2,
@@ -2676,32 +2956,32 @@ exports[`select2 1`] = `
\\"aria-activedescendant\\": \\"select2-result-label-2\\"
},
\\"childNodes\\": [],
\\"id\\": 81
\\"id\\": 42
}
},
{
\\"parentId\\": 77,
\\"previousId\\": 81,
\\"parentId\\": 38,
\\"previousId\\": 42,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\" \\",
\\"id\\": 82
\\"id\\": 43
}
},
{
\\"parentId\\": 75,
\\"previousId\\": 77,
\\"parentId\\": 36,
\\"previousId\\": 38,
\\"nextId\\": 45,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\" \\",
\\"id\\": 83
\\"id\\": 44
}
},
{
\\"parentId\\": 75,
\\"previousId\\": 83,
\\"parentId\\": 36,
\\"previousId\\": 44,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
@@ -2712,11 +2992,11 @@ exports[`select2 1`] = `
\\"id\\": \\"select2-results-1\\"
},
\\"childNodes\\": [],
\\"id\\": 84
\\"id\\": 45
}
},
{
\\"parentId\\": 84,
\\"parentId\\": 45,
\\"previousId\\": null,
\\"nextId\\": -1,
\\"node\\": {
@@ -2727,11 +3007,11 @@ exports[`select2 1`] = `
\\"role\\": \\"presentation\\"
},
\\"childNodes\\": [],
\\"id\\": 85
\\"id\\": 67
}
},
{
\\"parentId\\": 85,
\\"parentId\\": 67,
\\"previousId\\": null,
\\"nextId\\": null,
\\"node\\": {
@@ -2743,11 +3023,11 @@ exports[`select2 1`] = `
\\"role\\": \\"option\\"
},
\\"childNodes\\": [],
\\"id\\": 86
\\"id\\": 68
}
},
{
\\"parentId\\": 86,
\\"parentId\\": 68,
\\"previousId\\": null,
\\"nextId\\": -1,
\\"node\\": {
@@ -2757,22 +3037,22 @@ exports[`select2 1`] = `
\\"class\\": \\"select2-match\\"
},
\\"childNodes\\": [],
\\"id\\": 87
\\"id\\": 69
}
},
{
\\"parentId\\": 86,
\\"previousId\\": 87,
\\"parentId\\": 68,
\\"previousId\\": 69,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"A\\",
\\"id\\": 88
\\"id\\": 70
}
},
{
\\"parentId\\": 84,
\\"previousId\\": 85,
\\"parentId\\": 45,
\\"previousId\\": 67,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
@@ -2782,11 +3062,11 @@ exports[`select2 1`] = `
\\"role\\": \\"presentation\\"
},
\\"childNodes\\": [],
\\"id\\": 89
\\"id\\": 71
}
},
{
\\"parentId\\": 89,
\\"parentId\\": 71,
\\"previousId\\": null,
\\"nextId\\": null,
\\"node\\": {
@@ -2798,11 +3078,11 @@ exports[`select2 1`] = `
\\"role\\": \\"option\\"
},
\\"childNodes\\": [],
\\"id\\": 90
\\"id\\": 72
}
},
{
\\"parentId\\": 90,
\\"parentId\\": 72,
\\"previousId\\": null,
\\"nextId\\": -1,
\\"node\\": {
@@ -2812,23 +3092,23 @@ exports[`select2 1`] = `
\\"class\\": \\"select2-match\\"
},
\\"childNodes\\": [],
\\"id\\": 91
\\"id\\": 73
}
},
{
\\"parentId\\": 90,
\\"previousId\\": 91,
\\"parentId\\": 72,
\\"previousId\\": 73,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"B\\",
\\"id\\": 92
\\"id\\": 74
}
},
{
\\"parentId\\": 18,
\\"previousId\\": 66,
\\"nextId\\": 75,
\\"nextId\\": 36,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"div\\",
@@ -2838,7 +3118,7 @@ exports[`select2 1`] = `
\\"style\\": \\"\\"
},
\\"childNodes\\": [],
\\"id\\": 93
\\"id\\": 75
}
},
{
@@ -2848,7 +3128,7 @@ exports[`select2 1`] = `
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"2 results are available, use up and down arrow keys to navigate.\\",
\\"id\\": 94
\\"id\\": 76
}
}
]
@@ -2859,7 +3139,7 @@ exports[`select2 1`] = `
\\"data\\": {
\\"source\\": 2,
\\"type\\": 0,
\\"id\\": 93
\\"id\\": 75
}
},
{
@@ -2868,7 +3148,7 @@ exports[`select2 1`] = `
\\"source\\": 5,
\\"text\\": \\"\\",
\\"isChecked\\": false,
\\"id\\": 81
\\"id\\": 42
}
},
{
@@ -2885,7 +3165,7 @@ exports[`select2 1`] = `
\\"data\\": {
\\"source\\": 2,
\\"type\\": 1,
\\"id\\": 93
\\"id\\": 75
}
},
{
@@ -2893,7 +3173,7 @@ exports[`select2 1`] = `
\\"data\\": {
\\"source\\": 2,
\\"type\\": 6,
\\"id\\": 81
\\"id\\": 42
}
},
{
@@ -2911,13 +3191,13 @@ exports[`select2 1`] = `
\\"texts\\": [],
\\"attributes\\": [
{
\\"id\\": 95,
\\"id\\": 75,
\\"attributes\\": {
\\"style\\": \\"display: none;\\"
}
},
{
\\"id\\": 75,
\\"id\\": 36,
\\"attributes\\": {
\\"id\\": null
}
@@ -2935,7 +3215,7 @@ exports[`select2 1`] = `
}
},
{
\\"id\\": 81,
\\"id\\": 42,
\\"attributes\\": {
\\"class\\": \\"select2-input\\"
}
@@ -2944,22 +3224,22 @@ exports[`select2 1`] = `
\\"removes\\": [
{
\\"parentId\\": 18,
\\"id\\": 93
\\"id\\": 75
},
{
\\"parentId\\": 84,
\\"id\\": 85
\\"parentId\\": 45,
\\"id\\": 67
},
{
\\"parentId\\": 84,
\\"id\\": 89
\\"parentId\\": 45,
\\"id\\": 71
}
],
\\"adds\\": [
{
\\"parentId\\": 18,
\\"previousId\\": 66,
\\"nextId\\": 75,
\\"nextId\\": 36,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"div\\",
@@ -2969,7 +3249,7 @@ exports[`select2 1`] = `
\\"style\\": \\"display: none;\\"
},
\\"childNodes\\": [],
\\"id\\": 95
\\"id\\": 75
}
}
]
@@ -2980,7 +3260,7 @@ exports[`select2 1`] = `
\\"data\\": {
\\"source\\": 2,
\\"type\\": 0,
\\"id\\": 67
\\"id\\": 26
}
}
]"

View File

@@ -167,7 +167,7 @@ exports[`async-checkout 1`] = `
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
\\"id\\": 6
\\"id\\": 7
},
{
\\"type\\": 2,
@@ -182,13 +182,13 @@ exports[`async-checkout 1`] = `
{
\\"type\\": 3,
\\"textContent\\": \\"test\\",
\\"id\\": 9
\\"id\\": 10
}
],
\\"id\\": 8
\\"id\\": 9
}
],
\\"id\\": 7
\\"id\\": 8
}
],
\\"id\\": 4
@@ -213,31 +213,31 @@ exports[`async-checkout 1`] = `
\\"attributes\\": [],
\\"removes\\": [
{
\\"parentId\\": 7,
\\"id\\": 8
\\"parentId\\": 8,
\\"id\\": 9
}
],
\\"adds\\": [
{
\\"parentId\\": 4,
\\"previousId\\": 7,
\\"previousId\\": 8,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"span\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 10
\\"id\\": 9
}
},
{
\\"parentId\\": 10,
\\"parentId\\": 9,
\\"previousId\\": null,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"test\\",
\\"id\\": 11
\\"id\\": 10
}
}
]

View File

@@ -161,7 +161,7 @@ describe('record integration tests', function(this: ISuite) {
assertSnapshot(snapshots, __filename, 'block');
});
it('should record DOM node movement', async () => {
it('should record DOM node movement 1', async () => {
const page: puppeteer.Page = await this.browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'move-node.html'));
@@ -176,6 +176,21 @@ describe('record integration tests', function(this: ISuite) {
div.appendChild(span);
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'move-node');
assertSnapshot(snapshots, __filename, 'move-node-1');
});
it('should record DOM node movement 2', 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.createElement('div');
const span = document.querySelector('span')!;
document.body.appendChild(div);
div.appendChild(span);
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'move-node-2');
});
});