UNPKG

chrome-devtools-frontend

Version:
664 lines (602 loc) • 30.7 kB
// Copyright 2023 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 SDK from '../../core/sdk/sdk.js'; import {Printer} from '../../testing/PropertyParser.js'; import type * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js'; function matchSingleValue<T extends SDK.CSSPropertyParser.Match>( name: string, value: string, matcher: SDK.CSSPropertyParser.Matcher<T>): {ast: SDK.CSSPropertyParser.SyntaxTree|null, match: T|null, text: string} { const ast = SDK.CSSPropertyParser.tokenizeDeclaration(name, value); if (!ast) { return {ast, match: null, text: value}; } const matchedResult = SDK.CSSPropertyParser.BottomUpTreeMatching.walk(ast, [matcher]); const matchedNode = SDK.CSSPropertyParser.TreeSearch.find(ast, n => matchedResult.getMatch(n) instanceof matcher.matchType); const match = matchedNode && matchedResult.getMatch(matchedNode); return { ast, match: match instanceof matcher.matchType ? match : null, text: Printer.walk(ast).get(), }; } function injectVariableSubstitutions(variables: Record<string, string>) { const {getComputedText, getComputedTextRange, getMatch} = SDK.CSSPropertyParser.BottomUpTreeMatching.prototype; const variableNames = new Map<string, {varName: string, value: string}>(); function injectChunk(matching: SDK.CSSPropertyParser.BottomUpTreeMatching): void { if (matching.computedText.chunkCount === 0) { const propertyOffset = matching.ast.rule.indexOf(matching.ast.propertyName ?? '--'); assert.isAbove(propertyOffset, 0); for (const [varName, value] of Object.entries(variables)) { const varText = `var(${varName})`; for (let offset = matching.ast.rule.indexOf(varText); offset >= 0; offset = matching.ast.rule.indexOf(varText, offset + 1)) { matching.computedText.push( {text: varText, computedText: () => value, node: {} as CodeMirror.SyntaxNode}, offset - propertyOffset); } variableNames.set(varText, {varName, value}); } } } sinon.stub(SDK.CSSPropertyParser.BottomUpTreeMatching.prototype, 'getComputedText') .callsFake(function(this: SDK.CSSPropertyParser.BottomUpTreeMatching, node: CodeMirror.SyntaxNode): string { injectChunk(this); return getComputedText.call(this, node); }); sinon.stub(SDK.CSSPropertyParser.BottomUpTreeMatching.prototype, 'getComputedTextRange') .callsFake(function( this: SDK.CSSPropertyParser.BottomUpTreeMatching, from: CodeMirror.SyntaxNode, to: CodeMirror.SyntaxNode): string { injectChunk(this); return getComputedTextRange.call(this, from, to); }); sinon.stub(SDK.CSSPropertyParser.BottomUpTreeMatching.prototype, 'getMatch') .callsFake(function(this: SDK.CSSPropertyParser.BottomUpTreeMatching, node: CodeMirror.SyntaxNode): SDK.CSSPropertyParser.Match|undefined { injectChunk(this); const resolvedValue = variableNames.get(this.ast.text(node)); if (!resolvedValue) { return getMatch.call(this, node); } return new SDK.CSSPropertyParser.VariableMatch( this.ast.text(node), node, resolvedValue.varName, [], this, () => resolvedValue.value); }); } describe('Matchers for SDK.CSSPropertyParser.BottomUpTreeMatching', () => { it('parses colors', () => { for (const fail of ['red-blue', '#f', '#foobar', '', 'rgbz(1 2 2)', 'tan(45deg)']) { const {match, text} = matchSingleValue('color', fail, new SDK.CSSPropertyParserMatchers.ColorMatcher()); assert.isNull(match, text); } for (const succeed of ['rgb(/* R */155, /* G */51, /* B */255)', 'red', 'rgb(0 0 0)', 'rgba(0 0 0)', '#fff', '#ffff', '#ffffff', '#ffffffff']) { const {match, text} = matchSingleValue('color', succeed, new SDK.CSSPropertyParserMatchers.ColorMatcher()); assert.exists(match, text); assert.strictEqual(match.text, succeed); } // The property name matters: for (const fail of ['rgb(/* R */155, /* G */51, /* B */255)', 'red', 'rgb(0 0 0)', 'rgba(0 0 0)', '#fff', '#ffff', '#ffffff', '#ffffffff']) { const {match, text} = matchSingleValue('width', fail, new SDK.CSSPropertyParserMatchers.ColorMatcher()); assert.isNull(match, text); } }); it('parses colors in logical border properties', () => { for (const success of ['border-block-end', 'border-block-end-color', 'border-block-start', 'border-block-start-color', 'border-inline-end', 'border-inline-end-color', 'border-inline-start', 'border-inline-start-color']) { const {ast, match, text} = matchSingleValue(success, 'red', new SDK.CSSPropertyParserMatchers.ColorMatcher()); assert.exists(match, text); assert.strictEqual(match.text, 'red'); assert.strictEqual(ast?.propertyName, success); } }); it('parses linear gradients', () => { for (const succeed of ['linear-gradient(90deg, red, blue)', 'linear-gradient(to top left, red, blue)', 'linear-gradient(in oklab, red, blue)']) { const {match, text} = matchSingleValue('background', succeed, new SDK.CSSPropertyParserMatchers.LinearGradientMatcher()); assert.exists(match, text); assert.strictEqual(match.text, succeed); } for (const fail of ['linear-gradient(90deg, red, blue)', 'linear-gradient(to top left, red, blue)', 'linear-gradient(in oklab, red, blue)']) { const {match, text} = matchSingleValue('width', fail, new SDK.CSSPropertyParserMatchers.ColorMatcher()); assert.isNull(match, text); } }); it('parses colors in masks', () => { for (const succeed of ['mask', 'mask-image', 'mask-border', 'mask-border-source']) { const ast = SDK.CSSPropertyParser.tokenizeDeclaration(succeed, 'linear-gradient(to top, red, var(--other))'); assert.exists(ast, succeed); const matching = SDK.CSSPropertyParser.BottomUpTreeMatching.walk(ast, [new SDK.CSSPropertyParserMatchers.ColorMatcher()]); const colorNode = SDK.CSSPropertyParser.TreeSearch.find(ast, node => ast.text(node) === 'red'); assert.exists(colorNode); const match = matching.getMatch(colorNode); assert.exists(match); assert.instanceOf(match, SDK.CSSPropertyParserMatchers.ColorMatch); assert.strictEqual(match.text, 'red'); } }); it('parses color-mix with vars', () => { injectVariableSubstitutions({ '--interpolation': 'shorter', '--color1': 'red', '--percentage': '13%', '--rgb': 'shorter', '--space': 'in srgb', '--color2': '25% blue', '--multiple-colors': 'red, blue', }); { const {ast, match, text} = matchSingleValue( 'color', 'color-mix(in srgb var(--interpolation) hue, red var(--percentage), rgb(var(--rgb)))', new SDK.CSSPropertyParserMatchers.ColorMixMatcher()); assert.exists(ast, text); assert.exists(match, text); assert.deepEqual(match.space.map(n => ast.text(n)), ['in', 'srgb', 'var(--interpolation)', 'hue']); assert.strictEqual(match.color1.map(n => ast.text(n)).join(), 'red,var(--percentage)'); assert.strictEqual(match.color2.map(n => ast.text(n)).join(), 'rgb(var(--rgb))'); } { const {ast, match, text} = matchSingleValue( 'color', 'color-mix(var(--space), var(--color1), var(--color2))', new SDK.CSSPropertyParserMatchers.ColorMixMatcher()); assert.exists(ast, text); assert.exists(match, text); assert.strictEqual(match.space.map(n => ast.text(n)).join(), 'var(--space)'); assert.strictEqual(match.color1.map(n => ast.text(n)).join(), 'var(--color1)'); assert.strictEqual(match.color2.map(n => ast.text(n)).join(), 'var(--color2)'); } for (const fail of ['color-mix(var(--color1), var(--color1), var(--color2))', 'color-mix(var(--space), var(--color1) var(--percentage) var(--percentage), var(--color2))', 'color-mix(var(--space), var(--color1) 10% var(--percentage), var(--color2))', 'color-mix(var(--space), var(--color1), var(--color2) 15%)', 'color-mix(var(--space), var(--color1), var(--color2) var(--percentage))', 'color-mix(var(--space), var(--multiple-colors))', ]) { const {ast, match, text} = matchSingleValue('color', fail, new SDK.CSSPropertyParserMatchers.ColorMixMatcher()); assert.exists(ast, text); assert.isNull(match, text); } }); it('parses color-mix', () => { function check(space: string, color1: string, color2: string): void { const {ast, match, text} = matchSingleValue( 'color', `color-mix(${space}, ${color1}, ${color2})`, new SDK.CSSPropertyParserMatchers.ColorMixMatcher()); assert.exists(ast, text); assert.exists(match, text); assert.deepEqual(match.space.map(n => ast.text(n)).join(' '), space, text); assert.strictEqual(match.color1.map(n => ast.text(n)).join(' '), color1, text); assert.strictEqual(match.color2.map(n => ast.text(n)).join(' '), color2, text); } function checkFailure(space: string, color1: string, color2: string): void { const {match, text} = matchSingleValue( 'color', `color-mix(${space}, ${color1}, ${color2})`, new SDK.CSSPropertyParserMatchers.ColorMixMatcher()); assert.isNull(match, text); } check('in srgb shorter hue', 'red 35%', 'blue'); check('in /*asd*/ srgb shorter hue', 'red 35%', 'blue'); check('in srgb', 'red 35%', 'blue'); check('in srgb', '35% red', 'blue 16%'); check('in srgb', '/*a*/ 35% /*b*/ red /*c*/', '/*a*/ blue /*b*/ 16% /*c*/'); checkFailure('insrgb shorter hue', 'red 35%', 'blue'); checkFailure('/*asd*/srgb in', 'red 35%', 'blue'); checkFailure('in srgb', '0% red', 'blue 0%'); }); it('parses URLs', () => { const url = 'http://example.com'; { const {match, text} = matchSingleValue('background-image', `url(${url})`, new SDK.CSSPropertyParserMatchers.URLMatcher()); assert.exists(match); assert.strictEqual(match.url, url, text); } { const {match, text} = matchSingleValue('background-image', `url("${url}")`, new SDK.CSSPropertyParserMatchers.URLMatcher()); assert.exists(match); assert.strictEqual(match.url, url, text); } }); it('parses angles correctly', () => { for (const succeed of ['45deg', '1.3rad', '-25grad', '2.3turn']) { const {ast, match, text} = matchSingleValue('transform', succeed, new SDK.CSSPropertyParserMatchers.AngleMatcher()); assert.exists(ast, succeed); assert.exists(match, text); assert.strictEqual(match.text, succeed); } for (const fail of ['0DEG', '0', '123', '2em']) { const {match, text} = matchSingleValue('transform', fail, new SDK.CSSPropertyParserMatchers.AngleMatcher()); assert.isNull(match, text); } }); it('parses linkable names correctly', () => { function match(name: string, value: string) { const ast = SDK.CSSPropertyParser.tokenizeDeclaration(name, value); assert.exists(ast); const matchedResult = SDK.CSSPropertyParser.BottomUpTreeMatching.walk(ast, [ new SDK.CSSPropertyParserMatchers.LinkableNameMatcher(), ]); const matches = SDK.CSSPropertyParser.TreeSearch.findAll( ast, node => matchedResult.getMatch(node) instanceof SDK.CSSPropertyParserMatchers.LinkableNameMatch); return matches.map(m => matchedResult.getMatch(m)?.text); } assert.deepEqual(match('animation-name', 'first, second, -moz-third'), ['first', 'second', '-moz-third']); assert.deepEqual(match('animation-name', 'first'), ['first']); assert.deepEqual(match('font-palette', 'first'), ['first']); { assert.deepEqual(match('position-try-fallbacks', 'flip-block'), []); assert.deepEqual(match('position-try-fallbacks', '--one'), ['--one']); assert.deepEqual(match('position-try-fallbacks', '--one, --two'), ['--one', '--two']); } { assert.deepEqual(match('position-try', 'flip-block'), []); assert.deepEqual(match('position-try', '--one'), ['--one']); assert.deepEqual(match('position-try', '--one, --two'), ['--one', '--two']); } { injectVariableSubstitutions({ '--duration-and-easing': '1s linear', }); assert.deepEqual(match('animation', '1s linear --animation-name'), ['--animation-name']); assert.deepEqual(match('animation', '1s linear linear'), ['linear']); assert.deepEqual( match('animation', '1s linear --first-name, 1s ease-in --second-name'), ['--first-name', '--second-name']); assert.deepEqual(match('animation', '1s linear'), []); // Matching to variable names inside `var()` functions are fine as it is handled by variable renderer in usage. assert.deepEqual(match('animation', 'var(--duration-and-easing) linear'), ['--duration-and-easing', 'linear']); assert.deepEqual( match('animation', '1s linear var(--non-existent, --animation-name)'), ['--non-existent', '--animation-name']); assert.deepEqual(match('animation', '1s step-start 0s kf'), ['kf']); assert.deepEqual(match('animation', '1s step-end 0s kf'), ['kf']); assert.deepEqual(match('animation', '1s steps(1, jump-start) 0s kf'), ['kf']); assert.deepEqual(match('animation', '1s steps(1, jump-end) 0s kf'), ['kf']); assert.deepEqual(match('animation', '1s steps(1, jump-none) 0s kf'), ['kf']); assert.deepEqual(match('animation', '1s steps(1, start) 0s kf'), ['kf']); assert.deepEqual(match('animation', '1s steps(1, end) 0s kf'), ['kf']); } }); it('parses easing functions properly', () => { for (const succeed of ['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear(0 0%, 1 100%)', 'cubic-bezier(0.3, 0.3, 0.3, 0.3)']) { const {ast, match, text} = matchSingleValue('animation-timing-function', succeed, new SDK.CSSPropertyParserMatchers.BezierMatcher()); assert.exists(ast, succeed); assert.exists(match, text); assert.strictEqual(match.text, succeed); } const {ast, match, text} = matchSingleValue('border', 'ease-in', new SDK.CSSPropertyParserMatchers.BezierMatcher()); assert.exists(ast, 'border'); assert.isNull(match, text); }); it('parses strings correctly', () => { function match(name: string, value: string) { const ast = SDK.CSSPropertyParser.tokenizeDeclaration(name, value); assert.exists(ast); const matchedResult = SDK.CSSPropertyParser.BottomUpTreeMatching.walk(ast, [new SDK.CSSPropertyParserMatchers.StringMatcher()]); assert.exists(matchedResult); const match = SDK.CSSPropertyParser.TreeSearch.find( ast, node => matchedResult.getMatch(node) instanceof SDK.CSSPropertyParserMatchers.StringMatch); assert.exists(match); } match('quotes', '"\'" "\'"'); match('content', '"foobar"'); match('--image-file-accelerometer-back', 'url("devtools\:\/\/devtools\/bundled\/Images\/accelerometer-back\.svg")'); }); it('parses shadows correctly', () => { const {match, text} = matchSingleValue( 'box-shadow', '/*0*/3px 3px red, -1em 0 .4em /*a*/ olive /*b*/', new SDK.CSSPropertyParserMatchers.ShadowMatcher()); assert.exists(match, text); assert.strictEqual(match.text, '/*0*/3px 3px red, -1em 0 .4em /*a*/ olive'); }); it('parses fonts correctly', () => { for (const fontSize of ['-.23', 'smaller', '17px']) { const {ast, match, text} = matchSingleValue('font-size', fontSize, new SDK.CSSPropertyParserMatchers.FontMatcher()); assert.exists(ast, text); assert.exists(match, text); assert.strictEqual(match.text, fontSize); } { const ast = SDK.CSSPropertyParser.tokenizeDeclaration('font-family', '"Gill Sans", sans-serif'); assert.exists(ast); const matchedResult = SDK.CSSPropertyParser.BottomUpTreeMatching.walk(ast, [new SDK.CSSPropertyParserMatchers.FontMatcher()]); assert.exists(matchedResult); const matches = SDK.CSSPropertyParser.TreeSearch.findAll( ast, node => matchedResult.getMatch(node) instanceof SDK.CSSPropertyParserMatchers.FontMatch); assert.deepEqual(matches.map(m => matchedResult.getMatch(m)?.text), ['"Gill Sans", sans-serif']); } }); it('parses grid templates correctly', () => { injectVariableSubstitutions({ '--row': '"a a b"', '--row-with-names': '[name1] "a a" [name2]', '--line-name': '[name1]', '--double-row': '"a b" "b c"', }); { const {ast, match, text} = matchSingleValue('grid', '"a a"', new SDK.CSSPropertyParserMatchers.GridTemplateMatcher()); assert.exists(ast, text); assert.exists(match, text); assert.strictEqual(match.lines.map(line => line.map(n => ast.text(n)).join(' ')).join('\n'), '"a a"'); } { const {ast, match, text} = matchSingleValue( 'grid-template-areas', '"a a a" "b b b" "c c c"', new SDK.CSSPropertyParserMatchers.GridTemplateMatcher()); assert.exists(ast, text); assert.exists(match, text); assert.deepEqual( match.lines.map(line => line.map(n => ast.text(n)).join(' ')), ['"a a a"', '"b b b"', '"c c c"']); } { const {ast, match, text} = matchSingleValue( 'grid-template', '"a a a" var(--row) / auto 1fr auto', new SDK.CSSPropertyParserMatchers.GridTemplateMatcher()); assert.exists(ast, text); assert.exists(match, text); assert.deepEqual( match.lines.map(line => line.map(n => ast.text(n)).join(' ')), ['"a a a"', 'var(--row) / auto 1fr auto']); } { const {ast, match, text} = matchSingleValue( 'grid', '[header-top] "a a" var(--row-with-names) [main-top] "b b b" 1fr [main-bottom] / auto 1fr auto;', new SDK.CSSPropertyParserMatchers.GridTemplateMatcher()); assert.exists(ast, text); assert.exists(match, text); assert.deepEqual( match.lines.map(line => line.map(n => ast.text(n)).join(' ')), ['[header-top] "a a" var(--row-with-names)', '[main-top] "b b b" 1fr [main-bottom] / auto 1fr auto']); } { const {ast, match, text} = matchSingleValue( 'grid', '[header-top] "a a" "b b b" var(--line-name) "c c" / auto 1fr auto;', new SDK.CSSPropertyParserMatchers.GridTemplateMatcher()); assert.exists(ast, text); assert.exists(match, text); assert.deepEqual( match.lines.map(line => line.map(n => ast.text(n)).join(' ')), ['[header-top] "a a"', '"b b b" var(--line-name)', '"c c" / auto 1fr auto']); } { const {ast, match, text} = matchSingleValue( 'grid', '[line1] "a a" [line2] var(--double-row) "b b" / auto 1fr auto;', new SDK.CSSPropertyParserMatchers.GridTemplateMatcher()); assert.exists(ast, text); assert.exists(match, text); assert.deepEqual( match.lines.map(line => line.map(n => ast.text(n)).join(' ')), ['[line1] "a a" [line2]', 'var(--double-row)', '"b b" / auto 1fr auto']); } { const {ast, match, text} = matchSingleValue( 'grid', '"a a" var(--unresolved) / auto 1fr auto;', new SDK.CSSPropertyParserMatchers.GridTemplateMatcher()); assert.exists(ast, text); assert.exists(match, text); assert.deepEqual( match.lines.map(line => line.map(n => ast.text(n)).join(' ')), ['"a a" var(--unresolved) / auto 1fr auto']); } }); it('parses light-dark correctly', () => { for (const fail of ['light-dark()', 'light-dark(red)', 'light-dark(var(--foo))']) { const {match, text} = matchSingleValue('color', fail, new SDK.CSSPropertyParserMatchers.LightDarkColorMatcher()); assert.isNull(match, text); } for (const succeed of ['light-dark(red, blue)', 'light-dark(var(--foo), red)', 'light-dark(red, var(--foo))', 'light-dark(var(--foo), var(--bar))']) { const {ast, match, text} = matchSingleValue('color', succeed, new SDK.CSSPropertyParserMatchers.LightDarkColorMatcher()); assert.exists(ast, text); assert.exists(match, text); const [light, dark] = succeed.slice('light-dark('.length, -1).split(', '); assert.lengthOf(match.light, 1); assert.lengthOf(match.dark, 1); assert.strictEqual(ast.text(match.light[0]), light); assert.strictEqual(ast.text(match.dark[0]), dark); } // light-dark only applies to color properties const {match, text} = matchSingleValue('width', 'light-dark(red, blue)', new SDK.CSSPropertyParserMatchers.LightDarkColorMatcher()); assert.isNull(match, text); }); it('parses auto-base correctly', () => { for (const fail of ['-internal-auto-base()', '-internal-auto-base(block)', '-internal-auto-base(var(--foo))']) { const {match, text} = matchSingleValue('display', fail, new SDK.CSSPropertyParserMatchers.AutoBaseMatcher()); assert.isNull(match, text); } for (const [succeed, propertyName] of [ ['-internal-auto-base(red, blue)', 'color'], ['-internal-auto-base(var(--foo), red)', 'color'], ['-internal-auto-base(red, var(--foo))', 'color'], ['-internal-auto-base(var(--foo), var(--bar))', 'color'], ['-internal-auto-base(gray, coral)', 'background-color'], ['-internal-auto-base(inline, block)', 'display'], ['-internal-auto-base(center, right)', 'text-align'], ['-internal-auto-base(serif, cursive)', 'font-family'], ['-internal-auto-base(solid, dashed)', 'border-style'], ['-internal-auto-base(0, 0.5em)', 'border-radius'], ['-internal-auto-base(2px, 0.25em)', 'padding'], ['-internal-auto-base(1en, 3pt)', 'margin'], ]) { const {ast, match, text} = matchSingleValue(propertyName, succeed, new SDK.CSSPropertyParserMatchers.AutoBaseMatcher()); assert.exists(ast, text); assert.exists(match, text); const [auto, base] = succeed.slice('-internal-auto-base('.length, -1).split(', '); assert.lengthOf(match.auto, 1); assert.lengthOf(match.base, 1); assert.strictEqual(ast.text(match.auto[0]), auto); assert.strictEqual(ast.text(match.base[0]), base); } }); describe('AnchorFunctionMatcher', () => { it('should not match when it is not a call expression', () => { const {match, text} = matchSingleValue('left', 'anchor', new SDK.CSSPropertyParserMatchers.AnchorFunctionMatcher()); assert.isNull(match, text); }); it('should not match anchor() call without arguments', () => { const {match: anchorMatch} = matchSingleValue('left', 'anchor()', new SDK.CSSPropertyParserMatchers.AnchorFunctionMatcher()); assert.isNull(anchorMatch); }); it('should match anchor-size() call without arguments', () => { const {match: anchorSizeMatch, text: anchorSizeText} = matchSingleValue('width', 'anchor-size()', new SDK.CSSPropertyParserMatchers.AnchorFunctionMatcher()); assert.exists(anchorSizeMatch, anchorSizeText); }); it('should match if it is an anchor() or anchor-size() call', () => { const {match: anchorMatch, text: anchorText} = matchSingleValue('left', 'anchor(left)', new SDK.CSSPropertyParserMatchers.AnchorFunctionMatcher()); assert.exists(anchorMatch, anchorText); const {match: anchorSizeMatch, text: anchorSizeText} = matchSingleValue('width', 'anchor-size(width)', new SDK.CSSPropertyParserMatchers.AnchorFunctionMatcher()); assert.exists(anchorSizeMatch, anchorSizeText); }); it('should match dashed identifier as name from the first argument', () => { const {match: anchorMatch, text: anchorText} = matchSingleValue( 'left', 'anchor(--dashed-ident left)', new SDK.CSSPropertyParserMatchers.AnchorFunctionMatcher()); assert.exists(anchorMatch, anchorText); assert.strictEqual(anchorMatch.text, '--dashed-ident'); const {match: anchorSizeMatch, text: anchorSizeText} = matchSingleValue( 'width', 'anchor-size(--dashed-ident width)', new SDK.CSSPropertyParserMatchers.AnchorFunctionMatcher()); assert.exists(anchorSizeMatch, anchorSizeText); assert.strictEqual(anchorSizeMatch.text, '--dashed-ident'); }); it('should match dashed identifier as name from the second argument', () => { const {match: anchorMatch, text: anchorText} = matchSingleValue( 'left', 'anchor(right --dashed-ident)', new SDK.CSSPropertyParserMatchers.AnchorFunctionMatcher()); assert.exists(anchorMatch, anchorText); assert.strictEqual(anchorMatch.text, '--dashed-ident'); const {match: anchorSizeMatch, text: anchorSizeText} = matchSingleValue( 'width', 'anchor-size(height --dashed-ident)', new SDK.CSSPropertyParserMatchers.AnchorFunctionMatcher()); assert.exists(anchorSizeMatch, anchorSizeText); assert.strictEqual(anchorSizeMatch.text, '--dashed-ident'); }); }); describe('PositionAnchorMatcher', () => { it('should match `position-anchor` property with dashed identifier', () => { const {match, text} = matchSingleValue( 'position-anchor', '--dashed-ident', new SDK.CSSPropertyParserMatchers.PositionAnchorMatcher()); assert.exists(match, text); assert.strictEqual(match.text, '--dashed-ident'); }); it('should not match `position-anchor` property when it is not a dashed identifier', () => { const {match} = matchSingleValue( 'position-anchor', 'something-non-dashed', new SDK.CSSPropertyParserMatchers.PositionAnchorMatcher()); assert.isNull(match); }); }); describe('PositionTryMatcher', () => { it('should match `position-try[-fallbacks]` property with linkable names', () => { { const {match, text} = matchSingleValue( 'position-try', 'flip-block, --top, --bottom', new SDK.CSSPropertyParserMatchers.PositionTryMatcher()); assert.exists(match, text); assert.strictEqual(match.text, 'flip-block, --top, --bottom'); assert.lengthOf(match.preamble, 0); } { const {ast, match, text} = matchSingleValue( 'position-try', '/* comment */ most-height --top, --bottom', new SDK.CSSPropertyParserMatchers.PositionTryMatcher()); assert.exists(ast, text); assert.exists(match, text); assert.strictEqual(match.text, '/* comment */ most-height --top, --bottom'); assert.strictEqual( ast.textRange(match.preamble[0], match.preamble[match.preamble.length - 1]), '/* comment */ most-height'); } { const {match, text} = matchSingleValue( 'position-try-fallbacks', '/* comment */ flip-block, --top, /* comment */ --bottom', new SDK.CSSPropertyParserMatchers.PositionTryMatcher()); assert.exists(match, text); assert.strictEqual(match.text, '/* comment */ flip-block, --top, /* comment */ --bottom'); assert.lengthOf(match.preamble, 0); } { const {match} = matchSingleValue('position-try', 'revert', new SDK.CSSPropertyParserMatchers.PositionTryMatcher()); assert.isNull(match); } }); }); describe('SelectFunctionMatcher', () => { it('matches selecting functions', () => { const success = ['clamp(1px, 2px, 3px)', 'min(1, 2)', 'max(3, 4)']; for (const value of success) { const {match, text} = matchSingleValue('width', value, new SDK.CSSPropertyParserMatchers.SelectFunctionMatcher()); assert.exists(match, text); assert.strictEqual(match.text, value); assert.strictEqual(match.func, value.substr(0, value.indexOf('('))); assert.isAbove(match.args.length, 0); } const failure = ['clomp(1px, 2px, 3px)', 'min()']; for (const value of failure) { const {match, text} = matchSingleValue('width', value, new SDK.CSSPropertyParserMatchers.SelectFunctionMatcher()); assert.notExists(match, text); } }); }); it('matches lengths', () => { for (const unit of SDK.CSSPropertyParserMatchers.LengthMatcher.LENGTH_UNITS) { const {match, text} = matchSingleValue('min-width', `100${unit}`, new SDK.CSSPropertyParserMatchers.LengthMatcher()); assert.exists(match, text); assert.strictEqual(match.text, `100${unit}`); } }); it('match css keywords', () => { const propertyStub = sinon.createStubInstance(SDK.CSSProperty.CSSProperty); const matchedStylesStub = sinon.createStubInstance(SDK.CSSMatchedStyles.CSSMatchedStyles); for (const keyword of SDK.CSSMetadata.CSSWideKeywords) { const {match, text} = matchSingleValue( '--property', keyword, new SDK.CSSPropertyParserMatchers.CSSWideKeywordMatcher(propertyStub, matchedStylesStub)); assert.exists(match, text); assert.strictEqual(match.text, keyword); } const {match, text} = matchSingleValue( '--property', '1px inherits', new SDK.CSSPropertyParserMatchers.CSSWideKeywordMatcher(propertyStub, matchedStylesStub)); assert.notExists(match, text); }); it('match flex and grid values', () => { const good = [ 'flex', 'grid', 'inline-flex', 'inline-grid', 'block flex', 'block grid', 'inline flex', 'inline grid', 'inline grid !important', 'grid /* comment */', ]; const bad = ['flex block', 'grid inline', 'block', 'inline']; for (const value of good) { const {match, text} = matchSingleValue('display', value, new SDK.CSSPropertyParserMatchers.FlexGridMatcher()); assert.exists(match, text); assert.strictEqual(match.text.includes('flex'), match.isFlex); } for (const value of bad) { const {match, text} = matchSingleValue('display', value, new SDK.CSSPropertyParserMatchers.FlexGridMatcher()); assert.notExists(match, text); } }); });