From 3147034fe5b736e0bdbfb0a0a9e9416b31e9fc0e Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Tue, 26 May 2026 12:21:35 -0500 Subject: [PATCH] refactor: create utility fixtures & harnesses --- .gitignore | 1 + .../core/cache/poptart_cache_codecs_test.dart | 23 +- .../network/atproto_host_resolver_test.dart | 6 +- .../network/oauth_session_restorer_test.dart | 3 +- .../unauthorized_recovery_runner_test.dart | 3 +- .../network/xrpc_client_factory_test.dart | 5 +- test/core/widgets/lazurite_app_bar_test.dart | 43 +-- .../auth/data/auth_repository_test.dart | 8 +- .../auth/data/models/auth_models_test.dart | 1 - test/features/feed/bloc/feed_bloc_test.dart | 19 +- .../feed/cubit/post_thread_cubit_test.dart | 16 +- .../feed/cubit/similar_posts_cubit_test.dart | 15 +- .../feed/data/feed_repository_cache_test.dart | 2 +- .../feed/data/feed_repository_test.dart | 38 +-- .../post_thread_repository_cache_test.dart | 2 +- .../data/post_thread_repository_test.dart | 41 +-- .../presentation/feed_detail_screen_test.dart | 50 +--- .../presentation/home_feed_screen_test.dart | 6 +- .../media/media_route_payload_codec_test.dart | 12 +- .../post_card_with_actions_test.dart | 27 +- .../presentation/public_post_card_test.dart | 30 +- .../similar_posts_section_test.dart | 15 +- .../presentation/trending_screen_test.dart | 55 +--- .../bloc/notification_bloc_test.dart | 30 +- .../data/notification_repository_test.dart | 29 +- ...notification_deep_link_navigator_test.dart | 12 +- .../notification_local_mappers_test.dart | 38 +-- .../push_registration_service_test.dart | 2 +- .../widgets/notification_list_item_test.dart | 282 ++++-------------- .../profile/data/profile_repository_test.dart | 2 +- .../data/public_content_repository_test.dart | 49 ++- .../presentation/public_home_screen_test.dart | 52 ++-- .../presentation/public_navigation_test.dart | 5 +- .../search/bloc/search_bloc_test.dart | 8 +- .../search/cubit/hashtag_cubit_test.dart | 22 +- .../search/cubit/topic_cubit_test.dart | 20 +- .../search/data/hashtag_utils_test.dart | 16 +- .../search/data/post_text_extractor_test.dart | 38 +-- .../presentation/hashtag_screen_test.dart | 21 +- .../settings/data/video_repository_test.dart | 2 +- .../presentation/search_settings_test.dart | 32 +- .../presentation/settings_screen_test.dart | 84 +----- test/helpers/auth_fixtures.dart | 35 +++ test/helpers/feed_fixtures.dart | 75 +++++ test/helpers/notification_fixtures.dart | 23 ++ .../robots/notification_list_item_robot.dart | 53 ++++ test/helpers/router_harness.dart | 93 ++++++ test/helpers/settings_fixtures.dart | 54 ++++ .../utils => test/helpers}/test_utils.dart | 19 +- test/helpers/widget_harness.dart | 63 ++++ .../helpers/navigation_helpers_test.dart | 169 ++++------- .../widgets/error_state_test.dart | 12 +- 52 files changed, 809 insertions(+), 952 deletions(-) create mode 100644 test/helpers/auth_fixtures.dart create mode 100644 test/helpers/feed_fixtures.dart create mode 100644 test/helpers/notification_fixtures.dart create mode 100644 test/helpers/robots/notification_list_item_robot.dart create mode 100644 test/helpers/router_harness.dart create mode 100644 test/helpers/settings_fixtures.dart rename {lib/shared/utils => test/helpers}/test_utils.dart (72%) create mode 100644 test/helpers/widget_harness.dart diff --git a/.gitignore b/.gitignore index 5e8bf5e..516e7f1 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,4 @@ download/ AGENTS.md CLAUDE.md GEMINI.md +.vscode/ diff --git a/test/core/cache/poptart_cache_codecs_test.dart b/test/core/cache/poptart_cache_codecs_test.dart index 5af2547..e90fb56 100644 --- a/test/core/cache/poptart_cache_codecs_test.dart +++ b/test/core/cache/poptart_cache_codecs_test.dart @@ -1,9 +1,10 @@ -import 'package:poptart_core/poptart_core.dart'; import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/core/cache/poptart_cache_codecs.dart'; +import '../../helpers/feed_fixtures.dart'; + void main() { group('PoptartCacheCodecs', () { test('round-trips feed view posts through cache strings', () { @@ -47,18 +48,8 @@ void main() { }); } -FeedViewPost _feedViewPost() { - return FeedViewPost( - post: PostView( - uri: const AtUri('at://did:plc:author/app.bsky.feed.post/abc'), - cid: 'cid-123', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social'), - record: { - r'$type': 'app.bsky.feed.post', - 'text': 'Hello typed cache', - 'createdAt': DateTime.utc(2026, 5, 12).toIso8601String(), - }, - indexedAt: DateTime.utc(2026, 5, 12), - ), - ); -} +FeedViewPost _feedViewPost() => testFeedViewPost( + cid: 'cid-123', + record: testPostRecordJson(text: 'Hello typed cache', createdAt: DateTime.utc(2026, 5, 12)), + indexedAt: DateTime.utc(2026, 5, 12), +); diff --git a/test/core/network/atproto_host_resolver_test.dart b/test/core/network/atproto_host_resolver_test.dart index d3048ff..e73fc95 100644 --- a/test/core/network/atproto_host_resolver_test.dart +++ b/test/core/network/atproto_host_resolver_test.dart @@ -2,10 +2,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/core/network/atproto_host_resolver.dart'; import 'package:lazurite/features/auth/data/models/auth_models.dart'; +import '../../helpers/test_utils.dart'; + void main() { group('resolvePdsHost', () { test('uses stored PDS endpoint when restoring an opaque OAuth token', () { - final tokens = AuthTokens( + final tokens = testAuthTokens( accessToken: 'opaque-access-token', refreshToken: 'refresh-token', expiresAt: DateTime.utc(2030), @@ -24,7 +26,7 @@ void main() { }); test('falls back to stored service when opaque OAuth metadata is incomplete', () { - const tokens = AuthTokens( + final tokens = testAuthTokens( accessToken: 'opaque-access-token', refreshToken: 'refresh-token', did: 'did:plc:alice', diff --git a/test/core/network/oauth_session_restorer_test.dart b/test/core/network/oauth_session_restorer_test.dart index a91bbf7..f5094af 100644 --- a/test/core/network/oauth_session_restorer_test.dart +++ b/test/core/network/oauth_session_restorer_test.dart @@ -1,9 +1,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/core/network/oauth_session_restorer.dart'; import 'package:lazurite/features/auth/data/models/auth_models.dart'; -import 'package:lazurite/shared/utils/test_utils.dart'; import 'package:poptart_core/poptart_core.dart'; +import '../../helpers/test_utils.dart'; + void main() { group('restoreOAuthSessionFromTokens', () { test('passes stored PDS endpoint for opaque OAuth tokens', () { diff --git a/test/core/network/unauthorized_recovery_runner_test.dart b/test/core/network/unauthorized_recovery_runner_test.dart index 2dc2d9b..be1d6d8 100644 --- a/test/core/network/unauthorized_recovery_runner_test.dart +++ b/test/core/network/unauthorized_recovery_runner_test.dart @@ -1,8 +1,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/core/network/unauthorized_recovery_runner.dart'; -import 'package:lazurite/shared/utils/test_utils.dart'; import 'package:poptart_core/poptart_core.dart' show UnauthorizedException; +import '../../helpers/test_utils.dart'; + void main() { group('UnauthorizedRecoveryRunner', () { test('does not rebuild client when recovered tokens are for a different DID', () async { diff --git a/test/core/network/xrpc_client_factory_test.dart b/test/core/network/xrpc_client_factory_test.dart index 9f4dc0c..9d4b201 100644 --- a/test/core/network/xrpc_client_factory_test.dart +++ b/test/core/network/xrpc_client_factory_test.dart @@ -1,8 +1,9 @@ -import 'package:poptart_core/poptart_core.dart' as atp_core; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/core/network/xrpc_client_factory.dart'; import 'package:lazurite/features/auth/data/models/auth_models.dart'; -import 'package:lazurite/shared/utils/test_utils.dart'; +import 'package:poptart_core/poptart_core.dart' as atp_core; + +import '../../helpers/test_utils.dart'; void main() { group('xrpc_client_factory', () { diff --git a/test/core/widgets/lazurite_app_bar_test.dart b/test/core/widgets/lazurite_app_bar_test.dart index d8488c9..2cb84ba 100644 --- a/test/core/widgets/lazurite_app_bar_test.dart +++ b/test/core/widgets/lazurite_app_bar_test.dart @@ -3,15 +3,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/core/router/app_shell.dart'; -import 'package:lazurite/core/theme/app_theme.dart'; import 'package:lazurite/core/widgets/lazurite_app_bar.dart'; import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; -import 'package:lazurite/features/auth/data/models/auth_models.dart'; import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; import 'package:lazurite/features/settings/bloc/settings_cubit.dart'; import 'package:lazurite/features/settings/bloc/settings_state.dart'; import 'package:mocktail/mocktail.dart'; +import '../../helpers/settings_fixtures.dart'; +import '../../helpers/test_utils.dart'; + class MockAuthBloc extends MockBloc implements AuthBloc {} class MockConnectivityCubit extends MockCubit implements ConnectivityCubit {} @@ -23,11 +24,9 @@ void main() { late MockConnectivityCubit connectivityCubit; late MockSettingsCubit settingsCubit; - const tokens = AuthTokens( + final tokens = testAuthTokens( accessToken: 'access', refreshToken: 'refresh', - did: 'did:plc:test', - handle: 'test.bsky.social', displayName: 'River Tam', ); @@ -35,30 +34,16 @@ void main() { authBloc = MockAuthBloc(); connectivityCubit = MockConnectivityCubit(); settingsCubit = MockSettingsCubit(); - when(() => authBloc.state).thenReturn(const AuthState.authenticated(tokens)); - whenListen(authBloc, const Stream.empty(), initialState: const AuthState.authenticated(tokens)); + when(() => authBloc.state).thenReturn(AuthState.authenticated(tokens)); + whenListen(authBloc, const Stream.empty(), initialState: AuthState.authenticated(tokens)); when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); whenListen( connectivityCubit, const Stream.empty(), initialState: const ConnectivityState.online(), ); - when(() => settingsCubit.state).thenReturn( - const SettingsState( - themePalette: AppThemePalette.oxocarbon, - themeVariant: AppThemeVariant.dark, - useSystemTheme: false, - ), - ); - whenListen( - settingsCubit, - const Stream.empty(), - initialState: const SettingsState( - themePalette: AppThemePalette.oxocarbon, - themeVariant: AppThemeVariant.dark, - useSystemTheme: false, - ), - ); + when(() => settingsCubit.state).thenReturn(testSettingsState()); + whenListen(settingsCubit, const Stream.empty(), initialState: testSettingsState()); when(() => settingsCubit.setSimulateOffline(any())).thenAnswer((_) async {}); }); @@ -92,15 +77,9 @@ void main() { testWidgets('renders app bar when displayName is absent', (tester) async { authBloc = MockAuthBloc(); - const noDisplayName = AuthTokens( - accessToken: 'access', - refreshToken: 'refresh', - did: 'did:plc:test', - handle: 'alice.bsky.social', - displayName: null, - ); - when(() => authBloc.state).thenReturn(const AuthState.authenticated(noDisplayName)); - whenListen(authBloc, const Stream.empty(), initialState: const AuthState.authenticated(noDisplayName)); + final noDisplayName = testAuthTokens(accessToken: 'access', refreshToken: 'refresh', handle: 'alice.bsky.social'); + when(() => authBloc.state).thenReturn(AuthState.authenticated(noDisplayName)); + whenListen(authBloc, const Stream.empty(), initialState: AuthState.authenticated(noDisplayName)); await tester.pumpWidget(buildSubject(sectionLabel: 'Home')); await tester.pumpAndSettle(); diff --git a/test/features/auth/data/auth_repository_test.dart b/test/features/auth/data/auth_repository_test.dart index 9659dc0..2f9a335 100644 --- a/test/features/auth/data/auth_repository_test.dart +++ b/test/features/auth/data/auth_repository_test.dart @@ -6,7 +6,7 @@ import 'package:lazurite/core/database/app_database.dart'; import 'package:lazurite/core/network/slingshot_client.dart'; import 'package:lazurite/features/auth/data/auth_repository.dart'; import 'package:lazurite/features/auth/data/models/auth_models.dart'; -import 'package:lazurite/shared/utils/test_utils.dart'; +import '../../../helpers/test_utils.dart'; import 'package:mocktail/mocktail.dart'; import 'package:poptart_core/poptart_core.dart' as atcore; import 'package:poptart_oauth/poptart_oauth.dart'; @@ -1666,11 +1666,7 @@ void main() { resolveHandleDid: (_) async => 'did:plc:alice', resolveDidDocument: (_) async => const { 'service': [ - { - 'id': '#atproto_pds', - 'type': 'AtprotoPersonalDataServer', - 'serviceEndpoint': 'https://bsky.social', - }, + {'id': '#atproto_pds', 'type': 'AtprotoPersonalDataServer', 'serviceEndpoint': 'https://bsky.social'}, ], }, resolveAuthorizationServiceForPdsHost: (_) async => null, diff --git a/test/features/auth/data/models/auth_models_test.dart b/test/features/auth/data/models/auth_models_test.dart index 9479fd9..ce43dc6 100644 --- a/test/features/auth/data/models/auth_models_test.dart +++ b/test/features/auth/data/models/auth_models_test.dart @@ -96,7 +96,6 @@ void main() { expect(tokens.isExpired, isFalse); }); - test('should support value equality', () { const tokens1 = AuthTokens(accessToken: 'token', did: 'did:plc:abc123', handle: 'user.bsky.social'); const tokens2 = AuthTokens(accessToken: 'token', did: 'did:plc:abc123', handle: 'user.bsky.social'); diff --git a/test/features/feed/bloc/feed_bloc_test.dart b/test/features/feed/bloc/feed_bloc_test.dart index 4324a12..e3a31a2 100644 --- a/test/features/feed/bloc/feed_bloc_test.dart +++ b/test/features/feed/bloc/feed_bloc_test.dart @@ -1,12 +1,11 @@ -import 'package:poptart_core/poptart_core.dart'; import 'package:bloc_test/bloc_test.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; -import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/features/feed/bloc/feed_bloc.dart'; import 'package:lazurite/features/feed/data/feed_repository.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/feed_fixtures.dart'; + class MockFeedRepository extends Mock implements FeedRepository {} void main() { @@ -17,19 +16,7 @@ void main() { }); group('FeedBloc', () { - final samplePost = FeedViewPost( - post: PostView( - uri: const AtUri('at://did:plc:author/app.bsky.feed.post/abc'), - cid: 'cid-123', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social'), - record: { - r'$type': 'app.bsky.feed.post', - 'text': 'Hello world', - 'createdAt': DateTime.utc(2026, 3, 15).toIso8601String(), - }, - indexedAt: DateTime.utc(2026, 3, 15), - ), - ); + final samplePost = testFeedViewPost(cid: 'cid-123', record: testPostRecordJson(text: 'Hello world')); blocTest( 'loads a feed for the requested actor and filter', diff --git a/test/features/feed/cubit/post_thread_cubit_test.dart b/test/features/feed/cubit/post_thread_cubit_test.dart index eea5abd..2f8536c 100644 --- a/test/features/feed/cubit/post_thread_cubit_test.dart +++ b/test/features/feed/cubit/post_thread_cubit_test.dart @@ -1,5 +1,3 @@ -import 'package:poptart_core/poptart_core.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -7,6 +5,8 @@ import 'package:lazurite/features/feed/cubit/post_thread_cubit.dart'; import 'package:lazurite/features/feed/data/post_thread_repository.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/feed_fixtures.dart'; + class MockPostThreadRepository extends Mock implements PostThreadRepository {} void main() { @@ -19,17 +19,7 @@ void main() { const testUri = 'at://did:plc:author/app.bsky.feed.post/abc'; final sampleThread = ThreadViewPost( - post: PostView( - uri: const AtUri('at://did:plc:author/app.bsky.feed.post/abc'), - cid: 'cid-123', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social'), - record: { - r'$type': 'app.bsky.feed.post', - 'text': 'Test post', - 'createdAt': DateTime.utc(2026, 3, 15).toIso8601String(), - }, - indexedAt: DateTime.utc(2026, 3, 15), - ), + post: testPostView(cid: 'cid-123'), ); group('PostThreadCubit', () { diff --git a/test/features/feed/cubit/similar_posts_cubit_test.dart b/test/features/feed/cubit/similar_posts_cubit_test.dart index 346daa5..34d4b7e 100644 --- a/test/features/feed/cubit/similar_posts_cubit_test.dart +++ b/test/features/feed/cubit/similar_posts_cubit_test.dart @@ -1,4 +1,3 @@ -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -8,6 +7,8 @@ import 'package:lazurite/features/feed/cubit/similar_posts_cubit.dart'; import 'package:lazurite/features/feed/data/similar_posts_repository.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/feed_fixtures.dart'; + class MockBluesky extends Mock implements Bluesky {} void main() { @@ -82,14 +83,10 @@ ManyToManyItem _item(String otherSubject) => ManyToManyItem( otherSubject: otherSubject, ); -PostView _post(String uri) => PostView( - uri: AtUri(uri), +PostView _post(String uri) => testPostView( + uri: uri, cid: 'cid-$uri', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.example'), - record: { - r'$type': 'app.bsky.feed.post', - 'text': 'similar post', - 'createdAt': DateTime.utc(2026, 5, 23).toIso8601String(), - }, + author: testProfileViewBasic(handle: 'author.example'), + record: testPostRecordJson(text: 'similar post', createdAt: DateTime.utc(2026, 5, 23)), indexedAt: DateTime.utc(2026, 5, 23), ); diff --git a/test/features/feed/data/feed_repository_cache_test.dart b/test/features/feed/data/feed_repository_cache_test.dart index ef75ece..d88ce97 100644 --- a/test/features/feed/data/feed_repository_cache_test.dart +++ b/test/features/feed/data/feed_repository_cache_test.dart @@ -11,7 +11,7 @@ import 'package:http/http.dart' as http; import 'package:lazurite/core/cache/offline_cache_policy.dart'; import 'package:lazurite/core/database/app_database.dart'; import 'package:lazurite/features/feed/data/feed_repository.dart'; -import 'package:lazurite/shared/utils/test_utils.dart'; +import '../../../helpers/test_utils.dart'; import '../../../helpers/test_bluesky_client.dart'; diff --git a/test/features/feed/data/feed_repository_test.dart b/test/features/feed/data/feed_repository_test.dart index 6b85fbc..e525b7a 100644 --- a/test/features/feed/data/feed_repository_test.dart +++ b/test/features/feed/data/feed_repository_test.dart @@ -1,4 +1,3 @@ -import 'package:poptart_core/poptart_core.dart'; import 'package:bluesky_poptart/app/bsky/actor/defs.dart' hide ViewerState; import 'package:bluesky_poptart/app/bsky/actor/defs/viewer_state.dart' as actor_defs; import 'package:bluesky_poptart/app/bsky/embed/record.dart'; @@ -6,6 +5,9 @@ import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/features/feed/data/feed_repository.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:poptart_core/poptart_core.dart'; + +import '../../../helpers/feed_fixtures.dart'; class MockFeedRepository extends Mock implements FeedRepository {} @@ -17,18 +19,9 @@ void main() { }); group('FeedRepository contract', () { - final samplePost = FeedViewPost( - post: PostView( - uri: const AtUri('at://did:plc:author/app.bsky.feed.post/abc'), - cid: 'cid-123', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social'), - record: { - r'$type': 'app.bsky.feed.post', - 'text': 'Hello world', - 'createdAt': DateTime.utc(2026, 3, 15).toIso8601String(), - }, - indexedAt: DateTime.utc(2026, 3, 15), - ), + final samplePost = testFeedViewPost( + cid: 'cid-123', + record: testPostRecordJson(text: 'Hello world'), ); test('getTimeline returns FeedResult with posts and cursor', () async { @@ -69,23 +62,18 @@ void main() { ).thenAnswer((_) async => FeedResult(posts: [samplePost], cursor: 'next-cursor')); final result = await mockRepository.getFeed(feedUri: feedUri); - expect(result.posts.length, 1); }); test('getPreferences returns preferences list', () async { when(() => mockRepository.getPreferences()).thenAnswer((_) async => PreferencesResult(preferences: [])); - final result = await mockRepository.getPreferences(); - expect(result.preferences, isEmpty); }); test('putPreferences calls with correct parameters', () async { when(() => mockRepository.putPreferences(preferences: any(named: 'preferences'))).thenAnswer((_) async {}); - await mockRepository.putPreferences(preferences: []); - verify(() => mockRepository.putPreferences(preferences: [])).called(1); }); @@ -107,7 +95,6 @@ void main() { ).thenAnswer((_) async => [generator]); final result = await mockRepository.getSuggestedFeeds(); - expect(result.length, 1); expect(result.first.displayName, 'Discover'); }); @@ -124,9 +111,7 @@ void main() { ); when(() => mockRepository.getFeedGenerator(feedUri)).thenAnswer((_) async => generator); - final result = await mockRepository.getFeedGenerator(feedUri); - expect(result.displayName, 'Discover'); }); @@ -142,9 +127,7 @@ void main() { ); when(() => mockRepository.getFeedGenerators([feedUri])).thenAnswer((_) async => [generator]); - final result = await mockRepository.getFeedGenerators([feedUri]); - expect(result.length, 1); }); @@ -160,9 +143,7 @@ void main() { ); when(() => mockRepository.getFeedGenerator(feedUri)).thenAnswer((_) async => generator); - final result = await mockRepository.getFeedGenerator(feedUri); - expect(result.displayName, 'Discover'); }); @@ -178,17 +159,13 @@ void main() { ); when(() => mockRepository.getFeedGenerators([feedUri])).thenAnswer((_) async => [generator]); - final result = await mockRepository.getFeedGenerators([feedUri]); - expect(result.length, 1); }); test('getFeedGenerators returns empty list for empty input', () async { when(() => mockRepository.getFeedGenerators([])).thenAnswer((_) async => []); - final result = await mockRepository.getFeedGenerators([]); - expect(result, isEmpty); }); }); @@ -206,14 +183,12 @@ void main() { ); final result = FeedResult(posts: [post], cursor: 'cursor-1'); - expect(result.posts.length, 1); expect(result.cursor, 'cursor-1'); }); test('allows null cursor', () { final result = FeedResult(posts: const [], cursor: null); - expect(result.posts, isEmpty); expect(result.cursor, isNull); }); @@ -222,7 +197,6 @@ void main() { group('PreferencesResult', () { test('stores preferences list', () { final result = PreferencesResult(preferences: []); - expect(result.preferences, isEmpty); }); }); diff --git a/test/features/feed/data/post_thread_repository_cache_test.dart b/test/features/feed/data/post_thread_repository_cache_test.dart index 08899c3..463c9d3 100644 --- a/test/features/feed/data/post_thread_repository_cache_test.dart +++ b/test/features/feed/data/post_thread_repository_cache_test.dart @@ -10,7 +10,7 @@ import 'package:http/http.dart' as http; import 'package:lazurite/core/cache/offline_cache_policy.dart'; import 'package:lazurite/core/database/app_database.dart'; import 'package:lazurite/features/feed/data/post_thread_repository.dart'; -import 'package:lazurite/shared/utils/test_utils.dart'; +import '../../../helpers/test_utils.dart'; import '../../../helpers/test_bluesky_client.dart'; diff --git a/test/features/feed/data/post_thread_repository_test.dart b/test/features/feed/data/post_thread_repository_test.dart index c4ff4b2..6ba1b89 100644 --- a/test/features/feed/data/post_thread_repository_test.dart +++ b/test/features/feed/data/post_thread_repository_test.dart @@ -1,10 +1,10 @@ -import 'package:poptart_core/poptart_core.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/features/feed/data/post_thread_repository.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/feed_fixtures.dart'; + class MockPostThreadRepository extends Mock implements PostThreadRepository {} void main() { @@ -15,16 +15,9 @@ void main() { }); final sampleThreadViewPost = ThreadViewPost( - post: PostView( - uri: const AtUri('at://did:plc:author/app.bsky.feed.post/abc'), + post: testPostView( cid: 'cid-123', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social'), - record: { - r'$type': 'app.bsky.feed.post', - 'text': 'Hello world', - 'createdAt': DateTime.utc(2026, 3, 15).toIso8601String(), - }, - indexedAt: DateTime.utc(2026, 3, 15), + record: testPostRecordJson(text: 'Hello world'), ), ); @@ -42,15 +35,11 @@ void main() { test('getPostThread returns thread with parent', () async { const testUri = 'at://did:plc:author/app.bsky.feed.post/abc'; - final parentPost = PostView( - uri: const AtUri('at://did:plc:parent/app.bsky.feed.post/root'), + final parentPost = testPostView( + uri: 'at://did:plc:parent/app.bsky.feed.post/root', cid: 'cid-root', - author: const ProfileViewBasic(did: 'did:plc:parent', handle: 'parent.bsky.social'), - record: { - r'$type': 'app.bsky.feed.post', - 'text': 'Root post', - 'createdAt': DateTime.utc(2026, 3, 14).toIso8601String(), - }, + author: testProfileViewBasic(did: 'did:plc:parent', handle: 'parent.bsky.social'), + record: testPostRecordJson(text: 'Root post', createdAt: DateTime.utc(2026, 3, 14)), indexedAt: DateTime.utc(2026, 3, 14), ); final threadWithParent = ThreadViewPost( @@ -69,15 +58,11 @@ void main() { test('getPostThread returns thread with replies', () async { const testUri = 'at://did:plc:author/app.bsky.feed.post/abc'; - final replyPost = PostView( - uri: const AtUri('at://did:plc:reply/app.bsky.feed.post/reply1'), + final replyPost = testPostView( + uri: 'at://did:plc:reply/app.bsky.feed.post/reply1', cid: 'cid-reply', - author: const ProfileViewBasic(did: 'did:plc:reply', handle: 'reply.bsky.social'), - record: { - r'$type': 'app.bsky.feed.post', - 'text': 'Reply post', - 'createdAt': DateTime.utc(2026, 3, 15, 1).toIso8601String(), - }, + author: testProfileViewBasic(did: 'did:plc:reply', handle: 'reply.bsky.social'), + record: testPostRecordJson(text: 'Reply post', createdAt: DateTime.utc(2026, 3, 15, 1)), indexedAt: DateTime.utc(2026, 3, 15, 1), ); final threadWithReplies = ThreadViewPost( @@ -115,11 +100,9 @@ void main() { test('getPostThread with no parent returns thread without parent', () async { const testUri = 'at://did:plc:author/app.bsky.feed.post/abc'; final threadNoParent = ThreadViewPost(post: sampleThreadViewPost.post); - when(() => mockRepository.getPostThread(testUri)).thenAnswer((_) async => threadNoParent); final result = await mockRepository.getPostThread(testUri); - expect(result.parent, isNull); expect(result.replies, isNull); }); diff --git a/test/features/feed/presentation/feed_detail_screen_test.dart b/test/features/feed/presentation/feed_detail_screen_test.dart index 335ec11..4c7b597 100644 --- a/test/features/feed/presentation/feed_detail_screen_test.dart +++ b/test/features/feed/presentation/feed_detail_screen_test.dart @@ -1,14 +1,11 @@ import 'dart:async'; -import 'package:poptart_core/poptart_core.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; -import 'package:bluesky_poptart/app/bsky/feed/post.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:lazurite/core/theme/app_theme.dart'; import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; import 'package:lazurite/features/auth/data/models/auth_models.dart'; import 'package:lazurite/features/feed/data/feed_repository.dart'; @@ -16,6 +13,10 @@ import 'package:lazurite/features/feed/presentation/feed_detail_screen.dart'; import 'package:lazurite/features/settings/bloc/settings_cubit.dart'; import 'package:lazurite/features/settings/bloc/settings_state.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:poptart_core/poptart_core.dart'; + +import '../../../helpers/feed_fixtures.dart'; +import '../../../helpers/settings_fixtures.dart'; class MockFeedRepository extends Mock implements FeedRepository {} @@ -44,22 +45,8 @@ void main() { AuthTokens(accessToken: 'access', did: 'did:plc:me', handle: 'me.bsky.social'), ), ); - when(() => settingsCubit.state).thenReturn( - const SettingsState( - themePalette: AppThemePalette.oxocarbon, - themeVariant: AppThemeVariant.dark, - useSystemTheme: false, - ), - ); - whenListen( - settingsCubit, - const Stream.empty(), - initialState: const SettingsState( - themePalette: AppThemePalette.oxocarbon, - themeVariant: AppThemeVariant.dark, - useSystemTheme: false, - ), - ); + when(() => settingsCubit.state).thenReturn(testSettingsState()); + whenListen(settingsCubit, const Stream.empty(), initialState: testSettingsState()); }); Widget buildSubject({String? publicProviderKey}) { @@ -235,18 +222,13 @@ GeneratorView _generatorView({required String displayName, String? description}) ); } -FeedViewPost _feedViewPost() { - final record = FeedPostRecord(text: 'Public post body', createdAt: DateTime.utc(2026, 3, 16)); - return FeedViewPost( - post: PostView( - uri: const AtUri('at://did:plc:test/app.bsky.feed.post/xyz'), - cid: 'cid-xyz', - author: const ProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social', displayName: 'Test User'), - record: record.toJson(), - indexedAt: DateTime.utc(2026, 3, 16), - replyCount: 2, - repostCount: 3, - likeCount: 5, - ), - ); -} +FeedViewPost _feedViewPost() => testFeedViewPost( + uri: 'at://did:plc:test/app.bsky.feed.post/xyz', + cid: 'cid-xyz', + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social', displayName: 'Test User'), + record: testPostRecordJson(text: 'Public post body', createdAt: DateTime.utc(2026, 3, 16)), + indexedAt: DateTime.utc(2026, 3, 16), + replyCount: 2, + repostCount: 3, + likeCount: 5, +); diff --git a/test/features/feed/presentation/home_feed_screen_test.dart b/test/features/feed/presentation/home_feed_screen_test.dart index 71b6ddb..83285f2 100644 --- a/test/features/feed/presentation/home_feed_screen_test.dart +++ b/test/features/feed/presentation/home_feed_screen_test.dart @@ -84,7 +84,11 @@ void main() { when(() => connectivityCubit.state).thenReturn(connectivityState); whenListen(connectivityCubit, const Stream.empty(), initialState: connectivityState); when(() => settingsCubit.state).thenReturn(_settingsState(FeedLayout.comfortable)); - whenListen(settingsCubit, const Stream.empty(), initialState: _settingsState(FeedLayout.comfortable)); + whenListen( + settingsCubit, + const Stream.empty(), + initialState: _settingsState(FeedLayout.comfortable), + ); when(() => authBloc.state).thenReturn( const AuthState.authenticated(AuthTokens(accessToken: 'access', did: 'did:plc:test', handle: 'test.bsky.social')), ); diff --git a/test/features/feed/presentation/media/media_route_payload_codec_test.dart b/test/features/feed/presentation/media/media_route_payload_codec_test.dart index 5d23cfd..2c5cf8c 100644 --- a/test/features/feed/presentation/media/media_route_payload_codec_test.dart +++ b/test/features/feed/presentation/media/media_route_payload_codec_test.dart @@ -8,22 +8,14 @@ void main() { test('round-trips payload through URL-safe route location', () { final location = MediaRoutePayloadCodec.location( path: '/video', - payload: { - 'playlistUrl': 'https://example.com/video.m3u8', - 'isGif': true, - 'aspectRatio': 16 / 9, - }, + payload: {'playlistUrl': 'https://example.com/video.m3u8', 'isGif': true, 'aspectRatio': 16 / 9}, ); final uri = Uri.parse(location); final decoded = MediaRoutePayloadCodec.tryDecode(uri); expect(uri.path, '/video'); - expect(decoded, { - 'playlistUrl': 'https://example.com/video.m3u8', - 'isGif': true, - 'aspectRatio': 16 / 9, - }); + expect(decoded, {'playlistUrl': 'https://example.com/video.m3u8', 'isGif': true, 'aspectRatio': 16 / 9}); }); test('returns null for missing or malformed payloads', () { diff --git a/test/features/feed/presentation/post_card_with_actions_test.dart b/test/features/feed/presentation/post_card_with_actions_test.dart index e441ea5..cd40edb 100644 --- a/test/features/feed/presentation/post_card_with_actions_test.dart +++ b/test/features/feed/presentation/post_card_with_actions_test.dart @@ -1,9 +1,7 @@ import 'dart:async'; -import 'package:poptart_core/poptart_core.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; -import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:bloc_test/bloc_test.dart'; +import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,6 +11,9 @@ import 'package:lazurite/features/feed/cubit/saved_posts_cubit.dart'; import 'package:lazurite/features/feed/data/post_action_repository.dart'; import 'package:lazurite/features/feed/presentation/widgets/post_card_with_actions.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:poptart_core/poptart_core.dart'; + +import '../../../helpers/feed_fixtures.dart'; class MockPostActionRepository extends Mock implements PostActionRepository {} @@ -20,21 +21,11 @@ class MockSavedPostsCubit extends MockCubit implements SavedPos class MockConnectivityCubit extends MockCubit implements ConnectivityCubit {} -FeedViewPost _makePostView() { - return FeedViewPost( - post: PostView( - uri: const AtUri('at://did:plc:author/app.bsky.feed.post/abc123'), - cid: 'cid-abc123', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social'), - record: { - r'$type': 'app.bsky.feed.post', - 'text': 'Hello world', - 'createdAt': DateTime.utc(2026, 3, 15).toIso8601String(), - }, - indexedAt: DateTime.utc(2026, 3, 15), - ), - ); -} +FeedViewPost _makePostView() => testFeedViewPost( + uri: 'at://did:plc:author/app.bsky.feed.post/abc123', + cid: 'cid-abc123', + record: testPostRecordJson(text: 'Hello world'), +); void main() { setUpAll(() { diff --git a/test/features/feed/presentation/public_post_card_test.dart b/test/features/feed/presentation/public_post_card_test.dart index 0296b44..6684c2d 100644 --- a/test/features/feed/presentation/public_post_card_test.dart +++ b/test/features/feed/presentation/public_post_card_test.dart @@ -1,6 +1,4 @@ -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; -import 'package:bluesky_poptart/app/bsky/feed/post.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; @@ -8,7 +6,8 @@ import 'package:lazurite/core/theme/app_theme.dart'; import 'package:lazurite/features/feed/presentation/widgets/post_card.dart'; import 'package:lazurite/features/feed/presentation/widgets/post_card_with_actions.dart'; import 'package:lazurite/features/feed/presentation/widgets/public_post_card.dart'; -import 'package:poptart_core/poptart_core.dart'; + +import '../../../helpers/feed_fixtures.dart'; void main() { testWidgets('renders passive public counts and share action', (tester) async { @@ -116,18 +115,13 @@ Widget _buildSubject(FeedViewPost post, {required PostCardVariant variant}) => M ), ); -FeedViewPost _makePost() { - final record = FeedPostRecord(text: 'Public post', createdAt: DateTime.utc(2026, 3, 16)); - return FeedViewPost( - post: PostView( - uri: const AtUri('at://did:plc:test/app.bsky.feed.post/xyz'), - cid: 'cid-xyz', - author: const ProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social', displayName: 'Test User'), - record: record.toJson(), - indexedAt: DateTime.utc(2026, 3, 16), - replyCount: 2, - repostCount: 3, - likeCount: 5, - ), - ); -} +FeedViewPost _makePost() => testFeedViewPost( + uri: 'at://did:plc:test/app.bsky.feed.post/xyz', + cid: 'cid-xyz', + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social', displayName: 'Test User'), + record: testPostRecordJson(text: 'Public post', createdAt: DateTime.utc(2026, 3, 16)), + indexedAt: DateTime.utc(2026, 3, 16), + replyCount: 2, + repostCount: 3, + likeCount: 5, +); diff --git a/test/features/feed/presentation/similar_posts_section_test.dart b/test/features/feed/presentation/similar_posts_section_test.dart index 7402149..54ab522 100644 --- a/test/features/feed/presentation/similar_posts_section_test.dart +++ b/test/features/feed/presentation/similar_posts_section_test.dart @@ -1,4 +1,3 @@ -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -10,6 +9,8 @@ import 'package:lazurite/features/feed/data/similar_posts_repository.dart'; import 'package:lazurite/features/feed/presentation/widgets/similar_posts_section.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/feed_fixtures.dart'; + class MockBluesky extends Mock implements Bluesky {} void main() { @@ -59,14 +60,10 @@ ManyToManyItem _item(String otherSubject) => ManyToManyItem( otherSubject: otherSubject, ); -PostView _post(String uri) => PostView( - uri: AtUri(uri), +PostView _post(String uri) => testPostView( + uri: uri, cid: 'cid-$uri', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.example'), - record: { - r'$type': 'app.bsky.feed.post', - 'text': 'similar post body', - 'createdAt': DateTime.utc(2026, 5, 23).toIso8601String(), - }, + author: testProfileViewBasic(handle: 'author.example'), + record: testPostRecordJson(text: 'similar post body', createdAt: DateTime.utc(2026, 5, 23)), indexedAt: DateTime.utc(2026, 5, 23), ); diff --git a/test/features/feed/presentation/trending_screen_test.dart b/test/features/feed/presentation/trending_screen_test.dart index 94bc273..3a0e433 100644 --- a/test/features/feed/presentation/trending_screen_test.dart +++ b/test/features/feed/presentation/trending_screen_test.dart @@ -5,7 +5,6 @@ import 'package:bluesky_poptart/app/bsky/unspecced/defs.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:lazurite/core/theme/app_theme.dart'; import 'package:lazurite/features/feed/data/feed_repository.dart'; import 'package:lazurite/features/feed/data/trending_join.dart'; import 'package:lazurite/features/feed/presentation/trending_screen.dart'; @@ -13,6 +12,8 @@ import 'package:lazurite/features/settings/bloc/settings_cubit.dart'; import 'package:lazurite/features/settings/bloc/settings_state.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/settings_fixtures.dart'; + class MockFeedRepository extends Mock implements FeedRepository {} class MockSettingsCubit extends MockCubit implements SettingsCubit {} @@ -24,39 +25,22 @@ void main() { setUp(() { feedRepository = MockFeedRepository(); settingsCubit = MockSettingsCubit(); - when(() => settingsCubit.state).thenReturn( - const SettingsState( - themePalette: AppThemePalette.oxocarbon, - themeVariant: AppThemeVariant.dark, - useSystemTheme: false, - ), - ); - whenListen( - settingsCubit, - const Stream.empty(), - initialState: const SettingsState( - themePalette: AppThemePalette.oxocarbon, - themeVariant: AppThemeVariant.dark, - useSystemTheme: false, - ), - ); + when(() => settingsCubit.state).thenReturn(testSettingsState()); + whenListen(settingsCubit, const Stream.empty(), initialState: testSettingsState()); }); - Widget buildSubject() { - return MaterialApp( - home: RepositoryProvider.value( - value: feedRepository, - child: BlocProvider.value(value: settingsCubit, child: const TrendingScreen()), - ), - ); - } + Widget buildSubject() => MaterialApp( + home: RepositoryProvider.value( + value: feedRepository, + child: BlocProvider.value(value: settingsCubit, child: const TrendingScreen()), + ), + ); testWidgets('shows loading state while data is in flight', (tester) async { final completer = Completer(); when(() => feedRepository.getTrendingScreenData(limit: any(named: 'limit'))).thenAnswer((_) => completer.future); await tester.pumpWidget(buildSubject()); - expect(find.byType(CircularProgressIndicator), findsOneWidget); completer.complete(_data(topics: [_topic('Dart', '/topic/dart')], suggested: const [])); }); @@ -68,7 +52,6 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('No trending topics right now'), findsOneWidget); }); @@ -79,7 +62,6 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('Topics'), findsOneWidget); expect(find.text('Suggested'), findsNothing); }); @@ -91,7 +73,6 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('Topics'), findsOneWidget); expect(find.text('Suggested'), findsOneWidget); expect(find.text('Dart'), findsOneWidget); @@ -105,7 +86,6 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('Metadata temporarily unavailable'), findsOneWidget); }); @@ -114,13 +94,10 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('Failed to load trending'), findsOneWidget); expect(find.text('Retry'), findsOneWidget); - await tester.tap(find.text('Retry')); await tester.pumpAndSettle(); - verify(() => feedRepository.getTrendingScreenData(limit: 10)).called(greaterThanOrEqualTo(2)); }); } @@ -129,13 +106,11 @@ TrendingScreenData _data({ required List topics, required List suggested, bool metadataUnavailable = false, -}) { - return TrendingScreenData( - topics: topics.map((topic) => EnrichedTrendingTopic(topic: topic)).toList(growable: false), - suggested: suggested.map((topic) => EnrichedTrendingTopic(topic: topic)).toList(growable: false), - metadataUnavailable: metadataUnavailable, - ); -} +}) => TrendingScreenData( + topics: topics.map((topic) => EnrichedTrendingTopic(topic: topic)).toList(growable: false), + suggested: suggested.map((topic) => EnrichedTrendingTopic(topic: topic)).toList(growable: false), + metadataUnavailable: metadataUnavailable, +); TrendingTopic _topic(String topic, String link) => TrendingTopic(topic: topic, displayName: topic, link: link, description: 'About $topic'); diff --git a/test/features/notifications/bloc/notification_bloc_test.dart b/test/features/notifications/bloc/notification_bloc_test.dart index 9528868..e837ee2 100644 --- a/test/features/notifications/bloc/notification_bloc_test.dart +++ b/test/features/notifications/bloc/notification_bloc_test.dart @@ -1,4 +1,3 @@ -import 'package:poptart_core/poptart_core.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/notification/list_notifications.dart' as bsky; @@ -7,6 +6,8 @@ import 'package:lazurite/features/notifications/bloc/notification_bloc.dart'; import 'package:lazurite/features/notifications/data/notification_repository.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/notification_fixtures.dart'; + class MockNotificationRepository extends Mock implements NotificationRepository {} void main() { @@ -17,15 +18,7 @@ void main() { }); group('NotificationBloc', () { - final sampleNotification = bsky.Notification( - uri: AtUri.parse('at://did:plc:author/app.bsky.feed.post/abc'), - cid: 'cid-123', - author: const ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.like), - record: {r'$type': 'app.bsky.feed.post', 'text': 'Hello world'}, - isRead: false, - indexedAt: DateTime.utc(2026, 3, 15), - ); + final sampleNotification = testNotification(); blocTest( 'emits loading and loaded when NotificationsRequested succeeds', @@ -74,12 +67,12 @@ void main() { build: () => NotificationBloc(notificationRepository: mockNotificationRepository), seed: () => NotificationState.loaded(notifications: [sampleNotification], cursor: 'cursor-1', hasMore: true), setUp: () { - final secondNotification = bsky.Notification( - uri: AtUri.parse('at://did:plc:author2/app.bsky.feed.post/def'), + final secondNotification = testNotification( + uri: 'at://did:plc:author2/app.bsky.feed.post/def', cid: 'cid-456', author: const ProfileView(did: 'did:plc:author2', handle: 'author2.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.follow), - record: {}, + reason: bsky.KnownNotificationReason.follow, + record: const {}, isRead: true, indexedAt: DateTime.utc(2026, 3, 14), ); @@ -152,13 +145,12 @@ void main() { build: () => NotificationBloc(notificationRepository: mockNotificationRepository), seed: () => NotificationState.loaded(notifications: [sampleNotification], cursor: 'old-cursor', hasMore: true), setUp: () { - final newNotification = bsky.Notification( - uri: AtUri.parse('at://did:plc:new/app.bsky.feed.post/new'), + final newNotification = testNotification( + uri: 'at://did:plc:new/app.bsky.feed.post/new', cid: 'cid-new', author: const ProfileView(did: 'did:plc:new', handle: 'new.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.repost), - record: {}, - isRead: false, + reason: bsky.KnownNotificationReason.repost, + record: const {}, indexedAt: DateTime.utc(2026, 3, 16), ); when( diff --git a/test/features/notifications/data/notification_repository_test.dart b/test/features/notifications/data/notification_repository_test.dart index 2e9bbac..24ae2b6 100644 --- a/test/features/notifications/data/notification_repository_test.dart +++ b/test/features/notifications/data/notification_repository_test.dart @@ -1,12 +1,14 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:lazurite/core/network/poptart_client_adapter.dart'; -import 'package:lazurite/features/auth/data/models/auth_models.dart'; import 'package:lazurite/features/notifications/data/notification_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/notification/list_notifications.dart' as bsky; +import '../../../helpers/notification_fixtures.dart'; +import '../../../helpers/test_utils.dart'; + class MockNotificationRepository extends Mock implements NotificationRepository {} void main() { @@ -17,15 +19,7 @@ void main() { }); group('NotificationRepository contract', () { - final sampleNotification = bsky.Notification( - uri: AtUri.parse('at://did:plc:author/app.bsky.feed.post/abc'), - cid: 'cid-123', - author: const ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.like), - record: {r'$type': 'app.bsky.feed.post', 'text': 'Hello world'}, - isRead: false, - indexedAt: DateTime.utc(2026, 3, 15), - ); + final sampleNotification = testNotification(); test('listNotifications returns NotificationListResult with notifications and cursor', () async { when( @@ -119,12 +113,7 @@ void main() { bluesky: initialClient, onUnauthorized: () async { recoveryCalls += 1; - return const AuthTokens( - accessToken: 'fresh-access', - refreshToken: 'fresh-refresh', - did: 'did:plc:test', - handle: 'test.bsky.social', - ); + return testAuthTokens(accessToken: 'fresh-access', refreshToken: 'fresh-refresh'); }, blueskyClientFactory: (_) => refreshedClient, ); @@ -140,12 +129,12 @@ void main() { group('NotificationListResult', () { test('stores notifications and cursor', () { - final notification = bsky.Notification( - uri: AtUri.parse('at://did:plc:test/app.bsky.notification/1'), + final notification = testNotification( + uri: 'at://did:plc:test/app.bsky.notification/1', cid: 'cid', author: const ProfileView(did: 'did:plc:test', handle: 'test.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.follow), - record: {}, + reason: bsky.KnownNotificationReason.follow, + record: const {}, isRead: true, indexedAt: DateTime.now(), ); diff --git a/test/features/notifications/domain/notification_deep_link_navigator_test.dart b/test/features/notifications/domain/notification_deep_link_navigator_test.dart index 92ff987..d8923f8 100644 --- a/test/features/notifications/domain/notification_deep_link_navigator_test.dart +++ b/test/features/notifications/domain/notification_deep_link_navigator_test.dart @@ -4,6 +4,8 @@ import 'package:go_router/go_router.dart'; import 'package:lazurite/features/notifications/domain/notification_deep_link_navigator.dart'; import 'package:lazurite/features/notifications/domain/notification_local_models.dart'; +import '../../../helpers/widget_harness.dart'; + void main() { testWidgets('go navigation opens profile route from notification deep link', (tester) async { final router = GoRouter( @@ -19,15 +21,13 @@ void main() { ], ); - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); + await pumpTestRouterApp(tester, router); NotificationDeepLinkNavigator.navigate( router, const NotificationDeepLink(route: '/profile/did%3Aplc%3Aalice', navigationMode: NotificationTapNavigationMode.go), ); await tester.pumpAndSettle(); - expect(find.text('profile:did:plc:alice'), findsOneWidget); }); @@ -45,9 +45,7 @@ void main() { ], ); - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); - + await pumpTestRouterApp(tester, router); NotificationDeepLinkNavigator.navigate( router, const NotificationDeepLink( @@ -55,8 +53,8 @@ void main() { navigationMode: NotificationTapNavigationMode.push, ), ); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); expect(find.text('post:at://did:plc:test/app.bsky.feed.post/1'), findsOneWidget); }); } diff --git a/test/features/notifications/domain/notification_local_mappers_test.dart b/test/features/notifications/domain/notification_local_mappers_test.dart index bc1f73d..82647fe 100644 --- a/test/features/notifications/domain/notification_local_mappers_test.dart +++ b/test/features/notifications/domain/notification_local_mappers_test.dart @@ -1,20 +1,17 @@ import 'package:poptart_core/poptart_core.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/notification/list_notifications.dart' as bsky; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/features/notifications/domain/notification_local_mappers.dart'; import 'package:lazurite/features/notifications/domain/notification_local_models.dart'; +import '../../../helpers/notification_fixtures.dart'; + void main() { group('NotificationLocalMapper', () { test('maps follow notifications to profile route with go navigation', () { - final notification = bsky.Notification( - uri: AtUri.parse('at://did:plc:author/app.bsky.feed.post/abc'), - cid: 'cid-123', - author: const ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.follow), + final notification = testNotification( + reason: bsky.KnownNotificationReason.follow, record: const {}, - isRead: false, indexedAt: DateTime.utc(2026, 5, 1, 12), ); @@ -28,14 +25,11 @@ void main() { test('maps like notifications to post route using reasonSubject', () { final reasonSubject = AtUri.parse('at://did:plc:target/app.bsky.feed.post/xyz'); - final notification = bsky.Notification( - uri: AtUri.parse('at://did:plc:author/app.bsky.feed.like/abc'), - cid: 'cid-123', - author: const ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.like), + final notification = testNotification( + uri: 'at://did:plc:author/app.bsky.feed.like/abc', + reason: bsky.KnownNotificationReason.like, reasonSubject: reasonSubject, record: const {}, - isRead: false, indexedAt: DateTime.utc(2026, 5, 1, 12), ); @@ -49,14 +43,11 @@ void main() { test('maps like-via-repost notifications to post route using reasonSubject', () { final reasonSubject = AtUri.parse('at://did:plc:target/app.bsky.feed.post/reposted-post'); - final notification = bsky.Notification( - uri: AtUri.parse('at://did:plc:author/app.bsky.feed.like/def'), - cid: 'cid-123', - author: const ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.likeViaRepost), + final notification = testNotification( + uri: 'at://did:plc:author/app.bsky.feed.like/def', + reason: bsky.KnownNotificationReason.likeViaRepost, reasonSubject: reasonSubject, record: const {}, - isRead: false, indexedAt: DateTime.utc(2026, 5, 1, 12), ); @@ -71,14 +62,11 @@ void main() { test('maps starterpack-joined notifications to starter pack detail route', () { final starterPackUri = AtUri.parse('at://did:plc:author/app.bsky.graph.starterpack/sp1'); - final notification = bsky.Notification( - uri: AtUri.parse('at://did:plc:author/app.bsky.graph.starterpackjoin/abc'), - cid: 'cid-123', - author: const ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.starterpackJoined), + final notification = testNotification( + uri: 'at://did:plc:author/app.bsky.graph.starterpackjoin/abc', + reason: bsky.KnownNotificationReason.starterpackJoined, reasonSubject: starterPackUri, record: const {}, - isRead: false, indexedAt: DateTime.utc(2026, 5, 1, 12), ); diff --git a/test/features/notifications/domain/push_registration_service_test.dart b/test/features/notifications/domain/push_registration_service_test.dart index 7e9da9f..3bfdc57 100644 --- a/test/features/notifications/domain/push_registration_service_test.dart +++ b/test/features/notifications/domain/push_registration_service_test.dart @@ -5,7 +5,7 @@ import 'package:lazurite/features/auth/data/models/auth_models.dart'; import 'package:lazurite/features/notifications/data/notification_repository.dart'; import 'package:lazurite/features/notifications/domain/push_registration_service.dart'; import 'package:lazurite/features/notifications/domain/push_token_provider.dart'; -import 'package:lazurite/shared/utils/test_utils.dart'; +import '../../../helpers/test_utils.dart'; import 'package:poptart_core/poptart_core.dart' show HttpMethod; import 'package:mocktail/mocktail.dart'; diff --git a/test/features/notifications/presentation/widgets/notification_list_item_test.dart b/test/features/notifications/presentation/widgets/notification_list_item_test.dart index 8f901f7..49c2d88 100644 --- a/test/features/notifications/presentation/widgets/notification_list_item_test.dart +++ b/test/features/notifications/presentation/widgets/notification_list_item_test.dart @@ -1,265 +1,115 @@ -import 'package:poptart_core/poptart_core.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/notification/list_notifications.dart' as bsky; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:go_router/go_router.dart'; -import 'package:lazurite/features/notifications/presentation/widgets/notification_list_item.dart'; +import 'package:poptart_core/poptart_core.dart'; -bsky.Notification _makeNotification({required bsky.KnownNotificationReason reason, AtUri? uri, AtUri? reasonSubject}) { - return bsky.Notification( - uri: uri ?? AtUri.parse('at://did:plc:liker/app.bsky.feed.like/abc'), - cid: 'cid-abc', - author: const ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), - reason: bsky.NotificationReason.knownValue(data: reason), - reasonSubject: reasonSubject, - record: const {}, - isRead: true, - indexedAt: DateTime.utc(2026, 3, 17), - ); -} +import '../../../../helpers/robots/notification_list_item_robot.dart'; void main() { group('NotificationListItem tap navigation', () { testWidgets('follow notification navigates to author profile', (tester) async { - final notification = _makeNotification(reason: bsky.KnownNotificationReason.follow); - String? pushedRoute; - - final router = GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (context, state) => Scaffold(body: NotificationListItem(notification: notification)), - ), - GoRoute( - path: '/profile/:actor', - builder: (context, state) { - pushedRoute = state.uri.toString(); - return const Scaffold(body: Text('profile')); - }, - ), - ], + final robot = NotificationListItemRobot(tester); + await robot.pump( + notification: testNotification(reason: bsky.KnownNotificationReason.follow), + targetPath: '/profile/:actor', + target: const Scaffold(body: Text('profile')), ); - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); - - await tester.tap(find.byType(NotificationListItem)); - await tester.pumpAndSettle(); - - expect(pushedRoute, isNotNull); - expect(Uri.parse(pushedRoute!).path, '/profile/${Uri.encodeComponent('did:plc:author')}'); + await robot.tapItem(); + robot.expectProfileRoute('did:plc:author'); }); testWidgets('like notification uses reasonSubject to navigate to post', (tester) async { final postUri = AtUri.parse('at://did:plc:owner/app.bsky.feed.post/post123'); - final notification = _makeNotification( - reason: bsky.KnownNotificationReason.like, - uri: AtUri.parse('at://did:plc:liker/app.bsky.feed.like/abc'), - reasonSubject: postUri, - ); - String? pushedRoute; - - final router = GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (context, state) => Scaffold(body: NotificationListItem(notification: notification)), - ), - GoRoute( - path: '/post', - builder: (context, state) { - pushedRoute = state.uri.toString(); - return const Scaffold(body: Text('post thread')); - }, - ), - ], + final robot = NotificationListItemRobot(tester); + await robot.pump( + notification: testNotification( + reason: bsky.KnownNotificationReason.like, + uri: AtUri.parse('at://did:plc:liker/app.bsky.feed.like/abc'), + reasonSubject: postUri, + ), + targetPath: '/post', + target: const Scaffold(body: Text('post thread')), ); - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); - - await tester.tap(find.byType(NotificationListItem)); - await tester.pumpAndSettle(); - - expect(pushedRoute, isNotNull); - expect(Uri.parse(pushedRoute!).path, '/post'); - expect(Uri.decodeComponent(Uri.parse(pushedRoute!).queryParameters['uri']!), postUri.toString()); + await robot.tapItem(); + robot.expectPostRoute(postUri); }); testWidgets('repost notification uses reasonSubject to navigate to post', (tester) async { final postUri = AtUri.parse('at://did:plc:owner/app.bsky.feed.post/post456'); - final notification = _makeNotification( - reason: bsky.KnownNotificationReason.repost, - uri: AtUri.parse('at://did:plc:reposter/app.bsky.feed.repost/repost1'), - reasonSubject: postUri, - ); - String? pushedRoute; - - final router = GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (context, state) => Scaffold(body: NotificationListItem(notification: notification)), - ), - GoRoute( - path: '/post', - builder: (context, state) { - pushedRoute = state.uri.toString(); - return const Scaffold(body: Text('post thread')); - }, - ), - ], + final robot = NotificationListItemRobot(tester); + await robot.pump( + notification: testNotification( + reason: bsky.KnownNotificationReason.repost, + uri: AtUri.parse('at://did:plc:reposter/app.bsky.feed.repost/repost1'), + reasonSubject: postUri, + ), + targetPath: '/post', + target: const Scaffold(body: Text('post thread')), ); - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); - - await tester.tap(find.byType(NotificationListItem)); - await tester.pumpAndSettle(); - - expect(pushedRoute, isNotNull); - expect(Uri.parse(pushedRoute!).path, '/post'); - expect(Uri.decodeComponent(Uri.parse(pushedRoute!).queryParameters['uri']!), postUri.toString()); + await robot.tapItem(); + robot.expectPostRoute(postUri); }); testWidgets('like-via-repost notification uses reasonSubject to navigate to post', (tester) async { final postUri = AtUri.parse('at://did:plc:owner/app.bsky.feed.post/post789'); - final notification = _makeNotification( - reason: bsky.KnownNotificationReason.likeViaRepost, - uri: AtUri.parse('at://did:plc:liker/app.bsky.feed.like/like-via-repost'), - reasonSubject: postUri, + final robot = NotificationListItemRobot(tester); + await robot.pump( + notification: testNotification( + reason: bsky.KnownNotificationReason.likeViaRepost, + uri: AtUri.parse('at://did:plc:liker/app.bsky.feed.like/like-via-repost'), + reasonSubject: postUri, + ), + targetPath: '/post', + target: const Scaffold(body: Text('post thread')), ); - String? pushedRoute; - final router = GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (context, state) => Scaffold(body: NotificationListItem(notification: notification)), - ), - GoRoute( - path: '/post', - builder: (context, state) { - pushedRoute = state.uri.toString(); - return const Scaffold(body: Text('post thread')); - }, - ), - ], - ); - - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); - - await tester.tap(find.byType(NotificationListItem)); - await tester.pumpAndSettle(); - - expect(pushedRoute, isNotNull); - expect(Uri.parse(pushedRoute!).path, '/post'); - expect(Uri.decodeComponent(Uri.parse(pushedRoute!).queryParameters['uri']!), postUri.toString()); + await robot.tapItem(); + robot.expectPostRoute(postUri); }); testWidgets('starterpack-joined notification navigates to starter pack detail route', (tester) async { final starterPackUri = AtUri.parse('at://did:plc:author/app.bsky.graph.starterpack/sp1'); - final notification = _makeNotification( - reason: bsky.KnownNotificationReason.starterpackJoined, - reasonSubject: starterPackUri, + final robot = NotificationListItemRobot(tester); + await robot.pump( + notification: testNotification( + reason: bsky.KnownNotificationReason.starterpackJoined, + reasonSubject: starterPackUri, + ), + targetPath: '/starter-pack', + target: const Scaffold(body: Text('starter pack detail')), ); - String? pushedRoute; - - final router = GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (context, state) => Scaffold(body: NotificationListItem(notification: notification)), - ), - GoRoute( - path: '/starter-pack', - builder: (context, state) { - pushedRoute = state.uri.toString(); - return const Scaffold(body: Text('starter pack detail')); - }, - ), - ], - ); - - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); - - await tester.tap(find.byType(NotificationListItem)); - await tester.pumpAndSettle(); - expect(pushedRoute, isNotNull); - expect(Uri.parse(pushedRoute!).path, '/starter-pack'); - expect(Uri.decodeComponent(Uri.parse(pushedRoute!).queryParameters['uri']!), starterPackUri.toString()); + await robot.tapItem(); + robot.expectStarterPackRoute(starterPackUri); }); testWidgets('like notification falls back to uri when reasonSubject is null', (tester) async { final likeUri = AtUri.parse('at://did:plc:liker/app.bsky.feed.like/fallback'); - final notification = _makeNotification( - reason: bsky.KnownNotificationReason.like, - uri: likeUri, - reasonSubject: null, + final robot = NotificationListItemRobot(tester); + await robot.pump( + notification: testNotification(reason: bsky.KnownNotificationReason.like, uri: likeUri, reasonSubject: null), + targetPath: '/post', + target: const Scaffold(body: Text('post thread')), ); - String? pushedRoute; - final router = GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (context, state) => Scaffold(body: NotificationListItem(notification: notification)), - ), - GoRoute( - path: '/post', - builder: (context, state) { - pushedRoute = state.uri.toString(); - return const Scaffold(body: Text('post thread')); - }, - ), - ], - ); - - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); - - await tester.tap(find.byType(NotificationListItem)); - await tester.pumpAndSettle(); - - expect(pushedRoute, isNotNull); - expect(Uri.parse(pushedRoute!).path, '/post'); - expect(Uri.decodeComponent(Uri.parse(pushedRoute!).queryParameters['uri']!), likeUri.toString()); + await robot.tapItem(); + robot.expectPostRoute(likeUri); }); testWidgets('reply notification navigates to post using notification uri', (tester) async { final replyUri = AtUri.parse('at://did:plc:replier/app.bsky.feed.post/reply1'); - final notification = _makeNotification(reason: bsky.KnownNotificationReason.reply, uri: replyUri); - String? pushedRoute; - - final router = GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (context, state) => Scaffold(body: NotificationListItem(notification: notification)), - ), - GoRoute( - path: '/post', - builder: (context, state) { - pushedRoute = state.uri.toString(); - return const Scaffold(body: Text('post thread')); - }, - ), - ], + final robot = NotificationListItemRobot(tester); + await robot.pump( + notification: testNotification(reason: bsky.KnownNotificationReason.reply, uri: replyUri), + targetPath: '/post', + target: const Scaffold(body: Text('post thread')), ); - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); - - await tester.tap(find.byType(NotificationListItem)); - await tester.pumpAndSettle(); - - expect(pushedRoute, isNotNull); - expect(Uri.parse(pushedRoute!).path, '/post'); - expect(Uri.decodeComponent(Uri.parse(pushedRoute!).queryParameters['uri']!), replyUri.toString()); + await robot.tapItem(); + robot.expectPostRoute(replyUri); }); }); } diff --git a/test/features/profile/data/profile_repository_test.dart b/test/features/profile/data/profile_repository_test.dart index 091915e..e07536b 100644 --- a/test/features/profile/data/profile_repository_test.dart +++ b/test/features/profile/data/profile_repository_test.dart @@ -13,7 +13,7 @@ import 'package:http/http.dart' as http; import 'package:lazurite/core/database/app_database.dart'; import 'package:lazurite/core/network/poptart_client_adapter.dart' show Bluesky; import 'package:lazurite/features/profile/data/profile_repository.dart'; -import 'package:lazurite/shared/utils/test_utils.dart'; +import '../../../helpers/test_utils.dart'; import 'package:poptart_core/poptart_core.dart' as atp_core; import 'package:poptart_lex/com/atproto/repo/get_record.dart'; import 'package:poptart_lex/com/atproto/repo/put_record.dart'; diff --git a/test/features/public/data/public_content_repository_test.dart b/test/features/public/data/public_content_repository_test.dart index c490a43..bdc06e2 100644 --- a/test/features/public/data/public_content_repository_test.dart +++ b/test/features/public/data/public_content_repository_test.dart @@ -8,6 +8,8 @@ import 'package:lazurite/features/search/data/search_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:poptart_core/poptart_core.dart' as atcore; +import '../../../helpers/feed_fixtures.dart'; + class MockFeedRepository extends Mock implements FeedRepository {} class MockSearchRepository extends Mock implements SearchRepository {} @@ -77,7 +79,6 @@ void main() { ); final discover = await repository.loadDiscover(); - expect(discover.posts.single.post.uri.toString(), 'at://did:plc:author/app.bsky.feed.post/blacksky-trending'); expect(discover.cursor, 'next-blacksky'); verify( @@ -99,7 +100,6 @@ void main() { ); final result = await repository.loadFeeds(); - expect(result.feeds.single.displayName, 'Feed blacksky'); final captured = verify(() => feedRepository.getFeedGenerators(captureAny())).captured.single as List; expect(captured.map((uri) => uri.toString()), [ @@ -134,35 +134,28 @@ void main() { ); final result = await repository.searchFeeds(query: 'news'); - expect(result.feeds.single.displayName, 'Feed news'); expect(result.cursor, 'next'); verify(() => searchRepository.searchFeedGenerators(query: 'news', cursor: null, limit: 25)).called(1); }); } -GeneratorView _feed(String rkey) { - return GeneratorView( - uri: atcore.AtUri.parse('at://did:plc:feed/app.bsky.feed.generator/$rkey'), - cid: 'cid-$rkey', - did: 'did:web:feeds.example', - creator: const ProfileView(did: 'did:plc:feed', handle: 'feeds.example'), - displayName: 'Feed $rkey', - indexedAt: DateTime.utc(2026, 5, 18), - ); -} - -FeedViewPost _post(String rkey) { - return FeedViewPost( - post: PostView( - uri: atcore.AtUri.parse('at://did:plc:author/app.bsky.feed.post/$rkey'), - cid: 'cid-$rkey', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social', displayName: 'Author'), - record: {r'$type': 'app.bsky.feed.post', 'text': 'Post $rkey', 'createdAt': '2026-05-20T12:00:00.000Z'}, - indexedAt: DateTime.utc(2026, 5, 20), - replyCount: 0, - repostCount: 0, - likeCount: 0, - ), - ); -} +GeneratorView _feed(String rkey) => GeneratorView( + uri: atcore.AtUri.parse('at://did:plc:feed/app.bsky.feed.generator/$rkey'), + cid: 'cid-$rkey', + did: 'did:web:feeds.example', + creator: const ProfileView(did: 'did:plc:feed', handle: 'feeds.example'), + displayName: 'Feed $rkey', + indexedAt: DateTime.utc(2026, 5, 18), +); + +FeedViewPost _post(String rkey) => testFeedViewPost( + uri: 'at://did:plc:author/app.bsky.feed.post/$rkey', + cid: 'cid-$rkey', + author: testProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social', displayName: 'Author'), + record: testPostRecordJson(text: 'Post $rkey', createdAt: DateTime.utc(2026, 5, 20, 12)), + indexedAt: DateTime.utc(2026, 5, 20), + replyCount: 0, + repostCount: 0, + likeCount: 0, +); diff --git a/test/features/public/presentation/public_home_screen_test.dart b/test/features/public/presentation/public_home_screen_test.dart index bac56e8..3b90f4c 100644 --- a/test/features/public/presentation/public_home_screen_test.dart +++ b/test/features/public/presentation/public_home_screen_test.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; -import 'package:bluesky_poptart/app/bsky/feed/post.dart'; import 'package:bluesky_poptart/app/bsky/unspecced/defs.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -15,6 +14,8 @@ import 'package:lazurite/features/public/presentation/public_route_state.dart'; import 'package:mocktail/mocktail.dart'; import 'package:poptart_core/poptart_core.dart' as atcore; +import '../../../helpers/feed_fixtures.dart'; + class MockPublicContentRepository extends Mock implements PublicContentRepository {} class _MapPublicContentRepositoryResolver implements PublicContentRepositoryResolver { @@ -209,12 +210,10 @@ void main() { ), ); await tester.pumpAndSettle(); - expect(find.byKey(const ValueKey('public_post_card_footer')), findsOneWidget); await tester.tap(find.text('BlackSky')); await tester.pumpAndSettle(); - expect(find.text('BlackSky Trending'), findsOneWidget); expect(find.text('Black Topic'), findsOneWidget); verify(() => blueskyRepository.loadDiscover(cursor: null, limit: 25)).called(1); @@ -233,30 +232,23 @@ GeneratorView _feed(String rkey) { ); } -TrendView _trend(String topic) { - return TrendView( - topic: topic, - displayName: topic, - link: '/topic/1972', - startedAt: DateTime.utc(2026, 5, 18), - postCount: 212, - category: 'culture', - actors: const [], - ); -} - -FeedViewPost _post(String rkey) { - final record = FeedPostRecord(text: 'Post $rkey', createdAt: DateTime.utc(2026, 5, 20)); - return FeedViewPost( - post: PostView( - uri: atcore.AtUri.parse('at://did:plc:author/app.bsky.feed.post/$rkey'), - cid: 'cid-$rkey', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social', displayName: 'Author'), - record: record.toJson(), - indexedAt: DateTime.utc(2026, 5, 20), - replyCount: 1, - repostCount: 2, - likeCount: 3, - ), - ); -} +TrendView _trend(String topic) => TrendView( + topic: topic, + displayName: topic, + link: '/topic/1972', + startedAt: DateTime.utc(2026, 5, 18), + postCount: 212, + category: 'culture', + actors: const [], +); + +FeedViewPost _post(String rkey) => testFeedViewPost( + uri: 'at://did:plc:author/app.bsky.feed.post/$rkey', + cid: 'cid-$rkey', + author: testProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social', displayName: 'Author'), + record: testPostRecordJson(text: 'Post $rkey', createdAt: DateTime.utc(2026, 5, 20)), + indexedAt: DateTime.utc(2026, 5, 20), + replyCount: 1, + repostCount: 2, + likeCount: 3, +); diff --git a/test/features/public/presentation/public_navigation_test.dart b/test/features/public/presentation/public_navigation_test.dart index 44981e6..f94ad96 100644 --- a/test/features/public/presentation/public_navigation_test.dart +++ b/test/features/public/presentation/public_navigation_test.dart @@ -8,6 +8,8 @@ import 'package:lazurite/features/public/data/public_provider_context.dart'; import 'package:lazurite/features/public/presentation/public_navigation.dart'; import 'package:poptart_core/poptart_core.dart' as atcore; +import '../../../helpers/widget_harness.dart'; + void main() { testWidgets('public navigation helpers append provider query context', (tester) async { const context = PublicProviderContext(providerKey: AppViewProviders.blackskyKey); @@ -36,8 +38,7 @@ void main() { ], ); - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); + await pumpTestRouterApp(tester, router); navigateToPublicFeed(buttonContext, _feed(), context); await tester.pumpAndSettle(); diff --git a/test/features/search/bloc/search_bloc_test.dart b/test/features/search/bloc/search_bloc_test.dart index 73a09e2..b2b7d3d 100644 --- a/test/features/search/bloc/search_bloc_test.dart +++ b/test/features/search/bloc/search_bloc_test.dart @@ -12,6 +12,8 @@ import 'package:lazurite/features/typeahead/data/typeahead_repository.dart'; import 'package:lazurite/features/typeahead/data/typeahead_result.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/feed_fixtures.dart'; + class MockSearchRepository extends Mock implements SearchRepository {} class MockTypeaheadRepository extends Mock implements TypeaheadRepository {} @@ -23,11 +25,9 @@ void main() { late MockTypeaheadRepository mockTypeaheadRepository; late MockAppDatabase mockDatabase; - final samplePost = PostView( - uri: const AtUri('at://did:plc:author/app.bsky.feed.post/abc'), + final samplePost = testPostView( cid: 'cid-post', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social'), - record: const {r'$type': 'app.bsky.feed.post', 'text': 'Hello world', 'createdAt': '2026-01-01T00:00:00.000Z'}, + record: testPostRecordJson(text: 'Hello world', createdAt: DateTime.utc(2026, 1, 1)), indexedAt: DateTime.utc(2026, 1, 1), ); diff --git a/test/features/search/cubit/hashtag_cubit_test.dart b/test/features/search/cubit/hashtag_cubit_test.dart index 46a7e36..cb37ea4 100644 --- a/test/features/search/cubit/hashtag_cubit_test.dart +++ b/test/features/search/cubit/hashtag_cubit_test.dart @@ -1,22 +1,18 @@ -import 'package:poptart_core/poptart_core.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/features/search/cubit/hashtag_cubit.dart'; import 'package:lazurite/features/search/data/search_repository.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/feed_fixtures.dart'; + class MockSearchRepository extends Mock implements SearchRepository {} -PostView _post(String uri, String text) { - return PostView( - uri: AtUri.parse(uri), - cid: 'cid-${uri.hashCode}', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social'), - record: {r'$type': 'app.bsky.feed.post', 'text': text, 'createdAt': '2026-01-01T00:00:00.000Z'}, - indexedAt: DateTime.utc(2026, 1, 1), - ); -} +PostView _post(String uri, String text) => testPostView( + uri: uri, + record: testPostRecordJson(text: text, createdAt: DateTime.utc(2026, 1, 1)), + indexedAt: DateTime.utc(2026, 1, 1), +); void main() { late MockSearchRepository searchRepository; @@ -83,7 +79,6 @@ void main() { await cubit.initialize(); await cubit.switchSort(HashtagSort.latest); - expect(cubit.state.currentSort, HashtagSort.latest); expect(cubit.state.topTimeline.posts, [topPost]); expect(cubit.state.latestTimeline.posts, [latestPost]); @@ -113,7 +108,6 @@ void main() { await cubit.initialize(); await cubit.loadMoreCurrent(); - expect(cubit.state.topTimeline.posts, [firstPost, secondPost]); expect(cubit.state.topTimeline.cursor, isNull); }); @@ -143,7 +137,6 @@ void main() { await cubit.initialize(); await cubit.refreshCurrent(); - expect(cubit.state.topTimeline.posts, [refreshedPost]); }); @@ -180,7 +173,6 @@ void main() { addTearDown(cubit.close); await cubit.initialize(); - expect(cubit.state.topTimeline.status, HashtagTimelineStatus.error); expect(cubit.state.topTimeline.errorMessage, isNotNull); }); diff --git a/test/features/search/cubit/topic_cubit_test.dart b/test/features/search/cubit/topic_cubit_test.dart index 2da905f..3ce662b 100644 --- a/test/features/search/cubit/topic_cubit_test.dart +++ b/test/features/search/cubit/topic_cubit_test.dart @@ -1,22 +1,18 @@ -import 'package:poptart_core/poptart_core.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/features/search/cubit/topic_cubit.dart'; import 'package:lazurite/features/search/data/search_repository.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/feed_fixtures.dart'; + class MockSearchRepository extends Mock implements SearchRepository {} -PostView _post(String uri, String text) { - return PostView( - uri: AtUri.parse(uri), - cid: 'cid-${uri.hashCode}', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social'), - record: {r'$type': 'app.bsky.feed.post', 'text': text, 'createdAt': '2026-01-01T00:00:00.000Z'}, - indexedAt: DateTime.utc(2026, 1, 1), - ); -} +PostView _post(String uri, String text) => testPostView( + uri: uri, + record: testPostRecordJson(text: text, createdAt: DateTime.utc(2026, 1, 1)), + indexedAt: DateTime.utc(2026, 1, 1), +); void main() { late MockSearchRepository searchRepository; @@ -84,7 +80,6 @@ void main() { await cubit.initialize(); await cubit.switchSort(TopicSort.latest); - expect(cubit.state.currentSort, TopicSort.latest); expect(cubit.state.topTimeline.posts, [topPost]); expect(cubit.state.latestTimeline.posts, [latestPost]); @@ -114,7 +109,6 @@ void main() { await cubit.initialize(); await cubit.loadMoreCurrent(); - expect(cubit.state.topTimeline.posts, [firstPost, secondPost]); expect(cubit.state.topTimeline.cursor, isNull); }); diff --git a/test/features/search/data/hashtag_utils_test.dart b/test/features/search/data/hashtag_utils_test.dart index ee9e4f1..a9178ac 100644 --- a/test/features/search/data/hashtag_utils_test.dart +++ b/test/features/search/data/hashtag_utils_test.dart @@ -1,20 +1,13 @@ -import 'package:poptart_core/poptart_core.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/post.dart'; import 'package:bluesky_poptart/app/bsky/richtext/facet.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/features/search/data/hashtag_utils.dart'; -PostView _post(String uri, Map record) { - return PostView( - uri: AtUri.parse(uri), - cid: 'cid-${uri.hashCode}', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social'), - record: record, - indexedAt: DateTime.utc(2026, 1, 1), - ); -} +import '../../../helpers/feed_fixtures.dart'; + +PostView _post(String uri, Map record) => + testPostView(uri: uri, record: record, indexedAt: DateTime.utc(2026, 1, 1)); void main() { group('normalizeHashtag', () { @@ -44,7 +37,6 @@ void main() { ); final textRecord = FeedPostRecord(text: 'another #openweb #decentralized', createdAt: DateTime.utc(2026, 1, 1)); - final tags = extractRelatedHashtags([ _post('at://did:plc:author/app.bsky.feed.post/1', facetRecord.toJson()), _post('at://did:plc:author/app.bsky.feed.post/2', textRecord.toJson()), diff --git a/test/features/search/data/post_text_extractor_test.dart b/test/features/search/data/post_text_extractor_test.dart index 4828e95..b6f295c 100644 --- a/test/features/search/data/post_text_extractor_test.dart +++ b/test/features/search/data/post_text_extractor_test.dart @@ -1,5 +1,3 @@ -import 'package:poptart_core/poptart_core.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/embed/external.dart'; import 'package:bluesky_poptart/app/bsky/embed/images.dart'; import 'package:bluesky_poptart/app/bsky/embed/record.dart'; @@ -9,20 +7,16 @@ import 'package:bluesky_poptart/app/bsky/feed/post.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/features/search/data/post_text_extractor.dart'; -const _author = ProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'); -final _uri = AtUri.parse('at://did:plc:test/app.bsky.feed.post/xyz'); - -PostView _post({String text = '', UPostViewEmbed? embed}) { - final record = FeedPostRecord(text: text, createdAt: DateTime.utc(2026, 1, 1)); - return PostView( - uri: _uri, - cid: 'cid-test', - author: _author, - record: record.toJson(), - indexedAt: DateTime.utc(2026, 1, 1), - embed: embed, - ); -} +import '../../../helpers/feed_fixtures.dart'; + +PostView _post({String text = '', UPostViewEmbed? embed}) => testPostView( + uri: 'at://did:plc:test/app.bsky.feed.post/xyz', + cid: 'cid-test', + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), + record: FeedPostRecord(text: text, createdAt: DateTime.utc(2026, 1, 1)).toJson(), + indexedAt: DateTime.utc(2026, 1, 1), + embed: embed, +); UPostViewEmbed _imagesEmbed(List altTexts) { final images = altTexts @@ -42,13 +36,11 @@ UPostViewEmbed _externalEmbed({ required String title, required String description, String uri = 'https://example.com', -}) { - return UPostViewEmbed.embedExternalView( - data: EmbedExternalView( - external: EmbedExternalViewExternal(uri: uri, title: title, description: description), - ), - ); -} +}) => UPostViewEmbed.embedExternalView( + data: EmbedExternalView( + external: EmbedExternalViewExternal(uri: uri, title: title, description: description), + ), +); UPostViewEmbed _recordWithImagesEmbed(String postText, List altTexts) { return UPostViewEmbed.embedRecordWithMediaView( diff --git a/test/features/search/presentation/hashtag_screen_test.dart b/test/features/search/presentation/hashtag_screen_test.dart index 5ac7104..b46b852 100644 --- a/test/features/search/presentation/hashtag_screen_test.dart +++ b/test/features/search/presentation/hashtag_screen_test.dart @@ -1,5 +1,3 @@ -import 'package:poptart_core/poptart_core.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -11,17 +9,16 @@ import 'package:lazurite/features/search/data/search_repository.dart'; import 'package:lazurite/features/search/presentation/hashtag_screen.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/feed_fixtures.dart'; + class MockSearchRepository extends Mock implements SearchRepository {} -PostView _post({required String uri, required String text}) { - return PostView( - uri: AtUri.parse(uri), - cid: 'cid-${uri.hashCode}', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social', displayName: 'Author'), - record: {r'$type': 'app.bsky.feed.post', 'text': text, 'createdAt': DateTime.utc(2026, 1, 1).toIso8601String()}, - indexedAt: DateTime.utc(2026, 1, 1), - ); -} +PostView _post({required String uri, required String text}) => testPostView( + uri: uri, + author: testProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social', displayName: 'Author'), + record: testPostRecordJson(text: text, createdAt: DateTime.utc(2026, 1, 1)), + indexedAt: DateTime.utc(2026, 1, 1), +); void main() { late MockSearchRepository searchRepository; @@ -205,13 +202,11 @@ void main() { await tester.tap(find.text('Jump to hashtag')); await tester.pumpAndSettle(); - expect(find.text('Jump to hashtag'), findsAtLeastNWidgets(1)); expect(find.text('#openweb'), findsOneWidget); await tester.tap(find.text('#openweb')); await tester.pumpAndSettle(); - expect(find.text('#openweb'), findsOneWidget); expect(router.canPop(), isFalse); }); diff --git a/test/features/settings/data/video_repository_test.dart b/test/features/settings/data/video_repository_test.dart index 2231271..38de2f8 100644 --- a/test/features/settings/data/video_repository_test.dart +++ b/test/features/settings/data/video_repository_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:bluesky_poptart/app/bsky/video/get_upload_limits.dart'; import 'package:lazurite/features/settings/data/video_repository.dart'; -import 'package:lazurite/shared/utils/test_utils.dart'; +import '../../../helpers/test_utils.dart'; class FakeVideoUploadLimitsApi implements VideoUploadLimitsApi { FakeVideoUploadLimitsApi({ diff --git a/test/features/settings/presentation/search_settings_test.dart b/test/features/settings/presentation/search_settings_test.dart index 93ff91b..d045276 100644 --- a/test/features/settings/presentation/search_settings_test.dart +++ b/test/features/settings/presentation/search_settings_test.dart @@ -2,8 +2,6 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:lazurite/core/theme/app_theme.dart'; -import 'package:lazurite/core/theme/feed_layout.dart'; import 'package:lazurite/features/account/cubit/account_switcher_cubit.dart'; import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; import 'package:lazurite/features/auth/data/models/auth_models.dart'; @@ -12,20 +10,16 @@ import 'package:lazurite/features/settings/bloc/settings_state.dart'; import 'package:lazurite/features/settings/presentation/settings_screen.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/settings_fixtures.dart'; + class MockAuthBloc extends MockBloc implements AuthBloc {} class MockAccountSwitcherCubit extends MockCubit implements AccountSwitcherCubit {} class MockSettingsCubit extends MockCubit implements SettingsCubit {} -SettingsState _baseSettings({bool semanticSearchEnabled = false, int maxResults = 20}) => SettingsState( - themePalette: AppThemePalette.oxocarbon, - themeVariant: AppThemeVariant.dark, - useSystemTheme: false, - feedLayout: FeedLayout.comfortable, - semanticSearchEnabled: semanticSearchEnabled, - semanticSearchMaxResults: maxResults, -); +SettingsState _baseSettings({bool semanticSearchEnabled = false, int maxResults = 20}) => + testSettingsState(semanticSearchEnabled: semanticSearchEnabled, semanticSearchMaxResults: maxResults); void main() { const tokens = AuthTokens( @@ -60,16 +54,14 @@ void main() { when(() => settingsCubit.setTypeaheadProvider(any())).thenAnswer((_) async {}); }); - Widget buildSubject() { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: authBloc), - BlocProvider.value(value: accountSwitcherCubit), - BlocProvider.value(value: settingsCubit), - ], - child: const MaterialApp(home: SettingsScreen()), - ); - } + Widget buildSubject() => MultiBlocProvider( + providers: [ + BlocProvider.value(value: authBloc), + BlocProvider.value(value: accountSwitcherCubit), + BlocProvider.value(value: settingsCubit), + ], + child: const MaterialApp(home: SettingsScreen()), + ); group('Settings – Search section', () { testWidgets('shows Search section header', (tester) async { diff --git a/test/features/settings/presentation/settings_screen_test.dart b/test/features/settings/presentation/settings_screen_test.dart index a19c3c7..867d3ad 100644 --- a/test/features/settings/presentation/settings_screen_test.dart +++ b/test/features/settings/presentation/settings_screen_test.dart @@ -7,8 +7,6 @@ import 'package:lazurite/core/cache/local_cache_maintenance_service.dart'; import 'package:lazurite/core/crash_reporting/crash_reporting_service.dart'; import 'package:lazurite/core/database/app_database.dart'; import 'package:lazurite/core/network/app_view_provider.dart'; -import 'package:lazurite/core/theme/app_theme.dart'; -import 'package:lazurite/core/theme/feed_layout.dart'; import 'package:lazurite/core/theme/typography.dart'; import 'package:lazurite/features/account/cubit/account_switcher_cubit.dart'; import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; @@ -16,10 +14,12 @@ import 'package:lazurite/features/auth/data/models/auth_models.dart'; import 'package:lazurite/features/settings/bloc/settings_cubit.dart'; import 'package:lazurite/features/settings/bloc/settings_state.dart'; import 'package:lazurite/features/settings/presentation/settings_screen.dart'; -import 'package:lazurite/shared/utils/test_utils.dart'; import 'package:mocktail/mocktail.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import '../../../helpers/settings_fixtures.dart'; +import '../../../helpers/test_utils.dart'; + class MockAccountSwitcherCubit extends MockCubit implements AccountSwitcherCubit {} class MockAuthBloc extends MockBloc implements AuthBloc {} @@ -89,24 +89,8 @@ void main() { initialState: const AccountSwitcherState.ready(accounts: []), ); - when(() => settingsCubit.state).thenReturn( - const SettingsState( - themePalette: AppThemePalette.oxocarbon, - themeVariant: AppThemeVariant.dark, - useSystemTheme: false, - feedLayout: FeedLayout.comfortable, - ), - ); - whenListen( - settingsCubit, - const Stream.empty(), - initialState: const SettingsState( - themePalette: AppThemePalette.oxocarbon, - themeVariant: AppThemeVariant.dark, - useSystemTheme: false, - feedLayout: FeedLayout.comfortable, - ), - ); + when(() => settingsCubit.state).thenReturn(testSettingsState()); + whenListen(settingsCubit, const Stream.empty(), initialState: testSettingsState()); when(() => settingsCubit.setAppViewProvider(any())).thenAnswer((_) async {}); when(() => settingsCubit.setHeadingFontFamily(any())).thenAnswer((_) async {}); when(() => settingsCubit.setContentFontFamily(any())).thenAnswer((_) async {}); @@ -556,25 +540,11 @@ void main() { }); testWidgets('provider change confirmation can be cancelled', (tester) async { - when(() => settingsCubit.state).thenReturn( - const SettingsState( - themePalette: AppThemePalette.oxocarbon, - themeVariant: AppThemeVariant.dark, - useSystemTheme: false, - feedLayout: FeedLayout.comfortable, - appViewProvider: AppViewProviders.blueskyKey, - ), - ); + when(() => settingsCubit.state).thenReturn(testSettingsState(appViewProvider: AppViewProviders.blueskyKey)); whenListen( settingsCubit, const Stream.empty(), - initialState: const SettingsState( - themePalette: AppThemePalette.oxocarbon, - themeVariant: AppThemeVariant.dark, - useSystemTheme: false, - feedLayout: FeedLayout.comfortable, - appViewProvider: AppViewProviders.blueskyKey, - ), + initialState: testSettingsState(appViewProvider: AppViewProviders.blueskyKey), ); await tester.pumpWidget(buildSubject()); @@ -594,25 +564,11 @@ void main() { }); testWidgets('provider change confirmation applies selection when confirmed', (tester) async { - when(() => settingsCubit.state).thenReturn( - const SettingsState( - themePalette: AppThemePalette.oxocarbon, - themeVariant: AppThemeVariant.dark, - useSystemTheme: false, - feedLayout: FeedLayout.comfortable, - appViewProvider: AppViewProviders.blueskyKey, - ), - ); + when(() => settingsCubit.state).thenReturn(testSettingsState(appViewProvider: AppViewProviders.blueskyKey)); whenListen( settingsCubit, const Stream.empty(), - initialState: const SettingsState( - themePalette: AppThemePalette.oxocarbon, - themeVariant: AppThemeVariant.dark, - useSystemTheme: false, - feedLayout: FeedLayout.comfortable, - appViewProvider: AppViewProviders.blueskyKey, - ), + initialState: testSettingsState(appViewProvider: AppViewProviders.blueskyKey), ); await tester.pumpWidget(buildSubject()); @@ -653,7 +609,6 @@ void main() { await tester.scrollUntilVisible(find.text('ACCOUNT MAINTENANCE'), 300); await tester.pumpAndSettle(); - expect(find.text('ACCOUNT MAINTENANCE'), findsOneWidget); expect(find.text('Clean Follows'), findsOneWidget); expect(find.text('Audit and unfollow problematic accounts in bulk'), findsOneWidget); @@ -665,7 +620,6 @@ void main() { await tester.scrollUntilVisible(find.text('Terms of Service'), 300); await tester.pumpAndSettle(); - expect(find.text('Terms of Service'), findsOneWidget); expect(find.text('Privacy Policy'), findsOneWidget); }); @@ -673,7 +627,6 @@ void main() { testWidgets('hides back button when unauthenticated', (tester) async { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.byTooltip('Back'), findsNothing); expect(find.byTooltip('Open menu'), findsNothing); }); @@ -686,7 +639,6 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.byTooltip('Back'), findsOneWidget); expect(find.byTooltip('Open menu'), findsNothing); }); @@ -784,7 +736,6 @@ void main() { await tester.tap(find.text('Terms of Service')); await tester.pumpAndSettle(); - expect(find.text('terms-screen'), findsOneWidget); }); @@ -797,20 +748,17 @@ void main() { await tester.tap(find.text('Privacy Policy')); await tester.pumpAndSettle(); - expect(find.text('privacy-screen'), findsOneWidget); }); } -AuthTokens _authenticatedTokens() { - return const AuthTokens( - accessToken: 'access-token', - refreshToken: 'refresh-token', - did: 'did:plc:test', - handle: 'test.bsky.social', - displayName: 'Test User', - ); -} +AuthTokens _authenticatedTokens() => const AuthTokens( + accessToken: 'access-token', + refreshToken: 'refresh-token', + did: 'did:plc:test', + handle: 'test.bsky.social', + displayName: 'Test User', +); void _expectRightAlignedSelectedFontLabel(Widget widget, String label) { expect(widget, isA()); diff --git a/test/helpers/auth_fixtures.dart b/test/helpers/auth_fixtures.dart new file mode 100644 index 0000000..0696058 --- /dev/null +++ b/test/helpers/auth_fixtures.dart @@ -0,0 +1,35 @@ +import 'package:lazurite/features/auth/data/models/auth_models.dart'; + +AuthTokens testAuthTokens({ + String accessToken = 'access-token', + String? refreshToken = 'refresh-token', + String did = 'did:plc:test', + String handle = 'test.bsky.social', + String? displayName, + String? service = 'bsky.social', + DateTime? expiresAt, + String? oauthService, + String? oauthClientId, + String? oauthTokenType, + String? oauthScope, + String? dpopNonce, + String? dpopPublicKey, + String? dpopPrivateKey, + AuthMethod authMethod = AuthMethod.appPassword, +}) => AuthTokens( + accessToken: accessToken, + refreshToken: refreshToken, + expiresAt: expiresAt ?? DateTime.now().toUtc().add(const Duration(hours: 1)), + did: did, + handle: handle, + displayName: displayName, + service: service, + oauthService: oauthService, + oauthClientId: oauthClientId, + oauthTokenType: oauthTokenType, + oauthScope: oauthScope, + dpopNonce: dpopNonce, + dpopPublicKey: dpopPublicKey, + dpopPrivateKey: dpopPrivateKey, + authMethod: authMethod, +); diff --git a/test/helpers/feed_fixtures.dart b/test/helpers/feed_fixtures.dart new file mode 100644 index 0000000..4e9dced --- /dev/null +++ b/test/helpers/feed_fixtures.dart @@ -0,0 +1,75 @@ +import 'package:bluesky_poptart/app/bsky/actor/defs.dart' hide ViewerState; +import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; +import 'package:poptart_core/poptart_core.dart'; + +const testAuthorDid = 'did:plc:author'; +const testAuthorHandle = 'author.bsky.social'; +const testPostUri = 'at://did:plc:author/app.bsky.feed.post/abc'; + +ProfileViewBasic testProfileViewBasic({ + String did = testAuthorDid, + String handle = testAuthorHandle, + String? displayName, + String? avatar, +}) => ProfileViewBasic(did: did, handle: handle, displayName: displayName, avatar: avatar); + +Map testPostRecordJson({ + String text = 'Test post', + DateTime? createdAt, + Map extra = const {}, +}) => { + r'$type': 'app.bsky.feed.post', + 'text': text, + 'createdAt': (createdAt ?? DateTime.utc(2026, 3, 15)).toUtc().toIso8601String(), + ...extra, +}; + +PostView testPostView({ + String uri = testPostUri, + String? cid, + ProfileViewBasic? author, + Map? record, + DateTime? indexedAt, + int? replyCount, + int? repostCount, + int? likeCount, + int? quoteCount, + UPostViewEmbed? embed, +}) => PostView( + uri: AtUri.parse(uri), + cid: cid ?? 'cid-${uri.hashCode}', + author: author ?? testProfileViewBasic(), + record: record ?? testPostRecordJson(), + indexedAt: indexedAt ?? DateTime.utc(2026, 3, 15), + replyCount: replyCount, + repostCount: repostCount, + likeCount: likeCount, + quoteCount: quoteCount, + embed: embed, +); + +FeedViewPost testFeedViewPost({ + String uri = testPostUri, + String? cid, + ProfileViewBasic? author, + Map? record, + DateTime? indexedAt, + int? replyCount, + int? repostCount, + int? likeCount, + int? quoteCount, + UPostViewEmbed? embed, +}) => FeedViewPost( + post: testPostView( + uri: uri, + cid: cid, + author: author, + record: record, + indexedAt: indexedAt, + replyCount: replyCount, + repostCount: repostCount, + likeCount: likeCount, + quoteCount: quoteCount, + embed: embed, + ), +); diff --git a/test/helpers/notification_fixtures.dart b/test/helpers/notification_fixtures.dart new file mode 100644 index 0000000..86da297 --- /dev/null +++ b/test/helpers/notification_fixtures.dart @@ -0,0 +1,23 @@ +import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; +import 'package:bluesky_poptart/app/bsky/notification/list_notifications.dart' as bsky; +import 'package:poptart_core/poptart_core.dart'; + +bsky.Notification testNotification({ + bsky.KnownNotificationReason reason = bsky.KnownNotificationReason.like, + Object uri = 'at://did:plc:author/app.bsky.feed.post/abc', + String cid = 'cid-123', + ProfileView author = const ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), + AtUri? reasonSubject, + Map record = const {r'$type': 'app.bsky.feed.post', 'text': 'Hello world'}, + bool isRead = false, + DateTime? indexedAt, +}) => bsky.Notification( + uri: uri is AtUri ? uri : AtUri.parse(uri as String), + cid: cid, + author: author, + reason: bsky.NotificationReason.knownValue(data: reason), + reasonSubject: reasonSubject, + record: record, + isRead: isRead, + indexedAt: indexedAt ?? DateTime.utc(2026, 3, 15), +); diff --git a/test/helpers/robots/notification_list_item_robot.dart b/test/helpers/robots/notification_list_item_robot.dart new file mode 100644 index 0000000..2d3dd29 --- /dev/null +++ b/test/helpers/robots/notification_list_item_robot.dart @@ -0,0 +1,53 @@ +import 'package:bluesky_poptart/app/bsky/notification/list_notifications.dart' as bsky; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lazurite/features/notifications/presentation/widgets/notification_list_item.dart'; +import 'package:poptart_core/poptart_core.dart'; + +import '../router_harness.dart'; + +export '../notification_fixtures.dart'; + +class NotificationListItemRobot { + NotificationListItemRobot(this.tester); + + final WidgetTester tester; + TestRouterHarness? _harness; + + Uri get lastRoute { + final route = _harness?.lastUri; + expect(route, isNotNull); + return route!; + } + + Future pump({ + required bsky.Notification notification, + required String targetPath, + Widget target = const Scaffold(body: SizedBox.shrink()), + }) async { + _harness = TestRouterHarness.capturing( + home: Scaffold(body: NotificationListItem(notification: notification)), + targetPath: targetPath, + target: target, + ); + await _harness!.pump(tester); + } + + Future tapItem() async { + await _harness!.tapAndSettle(tester, find.byType(NotificationListItem)); + } + + void expectProfileRoute(String actor) { + expect(lastRoute.path, '/profile/${Uri.encodeComponent(actor)}'); + } + + void expectPostRoute(AtUri postUri) { + expect(lastRoute.path, '/post'); + expect(Uri.decodeComponent(lastRoute.queryParameters['uri']!), postUri.toString()); + } + + void expectStarterPackRoute(AtUri starterPackUri) { + expect(lastRoute.path, '/starter-pack'); + expect(Uri.decodeComponent(lastRoute.queryParameters['uri']!), starterPackUri.toString()); + } +} diff --git a/test/helpers/router_harness.dart b/test/helpers/router_harness.dart new file mode 100644 index 0000000..207224b --- /dev/null +++ b/test/helpers/router_harness.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; + +import 'widget_harness.dart'; + +class TestRouterHarness { + TestRouterHarness({required Widget home, String initialLocation = '/', List routes = const []}) + : router = GoRouter( + initialLocation: initialLocation, + routes: [ + GoRoute(path: '/', builder: (context, state) => home), + ...routes, + ], + ); + + TestRouterHarness.withRoutes({required List routes, String initialLocation = '/'}) + : router = GoRouter(initialLocation: initialLocation, routes: routes); + + TestRouterHarness._(this.router); + + factory TestRouterHarness.capturing({ + required Widget home, + required String targetPath, + String initialLocation = '/', + Widget target = const Scaffold(body: SizedBox.shrink()), + }) { + late final TestRouterHarness harness; + final router = GoRouter( + initialLocation: initialLocation, + routes: [ + GoRoute(path: '/', builder: (context, state) => home), + GoRoute( + path: targetPath, + builder: (context, state) { + harness.lastUri = state.uri; + return target; + }, + ), + ], + ); + harness = TestRouterHarness._(router); + return harness; + } + + final GoRouter router; + Uri? lastUri; + + String? get lastRoute => lastUri?.toString(); + String? get lastPath => lastUri?.path; + + GoRoute captureRoute({required String path, Widget child = const Scaffold(body: SizedBox.shrink())}) => GoRoute( + path: path, + builder: (context, state) { + lastUri = state.uri; + return child; + }, + ); + + Future pump(WidgetTester tester, {bool settle = true}) => pumpTestRouterApp(tester, router, settle: settle); + + Future tapAndSettle(WidgetTester tester, Finder finder) async { + await tester.tap(finder); + await tester.pumpAndSettle(); + } +} + +GoRoute capturedRoute({ + required String path, + required void Function(Uri uri) onRoute, + Widget child = const Scaffold(body: SizedBox.shrink()), +}) => GoRoute( + path: path, + builder: (context, state) { + onRoute(state.uri); + return child; + }, +); + +GoRoute profileCaptureRoute({ + required void Function(Uri uri) onRoute, + Widget child = const Scaffold(body: Text('profile')), +}) => capturedRoute(path: '/profile/:actor', onRoute: onRoute, child: child); + +GoRoute postCaptureRoute({ + required void Function(Uri uri) onRoute, + Widget child = const Scaffold(body: Text('post')), +}) => capturedRoute(path: '/post', onRoute: onRoute, child: child); + +GoRoute loginRoute({Widget child = const Scaffold(body: Text('login'))}) => GoRoute( + path: '/login', + builder: (context, state) => child, +); diff --git a/test/helpers/settings_fixtures.dart b/test/helpers/settings_fixtures.dart new file mode 100644 index 0000000..db43807 --- /dev/null +++ b/test/helpers/settings_fixtures.dart @@ -0,0 +1,54 @@ +import 'package:lazurite/core/theme/app_theme.dart'; +import 'package:lazurite/core/theme/feed_layout.dart'; +import 'package:lazurite/features/search/data/search_scope.dart'; +import 'package:lazurite/features/settings/bloc/settings_state.dart'; + +SettingsState testSettingsState({ + AppThemePalette themePalette = AppThemePalette.oxocarbon, + AppThemeVariant themeVariant = AppThemeVariant.dark, + bool useSystemTheme = false, + FeedLayout feedLayout = FeedLayout.comfortable, + bool animationsEnabled = true, + bool simulateOffline = false, + int? threadAutoCollapseDepth, + String constellationUrl = 'https://constellation.microcosm.blue', + bool semanticSearchEnabled = true, + SearchScope searchScope = SearchScope.both, + int semanticSearchMaxResults = 20, + String typeaheadProvider = 'bluesky', + String appViewProvider = 'bluesky', + bool crossProviderFallbackEnabled = false, + bool slingshotIdentityFallbackEnabled = false, + bool crashReportingEnabled = false, + bool crashReportingConsentPrompted = false, + int routingEpoch = 0, + String? appViewHealthSummary, + DateTime? appViewHealthCheckedAt, + bool appViewHealthRefreshing = false, + String? appViewLastFallback, + String? appViewLastError, +}) => SettingsState( + themePalette: themePalette, + themeVariant: themeVariant, + useSystemTheme: useSystemTheme, + feedLayout: feedLayout, + animationsEnabled: animationsEnabled, + simulateOffline: simulateOffline, + threadAutoCollapseDepth: threadAutoCollapseDepth, + constellationUrl: constellationUrl, + semanticSearchEnabled: semanticSearchEnabled, + searchScope: searchScope, + semanticSearchMaxResults: semanticSearchMaxResults, + typeaheadProvider: typeaheadProvider, + appViewProvider: appViewProvider, + crossProviderFallbackEnabled: crossProviderFallbackEnabled, + slingshotIdentityFallbackEnabled: slingshotIdentityFallbackEnabled, + crashReportingEnabled: crashReportingEnabled, + crashReportingConsentPrompted: crashReportingConsentPrompted, + routingEpoch: routingEpoch, + appViewHealthSummary: appViewHealthSummary, + appViewHealthCheckedAt: appViewHealthCheckedAt, + appViewHealthRefreshing: appViewHealthRefreshing, + appViewLastFallback: appViewLastFallback, + appViewLastError: appViewLastError, +); diff --git a/lib/shared/utils/test_utils.dart b/test/helpers/test_utils.dart similarity index 72% rename from lib/shared/utils/test_utils.dart rename to test/helpers/test_utils.dart index 5072333..10b50b6 100644 --- a/lib/shared/utils/test_utils.dart +++ b/test/helpers/test_utils.dart @@ -1,8 +1,9 @@ import 'dart:convert'; -import 'package:lazurite/features/auth/data/models/auth_models.dart'; import 'package:poptart_core/poptart_core.dart' as atcore; +export 'auth_fixtures.dart'; + String base64UrlEncode(Map value) => base64Url.encode(utf8.encode(jsonEncode(value))).replaceAll('=', ''); @@ -30,22 +31,6 @@ String buildJwt({ return '$header.$payload.signature'; } -AuthTokens testAuthTokens({ - String accessToken = 'access-token', - String? refreshToken = 'refresh-token', - String did = 'did:plc:test', - String handle = 'test.bsky.social', - String? service = 'bsky.social', - DateTime? expiresAt, -}) => AuthTokens( - accessToken: accessToken, - refreshToken: refreshToken, - expiresAt: expiresAt ?? DateTime.now().toUtc().add(const Duration(hours: 1)), - did: did, - handle: handle, - service: service, -); - atcore.UnauthorizedException testUnauthorizedException( String methodId, { atcore.HttpMethod method = atcore.HttpMethod.get, diff --git a/test/helpers/widget_harness.dart b/test/helpers/widget_harness.dart new file mode 100644 index 0000000..1737ddf --- /dev/null +++ b/test/helpers/widget_harness.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; + +Widget testScaffoldApp(Widget child, {ThemeData? theme, ThemeData? darkTheme, Locale? locale}) => MaterialApp( + theme: theme, + darkTheme: darkTheme, + locale: locale, + home: Scaffold(body: child), +); + +Widget testHomeApp(Widget home, {ThemeData? theme, ThemeData? darkTheme, Locale? locale}) => + MaterialApp(theme: theme, darkTheme: darkTheme, locale: locale, home: home); + +Widget testRouterApp(GoRouter router, {ThemeData? theme, ThemeData? darkTheme, Locale? locale}) => + MaterialApp.router(theme: theme, darkTheme: darkTheme, locale: locale, routerConfig: router); + +Future pumpTestScaffoldApp( + WidgetTester tester, + Widget child, { + ThemeData? theme, + ThemeData? darkTheme, + Locale? locale, + bool settle = false, +}) async { + await tester.pumpWidget(testScaffoldApp(child, theme: theme, darkTheme: darkTheme, locale: locale)); + if (settle) { + await tester.pumpAndSettle(); + } +} + +Future pumpTestHomeApp( + WidgetTester tester, + Widget home, { + ThemeData? theme, + ThemeData? darkTheme, + Locale? locale, + bool settle = false, +}) async { + await tester.pumpWidget(testHomeApp(home, theme: theme, darkTheme: darkTheme, locale: locale)); + if (settle) { + await tester.pumpAndSettle(); + } +} + +Future pumpTestRouterApp( + WidgetTester tester, + GoRouter router, { + ThemeData? theme, + ThemeData? darkTheme, + Locale? locale, + bool settle = true, +}) async { + await tester.pumpWidget(testRouterApp(router, theme: theme, darkTheme: darkTheme, locale: locale)); + if (settle) { + await tester.pumpAndSettle(); + } +} + +Future tapAndSettle(WidgetTester tester, Finder finder) async { + await tester.tap(finder); + await tester.pumpAndSettle(); +} diff --git a/test/shared/presentation/helpers/navigation_helpers_test.dart b/test/shared/presentation/helpers/navigation_helpers_test.dart index 895780d..a9e5d26 100644 --- a/test/shared/presentation/helpers/navigation_helpers_test.dart +++ b/test/shared/presentation/helpers/navigation_helpers_test.dart @@ -4,167 +4,116 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:lazurite/shared/presentation/helpers/navigation_helpers.dart'; +import '../../../helpers/router_harness.dart'; +import '../../../helpers/widget_harness.dart'; + +Widget _buttonRoute(VoidCallback onPressed, String label) => Scaffold( + body: Center( + child: FilledButton(onPressed: onPressed, child: Text(label)), + ), +); + void main() { group('navigation_helpers', () { testWidgets('navigateToProfile pushes encoded profile route', (tester) async { const actorDid = 'did:plc:alice.test'; - String? pushedRoute; - + Uri? pushedRoute; final router = GoRouter( routes: [ GoRoute( path: '/', - builder: (context, state) => Scaffold( - body: Center( - child: FilledButton(onPressed: () => navigateToProfile(context, actorDid), child: const Text('go')), - ), - ), - ), - GoRoute( - path: '/profile/:actor', - builder: (context, state) { - pushedRoute = state.uri.toString(); - return const Scaffold(body: Text('profile')); - }, + builder: (context, state) => _buttonRoute(() => navigateToProfile(context, actorDid), 'go'), ), + profileCaptureRoute(onRoute: (uri) => pushedRoute = uri), ], ); - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('go')); - await tester.pumpAndSettle(); + await pumpTestRouterApp(tester, router); + await tapAndSettle(tester, find.text('go')); expect(pushedRoute, isNotNull); - expect(Uri.parse(pushedRoute!).path, '/profile/${Uri.encodeComponent(actorDid)}'); + expect(pushedRoute!.path, '/profile/${Uri.encodeComponent(actorDid)}'); expect(router.canPop(), isTrue); }); testWidgets('navigateToProfile pushes from non-shell routes like /post', (tester) async { const actorDid = 'did:plc:alice.test'; - String? activePath; - + Uri? activeRoute; final router = GoRouter( initialLocation: '/post', routes: [ GoRoute( path: '/post', - builder: (context, state) => Scaffold( - body: Center( - child: FilledButton(onPressed: () => navigateToProfile(context, actorDid), child: const Text('go')), - ), - ), - ), - GoRoute( - path: '/profile/:actor', - builder: (context, state) { - activePath = state.uri.path; - return const Scaffold(body: Text('profile')); - }, + builder: (context, state) => _buttonRoute(() => navigateToProfile(context, actorDid), 'go'), ), + profileCaptureRoute(onRoute: (uri) => activeRoute = uri), ], ); - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('go')); - await tester.pumpAndSettle(); + await pumpTestRouterApp(tester, router); + await tapAndSettle(tester, find.text('go')); - expect(activePath, '/profile/${Uri.encodeComponent(actorDid)}'); + expect(activeRoute!.path, '/profile/${Uri.encodeComponent(actorDid)}'); expect(router.canPop(), isTrue); expect(tester.takeException(), isNull); }); testWidgets('navigateToProfile routes the current user to the profile tab root', (tester) async { const actorDid = 'did:plc:me.test'; - String? activePath; - + Uri? activeRoute; final router = GoRouter( initialLocation: '/post', routes: [ GoRoute( path: '/post', - builder: (context, state) => Scaffold( - body: Center( - child: FilledButton(onPressed: () => navigateToProfile(context, actorDid), child: const Text('go')), - ), - ), + builder: (context, state) => _buttonRoute(() => navigateToProfile(context, actorDid), 'go'), ), - GoRoute( + capturedRoute( path: '/profile/me', - builder: (context, state) { - activePath = state.uri.path; - return const Scaffold(body: Text('me')); - }, + onRoute: (uri) => activeRoute = uri, + child: const Scaffold(body: Text('me')), ), ], ); - await tester.pumpWidget( - RepositoryProvider.value( - value: actorDid, - child: MaterialApp.router(routerConfig: router), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('go')); + await tester.pumpWidget(RepositoryProvider.value(value: actorDid, child: testRouterApp(router))); await tester.pumpAndSettle(); + await tapAndSettle(tester, find.text('go')); - expect(activePath, '/profile/me'); + expect(activeRoute!.path, '/profile/me'); expect(router.canPop(), isFalse); expect(tester.takeException(), isNull); }); testWidgets('navigateToPost pushes encoded post route', (tester) async { const postUri = 'at://did:plc:alice/app.bsky.feed.post/123'; - String? pushedRoute; - + Uri? pushedRoute; final router = GoRouter( routes: [ - GoRoute( - path: '/', - builder: (context, state) => Scaffold( - body: Center( - child: FilledButton(onPressed: () => navigateToPost(context, postUri), child: const Text('go')), - ), - ), - ), - GoRoute( - path: '/post', - builder: (context, state) { - pushedRoute = state.uri.toString(); - return const Scaffold(body: Text('post')); - }, - ), + GoRoute(path: '/', builder: (context, state) => _buttonRoute(() => navigateToPost(context, postUri), 'go')), + postCaptureRoute(onRoute: (uri) => pushedRoute = uri), ], ); - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('go')); - await tester.pumpAndSettle(); + await pumpTestRouterApp(tester, router); + await tapAndSettle(tester, find.text('go')); expect(pushedRoute, isNotNull); - expect(Uri.parse(pushedRoute!).path, '/post'); - expect(Uri.parse(pushedRoute!).queryParameters['uri'], postUri); + expect(pushedRoute!.path, '/post'); + expect(pushedRoute!.queryParameters['uri'], postUri); }); testWidgets('returns null and does not throw without a router', (tester) async { Future? result; - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Builder( - builder: (context) { - result = navigateToProfile(context, 'did:plc:no-router'); - return const SizedBox.shrink(); - }, - ), + await pumpTestHomeApp( + tester, + Scaffold( + body: Builder( + builder: (context) { + result = navigateToProfile(context, 'did:plc:no-router'); + return const SizedBox.shrink(); + }, ), ), ); @@ -174,35 +123,21 @@ void main() { }); testWidgets('navigateToSettings pushes settings when a router is available', (tester) async { - String? activePath; - + Uri? activeRoute; final router = GoRouter( routes: [ - GoRoute( - path: '/', - builder: (context, state) => Scaffold( - body: Center( - child: FilledButton(onPressed: () => navigateToSettings(context), child: const Text('settings')), - ), - ), - ), - GoRoute( + GoRoute(path: '/', builder: (context, state) => _buttonRoute(() => navigateToSettings(context), 'settings')), + capturedRoute( path: '/settings', - builder: (context, state) { - activePath = state.uri.path; - return const Scaffold(body: Text('settings screen')); - }, + onRoute: (uri) => activeRoute = uri, + child: const Scaffold(body: Text('settings screen')), ), ], ); - await tester.pumpWidget(MaterialApp.router(routerConfig: router)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('settings')); - await tester.pumpAndSettle(); - - expect(activePath, '/settings'); + await pumpTestRouterApp(tester, router); + await tapAndSettle(tester, find.text('settings')); + expect(activeRoute!.path, '/settings'); expect(router.canPop(), isTrue); }); }); diff --git a/test/shared/presentation/widgets/error_state_test.dart b/test/shared/presentation/widgets/error_state_test.dart index d4607f6..2aa6a93 100644 --- a/test/shared/presentation/widgets/error_state_test.dart +++ b/test/shared/presentation/widgets/error_state_test.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/shared/presentation/widgets/error_state.dart'; +import '../../../helpers/widget_harness.dart'; + void main() { - Widget buildSubject(Widget child) { - return MaterialApp(home: Scaffold(body: child)); - } + Widget buildSubject(Widget child) => testScaffoldApp(child); testWidgets('renders title and message', (tester) async { await tester.pumpWidget( @@ -19,12 +19,8 @@ void main() { testWidgets('invokes retry callback', (tester) async { var retried = false; - await tester.pumpWidget(buildSubject(ErrorState(message: 'Failed', onRetry: () => retried = true))); - - await tester.tap(find.text('Retry')); - await tester.pump(); - + await tapAndSettle(tester, find.text('Retry')); expect(retried, isTrue); }); } -- 2.51.2