Files
livewire-material/tests/Feature/Components/ProgressTest.php
T
Andreas Reinhold / reiniandClaude Opus 5 e57063b0f4
tests / browser (chrome, chromium) (push) Successful in 2m8s
tests / browser (firefox, firefox) (push) Failing after 1m56s
tests / lint (push) Successful in 57s
tests / feature (8.4) (push) Successful in 1m5s
tests / feature (8.5) (push) Successful in 1m1s
tests / browser (safari, webkit) (push) Successful in 2m52s
Add M3 Expressive progress indicators
<x-progress>: linear or circular, flat or wavy, determinate or
indeterminate, 4px or thick, in any colour role, following a server value
through Livewire morphs or an Alpine expression in the browser. The first
frame is server-rendered SVG; Compose Material 3's drawing and keyframes
are ported to an Alpine component that animates only while something
moves and the indicator is on screen.

The previous commit already imported progress.js and included its
showcase section without the files; this adds them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 07:05:52 +02:00

108 lines
4.7 KiB
PHP

<?php
it('is a named, determinate progressbar with its value', function () {
expect((string) $this->blade('<x-progress value="42" />'))
->toContain('role="progressbar"')
->toContain('aria-label="Progress"')
->toContain('aria-valuemin="0"')
->toContain('aria-valuemax="100"')
->toContain('aria-valuenow="42"')
->toContain('data-value="42"')
->toContain('x-data="materialProgress"');
});
it('leaves out aria-valuenow while indeterminate', function () {
expect((string) $this->blade('<x-progress label="Uploading" />'))
->toContain('role="progressbar"')
->toContain('aria-label="Uploading"')
->not->toContain('aria-valuenow')
->not->toContain('data-value');
});
it('says nothing where something else already does', function () {
expect((string) $this->blade('<x-progress value="10" :label="false" />'))
->toContain('aria-hidden="true"')
->not->toContain('role="progressbar"')
->not->toContain('aria-label')
->not->toContain('aria-valuenow');
});
it('clamps the value into its range', function (string $template, string $now, string $max) {
expect((string) $this->blade($template))
->toContain("aria-valuenow=\"{$now}\"")
->toContain("aria-valuemax=\"{$max}\"")
->toContain("data-value=\"{$now}\"");
})->with([
'below zero' => ['<x-progress :value="-5" />', '0', '100'],
'above the maximum' => ['<x-progress :value="250" />', '100', '100'],
'a custom maximum' => ['<x-progress :value="3.5" :max="5" />', '3.5', '5'],
'a maximum that cannot be one' => ['<x-progress :value="30" :max="0" />', '30', '100'],
]);
it('draws the first frame on the server, at the value', function () {
expect((string) $this->blade('<x-progress value="42" />'))
->toContain('<svg x="42%" overflow="visible">')
->toContain('x2="42%"')
->toContain('<circle cx="100%"')
->toContain('wire:ignore');
expect((string) $this->blade('<x-progress value="25" circular />'))
->toContain('viewBox="0 0 40 40"')
->toContain('<path d="M20 2A18 18 0 0 1 38 20" stroke="currentColor" />')
->toContain('<path d="M36.579 27.01A18 18 0 1 1 12.99 3.421" />');
});
it('draws the active indicator in the colour and the track in its container', function (string $color, string $ink, string $track) {
expect((string) $this->blade('<x-progress value="50" :color="$color" />', ['color' => $color]))
->toContain($ink)
->toContain($track);
})->with([
'primary' => ['primary', 'text-primary', 'stroke-secondary-container'],
'secondary' => ['secondary', 'text-secondary', 'stroke-secondary-container'],
'tertiary' => ['tertiary', 'text-tertiary', 'stroke-tertiary-container'],
'error' => ['error', 'text-error', 'stroke-error-container'],
'unknown, as primary' => ['purple', 'text-primary', 'stroke-secondary-container'],
]);
it('sizes each shape as M3 does', function (string $template, string $size, array $flags) {
$html = (string) $this->blade($template);
expect($html)->toContain($size);
foreach (['data-circular', 'data-wavy', 'data-thick'] as $flag) {
in_array($flag, $flags, true)
? expect($html)->toContain($flag)
: expect($html)->not->toContain($flag);
}
})->with([
'linear' => ['<x-progress />', 'w-full h-1 ', []],
'linear thick' => ['<x-progress thick />', 'h-2 ', ['data-thick']],
'linear wavy' => ['<x-progress wavy />', 'h-2.5 ', ['data-wavy']],
'linear wavy thick' => ['<x-progress wavy thick />', 'h-3.5 ', ['data-wavy', 'data-thick']],
'circular' => ['<x-progress circular />', 'size-10 ', ['data-circular']],
'circular thick' => ['<x-progress circular thick />', 'size-11 ', ['data-circular', 'data-thick']],
'circular wavy' => ['<x-progress circular wavy />', 'size-12 ', ['data-circular', 'data-wavy']],
'circular wavy thick' => ['<x-progress circular wavy thick />', 'size-13 ', ['data-circular', 'data-wavy', 'data-thick']],
]);
it('takes the caller\'s size instead of its own', function () {
expect((string) $this->blade('<x-progress circular wavy class="size-24" />'))
->toContain('size-24')
->not->toContain('size-12');
expect((string) $this->blade('<x-progress class="w-64" />'))
->toContain('w-64')
->not->toContain('w-full');
});
it('follows a value bound in Alpine', function () {
$html = (string) $this->blade('<x-progress bind="upload.progress" max="1" />');
expect($html)
->toContain('x-bind:data-value="upload.progress"')
->toContain('x-bind:aria-valuenow="((v) =&gt; v === null')
->toContain('Math.min(Math.max(Number(v), 0), 1))(upload.progress)')
->toContain('data-max="1"')
->not->toContain(' data-value=');
});