menuData = $menuData; } public function setBreadcrumbsData(array $breadcrumbsData): void { $this->breadcrumbsData = $breadcrumbsData; } public function setShouldFailMenu(bool $shouldFail): void { $this->shouldFailMenu = $shouldFail; } public function setShouldFailBreadcrumbs(bool $shouldFail): void { $this->shouldFailBreadcrumbs = $shouldFail; } public function getNavigationMenu(): array { if ($this->shouldFailMenu) { throw new \Exception('Navigation service failed'); } return $this->menuData; } public function getBreadcrumbs(string $currentPath): array { if ($this->shouldFailBreadcrumbs) { throw new \Exception('Breadcrumbs service failed'); } return $this->breadcrumbsData; } } // Test stub for HttpRequest since it's readonly class TestHttpRequest { public string $path = '/admin'; } describe('AdminLayoutProcessor', function () { beforeEach(function () { $this->navigationService = new TestAdminNavigationService(); $this->request = new TestHttpRequest(); // Use reflection to create AdminLayoutProcessor with our test doubles $reflection = new \ReflectionClass(AdminLayoutProcessor::class); $constructor = $reflection->getConstructor(); // Create instance using reflection to bypass readonly constraints $this->processor = $reflection->newInstanceWithoutConstructor(); // Set the private properties $navProperty = $reflection->getProperty('navigationService'); $navProperty->setAccessible(true); $navProperty->setValue($this->processor, $this->navigationService); $requestProperty = $reflection->getProperty('request'); $requestProperty->setAccessible(true); $requestProperty->setValue($this->processor, $this->request); }); it('processes admin layout data with navigation and breadcrumbs', function () { $this->request->path = '/admin/dashboard'; $menuData = [ 'System' => [ 'icon' => 'server', 'items' => [ 'Dashboard' => '/admin', 'Health Check' => '/admin/system/health', ], ], ]; $breadcrumbsData = [ ['name' => 'Admin', 'url' => '/admin'], ['name' => 'Dashboard', 'url' => '/admin/dashboard'], ]; $this->navigationService->setMenuData($menuData); $this->navigationService->setBreadcrumbsData($breadcrumbsData); $inputData = new AdminLayoutData( title: 'Test Page', navigationMenu: new NavigationMenu([]), breadcrumbs: new BreadcrumbCollection([]), currentPath: '/admin' ); $result = $this->processor->processAdminLayout($inputData); expect($result)->toBeInstanceOf(AdminLayoutData::class); expect($result->navigationMenu->sections)->toHaveCount(1); expect($result->navigationMenu->sections[0]->name)->toBe('System'); expect($result->navigationMenu->sections[0]->items)->toHaveCount(2); expect($result->breadcrumbs->breadcrumbs)->toHaveCount(2); }); it('handles navigation service failure gracefully', function () { $this->request->path = '/admin/test'; $this->navigationService ->shouldReceive('getNavigationMenu') ->once() ->andThrow(new \Exception('Navigation service failed')); $this->navigationService ->shouldReceive('getBreadcrumbs') ->with('/admin/test') ->once() ->andReturn([['name' => 'Admin', 'url' => '/admin']]); $inputData = new AdminLayoutData( title: 'Test Page', navigationMenu: new NavigationMenu([]), breadcrumbs: new BreadcrumbCollection([]), currentPath: '/admin' ); $result = $this->processor->processAdminLayout($inputData); // Should have fallback menu expect($result->navigationMenu->sections)->toHaveCount(1); expect($result->navigationMenu->sections[0]->name)->toBe('System'); expect($result->navigationMenu->sections[0]->items)->toHaveCount(2); expect($result->navigationMenu->sections[0]->items[0]->name)->toBe('Dashboard'); }); it('handles breadcrumbs service failure gracefully', function () { $this->request->path = '/admin/test'; $this->navigationService ->shouldReceive('getNavigationMenu') ->once() ->andReturn([]); $this->navigationService ->shouldReceive('getBreadcrumbs') ->with('/admin/test') ->once() ->andThrow(new \Exception('Breadcrumbs service failed')); $inputData = new AdminLayoutData( title: 'Test Page', navigationMenu: new NavigationMenu([]), breadcrumbs: new BreadcrumbCollection([]), currentPath: '/admin' ); $result = $this->processor->processAdminLayout($inputData); // Should have fallback breadcrumbs expect($result->breadcrumbs->breadcrumbs)->toHaveCount(1); expect($result->breadcrumbs->breadcrumbs[0]->name)->toBe('Admin'); expect($result->breadcrumbs->breadcrumbs[0]->url)->toBe('/admin'); }); it('sets active state for navigation items based on current path', function () { $this->request->path = '/admin/system/health'; $menuData = [ 'System' => [ 'items' => [ 'Dashboard' => '/admin', 'Health Check' => '/admin/system/health', ], ], ]; $this->navigationService ->shouldReceive('getNavigationMenu') ->once() ->andReturn($menuData); $this->navigationService ->shouldReceive('getBreadcrumbs') ->once() ->andReturn([]); $inputData = new AdminLayoutData( title: 'Health Check', navigationMenu: new NavigationMenu([]), breadcrumbs: new BreadcrumbCollection([]), currentPath: '/admin' ); $result = $this->processor->processAdminLayout($inputData); $items = $result->navigationMenu->sections[0]->items; expect($items[0]->isActive)->toBeFalse(); // Dashboard expect($items[1]->isActive)->toBeTrue(); // Health Check (current path) }); it('preserves original layout data properties', function () { $this->request->path = '/admin'; $this->navigationService ->shouldReceive('getNavigationMenu') ->once() ->andReturn([]); $this->navigationService ->shouldReceive('getBreadcrumbs') ->once() ->andReturn([]); $inputData = new AdminLayoutData( title: 'Original Title', navigationMenu: new NavigationMenu([]), breadcrumbs: new BreadcrumbCollection([]), currentPath: '/admin', metaDescription: 'Original description', pageClass: 'original-class' ); $result = $this->processor->processAdminLayout($inputData); expect($result->title)->toBe('Original Title'); expect($result->metaDescription)->toBe('Original description'); expect($result->pageClass)->toBe('original-class'); }); });