import 'dart:convert'; import 'package:ac_web/Api/Api.dart'; import 'package:ac_web/Layouts/DesktopView.dart'; import 'package:ac_web/Layouts/MainApp.dart'; import 'package:ac_web/Layouts/MobileView.dart'; import 'package:ac_web/Pages/AgentAvailabilities.dart'; import 'package:ac_web/Widgets/LatestReportEntry.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; /// The pages talk to `AcApi.instance`, so every test runs against a mocked /// backend instead of a real one. void configureMockedApi() { final client = MockClient((request) async { final String path = request.url.path; if (path.endsWith('agents')) { return http.Response( jsonEncode([ { 'Name': 'Ada', 'Type': 'CODER', 'LastSeen': '2026-08-28T12:00:00Z', 'Status': 'WORKING', 'Tasks': [], }, ]), 200, headers: {'content-type': 'application/json'}, ); } if (path.endsWith('reports')) { return http.Response( jsonEncode([ { 'Id': 'report-1', 'Title': 'Nightly log', 'Author': 'Ada', 'Timestamp': '2026-08-27T10:00:00Z', 'Type': 'LOG', 'Text': 'All good.', }, ]), 200, headers: {'content-type': 'application/json'}, ); } return http.Response('Not found', 404); }); AcApi.configure(const ApiConfig(host: 'api.local', port: 5000), httpClient: client); } /// Lets the futures of the mocked api complete. `pumpAndSettle` cannot be used /// here because the loading state shows an endlessly spinning indicator. Future settleApiCalls(WidgetTester tester) async { await tester.pump(); await tester.pump(); } /// Resizes the test window and restores it again after the test. void setWindowSize(WidgetTester tester, Size size) { tester.view.physicalSize = size; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.reset); } void main() { setUp(configureMockedApi); tearDown(AcApi.reset); testWidgets('narrow windows show the mobile layout with its navigation bar', (tester) async { setWindowSize(tester, const Size(400, 800)); await tester.pumpWidget(const MainApp()); expect(find.byType(MobileView), findsOneWidget); expect(find.byType(DesktopView), findsNothing); expect(find.byType(NavigationBar), findsOneWidget); // The latest reports page is the one the mobile view starts on. expect(find.byType(CircularProgressIndicator), findsOneWidget); await settleApiCalls(tester); expect(find.byType(LatestReportEntry), findsOneWidget); expect(find.text('Nightly log'), findsOneWidget); }); testWidgets('wide windows show all three columns next to each other', (tester) async { setWindowSize(tester, const Size(DesktopView.minimumWidth + 200, 900)); await tester.pumpWidget(const MainApp()); await settleApiCalls(tester); expect(find.byType(DesktopView), findsOneWidget); expect(find.byType(MobileView), findsNothing); // Reports on the left, the latest reports in the center, agents on the right. expect(find.text('Code Review'), findsOneWidget); expect(find.byType(LatestReportEntry), findsOneWidget); expect(find.byType(AgentAvailabilities), findsOneWidget); expect(find.text('Ada'), findsOneWidget); }); testWidgets('a failing api is reported instead of spinning forever', (tester) async { AcApi.configure( const ApiConfig(host: 'api.local', port: 5000), httpClient: MockClient((request) async => throw http.ClientException('offline')), ); setWindowSize(tester, const Size(400, 800)); await tester.pumpWidget(const MainApp()); await settleApiCalls(tester); expect(find.byType(CircularProgressIndicator), findsNothing); expect(find.textContaining('Could not load reports'), findsOneWidget); }); }