MotorSignal-App/test/activity_screen_test.dart

252 lines
9.2 KiB
Dart

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<int> 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<List<MotorcycleYear>> getYears() async => [MotorcycleYear(2024)];
@override
Future<List<MotorcycleMake>> getMakes(int year) async => [MotorcycleMake(id: 'yamaha', name: 'Yamaha')];
@override
Future<List<MotorcycleModel>> getModels(int year, String makeId) async => [MotorcycleModel(id: 'r1', name: 'YZF-R1', makeId: makeId)];
@override
Future<List<MotorcycleVariant>> getVariants(int year, String makeId, String modelId) async => [MotorcycleVariant(id: 'std', name: 'Standard')];
@override
Future<MotorcycleSpecification?> 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<ActivityTab>), 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));
}
});
});
}