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
451 lines
33 KiB
PHP
451 lines
33 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\View;
|
|
use NoNameWeb\LivewireMaterial\Support\Stylesheets;
|
|
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
|
|
|
|
const GUARD_FIXTURES = __DIR__.'/../Fixtures/design-guard';
|
|
|
|
beforeEach(fn () => Stylesheets::resetCache());
|
|
|
|
/**
|
|
* @param list<string> $violations
|
|
* @return list<string>
|
|
*/
|
|
function fixtureRelative(array $violations): array
|
|
{
|
|
return array_map(fn (string $violation): string => str_replace(realpath(GUARD_FIXTURES).'/', '', $violation), $violations);
|
|
}
|
|
|
|
it('finds what compiles to nothing', function () {
|
|
$violations = DesignGuard::scan([realpath(GUARD_FIXTURES.'/app'), realpath(GUARD_FIXTURES.'/views')])->violations();
|
|
|
|
expect(fixtureRelative($violations))->toBe([
|
|
'app/Status.php:14 colour the theme does not declare `text-red-500`',
|
|
'views/page.blade.php:3 daisyUI class `card`',
|
|
"views/page.blade.php:3 Tailwind spacing utility `p-4` compiles to nothing — use a layout component's `gap`/`padding` prop (`<x-stack gap>`, `<x-surface padding>`), or `var(--md-sys-measurement-space*)` in your own CSS",
|
|
'views/page.blade.php:6 daisyUI class `btn-primary`',
|
|
'views/page.blade.php:9 Tailwind sizing utility `size-4` compiles to nothing — M3 keeps no size scale; write the literal length in your own CSS, or use `<x-pane width>`/`<x-grid min-item>` where it fits',
|
|
'views/page.blade.php:4 unknown Material Symbol `o-home`',
|
|
'views/page.blade.php:5 unknown Material Symbol `not_a_symbol`',
|
|
'views/page.blade.php:9 Blade directive `@class` inside a component tag, where it does not compile',
|
|
'views/page.blade.php:2 maryUI component `<x-mary-button`',
|
|
'views/page.blade.php:3 colour the theme does not declare `bg-base-200`',
|
|
'views/page.blade.php:6 colour the theme does not declare `text-red-600`',
|
|
]);
|
|
});
|
|
|
|
it('names the M3 breakpoint for a Tailwind prefix, with its 2.0.0 replacement', function () {
|
|
expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/breakpoints'))->violations()))->toBe([
|
|
"breakpoints/layout.blade.php:1 Tailwind breakpoint `sm:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 600px)` in your own CSS",
|
|
"breakpoints/layout.blade.php:1 Tailwind breakpoint `md:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 600px)` in your own CSS",
|
|
"breakpoints/layout.blade.php:1 Tailwind breakpoint `lg:` compiles to nothing — M3's expanded (840px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 840px)` in your own CSS",
|
|
"breakpoints/layout.blade.php:1 Tailwind breakpoint `xl:` compiles to nothing — M3's large (1200px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 1200px)` in your own CSS",
|
|
"breakpoints/layout.blade.php:1 Tailwind breakpoint `2xl:` compiles to nothing — M3's extra-large (1600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 1600px)` in your own CSS",
|
|
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-sm:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 600px)` in your own CSS",
|
|
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-md:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 600px)` in your own CSS",
|
|
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-lg:` compiles to nothing — M3's expanded (840px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 840px)` in your own CSS",
|
|
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-xl:` compiles to nothing — M3's large (1200px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 1200px)` in your own CSS",
|
|
"breakpoints/layout.blade.php:2 Tailwind breakpoint `max-2xl:` compiles to nothing — M3's extra-large (1600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width < 1600px)` in your own CSS",
|
|
]);
|
|
});
|
|
|
|
it('names the 2.0.0 replacement for a pseudo-class variant, but leaves a breakpoint prefix to its own finding', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/variant.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
"tailwind/variant.blade.php:1 Tailwind variant class `hover:underline` compiles to nothing — a state is `:has()`/`aria-*`/`data-md-*` in your own CSS, a breakpoint a layout component's `hide-below`/`hide-from`/`stack-below` prop",
|
|
"tailwind/variant.blade.php:1 Tailwind variant class `dark:opacity-50` compiles to nothing — a state is `:has()`/`aria-*`/`data-md-*` in your own CSS, a breakpoint a layout component's `hide-below`/`hide-from`/`stack-below` prop",
|
|
"tailwind/variant.blade.php:2 Tailwind variant class `group-hover:flex` compiles to nothing — a state is `:has()`/`aria-*`/`data-md-*` in your own CSS, a breakpoint a layout component's `hide-below`/`hide-from`/`stack-below` prop",
|
|
"tailwind/variant.blade.php:2 Tailwind variant class `focus:md:gap-2` compiles to nothing — a state is `:has()`/`aria-*`/`data-md-*` in your own CSS, a breakpoint a layout component's `hide-below`/`hide-from`/`stack-below` prop",
|
|
"tailwind/variant.blade.php:2 Tailwind breakpoint `md:` compiles to nothing — M3's medium (600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 600px)` in your own CSS",
|
|
]);
|
|
});
|
|
|
|
it('leaves a breakpoint name that is not a variant alone', function () {
|
|
$violations = DesignGuard::scan(__DIR__.'/../../resources/js/slider.js')->violations();
|
|
|
|
expect($violations)->toBe([]);
|
|
});
|
|
|
|
it('names the M3 corner for a Tailwind radius', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/scale/corners.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
'scale/corners.blade.php:1 value outside the M3 scale `rounded-none` — use `var(--md-sys-shape-corner-none)` in your own CSS, or `<x-surface corner="none">`',
|
|
'scale/corners.blade.php:1 value outside the M3 scale `rounded-xs` — use `var(--md-sys-shape-corner-xs)` in your own CSS, or `<x-surface corner="xs">`',
|
|
'scale/corners.blade.php:1 value outside the M3 scale `rounded-sm` — use `var(--md-sys-shape-corner-sm)` in your own CSS, or `<x-surface corner="sm">`',
|
|
'scale/corners.blade.php:1 value outside the M3 scale `rounded-md` — use `var(--md-sys-shape-corner-md)` in your own CSS, or `<x-surface corner="md">`',
|
|
'scale/corners.blade.php:1 value outside the M3 scale `rounded-lg` — use `var(--md-sys-shape-corner-lg)` in your own CSS, or `<x-surface corner="lg">`',
|
|
'scale/corners.blade.php:2 value outside the M3 scale `rounded-xl` — use `var(--md-sys-shape-corner-xl)` in your own CSS, or `<x-surface corner="xl">`',
|
|
'scale/corners.blade.php:2 value outside the M3 scale `rounded-2xl` — use `var(--md-sys-shape-corner-xxl)` in your own CSS, or `<x-surface corner="xxl">`',
|
|
'scale/corners.blade.php:2 value outside the M3 scale `rounded-3xl` — use `var(--md-sys-shape-corner-xxl)` in your own CSS, or `<x-surface corner="xxl">`',
|
|
'scale/corners.blade.php:2 value outside the M3 scale `rounded-4xl` — use `var(--md-sys-shape-corner-xxl)` in your own CSS, or `<x-surface corner="xxl">`',
|
|
'scale/corners.blade.php:2 value outside the M3 scale `rounded-full` — use `var(--md-sys-shape-corner-full)` in your own CSS, or `<x-surface corner="full">`',
|
|
'scale/corners.blade.php:3 value outside the M3 scale `rounded-t-lg` — use `var(--md-sys-shape-corner-lg)` in your own CSS, or `<x-surface corner="lg">`',
|
|
'scale/corners.blade.php:3 value outside the M3 scale `rounded-se-2xl` — use `var(--md-sys-shape-corner-xxl)` in your own CSS, or `<x-surface corner="xxl">`',
|
|
]);
|
|
});
|
|
|
|
it('names the M3 elevation level for a Tailwind shadow', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/scale/elevation.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
'scale/elevation.blade.php:1 value outside the M3 scale `shadow-2xs` — use `var(--md-sys-elevation-1)` in your own CSS',
|
|
'scale/elevation.blade.php:1 value outside the M3 scale `shadow-xs` — use `var(--md-sys-elevation-1)` in your own CSS',
|
|
'scale/elevation.blade.php:1 value outside the M3 scale `shadow-sm` — use `var(--md-sys-elevation-1)` in your own CSS',
|
|
'scale/elevation.blade.php:1 value outside the M3 scale `shadow-md` — use `var(--md-sys-elevation-2)` in your own CSS',
|
|
'scale/elevation.blade.php:2 value outside the M3 scale `shadow-lg` — use `var(--md-sys-elevation-3)` in your own CSS',
|
|
'scale/elevation.blade.php:2 value outside the M3 scale `shadow-xl` — use `var(--md-sys-elevation-4)` in your own CSS',
|
|
'scale/elevation.blade.php:2 value outside the M3 scale `shadow-2xl` — use `var(--md-sys-elevation-5)` in your own CSS',
|
|
]);
|
|
});
|
|
|
|
it('sends a size, a weight, a leading and a tracking to a type style', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/scale/type.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
'scale/type.blade.php:1 value outside the M3 scale `text-xs` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:1 value outside the M3 scale `text-sm` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:1 value outside the M3 scale `text-base` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:1 value outside the M3 scale `text-lg` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:1 value outside the M3 scale `text-xl` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:1 value outside the M3 scale `text-2xl` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:1 value outside the M3 scale `text-9xl` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:2 value outside the M3 scale `font-thin` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
'scale/type.blade.php:2 value outside the M3 scale `font-extralight` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
'scale/type.blade.php:2 value outside the M3 scale `font-light` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
'scale/type.blade.php:2 value outside the M3 scale `font-normal` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
'scale/type.blade.php:2 value outside the M3 scale `font-medium` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
'scale/type.blade.php:3 value outside the M3 scale `font-semibold` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
'scale/type.blade.php:3 value outside the M3 scale `font-bold` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
'scale/type.blade.php:3 value outside the M3 scale `font-extrabold` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
'scale/type.blade.php:3 value outside the M3 scale `font-black` — use one of the `md-type-emphasized-*` classes (text.css), or a `md-type-*` size already at the right weight',
|
|
'scale/type.blade.php:4 value outside the M3 scale `leading-none` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:4 value outside the M3 scale `leading-tight` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:4 value outside the M3 scale `leading-snug` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:4 value outside the M3 scale `leading-normal` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:4 value outside the M3 scale `leading-relaxed` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:4 value outside the M3 scale `leading-loose` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:4 value outside the M3 scale `leading-6` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:5 value outside the M3 scale `tracking-tighter` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:5 value outside the M3 scale `tracking-tight` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:5 value outside the M3 scale `tracking-normal` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:5 value outside the M3 scale `tracking-wide` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:5 value outside the M3 scale `tracking-wider` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
'scale/type.blade.php:5 value outside the M3 scale `tracking-widest` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
|
]);
|
|
});
|
|
|
|
it('sends an easing and a duration to the motion tokens', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/scale/motion.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
'scale/motion.blade.php:1 value outside the M3 scale `ease-linear` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
|
'scale/motion.blade.php:1 value outside the M3 scale `ease-in` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
|
'scale/motion.blade.php:1 value outside the M3 scale `ease-out` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
|
'scale/motion.blade.php:1 value outside the M3 scale `ease-in-out` — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition`',
|
|
'scale/motion.blade.php:2 value outside the M3 scale `duration-75` — pair `var(--md-sys-motion-…-duration)` with its easing in your own `transition`',
|
|
'scale/motion.blade.php:2 value outside the M3 scale `duration-300` — pair `var(--md-sys-motion-…-duration)` with its easing in your own `transition`',
|
|
'scale/motion.blade.php:2 value outside the M3 scale `duration-1000` — pair `var(--md-sys-motion-…-duration)` with its easing in your own `transition`',
|
|
]);
|
|
});
|
|
|
|
it('finds a colour written as a value', function () {
|
|
expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/colour'))->violations()))->toBe([
|
|
'colour/badge.blade.php:1 arbitrary colour `bg-[#1d7afc]`, use an M3 role',
|
|
'colour/badge.blade.php:1 arbitrary colour `text-[rgb(0_0_0)]`, use an M3 role',
|
|
'colour/badge.blade.php:1 arbitrary colour `border-[hsl(210_80%_50%)]`, use an M3 role',
|
|
'colour/badge.blade.php:2 arbitrary colour `fill-[oklch(0.7_0.1_250)]`, use an M3 role',
|
|
'colour/badge.blade.php:2 arbitrary colour `ring-[color-mix(in_oklab,var(--x)_50%,transparent)]`, use an M3 role',
|
|
]);
|
|
});
|
|
|
|
it('names the md-ink-*, divider/surface or token replacement for a dead M3 role utility', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/roles.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
'tailwind/roles.blade.php:1 Tailwind ink role `text-on-surface-variant` compiles to nothing — use `md-ink-variant` (text.css)',
|
|
'tailwind/roles.blade.php:1 Tailwind line role `border-outline-variant` compiles to nothing — use `<x-divider>` or `<x-surface outlined>` for a line, not a border utility',
|
|
'tailwind/roles.blade.php:2 Tailwind surface role `bg-surface-container` compiles to nothing — use `<x-surface level="surface-container">`',
|
|
'tailwind/roles.blade.php:2 Tailwind role utility `bg-primary` compiles to nothing — use `var(--md-sys-color-*)` in your own CSS',
|
|
]);
|
|
});
|
|
|
|
it('sends flex, grid and gap utilities to the layout components', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/layout.blade.php'))->violations());
|
|
|
|
$hint = 'compiles to nothing — use `<x-row>`, `<x-grid>` or `<x-stack>` and their `gap`/`align`/`justify`/`columns` props';
|
|
|
|
expect($violations)->toBe([
|
|
"tailwind/layout.blade.php:1 Tailwind layout utility `flex` {$hint}",
|
|
"tailwind/layout.blade.php:1 Tailwind layout utility `grid` {$hint}",
|
|
"tailwind/layout.blade.php:1 Tailwind layout utility `inline-flex` {$hint}",
|
|
"tailwind/layout.blade.php:1 Tailwind layout utility `flex-col` {$hint}",
|
|
"tailwind/layout.blade.php:2 Tailwind layout utility `grid-cols-3` {$hint}",
|
|
"tailwind/layout.blade.php:2 Tailwind layout utility `col-span-2` {$hint}",
|
|
"tailwind/layout.blade.php:2 Tailwind layout utility `items-center` {$hint}",
|
|
"tailwind/layout.blade.php:2 Tailwind layout utility `justify-between` {$hint}",
|
|
"tailwind/layout.blade.php:2 Tailwind layout utility `gap-4` {$hint}",
|
|
]);
|
|
});
|
|
|
|
it('sends padding, margin and space-between utilities to a layout component\'s gap/padding prop', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/spacing.blade.php'))->violations());
|
|
|
|
$hint = "compiles to nothing — use a layout component's `gap`/`padding` prop (`<x-stack gap>`, `<x-surface padding>`), or `var(--md-sys-measurement-space*)` in your own CSS";
|
|
|
|
expect($violations)->toBe([
|
|
"tailwind/spacing.blade.php:1 Tailwind spacing utility `p-4` {$hint}",
|
|
"tailwind/spacing.blade.php:1 Tailwind spacing utility `px-2` {$hint}",
|
|
"tailwind/spacing.blade.php:1 Tailwind spacing utility `mt-2` {$hint}",
|
|
"tailwind/spacing.blade.php:1 Tailwind spacing utility `space-y-4` {$hint}",
|
|
"tailwind/spacing.blade.php:2 Tailwind spacing utility `m-4` {$hint}",
|
|
"tailwind/spacing.blade.php:2 Tailwind spacing utility `space-x-2` {$hint}",
|
|
]);
|
|
});
|
|
|
|
it('sends width and height utilities to a literal length, since M3 keeps no size scale', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/sizing.blade.php'))->violations());
|
|
|
|
$hint = 'compiles to nothing — M3 keeps no size scale; write the literal length in your own CSS, or use `<x-pane width>`/`<x-grid min-item>` where it fits';
|
|
|
|
expect($violations)->toBe([
|
|
"tailwind/sizing.blade.php:1 Tailwind sizing utility `w-full` {$hint}",
|
|
"tailwind/sizing.blade.php:1 Tailwind sizing utility `h-screen` {$hint}",
|
|
"tailwind/sizing.blade.php:1 Tailwind sizing utility `min-w-0` {$hint}",
|
|
"tailwind/sizing.blade.php:1 Tailwind sizing utility `max-w-md` {$hint}",
|
|
"tailwind/sizing.blade.php:2 Tailwind sizing utility `size-4` {$hint}",
|
|
]);
|
|
});
|
|
|
|
it('sends hidden to hide-below/hide-from and every other display utility to a plain CSS rule', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/display.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
"tailwind/display.blade.php:1 Tailwind's `hidden` compiles to nothing — use a layout component's `hide-below`/`hide-from` prop, or the reset's `[hidden]` attribute",
|
|
'tailwind/display.blade.php:1 Tailwind display utility `block` compiles to nothing — write the `display` rule in your own CSS',
|
|
'tailwind/display.blade.php:2 Tailwind display utility `inline-block` compiles to nothing — write the `display` rule in your own CSS',
|
|
'tailwind/display.blade.php:2 Tailwind display utility `invisible` compiles to nothing — write the `display` rule in your own CSS',
|
|
]);
|
|
});
|
|
|
|
it('sends text-layout utilities to their md-* class', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/text.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
"tailwind/text.blade.php:1 Tailwind's `text-center` compiles to nothing — use `md-text-center` (text.css)",
|
|
"tailwind/text.blade.php:1 Tailwind's `truncate` compiles to nothing — use `md-truncate` (text.css)",
|
|
"tailwind/text.blade.php:2 Tailwind's `sr-only` compiles to nothing — use `md-visually-hidden` (text.css)",
|
|
"tailwind/text.blade.php:2 Tailwind's `whitespace-nowrap` compiles to nothing — use `md-nowrap` (text.css)",
|
|
"tailwind/text.blade.php:2 Tailwind's `line-clamp-2` compiles to nothing — use `md-line-clamp-2` (text.css)",
|
|
"tailwind/text.blade.php:2 Tailwind's `tabular-nums` compiles to nothing — use `md-tabular` (text.css)",
|
|
]);
|
|
});
|
|
|
|
it('exempts a class the application\'s own CSS declares, even when it is Tailwind-shaped', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/exempt'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
"exempt/view.blade.php:1 Tailwind spacing utility `mt-2` compiles to nothing — use a layout component's `gap`/`padding` prop (`<x-stack gap>`, `<x-surface padding>`), or `var(--md-sys-measurement-space*)` in your own CSS",
|
|
]);
|
|
});
|
|
|
|
it('reports an arbitrary [] value once, whatever utility it modifies, and strips a trailing !', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/arbitrary.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
'tailwind/arbitrary.blade.php:1 Tailwind arbitrary value `top-[10px]` compiles to nothing — write the literal value in your own CSS, or use an M3 token',
|
|
'tailwind/arbitrary.blade.php:1 Tailwind arbitrary value `grid-cols-[1fr_2fr]` compiles to nothing — write the literal value in your own CSS, or use an M3 token',
|
|
'tailwind/arbitrary.blade.php:2 Tailwind layout utility `flex` compiles to nothing — use `<x-row>`, `<x-grid>` or `<x-stack>` and their `gap`/`align`/`justify`/`columns` props',
|
|
]);
|
|
});
|
|
|
|
it('bans the two absolutes when asked', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/colour'))->forbidAbsolutes()->violations());
|
|
|
|
expect($violations)->toContain(
|
|
'colour/badge.blade.php:3 absolute colour `bg-white`, use an M3 role',
|
|
'colour/badge.blade.php:3 absolute colour `text-black`, use an M3 role',
|
|
'colour/badge.blade.php:3 absolute colour `border-white`, use an M3 role',
|
|
);
|
|
});
|
|
|
|
it('leaves the two absolutes alone by default', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/colour'))->violations());
|
|
|
|
expect($violations)->not->toContain('colour/badge.blade.php:3 absolute colour `bg-white`, use an M3 role');
|
|
});
|
|
|
|
it('bans opacity as emphasis on ink when asked', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/colour'))->forbidOpacityInk()->violations());
|
|
|
|
expect($violations)->toContain(
|
|
'colour/badge.blade.php:4 opacity on ink `text-on-surface/60`, use a role (`text-on-surface-variant`, `text-outline`)',
|
|
'colour/badge.blade.php:4 opacity on ink `bg-on-surface/12`, use a role (`text-on-surface-variant`, `text-outline`)',
|
|
'colour/badge.blade.php:4 opacity on ink `border-outline/38`, use a role (`text-on-surface-variant`, `text-outline`)',
|
|
);
|
|
});
|
|
|
|
it('leaves opacity on ink alone by default, as the package\'s own disabled styles use it', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/colour'))->violations());
|
|
|
|
expect($violations)->not->toContain('colour/badge.blade.php:4 opacity on ink `text-on-surface/60`, use a role (`text-on-surface-variant`, `text-outline`)');
|
|
});
|
|
|
|
it('bans the roles an application leaves out', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/views'))
|
|
->forbidColours(['tertiary'])
|
|
->violations());
|
|
|
|
expect($violations)->toContain('views/page.blade.php:7 colour the theme does not declare `text-tertiary`');
|
|
});
|
|
|
|
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:7 a retired utility');
|
|
});
|
|
|
|
it('names the missing package stylesheet and the exact @import line to add', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/split-button.blade.php'))
|
|
->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css'))
|
|
->violations());
|
|
|
|
expect($violations)->toBe([
|
|
"stylesheets/views/split-button.blade.php:1 `<x-split-button>` needs `components/split-button.css`, missing from stylesheets/foundation-only.css — add `@import '../../../../resources/css/components/split-button.css';`",
|
|
]);
|
|
});
|
|
|
|
it('counts what an imported package stylesheet already brings in, through its own imports', function () {
|
|
$violations = DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/dependencies.blade.php'))
|
|
->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/split-button.css'))
|
|
->violations();
|
|
|
|
expect($violations)->toBe([]);
|
|
});
|
|
|
|
it('needs nothing more once the entry imports all.css', function () {
|
|
$violations = DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views'))
|
|
->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/all.css'))
|
|
->violations();
|
|
|
|
expect($violations)->toBe([]);
|
|
});
|
|
|
|
it('requires foundation.css whatever the views hold', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/plain.blade.php'))
|
|
->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/missing-foundation.css'))
|
|
->violations());
|
|
|
|
expect($violations)->toBe([
|
|
"stylesheets/missing-foundation.css:1 the CSS entry never imports `foundation.css`, required by every package stylesheet — add `@import '../../../../resources/css/foundation.css';`",
|
|
]);
|
|
});
|
|
|
|
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 `<x-button>` needs `components/button.css`, missing from stylesheets/foundation-only.css — add `@import '../../../../resources/css/components/button.css';`",
|
|
]);
|
|
});
|
|
|
|
it('recognises a package tag written fully qualified', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/namespaced.blade.php'))
|
|
->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css'))
|
|
->violations());
|
|
|
|
expect($violations)->toBe([
|
|
"stylesheets/views/namespaced.blade.php:1 `<x-button>` needs `components/button.css`, missing from stylesheets/foundation-only.css — add `@import '../../../../resources/css/components/button.css';`",
|
|
]);
|
|
});
|
|
|
|
it('needs pagination.css when a view calls ->links()', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/pagination.blade.php'))
|
|
->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css'))
|
|
->violations());
|
|
|
|
expect($violations)->toBe([
|
|
"stylesheets/views/pagination.blade.php:1 `->links()` needs `components/pagination.css`, missing from stylesheets/foundation-only.css — add `@import '../../../../resources/css/components/pagination.css';`",
|
|
]);
|
|
});
|
|
|
|
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', '<span>the application\'s button</span>');
|
|
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 `<x-button>` is shadowed by the application's own component of the same name — the package's `<x-button>` never renders here",
|
|
]);
|
|
});
|
|
|
|
it('reports a package tag the application shadows with its own component class', function () {
|
|
if (! class_exists('App\View\Components\Card', false)) {
|
|
eval('namespace App\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 `<x-card>` is shadowed by the application's own component of the same name — the package's `<x-card>` never renders here",
|
|
]);
|
|
});
|
|
|
|
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 () {
|
|
$violations = DesignGuard::scan([__DIR__.'/../../resources/views', __DIR__.'/../../resources/js', __DIR__.'/../../src'])->violations();
|
|
|
|
expect($violations)->toBe([]);
|
|
});
|