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 {
|
try {
|
||||||
void styleSheet.replace?.(data.replace);
|
void styleSheet.replace?.(data.replace);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// for safety
|
// for safety
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.replaceSync)
|
if (typeof data.replaceSync === 'string')
|
||||||
try {
|
try {
|
||||||
styleSheet.replaceSync?.(data.replaceSync);
|
styleSheet.replaceSync?.(data.replaceSync);
|
||||||
} catch (e) {
|
} 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 canvasInIframe from './events/canvas-in-iframe';
|
||||||
import adoptedStyleSheet from './events/adopted-style-sheet';
|
import adoptedStyleSheet from './events/adopted-style-sheet';
|
||||||
import adoptedStyleSheetModification from './events/adopted-style-sheet-modification';
|
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 nestedStyleDeclarationEvents from './events/nested-style-declaration';
|
||||||
import styleDeclarationMissingRuleEvents from './events/style-declaration-missing-rule';
|
import styleDeclarationMissingRuleEvents from './events/style-declaration-missing-rule';
|
||||||
import documentReplacementEvents from './events/document-replacement';
|
import documentReplacementEvents from './events/document-replacement';
|
||||||
@@ -1066,14 +1070,76 @@ describe('replayer', function () {
|
|||||||
await check600ms();
|
await check600ms();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can replay StyleDeclaration events on nested CSS rules inside @media', async () => {
|
it('can clear adopted stylesheets with empty replaceSync', async () => {
|
||||||
await page.evaluate(`
|
await page.evaluate(`
|
||||||
events = ${JSON.stringify(nestedStyleDeclarationEvents)};
|
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;
|
const { Replayer } = rrweb;
|
||||||
var replayer = new Replayer(events, { showDebug: true });
|
var replayer = new Replayer(events, { showDebug: true });
|
||||||
replayer.pause(0);
|
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)};
|
||||||
|
const { Replayer } = rrweb;
|
||||||
|
var replayer = new Replayer(events, { showDebug: true });
|
||||||
|
replayer.pause(0);
|
||||||
|
`);
|
||||||
// At 250ms, setProperty on [0, 0] should change background-color to red
|
// At 250ms, setProperty on [0, 0] should change background-color to red
|
||||||
const bgColorAfterSet = await page.evaluate(`
|
const bgColorAfterSet = await page.evaluate(`
|
||||||
replayer.pause(250);
|
replayer.pause(250);
|
||||||
|
|||||||
Reference in New Issue
Block a user