Files
recipe-app/flutter/test/features/admin/presentation/admin_ai_panel_test.dart
T
Nils-Johan Gynther e492ea9a2e
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Failing after 2m19s
Test Suite / flutter-quality (push) Failing after 1m11s
test(admin-panel): add product context to AI trace warnings
- Added `productName` field to `AdminAiWarning` in test data
- Updated warning test cases to include product context for better traceability
2026-05-24 21:55:45 +02:00

271 lines
8.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:recipe_flutter/core/ui/app_shell.dart';
import 'package:recipe_flutter/features/admin/data/admin_repository.dart';
import 'package:recipe_flutter/features/admin/domain/admin_ai_trace.dart';
import 'package:recipe_flutter/features/admin/domain/admin_ai_trace_detail.dart';
import 'package:recipe_flutter/features/admin/presentation/admin_ai_panel.dart';
import 'package:recipe_flutter/features/admin/presentation/admin_screen.dart';
import 'package:recipe_flutter/features/auth/data/auth_providers.dart';
class _FakeAdminRepository implements AdminRepository {
final AdminAiTraceListResponse flyerList;
final AdminAiTraceListResponse receiptList;
final Map<String, AdminAiTraceDetail> details;
_FakeAdminRepository({
required this.flyerList,
required this.receiptList,
required this.details,
});
@override
Future<AdminAiTraceListResponse> listAiTraces({
required AdminAiTraceSource source,
int limit = 25,
String? cursor,
String? period,
bool onlyErrors = false,
}) async {
return source == AdminAiTraceSource.flyer ? flyerList : receiptList;
}
@override
Future<AdminAiTraceDetail> getAiTraceById(String traceId) async {
return details[traceId] ?? details.values.first;
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
Widget _buildPanelApp(AdminRepository repo) {
return ProviderScope(
overrides: [
adminRepositoryProvider.overrideWithValue(repo),
],
child: const MaterialApp(
home: Scaffold(
body: AdminAiPanel(embedded: true),
),
),
);
}
void main() {
final veryLargeOutput = '{"payload":"${List.filled(13050, 'x').join()}"}';
final flyerItem = AdminAiTraceListItem(
id: 'flyer-101',
source: AdminAiTraceSource.flyer,
status: AdminAiTraceStatus.warning,
createdAt: DateTime.parse('2026-05-20T12:34:56.000Z'),
userId: 7,
userLabel: 'admin',
sessionId: 101,
fileName: 'willys-v20.pdf',
model: 'ministral-8b-2512',
durationMs: 1880,
warningsCount: 2,
hasPrompt: true,
hasOutput: true,
error: 'Det finns 2 varningar i detaljvyn.',
);
final flyerDetail = AdminAiTraceDetail(
id: 'flyer-101',
source: AdminAiTraceSource.flyer,
status: AdminAiTraceStatus.warning,
createdAt: DateTime.parse('2026-05-20T12:34:56.000Z'),
userId: 7,
userLabel: 'admin',
sessionId: 101,
fileName: 'willys-v20.pdf',
model: 'ministral-8b-2512',
durationMs: 1880,
retryCount: 1,
chunkCount: 3,
warnings: const [
AdminAiWarning(
code: 'low_confidence',
kind: 'parse',
title: 'Låg parsningskvalitet',
message: 'Modellens säkerhet är låg, granska raden manuellt.',
severity: 'warning',
location: 'Steg: AI-parser',
itemIndex: 5,
productName: 'Test Product',
),
AdminAiWarning(
code: 'no_match',
kind: 'match',
title: 'Ingen produktmatchning',
message:
'Vi kunde inte hitta någon befintlig produkt som matchar texten på flyern.',
severity: 'warning',
location: 'Steg: matchning mot dina produkter',
itemIndex: 7,
productName: 'Another Test Product',
),
],
legacyWarnings: const ['parse:low_confidence', 'match:no_match'],
error: null,
prompt: 'Prompttext exempel',
rawOutput: veryLargeOutput,
normalizedOutput: null,
summary: const {'itemCount': 1},
);
group('Admin AI tab and panel', () {
testWidgets('Admin main route query tab=ai renders AI panel',
(tester) async {
final fakeRepo = _FakeAdminRepository(
flyerList:
AdminAiTraceListResponse(items: [flyerItem], nextCursor: null),
receiptList:
const AdminAiTraceListResponse(items: [], nextCursor: null),
details: {'flyer-101': flyerDetail},
);
await tester.pumpWidget(
ProviderScope(
overrides: [
adminRepositoryProvider.overrideWithValue(fakeRepo),
],
child: const MaterialApp(
home: Scaffold(
body: AdminScreen(initialTab: AdminViewTab.ai),
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Kvitto'), findsOneWidget);
expect(find.text('Flyer'), findsOneWidget);
expect(find.text('willys-v20.pdf'), findsOneWidget);
});
testWidgets('AppShell shows AI top chip and navigates with query',
(tester) async {
String? navigatedTo;
await tester.pumpWidget(
ProviderScope(
overrides: [
isAdminProvider.overrideWithValue(true),
],
child: MaterialApp(
home: AppShell(
location: '/admin?tab=ai',
onNavigateToPath: (path) => navigatedTo = path,
child: const SizedBox.shrink(),
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('AI'), findsOneWidget);
await tester.tap(find.text('Databas'));
await tester.pump();
expect(navigatedTo, '/admin?tab=database');
});
testWidgets('Source switching toggles between Flyer and Kvitto views',
(tester) async {
final fakeRepo = _FakeAdminRepository(
flyerList:
AdminAiTraceListResponse(items: [flyerItem], nextCursor: null),
receiptList:
const AdminAiTraceListResponse(items: [], nextCursor: null),
details: {'flyer-101': flyerDetail},
);
await tester.pumpWidget(_buildPanelApp(fakeRepo));
await tester.pumpAndSettle();
expect(find.text('willys-v20.pdf'), findsOneWidget);
await tester.tap(find.text('Kvitto'));
await tester.pumpAndSettle();
expect(find.text('Receipt trace-data saknas i recipe-api i denna fas.'),
findsOneWidget);
await tester.tap(find.text('Flyer'));
await tester.pumpAndSettle();
expect(find.text('willys-v20.pdf'), findsOneWidget);
});
testWidgets('Prompt/output are selectable and warning details are visible',
(tester) async {
await tester.binding.setSurfaceSize(const Size(1400, 1200));
final fakeRepo = _FakeAdminRepository(
flyerList:
AdminAiTraceListResponse(items: [flyerItem], nextCursor: null),
receiptList:
const AdminAiTraceListResponse(items: [], nextCursor: null),
details: {'flyer-101': flyerDetail},
);
await tester.pumpWidget(_buildPanelApp(fakeRepo));
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 500));
await tester.tap(find.byType(ListTile).first);
await tester.pumpAndSettle();
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('Sammanfattning'), findsOneWidget);
expect(find.text('Varningar (2)'), findsOneWidget);
expect(find.text('Låg parsningskvalitet'), findsOneWidget);
expect(find.text('Ingen produktmatchning'), findsOneWidget);
final detailScroll = find.byType(Scrollable).last;
await tester.scrollUntilVisible(
find.text('Model Output'),
200,
scrollable: detailScroll,
);
await tester.pumpAndSettle();
expect(find.text('Model Output'), findsOneWidget);
expect(find.byType(SelectableText), findsWidgets);
expect(find.text('Visa hela outputen'), findsOneWidget);
await tester.tap(find.text('Visa hela outputen'));
await tester.pumpAndSettle();
expect(find.text('Visa mindre'), findsOneWidget);
final copyPrompt = find.byTooltip('Kopiera');
final copyOutput = find.byTooltip('Kopiera JSON');
final copyWarnings = find.byTooltip('Kopiera alla varningar');
final copyErrorReport = find.text('Kopiera felrapport');
expect(copyPrompt, findsOneWidget);
expect(copyOutput, findsOneWidget);
expect(copyWarnings, findsOneWidget);
expect(copyErrorReport, findsOneWidget);
await tester.tap(copyPrompt);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
await tester.tap(copyOutput);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
await tester.tap(copyWarnings);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
await tester.tap(copyErrorReport);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
addTearDown(() => tester.binding.setSurfaceSize(null));
});
});
}