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