fix: ensure empty string replace/replaceSync clears stylesheets (#1774)
* fix: ensure empty string replace/replaceSync clears stylesheets --------- Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2026,14 +2026,14 @@ export class Replayer {
|
||||
}
|
||||
});
|
||||
|
||||
if (data.replace)
|
||||
if (typeof data.replace === 'string')
|
||||
try {
|
||||
void styleSheet.replace?.(data.replace);
|
||||
} catch (e) {
|
||||
// for safety
|
||||
}
|
||||
|
||||
if (data.replaceSync)
|
||||
if (typeof data.replaceSync === 'string')
|
||||
try {
|
||||
styleSheet.replaceSync?.(data.replaceSync);
|
||||
} catch (e) {
|
||||
|
||||
211
packages/rrweb/test/events/adopted-style-sheet-empty-replace.ts
Normal file
211
packages/rrweb/test/events/adopted-style-sheet-empty-replace.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { EventType, IncrementalSource } from '@rrweb/types';
|
||||
import type { eventWithTime } from '@rrweb/types';
|
||||
|
||||
/**
|
||||
* Test events for validating that empty string replace/replaceSync clears stylesheets.
|
||||
* This tests the fix for the bug where `if (data.replace)` would skip empty strings.
|
||||
*/
|
||||
const now = Date.now();
|
||||
export const emptyReplaceSyncEvents: eventWithTime[] = [
|
||||
{
|
||||
type: EventType.Meta,
|
||||
data: {
|
||||
href: 'about:blank',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
timestamp: now,
|
||||
},
|
||||
{
|
||||
type: EventType.FullSnapshot,
|
||||
data: {
|
||||
node: {
|
||||
type: 0,
|
||||
childNodes: [
|
||||
{
|
||||
type: 1,
|
||||
name: 'html',
|
||||
publicId: '',
|
||||
systemId: '',
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
tagName: 'html',
|
||||
attributes: {},
|
||||
childNodes: [
|
||||
{
|
||||
type: 2,
|
||||
tagName: 'head',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
id: 4,
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
tagName: 'body',
|
||||
attributes: {},
|
||||
childNodes: [
|
||||
{
|
||||
type: 2,
|
||||
tagName: 'div',
|
||||
attributes: {},
|
||||
childNodes: [
|
||||
{
|
||||
type: 3,
|
||||
textContent: 'test element',
|
||||
id: 6,
|
||||
},
|
||||
],
|
||||
id: 5,
|
||||
},
|
||||
],
|
||||
id: 3,
|
||||
},
|
||||
],
|
||||
id: 7,
|
||||
},
|
||||
],
|
||||
id: 1,
|
||||
},
|
||||
initialOffset: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
},
|
||||
timestamp: now + 100,
|
||||
},
|
||||
// Adopt a stylesheet with initial styles
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.AdoptedStyleSheet,
|
||||
id: 1,
|
||||
styles: [
|
||||
{
|
||||
styleId: 1,
|
||||
rules: [
|
||||
{
|
||||
rule: 'div { background: red; color: white; }',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
styleIds: [1],
|
||||
},
|
||||
timestamp: now + 200,
|
||||
},
|
||||
// Clear stylesheet using replaceSync('') - this was the bug!
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.StyleSheetRule,
|
||||
styleId: 1,
|
||||
replaceSync: '',
|
||||
},
|
||||
timestamp: now + 300,
|
||||
},
|
||||
];
|
||||
|
||||
export const emptyReplaceEvents: eventWithTime[] = [
|
||||
{
|
||||
type: EventType.Meta,
|
||||
data: {
|
||||
href: 'about:blank',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
timestamp: now,
|
||||
},
|
||||
{
|
||||
type: EventType.FullSnapshot,
|
||||
data: {
|
||||
node: {
|
||||
type: 0,
|
||||
childNodes: [
|
||||
{
|
||||
type: 1,
|
||||
name: 'html',
|
||||
publicId: '',
|
||||
systemId: '',
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
tagName: 'html',
|
||||
attributes: {},
|
||||
childNodes: [
|
||||
{
|
||||
type: 2,
|
||||
tagName: 'head',
|
||||
attributes: {},
|
||||
childNodes: [],
|
||||
id: 4,
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
tagName: 'body',
|
||||
attributes: {},
|
||||
childNodes: [
|
||||
{
|
||||
type: 2,
|
||||
tagName: 'div',
|
||||
attributes: {},
|
||||
childNodes: [
|
||||
{
|
||||
type: 3,
|
||||
textContent: 'test element',
|
||||
id: 6,
|
||||
},
|
||||
],
|
||||
id: 5,
|
||||
},
|
||||
],
|
||||
id: 3,
|
||||
},
|
||||
],
|
||||
id: 7,
|
||||
},
|
||||
],
|
||||
id: 1,
|
||||
},
|
||||
initialOffset: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
},
|
||||
timestamp: now + 100,
|
||||
},
|
||||
// Adopt a stylesheet with initial styles
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.AdoptedStyleSheet,
|
||||
id: 1,
|
||||
styles: [
|
||||
{
|
||||
styleId: 1,
|
||||
rules: [
|
||||
{
|
||||
rule: 'div { background: blue; color: yellow; }',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
styleIds: [1],
|
||||
},
|
||||
timestamp: now + 200,
|
||||
},
|
||||
// Clear stylesheet using replace('') - this was the bug!
|
||||
{
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.StyleSheetRule,
|
||||
styleId: 1,
|
||||
replace: '',
|
||||
},
|
||||
timestamp: now + 300,
|
||||
},
|
||||
];
|
||||
@@ -25,6 +25,10 @@ import StyleSheetTextMutation from './events/style-sheet-text-mutation';
|
||||
import canvasInIframe from './events/canvas-in-iframe';
|
||||
import adoptedStyleSheet from './events/adopted-style-sheet';
|
||||
import adoptedStyleSheetModification from './events/adopted-style-sheet-modification';
|
||||
import {
|
||||
emptyReplaceSyncEvents,
|
||||
emptyReplaceEvents,
|
||||
} from './events/adopted-style-sheet-empty-replace';
|
||||
import nestedStyleDeclarationEvents from './events/nested-style-declaration';
|
||||
import styleDeclarationMissingRuleEvents from './events/style-declaration-missing-rule';
|
||||
import documentReplacementEvents from './events/document-replacement';
|
||||
@@ -1066,6 +1070,69 @@ describe('replayer', function () {
|
||||
await check600ms();
|
||||
});
|
||||
|
||||
it('can clear adopted stylesheets with empty replaceSync', async () => {
|
||||
await page.evaluate(`
|
||||
events = ${JSON.stringify(emptyReplaceSyncEvents)};
|
||||
|
||||
const { Replayer } = rrweb;
|
||||
var replayer = new Replayer(events, { showDebug: true });
|
||||
replayer.pause(0);
|
||||
`);
|
||||
|
||||
const iframe = await page.$('iframe');
|
||||
const contentDocument = await iframe!.contentFrame()!;
|
||||
|
||||
// At 250ms, stylesheet should have rules
|
||||
await page.evaluate('replayer.pause(250);');
|
||||
expect(
|
||||
await contentDocument!.evaluate(
|
||||
() =>
|
||||
document.adoptedStyleSheets.length === 1 &&
|
||||
document.adoptedStyleSheets[0].cssRules.length === 1,
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
// At 350ms, stylesheet should be empty after replaceSync('')
|
||||
await page.evaluate('replayer.pause(350);');
|
||||
expect(
|
||||
await contentDocument!.evaluate(
|
||||
() =>
|
||||
document.adoptedStyleSheets.length === 1 &&
|
||||
document.adoptedStyleSheets[0].cssRules.length === 0,
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can clear adopted stylesheets with empty replace', async () => {
|
||||
await page.evaluate(`
|
||||
events = ${JSON.stringify(emptyReplaceEvents)};
|
||||
const { Replayer } = rrweb;
|
||||
var replayer = new Replayer(events, { showDebug: true });
|
||||
replayer.pause(0);
|
||||
`);
|
||||
|
||||
const iframe = await page.$('iframe');
|
||||
const contentDocument = await iframe!.contentFrame()!;
|
||||
|
||||
// At 250ms, stylesheet should have rules
|
||||
await page.evaluate('replayer.pause(250);');
|
||||
expect(
|
||||
await contentDocument!.evaluate(
|
||||
() =>
|
||||
document.adoptedStyleSheets.length === 1 &&
|
||||
document.adoptedStyleSheets[0].cssRules.length === 1,
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
// At 350ms, stylesheet should be empty after replace('')
|
||||
await page.evaluate('replayer.pause(350);');
|
||||
await contentDocument!.waitForFunction(
|
||||
() =>
|
||||
document.adoptedStyleSheets.length === 1 &&
|
||||
document.adoptedStyleSheets[0].cssRules.length === 0,
|
||||
);
|
||||
});
|
||||
|
||||
it('can replay StyleDeclaration events on nested CSS rules inside @media', async () => {
|
||||
await page.evaluate(`
|
||||
events = ${JSON.stringify(nestedStyleDeclarationEvents)};
|
||||
@@ -1073,7 +1140,6 @@ describe('replayer', function () {
|
||||
var replayer = new Replayer(events, { showDebug: true });
|
||||
replayer.pause(0);
|
||||
`);
|
||||
|
||||
// At 250ms, setProperty on [0, 0] should change background-color to red
|
||||
const bgColorAfterSet = await page.evaluate(`
|
||||
replayer.pause(250);
|
||||
|
||||
Reference in New Issue
Block a user