diff --git a/src/record/collection.ts b/src/record/collection.ts new file mode 100644 index 00000000..f384a342 --- /dev/null +++ b/src/record/collection.ts @@ -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, 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, n: Node): boolean { + const { parentNode } = n; + if (!parentNode) { + return false; + } + if (droppedSet.has(parentNode)) { + return true; + } + return isParentDropped(droppedSet, parentNode); +} diff --git a/src/record/observer.ts b/src/record/observer.ts index 537a3154..4794401a 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -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(); + const droppedSet = new Set(); + 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); } }); diff --git a/test/__snapshots__/integration.test.ts.snap b/test/__snapshots__/integration.test.ts.snap index 04d71403..1f2b7b5c 100644 --- a/test/__snapshots__/integration.test.ts.snap +++ b/test/__snapshots__/integration.test.ts.snap @@ -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`] = ` "[ { diff --git a/test/html/move-node.html b/test/html/move-node.html new file mode 100644 index 00000000..bbe607b7 --- /dev/null +++ b/test/html/move-node.html @@ -0,0 +1,12 @@ + + +
+

+
+ + + 1 + + + + diff --git a/test/integration.test.ts b/test/integration.test.ts index 1a4c86b7..18332b42 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -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'); + }); });