304 lines
11 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|