cyberhybridhub/test/admin/acceptance/admin_portal_acceptance_test.dart
2026-05-31 11:17:12 -05:00

226 lines
6.6 KiB
Dart

import 'package:cyberhybridhub/admin/models/market_history_admin_config.dart';
import 'package:cyberhybridhub/admin/models/sync_run_event.dart';
import 'package:cyberhybridhub/admin/repositories/sync_run_log_repository.dart';
import 'package:cyberhybridhub/admin/screens/market_history_log_screen.dart';
import 'package:cyberhybridhub/admin/widgets/sync_run_expansion_tile.dart';
import 'package:cyberhybridhub/theme/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
/// FLUTTER-TDD-PLAN.md Section 9 — automated definition-of-done checks.
class _AcceptanceController implements SyncRunLogController {
_AcceptanceController(this._states);
final List<SyncRunLogRepositoryState> _states;
int _index = 0;
bool? lastCleanupArchive;
SyncRunLogRepositoryState _current = const SyncRunLogRepositoryState(
pinned: <SyncRunEvent>[],
history: <SyncRunEvent>[],
nextBefore: null,
isLoading: true,
errorMessage: null,
);
@override
SyncRunLogRepositoryState get state => _current;
@override
Future<SyncRunLogRepositoryState> loadInitial({int limit = 50}) async {
_current = _states[_index];
return _current;
}
@override
Future<SyncRunLogRepositoryState> refresh({int limit = 50}) async {
_index = 0;
_current = _states[_index];
return _current;
}
@override
Future<SyncRunLogRepositoryState> loadMore({int limit = 50}) async {
if (_index < _states.length - 1) {
_index++;
}
_current = _states[_index];
return _current;
}
@override
Future<SyncRunLogRepositoryState> triggerResync() => refresh();
@override
Future<SyncRunLogRepositoryState> triggerCleanup({bool archive = false}) async {
lastCleanupArchive = archive;
return refresh();
}
}
SyncRunEvent _run({
required int id,
required String kind,
required DateTime startedAt,
String? error,
SyncRunSeverity severity = SyncRunSeverity.ok,
SyncRunStatus status = SyncRunStatus.success,
}) {
return SyncRunEvent(
id: id,
kind: kind,
startedAt: startedAt,
finishedAt: startedAt.add(const Duration(minutes: 1)),
rowsWritten: kind == 'cleanup' ? 0 : 100,
rowsRemoved: kind == 'cleanup' ? 50 : 0,
error: error,
severity: severity,
status: status,
durationMs: 60000,
summary: error ?? 'summary $id',
);
}
Future<void> _pump(
WidgetTester tester,
_AcceptanceController controller,
) async {
await tester.pumpWidget(
MaterialApp(
theme: buildAppTheme(),
home: MarketHistoryLogScreen(
controller: controller,
now: DateTime.utc(2026, 5, 27, 12),
autoRefreshInterval: null,
),
),
);
await tester.pumpAndSettle();
}
void main() {
testWidgets('newest history row appears first', (WidgetTester tester) async {
final DateTime t0 = DateTime.utc(2026, 5, 27, 10);
final _AcceptanceController controller = _AcceptanceController(
<SyncRunLogRepositoryState>[
SyncRunLogRepositoryState(
pinned: const <SyncRunEvent>[],
history: <SyncRunEvent>[
_run(id: 2, kind: 'cleanup', startedAt: t0, error: null),
_run(
id: 1,
kind: 'backfill',
startedAt: t0.subtract(const Duration(hours: 2)),
),
],
nextBefore: null,
isLoading: false,
errorMessage: null,
),
],
);
await _pump(tester, controller);
expect(find.textContaining('summary 2'), findsOneWidget);
});
testWidgets('pinned unresolved failures appear in needs attention', (
WidgetTester tester,
) async {
final DateTime t0 = DateTime.utc(2026, 5, 27, 10);
final _AcceptanceController controller = _AcceptanceController(
<SyncRunLogRepositoryState>[
SyncRunLogRepositoryState(
pinned: <SyncRunEvent>[
_run(
id: 9,
kind: 'backfill',
startedAt: t0,
error: '429 rate limited',
severity: SyncRunSeverity.rateLimit,
status: SyncRunStatus.failed,
),
],
history: const <SyncRunEvent>[],
nextBefore: null,
isLoading: false,
errorMessage: null,
),
],
);
await _pump(tester, controller);
expect(find.byKey(const Key('pinned-section')), findsOneWidget);
expect(find.byKey(const Key('sync-run-9')), findsOneWidget);
});
testWidgets('expanded row shows full error without raw payload labels', (
WidgetTester tester,
) async {
const String errorText = 'AlpacaMarketDataException: batch failed 500';
final _AcceptanceController controller = _AcceptanceController(
<SyncRunLogRepositoryState>[
SyncRunLogRepositoryState(
pinned: <SyncRunEvent>[
_run(
id: 11,
kind: 'backfill',
startedAt: DateTime.utc(2026, 5, 27, 10),
error: errorText,
severity: SyncRunSeverity.error,
status: SyncRunStatus.failed,
),
],
history: const <SyncRunEvent>[],
nextBefore: null,
isLoading: false,
errorMessage: null,
),
],
);
await _pump(tester, controller);
await tester.tap(find.byType(ExpansionTile));
await tester.pumpAndSettle();
expect(find.text(errorText), findsWidgets);
expect(find.textContaining('"raw"'), findsNothing);
expect(find.textContaining('bars'), findsNothing);
});
testWidgets('cleanup archive toggle passes archive flag when enabled', (
WidgetTester tester,
) async {
final DateTime t0 = DateTime.utc(2026, 5, 27, 10);
final _AcceptanceController controller = _AcceptanceController(
<SyncRunLogRepositoryState>[
SyncRunLogRepositoryState(
pinned: const <SyncRunEvent>[],
history: <SyncRunEvent>[_run(id: 3, kind: 'cleanup', startedAt: t0)],
nextBefore: null,
isLoading: false,
errorMessage: null,
config: const MarketHistoryAdminConfig(
archiveEnabled: true,
windowDays: 7,
retentionDays: 7,
syncEnabled: true,
),
),
],
);
await _pump(tester, controller);
await tester.tap(find.byKey(const Key('actions-menu')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('action-cleanup')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('cleanup-archive-toggle')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('cleanup-confirm')));
await tester.pumpAndSettle();
expect(controller.lastCleanupArchive, isTrue);
});
}