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('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-11 group-data-focused/thumb:h-9.5', 'h-12'], '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-primary data-active:text-secondary-container'); 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-primary data-active:bg-secondary-container'); 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-9') ->not->toContain('opacity-0 scale-75'); expect((string) $this->blade(''))->not->toContain('data-value-label'); }); 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'], ]);