Components read the device's safe-area insets straight from env(), which a browser test cannot fake and an application drawing its own status strip cannot extend. Every inset the package reads, in its CSS and its views, is now var(--material-safe-top|bottom|left|right, env(safe-area-inset-…)): unchanged while the variables are unset. The app shell's --material-bottom-bar also adds var(--material-bottom-extra, 0px), so an application that docks something on the phone's navigation bar (an offline banner) sets its height once and the snackbar, a fab button and the page's bottom padding clear it. A guard test fails on any env(safe-area-inset-*) outside such a variable, and AppShellTest's assertion on the bar height's class follows the new value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
105 lines
5.3 KiB
PHP
105 lines
5.3 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\Blade;
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
function shellDestinations(): array
|
|
{
|
|
return [
|
|
['title' => '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('<x-app-shell :destinations="$destinations">The page</x-app-shell>', ['destinations' => shellDestinations()]);
|
|
|
|
expect($html)
|
|
->toContain('data-navigation-rail="adaptive"')
|
|
->toContain('data-navigation-bar')
|
|
->toContain('<main id="content" tabindex="-1" wire:transition.navigate')
|
|
->toContain('max-lg:overflow-x-clip')
|
|
->toContain('The page')
|
|
->toContain('x-data="materialSnackbar"')
|
|
->toMatch('/<a\s+href="#content"\s+data-skip-link/')
|
|
->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('<x-app-shell :destinations="$destinations" />', ['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, '<a data-navigation-rail-item'))->toBe(4)
|
|
->and(substr_count($bar, '<a data-navigation-bar-item'))->toBe(2)
|
|
->and($bar)->toContain('Inbox')->toContain('Sent')->not->toContain('Travel')
|
|
->and($rail)->toContain('data-navigation-rail-heading>Labels</p>')
|
|
->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('<x-app-shell :destinations="$destinations" />', ['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('lifts the snackbar above the bar only when there is a bar', function () {
|
|
expect((string) $this->blade('<x-app-shell :destinations="$destinations" />', ['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('<x-app-shell :destinations="$destinations" />', ['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('<x-app-shell :destinations="$destinations" />', ['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('/(?<!,)env\(safe-area-inset/');
|
|
});
|
|
|
|
it('places each slot once', function () {
|
|
$html = (string) $this->blade(<<<'BLADE'
|
|
<x-app-shell :destinations="$destinations">
|
|
<x-slot:brand><span>BRAND</span></x-slot:brand>
|
|
<x-slot:rail-header><span>FAB</span></x-slot:rail-header>
|
|
<x-slot:rail-footer><span>FOOTER</span></x-slot:rail-footer>
|
|
<x-slot:actions><span>ACTIONS</span></x-slot:actions>
|
|
<x-slot:top><header>APP BAR</header></x-slot:top>
|
|
PAGE
|
|
</x-app-shell>
|
|
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.*<main.*PAGE/s');
|
|
});
|
|
|
|
it('serves the showcase\'s app shell pages', function () {
|
|
$html = $this->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();
|
|
});
|