done = 80; } public function render(): string { return <<<'BLADE'
BLADE; } } /** * A script run against one indicator in the showcase, scrolled into view first: frames are only * drawn while an indicator is on screen. `el` is its root; the script's value is the result. * Pest retries a failing assertion, and gives each attempt a second: keep scripts well inside it. */ function onProgress(string $label, string $body): string { return << { const el = document.querySelector('#progress [aria-label="{$label}"]') el.scrollIntoView({ block: 'center' }) const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) const active = () => el.querySelector('path[stroke="currentColor"]') const reach = () => { const box = active().getBBox(); return (box.x + box.width) / el.offsetWidth } await pause(150) {$body} })() JS; } function progressShowcase(array $options = []) { return visit('/material', $options)->waitForEvent('networkidle'); } it('draws a linear indicator to its value', function () { progressShowcase() ->assertNoJavaScriptErrors() ->assertScript(onProgress('Linear', <<<'JS' const box = active().getBBox() return Math.abs(box.x - 2) < 0.5 && Math.abs(box.x + box.width - el.offsetWidth * 0.4) < 1 && el.querySelectorAll('svg svg').length === 0 JS)); }); it('draws a circular arc to its value, from 12 o\'clock', function () { progressShowcase()->assertScript(onProgress('Circular', <<<'JS' const path = active() const end = path.getPointAtLength(path.getTotalLength()) const start = path.getPointAtLength(0) const turn = (point) => (Math.atan2(point.y - el.offsetHeight / 2, point.x - el.offsetWidth / 2) * 180 / Math.PI + 450) % 360 / 360 return Math.min(turn(start), 1 - turn(start)) < 0.005 && Math.abs(turn(end) - 0.65) < 0.005 JS)); }); it('draws a circular wave as far as its value', function () { progressShowcase()->assertScript(onProgress('Circular wavy', <<<'JS' const path = active() const [dash] = path.getAttribute('stroke-dasharray').split(' ').map(Number) return path.getAttribute('visibility') === null && Math.abs(dash / (Number(path.getAttribute('pathLength')) / 2) - 0.65) < 0.001 JS)); }); it('moves to a new bound value instead of jumping', function () { progressShowcase() ->assertScript(onProgress('Bound', <<<'JS' const before = reach() Alpine.$data(el).progress = 80 await pause(80) const during = reach() return Math.abs(before - 0.3) < 0.01 && during > before + 0.01 && during < 0.79 JS)) ->wait(1) ->assertScript(onProgress('Bound', "return Math.abs(reach() - 0.8) < 0.01 && el.getAttribute('aria-valuenow') === '80'")); }); it('turns indeterminate when a bound value is null, and back', function () { progressShowcase()->assertScript(onProgress('Bound', <<<'JS' Alpine.$data(el).progress = null await pause(100) const indeterminate = !el.hasAttribute('aria-valuenow') && !el.hasAttribute('data-value') const first = active().getAttribute('d') await pause(200) const moving = active().getAttribute('d') !== first Alpine.$data(el).progress = 50 await pause(100) return indeterminate && moving && el.getAttribute('aria-valuenow') === '50' && Math.abs(reach() - 0.5) < 0.01 JS)); }); it('follows a value the server changes through a Livewire morph', function () { Livewire::component('progress-morph-probe', ProgressMorphProbe::class); Route::middleware('web')->get('/progress-morph-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles @livewireScripts BLADE)); $reach = "(() => { const el = document.querySelector('[aria-label=\"Server\"]'); const box = el.querySelector('path[stroke=\"currentColor\"]').getBBox(); return (box.x + box.width) / el.offsetWidth })()"; $page = visit('/progress-morph-probe')->waitForEvent('networkidle') ->assertNoJavaScriptErrors() ->assertScript("Math.abs({$reach} - 0.2) < 0.01"); $page->click('Advance') ->wait(1.5) ->assertAttribute('[aria-label="Server"]', 'aria-valuenow', '80') ->assertScript("Math.abs({$reach} - 0.8) < 0.01") ->assertScript("document.querySelectorAll('[aria-label=\"Server\"] svg svg').length === 0"); }); it('animates while indeterminate', function () { progressShowcase() ->assertScript(onProgress('Linear, indeterminate', <<<'JS' const first = active().getAttribute('d') await pause(200) return active().getAttribute('d') !== first JS)) ->assertScript(onProgress('Circular wavy, indeterminate', <<<'JS' const group = el.querySelector('g') const first = group.getAttribute('transform') + active().getAttribute('stroke-dasharray') await pause(200) return group.getAttribute('transform') + active().getAttribute('stroke-dasharray') !== first JS)); }); it('holds still under reduced motion', function () { progressShowcase(['reducedMotion' => 'reduce']) ->assertScript(onProgress('Wavy, indeterminate', <<<'JS' const first = active().getAttribute('d') await pause(300) return active().getAttribute('d') === first JS)) ->assertScript(onProgress('Wavy', <<<'JS' const first = active().getAttribute('d') await pause(300) return active().getAttribute('d') === first JS)) ->assertScript(onProgress('Bound', <<<'JS' Alpine.$data(el).progress = 90 await pause(50) return Math.abs(reach() - 0.9) < 0.01 JS)); });