view("layouts/{$layout}.blade.php"); $this->assertStringContainsString('class="skip-link"', $markup); $this->assertStringContainsString('class="layout-shell', $markup); $this->assertStringContainsString('class="layout-sidebar', $markup); $this->assertStringContainsString('class="layout-topbar', $markup); $this->assertStringContainsString('id="main-content"', $markup); $this->assertStringContainsString('id="mobile-sidebar-backdrop"', $markup); $this->assertStringContainsString('aria-controls="sidebar"', $markup); $this->assertStringContainsString('aria-expanded="false"', $markup); } $this->assertStringNotContainsString('assets/images/logo.svg', $this->view('layouts/main.blade.php')); } public function test_mobile_sidebar_script_supports_dismissal_and_focus_state(): void { $script = $this->resource('js/alpine/components/mobile-nav.js'); $this->assertStringContainsString("event.key === 'Escape'", $script); $this->assertStringContainsString("mobileSidebarBackdrop?.addEventListener('click'", $script); $this->assertStringContainsString("mobileSidebarToggle.setAttribute('aria-expanded'", $script); $this->assertStringContainsString('mobileSidebarClose?.focus()', $script); } public function test_shared_shell_styles_include_focus_and_reduced_motion_support(): void { $stylesheet = $this->resource('css/app.css'); $this->assertStringContainsString(':focus-visible', $stylesheet); $this->assertStringContainsString('@media (prefers-reduced-motion: reduce)', $stylesheet); $this->assertStringContainsString('.layout-flash--success', $stylesheet); $this->assertStringContainsString('.admin-page-header__icon', $stylesheet); } public function test_shared_icon_only_controls_have_accessible_names(): void { $this->assertStringContainsString('aria-label="Search releases"', $this->view('partials/header-menu.blade.php')); $this->assertStringContainsString('aria-label="Close confirmation"', $this->view('partials/confirmation-modal.blade.php')); $this->assertStringContainsString('aria-label="Dismiss notification"', $this->view('partials/toast-notifications.blade.php')); } public function test_desktop_header_search_uses_separated_fully_rounded_controls(): void { $markup = $this->view('partials/header-menu.blade.php'); $this->assertStringContainsString('layout-primary-nav__search relative hidden items-center gap-2 lg:flex', $markup); $this->assertStringContainsString('id="header-search-category" name="t" class="rounded-lg', $markup); $this->assertStringContainsString('class="w-40 rounded-lg border border-gray-600', $markup); $this->assertStringContainsString('class="rounded-lg bg-primary-600', $markup); } public function test_profile_and_edit_profile_share_the_full_content_width(): void { $profile = $this->view('profile/index.blade.php'); $editProfile = $this->view('profile/edit.blade.php'); $this->assertStringContainsString('
', $editProfile); $this->assertStringNotContainsString('max-w-4xl', $editProfile); $this->assertStringNotContainsString('max-w-4xl', $profile); } public function test_first_party_layouts_apply_the_shared_view_treatment(): void { $this->assertStringContainsString('class="app-view-stack layout-page-container', $this->view('layouts/main.blade.php')); $this->assertStringContainsString('class="app-view-stack app-view-stack--admin', $this->view('layouts/admin.blade.php')); $this->assertStringContainsString('class="guest-view-stack', $this->view('layouts/guest.blade.php')); } public function test_shared_page_components_use_the_modern_workspace_primitives(): void { $this->assertStringContainsString('workspace-hero', $this->view('components/page-header.blade.php')); $this->assertStringContainsString('workspace-hero__glow', $this->view('components/page-header.blade.php')); $this->assertStringContainsString('aria-label="Breadcrumb"', $this->view('components/page-header.blade.php')); $this->assertStringContainsString('workspace-page-header', $this->view('components/admin/page-header.blade.php')); $this->assertStringContainsString('rounded-2xl', $this->view('components/panel.blade.php')); $this->assertStringContainsString('empty-state__icon', $this->view('components/empty-state.blade.php')); $this->assertStringContainsString('min-h-11', $this->view('components/input.blade.php')); $this->assertStringContainsString('rounded-xl', $this->view('components/select.blade.php')); $this->assertStringContainsString('catalog-results-toolbar', $this->view('components/cover-results-toolbar.blade.php')); $this->assertStringContainsString('catalog-release-row', $this->view('components/cover-release-list.blade.php')); $this->assertStringContainsString('aria-label="Result view options"', $this->view('components/view-toggle.blade.php')); $this->assertStringContainsString('aria-haspopup="menu"', $this->view('components/sort-dropdown.blade.php')); $this->assertStringContainsString('aria-label="Search releases"', $this->view('components/search-autocomplete.blade.php')); $this->assertStringContainsString('aria-label="Pagination"', $this->view('components/admin/pagination.blade.php')); } public function test_shared_user_hero_uses_compact_vertical_spacing(): void { $hero = $this->view('components/page-header.blade.php'); $this->assertStringContainsString('px-5 py-4', $hero); $this->assertStringContainsString('sm:px-7 sm:py-5', $hero); $this->assertStringContainsString('flex flex-col gap-4', $hero); $this->assertStringContainsString('aria-label="Breadcrumb" class="mb-2', $hero); $this->assertStringContainsString('class="mt-2 max-w-2xl text-sm leading-5', $hero); $this->assertStringContainsString('app-page-header__meta flex min-w-0 flex-wrap', $hero); $this->assertStringContainsString('@if(isset($stats) || isset($actions))', $hero); $this->assertStringNotContainsString('mt-3 flex flex-wrap gap-2 text-sm', $hero); $this->assertStringNotContainsString('sm:py-8', $hero); } public function test_shared_view_styles_cover_application_admin_and_guest_surfaces(): void { $stylesheet = $this->resource('css/app.css'); $this->assertStringContainsString('.app-view-stack > .surface-panel', $stylesheet); $this->assertStringContainsString('.workspace-page-header::after', $stylesheet); $this->assertStringContainsString('.workspace-hero__glow', $stylesheet); $this->assertStringContainsString('.workspace-hero__action', $stylesheet); $this->assertStringContainsString('.inline-search-widget', $stylesheet); $this->assertStringContainsString('.admin-data-table__head th', $stylesheet); $this->assertStringContainsString('.guest-view-stack', $stylesheet); $this->assertStringContainsString('.header-gradient', $stylesheet); $this->assertStringContainsString('.workspace-document-header', $stylesheet); } public function test_every_first_party_admin_page_uses_the_canonical_page_header(): void { $adminViews = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( __DIR__.'/../../resources/views/admin', \FilesystemIterator::SKIP_DOTS, ), ); foreach ($adminViews as $adminView) { if (! str_ends_with($adminView->getFilename(), '.blade.php')) { continue; } $markup = (string) file_get_contents($adminView->getPathname()); if (! str_contains($markup, "@extends('layouts.admin')")) { continue; } preg_match_all("/@include\\('([^']+)'/", $markup, $includedViews); foreach ($includedViews[1] as $includedView) { $markup .= $this->view(str_replace('.', '/', $includedView).'.blade.php'); } $relativePath = str_replace(__DIR__.'/../../resources/views/', '', $adminView->getPathname()); $this->assertStringContainsString( 'getFilename(), '.blade.php')) { continue; } $markup = (string) file_get_contents($userView->getPathname()); if (preg_match('/@extends\([\'\"]layouts\.main[\'\"]\)/', $markup) !== 1) { continue; } $relativePath = str_replace(__DIR__.'/../../resources/views/', '', $userView->getPathname()); $usesCanonicalHero = str_contains($markup, 'assertTrue($usesCanonicalHero, "{$relativePath} must use the canonical user hero."); } } public function test_first_party_content_fragments_use_the_canonical_user_hero(): void { foreach ([ 'browsegroup/index.blade.php', 'mymovies/add.blade.php', 'mymovies/index.blade.php', 'myshows/add.blade.php', 'myshows/index.blade.php', ] as $fragment) { $this->assertStringContainsString( 'view($fragment), "{$fragment} must use the canonical user hero.", ); } } public function test_catalog_hero_titles_use_category_names_without_a_library_suffix(): void { foreach ([ 'books/index.blade.php' => 'title="Books"', 'console/index.blade.php' => 'title="Console"', 'games/index.blade.php' => 'title="Games"', 'movies/index.blade.php' => '>Movies', 'music/index.blade.php' => 'title="Music"', 'xxx/index.blade.php' => 'title="Adult"', ] as $view => $categoryTitle) { $markup = $this->view($view); $this->assertStringContainsString($categoryTitle, $markup); $this->assertDoesNotMatchRegularExpression('/(?:Movie|Book|Console|Games|Music|Adult) library/', $markup); } } public function test_inline_search_reserves_independent_space_for_both_icons(): void { $markup = $this->view('components/inline-search.blade.php'); $stylesheet = $this->resource('css/app.css'); $this->assertStringContainsString('inline-search-widget__icon', $markup); $this->assertStringContainsString('inline-search-widget__input', $markup); $this->assertStringContainsString('inline-search-widget__button', $markup); $this->assertStringContainsString('grid-template-columns: minmax(0, 1fr) 2.5rem;', $stylesheet); $this->assertStringContainsString('gap: 0.5rem;', $stylesheet); $this->assertStringContainsString('padding-inline-start: 2.5rem !important;', $stylesheet); } public function test_project_owned_welcome_view_does_not_reference_a_missing_logo_asset(): void { $welcome = $this->view('welcome.blade.php'); $this->assertStringNotContainsString('assets/images/logo.svg', $welcome); $this->assertStringContainsString('workspace-brand-mark', $welcome); } private function view(string $path): string { return $this->resource("views/{$path}"); } private function resource(string $path): string { return (string) file_get_contents(__DIR__."/../../resources/{$path}"); } }