import 'dart:convert'; import 'package:drift/drift.dart' as drift; import 'package:drift/native.dart'; 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:motorcycle_dash/core/theme/theme.dart'; import 'package:motorcycle_dash/database/database.dart'; import 'package:motorcycle_dash/features/activity/activity_screen.dart'; import 'package:motorcycle_dash/features/garage/garage_providers.dart'; import 'package:motorcycle_dash/features/history/history_screen.dart'; import 'package:motorcycle_dash/features/records/records_screen.dart'; import 'package:motorcycle_dash/features/records/records_service.dart'; import 'package:motorcycle_dash/models/motorcycle_models.dart'; import 'package:motorcycle_dash/services/github/github_sync_service.dart'; import 'package:motorcycle_dash/services/motorcycle/motorcycle_repository.dart'; class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService { @override GitHubUploadState get state => GitHubUploadState.idle; @override String get statusMessage => ''; @override Set get queuedRideIds => const {}; @override bool isRideUploaded(int rideId) => false; @override String? getRideCommitSha(int rideId) => null; @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _FakeMotorcycleRepository implements MotorcycleRepository { @override Future> getYears() async => [MotorcycleYear(2024)]; @override Future> getMakes(int year) async => [MotorcycleMake(id: 'yamaha', name: 'Yamaha')]; @override Future> getModels(int year, String makeId) async => [MotorcycleModel(id: 'r1', name: 'YZF-R1', makeId: makeId)]; @override Future> getVariants(int year, String makeId, String modelId) async => [MotorcycleVariant(id: 'std', name: 'Standard')]; @override Future getSpecifications(MotorcycleSelection selection) async => null; @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); late AppDatabase db; late SharedPreferences prefs; setUp(() async { db = AppDatabase.forTesting(NativeDatabase.memory()); SharedPreferences.setMockInitialValues({ 'topSpeedMph': 120.0, 'fastest0To60': 3.5, 'fastest0To100': 7.2, 'maxLeanLeft': 42.0, 'maxLeanRight': 44.0, }); prefs = await SharedPreferences.getInstance(); final spec = MotorcycleSpecification( year: 2024, manufacturer: 'Yamaha', model: 'YZF-R1', displacementCc: 998, horsepower: 198, torqueLbFt: 83.0, wetWeightLbs: 448, claimedTopSpeedMph: 186, claimedZeroToSixtySec: 2.8, ); await db.into(db.garageMotorcycles).insert( GarageMotorcyclesCompanion.insert( year: 2024, makeId: 'yamaha', makeName: 'Yamaha', modelId: 'r1', modelName: 'YZF-R1', nickname: const drift.Value('Blue Thunder'), isDefault: const drift.Value(true), dateAdded: DateTime.now(), lastUpdated: DateTime.now(), specificationJson: drift.Value(json.encode(spec.toJson())), ), ); await db.into(db.rides).insert( RidesCompanion.insert( startTime: DateTime(2026, 1, 1, 10, 0), endTime: drift.Value(DateTime(2026, 1, 1, 10, 45)), distanceMiles: const drift.Value(25.5), topSpeedMph: const drift.Value(65.0), movingTime: const drift.Value(2400), stoppedTime: const drift.Value(300), distance: const drift.Value(25.5), topSpeed: const drift.Value(65.0), averageSpeed: const drift.Value(42.0), elevationGain: const drift.Value(350.0), elevationLoss: const drift.Value(300.0), maxLeanLeft: const drift.Value(34.0), maxLeanRight: const drift.Value(36.0), maxAccelG: const drift.Value(0.75), maxBrakingG: const drift.Value(0.85), motorcycleId: const drift.Value(1), motorcycleYear: const drift.Value(2024), motorcycleMake: const drift.Value('Yamaha'), motorcycleModel: const drift.Value('YZF-R1'), ), ); }); tearDown(() async { await db.close(); }); Widget buildTestActivityScreen({ ActivityTab initialTab = ActivityTab.rides, ThemeData? theme, Size size = const Size(400, 800), }) { return ProviderScope( overrides: [ databaseProvider.overrideWithValue(db), sharedPreferencesProvider.overrideWithValue(prefs), githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()), motorcycleRepositoryProvider.overrideWith((ref) => _FakeMotorcycleRepository()), ], child: MaterialApp( theme: theme ?? AppTheme.darkTheme, home: MediaQuery( data: MediaQueryData(size: size), child: ActivityScreen(initialTab: initialTab), ), ), ); } group('ActivityScreen Widget & Presentation Tests', () { testWidgets('Renders segmented button with Rides and Records tabs', (tester) async { await tester.pumpWidget(buildTestActivityScreen()); await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(); expect(find.byType(SegmentedButton), findsOneWidget); expect(find.text('Rides'), findsOneWidget); expect(find.text('Records'), findsOneWidget); await tester.pumpWidget(const SizedBox()); await tester.pump(const Duration(milliseconds: 100)); }); testWidgets('Defaults to Rides tab hosting embedded HistoryScreen without standalone AppBar', (tester) async { await tester.pumpWidget(buildTestActivityScreen()); await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(); expect(find.byType(HistoryScreen), findsOneWidget); expect(find.byType(RecordsScreen), findsNothing); // Embedded history screen suppresses standalone AppBar final historyFinder = find.byType(HistoryScreen); expect(find.descendant(of: historyFinder, matching: find.byType(AppBar)), findsNothing); await tester.pumpWidget(const SizedBox()); await tester.pump(const Duration(milliseconds: 100)); }); testWidgets('Switches to Records tab when tapped', (tester) async { await tester.pumpWidget(buildTestActivityScreen()); await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(); expect(find.byType(HistoryScreen), findsOneWidget); expect(find.byType(RecordsScreen), findsNothing); // Tap Records segment await tester.tap(find.text('Records')); await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(); expect(find.byType(RecordsScreen), findsOneWidget); expect(find.byType(HistoryScreen), findsNothing); // Tap Rides segment back await tester.tap(find.text('Rides')); await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(); expect(find.byType(HistoryScreen), findsOneWidget); expect(find.byType(RecordsScreen), findsNothing); await tester.pumpWidget(const SizedBox()); await tester.pump(const Duration(milliseconds: 100)); }); testWidgets('Initializes directly to Records tab when requested', (tester) async { await tester.pumpWidget(buildTestActivityScreen(initialTab: ActivityTab.records)); await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(); expect(find.byType(RecordsScreen), findsOneWidget); expect(find.byType(HistoryScreen), findsNothing); await tester.pumpWidget(const SizedBox()); await tester.pump(const Duration(milliseconds: 100)); }); testWidgets('Renders cleanly under Light Theme without exceptions', (tester) async { await tester.pumpWidget(buildTestActivityScreen(theme: AppTheme.lightTheme)); await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(find.byType(ActivityScreen), findsOneWidget); await tester.pumpWidget(const SizedBox()); await tester.pump(const Duration(milliseconds: 100)); }); testWidgets('Adapts cleanly across multiple viewports without overflow', (tester) async { const viewports = [ Size(320, 640), Size(412, 915), Size(800, 360), Size(1024, 768), ]; for (final vp in viewports) { tester.view.physicalSize = vp; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); await tester.pumpWidget(buildTestActivityScreen(size: vp)); await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(); expect(tester.takeException(), isNull, reason: 'No overflow on $vp'); expect(find.byType(ActivityScreen), findsOneWidget); await tester.pumpWidget(const SizedBox()); await tester.pump(const Duration(milliseconds: 100)); } }); }); }