Rewrite the error pages without Tailwind
Plan step 36 (containment group, error pages): errors::minimal's class lists move into resources/css/components/error-page.css, keyed on data-md-error-page/-art/-shape/-code/-headline/-message/-actions; body carries no hook of its own and is styled by a bare `body` selector, matching how ErrorPage::fallbackStyles() already styles it (no other rewritten view is a full HTML document). The section contract (title, code, headline, message, shape, actions) and the framework's 401/402 compatibility are untouched, since neither depends on a class or a hook. The shape's <x-shape> (a component outside this batch, and one with no "fill the parent" prop) is coloured and sized to its container by a plain `svg` descendant selector rather than a class or a new prop. The page's font stays a literal system stack, not var(--md-ref-typeface-brand): Tailwind's own `font-sans` utility this replaces was never the brand font either (resources/css/tokens/theme.css's --font-sans is the only utility that is), and an error page must read before any webfont has loaded. ErrorPage::fallbackStyles() draws onto the same hooks, renamed the same way (data-error-* -> data-md-error-*, data-error-fallback -> data-md-error-fallback) so the no-build path keeps working; how it is built (a hand-written heredoc, not Stylesheets::bundle()) is unchanged, per step 40. Updated in the same commit: tests/Feature/ErrorPagesTest.php and tests/Browser/ErrorPagesTest.php. The error pages' views live outside resources/views/components/, so they are not in ContainmentStylesheetsTest's dataset (its "imports what its view renders" check reads a fixed resources/views/components/<name>.blade.php path); adapted tests/Feature/ErrorPagesTest.php instead, with the same stylesheet- shape, import, ViewClasses, token/px-breakpoint and containment-block checks the dataset gives every other component. Imported from the Containment block of components.css. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a149970dd0
commit
a8c2d88fbb
@@ -9,7 +9,7 @@ function missingPage(string $colorScheme = 'light')
|
||||
$page = $colorScheme === 'dark' ? $page->inDarkMode() : $page->inLightMode();
|
||||
|
||||
return $page->waitForEvent('networkidle')
|
||||
->assertScript("document.readyState === 'complete' && document.querySelector('[data-error-page]') !== null");
|
||||
->assertScript("document.readyState === 'complete' && document.querySelector('[data-md-error-page]') !== null");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,10 +33,10 @@ function paintedIn(string $selector, string $role): string
|
||||
it('draws the 404 page in the scheme, in light and dark', function () {
|
||||
missingPage('light')
|
||||
->assertSee('Page not found')
|
||||
->assertVisible('[data-error-headline]')
|
||||
->assertVisible('[data-md-error-headline]')
|
||||
->assertScript("document.documentElement.getAttribute('data-theme') === 'light'")
|
||||
->assertScript(paintedIn('body', 'surface'))
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-error-shape] svg')).color !== getComputedStyle(document.body).color")
|
||||
->assertScript("getComputedStyle(document.querySelector('[data-md-error-shape] svg')).color !== getComputedStyle(document.body).color")
|
||||
->assertNoJavaScriptErrors();
|
||||
|
||||
missingPage('dark')
|
||||
@@ -48,7 +48,7 @@ it('draws the 404 page in the scheme, in light and dark', function () {
|
||||
it('fits a 400px screen without scrolling sideways', function () {
|
||||
missingPage()
|
||||
->resize(400, 800)
|
||||
->assertVisible('[data-error-headline]')
|
||||
->assertVisible('[data-md-error-headline]')
|
||||
->assertScript('document.documentElement.scrollWidth <= window.innerWidth')
|
||||
->assertScript("document.querySelector('[data-error-actions] a').getBoundingClientRect().right <= 400");
|
||||
->assertScript("document.querySelector('[data-md-error-actions] a').getBoundingClientRect().right <= 400");
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@ use Illuminate\Support\Facades\Vite;
|
||||
use Illuminate\Support\Str;
|
||||
use NoNameWeb\LivewireMaterial\LivewireMaterialServiceProvider;
|
||||
use NoNameWeb\LivewireMaterial\Support\Scheme;
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
|
||||
use NoNameWeb\LivewireMaterial\Tests\Support\ViewClasses;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->temporary = sys_get_temp_dir().'/livewire-material-errors-'.Str::random(8);
|
||||
@@ -39,7 +41,7 @@ it('renders the package page for each status', function (int $code, string $head
|
||||
$this->withoutVite()
|
||||
->get("/abort/{$code}")
|
||||
->assertStatus($code)
|
||||
->assertSee('data-error-page', false)
|
||||
->assertSee('data-md-error-page', false)
|
||||
->assertSee("{$code}</p>", false)
|
||||
->assertSee($headline)
|
||||
->assertSee($sentence);
|
||||
@@ -56,7 +58,7 @@ 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('data-md-error-page', false)
|
||||
->assertSee('Unauthorized')
|
||||
->assertSee('Go home');
|
||||
});
|
||||
@@ -86,9 +88,9 @@ it('lets the application\'s own error view win', function () {
|
||||
->get('/abort/404')
|
||||
->assertNotFound()
|
||||
->assertSee('The application view')
|
||||
->assertDontSee('data-error-page', false);
|
||||
->assertDontSee('data-md-error-page', false);
|
||||
|
||||
$this->get('/abort/403')->assertSee('data-error-page', false);
|
||||
$this->get('/abort/403')->assertSee('data-md-error-page', false);
|
||||
});
|
||||
|
||||
it('loads the application\'s Vite entries', function () {
|
||||
@@ -105,7 +107,7 @@ it('loads the application\'s Vite entries', function () {
|
||||
->assertNotFound()
|
||||
->assertSee('build/assets/app-probe.css', false)
|
||||
->assertSee('build/assets/app-probe.js', false)
|
||||
->assertDontSee('data-error-fallback', false);
|
||||
->assertDontSee('data-md-error-fallback', false);
|
||||
});
|
||||
|
||||
it('still renders, in the application\'s scheme, when the Vite manifest is missing', function () {
|
||||
@@ -121,7 +123,7 @@ it('still renders, in the application\'s scheme, when the Vite manifest is missi
|
||||
$this->get('/abort/500')
|
||||
->assertStatus(500)
|
||||
->assertSee('Something went wrong')
|
||||
->assertSee('<style data-error-fallback>', false)
|
||||
->assertSee('<style data-md-error-fallback>', false)
|
||||
->assertSee('--md-sys-color-surface: #fafaf0;', false)
|
||||
->assertSee('--md-sys-color-primary: #123456;', false)
|
||||
->assertSee('--md-sys-color-surface: #101010;', false)
|
||||
@@ -185,7 +187,7 @@ it('prerenders the 503 page for maintenance mode, without an exception', functio
|
||||
(new RegisterErrorViewPaths)();
|
||||
|
||||
expect(view('errors::503', ['retryAfter' => 60])->render())
|
||||
->toContain('data-error-page')
|
||||
->toContain('data-md-error-page')
|
||||
->toContain('We’ll be right back')
|
||||
->toContain('location.reload()');
|
||||
});
|
||||
@@ -209,3 +211,71 @@ it('sends an expired form back to its page to refresh', function () {
|
||||
->assertSee('href="'.url('/settings').'"', false)
|
||||
->assertSee('Refresh the page');
|
||||
});
|
||||
|
||||
/**
|
||||
* Plan step 36: `errors::minimal`'s class lists moved into
|
||||
* resources/css/components/error-page.css, keyed on `data-md-error-*`. Its view lives outside
|
||||
* resources/views/components/, so it is not in ContainmentStylesheetsTest's dataset (whose
|
||||
* "imports what its view renders" check reads a fixed resources/views/components/<name>.blade.php
|
||||
* path); this is its own small stylesheet-shape test instead, checking the same things.
|
||||
*/
|
||||
it('draws the error layout from a stylesheet shaped like every package stylesheet', function () {
|
||||
$css = ComponentStylesheet::read('error-page');
|
||||
|
||||
expect($css->css)->toStartWith('/*')
|
||||
->and($css->statements()[0] ?? null)->toBe('@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;')
|
||||
->and(array_slice($css->statements(), 1))->each->toMatch('/^@import \'\.\/[a-z-]+\.css\';$/')
|
||||
->and($css->blocks())->each->toBe('@layer material.components')
|
||||
->and($css->css)->not->toMatch('/@(?:tailwind|theme|utility|variant|custom-variant|apply|source|config|plugin|reference)\b|--(?:theme|spacing|alpha)\(|\btheme\(/');
|
||||
|
||||
foreach ($css->imports() as $import) {
|
||||
expect(is_file(dirname(ComponentStylesheet::path('error-page')).'/'.$import))->toBeTrue("error-page.css imports {$import}, which does not exist");
|
||||
}
|
||||
});
|
||||
|
||||
it('imports the stylesheet of every component the error layout renders', function () {
|
||||
preg_match_all('/<x-livewire-material::([a-z-]+)/', File::get(__DIR__.'/../../resources/views/error-pages/errors/minimal.blade.php'), $tags);
|
||||
|
||||
$rendered = collect($tags[1])->unique()
|
||||
->filter(fn (string $tag): bool => is_file(ComponentStylesheet::path($tag)) && str_contains(File::get(ComponentStylesheet::path($tag)), '@layer material.components'))
|
||||
->map(fn (string $tag): string => "./{$tag}.css")
|
||||
->values()
|
||||
->all();
|
||||
|
||||
expect(array_values(array_diff($rendered, ComponentStylesheet::read('error-page')->imports())))->toBe([]);
|
||||
});
|
||||
|
||||
it('writes no class list into the error layout but the interaction and text classes', function () {
|
||||
expect(ViewClasses::violations(File::get(__DIR__.'/../../resources/views/error-pages/errors/minimal.blade.php')))->toBe([]);
|
||||
});
|
||||
|
||||
it('takes its values from the tokens and its breakpoints in px', function () {
|
||||
$source = (string) preg_replace('~/\*.*?\*/~s', '', ComponentStylesheet::read('error-page')->css);
|
||||
|
||||
expect($source)->not->toMatch('/#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\(/i')
|
||||
->not->toMatch('/\bfont:(?!\s*var\(--md-sys-typescale-)/')
|
||||
->not->toMatch('/\btransition[a-z-]*:[^;]*(?:\d+m?s\b|\bease\b|ease-in|ease-out|cubic-bezier)/');
|
||||
|
||||
preg_match_all('/@media\s*([^{]+)\{/', $source, $queries);
|
||||
|
||||
foreach ($queries[1] as $query) {
|
||||
preg_match_all('/\(([^()]*)\)/', $query, $features);
|
||||
|
||||
foreach ($features[1] as $feature) {
|
||||
preg_match_all('/(\d*\.?\d+)(px|rem|em)\b/', $feature, $lengths, PREG_SET_ORDER);
|
||||
|
||||
foreach ($lengths as [, $number, $unit]) {
|
||||
expect($unit)->toBe('px', $query);
|
||||
expect(in_array($number, ['600', '840', '1200', '1600'], true))->toBeTrue("{$query} is not at an M3 breakpoint");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('is imported from the containment block of components.css', function () {
|
||||
$components = File::get(__DIR__.'/../../resources/css/components.css');
|
||||
$block = substr($components, (int) strpos($components, '/* Containment */'));
|
||||
$block = substr($block, 0, (int) strpos($block, '/* Navigation */'));
|
||||
|
||||
expect($block)->toContain("@import './components/error-page.css';");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user