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>
80 lines
4.4 KiB
PHP
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'");
|
|
});
|