diff --git a/test/features/front_hall/presentation/pages/front_hall_page_test.dart b/test/features/front_hall/presentation/pages/front_hall_page_test.dart new file mode 100644 index 0000000..e12dabf --- /dev/null +++ b/test/features/front_hall/presentation/pages/front_hall_page_test.dart @@ -0,0 +1,232 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tatlock_ui/features/front_hall/presentation/pages/front_hall_page.dart'; + +import '../../../../harness/test_harness.dart'; + +void main() { + final harness = TestHarness(); + + setUp(() => harness.setUp()); + tearDown(() => harness.tearDown()); + + // Set larger window size and suppress overflow errors + Future setLargeWindowSize(WidgetTester tester) async { + tester.view.physicalSize = const Size(1400, 900); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + // Suppress overflow errors - not what we're testing + final originalOnError = FlutterError.onError; + FlutterError.onError = (details) { + if (details.exceptionAsString().contains('overflowed')) { + return; // Ignore overflow errors + } + originalOnError?.call(details); + }; + addTearDown(() => FlutterError.onError = originalOnError); + } + + group('FrontHallPage', () { + testWidgets('displays welcome message', (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks(); + harness.givenSystemStats(); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pumpAndSettle(); + + expect(find.text('Welcome to Tatlock'), findsOneWidget); + expect(find.text('Your homelab dashboard is ready.'), findsOneWidget); + }); + + testWidgets('displays Quick Links panel header', (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks(); + harness.givenSystemStats(); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pumpAndSettle(); + + expect(find.text('Quick Links'), findsOneWidget); + expect(find.byIcon(Icons.link), findsOneWidget); + }); + + testWidgets('displays quick links from API', (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks(); + harness.givenSystemStats(); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pumpAndSettle(); + + // Should display quick link names from fixtures + expect(find.text('Portainer'), findsOneWidget); + expect(find.text('Gitea'), findsOneWidget); + expect(find.text('Jellyfin'), findsOneWidget); + }); + + testWidgets('displays System Stats section header', (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks(); + harness.givenSystemStats(); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pumpAndSettle(); + + expect(find.text('System Stats'), findsOneWidget); + expect(find.byIcon(Icons.monitor_heart), findsWidgets); + }); + + testWidgets('displays Environment section header', (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks(); + harness.givenSystemStats(); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pumpAndSettle(); + + expect(find.text('Environment'), findsOneWidget); + expect(find.byIcon(Icons.eco), findsOneWidget); + }); + + testWidgets('displays Settings button', (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks(); + harness.givenSystemStats(); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pumpAndSettle(); + + expect(find.text('Settings'), findsOneWidget); + expect(find.byIcon(Icons.settings), findsOneWidget); + }); + + testWidgets('displays refresh button in Quick Links panel', (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks(); + harness.givenSystemStats(); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.refresh), findsWidgets); + }); + + testWidgets('displays loading state for system stats initially', + (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks(); + harness.givenSystemStats(); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pump(); + + // Should show loading indicator before data loads + expect(find.byType(CircularProgressIndicator), findsWidgets); + + await tester.pumpAndSettle(); + }); + + testWidgets('displays system stats gauges after loading', (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks(); + harness.givenSystemStats(); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pumpAndSettle(); + + // Should display gauge labels + expect(find.text('CPU'), findsOneWidget); + expect(find.text('RAM'), findsOneWidget); + }); + + testWidgets('displays error state when system stats fails', (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks(); + harness.givenApiError( + method: 'GET', + path: '/tools/system/stats', + statusCode: 500, + message: 'Internal server error', + ); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pumpAndSettle(); + + // Should show system stats error message or error indicator + expect( + find.text('Failed to load system stats').evaluate().isNotEmpty || + find.byIcon(Icons.error_outline).evaluate().isNotEmpty, + isTrue, + reason: 'Should display system stats error', + ); + }); + + testWidgets('calls quick links API on mount', (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks(); + harness.givenSystemStats(); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pumpAndSettle(); + + harness.verifyApiCalled('GET', '/dashboard/quick-links'); + }); + + testWidgets('calls system stats API on mount', (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks(); + harness.givenSystemStats(); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pumpAndSettle(); + + harness.verifyApiCalled('GET', '/tools/system/stats'); + }); + + testWidgets('uses default links when API returns empty list', + (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks([]); // Empty list triggers fallback + harness.givenSystemStats(); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pumpAndSettle(); + + // Should display default links (from getDefaultQuickLinks) + expect(find.text('Jellyfin'), findsOneWidget); + expect(find.text('Open WebUI'), findsOneWidget); + }); + + testWidgets('displays category headers when multiple categories present', + (tester) async { + await setLargeWindowSize(tester); + harness.givenAuthenticatedUser(); + harness.givenQuickLinks(); // Fixtures have Infrastructure, Development, Home + harness.givenSystemStats(); + + await tester.pumpWidget(harness.wrap(const FrontHallPage())); + await tester.pumpAndSettle(); + + // Should display category headers (uppercase) + // At least INFRASTRUCTURE should be visible since Portainer is first + expect(find.text('INFRASTRUCTURE'), findsOneWidget); + }); + }); +} diff --git a/test/harness/fixtures.dart b/test/harness/fixtures.dart index e522b91..3f89bdc 100644 --- a/test/harness/fixtures.dart +++ b/test/harness/fixtures.dart @@ -202,36 +202,6 @@ class Fixtures { static Map group(int index) => Map.from(groups[index]); - // ============================================================ - // Quick Links - // ============================================================ - - static const quickLinks = [ - { - 'id': 1, - 'title': 'Portainer', - 'url': 'https://portainer.schweitz.net', - 'icon': 'dns', - 'category': 'Infrastructure', - 'position': 0, - 'is_visible': true, - 'link_type': 'iframe', - }, - { - 'id': 2, - 'title': 'Gitea', - 'url': 'https://git.schweitz.net', - 'icon': 'code', - 'category': 'Development', - 'position': 1, - 'is_visible': true, - 'link_type': 'new_tab', - }, - ]; - - static Map quickLink(int index) => - Map.from(quickLinks[index]); - // ============================================================ // Auth Sync Response // ============================================================ @@ -269,15 +239,83 @@ class Fixtures { } // ============================================================ - // System Stats + // System Stats (for DashboardContent) // ============================================================ static const systemStats = { - 'cpu_percent': 23.5, - 'memory_percent': 45.2, - 'disk_percent': 67.8, - 'uptime_seconds': 864000, - 'container_count': 12, - 'container_running': 10, + 'cpu': { + 'usage_percent': 23.5, + 'cores': 8, + 'load_1m': 1.2, + 'load_5m': 1.5, + 'load_15m': 1.3, + }, + 'memory': { + 'usage_percent': 45.2, + 'total_bytes': 17179869184, + 'used_bytes': 7771729306, + 'available_bytes': 9408139878, + }, + 'disks': [ + { + 'mount_point': '/', + 'device': '/dev/sda1', + 'fstype': 'ext4', + 'usage_percent': 67.8, + 'total_bytes': 500000000000, + 'used_bytes': 339000000000, + 'free_bytes': 161000000000, + }, + ], + 'network': { + 'bytes_sent': 1234567890, + 'bytes_recv': 9876543210, + 'bytes_total': 11111111100, + }, + 'gpu': { + 'available': false, + }, + 'hostname': 'test-server', + 'queried_at': '2024-01-15T10:30:00Z', }; + + // ============================================================ + // Quick Links + // ============================================================ + + static const quickLinks = [ + { + 'id': 1, + 'title': 'Portainer', + 'url': 'https://portainer.example.com', + 'icon': 'dns', + 'category': 'Infrastructure', + 'position': 0, + 'is_visible': true, + 'link_type': 'iframe', + }, + { + 'id': 2, + 'title': 'Gitea', + 'url': 'https://git.example.com', + 'icon': 'code', + 'category': 'Development', + 'position': 1, + 'is_visible': true, + 'link_type': 'new_tab', + }, + { + 'id': 3, + 'title': 'Jellyfin', + 'url': 'https://media.example.com', + 'icon': 'movie', + 'category': 'Home', + 'position': 2, + 'is_visible': true, + 'link_type': 'iframe', + }, + ]; + + static Map quickLink(int index) => + Map.from(quickLinks[index]); } diff --git a/test/harness/test_harness.dart b/test/harness/test_harness.dart index ccef4df..774d72d 100644 --- a/test/harness/test_harness.dart +++ b/test/harness/test_harness.dart @@ -164,7 +164,7 @@ class TestHarness { /// Set up mock quick links response. void givenQuickLinks([List>? links]) { - api.whenGet('/quick-links', links ?? Fixtures.quickLinks); + api.whenGet('/dashboard/quick-links', links ?? Fixtures.quickLinks); } /// Set up mock system stats response.