Files
livewire-material/tests/Feature/ErrorPagesTest.php
T
Andreas Reinhold / reiniandClaude Opus 5 fb42f004b1 Add colour profiles
An application lists named schemes in livewire-material.profiles, and
material:scheme without a seed generates them all into one stylesheet:
the default profile as the plain blocks, every profile under
<html data-scheme>, with descendant selectors so a nested data-theme
panel keeps the page's profile. The JSON keeps the 1.0 top level.

Scheme::resolveProfileUsing() lets the application name the active
profile; it is asked on every use and falls back to the default for an
unknown name or a resolver that throws. The head script writes it before
the first paint and keeps it through wire:navigate, and the mail theme
and the error pages' fallback draw it. <x-scheme-picker> chooses one and
previews it through $store.theme.previewScheme(); the showcase previews
the Workbench's profiles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 14:35:49 +02:00

212 lines
8.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
use Illuminate\Foundation\Exceptions\RegisterErrorViewPaths;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\Str;
use NoNameWeb\LivewireMaterial\LivewireMaterialServiceProvider;
use NoNameWeb\LivewireMaterial\Support\Scheme;
beforeEach(function () {
$this->temporary = sys_get_temp_dir().'/livewire-material-errors-'.Str::random(8);
File::ensureDirectoryExists($this->temporary);
Route::middleware('web')->get('/abort/{code}', fn (string $code) => abort((int) $code, request()->query('message', '')));
});
afterEach(function () {
File::deleteDirectory($this->temporary);
});
/**
* Testbench's skeleton application ships an errors/503.blade.php of its own — an application
* view, which rightly wins over the package's. A test of the package's 503 stands in an
* application without one.
*/
function withoutSkeletonErrorViews(): void
{
$views = test()->temporary.'/views';
File::ensureDirectoryExists($views);
config(['view.paths' => [$views, LivewireMaterialServiceProvider::errorViewPath()]]);
}
it('renders the package page for each status', function (int $code, string $headline, string $sentence) {
withoutSkeletonErrorViews();
$this->withoutVite()
->get("/abort/{$code}")
->assertStatus($code)
->assertSee('data-error-page', false)
->assertSee("{$code}</p>", false)
->assertSee($headline)
->assertSee($sentence);
})->with([
[403, 'You dont have access', 'Your account isnt allowed to open this page.'],
[404, 'Page not found', 'The page youre looking for doesnt exist or has moved.'],
[419, 'This page has expired', 'Refresh the page'],
[429, 'Slow down a little', 'Wait a moment, then try again.'],
[500, 'Something went wrong', 'An error on our side'],
[503, 'Well be right back', 'Were making some improvements.'],
]);
it('draws the framework\'s own error pages in the package layout', function () {
$this->withoutVite()
->get('/abort/401')
->assertStatus(401)
->assertSee('data-error-page', false)
->assertSee('Unauthorized')
->assertSee('Go home');
});
it('appends the error-view root to view.paths after the application\'s, before the view finder is built', function () {
$root = LivewireMaterialServiceProvider::errorViewPath();
expect(realpath($root))->toBe(realpath(__DIR__.'/../../resources/views/error-pages'))
->and(File::directories($root))->toBe([$root.'/errors'])
->and(config('view.paths'))->toBe([resource_path('views'), $root])
->and(app()->viewPath())->toBe(resource_path('views'))
->and(app('view')->getFinder()->getPaths())->toContain($root);
$provider = app()->getProvider(LivewireMaterialServiceProvider::class);
(fn () => $this->registerErrorViews())->call($provider);
expect(config('view.paths'))->toBe([resource_path('views'), $root]);
});
it('lets the application\'s own error view win', function () {
File::ensureDirectoryExists($this->temporary.'/errors');
File::put($this->temporary.'/errors/404.blade.php', 'The application view');
config(['view.paths' => [$this->temporary, ...config('view.paths')]]);
$this->withoutVite()
->get('/abort/404')
->assertNotFound()
->assertSee('The application view')
->assertDontSee('data-error-page', false);
$this->get('/abort/403')->assertSee('data-error-page', false);
});
it('loads the application\'s Vite entries', function () {
File::ensureDirectoryExists($this->temporary.'/build');
File::put($this->temporary.'/build/manifest.json', json_encode([
'workbench/resources/css/app.css' => ['file' => 'assets/app-probe.css', 'src' => 'workbench/resources/css/app.css', 'isEntry' => true],
'workbench/resources/js/app.js' => ['file' => 'assets/app-probe.js', 'src' => 'workbench/resources/js/app.js', 'isEntry' => true],
]));
app()->usePublicPath($this->temporary);
Vite::useHotFile($this->temporary.'/hot');
$this->get('/abort/404')
->assertNotFound()
->assertSee('build/assets/app-probe.css', false)
->assertSee('build/assets/app-probe.js', false)
->assertDontSee('data-error-fallback', false);
});
it('still renders, in the application\'s scheme, when the Vite manifest is missing', function () {
File::put($this->temporary.'/material-scheme.json', json_encode([
'light' => ['surface' => '#fafaf0', 'primary' => '#123456'],
'dark' => ['surface' => '#101010'],
]));
config(['livewire-material.scheme' => $this->temporary.'/material-scheme.json']);
app()->usePublicPath($this->temporary);
Vite::useHotFile($this->temporary.'/hot');
$this->get('/abort/500')
->assertStatus(500)
->assertSee('Something went wrong')
->assertSee('<style data-error-fallback>', false)
->assertSee('--md-sys-color-surface: #fafaf0;', false)
->assertSee('--md-sys-color-primary: #123456;', false)
->assertSee('--md-sys-color-surface: #101010;', false)
->assertSee('--md-sys-color-on-surface: #34313a;', false)
->assertDontSee('/build/', false);
});
it('draws the active colour profile when the Vite manifest is missing', function () {
File::put($this->temporary.'/material-scheme.json', json_encode([
'light' => ['primary' => '#4f46e5'],
'dark' => ['primary' => '#aaaaff'],
'default' => 'indigo',
'profiles' => [
'indigo' => ['label' => 'Indigo', 'light' => ['primary' => '#4f46e5'], 'dark' => ['primary' => '#aaaaff']],
'teal' => ['label' => 'Teal', 'light' => ['primary' => '#00897b'], 'dark' => ['primary' => '#80cbc4']],
],
]));
config(['livewire-material.scheme' => $this->temporary.'/material-scheme.json']);
app()->usePublicPath($this->temporary);
Vite::useHotFile($this->temporary.'/hot');
Scheme::resolveProfileUsing(fn (): string => 'teal');
try {
$this->get('/abort/500')
->assertStatus(500)
->assertSee('--md-sys-color-primary: #00897b;', false)
->assertSee('--md-sys-color-primary: #80cbc4;', false)
->assertDontSee('#4f46e5', false);
} finally {
Scheme::resolveProfileUsing(null);
}
});
it('shows the message an application passed for 403 and 503', function (int $code, string $message) {
withoutSkeletonErrorViews();
$this->withoutVite()
->get("/abort/{$code}?message=".urlencode($message))
->assertStatus($code)
->assertSee($message);
})->with([
[403, 'Only the owner can open this share.'],
[503, 'Back at 14:00 after the upgrade.'],
]);
it('does not take maintenance mode\'s "Service Unavailable" for a message', function () {
withoutSkeletonErrorViews();
$this->withoutVite()
->get('/abort/503?message=Service+Unavailable')
->assertStatus(503)
->assertSee('Were making some improvements.')
->assertDontSee('>Service Unavailable<', false);
});
it('prerenders the 503 page for maintenance mode, without an exception', function () {
withoutSkeletonErrorViews();
$this->withoutVite();
(new RegisterErrorViewPaths)();
expect(view('errors::503', ['retryAfter' => 60])->render())
->toContain('data-error-page')
->toContain('Well be right back')
->toContain('location.reload()');
});
it('offers Back only when the visitor came from another page', function () {
$this->withoutVite()
->get('/abort/404')
->assertSee('Go home')
->assertDontSee('Go back');
$this->withoutVite()
->get('/abort/404', ['Referer' => url('/shares')])
->assertSee('Go back')
->assertSee('href="'.url('/shares').'"', false);
});
it('sends an expired form back to its page to refresh', function () {
$this->withoutVite()
->get('/abort/419', ['Referer' => url('/settings')])
->assertStatus(419)
->assertSee('href="'.url('/settings').'"', false)
->assertSee('Refresh the page');
});