Fix: shadow dom bugs (#1049)

* Add test cases for bugs

* Fix shadow dom recording

When moving an element containing shadow dom
When adding an element to shadow dom before its attached to the dom

* Apply formatting changes

* Refactor in dom checking code

* Nodes don't get processed in more than one mutation buffer

* Constrain node mutations to one mutation buffer per request animation frame

* Make tests less flaky under heavy load

* Apply suggestions from code review

* Update packages/rrweb-snapshot/test/rebuild.test.ts

* Remove unused nodeSet

Co-authored-by: Yun Feng <yun.feng0817@gmail.com>
This commit is contained in:
Justin Halsall
2026-04-01 12:00:00 +08:00
committed by GitHub
parent e91433bb66
commit 236d7a3f3c
11 changed files with 831 additions and 29 deletions

View File

@@ -667,6 +667,119 @@ describe('record integration tests', function (this: ISuite) {
assertSnapshot(snapshots);
});
it('should record shadow DOM 2', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'blank.html'));
await page.evaluate(() => {
return new Promise((resolve) => {
const el = document.createElement('div') as HTMLDivElement;
el.attachShadow({ mode: 'open' });
(el.shadowRoot as ShadowRoot).appendChild(
document.createElement('input'),
);
setTimeout(() => {
document.body.append(el);
resolve(null);
}, 10);
});
});
await waitForRAF(page);
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('should record shadow DOM 3', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'blank.html'));
await page.evaluate(() => {
const el = document.createElement('div') as HTMLDivElement;
el.attachShadow({ mode: 'open' });
(el.shadowRoot as ShadowRoot).appendChild(
document.createElement('input'),
);
document.body.append(el);
});
await waitForRAF(page);
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('should record moved shadow DOM', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'blank.html'));
await page.evaluate(() => {
return new Promise((resolve) => {
const el = document.createElement('div') as HTMLDivElement;
el.attachShadow({ mode: 'open' });
(el.shadowRoot as ShadowRoot).appendChild(
document.createElement('input'),
);
document.body.append(el);
setTimeout(() => {
const newEl = document.createElement('div') as HTMLDivElement;
document.body.append(newEl);
newEl.append(el);
resolve(null);
}, 50);
});
});
await waitForRAF(page);
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('should record moved shadow DOM 2', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'blank.html'));
await page.evaluate(() => {
const el = document.createElement('div') as HTMLDivElement;
el.id = 'el';
el.attachShadow({ mode: 'open' });
(el.shadowRoot as ShadowRoot).appendChild(
document.createElement('input'),
);
document.body.append(el);
(el.shadowRoot as ShadowRoot).appendChild(document.createElement('span'));
(el.shadowRoot as ShadowRoot).appendChild(document.createElement('p'));
const newEl = document.createElement('div') as HTMLDivElement;
newEl.id = 'newEl';
document.body.append(newEl);
newEl.append(el);
const input = el.shadowRoot?.children[0] as HTMLInputElement;
const span = el.shadowRoot?.children[1] as HTMLSpanElement;
const p = el.shadowRoot?.children[2] as HTMLParagraphElement;
input.remove();
span.append(input);
p.append(input);
span.append(input);
setTimeout(() => {
p.append(input);
}, 0);
});
await waitForRAF(page);
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('should record nested iframes and shadow doms', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');