Files
livewire-material/tests/Feature/Components/LoadingTest.php
T
Andreas Reinhold / reiniandClaude Opus 5 f2a3c4d056 Size the loading indicator and publish the rail's value
Two gaps found rewriting the Boost skill for 2.0's plain CSS, each
closed the way M3 describes it.

`<x-loading size="96">`: M3 gives the loading indicator a responsive
size, 48dp by default and flexible from 24 to 240dp ("never exceed that
range"), with the container and the active shape in a fixed ratio. The
size had no prop, so an application wrote a width and height of its own;
`size` now takes a whole number of px in that range, written as
`--md-loading-size`, and the SVG keeps the 48:38 ratio as it scales. A
value outside the range is ignored, as `<x-icon>` and `<x-shape>` ignore
theirs, and an application's own width and height still win.

`--md-navigation-rail-value`: M3 Expressive's rail has two values,
collapsed and expanded (Compose's WideNavigationRailValue), and content
in a rail follows it. Without a hook an application copied the rail's
seven conditions — mode, `data-rail`, `data-rail-auto`, open, and the
window band — out of navigation-rail.css. The rail now publishes the
answer from the same branches that narrow it: `expanded` by default,
`collapsed` wherever it is drawn collapsed, so a style query in the
application's CSS switches at the first paint and at the same moment
as the rail's own items. A rail open over a scrim reads `expanded`, and
outside a rail the property is unset. A JS attribute would have missed
the first paint; a width container query would have lagged the collapse
animation.

Style queries on a custom property need Firefox 151, so the browser
floor moves from Firefox 147 to 151 (README, the CI note, UPGRADE's new
2.1.0 section); Chrome 125 and Safari 18.4 are unchanged.

Browser tests pin both in Chrome, Firefox and Safari: the indicator's
drawn box at 96 and 32px, and the rail's value — with a style query
acting on it — at the first paint for fixed modes, across the window
classes and the menu button for an adaptive rail, and open and closed
for a modal one. NavigationRailTest pins the value in each of the five
collapsed branches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 12:06:16 +02:00

78 lines
4.0 KiB
PHP

<?php
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
it('is a named progressbar, drawn by its stylesheet rather than by classes', function () {
$html = (string) $this->blade('<x-loading />');
expect($html)
->toContain('data-md-loading')
->toContain('role="progressbar"')
->toContain('aria-label="Loading"')
->toContain('<animateTransform')
->toMatch('/<svg data-md-loading-animated="data-md-loading-animated" aria-hidden="true" focusable="false"/')
->toMatch('/<svg data-md-loading-still="data-md-loading-still" aria-hidden="true" focusable="false"/')
->not->toContain('class=');
});
it('is 48px in the primary colour unless the caller sizes or colours it', function () {
$css = ComponentStylesheet::read('loading');
expect($css->declarations('[data-md-loading]'))
->toMatchArray(['inline-size' => 'var(--md-loading-size, 48px)', 'block-size' => 'var(--md-loading-size, 48px)', 'color' => 'var(--md-sys-color-primary)'])
// A caller's class lands on the root untouched, and outranks the package's layer.
->and((string) $this->blade('<x-loading class="app-upload-spinner" label="Uploading" />'))
->toContain('class="app-upload-spinner"')
->toContain('aria-label="Uploading"');
});
it('draws a size from M3\'s responsive range, 24 to 240px', function () {
$this->blade('<x-loading size="96" />')->assertSee('style="--md-loading-size: 96px"', false);
$this->blade('<x-loading size="24" />')->assertSee('style="--md-loading-size: 24px"', false);
$this->blade('<x-loading size="240" />')->assertSee('style="--md-loading-size: 240px"', false);
foreach (['16', '480', 'large'] as $outside) {
$this->blade("<x-loading size=\"{$outside}\" />")->assertDontSee('--md-loading-size', false);
}
});
it('sits on a primary-container circle when contained', function () {
expect((string) $this->blade('<x-loading contained />'))->toContain('data-md-contained')
->and((string) $this->blade('<x-loading />'))->not->toContain('data-md-contained')
->and(ComponentStylesheet::read('loading')->declarations('[data-md-loading][data-md-contained]'))->toBe([
'border-radius' => 'var(--md-sys-shape-corner-full)',
'background-color' => 'var(--md-sys-color-primary-container)',
'color' => 'var(--md-sys-color-on-primary-container)',
]);
});
it('rests the same shape under reduced motion', function () {
$css = ComponentStylesheet::read('loading');
expect($css->declarations('[data-md-loading] > [data-md-loading-still]'))->toBe(['display' => 'none'])
->and($css->declarations('[data-md-loading] > [data-md-loading-animated]', ['@media (prefers-reduced-motion: reduce)']))->toBe(['display' => 'none'])
->and($css->declarations('[data-md-loading] > [data-md-loading-still]', ['@media (prefers-reduced-motion: reduce)']))->toBe(['display' => 'block']);
});
it('says nothing where something else already does', function () {
expect((string) $this->blade('<x-loading :label="false" />'))
->toContain('aria-hidden="true"')
->not->toContain('role="progressbar"');
expect((string) $this->blade('<x-button label="Save" wire:click="save" spinner />'))
->toContain('<animateTransform')
->not->toContain('role="progressbar"');
});
it('animates without script and rests the same shape', function () {
$animated = file_get_contents(__DIR__.'/../../../resources/svg/loading-indicator/indeterminate.svg');
$static = file_get_contents(__DIR__.'/../../../resources/svg/loading-indicator/static.svg');
expect($animated)->toStartWith('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="currentColor">')
->toContain('repeatCount="indefinite"')
->not->toContain('<script')
->not->toContain('__ID__')
->and($static)->toStartWith('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="currentColor">')
->not->toContain('<animate');
});