feat(app): группированный сайдбар навигации на широком breakpoint
NavSidebar (>=1200): пункты меню сгруппированы под капс-заголовками, у активного пункта левая акцентная полоска, у «Здоровье» — счётчик замечаний качества данных из dataQualityProvider, внизу закреплён профиль (аватар-инициал + email из meProvider). Список направлений вынесен в nav_destinations.dart — общий для app_shell.dart и nav_sidebar.dart. На 600–1200 остаётся свёрнутый NavigationRail: в иконку-без-подписи заголовки групп и профиль всё равно не помещаются. NavSidebar смонтирован всё время сессии и держит dataQualityProvider и meProvider живыми — раньше эти запросы уходили только при открытии Обзора/Здоровья/Настроек.
This commit is contained in:
@@ -1,20 +1,57 @@
|
||||
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/features/home/providers.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/nav_sidebar.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 sidebar (>=1200) watches meProvider and dataQualityProvider for the profile
|
||||
// footer and the Здоровье counter — every wide-breakpoint pump needs both overridden.
|
||||
Widget wrap(double width, {String location = '/'}) {
|
||||
return MediaQuery(
|
||||
data: MediaQueryData(size: Size(width, 800)),
|
||||
child: MaterialApp(
|
||||
home: AppShell(location: location, child: Container()),
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
meProvider.overrideWith((ref) async => UserOut(email: 'ada@example.com', id: 1)),
|
||||
dataQualityProvider.overrideWith((ref) async => const Cached(<DataQualityRow>[])),
|
||||
],
|
||||
child: MediaQuery(
|
||||
data: MediaQueryData(size: Size(width, 800)),
|
||||
child: MaterialApp(
|
||||
home: AppShell(location: location, child: Container()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('shows a NavigationRail on a wide surface', (tester) async {
|
||||
testWidgets('shows the grouped sidebar on a wide surface', (tester) async {
|
||||
await tester.pumpWidget(wrap(1280));
|
||||
await tester.pump();
|
||||
expect(find.byType(NavSidebar), findsOneWidget);
|
||||
expect(find.byType(NavigationRail), findsNothing);
|
||||
expect(find.byType(NavigationBar), findsNothing);
|
||||
for (final label in navGroupLabels.values) {
|
||||
expect(find.text(label), findsOneWidget);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('shows a collapsed NavigationRail on a medium surface', (tester) async {
|
||||
await tester.pumpWidget(wrap(900));
|
||||
expect(find.byType(NavigationRail), findsOneWidget);
|
||||
expect(find.byType(NavSidebar), findsNothing);
|
||||
expect(find.byType(NavigationBar), findsNothing);
|
||||
});
|
||||
|
||||
@@ -22,10 +59,11 @@ void main() {
|
||||
await tester.pumpWidget(wrap(400));
|
||||
expect(find.byType(NavigationBar), findsOneWidget);
|
||||
expect(find.byType(NavigationRail), findsNothing);
|
||||
expect(find.byType(NavSidebar), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the resolve screen keeps Импорт selected', (tester) async {
|
||||
await tester.pumpWidget(wrap(1280, location: '/instruments/pending'));
|
||||
await tester.pumpWidget(wrap(900, location: '/instruments/pending'));
|
||||
final rail = tester.widget<NavigationRail>(find.byType(NavigationRail));
|
||||
expect(
|
||||
(rail.destinations[rail.selectedIndex!].label as Text).data,
|
||||
@@ -34,11 +72,40 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('a nested route keeps its own section selected', (tester) async {
|
||||
await tester.pumpWidget(wrap(1280, location: '/portfolio/instrument/311'));
|
||||
await tester.pumpWidget(wrap(900, location: '/portfolio/instrument/311'));
|
||||
final rail = tester.widget<NavigationRail>(find.byType(NavigationRail));
|
||||
expect(
|
||||
(rail.destinations[rail.selectedIndex!].label as Text).data,
|
||||
'Портфель',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('the sidebar keeps Импорт selected on the resolve screen', (tester) async {
|
||||
await tester.pumpWidget(wrap(1280, location: '/instruments/pending'));
|
||||
await tester.pump();
|
||||
final sidebar = tester.widget<NavSidebar>(find.byType(NavSidebar));
|
||||
expect(navDestinations[sidebar.selectedIndex].label, 'Импорт');
|
||||
});
|
||||
|
||||
testWidgets('the sidebar shows the Здоровье issue count', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
meProvider.overrideWith((ref) async => UserOut(email: 'ada@example.com', id: 1)),
|
||||
dataQualityProvider.overrideWith(
|
||||
(ref) async => Cached([
|
||||
_dataQualityRow(1),
|
||||
_dataQualityRow(2),
|
||||
]),
|
||||
),
|
||||
],
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(size: Size(1280, 800)),
|
||||
child: MaterialApp(home: AppShell(location: '/', child: Container())),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(find.text('2'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user