From 6ce32f7994bca995541ca7725df7aa06c892893d Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] filter text and attributes mutations which target tot a removed node --- src/record/observer.ts | 22 +- test/__snapshots__/integration.ts.snap | 397 ++++++++++++++++-- ...child-list.html => mutation-observer.html} | 1 + test/integration.ts | 75 +++- 4 files changed, 450 insertions(+), 45 deletions(-) rename test/html/{child-list.html => mutation-observer.html} (61%) diff --git a/src/record/observer.ts b/src/record/observer.ts index 3e9ad9a8..f0b58de8 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -154,14 +154,20 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver { }); cb({ - texts: texts.map(text => ({ - id: mirror.getId(text.node as INode), - value: text.value, - })), - attributes: attributes.map(attribute => ({ - id: mirror.getId(attribute.node as INode), - attributes: attribute.attributes, - })), + texts: texts + .map(text => ({ + id: mirror.getId(text.node as INode), + value: text.value, + })) + // text mutation without ID means the target node has been removed + .filter(text => text.id), + attributes: attributes + .map(attribute => ({ + id: mirror.getId(attribute.node as INode), + attributes: attribute.attributes, + })) + // attribute mutation without ID means the target node has been removed + .filter(attribute => attribute.id), removes, adds, }); diff --git a/test/__snapshots__/integration.ts.snap b/test/__snapshots__/integration.ts.snap index 4cde8b9f..1ac7fc12 100644 --- a/test/__snapshots__/integration.ts.snap +++ b/test/__snapshots__/integration.ts.snap @@ -1,5 +1,317 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`attributes 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {}, + \\"timestamp\\": 1542268800000 + }, + { + \\"type\\": 1, + \\"data\\": {}, + \\"timestamp\\": 1542268800000 + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 800, + \\"height\\": 600 + }, + \\"timestamp\\": 1542268800000 + }, + { + \\"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\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"mutation observer\\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 10 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 11 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 15 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 16 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + }, + \\"timestamp\\": 1542268800000 + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 4, + \\"attributes\\": { + \\"test\\": \\"true\\" + } + } + ], + \\"removes\\": [ + { + \\"parentId\\": 4, + \\"id\\": 9 + } + ], + \\"adds\\": [] + }, + \\"timestamp\\": 1542268800000 + } +]" +`; + +exports[`character-data 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {}, + \\"timestamp\\": 1542268800000 + }, + { + \\"type\\": 1, + \\"data\\": {}, + \\"timestamp\\": 1542268800000 + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 800, + \\"height\\": 600 + }, + \\"timestamp\\": 1542268800000 + }, + { + \\"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\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"mutation observer\\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 10 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 11 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 15 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 16 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + }, + \\"timestamp\\": 1542268800000 + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [ + { + \\"id\\": 7, + \\"value\\": \\"mutated\\" + } + ], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 4, + \\"id\\": 9 + } + ], + \\"adds\\": [] + }, + \\"timestamp\\": 1542268800000 + } +]" +`; + exports[`child-list 1`] = ` "[ { @@ -51,33 +363,51 @@ exports[`child-list 1`] = ` }, { \\"type\\": 2, - \\"tagName\\": \\"ul\\", + \\"tagName\\": \\"p\\", \\"attributes\\": {}, \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", + \\"textContent\\": \\"mutation observer\\", \\"id\\": 7 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"li\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 8 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 9 } ], \\"id\\": 6 }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 10 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 11 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 9 + }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n\\\\n \\", - \\"id\\": 10 + \\"id\\": 13 }, { \\"type\\": 2, @@ -87,15 +417,15 @@ exports[`child-list 1`] = ` { \\"type\\": 3, \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 12 + \\"id\\": 15 } ], - \\"id\\": 11 + \\"id\\": 14 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\\\n \\", - \\"id\\": 13 + \\"id\\": 16 } ], \\"id\\": 4 @@ -122,10 +452,23 @@ exports[`child-list 1`] = ` \\"removes\\": [ { \\"parentId\\": 4, - \\"id\\": 6 + \\"id\\": 9 } ], - \\"adds\\": [] + \\"adds\\": [ + { + \\"parentId\\": 6, + \\"previousId\\": 7, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 17 + } + } + ] }, \\"timestamp\\": 1542268800000 } @@ -586,20 +929,6 @@ exports[`form 1`] = ` }, \\"timestamp\\": 1542268800000 }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 1, - \\"positions\\": [ - { - \\"x\\": 204, - \\"y\\": 117, - \\"timeOffset\\": 0 - } - ] - }, - \\"timestamp\\": 1542268800000 - }, { \\"type\\": 3, \\"data\\": { diff --git a/test/html/child-list.html b/test/html/mutation-observer.html similarity index 61% rename from test/html/child-list.html rename to test/html/mutation-observer.html index 45dee83a..d5b84059 100644 --- a/test/html/child-list.html +++ b/test/html/mutation-observer.html @@ -1,4 +1,5 @@ +

mutation observer

diff --git a/test/integration.ts b/test/integration.ts index 9c65f69c..18c87ce4 100644 --- a/test/integration.ts +++ b/test/integration.ts @@ -6,6 +6,7 @@ import * as rollup from 'rollup'; import typescript = require('rollup-plugin-typescript'); import resolve = require('rollup-plugin-node-resolve'); import { SnapshotState, toMatchSnapshot } from 'jest-snapshot'; +import { incrementalSnapshotEvent } from '../src/types'; function matchSnapshot(actual: string, testFile: string, testTitle: string) { const snapshotState = new SnapshotState(testFile, { @@ -21,6 +22,24 @@ function matchSnapshot(actual: string, testFile: string, testTitle: string) { return result; } +/** + * Puppeteer may cast random mouse move which make our tests flaky. + * So we only do snapshot test with filtered events. + * @param snapshots incrementalSnapshotEvent[] + */ +function stringifySnapshots(snapshots: incrementalSnapshotEvent[]): string { + return JSON.stringify( + snapshots.filter(s => { + if (s.type === 3 && s.data.source === 1) { + return false; + } + return true; + }), + null, + 2, + ); +} + describe('record integration tests', () => { function getHtml(fileName: string): string { const filePath = path.resolve(__dirname, `./html/${fileName}`); @@ -78,7 +97,7 @@ describe('record integration tests', () => { const snapshots = await page.evaluate('window.snapshots'); const result = matchSnapshot( - JSON.stringify(snapshots, null, 2), + stringifySnapshots(snapshots), __filename, 'form', ); @@ -88,21 +107,71 @@ describe('record integration tests', () => { it('can record childList mutations', async () => { const page: puppeteer.Page = await this.browser.newPage(); await page.goto('about:blank'); - await page.setContent(getHtml.call(this, 'child-list.html')); + await page.setContent(getHtml.call(this, 'mutation-observer.html')); await page.evaluate(() => { const li = document.createElement('li'); const ul = document.querySelector('ul') as HTMLUListElement; ul.appendChild(li); document.body.removeChild(ul); + const p = document.querySelector('p') as HTMLParagraphElement; + p.appendChild(document.createElement('span')); }); const snapshots = await page.evaluate('window.snapshots'); const result = matchSnapshot( - JSON.stringify(snapshots, null, 2), + stringifySnapshots(snapshots), __filename, 'child-list', ); assert(result.pass, result.pass ? '' : result.report()); }).timeout(5000); + + it('can record character data muatations', async () => { + const page: puppeteer.Page = await this.browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'mutation-observer.html')); + + await page.evaluate(() => { + const li = document.createElement('li'); + const ul = document.querySelector('ul') as HTMLUListElement; + ul.appendChild(li); + li.innerText = 'new list item'; + li.innerText = 'new list item edit'; + document.body.removeChild(ul); + const p = document.querySelector('p') as HTMLParagraphElement; + p.innerText = 'mutated'; + }); + + const snapshots = await page.evaluate('window.snapshots'); + const result = matchSnapshot( + stringifySnapshots(snapshots), + __filename, + 'character-data', + ); + assert(result.pass, result.pass ? '' : result.report()); + }); + + it('can record attribute mutation', async () => { + const page: puppeteer.Page = await this.browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'mutation-observer.html')); + + await page.evaluate(() => { + const li = document.createElement('li'); + const ul = document.querySelector('ul') as HTMLUListElement; + ul.appendChild(li); + li.setAttribute('foo', 'bar'); + document.body.removeChild(ul); + document.body.setAttribute('test', 'true'); + }); + + const snapshots = await page.evaluate('window.snapshots'); + const result = matchSnapshot( + stringifySnapshots(snapshots), + __filename, + 'attributes', + ); + assert(result.pass, result.pass ? '' : result.report()); + }); });