stylesheet = sys_get_temp_dir().'/material-scheme-'.Str::random(8).'.css'; $this->data = Str::replaceLast('.css', '.json', $this->stylesheet); }); afterEach(function () { File::delete([$this->stylesheet, $this->data]); }); it('writes the scheme as a stylesheet and as data', function () { $this->artisan('material:scheme', ['seed' => '#4F46E5', '--output' => $this->stylesheet]) ->assertSuccessful(); $scheme = json_decode(File::get($this->data), true); $stylesheet = File::get($this->stylesheet); expect($scheme) ->seed->toBe('#4f46e5') ->variant->toBe('tonal-spot') ->spec->toBe('2025') ->and(array_keys($scheme['dark']))->toEqual(array_keys($scheme['light'])) ->and($scheme['light'])->toHaveKeys(['primary', 'on-primary', 'tertiary-container', 'surface-container-high', 'outline-variant', 'success', 'on-warning-container', 'info-container']) ->and($scheme['light']['surface'])->not->toBe($scheme['dark']['surface']) ->and($scheme['light']['inverse-success'])->toBe($scheme['dark']['success']); foreach (['light' => "[data-theme='light']", 'dark' => "[data-theme='dark']"] as $theme => $selector) { $block = Str::of($stylesheet)->after($selector)->before('}')->toString(); expect($block)->toContain("color-scheme: {$theme};"); foreach ($scheme[$theme] as $role => $hex) { expect($block)->toContain("--md-sys-color-{$role}: {$hex};"); } } expect($stylesheet)->toContain('php artisan material:scheme "#4f46e5" --variant=tonal-spot'); }); it('generates a different scheme per variant', function () { $this->artisan('material:scheme', ['seed' => '#4f46e5', '--variant' => 'vibrant', '--output' => $this->stylesheet]) ->assertSuccessful(); $vibrant = json_decode(File::get($this->data), true); $this->artisan('material:scheme', ['seed' => '#4f46e5', '--output' => $this->stylesheet]) ->assertSuccessful(); $tonalSpot = json_decode(File::get($this->data), true); expect($vibrant['variant'])->toBe('vibrant') ->and($vibrant['light']['primary'])->not->toBe($tonalSpot['light']['primary']); }); it('refuses a seed that is not a colour', function () { $this->artisan('material:scheme', ['seed' => 'indigo', '--output' => $this->stylesheet]) ->expectsOutputToContain('#rrggbb') ->assertFailed(); expect(File::exists($this->stylesheet))->toBeFalse(); }); it('refuses an unknown variant', function () { $this->artisan('material:scheme', ['seed' => '#4f46e5', '--variant' => 'loud', '--output' => $this->stylesheet]) ->expectsOutputToContain('Unknown variant "loud"') ->assertFailed(); }); it('fails without writing anything when node cannot run', function () { config(['livewire-material.node' => '/nonexistent/node']); $this->artisan('material:scheme', ['seed' => '#4f46e5', '--output' => $this->stylesheet]) ->assertFailed(); expect(File::exists($this->stylesheet))->toBeFalse(); }); it('generates every configured profile into one stylesheet keyed by data-scheme', function () { config([ 'livewire-material.profiles' => [ 'indigo' => ['label' => 'Indigo', 'seed' => '#4f46e5', 'variant' => 'vibrant'], 'teal' => ['label' => 'Teal', 'seed' => '#00897b', 'variant' => 'vibrant'], 'graphite' => ['seed' => '#5f6368', 'variant' => 'neutral'], ], 'livewire-material.profile' => 'teal', ]); $this->artisan('material:scheme', ['--output' => $this->stylesheet])->assertSuccessful(); $scheme = json_decode(File::get($this->data), true); $stylesheet = File::get($this->stylesheet); expect($scheme['default'])->toBe('teal') ->and(array_keys($scheme['profiles']))->toBe(['indigo', 'teal', 'graphite']) ->and($scheme['profiles']['graphite'])->label->toBe('Graphite')->variant->toBe('neutral') ->and($scheme['profiles']['indigo']['light']['primary'])->not->toBe($scheme['profiles']['teal']['light']['primary']) // The top level is the default profile, as a 1.0 reader expects. ->and($scheme['light'])->toBe($scheme['profiles']['teal']['light']) ->and($scheme['dark'])->toBe($scheme['profiles']['teal']['dark']) ->and($scheme['seed'])->toBe('#00897b') ->and($stylesheet)->toContain('php artisan material:scheme'."\n"); // The default's plain blocks come first, so a profile's single-attribute selector follows :root. expect(strpos($stylesheet, ":root,\n[data-theme='light'] {"))->toBeLessThan(strpos($stylesheet, "[data-scheme='indigo'],")); foreach ($scheme['profiles'] as $name => $profile) { foreach (['light' => "[data-scheme='{$name}'] [data-theme='light'] {", 'dark' => "[data-scheme='{$name}'] [data-theme='dark'] {"] as $theme => $selector) { $block = Str::of($stylesheet)->after($selector)->before('}')->toString(); expect($block)->toContain("color-scheme: {$theme};") ->toContain("--md-sys-color-primary: {$profile[$theme]['primary']};") ->toContain("--md-sys-color-surface: {$profile[$theme]['surface']};"); } } }); it('names the profile a generator error belongs to', function () { config(['livewire-material.profiles' => ['indigo' => ['seed' => '#4f46e5'], 'broken' => ['seed' => 'teal']]]); $this->artisan('material:scheme', ['--output' => $this->stylesheet]) ->expectsOutputToContain('Profile "broken"') ->assertFailed(); expect(File::exists($this->stylesheet))->toBeFalse(); }); it('asks for a seed or profiles when it has neither', function () { config(['livewire-material.profiles' => []]); $this->artisan('material:scheme', ['--output' => $this->stylesheet]) ->expectsOutputToContain('livewire-material.profiles') ->assertFailed(); }); it('refuses a profile name that cannot be an attribute value', function () { config(['livewire-material.profiles' => ['Ocean Blue' => ['seed' => '#0b57d0']]]); $this->artisan('material:scheme', ['--output' => $this->stylesheet]) ->expectsOutputToContain('"Ocean Blue" is not') ->assertFailed(); });