path = sys_get_temp_dir().'/scheme-picker-'.uniqid().'.json'; File::put($this->path, json_encode([ 'default' => 'indigo', 'profiles' => [ 'indigo' => ['label' => 'Indigo', 'light' => ['primary' => '#4f46e5', 'secondary' => '#5b5d72', 'tertiary' => '#77536d'], 'dark' => ['primary' => '#c0c1ff', 'secondary' => '#c4c5dd', 'tertiary' => '#e6bad7']], 'teal' => ['label' => 'Teal', 'light' => ['primary' => '#00897b', 'secondary' => '#4a635f', 'tertiary' => '#456179'], 'dark' => ['primary' => '#80cbc4', 'secondary' => '#b1ccc6', 'tertiary' => '#adcae5']], ], ])); config(['livewire-material.scheme' => $this->path]); }); afterEach(function () { File::delete($this->path); }); it('draws a radio per generated profile, bound, named and previewing on change', function () { $html = (string) $this->blade(''); expect($html) ->toContain('Colour profile') ->toContain('Applies after saving') ->toContain('data-scheme-option="indigo"') ->toContain('data-scheme-option="teal"') ->toMatch('/toContain('x-on:change="$store.theme.previewScheme($event.target.value)"') ->toContain('>Teal') ->toContain('style="--swatch-light: #00897b; --swatch-dark: #80cbc4"'); }); it('puts its other attributes on the group and the binding on the radios', function () { $html = (string) $this->blade(''); expect($html)->toMatch('//') ->and(substr_count($html, 'wire:model="colorProfile"'))->toBe(2) ->and(substr_count($html, 'data-test="color-profile"'))->toBe(1); }); it('swaps the dots to the contrast level on screen', function () { File::put($this->path, json_encode([ 'default' => 'indigo', 'profiles' => [ 'indigo' => [ 'label' => 'Indigo', 'light' => ['primary' => '#4f46e5', 'secondary' => '#5b5d72', 'tertiary' => '#77536d'], 'dark' => ['primary' => '#c0c1ff', 'secondary' => '#c4c5dd', 'tertiary' => '#e6bad7'], 'contrast' => ['high' => ['light' => ['primary' => '#241bb0'], 'dark' => ['primary' => '#eeeaff']]], ], ], ])); $html = (string) $this->blade(''); expect($html) // Standard is the inline pair, so a dot is painted before Alpine runs. ->toContain('style="--swatch-light: #4f46e5; --swatch-dark: #c0c1ff"') ->toContain('\u0022standard\u0022:{\u0022light\u0022:{\u0022primary\u0022:\u0022#4f46e5\u0022') ->toContain('\u0022high\u0022:{\u0022light\u0022:{\u0022primary\u0022:\u0022#241bb0\u0022') // A level the file does not carry repeats the standard colours rather than none. ->toContain('\u0022medium\u0022:{\u0022light\u0022:{\u0022primary\u0022:\u0022#4f46e5\u0022') ->toContain('swatches[\'indigo\'][$store.theme.resolvedContrast].light[\'primary\']') ->toContain('swatches[\'indigo\'][$store.theme.resolvedContrast].dark[\'tertiary\']'); }); it('keeps its inline styles to the scheme file\'s checked colours', function () { File::put($this->path, json_encode([ 'default' => 'indigo', 'profiles' => ['indigo' => ['label' => 'Indigo', 'light' => ['primary' => 'red; background: url(x)'], 'dark' => []]], ])); $html = (string) $this->blade(''); preg_match_all('/\sstyle="([^"]*)"/', $html, $styles); expect($styles[1])->not->toBeEmpty() ->each->toMatch('/^--swatch-light: #[0-9a-fA-F]{6}; --swatch-dark: #[0-9a-fA-F]{6}$/') // The colours Alpine swaps in are the same filtered ones, never the file's own text. ->and($html)->not->toContain('url('); }); it('shows a validation message for the bound property instead of the hint', function () { $this->withViewErrors(['colorProfile' => ['Choose one of the colour profiles.']]) ->blade('') ->assertSee('Choose one of the colour profiles.') ->assertDontSee('Applies after saving'); }); it('renders nothing for a single scheme', function () { File::put($this->path, json_encode(['light' => ['primary' => '#123456'], 'dark' => []])); expect(trim((string) $this->blade('')))->toBe(''); });