]*\bclass="([^"]*)"/', (string) test()->blade($blade), $matches);
return $matches[1] ?? '';
}
it('is a text button in the primary colour by default', function () {
expect(buttonClasses(''))
->toContain('text-primary')
->not->toContain('bg-primary');
});
it('draws each M3 variant', function (string $variant, string $classes) {
expect(buttonClasses(""))->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(''))->toContain('bg-tertiary text-on-tertiary')
->and(buttonClasses(''))->toContain('bg-error-container text-on-error-container')
->and(buttonClasses(''))->toContain('text-success');
});
it('keeps the maryUI and ReStride shorthands', function () {
expect(buttonClasses(''))->toContain('bg-primary text-on-primary')
->and(buttonClasses(''))->toContain('bg-error text-on-error')
->and(buttonClasses(''))->toContain('bg-warning text-on-warning');
});
it('falls back to the defaults for values it does not know', function () {
expect(buttonClasses(''))
->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("");
expect($html)->toContain($classes)->toContain($icon);
})->with([
'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'],
'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(''))->toContain('rounded-corner-full')->toContain('active:rounded-corner-md')
->and(buttonClasses(''))->toContain('rounded-corner-lg')
->and(buttonClasses(''))->toContain('rounded-corner-xl')->toContain('active:rounded-corner-lg');
});
it('reaches a 48px touch target below the medium size', function () {
expect(buttonClasses(''))->toContain('touch-target')
->and(buttonClasses(''))->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(''))->toContain($geometry('filled/favorite'))
->and((string) $this->blade(''))->toContain($geometry('outlined/favorite'))
->and((string) $this->blade(''))->toContain($geometry('filled/favorite'));
// 20px glyphs come from the 20px cut; 24px ones stay on the default.
expect((string) $this->blade(''))->toContain($geometry('outlined-20/favorite'))
->and((string) $this->blade(''))->toContain($geometry('outlined/favorite'));
});
it('is an icon button named by its tooltip when it has no label', function () {
$html = (string) $this->blade('');
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(''))->toContain('h-10 w-13')
->and(buttonClasses(''))->toContain('h-34 w-26');
});
it('toggles with aria-pressed and M3\'s selected colours and shape', function () {
expect((string) $this->blade(''))->toContain('aria-pressed="true"')
->and(buttonClasses(''))->toContain('bg-primary')->toContain('rounded-corner-md')
->and(buttonClasses(''))->toContain('bg-surface-container text-on-surface-variant')->toContain('rounded-corner-full')
->and(buttonClasses(''))->toContain('bg-secondary text-on-secondary')
->and(buttonClasses(''))->toContain('bg-inverse-surface text-inverse-on-surface');
});
it('squares a selected round icon button and rounds a selected square one', function () {
expect(buttonClasses(''))->toContain('rounded-corner-md')
->and(buttonClasses(''))->toContain('rounded-corner-full')
->and((string) $this->blade(''))->toContain('text-primary');
});
it('navigates inside the app, and leaves it for an external link', function () {
$this->blade('')
->assertSee('assertSee('href="/admin/shares"', false)
->assertSee('wire:navigate', false)
->assertDontSee('type="button"', false);
$this->blade('')
->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('')
->assertSee('disabled="disabled"', false)
->assertSee('disabled:bg-on-surface/10', false);
$this->blade('')
->assertSee('aria-disabled="true"', false)
->assertSee('tabindex="-1"', false);
});
it('shows the loading indicator while its own action runs, in the button\'s own ink', function () {
$this->blade('')
->assertSee('wire:loading.attr="disabled"', false)
->assertSee('wire:target="save"', false)
->assertSee('size-5 text-current', false)
->assertDontSee('text-primary"', false);
$this->blade('')
->assertSee('wire:target="upload"', false);
});
it('hides a responsive label below expanded', function () {
$this->blade('')
->assertSee('New share', false);
});
it('anchors its tooltip to itself', function () {
$html = (string) $this->blade('');
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('');
expect(substr_count($html, '