Plan step 38's last batch cleared the one real medium: this premise test relied on (containment.blade.php's Lists example, now <x-grid :columns="['compact' => 1, 'medium' => 2]">) — its own goal, not a regression, but it leaves nothing left over to find. The test now builds a real two-heredocs-deep fixture (an @php block nesting a <<<'BLADE' example, exactly how a showcase section is shaped) and reuses the first test's own pattern and helper against it, so the scan is proven on demand instead of by whatever Tailwind happens to still be lying around. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
112 lines
5.1 KiB
PHP
112 lines
5.1 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'"));
|
|
|
|
// 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);
|
|
}
|
|
});
|