Monkeypatch each iframe (#716)

* `setTimeout` and `clearTimeout` are global functions. Think the window versions of them were for the following reason: https://stackoverflow.com/questions/60245787/

* Comments and extra test here helped me understand which inserts were expected and which are to be ignored

* Add a test for the style setProperty/removeProperty added in #671

* Add a test to ensure that listeners get added correctly in nested iframes - particularly important for those which rely on prototype monkeypatching

* Pass in the window object from the current iframe so that monkeypatching applies to all windows

* Satisfy typings

* No need to insert an iframe as there's one already set up for us

* Enable the console logger to also intercept log messages within iframes

* There's no tests for FontFace but presumably the monkeypatching here works similarly to the others
This commit is contained in:
Eoghan Murray
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 036472280c
commit 4b6efde134
11 changed files with 600 additions and 50 deletions

View File

@@ -22,7 +22,7 @@ type LogRecordOptions = {
level?: LogLevel[];
lengthThreshold?: number;
stringifyOptions?: StringifyOptions;
logger?: Logger;
logger?: Logger | string;
};
const defaultLogOptions: LogRecordOptions = {
@@ -48,7 +48,7 @@ const defaultLogOptions: LogRecordOptions = {
'warn',
],
lengthThreshold: 1000,
logger: console,
logger: 'console',
};
export type LogData = {
@@ -106,12 +106,19 @@ export type Logger = {
function initLogObserver(
cb: logCallback,
win: Window, // top window or in an iframe
logOptions: LogRecordOptions,
): listenerHandler {
const logger = logOptions.logger;
if (!logger) {
const loggerType = logOptions.logger;
if (!loggerType) {
return () => {};
}
let logger: Logger;
if (typeof loggerType === 'string') {
logger = (win as any)[loggerType];
} else {
logger = loggerType;
}
let logCount = 0;
const cancelHandlers: listenerHandler[] = [];
// add listener to thrown errors

View File

@@ -519,10 +519,11 @@ function getNestedCSSRulePositions(rule: CSSRule): number[] {
function initStyleSheetObserver(
cb: styleSheetRuleCallback,
win: Window,
mirror: Mirror,
): listenerHandler {
const insertRule = CSSStyleSheet.prototype.insertRule;
CSSStyleSheet.prototype.insertRule = function (rule: string, index?: number) {
const insertRule = (win as any).CSSStyleSheet.prototype.insertRule;
(win as any).CSSStyleSheet.prototype.insertRule = function (rule: string, index?: number) {
const id = mirror.getId(this.ownerNode as INode);
if (id !== -1) {
cb({
@@ -533,8 +534,8 @@ function initStyleSheetObserver(
return insertRule.apply(this, arguments);
};
const deleteRule = CSSStyleSheet.prototype.deleteRule;
CSSStyleSheet.prototype.deleteRule = function (index: number) {
const deleteRule = (win as any).CSSStyleSheet.prototype.deleteRule;
(win as any).CSSStyleSheet.prototype.deleteRule = function (index: number) {
const id = mirror.getId(this.ownerNode as INode);
if (id !== -1) {
cb({
@@ -549,20 +550,20 @@ function initStyleSheetObserver(
[key: string]: GroupingCSSRuleTypes;
} = {};
if (isCSSGroupingRuleSupported) {
supportedNestedCSSRuleTypes['CSSGroupingRule'] = CSSGroupingRule;
supportedNestedCSSRuleTypes['CSSGroupingRule'] = (win as any).CSSGroupingRule;
} else {
// Some browsers (Safari) don't support CSSGroupingRule
// https://caniuse.com/?search=cssgroupingrule
// fall back to monkey patching classes that would have inherited from CSSGroupingRule
if (isCSSMediaRuleSupported) {
supportedNestedCSSRuleTypes['CSSMediaRule'] = CSSMediaRule;
supportedNestedCSSRuleTypes['CSSMediaRule'] = (win as any).CSSMediaRule;
}
if (isCSSConditionRuleSupported) {
supportedNestedCSSRuleTypes['CSSConditionRule'] = CSSConditionRule;
supportedNestedCSSRuleTypes['CSSConditionRule'] = (win as any).CSSConditionRule;
}
if (isCSSSupportsRuleSupported) {
supportedNestedCSSRuleTypes['CSSSupportsRule'] = CSSSupportsRule;
supportedNestedCSSRuleTypes['CSSSupportsRule'] = (win as any).CSSSupportsRule;
}
}
@@ -611,8 +612,8 @@ function initStyleSheetObserver(
});
return () => {
CSSStyleSheet.prototype.insertRule = insertRule;
CSSStyleSheet.prototype.deleteRule = deleteRule;
(win as any).CSSStyleSheet.prototype.insertRule = insertRule;
(win as any).CSSStyleSheet.prototype.deleteRule = deleteRule;
Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => {
type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule;
type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule;
@@ -622,14 +623,15 @@ function initStyleSheetObserver(
function initStyleDeclarationObserver(
cb: styleDeclarationCallback,
win: Window,
mirror: Mirror,
): listenerHandler {
const setProperty = CSSStyleDeclaration.prototype.setProperty;
CSSStyleDeclaration.prototype.setProperty = function (
const setProperty = (win as any).CSSStyleDeclaration.prototype.setProperty;
(win as any).CSSStyleDeclaration.prototype.setProperty = function (
this: CSSStyleDeclaration,
property,
value,
priority,
property: string,
value: string,
priority: string,
) {
const id = mirror.getId(
(this.parentRule?.parentStyleSheet?.ownerNode as unknown) as INode,
@@ -648,10 +650,10 @@ function initStyleDeclarationObserver(
return setProperty.apply(this, arguments);
};
const removeProperty = CSSStyleDeclaration.prototype.removeProperty;
CSSStyleDeclaration.prototype.removeProperty = function (
const removeProperty = (win as any).CSSStyleDeclaration.prototype.removeProperty;
(win as any).CSSStyleDeclaration.prototype.removeProperty = function (
this: CSSStyleDeclaration,
property,
property: string,
) {
const id = mirror.getId(
(this.parentRule?.parentStyleSheet?.ownerNode as unknown) as INode,
@@ -669,8 +671,8 @@ function initStyleDeclarationObserver(
};
return () => {
CSSStyleDeclaration.prototype.setProperty = setProperty;
CSSStyleDeclaration.prototype.removeProperty = removeProperty;
(win as any).CSSStyleDeclaration.prototype.setProperty = setProperty;
(win as any).CSSStyleDeclaration.prototype.removeProperty = removeProperty;
};
}
@@ -702,22 +704,23 @@ function initMediaInteractionObserver(
function initCanvasMutationObserver(
cb: canvasMutationCallback,
win: Window,
blockClass: blockClass,
mirror: Mirror,
): listenerHandler {
const props = Object.getOwnPropertyNames(CanvasRenderingContext2D.prototype);
const props = Object.getOwnPropertyNames((win as any).CanvasRenderingContext2D.prototype);
const handlers: listenerHandler[] = [];
for (const prop of props) {
try {
if (
typeof CanvasRenderingContext2D.prototype[
typeof (win as any).CanvasRenderingContext2D.prototype[
prop as keyof CanvasRenderingContext2D
] !== 'function'
) {
continue;
}
const restoreHandler = patch(
CanvasRenderingContext2D.prototype,
(win as any).CanvasRenderingContext2D.prototype,
prop,
function (original) {
return function (
@@ -758,7 +761,7 @@ function initCanvasMutationObserver(
handlers.push(restoreHandler);
} catch {
const hookHandler = hookSetter<CanvasRenderingContext2D>(
CanvasRenderingContext2D.prototype,
(win as any).CanvasRenderingContext2D.prototype,
prop,
{
set(v) {
@@ -779,14 +782,19 @@ function initCanvasMutationObserver(
};
}
function initFontObserver(cb: fontCallback): listenerHandler {
function initFontObserver(
cb: fontCallback,
doc: Document,
): listenerHandler {
const win = doc.defaultView;
const handlers: listenerHandler[] = [];
const fontMap = new WeakMap<FontFace, fontParam>();
const originalFontFace = FontFace;
const originalFontFace = (win as any).FontFace;
// tslint:disable-next-line: no-any
(window as any).FontFace = function FontFace(
(win as any).FontFace = function FontFace(
family: string,
source: string | ArrayBufferView,
descriptors?: FontFaceDescriptors,
@@ -805,7 +813,7 @@ function initFontObserver(cb: fontCallback): listenerHandler {
return fontFace;
};
const restoreHandler = patch(document.fonts, 'add', function (original) {
const restoreHandler = patch(doc.fonts, 'add', function (original) {
return function (this: FontFaceSet, fontFace: FontFace) {
setTimeout(() => {
const p = fontMap.get(fontFace);
@@ -820,7 +828,7 @@ function initFontObserver(cb: fontCallback): listenerHandler {
handlers.push(() => {
// tslint:disable-next-line: no-any
(window as any).FontFace = originalFontFace;
(win as any).FontFace = originalFontFace;
});
handlers.push(restoreHandler);
@@ -971,22 +979,35 @@ export function initObservers(
o.blockClass,
o.mirror,
);
const currentWindow = o.doc.defaultView as Window; // basically document.window
const styleSheetObserver = initStyleSheetObserver(
o.styleSheetRuleCb,
currentWindow,
o.mirror,
);
const styleDeclarationObserver = initStyleDeclarationObserver(
o.styleDeclarationCb,
currentWindow,
o.mirror,
);
const canvasMutationObserver = o.recordCanvas
? initCanvasMutationObserver(o.canvasMutationCb, o.blockClass, o.mirror)
: () => {};
const fontObserver = o.collectFonts ? initFontObserver(o.fontCb) : () => {};
? initCanvasMutationObserver(
o.canvasMutationCb,
currentWindow,
o.blockClass,
o.mirror,
) : () => {};
const fontObserver = o.collectFonts ? initFontObserver(o.fontCb, o.doc) : () => {};
// plugins
const pluginHandlers: listenerHandler[] = [];
for (const plugin of o.plugins) {
pluginHandlers.push(plugin.observer(plugin.callback, plugin.options));
pluginHandlers.push(plugin.observer(
plugin.callback,
currentWindow,
plugin.options,
));
}
return () => {

View File

@@ -759,7 +759,7 @@ export class Replayer {
const head = this.iframe.contentDocument?.head;
if (head) {
const unloadSheets: Set<HTMLLinkElement> = new Set();
let timer: number;
let timer: ReturnType<typeof setTimeout> | -1;
let beforeLoadState = this.service.state;
const stateHandler = () => {
beforeLoadState = this.service.state;
@@ -784,7 +784,7 @@ export class Replayer {
}
this.emitter.emit(ReplayerEvents.LoadStylesheetEnd);
if (timer) {
window.clearTimeout(timer);
clearTimeout(timer);
}
unsubscribe();
}
@@ -796,7 +796,7 @@ export class Replayer {
// find some unload sheets after iterate
this.service.send({ type: 'PAUSE' });
this.emitter.emit(ReplayerEvents.LoadStylesheetStart);
timer = window.setTimeout(() => {
timer = setTimeout(() => {
if (beforeLoadState.matches('playing')) {
this.play(this.getCurrentTime());
}

View File

@@ -201,7 +201,7 @@ export type SamplingStrategy = Partial<{
export type RecordPlugin<TOptions = unknown> = {
name: string;
observer: (cb: Function, options: TOptions) => listenerHandler;
observer: (cb: Function, win: Window, options: TOptions) => listenerHandler;
options: TOptions;
};

View File

@@ -111,7 +111,7 @@ export function throttle<T>(
wait: number,
options: throttleOptions = {},
) {
let timeout: number | null = null;
let timeout: ReturnType<typeof setTimeout> | null = null;
let previous = 0;
// tslint:disable-next-line: only-arrow-functions
return function (arg: T) {
@@ -124,13 +124,13 @@ export function throttle<T>(
let args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
window.clearTimeout(timeout);
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout && options.trailing !== false) {
timeout = window.setTimeout(() => {
timeout = setTimeout(() => {
previous = options.leading === false ? 0 : Date.now();
timeout = null;
func.apply(context, args);

View File

@@ -3347,6 +3347,88 @@ exports[`log 1`] = `
\\"payload\\": []
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 16,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"iframe\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 21
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"adds\\": [
{
\\"parentId\\": 21,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"rootId\\": 22,
\\"id\\": 24
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"rootId\\": 22,
\\"id\\": 25
}
],
\\"rootId\\": 22,
\\"id\\": 23
}
],
\\"id\\": 22
}
}
],
\\"removes\\": [],
\\"texts\\": [],
\\"attributes\\": [],
\\"isAttachIframe\\": true
}
},
{
\\"type\\": 6,
\\"data\\": {
\\"plugin\\": \\"rrweb/console@1\\",
\\"payload\\": {
\\"level\\": \\"log\\",
\\"trace\\": [],
\\"payload\\": [
\\"\\\\\\"from iframe\\\\\\"\\"
]
}
}
}
]"
`;

View File

@@ -329,6 +329,234 @@ exports[`custom-event 1`] = `
]"
`;
exports[`iframe-stylesheet-mutations 1`] = `
"[
{
\\"type\\": 4,
\\"data\\": {
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"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\\": \\"iframe\\",
\\"attributes\\": {
\\"srcdoc\\": \\"<button>Mysterious Button</button>\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n </body>\\\\n </html>\\\\n \\",
\\"id\\": 7
}
],
\\"id\\": 6
}
],
\\"id\\": 4
}
],
\\"id\\": 2
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"adds\\": [
{
\\"parentId\\": 6,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"rootId\\": 8,
\\"id\\": 10
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"button\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"Mysterious Button\\",
\\"rootId\\": 8,
\\"id\\": 13
}
],
\\"rootId\\": 8,
\\"id\\": 12
}
],
\\"rootId\\": 8,
\\"id\\": 11
}
],
\\"rootId\\": 8,
\\"id\\": 9
}
],
\\"id\\": 8
}
}
],
\\"removes\\": [],
\\"texts\\": [],
\\"attributes\\": [],
\\"isAttachIframe\\": true
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 10,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"style\\",
\\"attributes\\": {
\\"_cssText\\": \\"body { background: rgb(0, 0, 0); }@media {\\\\n body { background: rgb(0, 0, 0); }\\\\n}\\"
},
\\"childNodes\\": [],
\\"rootId\\": 8,
\\"id\\": 14
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 8,
\\"id\\": 14,
\\"adds\\": [
{
\\"rule\\": \\"body { color: #fff; }\\"
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 8,
\\"id\\": 14,
\\"adds\\": [
{
\\"rule\\": \\"body { color: #ccc; }\\",
\\"index\\": [
2,
0
]
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 8,
\\"id\\": 14,
\\"removes\\": [
{
\\"index\\": 0
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 13,
\\"id\\": 14,
\\"set\\": {
\\"property\\": \\"color\\",
\\"value\\": \\"green\\"
},
\\"index\\": [
0
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 8,
\\"id\\": 14,
\\"removes\\": [
{
\\"index\\": [
1,
0
]
}
]
}
}
]"
`;
exports[`nested-stylesheet-rules 1`] = `
"[
{
@@ -471,6 +699,128 @@ exports[`nested-stylesheet-rules 1`] = `
]"
`;
exports[`stylesheet-properties 1`] = `
"[
{
\\"type\\": 4,
\\"data\\": {
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"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\\": \\"input\\",
\\"attributes\\": {
\\"type\\": \\"text\\",
\\"size\\": \\"40\\"
},
\\"childNodes\\": [],
\\"id\\": 6
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
\\"id\\": 7
}
],
\\"id\\": 4
}
],
\\"id\\": 2
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 3,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"style\\",
\\"attributes\\": {
\\"_cssText\\": \\"body { background: rgb(0, 0, 0); }\\"
},
\\"childNodes\\": [],
\\"id\\": 8
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 13,
\\"id\\": 8,
\\"set\\": {
\\"property\\": \\"color\\",
\\"value\\": \\"green\\"
},
\\"index\\": [
0
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 13,
\\"id\\": 8,
\\"remove\\": {
\\"property\\": \\"background\\"
},
\\"index\\": [
0
]
}
}
]"
`;
exports[`stylesheet-rules 1`] = `
"[
{

View File

@@ -471,8 +471,15 @@ describe('record integration tests', function (this: ISuite) {
console.trace('trace');
console.warn('warn');
console.clear();
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
});
await page.frames()[1].evaluate(() => {
console.log('from iframe');
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'log');
});

View File

@@ -220,9 +220,11 @@ describe('record', function (this: ISuite) {
document.head.appendChild(styleElement);
const styleSheet = <CSSStyleSheet>styleElement.sheet;
// begin: pre-serialization
const ruleIdx0 = styleSheet.insertRule('body { background: #000; }');
const ruleIdx1 = styleSheet.insertRule('body { background: #111; }');
styleSheet.deleteRule(ruleIdx1);
// end: pre-serialization
setTimeout(() => {
styleSheet.insertRule('body { color: #fff; }');
}, 0);
@@ -239,14 +241,15 @@ describe('record', function (this: ISuite) {
e.type === EventType.IncrementalSnapshot &&
e.data.source === IncrementalSource.StyleSheetRule,
);
const addRuleCount = styleSheetRuleEvents.filter((e) =>
const addRules = styleSheetRuleEvents.filter((e) =>
Boolean((e.data as styleSheetRuleData).adds),
).length;
);
const removeRuleCount = styleSheetRuleEvents.filter((e) =>
Boolean((e.data as styleSheetRuleData).removes),
).length;
// sync insert/delete should be ignored
expect(addRuleCount).to.equal(2);
// pre-serialization insert/delete should be ignored
expect(addRules.length).to.equal(2);
expect((addRules[0].data as styleSheetRuleData).adds).to.deep.include({rule: "body { color: #fff; }"});
expect(removeRuleCount).to.equal(1);
assertSnapshot(this.events, __filename, 'stylesheet-rules');
});
@@ -311,6 +314,30 @@ describe('record', function (this: ISuite) {
});
it('captures nested stylesheet rules', captureNestedStylesheetRulesTest);
});
it('captures style property changes', async () => {
await this.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
record({
emit: ((window as unknown) as IWindow).emit,
});
const styleElement = document.createElement('style');
document.head.appendChild(styleElement);
const styleSheet = <CSSStyleSheet>styleElement.sheet;
styleSheet.insertRule('body { background: #000; }');
setTimeout(() => {
(styleSheet.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green');
(styleSheet.cssRules[0] as CSSStyleRule).style.removeProperty('background');
}, 0);
});
await this.page.waitForTimeout(50);
assertSnapshot(this.events, __filename, 'stylesheet-properties');
});
});
describe('record iframes', function (this: ISuite) {
@@ -350,4 +377,60 @@ describe('record iframes', function (this: ISuite) {
EventType.IncrementalSnapshot,
]);
});
it('captures stylesheet mutations in iframes', async () => {
await this.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
record({
// need to reference window.top for when we are in an iframe!
emit: ((window.top as unknown) as IWindow).emit,
});
const iframe = document.querySelector('iframe');
// outer timeout is needed to wait for initStyleSheetObserver on iframe to be set up
setTimeout(() => {
const idoc = (iframe as HTMLIFrameElement).contentDocument!;
const styleElement = idoc.createElement('style');
idoc.head.appendChild(styleElement);
const styleSheet = <CSSStyleSheet>styleElement.sheet;
styleSheet.insertRule('@media {}');
const atMediaRule = styleSheet.cssRules[0] as CSSMediaRule;
const atRuleIdx0 = atMediaRule.insertRule('body { background: #000; }', 0);
const ruleIdx0 = styleSheet.insertRule('body { background: #000; }'); // inserted before above
// pre-serialization insert/delete above should be ignored
setTimeout(() => {
styleSheet.insertRule('body { color: #fff; }');
atMediaRule.insertRule('body { color: #ccc; }', 0);
}, 0);
setTimeout(() => {
styleSheet.deleteRule(ruleIdx0);
(styleSheet.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green');
}, 5);
setTimeout(() =>{
atMediaRule.deleteRule(atRuleIdx0);
}, 10);
}, 10);
});
await this.page.waitForTimeout(50);
const styleRelatedEvents = this.events.filter(
(e) =>
e.type === EventType.IncrementalSnapshot &&
(e.data.source === IncrementalSource.StyleSheetRule ||
e.data.source === IncrementalSource.StyleDeclaration),
);
const addRuleCount = styleRelatedEvents.filter((e) =>
Boolean((e.data as styleSheetRuleData).adds),
).length;
const removeRuleCount = styleRelatedEvents.filter((e) =>
Boolean((e.data as styleSheetRuleData).removes),
).length;
expect(styleRelatedEvents.length).to.equal(5);
expect(addRuleCount).to.equal(2);
expect(removeRuleCount).to.equal(2);
assertSnapshot(this.events, __filename, 'iframe-stylesheet-mutations');
});
});

View File

@@ -8,7 +8,7 @@ declare type LogRecordOptions = {
level?: LogLevel[];
lengthThreshold?: number;
stringifyOptions?: StringifyOptions;
logger?: Logger;
logger?: Logger | string;
};
export declare type LogData = {
level: LogLevel;

View File

@@ -126,7 +126,7 @@ export declare type SamplingStrategy = Partial<{
}>;
export declare type RecordPlugin<TOptions = unknown> = {
name: string;
observer: (cb: Function, options: TOptions) => listenerHandler;
observer: (cb: Function, win: Window, options: TOptions) => listenerHandler;
options: TOptions;
};
export declare type recordOptions<T> = {