$violations * @return list */ 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:6 daisyUI class `btn-primary`', '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 `violations()))->toBe([ 'breakpoints/layout.blade.php:1 Tailwind breakpoint `sm:`, use `medium:`', 'breakpoints/layout.blade.php:1 Tailwind breakpoint `md:`, use `medium:`', 'breakpoints/layout.blade.php:1 Tailwind breakpoint `lg:`, use `expanded:`', 'breakpoints/layout.blade.php:1 Tailwind breakpoint `xl:`, use `large:`', 'breakpoints/layout.blade.php:1 Tailwind breakpoint `2xl:`, use `extra-large:`', 'breakpoints/layout.blade.php:2 Tailwind breakpoint `max-sm:`, use `max-medium:`', 'breakpoints/layout.blade.php:2 Tailwind breakpoint `max-md:`, use `max-medium:`', 'breakpoints/layout.blade.php:2 Tailwind breakpoint `max-lg:`, use `max-expanded:`', 'breakpoints/layout.blade.php:2 Tailwind breakpoint `max-xl:`, use `max-large:`', 'breakpoints/layout.blade.php:2 Tailwind breakpoint `max-2xl:`, use `max-extra-large:`', 'breakpoints/layout.blade.php:3 Tailwind breakpoint `sm:`, use `medium:`', 'breakpoints/layout.blade.php:3 Tailwind breakpoint `md:`, use `medium:`', ]); }); 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 `rounded-corner-none`', 'scale/corners.blade.php:1 value outside the M3 scale `rounded-xs`, use `rounded-corner-xs`', 'scale/corners.blade.php:1 value outside the M3 scale `rounded-sm`, use `rounded-corner-sm`', 'scale/corners.blade.php:1 value outside the M3 scale `rounded-md`, use `rounded-corner-md`', 'scale/corners.blade.php:1 value outside the M3 scale `rounded-lg`, use `rounded-corner-lg`', 'scale/corners.blade.php:2 value outside the M3 scale `rounded-xl`, use `rounded-corner-xl`', 'scale/corners.blade.php:2 value outside the M3 scale `rounded-2xl`, use `rounded-corner-xxl`', 'scale/corners.blade.php:2 value outside the M3 scale `rounded-3xl`, use `rounded-corner-xxl`', 'scale/corners.blade.php:2 value outside the M3 scale `rounded-4xl`, use `rounded-corner-xxl`', 'scale/corners.blade.php:2 value outside the M3 scale `rounded-full`, use `rounded-corner-full`', 'scale/corners.blade.php:3 value outside the M3 scale `rounded-t-lg`, use `rounded-t-corner-lg`', 'scale/corners.blade.php:3 value outside the M3 scale `rounded-se-2xl`, use `rounded-se-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 `shadow-elevation-1`', 'scale/elevation.blade.php:1 value outside the M3 scale `shadow-xs`, use `shadow-elevation-1`', 'scale/elevation.blade.php:1 value outside the M3 scale `shadow-sm`, use `shadow-elevation-1`', 'scale/elevation.blade.php:1 value outside the M3 scale `shadow-md`, use `shadow-elevation-2`', 'scale/elevation.blade.php:2 value outside the M3 scale `shadow-lg`, use `shadow-elevation-3`', 'scale/elevation.blade.php:2 value outside the M3 scale `shadow-xl`, use `shadow-elevation-4`', 'scale/elevation.blade.php:2 value outside the M3 scale `shadow-2xl`, use `shadow-elevation-5`', ]); }); 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 a `type-*` style', 'scale/type.blade.php:1 value outside the M3 scale `text-sm`, use a `type-*` style', 'scale/type.blade.php:1 value outside the M3 scale `text-base`, use a `type-*` style', 'scale/type.blade.php:1 value outside the M3 scale `text-lg`, use a `type-*` style', 'scale/type.blade.php:1 value outside the M3 scale `text-xl`, use a `type-*` style', 'scale/type.blade.php:1 value outside the M3 scale `text-2xl`, use a `type-*` style', 'scale/type.blade.php:1 value outside the M3 scale `text-9xl`, use a `type-*` style', 'scale/type.blade.php:2 value outside the M3 scale `font-thin`, use a `type-emphasized-*` style', 'scale/type.blade.php:2 value outside the M3 scale `font-extralight`, use a `type-emphasized-*` style', 'scale/type.blade.php:2 value outside the M3 scale `font-light`, use a `type-emphasized-*` style', 'scale/type.blade.php:2 value outside the M3 scale `font-normal`, use a `type-emphasized-*` style', 'scale/type.blade.php:2 value outside the M3 scale `font-medium`, use a `type-emphasized-*` style', 'scale/type.blade.php:3 value outside the M3 scale `font-semibold`, use a `type-emphasized-*` style', 'scale/type.blade.php:3 value outside the M3 scale `font-bold`, use a `type-emphasized-*` style', 'scale/type.blade.php:3 value outside the M3 scale `font-extrabold`, use a `type-emphasized-*` style', 'scale/type.blade.php:3 value outside the M3 scale `font-black`, use a `type-emphasized-*` style', 'scale/type.blade.php:4 value outside the M3 scale `leading-none`, use a `type-*` style', 'scale/type.blade.php:4 value outside the M3 scale `leading-tight`, use a `type-*` style', 'scale/type.blade.php:4 value outside the M3 scale `leading-snug`, use a `type-*` style', 'scale/type.blade.php:4 value outside the M3 scale `leading-normal`, use a `type-*` style', 'scale/type.blade.php:4 value outside the M3 scale `leading-relaxed`, use a `type-*` style', 'scale/type.blade.php:4 value outside the M3 scale `leading-loose`, use a `type-*` style', 'scale/type.blade.php:4 value outside the M3 scale `leading-6`, use a `type-*` style', 'scale/type.blade.php:5 value outside the M3 scale `tracking-tighter`, use a `type-*` style', 'scale/type.blade.php:5 value outside the M3 scale `tracking-tight`, use a `type-*` style', 'scale/type.blade.php:5 value outside the M3 scale `tracking-normal`, use a `type-*` style', 'scale/type.blade.php:5 value outside the M3 scale `tracking-wide`, use a `type-*` style', 'scale/type.blade.php:5 value outside the M3 scale `tracking-wider`, use a `type-*` style', 'scale/type.blade.php:5 value outside the M3 scale `tracking-widest`, use a `type-*` style', ]); }); 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`, use `ease-standard` or an `ease-spatial-*`/`ease-effects-*` with its duration', 'scale/motion.blade.php:1 value outside the M3 scale `ease-in`, use `ease-standard` or an `ease-spatial-*`/`ease-effects-*` with its duration', 'scale/motion.blade.php:1 value outside the M3 scale `ease-out`, use `ease-standard` or an `ease-spatial-*`/`ease-effects-*` with its duration', 'scale/motion.blade.php:1 value outside the M3 scale `ease-in-out`, use `ease-standard` or an `ease-spatial-*`/`ease-effects-*` with its duration', 'scale/motion.blade.php:2 value outside the M3 scale `duration-75`, use `duration-(--md-sys-motion-…-duration)` paired with its easing', 'scale/motion.blade.php:2 value outside the M3 scale `duration-300`, use `duration-(--md-sys-motion-…-duration)` paired with its easing', 'scale/motion.blade.php:2 value outside the M3 scale `duration-1000`, use `duration-(--md-sys-motion-…-duration)` paired with its easing', ]); }); 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('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('passes the package\'s own views', function () { $violations = DesignGuard::scan([__DIR__.'/../../resources/views', __DIR__.'/../../resources/js', __DIR__.'/../../src'])->violations(); expect($violations)->toBe([]); });