declarations("[data-md-field-button]:disabled::before, [data-md-field-button][aria-disabled='true']::before"))->toBe(['display' => 'none']); }); it('draws an outlined field with a notched label unless told otherwise', function () { $html = (string) $this->blade(''); expect($html) ->toContain('data-md-field') ->toContain('data-md-input') ->toContain('data-md-variant="outlined"') ->toContain('Share name') ->toMatch('/Share name<\/label>/') ->toContain('placeholder=" "') ->toContain('data-md-field-control') ->not->toContain('class='); }); it('draws a filled field on request, or by the application\'s default', function () { expect((string) $this->blade(''))->toContain('data-md-variant="filled"'); config(['livewire-material.fields.variant' => 'filled']); expect((string) $this->blade(''))->toContain('data-md-variant="filled"') ->and((string) $this->blade(''))->toContain('data-md-variant="outlined"') ->and((string) $this->blade(''))->toContain('data-md-variant="filled"'); }); it('ties the label, the hint and the control together', function () { $html = (string) $this->blade(''); expect($html) ->toContain('') ->toContain('id="share-name"') ->toContain('aria-describedby="share-name-support"') ->toMatch('/Recipients see this name<\/p>/') ->toContain('wire:model="name"') ->not->toContain('aria-invalid'); }); it('replaces the hint with the errors for its model', function () { $html = (string) $this->withViewErrors(['email' => ['Enter a valid email address.']]) ->blade(''); expect($html) ->toContain('data-md-invalid') ->toContain('aria-invalid="true"') ->toContain('role="alert"') ->toContain('aria-describedby="email-support"') ->toContain('Enter a valid email address.') ->not->toContain('We never share it'); }); it('pairs the error colours with a trailing error icon', function () { $errors = ['email' => ['Enter a valid email address.']]; expect((string) $this->withViewErrors($errors)->blade('')) ->toMatch('/]*data-md-field-trailing)(?=[^>]*data-md-field-error)(?=[^>]*aria-label="Error")(?=[^>]*role="img")[^>]*>/') ->and((string) $this->withViewErrors($errors)->blade('')) ->not->toContain('data-md-field-error') ->and((string) $this->withViewErrors($errors)->blade('')) ->not->toContain('data-md-field-error') ->and((string) $this->blade('')) ->not->toContain('data-md-field-error'); }); it('puts icons, affixes and the size on the field, and every other attribute on the input', function () { $html = (string) $this->blade(''); expect($html) ->toMatch('//') ->toContain('CHF') ->toContain('per month') ->toContain('type="number"') ->toContain('min="0"') // An sm field's icons are 20px, drawn from the 20 cut. ->and(substr_count($html, '--md-icon-size: 20px'))->toBe(2) ->and(substr_count($html, 'toBe(2); }); it('offers to clear and to copy the value', function () { $html = (string) $this->blade(''); expect($html) ->toContain('data-md-field-clear') ->toContain('aria-label="Clear"') ->toContain('data-md-field-copy') ->toContain("\$el.closest('[data-md-field]').querySelector('[data-md-field-control]')") // A labelled field is md: its icons are 24px. ->toContain('--md-icon-size: 24px') ->toContain('navigator.clipboard.writeText') ->toContain('aria-label="Copy"'); }); it('lets a password be shown and hidden again', function () { $html = (string) $this->blade(''); expect($html) ->toContain('type="password"') ->not->toContain('type="text"') ->toContain('x-bind:type="shown ? \'text\' : \'password\'"') ->toContain('data-md-password') ->toMatch('/]*data-md-field-button)(?=[^>]*data-md-field-reveal)[^>]*>/') ->toContain('aria-label="Show password"') // The flipping label already says the state; aria-pressed would say it again, inverted. ->not->toContain('aria-pressed') ->toContain('autocomplete="current-password"'); }); it('grows a textarea from its rows up to its maximum, or leaves it to be resized', function () { expect((string) $this->blade('')) ->toContain('rows="2"') ->toContain('data-md-textarea') ->toContain('data-md-autogrow') ->toContain('--field-rows: 2;') ->toContain('--field-max-rows: 6;') ->and((string) $this->blade('')) ->toContain('rows="3"') ->not->toContain('data-md-autogrow'); }); it('draws a native select whose label always floats', function () { $html = (string) $this->blade(<<<'BLADE' BLADE); expect($html) ->toContain('data-md-floated') ->toContain('data-md-select') ->toMatch('/]*data-md-field-control)[^>]*>/') ->toContain('toContain('wire:model="hours"') ->toContain('Choose…') ->toContain('1 hour') ->toContain('30 days') ->toContain('data-md-field-arrow'); }); it('takes a select\'s options from its slot as well', function () { expect((string) $this->blade('Small')) ->toContain('Small'); }); it('collects a file field\'s own errors and each file\'s', function () { $html = (string) $this->withViewErrors([ 'photos' => ['Choose at most 10 photos.'], 'photos.0' => ['photo.jpg is larger than 2 GB.'], ])->blade(''); expect($html) ->toContain('type="file"') ->toContain('multiple') ->toContain('data-md-file') ->toContain('data-md-floated') ->toContain('Choose at most 10 photos.') ->toContain('photo.jpg is larger than 2 GB.'); }); it('counts the characters of a field that has a maximum', function () { $html = (string) $this->blade(''); expect($html) ->toContain('data-md-field-support-row') ->toContain('data-md-field-counter') ->toContain('x-bind:data-md-over="used > max"') ->toContain('') ->toContain('maxlength="60"') ->toContain('max: 60,') ->toContain('>0/60') ->toContain('aria-live="polite"') ->toContain('Shown on your profile'); expect((string) $this->blade('')) ->toContain('data-md-field-counter') ->toContain('>0/140'); }); it('shows no counter without `counter` or without a maximum to count against', function () { expect((string) $this->blade(''))->not->toContain('data-md-field-counter') ->and((string) $this->blade(''))->not->toContain('data-md-field-counter') ->and((string) $this->blade(''))->not->toContain('data-md-field-counter'); }); it('bounds a field\'s width from medium unless it is told to fill its pane', function () { expect(file_get_contents(__DIR__.'/../../../resources/css/components/field.css')) ->toContain('@media (width >= 600px)') ->toMatch('/\[data-md-field\]:not\(\[data-md-full\]\)\s*\{\s*max-width: 40rem;/'); expect((string) $this->blade(''))->toContain('data-md-full') ->and((string) $this->blade(''))->toContain('data-md-full') ->and((string) $this->blade(''))->not->toContain('data-md-full'); }); it('lays out a form with its actions under an optional divider', function () { $html = (string) $this->blade(<<<'BLADE' Save BLADE); expect($html) ->toContain('') ->toContain('role="separator"') ->toContain('') ->not->toContain('grid-cols-1') ->toContain('Save') ->and((string) $this->blade('Save')) ->not->toContain('role="separator"'); }); it('draws a form\'s column and its actions from its stylesheet', function () { $css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/form.css'); expect($css)->toContain('@layer material.components') ->toContain('grid-template-columns: minmax(0, 1fr);') ->toContain('gap: var(--md-sys-measurement-space200);') ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/form.css';"); }); it('keeps the field\'s chrome in its own stylesheet in the components layer', function () { $css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/field.css'); expect($css)->toContain('@layer material.components') ->toContain("@import './icon.css';") ->toContain('[data-md-field-box]') ->not->toMatch('/(?and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/field.css';") ->and((string) file_get_contents(__DIR__.'/../../../resources/css/tailwind.css'))->not->toContain('field.css'); }); it('takes a custom control marked as the field\'s control, and a hint class', function () { $html = (string) $this->blade(<<<'BLADE' BLADE); expect($html) ->toContain('') ->toMatch('/In the future<\/p>/') ->toContain(''); }); it('gives the one-line field a stylesheet that brings the field and its icons', function () { $css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/input.css'); expect($css)->toStartWith('/*') ->toContain("@import './field.css';") ->toContain("@import './icon.css';") ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/input.css';"); }); it('gives the password field a stylesheet that brings the field and its icons', function () { expect((string) file_get_contents(__DIR__.'/../../../resources/css/components/password.css')) ->toContain("@import './field.css';") ->toContain("@import './icon.css';") ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/password.css';"); }); it('draws what a textarea changes in the field from its own stylesheet', function () { $css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/textarea.css'); expect($css)->toContain("@import './field.css';") ->toContain('@layer material.components') ->toContain('textarea[data-md-field-control][data-md-autogrow]') ->toContain('field-sizing: content;') ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components/field.css'))->not->toContain('textarea[') ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/textarea.css';"); }); it('draws what a select changes in the field from its own stylesheet', function () { $css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/select.css'); expect($css)->toContain("@import './field.css';") ->toContain('@supports (appearance: base-select)') ->toContain('select[data-md-field-control]:open') ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components/field.css'))->not->toContain('select[') ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/select.css';"); }); it('draws the file picker\'s button from its own stylesheet', function () { $css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/file.css'); expect($css)->toContain("@import './field.css';") ->toContain("input[type='file'][data-md-field-control]::file-selector-button") ->toContain('background-color: var(--md-sys-color-secondary-container);') ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components/field.css'))->not->toContain("[type='file']") ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/file.css';"); });
Recipients see this name<\/p>/') ->toContain('wire:model="name"') ->not->toContain('aria-invalid'); }); it('replaces the hint with the errors for its model', function () { $html = (string) $this->withViewErrors(['email' => ['Enter a valid email address.']]) ->blade(''); expect($html) ->toContain('data-md-invalid') ->toContain('aria-invalid="true"') ->toContain('role="alert"') ->toContain('aria-describedby="email-support"') ->toContain('Enter a valid email address.') ->not->toContain('We never share it'); }); it('pairs the error colours with a trailing error icon', function () { $errors = ['email' => ['Enter a valid email address.']]; expect((string) $this->withViewErrors($errors)->blade('')) ->toMatch('/]*data-md-field-trailing)(?=[^>]*data-md-field-error)(?=[^>]*aria-label="Error")(?=[^>]*role="img")[^>]*>/') ->and((string) $this->withViewErrors($errors)->blade('')) ->not->toContain('data-md-field-error') ->and((string) $this->withViewErrors($errors)->blade('')) ->not->toContain('data-md-field-error') ->and((string) $this->blade('')) ->not->toContain('data-md-field-error'); }); it('puts icons, affixes and the size on the field, and every other attribute on the input', function () { $html = (string) $this->blade(''); expect($html) ->toMatch('//') ->toContain('CHF') ->toContain('per month') ->toContain('type="number"') ->toContain('min="0"') // An sm field's icons are 20px, drawn from the 20 cut. ->and(substr_count($html, '--md-icon-size: 20px'))->toBe(2) ->and(substr_count($html, 'toBe(2); }); it('offers to clear and to copy the value', function () { $html = (string) $this->blade(''); expect($html) ->toContain('data-md-field-clear') ->toContain('aria-label="Clear"') ->toContain('data-md-field-copy') ->toContain("\$el.closest('[data-md-field]').querySelector('[data-md-field-control]')") // A labelled field is md: its icons are 24px. ->toContain('--md-icon-size: 24px') ->toContain('navigator.clipboard.writeText') ->toContain('aria-label="Copy"'); }); it('lets a password be shown and hidden again', function () { $html = (string) $this->blade(''); expect($html) ->toContain('type="password"') ->not->toContain('type="text"') ->toContain('x-bind:type="shown ? \'text\' : \'password\'"') ->toContain('data-md-password') ->toMatch('/]*data-md-field-button)(?=[^>]*data-md-field-reveal)[^>]*>/') ->toContain('aria-label="Show password"') // The flipping label already says the state; aria-pressed would say it again, inverted. ->not->toContain('aria-pressed') ->toContain('autocomplete="current-password"'); }); it('grows a textarea from its rows up to its maximum, or leaves it to be resized', function () { expect((string) $this->blade('')) ->toContain('rows="2"') ->toContain('data-md-textarea') ->toContain('data-md-autogrow') ->toContain('--field-rows: 2;') ->toContain('--field-max-rows: 6;') ->and((string) $this->blade('')) ->toContain('rows="3"') ->not->toContain('data-md-autogrow'); }); it('draws a native select whose label always floats', function () { $html = (string) $this->blade(<<<'BLADE' BLADE); expect($html) ->toContain('data-md-floated') ->toContain('data-md-select') ->toMatch('/]*data-md-field-control)[^>]*>/') ->toContain('toContain('wire:model="hours"') ->toContain('Choose…') ->toContain('1 hour') ->toContain('30 days') ->toContain('data-md-field-arrow'); }); it('takes a select\'s options from its slot as well', function () { expect((string) $this->blade('Small')) ->toContain('Small'); }); it('collects a file field\'s own errors and each file\'s', function () { $html = (string) $this->withViewErrors([ 'photos' => ['Choose at most 10 photos.'], 'photos.0' => ['photo.jpg is larger than 2 GB.'], ])->blade(''); expect($html) ->toContain('type="file"') ->toContain('multiple') ->toContain('data-md-file') ->toContain('data-md-floated') ->toContain('Choose at most 10 photos.') ->toContain('photo.jpg is larger than 2 GB.'); }); it('counts the characters of a field that has a maximum', function () { $html = (string) $this->blade(''); expect($html) ->toContain('data-md-field-support-row') ->toContain('data-md-field-counter') ->toContain('x-bind:data-md-over="used > max"') ->toContain('') ->toContain('maxlength="60"') ->toContain('max: 60,') ->toContain('>0/60') ->toContain('aria-live="polite"') ->toContain('Shown on your profile'); expect((string) $this->blade('')) ->toContain('data-md-field-counter') ->toContain('>0/140'); }); it('shows no counter without `counter` or without a maximum to count against', function () { expect((string) $this->blade(''))->not->toContain('data-md-field-counter') ->and((string) $this->blade(''))->not->toContain('data-md-field-counter') ->and((string) $this->blade(''))->not->toContain('data-md-field-counter'); }); it('bounds a field\'s width from medium unless it is told to fill its pane', function () { expect(file_get_contents(__DIR__.'/../../../resources/css/components/field.css')) ->toContain('@media (width >= 600px)') ->toMatch('/\[data-md-field\]:not\(\[data-md-full\]\)\s*\{\s*max-width: 40rem;/'); expect((string) $this->blade(''))->toContain('data-md-full') ->and((string) $this->blade(''))->toContain('data-md-full') ->and((string) $this->blade(''))->not->toContain('data-md-full'); }); it('lays out a form with its actions under an optional divider', function () { $html = (string) $this->blade(<<<'BLADE' Save BLADE); expect($html) ->toContain('') ->toContain('role="separator"') ->toContain('') ->not->toContain('grid-cols-1') ->toContain('Save') ->and((string) $this->blade('Save')) ->not->toContain('role="separator"'); }); it('draws a form\'s column and its actions from its stylesheet', function () { $css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/form.css'); expect($css)->toContain('@layer material.components') ->toContain('grid-template-columns: minmax(0, 1fr);') ->toContain('gap: var(--md-sys-measurement-space200);') ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/form.css';"); }); it('keeps the field\'s chrome in its own stylesheet in the components layer', function () { $css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/field.css'); expect($css)->toContain('@layer material.components') ->toContain("@import './icon.css';") ->toContain('[data-md-field-box]') ->not->toMatch('/(?and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/field.css';") ->and((string) file_get_contents(__DIR__.'/../../../resources/css/tailwind.css'))->not->toContain('field.css'); }); it('takes a custom control marked as the field\'s control, and a hint class', function () { $html = (string) $this->blade(<<<'BLADE' BLADE); expect($html) ->toContain('') ->toMatch('/In the future<\/p>/') ->toContain(''); }); it('gives the one-line field a stylesheet that brings the field and its icons', function () { $css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/input.css'); expect($css)->toStartWith('/*') ->toContain("@import './field.css';") ->toContain("@import './icon.css';") ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/input.css';"); }); it('gives the password field a stylesheet that brings the field and its icons', function () { expect((string) file_get_contents(__DIR__.'/../../../resources/css/components/password.css')) ->toContain("@import './field.css';") ->toContain("@import './icon.css';") ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/password.css';"); }); it('draws what a textarea changes in the field from its own stylesheet', function () { $css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/textarea.css'); expect($css)->toContain("@import './field.css';") ->toContain('@layer material.components') ->toContain('textarea[data-md-field-control][data-md-autogrow]') ->toContain('field-sizing: content;') ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components/field.css'))->not->toContain('textarea[') ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/textarea.css';"); }); it('draws what a select changes in the field from its own stylesheet', function () { $css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/select.css'); expect($css)->toContain("@import './field.css';") ->toContain('@supports (appearance: base-select)') ->toContain('select[data-md-field-control]:open') ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components/field.css'))->not->toContain('select[') ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/select.css';"); }); it('draws the file picker\'s button from its own stylesheet', function () { $css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/file.css'); expect($css)->toContain("@import './field.css';") ->toContain("input[type='file'][data-md-field-control]::file-selector-button") ->toContain('background-color: var(--md-sys-color-secondary-container);') ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components/field.css'))->not->toContain("[type='file']") ->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/file.css';"); });
In the future<\/p>/') ->toContain('