The showcase and the error pages set grayscale font smoothing on themselves, but the foundation did not, so an application that dropped Tailwind's antialiased class rendered heavier text on macOS than the package's own pages, silently. base.css now sets it on the page; the showcase's and error page's copies go, and the guard tells a leftover antialiased class to go too. Decided with the user (plan step 46). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
614 lines
55 KiB
PHP
614 lines
55 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 Tailwind palette colour `text-red-500` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS',
|
|
'views/page.blade.php:2 Tailwind spacing utility `p-4` compiles to nothing — use `padding="space200"` (16px) on `<x-surface>`, or `var(--md-sys-measurement-space200)` in your own CSS',
|
|
'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:5 Tailwind palette colour `text-red-600` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS',
|
|
'views/page.blade.php:6 1.x utility `focus-ring` compiles to nothing — use `md-focus-ring` (interaction.css)',
|
|
'views/page.blade.php:6 Tailwind colour utility `text-tertiary` compiles to nothing — use `var(--md-sys-color-tertiary)` in your own CSS',
|
|
'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 Tailwind sizing utility `size-4` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column's measure, `<x-icon size>` an icon's; anything else is a length in your own CSS",
|
|
]);
|
|
});
|
|
|
|
it('names what a component tag takes in place of each directive', function () {
|
|
expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/directives'))->violations()))->toBe([
|
|
'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: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',
|
|
]);
|
|
});
|
|
|
|
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 `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: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 `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 `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 `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",
|
|
"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-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-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-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",
|
|
]);
|
|
});
|
|
|
|
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 `dark:opacity-50` compiles to nothing — the roles already switch with the theme; a dark-only rule is `[data-theme='dark'] …` in your own CSS",
|
|
"tailwind/variant.blade.php:1 Tailwind variant class `hover:underline` compiles to nothing — M3's hover, focus and press states are `md-state-layer` and `md-focus-ring` (interaction.css); any other state rule is `:hover`/`:focus-visible` in your own CSS",
|
|
"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",
|
|
"tailwind/variant.blade.php:2 Tailwind variant class `focus:md:gap-2` compiles to nothing — M3's hover, focus and press states are `md-state-layer` and `md-focus-ring` (interaction.css); any other state rule is `:hover`/`:focus-visible` in your own CSS",
|
|
'tailwind/variant.blade.php:2 Tailwind variant class `group-hover:flex` compiles to nothing — `:has()` or a descendant selector 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-lg` — use `var(--md-sys-shape-corner-lg)` in your own CSS, or `<x-surface corner="lg">`',
|
|
'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-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-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-xs` — use `var(--md-sys-shape-corner-xs)` in your own CSS, or `<x-surface corner="xs">`',
|
|
'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: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: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">`',
|
|
'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">`',
|
|
]);
|
|
});
|
|
|
|
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-md` — use `var(--md-sys-elevation-2)` 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-xs` — use `var(--md-sys-elevation-1)` 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',
|
|
'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',
|
|
]);
|
|
});
|
|
|
|
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-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: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-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-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-xs` — 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-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-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: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-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: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: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-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: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: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-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-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-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-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-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-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-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-in-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` — 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-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-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-1000` — 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-75` — pair `var(--md-sys-motion-…-duration)` with its easing in your own `transition`',
|
|
]);
|
|
});
|
|
|
|
it('leaves an easing or a duration word alone in a JavaScript string outside a class list', function () {
|
|
// scale/motion.js reads a real CSS keyword out of matchMedia() into a variable named `easing`
|
|
// (`'linear'`/`'ease-out'`), the shape the plan calls out as a false positive: nothing here
|
|
// is a class list, so the class-context match above never sees it, and the family is off
|
|
// outsideTheScale()'s line scan since the fix (see DesignGuard::outsideTheScale()'s header).
|
|
$violations = DesignGuard::scan(realpath(GUARD_FIXTURES.'/scale/motion.js'))->violations();
|
|
|
|
expect($violations)->toBe([]);
|
|
});
|
|
|
|
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 `border-[hsl(210_80%_50%)]`, use an M3 role',
|
|
'colour/badge.blade.php:1 arbitrary colour `text-[rgb(0_0_0)]`, 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',
|
|
'colour/badge.blade.php:3 Tailwind colour utility `bg-white` compiles to nothing — M3 paints with roles, never white or black: `<x-surface level="surface-container-lowest">` or `var(--md-sys-color-surface-container-lowest)` for a white surface, an `on-` role (`md-ink`, `var(--md-sys-color-on-primary)`) for ink',
|
|
'colour/badge.blade.php:3 Tailwind colour utility `border-white` compiles to nothing — M3 paints with roles, never white or black: `<x-surface level="surface-container-lowest">` or `var(--md-sys-color-surface-container-lowest)` for a white surface, an `on-` role (`md-ink`, `var(--md-sys-color-on-primary)`) for ink',
|
|
'colour/badge.blade.php:3 Tailwind colour utility `text-black` compiles to nothing — M3 paints with roles, never white or black: `<x-surface level="surface-container-lowest">` or `var(--md-sys-color-surface-container-lowest)` for a white surface, an `on-` role (`md-ink`, `var(--md-sys-color-on-primary)`) for ink',
|
|
"colour/badge.blade.php:4 Tailwind colour utility `bg-on-surface/12` compiles to nothing — M3's hover, focus and press overlays are `md-state-layer`; any other tint is `color-mix(in srgb, var(--md-sys-color-on-surface) <n>%, transparent)` in your own CSS",
|
|
"colour/badge.blade.php:4 Tailwind colour utility `border-outline/38` compiles to nothing — M3's hover, focus and press overlays are `md-state-layer`; any other tint is `color-mix(in srgb, var(--md-sys-color-outline) <n>%, transparent)` in your own CSS",
|
|
"colour/badge.blade.php:4 Tailwind colour utility `text-on-surface/60` compiles to nothing — M3's quieter text is a role, not a faded one: `md-ink-variant` or `md-ink-quiet` (text.css)",
|
|
]);
|
|
});
|
|
|
|
it('sends 1.x ink names, the state-layer opacities and every other role to their 2.0.0 replacement', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/inks.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
'tailwind/inks.blade.php:1 Tailwind colour utility `border-divider` compiles to nothing — a line is `<x-divider>` or `<x-surface outlined>`, not a border utility',
|
|
'tailwind/inks.blade.php:1 Tailwind colour utility `text-meta` compiles to nothing — use `md-ink-variant` (text.css)',
|
|
'tailwind/inks.blade.php:1 Tailwind colour utility `text-quiet` compiles to nothing — use `md-ink-quiet` (text.css)',
|
|
'tailwind/inks.blade.php:2 Tailwind colour utility `bg-background` compiles to nothing — use `<x-surface level="surface">`',
|
|
'tailwind/inks.blade.php:2 Tailwind colour utility `text-current` compiles to nothing — write `currentColor` in your own CSS',
|
|
'tailwind/inks.blade.php:2 Tailwind colour utility `text-on-primary-container` compiles to nothing — use `var(--md-sys-color-on-primary-container)` in your own CSS',
|
|
]);
|
|
});
|
|
|
|
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 colour utility `border-outline-variant` compiles to nothing — a line is `<x-divider>` or `<x-surface outlined>`, not a border utility',
|
|
'tailwind/roles.blade.php:1 Tailwind colour utility `text-on-surface-variant` compiles to nothing — use `md-ink-variant` (text.css)',
|
|
'tailwind/roles.blade.php:2 Tailwind colour utility `bg-primary` compiles to nothing — use `var(--md-sys-color-primary)` in your own CSS',
|
|
'tailwind/roles.blade.php:2 Tailwind colour utility `bg-surface-container` compiles to nothing — use `<x-surface level="surface-container">`',
|
|
]);
|
|
});
|
|
|
|
it('sends flex, grid and gap utilities to the layout component and prop that replaces each', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/layout.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
'tailwind/layout.blade.php:1 Tailwind layout utility `flex-col` compiles to nothing — use `<x-stack>` (`gap`, `align`)',
|
|
'tailwind/layout.blade.php:1 Tailwind layout utility `flex` compiles to nothing — use `<x-row>` (`gap`, `align`, `justify`, `wrap`, `stack-below`), or `<x-stack>` for a column',
|
|
'tailwind/layout.blade.php:1 Tailwind layout utility `grid` compiles to nothing — use `<x-grid>` (`:columns` per breakpoint, `gap`, `min-item`), or `<x-feed>` for a grid of cards',
|
|
'tailwind/layout.blade.php:1 Tailwind layout utility `inline-flex` compiles to nothing — use `<x-row>` (`gap`, `align`, `justify`, `wrap`, `stack-below`), or `<x-stack>` for a column',
|
|
"tailwind/layout.blade.php:2 Tailwind flex/grid item utility `col-span-2` compiles to nothing — the layout components arrange their children; an item's own `flex`, `order` or `grid-column` is a rule in your own CSS",
|
|
"tailwind/layout.blade.php:2 Tailwind layout utility `grid-cols-3` compiles to nothing — use `<x-grid :columns=\"3\">`, or a per-breakpoint map (`:columns=\"['compact' => 1, 'medium' => 3]\"`)",
|
|
'tailwind/layout.blade.php:2 Tailwind layout utility `items-center` compiles to nothing — use `align="center"` on `<x-row>` or `<x-stack>`',
|
|
'tailwind/layout.blade.php:2 Tailwind layout utility `justify-between` compiles to nothing — use `justify="between"` on `<x-row>`',
|
|
'tailwind/layout.blade.php:2 Tailwind spacing utility `gap-4` compiles to nothing — use `gap="space200"` (16px) on `<x-row>`, `<x-stack>`, `<x-grid>` or `<x-feed>`',
|
|
]);
|
|
});
|
|
|
|
it('sends padding, margin and space-between utilities to the M3 spacing step they are (gap-4 is space200)', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/spacing.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
"tailwind/spacing.blade.php:1 Tailwind spacing utility `mt-2` compiles to nothing — space between siblings is a layout component's `gap=\"space100\"` (8px); any other margin is `var(--md-sys-measurement-space100)` in your own CSS",
|
|
'tailwind/spacing.blade.php:1 Tailwind spacing utility `p-4` compiles to nothing — use `padding="space200"` (16px) on `<x-surface>`, or `var(--md-sys-measurement-space200)` in your own CSS',
|
|
'tailwind/spacing.blade.php:1 Tailwind spacing utility `px-2` compiles to nothing — use `padding="space100"` (8px) on `<x-surface>`, or `var(--md-sys-measurement-space100)` in your own CSS',
|
|
'tailwind/spacing.blade.php:1 Tailwind spacing utility `space-y-4` compiles to nothing — use `<x-stack>` with `gap="space200"` (16px)',
|
|
"tailwind/spacing.blade.php:2 Tailwind spacing utility `m-4` compiles to nothing — space between siblings is a layout component's `gap=\"space200\"` (16px); any other margin is `var(--md-sys-measurement-space200)` in your own CSS",
|
|
'tailwind/spacing.blade.php:2 Tailwind spacing utility `space-x-2` compiles to nothing — use `<x-row>` with `gap="space100"` (8px)',
|
|
]);
|
|
});
|
|
|
|
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());
|
|
|
|
expect($violations)->toBe([
|
|
"tailwind/sizing.blade.php:1 Tailwind sizing utility `h-screen` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column's measure, `<x-icon size>` an icon's; anything else is a length in your own CSS",
|
|
"tailwind/sizing.blade.php:1 Tailwind sizing utility `max-w-md` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column's measure, `<x-icon size>` an icon's; anything else is a length in your own CSS",
|
|
"tailwind/sizing.blade.php:1 Tailwind sizing utility `min-w-0` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column's measure, `<x-icon size>` an icon's; anything else is a length in your own CSS",
|
|
"tailwind/sizing.blade.php:1 Tailwind sizing utility `w-full` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column's measure, `<x-icon size>` an icon's; anything else is a length in your own CSS",
|
|
"tailwind/sizing.blade.php:2 Tailwind sizing utility `size-4` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column's measure, `<x-icon size>` an icon's; anything else is a length in your own CSS",
|
|
]);
|
|
});
|
|
|
|
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 display utility `block` compiles to nothing — write the `display` rule in your own CSS',
|
|
"tailwind/display.blade.php:1 Tailwind's `hidden` compiles to nothing — use a layout component's `hide-below`/`hide-from` prop, the `hidden` attribute (the reset keeps it hidden), or `x-show`",
|
|
'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 `line-clamp-2` compiles to nothing — use `md-line-clamp-2` (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 `tabular-nums` compiles to nothing — use `md-tabular` (text.css)",
|
|
"tailwind/text.blade.php:2 Tailwind's `whitespace-nowrap` compiles to nothing — use `md-nowrap` (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 — space between siblings is a layout component's `gap=\"space100\"` (8px); any other margin is `var(--md-sys-measurement-space100)` 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 `grid-cols-[1fr_2fr]` compiles to nothing — write the literal value in your own CSS, or use an M3 token',
|
|
'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:2 Tailwind layout utility `flex` compiles to nothing — use `<x-row>` (`gap`, `align`, `justify`, `wrap`, `stack-below`), or `<x-stack>` for a column',
|
|
'tailwind/arbitrary.blade.php:4 Tailwind arbitrary property `[--material-bottom-bar:5rem]` compiles to nothing — write `--material-bottom-bar` in your own CSS',
|
|
"tailwind/arbitrary.blade.php:4 Tailwind arbitrary property `[font-variation-settings:'ROND'_100]` compiles to nothing — write `font-variation-settings` in your own CSS",
|
|
]);
|
|
});
|
|
|
|
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 Tailwind colour utility `bg-tertiary` compiles to nothing — use `var(--md-sys-color-tertiary)` in your own CSS',
|
|
"forbidden-colours/page.blade.php:7 `--md-sys-color-primary-container`: role `primary-container` is not part of this application's palette",
|
|
]);
|
|
});
|
|
|
|
it('leaves every role alone until an application forbids one', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/forbidden-colours'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
'forbidden-colours/page.blade.php:5 Tailwind colour utility `bg-tertiary` compiles to nothing — use `var(--md-sys-color-tertiary)` in your own CSS',
|
|
]);
|
|
});
|
|
|
|
it('names the md-* class or token for a 1.x utility that compiled through the package\'s Tailwind theme', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/one-x.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
'tailwind/one-x.blade.php:1 1.x utility `focus-ring` compiles to nothing — use `md-focus-ring` (interaction.css)',
|
|
'tailwind/one-x.blade.php:1 1.x utility `link` compiles to nothing — use `md-link` (interaction.css)',
|
|
'tailwind/one-x.blade.php:1 1.x utility `state-layer` compiles to nothing — use `md-state-layer` (interaction.css)',
|
|
'tailwind/one-x.blade.php:1 1.x utility `type-body-md` compiles to nothing — use `md-type-body-md` (text.css)',
|
|
'tailwind/one-x.blade.php:1 1.x utility `type-emphasized-title-lg` compiles to nothing — use `md-type-emphasized-title-lg` (text.css)',
|
|
'tailwind/one-x.blade.php:2 1.x utility `ease-emphasized-decelerate` compiles to nothing — use `var(--md-sys-motion-easing-emphasized-decelerate)` in your own `transition`',
|
|
'tailwind/one-x.blade.php:2 1.x utility `ease-spatial-fast` compiles to nothing — pair `var(--md-sys-motion-spatial-fast)` with `var(--md-sys-motion-spatial-fast-duration)` in your own `transition`',
|
|
'tailwind/one-x.blade.php:2 1.x utility `rounded-corner-lg` compiles to nothing — use `var(--md-sys-shape-corner-lg)` in your own CSS, or `<x-surface corner="lg">`',
|
|
'tailwind/one-x.blade.php:2 1.x utility `rounded-t-corner-xl-increased` compiles to nothing — use `var(--md-sys-shape-corner-xl-increased)` in your own CSS, or `<x-surface corner="xl-increased">`',
|
|
'tailwind/one-x.blade.php:2 1.x utility `shadow-elevation-2` compiles to nothing — use `var(--md-sys-elevation-2)` in your own CSS',
|
|
'tailwind/one-x.blade.php:2 Tailwind custom-property utility `duration-(--md-sys-motion-spatial-fast-duration)` compiles to nothing — write `var(--md-sys-motion-spatial-fast-duration)` in your own CSS',
|
|
]);
|
|
});
|
|
|
|
it('reports position, border, effect, interactivity and text utilities with the rule to write instead', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/utilities.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
"tailwind/utilities.blade.php:1 Tailwind flex/grid item utility `col-start-2` compiles to nothing — the layout components arrange their children; an item's own `flex`, `order` or `grid-column` is a rule in your own CSS",
|
|
"tailwind/utilities.blade.php:1 Tailwind flex/grid item utility `justify-self-end` compiles to nothing — the layout components arrange their children; an item's own `flex`, `order` or `grid-column` is a rule in your own CSS",
|
|
"tailwind/utilities.blade.php:1 Tailwind flex/grid item utility `shrink-0` compiles to nothing — the layout components arrange their children; an item's own `flex`, `order` or `grid-column` is a rule in your own CSS",
|
|
'tailwind/utilities.blade.php:1 Tailwind inset utility `-top-2` compiles to nothing — write the offset in your own CSS, from `var(--md-sys-measurement-space*)` where it is a spacing step',
|
|
'tailwind/utilities.blade.php:1 Tailwind inset utility `inset-0` compiles to nothing — write the offset in your own CSS, from `var(--md-sys-measurement-space*)` where it is a spacing step',
|
|
'tailwind/utilities.blade.php:1 Tailwind overflow utility `overflow-hidden` compiles to nothing — write `overflow` in your own CSS',
|
|
"tailwind/utilities.blade.php:1 Tailwind position utility `relative` compiles to nothing — write `position` in your own CSS (a FAB goes in `<x-scaffold>`'s `fab` slot)",
|
|
'tailwind/utilities.blade.php:1 Tailwind z-index utility `z-10` compiles to nothing — write `z-index` in your own CSS',
|
|
"tailwind/utilities.blade.php:1 Tailwind's `container` compiles to nothing — use `<x-pane width>`, which sets M3's margins and a measure",
|
|
'tailwind/utilities.blade.php:2 Tailwind border utility `border-t-2` compiles to nothing — a line is `<x-divider>` or `<x-surface outlined>`; any other border is your own CSS, in `var(--md-sys-color-outline-variant)`',
|
|
'tailwind/utilities.blade.php:2 Tailwind border utility `border` compiles to nothing — a line is `<x-divider>` or `<x-surface outlined>`; any other border is your own CSS, in `var(--md-sys-color-outline-variant)`',
|
|
'tailwind/utilities.blade.php:2 Tailwind effect utility `-rotate-45` compiles to nothing — write the rule in your own CSS',
|
|
'tailwind/utilities.blade.php:2 Tailwind interactivity utility `cursor-pointer` compiles to nothing — write the rule in your own CSS',
|
|
'tailwind/utilities.blade.php:2 Tailwind interactivity utility `select-none` compiles to nothing — write the rule in your own CSS',
|
|
'tailwind/utilities.blade.php:2 Tailwind motion utility `transition` compiles to nothing — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition` or `animation`',
|
|
"tailwind/utilities.blade.php:2 Tailwind opacity utility `opacity-50` compiles to nothing — write `opacity` in your own CSS (M3's disabled content is 38 %)",
|
|
"tailwind/utilities.blade.php:2 Tailwind outline utility `outline-none` compiles to nothing — M3's focus indicator is `md-focus-ring` (interaction.css); any other outline is your own CSS",
|
|
"tailwind/utilities.blade.php:2 Tailwind outline utility `ring-2` compiles to nothing — M3's focus indicator is `md-focus-ring` (interaction.css); any other outline is your own CSS",
|
|
'tailwind/utilities.blade.php:2 Tailwind radius utility `rounded` compiles to nothing — use `var(--md-sys-shape-corner-xs)` in your own CSS, or `<x-surface corner="xs">`',
|
|
'tailwind/utilities.blade.php:2 Tailwind shadow utility `shadow` compiles to nothing — use `var(--md-sys-elevation-*)` in your own CSS',
|
|
'tailwind/utilities.blade.php:3 Tailwind background utility `bg-clip-text` compiles to nothing — write the rule in your own CSS',
|
|
'tailwind/utilities.blade.php:3 Tailwind display utility `table` compiles to nothing — write the `display` rule in your own CSS',
|
|
'tailwind/utilities.blade.php:3 Tailwind media utility `aspect-video` compiles to nothing — write `aspect-ratio`/`object-fit` in your own CSS',
|
|
'tailwind/utilities.blade.php:3 Tailwind media utility `object-cover` compiles to nothing — write `aspect-ratio`/`object-fit` in your own CSS',
|
|
'tailwind/utilities.blade.php:3 Tailwind text utility `font-mono` compiles to nothing — write the rule in your own CSS',
|
|
'tailwind/utilities.blade.php:3 Tailwind text utility `list-disc` compiles to nothing — write the rule in your own CSS',
|
|
'tailwind/utilities.blade.php:3 Tailwind text utility `uppercase` compiles to nothing — write the rule in your own CSS',
|
|
"tailwind/utilities.blade.php:3 Tailwind's `antialiased` compiles to nothing — the foundation already smooths text in grayscale (base.css); drop it",
|
|
"tailwind/utilities.blade.php:3 Tailwind's `font-sans` compiles to nothing — the foundation already sets the brand typeface; any other `font-family` is your own CSS",
|
|
"tailwind/utilities.blade.php:3 Tailwind's `text-end` compiles to nothing — use `md-text-end` (text.css)",
|
|
"tailwind/utilities.blade.php:3 Tailwind's `underline` compiles to nothing — `md-link` draws a link (interaction.css); any other decoration is your own CSS",
|
|
]);
|
|
});
|
|
|
|
it('reads class lists in Alpine, Livewire and PHP bindings, but not a string compared in a condition', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/bindings.blade.php'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
"tailwind/bindings.blade.php:1 Tailwind opacity utility `opacity-50` compiles to nothing — write `opacity` in your own CSS (M3's disabled content is 38 %)",
|
|
"tailwind/bindings.blade.php:1 Tailwind's `hidden` compiles to nothing — use a layout component's `hide-below`/`hide-from` prop, the `hidden` attribute (the reset keeps it hidden), or `x-show`",
|
|
'tailwind/bindings.blade.php:2 Tailwind effect utility `scale-95` compiles to nothing — write the rule in your own CSS',
|
|
'tailwind/bindings.blade.php:2 Tailwind motion utility `transition` compiles to nothing — pair `var(--md-sys-motion-spatial-*)`/`var(--md-sys-motion-effects-*)` with its `-duration` in your own `transition` or `animation`',
|
|
'tailwind/bindings.blade.php:2 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`',
|
|
'tailwind/bindings.blade.php:3 Tailwind spacing utility `gap-2` compiles to nothing — use `gap="space100"` (8px) on `<x-row>`, `<x-stack>`, `<x-grid>` or `<x-feed>`',
|
|
"tailwind/bindings.blade.php:4 Tailwind spacing utility `mt-2` compiles to nothing — space between siblings is a layout component's `gap=\"space100\"` (8px); any other margin is `var(--md-sys-measurement-space100)` in your own CSS",
|
|
'tailwind/bindings.blade.php:6 Tailwind layout utility `flex` compiles to nothing — use `<x-row>` (`gap`, `align`, `justify`, `wrap`, `stack-below`), or `<x-stack>` for a column',
|
|
]);
|
|
});
|
|
|
|
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 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('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 Tailwind spacing utility `mt-2` compiles to nothing — space between siblings is a layout component's `gap=\"space100\"` (8px); any other margin is `var(--md-sys-measurement-space100)` in your own CSS";
|
|
$button = "stylesheets/views-foreign/page.blade.php:2 `<x-button>` 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('says so when the CSS entry does not exist', function () {
|
|
$violations = DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/plain.blade.php'))
|
|
->missingStylesheets('/nowhere/app.css')
|
|
->violations();
|
|
|
|
expect($violations)->toBe(['/nowhere/app.css:1 the CSS entry `missingStylesheets()` names does not exist']);
|
|
});
|
|
|
|
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 () {
|
|
// Under a namespace of its own, which Blade's class guess reads from the application, so the
|
|
// class cannot shadow `<x-card>` 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 `<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: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 `<`",
|
|
]);
|
|
});
|
|
|
|
it('leaves a box-shadow ring in a colour role alone, but still reports a blurred or literal-coloured one', function () {
|
|
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/shadows/rings.css'))->violations());
|
|
|
|
expect($violations)->toBe([
|
|
'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('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). One line also names a
|
|
// palette colour and a spacing utility, from two checks that run in the opposite order.
|
|
File::put($dir.'/a.css', "body {\n color: #ff0000;\n}\n");
|
|
File::put($dir.'/z.blade.php', '<div class="bg-red-500 p-4"></div>');
|
|
|
|
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 Tailwind palette colour `bg-red-500` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS",
|
|
"{$dir}/z.blade.php:1 Tailwind spacing utility `p-4` compiles to nothing — use `padding=\"space200\"` (16px) on `<x-surface>`, or `var(--md-sys-measurement-space200)` in your own CSS",
|
|
]);
|
|
});
|
|
|
|
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, 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'];
|
|
|
|
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([]);
|
|
});
|