Plan step 37: all.css replaces the two interim import lists with one entry for an application that wants everything — the foundation, every layout stylesheet and every component stylesheet, grouped under the same block comments components.css used, plus a Layout block. It also directly imports the three files nothing imported by name before (layout/spacing.css, layout/visibility.css, components/selection.css), so every file under components/ and layout/ is now one @import away. Every test that read a block of components.css or layout.css now reads the matching block of all.css through one shared helper (allCssBlock(), in tests/Pest.php so it loads for any test run) instead of repeating the same substr() search in each file. StylesheetsTest.php's shape, Tailwind-free and breakpoint checks, previously run twice (once for the foundation, once for the layout tree), now run once over the whole tree all.css reaches, since every component and layout stylesheet is plain CSS after step 36; a new test asserts all.css imports everything under components/ and layout/ exactly once. The Workbench imports all.css in place of layout.css and components.css. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
142 lines
6.5 KiB
PHP
142 lines
6.5 KiB
PHP
<?php
|
|
|
|
use Illuminate\View\ViewException;
|
|
use NoNameWeb\LivewireMaterial\Support\SvgFile;
|
|
|
|
/**
|
|
* What a symbol file draws, from its first shape on: the part that tells one cut from
|
|
* another, since every file opens with the same `<svg>` and viewBox.
|
|
*/
|
|
function symbolGeometry(string $file): string
|
|
{
|
|
$svg = trim((string) file_get_contents(__DIR__.'/../../../resources/svg/symbols/'.$file.'.svg'));
|
|
|
|
return substr($svg, (int) strpos($svg, '><') + 1);
|
|
}
|
|
|
|
it('draws a symbol inline, hidden from screen readers, 24px by default', function () {
|
|
$this->blade('<x-icon name="calendar_month" />')
|
|
->assertSee('data-md-icon', false)
|
|
->assertDontSee('--md-icon-size', false)
|
|
->assertDontSee('class=', false)
|
|
->assertSee('aria-hidden="true"', false)
|
|
->assertSee('fill="currentColor"', false)
|
|
->assertSee('viewBox="0 -960 960 960"', false);
|
|
});
|
|
|
|
it('draws the size it is given, and still takes a caller\'s class', function () {
|
|
$this->blade('<x-icon name="calendar_month" size="32" />')
|
|
->assertSee('style="--md-icon-size: 32px"', false);
|
|
|
|
$this->blade('<x-icon name="calendar_month" class="size-4 text-primary" />')
|
|
->assertSee('class="size-4 text-primary"', false);
|
|
});
|
|
|
|
it('ignores a size that is not a whole number of pixels in range', function () {
|
|
foreach (['large', '4', '300', '20.5'] as $size) {
|
|
$this->blade('<x-icon name="calendar_month" size="'.$size.'" />')
|
|
->assertDontSee('--md-icon-size', false);
|
|
}
|
|
});
|
|
|
|
it('takes the 20 cut for a size of 20 or less, unless optical says otherwise', function () {
|
|
expect((string) $this->blade('<x-icon name="home" size="20" />'))->toContain(symbolGeometry('outlined-20/home'))
|
|
->and((string) $this->blade('<x-icon name="home" size="18" />'))->toContain(symbolGeometry('outlined-20/home'))
|
|
->and((string) $this->blade('<x-icon name="home" size="24" />'))->toContain(symbolGeometry('outlined/home'))
|
|
->and((string) $this->blade('<x-icon name="home" size="20" optical="24" />'))->toContain(symbolGeometry('outlined/home'));
|
|
});
|
|
|
|
it('mirrors a directional symbol only when asked', function () {
|
|
$this->blade('<x-icon name="arrow_back" mirror-rtl />')->assertSee('data-md-mirror-rtl', false);
|
|
$this->blade('<x-icon name="arrow_back" />')->assertDontSee('data-md-mirror-rtl', false);
|
|
});
|
|
|
|
it('draws its size from a stylesheet in the components layer', function () {
|
|
$css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/icon.css');
|
|
|
|
expect($css)->toContain('@layer material.components')
|
|
->toContain('inline-size: var(--md-icon-size, 24px)')
|
|
->toContain('[data-md-icon][data-md-mirror-rtl]:dir(rtl)')
|
|
->and((string) file_get_contents(__DIR__.'/../../../resources/css/all.css'))->toContain("@import './components/icon.css';");
|
|
});
|
|
|
|
it('names the icon for a screen reader when it carries the meaning alone', function () {
|
|
$this->blade('<x-icon name="lock" label="Password protected" />')
|
|
->assertSee('aria-label="Password protected"', false)
|
|
->assertSee('role="img"', false)
|
|
->assertDontSee('aria-hidden', false);
|
|
});
|
|
|
|
it('draws the filled symbol when asked', function () {
|
|
$outlined = (string) $this->blade('<x-icon name="favorite" />');
|
|
$filled = (string) $this->blade('<x-icon name="favorite" filled />');
|
|
|
|
expect($filled)->not->toBe($outlined)
|
|
->and($filled)->toContain('<path d=');
|
|
});
|
|
|
|
it('draws the optical size 20 cut when the caller asks for it', function () {
|
|
$standard = (string) $this->blade('<x-icon name="home" class="size-5" />');
|
|
$dense = (string) $this->blade('<x-icon name="home" class="size-5" optical="20" />');
|
|
|
|
expect($dense)->not->toBe($standard)
|
|
->and($dense)->toContain('class="size-5"')
|
|
->and($dense)->toContain('fill="currentColor"')
|
|
->and($dense)->toContain(symbolGeometry('outlined-20/home'));
|
|
});
|
|
|
|
it('combines the filled state with the 20 cut', function () {
|
|
$outlined = (string) $this->blade('<x-icon name="favorite" optical="20" />');
|
|
$filled = (string) $this->blade('<x-icon name="favorite" optical="20" filled />');
|
|
|
|
expect($filled)->not->toBe($outlined)
|
|
->and($outlined)->toContain(symbolGeometry('outlined-20/favorite'))
|
|
->and($filled)->toContain(symbolGeometry('filled-20/favorite'))
|
|
->and($filled)->not->toContain(symbolGeometry('filled/favorite'));
|
|
});
|
|
|
|
it('falls back to the 24 cut for an optical size it does not ship', function () {
|
|
$standard = (string) $this->blade('<x-icon name="home" />');
|
|
|
|
expect((string) $this->blade('<x-icon name="home" optical="40" />'))->toBe($standard)
|
|
->and((string) $this->blade('<x-icon name="home" optical="24" />'))->toBe($standard)
|
|
->and((string) $this->blade('<x-icon name="home" optical="" />'))->toBe($standard);
|
|
});
|
|
|
|
it('refuses a name that is not a symbol', function () {
|
|
expect(fn () => $this->blade('<x-icon name="not_a_symbol_at_all" />'))
|
|
->toThrow(ViewException::class, 'There is no Material Symbol named [not_a_symbol_at_all].');
|
|
});
|
|
|
|
it('points a Heroicon name at the symbol catalogue', function () {
|
|
expect(fn () => $this->blade('<x-icon name="o-calendar-days" />'))
|
|
->toThrow(ViewException::class, 'is not a Material Symbol name');
|
|
});
|
|
|
|
it('ships every symbol outlined and filled in both cuts, painted in the text colour', function () {
|
|
$symbols = SvgFile::symbolNames();
|
|
|
|
expect(count($symbols))->toBeGreaterThan(4000);
|
|
|
|
foreach (['filled', 'outlined-20', 'filled-20'] as $folder) {
|
|
$names = array_map(fn (string $file): string => basename($file, '.svg'), glob(__DIR__."/../../../resources/svg/symbols/{$folder}/*.svg"));
|
|
|
|
expect($names)->toEqual($symbols, "{$folder} holds a different catalogue than outlined");
|
|
}
|
|
|
|
foreach (['outlined', 'filled', 'outlined-20', 'filled-20'] as $folder) {
|
|
foreach (['home', 'search', 'cloud_upload', 'content_copy', 'delete'] as $name) {
|
|
expect(file_get_contents(__DIR__."/../../../resources/svg/symbols/{$folder}/{$name}.svg"))
|
|
->toStartWith('<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path')
|
|
->not->toContain('width=');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('draws the two cuts from different geometry, not the same file scaled', function () {
|
|
foreach (['home', 'search', 'cloud_upload', 'content_copy', 'delete'] as $name) {
|
|
expect(symbolGeometry("outlined-20/{$name}"))->not->toBe(symbolGeometry("outlined/{$name}"))
|
|
->and(symbolGeometry("filled-20/{$name}"))->not->toBe(symbolGeometry("filled/{$name}"));
|
|
}
|
|
});
|