MotorSignal-App/test/main_layout_responsive_test...

304 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:motorcycle_dash/core/theme/theme.dart';
import 'package:motorcycle_dash/core/widgets/main_layout.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
Widget buildTestRouter({
required Size surfaceSize,
ThemeData? theme,
int initialIndex = 0,
void Function(int)? onIndexChanged,
}) {
final router = GoRouter(
initialLocation: switch (initialIndex) {
1 => '/map',
2 => '/activity',
3 => '/garage',
4 => '/social',
5 => '/settings',
_ => '/',
},
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return MainLayout(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const Scaffold(
body: Center(child: Text('Dash Screen Content')),
),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/map',
builder: (context, state) => const Scaffold(
body: Center(child: Text('Map Screen Content')),
),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/activity',
builder: (context, state) => const Scaffold(
body: Center(child: Text('Activity Screen Content')),
),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/garage',
builder: (context, state) => const Scaffold(
body: Center(child: Text('Garage Screen Content')),
),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/social',
builder: (context, state) => const Scaffold(
body: Center(child: Text('Social Screen Content')),
),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/settings',
builder: (context, state) => const Scaffold(
body: Center(child: Text('Settings Screen Content')),
),
),
],
),
],
),
],
);
return ProviderScope(
child: MaterialApp.router(
theme: theme ?? AppTheme.darkTheme,
routerConfig: router,
),
);
}
group('MainLayout Viewport Matrix Tests', () {
const testViewports = <String, Size>{
'320x640 (Narrow Portrait)': Size(320, 640),
'400x800 (Standard Portrait)': Size(400, 800),
'412x915 (Pixel 8a Portrait)': Size(412, 915),
'840x390 (Short Landscape)': Size(840, 390),
'780x360 (Short Landscape)': Size(780, 360),
'800x360 (Motorcycle Cockpit Mount)': Size(800, 360),
'600x900 (Medium Tablet)': Size(600, 900),
'840x600 (Medium Landscape)': Size(840, 600),
'1024x768 (Expanded Tablet)': Size(1024, 768),
};
for (final entry in testViewports.entries) {
testWidgets('Renders cleanly without overflow on ${entry.key}', (tester) async {
tester.view.physicalSize = entry.value;
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(buildTestRouter(surfaceSize: entry.value));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull, reason: 'No RenderFlex overflow on ${entry.key}');
expect(find.text('Dash Screen Content'), findsOneWidget);
// Verify all 6 navigation labels exist regardless of layout
expect(find.text('Dash'), findsOneWidget);
expect(find.text('Map'), findsOneWidget);
expect(find.text('Activity'), findsOneWidget);
expect(find.text('Garage'), findsOneWidget);
expect(find.text('Social'), findsOneWidget);
expect(find.text('Settings'), findsOneWidget);
});
}
});
group('MainLayout Navigation Presentation Adaptation', () {
testWidgets('Presents BottomNavigationBar on compact portrait (400x800)', (tester) async {
tester.view.physicalSize = const Size(400, 800);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(buildTestRouter(surfaceSize: const Size(400, 800)));
await tester.pumpAndSettle();
expect(find.byType(BottomNavigationBar), findsOneWidget);
expect(find.byType(NavigationRail), findsNothing);
expect(find.byKey(const ValueKey('nav_dash_icon')), findsOneWidget);
expect(find.byKey(const ValueKey('nav_map_icon')), findsOneWidget);
});
testWidgets('Presents NavigationRail on short landscape phone mount (800x360)', (tester) async {
tester.view.physicalSize = const Size(800, 360);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(buildTestRouter(surfaceSize: const Size(800, 360)));
await tester.pumpAndSettle();
expect(find.byType(NavigationRail), findsOneWidget);
expect(find.byType(BottomNavigationBar), findsNothing);
expect(find.byKey(const ValueKey('rail_dash_icon')), findsOneWidget);
expect(find.byKey(const ValueKey('rail_map_icon')), findsOneWidget);
});
testWidgets('Presents NavigationRail on medium width (600x900)', (tester) async {
tester.view.physicalSize = const Size(600, 900);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(buildTestRouter(surfaceSize: const Size(600, 900)));
await tester.pumpAndSettle();
expect(find.byType(NavigationRail), findsOneWidget);
expect(find.byType(BottomNavigationBar), findsNothing);
});
});
group('MainLayout Navigation Interactions & Touch Targets', () {
testWidgets('Tapping destinations switches routes cleanly on BottomNavigationBar', (tester) async {
tester.view.physicalSize = const Size(400, 800);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(buildTestRouter(surfaceSize: const Size(400, 800)));
await tester.pumpAndSettle();
expect(find.text('Dash Screen Content'), findsOneWidget);
await tester.tap(find.text('Garage'));
await tester.pumpAndSettle();
expect(find.text('Garage Screen Content'), findsOneWidget);
await tester.tap(find.text('Settings'));
await tester.pumpAndSettle();
expect(find.text('Settings Screen Content'), findsOneWidget);
});
testWidgets('Tapping destinations switches routes cleanly on NavigationRail', (tester) async {
tester.view.physicalSize = const Size(800, 360);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(buildTestRouter(surfaceSize: const Size(800, 360)));
await tester.pumpAndSettle();
expect(find.text('Dash Screen Content'), findsOneWidget);
await tester.tap(find.text('Map'));
await tester.pumpAndSettle();
expect(find.text('Map Screen Content'), findsOneWidget);
await tester.tap(find.text('Activity'));
await tester.pumpAndSettle();
expect(find.text('Activity Screen Content'), findsOneWidget);
});
testWidgets('NavigationRail touch targets have adequate interactive hit bounds', (tester) async {
tester.view.physicalSize = const Size(800, 360);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(buildTestRouter(surfaceSize: const Size(800, 360)));
await tester.pumpAndSettle();
final railFinder = find.byType(NavigationRail);
expect(railFinder, findsOneWidget);
final railSize = tester.getSize(railFinder);
expect(railSize.width, greaterThanOrEqualTo(48.0));
});
});
group('MainLayout State & Theme Preservation Across Resize', () {
testWidgets('Preserves selected route and active content across viewport resize', (tester) async {
// Start in portrait (400x800)
tester.view.physicalSize = const Size(400, 800);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(buildTestRouter(surfaceSize: const Size(400, 800)));
await tester.pumpAndSettle();
// Navigate to Activity
await tester.tap(find.text('Activity'));
await tester.pumpAndSettle();
expect(find.text('Activity Screen Content'), findsOneWidget);
// Rotate / Resize to short landscape (800x360)
tester.view.physicalSize = const Size(800, 360);
await tester.pumpAndSettle();
// Shell adapts to NavigationRail, while maintaining Activity selection!
expect(find.byType(NavigationRail), findsOneWidget);
expect(find.text('Activity Screen Content'), findsOneWidget);
// Resize to large tablet (1024x768)
tester.view.physicalSize = const Size(1024, 768);
await tester.pumpAndSettle();
expect(find.text('Activity Screen Content'), findsOneWidget);
// Resize back to portrait (320x640)
tester.view.physicalSize = const Size(320, 640);
await tester.pumpAndSettle();
expect(find.byType(BottomNavigationBar), findsOneWidget);
expect(find.text('Activity Screen Content'), findsOneWidget);
});
testWidgets('Renders cleanly under Light Theme without visual regressions', (tester) async {
tester.view.physicalSize = const Size(400, 800);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
buildTestRouter(
surfaceSize: const Size(400, 800),
theme: AppTheme.lightTheme,
),
);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
expect(find.text('Dash Screen Content'), findsOneWidget);
});
});
}