Add nested css recording for safari
This commit is contained in:
Justin Halsall
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 4ceb7eaead
commit 793ff43ed1
2 changed files with 107 additions and 44 deletions

View File

@@ -60,7 +60,10 @@ type WindowWithAngularZone = Window & {
export const mutationBuffers: MutationBuffer[] = [];
const isCSSGroupingRuleSupported = typeof CSSGroupingRule !== "undefined"
const isCSSGroupingRuleSupported = typeof CSSGroupingRule !== 'undefined';
const isCSSMediaRuleSupported = typeof CSSMediaRule !== 'undefined';
const isCSSSupportsRuleSupported = typeof CSSSupportsRule !== 'undefined';
const isCSSConditionRuleSupported = typeof CSSConditionRule !== 'undefined';
function getEventTarget(event: Event): EventTarget | null {
try {
@@ -475,12 +478,32 @@ function initInputObserver(
};
}
type GroupingCSSRule =
| CSSGroupingRule
| CSSMediaRule
| CSSSupportsRule
| CSSConditionRule;
type GroupingCSSRuleTypes =
| typeof CSSGroupingRule
| typeof CSSMediaRule
| typeof CSSSupportsRule
| typeof CSSConditionRule;
function getNestedCSSRulePositions(rule: CSSRule): number[] {
const positions: number[] = [];
function recurse(childRule: CSSRule, pos: number[]) {
if (isCSSGroupingRuleSupported && childRule.parentRule instanceof CSSGroupingRule) {
if (
(isCSSGroupingRuleSupported &&
childRule.parentRule instanceof CSSGroupingRule) ||
(isCSSMediaRuleSupported &&
childRule.parentRule instanceof CSSMediaRule) ||
(isCSSSupportsRuleSupported &&
childRule.parentRule instanceof CSSSupportsRule) ||
(isCSSConditionRuleSupported &&
childRule.parentRule instanceof CSSConditionRule)
) {
const rules = Array.from(
(childRule.parentRule as CSSGroupingRule).cssRules,
(childRule.parentRule as GroupingCSSRule).cssRules,
);
const index = rules.indexOf(childRule);
pos.unshift(index);
@@ -522,53 +545,78 @@ function initStyleSheetObserver(
return deleteRule.apply(this, arguments);
};
if (!isCSSGroupingRuleSupported) {
return () => {
CSSStyleSheet.prototype.insertRule = insertRule;
CSSStyleSheet.prototype.deleteRule = deleteRule;
};
const supportedNestedCSSRuleTypes: {
[key: string]: GroupingCSSRuleTypes;
} = {};
if (isCSSGroupingRuleSupported) {
supportedNestedCSSRuleTypes['CSSGroupingRule'] = 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;
}
if (isCSSConditionRuleSupported) {
supportedNestedCSSRuleTypes['CSSConditionRule'] = CSSConditionRule;
}
if (isCSSSupportsRuleSupported) {
supportedNestedCSSRuleTypes['CSSSupportsRule'] = CSSSupportsRule;
}
}
const groupingInsertRule = CSSGroupingRule.prototype.insertRule;
CSSGroupingRule.prototype.insertRule = function (
rule: string,
index?: number,
) {
const id = mirror.getId(this.parentStyleSheet.ownerNode as INode);
if (id !== -1) {
cb({
id,
adds: [
{
rule,
index: [
...getNestedCSSRulePositions(this),
index || 0, // defaults to 0
],
},
],
});
}
return groupingInsertRule.apply(this, arguments);
};
const unmodifiedFunctions: {
[key: string]: {
insertRule: (rule: string, index?: number) => number;
deleteRule: (index: number) => void;
};
} = {};
const groupingDeleteRule = CSSGroupingRule.prototype.deleteRule;
CSSGroupingRule.prototype.deleteRule = function (index: number) {
const id = mirror.getId(this.parentStyleSheet.ownerNode as INode);
if (id !== -1) {
cb({
id,
removes: [{ index: [...getNestedCSSRulePositions(this), index] }],
});
}
return groupingDeleteRule.apply(this, arguments);
};
Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => {
unmodifiedFunctions[typeKey] = {
insertRule: (type as GroupingCSSRuleTypes).prototype.insertRule,
deleteRule: (type as GroupingCSSRuleTypes).prototype.deleteRule,
};
type.prototype.insertRule = function (rule: string, index?: number) {
const id = mirror.getId(this.parentStyleSheet.ownerNode as INode);
if (id !== -1) {
cb({
id,
adds: [
{
rule,
index: [
...getNestedCSSRulePositions(this),
index || 0, // defaults to 0
],
},
],
});
}
return unmodifiedFunctions[typeKey].insertRule.apply(this, arguments);
};
type.prototype.deleteRule = function (index: number) {
const id = mirror.getId(this.parentStyleSheet.ownerNode as INode);
if (id !== -1) {
cb({
id,
removes: [{ index: [...getNestedCSSRulePositions(this), index] }],
});
}
return unmodifiedFunctions[typeKey].deleteRule.apply(this, arguments);
};
});
return () => {
CSSStyleSheet.prototype.insertRule = insertRule;
CSSStyleSheet.prototype.deleteRule = deleteRule;
CSSGroupingRule.prototype.insertRule = groupingInsertRule;
CSSGroupingRule.prototype.deleteRule = groupingDeleteRule;
Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => {
type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule;
type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule;
});
};
}

View File

@@ -251,7 +251,7 @@ describe('record', function (this: ISuite) {
assertSnapshot(this.events, __filename, 'stylesheet-rules');
});
it('captures nested stylesheet rules', async () => {
const captureNestedStylesheetRulesTest = async () => {
await this.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
@@ -295,6 +295,21 @@ describe('record', function (this: ISuite) {
expect(addRuleCount).to.equal(2);
expect(removeRuleCount).to.equal(1);
assertSnapshot(this.events, __filename, 'nested-stylesheet-rules');
};
it('captures nested stylesheet rules', captureNestedStylesheetRulesTest);
describe('without CSSGroupingRule support', () => {
// Safari currently doesn't support CSSGroupingRule, let's test without that
// https://caniuse.com/?search=CSSGroupingRule
beforeEach(async () => {
await this.page.evaluate(() => {
/* @ts-ignore: override CSSGroupingRule */
CSSGroupingRule = undefined;
});
// load a fresh rrweb recorder without CSSGroupingRule
await this.page.evaluate(this.code);
});
it('captures nested stylesheet rules', captureNestedStylesheetRulesTest);
});
});