Files
livewire-material/tests/Feature/Components/SearchTest.php
T
Andreas Reinhold / reiniandClaude Opus 5 4141ebe7b0 Draw the search without Tailwind
<x-search> renders data-md-search with data-md-trigger and binds
data-md-open and data-md-full-screen; its parts become data-md-search-*
and the empty row data-md-search-empty. search.css moves into
material.components on px, spacing and state tokens (plan step 36).
search.js and the showcase layout follow the renamed hooks.

Browser tests cover the polite result count as it changes, the icon
entry point expanding full screen and returning focus, and suggestions
swapping to results on the first key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 15:04:50 +02:00

120 lines
5.3 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')
->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-search-bar] {")
->not->toMatch('/\\d(?:\\.\\d+)?rem/')
->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/search.css';")
->and((string) file_get_contents(__DIR__.'/../../../resources/css/tailwind.css'))->not->toContain('search.css');
});