From f31acf82d7b1fd8f84f7e9ab304484745b6006e7 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Tue, 15 Sep 2026 01:12:10 +0200 Subject: [PATCH] Pin every written-out copy of the rail's collapsed conditions Plan step 36, navigation review. The Tailwind-era rail-collapsed: variant is now plain selectors in 26 rules, each repeated in five window bands, across navigation-rail.css, navigation-rail-item.css, navigation-rail-section.css and layout/scaffold.css; the tests pinned a few of them. A new test walks every rule whose :where() names the rail and fails when a copy drifts from the seven conditions for its band, or when a rule is drawn collapsed in some bands but not all five. ComponentStylesheet gains rules() for it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- .../Feature/Components/NavigationRailTest.php | 67 +++++++++++++++++++ tests/Support/ComponentStylesheet.php | 11 +++ 2 files changed, 78 insertions(+) diff --git a/tests/Feature/Components/NavigationRailTest.php b/tests/Feature/Components/NavigationRailTest.php index 04c4607f..cbe10bb6 100644 --- a/tests/Feature/Components/NavigationRailTest.php +++ b/tests/Feature/Components/NavigationRailTest.php @@ -228,3 +228,70 @@ it('draws the item\'s own state layer split between the item and its indicator', ->and($item->declarations('[data-md-navigation-rail-item][data-md-navigation-rail-item] [data-md-navigation-indicator]::before')) ->toBe(['background-color' => 'var(--md-sys-color-on-secondary-container)', 'opacity' => 'var(--navigation-indicator-layer)']); }); + +/** + * Plain CSS has no variant to name "drawn collapsed" once, so every rule with a collapsed shape + * writes the conditions out: navigation-rail.css, navigation-rail-item.css, + * navigation-rail-section.css, and layout/scaffold.css for the actions row inside the rail's footer. + * This walks every copy in those files — not one pinned rule — and fails if any drifts from the + * seven conditions the Tailwind-era `rail-collapsed:` variant had, or if a rule has its collapsed + * shape in some window bands but not in all five. + */ +dataset('stylesheets that draw a collapsed rail', [ + 'navigation-rail', + 'navigation-rail-item', + 'navigation-rail-section', + '../layout/scaffold', +]); + +it('writes "collapsed" with the same seven conditions in every copy, in every band', function (string $name) { + $closed = ':not([data-md-open])'; + $bands = [ + 'always' => ["[data-md-navigation-rail='collapsed']", "[data-rail='collapsed'] [data-md-navigation-rail='collapsible']{$closed}", "[data-md-navigation-rail='modal']{$closed}"], + '@media (width < 600px)' => ["[data-md-navigation-rail='collapsible']{$closed}"], + '@media (width < 840px)' => ["[data-md-navigation-rail='adaptive']{$closed}"], + '@media (840px <= width < 1200px)' => [":is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']{$closed}"], + '@media (width >= 1200px)' => ["[data-rail='collapsed'] [data-md-navigation-rail='adaptive']{$closed}"], + ]; + + // A condition matches the rail itself and everything in it; the scaffold's actions row can only + // ever be inside a rail, so its copy keeps the descendant half alone. + $both = fn (array $conditions): array => array_merge(...array_map(fn (string $condition): array => [$condition, "{$condition} *"], $conditions)); + $inside = fn (array $conditions): array => array_map(fn (string $condition): string => "{$condition} *", $conditions); + + $groups = []; + + foreach (ComponentStylesheet::read($name)->rules() as $rule) { + preg_match_all('/:where\(((?:[^()]++|\((?1)\))*)\)/', $rule['selector'], $wheres, PREG_SET_ORDER); + $wheres = array_filter($wheres, fn (array $where): bool => str_contains($where[1], 'data-md-navigation-rail=')); + + if ($wheres === []) { + continue; + } + + $media = array_values(array_filter($rule['at'], fn (string $at): bool => str_starts_with($at, '@media'))); + $band = match ($media) { + [] => 'always', + ['@media (width >= 840px)', '@media (width < 1200px)'] => '@media (840px <= width < 1200px)', + default => implode(' › ', $media), + }; + + expect(array_key_exists($band, $bands))->toBeTrue("{$name}.css: `{$rule['selector']}` sits in {$band}, which is no band of the rail's"); + + foreach ($wheres as [$where, $conditions]) { + $written = array_map(trim(...), preg_split('/,(?![^()]*\))/', $conditions)); + + expect(in_array($written, [$both($bands[$band]), $inside($bands[$band])], true)) + ->toBeTrue("{$name}.css: `{$where}` in {$band} is not the rail's collapsed conditions for that band"); + } + + $key = str_replace(array_column($wheres, 0), '', $rule['selector']).' '.json_encode($rule['declarations']); + $groups[$key][] = $band; + } + + expect($groups)->not->toBeEmpty(); + + foreach ($groups as $key => $found) { + expect(array_values(array_unique($found)))->toEqualCanonicalizing(array_keys($bands), "{$name}.css: {$key} is drawn collapsed in some bands only"); + } +})->with('stylesheets that draw a collapsed rail'); diff --git a/tests/Support/ComponentStylesheet.php b/tests/Support/ComponentStylesheet.php index df9cf518..05e4023e 100644 --- a/tests/Support/ComponentStylesheet.php +++ b/tests/Support/ComponentStylesheet.php @@ -117,6 +117,17 @@ final class ComponentStylesheet return $declarations; } + /** + * Every rule, nesting resolved, in source order: for a test that has to walk all of them rather + * than name one. + * + * @return list, declarations: array}> + */ + public function rules(): array + { + return $this->rules; + } + public function has(string $selector, array $at = []): bool { try {