Files
Andreas Reinhold / reiniandClaude Opus 5 f5f040516e Smooth the page's text in the foundation, not per surface
The showcase and the error pages set grayscale font smoothing on
themselves, but the foundation did not, so an application that dropped
Tailwind's antialiased class rendered heavier text on macOS than the
package's own pages, silently. base.css now sets it on the page; the
showcase's and error page's copies go, and the guard tells a leftover
antialiased class to go too. Decided with the user (plan step 46).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 12:41:20 +02:00

163 lines
6.7 KiB
PHP

<?php
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\Str;
use NoNameWeb\LivewireMaterial\Http\Controllers\ShowcaseAssetController;
use NoNameWeb\LivewireMaterial\Support\Stylesheets;
/**
* The showcase's own stylesheet route (plan step 38): `all.css` plus `showcase.css`, bundled by
* `Stylesheets::bundle()` and served without the application's Vite build, and the two package
* folders its relative `url()`s point into.
*/
afterEach(function () {
Stylesheets::resetCache();
});
it('serves the bundle as long-cached CSS at the hash of its own content', function () {
$response = $this->get(ShowcaseAssetController::url())
->assertOk()
->assertHeader('Content-Type', 'text/css; charset=utf-8')
->assertHeader('Cache-Control', 'immutable, max-age=31536000, public');
expect($response->getContent())
->toContain('[data-md-showcase-brand]')
->toContain('[data-md-pane]')
->toContain('@layer material.reset, material.tokens');
});
it('redirects a hash that does not match the current bundle to the current one', function () {
$stale = route('livewire-material.stylesheet', ['hash' => '0000000000000000']);
$this->get($stale)->assertRedirect(ShowcaseAssetController::url());
});
it('serves the font and svg files the bundle references, mime-typed by extension', function () {
$css = $this->get(ShowcaseAssetController::url())->getContent();
$fontUrl = route('livewire-material.asset', ['path' => 'fonts/google-sans-flex/GoogleSansFlex-Latin.woff2'], false);
$svgUrl = route('livewire-material.asset', ['path' => 'svg/symbols/outlined/check.svg'], false);
expect($css)->toContain($fontUrl)->toContain($svgUrl);
$this->get($fontUrl)
->assertOk()
->assertHeader('Content-Type', 'font/woff2')
->assertHeader('Cache-Control', 'immutable, max-age=31536000, public');
$this->get($svgUrl)
->assertOk()
->assertHeader('Content-Type', 'image/svg+xml');
});
it('404s a path that climbs out of the two served folders', function () {
$this->get('/material/assets/fonts/../../composer.json')->assertNotFound();
});
it('404s every other way out of the two served folders', function (string $path) {
$this->get('/material/assets/'.$path)->assertNotFound();
})->with([
'encoded dot segments' => 'fonts/%2e%2e/%2e%2e/composer.json',
'encoded slashes' => 'fonts/..%2f..%2fcomposer.json',
'backslashes' => 'fonts/google-sans-flex/..%5c..%5c..%5ccomposer.json',
'an absolute path' => '/etc/hosts',
'an absolute path inside a folder' => 'fonts//etc/hosts',
'a null byte' => 'fonts/google-sans-flex/GoogleSansFlex-Latin.woff2%00.txt',
'a directory' => 'svg/symbols',
'a folder itself' => 'fonts/',
'a very long path' => 'fonts/'.str_repeat('a/', 3000).'x.woff2',
]);
it('404s a symbolic link inside a served folder that points out of it', function () {
$link = __DIR__.'/../../resources/svg/showcase-assets-test-link.svg';
symlink(realpath(__DIR__.'/../../composer.json'), $link);
try {
$this->get('/material/assets/svg/showcase-assets-test-link.svg')->assertNotFound();
} finally {
unlink($link);
}
});
it('404s a folder the showcase does not serve', function () {
$this->get('/material/assets/css/all.css')->assertNotFound();
});
it('404s an extension the showcase does not serve, even inside a served folder', function () {
$this->get('/material/assets/fonts/google-sans-flex/OFL.txt')->assertNotFound();
});
it('bundles the application generated scheme, unlayered, when one sits beside its configured JSON', function () {
$dir = sys_get_temp_dir().'/livewire-material-showcase-scheme-'.Str::random(8);
mkdir($dir);
file_put_contents($dir.'/material-scheme.css', ":root { --md-showcase-test-marker: #123456; }\n");
config(['livewire-material.scheme' => $dir.'/material-scheme.json']);
Stylesheets::resetCache();
try {
$this->get(ShowcaseAssetController::url())
->assertOk()
->assertSee('--md-showcase-test-marker: #123456', false);
} finally {
unlink($dir.'/material-scheme.css');
rmdir($dir);
}
});
it('falls back to Scheme::load() in the default shape when no generated stylesheet is found', function () {
config(['livewire-material.scheme' => sys_get_temp_dir().'/livewire-material-showcase-missing-'.Str::random(8).'.json']);
Stylesheets::resetCache();
$css = $this->get(ShowcaseAssetController::url())->assertOk()->getContent();
// The fallback's own one-line selector list, distinct from tokens/scheme.css's multi-line one
// (already in all.css), so its presence here can only come from the appended fallback block.
expect($css)->toContain("[data-contrast='medium'], [data-contrast='medium'][data-theme='light'], [data-contrast='medium'] [data-theme='light']");
});
it('mounts none of the showcase asset routes when the showcase is off', function () {
putenv('MATERIAL_SHOWCASE=false');
$_ENV['MATERIAL_SHOWCASE'] = $_SERVER['MATERIAL_SHOWCASE'] = 'false';
$this->refreshApplication();
try {
$this->get('/material/showcase.0000000000000000.css')->assertNotFound();
$this->get('/material/assets/fonts/google-sans-flex/GoogleSansFlex-Latin.woff2')->assertNotFound();
} finally {
putenv('MATERIAL_SHOWCASE=true');
$_ENV['MATERIAL_SHOWCASE'] = $_SERVER['MATERIAL_SHOWCASE'] = 'true';
$this->refreshApplication();
}
});
it('passes Vite the application entries but its stylesheets, on every kind of showcase page', function () {
$hot = sys_get_temp_dir().'/livewire-material-showcase-hot-'.Str::random(8);
file_put_contents($hot, 'http://vite.test');
Vite::useHotFile($hot);
config(['livewire-material.showcase.vite' => ['resources/css/app.css', 'resources/sass/extra.scss', 'resources/js/app.js']]);
try {
expect(ShowcaseAssetController::scripts())->toBe(['resources/js/app.js']);
foreach (['/material', '/material/progress', '/material/layout/feed', '/material/shell'] as $page) {
$this->get($page)
->assertOk()
->assertSee('http://vite.test/resources/js/app.js', false)
->assertDontSee('http://vite.test/resources/css/app.css', false)
->assertDontSee('http://vite.test/resources/sass/extra.scss', false)
->assertSee(ShowcaseAssetController::url(), false);
}
// A list of stylesheets alone leaves @vite() out, rather than reading a manifest for nothing.
config(['livewire-material.showcase.vite' => ['resources/css/app.css']]);
expect(ShowcaseAssetController::scripts())->toBe([]);
$this->get('/material/progress')->assertOk()->assertDontSee('http://vite.test', false);
} finally {
unlink($hot);
}
});