Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
171 lines
5.9 KiB
Dart
171 lines
5.9 KiB
Dart
import 'package:fintracker_api/fintracker_api.dart';
|
|
import 'package:fintracker_app/core/api/common_providers.dart';
|
|
import 'package:fintracker_app/core/cache/cached.dart';
|
|
import 'package:fintracker_app/core/widgets/service_mark.dart';
|
|
import 'package:fintracker_app/features/home/providers.dart';
|
|
import 'package:fintracker_app/features/shell/analytics_tabs.dart';
|
|
import 'package:fintracker_app/features/shell/app_shell.dart';
|
|
import 'package:fintracker_app/features/shell/nav_destinations.dart';
|
|
import 'package:fintracker_app/features/shell/top_nav.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
DataQualityRow _dataQualityRow(int id) => DataQualityRow(
|
|
checkName: 'x',
|
|
computedAt: DateTime(2026, 9, 19),
|
|
count: 1,
|
|
detail: 'issue $id',
|
|
id: id,
|
|
ref: null,
|
|
severity: 'warning',
|
|
);
|
|
|
|
void main() {
|
|
// The top bar watches meProvider and dataQualityProvider for the profile avatar and the
|
|
// Здоровье counter — every pump at or above 600 needs both overridden.
|
|
Widget wrap(
|
|
double width, {
|
|
String location = '/',
|
|
List<DataQualityRow> issues = const [],
|
|
}) {
|
|
return ProviderScope(
|
|
overrides: [
|
|
meProvider.overrideWith(
|
|
(ref) async => UserOut(email: 'ada@example.com', id: 1),
|
|
),
|
|
dataQualityProvider.overrideWith((ref) async => Cached(issues)),
|
|
],
|
|
child: MediaQuery(
|
|
data: MediaQueryData(size: Size(width, 800)),
|
|
child: MaterialApp(
|
|
home: AppShell(location: location, child: Container()),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
testWidgets('shows the top bar on a wide surface', (tester) async {
|
|
await tester.pumpWidget(wrap(1280));
|
|
await tester.pump();
|
|
expect(find.byType(TopNav), findsOneWidget);
|
|
expect(find.byType(NavigationRail), findsNothing);
|
|
expect(find.byType(NavigationBar), findsNothing);
|
|
for (final label in [
|
|
'Обзор',
|
|
'Аналитика',
|
|
'Портфель',
|
|
'Счета',
|
|
'Операции',
|
|
'Добавить',
|
|
]) {
|
|
expect(find.text(label), findsOneWidget, reason: label);
|
|
}
|
|
});
|
|
|
|
testWidgets(
|
|
'keeps the top bar on a medium surface, with «Добавить» as an icon',
|
|
(tester) async {
|
|
await tester.pumpWidget(wrap(700));
|
|
await tester.pump();
|
|
expect(find.byType(TopNav), findsOneWidget);
|
|
expect(find.byType(NavigationBar), findsNothing);
|
|
expect(find.text('Добавить'), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets('shows a bottom NavigationBar on a narrow surface', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(wrap(400));
|
|
expect(find.byType(NavigationBar), findsOneWidget);
|
|
expect(find.byType(TopNav), findsNothing);
|
|
});
|
|
|
|
testWidgets('the resolve screen keeps Импорт selected', (tester) async {
|
|
await tester.pumpWidget(wrap(1280, location: '/instruments/pending'));
|
|
await tester.pump();
|
|
final nav = tester.widget<TopNav>(find.byType(TopNav));
|
|
expect(navDestinations[nav.selectedIndex].label, 'Импорт');
|
|
// a ledger screen shows its own name in place of «Операции»
|
|
expect(find.text('Импорт'), findsOneWidget);
|
|
expect(find.text('Операции'), findsNothing);
|
|
});
|
|
|
|
testWidgets('a nested route keeps its own section selected', (tester) async {
|
|
await tester.pumpWidget(wrap(1280, location: '/portfolio/instrument/311'));
|
|
await tester.pump();
|
|
final nav = tester.widget<TopNav>(find.byType(TopNav));
|
|
expect(navDestinations[nav.selectedIndex].label, 'Портфель');
|
|
});
|
|
|
|
testWidgets(
|
|
'the analytics screens share one tab strip and highlight Аналитика',
|
|
(tester) async {
|
|
await tester.pumpWidget(wrap(1280, location: '/income'));
|
|
await tester.pump();
|
|
final nav = tester.widget<TopNav>(find.byType(TopNav));
|
|
expect(navDestinations[nav.selectedIndex].label, 'Аналитика');
|
|
expect(find.byType(AnalyticsTabs), findsOneWidget);
|
|
for (final label in [
|
|
'Общее',
|
|
'Дивиденды',
|
|
'Ребалансировка',
|
|
'Цели',
|
|
'Налоги',
|
|
'Портфели',
|
|
]) {
|
|
expect(find.text(label), findsOneWidget, reason: label);
|
|
}
|
|
},
|
|
);
|
|
|
|
testWidgets('other sections have no analytics tab strip', (tester) async {
|
|
await tester.pumpWidget(wrap(1280, location: '/accounts'));
|
|
await tester.pump();
|
|
expect(find.byType(AnalyticsTabs), findsNothing);
|
|
});
|
|
|
|
testWidgets('the top bar shows the Здоровье issue count', (tester) async {
|
|
await tester.pumpWidget(
|
|
wrap(1280, issues: [_dataQualityRow(1), _dataQualityRow(2)]),
|
|
);
|
|
await tester.pump();
|
|
expect(find.text('2'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets(
|
|
'on a wide monitor the top bar content lines up with the page column',
|
|
(tester) async {
|
|
tester.view.physicalSize = const Size(2400, 900);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.reset);
|
|
await tester.pumpWidget(wrap(2400));
|
|
await tester.pump();
|
|
|
|
// the page column is 1440 wide and centred: it starts at 480 and ends at 1920; the cards
|
|
// inside have 16 px of padding, and the bar's content must sit on the same edges
|
|
final logo = tester.getTopLeft(find.byType(ServiceMark));
|
|
expect(
|
|
logo.dx,
|
|
480 + 16,
|
|
reason: 'the logo starts at the column edge plus the card padding',
|
|
);
|
|
expect(tester.getTopRight(find.byType(CircleAvatar)).dx, 1920 - 16);
|
|
},
|
|
);
|
|
|
|
testWidgets('on a normal window the top bar keeps the plain 16 px margin', (
|
|
tester,
|
|
) async {
|
|
tester.view.physicalSize = const Size(1200, 900);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.reset);
|
|
await tester.pumpWidget(wrap(1200));
|
|
await tester.pump();
|
|
|
|
expect(tester.getTopLeft(find.byType(ServiceMark)).dx, 16);
|
|
expect(tester.getTopRight(find.byType(CircleAvatar)).dx, 1200 - 16);
|
|
});
|
|
}
|