Plan step 36 ("Interaction is the shared classes", the user, 2026-09-14).
The action and the close button hand-rolled md-state-layer, md-focus-ring
and md-touch-target, and toast.css's own header said so; both now render
the classes and keep only their own size and colour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
95 lines
5.0 KiB
PHP
95 lines
5.0 KiB
PHP
<?php
|
|
|
|
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
|
|
|
it('hosts the snackbar queue, kept across wire:navigate', function () {
|
|
$html = (string) $this->blade('<x-toast />');
|
|
|
|
expect($html)
|
|
->toContain('x-persist="material-toast"')
|
|
->toContain('x-data="materialSnackbar"')
|
|
->toContain('data-md-toast')
|
|
->toContain('data-md-position="bottom"')
|
|
->and(ComponentStylesheet::read('toast')->declarations('[data-md-toast-snackbar]'))->toMatchArray([
|
|
'background-color' => 'var(--md-sys-color-inverse-surface)',
|
|
'color' => 'var(--md-sys-color-inverse-on-surface)',
|
|
]);
|
|
});
|
|
|
|
it('draws no state icon, and reaches 48px from its 40px controls', function () {
|
|
$html = (string) $this->blade('<x-toast />');
|
|
$css = ComponentStylesheet::read('toast');
|
|
|
|
// M3 tells you to avoid an icon in a snackbar; the type is left to pick the announcement role.
|
|
expect(substr_count($html, '<svg'))->toBe(1)
|
|
->and($html)->not->toContain('text-inverse-success')
|
|
->toContain('data-md-toast-action')
|
|
->toContain('data-md-toast-dismiss')
|
|
// 48px, from the foundation's md-touch-target class, not a copy of its own.
|
|
->toMatch('/class="md-state-layer md-focus-ring md-touch-target" data-md-toast-action/')
|
|
->toMatch('/class="md-state-layer md-focus-ring md-touch-target" data-md-toast-dismiss/')
|
|
->and($css->declarations(':is([data-md-toast-action], [data-md-toast-dismiss])'))
|
|
->toHaveKey('border-radius', 'var(--md-sys-shape-corner-full)')
|
|
->and($css->has(':is([data-md-toast-action], [data-md-toast-dismiss])::after'))->toBeFalse()
|
|
->and($css->declarations('[data-md-toast-dismiss]'))->toMatchArray([
|
|
'width' => 'var(--md-sys-measurement-space500)',
|
|
'height' => 'var(--md-sys-measurement-space500)',
|
|
]);
|
|
});
|
|
|
|
it('grows to M3\'s two-line height, and wraps the action below on a compact window', function () {
|
|
$html = (string) $this->blade('<x-toast />');
|
|
$css = ComponentStylesheet::read('toast');
|
|
|
|
// SnackbarTokens: 48dp for one line, 68dp for two — a description is that second line.
|
|
expect($html)
|
|
->toContain('x-bind:data-md-two-line="current.description ? \'\' : null"')
|
|
->toContain('x-bind:data-md-toast-wrap="current.description && current.action ? \'\' : null"')
|
|
->and($css->declarations('[data-md-toast-snackbar]'))->toHaveKey('min-height', 'var(--md-sys-measurement-space600)')
|
|
->and($css->declarations('[data-md-toast-snackbar][data-md-two-line]'))->toBe(['min-height' => '68px'])
|
|
// The text grows from a zero basis, so a long title shrinks beside the action rather than
|
|
// wrapping it onto a line of its own where there is room for both.
|
|
->and($css->declarations('[data-md-toast-content]'))->toHaveKey('flex', '1')
|
|
->and($css->declarations('[data-md-toast-content][data-md-toast-wrap]', ['@media (width < 600px)']))->toBe(['flex-basis' => '100%'])
|
|
->and($css->declarations('[data-md-toast-action]'))->toHaveKey('height', 'var(--md-sys-measurement-space500)');
|
|
});
|
|
|
|
it('can sit at the start, growing its margin from medium', function () {
|
|
$css = ComponentStylesheet::read('toast');
|
|
|
|
expect((string) $this->blade('<x-toast position="bottom-start" />'))->toContain('data-md-position="bottom-start"')
|
|
->and($css->declarations("[data-md-toast][data-md-position='bottom-start']"))->toBe(['justify-content' => 'flex-start'])
|
|
->and($css->declarations("[data-md-toast][data-md-position='bottom-start']", ['@media (width >= 600px)']))->toBe([
|
|
'inset-inline-start' => 'var(--md-sys-measurement-space300)',
|
|
]);
|
|
});
|
|
|
|
it('falls back to bottom for an unknown position', function () {
|
|
expect((string) $this->blade('<x-toast position="top" />'))->toContain('data-md-position="bottom"');
|
|
});
|
|
|
|
it('marks the snackbar and its action for tests and styling', function () {
|
|
$html = (string) $this->blade('<x-toast />');
|
|
|
|
expect($html)
|
|
->toMatch('/<div\s[^>]*\bdata-md-toast-snackbar\b/')
|
|
->toMatch('/<button\s[^>]*\bdata-md-toast-action\b[^>]*x-on:click="act\(\)"/')
|
|
->toMatch('/<button\s[^>]*\bdata-md-toast-dismiss\b[^>]*x-on:click="dismiss\(\)"/');
|
|
});
|
|
|
|
it('keeps the live region in the page, polite, around the snackbar that comes and goes', function () {
|
|
$html = (string) $this->blade('<x-toast />');
|
|
|
|
expect($html)
|
|
->toMatch('/<div\s[^>]*x-data="materialSnackbar"[^>]*aria-live="polite"/')
|
|
->toContain('aria-atomic="true"')
|
|
->toContain("x-bind:role=\"current && (current.type === 'error' || current.type === 'warning') ? 'alert' : 'status'\"")
|
|
->and(substr_count($html, 'aria-live'))->toBe(1);
|
|
});
|
|
|
|
it('lifts above a bottom bar, publishing its own height for a FAB to clear', function () {
|
|
expect(ComponentStylesheet::read('toast')->declarations('[data-md-toast]'))->toMatchArray([
|
|
'bottom' => 'calc(var(--material-bottom-bar, 0px) + var(--md-sys-measurement-space200))',
|
|
]);
|
|
});
|