Files
livewire-material/tests/Feature/Components/CardTest.php
T
Andreas Reinhold / reiniandClaude Opus 5 c298b19e75 Draw the dragged layer on a card that is not a row
Plan step 36 review of f04e31b3. The card rendered md-state-layer only
on a row, so a card the application marks data-md-dragged without being
a row, as the showcase's "A card being dragged" is, lost the 16% dragged
layer its old inset shadow drew and its header still promises. Every
card now renders the class, since the attribute arrives from script
after render, and card.css keeps the layer off a card that is neither a
row nor dragged. The divider under the header is matched as its child.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 22:00:49 +02:00

96 lines
5.4 KiB
PHP

<?php
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
it('draws M3\'s three cards, each naming its variant for its own colour and per-state elevation', function (string $variant) {
$html = (string) $this->blade("<x-card variant=\"{$variant}\">Body</x-card>");
$css = ComponentStylesheet::read('card');
expect($html)->toContain("data-md-card=\"{$variant}\"")
->and($css->has("[data-md-card='{$variant}']"))->toBeTrue();
})->with(['filled', 'elevated', 'outlined']);
it('is filled unless it knows the variant', function () {
expect((string) $this->blade('<x-card variant="glass">Body</x-card>'))->toContain('data-md-card="filled"');
});
it('lays out a header, menu, media, body and actions', function () {
$html = (string) $this->blade(<<<'BLADE'
<x-card title="holiday-photos.zip" subtitle="248 MB" separator>
<x-slot:figure><img src="/x.png" alt=""></x-slot:figure>
<x-slot:menu><button></button></x-slot:menu>
Expires in 3 days.
<x-slot:actions><button>Copy link</button></x-slot:actions>
</x-card>
BLADE);
expect($html)
->toContain('data-md-card-figure')
->toContain('<img src="/x.png" alt="">')
->toContain('<h3 data-md-card-title>holiday-photos.zip</h3>')
->toContain('data-md-card-menu')
->toContain('248 MB')
->toContain('<button>⋮</button>')
->toContain('role="separator"')
->toContain('data-md-card-content')
->toContain('Expires in 3 days.')
->toContain('data-md-card-actions')
->toContain('<button>Copy link</button>')
->and(strpos($html, 'Expires'))->toBeLessThan(strpos($html, 'Copy link'));
});
it('passes row attributes through and renders the shared interaction classes for a row', function () {
$html = (string) $this->blade('<x-card data-md-list-row><a href="/s/1" data-md-list-open>Open</a></x-card>');
expect($html)
->toContain('data-md-list-row')
->toContain('data-md-list-open')
->toContain('class="md-state-layer md-focus-ring"')
// Any card can be carried, so each draws the shared layer, which card.css keeps off one
// that is neither a row nor dragged.
->and((string) $this->blade('<x-card title="Share">Body</x-card>'))
->toContain('class="md-state-layer"')
->not->toContain('md-focus-ring')
->and(ComponentStylesheet::read('card')->declarations('[data-md-card]:not([data-md-list-row], [data-md-dragged])::before'))
->toBe(['display' => 'none']);
});
it('makes a directly actionable card the one tab stop, named by its title', function () {
$html = (string) $this->blade('<x-card actionable title="holiday-photos.zip"><a href="/s/1" data-md-list-open tabindex="-1">Open</a></x-card>');
expect($html)
->toContain('data-md-list-row')
->toContain('data-md-list-actionable')
->toContain('tabindex="0"')
->toContain('role="button"')
->toContain('aria-label="holiday-photos.zip"')
->toContain('class="md-state-layer md-focus-ring"')
->and((string) $this->blade('<x-card actionable role="link" title="Share">Body</x-card>'))
->toContain('role="link"')
->and(substr_count((string) $this->blade('<x-card actionable role="link" title="Share">Body</x-card>'), 'role="link"'))->toBe(1)
->and((string) $this->blade('<x-card title="Share">Body</x-card>'))
->not->toContain('data-md-list-actionable')
->not->toContain('tabindex');
});
it('draws M3\'s per-state elevation and dragged state from the tokens, never a hand-made shadow', function () {
$css = ComponentStylesheet::read('card');
expect((string) $this->blade('<x-card data-md-dragged variant="elevated">Body</x-card>'))->toContain('data-md-dragged')
->and($css->declarations('[data-md-card][data-md-dragged]'))->toBe(['box-shadow' => 'var(--md-sys-elevation-3)'])
->and($css->declarations("[data-md-card='elevated'][data-md-dragged]"))->toBe(['box-shadow' => 'var(--md-sys-elevation-4)'])
->and($css->declarations("[data-md-card='elevated']"))->toHaveKey('box-shadow', 'var(--md-sys-elevation-1)')
->and($css->has('[data-md-card][data-md-list-row]:not([data-md-dragged]):hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):hover))', ['@media (hover: hover)']))->toBeTrue()
->and($css->declarations("[data-md-card='elevated'][data-md-list-row]:not([data-md-dragged]):hover:not(:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):hover))", ['@media (hover: hover)']))->toBe(['box-shadow' => 'var(--md-sys-elevation-2)'])
->and($css->declarations('[data-md-card][data-md-list-row]:not([data-md-dragged]):hover:has(:is(a, button, input, select, textarea, label, summary):not([data-md-list-open]):hover)::before', ['@media (hover: hover)']))->toBe(['opacity' => '0']);
});
it('rings the row from the card itself when actionable, or from a has() when its opener is focused', function () {
$css = ComponentStylesheet::read('card');
expect($css->declarations('[data-md-card][data-md-list-row]:has([data-md-list-open]:focus-visible)'))
->toBe(['outline' => '3px solid var(--md-sys-color-secondary)', 'outline-offset' => '2px'])
->and($css->declarations('[data-md-card][data-md-list-row] [data-md-list-open]:focus-visible'))
->toBe(['outline' => 'none']);
});