import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/core/theme/theme_extensions.dart'; import 'package:motorcycle_dash/database/database.dart'; import 'package:motorcycle_dash/features/social/feed/social_feed_screen.dart'; import 'package:motorcycle_dash/features/social/feed/widgets/feed_comments_bottom_sheet.dart'; import 'package:motorcycle_dash/features/social/feed/widgets/feed_telemetry_inspector_modal.dart'; import 'package:motorcycle_dash/features/social/feed/widgets/share_session_modal.dart'; import 'package:motorcycle_dash/features/social/social_hub_screen.dart'; import 'package:motorcycle_dash/services/map/map_provider_interface.dart'; import 'package:motorcycle_dash/services/social/social_feed_models.dart'; import 'package:motorcycle_dash/services/social/social_feed_providers.dart'; import 'package:motorcycle_dash/services/social/social_feed_service.dart'; Widget _createTestApp({required Widget child, List overrides = const []}) { return ProviderScope( overrides: overrides, child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: const [ MotorSignalThemeColors.dark, ], ), home: Scaffold(body: child), ), ); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('SocialFeed Models Unit Tests', () { test('FeedFilter display names', () { expect(FeedFilter.allActivity.displayName, 'All Activity'); expect(FeedFilter.clubOnly.displayName, 'Club Only'); expect(FeedFilter.nearbyPacks.displayName, 'Nearby Packs'); }); test('FeedPostComment toJson and fromJson roundtrip', () { final comment = FeedPostComment( id: 'c123', authorUid: 'uid_test', authorDisplayName: 'Test Rider', authorHandle: 'testrider', content: 'Great pace through section 2!', createdAt: DateTime(2026, 9, 29, 10, 0), ); final json = comment.toJson(); expect(json['id'], 'c123'); expect(json['authorUid'], 'uid_test'); expect(json['content'], 'Great pace through section 2!'); final parsed = FeedPostComment.fromJson({ 'id': 'c123', 'authorUid': 'uid_test', 'authorDisplayName': 'Test Rider', 'authorHandle': 'testrider', 'content': 'Great pace through section 2!', 'createdAt': '2026-09-29T10:00:00.000', }); expect(parsed.id, 'c123'); expect(parsed.authorDisplayName, 'Test Rider'); expect(parsed.content, 'Great pace through section 2!'); }); test('SocialFeedItem serialization and copyWith', () { final item = SocialFeedItem( id: 'feed_1', authorUid: 'uid_1', authorDisplayName: 'Apex Marcus', authorHandle: 'ApexMarcus', authorBadge: 'PRO', clubName: 'Bay Area Twisties Club', locationName: "Alice's Restaurant", timestamp: DateTime(2026, 9, 29, 12, 0), rideTitle: 'Sunday Skyline Carve', bikeModel: '2024 Ducati Streetfighter V4', distanceMiles: 68.4, durationSeconds: 4440, maxLeanAngleDeg: 51.0, maxLeanSide: 'RIGHT', paceFactor: '1.32x Spirited', hazardCount: 0, elevationFt: 2140, snapshotType: FeedSnapshotType.routeTrack, routePoints: const [AppLatLng(37.38, -122.26)], fistBumpCount: 42, fistBumpUserIds: const ['uid_other'], comments: const [], ); expect(item.isFistBumpedBy('uid_other'), isTrue); expect(item.isFistBumpedBy('uid_nonexistent'), isFalse); final updated = item.copyWith(fistBumpCount: 43); expect(updated.fistBumpCount, 43); expect(updated.id, 'feed_1'); final json = item.toJson(); expect(json['id'], 'feed_1'); expect(json['authorHandle'], 'ApexMarcus'); expect(json['distanceMiles'], 68.4); expect(json['maxLeanAngleDeg'], 51.0); }); }); group('SocialFeedService Unit Tests', () { late SocialFeedService service; setUp(() { service = SocialFeedService(); }); tearDown(() { service.dispose(); }); test('Initializes with default canonical items', () async { final stream = service.getFeedStream(filter: FeedFilter.allActivity); final items = await stream.first; expect(items.length, greaterThanOrEqualTo(3)); expect(items.first.authorHandle, 'ApexMarcus'); }); test('Filters feed by clubOnly and nearbyPacks', () async { final clubStream = service.getFeedStream( filter: FeedFilter.clubOnly, userClubName: 'Pacific Coast Riders', ); final clubItems = await clubStream.first; expect(clubItems.every((i) => i.clubName == 'Pacific Coast Riders'), isTrue); final nearbyStream = service.getFeedStream(filter: FeedFilter.nearbyPacks); final nearbyItems = await nearbyStream.first; expect(nearbyItems.every((i) => i.distanceMiles < 60.0), isTrue); }); test('Toggles fist bump reaction', () async { final itemsBefore = await service.getFeedStream().first; final target = itemsBefore.first; final initialCount = target.fistBumpCount; // Bump await service.toggleFistBump(postId: target.id, userUid: 'test_rider_uid'); final itemsAfterBump = await service.getFeedStream().first; final bumpedItem = itemsAfterBump.firstWhere((i) => i.id == target.id); expect(bumpedItem.fistBumpCount, initialCount + 1); expect(bumpedItem.isFistBumpedBy('test_rider_uid'), isTrue); // Unbump await service.toggleFistBump(postId: target.id, userUid: 'test_rider_uid'); final itemsAfterUnbump = await service.getFeedStream().first; final unbumpedItem = itemsAfterUnbump.firstWhere((i) => i.id == target.id); expect(unbumpedItem.fistBumpCount, initialCount); expect(unbumpedItem.isFistBumpedBy('test_rider_uid'), isFalse); }); test('Adds comment to post', () async { final comment = FeedPostComment( id: 'new_c', authorUid: 'uid_test', authorDisplayName: 'Dave S', content: 'Clean line through section 2!', createdAt: DateTime.now(), ); await service.addComment(postId: 'feed_post_1', comment: comment); final items = await service.getFeedStream().first; final post = items.firstWhere((i) => i.id == 'feed_post_1'); expect(post.comments.any((c) => c.content == 'Clean line through section 2!'), isTrue); }); test('Shares recent ride to community feed', () async { final newPost = await service.shareRecentRide( authorUid: 'uid_author', authorDisplayName: 'Marcus Vance', authorHandle: 'ApexMarcus', locationName: 'Pescadero Creek', rideTitle: 'Late Afternoon Run', bikeModel: 'Ducati Panigale V4 S', distanceMiles: 38.2, durationSeconds: 2100, maxLeanAngleDeg: 52.0, ); expect(newPost.rideTitle, 'Late Afternoon Run'); expect(newPost.bikeModel, 'Ducati Panigale V4 S'); final items = await service.getFeedStream().first; expect(items.first.id, newPost.id); expect(items.first.rideTitle, 'Late Afternoon Run'); }); }); group('SocialFeedScreen & Sub-Widgets Widget Tests', () { testWidgets('Renders SocialFeedScreen with header pills and tabs', (tester) async { await tester.binding.setSurfaceSize(const Size(400, 800)); await tester.pumpWidget( _createTestApp( child: const SocialFeedScreen(), ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); expect(find.text('Community Feed'), findsOneWidget); expect(find.text('SQUAD & CANYON FEED'), findsOneWidget); expect(find.byKey(const ValueKey('feed_search_toggle_button')), findsOneWidget); expect(find.text('All Activity'), findsOneWidget); expect(find.text('Club Only'), findsOneWidget); expect(find.text('Nearby Packs'), findsOneWidget); expect(find.text('SHARE RECENT SESSION'), findsOneWidget); // Verify Feed item contents expect(find.text('@ApexMarcus'), findsOneWidget); expect(find.text('Sunday Skyline & Pescadero Carve'), findsOneWidget); expect(find.text('2024 Ducati Streetfighter V4'), findsOneWidget); expect(find.text('51°'), findsOneWidget); // Switch to Club Only tab await tester.tap(find.text('Club Only')); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); // Switch to Nearby Packs tab await tester.tap(find.text('Nearby Packs')); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); }); testWidgets('Tapping Fist Bump updates bump count with scale animation', (tester) async { await tester.binding.setSurfaceSize(const Size(400, 800)); final service = SocialFeedService(); await tester.pumpWidget( _createTestApp( overrides: [ socialFeedServiceProvider.overrideWithValue(service), ], child: const SocialFeedScreen(), ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); // Scroll slightly so the card action bar clears the sticky button await tester.drag(find.byType(ListView), const Offset(0, -180)); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); expect(find.text('42 Bumps'), findsOneWidget); // Tap fist bump await tester.tap(find.byKey(const ValueKey('fist_bump_button_feed_post_1'))); await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); expect(find.text('43 Bumps'), findsOneWidget); }); testWidgets('Comments bottom sheet opens and displays comments', (tester) async { await tester.binding.setSurfaceSize(const Size(400, 800)); final item = SocialFeedItem( id: 'test_item', authorUid: 'uid_1', authorDisplayName: 'Sarah Chen', authorHandle: 'SarahSweep', locationName: 'Mulholland Hwy', timestamp: DateTime.now(), rideTitle: 'The Snake Pace Lap', bikeModel: 'Yamaha MT-09 SP', distanceMiles: 44.2, durationSeconds: 2900, maxLeanAngleDeg: 48.0, comments: [ FeedPostComment( id: 'c1', authorUid: 'uid_2', authorDisplayName: 'Marcus Vance', authorHandle: 'ApexMarcus', content: 'Great line through the esses!', createdAt: DateTime.now(), ), ], ); await tester.pumpWidget( _createTestApp( child: Builder( builder: (context) => ElevatedButton( onPressed: () => FeedCommentsBottomSheet.show(context, item), child: const Text('Open Comments'), ), ), ), ); await tester.pumpAndSettle(); await tester.tap(find.text('Open Comments')); await tester.pumpAndSettle(); expect(find.text('Rider Comments'), findsOneWidget); expect(find.text('Great line through the esses!'), findsOneWidget); expect(find.text('Marcus Vance'), findsOneWidget); }); testWidgets('ShareSessionModal renders and allows form submission', (tester) async { await tester.binding.setSurfaceSize(const Size(400, 900)); final service = SocialFeedService(); await tester.pumpWidget( _createTestApp( overrides: [ socialFeedServiceProvider.overrideWithValue(service), ], child: Builder( builder: (context) => ElevatedButton( onPressed: () => ShareSessionModal.show(context), child: const Text('Open Share Modal'), ), ), ), ); await tester.pumpAndSettle(); await tester.tap(find.text('Open Share Modal')); await tester.pumpAndSettle(); expect(find.text('Share Recent Session'), findsOneWidget); expect(find.text('PUBLISH TO COMMUNITY FEED'), findsOneWidget); await tester.tap(find.text('PUBLISH TO COMMUNITY FEED')); await tester.pumpAndSettle(); // Modal closed and snackbar shown expect(find.text('Ride session published to Community Feed!'), findsOneWidget); }); testWidgets('SocialHubScreen contains Community Feed card', (tester) async { await tester.binding.setSurfaceSize(const Size(400, 800)); await tester.pumpWidget( _createTestApp( child: const SocialHubScreen(), ), ); await tester.pumpAndSettle(); expect(find.byKey(const ValueKey('social_card_feed')), findsOneWidget); expect(find.text('Community Feed'), findsOneWidget); }); testWidgets('Tapping INSPECT TELEMETRY GRAPH opens FeedTelemetryInspectorModal', (tester) async { await tester.binding.setSurfaceSize(const Size(400, 1000)); final service = SocialFeedService(); await tester.pumpWidget( _createTestApp( overrides: [ socialFeedServiceProvider.overrideWithValue(service), ], child: const SocialFeedScreen(), ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); // Scroll so the card inspect telemetry button is visible await tester.drag(find.byType(ListView), const Offset(0, -100)); await tester.pumpAndSettle(); final inspectBtn = find.text('INSPECT TELEMETRY GRAPH'); expect(inspectBtn, findsWidgets); await tester.tap(inspectBtn.first); await tester.pumpAndSettle(); // Verify inspector modal opened with metrics and tabs expect(find.text('PEAK LEAN'), findsOneWidget); expect(find.text('LEAN & APEX'), findsOneWidget); expect(find.text('SPEED & G-FORCE'), findsOneWidget); expect(find.text('OPEN FULL RIDE REPLAY'), findsOneWidget); }); }); group('BUG-003: Share Recent Session Read-Only Telemetry Integrity Tests', () { test( 'SocialFeedService.shareRecentRide enforces authoritativeRide telemetry over forged input parameters', () async { final service = SocialFeedService(); addTearDown(service.dispose); final recordedRide = Ride( id: 77, startTime: DateTime(2026, 10, 10, 8, 0), endTime: DateTime(2026, 10, 10, 8, 42), distance: 31.4, distanceMiles: 31.4, movingTime: 2520, // 42 mins stoppedTime: 0, topSpeed: 74.5, topSpeedMph: 74.5, averageSpeed: 44.8, maxLeanLeft: 47.0, maxLeanRight: 39.0, maxAccelG: 0.55, maxBrakingG: 0.62, elevationGain: 300.0, // ~984 ft elevationLoss: 100.0, ); final published = await service.shareRecentRide( authorUid: 'uid_1', authorDisplayName: 'Test Rider', authorHandle: 'test_rider', locationName: 'Highway 9', rideTitle: 'Morning Run', bikeModel: 'Yamaha R1', // Attempt to pass forged telemetry values alongside authoritativeRide distanceMiles: 999.9, durationSeconds: 99999, maxLeanAngleDeg: 88.0, maxLeanSide: 'RIGHT', elevationFt: 50000, authoritativeRide: recordedRide, routePoints: const [ AppLatLng(37.2500, -122.1200), AppLatLng(37.2900, -122.1600), ], ); expect(published.rideId, 77); expect(published.distanceMiles, closeTo(31.4, 0.001)); expect(published.durationSeconds, 2520); expect(published.maxLeanAngleDeg, closeTo(47.0, 0.001)); expect(published.maxLeanSide, 'LEFT'); expect(published.elevationFt, (300.0 * 3.28084).round()); }, ); testWidgets( 'ShareSessionModal renders telemetry as read-only tiles and publishes authoritative Ride values', (tester) async { await tester.binding.setSurfaceSize(const Size(400, 900)); final service = SocialFeedService(); addTearDown(service.dispose); final recordedRide = Ride( id: 42, startTime: DateTime(2026, 10, 12, 9, 0), endTime: DateTime(2026, 10, 12, 9, 42), distance: 31.4, distanceMiles: 31.4, movingTime: 2520, // 42 mins stoppedTime: 0, topSpeed: 78.0, topSpeedMph: 78.0, averageSpeed: 45.2, maxLeanLeft: 47.0, maxLeanRight: 41.0, maxAccelG: 0.5, maxBrakingG: 0.6, elevationGain: 450.0, elevationLoss: 150.0, motorcycleYear: 2024, motorcycleMake: 'Aprilia', motorcycleModel: 'RSV4 Factory', ); await tester.pumpWidget( _createTestApp( overrides: [ socialFeedServiceProvider.overrideWithValue(service), ], child: Builder( builder: (context) => ElevatedButton( onPressed: () => ShareSessionModal.show( context, initialRide: recordedRide, ), child: const Text('Open Share Modal'), ), ), ), ); await tester.pumpAndSettle(); await tester.tap(find.text('Open Share Modal')); await tester.pumpAndSettle(); // Only 3 editable TextFormFields exist (Title, Motorcycle Model, Location) // Telemetry fields (Distance, Time, Lean) are NOT editable TextFormFields expect(find.byType(TextFormField), findsNWidgets(3)); // Verify read-only telemetry tiles display the authoritative Ride metrics expect( find.byKey(const ValueKey('share_session_readonly_distance')), findsOneWidget, ); expect( find.byKey(const ValueKey('share_session_readonly_duration')), findsOneWidget, ); expect( find.byKey(const ValueKey('share_session_readonly_lean')), findsOneWidget, ); expect( find.byKey(const ValueKey('share_session_readonly_lean_side')), findsOneWidget, ); expect(find.text('31.4'), findsOneWidget); expect(find.text('42'), findsOneWidget); expect(find.text('47°'), findsOneWidget); expect(find.text('LEFT'), findsOneWidget); // Edit metadata (Title) and publish await tester.enterText( find.byType(TextFormField).first, 'Verified Highway 9 Dawn Patrol', ); await tester.tap(find.text('PUBLISH TO COMMUNITY FEED')); await tester.pumpAndSettle(); final feedItems = await service.getFeedStream().first; final latest = feedItems.first; expect(latest.rideTitle, 'Verified Highway 9 Dawn Patrol'); expect(latest.distanceMiles, closeTo(31.4, 0.001)); expect(latest.durationSeconds, 2520); expect(latest.maxLeanAngleDeg, closeTo(47.0, 0.001)); expect(latest.maxLeanSide, 'LEFT'); }, ); }); group('BUG-004: Published Ride Real Telemetry & GPS Verified Badge Tests', () { test( 'SocialFeedItem.verifyGpsCriteria and hasVerifiedGps reject demo posts, forged JSON, and zero-displacement routes', () { // 1. Demo post with valid routePoints still must NOT be GPS verified final demoPost = SocialFeedItem( id: 'demo_1', authorUid: 'uid_1', authorDisplayName: 'Demo Rider', authorHandle: 'demo', locationName: 'Skyline', timestamp: DateTime(2026, 10, 12), rideTitle: 'Demo Ride', bikeModel: 'R1', distanceMiles: 40.0, durationSeconds: 2400, maxLeanAngleDeg: 45.0, isDemo: true, isGpsVerified: true, routePoints: const [ AppLatLng(37.3861, -122.2641), AppLatLng(37.2500, -122.1800), ], ); expect(demoPost.hasVerifiedGps, isFalse); // 2. Non-demo post with < 2 points or zero displacement (< 100m) must NOT be GPS verified final stationaryPost = SocialFeedItem( id: 'stat_1', authorUid: 'uid_1', authorDisplayName: 'Rider', authorHandle: 'rider', locationName: 'Garage', timestamp: DateTime(2026, 10, 12), rideTitle: 'Garage Idle', bikeModel: 'R1', distanceMiles: 5.0, durationSeconds: 600, maxLeanAngleDeg: 10.0, isDemo: false, isGpsVerified: true, routePoints: const [ AppLatLng(37.38610, -122.26410), AppLatLng(37.38611, -122.26411), ], ); expect(stationaryPost.hasVerifiedGps, isFalse); // 3. Untrusted Firestore JSON claiming isGpsVerified: true with empty routePoints is rejected final forgedFromJson = SocialFeedItem.fromJson({ 'id': 'forged_1', 'authorUid': 'uid_forger', 'authorDisplayName': 'Forger', 'authorHandle': 'forger', 'locationName': 'Nowhere', 'timestamp': '2026-10-12T10:00:00.000Z', 'rideTitle': 'Fake Verified Run', 'bikeModel': 'H2R', 'distanceMiles': 100.0, 'durationSeconds': 3600, 'maxLeanAngleDeg': 60.0, 'isDemo': false, 'isGpsVerified': true, 'routePoints': >[], }); expect(forgedFromJson.isGpsVerified, isFalse); expect(forgedFromJson.hasVerifiedGps, isFalse); // 4. Real non-demo ride with >= 2 distinct GPS coordinates (>= 100m), positive distance & duration is verified final validPost = SocialFeedItem( id: 'valid_1', authorUid: 'uid_real', authorDisplayName: 'Real Rider', authorHandle: 'realrider', locationName: 'Highway 9', timestamp: DateTime(2026, 10, 12), rideTitle: 'Real Canyon Run', bikeModel: 'Street Triple RS', distanceMiles: 24.5, durationSeconds: 1800, maxLeanAngleDeg: 44.0, isDemo: false, isGpsVerified: true, routePoints: const [ AppLatLng(37.2500, -122.1200), AppLatLng(37.2900, -122.1600), ], ); expect(validPost.hasVerifiedGps, isTrue); }, ); testWidgets( 'Unverified/demo feed posts hide GPS VERIFIED badge and show explicit empty telemetry state in inspector', (tester) async { await tester.binding.setSurfaceSize(const Size(400, 1000)); final service = SocialFeedService(); addTearDown(service.dispose); await tester.pumpWidget( _createTestApp( overrides: [ socialFeedServiceProvider.overrideWithValue(service), ], child: const SocialFeedScreen(), ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); // Default feed items are demo items (isDemo: true), so GPS VERIFIED badge must NOT appear expect(find.text('GPS VERIFIED'), findsNothing); expect(find.text('NO GPS TRACK RECORDED'), findsWidgets); // Open telemetry inspector on unverified/demo post await tester.drag(find.byType(ListView), const Offset(0, -100)); await tester.pumpAndSettle(); await tester.tap(find.text('INSPECT TELEMETRY GRAPH').first); await tester.pumpAndSettle(); // Must show explicit no-telemetry-samples state instead of synthetic sine wave expect( find.byKey(const ValueKey('inspector_no_telemetry_state')), findsOneWidget, ); expect(find.text('NO RECORDED TELEMETRY SAMPLES'), findsOneWidget); expect(find.text('TELEMETRY TRACE UNAVAILABLE'), findsOneWidget); }, ); testWidgets( 'Verified ride with recorded telemetrySamples renders GPS VERIFIED badge and real interpolated scrubber values', (tester) async { await tester.binding.setSurfaceSize(const Size(400, 1000)); final verifiedItem = SocialFeedItem( id: 'verified_post_99', rideId: 99, authorUid: 'uid_verified', authorDisplayName: 'Elena Rostova', authorHandle: 'ElenaApex', locationName: 'Angeles Crest Hwy', timestamp: DateTime.now(), rideTitle: 'Verified Morning Telemetry Run', bikeModel: '2024 Aprilia Tuono V4', distanceMiles: 20.0, durationSeconds: 1500, maxLeanAngleDeg: 46.0, maxLeanSide: 'LEFT', elevationFt: 1800, isDemo: false, isGpsVerified: true, routePoints: const [ AppLatLng(34.2000, -118.1500), AppLatLng(34.2400, -118.1100), AppLatLng(34.2800, -118.0700), ], telemetrySamples: const [ FeedTelemetrySample( progress: 0.0, speedMph: 35.0, leanAngleDeg: 10.0, lateralG: 0.18, altitudeFt: 1200.0, latitude: 34.2000, longitude: -118.1500, ), FeedTelemetrySample( progress: 0.5, speedMph: 63.0, leanAngleDeg: -46.0, lateralG: -0.84, altitudeFt: 2400.0, latitude: 34.2400, longitude: -118.1100, ), FeedTelemetrySample( progress: 1.0, speedMph: 42.0, leanAngleDeg: 15.0, lateralG: 0.25, altitudeFt: 3000.0, latitude: 34.2800, longitude: -118.0700, ), ], ); await tester.pumpWidget( _createTestApp( child: Builder( builder: (context) => Column( children: [ if (verifiedItem.hasVerifiedGps) const Text('GPS VERIFIED'), ElevatedButton( onPressed: () => FeedTelemetryInspectorModal.show(context, verifiedItem), child: const Text('Inspect Verified Ride'), ), ], ), ), ), ); await tester.pumpAndSettle(); expect(verifiedItem.hasVerifiedGps, isTrue); expect(find.text('GPS VERIFIED'), findsOneWidget); await tester.tap(find.text('Inspect Verified Ride')); await tester.pumpAndSettle(); // No empty-state overlay when real telemetry samples exist expect( find.byKey(const ValueKey('inspector_no_telemetry_state')), findsNothing, ); // At default _scrubFraction = 0.5, scrubber readout matches sample[1] (63 MPH, 46°, 0.84G) expect(find.text('POINT: 10.0 MI (50%)'), findsOneWidget); expect(find.text('63 MPH'), findsOneWidget); expect(find.text('46°'), findsWidgets); expect(find.text('0.84G'), findsOneWidget); }, ); }); group('BUG-002: Social Feed Typography & Theme Contrast Tests', () { double contrastRatio(Color fg, Color bg) { final l1 = fg.computeLuminance(); final l2 = bg.computeLuminance(); final brightest = l1 > l2 ? l1 : l2; final darkest = l1 > l2 ? l2 : l1; return (brightest + 0.05) / (darkest + 0.05); } for (final isLight in [false, true]) { testWidgets( 'SocialFeedScreen and FeedCommentsBottomSheet maintain WCAG AA (>= 4.5:1) contrast in ${isLight ? "Light" : "Dark"} mode', (tester) async { await tester.binding.setSurfaceSize(const Size(412, 915)); final themeColors = isLight ? MotorSignalThemeColors.light : MotorSignalThemeColors.dark; final baseTheme = isLight ? ThemeData.light() : ThemeData.dark(); final service = SocialFeedService(); addTearDown(service.dispose); await tester.pumpWidget( ProviderScope( overrides: [ socialFeedServiceProvider.overrideWithValue(service), ], child: MediaQuery( data: const MediaQueryData( size: Size(412, 915), textScaler: TextScaler.linear(1.2), ), child: MaterialApp( theme: baseTheme.copyWith( extensions: [themeColors], ), home: const SocialFeedScreen(), ), ), ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); expect(tester.takeException(), isNull); // 1. AppBar title contrast against canvas surface final appBarTitle = tester.widget(find.text('Community Feed')); expect( contrastRatio(appBarTitle.style!.color!, themeColors.surfaceCanvas), greaterThanOrEqualTo(4.5), ); // 2. Card primary text (@authorHandle and rideTitle) against card primary surface final handleText = tester.widget(find.text('@ApexMarcus')); expect( contrastRatio( handleText.style!.color!, themeColors.surfaceCardPrimary, ), greaterThanOrEqualTo(4.5), ); final rideTitleText = tester.widget( find.text('Sunday Skyline & Pescadero Carve'), ); expect( contrastRatio( rideTitleText.style!.color!, themeColors.surfaceCardPrimary, ), greaterThanOrEqualTo(4.5), ); // 3. Card secondary text (clubName) against card primary surface final clubText = tester.widget( find.text('Bay Area Twisties Club'), ); expect( contrastRatio( clubText.style!.color!, themeColors.surfaceCardPrimary, ), greaterThanOrEqualTo(4.5), ); // 4. Filter tabs (selected vs unselected) contrast final selectedFilterText = tester.widget( find.text('All Activity'), ); final selectedFilterBg = isLight ? const Color(0xFF0047B3) : const Color(0xFF00E5FF); expect( contrastRatio(selectedFilterText.style!.color!, selectedFilterBg), greaterThanOrEqualTo(4.5), ); final unselectedFilterText = tester.widget( find.text('Club Only'), ); expect( contrastRatio( unselectedFilterText.style!.color!, themeColors.surfaceCardSecondary, ), greaterThanOrEqualTo(4.5), ); // 5. Telemetry metric labels and values contrast against metric cell surface final distLabel = tester.widget(find.text('DISTANCE').first); expect( contrastRatio( distLabel.style!.color!, themeColors.surfaceCardPrimary, ), greaterThanOrEqualTo(4.5), ); final leanValue = tester.widget(find.text('51°').first); expect( contrastRatio( leanValue.style!.color!, themeColors.surfaceCardPrimary, ), greaterThanOrEqualTo(4.5), ); }, ); } }); group('BUG-005: GPX Export Snackbar Horizontal Overflow Tests', () { for (final viewport in const [ Size(320, 640), Size(360, 800), Size(800, 480), ]) { testWidgets( 'GPX export snackbar renders long rideTitle without RenderFlex overflow at ${viewport.width.toInt()}x${viewport.height.toInt()} (1.5x text scale)', (tester) async { await tester.binding.setSurfaceSize(viewport); final service = SocialFeedService(); addTearDown(service.dispose); const longRideTitle = 'Ultra Long Sunday Skyline Boulevard & Pescadero Creek Canyon Telemetry Endurance Carve Session With Full Squad Pack'; final post = await service.shareRecentRide( authorUid: 'uid_overflow_test', authorDisplayName: 'Marcus Vance', authorHandle: 'ApexMarcus', locationName: 'Skyline Blvd', rideTitle: longRideTitle, bikeModel: '2024 Ducati Streetfighter V4', distanceMiles: 68.4, durationSeconds: 4440, maxLeanAngleDeg: 51.0, ); await tester.pumpWidget( ProviderScope( overrides: [ socialFeedServiceProvider.overrideWithValue(service), ], child: MediaQuery( data: MediaQueryData( size: viewport, textScaler: const TextScaler.linear(1.5), ), child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: const [MotorSignalThemeColors.dark], ), home: const SocialFeedScreen(), ), ), ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); final gpxButton = find.byKey( ValueKey('gpx_export_button_${post.id}'), ); await tester.scrollUntilVisible( gpxButton, 120.0, scrollable: find.descendant( of: find.byKey(const ValueKey('social_feed_list_view')), matching: find.byType(Scrollable), ), ); await tester.pumpAndSettle(); await tester.tap(gpxButton); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); // Must not throw any RenderFlex overflow exception expect(tester.takeException(), isNull); expect( find.text('GPX track exported for $longRideTitle'), findsOneWidget, ); }, ); } }); }