MotorSignal-App/test/community_social_feed_test....

389 lines
14 KiB
Dart

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<Override> 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);
});
});
}