blade(''); 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('/]*aria-expanded/s'); expect($html)->not->toMatch('/]*aria-expanded/s'); }); it('announces how many results there are, politely', function () { $html = (string) $this->blade(''); 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('holiday.zip')) ->toContain('data-md-search-results role="list"') ->toContain('holiday.zip') ->and((string) $this->blade('No shares match.')) ->toContain('

No shares match.

') ->not->toContain('data-md-search-results role="list"'); }); it('offers M3\'s search-icon entry point', function () { $html = (string) $this->blade(''); 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('')) ->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' holiday.zip Recent shares 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('holiday.zip')) ->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(''); expect($html) ->toContain('placeholder="Search"') ->toContain('aria-label="Search your shares"') ->toContain('data-md-search-trailing') ->toContain('') ->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/all.css'))->toContain("@import './components/search.css';"); });