173 lines
5.3 KiB
Dart
173 lines
5.3 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:motorcycle_dash/core/theme/theme_extensions.dart';
|
|
import 'package:motorcycle_dash/features/map/map_screen.dart';
|
|
import 'package:motorcycle_dash/database/database.dart';
|
|
import 'package:drift/native.dart';
|
|
import 'package:drift/drift.dart' as drift;
|
|
|
|
class _TestHttpOverrides extends HttpOverrides {
|
|
@override
|
|
HttpClient createHttpClient(SecurityContext? context) => _MockHttpClient();
|
|
}
|
|
|
|
class _MockHttpHeaders extends Fake implements HttpHeaders {
|
|
@override
|
|
void add(String name, Object value, {bool preserveHeaderCase = false}) {}
|
|
@override
|
|
void set(String name, Object value, {bool preserveHeaderCase = false}) {}
|
|
}
|
|
|
|
class _MockHttpClient extends Fake implements HttpClient {
|
|
@override
|
|
Future<HttpClientRequest> getUrl(Uri url) async => _MockHttpRequest();
|
|
|
|
@override
|
|
Future<HttpClientRequest> openUrl(String method, Uri url) async => _MockHttpRequest();
|
|
|
|
@override
|
|
void close({bool force = false}) {}
|
|
}
|
|
|
|
class _MockHttpRequest extends Fake implements HttpClientRequest {
|
|
@override
|
|
final HttpHeaders headers = _MockHttpHeaders();
|
|
|
|
@override
|
|
bool followRedirects = true;
|
|
|
|
@override
|
|
int maxRedirects = 5;
|
|
|
|
@override
|
|
int contentLength = -1;
|
|
|
|
@override
|
|
Future<HttpClientResponse> close() async => _MockHttpResponse();
|
|
}
|
|
|
|
class _MockHttpResponse extends Fake implements HttpClientResponse {
|
|
@override
|
|
int get statusCode => 200;
|
|
@override
|
|
int get contentLength => _transparent1x1Png.length;
|
|
@override
|
|
HttpClientResponseCompressionState get compressionState =>
|
|
HttpClientResponseCompressionState.notCompressed;
|
|
@override
|
|
StreamSubscription<List<int>> listen(
|
|
void Function(List<int> event)? onData, {
|
|
Function? onError,
|
|
void Function()? onDone,
|
|
bool? cancelOnError,
|
|
}) {
|
|
return Stream<List<int>>.value(_transparent1x1Png).listen(
|
|
onData,
|
|
onError: onError,
|
|
onDone: onDone,
|
|
cancelOnError: cancelOnError,
|
|
);
|
|
}
|
|
}
|
|
|
|
final _transparent1x1Png = <int>[
|
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
|
|
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
|
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00,
|
|
0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00,
|
|
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49,
|
|
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
|
];
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
late AppDatabase db;
|
|
|
|
setUp(() async {
|
|
HttpOverrides.global = _TestHttpOverrides();
|
|
db = AppDatabase.forTesting(NativeDatabase.memory());
|
|
|
|
// Insert sample ride and telemetry breadcrumbs
|
|
await db.into(db.rides).insert(
|
|
RidesCompanion.insert(
|
|
id: const drift.Value(1),
|
|
startTime: DateTime(2026, 1, 1, 10, 0),
|
|
distanceMiles: const drift.Value(15.2),
|
|
topSpeedMph: const drift.Value(75.0),
|
|
movingTime: const drift.Value(900),
|
|
stoppedTime: const drift.Value(60),
|
|
),
|
|
);
|
|
|
|
for (int i = 0; i < 10; i++) {
|
|
await db.into(db.telemetrySamples).insert(
|
|
TelemetrySamplesCompanion.insert(
|
|
rideId: 1,
|
|
timestamp: DateTime(2026, 1, 1, 10, 0, i * 5),
|
|
latitude: drift.Value(37.3861 + (i * 0.002)),
|
|
longitude: drift.Value(-122.2641 + (i * 0.002)),
|
|
speed: drift.Value(35.0 + i),
|
|
leanAngle: drift.Value(12.0),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
|
|
tearDown(() async {
|
|
HttpOverrides.global = null;
|
|
await db.close();
|
|
});
|
|
|
|
Widget buildSubject() {
|
|
return ProviderScope(
|
|
overrides: [
|
|
databaseProvider.overrideWithValue(db),
|
|
],
|
|
child: MaterialApp(
|
|
theme: ThemeData.dark().copyWith(
|
|
extensions: [MotorSignalThemeColors.dark],
|
|
),
|
|
home: const MapScreen(rideId: 1),
|
|
),
|
|
);
|
|
}
|
|
|
|
testWidgets('Portrait: renders both appbar action and floating replay action button', (tester) async {
|
|
tester.view.physicalSize = const Size(360, 800);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await tester.pumpWidget(buildSubject());
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const ValueKey('play_replay_appbar_button')), findsOneWidget);
|
|
expect(find.byKey(const ValueKey('fire_off_ride_preview_button')), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await tester.pumpWidget(const SizedBox());
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
});
|
|
|
|
testWidgets('Landscape: removes duplicate floating replay action to keep map unobstructed (#60)', (tester) async {
|
|
tester.view.physicalSize = const Size(800, 360);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(() => tester.view.resetPhysicalSize());
|
|
|
|
await tester.pumpWidget(buildSubject());
|
|
await tester.pumpAndSettle();
|
|
|
|
// Appbar replay action remains accessible
|
|
expect(find.byKey(const ValueKey('play_replay_appbar_button')), findsOneWidget);
|
|
// Floating button is hidden in landscape
|
|
expect(find.byKey(const ValueKey('fire_off_ride_preview_button')), findsNothing);
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await tester.pumpWidget(const SizedBox());
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
});
|
|
}
|