<x-section-nav> and <x-app-shell> mark an item current when its URL is the request's. While a Livewire component on the page re-renders, the request is Livewire's update endpoint, so no section or destination stayed lit after the morph. Both now compare with the page's URL from Livewire::originalUrl() during a Livewire update request; a normal request compares exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
108 lines
4.6 KiB
PHP
108 lines
4.6 KiB
PHP
<?php
|
|
|
|
use Livewire\Component;
|
|
use Livewire\Livewire;
|
|
|
|
it('renders the tablist on the server with the first enabled tab chosen', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<x-tabs id="share" :tabs="[['name' => 'files', 'label' => 'Files', 'disabled' => true], ['name' => 'people', 'label' => 'People', 'icon' => 'group', 'badge' => 3]]">
|
|
<x-tab name="people">Anna</x-tab>
|
|
</x-tabs>
|
|
BLADE);
|
|
|
|
expect($html)
|
|
->toContain('role="tablist"')
|
|
->toContain('data-variant="primary"')
|
|
->toContain('id="share-people"')
|
|
->toContain('aria-controls="share-people-panel"')
|
|
->toMatch('/data-tab="people"\s+aria-controls="share-people-panel"\s+aria-selected="true"\s+tabindex="0"/')
|
|
->toMatch('/data-tab="files"\s+aria-controls="share-files-panel"\s+aria-selected="false"\s+tabindex="-1"/')
|
|
->toContain('disabled')
|
|
->toContain('data-tab-indicator')
|
|
->toContain('--tabs-indicator: share-indicator')
|
|
->toContain('x-modelable="selected"')
|
|
->toContain('role="tabpanel"')
|
|
->toContain('>3</span>');
|
|
});
|
|
|
|
it('takes a variant, stacks icons and scrolls on request', function () {
|
|
expect((string) $this->blade('<x-tabs variant="secondary" stacked scrollable :tabs="[[\'name\' => \'a\', \'label\' => \'A\']]" />'))
|
|
->toContain('data-variant="secondary"')
|
|
->not->toContain('data-stacked')
|
|
->toContain('data-scrollable')
|
|
->and((string) $this->blade('<x-tabs stacked selected="b" :tabs="[[\'name\' => \'a\', \'label\' => \'A\'], [\'name\' => \'b\', \'label\' => \'B\']]" />'))
|
|
->toContain('data-stacked')
|
|
->toMatch('/data-tab="b"\s+aria-controls="[^"]+"\s+aria-selected="true"/');
|
|
});
|
|
|
|
it('entangles the chosen tab with Livewire and renders the one the property names', function () {
|
|
Livewire::component('tabs-probe', new class extends Component
|
|
{
|
|
public string $tab = 'people';
|
|
|
|
public function render(): string
|
|
{
|
|
return '<div><x-tabs wire:model.live="tab" :tabs="[[\'name\' => \'files\', \'label\' => \'Files\'], [\'name\' => \'people\', \'label\' => \'People\']]" /></div>';
|
|
}
|
|
});
|
|
|
|
expect(Livewire::test('tabs-probe')->html())
|
|
->toContain(".entangle('tab').live")
|
|
->not->toContain('x-modelable')
|
|
->toMatch('/data-tab="people"\s+aria-controls="[^"]+"\s+aria-selected="true"/');
|
|
});
|
|
|
|
it('draws section navigation as secondary tabs and a picker', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<x-section-nav label="Settings" :items="[
|
|
['title' => 'Profile', 'icon' => 'person', 'url' => '/settings/profile'],
|
|
['title' => 'Security', 'icon' => 'lock', 'url' => '/settings/security', 'active' => true, 'badge' => 1],
|
|
]" />
|
|
BLADE);
|
|
|
|
expect($html)
|
|
->toContain('aria-label="Settings"')
|
|
->toContain('data-section-picker')
|
|
->toContain('data-variant="secondary"')
|
|
->toContain('sm:flex')
|
|
->toMatch('/href="\/settings\/security"\s+data-tab\s+aria-current="page"\s+wire:navigate/')
|
|
->not->toMatch('/href="\/settings\/profile"\s+data-tab\s+aria-current/')
|
|
->toContain('role="menuitemcheckbox"');
|
|
});
|
|
|
|
it('marks the section whose url is the request\'s, and wraps many sections onto a grid', function () {
|
|
$this->get('/');
|
|
|
|
$items = collect(range(1, 7))->map(fn (int $n): array => ['title' => "S{$n}", 'url' => $n === 3 ? url('/') : "/s/{$n}"])->all();
|
|
|
|
expect((string) $this->blade('<x-section-nav :items="$items" no-wire-navigate />', ['items' => $items]))
|
|
->toContain('sm:grid sm:grid-cols-4 xl:flex')
|
|
->toMatch('/data-tab\s+aria-current="page"\s*>\s*<span data-tab-content>\s*<span class="truncate">S3/')
|
|
->not->toContain('wire:navigate');
|
|
});
|
|
|
|
it('keeps the page\'s section current while a Livewire component on it updates', function () {
|
|
Livewire::component('section-nav-probe', new class extends Component
|
|
{
|
|
public string $page = '';
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->page = url()->current();
|
|
}
|
|
|
|
public function render(): string
|
|
{
|
|
return '<div><x-section-nav :items="[[\'title\' => \'Here\', \'url\' => $page], [\'title\' => \'Elsewhere\', \'url\' => \'/elsewhere\']]" no-wire-navigate /></div>';
|
|
}
|
|
});
|
|
|
|
$current = '/href="[^"]*\/livewire-unit-test-endpoint\/[^"]*"\s+data-tab\s+aria-current="page"/';
|
|
|
|
$probe = Livewire::test('section-nav-probe');
|
|
|
|
expect($probe->html())->toMatch($current)
|
|
->and($probe->call('$refresh')->html())->toMatch($current)
|
|
->and(substr_count($probe->html(), 'aria-current="page"'))->toBe(1);
|
|
});
|