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
+109
View File
@@ -0,0 +1,109 @@
import 'package:fintracker_api/fintracker_api.dart';
import 'package:fintracker_app/features/portfolio/instrument_edit_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
InstrumentOut _instrument() => InstrumentOut(
id: 1,
assetClass: 'share',
isin: 'RU0008958863',
figi: null,
ticker: 'MSNG',
board: 'TQBR',
exchange: null,
name: 'Мосэнерго',
issuer: null,
currency: 'RUB',
lot: 1,
nominal: null,
nominalCurrency: null,
maturityDate: null,
sector: null,
country: null,
isActive: true,
);
Future<InstrumentPatch?> _open(
WidgetTester tester,
Future<void> Function() interact,
) async {
InstrumentPatch? result;
var closed = false;
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (context) => Scaffold(
body: TextButton(
onPressed: () async {
result = await showDialog<InstrumentPatch>(
context: context,
builder: (_) => InstrumentEditDialog(instrument: _instrument()),
);
closed = true;
},
child: const Text('open'),
),
),
),
),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
await interact();
await tester.pumpAndSettle();
expect(closed, isTrue);
return result;
}
void main() {
testWidgets('only the changed lot goes into the patch', (tester) async {
final patch = await _open(tester, () async {
await tester.enterText(find.widgetWithText(TextFormField, '1'), '1000');
await tester.tap(find.text('Сохранить'));
});
expect(patch, isNotNull);
expect(patch!.lot, 1000);
expect(patch.name, isNull);
expect(patch.board, isNull);
expect(patch.assetClass, isNull);
});
testWidgets('saving without changes closes with nothing to send', (
tester,
) async {
final patch = await _open(tester, () async {
await tester.tap(find.text('Сохранить'));
});
expect(patch, isNull);
});
testWidgets('a lot below 1 is refused and the dialog stays open', (
tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (context) => Scaffold(
body: TextButton(
onPressed: () => showDialog<InstrumentPatch>(
context: context,
builder: (_) => InstrumentEditDialog(instrument: _instrument()),
),
child: const Text('open'),
),
),
),
),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
await tester.enterText(find.widgetWithText(TextFormField, '1'), '0');
await tester.tap(find.text('Сохранить'));
await tester.pumpAndSettle();
expect(find.text('Целое число не меньше 1'), findsOneWidget);
expect(find.text('Сохранить'), findsOneWidget);
});
}