chrome-devtools-frontend
Version:
Chrome DevTools UI
779 lines (720 loc) • 35.8 kB
text/typescript
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Protocol from '../../generated/protocol.js';
import {createTarget} from '../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../testing/MockConnection.js';
import * as SDK from './sdk.js';
function ruleMatch(
selector: string, cssProperties: Protocol.CSS.CSSProperty[], range?: Protocol.CSS.SourceRange,
styleSheetId = '0' as Protocol.CSS.StyleSheetId): Protocol.CSS.RuleMatch {
return {
rule: {
selectorList: {selectors: [{text: selector}], text: selector},
origin: Protocol.CSS.StyleSheetOrigin.Regular,
style: {
cssProperties,
styleSheetId,
range,
shorthandEntries: [],
},
},
matchingSelectors: [0],
};
}
function createMatchedStyles(
payload: Partial<SDK.CSSMatchedStyles.CSSMatchedStylesPayload>, node?: SDK.DOMModel.DOMNode) {
if (!node) {
node = sinon.createStubInstance(SDK.DOMModel.DOMNode);
node.id = 1 as Protocol.DOM.NodeId;
}
return SDK.CSSMatchedStyles.CSSMatchedStyles.create({
cssModel: sinon.createStubInstance(SDK.CSSModel.CSSModel),
node,
inlinePayload: null,
attributesPayload: null,
matchedPayload: [],
pseudoPayload: [],
inheritedPayload: [],
inheritedPseudoPayload: [],
animationsPayload: [],
parentLayoutNodeId: undefined,
positionTryRules: [],
propertyRules: [],
cssPropertyRegistrations: [],
fontPaletteValuesRule: undefined,
activePositionFallbackIndex: -1,
animationStylesPayload: [],
transitionsStylePayload: null,
inheritedAnimatedPayload: [],
...payload,
});
}
describe('CSSMatchedStyles', () => {
describe('computeCSSVariable', () => {
const testCssValueEquals = async (text: string, expectedValue: unknown) => {
const matchedStyles = await createMatchedStyles({
matchedPayload: [
ruleMatch(
'div',
[
{name: '--diamond', value: 'var(--diamond-a) var(--diamond-b)'},
{name: '--diamond-a', value: 'var(--foo)'},
{name: '--diamond-b', value: 'var(--foo)'},
{name: '--foo', value: 'active-foo'},
{name: '--baz', value: 'active-baz !important', important: true},
{name: '--baz', value: 'passive-baz'},
{name: '--dark', value: 'darkgrey'},
{name: '--empty', value: ''},
{name: '--empty2', value: 'var(--empty)'},
{name: '--light', value: 'lightgrey'},
{name: '--theme', value: 'var(--dark)'},
{name: '--shadow', value: '1px var(--theme)'},
{name: '--width', value: '1px'},
{name: '--a', value: 'a'},
{name: '--b', value: 'var(--a)'},
{name: '--valid-fallback', value: 'var(--non-existent, fallback-value)'},
{name: '--var-reference-in-fallback', value: 'var(--non-existent, var(--foo))'},
{name: '--itself', value: 'var(--itself)'},
{name: '--itself-complex', value: '10px var(--itself-complex)'},
{name: '--cycle-1', value: 'var(--cycle-2)'},
{name: '--cycle-2', value: 'var(--cycle-1)'},
{name: '--cycle-a', value: 'var(--cycle-b, 50px)'},
{name: '--cycle-b', value: 'var(--cycle-a)'},
{name: '--cycle-in-fallback', value: 'var(--non-existent, var(--cycle-a))'},
{name: '--non-existent-fallback', value: 'var(--non-existent, var(--another-non-existent))'},
{name: '--out-of-cycle', value: 'var(--cycle-2, 20px)'},
{name: '--non-inherited', value: 'var(--inherited)'},
{name: '--also-inherited-overloaded', value: 'this is overloaded here'},
]),
ruleMatch(
'html',
[
{name: '--inherited', value: 'var(--also-inherited-overloaded)'},
{name: '--also-inherited-overloaded', value: 'inherited and overloaded'},
]),
],
});
const actualValue = matchedStyles.computeCSSVariable(matchedStyles.nodeStyles()[0], text)?.value ?? null;
assert.strictEqual(actualValue, expectedValue);
};
it('should correctly compute the value of an expression that uses a variable', async () => {
await testCssValueEquals('--foo', 'active-foo');
await testCssValueEquals('--baz', 'active-baz !important');
await testCssValueEquals('--does-not-exist', null);
await testCssValueEquals('--dark', 'darkgrey');
await testCssValueEquals('--light', 'lightgrey');
await testCssValueEquals('--theme', 'darkgrey');
await testCssValueEquals('--shadow', '1px darkgrey');
await testCssValueEquals('--width', '1px');
await testCssValueEquals('--diamond', 'active-foo active-foo');
await testCssValueEquals('--empty', '');
await testCssValueEquals('--empty2', '');
});
it('correctly resolves the declaration', async () => {
const node = sinon.createStubInstance(SDK.DOMModel.DOMNode);
node.id = 1 as Protocol.DOM.NodeId;
node.parentNode = sinon.createStubInstance(SDK.DOMModel.DOMNode);
node.parentNode.id = 2 as Protocol.DOM.NodeId;
node.parentNode.parentNode = sinon.createStubInstance(SDK.DOMModel.DOMNode);
node.parentNode.parentNode.id = 3 as Protocol.DOM.NodeId;
node.parentNode.parentNode.parentNode = sinon.createStubInstance(SDK.DOMModel.DOMNode);
node.parentNode.parentNode.parentNode.id = 4 as Protocol.DOM.NodeId;
const matchedStyles = await createMatchedStyles({
node,
matchedPayload: [ruleMatch('div', [{name: '--foo', value: 'foo1'}])], // styleFoo1
inheritedPayload: [
{
matchedCSSRules: [ruleMatch('div', [{name: '--bar', value: 'bar'}, {name: '--foo', value: 'foo2'}])],
}, // styleFoo2
{matchedCSSRules: [ruleMatch('div', [{name: '--baz', value: 'baz'}])]}, // styleBaz
{matchedCSSRules: [ruleMatch('div', [{name: '--foo', value: 'foo3'}])]}, // styleFoo3
],
propertyRules: [{
origin: Protocol.CSS.StyleSheetOrigin.Regular,
style: {
cssProperties: [
{name: 'syntax', value: '*'},
{name: 'inherits', value: 'true'},
{name: 'initial-value', value: 'bar0'},
],
shorthandEntries: [],
},
propertyName: {text: '--bar'},
}],
});
// Compute the variable value as it is visible to `startingCascade` and compare with the expectation
const testComputedVariableValueEquals =
(name: string, startingCascade: SDK.CSSStyleDeclaration.CSSStyleDeclaration, expectedValue: string,
expectedDeclaration: SDK.CSSProperty.CSSProperty|SDK.CSSMatchedStyles.CSSRegisteredProperty) => {
const {value, declaration} = matchedStyles.computeCSSVariable(startingCascade, name)!;
assert.strictEqual(value, expectedValue);
assert.strictEqual(declaration.declaration, expectedDeclaration);
};
const styles = matchedStyles.nodeStyles();
const styleFoo1 = styles.find(style => style.allProperties().find(p => p.value === 'foo1'));
const styleFoo2 = styles.find(style => style.allProperties().find(p => p.value === 'foo2'));
const styleFoo3 = styles.find(style => style.allProperties().find(p => p.value === 'foo3'));
const styleBaz = styles.find(style => style.allProperties().find(p => p.value === 'baz'));
assert.exists(styleFoo1);
assert.exists(styleFoo2);
assert.exists(styleFoo3);
assert.exists(styleBaz);
testComputedVariableValueEquals('--foo', styleFoo1, 'foo1', styleFoo1.leadingProperties()[0]);
testComputedVariableValueEquals('--bar', styleFoo1, 'bar', styleFoo2.leadingProperties()[0]);
testComputedVariableValueEquals('--foo', styleFoo2, 'foo2', styleFoo2.leadingProperties()[1]);
testComputedVariableValueEquals('--bar', styleFoo3, 'bar0', matchedStyles.registeredProperties()[0]);
testComputedVariableValueEquals('--foo', styleBaz, 'foo3', styleFoo3.leadingProperties()[0]);
});
describe('cyclic references', () => {
it('should return `null` when the variable references itself', async () => {
await testCssValueEquals('--itself', null);
await testCssValueEquals('--itself-complex', null);
});
it('should return `null` when there is a simple cycle (1->2->1)', async () => {
await testCssValueEquals('--cycle-1', null);
});
it('should return `null` if the var reference is inside the cycle', async () => {
await testCssValueEquals('--cycle-a', null);
});
it('should return fallback value if the expression is not inside the cycle', async () => {
await testCssValueEquals('--out-of-cycle', '20px');
});
});
describe('var references inside fallback', () => {
it('should resolve a `var()` reference inside fallback value too', async () => {
await testCssValueEquals('--var-reference-in-fallback', 'active-foo');
});
it('should return null when the fallback value contains a cyclic reference', async () => {
await testCssValueEquals('--cycle-in-fallback', null);
});
it('should return null when the fallback value is non existent too', async () => {
await testCssValueEquals('--non-existent-fallback', null);
});
});
it('should resolve a `var()` reference with nothing else', async () => {
await testCssValueEquals('--a', 'a');
});
it('should resolve a `var()` reference until no `var()` references left', async () => {
await testCssValueEquals('--b', 'a');
});
it('should resolve to fallback if the referenced variable does not exist', async () => {
await testCssValueEquals('--valid-fallback', 'fallback-value');
});
it('should correctly resolve the `var()` reference for complex inheritance case', async () => {
await testCssValueEquals('--non-inherited', 'inherited and overloaded');
});
it('resolves vars with css keywords', async () => {
const node = sinon.createStubInstance(SDK.DOMModel.DOMNode);
node.id = 1 as Protocol.DOM.NodeId;
const parent = sinon.createStubInstance(SDK.DOMModel.DOMNode);
parent.id = 2 as Protocol.DOM.NodeId;
node.parentNode = parent;
const matchedStyles = await createMatchedStyles(
{
matchedPayload: [ruleMatch('div', [{name: '--color', value: 'inherit'}])],
inheritedPayload: [{matchedCSSRules: [ruleMatch('div', [{name: '--color', value: 'inherited-color'}])]}],
},
node);
assert.strictEqual(
matchedStyles.computeCSSVariable(matchedStyles.nodeStyles()[0], '--color')?.value, 'inherited-color');
});
it('correcty handles cycles', async () => {
async function compute(name: string, styleRules: string[], inheritedRules: string[][]) {
const ruleToRuleMatch = (rule: string, index: number) => ruleMatch(
`.${index}`,
rule.split(';')
.filter(decl => decl.trim())
.map(decl => decl.split(':'))
.map(decl => ({name: decl[0].trim(), value: decl.slice(1).join(':').trim()})));
const matchedPayload = styleRules.map(ruleToRuleMatch);
const inheritedPayload = inheritedRules.map(
ruleTexts => ({matchedCSSRules: ruleTexts.map((rule, i) => ruleToRuleMatch(rule, i + styleRules.length))}));
const matchedStyles = await createMatchedStyles({matchedPayload, inheritedPayload});
return matchedStyles.computeCSSVariable(matchedStyles.nodeStyles()[0], name)?.value ?? null;
}
const simpleCycle = `
--a: var(--b);
--b: var(--a);
`;
assert.isNull(await compute('--a', [simpleCycle], []));
assert.isNull(await compute('--b', [simpleCycle], []));
const cycleOnUnusedFallback = `
--a: 2;
--b: var(--a, var(--c));
--c: var(--b);
`;
assert.strictEqual(await compute('--a', [cycleOnUnusedFallback], []), '2');
assert.strictEqual(await compute('--b', [cycleOnUnusedFallback], []), '2');
assert.strictEqual(await compute('--c', [cycleOnUnusedFallback], []), '2');
const simpleCycleWithFallbacks = `
--a: var(--b, 1);
--b: var(--a, 2);
`;
assert.isNull(await compute('--a', [simpleCycleWithFallbacks], []));
assert.isNull(await compute('--b', [simpleCycleWithFallbacks], []));
const longerCycle = `
--a: var(--b);
--b: var(--c);
--c: var(--a);
`;
assert.isNull(await compute('--a', [longerCycle], []));
assert.isNull(await compute('--b', [longerCycle], []));
assert.isNull(await compute('--c', [longerCycle], []));
const longerCycleWithFallbacks = `
--a: var(--b, 2);
--b: var(--c, 3);
--c: var(--a, 4);
`;
assert.isNull(await compute('--a', [longerCycleWithFallbacks], []));
assert.isNull(await compute('--b', [longerCycleWithFallbacks], []));
assert.isNull(await compute('--c', [longerCycleWithFallbacks], []));
const pointingIntoCycle = `
${longerCycle}
--d: var(--a);
--e: var(--b);
`;
assert.isNull(await compute('--a', [pointingIntoCycle], []));
assert.isNull(await compute('--b', [pointingIntoCycle], []));
assert.isNull(await compute('--c', [pointingIntoCycle], []));
assert.isNull(await compute('--d', [pointingIntoCycle], []));
assert.isNull(await compute('--e', [pointingIntoCycle], []));
const pointingIntoCycleWithFallback = `
${longerCycle}
--d: var(--a, 4);
--e: var(--b, 5);
`;
assert.isNull(await compute('--a', [pointingIntoCycleWithFallback], []));
assert.isNull(await compute('--b', [pointingIntoCycleWithFallback], []));
assert.isNull(await compute('--c', [pointingIntoCycleWithFallback], []));
assert.strictEqual(await compute('--d', [pointingIntoCycleWithFallback], []), '4');
assert.strictEqual(await compute('--e', [pointingIntoCycleWithFallback], []), '5');
const multipleEdges = `
--a: var(--b);
--b: var(--c) var(--d);
--c: var(--a) var(--b);
--d: var(--c);
`;
assert.isNull(await compute('--a', [multipleEdges], []));
assert.isNull(await compute('--b', [multipleEdges], []));
assert.isNull(await compute('--c', [multipleEdges], []));
assert.isNull(await compute('--d', [multipleEdges], []));
const pointingIntoMultipleEdgeCycle = `
${multipleEdges}
--e: var(--c) var(--d);
`;
assert.isNull(await compute('--a', [pointingIntoMultipleEdgeCycle], []));
assert.isNull(await compute('--b', [pointingIntoMultipleEdgeCycle], []));
assert.isNull(await compute('--c', [pointingIntoMultipleEdgeCycle], []));
assert.isNull(await compute('--d', [pointingIntoMultipleEdgeCycle], []));
assert.isNull(await compute('--e', [pointingIntoMultipleEdgeCycle], []));
const pointingIntoMultipleEdgeCycleWithFallback = `
${multipleEdges}
--e: var(--c, 4) var(--d, 5);
`;
assert.isNull(await compute('--a', [pointingIntoMultipleEdgeCycleWithFallback], []));
assert.isNull(await compute('--b', [pointingIntoMultipleEdgeCycleWithFallback], []));
assert.isNull(await compute('--c', [pointingIntoMultipleEdgeCycleWithFallback], []));
assert.isNull(await compute('--d', [pointingIntoMultipleEdgeCycleWithFallback], []));
assert.strictEqual(await compute('--e', [pointingIntoMultipleEdgeCycleWithFallback], []), '4 5');
const multipleCyclesWithFallback = `
${longerCycle}
--d: var(--e);
--e: var(--f);
--f: var(--d);
--g: var(--a, var(--d, 5));
`;
assert.isNull(await compute('--a', [multipleCyclesWithFallback], []));
assert.isNull(await compute('--b', [multipleCyclesWithFallback], []));
assert.isNull(await compute('--c', [multipleCyclesWithFallback], []));
assert.isNull(await compute('--d', [multipleCyclesWithFallback], []));
assert.isNull(await compute('--e', [multipleCyclesWithFallback], []));
assert.isNull(await compute('--f', [multipleCyclesWithFallback], []));
assert.strictEqual(await compute('--g', [multipleCyclesWithFallback], []), '5');
const notACycle = `
--a: var(--b, 1);
`;
const inherited = `
--a: var(--b);
--b: var(--a);
`;
assert.strictEqual(await compute('--a', [notACycle], [[inherited]]), '1');
assert.isNull(await compute('--b', [notACycle], [[inherited]]));
});
});
it('does not hide inherited rules that also apply directly to the node if it contains custom properties',
async () => {
const parentNode = sinon.createStubInstance(SDK.DOMModel.DOMNode);
parentNode.id = 0 as Protocol.DOM.NodeId;
const node = sinon.createStubInstance(SDK.DOMModel.DOMNode);
node.parentNode = parentNode;
node.id = 1 as Protocol.DOM.NodeId;
const startColumn = 0, endColumn = 1;
const matchedPayload = [
ruleMatch('body', [{name: '--var', value: 'blue'}], {startLine: 0, startColumn, endLine: 0, endColumn}),
ruleMatch('*', [{name: 'color', value: 'var(--var)'}], {startLine: 1, startColumn, endLine: 1, endColumn}),
ruleMatch('*', [{name: '--var', value: 'red'}], {startLine: 2, startColumn, endLine: 2, endColumn}),
];
const inheritedPayload = [{matchedCSSRules: matchedPayload.slice(1)}];
const matchedStyles = await createMatchedStyles({
node,
matchedPayload,
inheritedPayload,
});
assert.deepEqual(matchedStyles.nodeStyles().map(style => style.allProperties().map(prop => prop.propertyText)), [
['--var: red;'],
['color: var(--var);'],
['--var: blue;'],
['--var: red;'],
]);
});
describe('resolveGlobalKeyword', () => {
const inheritedPayload: Protocol.CSS.InheritedStyleEntry[] = [{
matchedCSSRules: [ruleMatch(
'.parent',
[
{name: 'color', value: 'color-inherited'},
{name: 'border', value: 'border-inherited'},
{name: '--inherited-is-inherited', value: 'inherited-is-inherited'},
{name: '--non-inherited-is-inherited', value: 'non-inherited-is-inherited'},
{name: '--unregistered-is-inherited', value: 'unregistered-is-inherited'},
])],
}];
const cssPropertyRegistrations: Protocol.CSS.CSSPropertyRegistration[] = [
{
propertyName: '--inherited-is-inherited',
syntax: '"<color>"',
initialValue: {text: 'inherited-is-inherited-initial'},
inherits: true,
},
{
propertyName: '--inherited-is-not-inherited',
syntax: '"<color>"',
initialValue: {text: 'inherited-is-not-inherited-initial'},
inherits: true,
},
{
propertyName: '--non-inherited-is-inherited',
syntax: '"<color>"',
initialValue: {text: 'non-inherited-is-inherited-initial'},
inherits: false,
},
{
propertyName: '--non-inherited-is-not-inherited',
syntax: '"<color>"',
initialValue: {text: 'non-inherited-is-not-inherited-initial'},
inherits: false,
},
];
function checkResolution(
matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles,
properties: Array<Protocol.CSS.CSSProperty&{expectedValue?: string}>): void {
const ownProperties = new Map(matchedStyles.nodeStyles()
.find(style => style.type === SDK.CSSStyleDeclaration.Type.Regular)
?.allProperties()
.map(property => [property.name, property]));
for (const {name: propertyName, expectedValue} of properties) {
const property = ownProperties.get(propertyName);
assert.isOk(property);
let resolvedValue: SDK.CSSMatchedStyles.CSSValueSource|null = new SDK.CSSMatchedStyles.CSSValueSource(property);
while (resolvedValue?.value && SDK.CSSMetadata.CSSMetadata.isCSSWideKeyword(resolvedValue?.value)) {
const {declaration, value} = resolvedValue;
if (!(declaration instanceof SDK.CSSProperty.CSSProperty)) {
break;
}
resolvedValue = matchedStyles.resolveGlobalKeyword(declaration, value);
}
assert.strictEqual(resolvedValue?.value, expectedValue, propertyName);
}
}
let node: sinon.SinonStubbedInstance<SDK.DOMModel.DOMNode>;
beforeEach(() => {
node = sinon.createStubInstance(SDK.DOMModel.DOMNode);
node.id = 1 as Protocol.DOM.NodeId;
node.nodeType.returns(Node.ELEMENT_NODE);
const parent = sinon.createStubInstance(SDK.DOMModel.DOMNode);
parent.id = 2 as Protocol.DOM.NodeId;
parent.nodeType.returns(Node.ELEMENT_NODE);
node.parentNode = parent;
});
it('correctly resolves the keyword `unset`', async () => {
const properties = [
// Value is undefined
{name: 'background-color', value: 'unset', expectedValue: undefined},
// Property inherits, Value is inherited
{name: 'color', value: 'unset', expectedValue: 'color-inherited'},
// Property inherits, Value is not inherited
{name: 'font-size', value: 'unset', expectedValue: undefined},
// Value is not inherited
{name: 'border', value: 'unset', expectedValue: undefined},
// Value is not inherited
{name: 'margin', value: 'unset', expectedValue: undefined},
// Property inherits, Value is inherited
{name: '--inherited-is-inherited', value: 'unset', expectedValue: 'inherited-is-inherited'},
// Property inherits, Value is not inherited, so fall back to initial
{name: '--inherited-is-not-inherited', value: 'unset', expectedValue: 'inherited-is-not-inherited-initial'},
// Value is not inherited, so fall back to initial
{name: '--non-inherited-is-inherited', value: 'unset', expectedValue: 'non-inherited-is-inherited-initial'},
// Value is not inherited, so fall back to initial
{
name: '--non-inherited-is-not-inherited',
value: 'unset',
expectedValue: 'non-inherited-is-not-inherited-initial',
},
// Value is inherited
{name: '--unregistered-is-inherited', value: 'unset', expectedValue: 'unregistered-is-inherited'},
// Value is undefined
{name: '--unregistered-is-not-inherited', value: 'unset', expectedValue: undefined},
];
const matchedStyles = await createMatchedStyles(
{matchedPayload: [ruleMatch('div', properties)], inheritedPayload, cssPropertyRegistrations}, node);
checkResolution(matchedStyles, properties);
});
it('correctly resolves the keyword `inherits`', async () => {
const properties = [
// Value is undefined
{name: 'background-color', value: 'inherit', expectedValue: undefined},
// Property inherits, Value is inherited
{name: 'color', value: 'inherit', expectedValue: 'color-inherited'},
// Property inherits, Value is not inherited
{name: 'font-size', value: 'inherit', expectedValue: undefined},
// Property doesn't inherit, but Value is inherited
{name: 'border', value: 'inherit', expectedValue: 'border-inherited'},
// Value is not inherited
{name: 'margin', value: 'inherit', expectedValue: undefined},
// Property inherits, Value is inherited
{name: '--inherited-is-inherited', value: 'inherit', expectedValue: 'inherited-is-inherited'},
// Property inherits, Value is not inherited, so fall back to initial
{name: '--inherited-is-not-inherited', value: 'inherit', expectedValue: 'inherited-is-not-inherited-initial'},
// Property doesn't inherit, but Value is inherited
{name: '--non-inherited-is-inherited', value: 'inherit', expectedValue: 'non-inherited-is-inherited'},
// Value is not inherited, so fall back to initial
{
name: '--non-inherited-is-not-inherited',
value: 'inherit',
expectedValue: 'non-inherited-is-not-inherited-initial',
},
// Value is inherited
{name: '--unregistered-is-inherited', value: 'inherit', expectedValue: 'unregistered-is-inherited'},
// Value is undefined
{name: '--unregistered-is-not-inherited', value: 'inherit', expectedValue: undefined},
];
const matchedStyles = await createMatchedStyles(
{matchedPayload: [ruleMatch('div', properties)], inheritedPayload, cssPropertyRegistrations}, node);
checkResolution(matchedStyles, properties);
});
it('correctly resolves the keyword `initial`', async () => {
const properties = [
// Value is undefined
{name: 'background-color', value: 'initial', expectedValue: undefined},
// Property inherits, Value is inherited
{name: 'color', value: 'initial', expectedValue: undefined},
// Property inherits, Value is not inherited
{name: 'font-size', value: 'initial', expectedValue: undefined},
// Property doesn't inherit
{name: 'border', value: 'initial', expectedValue: undefined},
// Value is not inherited
{name: 'margin', value: 'initial', expectedValue: undefined},
// Property inherits, Value is inherited
{name: '--inherited-is-inherited', value: 'initial', expectedValue: 'inherited-is-inherited-initial'},
// Property inherits, Value is not inherited
{name: '--inherited-is-not-inherited', value: 'initial', expectedValue: 'inherited-is-not-inherited-initial'},
// Value is not inherited
{name: '--non-inherited-is-inherited', value: 'initial', expectedValue: 'non-inherited-is-inherited-initial'},
// Value is not inherited
{
name: '--non-inherited-is-not-inherited',
value: 'initial',
expectedValue: 'non-inherited-is-not-inherited-initial',
},
// Value is inherited
{name: '--unregistered-is-inherited', value: 'initial', expectedValue: undefined},
// Value is undefined
{name: '--unregistered-is-not-inherited', value: 'initial', expectedValue: undefined},
];
const matchedStyles = await createMatchedStyles(
{matchedPayload: [ruleMatch('div', properties)], inheritedPayload, cssPropertyRegistrations}, node);
checkResolution(matchedStyles, properties);
});
it('correctly resolves the keyword `revert`', async () => {
const properties = [
// authored -> user
{name: 'font-variant', value: 'revert', expectedValue: 'user-font-variant'},
// authored -> user -> ua
{name: 'font-size', value: 'revert', expectedValue: 'ua-font-size'},
// authored -> ua
{name: 'font-weight', value: 'revert', expectedValue: 'ua-font-weight'},
// authored -> user -> ua -> void
{name: 'font-family', value: 'revert', expectedValue: undefined},
// authored -> inherited
{name: 'color', value: 'revert', expectedValue: 'color-inherited'},
{name: '--inherited-is-inherited', value: 'revert', expectedValue: 'inherited-is-inherited'},
// authored -> initial
{name: '--inherited-is-not-inherited', value: 'revert', expectedValue: 'inherited-is-not-inherited-initial'},
{name: '--non-inherited-is-inherited', value: 'revert', expectedValue: 'non-inherited-is-inherited-initial'},
{
name: '--non-inherited-is-not-inherited',
value: 'revert',
expectedValue: 'non-inherited-is-not-inherited-initial',
},
];
const userRule = ruleMatch('div', [
{name: 'font-variant', value: 'user-font-variant'},
{name: 'font-size', value: 'revert'},
{name: 'font-family', value: 'revert'},
]);
userRule.rule.origin = Protocol.CSS.StyleSheetOrigin.Injected;
const uaRule = ruleMatch('div', [
{name: 'font-variant', value: 'ua-font-variant'},
{name: 'font-size', value: 'ua-font-size'},
{name: 'font-weight', value: 'ua-font-weight'},
{name: 'font-family', value: 'revert'},
]);
uaRule.rule.origin = Protocol.CSS.StyleSheetOrigin.UserAgent;
const matchedStyles = await createMatchedStyles(
{
matchedPayload: [uaRule, userRule, ruleMatch('div', properties)],
inheritedPayload,
cssPropertyRegistrations,
},
node);
checkResolution(matchedStyles, properties);
});
it('correctly resolves the keyword `revert-layer`', async () => {
const inlinePayload = ruleMatch('', [{name: '--element-attached', value: 'revert-layer'}]).rule.style;
const properties = [
{name: '--element-attached', value: 'author-origin', expectedValue: 'author-origin'},
{name: 'font-family', value: 'revert-layer', expectedValue: 'next-layer'},
{name: 'font-weight', value: 'revert-layer', expectedValue: 'one-more-layer'},
// Value is undefined
{name: 'background-color', value: 'revert-layer', expectedValue: undefined},
// Property inherits, Value is inherited
{name: 'color', value: 'revert-layer', expectedValue: 'color-inherited'},
// Property inherits, Value is not inherited
{name: 'font-size', value: 'revert-layer', expectedValue: undefined},
// Property doesn't inherit
{name: 'border', value: 'revert-layer', expectedValue: undefined},
// Value is not inherited
{name: 'margin', value: 'revert-layer', expectedValue: undefined},
// Property inherits, Value is inherited
{name: '--inherited-is-inherited', value: 'revert-layer', expectedValue: 'inherited-is-inherited'},
// Property inherits, Value is not inherited
{
name: '--inherited-is-not-inherited',
value: 'revert-layer',
expectedValue: 'inherited-is-not-inherited-initial',
},
// Value is not inherited
{
name: '--non-inherited-is-inherited',
value: 'revert-layer',
expectedValue: 'non-inherited-is-inherited-initial',
},
// Value is not inherited
{
name: '--non-inherited-is-not-inherited',
value: 'revert-layer',
expectedValue: 'non-inherited-is-not-inherited-initial',
},
// Value is inherited
{name: '--unregistered-is-inherited', value: 'revert-layer', expectedValue: 'unregistered-is-inherited'},
// Value is undefined
{name: '--unregistered-is-not-inherited', value: 'revert-layer', expectedValue: undefined},
];
const mainRule = ruleMatch('div', properties);
mainRule.rule.layers = [{text: 'layer1'}];
const sameLayer = ruleMatch('div', [{name: 'font-family', value: 'same-layer'}]);
sameLayer.rule.layers = mainRule.rule.layers;
const nextLayer = ruleMatch('div', [{name: 'font-family', value: 'next-layer'}]);
nextLayer.rule.layers = [{text: 'layer2'}];
const oneMoreLayer = ruleMatch('div', [{name: 'font-weight', value: 'one-more-layer'}]);
oneMoreLayer.rule.layers = [{text: 'outer'}, {text: 'layer3'}];
const uaRule = ruleMatch('div', [{name: 'font-variant', value: 'ua-font-variant'}]);
uaRule.rule.origin = Protocol.CSS.StyleSheetOrigin.UserAgent;
const matchedStyles = await createMatchedStyles(
{
inlinePayload,
matchedPayload: [uaRule, oneMoreLayer, nextLayer, sameLayer, mainRule],
inheritedPayload,
cssPropertyRegistrations,
},
node);
checkResolution(matchedStyles, properties);
// Check for inline style
const inlineProperty = matchedStyles.nodeStyles()
.find(style => style.type === SDK.CSSStyleDeclaration.Type.Inline)
?.allProperties()
?.find(property => property.name === '--element-attached');
assert.isOk(inlineProperty);
const resolved = matchedStyles.resolveGlobalKeyword(inlineProperty, SDK.CSSMetadata.CSSWideKeyword.REVERT_LAYER);
assert.strictEqual(resolved?.value, 'author-origin');
});
});
});
describeWithMockConnection('NodeCascade', () => {
it('correctly marks custom properties as Overloaded if they are registered as inherits: false', async () => {
const target = createTarget();
const cssModel = new SDK.CSSModel.CSSModel(target);
const parentNode = sinon.createStubInstance(SDK.DOMModel.DOMNode);
parentNode.id = 0 as Protocol.DOM.NodeId;
const node = sinon.createStubInstance(SDK.DOMModel.DOMNode);
node.parentNode = parentNode;
node.id = 1 as Protocol.DOM.NodeId;
const inheritablePropertyPayload: Protocol.CSS.CSSProperty = {name: '--inheritable', value: 'green'};
const nonInheritablePropertyPayload: Protocol.CSS.CSSProperty = {name: '--non-inheritable', value: 'green'};
const matchedCSSRules: Protocol.CSS.RuleMatch[] = [{
matchingSelectors: [0],
rule: {
selectorList: {selectors: [{text: 'div'}], text: 'div'},
origin: Protocol.CSS.StyleSheetOrigin.Regular,
style: {
cssProperties: [inheritablePropertyPayload, nonInheritablePropertyPayload],
shorthandEntries: [],
},
},
}];
const cssPropertyRegistrations = [
{
propertyName: inheritablePropertyPayload.name,
initialValue: {text: 'blue'},
inherits: true,
syntax: '<color>',
},
{
propertyName: nonInheritablePropertyPayload.name,
initialValue: {text: 'red'},
inherits: false,
syntax: '<color>',
},
];
const matchedStyles = await createMatchedStyles({
cssModel,
node,
matchedPayload: [
ruleMatch('div', []),
],
inheritedPayload: [{matchedCSSRules}],
cssPropertyRegistrations,
});
const style = matchedStyles.nodeStyles()[1];
const [inheritableProperty, nonInheritableProperty] = style.allProperties();
assert.strictEqual(
matchedStyles.propertyState(nonInheritableProperty), SDK.CSSMatchedStyles.PropertyState.OVERLOADED);
assert.strictEqual(matchedStyles.propertyState(inheritableProperty), SDK.CSSMatchedStyles.PropertyState.ACTIVE);
});
it('correctly computes active properties for nested at-rules', async () => {
const outerRule = ruleMatch('a', [{name: 'color', value: 'var(--inner)'}]);
const nestedRule = ruleMatch('&', [{name: '--inner', value: 'red'}]);
nestedRule.rule.nestingSelectors = ['a'];
nestedRule.rule.selectorList = {selectors: [], text: '&'};
nestedRule.rule.supports = [{
text: '(--var:s)',
active: true,
styleSheetId: nestedRule.rule.styleSheetId,
}];
const matchedStyles = await createMatchedStyles({
matchedPayload: [outerRule, nestedRule],
});
assert.deepEqual(matchedStyles.availableCSSVariables(matchedStyles.nodeStyles()[0]), ['--inner']);
});
});