Files
livewire-material/tests/Feature/Components/ListDetailTest.php
T
Andreas Reinhold / reiniandClaude Opus 5 247c596c3a Cut duplicated and speculative code across the package
An over-engineering audit of the whole tree, applied in five reviewed
batches. Behaviour stays the same except where UPGRADE.md says otherwise.

PHP: the showcase and error-page stylesheets are prebuilt into
resources/dist by bin/stylesheets.mjs, through Vite's own postcss-import
(first occurrence kept, the order an application's build gives), instead
of Stylesheets::bundle() inlining imports on every request; only the
import walk DesignGuard needs stays. SchemeStylesheet::withProfiles()
replaces three copies of the scheme-plus-profiles loop, material:scheme
leaves spec and contrast checks to the node script that already made
them, and the error page's scheme cache, the hashed view namespace, the
translations path with no lang/ folder and DesignGuard's 1.x-name hints
are gone.

JS: the androidx shape port progress.js and both bin scripts each carried
lives once in resources/js/shapes.js (the generated SVGs are unchanged);
util.js holds ringIndex(), ms(), reopenGuard() and remember(), which
were written out several times; listeners are released through
AbortController; tooltip.js's hoverPopover() serves the rich tooltip too.

CSS: every rule for an element inside the navigation rail queries
`--md-navigation-rail-value` instead of repeating the seven collapsed
conditions under five media branches; badge, alert, progress, slider and
button read one non-inheriting colour-role table (components/color.css);
the dialog chrome, the submenu's popover chrome, the chip's state layer
and touch target, and the visually-hidden inputs use the shared rules
they copied; foundation/tokens.css is folded into foundation.css.

Views: Support\Field and Support\Link replace the error-key, bound-value
and link-attribute blocks copied into the fields and link components;
the timepicker period group, the menu filter and the showcase head are
partials; the datepicker's steppers and entry fields are loops; component
docblocks no longer restate SKILL.md.

Tests and tooling: one dataset-driven ComponentStylesheetsTest replaces
four per-group files, DesignGuardTest and the layout-component tests use
datasets, browser tests share one ready() helper, CSS parsing lives in
ComponentStylesheet alone. docs/audits and the finding IDs citing it are
removed, as are pestphp/pest-plugin-laravel, the unused composer scripts
and check:font; the lint job runs in the feature job, which now installs
node packages so the prebuilt-stylesheet staleness test runs in CI.

Feature suite 1177 passed, Chrome browser suite 299 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:29:21 +02:00

80 lines
4.4 KiB
PHP

<?php
use Livewire\Component;
use Livewire\Livewire;
it('draws the list pane and the detail pane, each focusable as a whole, with nothing selected', function () {
$html = (string) $this->blade(<<<'BLADE'
<x-list-detail>
<x-slot:list><ul>LIST</ul></x-slot:list>
<x-slot:detail><article>DETAIL</article></x-slot:detail>
</x-list-detail>
BLADE);
expect(layoutRoot($html))->toMatchArray([
'<' => 'div',
'data-md-list-detail' => 'data-md-list-detail',
'x-data' => 'materialListDetail( null )',
'x-modelable' => 'selected',
'x-bind:data-md-selected' => "hasSelection ? '' : null",
'wire:ignore.self' => '',
])->not->toHaveKey('data-md-selected')
->and($html)->toMatch('/<div\s+data-md-list-detail-pane="list"\s+tabindex="-1"\s+x-ref="list"\s+x-on:focusin="remember\(\$event\)"\s+x-on:click="remember\(\$event\)"\s*><ul>LIST<\/ul><\/div>/')
->toMatch('/<div data-md-list-detail-pane="detail" tabindex="-1" x-ref="detail">.*<article>DETAIL<\/article>\s*<\/div>\s*<\/div>\s*$/s');
});
it('renders the pane that is selected from the first paint, given once or bound to Livewire', function () {
expect(layoutRoot((string) $this->blade('<x-list-detail :selected="7" />')))->toMatchArray(['data-md-selected' => 'data-md-selected', 'x-data' => 'materialListDetail( 7 )'])
->and(layoutRoot((string) $this->blade('<x-list-detail selected="" />')))->not->toHaveKey('data-md-selected')
->and(layoutRoot((string) $this->blade('<x-list-detail :selected="false" />')))->not->toHaveKey('data-md-selected')
->and(layoutRoot((string) $this->blade('<x-list-detail :selected="0" />')))->toHaveKey('data-md-selected');
Livewire::component('list-detail-render-probe', new class extends Component
{
public ?int $messageId = 3;
public function render(): string
{
return '<div><x-list-detail wire:model.live="messageId"><x-slot:list>LIST</x-slot:list><x-slot:detail>DETAIL</x-slot:detail></x-list-detail></div>';
}
});
$probe = Livewire::test('list-detail-render-probe');
$html = $probe->html();
// The server renders the pane from the property, and Alpine takes the property over; the
// binding stays off the element, where Livewire would look for an input.
expect($html)->toContain('data-md-selected')
->toMatch("/x-data=\"materialListDetail\\(\\s*window\\.Livewire\\.find\\('[^']+'\\)\\.entangle\\('messageId'\\)\\.live\\s*\\)\"/")
->not->toContain('x-modelable')
->not->toContain('wire:model');
$probe->set('messageId', null);
expect($probe->html())->not->toMatch('/data-md-list-detail[^>]*data-md-selected/');
});
it('draws its own back row above the detail, unless the detail brings one', function () {
$own = (string) $this->blade('<x-list-detail><x-slot:list>LIST</x-slot:list><x-slot:detail>DETAIL</x-slot:detail></x-list-detail>');
$pane = (string) $this->blade(<<<'BLADE'
<x-list-detail>
<x-slot:list>LIST</x-slot:list>
<x-slot:detail><x-pane title="Message" back>DETAIL</x-pane></x-slot:detail>
</x-list-detail>
BLADE);
expect($own)->toMatch('/x-ref="detail">\s*<div data-md-list-detail-back-row>\s*<span data-md-list-detail-back>\s*<button[^>]*aria-label="Back"[^>]*x-on:click="back\(\)"/s')
->and(substr_count($pane, 'data-md-list-detail-back'))->toBe(1)
->and($pane)->not->toContain('data-md-list-detail-back-row');
});
it('shows one pane below expanded and both from it, at M3\'s fixed pane widths', function () {
$css = (string) file_get_contents(__DIR__.'/../../../resources/css/layout/list-detail.css');
expect($css)->toContain("@media (width >= 840px) {\n grid-template-columns: 360px minmax(0, 1fr);\n column-gap: var(--md-sys-measurement-space300);")
->toContain("@media (width >= 1200px) {\n grid-template-columns: 412px minmax(0, 1fr);")
->toContain("@media (width < 840px) {\n [data-md-list-detail]:not([data-md-selected]) > [data-md-list-detail-pane='detail'],\n [data-md-list-detail][data-md-selected] > [data-md-list-detail-pane='list'] {\n display: none;")
->toContain("[data-md-list-detail-back] [data-md-icon]:is([dir='rtl'], [dir='rtl'] *)")
->and((string) file_get_contents(__DIR__.'/../../../resources/js/material.js'))->toContain("import './layout.js'");
});