Files
livewire-material/tests/Feature/ShowcaseTest.php
T
Andreas Reinhold / reiniandClaude Sonnet 5 487176d345 Give each canonical layout a page of its own in the Layout section
Plan step 38: the Layout section's Overview keeps its breakpoint
table and live "This window" readout (now a shared
<x-showcase::breakpoint-readout> partial, and the five breakpoint
cards a real range-queried data-md-showcase-breakpoint-card instead
of Tailwind's max-medium:/medium:max-expanded: variants), but the
three canonical layouts move off it onto a page of their own:
/material/layout/{list-detail,supporting-pane,feed}
(ShowcaseController::layout(), routes/showcase.php), each built on
the real component, sharing one <x-section-nav> Sections::
layoutPages() feeds. The search index gains an entry per page with
its own URL, and the component-homes scan now also reads the three
pages' own source so <x-list-detail>, <x-supporting-pane> and
<x-feed> keep a home.

The whole section moves onto the layout components and md-* classes:
no bespoke Tailwind grid or table styling remains.

LayoutTest's browser test now visits the three dedicated pages
instead of one shared /material/layout; a new Feature test asserts
each page renders and is linked from the Layout section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 04:22:38 +02:00

171 lines
6.5 KiB
PHP

<?php
use Illuminate\Support\Facades\File;
use NoNameWeb\LivewireMaterial\Showcase\Sections;
use NoNameWeb\LivewireMaterial\Support\SvgFile;
/**
* Reboot the application with the showcase switched on or off. The routes are mounted
* while the provider boots, so the flag has to be in the environment before that.
*/
function rebootWithShowcase(bool $enabled): void
{
$value = $enabled ? 'true' : 'false';
putenv("MATERIAL_SHOWCASE={$value}");
$_ENV['MATERIAL_SHOWCASE'] = $_SERVER['MATERIAL_SHOWCASE'] = $value;
test()->refreshApplication();
}
afterEach(function () {
rebootWithShowcase(true);
});
it('mounts the showcase when enabled', function () {
$this->withoutVite()
->get('/material')
->assertOk()
->assertSee('Livewire Material');
});
it('gives every section a page of its own, linked from the overview and the rail', function () {
$overview = $this->withoutVite()->get('/material')->assertOk();
foreach (Sections::all() as $key => $section) {
$overview->assertSee(route('livewire-material.section', $key), false);
$this->withoutVite()
->get("/material/{$key}")
->assertOk()
->assertSee("<title>{$section['title']} · Livewire Material</title>", false)
->assertSee('id="'.$key.'"', false);
}
$this->get('/material/not-a-section')->assertNotFound();
});
it('gives each canonical layout a page of its own, linked from the Layout section', function () {
foreach (['list-detail', 'supporting-pane', 'feed'] as $page) {
test()->withoutVite()->get("/material/layout/{$page}")
->assertOk()
->assertSee('id="example-'.$page.'"', false)
// Its own navigation reaches the other three pages, including itself.
->assertSee(route('livewire-material.layout', 'list-detail', false), false)
->assertSee(route('livewire-material.layout', 'supporting-pane', false), false)
->assertSee(route('livewire-material.layout', 'feed', false), false);
}
test()->withoutVite()->get('/material/layout')
->assertOk()
->assertSee(route('livewire-material.layout', 'list-detail', false), false)
->assertSee(route('livewire-material.layout', 'supporting-pane', false), false)
->assertSee(route('livewire-material.layout', 'feed', false), false);
});
it('indexes every section, example and component for the search, each linking to a place that exists', function () {
$index = collect($this->getJson('/material/search.json')->assertOk()->json());
expect($index->where('kind', 'Section')->pluck('title')->all())
->toBe(collect(Sections::all())->pluck('title')->all())
->and($index->where('kind', 'Component')->pluck('title')->all())
->toContain('<x-datepicker>', '<x-button>', '<x-scaffold>')
->and($index->firstWhere('title', '<x-datepicker>')['url'])->toBe('/material/pickers');
$index->where('kind', 'Example')
->groupBy(fn (array $entry): string => strtok($entry['url'], '#'))
->each(function ($examples, string $page): void {
$html = $this->withoutVite()->get($page)->assertOk()->getContent();
foreach ($examples as $example) {
expect($html)->toContain('id="'.substr($example['url'], strpos($example['url'], '#') + 1).'"');
}
});
});
it('does not mount the showcase when disabled', function () {
rebootWithShowcase(false);
$this->get('/material')->assertNotFound();
});
it('compiles every package view with the showcase off, as view:cache does in production', function () {
rebootWithShowcase(false);
$compiled = sys_get_temp_dir().'/livewire-material-view-cache-'.uniqid();
config(['view.compiled' => $compiled]);
File::ensureDirectoryExists($compiled);
try {
$this->artisan('view:cache')->assertSuccessful();
} finally {
File::deleteDirectory($compiled);
}
});
it('serves a symbol for the icon search', function () {
$this->get('/material/symbols/filled/favorite.svg')
->assertOk()
->assertHeader('Content-Type', 'image/svg+xml')
->assertSee('viewBox="0 -960 960 960"', false);
});
it('serves the 24 cut by default and the 20 cut on request', function () {
$geometry = function (string $folder): string {
$svg = trim((string) file_get_contents(__DIR__."/../../resources/svg/symbols/{$folder}/home.svg"));
return substr($svg, (int) strpos($svg, '<path'));
};
$standard = $this->get('/material/symbols/outlined/home.svg')->assertOk()->getContent();
$dense = $this->get('/material/symbols/outlined/home.svg?optical=20')->assertOk()->getContent();
expect($dense)->not->toBe($standard)
->and($standard)->toContain($geometry('outlined'))
->and($dense)->toContain($geometry('outlined-20'))
->and($this->get('/material/symbols/outlined/home.svg?optical=40')->getContent())->toBe($standard);
});
it('answers 404 for a symbol or style that does not exist', function () {
$this->get('/material/symbols/outlined/not_a_symbol_at_all.svg')->assertNotFound();
$this->get('/material/symbols/sharp/favorite.svg')->assertNotFound();
});
it('lists the symbol names for the icon search', function () {
$this->getJson('/material/symbols.json')
->assertOk()
->assertJsonFragment(['calendar_month'])
->assertJsonCount(count(SvgFile::symbolNames()));
});
it('previews an error page through the exception handler', function () {
$this->withoutVite()
->get('/material/errors/404')
->assertNotFound()
->assertSee('Page not found');
$this->get('/material/errors/418')->assertNotFound();
});
it('previews a sample mail in the package theme', function () {
$this->get('/material/mail')
->assertOk()
->assertSee('class="button button-primary"', false)
->assertSee('border-radius: 9999px', false)
->assertSee('Unsubscribe');
});
it('shows every component somewhere in the showcase', function () {
$showcase = collect(File::allFiles(__DIR__.'/../../resources/views/showcase'))
->map(fn (SplFileInfo $file): string => $file->getContents())
->implode("\n");
$absent = collect(File::files(__DIR__.'/../../resources/views/components'))
->map(fn (SplFileInfo $file): string => $file->getBasename('.blade.php'))
->reject(fn (string $name): bool => preg_match('/(?:<|&lt;)x-(?:livewire-material::)?'.preg_quote($name, '/').'(?:[\s\/>]|&gt;)/', $showcase) === 1)
->values()
->all();
expect($absent)->toBe([]);
});