Files
michaelschiemer/tests/Unit/Framework/Router/RouteResponderTest.php
Michael Schiemer 8f3c15ddbb fix(console): comprehensive TUI rendering fixes
- Fix Enter key detection: handle multiple Enter key formats (\n, \r, \r\n)
- Reduce flickering: lower render frequency from 60 FPS to 30 FPS
- Fix menu bar visibility: re-render menu bar after content to prevent overwriting
- Fix content positioning: explicit line positioning for categories and commands
- Fix line shifting: clear lines before writing, control newlines manually
- Limit visible items: prevent overflow with maxVisibleCategories/Commands
- Improve CPU usage: increase sleep interval when no events processed

This fixes:
- Enter key not working for selection
- Strong flickering of the application
- Menu bar not visible or being overwritten
- Top half of selection list not displayed
- Lines being shifted/misaligned
2025-11-10 11:06:07 +01:00

334 lines
11 KiB
PHP

<?php
declare(strict_types=1);
use App\Application\Admin\Service\AdminLayoutProcessor;
use App\Framework\Core\PathProvider;
use App\Framework\DI\DefaultContainer;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
use App\Framework\Http\RequestId;
use App\Framework\Meta\MetaData;
use App\Framework\Router\Result\ViewResult;
use App\Framework\Router\RouteResponder;
use App\Framework\View\RenderContext;
use App\Framework\View\TemplateRenderer;
describe('RouteResponder - Admin Layout Integration', function () {
beforeEach(function () {
// PathProvider and DefaultContainer are final, so we need real instances
$this->pathProvider = new PathProvider(__DIR__ . '/../../../../');
$this->container = new DefaultContainer();
$this->templateRenderer = Mockery::mock(TemplateRenderer::class);
// RequestId needs a secret for tests
$this->requestId = new RequestId('test-secret-for-unit-tests');
});
it('detects admin routes correctly', function () {
$request = new HttpRequest(
method: Method::GET,
path: '/admin/dashboard',
id: $this->requestId
);
$responder = new RouteResponder(
$this->pathProvider,
$this->container,
$this->templateRenderer,
$request
);
// Use reflection to test private method
$reflection = new ReflectionClass($responder);
$method = $reflection->getMethod('isAdminRoute');
$method->setAccessible(true);
expect($method->invoke($responder, '/admin/dashboard'))->toBeTrue();
expect($method->invoke($responder, '/admin'))->toBeTrue();
expect($method->invoke($responder, '/admin/users'))->toBeTrue();
expect($method->invoke($responder, '/'))->toBeFalse();
expect($method->invoke($responder, '/api/users'))->toBeFalse();
expect($method->invoke($responder, '/admin-api/users'))->toBeFalse();
});
it('enriches admin routes with layout data', function () {
$request = new HttpRequest(
method: Method::GET,
path: '/admin/dashboard',
id: $this->requestId
);
$layoutProcessor = Mockery::mock(AdminLayoutProcessor::class);
$layoutProcessor->shouldReceive('processLayoutFromArray')
->once()
->with(['title' => 'Dashboard'])
->andReturn([
'title' => 'Dashboard',
'navigation_menu' => ['System' => ['items' => []]],
'breadcrumbs' => [],
'current_path' => '/admin/dashboard',
]);
// Register layout processor in real container
$this->container->instance(AdminLayoutProcessor::class, $layoutProcessor);
$this->templateRenderer->shouldReceive('render')
->once()
->andReturn('<html>rendered</html>');
$responder = new RouteResponder(
$this->pathProvider,
$this->container,
$this->templateRenderer,
$request
);
$viewResult = new ViewResult(
template: 'dashboard',
metaData: new MetaData('Dashboard'),
data: ['title' => 'Dashboard']
);
$context = $responder->getContext($viewResult);
expect($context)->toBeInstanceOf(RenderContext::class);
expect($context->data)->toHaveKey('navigation_menu');
expect($context->data)->toHaveKey('breadcrumbs');
expect($context->data)->toHaveKey('current_path');
});
it('does not enrich non-admin routes', function () {
$request = new HttpRequest(
method: Method::GET,
path: '/home',
id: $this->requestId
);
$this->templateRenderer->shouldReceive('render')
->once()
->andReturn('<html>rendered</html>');
$responder = new RouteResponder(
$this->pathProvider,
$this->container,
$this->templateRenderer,
$request
);
$viewResult = new ViewResult(
template: 'home',
metaData: new MetaData('Home'),
data: ['title' => 'Home']
);
$context = $responder->getContext($viewResult);
expect($context)->toBeInstanceOf(RenderContext::class);
expect($context->data)->not->toHaveKey('navigation_menu');
expect($context->data)->not->toHaveKey('breadcrumbs');
expect($context->data['title'])->toBe('Home');
});
it('handles missing AdminLayoutProcessor gracefully', function () {
$request = new HttpRequest(
method: Method::GET,
path: '/admin/dashboard',
id: $this->requestId
);
// Container doesn't have AdminLayoutProcessor registered
// (it's a fresh container without it)
$this->templateRenderer->shouldReceive('render')
->once()
->andReturn('<html>rendered</html>');
$responder = new RouteResponder(
$this->pathProvider,
$this->container,
$this->templateRenderer,
$request
);
$viewResult = new ViewResult(
template: 'dashboard',
metaData: new MetaData('Dashboard'),
data: ['title' => 'Dashboard']
);
// Should not throw, but return original data
$context = $responder->getContext($viewResult);
expect($context)->toBeInstanceOf(RenderContext::class);
expect($context->data['title'])->toBe('Dashboard');
});
it('handles AdminLayoutProcessor exceptions gracefully', function () {
$request = new HttpRequest(
method: Method::GET,
path: '/admin/dashboard',
id: $this->requestId
);
$layoutProcessor = Mockery::mock(AdminLayoutProcessor::class);
$layoutProcessor->shouldReceive('processLayoutFromArray')
->once()
->andThrow(new RuntimeException('Layout processing failed'));
// Register layout processor that throws exception
$this->container->instance(AdminLayoutProcessor::class, $layoutProcessor);
$this->templateRenderer->shouldReceive('render')
->once()
->andReturn('<html>rendered</html>');
$responder = new RouteResponder(
$this->pathProvider,
$this->container,
$this->templateRenderer,
$request
);
$viewResult = new ViewResult(
template: 'dashboard',
metaData: new MetaData('Dashboard'),
data: ['title' => 'Dashboard']
);
// Should not throw, but return original data
$context = $responder->getContext($viewResult);
expect($context)->toBeInstanceOf(RenderContext::class);
expect($context->data['title'])->toBe('Dashboard');
});
it('preserves original data when enriching', function () {
$request = new HttpRequest(
method: Method::GET,
path: '/admin/users',
id: $this->requestId
);
$originalData = [
'title' => 'Users',
'users' => [['id' => 1, 'name' => 'John']],
'pagination' => ['page' => 1],
];
$layoutProcessor = Mockery::mock(AdminLayoutProcessor::class);
$layoutProcessor->shouldReceive('processLayoutFromArray')
->once()
->with($originalData)
->andReturn(array_merge($originalData, [
'navigation_menu' => ['Content' => ['items' => []]],
'breadcrumbs' => [],
'current_path' => '/admin/users',
]));
// Register layout processor in real container
$this->container->instance(AdminLayoutProcessor::class, $layoutProcessor);
$this->templateRenderer->shouldReceive('render')
->once()
->andReturn('<html>rendered</html>');
$responder = new RouteResponder(
$this->pathProvider,
$this->container,
$this->templateRenderer,
$request
);
$viewResult = new ViewResult(
template: 'users',
metaData: new MetaData('Users'),
data: $originalData
);
$context = $responder->getContext($viewResult);
expect($context->data)->toHaveKey('users');
expect($context->data)->toHaveKey('pagination');
expect($context->data)->toHaveKey('navigation_menu');
expect($context->data['users'])->toBe($originalData['users']);
});
});
describe('RouteResponder - Admin Route Detection Edge Cases', function () {
beforeEach(function () {
// PathProvider and DefaultContainer are final, so we need real instances
$this->pathProvider = new PathProvider(__DIR__ . '/../../../../');
$this->container = new DefaultContainer();
$this->templateRenderer = Mockery::mock(TemplateRenderer::class);
// RequestId needs a secret for tests
$this->requestId = new RequestId('test-secret-for-unit-tests');
});
it('handles exact /admin path', function () {
$request = new HttpRequest(
method: Method::GET,
path: '/admin',
id: $this->requestId
);
$layoutProcessor = Mockery::mock(AdminLayoutProcessor::class);
$layoutProcessor->shouldReceive('processLayoutFromArray')
->once()
->andReturn(['title' => 'Admin', 'navigation_menu' => [], 'breadcrumbs' => []]);
// Register layout processor in real container
$this->container->instance(AdminLayoutProcessor::class, $layoutProcessor);
$this->templateRenderer->shouldReceive('render')
->once()
->andReturn('<html>rendered</html>');
$responder = new RouteResponder(
$this->pathProvider,
$this->container,
$this->templateRenderer,
$request
);
$viewResult = new ViewResult(
template: 'admin',
metaData: new MetaData('Admin'),
data: ['title' => 'Admin']
);
$context = $responder->getContext($viewResult);
expect($context->data)->toHaveKey('navigation_menu');
});
it('does not treat /admin-api as admin route', function () {
$request = new HttpRequest(
method: Method::GET,
path: '/admin-api/users',
id: $this->requestId
);
$this->templateRenderer->shouldReceive('render')
->once()
->andReturn('<html>rendered</html>');
$responder = new RouteResponder(
$this->pathProvider,
$this->container,
$this->templateRenderer,
$request
);
$viewResult = new ViewResult(
template: 'api',
metaData: new MetaData('API'),
data: ['title' => 'API']
);
$context = $responder->getContext($viewResult);
expect($context->data)->not->toHaveKey('navigation_menu');
});
});