UNPKG

chrome-devtools-frontend

Version:
779 lines (720 loc) • 35.8 kB
// 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']); }); });