Files
livewire-material/tests/Feature/BreakpointsTest.php
T
Andreas Reinhold / reiniandClaude Sonnet 5 6897306e18 Prove no Tailwind utility class remains in resources/ or tests/
StylesheetsTest.php's new test runs DesignGuard's own family table (check
(i)) over resources/views, resources/js, src, tests/Browser and
tests/Feature — the guard turned on the package's own source, so the two
never drift apart. tests/Feature/DesignGuardTest.php and
tests/Fixtures/design-guard/ carry Tailwind on purpose and are left out.

The scan found Tailwind-shaped class strings used as caller classes in 21
test files (a component test proving a caller's class lands on the root or
a caller's size wins, mostly), left over from before Tailwind cleared the
whole stack: replaced with neutral application-style names (`app-hero-icon`)
or, where one already carries the right meaning, an `md-*` class
(`md-text-end`, `md-ink-warning`). BreakpointsTest.php's own heredoc probe —
which needs a genuine Tailwind-shaped breakpoint prefix to prove its
detection works — now builds that one string from a placeholder at runtime,
so its own source stays clean for this same scan.

Plan step 42 (Phase F), Part A.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 09:32:36 +02:00

119 lines
5.6 KiB
PHP

<?php
use Illuminate\Support\Facades\File;
/**
* Breakpoints are M3's window size classes and only those: compact below `medium`, then medium 600,
* expanded 840, large 1200 and extra-large 1600 (docs/reference/m3/foundations.md § Layout,
* foundations-supplement.md § Breakpoints). Tailwind is gone from the whole stack (plan step 39),
* so a leftover `sm:` class or a media query at one of its screen widths no longer compiles to
* nothing — a browser just as silently ignores the invalid class or the wrong-width rule, so the
* mistake is exactly as easy to miss. These two scans are the guard: one for a Tailwind-shaped
* prefix (a habit, or an example pasted from elsewhere), one for the media queries a stylesheet or
* a script writes by hand.
*
* @return list<SplFileInfo>
*/
function breakpointSources(): array
{
return collect(['views', 'css', 'js'])
->flatMap(fn (string $directory): array => File::allFiles(__DIR__.'/../../resources/'.$directory))
->filter(fn (SplFileInfo $file): bool => in_array($file->getExtension(), ['php', 'css', 'js'], true))
->values()
->all();
}
/**
* Every line of a file that matches, as `path:line: the line`.
*
* @return list<string>
*/
function breakpointOffenders(SplFileInfo $file, string $pattern): array
{
$lines = preg_split('/\R/', $file->getContents());
return collect($lines)
->filter(fn (string $line): bool => preg_match($pattern, $line) === 1)
->map(fn (string $line, int $index): string => $file->getRelativePathname().':'.($index + 1).': '.trim($line))
->values()
->all();
}
it('writes no Tailwind breakpoint prefix in any view, stylesheet or script', function () {
// A prefix, not a name: `--radius-corner-sm:` and slider.js's `sm: { corner: 8 }` are excluded
// by the character before and the one after, and `@md:` is a container query, a different
// thing — a component sized by the room it has, not by the window.
$pattern = '/(?<![\w@-])(?:max-)?(?:sm|md|lg|xl|2xl):[a-z\[(-]/';
$offenders = collect(breakpointSources())
->flatMap(fn (SplFileInfo $file): array => breakpointOffenders($file, $pattern))
->values();
expect($offenders->all())->toBe([]);
});
it('writes no media query at a Tailwind screen width', function () {
// 40rem/640px, 48rem/768px, 64rem/1024px, 80rem/1280px and the 39.99rem that stood for "below
// 640" — none of them is an M3 boundary. The M3 ones are 37.5, 52.5, 75 and 100rem, and a
// script asks resources/js/breakpoints.js for them instead of writing its own string.
$widths = '/(?<![\d.])(?:40|48|64|80|39\.99)rem|(?<![\d.])(?:640|768|1024|1280)px/';
$offenders = collect(breakpointSources())
->flatMap(function (SplFileInfo $file) use ($widths): array {
// Only the condition of a query, so `max-width: min(40rem, 70dvh)` — a real width, not
// a breakpoint — and a 640px cap on a bottom sheet are left alone.
preg_match_all('/@(?:media|container)\b[^{]*|matchMedia\(\s*[\'"`][^\'"`]*/', $file->getContents(), $matches);
return collect($matches[0])
->filter(fn (string $condition): bool => preg_match($widths, $condition) === 1)
->map(fn (string $condition): string => $file->getRelativePathname().': '.trim($condition))
->values()
->all();
})
->values();
expect($offenders->all())->toBe([]);
});
it('scans the showcase example heredocs too, where the package teaches its own markup', function () {
$sections = collect(breakpointSources())
->filter(fn (SplFileInfo $file): bool => str_contains($file->getRelativePathname(), 'showcase'.DIRECTORY_SEPARATOR.'sections'))
->filter(fn (SplFileInfo $file): bool => str_contains($file->getContents(), "<<<'BLADE'"));
// Every section still teaches its examples through a heredoc nested inside an @php block
// (plan step 38 rewrote what is inside them, not the mechanism), so the two scans above —
// which read a file's raw text rather than its parsed Blade or PHP — still reach them.
expect($sections)->not->toBeEmpty();
// The premise itself, proven on a fixture now that plan step 38 has cleared every Tailwind
// breakpoint prefix out of the showcase (its own goal, so production content can no longer
// carry one): a real file, shaped exactly like a showcase section (an outer heredoc nesting an
// inner one, `@php … <<<'BLADE' … BLADE, … @endphp`), still gives up a prefix hiding two
// heredocs deep to the same pattern and helper the first test above uses.
$directory = sys_get_temp_dir().'/breakpoint-heredoc-probe-'.uniqid();
File::ensureDirectoryExists($directory);
// Built from parts, not written whole: the design guard's own "no Tailwind utility class in
// resources/ or tests/" scan (StylesheetsTest.php) reads this file's raw source too, and a
// real Tailwind breakpoint prefix sitting in a class attribute here would be exactly what
// that scan looks for — the placeholder keeps this file's own source clean of one.
File::put($directory.'/probe.blade.php', str_replace('__BREAKPOINT__', 'md:', <<<'OUTER'
@php
$examples = [
'Probe' => <<<'BLADE'
<div class="__BREAKPOINT__flex"></div>
BLADE,
];
@endphp
OUTER));
try {
$pattern = '/(?<![\w@-])(?:max-)?(?:sm|md|lg|xl|2xl):[a-z\[(-]/';
$probe = File::allFiles($directory)[0];
expect(breakpointOffenders($probe, $pattern))->not->toBeEmpty();
} finally {
File::deleteDirectory($directory);
}
});