Files
OnBudget/test/features/notification_parsing/presentation/inbox_card_test.dart
T
2026-05-30 00:18:06 +03:00

178 lines
5.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:new_budget/l10n/app_localizations.dart';
import 'package:new_budget/src/app/theme/app_theme.dart';
import 'package:new_budget/src/core/database/converters/enum_converters.dart';
import 'package:new_budget/src/features/categories/domain/entities/category.dart';
import 'package:new_budget/src/features/notification_parsing/application/inbox_controller.dart';
import 'package:new_budget/src/features/notification_parsing/data/parser/draft_codec.dart';
import 'package:new_budget/src/features/notification_parsing/domain/entities/parse_draft.dart';
import 'package:new_budget/src/features/notification_parsing/domain/entities/raw_message.dart';
import 'package:new_budget/src/features/notification_parsing/domain/enums.dart';
import 'package:new_budget/src/features/notification_parsing/presentation/widgets/inbox_card.dart';
final _now = DateTime(2026, 5, 29, 14, 5);
class FakeInboxController extends InboxController {
int createRuleCalls = 0;
int confirmOnceCalls = 0;
int ignoreCalls = 0;
String? lastCreateRuleCategory;
@override
AsyncValue<void> build() => const AsyncData(null);
@override
Future<void> createRule({
required String userId,
required RawMessage message,
required ParseDraft draft,
required String accountId,
String? categoryId,
required String merchantCanonical,
required String pattern,
MatchMode matchMode = MatchMode.contains,
bool bindAccount = true,
}) async {
createRuleCalls++;
lastCreateRuleCategory = categoryId;
}
@override
Future<void> confirmOnce({
required String userId,
required RawMessage message,
required ParseDraft draft,
required String accountId,
String? categoryId,
bool bindAccount = true,
}) async {
confirmOnceCalls++;
}
@override
Future<void> ignore(RawMessage message) async {
ignoreCalls++;
}
}
RawMessage _recognizedMessage() {
final draft = ParseDraft(
rawMessageId: 'msg1',
type: TransactionType.expense,
amount: 124000,
cardLast4: '3456',
merchantRaw: 'PYATEROCHKA',
merchantCanonical: 'PYATEROCHKA',
categoryId: 'cat1',
kind: TxKind.purchase,
source: ParseSource.regex,
);
return RawMessage(
id: 'msg1',
userId: 'u1',
packageName: 'ru.sberbankmobile',
body: 'Покупка 1240 ₽ Карта *3456 PYATEROCHKA',
receivedAt: _now,
dedupHash: 'h',
status: RawMessageStatus.inbox,
draftJson: encodeDraftBundle(draft, null),
createdAt: _now,
);
}
Widget _host(FakeInboxController fake, RawMessage message) {
return ProviderScope(
overrides: [
inboxControllerProvider.overrideWith(() => fake),
],
child: MaterialApp(
theme: AppTheme.light(),
locale: const Locale('ru'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: InboxCard(
message: message,
userId: 'u1',
defaultAccountId: 'acc1',
categoryById: const {
'cat1': Category(
id: 'cat1',
userId: 'u1',
name: 'Продукты',
type: CategoryType.expense,
archived: false,
),
},
),
),
),
);
}
void main() {
setUpAll(() {
GoogleFonts.config.allowRuntimeFetching = false;
});
late FakeInboxController fake;
setUp(() => fake = FakeInboxController());
testWidgets('renders merchant, category and amount', (tester) async {
await tester.pumpWidget(_host(fake, _recognizedMessage()));
await tester.pump();
expect(find.text('PYATEROCHKA'), findsOneWidget);
expect(find.textContaining('Продукты'), findsWidgets);
expect(find.textContaining('240'), findsWidgets); // money formatted
});
testWidgets('tap "create rule" calls controller with category', (tester) async {
await tester.pumpWidget(_host(fake, _recognizedMessage()));
await tester.pump();
// Главная часть кнопки = ярлык «Правило: … → …». Категория задана →
// createRule вызывается напрямую, без открытия редактора.
final l10n = await AppLocalizations.delegate.load(const Locale('ru'));
await tester.tap(find.text(l10n.inboxCreateRule('PYATEROCHKA', 'Продукты')));
await tester.pump();
expect(fake.createRuleCalls, 1);
expect(fake.lastCreateRuleCategory, 'cat1');
});
testWidgets('tap confirm-once calls controller', (tester) async {
await tester.pumpWidget(_host(fake, _recognizedMessage()));
await tester.pump();
await tester.tap(find.byIcon(Icons.check));
await tester.pump();
expect(fake.confirmOnceCalls, 1);
expect(fake.createRuleCalls, 0);
});
testWidgets('tap ignore calls controller', (tester) async {
await tester.pumpWidget(_host(fake, _recognizedMessage()));
await tester.pump();
await tester.tap(find.byIcon(Icons.close));
await tester.pump();
expect(fake.ignoreCalls, 1);
});
testWidgets('unrecognized message shows manual-add fallback', (tester) async {
final msg = _recognizedMessage().copyWith(draftJson: null);
await tester.pumpWidget(_host(fake, msg));
await tester.pump();
final l10n = await AppLocalizations.delegate.load(const Locale('ru'));
expect(find.text(l10n.inboxUnrecognized), findsOneWidget);
expect(find.text(l10n.inboxAddManually), findsOneWidget);
});
}