Files
fin-tracker/app/test/accounts_page_test.dart
Dmitry 62d36aa3e8 feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события
Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
2026-09-19 22:14:21 +03:00

259 lines
8.0 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<int>, AccountPatch)>[];
@override
Future<String?> patch(Iterable<int> 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<AccountOut> _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<AccountOut> 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<AccountRole>, 'Сбережения'),
);
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',
);
},
);
}