From 471d927e644883a205ed9c6190f01698a72e81e3 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Thu, 17 Sep 2026 08:05:33 +0200 Subject: [PATCH] Count a row written by hand in the design guard's stylesheet checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `data-md-list-row` on an application's own `
  • `, `
    `, `` or `` draws its hover, focus and press state layer, its cursor and its `data-md-selected` fill from list-item.css, but missingStylesheets() and unusedStylesheets() read only component tags and `->links()`. ReStride had the stylesheet only because table.css imports it: had its last `` gone, every hand-made row would have lost its states and its fill without a finding, and importing list-item.css for the rows directly was reported unused. A Blade view outside the package's own `resources/views` that writes a hook in the new `HOOK_STYLESHEETS` now needs that hook's stylesheet in both checks, named at the earliest line that needs it. The one entry is `data-md-list-row` → `components/list-item.css`, except on `` (any spelling), whose row card.css draws; the card's opening tag is blanked out before the hook is read, and a selector such as `[data-md-list-row]` or a name in backticks writes no row. Nothing else list-rows.js, the skill or the README has an application write needs a stylesheet of its own: `data-md-list-open` and a row's `data-md-selected` are drawn only through the row, a selected row in `` by table.css, `data-md-dragged` by foundation.css's `md-state-layer` or card.css, `data-md-field-control` by field.css inside ``. The constant's docblock says so, and the guard's docblocks, the README, the skill and UPGRADE.md describe the check. A fixture test reads a hand-made `
  • ` and `` row and a card-only view (plain and namespaced ``, and a selector in a script) against both checks; it fails without the change. A second test scans the package's list-item and card views and expects no row finding; it fails when package views are read. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 2 +- UPGRADE.md | 9 + .../livewire-material-development/SKILL.md | 4 +- src/Testing/DesignGuard.php | 184 +++++++++++++----- tests/Feature/DesignGuardTest.php | 33 ++++ .../design-guard/stylesheets/rows.css | 3 + .../stylesheets/views/card-rows.blade.php | 9 + .../stylesheets/views/rows.blade.php | 11 ++ 8 files changed, 208 insertions(+), 47 deletions(-) create mode 100644 tests/Fixtures/design-guard/stylesheets/rows.css create mode 100644 tests/Fixtures/design-guard/stylesheets/views/card-rows.blade.php create mode 100644 tests/Fixtures/design-guard/stylesheets/views/rows.blade.php diff --git a/README.md b/README.md index f5ec6432..c4f99351 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ it('uses only what compiles', function () { The guard reads every path it is given and fails, with `path:line` and the 2.0.0 replacement, on any Tailwind utility or variant still in a view, PHP or JS file, a colour of the application's own Tailwind theme (`bg-brand`) included — none compiles, since the application carries no Tailwind — and on 1.x's own utilities (`type-body-md`, `text-meta`, `rounded-corner-lg`, `focus-ring`): a layout component and prop (`flex gap-4` → ``), an `md-*` class (`truncate` → `md-truncate`), or a token for the application's own CSS (`rounded-lg` → `var(--md-sys-shape-corner-lg)`). A class the application's own stylesheets declare is exempt. In the `.css` files it is given (`material-scheme.css` skipped) it fails on a literal colour, radius, shadow, font, easing or duration, and on a media query off M3's 600/840/1200/1600px; a `var()`, or a `calc()`/`min()`/`max()`/`clamp()` built on one, is fine. It also fails on unknown Material Symbol names and Blade directives written inside component tags. Markdown mail components (under `mail.markdown.paths`, `resources/views/vendor/mail` by default) are the mail theme's to style: their classes are not read as Tailwind, and a mail theme stylesheet there is neither checked for literals nor a source of exempt classes. -`missingStylesheets($cssEntry)` checks the entry's relative `@import` graph, followed through every package file's own imports, against the package tags the views render (unprefixed, under the configured prefix, or ``) and `->links()`, and names each missing `@import` line once; a tag the application shadows with its own component is reported instead. It reads imports only, so leave `resource_path('css')` out of `scan()` while the stylesheets still hold literals. `forbidColours([...])` names roles the application leaves out, and fails wherever one (with its `on-` and container roles) is still written: `var(--md-sys-color-…)` in CSS or an inline `style`, its `md-ink-*` class, or a component's `color`/`tone` prop. `forbid($pattern, $reason)` adds any pattern of your own, line by line. +`missingStylesheets($cssEntry)` checks the entry's relative `@import` graph, followed through every package file's own imports, against the package tags the views render (unprefixed, under the configured prefix, or ``), `->links()` and the rows they write by hand (`data-md-list-row` on anything but `` needs `components/list-item.css`), and names each missing `@import` line once; a tag the application shadows with its own component is reported instead. It reads imports only, so leave `resource_path('css')` out of `scan()` while the stylesheets still hold literals. `forbidColours([...])` names roles the application leaves out, and fails wherever one (with its `on-` and container roles) is still written: `var(--md-sys-color-…)` in CSS or an inline `style`, its `md-ink-*` class, or a component's `color`/`tone` prop. `forbid($pattern, $reason)` adds any pattern of your own, line by line. ## AI agents diff --git a/UPGRADE.md b/UPGRADE.md index 8466a52e..e1b2e627 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -24,6 +24,15 @@ family now too: `border-collapse`, `border-spacing-*`, `from-10%`, `via-none`, `fill-none`, `stroke-*`, `text-shadow-*`, `text-inherit`, and Tailwind 4.2's `mauve`, `olive`, `mist` and `taupe` palettes. A test that expects no findings may fail on them. +- **`DesignGuard`** counts a row written by hand. `data-md-list-row` on an `
  • `, a `
    `, a + `` or `` takes its hover, focus and press state layer and its selected fill from + `components/list-item.css`, but `missingStylesheets()` and `unusedStylesheets()` read only tags + and `->links()`: an application had the stylesheet through `table.css` or ``, lost + it without a finding when the last of those left its views, and an `@import` of `list-item.css` + for its own rows was reported unused. A view of the application that writes `data-md-list-row` + on anything but ``, whose row card.css draws, now needs `list-item.css`; a selector such + as `[data-md-list-row]` and the package's own views do not count. A test that expects no findings + may name the `@import` to add, and a filter written around the unused report can go. - **A selected row written by hand** — `data-md-list-row` with `data-md-selected` on an `
  • `, a `
    ` or a ``, not `` or `` — is drawn in `secondary-container` with `on-secondary-container` ink again, as 1.x's `data-list-row` with `data-selected` was; 2.0.0 diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index ef26d350..477053fc 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -514,7 +514,7 @@ The `illustration` slot draws the application's own artwork in place of the shap `variant`: `filled` (default, surface-container-highest), `elevated`, `outlined`; medium corner. Props `title`, `subtitle`, `separator`, `heading` (the title's element, `h3` by default: pass `h2` for a card straight under the page's `h1`); slots `figure` (full-bleed media), `menu` (top-end), `actions` (end-aligned). A card holds content and actions about one subject (M3); a table of many rows or a group of settings is a headed section, not a card. Choose its fill with `variant`, not a background of your own. -A card or list item that opens something is a **row**: `data-md-list-row` on it and `data-md-list-open` on its one opener (the title link or a button). A press anywhere else on the row reaches the opener; its other controls keep their own presses. Never wrap a card in `` or use a stretched link. A row answers with the state layer and one step of elevation; its corner does not move. `data-md-selected` on a row that is not a card — your own `
  • `, `
    ` or ``, the one whose sheet is open — fills it secondary-container with on-secondary-container ink, the state layer over the fill (`` writes it itself); a card takes no selected fill. +A card or list item that opens something is a **row**: `data-md-list-row` on it and `data-md-list-open` on its one opener (the title link or a button). A press anywhere else on the row reaches the opener; its other controls keep their own presses. Never wrap a card in `` or use a stretched link. A row answers with the state layer and one step of elevation; its corner does not move. `data-md-selected` on a row that is not a card — your own `
  • `, `
    ` or ``, the one whose sheet is open — fills it secondary-container with on-secondary-container ink, the state layer over the fill (`` writes it itself); a card takes no selected fill. A row that is not a card draws its state layer and that fill from `components/list-item.css`, so the CSS entry imports it even where no `` or `` renders (the design guard's `missingStylesheets()` asks for it). ```blade @@ -1212,7 +1212,7 @@ In the `.css` files it is given (`material-scheme.css` skipped) it fails on a li Markdown mail components — whatever lies under `config('mail.markdown.paths')`, `resources/views/vendor/mail` by default — are drawn by the mail theme, not by your CSS entry, and its classes (`table`, `button`, `panel`, `break-all`) share Tailwind's names. So the guard skips the class checks there (it still reads their icon names, the directives in their component tags and your own bans), and a mail theme stylesheet under that path, which has to write literal values, is neither checked for literals nor a source of exempt classes. -`missingStylesheets($cssEntry)` follows the entry's relative `@import`s through every package file's own imports (`split-button.css` counts `button.css` and `menu.css` too; `tailwindcss` or a font URL is skipped) and checks them against the package tags a scanned view renders (plain, prefixed or ``) and `->links()`; each missing stylesheet names its `@import` line once, and a tag the application shadows with its own component is reported instead — the application's component wins in Blade. It reads imports only: leave `resource_path('css')` out of `scan()` until the stylesheets are on tokens. `unusedStylesheets($cssEntry)` is the other way round: a package stylesheet the entry imports directly that no scanned view needs, even through a needed stylesheet's own imports, named at its `@import` line to remove (an entry importing `all.css` is left alone). `forbidColours([...])` fails wherever a left-out role (with its `on-` and container roles) is still written: `var(--md-sys-color-…)` in CSS or an inline `style`, its `md-ink-*` class, or a component's `color`/`tone` prop. `forbid($pattern, $reason)` adds a pattern of your own. +`missingStylesheets($cssEntry)` follows the entry's relative `@import`s through every package file's own imports (`split-button.css` counts `button.css` and `menu.css` too; `tailwindcss` or a font URL is skipped) and checks them against the package tags a scanned view renders (plain, prefixed or ``), `->links()`, and a row a view of yours writes by hand (`data-md-list-row` on anything but ``, which needs `components/list-item.css`; the package's own views are not read for it); each missing stylesheet names its `@import` line once, and a tag the application shadows with its own component is reported instead — the application's component wins in Blade. It reads imports only: leave `resource_path('css')` out of `scan()` until the stylesheets are on tokens. `unusedStylesheets($cssEntry)` is the other way round: a package stylesheet the entry imports directly that no scanned view needs — no tag, `->links()` or row written by hand — even through a needed stylesheet's own imports, named at its `@import` line to remove (an entry importing `all.css` is left alone). `forbidColours([...])` fails wherever a left-out role (with its `on-` and container roles) is still written: `var(--md-sys-color-…)` in CSS or an inline `style`, its `md-ink-*` class, or a component's `color`/`tone` prop. `forbid($pattern, $reason)` adds a pattern of your own. ## Conventions diff --git a/src/Testing/DesignGuard.php b/src/Testing/DesignGuard.php index 8126783a..f2b45d6a 100644 --- a/src/Testing/DesignGuard.php +++ b/src/Testing/DesignGuard.php @@ -28,13 +28,16 @@ use Symfony\Component\Finder\Finder; * (ii) `missingStylesheets($cssEntry)`: a package component tag used in a view — unprefixed, * under the configured prefix, or `` — whose stylesheet the * entry's relative `@import` graph does not reach (followed through every package file's - * own imports; a package name or URL the entry also imports is skipped, never fatal), and - * `->links()` needing `pagination.css`; each names the missing `@import` line once, at its - * first use. A tag the application shadows with its own component of the same name is - * reported instead — the application's component wins in Blade, so the package's - * stylesheet is moot. `unusedStylesheets($cssEntry)` reports the other way: a package - * stylesheet the entry imports directly that no scanned view needs, not even through - * another needed stylesheet's imports (an entry importing `all.css` is left alone). + * own imports; a package name or URL the entry also imports is skipped, never fatal), + * `->links()` needing `pagination.css`, and a hook an application's view writes on markup + * of its own whose rules a component's stylesheet holds — `data-md-list-row` on anything + * but `` needing `list-item.css` (`HOOK_STYLESHEETS`); each names the missing + * `@import` line once, at its first use. A tag the application shadows with its own + * component of the same name is reported instead — the application's component wins in + * Blade, so the package's stylesheet is moot. `unusedStylesheets($cssEntry)` reports the + * other way: a package stylesheet the entry imports directly that no scanned view needs, + * not even through another needed stylesheet's imports (an entry importing `all.css` is + * left alone). * (iii) every `.css` file among the scanned paths, outside the package and excluding the * generated `material-scheme.css`: a literal colour, radius, shadow, font size, weight, * line height, letter spacing, easing or duration, and a media query at a width other @@ -225,6 +228,24 @@ class DesignGuard protected const string THEME_COLOUR_UTILITY = '/^(?bg|text|border(?:-(?:[trblxyse]|bs|be))?|divide|ring(?:-offset)?|outline|fill|stroke|decoration|accent|caret|placeholder|shadow|from|via|to)-' .'(?[a-z][a-z\d]*(?:-[a-z\d]+)*)(?:\/(?\d{1,3}|\[[^\]]*\]|\(--[\w-]+\)))?$/'; + /** + * The hooks an application writes on markup of its own, not through a component tag, whose + * rules live in a component's stylesheet, and that stylesheet (check ii). `data-md-list-row` + * on an `
  • `, a `
    `, a `` or a layout component such as `` draws its hover, + * focus and press state layer and its `data-md-selected` fill from list-item.css, which no tag + * asks for when the page renders no `` and no ``. `except` names the + * package tags that draw the hook themselves: `` is card.css's row. + * + * Nothing else that list-rows.js, the skill or the README has an application write needs a + * stylesheet of its own: `data-md-list-open` and a row's `data-md-selected` are drawn only + * through the row, a selected row in `` by table.css, `data-md-dragged` by + * foundation.css's `md-state-layer` (and card.css on ``), and `data-md-field-control` + * by field.css inside ``. + */ + protected const array HOOK_STYLESHEETS = [ + 'data-md-list-row' => ['stylesheet' => 'components/list-item.css', 'except' => ['card']], + ]; + /** 1.x's semantic ink names, and the M3 role each resolved to (UPGRADE.md § 4). */ protected const array INK_ALIASES = [ 'body' => 'on-surface-variant', @@ -335,15 +356,16 @@ class DesignGuard /** * Check (ii): every package component tag used in a scanned view — unprefixed, under the - * configured prefix, or `` — and every `->links()` call, against - * `$cssEntry`'s `@import` graph (followed through each package file's own imports). A missing - * one names the `@import` line to add; a tag the application shadows with its own component - * of the same name is reported instead, since the package's stylesheet is then moot. Each - * missing stylesheet and each shadowed tag is reported once, at its first use. Only the - * imports are read here: the literal values in the application's own CSS are check (iii)'s, - * which reads the `.css` files `scan()` is given, entry or not — so an application part-way - * through its migration can check its imports before its stylesheets are on tokens. The - * classes the entry's own imports declare do join check (i)'s exemptions. + * configured prefix, or `` — every `->links()` call, and every hook in + * `HOOK_STYLESHEETS` a view outside the package writes on its own markup (a row's + * `data-md-list-row`), against `$cssEntry`'s `@import` graph (followed through each package + * file's own imports). A missing one names the `@import` line to add; a tag the application + * shadows with its own component of the same name is reported instead, since the package's + * stylesheet is then moot. Each missing stylesheet and each shadowed tag is reported once, at + * its first use. Only the imports are read here: the literal values in the application's own + * CSS are check (iii)'s, which reads the `.css` files `scan()` is given, entry or not — so an + * application part-way through its migration can check its imports before its stylesheets + * are on tokens. The classes the entry's own imports declare do join check (i)'s exemptions. */ public function missingStylesheets(string $cssEntry): static { @@ -355,10 +377,11 @@ class DesignGuard /** * Check (ii) the other way round: each package stylesheet `$cssEntry` imports directly that no - * scanned Blade view needs — no package tag it renders, no `->links()`, and not reached through - * the imports of a stylesheet that is needed — named at its `@import` line, to remove. A - * component whose last use left the views otherwise keeps its CSS in every page. `foundation.css` - * is always needed; an entry that imports `all.css` has chosen everything and is not read. + * scanned Blade view needs — no package tag it renders, no `->links()`, no hook it writes by + * hand (`HOOK_STYLESHEETS`), and not reached through the imports of a stylesheet that is + * needed — named at its `@import` line, to remove. A component whose last use left the views + * otherwise keeps its CSS in every page. `foundation.css` is always needed; an entry that + * imports `all.css` has chosen everything and is not read. */ public function unusedStylesheets(string $cssEntry): static { @@ -405,6 +428,7 @@ class DesignGuard $where = $this->relative($file->getPathname()); $isBlade = str_ends_with($file->getFilename(), '.blade.php'); $readsClasses = ! $this->isUnder($file->getPathname(), $mailPaths); + $readsHooks = ! $this->isUnder($file->getPathname(), array_filter([static::packageViewRoot()])); if ($isBlade) { $contents = $this->withoutBladeComments($contents); @@ -432,13 +456,13 @@ class DesignGuard } if ($this->cssEntry !== null) { - foreach ($this->missingStylesheetViolations($contents, $resolved, $reported) as [$line, $what]) { + foreach ($this->missingStylesheetViolations($contents, $resolved, $reported, $readsHooks) as [$line, $what]) { $violations[] = "{$where}:{$line} {$what}"; } } if ($this->unusedEntry !== null) { - $needed = [...$needed, ...$this->neededStylesheets($contents)]; + $needed = [...$needed, ...$this->neededStylesheets($contents, $readsHooks)]; } } @@ -471,12 +495,13 @@ class DesignGuard } /** - * The package stylesheets a Blade view's own tags and `->links()` calls need, before their - * imports are followed. + * The package stylesheets a Blade view's own tags, `->links()` calls and, unless `$readsHooks` + * is false (a package view), the hooks it writes by hand need, before their imports are + * followed. * * @return list */ - protected function neededStylesheets(string $contents): array + protected function neededStylesheets(string $contents, bool $readsHooks = true): array { $needed = []; @@ -490,6 +515,10 @@ class DesignGuard } } + if ($readsHooks) { + $needed = [...$needed, ...array_column($this->hookUsages($contents), 2)]; + } + if ($this->paginationUsages($contents) !== []) { $needed[] = static::packageCssRoot().'/components/pagination.css'; } @@ -1502,19 +1531,74 @@ class DesignGuard return array_map(fn (array $match): int => substr_count(substr($contents, 0, $match[1]), "\n") + 1, $matches[0]); } + /** + * Every hook in `HOOK_STYLESHEETS` a Blade view writes, each as [line, hook, stylesheet]: an + * attribute of a plain element or of a component tag other than those `except` names (blanked + * out first), or a name a PHP array or a script sets. A selector (`[data-md-list-row]`), a name + * in backticks and a longer name that starts with the hook write no hook. + * + * @return list + */ + protected function hookUsages(string $contents): array + { + $found = []; + + foreach (self::HOOK_STYLESHEETS as $hook => ['stylesheet' => $stylesheet, 'except' => $except]) { + if (! str_contains($contents, $hook)) { + continue; + } + + $markup = $this->withoutPackageTags($contents, $except); + + preg_match_all('/(?` — blanked out, line breaks kept. A quoted + * attribute value or a `{{ … }}` inside the tag may hold a `>`. + * + * @param list $names + */ + protected function withoutPackageTags(string $contents, array $names): string + { + if ($names === []) { + return $contents; + } + + $prefix = (string) config('livewire-material.prefix', ''); + $namespaces = $prefix !== '' ? ['livewire-material', $prefix] : ['livewire-material']; + + return (string) preg_replace_callback( + '/alternation($namespaces).')::)?(?:'.$this->alternation($names).')(?=[\s\/>])(?:\{\{.*?\}\}|"[^"]*"|\'[^\']*\'|[^>"\'])*+>?/s', + fn (array $tag): string => (string) preg_replace('/[^\n]/', ' ', $tag[0]), + $contents, + ); + } + /** * Check (ii)'s findings for one file: a package tag whose stylesheet `$resolved` (every file * `Stylesheets::resolvedFiles()` reached from the CSS entry, keyed by real path) does not - * contain, a tag the application shadows, and a `->links()` needing `pagination.css` — each - * only the first time `$reported` (shared across every file of one `violations()` run) sees it. + * contain, a tag the application shadows, a hook written by hand whose stylesheet it does not + * contain (unless `$readsHooks` is false, for a package view), and a `->links()` needing + * `pagination.css` — each only the first time `$reported` (shared across every file of one + * `violations()` run) sees it, at the earliest line in the file that needs it. * * @param array $resolved * @param array $reported * @return list */ - protected function missingStylesheetViolations(string $contents, array $resolved, array &$reported): array + protected function missingStylesheetViolations(string $contents, array $resolved, array &$reported, bool $readsHooks = true): array { $found = []; + $needs = []; foreach ($this->packageTagUsages($contents) as [$line, $name, $spelling]) { if ($spelling === 'plain' && $this->shadowedByApplication($name)) { @@ -1526,29 +1610,32 @@ class DesignGuard continue; } - $stylesheet = static::packageStylesheetFor($name); - - if ($stylesheet !== null && ! isset($resolved[$stylesheet]) && ! isset($reported[$stylesheet])) { - $reported[$stylesheet] = true; - $found[] = [$line, sprintf( - "`` needs `%s`, missing from %s — add `@import '%s';`", - $name, - static::packageRelativeName($stylesheet), - $this->relative($this->cssEntry), - $this->importLine($stylesheet), - )]; + if (($stylesheet = static::packageStylesheetFor($name)) !== null) { + $needs[] = [$line, "``", $stylesheet]; } } - $pagination = static::packageCssRoot().'/components/pagination.css'; + if ($readsHooks) { + foreach ($this->hookUsages($contents) as [$line, $hook, $stylesheet]) { + $needs[] = [$line, "`{$hook}`", $stylesheet]; + } + } foreach ($this->paginationUsages($contents) as $line) { - if (! isset($resolved[$pagination]) && ! isset($reported[$pagination])) { - $reported[$pagination] = true; + $needs[] = [$line, '`->links()`', static::packageCssRoot().'/components/pagination.css']; + } + + usort($needs, fn (array $a, array $b): int => $a[0] <=> $b[0]); + + foreach ($needs as [$line, $what, $stylesheet]) { + if (! isset($resolved[$stylesheet]) && ! isset($reported[$stylesheet])) { + $reported[$stylesheet] = true; $found[] = [$line, sprintf( - "`->links()` needs `components/pagination.css`, missing from %s — add `@import '%s';`", + "%s needs `%s`, missing from %s — add `@import '%s';`", + $what, + static::packageRelativeName($stylesheet), $this->relative($this->cssEntry), - $this->importLine($pagination), + $this->importLine($stylesheet), )]; } } @@ -1699,6 +1786,15 @@ class DesignGuard return (string) realpath(dirname(__DIR__, 2).'/resources/views/components'); } + /** + * This package's own `resources/views`: its components, showcase, pagination and error pages. + * A hook written there is a tag's own, so check (ii) reads hooks only outside it. + */ + protected static function packageViewRoot(): string + { + return (string) realpath(dirname(__DIR__, 2).'/resources/views'); + } + protected static function isPackageTag(string $name): bool { $root = static::packageComponentViewRoot(); diff --git a/tests/Feature/DesignGuardTest.php b/tests/Feature/DesignGuardTest.php index f397b417..28a49644 100644 --- a/tests/Feature/DesignGuardTest.php +++ b/tests/Feature/DesignGuardTest.php @@ -583,6 +583,39 @@ it('needs pagination.css when a view calls ->links()', function () { ]); }); +it('needs list-item.css for a row written by hand, and leaves a card row to card.css', function () { + // rows.blade.php writes `data-md-list-row` on an
  • and a and renders no package tag; + // card-rows.blade.php writes it only on , whose row card.css draws, and in a selector. + $rows = realpath(GUARD_FIXTURES.'/stylesheets/views/rows.blade.php'); + $cardRows = realpath(GUARD_FIXTURES.'/stylesheets/views/card-rows.blade.php'); + $foundationOnly = realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css'); + $entry = realpath(GUARD_FIXTURES.'/stylesheets/rows.css'); + + expect(fixtureRelative(DesignGuard::scan($rows)->missingStylesheets($foundationOnly)->violations()))->toBe([ + "stylesheets/views/rows.blade.php:3 `data-md-list-row` needs `components/list-item.css`, missing from stylesheets/foundation-only.css — add `@import '../../../../resources/css/components/list-item.css';`", + ]) + ->and(fixtureRelative(DesignGuard::scan($rows)->unusedStylesheets($entry)->violations()))->toBe([ + "stylesheets/rows.css:2 `components/card.css` is imported, but no scanned view renders a component that needs it — remove `@import '../../../../resources/css/components/card.css';`", + ]) + ->and(fixtureRelative(DesignGuard::scan($cardRows)->missingStylesheets($foundationOnly)->violations()))->toBe([ + "stylesheets/views/card-rows.blade.php:1 `` needs `components/card.css`, missing from stylesheets/foundation-only.css — add `@import '../../../../resources/css/components/card.css';`", + ]) + ->and(fixtureRelative(DesignGuard::scan($cardRows)->unusedStylesheets($entry)->violations()))->toBe([ + "stylesheets/rows.css:3 `components/list-item.css` is imported, but no scanned view renders a component that needs it — remove `@import '../../../../resources/css/components/list-item.css';`", + ]); +}); + +it('reads a row written by hand in the application\'s views only, not in the package\'s own', function () { + // list-item.blade.php and card.blade.php write `data-md-list-row` for the tags that render them. + $components = realpath(__DIR__.'/../../resources/views/components'); + + $violations = DesignGuard::scan([$components.'/list-item.blade.php', $components.'/card.blade.php']) + ->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css')) + ->violations(); + + expect(array_values(array_filter($violations, fn (string $violation): bool => str_contains($violation, '`data-md-list-row`'))))->toBe([]); +}); + it('reports a package tag the application shadows with its own anonymous component', function () { $views = sys_get_temp_dir().'/livewire-material-guard-shadow-'.uniqid(); File::ensureDirectoryExists($views.'/components'); diff --git a/tests/Fixtures/design-guard/stylesheets/rows.css b/tests/Fixtures/design-guard/stylesheets/rows.css new file mode 100644 index 00000000..9b9db9a4 --- /dev/null +++ b/tests/Fixtures/design-guard/stylesheets/rows.css @@ -0,0 +1,3 @@ +@import '../../../../resources/css/foundation.css'; +@import '../../../../resources/css/components/card.css'; +@import '../../../../resources/css/components/list-item.css'; diff --git a/tests/Fixtures/design-guard/stylesheets/views/card-rows.blade.php b/tests/Fixtures/design-guard/stylesheets/views/card-rows.blade.php new file mode 100644 index 00000000..893790fb --- /dev/null +++ b/tests/Fixtures/design-guard/stylesheets/views/card-rows.blade.php @@ -0,0 +1,9 @@ + + Report + + + Report + + diff --git a/tests/Fixtures/design-guard/stylesheets/views/rows.blade.php b/tests/Fixtures/design-guard/stylesheets/views/rows.blade.php new file mode 100644 index 00000000..4faac088 --- /dev/null +++ b/tests/Fixtures/design-guard/stylesheets/views/rows.blade.php @@ -0,0 +1,11 @@ +{{-- A row written by hand: `data-md-list-row` in a comment writes none. --}} + + + + + +
    Report