The device renders in Chrome 113, which predates relaxed CSS nesting, so
a nested rule whose selector starts with an element name is not a parse
error anyone would notice -- the rule simply does not exist, there and
nowhere else. Three were live in `index.css`, and the one that mattered
was the `text-overflow: ellipsis` on the bottom bar's title and artist,
which had therefore never truncated on the device. No tier here can see
the class at all: the component tier, the e2e tier and `make ui-visual`
all run a current engine, where the rule applies normally.
So `make css-check` carries a second script. It reads `index.css` and
the `css` literals in `src/**/*.ts` alike, since a shadow-root
stylesheet is parsed by the same engine, and it names the file, the line
and the fix -- a leading `&`, which is valid in both syntaxes.
The detection walks blocks rather than matching lines, and both things
it has to get right fall out of one rule: a rule is nested when a
*style* rule is somewhere above it, not when its immediate parent is a
block. That leaves `@media (...) { bottom-nav { ... } }` at the top
level alone, which is the majority of what a regex over the file would
report, and still flags the same rule inside an at-rule that is itself
inside a style rule. Strings and comments are read through, so a brace
in a `url()` is not a block.
The tree has no violation left, so the check would pass just as happily
over an empty glob: it refuses one, and `test/utils/css-nesting.test.ts`
pins the semantics that make the sweep mean something. The literal
scanner the two checks share is lifted into `css-literals.mjs`
unchanged, except that a `${}` substitution is now blanked keeping its
newlines so a line number survives it.
Closes #154
80 lines
2.6 KiB
TypeScript
80 lines
2.6 KiB
TypeScript
/**
|
|
* The nesting check's own semantics.
|
|
*
|
|
* `make css-check` runs it over a tree that currently has no violation,
|
|
* so the check passing says nothing about whether it can still find
|
|
* one. What it has to get right is two distinctions, and both are the
|
|
* kind a regex over the file gets wrong: a rule directly inside an
|
|
* at-rule is not nested, and a brace inside a string or a comment is
|
|
* not a block.
|
|
*
|
|
* The rule it enforces is the device's: Chrome 113 predates relaxed CSS
|
|
* nesting, so a nested selector starting with an element name is
|
|
* dropped in silence. See `scripts/css-nesting.mjs`.
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import { findBareNestedRules } from '../../scripts/css-nesting.mjs';
|
|
|
|
describe('the nested-rule check', () => {
|
|
it('flags a nested rule that starts with an element name', () => {
|
|
const found = findBareNestedRules(
|
|
'.bottom-bar {\n color: red;\n\n audio-player { margin: 0 }\n}',
|
|
);
|
|
|
|
expect(found).toEqual([{ line: 4, selector: 'audio-player' }]);
|
|
});
|
|
|
|
it('accepts the same rule written with a leading &', () => {
|
|
expect(
|
|
findBareNestedRules('.bottom-bar {\n & audio-player { margin: 0 }\n}'),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('accepts a nested selector that starts with any other symbol', () => {
|
|
expect(
|
|
findBareNestedRules('.bar {\n #track-info { color: red }\n}'),
|
|
).toEqual([]);
|
|
expect(findBareNestedRules('.bar {\n :host { color: red }\n}')).toEqual(
|
|
[],
|
|
);
|
|
});
|
|
|
|
it('leaves a top-level rule alone, element name or not', () => {
|
|
expect(findBareNestedRules('p {\n margin: 0;\n}')).toEqual([]);
|
|
});
|
|
|
|
/**
|
|
* The majority of what a naive sweep would report: a media query at
|
|
* the top level holds ordinary rules, not nested ones.
|
|
*/
|
|
it('leaves a rule directly inside an at-rule alone', () => {
|
|
expect(
|
|
findBareNestedRules(
|
|
'@media (max-width: 599px) {\n bottom-nav { display: flex }\n}',
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
/**
|
|
* And the other half of that: what decides it is whether a style rule
|
|
* is anywhere above, not what the immediate parent is.
|
|
*/
|
|
it('flags one inside an at-rule that is itself inside a rule', () => {
|
|
expect(
|
|
findBareNestedRules(
|
|
'.bar {\n @media (min-width: 900px) {\n audio-player { margin: 0 }\n }\n}',
|
|
),
|
|
).toEqual([{ line: 3, selector: 'audio-player' }]);
|
|
});
|
|
|
|
it('reads through a brace in a string or a comment', () => {
|
|
expect(
|
|
findBareNestedRules('.a {\n background: url("x{y}");\n}'),
|
|
).toEqual([]);
|
|
expect(
|
|
findBareNestedRules('.a {\n /* audio-player { x: y } */\n}'),
|
|
).toEqual([]);
|
|
});
|
|
});
|