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/features/replay/replay_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 a test ride and telemetry samples await db.into(db.rides).insert( RidesCompanion.insert( id: const drift.Value(1), startTime: DateTime.now(), distanceMiles: const drift.Value(10.0), topSpeedMph: const drift.Value(60.0), movingTime: const drift.Value(600), stoppedTime: const drift.Value(60), ), ); for (int i = 0; i < 20; i++) { await db.into(db.telemetrySamples).insert( TelemetrySamplesCompanion.insert( rideId: 1, timestamp: DateTime.now().add(Duration(seconds: i)), latitude: drift.Value(34.0 + (i * 0.001)), longitude: drift.Value(-84.0 + (i * 0.001)), speed: drift.Value(20.0 + (i * 2.0)), leanAngle: drift.Value(i < 10 ? 10.0 : 25.0), ), ); } }); tearDown(() async { HttpOverrides.global = null; await db.close(); }); testWidgets('ReplayScreen corner apex header renders without overflow on 320px screen', (tester) async { tester.view.physicalSize = const Size(320, 800); tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await tester.pumpWidget( ProviderScope( overrides: [ databaseProvider.overrideWithValue(db), ], child: const MaterialApp( home: Scaffold( body: ReplayScreen(rideId: 1), ), ), ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(find.text('CORNER APEX BREAKDOWN & GRADING'), findsOneWidget); }); testWidgets('ReplayScreen corner apex header renders without overflow on 412px Pixel screen', (tester) async { tester.view.physicalSize = const Size(412, 915); tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await tester.pumpWidget( ProviderScope( overrides: [ databaseProvider.overrideWithValue(db), ], child: const MaterialApp( home: Scaffold( body: ReplayScreen(rideId: 1), ), ), ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(find.text('CORNER APEX BREAKDOWN & GRADING'), findsOneWidget); }); testWidgets('BUG-014 (#61): ReplayScreen renders responsive horizontal split in 800x360 landscape without overflow', (tester) async { tester.view.physicalSize = const Size(800, 360); tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await tester.pumpWidget( ProviderScope( overrides: [ databaseProvider.overrideWithValue(db), ], child: const MaterialApp( home: Scaffold( body: ReplayScreen(rideId: 1), ), ), ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); // Verifies both map and breakdown table are rendered concurrently in viewport expect(find.text('CORNER APEX BREAKDOWN & GRADING'), findsOneWidget); expect(find.text('SPEED'), findsOneWidget); expect(find.text('LEAN'), findsOneWidget); expect(find.text('G'), findsOneWidget); expect(find.byType(Slider), findsOneWidget); expect(find.byType(Row), findsWidgets); await tester.pumpWidget(const SizedBox()); await tester.pump(const Duration(milliseconds: 100)); }); testWidgets('BUG-014 (#61): ReplayScreen renders without overflow on 640x320 compact landscape', (tester) async { tester.view.physicalSize = const Size(640, 320); tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await tester.pumpWidget( ProviderScope( overrides: [ databaseProvider.overrideWithValue(db), ], child: const MaterialApp( home: Scaffold( body: ReplayScreen(rideId: 1), ), ), ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(find.text('CORNER APEX BREAKDOWN & GRADING'), findsOneWidget); await tester.pumpWidget(const SizedBox()); await tester.pump(const Duration(milliseconds: 100)); }); }