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 getUrl(Uri url) async => _MockHttpRequest(); @override Future 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 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> listen( void Function(List event)? onData, { Function? onError, void Function()? onDone, bool? cancelOnError, }) { return Stream>.value(_transparent1x1Png).listen( onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError, ); } } final _transparent1x1Png = [ 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)); }); }