From 95dd144227417039eb02c59f2bb884b98d0e7d54 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] child nodes without __sn now remove without error (#307) --- src/utils.ts | 8 ++-- test/replayer.test.ts | 24 +++++++++-- test/utils.ts | 96 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 7 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index e97612c2..2859ce4c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -307,9 +307,11 @@ export class TreeIndex { const deepRemoveFromMirror = (id: number) => { this.removeIdSet.add(id); const node = mirror.getNode(id); - node?.childNodes.forEach((childNode) => - deepRemoveFromMirror(((childNode as unknown) as INode).__sn.id), - ); + node?.childNodes.forEach((childNode) => { + if ('__sn' in childNode) { + deepRemoveFromMirror(((childNode as unknown) as INode).__sn.id) + } + }); }; const deepRemoveFromTreeIndex = (node: TreeNode) => { this.removeIdSet.add(node.id); diff --git a/test/replayer.test.ts b/test/replayer.test.ts index 731bd9c4..97fec985 100644 --- a/test/replayer.test.ts +++ b/test/replayer.test.ts @@ -5,8 +5,11 @@ import * as path from 'path'; import * as puppeteer from 'puppeteer'; import { expect } from 'chai'; import { Suite } from 'mocha'; -import { launchPuppeteer, sampleEvents as events } from './utils'; -import { EventType } from '../src/types'; +import { + launchPuppeteer, + sampleEvents as events, + sampleStyleSheetRemoveEvents as stylesheetRemoveEvents +} from './utils'; interface ISuite extends Suite { code: string; @@ -28,7 +31,7 @@ describe('replayer', function (this: ISuite) { const page: puppeteer.Page = await this.browser.newPage(); await page.goto('about:blank'); await page.evaluate(this.code); - await page.evaluate(`const events = ${JSON.stringify(events)}`); + await page.evaluate(`let events = ${JSON.stringify(events)}`); this.page = page; page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); @@ -132,6 +135,19 @@ describe('replayer', function (this: ISuite) { expect(currentState).to.equal('paused'); }); + it('can handle removing style elements', async () => { + await this.page.evaluate(`events = ${JSON.stringify(stylesheetRemoveEvents)}`); + const actionLength = await this.page.evaluate(` + const { Replayer } = rrweb; + const replayer = new Replayer(events); + replayer.play(2500); + replayer['timer']['actions'].length; + `); + expect(actionLength).to.equal( + stylesheetRemoveEvents.filter((e) => e.timestamp - stylesheetRemoveEvents[0].timestamp >= 2500).length, + ); + }); + it('can stream events in live mode', async () => { const status = await this.page.evaluate(` const { Replayer } = rrweb; @@ -142,5 +158,5 @@ describe('replayer', function (this: ISuite) { replayer.service.state.value; `); expect(status).to.equal('live'); - }) + }); }); diff --git a/test/utils.ts b/test/utils.ts index 2372e2ba..0776b963 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -202,3 +202,99 @@ export const sampleEvents: eventWithTime[] = [ timestamp: now + 4000, }, ]; + +export const sampleStyleSheetRemoveEvents: eventWithTime[] = [ + { + type: EventType.DomContentLoaded, + data: {}, + timestamp: now, + }, + { + type: EventType.Load, + data: {}, + timestamp: now + 1000, + }, + { + type: EventType.Meta, + data: { + href: 'http://localhost', + width: 1000, + height: 800, + }, + timestamp: now + 1000, + }, + { + type: EventType.FullSnapshot, + data: { + node: { + type: 0, + childNodes: [ + { + type: 2, + tagName: 'html', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: "style", + attributes: { + "data-jss": "", + "data-meta": "OverlayDrawer", + _cssText: ".OverlayDrawer-modal-187 { }.OverlayDrawer-paper-188 { width: 100%; }@media (min-width: 48em) {\n .OverlayDrawer-paper-188 { width: 38rem; }\n}@media (min-width: 48em) {\n}@media (min-width: 48em) {\n}" + }, + childNodes: [ + { + type: 3, + textContent: "\n", + isStyle: true, + id: 5 + }, + ], + id: 4 + }, + + ], + id: 3, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [], + id: 6, + }, + ], + id: 2, + }, + ], + id: 1, + }, + initialOffset: { + top: 0, + left: 0, + }, + }, + timestamp: now + 1000, + }, + { + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + texts: [], + attributes: [], + removes: [ + { + parentId: 3, + id: 4 + } + ], + adds: [] + }, + timestamp: now + 2000, + }, +];