*/ 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'")); // 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(); });