')
->toContain('')
->toContain('');
});
it('gives a medium or large bar a collapsed title in its row and the heading under it', function () {
$html = (string) $this->blade('');
expect($html)
->toContain('data-variant="large"')
->not->toContain('data-sticky')
->toContain('
')
->toContain('Settings')
->toContain('
')
->toContain('
Settings
');
});
it('puts a search bar in the row of a search app bar', function () {
expect((string) $this->blade(''))
->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(''))
->toContain('data-variant="small"')
->toContain('
');
});
it('draws floating and docked toolbars', function () {
$html = (string) $this->blade(<<<'BLADE'
BLADE);
expect($html)
->toContain('
')
->toContain('role="toolbar"')
->toContain('materialToolbar(true)')
->toContain('data-variant="floating"')
->toContain('aria-orientation="vertical"')
->toContain('data-vibrant')
->toContain('aria-label="Formatting"')
->toContain('')
->and((string) $this->blade(''))
->toContain('data-variant="docked"')
->not->toContain('data-vibrant')
->not->toContain('data-vertical')
->toContain('data-toolbar-place="bottom"')
->not->toContain('data-toolbar-group');
});
it('keeps a toolbar at the bottom clear of the navigation bar', function () {
$css = file_get_contents(__DIR__.'/../../../resources/css/components/toolbar.css');
expect($css)
// Never a sum: --material-bottom-bar already swallows the bottom safe area (N-02).
->toContain('bottom: calc(max(var(--material-bottom-bar, 0px), var(--material-safe-bottom, env(safe-area-inset-bottom))) + 1rem);')
->toContain('bottom: var(--material-bottom-bar, 0px);');
});
it('offers M3\'s three contrast levels, marking the one in force', function () {
expect((string) $this->blade(''))
->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(''))
->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(''))
->toContain('data-theme-toggle="cycle"')
->toContain("{ light: 'dark', dark: 'system', system: 'light' }")
->and((string) $this->blade(''))
->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'
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(''))
->toContain('')
->not->toContain('data-account-theme');
});