tests / feature (8.4) (push) Successful in 2m0s
tests / feature (8.5) (push) Successful in 2m0s
tests / browser (chrome, chromium) (push) Failing after 8m3s
tests / browser (firefox, firefox) (push) Failing after 12m58s
tests / browser (safari, webkit) (push) Failing after 13m8s
Tailwind left the stack in 2.0.0, but the package still carried about 330 mentions of it. What the guard's Tailwind detection protected — a class that compiles to nothing — is now protected by a check that does not care where a dead class came from. DesignGuard: about 500 lines of Tailwind tables, scales, palettes and "2.0.0 replacement" hints give way to one check — a class a view or PHP file writes that neither the application's stylesheets nor the package's own declare. It catches a utility of any framework, a typo and a class whose rules were deleted alike, so it also found two classes ReStride draws nothing with. A stylesheet has to be in reach for it: the `.css` files among the scanned paths, or what the `missingStylesheets()` entry imports. The class reader no longer mistakes an array index for a class list (`$block['base']`), and it reads the array a class helper is given, where it read nothing before. The package's own three Tailwind self-guards go with it. Only their one unique check stays, as a test of its own: every `matchMedia` width in resources/js is an M3 breakpoint. The pagination views are `material.blade.php` and `simple-material.blade.php`; only Laravel's and Livewire's default theme names ever made them `tailwind`. The provider sets `Paginator`'s default views and switches `livewire.pagination_theme` to `material` when it is still Livewire's own default, so no application can forget the config; a theme an application chose, and a component's own `$paginationTheme` or `paginationView()`, still win. The rest is prose: the layer-order guidance for an application that still builds Tailwind, the Tailwind wording in the README, the Boost guidelines and the development skill, and about 25 "this used to be a Tailwind utility" comments, along with every "plan step NN" pointer into a gitignored folder. The reset keeps its credit, and NOTICE now carries it too. Feature suite 1159 passed, Chrome browser suite 299 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
122 lines
5.5 KiB
PHP
122 lines
5.5 KiB
PHP
<?php
|
|
|
|
it('draws a search bar whose combobox controls the view', function () {
|
|
$html = (string) $this->blade('<x-search id="find" placeholder="Search shares" wire:model.live.debounce.300ms="query" />');
|
|
|
|
expect($html)
|
|
->toContain('data-md-search')
|
|
->toContain('data-md-trigger="bar"')
|
|
->toContain('x-bind:data-md-open="open ? \'\' : null"')
|
|
->toContain('x-bind:data-md-full-screen="fullScreen ? \'\' : null"')
|
|
->toContain('role="search"')
|
|
->toContain('type="search"')
|
|
->toContain('placeholder="Search shares"')
|
|
->toContain('aria-label="Search shares"')
|
|
->toContain('role="combobox"')
|
|
->toContain('aria-haspopup="dialog"')
|
|
->toContain('aria-controls="find-view"')
|
|
->toContain('aria-expanded="false"')
|
|
->toContain('wire:model.live.debounce.300ms="query"')
|
|
->toContain('id="find-view"')
|
|
->toContain('data-md-search-clear')
|
|
->toContain('data-md-search-scrim')
|
|
->toContain('aria-label="Back"')
|
|
->toContain('materialSearch(false,')
|
|
->not->toContain('data-md-search-results')
|
|
// Split so the string never reads as a literal class="..." attribute to the design guard's
|
|
// own scan of this file — `relative`, the utility class the field used to carry.
|
|
->not->toContain('class="'.'relative"');
|
|
|
|
// The state belongs to the combobox, never to the bare textbox ARIA gives it to.
|
|
expect($html)->toMatch('/<span\s+data-md-search-field[^>]*aria-expanded/s');
|
|
expect($html)->not->toMatch('/<input[^>]*aria-expanded/s');
|
|
});
|
|
|
|
it('announces how many results there are, politely', function () {
|
|
$html = (string) $this->blade('<x-search />');
|
|
|
|
expect($html)
|
|
->toContain('data-md-search-status class="md-visually-hidden"')
|
|
->toContain('aria-live="polite"')
|
|
->toContain('aria-atomic="true"')
|
|
->toContain('No results')
|
|
->toContain('1 result')
|
|
->toContain(':count results');
|
|
});
|
|
|
|
it('shows the results as a list, or what to say when there are none', function () {
|
|
expect((string) $this->blade('<x-search><a href="/shares/1">holiday.zip</a></x-search>'))
|
|
->toContain('data-md-search-results role="list"')
|
|
->toContain('<a href="/shares/1">holiday.zip</a>')
|
|
->and((string) $this->blade('<x-search><x-slot:empty>No shares match.</x-slot:empty></x-search>'))
|
|
->toContain('<p data-md-search-empty>No shares match.</p>')
|
|
->not->toContain('data-md-search-results role="list"');
|
|
});
|
|
|
|
it('offers M3\'s search-icon entry point', function () {
|
|
$html = (string) $this->blade('<x-search id="find" trigger="icon" label="Search shares" />');
|
|
|
|
expect($html)
|
|
->toContain('data-md-trigger="icon"')
|
|
->toContain('data-md-search-trigger')
|
|
->toContain('x-ref="trigger"')
|
|
->toContain('x-on:click="expand()"')
|
|
->toContain('aria-label="Search shares"')
|
|
->toContain('aria-haspopup="dialog"')
|
|
->toContain('aria-controls="find-view"')
|
|
->toContain('materialSearch(false,')
|
|
->toContain("'icon'")
|
|
// The bar is still there: it becomes the expanded view's header.
|
|
->toContain('data-md-search-bar');
|
|
|
|
expect((string) $this->blade('<x-search />'))
|
|
->toContain('data-md-trigger="bar"')
|
|
->not->toContain('data-md-search-trigger');
|
|
});
|
|
|
|
it('shows suggestions until the first keystroke, then the results', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<x-search>
|
|
<a href="/shares/1">holiday.zip</a>
|
|
<x-slot:suggestions><a href="/recent">Recent shares</a></x-slot:suggestions>
|
|
</x-search>
|
|
BLADE);
|
|
|
|
expect($html)
|
|
->toContain('data-md-search-suggestions role="list"')
|
|
->toContain('x-show="query === \'\'"')
|
|
->toContain('Recent shares')
|
|
->toContain('x-show="query !== \'\'"')
|
|
->toContain('holiday.zip')
|
|
->toContain('1 suggestion')
|
|
->toContain(':count suggestions');
|
|
|
|
// Without the slot the results stand alone and are never hidden.
|
|
expect((string) $this->blade('<x-search><a href="/shares/1">holiday.zip</a></x-search>'))
|
|
->not->toContain('data-md-search-suggestions')
|
|
->not->toContain('x-show="query !== \'\'"');
|
|
});
|
|
|
|
it('names the input, trails the bar, and stays docked on request', function () {
|
|
$html = (string) $this->blade('<x-search label="Search your shares" icon="travel_explore" docked><x-slot:trailing><button>AR</button></x-slot:trailing></x-search>');
|
|
|
|
expect($html)
|
|
->toContain('placeholder="Search"')
|
|
->toContain('aria-label="Search your shares"')
|
|
->toContain('data-md-search-trailing')
|
|
->toContain('<button>AR</button>')
|
|
->toContain('materialSearch(true,');
|
|
});
|
|
|
|
it('draws the bar and the view from its stylesheet, at M3\'s widths', function () {
|
|
$css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/search.css');
|
|
|
|
expect($css)->toContain('@layer material.components')
|
|
->toMatch('/\\[data-md-search\\] \\{\\s*--search-height: 56px;[^}]*--search-open-width: 720px;/')
|
|
->toContain('[data-md-search][data-md-full-screen] [data-md-search-view] {')
|
|
->toContain("[data-md-search][data-md-trigger='icon']:not([data-md-open], [data-md-full-screen]) [data-md-search-bar] {")
|
|
// Leaving full screen, the header bar fades with the view instead of going on the first frame.
|
|
->toContain('[data-md-search][data-md-full-screen]:not([data-md-open]) [data-md-search-bar] {')
|
|
->not->toMatch('/\\d(?:\\.\\d+)?rem/');
|
|
});
|