Plan step 38 review: ShowcaseController::layout() passes a title
("List-detail · Layout"), but the frame overwrote it with the section's
own, so all three pages and the Layout overview shared one title. The
frame keeps a title it was given; the layout page test asserts each.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
237 lines
9.9 KiB
PHP
237 lines
9.9 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);
|
|
|
|
$html = $this->withoutVite()
|
|
->get("/material/{$key}")
|
|
->assertOk()
|
|
->assertSee("<title>{$section['title']} · Livewire Material</title>", false)
|
|
->assertSee('id="'.$key.'"', false)
|
|
->getContent();
|
|
|
|
// The page's <h1> names the section; the section's own <h2> is for a screen reader only,
|
|
// so the title is not drawn twice.
|
|
preg_match_all('/<h2\b[^>]*>/', $html, $headings);
|
|
|
|
expect($headings[0][0] ?? null)->toBe('<h2 class="md-visually-hidden">', "{$key}'s own <h2> is drawn");
|
|
}
|
|
|
|
$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' => 'List-detail', 'supporting-pane' => 'Supporting pane', 'feed' => 'Feed'] as $page => $title) {
|
|
test()->withoutVite()->get("/material/layout/{$page}")
|
|
->assertOk()
|
|
->assertSee("<title>{$title} · Layout · Livewire Material</title>", false)
|
|
->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('indexes exactly the examples each page renders, so an example the index misses fails here', function () {
|
|
$index = collect($this->getJson('/material/search.json')->assertOk()->json())->where('kind', 'Example');
|
|
$pages = collect(Sections::all())->keys()->map(fn (string $key): string => route('livewire-material.section', $key, false))
|
|
->merge(collect(Sections::layoutPages())->except('layout')->pluck('url'));
|
|
|
|
foreach ($pages as $page) {
|
|
$html = $this->withoutVite()->get($page)->assertOk()->getContent();
|
|
|
|
preg_match_all('/<[a-z]+\b[^>]*\bdata-md-showcase-example\b[^>]*>/', $html, $tags);
|
|
|
|
$rendered = collect($tags[0])
|
|
->map(fn (string $tag): ?string => preg_match('/\bid="([^"]+)"/', $tag, $id) === 1 ? $id[1] : null)
|
|
->filter()
|
|
->sort()
|
|
->values()
|
|
->all();
|
|
|
|
$indexed = $index->filter(fn (array $entry): bool => strtok($entry['url'], '#') === $page)
|
|
->map(fn (array $entry): string => substr($entry['url'], strpos($entry['url'], '#') + 1))
|
|
->sort()
|
|
->values()
|
|
->all();
|
|
|
|
expect($indexed)->toBe($rendered, "{$page}: the search index and the page disagree on its examples");
|
|
}
|
|
});
|
|
|
|
it('writes every example as an application writes it', function () {
|
|
$files = collect(File::glob(__DIR__.'/../../resources/views/showcase/sections/*.blade.php'))
|
|
->merge(File::glob(__DIR__.'/../../resources/views/showcase/layout/*.blade.php'));
|
|
|
|
$examples = $files->flatMap(function (string $file): array {
|
|
preg_match_all("/<<<'BLADE'\n(.*?)^\s*BLADE[,;]/sm", File::get($file), $matches);
|
|
|
|
return array_map(fn (string $code): array => [basename($file), $code], $matches[1]);
|
|
});
|
|
|
|
expect($examples)->not->toBeEmpty();
|
|
|
|
foreach ($examples as [$file, $code]) {
|
|
// The example component rewrites unprefixed tags to a configured prefix; a namespaced tag
|
|
// would show an application a form it does not write, and escape that rewrite.
|
|
expect(str_contains($code, 'x-livewire-material::'))->toBeFalse("{$file} writes a namespaced tag in an example");
|
|
|
|
// Nor a hook only showcase.css draws, which would do nothing in the application's page.
|
|
expect(str_contains($code, 'data-md-showcase'))->toBeFalse("{$file} writes a showcase hook in an example");
|
|
|
|
// A class is a text or interaction class, or one of the application's own that
|
|
// showcase.css stands in for, each named in its header.
|
|
preg_match_all('/class="([^"]*)"/', $code, $classes);
|
|
|
|
foreach (preg_split('/\s+/', implode(' ', $classes[1]), -1, PREG_SPLIT_NO_EMPTY) as $class) {
|
|
expect(str_starts_with($class, 'md-') || in_array($class, ['showcase-w-narrow', 'showcase-ink-tertiary', 'showcase-ink-secondary'], true))
|
|
->toBeTrue("{$file} writes the class `{$class}` in an example");
|
|
}
|
|
}
|
|
});
|
|
|
|
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('/(?:<|<)x-(?:livewire-material::)?'.preg_quote($name, '/').'(?:[\s\/>]|>)/', $showcase) === 1)
|
|
->values()
|
|
->all();
|
|
|
|
expect($absent)->toBe([]);
|
|
});
|