Share one view class rule between the stylesheet tests

Plan step 36 review. The brief and step 34 let a package view write the
md-* text classes beside the three interaction classes, but both
stylesheet tests allowed only the latter, and each kept its own copy of
the check. tests/Support/ViewClasses.php is now the one rule: the
interaction classes, the text classes read from text.css, and a caller's
class handed on whole. It also closes the holes the copies left: a
single-quoted :class, an echo mixed into a class list, a hint-, icon- or
box-class literal, a 'class' => entry that is not a plain literal, and
->class('…') as a string.

InputStylesheetsTest's exemptions narrow too: the autofill transition is
skipped in field.css alone; an orientation query may name any px height
but never a width off a breakpoint; a box-shadow must be none, an
elevation level, or a 0 0 0 ring in a colour role.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 19:31:02 +02:00
co-authored by Claude Opus 5
parent d46b07c13c
commit a3eef657cc
3 changed files with 192 additions and 167 deletions
@@ -2,10 +2,12 @@
use Illuminate\Support\Facades\File;
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses;
/**
* The actions and communication components, rewritten without Tailwind (plan step 36): each view
* renders `data-md-*` attributes and no class list of its own, and each has a stylesheet in
* renders `data-md-*` attributes and no class list of its own beyond the interaction and text
* classes (tests/Support/ViewClasses.php), and each has a stylesheet in
* `material.components` that imports the stylesheets of the components its view renders, writes
* its values from the tokens and its breakpoints as px range queries, and is imported from the
* "Actions and communication" block of components.css.
@@ -61,56 +63,8 @@ it('imports the stylesheet of every component its view renders', function (strin
expect(array_values(array_diff($rendered, ComponentStylesheet::read($name)->imports())))->toBe([]);
})->with('action components');
it('writes no class list into the view but the interaction classes', function (string $name) {
$view = File::get(__DIR__."/../../../resources/views/components/{$name}.blade.php");
$allowed = ['md-state-layer', 'md-focus-ring', 'md-touch-target'];
// `@class()`, `Arr::toCssClasses` maps and Alpine's `:class` binding are never how a class
// reaches the page here; the interaction classes and a caller's own class are the only source.
expect($view)->not->toMatch('/@class\(|toCssClasses|(?:x-bind)?:class=/');
// A literal `class="…"` may hold only the interaction classes — any subset, any order, with a
// simple `@if (…) … @endif` around one token — or a caller's value forwarded whole (`hint-class`,
// `icon-class`: `class="{{ $…}}"`).
preg_match_all('/(?<![\w:-])class=(["\'])(.*?)\1/s', $view, $classAttributes, PREG_SET_ORDER);
foreach ($classAttributes as [, , $content]) {
if (preg_match('/^\{\{\s*\$/', trim($content)) === 1) {
continue;
}
$stripped = (string) preg_replace('/\{\{.*?\}\}|@if\s*\([^()]*\)|@unless\s*\([^()]*\)|@else|@endif|@endunless/s', ' ', $content);
$tokens = array_values(array_filter(preg_split('/\s+/', trim($stripped))));
expect(array_diff($tokens, $allowed))->toBe([], "{$name}.blade.php writes class=\"{$content}\", not only the interaction classes");
}
// `$attributes->class([…])` merges the interaction classes with the caller's own, per the
// brief: "merges through $attributes->class() only where the root takes the caller's class".
// Only the array's own items are checked — a condition may nest its own brackets
// (`in_array($size, ['xs', 'sm'], true)`), which are no class list of the view's writing.
preg_match_all('/->class\(\[(.*?)\]\)/s', $view, $classCalls, PREG_SET_ORDER);
foreach ($classCalls as [, $args]) {
$depth = 0;
for ($i = 0, $length = strlen($args); $i < $length; $i++) {
$char = $args[$i];
if ($char === '[' || $char === '(') {
$depth++;
} elseif ($char === ']' || $char === ')') {
$depth--;
} elseif ($depth === 0 && $char === "'") {
$end = strpos($args, "'", $i + 1);
$item = substr($args, $i + 1, $end - $i - 1);
expect(in_array($item, $allowed, true))->toBeTrue("{$name}.blade.php: ->class() writes [{$item}], not an interaction class");
$i = $end;
}
}
}
it('writes no class list into the view but the interaction and text classes', function (string $name) {
expect(ViewClasses::violations(File::get(__DIR__."/../../../resources/views/components/{$name}.blade.php")))->toBe([]);
})->with('action components');
it('takes its values from the tokens and its breakpoints in px', function (string $name) {