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
This commit is contained in:
2025-11-10 11:06:07 +01:00
parent 6bc78f5540
commit 8f3c15ddbb
106 changed files with 9082 additions and 4483 deletions

View File

@@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
use App\Application\Admin\Registry\AdminNavigationRegistry;
use App\Application\Admin\Service\AdminLayoutProcessor;
use App\Application\Admin\Service\AdminNavigationService;
use App\Application\Admin\ValueObjects\AdminLayoutData;
use App\Framework\Http\HttpRequest;
describe('AdminLayoutProcessor', function () {
beforeEach(function () {
$this->navigationService = Mockery::mock(AdminNavigationService::class);
$this->request = Mockery::mock(HttpRequest::class);
});
it('processes admin layout data correctly', function () {
$this->request->path = '/admin/dashboard';
$this->request->shouldReceive('getPath')
->andReturn('/admin/dashboard');
$this->navigationService->shouldReceive('getNavigationMenu')
->once()
->andReturn([
'System' => [
'icon' => 'server',
'items' => [
'Dashboard' => '/admin',
'Health' => '/admin/system/health',
],
],
]);
$this->navigationService->shouldReceive('getBreadcrumbs')
->once()
->with('/admin/dashboard')
->andReturn([
['name' => 'Admin', 'url' => '/admin'],
['name' => 'Dashboard', 'url' => '/admin/dashboard'],
]);
$processor = new AdminLayoutProcessor(
$this->navigationService,
$this->request
);
$layoutData = AdminLayoutData::fromArray([
'title' => 'Dashboard',
'currentPath' => '/admin/dashboard',
]);
$result = $processor->processAdminLayout($layoutData);
expect($result)->toBeInstanceOf(AdminLayoutData::class);
expect($result->navigationMenu)->not->toBeNull();
expect($result->breadcrumbs)->not->toBeNull();
});
it('handles navigation service exceptions gracefully', function () {
$this->request->path = '/admin/dashboard';
$this->request->shouldReceive('getPath')
->andReturn('/admin/dashboard');
$this->navigationService->shouldReceive('getNavigationMenu')
->once()
->andThrow(new RuntimeException('Navigation failed'));
$processor = new AdminLayoutProcessor(
$this->navigationService,
$this->request
);
$layoutData = AdminLayoutData::fromArray([
'title' => 'Dashboard',
'currentPath' => '/admin/dashboard',
]);
// Should not throw, but use fallback menu
$result = $processor->processAdminLayout($layoutData);
expect($result)->toBeInstanceOf(AdminLayoutData::class);
expect($result->navigationMenu)->not->toBeNull();
});
it('handles breadcrumb service exceptions gracefully', function () {
$this->request->path = '/admin/dashboard';
$this->request->shouldReceive('getPath')
->andReturn('/admin/dashboard');
$this->navigationService->shouldReceive('getNavigationMenu')
->once()
->andReturn([
'System' => [
'icon' => 'server',
'items' => ['Dashboard' => '/admin'],
],
]);
$this->navigationService->shouldReceive('getBreadcrumbs')
->once()
->andThrow(new RuntimeException('Breadcrumbs failed'));
$processor = new AdminLayoutProcessor(
$this->navigationService,
$this->request
);
$layoutData = AdminLayoutData::fromArray([
'title' => 'Dashboard',
'currentPath' => '/admin/dashboard',
]);
// Should not throw, but use fallback breadcrumbs
$result = $processor->processAdminLayout($layoutData);
expect($result)->toBeInstanceOf(AdminLayoutData::class);
expect($result->breadcrumbs)->not->toBeNull();
});
it('processes layout from array correctly', function () {
$this->request->path = '/admin/users';
$this->request->shouldReceive('getPath')
->andReturn('/admin/users');
$this->navigationService->shouldReceive('getNavigationMenu')
->once()
->andReturn([
'Content' => [
'icon' => 'photo',
'items' => ['Users' => '/admin/users'],
],
]);
$this->navigationService->shouldReceive('getBreadcrumbs')
->once()
->andReturn([
['name' => 'Admin', 'url' => '/admin'],
['name' => 'Users', 'url' => '/admin/users'],
]);
$processor = new AdminLayoutProcessor(
$this->navigationService,
$this->request
);
$data = [
'title' => 'Users',
'users' => [['id' => 1, 'name' => 'John']],
];
$result = $processor->processLayoutFromArray($data);
expect($result)->toBeArray();
expect($result)->toHaveKey('title');
expect($result)->toHaveKey('users');
expect($result)->toHaveKey('navigation_menu');
expect($result)->toHaveKey('breadcrumbs');
expect($result)->toHaveKey('current_path');
});
it('merges original data with layout data', function () {
$this->request->path = '/admin/analytics';
$this->request->shouldReceive('getPath')
->andReturn('/admin/analytics');
$this->navigationService->shouldReceive('getNavigationMenu')
->once()
->andReturn([
'Analytics' => [
'icon' => 'chart-bar',
'items' => ['Dashboard' => '/admin/analytics'],
],
]);
$this->navigationService->shouldReceive('getBreadcrumbs')
->once()
->andReturn([
['name' => 'Admin', 'url' => '/admin'],
['name' => 'Analytics', 'url' => '/admin/analytics'],
]);
$processor = new AdminLayoutProcessor(
$this->navigationService,
$this->request
);
$originalData = [
'title' => 'Analytics',
'stats' => ['page_views' => 1000],
'custom_field' => 'custom_value',
];
$result = $processor->processLayoutFromArray($originalData);
expect($result['title'])->toBe('Analytics');
expect($result['stats'])->toBe($originalData['stats']);
expect($result['custom_field'])->toBe('custom_value');
expect($result)->toHaveKey('navigation_menu');
});
});