206 lines
6.0 KiB
Dart
206 lines
6.0 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:ac_web/Api/Api.dart';
|
|
import 'package:ac_web/Pages/AgentDetails.dart';
|
|
import 'package:ac_web/Widgets/AgentAvailabilityColumn.dart';
|
|
import 'package:ac_web/Widgets/AutoRefreshState.dart';
|
|
import 'package:ac_web/Widgets/LatestReportColumn.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';
|
|
|
|
/// Backend that answers every request with the agent and the report of the
|
|
/// current "round", so that a refresh can be told apart from the first load.
|
|
class _RoundBasedBackend {
|
|
|
|
int mRound = 0;
|
|
|
|
/// When set, every request fails instead of answering.
|
|
bool mIsOffline = false;
|
|
|
|
/// Number of requests the widgets sent, failing ones included.
|
|
int mRequestCount = 0;
|
|
|
|
/// Names handed out to the single agent, one per round.
|
|
static const List<String> agentNames = <String>['Ada', 'Grace', 'Alan'];
|
|
|
|
http.Client createClient() {
|
|
return MockClient((request) async {
|
|
|
|
mRequestCount++;
|
|
|
|
if (mIsOffline) {
|
|
throw http.ClientException('offline');
|
|
}
|
|
|
|
final String name = agentNames[mRound % agentNames.length];
|
|
final String path = request.url.path;
|
|
|
|
if (path.endsWith('agents')) {
|
|
return _json([
|
|
{
|
|
'Name': name,
|
|
'Type': 'CODER',
|
|
'LastSeen': '2026-08-28T12:00:00Z',
|
|
'Status': 'WORKING',
|
|
'Tasks': <dynamic>[],
|
|
},
|
|
]);
|
|
}
|
|
|
|
if (path.endsWith('reports')) {
|
|
return _json([
|
|
{
|
|
'Id': 'report-1',
|
|
'Title': 'Report of $name',
|
|
'Author': name,
|
|
'Timestamp': '2026-08-27T10:00:00Z',
|
|
'Type': 'LOG',
|
|
'Text': 'All good.',
|
|
},
|
|
]);
|
|
}
|
|
|
|
return http.Response('Not found', 404);
|
|
});
|
|
}
|
|
|
|
http.Response _json(Object body) {
|
|
return http.Response(
|
|
jsonEncode(body),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Lets the futures of the mocked api complete. `pumpAndSettle` cannot be used
|
|
/// here because the loading state shows an endlessly spinning indicator.
|
|
Future<void> settleApiCalls(WidgetTester tester) async {
|
|
await tester.pump();
|
|
await tester.pump();
|
|
}
|
|
|
|
/// Jumps to the next automatic refresh and lets its request complete.
|
|
Future<void> passRefreshInterval(WidgetTester tester) async {
|
|
await tester.pump(AutoRefreshState.refreshInterval);
|
|
await settleApiCalls(tester);
|
|
}
|
|
|
|
Widget wrap(Widget child) {
|
|
return MaterialApp(home: Scaffold(body: child));
|
|
}
|
|
|
|
void main() {
|
|
|
|
late _RoundBasedBackend backend;
|
|
|
|
setUp(() {
|
|
backend = _RoundBasedBackend();
|
|
AcApi.configure(
|
|
const ApiConfig(host: 'api.local', port: 5000),
|
|
httpClient: backend.createClient(),
|
|
);
|
|
});
|
|
|
|
tearDown(AcApi.reset);
|
|
|
|
testWidgets('the agent list picks up changes of the backend on its own', (tester) async {
|
|
await tester.pumpWidget(wrap(const AgentAvailabilityColumn()));
|
|
await settleApiCalls(tester);
|
|
|
|
expect(find.text('Ada'), findsOneWidget);
|
|
|
|
backend.mRound = 1;
|
|
await passRefreshInterval(tester);
|
|
|
|
expect(find.text('Grace'), findsOneWidget);
|
|
expect(find.text('Ada'), findsNothing);
|
|
});
|
|
|
|
testWidgets('the reports pick up changes of the backend on its own', (tester) async {
|
|
await tester.pumpWidget(wrap(const LatestReportColumn()));
|
|
await settleApiCalls(tester);
|
|
|
|
expect(find.text('Report of Ada'), findsOneWidget);
|
|
|
|
backend.mRound = 1;
|
|
await passRefreshInterval(tester);
|
|
|
|
expect(find.text('Report of Grace'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a refresh keeps the current data on screen instead of a spinner', (tester) async {
|
|
await tester.pumpWidget(wrap(const AgentAvailabilityColumn()));
|
|
await settleApiCalls(tester);
|
|
|
|
backend.mRound = 1;
|
|
|
|
// The frame in which the refresh got triggered still shows the old agent,
|
|
// the new one only replaces it once the request came back.
|
|
await tester.pump(AutoRefreshState.refreshInterval);
|
|
|
|
expect(find.byType(CircularProgressIndicator), findsNothing);
|
|
expect(find.text('Ada'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a failing refresh keeps the data that is already shown', (tester) async {
|
|
await tester.pumpWidget(wrap(const AgentAvailabilityColumn()));
|
|
await settleApiCalls(tester);
|
|
|
|
backend.mIsOffline = true;
|
|
await passRefreshInterval(tester);
|
|
|
|
expect(find.text('Ada'), findsOneWidget);
|
|
expect(find.textContaining('Could not load agents'), findsNothing);
|
|
|
|
// As soon as the api answers again, the refresh brings in the new data.
|
|
backend.mIsOffline = false;
|
|
backend.mRound = 2;
|
|
await passRefreshInterval(tester);
|
|
|
|
expect(find.text('Alan'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('the details page refreshes its agent without reporting anything', (tester) async {
|
|
final AgentDto agent = AgentDto(
|
|
name: 'Ada',
|
|
type: EAgentType.coder,
|
|
status: EAgentStatus.working,
|
|
lastSeen: DateTime.utc(2026, 8, 28, 12),
|
|
tasks: const <AgentTaskDto>[],
|
|
);
|
|
|
|
await tester.pumpWidget(MaterialApp(home: AgentDetails(agent: agent)));
|
|
await settleApiCalls(tester);
|
|
|
|
// The agent vanished from the api - the automatic refresh must not bother
|
|
// the user with a message about it.
|
|
backend.mRound = 1;
|
|
await passRefreshInterval(tester);
|
|
|
|
expect(find.byType(SnackBar), findsNothing);
|
|
|
|
// A reload the user asked for still reports it.
|
|
await tester.tap(find.byIcon(Icons.refresh));
|
|
await settleApiCalls(tester);
|
|
|
|
expect(find.byType(SnackBar), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('the refresh stops when the widget is gone', (tester) async {
|
|
await tester.pumpWidget(wrap(const AgentAvailabilityColumn()));
|
|
await settleApiCalls(tester);
|
|
|
|
await tester.pumpWidget(wrap(const SizedBox()));
|
|
|
|
final int requestsBefore = backend.mRequestCount;
|
|
|
|
// The timer of a disposed widget must not keep asking the api.
|
|
await passRefreshInterval(tester);
|
|
|
|
expect(backend.mRequestCount, requestsBefore);
|
|
});
|
|
}
|