Files
livewire-material/tests/Feature/BreakpointsTest.php
T
Andreas Reinhold / reiniandClaude Fable 5.1 9fb3126038 Scan for Tailwind breakpoints, and drive the shell by class in the browser
Plan steps 15-17's testing. `tests/Feature/BreakpointsTest.php` reads every
file under resources/views, resources/css and resources/js — showcase example
heredocs included — and fails on any Tailwind breakpoint prefix and on any
media query or `matchMedia` string at 40, 48, 64 or 80rem (or 640/768/1024/
1280px, or the 39.99rem that stood for "below 640"). Both scans were checked
against a deliberately reintroduced `sm:w-auto` and an 80rem query.

Every render test that asserted a `sm:`/`lg:`/`xl:` class now asserts the M3
class, the drawer's pane query is 52.5rem, and the theme script keeps
`data-rail-auto` through `wire:navigate`.

NavigationTest drives the shell at 599/600/839/840/1199/1200/1600 with the
expectation each class carries: the bar on a compact window, the 96px rail from
the medium class's first pixel, a standard rail collapsed through expanded whose
menu button widens it in place with no scrim, and the rail expanded to begin
with from large and at extra-large. (Not run here — the Browser suite is out of
scope for this stream.)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 05:35:43 +02:00

86 lines
4.0 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's own screens are cleared in
* resources/css/tokens/theme.css — `--breakpoint-*: initial` — so an `sm:` left behind compiles to
* nothing at all and the rule it carried silently disappears. These two scans are the guard: one
* for the utilities, 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'"));
// The scan reads whole files, so nothing above can be hiding inside a heredoc; this only holds
// the premise up, in case the examples ever move out of them.
expect($sections)->not->toBeEmpty()
->and($sections->filter(fn (SplFileInfo $file): bool => str_contains($file->getContents(), 'medium:'))->isNotEmpty())->toBeTrue();
});