import 'package:fintracker_api/fintracker_api.dart'; import 'package:fintracker_app/core/cache/cached.dart'; import 'package:fintracker_app/core/widgets/money_text.dart'; import 'package:fintracker_app/features/accounts/accounts_page.dart'; import 'package:fintracker_app/features/accounts/actions.dart'; import 'package:fintracker_app/features/accounts/providers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; AccountOut _account( int id, String name, AccountKind kind, { String? balance, String? valueRub, bool disabled = false, bool archived = false, AccountRole? role, String source = 'zenmoney', }) => AccountOut( id: id, kind: kind, source_: source, sourceId: 'acc-$id', broker: null, name: name, currency: 'RUB', role: role ?? (kind == AccountKind.broker ? AccountRole.investment : AccountRole.liquid), includeInNetWorth: true, mirrorOfAccountId: null, primaryEventSource: null, archived: archived, disabled: disabled, openedAt: null, balance: balance, balanceAsOf: null, startBalance: null, creditLimit: null, valueRub: valueRub, ); class _FakeActions implements AccountActions { final calls = <(List, AccountPatch)>[]; @override Future patch(Iterable ids, AccountPatch patch) async { calls.add((ids.toList(), patch)); return null; } } /// Rows sort as: active by type then name, then disabled — so «Карта» (1), «Брокер» (3), /// «Старая карта» (2, disabled). List _sample() => [ _account(1, 'Карта', AccountKind.zmCard, balance: '500'), _account( 2, 'Старая карта', AccountKind.zmCard, balance: '20', disabled: true, ), _account( 3, 'Брокер', AccountKind.broker, valueRub: '1044264', source: 'tinvest', ), ]; Future<_FakeActions> _open( WidgetTester tester, List accounts, { double width = 1200, }) async { tester.view.physicalSize = Size(width, 1000); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final fake = _FakeActions(); await tester.pumpWidget( ProviderScope( overrides: [ accountsProvider.overrideWith((ref) async => Cached(accounts)), accountActionsProvider.overrideWithValue(fake), ], child: const MaterialApp(home: AccountsPage()), ), ); await tester.pumpAndSettle(); return fake; } void main() { testWidgets( 'a broker account shows its ledger valuation, a card its balance', (tester) async { await _open(tester, _sample()); expect(find.text(MoneyText.format('1044264', 'RUB')), findsOneWidget); expect(find.text(MoneyText.format('500', 'RUB')), findsOneWidget); // the source tells a ZenMoney account from a broker one expect(find.textContaining('T-Invest'), findsOneWidget); expect(find.textContaining('ZenMoney'), findsWidgets); }, ); testWidgets('no amount at all reads as a dash, not as zero', (tester) async { await _open(tester, [_account(1, 'Пустой брокер', AccountKind.broker)]); expect(find.text('—'), findsOneWidget); }); testWidgets('the status chips count the accounts and filter the list', ( tester, ) async { await _open(tester, _sample()); expect(find.text('Все 3'), findsOneWidget); expect(find.text('Активные 2'), findsOneWidget); expect(find.text('Отключённые 1'), findsOneWidget); expect(find.text('Архивные 0'), findsOneWidget); expect( find.text('Старая карта'), findsOneWidget, reason: 'all shows the disabled one too', ); await tester.tap(find.text('Отключённые 1')); await tester.pumpAndSettle(); expect(find.text('Старая карта'), findsOneWidget); expect(find.text('Карта'), findsNothing); expect(find.text('Брокер'), findsNothing); }); testWidgets('the type chips and the search narrow the list', (tester) async { await _open(tester, _sample()); await tester.tap(find.widgetWithText(FilterChip, 'Инвестиции')); await tester.pumpAndSettle(); expect(find.text('Брокер'), findsOneWidget); expect(find.text('Карта'), findsNothing); await tester.tap(find.widgetWithText(FilterChip, 'Инвестиции')); await tester.enterText(find.byType(TextField), 'старая'); await tester.pumpAndSettle(); expect(find.text('Старая карта'), findsOneWidget); expect(find.text('Брокер'), findsNothing); await tester.enterText(find.byType(TextField), 'nothing like it'); await tester.pumpAndSettle(); expect(find.text('Ничего не найдено'), findsOneWidget); }); testWidgets( 'the «Активен» switch on a row sends one patch for that account', (tester) async { final fake = await _open(tester, _sample()); // per row the switches are «В капитал» then «Активен»; the first row is «Карта» (id 1) await tester.tap(find.byType(Switch).at(1)); await tester.pumpAndSettle(); expect(fake.calls, hasLength(1)); expect(fake.calls.single.$1, [1]); expect(fake.calls.single.$2.disabled, isTrue); }, ); testWidgets('switching a disabled account back on sends disabled: false', ( tester, ) async { final fake = await _open(tester, _sample()); await tester.tap(find.byType(Switch).at(5)); // «Старая карта», «Активен» await tester.pumpAndSettle(); expect(fake.calls.single.$1, [2]); expect(fake.calls.single.$2.disabled, isFalse); }); testWidgets('the type pill offers the four types and sends the chosen one', ( tester, ) async { final fake = await _open(tester, _sample()); await tester.tap(find.byTooltip('Тип счёта').first); await tester.pumpAndSettle(); await tester.tap( find.widgetWithText(CheckedPopupMenuItem, 'Сбережения'), ); await tester.pumpAndSettle(); expect(fake.calls.single.$1, [1]); expect(fake.calls.single.$2.role, AccountRole.savings); }); testWidgets( 'a selection turns into bulk actions and clears itself afterwards', (tester) async { final fake = await _open(tester, _sample()); // checkbox 0 is «select all»; rows follow in list order: Карта (1), Брокер (3) await tester.tap(find.byType(Checkbox).at(1)); await tester.tap(find.byType(Checkbox).at(2)); await tester.pumpAndSettle(); expect(find.text('Выбрано: 2'), findsOneWidget); await tester.tap(find.text('Отключить')); await tester.pumpAndSettle(); expect(fake.calls.single.$1, unorderedEquals([1, 3])); expect(fake.calls.single.$2.disabled, isTrue); expect( find.textContaining('Выбрано'), findsNothing, reason: 'the selection is spent', ); }, ); testWidgets('«select all» takes exactly what the filter shows', ( tester, ) async { final fake = await _open(tester, _sample()); await tester.tap(find.text('Отключённые 1')); await tester.pumpAndSettle(); await tester.tap(find.byType(Checkbox).first); await tester.pumpAndSettle(); expect(find.text('Выбрано: 1'), findsOneWidget); await tester.tap(find.text('Включить')); await tester.pumpAndSettle(); expect(fake.calls.single.$1, [2]); expect(fake.calls.single.$2.disabled, isFalse); }); testWidgets( 'a narrow window puts the controls on a second line with their labels', (tester) async { await _open(tester, _sample(), width: 500); expect(find.text('Активен'), findsNWidgets(3)); expect(find.text('В капитал'), findsNWidgets(3)); expect( find.text('Баланс / стоимость'), findsNothing, reason: 'no table header on a phone', ); }, ); }