filter text and attributes mutations which target tot a removed node

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent 7c35cb2f49
commit 6ce32f7994
4 changed files with 450 additions and 45 deletions

View File

@@ -154,14 +154,20 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
}); });
cb({ cb({
texts: texts.map(text => ({ texts: texts
id: mirror.getId(text.node as INode), .map(text => ({
value: text.value, id: mirror.getId(text.node as INode),
})), value: text.value,
attributes: attributes.map(attribute => ({ }))
id: mirror.getId(attribute.node as INode), // text mutation without ID means the target node has been removed
attributes: attribute.attributes, .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, removes,
adds, adds,
}); });

View File

@@ -1,5 +1,317 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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`] = ` exports[`child-list 1`] = `
"[ "[
{ {
@@ -51,33 +363,51 @@ exports[`child-list 1`] = `
}, },
{ {
\\"type\\": 2, \\"type\\": 2,
\\"tagName\\": \\"ul\\", \\"tagName\\": \\"p\\",
\\"attributes\\": {}, \\"attributes\\": {},
\\"childNodes\\": [ \\"childNodes\\": [
{ {
\\"type\\": 3, \\"type\\": 3,
\\"textContent\\": \\"\\\\n \\", \\"textContent\\": \\"mutation observer\\",
\\"id\\": 7 \\"id\\": 7
},
{
\\"type\\": 2,
\\"tagName\\": \\"li\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 8
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 9
} }
], ],
\\"id\\": 6 \\"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, \\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\n \\", \\"textContent\\": \\"\\\\n\\\\n \\",
\\"id\\": 10 \\"id\\": 13
}, },
{ {
\\"type\\": 2, \\"type\\": 2,
@@ -87,15 +417,15 @@ exports[`child-list 1`] = `
{ {
\\"type\\": 3, \\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 12 \\"id\\": 15
} }
], ],
\\"id\\": 11 \\"id\\": 14
}, },
{ {
\\"type\\": 3, \\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\", \\"textContent\\": \\"\\\\n \\\\n \\",
\\"id\\": 13 \\"id\\": 16
} }
], ],
\\"id\\": 4 \\"id\\": 4
@@ -122,10 +452,23 @@ exports[`child-list 1`] = `
\\"removes\\": [ \\"removes\\": [
{ {
\\"parentId\\": 4, \\"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 \\"timestamp\\": 1542268800000
} }
@@ -586,20 +929,6 @@ exports[`form 1`] = `
}, },
\\"timestamp\\": 1542268800000 \\"timestamp\\": 1542268800000
}, },
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 1,
\\"positions\\": [
{
\\"x\\": 204,
\\"y\\": 117,
\\"timeOffset\\": 0
}
]
},
\\"timestamp\\": 1542268800000
},
{ {
\\"type\\": 3, \\"type\\": 3,
\\"data\\": { \\"data\\": {

View File

@@ -1,4 +1,5 @@
<body> <body>
<p>mutation observer</p>
<ul> <ul>
<li></li> <li></li>
</ul> </ul>

View File

@@ -6,6 +6,7 @@ import * as rollup from 'rollup';
import typescript = require('rollup-plugin-typescript'); import typescript = require('rollup-plugin-typescript');
import resolve = require('rollup-plugin-node-resolve'); import resolve = require('rollup-plugin-node-resolve');
import { SnapshotState, toMatchSnapshot } from 'jest-snapshot'; import { SnapshotState, toMatchSnapshot } from 'jest-snapshot';
import { incrementalSnapshotEvent } from '../src/types';
function matchSnapshot(actual: string, testFile: string, testTitle: string) { function matchSnapshot(actual: string, testFile: string, testTitle: string) {
const snapshotState = new SnapshotState(testFile, { const snapshotState = new SnapshotState(testFile, {
@@ -21,6 +22,24 @@ function matchSnapshot(actual: string, testFile: string, testTitle: string) {
return result; 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', () => { describe('record integration tests', () => {
function getHtml(fileName: string): string { function getHtml(fileName: string): string {
const filePath = path.resolve(__dirname, `./html/${fileName}`); const filePath = path.resolve(__dirname, `./html/${fileName}`);
@@ -78,7 +97,7 @@ describe('record integration tests', () => {
const snapshots = await page.evaluate('window.snapshots'); const snapshots = await page.evaluate('window.snapshots');
const result = matchSnapshot( const result = matchSnapshot(
JSON.stringify(snapshots, null, 2), stringifySnapshots(snapshots),
__filename, __filename,
'form', 'form',
); );
@@ -88,21 +107,71 @@ describe('record integration tests', () => {
it('can record childList mutations', async () => { it('can record childList mutations', async () => {
const page: puppeteer.Page = await this.browser.newPage(); const page: puppeteer.Page = await this.browser.newPage();
await page.goto('about:blank'); 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(() => { await page.evaluate(() => {
const li = document.createElement('li'); const li = document.createElement('li');
const ul = document.querySelector('ul') as HTMLUListElement; const ul = document.querySelector('ul') as HTMLUListElement;
ul.appendChild(li); ul.appendChild(li);
document.body.removeChild(ul); document.body.removeChild(ul);
const p = document.querySelector('p') as HTMLParagraphElement;
p.appendChild(document.createElement('span'));
}); });
const snapshots = await page.evaluate('window.snapshots'); const snapshots = await page.evaluate('window.snapshots');
const result = matchSnapshot( const result = matchSnapshot(
JSON.stringify(snapshots, null, 2), stringifySnapshots(snapshots),
__filename, __filename,
'child-list', 'child-list',
); );
assert(result.pass, result.pass ? '' : result.report()); assert(result.pass, result.pass ? '' : result.report());
}).timeout(5000); }).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());
});
}); });