*/ public array $files = []; public string $audience = 'anyone'; public bool $public = false; public string $message = ''; public function save(): void { $this->validate(['name' => 'required']); } public function render(): string { return <<<'BLADE'

name: {{ $name }}

hours: {{ $hours }}

files: {{ implode(',', $files) }}

audience: {{ $audience }}

public: {{ var_export($public, true) }}

@foreach (['a', 'b', 'c'] as $file) @endforeach
BLADE; } } function fieldProbe() { Livewire::component('field-probe', FieldProbe::class); Route::middleware('web')->get('/field-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles @livewireScripts BLADE)); return visit('/field-probe')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } it('floats the label while the field has focus or holds a value', function () { $label = "getComputedStyle(document.querySelector('label[for=\"name-field\"]')).fontSize"; $page = fieldProbe()->assertScript("{$label} === '16px'"); $page->click('#name-field')->assertScript("{$label} === '12px'"); $page->type('#name-field', 'Holiday') ->assertSeeIn('#name', 'Holiday'); $page->click('#password-field')->assertScript("{$label} === '12px'"); }); it('clears a field and tells Livewire', function () { fieldProbe() ->type('#name-field', 'Holiday') ->assertSeeIn('#name', 'Holiday') ->click('[data-md-field-clear]') ->assertScript("document.querySelector('#name-field').value === ''") ->assertScript("document.querySelector('#name').textContent === ''") ->assertScript("document.activeElement.id === 'name-field'"); }); it('copies a field\'s value and says so', function () { $page = fieldProbe()->type('#name-field', 'Holiday'); $page->script("window.eval(\"Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: async (text) => { window.copied = text } } })\")"); $page->click('[data-md-field-copy]') ->assertScript("window.copied === 'Holiday'") ->assertSee('Copied to the clipboard'); }); it('shows and hides a password', function () { fieldProbe() ->type('#password-field', 'secret') ->click('[data-md-field-reveal]') ->assertScript("document.querySelector('#password-field').type === 'text'") ->assertAttribute('[data-md-field-reveal]', 'aria-label', 'Hide password') ->click('[data-md-field-reveal]') ->assertScript("document.querySelector('#password-field').type === 'password'"); }); it('shows the server\'s error in place of the hint', function () { fieldProbe() ->click('button:has-text("Save")') ->assertSee('The name field is required.') ->assertAttribute('#name-field', 'aria-invalid', 'true') ->assertScript("document.querySelector('#name-field').closest('[data-md-field]').hasAttribute('data-md-invalid')"); }); it('binds a select to Livewire', function () { fieldProbe() ->select('#hours-field', '1') ->assertSeeIn('#hours', '1'); }); it('keeps a partly ticked checkbox in step with the server', function () { $all = "document.querySelector('#all')"; $page = fieldProbe()->assertScript("! {$all}.indeterminate"); $page->click('label[for="file-a"]') ->assertSeeIn('#files', 'a') ->assertScript("{$all}.hasAttribute('data-indeterminate') && {$all}.indeterminate"); $page->click('label[for="file-b"]') ->click('label[for="file-c"]') ->assertSeeIn('#files', 'a,b,c') ->assertScript("! {$all}.hasAttribute('data-indeterminate') && ! {$all}.indeterminate"); }); it('moves between radio buttons with the arrow keys', function () { fieldProbe() ->keys('input[type="radio"][value="anyone"]', 'ArrowDown') ->assertSeeIn('#audience', 'team') ->assertScript("document.querySelector('input[type=\"radio\"][value=\"team\"]').checked"); }); it('turns a switch on from the keyboard and from its label', function () { $page = fieldProbe() ->keys('#public', 'Space') ->assertSeeIn('#public-state', 'true'); $page->click('label[for="public"]') ->assertSeeIn('#public-state', 'false') ->assertAttribute('#public', 'role', 'switch'); }); it('grows a textarea with its text up to its maximum', function () { $height = "document.querySelector('#message').getBoundingClientRect().height"; $page = fieldProbe(); $rows = $page->script("({$height})"); $page->type('#message', "one\ntwo\nthree") ->assertScript("({$height}) > {$rows}"); $three = $page->script("({$height})"); $page->type('#message', "one\ntwo\nthree\nfour\nfive\nsix\nseven") ->assertScript("Math.abs(({$height}) - ({$three} + 24)) < 2") ->assertScript("document.querySelector('#message').scrollHeight > document.querySelector('#message').clientHeight"); }); it('pairs the server\'s error with a trailing error icon in the error colour', function () { $icon = "document.querySelector('[data-md-field]:has(#plain-name-field) [data-md-field-error]')"; fieldProbe() ->assertScript("{$icon} === null") ->click('button:has-text("Save")') ->assertSee('The name field is required.') // A field that trails with buttons of its own gives the icon's place to them. ->assertScript("document.querySelector('[data-md-field]:has(#name-field)').hasAttribute('data-md-invalid') && document.querySelector('[data-md-field]:has(#name-field) [data-md-field-error]') === null") ->assertScript("{$icon}.getAttribute('aria-label') === 'Error' && {$icon}.getAttribute('role') === 'img'") ->assertScript("{$icon}.getBoundingClientRect().width === 24") ->assertScript("getComputedStyle({$icon}).color === getComputedStyle(document.querySelector('[data-md-field]:has(#plain-name-field) [data-md-field-support]')).color"); }); it('lights an enabled field\'s outline on hover, and never a disabled one\'s', function () { $edge = fn (string $id): string => "getComputedStyle(document.querySelector('[data-md-field]:has(#{$id}) [data-md-field-outline]')).borderTopColor"; $page = fieldProbe(); $resting = $page->script("({$edge('name-field')})"); $disabled = $page->script("({$edge('off-field')})"); $page->hover('[data-md-field]:has(#name-field) [data-md-field-box]') ->assertScript("({$edge('name-field')}) !== '{$resting}'"); $page->hover('[data-md-field]:has(#off-field) [data-md-field-box]') ->assertScript("({$edge('off-field')}) === '{$disabled}'") // The disabled edge is on-surface at 12%, which is translucent. ->assertScript("/rgba?\\(.*,\\s*0?\\.\\d+\\)|color\\(srgb .* \\/ 0?\\.\\d+\\)/.test('{$disabled}')"); }); it('counts the characters as they are typed, marks a count past the maximum and says it once typing stops', function () { $counter = "document.querySelector('[data-md-field]:has(#bio-field) [data-md-field-counter]')"; $page = fieldProbe() ->assertScript("{$counter}.querySelector('[aria-hidden]').textContent === '0/10'") ->type('#bio-field', 'Hello') ->assertScript("{$counter}.querySelector('[aria-hidden]').textContent === '5/10'") ->assertScript("! {$counter}.hasAttribute('data-md-over')"); $within = $page->script("getComputedStyle({$counter}).color"); // maxlength stops typing at the maximum, but a value put in from elsewhere can pass it. $page->script("(() => { const input = document.querySelector('#bio-field'); input.value = 'Hello there!'; input.dispatchEvent(new Event('input', { bubbles: true })); })()"); $page->assertScript("{$counter}.querySelector('[aria-hidden]').textContent === '12/10'") ->assertScript("{$counter}.hasAttribute('data-md-over')") ->assertScript("getComputedStyle({$counter}).color !== '{$within}'") ->assertScript("{$counter}.querySelector('[aria-live]').textContent === ''") ->wait(1.3) ->assertScript("{$counter}.querySelector('[aria-live=\\'polite\\']').textContent === 'Character count, 12/10'"); }); function fieldWidthProbe(int $width) { Route::middleware('web')->get('/field-width-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite'))
BLADE)); return visit('/field-width-probe')->resize($width, 800)->waitForEvent('networkidle'); } it('bounds a field to 40rem from 600px, unless the caller\'s width rule or full says otherwise', function () { $width = fn (string $id): string => "document.querySelector('[data-md-field]:has(#{$id})').getBoundingClientRect().width"; fieldWidthProbe(599) ->assertScript("({$width('bounded')}) === 1400") ->assertScript("({$width('narrow')}) === 200") ->assertScript("({$width('full')}) === 1400"); fieldWidthProbe(600) ->assertScript("({$width('bounded')}) === 640") ->assertScript("({$width('narrow')}) === 200") ->assertScript("({$width('full')}) === 1400"); }); it('draws the field at M3\'s geometry and the file picker\'s button as a tonal pill', function () { $box = fn (string $id): string => "document.querySelector('[data-md-field]:has(#{$id}) [data-md-field-box]').getBoundingClientRect()"; fieldProbe() ->assertScript("({$box('name-field')}).height === 56") // The control starts after the box's 16px padding. ->assertScript("document.querySelector('#name-field').getBoundingClientRect().left - ({$box('name-field')}).left === 16") ->assertScript("getComputedStyle(document.querySelector('#upload-field'), '::file-selector-button').height === '32px'") ->assertScript("getComputedStyle(document.querySelector('#upload-field'), '::file-selector-button').borderTopLeftRadius !== '0px'") ->assertScript("getComputedStyle(document.querySelector('#upload-field')).color === 'rgba(0, 0, 0, 0)'"); });