Plan steps 15-17's testing. `tests/Feature/BreakpointsTest.php` reads every file under resources/views, resources/css and resources/js — showcase example heredocs included — and fails on any Tailwind breakpoint prefix and on any media query or `matchMedia` string at 40, 48, 64 or 80rem (or 640/768/1024/ 1280px, or the 39.99rem that stood for "below 640"). Both scans were checked against a deliberately reintroduced `sm:w-auto` and an 80rem query. Every render test that asserted a `sm:`/`lg:`/`xl:` class now asserts the M3 class, the drawer's pane query is 52.5rem, and the theme script keeps `data-rail-auto` through `wire:navigate`. NavigationTest drives the shell at 599/600/839/840/1199/1200/1600 with the expectation each class carries: the bar on a compact window, the 96px rail from the medium class's first pixel, a standard rail collapsed through expanded whose menu button widens it in place with no scrim, and the rail expanded to begin with from large and at extra-large. (Not run here — the Browser suite is out of scope for this stream.) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
166 lines
8.3 KiB
PHP
166 lines
8.3 KiB
PHP
<?php
|
|
|
|
/**
|
|
* The classes on the rendered button element.
|
|
*/
|
|
function buttonClasses(string $blade): string
|
|
{
|
|
preg_match('/<(?:button|a)\b[^>]*\bclass="([^"]*)"/', (string) test()->blade($blade), $matches);
|
|
|
|
return $matches[1] ?? '';
|
|
}
|
|
|
|
it('is a text button in the primary colour by default', function () {
|
|
expect(buttonClasses('<x-button label="Cancel" />'))
|
|
->toContain('text-primary')
|
|
->not->toContain('bg-primary');
|
|
});
|
|
|
|
it('draws each M3 variant', function (string $variant, string $classes) {
|
|
expect(buttonClasses("<x-button label=\"Go\" variant=\"{$variant}\" />"))->toContain($classes);
|
|
})->with([
|
|
'filled' => ['filled', 'bg-primary text-on-primary'],
|
|
'tonal' => ['tonal', 'bg-secondary-container text-on-secondary-container'],
|
|
'outlined' => ['outlined', 'border-outline-variant text-on-surface-variant'],
|
|
'elevated' => ['elevated', 'bg-surface-container-low text-primary'],
|
|
'text' => ['text', 'text-primary'],
|
|
]);
|
|
|
|
it('draws a variant in another colour role', function () {
|
|
expect(buttonClasses('<x-button label="Go" variant="filled" color="tertiary" />'))->toContain('bg-tertiary text-on-tertiary')
|
|
->and(buttonClasses('<x-button label="Go" variant="tonal" color="error" />'))->toContain('bg-error-container text-on-error-container')
|
|
->and(buttonClasses('<x-button label="Go" variant="outlined" tone="success" />'))->toContain('text-success');
|
|
});
|
|
|
|
it('keeps the maryUI and ReStride shorthands', function () {
|
|
expect(buttonClasses('<x-button label="Save" primary />'))->toContain('bg-primary text-on-primary')
|
|
->and(buttonClasses('<x-button label="Delete" danger />'))->toContain('bg-error text-on-error')
|
|
->and(buttonClasses('<x-button label="Take down" caution />'))->toContain('bg-warning text-on-warning');
|
|
});
|
|
|
|
it('falls back to the defaults for values it does not know', function () {
|
|
expect(buttonClasses('<x-button label="Go" variant="glass" color="purple" size="huge" />'))
|
|
->toContain('text-primary')
|
|
->toContain('h-10 gap-2 px-4 type-label-lg');
|
|
});
|
|
|
|
it('sizes a label button on Expressive\'s scale', function (string $size, string $classes, string $icon) {
|
|
$html = (string) $this->blade("<x-button label=\"Upload\" icon=\"upload\" size=\"{$size}\" />");
|
|
|
|
expect($html)->toContain($classes)->toContain($icon);
|
|
})->with([
|
|
'xs' => ['xs', 'h-8 gap-2 px-3 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'],
|
|
'xl' => ['xl', 'h-34 gap-4 px-16 type-headline-lg', 'size-10'],
|
|
]);
|
|
|
|
it('rounds by default, squares on request, and squares off further while pressed', function () {
|
|
expect(buttonClasses('<x-button label="Go" size="md" />'))->toContain('rounded-corner-full')->toContain('active:rounded-corner-md')
|
|
->and(buttonClasses('<x-button label="Go" size="md" shape="square" />'))->toContain('rounded-corner-lg')
|
|
->and(buttonClasses('<x-button label="Go" size="xl" shape="square" />'))->toContain('rounded-corner-xl')->toContain('active:rounded-corner-lg');
|
|
});
|
|
|
|
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');
|
|
});
|
|
|
|
it('is an icon button named by its tooltip when it has no label', function () {
|
|
$html = (string) $this->blade('<x-button icon="close" tooltip="Close menu" />');
|
|
|
|
expect($html)
|
|
->toContain('aria-label="Close menu"')
|
|
->toContain('size-10')
|
|
->toContain('text-on-surface-variant')
|
|
->toContain('popover="manual"')
|
|
->toContain('aria-hidden="true"');
|
|
});
|
|
|
|
it('sizes an icon button by width', function () {
|
|
expect(buttonClasses('<x-button icon="share" label="" width="wide" aria-label="Share" />'))->toContain('h-10 w-13')
|
|
->and(buttonClasses('<x-button icon="share" size="xl" width="narrow" aria-label="Share" />'))->toContain('h-34 w-26');
|
|
});
|
|
|
|
it('toggles with aria-pressed and M3\'s selected colours and shape', function () {
|
|
expect((string) $this->blade('<x-button label="Bold" variant="filled" :selected="true" />'))->toContain('aria-pressed="true"')
|
|
->and(buttonClasses('<x-button label="Bold" variant="filled" :selected="true" />'))->toContain('bg-primary')->toContain('rounded-corner-md')
|
|
->and(buttonClasses('<x-button label="Bold" variant="filled" :selected="false" />'))->toContain('bg-surface-container text-on-surface-variant')->toContain('rounded-corner-full')
|
|
->and(buttonClasses('<x-button label="Bold" variant="tonal" :selected="true" />'))->toContain('bg-secondary text-on-secondary')
|
|
->and(buttonClasses('<x-button label="Bold" variant="outlined" :selected="true" />'))->toContain('bg-inverse-surface text-inverse-on-surface');
|
|
});
|
|
|
|
it('squares a selected round icon button and rounds a selected square one', function () {
|
|
expect(buttonClasses('<x-button icon="favorite" aria-label="Like" variant="tonal" :selected="true" />'))->toContain('rounded-corner-md')
|
|
->and(buttonClasses('<x-button icon="favorite" aria-label="Like" variant="tonal" shape="square" :selected="true" />'))->toContain('rounded-corner-full')
|
|
->and((string) $this->blade('<x-button icon="favorite" aria-label="Like" :selected="true" />'))->toContain('text-primary');
|
|
});
|
|
|
|
it('navigates inside the app, and leaves it for an external link', function () {
|
|
$this->blade('<x-button label="Shares" link="/admin/shares" />')
|
|
->assertSee('<a ', false)
|
|
->assertSee('href="/admin/shares"', false)
|
|
->assertSee('wire:navigate', false)
|
|
->assertDontSee('type="button"', false);
|
|
|
|
$this->blade('<x-button label="M3" link="https://m3.material.io" external />')
|
|
->assertSee('target="_blank"', false)
|
|
->assertSee('rel="noopener"', false)
|
|
->assertDontSee('wire:navigate', false);
|
|
});
|
|
|
|
it('disables a button, and a link as far as a link can be', function () {
|
|
$this->blade('<x-button label="Save" variant="filled" disabled />')
|
|
->assertSee('disabled="disabled"', false)
|
|
->assertSee('disabled:bg-on-surface/10', false);
|
|
|
|
$this->blade('<x-button label="Shares" link="/admin/shares" disabled />')
|
|
->assertSee('aria-disabled="true"', false)
|
|
->assertSee('tabindex="-1"', false);
|
|
});
|
|
|
|
it('shows the loading indicator while its own action runs', function () {
|
|
$this->blade('<x-button label="Save" wire:click="save" spinner />')
|
|
->assertSee('wire:loading.attr="disabled"', false)
|
|
->assertSee('wire:target="save"', false);
|
|
|
|
$this->blade('<x-button label="Save" wire:click="save" spinner="upload" />')
|
|
->assertSee('wire:target="upload"', false);
|
|
});
|
|
|
|
it('hides a responsive label below expanded', function () {
|
|
$this->blade('<x-button label="New share" icon="add" responsive />')
|
|
->assertSee('<span class="max-expanded:hidden">New share</span>', false);
|
|
});
|
|
|
|
it('anchors its tooltip to itself', function () {
|
|
$html = (string) $this->blade('<x-button label="Save" tooltip-bottom="Saves the draft" />');
|
|
|
|
preg_match('/anchor-name: (--material-button-[a-z0-9]+)/', $html, $anchor);
|
|
|
|
expect($anchor)->not->toBeEmpty()
|
|
->and($html)->toContain("position-anchor: {$anchor[1]}")
|
|
->toContain('[position-area:bottom]')
|
|
->not->toContain('aria-label="Saves the draft"');
|
|
});
|
|
|
|
it('is a FAB on a compact window and a filled button from medium, in one element', function () {
|
|
$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');
|
|
});
|
|
|
|
it('submits a form when asked', function () {
|
|
$this->blade('<x-button label="Save" type="submit" />')
|
|
->assertSee('type="submit"', false);
|
|
});
|
|
|
|
it('keeps aria-pressed off a selected link, which is not a toggle', function () {
|
|
expect((string) $this->blade('<x-button label="Plans" link="/plans" :selected="true" />'))
|
|
->not->toContain('aria-pressed')
|
|
->and((string) $this->blade('<x-button label="Bold" :selected="true" />'))
|
|
->toContain('aria-pressed="true"');
|
|
});
|