blade(''); expect($html) ->toContain('toContain('type="range"') ->toContain('id="volume"') ->toContain('name="volume"') ->toContain('min="0"') ->toContain('max="200"') ->toContain('step="5"') ->toContain('value="40"') ->toContain('x-data="materialSlider"') ->toContain('x-on:pointerdown="press($event)"') ->toContain('data-slider-drawing wire:ignore aria-hidden="true"') ->toContain('noscript:appearance-auto') ->not->toContain('role="group"'); }); it('draws the first frame on the server: the track split around the handle, and the stop', function () { $html = (string) $this->blade(''); expect($html) ->toMatch('/data-segment="active"\s+class="[^"]*bg-primary[^"]*"\s+style="left: calc\(0% \+ 0px\); width: calc\(40% - 8px\); border-radius: 8px 2px 2px 8px"/') ->toMatch('/data-segment="end"\s+class="[^"]*bg-secondary-container[^"]*"\s+style="left: calc\(40% \+ 8px\); width: calc\(60% - 8px\); border-radius: 2px 8px 8px 2px"/') ->toMatch('/data-segment="start"\s+hidden/') ->toContain('style="left: calc(100% - 8px)"') ->toContain('') ->toMatch('/data-value-label\s+class="[^"]*opacity-0[^"]*"\s*>40<\/span>/'); }); it('draws M3\'s 44x48 value indicator and puts the stop on the inactive track', function () { expect((string) $this->blade('')) ->toContain('flex h-11 min-w-12 items-center') ->toMatch('/data-value-label[^>]*class="[^"]*origin-bottom/') ->toMatch('/data-stop="end"[^>]*class="[^"]*bg-on-secondary-container/'); }); it('snaps and clamps the value to the step grid', function (string $template, string $value) { expect((string) $this->blade($template))->toContain("value=\"{$value}\""); })->with([ 'between steps' => ['', '25'], 'below the minimum' => ['', '0'], 'past the last step' => ['', '9'], 'a fraction' => ['', '0.3'], 'continuous' => ['', '12.345'], 'centred without a value' => ['', '0'], ]); it('binds wire:model and x-model to the input as numbers, and names it after the property', function () { expect((string) $this->blade('')) ->toContain('wire:model.number.live.debounce.250ms="volume"') ->toContain('name="volume"'); expect((string) $this->blade('')) ->toContain('x-model.number="volume"') ->toContain('x-on:change="saved = true"'); expect((string) $this->blade(''))->toContain('x-model.number="volume"'); }); it('gives a range two inputs bound to the ends of an array, in a named group', function () { $html = (string) $this->blade(''); expect($html) ->toContain('role="group"') ->toContain('aria-labelledby="price-label"') ->toContain('wire:model.number="price.0"') ->toContain('wire:model.number="price.1"') ->toContain('aria-label="Range start"') ->toContain('aria-label="Range end"') ->toContain('id="price-end"') ->toContain('data-handle="end"') ->not->toContain('toBe(2) // Out of order, the ends are sorted, as Compose sorts them. ->and(strpos($html, 'value="20"'))->toBeLessThan(strpos($html, 'value="80"')); expect((string) $this->blade('')) ->toContain('x-model.number="price[0]"') ->toContain('x-model.number="price[1]"'); }); it('draws the value of the bound Livewire property', function () { $component = new class extends Component { public int $volume = 70; /** @var array */ public array $price = [100, 400]; public function render(): string { return <<<'BLADE' BLADE; } }; Livewire::test($component) ->assertSeeHtml('value="70"') ->assertSeeHtml('style="left: calc(70% + 0px)"') ->assertSeeHtml('value="100"') ->assertSeeHtml('value="400"'); }); it('replaces the hint with the validation message and marks the input invalid', function () { $errors = (new ViewErrorBag)->put('default', new MessageBag(['volume' => ['Too loud.'], 'price.1' => ['Too expensive.']])); $this->withViewErrors([]); view()->share('errors', $errors); expect((string) $this->blade('')) ->toContain('Too loud.') ->toContain('text-error') ->toContain('aria-invalid="true"') ->toContain('aria-describedby="volume-hint"') ->not->toContain('Quietly'); expect((string) $this->blade('')) ->toContain('Too expensive.') ->not->toContain('Per night'); expect((string) $this->blade('')) ->toContain('Centre is even') ->not->toContain('aria-invalid'); }); it('sizes the track and the handle by Expressive\'s tokens', function (string $size, string $track, string $handle, string $body) { expect((string) $this->blade('', ['size' => $size])) ->toContain("data-size=\"{$size}\"") ->toContain("-translate-y-1/2 {$track}\"") ->toContain($handle) ->toMatch("/group\\/slider [^\"]* {$body} /"); })->with([ 'xs' => ['xs', 'h-4', 'h-11 group-data-focused/thumb:h-9.5', 'h-12'], 'sm' => ['sm', 'h-6', 'h-11 group-data-focused/thumb:h-9.5', 'h-12'], 'md' => ['md', 'h-10', 'h-13 group-data-focused/thumb:h-11.5', 'h-13'], 'lg' => ['lg', 'h-14', 'h-17 group-data-focused/thumb:h-15.5', 'h-17'], 'xl' => ['xl', 'h-24', 'h-27 group-data-focused/thumb:h-25.5', 'h-27'], ]); it('insets an icon in the track from md, but not on a small, range or centred slider', function () { expect((string) $this->blade('')) ->toContain('data-track-icon') ->toMatch('/data-track-icon\s+data-active/') ->toContain('text-on-secondary-container data-active:text-on-primary'); expect((string) $this->blade('')) ->toMatch('/data-track-icon\s+class/') ->toContain('size-8'); foreach (['', '', ''] as $template) { expect((string) $this->blade($template))->not->toContain('data-track-icon'); } }); it('fills a centred slider from the middle', function () { expect((string) $this->blade('')) ->toContain('data-centered') ->toMatch('/data-segment="start"\s+class="[^"]*"\s+style="left: calc\(0% \+ 0px\); width: calc\(50% - 6px\)/') ->toMatch('/data-segment="active"\s+class="[^"]*"\s+style="left: calc\(50% \+ 0px\); width: calc\(25% - 8px\)/') ->toMatch('/data-segment="end"\s+class="[^"]*"\s+style="left: calc\(75% \+ 8px\)/'); }); it('marks every step when asked, and no more than 200 of them', function () { $html = (string) $this->blade(''); expect(substr_count($html, 'data-tick="'))->toBe(11) ->and($html)->toContain('data-tick="0.5"') ->and($html)->toContain('bg-on-secondary-container data-active:bg-on-primary'); expect((string) $this->blade(''))->not->toContain('data-tick=') ->and((string) $this->blade(''))->not->toContain('data-tick=') ->and((string) $this->blade(''))->not->toContain('data-tick='); }); it('shows the value label on drag, always, or never', function () { expect((string) $this->blade(''))->toContain('opacity-0 scale-75 group-data-pressed/thumb:opacity-100'); expect((string) $this->blade('')) ->toContain('data-value-label') ->toContain('mt-12') ->not->toContain('opacity-0 scale-75'); expect((string) $this->blade(''))->not->toContain('data-value-label'); }); it('stands a slider up on request, turning the drawing a quarter', function () { $html = (string) $this->blade(''); expect($html) ->toContain('data-orientation="vertical"') ->toContain('aria-orientation="vertical"') // The slider is as wide as a horizontal one is tall, and as long as its wrapper. ->toContain('w-12') ->toContain('touch-pan-x h-48 min-h-0 flex-auto [container-type:size]') ->toContain('h-[100cqw] w-[calc(100cqh-0.25rem)] -translate-1/2 -rotate-90') // The value label is turned back, so it lands beside the handle and reads across. ->toMatch('/data-value-label[^>]*class="[^"]*rotate-90/') ->not->toContain('origin-bottom') ->not->toContain('rtl:-scale-x-100'); // The sizes are the horizontal ones, across instead of along. expect((string) $this->blade(''))->toContain('w-27') ->and((string) $this->blade(''))->toContain('ms-12'); }); it('keeps a range slider horizontal, as M3 asks', function () { expect((string) $this->blade('')) ->not->toContain('data-orientation') ->not->toContain('aria-orientation') ->toContain('h-12'); }); it('draws in the colour and its container, and greys out when disabled', function (string $color, string $active, string $inactive) { expect((string) $this->blade('', ['color' => $color])) ->toContain("absolute inset-y-0 {$active} group-has-disabled/slider:bg-on-surface/38") ->toContain("absolute inset-y-0 {$inactive} group-has-disabled/slider:bg-on-surface/12") ->toMatch('/]*\sdisabled[\s>]/'); })->with([ 'primary' => ['primary', 'bg-primary', 'bg-secondary-container'], 'tertiary' => ['tertiary', 'bg-tertiary', 'bg-tertiary-container'], 'error' => ['error', 'bg-error', 'bg-error-container'], 'unknown' => ['pink', 'bg-primary', 'bg-secondary-container'], ]);