]*>/s', (string) test()->blade($blade), $matches);
return $matches[0] ?? '';
}
it('is an outlined assist chip, a button, by default', function () {
$html = (string) $this->blade(' ');
expect(chipTag(' ', 'button'))
->toBe('')
->and(chipTag(' ', 'button'))
->toContain('data-md-icon-end=""')
->not->toContain('data-md-icon-start')
->and($html)
->toMatch('/]*--md-icon-size: 18px)(?=[^>]*data-md-chip-icon)[^>]*>/')
->toMatch('/]*--md-icon-size: 18px)(?=[^>]*data-md-chip-trailing)[^>]*>/')
->toContain('Add to calendar ')
->not->toContain('class=');
});
it('draws a chip at Compose\'s size, corner, type and padding from its stylesheet', function () {
$css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/chip.css');
expect($css)->toContain('@layer material.components')
->toContain("@import './icon.css';")
->toMatch('/\[data-md-chip\] \{[^}]*height: 32px;[^}]*border-radius: var\(--md-sys-shape-corner-sm\);[^}]*font: var\(--md-sys-typescale-label-lg\);/')
// 16px beside a label and 8px beside an icon, each 1px short for the border.
->toContain('padding-inline: 15px;')
->toContain('padding-inline-start: 7px;')
->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/chip.css';");
});
it('pads a chip without icons by 16px on both sides', function () {
expect(chipTag(' ', 'button'))
->not->toContain('data-md-icon-start')
->not->toContain('data-md-icon-end');
});
it('falls back to an assist chip for a type it does not know', function () {
expect(chipTag(' ', 'button'))->toContain('data-md-chip="assist"');
});
it('draws an elevated chip on surface-container-low at elevation 1', function () {
expect(chipTag(' ', 'button'))->toContain('data-md-elevated=""')
->and(chipTag(' ', 'button'))->not->toContain('data-md-elevated')
// Input chips are flat only.
->and(chipTag(' ', 'span'))->not->toContain('data-md-elevated')
->and((string) file_get_contents(__DIR__.'/../../../resources/css/components/chip.css'))
->toMatch('/\[data-md-chip\]\[data-md-elevated\] \{[^}]*background-color: var\(--md-sys-color-surface-container-low\);[^}]*box-shadow: var\(--md-sys-elevation-1\);/');
});
it('greys a disabled chip, flat or elevated', function () {
expect(chipTag(' ', 'button'))
->toContain('data-md-disabled=""')
->toMatch('/\sdisabled="disabled"/')
->and(chipTag(' ', 'button'))
->toContain('data-md-elevated=""')
->toContain('data-md-disabled=""');
});
it('links with wire:navigate, and a disabled link leaves the tab order', function () {
expect(chipTag(' ', 'a'))
->toContain('href="/directions"')
->toContain('wire:navigate')
->not->toContain('type=')
->and(chipTag(' ', 'a'))
->toContain('target="_blank"')
->toContain('rel="noopener"')
->not->toContain('wire:navigate')
->and(chipTag(' ', 'a'))
->toContain('aria-disabled="true"')
->toContain('tabindex="-1"')
->toContain('data-md-disabled=""');
});
it('draws a suggestion chip in on-surface-variant', function () {
expect(chipTag(' ', 'button'))
->toContain('data-md-chip="suggestion"')
->and(chipTag(' ', 'button'))
->toContain('data-md-elevated=""');
});
it('attaches a plain tooltip', function () {
$html = (string) $this->blade(' ');
expect($html)
->toContain('popover="manual"')
->toContain('Share this file')
->toMatch('/]*style="anchor-name: --material-chip-[a-z0-9]{10}"/');
});
it('makes a filter chip without a model a toggle button the caller owns', function () {
$html = (string) $this->blade(' ');
expect(chipTag(' ', 'button'))
->toContain('data-md-chip="filter"')
->toContain('aria-pressed="true"')
->toContain('wire:click="toggleStarred"')
->and(chipTag(' ', 'button'))
->toContain('aria-pressed="false"')
->and($html)
->not->toContain('type="checkbox"')
->toContain('')
->toContain('data-md-chip-check');
});
it('makes a bound filter chip a native checkbox under the chip', function () {
$html = (string) $this->blade(' ');
expect(chipTag(' ', 'label'))
->toBe('')
->and(chipTag(' ', 'input'))
->toContain('type="checkbox"')
->toContain('value="photos"')
->toContain('x-model="kinds"')
->toContain('class="md-visually-hidden"')
->not->toContain('me-4')
->not->toContain('checked')
->and($html)
->toContain('data-md-chip-check');
});
it('submits a named filter chip, checked when selected, and greys a disabled one', function () {
expect(chipTag(' ', 'input'))
->toContain('name="starred"')
->toContain('checked')
->and(chipTag(' ', 'input'))
->toContain('disabled')
->and(chipTag(' ', 'label'))
->toContain('data-md-disabled=""');
});
it('swaps a filter chip\'s icon for the check when selected', function () {
$html = (string) $this->blade(' ');
expect($html)
->toMatch('/\s*]*data-md-chip-icon[^>]*>.*?<\/svg>\s*]*data-md-chip-check/s')
->toContain('data-md-elevated=""')
->and(substr_count($html, 'toBe(2)
->and((string) file_get_contents(__DIR__.'/../../../resources/css/components/chip.css'))
->toContain("[data-md-chip]:is(:has(:checked), [aria-pressed='true']) [data-md-chip-check-slot] [data-md-chip-icon] {");
});
it('draws an input chip with an avatar, an icon or neither', function () {
expect(chipTag(' ', 'span'))
->toContain('data-md-chip="input"')
->toContain('data-md-avatar=""')
->not->toContain('x-data')
->and((string) $this->blade(' '))
->toContain('AM ')
->not->toContain('and((string) $this->blade(' '))
->toContain(' ')
->and(chipTag(' ', 'span'))
->toContain('data-md-icon-start=""')
->not->toContain('data-md-avatar');
});
it('makes an input chip a button when it has its own action, and selects it', function () {
expect(chipTag(' ', 'button'))
->toContain('data-md-chip-action')
->toContain('aria-pressed="true"')
->toContain('wire:click="open(1)"')
->and(chipTag(' ', 'span'))
->toContain('data-md-selected=""')
// An action of its own lights the leading icon on hover, focus and press.
->and(chipTag(' ', 'span'))
->toContain('data-md-actionable=""')
->and(chipTag(' ', 'span'))
->not->toContain('data-md-actionable');
});
it('gives a removable input chip a named remove button that calls wire:remove', function () {
$html = (string) $this->blade(' ');
expect(chipTag(' ', 'span'))
->toContain('x-data="materialChip"')
->toContain('x-on:keydown="removeOnKey($event)"')
->toContain('data-md-removable=""')
->not->toContain('wire:remove')
->and($html)
->toContain('data-md-chip-remove aria-label="Remove anna@example.com"')
->toContain('wire:click="removeRecipient(3)"')
->toContain('x-on:click="removeChip($event)"')
->toContain(' ')
->and((string) file_get_contents(__DIR__.'/../../../resources/css/components/chip.css'))
// 18px icon, 15px out on every side: M3's 48dp minimum for the close icon's target.
->toMatch('/\[data-md-chip-remove\] \{.*?&::after \{\s*content: \'\';\s*position: absolute;\s*inset: -15px;/s');
});
it('runs an Alpine remove expression, or takes the chip off the page with neither', function () {
expect((string) $this->blade(' '))
->toContain('x-on:click="removeChip($event); tags.splice(0, 1)"')
->not->toContain('wire:click')
->and((string) $this->blade(' '))
->toContain('x-on:click="removeChip($event); $root.remove()"');
});
it('leaves a client-rendered chip\'s remove button to be named in the browser', function () {
expect((string) $this->blade(' '))
->toContain('data-md-chip-remove="Remove :label"')
->not->toContain('aria-label=');
});
it('disables a removable input chip and its remove button', function () {
$html = (string) $this->blade(' ');
expect($html)
->toContain('data-md-disabled=""')
->toMatch('/]*data-md-chip-remove[^>]*disabled/s');
});
it('groups chips in a wrapping row named by its label, with a hint', function () {
$html = (string) $this->blade(' ');
preg_match('/aria-labelledby="([^"]+)"/', $html, $labelledBy);
preg_match('/aria-describedby="([^"]+)"/', $html, $describedBy);
expect($html)
->toContain('role="group"')
->toContain('data-chip-set x-data="materialChipSet"')
->toContain('flex flex-wrap gap-2')
// M3's chip keyboard: the set is one tab stop and the arrows walk it.
->toContain('x-on:keydown="key($event)"')
->toContain('x-on:focusin="rove($event.target)"')
->toContain("id=\"{$labelledBy[1]}\" class=\"mb-2 type-label-lg text-on-surface-variant\">File types
")
->toContain("id=\"{$describedBy[1]}\" class=\"mt-1 type-body-sm text-on-surface-variant\">Show only these");
});
it('scrolls a chip set on one line with fading edges', function () {
expect((string) $this->blade(' '))
->toContain('aria-label="Sort"')
->toContain('x-data="materialChipSet"')
->toContain('x-ref="row"')
->toContain('wire:ignore.self')
->toContain('overflow-x-auto')
->toContain('data-scroll-start:[--chip-fade-start:1.5rem]')
->not->toContain('flex-wrap');
});
it('puts a scroll button over each fading edge, for a pointer that cannot swipe', function () {
$html = (string) $this->blade(' ');
expect($html)
->toContain('data-chip-scroll="start"')
->toContain('data-chip-scroll="end"')
->toContain('x-on:click="nudge(\'start\')"')
->toContain('x-on:click="nudge(\'end\')"')
// Only while the row can still scroll that way, and only where the pointer is fine.
->toContain('peer-data-scroll-start:pointer-fine:grid')
->toContain('peer-data-scroll-end:pointer-fine:grid')
// A pointer affordance, not a tab stop: the arrows already walk every chip.
->toContain('tabindex="-1"')
->toContain('aria-hidden="true"')
// A chip the keyboard reaches clears the buttons as well as the fade.
->toContain('pointer-fine:scroll-px-10');
// A wrapping set has no edge to scroll towards.
expect((string) $this->blade(' '))
->not->toContain('data-chip-scroll');
});
it('binds filter chips to a Livewire array and shows the set\'s validation message in place of its hint', function () {
$component = new class extends Component
{
/** @var list */
public array $kinds = ['photos'];
public function save(): void
{
$this->validate(['kinds' => 'max:1'], ['kinds.max' => 'Pick one kind.']);
}
public function render(): string
{
return <<<'BLADE'
BLADE;
}
};
Livewire::test($component)
->assertSeeHtml('wire:model.live="kinds"')
->assertSeeHtml('value="photos" checked')
->assertDontSeeHtml('value="documents" checked')
->assertSee('Show only these')
->set('kinds', ['photos', 'documents'])
->assertSeeHtml('value="documents" checked')
->call('save')
->assertSee('Pick one kind.')
->assertDontSee('Show only these');
});