178 lines
5.6 KiB
Dart
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);
|
|
});
|
|
}
|