Files
livewire-material/tests/Feature/Components/AppBarTest.php
T
Andreas Reinhold / reiniandClaude Opus 5 4a4982b1ef Reach M3's 48px target from every 40px navigation control
The theme toggle and the account-menu avatar are drawn 40px and had no target
extension; both now carry `touch-target`, and the avatar's clipping moves from
the button (which would have cut the pseudo-target off) to the <img>. The rail's
hand-rolled `after:` target becomes the same utility.
Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-01).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 05:52:09 +02:00

124 lines
5.5 KiB
PHP

<?php
it('draws a small, sticky app bar with a heading, navigation and actions', function () {
$html = (string) $this->blade(<<<'BLADE'
<x-app-bar title="Shares" subtitle="12 active">
<x-slot:navigation><button>menu</button></x-slot:navigation>
<x-slot:actions><button>search</button></x-slot:actions>
</x-app-bar>
BLADE);
expect($html)
->toContain('<header')
->toContain('x-data="materialAppBar"')
->toContain('data-variant="small"')
->toContain('data-sticky')
->toContain('data-subtitled')
->toContain('<h1 data-app-bar-title>Shares</h1>')
->toContain('<p data-app-bar-subtitle>12 active</p>')
->toContain('<div data-app-bar-leading><button>menu</button></div>')
->toContain('<div data-app-bar-trailing><button>search</button></div>');
});
it('gives a medium or large bar a collapsed title in its row and the heading under it', function () {
$html = (string) $this->blade('<x-app-bar variant="large" title="Settings" heading="h2" :sticky="false" />');
expect($html)
->toContain('data-variant="large"')
->not->toContain('data-sticky')
->toContain('<div data-app-bar-headline aria-hidden="true" >')
->toContain('<span data-app-bar-title>Settings</span>')
->toContain('<div data-app-bar-expanded>')
->toContain('<h2 data-app-bar-title>Settings</h2>');
});
it('puts a search bar in the row of a search app bar', function () {
expect((string) $this->blade('<x-app-bar variant="search"><x-search placeholder="Search shares" /></x-app-bar>'))
->toContain('data-app-bar-search')
->toContain('placeholder="Search shares"');
});
it('falls back to a small bar and an h1 for unknown values', function () {
expect((string) $this->blade('<x-app-bar variant="huge" heading="section" title="Shares" />'))
->toContain('data-variant="small"')
->toContain('<h1 data-app-bar-title>');
});
it('draws floating and docked toolbars', function () {
$html = (string) $this->blade(<<<'BLADE'
<x-toolbar label="Formatting" vibrant vertical place="end">
<button>B</button>
<x-slot:fab><button>+</button></x-slot:fab>
</x-toolbar>
BLADE);
expect($html)
->toContain('<div data-toolbar-group data-vertical data-toolbar-place="end" >')
->toContain('role="toolbar"')
->toContain('materialToolbar(true)')
->toContain('data-variant="floating"')
->toContain('aria-orientation="vertical"')
->toContain('data-vibrant')
->toContain('aria-label="Formatting"')
->toContain('<button>+</button>')
->and((string) $this->blade('<x-toolbar variant="docked" vibrant vertical place="bottom"><button>B</button></x-toolbar>'))
->toContain('data-variant="docked"')
->not->toContain('data-vibrant')
->not->toContain('data-vertical')
->toContain('data-toolbar-place="bottom"')
->not->toContain('data-toolbar-group');
});
it('offers M3\'s three contrast levels, marking the one in force', function () {
expect((string) $this->blade('<x-theme-toggle mode="contrast" />'))
->toContain('data-theme-toggle="contrast"')
->toContain('role="radiogroup"')
->toContain('aria-label="Contrast"')
->toContain('data-contrast-option="standard"')
->toContain('data-contrast-option="medium"')
->toContain('data-contrast-option="high"')
->toContain("\$store.theme.setContrast('high')")
// The row follows the resolved level, so the operating system's shows under `system`.
->toContain("(\$store.theme.resolvedContrast === 'medium').toString()");
});
it('switches the theme through the store in three shapes', function () {
expect((string) $this->blade('<x-theme-toggle />'))
->toContain('data-theme-toggle="toggle"')
->toContain('aria-label="Dark theme"')
// 40px drawn, 48px reached: M3's minimum target (N-01).
->toContain('touch-target')
->toContain('$store.theme.toggle()')
->and((string) $this->blade('<x-theme-toggle mode="cycle" />'))
->toContain('data-theme-toggle="cycle"')
->toContain("{ light: 'dark', dark: 'system', system: 'light' }")
->and((string) $this->blade('<x-theme-toggle mode="picker" />'))
->toContain('role="radiogroup"')
->toContain('data-theme-option="system"')
->toContain("\$store.theme.set('dark')");
});
it('opens an account menu from the initials of a name', function () {
$html = (string) $this->blade(<<<'BLADE'
<x-account-menu name="Anna Maria Keller" email="anna@example.com">
<x-menu-item label="Settings" />
<x-slot:footer><x-menu-item label="Sign out" /></x-slot:footer>
</x-account-menu>
BLADE);
expect($html)
->toContain('aria-label="Account"')
->toContain('data-account-menu')
// The 40px avatar reaches 48px, and nothing clips the pseudo-target (N-01).
->toContain('touch-target')
->not->toContain('overflow-hidden')
->toMatch('/>\s*AM\s*<\/button>/')
->toContain('anna@example.com')
->toContain('Settings')
->toContain('data-account-theme')
->toContain('Sign out')
->and((string) $this->blade('<x-account-menu avatar="/anna.jpg" :theme="false" />'))
->toContain('<img src="/anna.jpg" alt="" class="size-full rounded-corner-full object-cover" />')
->not->toContain('data-account-theme');
});