Add nested css recording for safari
This commit is contained in:
Justin Halsall
2021-09-21 16:36:45 +02:00
committed by GitHub
parent e441a05928
commit e67eb1cd09
2 changed files with 107 additions and 44 deletions

View File

@@ -60,7 +60,10 @@ type WindowWithAngularZone = Window & {
export const mutationBuffers: MutationBuffer[] = []; 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 { function getEventTarget(event: Event): EventTarget | null {
try { 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[] { function getNestedCSSRulePositions(rule: CSSRule): number[] {
const positions: number[] = []; const positions: number[] = [];
function recurse(childRule: CSSRule, pos: 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( const rules = Array.from(
(childRule.parentRule as CSSGroupingRule).cssRules, (childRule.parentRule as GroupingCSSRule).cssRules,
); );
const index = rules.indexOf(childRule); const index = rules.indexOf(childRule);
pos.unshift(index); pos.unshift(index);
@@ -522,53 +545,78 @@ function initStyleSheetObserver(
return deleteRule.apply(this, arguments); return deleteRule.apply(this, arguments);
}; };
if (!isCSSGroupingRuleSupported) { const supportedNestedCSSRuleTypes: {
return () => { [key: string]: GroupingCSSRuleTypes;
CSSStyleSheet.prototype.insertRule = insertRule; } = {};
CSSStyleSheet.prototype.deleteRule = deleteRule; 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; const unmodifiedFunctions: {
CSSGroupingRule.prototype.insertRule = function ( [key: string]: {
rule: string, insertRule: (rule: string, index?: number) => number;
index?: number, deleteRule: (index: number) => void;
) { };
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 groupingDeleteRule = CSSGroupingRule.prototype.deleteRule; Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => {
CSSGroupingRule.prototype.deleteRule = function (index: number) { unmodifiedFunctions[typeKey] = {
const id = mirror.getId(this.parentStyleSheet.ownerNode as INode); insertRule: (type as GroupingCSSRuleTypes).prototype.insertRule,
if (id !== -1) { deleteRule: (type as GroupingCSSRuleTypes).prototype.deleteRule,
cb({ };
id,
removes: [{ index: [...getNestedCSSRulePositions(this), index] }], type.prototype.insertRule = function (rule: string, index?: number) {
}); const id = mirror.getId(this.parentStyleSheet.ownerNode as INode);
} if (id !== -1) {
return groupingDeleteRule.apply(this, arguments); 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 () => { return () => {
CSSStyleSheet.prototype.insertRule = insertRule; CSSStyleSheet.prototype.insertRule = insertRule;
CSSStyleSheet.prototype.deleteRule = deleteRule; CSSStyleSheet.prototype.deleteRule = deleteRule;
CSSGroupingRule.prototype.insertRule = groupingInsertRule; Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => {
CSSGroupingRule.prototype.deleteRule = groupingDeleteRule; 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'); assertSnapshot(this.events, __filename, 'stylesheet-rules');
}); });
it('captures nested stylesheet rules', async () => { const captureNestedStylesheetRulesTest = async () => {
await this.page.evaluate(() => { await this.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb; const { record } = ((window as unknown) as IWindow).rrweb;
@@ -295,6 +295,21 @@ describe('record', function (this: ISuite) {
expect(addRuleCount).to.equal(2); expect(addRuleCount).to.equal(2);
expect(removeRuleCount).to.equal(1); expect(removeRuleCount).to.equal(1);
assertSnapshot(this.events, __filename, 'nested-stylesheet-rules'); 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);
}); });
}); });