1169 lines
46 KiB
Dart
1169 lines
46 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:drift/native.dart';
|
|
import 'package:geolocator/geolocator.dart';
|
|
import 'package:drift/drift.dart' as drift;
|
|
|
|
import 'package:motorcycle_dash/core/router.dart';
|
|
import 'package:motorcycle_dash/core/theme/theme.dart';
|
|
import 'package:motorcycle_dash/core/widgets/main_layout.dart';
|
|
import 'package:motorcycle_dash/database/database.dart' hide FieldTestEvent, FieldTestSummary, TelemetrySample;
|
|
import 'package:motorcycle_dash/models/field_test_event.dart';
|
|
import 'package:motorcycle_dash/features/dashboard/dashboard_screen.dart';
|
|
import 'package:motorcycle_dash/features/map/map_screen.dart';
|
|
import 'package:motorcycle_dash/features/history/history_screen.dart';
|
|
import 'package:motorcycle_dash/features/history/ride_detail_screen.dart';
|
|
import 'package:motorcycle_dash/features/records/records_screen.dart';
|
|
import 'package:motorcycle_dash/features/records/records_service.dart';
|
|
import 'package:motorcycle_dash/features/garage/garage_screen.dart';
|
|
import 'package:motorcycle_dash/features/garage/add_motorcycle_screen.dart';
|
|
import 'package:motorcycle_dash/features/garage/motorcycle_detail_screen.dart';
|
|
import 'package:motorcycle_dash/features/garage/garage_providers.dart';
|
|
import 'package:motorcycle_dash/features/settings/settings_screen.dart';
|
|
import 'package:motorcycle_dash/features/settings/account_screen.dart';
|
|
import 'package:motorcycle_dash/features/entitlements/pro_screen.dart';
|
|
import 'package:motorcycle_dash/features/social/rider_profile_screen.dart';
|
|
import 'package:motorcycle_dash/features/social/friends_screen.dart';
|
|
import 'package:motorcycle_dash/features/settings/diagnostics_screen.dart';
|
|
import 'package:motorcycle_dash/features/settings/field_test_live_log_screen.dart';
|
|
import 'package:motorcycle_dash/features/settings/field_test_report_screen.dart';
|
|
import 'package:motorcycle_dash/features/field_test/field_test_pre_ride_check_screen.dart';
|
|
import 'package:motorcycle_dash/features/group_rides/group_rides_hub_screen.dart';
|
|
import 'package:motorcycle_dash/features/group_rides/create_group_ride_screen.dart';
|
|
import 'package:motorcycle_dash/features/group_rides/group_ride_detail_screen.dart';
|
|
import 'package:motorcycle_dash/features/group_rides/live_group_ride_screen.dart';
|
|
import 'package:motorcycle_dash/features/routes/route_planner_screen.dart';
|
|
import 'package:motorcycle_dash/features/routes/route_detail_screen.dart';
|
|
|
|
import 'package:motorcycle_dash/features/rides/ride_state_machine.dart';
|
|
import 'package:motorcycle_dash/models/motorcycle_models.dart';
|
|
import 'package:motorcycle_dash/services/alerts/road_alert_service.dart';
|
|
import 'package:motorcycle_dash/services/motorcycle/motorcycle_repository.dart';
|
|
import 'package:motorcycle_dash/services/auth/auth_providers.dart';
|
|
import 'package:motorcycle_dash/services/auth/auth_service.dart';
|
|
import 'package:motorcycle_dash/services/auth/auth_state.dart';
|
|
import 'package:motorcycle_dash/services/auth/auth_user.dart';
|
|
import 'package:motorcycle_dash/services/billing/billing_providers.dart';
|
|
import 'package:motorcycle_dash/services/billing/fake_billing_service.dart';
|
|
import 'package:motorcycle_dash/services/entitlements/entitlement_providers.dart';
|
|
import 'package:motorcycle_dash/services/entitlements/entitlement_storage.dart';
|
|
import 'package:motorcycle_dash/services/field_test/field_test_logger.dart';
|
|
import 'package:motorcycle_dash/services/location_service.dart';
|
|
import 'package:motorcycle_dash/services/group_rides/group_ride_models.dart';
|
|
import 'package:motorcycle_dash/services/group_rides/group_ride_providers.dart';
|
|
import 'package:motorcycle_dash/services/group_rides/group_ride_service.dart';
|
|
import 'package:motorcycle_dash/services/routes/route_models.dart';
|
|
import 'package:motorcycle_dash/services/routes/route_providers.dart';
|
|
import 'package:motorcycle_dash/services/routes/route_service.dart';
|
|
import 'package:motorcycle_dash/services/social/rider_profile_models.dart';
|
|
import 'package:motorcycle_dash/services/social/social_providers.dart';
|
|
import 'package:motorcycle_dash/services/map/map_providers.dart';
|
|
import 'package:motorcycle_dash/services/map/map_provider_interface.dart';
|
|
|
|
// Canonical 9-viewport regression matrix definitions
|
|
const Map<String, Size> canonicalViewports = {
|
|
'compact_portrait_320x640': Size(320, 640),
|
|
'standard_portrait_400x800': Size(400, 800),
|
|
'pixel7_portrait_412x915': Size(412, 915),
|
|
'compact_landscape_640x320': Size(640, 320),
|
|
'short_landscape_780x360': Size(780, 360),
|
|
'standard_landscape_800x360': Size(800, 360),
|
|
'wide_landscape_840x390': Size(840, 390),
|
|
'portrait_tablet_768x1024': Size(768, 1024),
|
|
'landscape_tablet_1024x768': Size(1024, 768),
|
|
};
|
|
|
|
// IME virtual keyboard inset viewports
|
|
const Map<String, Size> imeViewports = {
|
|
'compact_portrait_ime': Size(320, 640),
|
|
'standard_portrait_ime': Size(400, 800),
|
|
'compact_landscape_ime': Size(640, 320),
|
|
};
|
|
|
|
class _FakeAuth implements AuthService {
|
|
AuthUser? user;
|
|
final _states = StreamController<AuthState>.broadcast();
|
|
|
|
_FakeAuth({this.user});
|
|
|
|
@override
|
|
Stream<AuthState> get authStateChanges => _states.stream;
|
|
|
|
@override
|
|
AuthState get currentState => user == null
|
|
? const AuthState.unauthenticated(isFirebaseConfigured: true)
|
|
: (user!.isAnonymous
|
|
? AuthState.anonymous(user!, isFirebaseConfigured: true)
|
|
: AuthState.authenticated(user!, isFirebaseConfigured: true));
|
|
|
|
@override
|
|
AuthUser? get currentUser => user;
|
|
|
|
@override
|
|
bool get isConfigured => true;
|
|
|
|
@override
|
|
bool get isEmailVerified => user?.emailVerified ?? false;
|
|
|
|
@override
|
|
Future<void> deleteCurrentUser() async {
|
|
user = null;
|
|
_states.add(currentState);
|
|
}
|
|
|
|
@override
|
|
void dispose() => _states.close();
|
|
|
|
@override
|
|
Future<String?> getIdToken([bool forceRefresh = false]) async => 'fake-token';
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
}
|
|
|
|
class _FakeLocationService extends ChangeNotifier implements LocationService {
|
|
final StreamController<Position> _controller = StreamController<Position>.broadcast();
|
|
|
|
@override
|
|
Stream<Position> getFilteredPositionStream() => _controller.stream;
|
|
|
|
@override
|
|
Future<bool> requestPermissions() async => true;
|
|
|
|
@override
|
|
Future<bool> hasPermissions() async => true;
|
|
|
|
@override
|
|
void Function(Position, bool, String?)? onGpsDiagnostic;
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.close();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
}
|
|
|
|
class _FakeMotorcycleRepository implements MotorcycleRepository {
|
|
@override
|
|
Future<List<MotorcycleYear>> getYears() async => [MotorcycleYear(2024)];
|
|
|
|
@override
|
|
Future<List<MotorcycleMake>> getMakes(int year) async => [
|
|
MotorcycleMake(id: 'triumph', name: 'Triumph'),
|
|
];
|
|
|
|
@override
|
|
Future<List<MotorcycleModel>> getModels(int year, String makeId) async => [
|
|
MotorcycleModel(
|
|
id: 'speed-triple',
|
|
name: 'Speed Triple 1200 RS',
|
|
year: 2024,
|
|
makeId: 'triumph',
|
|
),
|
|
];
|
|
|
|
@override
|
|
Future<List<MotorcycleVariant>> getVariants(int year, String makeId, String modelId) async => [];
|
|
|
|
@override
|
|
Future<MotorcycleSpecification?> getSpecifications(MotorcycleSelection selection) async => null;
|
|
}
|
|
|
|
class _FakeFieldTestLogger implements FieldTestLogger {
|
|
@override
|
|
int? get activeRideId => null;
|
|
@override
|
|
bool get isSessionActive => false;
|
|
@override
|
|
DateTime? get sessionStartTime => null;
|
|
@override
|
|
double get currentQualityEstimate => 100.0;
|
|
@override
|
|
double get currentSampleRateHz => 20.0;
|
|
@override
|
|
double get lastGpsAccuracy => 2.5;
|
|
@override
|
|
int get lastGpsAgeMs => 50;
|
|
@override
|
|
double get averageTimingJitterMs => 1.2;
|
|
@override
|
|
int get currentQueueDepth => 0;
|
|
@override
|
|
int get peakQueueDepth => 0;
|
|
@override
|
|
int get queueCapacity => 1000;
|
|
@override
|
|
int get totalEnqueued => 0;
|
|
@override
|
|
int get totalFlushed => 0;
|
|
@override
|
|
int get droppedEventsCount => 0;
|
|
@override
|
|
String? get oldestDroppedItem => null;
|
|
@override
|
|
String? get newestDroppedItem => null;
|
|
@override
|
|
int get lastFlushDurationMs => 0;
|
|
@override
|
|
int get failedFlushCount => 0;
|
|
int get telemetryGapCount => 0;
|
|
int get gpsStaleCount => 0;
|
|
int get gpsJumpRejectCount => 0;
|
|
int get imuSaturationCount => 0;
|
|
int get accelerationEventCount => 0;
|
|
int get brakingEventCount => 0;
|
|
@override
|
|
List<FieldTestEvent> get timelineEvents => const [];
|
|
@override
|
|
Stream<List<FieldTestEvent>> get timelineStream => const Stream.empty();
|
|
@override
|
|
void dispose() {}
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
}
|
|
|
|
class _FakeGroupRideService implements GroupRideService {
|
|
@override
|
|
Stream<GroupRide?> groupRideStream(String groupRideId) => watchGroupRide(groupRideId);
|
|
|
|
@override
|
|
Stream<List<GroupRideParticipant>> participantsStream(String rideId) => watchParticipants(rideId);
|
|
|
|
Stream<GroupRide?> watchGroupRide(String rideId) {
|
|
return Stream.value(GroupRide(
|
|
id: rideId,
|
|
organizerUid: 'user-1',
|
|
organizerHandle: 'canyon_lead',
|
|
organizerDisplayName: 'Canyon Lead',
|
|
title: 'Weekend Mountain Ride',
|
|
description: 'Twisty canyon roads and scenic outlooks',
|
|
meetingPoint: const GroupRideLocation(
|
|
latitude: 37.0,
|
|
longitude: -122.0,
|
|
displayName: 'Mountain Viewpoint',
|
|
locationType: GroupRideLocationType.meetup,
|
|
),
|
|
scheduledStartAt: DateTime.now().add(const Duration(hours: 2)),
|
|
departureAt: DateTime.now().add(const Duration(hours: 2, minutes: 30)),
|
|
visibility: GroupRideVisibility.public,
|
|
joinPolicy: GroupRideJoinPolicy.open,
|
|
status: GroupRideStatus.scheduled,
|
|
participantCount: 3,
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
));
|
|
}
|
|
|
|
Stream<List<GroupRide>> watchUpcomingRides() {
|
|
return Stream.value([
|
|
GroupRide(
|
|
id: 'ride-1',
|
|
organizerUid: 'user-1',
|
|
organizerHandle: 'canyon_lead',
|
|
organizerDisplayName: 'Canyon Lead',
|
|
title: 'Morning Cruise',
|
|
description: 'Casual morning cruise through the hills',
|
|
meetingPoint: const GroupRideLocation(
|
|
latitude: 37.0,
|
|
longitude: -122.0,
|
|
displayName: 'Basecamp',
|
|
locationType: GroupRideLocationType.meetup,
|
|
),
|
|
scheduledStartAt: DateTime.now().add(const Duration(hours: 4)),
|
|
departureAt: DateTime.now().add(const Duration(hours: 4, minutes: 30)),
|
|
visibility: GroupRideVisibility.public,
|
|
joinPolicy: GroupRideJoinPolicy.open,
|
|
status: GroupRideStatus.scheduled,
|
|
participantCount: 4,
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
]);
|
|
}
|
|
|
|
Stream<List<GroupRide>> watchActiveRides() => Stream.value([]);
|
|
Stream<List<GroupRide>> watchMyRides() => Stream.value([]);
|
|
Stream<List<GroupRideParticipant>> watchParticipants(String rideId) => Stream.value([
|
|
GroupRideParticipant(
|
|
userId: 'user-1',
|
|
riderHandle: 'lead_rider',
|
|
riderDisplayName: 'Leader Rider',
|
|
role: GroupRideRole.lead,
|
|
status: ParticipantStatus.accepted,
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
]);
|
|
|
|
@override
|
|
Stream<List<LiveGroupPresence>> livePresenceStream(String rideId) => Stream.value([]);
|
|
|
|
@override
|
|
Future<void> clearLivePresence({
|
|
required String callerUid,
|
|
required String groupRideId,
|
|
}) async {}
|
|
|
|
Future<void> updateLivePresence({
|
|
required String callerUid,
|
|
required String groupRideId,
|
|
required LiveGroupPresence presence,
|
|
}) async {}
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
}
|
|
|
|
class _FakeRouteService implements RouteService {
|
|
final Map<String, MotorSignalRoute> _routes = {
|
|
'route-1': MotorSignalRoute(
|
|
id: 'route-1',
|
|
ownerUid: 'user-1',
|
|
ownerHandle: 'testrider',
|
|
title: 'Pacific Coast Highway',
|
|
description: 'Scenic ocean run',
|
|
visibility: RouteVisibility.public,
|
|
stops: const [
|
|
RouteStop(
|
|
id: 's1',
|
|
order: 0,
|
|
type: RouteStopType.start,
|
|
displayName: 'Santa Monica Pier',
|
|
latitude: 34.01,
|
|
longitude: -118.49,
|
|
address: 'Santa Monica, CA',
|
|
notes: 'Meet at pier entrance',
|
|
),
|
|
RouteStop(
|
|
id: 's2',
|
|
order: 1,
|
|
type: RouteStopType.destination,
|
|
displayName: 'Malibu Overlook',
|
|
latitude: 34.03,
|
|
longitude: -118.68,
|
|
address: 'Malibu, CA',
|
|
notes: 'End of ride photo spot',
|
|
),
|
|
],
|
|
straightLineDistanceMeters: 25000,
|
|
source: RouteSource.planner,
|
|
version: 1,
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
};
|
|
|
|
@override
|
|
Future<MotorSignalRoute?> getRoute(String routeId) async => _routes[routeId];
|
|
Stream<MotorSignalRoute?> watchRoute(String routeId) => Stream.value(_routes[routeId]);
|
|
@override
|
|
Stream<MotorSignalRoute?> routeStream(String routeId) => watchRoute(routeId);
|
|
Stream<List<MotorSignalRoute>> watchMyRoutes() => Stream.value(_routes.values.toList());
|
|
Stream<List<MotorSignalRoute>> watchPublicRoutes() => Stream.value(_routes.values.toList());
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
}
|
|
|
|
class _FakeRideStateMachine extends StateNotifier<RideSession>
|
|
implements RideStateMachine {
|
|
_FakeRideStateMachine(super.initialState);
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
}
|
|
|
|
class _FakeMapProvider implements AppMapProvider {
|
|
@override
|
|
AppMapType get type => AppMapType.openStreetMap;
|
|
@override
|
|
String get name => 'FakeMap';
|
|
|
|
@override
|
|
Widget buildMapView({
|
|
required BuildContext context,
|
|
required AppLatLng center,
|
|
required double zoom,
|
|
required List<AppMapMarker> markers,
|
|
required List<AppPolyline> polylines,
|
|
required bool isFollowMode,
|
|
required MapConfig config,
|
|
required ValueChanged<AppLatLng>? onPositionChanged,
|
|
required VoidCallback? onUserInteraction,
|
|
}) {
|
|
return Container(
|
|
key: const ValueKey('fake_map_view'),
|
|
color: Colors.black,
|
|
child: const Center(child: Text('MAP_VIEW')),
|
|
);
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
late AppDatabase db;
|
|
late SharedPreferences prefs;
|
|
late _FakeAuth authService;
|
|
late FakeBillingService billingService;
|
|
late InMemoryEntitlementStorage entitlementStorage;
|
|
late _FakeGroupRideService groupRideService;
|
|
late _FakeRouteService routeService;
|
|
late _FakeLocationService locationService;
|
|
late _FakeMotorcycleRepository motorcycleRepo;
|
|
late _FakeFieldTestLogger fieldTestLogger;
|
|
|
|
setUp(() async {
|
|
db = AppDatabase.forTesting(NativeDatabase.memory());
|
|
SharedPreferences.setMockInitialValues({
|
|
'app_theme_mode': 'dark',
|
|
});
|
|
prefs = await SharedPreferences.getInstance();
|
|
|
|
authService = _FakeAuth(
|
|
user: const AuthUser(
|
|
uid: 'test-user-123',
|
|
email: 'rider@example.com',
|
|
displayName: 'Test Rider',
|
|
),
|
|
);
|
|
billingService = FakeBillingService();
|
|
entitlementStorage = InMemoryEntitlementStorage();
|
|
groupRideService = _FakeGroupRideService();
|
|
routeService = _FakeRouteService();
|
|
locationService = _FakeLocationService();
|
|
motorcycleRepo = _FakeMotorcycleRepository();
|
|
fieldTestLogger = _FakeFieldTestLogger();
|
|
|
|
// Populate a test motorcycle in Drift DB
|
|
await db.into(db.garageMotorcycles).insert(
|
|
GarageMotorcyclesCompanion.insert(
|
|
year: 2024,
|
|
makeId: 'triumph',
|
|
makeName: 'Triumph',
|
|
modelId: 'speed-triple',
|
|
modelName: 'Speed Triple 1200 RS',
|
|
dateAdded: DateTime.now(),
|
|
lastUpdated: DateTime.now(),
|
|
isDefault: const drift.Value(true),
|
|
),
|
|
);
|
|
|
|
// Populate a completed test ride in Drift DB
|
|
await db.into(db.rides).insert(
|
|
RidesCompanion.insert(
|
|
startTime: DateTime.now().subtract(const Duration(hours: 1)),
|
|
endTime: drift.Value(DateTime.now().subtract(const Duration(minutes: 10))),
|
|
distanceMiles: const drift.Value(24.5),
|
|
movingTime: const drift.Value(1800),
|
|
topSpeedMph: const drift.Value(72.4),
|
|
maxLeanLeft: const drift.Value(42.1),
|
|
maxLeanRight: const drift.Value(44.8),
|
|
maxBrakingG: const drift.Value(0.88),
|
|
maxAccelG: const drift.Value(0.65),
|
|
),
|
|
);
|
|
});
|
|
|
|
tearDown(() async {
|
|
locationService.dispose();
|
|
authService.dispose();
|
|
await db.close();
|
|
});
|
|
|
|
Widget buildTestApp({
|
|
required Widget child,
|
|
ThemeMode themeMode = ThemeMode.dark,
|
|
AuthUser? customUser,
|
|
}) {
|
|
final activeAuth = customUser != null ? _FakeAuth(user: customUser) : authService;
|
|
|
|
return ProviderScope(
|
|
overrides: [
|
|
databaseProvider.overrideWithValue(db),
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(activeAuth),
|
|
billingServiceProvider.overrideWithValue(billingService),
|
|
entitlementStorageProvider.overrideWithValue(entitlementStorage),
|
|
groupRideServiceProvider.overrideWithValue(groupRideService),
|
|
routeServiceProvider.overrideWithValue(routeService),
|
|
activeMapProvider.overrideWithValue(_FakeMapProvider()),
|
|
locationServiceProvider.overrideWithValue(locationService),
|
|
motorcycleRepositoryProvider.overrideWithValue(motorcycleRepo),
|
|
fieldTestLoggerProvider.overrideWithValue(fieldTestLogger),
|
|
activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])),
|
|
locationPermissionGrantedProvider.overrideWith((ref) => Future.value(true)),
|
|
myRiderProfileProvider.overrideWith((ref) => Stream.value(
|
|
RiderProfile(
|
|
uid: 'user-1',
|
|
handle: 'testrider',
|
|
displayName: 'Test Rider',
|
|
bio: 'Motorcycle enthusiast',
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
)),
|
|
myFriendsProvider.overrideWith((ref) => Stream.value([])),
|
|
myIncomingRequestsProvider.overrideWith((ref) => Stream.value([])),
|
|
myOutgoingRequestsProvider.overrideWith((ref) => Stream.value([])),
|
|
],
|
|
child: MaterialApp(
|
|
debugShowCheckedModeBanner: false,
|
|
theme: AppTheme.lightTheme,
|
|
darkTheme: AppTheme.darkTheme,
|
|
themeMode: themeMode,
|
|
home: Scaffold(body: child),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> pumpView(WidgetTester tester, Widget app) async {
|
|
await tester.pumpWidget(app);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
}
|
|
|
|
Future<void> unmountView(WidgetTester tester) async {
|
|
await tester.pumpWidget(const SizedBox());
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
}
|
|
|
|
// =========================================================================
|
|
// 1. WHOLE-APP CANONICAL 9-VIEWPORT RESPONSIVE MATRIX
|
|
// =========================================================================
|
|
group('UI-2I Whole-App 9-Viewport Responsive Matrix', () {
|
|
testWidgets('DashboardScreen renders cleanly across all 9 canonical viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const DashboardScreen()));
|
|
|
|
expect(find.byType(DashboardScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('MapScreen renders cleanly across all 9 canonical viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const MapScreen()));
|
|
|
|
expect(find.byType(MapScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('HistoryScreen renders cleanly across all 9 canonical viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const HistoryScreen()));
|
|
|
|
expect(find.byType(HistoryScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('RecordsScreen renders cleanly across all 9 canonical viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const RecordsScreen()));
|
|
|
|
expect(find.byType(RecordsScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('GarageScreen renders cleanly across all 9 canonical viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const GarageScreen()));
|
|
|
|
expect(find.byType(GarageScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('SettingsScreen renders cleanly across all 9 canonical viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const SettingsScreen()));
|
|
|
|
expect(find.byType(SettingsScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('AccountScreen (authenticated) renders cleanly across all 9 viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const AccountScreen()));
|
|
|
|
expect(find.byType(AccountScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('AccountScreen (unauthenticated) renders cleanly across all 9 viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const AccountScreen(), customUser: null));
|
|
|
|
expect(find.byType(AccountScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('ProScreen renders cleanly across all 9 viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const ProScreen()));
|
|
|
|
expect(find.byType(ProScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('DiagnosticsScreen renders cleanly across all 9 viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const DiagnosticsScreen()));
|
|
|
|
expect(find.byType(DiagnosticsScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('FieldTestPreRideCheckScreen renders cleanly across all 9 viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const FieldTestPreRideCheckScreen()));
|
|
|
|
expect(find.byType(FieldTestPreRideCheckScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('FieldTestLiveLogScreen renders cleanly across all 9 viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const FieldTestLiveLogScreen()));
|
|
|
|
expect(find.byType(FieldTestLiveLogScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('FieldTestReportScreen renders cleanly across all 9 viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const FieldTestReportScreen()));
|
|
|
|
expect(find.byType(FieldTestReportScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('GroupRidesHubScreen renders cleanly across all 9 viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const GroupRidesHubScreen()));
|
|
|
|
expect(find.byType(GroupRidesHubScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('CreateGroupRideScreen renders cleanly across all 9 viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const CreateGroupRideScreen()));
|
|
|
|
expect(find.byType(CreateGroupRideScreen), findsOneWidget, reason: 'Failed on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('GroupRideDetailScreen & LiveGroupRideScreen render cleanly across all 9 viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const GroupRideDetailScreen(rideId: 'ride-1')));
|
|
expect(find.byType(GroupRideDetailScreen), findsOneWidget, reason: 'Failed Detail on ${entry.key}');
|
|
|
|
await pumpView(tester, buildTestApp(child: const LiveGroupRideScreen(rideId: 'ride-1')));
|
|
expect(find.byType(LiveGroupRideScreen), findsOneWidget, reason: 'Failed Live on ${entry.key}');
|
|
expect(tester.takeException(), isNull);
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('RoutePlannerScreen & RouteDetailScreen render cleanly across all 9 viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const RoutePlannerScreen()));
|
|
expect(find.byType(RoutePlannerScreen), findsOneWidget, reason: 'Failed Planner on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on RoutePlanner on ${entry.key}');
|
|
|
|
await pumpView(tester, buildTestApp(child: const RouteDetailScreen(routeId: 'route-1')));
|
|
expect(find.byType(RouteDetailScreen), findsOneWidget, reason: 'Failed RouteDetail on ${entry.key}');
|
|
expect(tester.takeException(), isNull, reason: 'Exception on RouteDetail on ${entry.key}');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('AddMotorcycleScreen & MotorcycleDetailScreen render cleanly across all 9 viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const AddMotorcycleScreen()));
|
|
expect(find.byType(AddMotorcycleScreen), findsOneWidget, reason: 'Failed AddMotorcycle on ${entry.key}');
|
|
|
|
await pumpView(tester, buildTestApp(child: const MotorcycleDetailScreen(motorcycleId: 1)));
|
|
expect(find.byType(MotorcycleDetailScreen), findsOneWidget, reason: 'Failed MotorcycleDetail on ${entry.key}');
|
|
expect(tester.takeException(), isNull);
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('RiderProfileScreen & FriendsScreen render cleanly across all 9 viewports', (tester) async {
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: const RiderProfileScreen()));
|
|
expect(find.byType(RiderProfileScreen), findsOneWidget, reason: 'Failed RiderProfile on ${entry.key}');
|
|
|
|
await pumpView(tester, buildTestApp(child: const FriendsScreen()));
|
|
expect(find.byType(FriendsScreen), findsOneWidget, reason: 'Failed FriendsScreen on ${entry.key}');
|
|
expect(tester.takeException(), isNull);
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('RideDetailScreen renders cleanly across all 9 viewports', (tester) async {
|
|
final ride = Ride(
|
|
id: 1,
|
|
startTime: DateTime.now().subtract(const Duration(hours: 1)),
|
|
endTime: DateTime.now().subtract(const Duration(minutes: 10)),
|
|
distanceMiles: 24.5,
|
|
movingTime: 1800,
|
|
topSpeedMph: 72.4,
|
|
maxLeanLeft: 42.1,
|
|
maxLeanRight: 44.8,
|
|
maxBrakingG: 0.88,
|
|
maxAccelG: 0.65,
|
|
distance: 24.5,
|
|
stoppedTime: 100,
|
|
averageSpeed: 48.0,
|
|
topSpeed: 72.4,
|
|
elevationGain: 350.0,
|
|
elevationLoss: 320.0,
|
|
);
|
|
|
|
for (final entry in canonicalViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(tester, buildTestApp(child: RideDetailScreen(ride: ride)));
|
|
|
|
expect(find.byType(RideDetailScreen), findsOneWidget, reason: 'Failed RideDetail on ${entry.key}');
|
|
expect(tester.takeException(), isNull);
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 2. IME KEYBOARD INSET RESILIENCY MATRIX
|
|
// =========================================================================
|
|
group('UI-2I IME Keyboard-Sensitive Form Resiliency Matrix', () {
|
|
testWidgets('AddMotorcycleScreen remains scrollable and submit reachable with virtual keyboard', (tester) async {
|
|
for (final entry in imeViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
tester.view.viewPadding = const FakeViewPadding(bottom: 300);
|
|
addTearDown(() {
|
|
tester.view.resetPhysicalSize();
|
|
tester.view.resetViewPadding();
|
|
});
|
|
|
|
await pumpView(tester, buildTestApp(child: const AddMotorcycleScreen()));
|
|
|
|
expect(find.byType(AddMotorcycleScreen), findsOneWidget);
|
|
expect(tester.takeException(), isNull, reason: 'Overflow on ${entry.key} with keyboard');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('CreateGroupRideScreen remains scrollable with virtual keyboard', (tester) async {
|
|
for (final entry in imeViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
tester.view.viewPadding = const FakeViewPadding(bottom: 300);
|
|
addTearDown(() {
|
|
tester.view.resetPhysicalSize();
|
|
tester.view.resetViewPadding();
|
|
});
|
|
|
|
await pumpView(tester, buildTestApp(child: const CreateGroupRideScreen()));
|
|
|
|
expect(find.byType(CreateGroupRideScreen), findsOneWidget);
|
|
expect(tester.takeException(), isNull, reason: 'Overflow on ${entry.key} with keyboard');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('AccountScreen auth form remains scrollable with virtual keyboard', (tester) async {
|
|
for (final entry in imeViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
tester.view.viewPadding = const FakeViewPadding(bottom: 300);
|
|
addTearDown(() {
|
|
tester.view.resetPhysicalSize();
|
|
tester.view.resetViewPadding();
|
|
});
|
|
|
|
await pumpView(tester, buildTestApp(
|
|
child: const AccountScreen(),
|
|
customUser: null,
|
|
));
|
|
|
|
expect(find.byType(AccountScreen), findsOneWidget);
|
|
expect(tester.takeException(), isNull, reason: 'Overflow on ${entry.key} with keyboard');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('RoutePlannerScreen search & stop input remains reachable with virtual keyboard', (tester) async {
|
|
for (final entry in imeViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
tester.view.viewPadding = const FakeViewPadding(bottom: 300);
|
|
addTearDown(() {
|
|
tester.view.resetPhysicalSize();
|
|
tester.view.resetViewPadding();
|
|
});
|
|
|
|
await pumpView(tester, buildTestApp(child: const RoutePlannerScreen()));
|
|
|
|
expect(find.byType(RoutePlannerScreen), findsOneWidget);
|
|
expect(tester.takeException(), isNull, reason: 'Overflow on ${entry.key} with keyboard');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('RiderProfileScreen handle & bio inputs remain reachable with virtual keyboard', (tester) async {
|
|
for (final entry in imeViewports.entries) {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
tester.view.viewPadding = const FakeViewPadding(bottom: 300);
|
|
addTearDown(() {
|
|
tester.view.resetPhysicalSize();
|
|
tester.view.resetViewPadding();
|
|
});
|
|
|
|
await pumpView(tester, buildTestApp(child: const RiderProfileScreen()));
|
|
|
|
expect(find.byType(RiderProfileScreen), findsOneWidget);
|
|
expect(tester.takeException(), isNull, reason: 'Overflow on ${entry.key} with keyboard');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 3. THEME SWITCHING & STATE PRESERVATION MATRIX
|
|
// =========================================================================
|
|
group('UI-2I Theme Switching & State Preservation Matrix', () {
|
|
testWidgets('Seamless Theme Transition Dark -> Light -> System -> Dark preserves screen state', (tester) async {
|
|
tester.view.physicalSize = const Size(400, 800);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
final themeModes = [ThemeMode.dark, ThemeMode.light, ThemeMode.system, ThemeMode.dark];
|
|
|
|
for (final mode in themeModes) {
|
|
await pumpView(tester, buildTestApp(
|
|
child: const DashboardScreen(),
|
|
themeMode: mode,
|
|
));
|
|
|
|
expect(find.byType(DashboardScreen), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('MainLayout navigation shell remains intact when resizing across breakpoints', (tester) async {
|
|
final sizes = [
|
|
const Size(320, 640), // Compact
|
|
const Size(640, 320), // Compact Landscape
|
|
const Size(768, 1024), // Medium Tablet
|
|
const Size(1024, 768), // Expanded Tablet
|
|
const Size(400, 800), // Standard Phone
|
|
];
|
|
|
|
for (final size in sizes) {
|
|
tester.view.physicalSize = size;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await pumpView(
|
|
tester,
|
|
ProviderScope(
|
|
overrides: [
|
|
databaseProvider.overrideWithValue(db),
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(authService),
|
|
billingServiceProvider.overrideWithValue(billingService),
|
|
entitlementStorageProvider.overrideWithValue(entitlementStorage),
|
|
groupRideServiceProvider.overrideWithValue(groupRideService),
|
|
routeServiceProvider.overrideWithValue(routeService),
|
|
activeMapProvider.overrideWithValue(_FakeMapProvider()),
|
|
locationServiceProvider.overrideWithValue(locationService),
|
|
motorcycleRepositoryProvider.overrideWithValue(motorcycleRepo),
|
|
fieldTestLoggerProvider.overrideWithValue(fieldTestLogger),
|
|
activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])),
|
|
locationPermissionGrantedProvider.overrideWith((ref) => Future.value(true)),
|
|
],
|
|
child: MaterialApp.router(
|
|
routerConfig: appRouter,
|
|
theme: AppTheme.lightTheme,
|
|
darkTheme: AppTheme.darkTheme,
|
|
themeMode: ThemeMode.dark,
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byType(MainLayout), findsOneWidget, reason: 'Failed MainLayout at $size');
|
|
expect(tester.takeException(), isNull);
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 4. RIDER SAFETY UI GATES VERIFICATION
|
|
// =========================================================================
|
|
group('UI-2I Rider Safety UI Gate Invariants', () {
|
|
testWidgets('Active recording ride does not trigger disruptive modal sheets', (tester) async {
|
|
tester.view.physicalSize = const Size(400, 800);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
final activeSession = RideSession(
|
|
state: RideState.riding,
|
|
startTime: DateTime.now().subtract(const Duration(minutes: 5)),
|
|
);
|
|
|
|
await pumpView(
|
|
tester,
|
|
ProviderScope(
|
|
overrides: [
|
|
databaseProvider.overrideWithValue(db),
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(authService),
|
|
billingServiceProvider.overrideWithValue(billingService),
|
|
entitlementStorageProvider.overrideWithValue(entitlementStorage),
|
|
groupRideServiceProvider.overrideWithValue(groupRideService),
|
|
routeServiceProvider.overrideWithValue(routeService),
|
|
activeMapProvider.overrideWithValue(_FakeMapProvider()),
|
|
locationServiceProvider.overrideWithValue(locationService),
|
|
motorcycleRepositoryProvider.overrideWithValue(motorcycleRepo),
|
|
fieldTestLoggerProvider.overrideWithValue(fieldTestLogger),
|
|
activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])),
|
|
locationPermissionGrantedProvider.overrideWith((ref) => Future.value(true)),
|
|
rideStateMachineProvider.overrideWith((ref) => _FakeRideStateMachine(activeSession)),
|
|
],
|
|
child: MaterialApp(
|
|
debugShowCheckedModeBanner: false,
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(body: DashboardScreen()),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Ensure no BottomSheet or AlertDialog is automatically presented
|
|
expect(find.byType(BottomSheet), findsNothing);
|
|
expect(find.byType(AlertDialog), findsNothing);
|
|
expect(find.byType(DashboardScreen), findsOneWidget);
|
|
|
|
await unmountView(tester);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 5. NAVIGATION FLOWS & DEEP ROUTE TRANSITIONS
|
|
// =========================================================================
|
|
group('UI-2I Navigation Flows & Deep Route Transitions', () {
|
|
testWidgets('App shell navigates cleanly across all primary destinations', (tester) async {
|
|
tester.view.physicalSize = const Size(400, 800);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
final destinations = [
|
|
'/',
|
|
'/map',
|
|
'/history',
|
|
'/records',
|
|
'/garage',
|
|
'/settings',
|
|
'/',
|
|
];
|
|
|
|
for (final path in destinations) {
|
|
appRouter.go(path);
|
|
await pumpView(
|
|
tester,
|
|
ProviderScope(
|
|
overrides: [
|
|
databaseProvider.overrideWithValue(db),
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(authService),
|
|
billingServiceProvider.overrideWithValue(billingService),
|
|
entitlementStorageProvider.overrideWithValue(entitlementStorage),
|
|
groupRideServiceProvider.overrideWithValue(groupRideService),
|
|
routeServiceProvider.overrideWithValue(routeService),
|
|
activeMapProvider.overrideWithValue(_FakeMapProvider()),
|
|
locationServiceProvider.overrideWithValue(locationService),
|
|
motorcycleRepositoryProvider.overrideWithValue(motorcycleRepo),
|
|
fieldTestLoggerProvider.overrideWithValue(fieldTestLogger),
|
|
activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])),
|
|
locationPermissionGrantedProvider.overrideWith((ref) => Future.value(true)),
|
|
],
|
|
child: MaterialApp.router(
|
|
routerConfig: appRouter,
|
|
theme: AppTheme.darkTheme,
|
|
),
|
|
),
|
|
);
|
|
expect(tester.takeException(), isNull, reason: 'Navigation failed to $path');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
|
|
testWidgets('Deep secondary route transitions render cleanly without exception', (tester) async {
|
|
tester.view.physicalSize = const Size(400, 800);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
final deepRoutes = [
|
|
'/history/1',
|
|
'/garage/1',
|
|
'/routes/route-1',
|
|
'/group-rides/ride-1',
|
|
'/settings/account',
|
|
'/settings/pro',
|
|
'/settings/diagnostics',
|
|
'/settings/diagnostics/live-log',
|
|
'/settings/diagnostics/report',
|
|
];
|
|
|
|
for (final path in deepRoutes) {
|
|
appRouter.go(path);
|
|
await pumpView(
|
|
tester,
|
|
ProviderScope(
|
|
overrides: [
|
|
databaseProvider.overrideWithValue(db),
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(authService),
|
|
billingServiceProvider.overrideWithValue(billingService),
|
|
entitlementStorageProvider.overrideWithValue(entitlementStorage),
|
|
groupRideServiceProvider.overrideWithValue(groupRideService),
|
|
routeServiceProvider.overrideWithValue(routeService),
|
|
activeMapProvider.overrideWithValue(_FakeMapProvider()),
|
|
locationServiceProvider.overrideWithValue(locationService),
|
|
motorcycleRepositoryProvider.overrideWithValue(motorcycleRepo),
|
|
fieldTestLoggerProvider.overrideWithValue(fieldTestLogger),
|
|
activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])),
|
|
locationPermissionGrantedProvider.overrideWith((ref) => Future.value(true)),
|
|
],
|
|
child: MaterialApp.router(
|
|
routerConfig: appRouter,
|
|
theme: AppTheme.darkTheme,
|
|
),
|
|
),
|
|
);
|
|
expect(tester.takeException(), isNull, reason: 'Deep route failed for $path');
|
|
}
|
|
await unmountView(tester);
|
|
});
|
|
});
|
|
}
|