*/ 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 */ 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 = '/(?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 = '/(?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'
BLADE, ]; @endphp OUTER)); try { $pattern = '/(?not->toBeEmpty(); } finally { File::deleteDirectory($directory); } });