389 lines
14 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|