$violations * @return list */ function fixtureRelative(array $violations): array { return array_map(fn (string $violation): string => str_replace(realpath(GUARD_FIXTURES).'/', '', $violation), $violations); } /** * Check (i)'s message for one class: it names no replacement, since a class no stylesheet declares * is dead whatever it was once meant to do. */ function undeclared(string $class): string { return "class `{$class}` is declared in no stylesheet — the package declares `md-*`, the rest is your own CSS"; } /** * Every check that only scans a fixture, optionally names the CSS entry `missingStylesheets()` * should check against, and compares the result to an exact violations list — most of this file, * kept as one `it()` over a `[fixture, entry, expected]` dataset instead of one near-identical test * per check. A test that does more (chains `forbidColours()`/`forbid()`/`unusedStylesheets()`, * compares two scans against each other, mutates config or the view finder, or asserts more than * once) stays its own test, further down. */ dataset('design guard scans', [ // app/Status.php returns class names from a PHP `match`, and views/page.blade.php writes one in // running text and in a Blade comment: none of the three is a class list, so none is read. 'finds what compiles to nothing' => [ [realpath(GUARD_FIXTURES.'/app'), realpath(GUARD_FIXTURES.'/views')], null, [ 'views/page.blade.php:2 '.undeclared('p-4'), 'views/page.blade.php:3 unknown Material Symbol `o-home`', 'views/page.blade.php:4 unknown Material Symbol `not_a_symbol`', 'views/page.blade.php:6 '.undeclared('focus-ring'), 'views/page.blade.php:6 '.undeclared('text-tertiary'), 'views/page.blade.php:8 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`', 'views/page.blade.php:8 '.undeclared('size-4'), ], ], 'passes a class the application\'s own stylesheet declares, and reports the one it does not' => [ [realpath(GUARD_FIXTURES.'/declared/view.blade.php'), realpath(GUARD_FIXTURES.'/declared/app.css')], null, ['declared/view.blade.php:1 '.undeclared('mt-2')], ], 'reads a class list in an attribute, an Alpine binding and a PHP one, but not a string a condition compares or a call takes' => [ [realpath(GUARD_FIXTURES.'/declared/bindings.blade.php'), realpath(GUARD_FIXTURES.'/declared/app.css')], null, [ 'declared/bindings.blade.php:1 '.undeclared('collapsed'), 'declared/bindings.blade.php:1 '.undeclared('dashboard-busy'), 'declared/bindings.blade.php:1 '.undeclared('is-open'), 'declared/bindings.blade.php:2 '.undeclared('dashboard-enter-start'), 'declared/bindings.blade.php:2 '.undeclared('dashboard-enter'), 'declared/bindings.blade.php:3 '.undeclared('share-list'), 'declared/bindings.blade.php:4 '.undeclared('dashboard-lead'), 'declared/bindings.blade.php:6 '.undeclared('share-list'), ], ], 'reads the keys of a class list, the array a class helper is given and every branch of a ternary, never a string that indexes an array' => [ [realpath(GUARD_FIXTURES.'/declared/lookups.blade.php'), realpath(GUARD_FIXTURES.'/declared/app.css')], null, [ 'declared/lookups.blade.php:4 '.undeclared('product-shot-base'), 'declared/lookups.blade.php:8 '.undeclared('routes-show-run-chosen'), 'declared/lookups.blade.php:9 '.undeclared('shot-helper-one'), 'declared/lookups.blade.php:9 '.undeclared('shot-helper-two'), 'declared/lookups.blade.php:10 '.undeclared('shot-tern-a'), 'declared/lookups.blade.php:10 '.undeclared('shot-tern-b'), 'declared/lookups.blade.php:10 '.undeclared('shot-tern-c'), ], ], 'passes every class the package itself declares, showcase included' => [ realpath(GUARD_FIXTURES.'/declared/package-classes.blade.php'), null, [], ], 'names what a component tag takes in place of each directive' => [ realpath(GUARD_FIXTURES.'/directives'), null, [ 'directives/tags.blade.php:1 Blade directive `@js` inside a component tag, where it does not compile — use `{{ \\Illuminate\\Support\\Js::from(…) }}`', 'directives/tags.blade.php:2 Blade directive `@json` inside a component tag, where it does not compile — use `{{ json_encode(…) }}`', 'directives/tags.blade.php:3 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`', 'directives/tags.blade.php:3 '.undeclared('is-open'), 'directives/tags.blade.php:4 Blade directive `@style` inside a component tag, where it does not compile — use `:style="\\Illuminate\\Support\\Arr::toCssStyles([…])"`', "directives/tags.blade.php:5 Blade directive `@entangle` inside a component tag, where it does not compile — use `\$wire.entangle('…')` in the Alpine expression", 'directives/tags.blade.php:6 Blade directive `@disabled` inside a component tag, where it does not compile — use `:disabled="…"`', 'directives/tags.blade.php:7 Blade directive `@if` inside a component tag, where it does not compile — use a `:prop` binding or `{{ }}`, or move it to a plain element inside the slot', ], ], 'leaves every role alone until an application forbids one' => [ realpath(GUARD_FIXTURES.'/forbidden-colours'), null, [ 'forbidden-colours/page.blade.php:5 '.undeclared('bg-tertiary'), ], ], 'names the missing package stylesheet and the exact @import line to add' => [ realpath(GUARD_FIXTURES.'/stylesheets/views/split-button.blade.php'), realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css'), [ "stylesheets/views/split-button.blade.php:1 `` needs `components/split-button.css`, missing from stylesheets/foundation-only.css — add `@import '../../../../resources/css/components/split-button.css';`", ], ], 'counts what an imported package stylesheet already brings in, through its own imports' => [ realpath(GUARD_FIXTURES.'/stylesheets/views/dependencies.blade.php'), realpath(GUARD_FIXTURES.'/stylesheets/split-button.css'), [], ], 'needs nothing more once the entry imports all.css' => [ realpath(GUARD_FIXTURES.'/stylesheets/views'), realpath(GUARD_FIXTURES.'/stylesheets/all.css'), [], ], 'says so when the CSS entry does not exist' => [ realpath(GUARD_FIXTURES.'/stylesheets/views/plain.blade.php'), '/nowhere/app.css', ['/nowhere/app.css:1 the CSS entry `missingStylesheets()` names does not exist'], ], 'requires foundation.css whatever the views hold' => [ realpath(GUARD_FIXTURES.'/stylesheets/views/plain.blade.php'), realpath(GUARD_FIXTURES.'/stylesheets/missing-foundation.css'), [ "stylesheets/missing-foundation.css:1 the CSS entry never imports `foundation.css`, required by every package stylesheet — add `@import '../../../../resources/css/foundation.css';`", ], ], 'recognises a package tag written fully qualified' => [ realpath(GUARD_FIXTURES.'/stylesheets/views/namespaced.blade.php'), realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css'), [ "stylesheets/views/namespaced.blade.php:1 `` needs `components/button.css`, missing from stylesheets/foundation-only.css — add `@import '../../../../resources/css/components/button.css';`", ], ], 'needs pagination.css when a view calls ->links()' => [ realpath(GUARD_FIXTURES.'/stylesheets/views/pagination.blade.php'), realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css'), [ "stylesheets/views/pagination.blade.php:1 `->links()` needs `components/pagination.css`, missing from stylesheets/foundation-only.css — add `@import '../../../../resources/css/components/pagination.css';`", ], ], "finds every literal design value and off-scale media query in the application's own CSS" => [ realpath(GUARD_FIXTURES.'/app-css/app.css'), null, [ '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:5 literal radius `border-radius: 12px` — use `var(--md-sys-shape-corner-*)`', 'app-css/app.css:6 literal colour `rgba(0, 0, 0, 0.2)` — use `var(--md-sys-color-*)`', '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` — set the whole style with `font: var(--md-sys-typescale-*)` and its `-tracking`, or an `md-type-*` class', 'app-css/app.css:8 literal font weight `font-weight: 600` — use an `md-type-emphasized-*` class, or `var(--md-ref-typeface-weight-regular|medium|bold)`', 'app-css/app.css:9 literal line height `line-height: 1.4` — set the whole style with `font: var(--md-sys-typescale-*)`, or an `md-type-*` class', 'app-css/app.css:10 literal letter spacing `letter-spacing: 0.02em` — use `var(--md-sys-typescale-*-tracking)`', '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: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:24 literal colour `white` — use `var(--md-sys-color-*)`', 'app-css/app.css:25 literal colour `#fff` — use `var(--md-sys-color-*)`', "app-css/app.css:34 media query width `700px` is not one of M3's breakpoints — use 600, 840, 1200 or 1600px (medium, expanded, large, extra-large) with `>=` or `<`", 'app-css/app.css:43 literal radius `border-top-left-radius: calc(8px + 4px)` — use `var(--md-sys-shape-corner-*)`', 'app-css/app.css:46 literal font `font: 600 14px/20px sans-serif` — use `font: var(--md-sys-typescale-*)` with its `-tracking`, or an `md-type-*` class', 'app-css/app.css:47 literal font size `font-size: clamp(1rem, 2vw, 2rem)` — set the whole style with `font: var(--md-sys-typescale-*)` and its `-tracking`, or an `md-type-*` class', 'app-css/app.css:48 literal line height `line-height: 1.5` — set the whole style with `font: var(--md-sys-typescale-*)`, or an `md-type-*` class', "app-css/app.css:51 media query width `839px` is not one of M3's breakpoints — use 600, 840, 1200 or 1600px (medium, expanded, large, extra-large) with `>=` or `<`", "app-css/app.css:53 media query width `40rem` is not one of M3's breakpoints — use 600, 840, 1200 or 1600px (medium, expanded, large, extra-large) with `>=` or `<`", ], ], // SealShare's drop zone pairs a spatial spring for scale with an effects spring for opacity in // one transition-duration list; blanking both var()s used to leave a bare comma the guard called // literal. 'accepts a list of values when every item is a token, and quotes a mixed one as written' => [ realpath(GUARD_FIXTURES.'/css-lists/lists.css'), null, [ 'css-lists/lists.css:8 literal duration `transition-duration: var(--md-sys-motion-spatial-slow-duration), 200ms` — use `var(--md-sys-motion-…-duration)`, paired with its easing', 'css-lists/lists.css:9 literal easing `transition-timing-function: var(--md-sys-motion-spatial-slow), ease-out` — use `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)`, paired with its `-duration`', ], ], 'leaves a box-shadow ring in a colour role alone, but still reports a blurred or literal-coloured one' => [ realpath(GUARD_FIXTURES.'/shadows/rings.css'), null, [ 'shadows/rings.css:16 literal colour `rgba(0, 0, 0, 0.2)` — use `var(--md-sys-color-*)`', 'shadows/rings.css:16 literal shadow `box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2)` — use `var(--md-sys-elevation-*)`', 'shadows/rings.css:20 literal colour `#000` — use `var(--md-sys-color-*)`', 'shadows/rings.css:20 literal shadow `box-shadow: inset 0 0 4px 1px #000` — use `var(--md-sys-elevation-*)`', ], ], ]); it('scans a fixture and reports exactly the expected violations', function (string|array $fixture, ?string $entry, array $expected) { $guard = DesignGuard::scan($fixture); expect(fixtureRelative(($entry === null ? $guard : $guard->missingStylesheets($entry))->violations()))->toBe($expected); })->with('design guard scans'); it('leaves a Markdown mail component\'s classes and its theme to the mail theme, but still reads its icons and directives', function () { $mail = realpath(GUARD_FIXTURES.'/mail/views'); $icon = 'mail/views/vendor/mail/html/sessions.blade.php:7 unknown Material Symbol `not_a_symbol`'; $directive = 'mail/views/vendor/mail/html/sessions.blade.php:8 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`'; config(['mail.markdown.paths' => [$mail.'/vendor/mail']]); // The theme's `.table` declares nothing outside the mail components, and its literals are its own. expect(fixtureRelative(DesignGuard::scan($mail)->violations()))->toBe([ 'mail/views/page.blade.php:1 '.undeclared('table'), $icon, $directive, ]); // Outside a mail component path the same files read as the application's own: the theme's // classes against its own rules, which declare `table` and nothing else, and its literals as // check (iii)'s. config(['mail.markdown.paths' => []]); expect(fixtureRelative(DesignGuard::scan($mail)->violations()))->toBe([ 'mail/views/vendor/mail/html/sessions.blade.php:3 '.undeclared('sessions'), 'mail/views/vendor/mail/html/sessions.blade.php:3 '.undeclared('text-sm'), 'mail/views/vendor/mail/html/sessions.blade.php:4 '.undeclared('break-all'), $icon, $directive, 'mail/views/vendor/mail/html/sessions.blade.php:8 '.undeclared('wide'), 'mail/views/vendor/mail/html/themes/default.css:2 literal colour `#3d4852` — use `var(--md-sys-color-*)`', 'mail/views/vendor/mail/html/themes/default.css:3 literal font size `font-size: 14px` — set the whole style with `font: var(--md-sys-typescale-*)` and its `-tracking`, or an `md-type-*` class', ]); }); it('bans the roles an application leaves out, wherever 2.0.0 writes one', function () { $violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/forbidden-colours')) ->forbidColours(['tertiary', 'primary-container', 'error']) ->violations()); expect($violations)->toBe([ "forbidden-colours/app.css:3 `--md-sys-color-tertiary-container`: role `tertiary` is not part of this application's palette", "forbidden-colours/app.css:4 `--md-sys-color-on-primary-container`: role `primary-container` is not part of this application's palette", "forbidden-colours/page.blade.php:1 `color=\"tertiary\"`: role `tertiary` is not part of this application's palette", "forbidden-colours/page.blade.php:2 `:tone=\"'tertiary'\"`: role `tertiary` is not part of this application's palette", "forbidden-colours/page.blade.php:3 `:color=\"\$failed ? 'error' : 'info'\"`: role `error` is not part of this application's palette", "forbidden-colours/page.blade.php:4 `--md-sys-color-on-tertiary-container`: role `tertiary` is not part of this application's palette", "forbidden-colours/page.blade.php:4 `md-ink-error`: role `error` is not part of this application's palette", 'forbidden-colours/page.blade.php:5 '.undeclared('bg-tertiary'), "forbidden-colours/page.blade.php:7 `--md-sys-color-primary-container`: role `primary-container` is not part of this application's palette", ]); }); it('bans any further pattern', function () { $violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/views')) ->forbid('/\bfocus-ring\b/', 'a retired utility') ->violations()); expect($violations)->toContain('views/page.blade.php:6 a retired utility'); }); it('names each stylesheet the entry imports that no scanned view needs, and leaves all.css alone', function () { // dependencies.blade.php renders , and : menu.css is needed // (and reached through split-button.css too); card.css and stack.css serve nothing on the page. $entry = realpath(GUARD_FIXTURES.'/stylesheets/unused.css'); expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/dependencies.blade.php'))->unusedStylesheets($entry)->violations()))->toBe([ "stylesheets/unused.css:4 `components/card.css` is imported, but no scanned view renders a component that needs it — remove `@import '../../../../resources/css/components/card.css';`", "stylesheets/unused.css:5 `layout/stack.css` is imported, but no scanned view renders a component that needs it — remove `@import '../../../../resources/css/layout/stack.css';`", ]) // missingStylesheets() alone never reports an unused import. ->and(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/dependencies.blade.php'))->missingStylesheets($entry)->violations())->toBe([]) ->and(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/dependencies.blade.php'))->unusedStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/all.css'))->violations())->toBe([]); }); it('follows an entry that imports what only a Vite build resolves, reports each missing stylesheet once, and leaves the entry\'s literals to scan()', function () { $entry = realpath(GUARD_FIXTURES.'/stylesheets/foreign-imports.css'); $mt2 = 'stylesheets/views-foreign/page.blade.php:1 '.undeclared('mt-2'); $button = "stylesheets/views-foreign/page.blade.php:2 `` needs `components/button.css`, missing from stylesheets/foreign-imports.css — add `@import '../../../../resources/css/components/button.css';`"; expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views-foreign'))->missingStylesheets($entry)->violations())) ->toBe([$mt2, $button]) ->and(fixtureRelative(DesignGuard::scan([realpath(GUARD_FIXTURES.'/stylesheets/views-foreign'), $entry])->missingStylesheets($entry)->violations())) ->toBe(['stylesheets/foreign-imports.css:7 literal colour `#ff0000` — use `var(--md-sys-color-*)`', $mt2, $button]); }); it('recognises a package tag written under the configured prefix', function () { config(['livewire-material.prefix' => 'm']); $violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/prefixed.blade.php')) ->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css')) ->violations()); expect($violations)->toBe([ "stylesheets/views/prefixed.blade.php:1 `` needs `components/button.css`, missing from stylesheets/foundation-only.css — add `@import '../../../../resources/css/components/button.css';`", ]); }); 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'); File::put($views.'/components/button.blade.php', 'the application\'s button'); View::getFinder()->prependLocation($views); try { $violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/shadow.blade.php')) ->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/all.css')) ->violations()); } finally { File::deleteDirectory($views); } expect($violations)->toBe([ "stylesheets/views/shadow.blade.php:1 `` is shadowed by the application's own component of the same name — the package's `` never renders here", ]); }); it('reports a package tag the application shadows with its own component class', function () { // Under a namespace of its own, which Blade's class guess reads from the application, so the // class cannot shadow `` for any later test in this process. (fn () => $this->namespace = 'DesignGuardShadow\\')->call(app()); if (! class_exists('DesignGuardShadow\View\Components\Card', false)) { eval('namespace DesignGuardShadow\View\Components; class Card {}'); } $violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/shadow-class.blade.php')) ->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/all.css')) ->violations()); expect($violations)->toBe([ "stylesheets/views/shadow-class.blade.php:1 `` is shadowed by the application's own component of the same name — the package's `` never renders here", ]); }); 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('sorts every finding by path, then line, then message, whichever check produced it', function () { $dir = sys_get_temp_dir().'/livewire-material-guard-sort-'.uniqid(); File::ensureDirectoryExists($dir); $dir = realpath($dir); // check (iii)'s application-CSS pass always runs last, after every view's own violations — // an unsorted result would put a.css's finding after z.blade.php's, even though "a.css" // sorts before "z.blade.php" (Finder itself already walks views alphabetically, so this is // the one place natural order and sorted order actually diverge). File::put($dir.'/a.css', "body {\n color: #ff0000;\n}\n"); File::put($dir.'/z.blade.php', '
    '); try { $violations = DesignGuard::scan($dir)->violations(); } finally { File::deleteDirectory($dir); } expect($violations)->toBe([ "{$dir}/a.css:2 literal colour `#ff0000` — use `var(--md-sys-color-*)`", "{$dir}/z.blade.php:1 ".undeclared('bg-red-500'), "{$dir}/z.blade.php:1 ".undeclared('p-4'), ]); }); it('passes the package\'s own views, showcase and Workbench, every stylesheet imported through all.css', function () { $root = realpath(__DIR__.'/../..'); $paths = [$root.'/resources/views', $root.'/resources/js', $root.'/src', $root.'/workbench/resources']; // The package's own Markdown mail components are the shape `mail.markdown.paths` names: their // classes come from mail/theme.blade.php, a stylesheet Blade renders, not from a CSS entry. config(['mail.markdown.paths' => [$root.'/resources/views/mail']]); expect(DesignGuard::scan($paths)->missingStylesheets($root.'/resources/css/all.css')->violations())->toBe([]) // …and the stylesheet check is not vacuous there: the showcase's plain tags do need imports. ->and(DesignGuard::scan($root.'/resources/views/showcase')->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css'))->violations())->not->toBe([]); });