Test check (iii) and fix two bugs it exposed in the application-CSS scan
Plan step 41(iii): fixtures/tests for every literal kind (colour, radius, shadow, font size/weight, line height, letter spacing, easing, duration) and an off-scale media query, plus material-scheme.css excluded by name and by its generated header wherever it sits among the scanned paths. Also adds a fixture proving Tailwind-shaped bare words in running prose are never read as classes. Two fixes found by writing the fixtures rather than only the implementation: a directly-scanned .css file was also running through the Blade/PHP/JS checks (the same file's `ease-in-out` in a `transition` value tripped the old scale check a second time), so a CSS file is now check (iii)'s alone; and `withoutTokenFunctions()` leaves a `var()`/`calc()` call's own name and parentheses standing once its arguments are blanked, which `isSafeLiteralValue()` did not yet recognise as the same "nothing to see here" as an empty value. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b998683762
commit
09f4edd31d
@@ -325,6 +325,10 @@ class DesignGuard
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->files() as $file) {
|
foreach ($this->files() as $file) {
|
||||||
|
if (str_ends_with($file->getFilename(), '.css')) {
|
||||||
|
continue; // A directly-scanned CSS file is check (iii)'s alone; these checks read Blade, PHP and JS.
|
||||||
|
}
|
||||||
|
|
||||||
$contents = (string) file_get_contents($file->getPathname());
|
$contents = (string) file_get_contents($file->getPathname());
|
||||||
$where = $this->relative($file->getPathname());
|
$where = $this->relative($file->getPathname());
|
||||||
$isBlade = str_ends_with($file->getFilename(), '.blade.php');
|
$isBlade = str_ends_with($file->getFilename(), '.blade.php');
|
||||||
@@ -671,7 +675,7 @@ class DesignGuard
|
|||||||
*/
|
*/
|
||||||
protected function literalColours(string $css): array
|
protected function literalColours(string $css): array
|
||||||
{
|
{
|
||||||
$pattern = '/(?<![\w#-])(?:#[0-9a-fA-F]{3,8}\b|(?:rgb|rgba|hsl|hsla|oklch|oklab|lch|lab|color)\(|(?<=[:,(]\s{0,20})(?:'.self::CSS_NAMED_COLOURS.')\b(?!-))/i';
|
$pattern = '/(?<![\w#-])(?:#[0-9a-fA-F]{3,8}\b|(?:rgb|rgba|hsl|hsla|oklch|oklab|lch|lab|color)\([^)]*\)|(?<=[:,(]\s{0,20})(?:'.self::CSS_NAMED_COLOURS.')\b(?!-))/i';
|
||||||
|
|
||||||
preg_match_all($pattern, $css, $matches, PREG_OFFSET_CAPTURE);
|
preg_match_all($pattern, $css, $matches, PREG_OFFSET_CAPTURE);
|
||||||
|
|
||||||
@@ -731,9 +735,16 @@ class DesignGuard
|
|||||||
return $found;
|
return $found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A value `withoutTokenFunctions()` left with nothing but the empty shell of a `var()`/`calc()`
|
||||||
|
* call — whitespace where the call's own argument list used to be, the call's own name and
|
||||||
|
* parentheses still standing — is exactly as fine as one masked away entirely.
|
||||||
|
*/
|
||||||
protected function isSafeLiteralValue(string $value): bool
|
protected function isSafeLiteralValue(string $value): bool
|
||||||
{
|
{
|
||||||
return trim($value) === '' || in_array(strtolower(trim($value)), ['0', '0px', '0s', 'none', 'inherit', 'initial', 'unset', 'normal', 'auto'], true);
|
$value = trim((string) preg_replace('/\b(?:var|calc)\(\s*\)/i', '', $value));
|
||||||
|
|
||||||
|
return $value === '' || in_array(strtolower($value), ['0', '0px', '0s', 'none', 'inherit', 'initial', 'unset', 'normal', 'auto'], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -411,6 +411,38 @@ it('reports a package tag the application shadows with its own component class',
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('finds every literal design value and off-scale media query in the application\'s own CSS', function () {
|
||||||
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/app-css/app.css'))->violations());
|
||||||
|
|
||||||
|
expect($violations)->toBe([
|
||||||
|
'app-css/app.css:3 literal colour `#ff0000` — use `var(--md-sys-color-*)`',
|
||||||
|
'app-css/app.css:4 literal colour `rgb(0, 0, 0)` — use `var(--md-sys-color-*)`',
|
||||||
|
'app-css/app.css:6 literal colour `rgba(0, 0, 0, 0.2)` — use `var(--md-sys-color-*)`',
|
||||||
|
'app-css/app.css:5 literal radius `border-radius: 12px` — use `var(--md-sys-shape-corner-*)`',
|
||||||
|
'app-css/app.css:6 literal shadow `box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2)` — use `var(--md-sys-elevation-*)`',
|
||||||
|
'app-css/app.css:7 literal font size `font-size: 14px` — use `var(--md-sys-typescale-*)`, which sets size, line height and weight together',
|
||||||
|
'app-css/app.css:8 literal font weight `font-weight: 600` — use `var(--md-sys-typescale-*)` or `var(--md-sys-typescale-emphasized-*)`',
|
||||||
|
'app-css/app.css:9 literal line height `line-height: 1.4` — use `var(--md-sys-typescale-*)`, which sets it with the size',
|
||||||
|
'app-css/app.css:10 literal letter spacing `letter-spacing: 0.02em` — use `var(--md-sys-typescale-*-tracking)`',
|
||||||
|
'app-css/app.css:11 literal easing in `transition: opacity 200ms ease-in-out` — use `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)`, paired with its `-duration`',
|
||||||
|
'app-css/app.css:11 literal duration in `transition: opacity 200ms ease-in-out` — use `var(--md-sys-motion-…-duration)`, paired with its easing',
|
||||||
|
"app-css/app.css:21 media query width `700px` is not one of M3's breakpoints — use 600, 840, 1200 or 1600px (medium, expanded, large, extra-large)",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores the generated material-scheme.css, by name and by its generated header, wherever it sits among the scanned paths', function () {
|
||||||
|
$withScheme = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/app-css'))->violations());
|
||||||
|
$withoutScheme = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/app-css/app.css'))->violations());
|
||||||
|
|
||||||
|
expect($withScheme)->toBe($withoutScheme);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never reads a Tailwind-shaped word out of running text', function () {
|
||||||
|
$violations = DesignGuard::scan(realpath(GUARD_FIXTURES.'/running-text.blade.php'))->violations();
|
||||||
|
|
||||||
|
expect($violations)->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('passes the package\'s own views', function () {
|
it('passes the package\'s own views', function () {
|
||||||
$violations = DesignGuard::scan([__DIR__.'/../../resources/views', __DIR__.'/../../resources/js', __DIR__.'/../../src'])->violations();
|
$violations = DesignGuard::scan([__DIR__.'/../../resources/views', __DIR__.'/../../resources/js', __DIR__.'/../../src'])->violations();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/* comments are ignored */
|
||||||
|
.card {
|
||||||
|
color: #ff0000;
|
||||||
|
background: rgb(0, 0, 0);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
transition: opacity 200ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fine {
|
||||||
|
border-radius: var(--md-sys-shape-corner-lg);
|
||||||
|
color: var(--md-sys-color-primary);
|
||||||
|
padding: calc(var(--md-sys-measurement-space100) * 2);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 700px) {
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/*
|
||||||
|
* Material 3 colour roles, generated by Google's material-color-utilities (spec 2025).
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--md-sys-color-primary: #6750a4;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
:root {
|
||||||
|
--md-sys-color-primary: #6750a4;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{{-- Running text is not a class list: none of Tailwind's bare-word utilities should be read out
|
||||||
|
of prose just because they share a spelling with one. --}}
|
||||||
|
<p>This creates a grid of cards, in a flex layout that stays hidden below medium and stacks into a
|
||||||
|
block once the pane narrows. Nothing here truncates or grows, and the sample is not a table.</p>
|
||||||
Reference in New Issue
Block a user