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({
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,
});

View File

@@ -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\\": {

View File

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

View File

@@ -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());
});
});