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/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/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.text('24 ACTIVE'), 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); }); }); }