252 lines
9.2 KiB
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));
|
|
}
|
|
});
|
|
});
|
|
}
|