blade(<<<'BLADE'
BLADE);
preg_match('/anchor-name: (--material-menu-([a-z0-9]+))/', $html, $anchor);
expect($anchor)->not->toBeEmpty()
->and($html)
->toContain('x-data="materialMenu"')
->toContain('')
->toContain('popover="auto"')
->toContain('role="menu"')
->toContain('aria-label="Share actions"')
->toContain("id=\"material-menu-{$anchor[2]}\"")
->toContain("position-anchor: {$anchor[1]}")
->toContain('bg-surface-container-low')
->toContain('[position-area:bottom_span-right]');
});
it('scrolls a menu too long for the window instead of running off it', function () {
expect((string) $this->blade(''))
->toContain('max-h-[min(18rem,calc(100dvh-2rem))] overflow-y-auto')
->not->toContain('overflow-visible');
});
it('opens at the position asked for, in the vibrant colours on request', function () {
$html = (string) $this->blade('');
expect($html)->toContain('[position-area:top_span-left]')->toContain('bg-tertiary-container text-on-tertiary-container');
});
it('draws an item as a menuitem button', function () {
$html = (string) $this->blade('');
expect($html)
->toContain('role="menuitem"')
->toContain('tabindex="-1"')
->toContain('type="button"')
->toContain('wire:click="download"')
->toContain('Download')
->toContain('⌘D')
->toContain('As a ZIP')
->toContain('size-5 text-on-surface-variant');
});
it('makes a selectable item a menuitemcheckbox, ticked at its end', function () {
$check = trim((string) file_get_contents(__DIR__.'/../../../resources/svg/symbols/outlined-20/check.svg'));
$check = substr($check, (int) strpos($check, '><') + 1);
expect((string) $this->blade(''))
->toContain('role="menuitemcheckbox"')
->toContain('aria-checked="true"')
->toContain('bg-tertiary-container')
->toContain('data-keep-open')
->toContain($check)
->and((string) $this->blade(''))
->toContain('aria-checked="false"')
->not->toContain($check)
->and((string) $this->blade(''))
->not->toContain($check);
});
it('gives an item M3\'s 48px row and 16px sides, and the separator its 8px', function () {
expect((string) $this->blade(''))->toContain('min-h-12')->toContain('px-4')
->and((string) $this->blade(''))->toContain('mx-4 my-2')
->and((string) $this->blade(''))->toContain('px-4 pt-2 pb-1');
});
it('marks the page an item leads to, and carries a badge', function () {
$html = (string) $this->blade('');
expect($html)
->toContain('role="menuitem"')
->toContain('aria-current="page"')
->not->toContain('aria-checked')
->toContain('bg-secondary-container text-on-secondary-container')
->toContain('>3<')
->and((string) $this->blade(''))
->not->toContain('aria-current')
->not->toContain('secondary-container');
});
it('links an item, and keeps a disabled one out of reach', function () {
$this->blade('')
->assertSee('href="/settings"', false)
->assertSee('wire:navigate', false);
$this->blade('')
->assertSee('aria-disabled="true"', false)
->assertSee('pointer-events-none', false);
});
it('separates and labels groups', function () {
$this->blade('')->assertSee('role="separator"', false);
$this->blade('')
->assertSee('role="group" aria-label="Sort by"', false);
});
it('separates clusters by a gap instead of a divider when asked', function () {
$html = (string) $this->blade('');
// M3 Expressive's grouped layout: 2px between the items of a cluster, 8px between clusters —
// the same 8px the divider keeps above and below its line.
expect($html)
->toContain('role="group"')
->toContain('not-first:mt-2')
->toContain('space-y-0.5')
->not->toContain('aria-label')
->not->toContain('py-1 first:pt-0')
->and((string) $this->blade(''))
->toContain('aria-label="Then"')
->toContain('space-y-0.5')
->and((string) $this->blade(''))
->toContain('py-1 first:pt-0 last:pb-0')
->not->toContain('space-y-0.5');
});
it('adds icon-class to the leading icon, over its own colour but not over disabled', function () {
$leading = fn (string $html): string => preg_match('/