From eaf339ed79551e3e3fbd5a90ea949090726ba384 Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] 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. --- src/record/collection.ts | 6 +- src/record/observer.ts | 109 ++++- src/replay/index.ts | 25 +- test/__snapshots__/integration.test.ts.snap | 492 +++++++++++++++----- test/__snapshots__/record.test.ts.snap | 20 +- test/integration.test.ts | 19 +- 6 files changed, 522 insertions(+), 149 deletions(-) diff --git a/src/record/collection.ts b/src/record/collection.ts index f384a342..01df8c6e 100644 --- a/src/record/collection.ts +++ b/src/record/collection.ts @@ -29,13 +29,13 @@ export function isParentRemoved( return isParentRemoved(removes, parentNode); } -export function isParentDropped(droppedSet: Set, n: Node): boolean { +export function isAncestorInSet(set: Set, 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); } diff --git a/src/record/observer.ts b/src/record/observer.ts index 51484073..1907641e 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -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(); + const addedSet = new Set(); + const movedSet = new Set(); const droppedSet = new Set(); - const genAdds = (n: Node) => { + const movedMap: Record = {}; + + 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 => ({ diff --git a/src/replay/index.ts b/src/replay/index.ts index 75007bc5..9539b46d 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -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); } diff --git a/test/__snapshots__/integration.test.ts.snap b/test/__snapshots__/integration.test.ts.snap index 56a09d17..74231900 100644 --- a/test/__snapshots__/integration.test.ts.snap +++ b/test/__snapshots__/integration.test.ts.snap @@ -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 } } ]" diff --git a/test/__snapshots__/record.test.ts.snap b/test/__snapshots__/record.test.ts.snap index f23f0c8d..22b67cdd 100644 --- a/test/__snapshots__/record.test.ts.snap +++ b/test/__snapshots__/record.test.ts.snap @@ -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 } } ] diff --git a/test/integration.test.ts b/test/integration.test.ts index 8501e400..9eb59776 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -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'); }); });