Merge branch 'worktree-agent-a6b15d4e17dad5970'
This commit is contained in:
@@ -4,17 +4,18 @@ it('states a notice in its state\'s container, with the state\'s icon', function
|
||||
$html = (string) $this->blade('<x-alert title="Storage almost full" description="3.8 of 4 GB" color="warning" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('role="alert"')
|
||||
->toContain('role="status"')
|
||||
->toContain('bg-warning-container text-on-warning-container')
|
||||
->toContain('Storage almost full')
|
||||
->toContain('3.8 of 4 GB')
|
||||
->toContain('<svg');
|
||||
});
|
||||
|
||||
it('is a status unless it is an error or a warning', function () {
|
||||
it('is a status whatever its colour, and interrupts only when asked to', function () {
|
||||
expect((string) $this->blade('<x-alert description="Saved." tone="success" />'))->toContain('role="status"')->toContain('bg-success-container')
|
||||
->and((string) $this->blade('<x-alert description="Heads up." />'))->toContain('role="status"')->toContain('bg-info-container')
|
||||
->and((string) $this->blade('<x-alert description="Broken." color="error" />'))->toContain('role="alert"');
|
||||
->and((string) $this->blade('<x-alert description="Broken." color="error" />'))->toContain('role="status"')
|
||||
->and((string) $this->blade('<x-alert description="Broken." color="error" assertive />'))->toContain('role="alert"');
|
||||
});
|
||||
|
||||
it('takes actions, drops its icon on request, and can be dismissed', function () {
|
||||
@@ -30,5 +31,6 @@ it('takes actions, drops its icon on request, and can be dismissed', function ()
|
||||
->toContain('Try again')
|
||||
->toContain('x-data="{ shown: true }"')
|
||||
->toContain('aria-label="Dismiss"')
|
||||
->toContain('touch-target')
|
||||
->and(substr_count($html, '<svg'))->toBe(1);
|
||||
});
|
||||
|
||||
@@ -14,9 +14,9 @@ it('is M3\'s large badge with a count, capped by max', function () {
|
||||
});
|
||||
|
||||
it('pins itself to the corner of an icon when floating', function () {
|
||||
expect((string) $this->blade('<x-badge floating />'))->toContain('absolute top-0.5 end-0.5')
|
||||
expect((string) $this->blade('<x-badge floating />'))->toContain('absolute top-0 end-0')
|
||||
->and((string) $this->blade('<x-badge value="3" floating label="3 new messages" />'))
|
||||
->toContain('absolute -top-1')
|
||||
->toContain('absolute -top-0.5')
|
||||
->toContain('aria-label="3 new messages"')
|
||||
->not->toContain('aria-hidden');
|
||||
});
|
||||
@@ -27,7 +27,7 @@ it('draws a status label in a container or an outline', function () {
|
||||
->toContain('bg-success-container text-on-success-container')
|
||||
->not->toContain('aria-hidden')
|
||||
->and((string) $this->blade('<x-badge value="Pro" outline />'))
|
||||
->toContain('border border-outline-variant text-on-surface-variant')
|
||||
->toContain('border border-outline text-on-surface-variant')
|
||||
->not->toContain('bg-error');
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ it('draws neutral ink on every variant', function () {
|
||||
expect((string) $this->blade('<x-badge color="neutral" />'))->toContain('size-1.5 rounded-corner-full bg-on-surface-variant text-surface')
|
||||
->and((string) $this->blade('<x-badge value="7" color="neutral" />'))->toContain('tabular-nums bg-on-surface-variant text-surface')
|
||||
->and((string) $this->blade('<x-badge value="Draft" tone="neutral" tonal />'))->toContain('type-label-md bg-surface-container-high text-on-surface-variant')
|
||||
->and((string) $this->blade('<x-badge value="Draft" color="neutral" outline />'))->toContain('type-label-md border border-outline-variant text-on-surface-variant');
|
||||
->and((string) $this->blade('<x-badge value="Draft" color="neutral" outline />'))->toContain('type-label-md border border-outline text-on-surface-variant');
|
||||
});
|
||||
|
||||
it('leaves a plain badge to the caller\'s colour classes', function (string $badge, string $shape) {
|
||||
@@ -73,7 +73,7 @@ it('keeps its default colours, and falls back to error on a colour it does not k
|
||||
->and((string) $this->blade('<x-badge value="Active" tonal />'))
|
||||
->toContain('class="inline-flex shrink-0 items-center justify-center whitespace-nowrap h-6 gap-1 rounded-corner-sm px-2 type-label-md bg-error-container text-on-error-container"')
|
||||
->and((string) $this->blade('<x-badge value="Pro" outline color="success" />'))
|
||||
->toContain('class="inline-flex shrink-0 items-center justify-center whitespace-nowrap h-6 gap-1 rounded-corner-sm px-2 type-label-md border border-outline-variant text-on-surface-variant"')
|
||||
->toContain('class="inline-flex shrink-0 items-center justify-center whitespace-nowrap h-6 gap-1 rounded-corner-sm px-2 type-label-md border border-outline text-on-surface-variant"')
|
||||
->and((string) $this->blade('<x-badge value="3" color="sport-run" />'))->toContain('bg-error text-on-error');
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,11 @@ it('groups buttons with the spacing of their size', function () {
|
||||
it('connects buttons 2px apart', function () {
|
||||
expect((string) $this->blade('<x-button-group connected><x-button label="Day" /></x-button-group>'))
|
||||
->toContain('data-button-group="connected"')
|
||||
->toContain('gap-0.5')
|
||||
->toContain('gap-0.5');
|
||||
});
|
||||
|
||||
it('never wraps a group onto a second line', function () {
|
||||
expect((string) $this->blade('<x-button-group><x-button label="Day" /></x-button-group>'))
|
||||
->not->toContain('flex-wrap');
|
||||
});
|
||||
|
||||
@@ -46,6 +50,16 @@ it('draws a choice as connected radios', function () {
|
||||
->and(substr_count($html, '<svg'))->toBe(1);
|
||||
});
|
||||
|
||||
it('keeps a 48px target and a 48px minimum width on the smallest connected segments', function () {
|
||||
$options = [['id' => 'a', 'name' => 'A']];
|
||||
|
||||
expect((string) $this->blade('<x-group size="sm" name="x" :$options />', ['options' => $options]))
|
||||
->toContain('touch-target min-w-12')
|
||||
->and((string) $this->blade('<x-group size="md" name="x" :$options />', ['options' => $options]))
|
||||
->not->toContain('touch-target')
|
||||
->toContain('min-w-0');
|
||||
});
|
||||
|
||||
it('draws several choices as checkboxes', function () {
|
||||
expect((string) $this->blade('<x-group name="days" multiple variant="outlined" :options="[[\'id\' => \'mon\', \'name\' => \'Mon\']]" />'))
|
||||
->toContain('type="checkbox"')
|
||||
|
||||
@@ -49,7 +49,7 @@ it('sizes a label button on Expressive\'s scale', function (string $size, string
|
||||
|
||||
expect($html)->toContain($classes)->toContain($icon);
|
||||
})->with([
|
||||
'xs' => ['xs', 'h-8 gap-2 px-3 type-label-lg', 'size-5'],
|
||||
'xs' => ['xs', 'h-8 gap-2 px-4 type-label-lg', 'size-5'],
|
||||
'sm' => ['sm', 'h-10 gap-2 px-4 type-label-lg', 'size-5'],
|
||||
'md' => ['md', 'h-14 gap-2 px-6 type-title-md', 'size-6'],
|
||||
'lg' => ['lg', 'h-24 gap-3 px-12 type-headline-sm', 'size-8'],
|
||||
@@ -63,8 +63,25 @@ it('rounds by default, squares on request, and squares off further while pressed
|
||||
});
|
||||
|
||||
it('reaches a 48px touch target below the medium size', function () {
|
||||
expect(buttonClasses('<x-button label="Go" size="sm" />'))->toContain('after:min-h-12')
|
||||
->and(buttonClasses('<x-button label="Go" size="md" />'))->not->toContain('after:min-h-12');
|
||||
expect(buttonClasses('<x-button label="Go" size="sm" />'))->toContain('touch-target')
|
||||
->and(buttonClasses('<x-button label="Go" size="md" />'))->not->toContain('touch-target');
|
||||
});
|
||||
|
||||
it('fills a default icon button\'s glyph, and draws a 20px one from the 20px cut', function () {
|
||||
$geometry = function (string $file): string {
|
||||
$svg = trim((string) file_get_contents(__DIR__.'/../../../resources/svg/symbols/'.$file.'.svg'));
|
||||
|
||||
return substr($svg, (int) strpos($svg, '><') + 1);
|
||||
};
|
||||
|
||||
// A default icon button is filled, a toggle still reads outlined unselected, filled selected.
|
||||
expect((string) $this->blade('<x-button icon="favorite" aria-label="Keep" />'))->toContain($geometry('filled/favorite'))
|
||||
->and((string) $this->blade('<x-button icon="favorite" aria-label="Keep" :selected="false" />'))->toContain($geometry('outlined/favorite'))
|
||||
->and((string) $this->blade('<x-button icon="favorite" aria-label="Keep" :selected="true" />'))->toContain($geometry('filled/favorite'));
|
||||
|
||||
// 20px glyphs come from the 20px cut; 24px ones stay on the default.
|
||||
expect((string) $this->blade('<x-button label="Keep" icon="favorite" />'))->toContain($geometry('outlined-20/favorite'))
|
||||
->and((string) $this->blade('<x-button label="Keep" icon="favorite" size="md" />'))->toContain($geometry('outlined/favorite'));
|
||||
});
|
||||
|
||||
it('is an icon button named by its tooltip when it has no label', function () {
|
||||
@@ -120,10 +137,12 @@ it('disables a button, and a link as far as a link can be', function () {
|
||||
->assertSee('tabindex="-1"', false);
|
||||
});
|
||||
|
||||
it('shows the loading indicator while its own action runs', function () {
|
||||
it('shows the loading indicator while its own action runs, in the button\'s own ink', function () {
|
||||
$this->blade('<x-button label="Save" wire:click="save" spinner />')
|
||||
->assertSee('wire:loading.attr="disabled"', false)
|
||||
->assertSee('wire:target="save"', false);
|
||||
->assertSee('wire:target="save"', false)
|
||||
->assertSee('size-5 text-current', false)
|
||||
->assertDontSee('text-primary"', false);
|
||||
|
||||
$this->blade('<x-button label="Save" wire:click="save" spinner="upload" />')
|
||||
->assertSee('wire:target="upload"', false);
|
||||
@@ -149,7 +168,9 @@ it('is a FAB on a compact window and a filled button from medium, in one element
|
||||
$html = (string) $this->blade('<x-button label="New share" icon="add" fab />');
|
||||
|
||||
expect(substr_count($html, '<button'))->toBe(1)
|
||||
->and($html)->toContain('max-medium:fixed')->toContain('max-medium:bg-primary-container')->toContain('bg-primary text-on-primary');
|
||||
->and($html)->toContain('max-medium:fixed')->toContain('max-medium:bg-primary-container')->toContain('bg-primary text-on-primary')
|
||||
// M3 puts a snackbar above a FAB, never over one: the host publishes its height.
|
||||
->toContain('max-medium:bottom-[calc(var(--material-bottom-bar,0px)+var(--material-snackbar-height,0px)+1rem)]');
|
||||
});
|
||||
|
||||
it('submits a form when asked', function () {
|
||||
|
||||
@@ -17,14 +17,42 @@ it('sizes a FAB at 56, 80 and 96px', function (string $size, string $classes, st
|
||||
'lg' => ['lg', 'size-24 rounded-corner-xl', 'size-8'],
|
||||
]);
|
||||
|
||||
it('fills the FAB\'s glyph, as M3 asks twice over', function () {
|
||||
$svg = trim((string) file_get_contents(__DIR__.'/../../../resources/svg/symbols/filled/add.svg'));
|
||||
$filled = substr($svg, (int) strpos($svg, '><') + 1);
|
||||
|
||||
expect((string) $this->blade('<x-fab icon="add" aria-label="New" />'))->toContain($filled)
|
||||
->and((string) $this->blade('<x-fab-menu label="New" />'))->toContain($filled);
|
||||
});
|
||||
|
||||
it('extends with a label', function () {
|
||||
expect((string) $this->blade('<x-fab icon="upload" label="Upload" size="md" color="tertiary" variant="filled" />'))
|
||||
->toContain('h-20 min-w-20 gap-3 px-[26px] rounded-corner-lg-increased type-title-lg')
|
||||
->toContain('h-20 min-w-20 gap-4 px-[26px] rounded-corner-lg-increased type-title-lg')
|
||||
->toContain('bg-tertiary text-on-tertiary')
|
||||
->toContain('<span>Upload</span>')
|
||||
->not->toContain('aria-label');
|
||||
});
|
||||
|
||||
it('gives the extended FAB M3\'s gaps and its 80px minimum width', function (string $size, string $classes) {
|
||||
expect((string) $this->blade("<x-fab icon=\"add\" label=\"New\" size=\"{$size}\" />"))->toContain($classes);
|
||||
})->with([
|
||||
'sm' => ['sm', 'h-14 min-w-20 gap-2'],
|
||||
'md' => ['md', 'h-20 min-w-20 gap-4'],
|
||||
'lg' => ['lg', 'h-24 min-w-24 gap-5'],
|
||||
]);
|
||||
|
||||
it('marks its root for the places that draw a nested FAB their own way', function () {
|
||||
expect((string) $this->blade('<x-fab icon="add" aria-label="New" />'))->toContain('data-fab');
|
||||
});
|
||||
|
||||
it('cannot be disabled, because M3 says to remove a FAB instead', function () {
|
||||
$html = (string) $this->blade('<x-fab icon="add" aria-label="New" disabled />');
|
||||
|
||||
// `disabled` is no longer a prop, so it falls through as a plain attribute rather than
|
||||
// painting M3's disabled treatment.
|
||||
expect($html)->not->toContain('disabled:bg-on-surface/10')->not->toContain('disabled:shadow-none');
|
||||
});
|
||||
|
||||
it('opens a FAB menu of end-aligned actions above it', function () {
|
||||
$html = (string) $this->blade(<<<'BLADE'
|
||||
<x-fab-menu label="New" color="secondary">
|
||||
@@ -42,5 +70,13 @@ it('opens a FAB menu of end-aligned actions above it', function () {
|
||||
->toContain('role="menuitem"')
|
||||
->toContain('wire:click="upload"')
|
||||
->toContain('h-14')
|
||||
->toContain('Upload files');
|
||||
->toContain('Upload files')
|
||||
->toContain('max-h-[calc(100dvh-8rem)] overflow-y-auto')
|
||||
->toContain('data-fab-menu');
|
||||
});
|
||||
|
||||
it('names the FAB menu even when the caller forgets to', function () {
|
||||
expect((string) $this->blade('<x-fab-menu />'))
|
||||
->toContain('aria-label="Toggle menu"')
|
||||
->and(substr_count((string) $this->blade('<x-fab-menu />'), 'aria-label="Toggle menu"'))->toBe(2);
|
||||
});
|
||||
|
||||
@@ -23,6 +23,12 @@ it('opens a popover menu from its trigger', function () {
|
||||
->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('<x-menu><x-slot:trigger><button>x</button></x-slot:trigger></x-menu>'))
|
||||
->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('<x-menu position="top-end" vibrant><x-slot:trigger><button>x</button></x-slot:trigger></x-menu>');
|
||||
|
||||
@@ -43,14 +49,27 @@ it('draws an item as a menuitem button', function () {
|
||||
->toContain('size-5 text-on-surface-variant');
|
||||
});
|
||||
|
||||
it('makes a selectable item a menuitemcheckbox', function () {
|
||||
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('<x-menu-item label="Newest" :selected="true" keep-open />'))
|
||||
->toContain('role="menuitemcheckbox"')
|
||||
->toContain('aria-checked="true"')
|
||||
->toContain('bg-tertiary-container')
|
||||
->toContain('data-keep-open')
|
||||
->toContain($check)
|
||||
->and((string) $this->blade('<x-menu-item label="Largest" :selected="false" />'))
|
||||
->toContain('aria-checked="false"');
|
||||
->toContain('aria-checked="false"')
|
||||
->not->toContain($check)
|
||||
->and((string) $this->blade('<x-menu-item label="Newest" :selected="true" icon-right="chevron_right" />'))
|
||||
->not->toContain($check);
|
||||
});
|
||||
|
||||
it('gives an item M3\'s 48px row and 16px sides, and the separator its 8px', function () {
|
||||
expect((string) $this->blade('<x-menu-item label="Copy" />'))->toContain('min-h-12')->toContain('px-4')
|
||||
->and((string) $this->blade('<x-menu-separator />'))->toContain('mx-4 my-2')
|
||||
->and((string) $this->blade('<x-menu-group label="Sort by" />'))->toContain('px-4 pt-2 pb-1');
|
||||
});
|
||||
|
||||
it('marks the page an item leads to, and carries a badge', function () {
|
||||
|
||||
@@ -10,6 +10,17 @@ it('hosts the snackbar queue, kept across wire:navigate', function () {
|
||||
->toContain('justify-center');
|
||||
});
|
||||
|
||||
it('draws no state icon, and reaches 48px from its 40px controls', function () {
|
||||
$html = (string) $this->blade('<x-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')
|
||||
->not->toContain('opacity-80')
|
||||
->toContain('data-toast-action class="state-layer focus-ring touch-target')
|
||||
->toContain('state-layer focus-ring touch-target inline-flex size-10');
|
||||
});
|
||||
|
||||
it('can sit at the start', function () {
|
||||
expect((string) $this->blade('<x-toast position="bottom-start" />'))->toContain('justify-start');
|
||||
});
|
||||
@@ -18,6 +29,16 @@ it('marks the snackbar and its action for tests and styling', function () {
|
||||
$html = (string) $this->blade('<x-toast />');
|
||||
|
||||
expect($html)
|
||||
->toMatch('/<div\s[^>]*\bdata-toast\b[^>]*aria-live="polite"/')
|
||||
->toMatch('/<div\s[^>]*\bdata-toast\b/')
|
||||
->toMatch('/<button\s[^>]*\bdata-toast-action\b[^>]*x-on:click="act\(\)"/');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user