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

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);
});
}