Files
livewire-material/tests/Feature/BreakpointsTest.php
T
Andreas Reinhold / reiniandClaude Sonnet 5 7e95b4a91b Retire BreakpointsTest's docblock reference to the deleted theme.css
Plan step 39 (part 5). Tailwind no longer clears its own breakpoint
scale anywhere in the stack, so the guard's rationale changes from "a
leftover sm: compiles to nothing" to "a browser drops the invalid
class or wrong-width rule just as silently" — the scans themselves
already worked over plain files and needed no change.

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

114 lines
5.2 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);
File::put($directory.'/probe.blade.php', <<<'OUTER'
@php
$examples = [
'Probe' => <<<'BLADE'
<div class="md: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);
}
});