*/ function badgeAttributes(string $blade): array { preg_match('/]*)>/', (string) test()->blade($blade), $tag); preg_match_all('/([\w:-]+)="([^"]*)"/', $tag[1] ?? '', $pairs, PREG_SET_ORDER); return collect($pairs)->mapWithKeys(fn (array $pair): array => [$pair[1] => $pair[2]])->all(); } it('is M3\'s small badge without a value, hidden from screen readers', function () { expect(badgeAttributes(''))->toBe([ 'data-md-badge' => 'data-md-badge', 'data-md-dot' => 'data-md-dot', 'data-md-color' => 'error', 'aria-hidden' => 'true', ]) ->and(ComponentStylesheet::read('badge')->declarations('[data-md-badge][data-md-dot]'))->toBe([ 'inline-size' => '6px', 'block-size' => '6px', 'border-radius' => 'var(--md-sys-shape-corner-full)', ]); }); it('is M3\'s large badge with a count, capped by max', function () { $css = ComponentStylesheet::read('badge'); expect((string) $this->blade(''))->toContain('>4')->not->toContain('data-md-dot')->not->toContain('data-md-variant') ->and((string) $this->blade(''))->toContain('>999+') ->and((string) $this->blade(''))->toContain('>12')->toContain('data-md-color="primary"') ->and($css->declarations('[data-md-badge]:not([data-md-dot], [data-md-variant])'))->toMatchArray([ 'min-inline-size' => '16px', 'block-size' => '16px', 'border-radius' => 'var(--md-sys-shape-corner-full)', 'padding-inline' => 'var(--md-sys-measurement-space50)', 'font' => 'var(--md-sys-typescale-label-sm)', 'font-variant-numeric' => 'tabular-nums', ]); }); it('pins itself to the corner of an icon when floating', function () { $css = ComponentStylesheet::read('badge'); expect((string) $this->blade(''))->toContain('data-md-floating') ->and(badgeAttributes('')) ->toMatchArray(['data-md-floating' => 'data-md-floating', 'aria-label' => '3 new messages']) ->not->toHaveKey('aria-hidden') ->and($css->declarations('[data-md-badge][data-md-floating]'))->toBe([ 'position' => 'absolute', 'inset-block-start' => 'calc(-1 * var(--md-sys-measurement-space25))', 'inset-inline-start' => 'calc(100% - 12px)', ]) ->and($css->declarations('[data-md-badge][data-md-floating][data-md-dot]'))->toBe(['inset-block-start' => '0', 'inset-inline' => 'auto 0']); }); it('draws a status label in a container or an outline, spoken like any label', function () { $css = ComponentStylesheet::read('badge'); expect(badgeAttributes('')) ->toBe(['data-md-badge' => 'data-md-badge', 'data-md-variant' => 'tonal', 'data-md-color' => 'success']) ->and(badgeAttributes('')) ->toBe(['data-md-badge' => 'data-md-badge', 'data-md-variant' => 'outline', 'data-md-color' => 'error']) ->and($css->declarations('[data-md-badge][data-md-variant]'))->toMatchArray([ 'gap' => 'var(--md-sys-measurement-space50)', 'block-size' => '24px', 'border-radius' => 'var(--md-sys-shape-corner-sm)', 'padding-inline' => 'var(--md-sys-measurement-space100)', 'font' => 'var(--md-sys-typescale-label-md)', ]) ->and($css->declarations("[data-md-badge]:not([data-md-color='plain'])[data-md-variant='tonal']"))->toBe([ 'background-color' => 'var(--md-badge-container)', 'color' => 'var(--md-badge-on-container)', ]) // The outline role, not outline-variant: a badge needs 3:1 against what is behind it. ->and($css->declarations("[data-md-badge]:not([data-md-color='plain'])[data-md-variant='outline']"))->toBe([ 'border-color' => 'var(--md-sys-color-outline)', 'background-color' => 'transparent', 'color' => 'var(--md-sys-color-on-surface-variant)', ]) ->and($css->declarations("[data-md-badge][data-md-variant='outline']"))->toBe(['border' => '1px solid']); }); it('resolves solid over tonal, and either over outline', function () { expect(badgeAttributes(''))->toMatchArray(['data-md-variant' => 'solid', 'data-md-color' => 'primary']) ->and(badgeAttributes(''))->toMatchArray(['data-md-variant' => 'tonal']) ->and(badgeAttributes(''))->toMatchArray(['data-md-dot' => 'data-md-dot'])->not->toHaveKey('data-md-variant') ->and((string) $this->blade(''))->toContain('>Built in<')->not->toContain('aria-hidden'); }); it('renders its slot as HTML, and is a dot while the slot holds nothing but comments', function () { $html = (string) $this->blade(' Pro'); expect($html) ->toContain('data-md-variant="tonal"') ->toContain('toContain(' Pro') ->not->toContain('<svg') ->and((string) $this->blade(''))->toContain('<b>4</b>') ->and((string) $this->blade("\n \n"))->toContain('data-md-dot') ->and((string) $this->blade('120'))->toContain('>99+'); }); it('paints each colour from its pair of roles, and neutral without a hue', function (string $color, array $roles) { expect(ComponentStylesheet::read('badge')->declarations("[data-md-badge][data-md-color='{$color}']"))->toBe([ '--md-badge-color' => "var(--md-sys-color-{$roles[0]})", '--md-badge-on-color' => "var(--md-sys-color-{$roles[1]})", '--md-badge-container' => "var(--md-sys-color-{$roles[2]})", '--md-badge-on-container' => "var(--md-sys-color-{$roles[3]})", ]); })->with([ 'primary' => ['primary', ['primary', 'on-primary', 'primary-container', 'on-primary-container']], 'secondary' => ['secondary', ['secondary', 'on-secondary', 'secondary-container', 'on-secondary-container']], 'tertiary' => ['tertiary', ['tertiary', 'on-tertiary', 'tertiary-container', 'on-tertiary-container']], 'success' => ['success', ['success', 'on-success', 'success-container', 'on-success-container']], 'warning' => ['warning', ['warning', 'on-warning', 'warning-container', 'on-warning-container']], 'info' => ['info', ['info', 'on-info', 'info-container', 'on-info-container']], 'neutral' => ['neutral', ['on-surface-variant', 'surface', 'surface-container-high', 'on-surface-variant']], ]); it('is error by default, and falls back to error on a colour it does not know', function () { $css = ComponentStylesheet::read('badge'); expect($css->declarations('[data-md-badge]'))->toMatchArray([ '--md-badge-color' => 'var(--md-sys-color-error)', '--md-badge-on-color' => 'var(--md-sys-color-on-error)', ]) ->and($css->declarations("[data-md-badge]:not([data-md-color='plain'])"))->toBe([ 'background-color' => 'var(--md-badge-color)', 'color' => 'var(--md-badge-on-color)', ]) ->and(badgeAttributes(''))->toMatchArray(['data-md-color' => 'error']); }); it('leaves a plain badge, and every colour, to the caller\'s CSS', function () { // `plain` matches none of the colour rules; a caller's class lands on the root untouched. expect(badgeAttributes('')) ->toMatchArray(['data-md-color' => 'plain', 'data-md-variant' => 'tonal', 'class' => 'bg-tertiary-container text-on-tertiary-container']); });