'Inbox', 'icon' => 'inbox', 'url' => '/inbox', 'badge' => 4], ['title' => 'Sent', 'icon' => 'send', 'url' => '/sent', 'active' => true], ['title' => 'Travel', 'icon' => 'flight', 'url' => '/travel', 'section' => 'Labels', 'bar' => false], ['title' => 'Receipts', 'icon' => 'receipt_long', 'url' => '/receipts', 'section' => 'Labels', 'bar' => false, 'navigate' => false], ]; } it('draws the adaptive rail, the bar, the content region and the snackbar host', function () { $html = (string) $this->blade('The page', ['destinations' => shellDestinations()]); expect($html) ->toContain('data-navigation-rail="adaptive"') ->toContain('data-navigation-bar') ->toContain('
toContain('max-lg:overflow-x-clip') ->toContain('The page') ->toContain('x-data="materialSnackbar"') ->toMatch('/and(strpos($html, 'data-skip-link'))->toBeLessThan(strpos($html, 'data-navigation-rail=')); }); it('puts every destination in the rail and only those marked for the bar in the bar', function () { $html = (string) $this->blade('', ['destinations' => shellDestinations()]); $rail = substr($html, strpos($html, 'data-navigation-rail='), strpos($html, 'data-app-shell-bar') - strpos($html, 'data-navigation-rail=')); $bar = substr($html, strpos($html, 'data-app-shell-bar')); expect(substr_count($rail, 'toBe(4) ->and(substr_count($bar, 'toBe(2) ->and($bar)->toContain('Inbox')->toContain('Sent')->not->toContain('Travel') ->and($rail)->toContain('data-navigation-rail-heading>Labels

') ->and(substr_count($rail, 'data-navigation-rail-section'))->toBe(1) ->and($rail)->toMatch('/href="\/receipts"(?![^>]*wire:navigate)/') ->and(substr_count($html, 'aria-current="page"'))->toBe(2); }); it('marks the destination at the current URL when none says it is active', function () { Route::get('/shell-probe/inbox', fn () => Blade::render('', ['destinations' => [ ['title' => 'Inbox', 'icon' => 'inbox', 'url' => url('/shell-probe/inbox')], ['title' => 'Sent', 'icon' => 'send', 'url' => url('/shell-probe/sent')], ]])); $html = $this->withoutVite()->get('/shell-probe/inbox')->assertOk()->getContent(); expect($html)->toMatch('/href="[^"]*\/shell-probe\/inbox"[^>]*aria-current="page"/') ->and(substr_count($html, 'aria-current="page"'))->toBe(2); }); it('keeps the destination at the page\'s URL current while a Livewire component on it updates', function () { Livewire::component('app-shell-probe', new class extends Component { public string $page = ''; public function mount(): void { $this->page = url()->current(); } public function render(): string { return '
'; } }); $probe = Livewire::test('app-shell-probe'); expect(substr_count($probe->html(), 'aria-current="page"'))->toBe(2) ->and(substr_count($probe->call('$refresh')->html(), 'aria-current="page"'))->toBe(2) ->and($probe->html())->toMatch('/href="[^"]*\/livewire-unit-test-endpoint\/[^"]*"[^>]*aria-current="page"/'); }); it('lifts the snackbar above the bar only when there is a bar', function () { expect((string) $this->blade('', ['destinations' => shellDestinations()])) ->toContain('max-sm:[--material-bottom-bar:calc(4rem+var(--material-safe-bottom,env(safe-area-inset-bottom))+var(--material-bottom-extra,0px))]') ->and((string) $this->blade('', ['destinations' => [['title' => 'Inbox', 'icon' => 'inbox', 'url' => '/inbox', 'bar' => false]]])) ->not->toContain('data-app-shell-bar') ->not->toContain('--material-bottom-bar:'); }); it('reads the safe area and anything docked on the bar through variables an application can set', function () { $html = (string) $this->blade('', ['destinations' => shellDestinations()]); expect($html) ->toContain('+var(--material-bottom-extra,0px))]') ->toContain('focus:top-[calc(var(--material-safe-top,env(safe-area-inset-top))+1rem)]') ->not->toMatch('/(?blade(<<<'BLADE' BRAND FAB FOOTER ACTIONS
APP BAR
PAGE
BLADE, ['destinations' => shellDestinations()]); foreach (['BRAND', 'FAB', 'FOOTER', 'ACTIONS', 'APP BAR', 'PAGE'] as $slot) { expect(substr_count($html, $slot))->toBe(1); } expect($html)->toMatch('/data-navigation-rail-header.*BRAND.*FAB.*data-navigation-rail-footer.*FOOTER.*data-app-shell-actions.*ACTIONS.*APP BAR.*withoutVite() ->get('/material/shell/starred') ->assertOk() ->assertSee('This is the starred page.') ->getContent(); expect($html)->toMatch('/href="[^"]*\/material\/shell\/starred"[^>]*aria-current="page"/'); $this->withoutVite()->get('/material/shell')->assertOk()->assertSee('This is the inbox page.'); $this->withoutVite()->get('/material/shell/elsewhere')->assertNotFound(); });