feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события

Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
This commit is contained in:
Dmitry
2026-09-19 22:14:21 +03:00
parent a559d6de3e
commit 62d36aa3e8
73 changed files with 6406 additions and 1192 deletions
+95
View File
@@ -0,0 +1,95 @@
import 'package:fintracker_api/fintracker_api.dart';
import 'package:fintracker_app/core/cache/cached.dart';
import 'package:fintracker_app/features/accounts/providers.dart';
import 'package:fintracker_app/features/portfolios/portfolios_page.dart';
import 'package:fintracker_app/features/portfolios/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) => AccountOut(
id: id,
kind: kind,
source_: 'test',
sourceId: 'acc-$id',
broker: null,
name: name,
currency: 'RUB',
role: AccountRole.investment,
includeInNetWorth: true,
mirrorOfAccountId: null,
primaryEventSource: null,
archived: false,
disabled: false,
openedAt: null,
balance: null,
balanceAsOf: null,
startBalance: null,
creditLimit: null,
);
Widget _app(List<PortfolioOut> portfolios, List<AccountOut> accounts) =>
ProviderScope(
overrides: [
portfolioListProvider.overrideWith((ref) async => portfolios),
accountsProvider.overrideWith((ref) async => Cached(accounts)),
],
child: const MaterialApp(home: PortfoliosPage()),
);
void main() {
testWidgets('an empty list explains what a portfolio is', (tester) async {
await tester.pumpWidget(_app(const [], const []));
await tester.pumpAndSettle();
expect(find.textContaining('Портфелей пока нет'), findsOneWidget);
expect(find.text('Новый портфель'), findsOneWidget);
});
testWidgets('a portfolio shows its accounts by name', (tester) async {
await tester.pumpWidget(
_app(
[
PortfolioOut(
id: 1,
name: 'Основной',
baseCurrency: 'RUB',
accountIds: [10, 11],
),
],
[
_account(10, 'Брокер А', AccountKind.broker),
_account(11, 'Брокер Б', AccountKind.broker),
],
),
);
await tester.pumpAndSettle();
expect(find.text('Основной'), findsOneWidget);
expect(find.text('2 счёта: Брокер А, Брокер Б'), findsOneWidget);
});
testWidgets('the new-portfolio dialog needs a name and lists brokers first', (
tester,
) async {
await tester.pumpWidget(
_app(const [], [
_account(1, 'Карта', AccountKind.zmCard),
_account(2, 'Брокер', AccountKind.broker),
]),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Новый портфель'));
await tester.pumpAndSettle();
final titles = tester
.widgetList<CheckboxListTile>(find.byType(CheckboxListTile))
.toList();
expect((titles.first.title as Text).data, 'Брокер');
await tester.tap(find.text('Сохранить'));
await tester.pumpAndSettle();
expect(find.text('Обязательное поле'), findsOneWidget);
});
}