| // Copyright 2022 The Chromium Authors |
| // 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 {renderElementIntoDOM} from '../../testing/DOMHelpers.js'; |
| import {createTarget} from '../../testing/EnvironmentHelpers.js'; |
| import {describeWithMockConnection, setMockConnectionResponseHandler} from '../../testing/MockConnection.js'; |
| import {getMatchedStyles, ruleMatch} from '../../testing/StyleHelpers.js'; |
| |
| import * as SDK from './sdk.js'; |
| |
| describe('CSSMatchedStyles', () => { |
| describe('computeCSSVariable', () => { |
| const testCssValueEquals = async (text: string, expectedValue: unknown) => { |
| const matchedStyles = await getMatchedStyles({ |
| 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 getMatchedStyles({ |
| 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]); |
| }); |
| |
| it('correctly resolves highlight properties', async () => { |
| const highlightNode = sinon.createStubInstance(SDK.DOMModel.DOMNode); |
| const node = sinon.createStubInstance(SDK.DOMModel.DOMNode); |
| const parent = sinon.createStubInstance(SDK.DOMModel.DOMNode); |
| const parentHighlight = sinon.createStubInstance(SDK.DOMModel.DOMNode); |
| node.id = 1 as Protocol.DOM.NodeId; |
| node.parentNode = parent; |
| parent.id = 2 as Protocol.DOM.NodeId; |
| highlightNode.id = 3 as Protocol.DOM.NodeId; |
| highlightNode.parentNode = node; |
| parentHighlight.id = 4 as Protocol.DOM.NodeId; |
| parentHighlight.parentNode = parent; |
| node.pseudoElements.returns(new Map([[Protocol.DOM.PseudoType.Highlight, [highlightNode]]])); |
| parent.pseudoElements.returns(new Map([[Protocol.DOM.PseudoType.Highlight, [parentHighlight]]])); |
| |
| const matchedStyles = await getMatchedStyles({ |
| node, |
| matchedPayload: [ruleMatch('div.b', [{name: '--color', value: 'green'}])], |
| inheritedPayload: [ |
| { |
| matchedCSSRules: |
| [ruleMatch('div.a', [{name: '--color', value: 'yellow'}])], |
| }, |
| ], |
| pseudoPayload: [ |
| { |
| pseudoType: Protocol.DOM.PseudoType.Highlight, |
| pseudoIdentifier: 'highlight-foo', |
| matches: [ruleMatch('.b::highlight(highlight-foo)', [{name: '--text-color', value: 'var(--color)'}])], |
| }, |
| ], |
| inheritedPseudoPayload: [{ |
| pseudoElements: [{ |
| pseudoType: Protocol.DOM.PseudoType.Highlight, |
| pseudoIdentifier: 'highlight-foo', |
| matches: [ruleMatch( |
| '.a::highlight(highlight-foo)', |
| [{name: '--color', value: 'blue'},])], |
| }] |
| }] |
| }); |
| |
| // 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) => { |
| const {value} = matchedStyles.computeCSSVariable(startingCascade, name)!; |
| assert.strictEqual(value, expectedValue); |
| }; |
| |
| const styles = matchedStyles.customHighlightPseudoStyles('highlight-foo'); |
| assert.lengthOf(styles, 2); |
| const highlightStyle = styles[0]; |
| testComputedVariableValueEquals('--text-color', highlightStyle, 'green'); |
| }); |
| |
| it('correctly resolves variables in pseudo elements', async () => { |
| const afterNode = sinon.createStubInstance(SDK.DOMModel.DOMNode); |
| const node = sinon.createStubInstance(SDK.DOMModel.DOMNode); |
| node.id = 1 as Protocol.DOM.NodeId; |
| afterNode.id = 2 as Protocol.DOM.NodeId; |
| afterNode.parentNode = node; |
| node.pseudoElements.returns(new Map([[Protocol.DOM.PseudoType.After, [afterNode]]])); |
| |
| const matchedStyles = await getMatchedStyles({ |
| node, |
| matchedPayload: [ruleMatch('div.b', [{name: '--color', value: 'green'}, {name: '--bg-color', value: 'red'}])], |
| pseudoPayload: [ |
| { |
| pseudoType: Protocol.DOM.PseudoType.After, |
| matches: [ruleMatch( |
| '.b::after', |
| [ |
| {name: '--color', value: 'blue'}, |
| {name: '--color2', value: 'var(--color)'}, |
| {name: '--color3', value: 'var(--bg-color)'}, |
| ])], |
| }, |
| ], |
| }); |
| |
| // 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) => { |
| const {value} = matchedStyles.computeCSSVariable(startingCascade, name)!; |
| assert.strictEqual(value, expectedValue); |
| }; |
| |
| const styles = matchedStyles.pseudoStyles(Protocol.DOM.PseudoType.After); |
| assert.lengthOf(styles, 1); |
| const pseudoStyle = styles[0]; |
| testComputedVariableValueEquals('--color', pseudoStyle, 'blue'); |
| testComputedVariableValueEquals('--color2', pseudoStyle, 'blue'); |
| testComputedVariableValueEquals('--color3', pseudoStyle, 'red'); |
| }); |
| |
| 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 getMatchedStyles({ |
| 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 getMatchedStyles({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'}], {range: {startLine: 0, startColumn, endLine: 0, endColumn}}), |
| ruleMatch( |
| '*', [{name: 'color', value: 'var(--var)'}], {range: {startLine: 1, startColumn, endLine: 1, endColumn}}), |
| ruleMatch('*', [{name: '--var', value: 'red'}], {range: {startLine: 2, startColumn, endLine: 2, endColumn}}), |
| ]; |
| const inheritedPayload = [{matchedCSSRules: matchedPayload.slice(1)}]; |
| const matchedStyles = await getMatchedStyles({ |
| 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 getMatchedStyles( |
| {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 getMatchedStyles( |
| {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 getMatchedStyles( |
| {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 getMatchedStyles({ |
| 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 getMatchedStyles({ |
| 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'); |
| }); |
| }); |
| |
| it('can correctly resolve properties by name', async () => { |
| const 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; |
| const matchedPayload = [ |
| // Property is found in the same rule |
| ruleMatch('div', {'--a': 'A', '--b': 'B'}), |
| // ... in a sibling rule |
| ruleMatch('div', {'--c': 'C', '--d': 'D'}), |
| ]; |
| const inheritedPayload = [{ |
| matchedCSSRules: [ |
| // ... in an inherited rule |
| ruleMatch('div', {'--e': 'E'}), |
| ruleMatch('div', {'--f': 'F'}), |
| ruleMatch('div', {'background-color': 'x'}), |
| ruleMatch('div', {color: 'y'}), |
| ] |
| }]; |
| const matchedStyles = await getMatchedStyles({node, matchedPayload, inheritedPayload}); |
| |
| const styles = matchedStyles.nodeStyles(); |
| assert.lengthOf(styles, 5); |
| assert.isNull(matchedStyles.resolveProperty('--z', styles[0])); |
| assert.strictEqual(matchedStyles.resolveProperty('--a', styles[0])?.value, 'A'); |
| assert.strictEqual(matchedStyles.resolveProperty('--b', styles[0])?.value, 'B'); |
| assert.strictEqual(matchedStyles.resolveProperty('--a', styles[1])?.value, 'A'); |
| assert.strictEqual(matchedStyles.resolveProperty('--b', styles[1])?.value, 'B'); |
| assert.strictEqual(matchedStyles.resolveProperty('--e', styles[0])?.value, 'E'); |
| assert.strictEqual(matchedStyles.resolveProperty('--f', styles[0])?.value, 'F'); |
| assert.strictEqual(matchedStyles.resolveProperty('--e', styles[1])?.value, 'E'); |
| assert.strictEqual(matchedStyles.resolveProperty('--f', styles[1])?.value, 'F'); |
| assert.strictEqual(matchedStyles.resolveProperty('color', styles[0])?.value, 'y'); |
| assert.isNull(matchedStyles.resolveProperty('background-color', styles[0])); |
| }); |
| |
| it('reads attributes from the parent node of a pseudo-element', async () => { |
| const node = sinon.createStubInstance(SDK.DOMModel.DOMNode); |
| node.id = 1 as Protocol.DOM.NodeId; |
| node.nodeType.returns(Node.ELEMENT_NODE); |
| node.getAttribute.callsFake((name: string) => `parent-${name}`); |
| const pseudoElement = sinon.createStubInstance(SDK.DOMModel.DOMNode); |
| pseudoElement.id = 2 as Protocol.DOM.NodeId; |
| pseudoElement.nodeType.returns(Node.ELEMENT_NODE); |
| pseudoElement.parentNode = node; |
| pseudoElement.pseudoType.returns('before'); |
| const matchedStyles = await getMatchedStyles({ |
| matchedPayload: [ruleMatch('div::before', {content: 'attr(data-content)'})], |
| node: pseudoElement, |
| }); |
| |
| const property = matchedStyles.rawAttributeValueFromStyle(matchedStyles.nodeStyles()[0], 'data-content'); |
| assert.strictEqual(property, 'parent-data-content'); |
| }); |
| |
| it('evaluates variables with attr() calls in the same way as blink', async () => { |
| const attributes = [ |
| {name: 'data-test-nonexistent', value: 'attr(data-nonexistent)'}, |
| {name: 'data-test-nonexistent-raw-string', value: 'attr(data-nonexistent raw-string)'}, |
| {name: 'data-test-empty', value: ''}, |
| {name: 'data-test-self-loop', value: 'attr(data-test-self-loop)'}, |
| {name: 'data-test-loop-1', value: 'attr(data-test-loop-2 type(<length>), 1px)'}, |
| {name: 'data-test-loop-2', value: 'var(--test-loop-1, 2px)'}, |
| {name: 'data-test-loop-indirect-2', value: 'attr(data-test-loop-1 type(<length>), 6px)'}, |
| {name: 'data-test-number', value: '70'}, |
| {name: 'data-test-length', value: 'attr(data-test-number in)'}, |
| {name: 'data-test-bad-unit', value: 'attr(data-test-number parsecs, 2px)'}, |
| {name: 'data-test-bad-type', value: 'attr(data-test-number type(<nomber>), 2)'}, |
| {name: 'data-cant-parse', value: 'attr('}, |
| ]; |
| const variables = [ |
| {name: '--test-missing', value: 'attr(data-nonexistent)'}, |
| {name: '--test-missing-fallback', value: 'attr(data-nonexistent, 2px)'}, |
| {name: '--test-missing-var-indirect', value: 'var(--test-missing, 2px)'}, |
| {name: '--test-missing-attr-indirect-raw', value: 'attr(data-test-nonexistent, 2px)'}, |
| {name: '--test-missing-attr-indirect', value: 'attr(data-test-nonexistent type(*), 2px)'}, |
| {name: '--test-missing-raw-string', value: 'attr(data-nonexistent raw-string)'}, |
| {name: '--test-missing-raw-string-fallback', value: 'attr(data-nonexistent raw-string, 2px)'}, |
| {name: '--test-missing-raw-string-var-indirect', value: 'var(--test-missing-raw-string, 2px)'}, |
| {name: '--test-missing-raw-string-attr-indirect', value: 'attr(data-test-nonexistent-raw-string, 2px)'}, |
| {name: '--test-empty-any', value: 'attr(data-test-empty type(*))'}, |
| {name: '--test-empty-number', value: 'attr(data-test-empty type(<number>))'}, |
| {name: '--test-empty-any-fallback', value: 'attr(data-test-empty type(*), 2px)'}, |
| {name: '--test-empty-number-fallback', value: 'attr(data-test-empty type(<number>), 2px)'}, |
| {name: '--test-self-loop', value: 'attr(data-test-self-loop type(*))'}, |
| {name: '--test-self-loop-raw', value: 'attr(data-test-self-loop)'}, |
| {name: '--test-self-loop-fallback', value: 'attr(data-test-self-loop type(*), 2px)'}, |
| {name: '--test-self-loop-fallback-2', value: 'var(--test-self-loop-fallback, 4px)'}, |
| {name: '--test-self-loop-fallback-self', value: 'attr(data-test-self-loop type(*), attr(data-test-self-loop))'}, |
| {name: '--test-loop-1', value: 'var(--test-loop-2, 3px)'}, |
| {name: '--test-loop-2', value: 'attr(data-test-loop-1 type(<length>), 4px)'}, |
| {name: '--test-loop-indirect-1', value: 'attr(data-test-loop-1 type(<length>), 5px)'}, |
| {name: '--test-loop-indirect-2', value: 'attr(data-test-loop-indirect-2 type(<length>), 7px)'}, |
| {name: '--test-number', value: 'attr(data-test-number type(<number>))'}, |
| {name: '--test-number-to-length', value: 'attr(data-test-number in)'}, |
| { |
| name: '--test-number-as-length', |
| value: 'attr(data-test-number type(<length>), var(--test-self-loop-fallback-2))' |
| }, |
| {name: '--test-length', value: 'attr(data-test-length type(<length>))'}, |
| {name: '--test-bad-unit-indirect', value: 'attr(data-test-bad-unit type(*), 4px)'}, |
| {name: '--test-bad-type-indirect', value: 'attr(data-test-bad-type type(*), 4)'}, |
| {name: '--test-cant-parse', value: 'attr(data-cant-parse type(*), red)'}, |
| ]; |
| // Create an element |
| const element = document.createElement('div'); |
| for (const {name, value} of attributes) { |
| element.setAttribute(name, value); |
| } |
| for (const {name, value} of variables) { |
| element.style.setProperty(name, value); |
| } |
| |
| renderElementIntoDOM(element); |
| const computedProperties = element.computedStyleMap(); |
| |
| const node = sinon.createStubInstance(SDK.DOMModel.DOMNode); |
| node.id = 1 as Protocol.DOM.NodeId; |
| node.nodeType.returns(Node.ELEMENT_NODE); |
| // Create a sinon stub to return the requested attribute |
| node.getAttribute.callsFake((name: string) => attributes.find(attr => attr.name === name)?.value); |
| |
| const matchedStyles = await getMatchedStyles({ |
| matchedPayload: [ruleMatch('div', variables)], |
| node, |
| }); |
| |
| for (const {name} of variables) { |
| const frontendComputedValue = |
| matchedStyles.computeCSSVariable(matchedStyles.nodeStyles()[0], name)?.value ?? null; |
| const backendComputedValue = computedProperties.get(name) ?? null; |
| if (backendComputedValue === null) { |
| assert.isNull(frontendComputedValue, `evaluating variable ${name}`); |
| } else { |
| assert.strictEqual(frontendComputedValue, backendComputedValue.toString(), `evaluating variable ${name}`); |
| } |
| } |
| }); |
| }); |
| |
| describeWithMockConnection('NodeCascade', () => { |
| it('correctly marks custom properties as Overloaded if they are registered as inherits: false', async () => { |
| setMockConnectionResponseHandler( |
| 'CSS.getEnvironmentVariables', () => ({} as Protocol.CSS.GetEnvironmentVariablesResponse)); |
| 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 getMatchedStyles({ |
| 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 getMatchedStyles({ |
| matchedPayload: [outerRule, nestedRule], |
| }); |
| |
| assert.deepEqual(matchedStyles.availableCSSVariables(matchedStyles.nodeStyles()[0]), ['--inner']); |
| }); |
| |
| describe('isPropertyOverriddenByAnimation', () => { |
| it('returns true when a property is overridden by an animation', async () => { |
| const animationStyle = { |
| style: { |
| cssProperties: [{name: 'opacity', value: '1'}], |
| shorthandEntries: [], |
| }, |
| } as Protocol.CSS.CSSAnimationStyle; |
| |
| const matchedStyles = await getMatchedStyles({ |
| matchedPayload: [ruleMatch('div', [{name: 'opacity', value: '0.5'}])], |
| animationStylesPayload: [animationStyle], |
| }); |
| |
| const styles = matchedStyles.nodeStyles(); |
| const regularStyle = styles.find(style => style.type === SDK.CSSStyleDeclaration.Type.Regular); |
| assert.exists(regularStyle); |
| const property = regularStyle.allProperties().find(p => p.name === 'opacity'); |
| assert.exists(property); |
| |
| assert.isTrue(matchedStyles.isPropertyOverriddenByAnimation(property)); |
| }); |
| |
| it('returns true when a property is overridden by a transition', async () => { |
| const transitionStyle = { |
| cssProperties: [{name: 'opacity', value: '1'}], |
| shorthandEntries: [], |
| } as Protocol.CSS.CSSStyle; |
| |
| const matchedStyles = await getMatchedStyles({ |
| matchedPayload: [ruleMatch('div', [{name: 'opacity', value: '0.5'}])], |
| transitionsStylePayload: transitionStyle, |
| }); |
| |
| const styles = matchedStyles.nodeStyles(); |
| const regularStyle = styles.find(style => style.type === SDK.CSSStyleDeclaration.Type.Regular); |
| assert.exists(regularStyle); |
| const property = regularStyle.allProperties().find(p => p.name === 'opacity'); |
| assert.exists(property); |
| |
| assert.isTrue(matchedStyles.isPropertyOverriddenByAnimation(property)); |
| }); |
| |
| it('returns false when a property is overridden by another regular property', async () => { |
| const matchedStyles = await getMatchedStyles({ |
| matchedPayload: [ |
| ruleMatch('div', [{name: 'opacity', value: '0.5'}]), |
| ruleMatch('div.active', [{name: 'opacity', value: '1'}]), // Higher specificity |
| ], |
| }); |
| |
| const styles = matchedStyles.nodeStyles(); |
| const regularStyle = styles.find( |
| style => style.type === SDK.CSSStyleDeclaration.Type.Regular && |
| style.allProperties().find(p => p.value === '0.5')); |
| assert.exists(regularStyle); |
| const property = regularStyle.allProperties().find(p => p.name === 'opacity'); |
| assert.exists(property); |
| |
| assert.isFalse(matchedStyles.isPropertyOverriddenByAnimation(property)); |
| }); |
| }); |
| }); |