From e0003b6b409365d66ea4cdb8b2cd653d2901534c Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Tue, 26 May 2026 13:02:43 -0500 Subject: [PATCH] refactor: complete fixture migration for resources * move fixtures to dedicated dir * add shared assertion helpers --- test/app/lazurite_app_test.dart | 8 +- test/core/app/app_version_test.dart | 70 +--- .../core/cache/poptart_cache_codecs_test.dart | 2 +- ...ctor_repository_service_resolver_test.dart | 47 +-- .../network/atproto_host_resolver_test.dart | 20 +- .../network/oauth_session_restorer_test.dart | 2 +- .../unauthorized_recovery_runner_test.dart | 3 +- .../network/xrpc_client_factory_test.dart | 2 +- test/core/router/app_router_test.dart | 63 ++-- test/core/widgets/lazurite_app_bar_test.dart | 25 +- .../account_switcher_sheet_test.dart | 7 +- .../presentation/alerts_screen_test.dart | 27 +- .../auth/data/auth_repository_test.dart | 91 ++--- .../auth/data/models/auth_models_test.dart | 63 +++- .../auth/presentation/login_screen_test.dart | 4 +- .../presentation/compose_screen_test.dart | 8 +- .../devtools/cubit/dev_tools_cubit_test.dart | 13 +- test/features/feed/bloc/feed_bloc_test.dart | 7 +- .../feed/cubit/post_thread_cubit_test.dart | 8 +- .../feed/cubit/similar_posts_cubit_test.dart | 4 +- .../feed/data/feed_repository_cache_test.dart | 3 +- .../feed/data/feed_repository_test.dart | 2 +- .../data/liked_posts_repository_test.dart | 27 +- .../post_thread_repository_cache_test.dart | 3 +- .../data/post_thread_repository_test.dart | 2 +- .../data/similar_posts_repository_test.dart | 15 +- .../presentation/compact_post_card_test.dart | 4 +- .../presentation/feed_detail_screen_test.dart | 8 +- .../presentation/home_feed_screen_test.dart | 4 +- .../feed/presentation/post_card_test.dart | 336 ++++++++---------- .../post_card_with_actions_test.dart | 10 +- .../post_quote_repost_sheet_test.dart | 4 +- .../post_thread_edit_flow_test.dart | 8 +- .../presentation/public_post_card_test.dart | 2 +- .../presentation/saved_posts_screen_test.dart | 8 +- .../similar_posts_section_test.dart | 2 +- .../presentation/trending_screen_test.dart | 6 +- .../lists/cubit/add_to_list_cubit_test.dart | 14 +- .../lists/data/list_repository_test.dart | 88 ++--- .../presentation/list_detail_screen_test.dart | 22 +- .../list_members_screen_test.dart | 19 +- .../presentation/my_lists_screen_test.dart | 14 +- .../presentation/convo_list_screen_test.dart | 23 +- .../message_thread_screen_test.dart | 4 +- .../data/moderation_service_test.dart | 103 +++--- .../bloc/notification_bloc_test.dart | 2 +- .../data/notification_repository_test.dart | 8 +- .../notification_local_mappers_test.dart | 4 +- .../push_registration_service_test.dart | 2 +- .../notifications_screen_test.dart | 105 +++--- .../profile/data/profile_repository_test.dart | 3 +- .../profile_connections_screen_test.dart | 8 +- .../profile_context_screen_test.dart | 10 +- .../profile_edit_screen_test.dart | 31 +- .../presentation/profile_screen_test.dart | 128 +++---- .../widgets/suggested_follows_sheet_test.dart | 17 +- .../data/public_content_repository_test.dart | 2 +- .../presentation/public_home_screen_test.dart | 2 +- .../search/bloc/search_bloc_test.dart | 47 +-- .../search/cubit/hashtag_cubit_test.dart | 2 +- .../search/cubit/topic_cubit_test.dart | 2 +- .../search/data/hashtag_utils_test.dart | 2 +- .../search/data/post_text_extractor_test.dart | 2 +- .../presentation/hashtag_screen_test.dart | 2 +- .../profile_post_search_screen_test.dart | 39 +- .../presentation/search_screen_test.dart | 36 +- .../settings/data/video_repository_test.dart | 8 +- .../presentation/search_settings_test.dart | 16 +- .../presentation/settings_screen_test.dart | 36 +- .../starter_pack_detail_screen_test.dart | 7 +- .../typeahead_text_field_test.dart | 7 +- test/helpers/assertion_helpers.dart | 54 +++ test/helpers/auth_fixtures.dart | 35 -- test/helpers/connectivity_helpers.dart | 12 + test/helpers/fixtures/auth.dart | 161 +++++++++ .../feed.dart} | 9 + test/helpers/fixtures/graph.dart | 71 ++++ test/helpers/fixtures/network.dart | 29 ++ .../notification.dart} | 0 test/helpers/fixtures/package_info.dart | 17 + .../settings.dart} | 0 .../robots/notification_list_item_robot.dart | 2 +- test/helpers/router_harness.dart | 45 ++- test/helpers/search_helpers.dart | 51 +++ test/helpers/shared_test_helpers_test.dart | 141 ++++++++ test/helpers/test_utils.dart | 45 --- 86 files changed, 1259 insertions(+), 1146 deletions(-) create mode 100644 test/helpers/assertion_helpers.dart delete mode 100644 test/helpers/auth_fixtures.dart create mode 100644 test/helpers/connectivity_helpers.dart create mode 100644 test/helpers/fixtures/auth.dart rename test/helpers/{feed_fixtures.dart => fixtures/feed.dart} (89%) create mode 100644 test/helpers/fixtures/graph.dart create mode 100644 test/helpers/fixtures/network.dart rename test/helpers/{notification_fixtures.dart => fixtures/notification.dart} (100%) create mode 100644 test/helpers/fixtures/package_info.dart rename test/helpers/{settings_fixtures.dart => fixtures/settings.dart} (100%) create mode 100644 test/helpers/search_helpers.dart create mode 100644 test/helpers/shared_test_helpers_test.dart delete mode 100644 test/helpers/test_utils.dart diff --git a/test/app/lazurite_app_test.dart b/test/app/lazurite_app_test.dart index 17a2916..680866e 100644 --- a/test/app/lazurite_app_test.dart +++ b/test/app/lazurite_app_test.dart @@ -21,6 +21,7 @@ import 'package:lazurite/features/settings/bloc/settings_cubit.dart'; import 'package:lazurite/features/settings/bloc/settings_state.dart'; import 'package:lazurite/app/lazurite_app.dart'; import 'package:mocktail/mocktail.dart'; +import '../helpers/connectivity_helpers.dart'; class MockAuthBloc extends MockBloc implements AuthBloc {} @@ -75,12 +76,7 @@ void main() { when(() => settingsCubit.state).thenReturn(settingsState); whenListen(settingsCubit, const Stream.empty(), initialState: settingsState); when(() => settingsCubit.refreshAppViewHealth()).thenAnswer((_) async {}); - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); - whenListen( - connectivityCubit, - const Stream.empty(), - initialState: const ConnectivityState.online(), - ); + stubConnectivityCubit(connectivityCubit, state: const ConnectivityState.online()); when(() => connectivityCubit.setSimulatedOffline(any())).thenReturn(null); when(() => connectivityCubit.close()).thenAnswer((_) async {}); when(() => accountSwitcherCubit.state).thenReturn(const AccountSwitcherState.ready(accounts: [])); diff --git a/test/core/app/app_version_test.dart b/test/core/app/app_version_test.dart index 0072edb..3e0c792 100644 --- a/test/core/app/app_version_test.dart +++ b/test/core/app/app_version_test.dart @@ -1,106 +1,56 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/core/app/app_version.dart'; -import 'package:package_info_plus/package_info_plus.dart'; + +import '../../helpers/fixtures/package_info.dart'; void main() { group('AppVersion', () { test('shows current prerelease label for platform-safe numeric versions', () { - final label = AppVersion.displayLabelFor( - PackageInfo( - appName: 'Lazurite', - packageName: 'org.stormlightlabs.lazurite', - version: '1.0.0', - buildNumber: '6', - ), - ); + final label = AppVersion.displayLabelFor(testPackageInfo()); expect(label, equals('Lazurite v1.0.0 alpha 6')); }); test('uses app name from package metadata', () { final label = AppVersion.displayLabelFor( - PackageInfo( - appName: 'Lazurite Nightly', - packageName: 'org.stormlightlabs.lazurite.nightly', - version: '1.0.0', - buildNumber: '6', - ), + testPackageInfo(appName: 'Lazurite Nightly', packageName: 'org.stormlightlabs.lazurite.nightly'), ); expect(label, equals('Lazurite Nightly v1.0.0 alpha 6')); }); test('falls back to Lazurite when package app name is empty', () { - final label = AppVersion.displayLabelFor( - PackageInfo(appName: ' ', packageName: 'org.stormlightlabs.lazurite', version: '1.0.0', buildNumber: '6'), - ); + final label = AppVersion.displayLabelFor(testPackageInfo(appName: ' ')); expect(label, equals('Lazurite v1.0.0 alpha 6')); }); test('shows prerelease channel and build number together', () { - final label = AppVersion.displayLabelFor( - PackageInfo( - appName: 'Lazurite', - packageName: 'org.stormlightlabs.lazurite', - version: '1.0.0-alpha.6', - buildNumber: '6', - ), - ); + final label = AppVersion.displayLabelFor(testPackageInfo(version: '1.0.0-alpha.6')); expect(label, equals('Lazurite v1.0.0 alpha 6')); }); test('uses build number as prerelease number when version has only the channel', () { - final label = AppVersion.displayLabelFor( - PackageInfo( - appName: 'Lazurite', - packageName: 'org.stormlightlabs.lazurite', - version: '1.0.0-alpha', - buildNumber: '6', - ), - ); + final label = AppVersion.displayLabelFor(testPackageInfo(version: '1.0.0-alpha')); expect(label, equals('Lazurite v1.0.0 alpha 6')); }); test('shows native build separately when prerelease number differs', () { - final label = AppVersion.displayLabelFor( - PackageInfo( - appName: 'Lazurite', - packageName: 'org.stormlightlabs.lazurite', - version: '1.0.0-alpha.6', - buildNumber: '42', - ), - ); + final label = AppVersion.displayLabelFor(testPackageInfo(version: '1.0.0-alpha.6', buildNumber: '42')); expect(label, equals('Lazurite v1.0.0 alpha 6 (build 42)')); }); test('shows build number for stable versions', () { - final label = AppVersion.displayLabelFor( - PackageInfo( - appName: 'Lazurite', - packageName: 'org.stormlightlabs.lazurite', - version: '1.0.0', - buildNumber: '42', - ), - prereleaseLabel: null, - ); + final label = AppVersion.displayLabelFor(testPackageInfo(buildNumber: '42'), prereleaseLabel: null); expect(label, equals('Lazurite v1.0.0 (build 42)')); }); test('omits duplicate iOS build number fallback', () { - final label = AppVersion.displayLabelFor( - PackageInfo( - appName: 'Lazurite', - packageName: 'org.stormlightlabs.lazurite', - version: '1.0.0', - buildNumber: '1.0.0', - ), - prereleaseLabel: null, - ); + final label = AppVersion.displayLabelFor(testPackageInfo(buildNumber: '1.0.0'), prereleaseLabel: null); expect(label, equals('Lazurite v1.0.0')); }); diff --git a/test/core/cache/poptart_cache_codecs_test.dart b/test/core/cache/poptart_cache_codecs_test.dart index e90fb56..619ae30 100644 --- a/test/core/cache/poptart_cache_codecs_test.dart +++ b/test/core/cache/poptart_cache_codecs_test.dart @@ -3,7 +3,7 @@ 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'; +import '../../helpers/fixtures/feed.dart'; void main() { group('PoptartCacheCodecs', () { diff --git a/test/core/network/actor_repository_service_resolver_test.dart b/test/core/network/actor_repository_service_resolver_test.dart index 5be9ac8..09df53e 100644 --- a/test/core/network/actor_repository_service_resolver_test.dart +++ b/test/core/network/actor_repository_service_resolver_test.dart @@ -5,6 +5,8 @@ import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:lazurite/core/network/actor_repository_service_resolver.dart'; +import '../../helpers/fixtures/network.dart'; + void main() { group('ActorRepositoryServiceResolver', () { test('resolves handle through public identity host and then DID doc', () async { @@ -18,15 +20,7 @@ void main() { } if (request.url.host == 'plc.directory' && request.url.path == '/did:plc:alice') { return http.Response( - jsonEncode({ - 'service': [ - { - 'id': '#atproto_pds', - 'type': 'AtprotoPersonalDataServer', - 'serviceEndpoint': 'https://alice.us-east.host.bsky.network', - }, - ], - }), + jsonEncode(testDidDocument(serviceEndpoint: 'https://alice.us-east.host.bsky.network')), 200, ); } @@ -50,18 +44,7 @@ void main() { httpClient: MockClient((request) async { requestedUris.add(request.url); if (request.url.host == 'example.com' && request.url.path == '/users/alice/did.json') { - return http.Response( - jsonEncode({ - 'service': [ - { - 'id': '#atproto_pds', - 'type': 'AtprotoPersonalDataServer', - 'serviceEndpoint': 'https://pds.example.com', - }, - ], - }), - 200, - ); + return http.Response(jsonEncode(testDidDocument(serviceEndpoint: 'https://pds.example.com')), 200); } return http.Response('not found', 404); }), @@ -89,18 +72,7 @@ void main() { return http.Response(jsonEncode({'did': 'did:plc:fallback'}), 200); } if (request.url.host == 'plc.directory' && request.url.path == '/did:plc:fallback') { - return http.Response( - jsonEncode({ - 'service': [ - { - 'id': '#atproto_pds', - 'type': 'AtprotoPersonalDataServer', - 'serviceEndpoint': 'https://fallback.host', - }, - ], - }), - 200, - ); + return http.Response(jsonEncode(testDidDocument(serviceEndpoint: 'https://fallback.host')), 200); } return http.Response('not found', 404); }), @@ -123,14 +95,7 @@ void main() { return http.Response(jsonEncode({'did': 'did:plc:cache'}), 200); } if (request.url.host == 'plc.directory') { - return http.Response( - jsonEncode({ - 'service': [ - {'id': '#atproto_pds', 'type': 'AtprotoPersonalDataServer', 'serviceEndpoint': 'https://cache.host'}, - ], - }), - 200, - ); + return http.Response(jsonEncode(testDidDocument(serviceEndpoint: 'https://cache.host')), 200); } return http.Response('not found', 404); }), diff --git a/test/core/network/atproto_host_resolver_test.dart b/test/core/network/atproto_host_resolver_test.dart index e73fc95..b9355b5 100644 --- a/test/core/network/atproto_host_resolver_test.dart +++ b/test/core/network/atproto_host_resolver_test.dart @@ -1,40 +1,26 @@ 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'; +import '../../helpers/fixtures/auth.dart'; void main() { group('resolvePdsHost', () { test('uses stored PDS endpoint when restoring an opaque OAuth token', () { - final tokens = testAuthTokens( - accessToken: 'opaque-access-token', - refreshToken: 'refresh-token', + final tokens = testOpaqueOAuthTokens( expiresAt: DateTime.utc(2030), - did: 'did:plc:alice', - handle: 'alice.bsky.social', service: 'https://porcini.us-east.host.bsky.network', oauthClientId: 'https://client.example/metadata.json', - oauthTokenType: 'DPoP', - oauthScope: 'atproto transition:generic', dpopPublicKey: 'public-key', dpopPrivateKey: 'private-key', - authMethod: AuthMethod.oauth, ); expect(resolvePdsHost(tokens), 'porcini.us-east.host.bsky.network'); }); test('falls back to stored service when opaque OAuth metadata is incomplete', () { - final tokens = testAuthTokens( - accessToken: 'opaque-access-token', - refreshToken: 'refresh-token', - did: 'did:plc:alice', - handle: 'alice.bsky.social', + final tokens = testOpaqueOAuthTokens( service: 'porcini.us-east.host.bsky.network', dpopPublicKey: 'public-key', dpopPrivateKey: 'private-key', - authMethod: AuthMethod.oauth, ); expect(resolvePdsHost(tokens), 'porcini.us-east.host.bsky.network'); diff --git a/test/core/network/oauth_session_restorer_test.dart b/test/core/network/oauth_session_restorer_test.dart index f5094af..19d3bcb 100644 --- a/test/core/network/oauth_session_restorer_test.dart +++ b/test/core/network/oauth_session_restorer_test.dart @@ -3,7 +3,7 @@ import 'package:lazurite/core/network/oauth_session_restorer.dart'; import 'package:lazurite/features/auth/data/models/auth_models.dart'; import 'package:poptart_core/poptart_core.dart'; -import '../../helpers/test_utils.dart'; +import '../../helpers/fixtures/auth.dart'; void main() { group('restoreOAuthSessionFromTokens', () { diff --git a/test/core/network/unauthorized_recovery_runner_test.dart b/test/core/network/unauthorized_recovery_runner_test.dart index be1d6d8..64dbe24 100644 --- a/test/core/network/unauthorized_recovery_runner_test.dart +++ b/test/core/network/unauthorized_recovery_runner_test.dart @@ -2,7 +2,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/core/network/unauthorized_recovery_runner.dart'; import 'package:poptart_core/poptart_core.dart' show UnauthorizedException; -import '../../helpers/test_utils.dart'; +import '../../helpers/fixtures/auth.dart'; +import '../../helpers/fixtures/network.dart'; void main() { group('UnauthorizedRecoveryRunner', () { diff --git a/test/core/network/xrpc_client_factory_test.dart b/test/core/network/xrpc_client_factory_test.dart index 9d4b201..83d035a 100644 --- a/test/core/network/xrpc_client_factory_test.dart +++ b/test/core/network/xrpc_client_factory_test.dart @@ -3,7 +3,7 @@ import 'package:lazurite/core/network/xrpc_client_factory.dart'; import 'package:lazurite/features/auth/data/models/auth_models.dart'; import 'package:poptart_core/poptart_core.dart' as atp_core; -import '../../helpers/test_utils.dart'; +import '../../helpers/fixtures/auth.dart'; void main() { group('xrpc_client_factory', () { diff --git a/test/core/router/app_router_test.dart b/test/core/router/app_router_test.dart index e5fe7d4..f6e9dad 100644 --- a/test/core/router/app_router_test.dart +++ b/test/core/router/app_router_test.dart @@ -14,7 +14,6 @@ import 'package:lazurite/core/router/app_router.dart'; import 'package:lazurite/core/theme/app_theme.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'; import 'package:lazurite/features/auth/presentation/oauth_callback_screen.dart'; import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; import 'package:lazurite/features/feed/bloc/feed_bloc.dart'; @@ -37,6 +36,10 @@ import 'package:lazurite/features/typeahead/data/typeahead_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:poptart_core/poptart_core.dart' as atcore; +import '../../helpers/fixtures/feed.dart'; +import '../../helpers/fixtures/auth.dart'; +import '../../helpers/connectivity_helpers.dart'; + class MockAuthBloc extends MockBloc implements AuthBloc {} class MockFeedPreferencesCubit extends MockCubit implements FeedPreferencesCubit {} @@ -128,13 +131,7 @@ void main() { late StreamController authController; late AuthState currentAuthState; - const tokens = AuthTokens( - accessToken: 'access', - refreshToken: 'refresh', - did: 'did:plc:me', - handle: 'me.bsky.social', - displayName: 'River Tam', - ); + final tokens = testRiverTokens(); final profile = ProfileViewDetailed( did: 'did:plc:me', @@ -170,7 +167,7 @@ void main() { typeaheadRepository = MockTypeaheadRepository(); database = MockAppDatabase(); authController = StreamController.broadcast(); - currentAuthState = const AuthState.authenticated(tokens); + currentAuthState = AuthState.authenticated(tokens); when(() => authBloc.state).thenAnswer((_) => currentAuthState); when(() => authBloc.handleOAuthRedirectUri(any())).thenAnswer((_) async => false); @@ -187,7 +184,7 @@ void main() { ), ); when(() => settingsCubit.setAppViewProvider(any())).thenAnswer((_) async {}); - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); + stubConnectivityCubit(connectivityCubit); when(() => accountSwitcherCubit.state).thenReturn(const AccountSwitcherState.ready(accounts: [])); when(() => accountSwitcherCubit.loadAccounts()).thenAnswer((_) async {}); when(() => unreadCountCubit.state).thenReturn(const UnreadCountState(0)); @@ -568,7 +565,7 @@ void main() { }); testWidgets('authenticated root remains on the home feed', (tester) async { - currentAuthState = const AuthState.authenticated(tokens); + currentAuthState = AuthState.authenticated(tokens); when(() => authBloc.state).thenReturn(currentAuthState); whenListen(authBloc, Stream.value(currentAuthState), initialState: currentAuthState); @@ -1320,7 +1317,7 @@ void main() { }); testWidgets('authenticated settings back button falls back to home when there is no stack to pop', (tester) async { - currentAuthState = const AuthState.authenticated(tokens); + currentAuthState = AuthState.authenticated(tokens); when(() => authBloc.state).thenReturn(currentAuthState); whenListen(authBloc, Stream.value(currentAuthState), initialState: currentAuthState); @@ -1335,15 +1332,13 @@ void main() { await tester.tap(find.byTooltip('Back')); await tester.pumpAndSettle(); - expect(find.text('HOME'), findsAtLeastNWidgets(1)); expect(find.text('APPEARANCE'), findsNothing); - router.dispose(); }); testWidgets('authenticated settings back button returns to profile when opened from profile', (tester) async { - currentAuthState = const AuthState.authenticated(tokens); + currentAuthState = AuthState.authenticated(tokens); when(() => authBloc.state).thenReturn(currentAuthState); whenListen(authBloc, Stream.value(currentAuthState), initialState: currentAuthState); @@ -1361,10 +1356,8 @@ void main() { await tester.tap(find.byTooltip('Back')); await tester.pumpAndSettle(); - expect(find.text('RIVER TAM'), findsOneWidget); expect(find.text('APPEARANCE'), findsNothing); - router.dispose(); }); @@ -1391,7 +1384,6 @@ void main() { await tester.pump(const Duration(milliseconds: 500)); expect(find.text('No feeds pinned'), findsOneWidget); - router.dispose(); }); @@ -1407,7 +1399,6 @@ void main() { expect(find.byType(CupertinoPageTransition), findsWidgets); expect(router.canPop(), isTrue); - router.dispose(); }); @@ -1433,7 +1424,6 @@ void main() { await tester.pumpAndSettle(); expect(find.byKey(const ValueKey('login-continue-button')), findsOneWidget); - router.dispose(); }); @@ -1448,7 +1438,6 @@ void main() { expect(find.byKey(const ValueKey('login-continue-button')), findsOneWidget); expect(find.byKey(const ValueKey('unauthenticated-navigation-bar')), findsNothing); - router.dispose(); }); @@ -1490,7 +1479,6 @@ void main() { expect(router.routeInformationProvider.value.uri.path, isNot(equals(OAuthCallbackScreen.routePath))); expect(find.text('No feeds pinned'), findsOneWidget); - router.dispose(); }); @@ -1524,7 +1512,6 @@ void main() { expect(router.routeInformationProvider.value.uri.path, isNot(equals(OAuthCallbackScreen.routePath))); expect(find.text('No feeds pinned'), findsOneWidget); - router.dispose(); }); @@ -1555,22 +1542,14 @@ void main() { }); } -FeedViewPost _publicFeedPost() { - final record = FeedPostRecord(text: 'Public route post', createdAt: DateTime.utc(2026, 5, 18)); - return FeedViewPost( - post: PostView( - uri: atcore.AtUri.parse('at://did:plc:author/app.bsky.feed.post/route'), - cid: 'cid-route', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social'), - record: record.toJson(), - indexedAt: DateTime.utc(2026, 5, 18), - replyCount: 1, - repostCount: 2, - likeCount: 3, - ), - ); -} - -ThreadViewPost _publicThread() { - return ThreadViewPost(post: _publicFeedPost().post); -} +FeedViewPost _publicFeedPost() => testFeedViewPost( + uri: 'at://did:plc:author/app.bsky.feed.post/route', + cid: 'cid-route', + record: FeedPostRecord(text: 'Public route post', createdAt: DateTime.utc(2026, 5, 18)).toJson(), + indexedAt: DateTime.utc(2026, 5, 18), + replyCount: 1, + repostCount: 2, + likeCount: 3, +); + +ThreadViewPost _publicThread() => ThreadViewPost(post: _publicFeedPost().post); diff --git a/test/core/widgets/lazurite_app_bar_test.dart b/test/core/widgets/lazurite_app_bar_test.dart index 2cb84ba..8b88b59 100644 --- a/test/core/widgets/lazurite_app_bar_test.dart +++ b/test/core/widgets/lazurite_app_bar_test.dart @@ -10,8 +10,9 @@ 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'; +import '../../helpers/fixtures/settings.dart'; +import '../../helpers/fixtures/auth.dart'; +import '../../helpers/connectivity_helpers.dart'; class MockAuthBloc extends MockBloc implements AuthBloc {} @@ -24,11 +25,7 @@ void main() { late MockConnectivityCubit connectivityCubit; late MockSettingsCubit settingsCubit; - final tokens = testAuthTokens( - accessToken: 'access', - refreshToken: 'refresh', - displayName: 'River Tam', - ); + final tokens = testAuthTokens(accessToken: 'access', refreshToken: 'refresh', displayName: 'River Tam'); setUp(() { authBloc = MockAuthBloc(); @@ -36,12 +33,7 @@ void main() { settingsCubit = MockSettingsCubit(); 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(), - ); + stubConnectivityCubit(connectivityCubit, state: const ConnectivityState.online()); when(() => settingsCubit.state).thenReturn(testSettingsState()); whenListen(settingsCubit, const Stream.empty(), initialState: testSettingsState()); when(() => settingsCubit.setSimulateOffline(any())).thenAnswer((_) async {}); @@ -88,12 +80,7 @@ void main() { }); testWidgets('shows simulated offline indicator and lets the user disable it', (tester) async { - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online(isSimulatedOffline: true)); - whenListen( - connectivityCubit, - const Stream.empty(), - initialState: const ConnectivityState.online(isSimulatedOffline: true), - ); + stubConnectivityCubit(connectivityCubit, state: const ConnectivityState.online(isSimulatedOffline: true)); await tester.pumpWidget(buildSubject(sectionLabel: 'Home')); await tester.pumpAndSettle(); diff --git a/test/features/account/presentation/account_switcher_sheet_test.dart b/test/features/account/presentation/account_switcher_sheet_test.dart index b648861..25676c2 100644 --- a/test/features/account/presentation/account_switcher_sheet_test.dart +++ b/test/features/account/presentation/account_switcher_sheet_test.dart @@ -11,6 +11,7 @@ import 'package:lazurite/features/auth/data/models/auth_models.dart'; 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/assertion_helpers.dart'; class MockAccountSwitcherCubit extends MockCubit implements AccountSwitcherCubit {} @@ -144,10 +145,8 @@ void main() { await openSheet(tester); - expect(find.text('Alice'), findsOneWidget); - expect(find.text('@alice.bsky.social'), findsOneWidget); - expect(find.text('bob.bsky.social'), findsOneWidget); - expect(find.text('@bob.bsky.social'), findsOneWidget); + expectAccountRow(displayName: 'Alice', handle: 'alice.bsky.social'); + expectAccountRow(displayName: 'bob.bsky.social', handle: 'bob.bsky.social'); }); testWidgets('shows checkmark only on active account', (tester) async { diff --git a/test/features/alerts/presentation/alerts_screen_test.dart b/test/features/alerts/presentation/alerts_screen_test.dart index c077345..371ea11 100644 --- a/test/features/alerts/presentation/alerts_screen_test.dart +++ b/test/features/alerts/presentation/alerts_screen_test.dart @@ -1,9 +1,7 @@ -import 'package:poptart_core/poptart_core.dart'; +import 'package:bloc_test/bloc_test.dart'; import 'package:bluesky_poptart/app/bsky/actor/defs.dart' as app_actor; -import 'package:bluesky_poptart/app/bsky/notification/list_notifications.dart' as bsky; import 'package:bluesky_poptart/chat/bsky/actor/defs.dart' as chat_actor; import 'package:bluesky_poptart/chat/bsky/convo/defs.dart'; -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'; @@ -18,6 +16,9 @@ import 'package:lazurite/features/notifications/data/notification_repository.dar import 'package:lazurite/shared/presentation/widgets/app_screen_entrance.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/fixtures/notification.dart'; +import '../../../helpers/connectivity_helpers.dart'; + class MockNotificationRepository extends Mock implements NotificationRepository {} class MockConvoRepository extends Mock implements ConvoRepository {} @@ -33,12 +34,7 @@ void main() { notificationRepository = MockNotificationRepository(); convoRepository = MockConvoRepository(); connectivityCubit = MockConnectivityCubit(); - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); - whenListen( - connectivityCubit, - const Stream.empty(), - initialState: const ConnectivityState.online(), - ); + stubConnectivityCubit(connectivityCubit, state: const ConnectivityState.online()); when( () => notificationRepository.listNotifications( @@ -48,13 +44,10 @@ void main() { ).thenAnswer( (_) async => NotificationListResult( notifications: [ - bsky.Notification( - uri: AtUri.parse('at://did:plc:alice/app.bsky.feed.post/abc'), - cid: 'cid-123', + testNotification( + uri: 'at://did:plc:alice/app.bsky.feed.post/abc', author: const app_actor.ProfileView(did: 'did:plc:alice', handle: 'alice.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.like), - record: {'text': 'Test post'}, - isRead: false, + record: const {'text': 'Test post'}, indexedAt: DateTime.now(), ), ], @@ -177,7 +170,6 @@ void main() { testWidgets('shows notifications, messages, and requests tabs', (tester) async { await tester.pumpWidget(buildSubject('/alerts')); await tester.pumpAndSettle(); - expect(find.byType(AppScreenEntrance), findsOneWidget); expect(find.text('Notifications'), findsOneWidget); expect(find.text('Messages'), findsOneWidget); @@ -188,7 +180,6 @@ void main() { testWidgets('shows unread badges for notifications and messages tabs', (tester) async { await tester.pumpWidget(buildSubject('/alerts')); await tester.pumpAndSettle(); - expect(find.byKey(const ValueKey('alerts-tab-unread-notifications')), findsOneWidget); expect(find.byKey(const ValueKey('alerts-tab-unread-messages')), findsOneWidget); expect(find.text('1'), findsOneWidget); @@ -198,7 +189,6 @@ void main() { testWidgets('opens messages tab from deep link', (tester) async { await tester.pumpWidget(buildSubject('/alerts/messages')); await tester.pumpAndSettle(); - expect(find.text('other.bsky.social'), findsOneWidget); expect(find.text('Mark All Read'), findsNothing); }); @@ -206,7 +196,6 @@ void main() { testWidgets('opens requests tab from deep link', (tester) async { await tester.pumpWidget(buildSubject('/alerts/requests')); await tester.pumpAndSettle(); - expect(find.text('requester.bsky.social'), findsOneWidget); expect(find.text('No message requests'), findsNothing); }); diff --git a/test/features/auth/data/auth_repository_test.dart b/test/features/auth/data/auth_repository_test.dart index 2f9a335..fb25747 100644 --- a/test/features/auth/data/auth_repository_test.dart +++ b/test/features/auth/data/auth_repository_test.dart @@ -6,12 +6,14 @@ 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 '../../../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'; import 'package:url_launcher/url_launcher.dart'; +import '../../../helpers/fixtures/auth.dart'; +import '../../../helpers/fixtures/network.dart'; + class MockAppDatabase extends Mock implements AppDatabase {} class MockSlingshotClient extends Mock implements SlingshotClient {} @@ -143,7 +145,7 @@ void main() { group('saveSession', () { test('should save session to database', () async { - const tokens = AuthTokens( + final tokens = testAuthTokens( accessToken: 'access_token', refreshToken: 'refresh_token', did: 'did:plc:abc123', @@ -161,7 +163,7 @@ void main() { }); test('persists OAuth restore metadata when saving an OAuth session', () async { - final tokens = AuthTokens( + final tokens = testAuthTokens( accessToken: 'opaque-access-token', refreshToken: 'refresh-token', expiresAt: DateTime.utc(2030), @@ -187,7 +189,7 @@ void main() { }); test('should mark the saved session active when requested', () async { - const tokens = AuthTokens(accessToken: 'access_token', did: 'did:plc:abc123', handle: 'user.bsky.social'); + final tokens = testAuthTokens(accessToken: 'access_token', did: 'did:plc:abc123', handle: 'user.bsky.social'); when(() => mockDatabase.insertAccount(any())).thenAnswer((_) async => 1); when( @@ -222,7 +224,6 @@ void main() { when(() => mockDatabase.getActiveAccount()).thenAnswer((_) async => account); final restored = await authRepository.restoreSession(); - expect(restored, isNotNull); expect(restored!.handle, equals('user.bsky.social')); }); @@ -253,7 +254,6 @@ void main() { when(() => mockDatabase.getActiveAccount()).thenAnswer((_) async => account); final restored = await authRepository.restoreSession(); - expect(restored, isNotNull); expect(restored!.did, equals(account.did)); expect(restored.isExpired, isTrue); @@ -288,7 +288,7 @@ void main() { }, ); - const currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: 'expired-access-token', refreshToken: 'refresh-token', did: 'did:plc:abc123', @@ -327,7 +327,6 @@ void main() { allowRefreshToComplete.complete(); final refreshed = await Future.wait([firstRefresh, secondRefresh]); - expect(refreshCalls, equals(1)); expect(refreshed.map((tokens) => tokens?.refreshToken), everyElement('new-refresh-token')); }); @@ -339,7 +338,7 @@ void main() { throw StateError('stale refresh token should not be used'), ); - const currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: 'expired-access-token', refreshToken: 'stale-refresh-token', did: 'did:plc:abc123', @@ -347,7 +346,7 @@ void main() { service: 'bsky.social', authMethod: AuthMethod.appPassword, ); - final newerSession = AuthTokens( + final newerSession = testAuthTokens( accessToken: 'new-access-token', refreshToken: 'new-refresh-token', expiresAt: DateTime.now().add(const Duration(hours: 1)), @@ -405,7 +404,7 @@ void main() { }, ); - const currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: 'expired-access-token', refreshToken: 'old-refresh-token', did: 'did:plc:abc123', @@ -413,7 +412,7 @@ void main() { service: 'bsky.social', authMethod: AuthMethod.appPassword, ); - final newerSession = AuthTokens( + final newerSession = testAuthTokens( accessToken: 'newer-access-token', refreshToken: 'newer-refresh-token', expiresAt: DateTime.now().add(const Duration(hours: 1)), @@ -451,7 +450,6 @@ void main() { ).thenAnswer((_) async => false); final refreshed = await authRepository.refreshSession(currentSession); - expect(refreshed, isNotNull); expect(refreshed!.refreshToken, equals('newer-refresh-token')); verifyNever(() => mockDatabase.insertAccount(any())); @@ -476,7 +474,7 @@ void main() { }, ); - const currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: 'expired-access-token', refreshToken: 'old-refresh-token', did: 'did:plc:abc123', @@ -525,7 +523,7 @@ void main() { throw StateError('refresh should be handled by the lock holder'), ); - const currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: 'expired-access-token', refreshToken: 'old-refresh-token', did: 'did:plc:abc123', @@ -534,7 +532,7 @@ void main() { authMethod: AuthMethod.appPassword, ); - final newerSession = AuthTokens( + final newerSession = testAuthTokens( accessToken: 'new-access-token', refreshToken: 'new-refresh-token', expiresAt: DateTime.now().add(const Duration(hours: 1)), @@ -586,7 +584,7 @@ void main() { throw Exception('refresh service unavailable'), ); - const currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: 'expired-access-token', refreshToken: 'refresh-token', did: 'did:plc:abc123', @@ -608,7 +606,7 @@ void main() { throw _unauthorizedRefreshException(), ); - const currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: 'expired-access-token', refreshToken: 'refresh-token', did: 'did:plc:abc123', @@ -627,7 +625,6 @@ void main() { when(() => mockDatabase.deleteSetting(AppDatabase.activeAccountDidSettingKey)).thenAnswer((_) async => 1); await expectLater(authRepository.refreshSession(currentSession), throwsA(isA())); - verify(() => mockDatabase.deleteAccount(currentSession.did)).called(1); verify(() => mockDatabase.deleteSetting(AppDatabase.activeAccountDidSettingKey)).called(1); }); @@ -639,7 +636,7 @@ void main() { throw _unauthorizedRefreshException(), ); - const currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: 'expired-access-token', refreshToken: 'stale-refresh-token', did: 'did:plc:abc123', @@ -647,7 +644,7 @@ void main() { service: 'bsky.social', authMethod: AuthMethod.appPassword, ); - const newerSession = AuthTokens( + final newerSession = testAuthTokens( accessToken: 'new-access-token', refreshToken: 'new-refresh-token', did: 'did:plc:abc123', @@ -684,7 +681,6 @@ void main() { storedAuthService: 'https://oauth.custom.example', issuer: null, ); - expect(candidates, equals(['oauth.custom.example', 'bsky.social'])); }); @@ -715,7 +711,7 @@ void main() { ); }, ); - final currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: 'opaque-access-token', refreshToken: 'refresh-token', expiresAt: DateTime.utc(2029), @@ -755,7 +751,6 @@ void main() { ).thenAnswer((_) async => true); final refreshed = await authRepository.refreshSession(currentSession); - expect(restoredSession.accessToken, 'opaque-access-token'); expect(restoredSession.sub, currentSession.did); expect(restoredSession.scope, currentSession.oauthScope); @@ -806,7 +801,7 @@ void main() { }, ); - const currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: 'REPLACE_ME', refreshToken: 'refresh-token', did: 'did:plc:abc123', @@ -889,7 +884,7 @@ void main() { }, ); - final currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: expiredAccessToken, refreshToken: 'refresh-token', did: 'did:plc:abc123', @@ -978,7 +973,7 @@ void main() { }, ); - const currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: 'REPLACE_ME', refreshToken: 'refresh-token', did: 'did:plc:abc123', @@ -1062,7 +1057,7 @@ void main() { }, ); - const currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: 'REPLACE_ME', refreshToken: 'refresh-token', did: 'did:plc:abc123', @@ -1101,7 +1096,6 @@ void main() { ).thenAnswer((_) async => true); final refreshed = await authRepository.refreshSession(sessionWithJwt); - expect(refreshed, isNotNull); expect(requestedClientIds, equals([AuthRepository.kClientId])); }); @@ -1125,7 +1119,7 @@ void main() { }, ); - final currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: expiredAccessToken, refreshToken: 'refresh-token', did: 'did:plc:abc123', @@ -1164,7 +1158,7 @@ void main() { }, ); - final currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: expiredAccessToken, refreshToken: 'refresh-token', did: 'did:plc:abc123', @@ -1188,7 +1182,6 @@ void main() { when(() => mockDatabase.deleteSetting(AppDatabase.activeAccountDidSettingKey)).thenAnswer((_) async => 1); await expectLater(authRepository.refreshSession(currentSession), throwsA(isA())); - verify(() => mockDatabase.deleteAccount(currentSession.did)).called(1); verify(() => mockDatabase.deleteSetting(AppDatabase.activeAccountDidSettingKey)).called(1); }); @@ -1219,7 +1212,7 @@ void main() { }, ); - final currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: expiredAccessToken, refreshToken: 'stale-refresh-token', did: 'did:plc:abc123', @@ -1243,7 +1236,6 @@ void main() { ).thenAnswer((_) async => _accountForTokens(newerSession)); final refreshed = await authRepository.refreshSession(currentSession); - expect(refreshed, isNotNull); expect(refreshed!.refreshToken, equals('new-refresh-token')); verifyNever(() => mockDatabase.deleteAccount(any())); @@ -1274,7 +1266,7 @@ void main() { }, ); - final currentSession = AuthTokens( + final currentSession = testAuthTokens( accessToken: expiredAccessToken, refreshToken: 'refresh-token', did: 'did:plc:abc123', @@ -1303,7 +1295,6 @@ void main() { resolvedPdsHost: 'https://porcini.us-east.host.bsky.network', resolvedAuthService: 'https://bsky.social', ); - expect(candidates, equals(['bsky.social', 'blacksky.community', 'porcini.us-east.host.bsky.network'])); }); @@ -1313,7 +1304,6 @@ void main() { resolvedPdsHost: 'bsky.social', resolvedAuthService: 'bsky.social', ); - expect(candidates, equals(['bsky.social'])); }); }); @@ -1349,15 +1339,8 @@ void main() { loadClientMetadata: (_) async => _testClientMetadata(), oauthServiceResolver: () => 'pending-auth.example', resolveHandleDid: (_) async => 'did:plc:alice', - resolveDidDocument: (_) async => const { - 'service': [ - { - 'id': '#atproto_pds', - 'type': 'AtprotoPersonalDataServer', - 'serviceEndpoint': 'https://porcini.us-east.host.bsky.network', - }, - ], - }, + resolveDidDocument: (_) async => + testDidDocument(serviceEndpoint: 'https://porcini.us-east.host.bsky.network'), resolveAuthorizationServiceForPdsHost: (_) async => null, launchUrlWithMode: (url, _) async { launchedUrls.add(url); @@ -1394,7 +1377,7 @@ void main() { required fallbackPdsHost, required oauthService, oauthClientId, - }) async => AuthTokens( + }) async => testAuthTokens( accessToken: session.accessToken, refreshToken: session.refreshToken, expiresAt: session.expiresAt, @@ -1512,9 +1495,7 @@ void main() { authRepository = AuthRepository( database: mockDatabase, resolveDidDocument: (_) async => { - 'service': [ - {'id': '#atproto_pds', 'type': 'AtprotoPersonalDataServer', 'serviceEndpoint': 'https://pds.example'}, - ], + 'service': [testPdsService(serviceEndpoint: 'https://pds.example')], }, ); @@ -1625,7 +1606,7 @@ void main() { group('oauth callback exchange coordination', () { test('joins duplicate callback deliveries to one token exchange', () async { - const tokens = AuthTokens(accessToken: 'access', did: 'did:plc:abc123', handle: 'user.bsky.social'); + final tokens = testAuthTokens(accessToken: 'access', did: 'did:plc:abc123', handle: 'user.bsky.social'); final exchangeCompleter = Completer(); var exchangeCalls = 0; @@ -1664,10 +1645,8 @@ void main() { loadClientMetadata: (_) async => _testClientMetadata(), oauthServiceResolver: () => 'bsky.social', resolveHandleDid: (_) async => 'did:plc:alice', - resolveDidDocument: (_) async => const { - 'service': [ - {'id': '#atproto_pds', 'type': 'AtprotoPersonalDataServer', 'serviceEndpoint': 'https://bsky.social'}, - ], + resolveDidDocument: (_) async => { + 'service': [testPdsService(serviceEndpoint: 'https://bsky.social')], }, resolveAuthorizationServiceForPdsHost: (_) async => null, oauthAuthorizeSession: (client, identity) async { @@ -1876,7 +1855,6 @@ void main() { ); await authRepository.dismissOAuthBrowserForLaunchMode(LaunchMode.inAppBrowserView); - expect(supportChecks, equals(1)); expect(closeCalls, equals(1)); }); @@ -1897,7 +1875,6 @@ void main() { ); await authRepository.dismissOAuthBrowserForLaunchMode(LaunchMode.externalApplication); - expect(supportChecks, equals(0)); expect(closeCalls, equals(0)); }); diff --git a/test/features/auth/data/models/auth_models_test.dart b/test/features/auth/data/models/auth_models_test.dart index ce43dc6..926ab38 100644 --- a/test/features/auth/data/models/auth_models_test.dart +++ b/test/features/auth/data/models/auth_models_test.dart @@ -1,10 +1,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/features/auth/data/models/auth_models.dart'; +import '../../../../helpers/fixtures/auth.dart'; + void main() { group('AuthTokens', () { test('should create AuthTokens with all fields', () { - const tokens = AuthTokens( + final tokens = testAuthTokens( accessToken: 'access_token', refreshToken: 'refresh_token', did: 'did:plc:abc123', @@ -26,7 +28,14 @@ void main() { }); test('should create AuthTokens without optional fields', () { - const tokens = AuthTokens(accessToken: 'access_token', did: 'did:plc:abc123', handle: 'user.bsky.social'); + final tokens = testAuthTokens( + accessToken: 'access_token', + refreshToken: null, + did: 'did:plc:abc123', + handle: 'user.bsky.social', + service: null, + expiresAt: null, + ); expect(tokens.accessToken, equals('access_token')); expect(tokens.refreshToken, isNull); @@ -36,7 +45,14 @@ void main() { }); test('should copy with new values', () { - const tokens = AuthTokens(accessToken: 'old_token', did: 'did:plc:abc123', handle: 'user.bsky.social'); + final tokens = testAuthTokens( + accessToken: 'old_token', + refreshToken: null, + did: 'did:plc:abc123', + handle: 'user.bsky.social', + service: null, + expiresAt: null, + ); final newTokens = tokens.copyWith( accessToken: 'new_token', @@ -53,7 +69,7 @@ void main() { }); test('should identify oauth-backed sessions', () { - const tokens = AuthTokens( + final tokens = testAuthTokens( accessToken: 'access_token', refreshToken: 'refresh_token', did: 'did:plc:abc123', @@ -68,10 +84,12 @@ void main() { test('should check if tokens are expired', () { final expiredDate = DateTime.now().subtract(const Duration(hours: 1)); - final tokens = AuthTokens( + final tokens = testAuthTokens( accessToken: 'token', + refreshToken: null, did: 'did:plc:abc123', handle: 'user.bsky.social', + service: null, expiresAt: expiredDate, ); @@ -80,10 +98,12 @@ void main() { test('should check if tokens are not expired', () { final futureDate = DateTime.now().add(const Duration(hours: 1)); - final tokens = AuthTokens( + final tokens = testAuthTokens( accessToken: 'token', + refreshToken: null, did: 'did:plc:abc123', handle: 'user.bsky.social', + service: null, expiresAt: futureDate, ); @@ -91,14 +111,35 @@ void main() { }); test('should check if tokens without expiry are not expired', () { - const tokens = AuthTokens(accessToken: 'token', did: 'did:plc:abc123', handle: 'user.bsky.social'); + final tokens = testAuthTokens( + accessToken: 'token', + refreshToken: null, + did: 'did:plc:abc123', + handle: 'user.bsky.social', + service: null, + expiresAt: null, + ); 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'); + final tokens1 = testAuthTokens( + accessToken: 'token', + refreshToken: null, + did: 'did:plc:abc123', + handle: 'user.bsky.social', + service: null, + expiresAt: null, + ); + final tokens2 = testAuthTokens( + accessToken: 'token', + refreshToken: null, + did: 'did:plc:abc123', + handle: 'user.bsky.social', + service: null, + expiresAt: null, + ); expect(tokens1, equals(tokens2)); }); @@ -123,7 +164,6 @@ void main() { test('should create User without optional fields', () { const user = User(did: 'did:plc:abc123', handle: 'user.bsky.social'); - expect(user.did, equals('did:plc:abc123')); expect(user.handle, equals('user.bsky.social')); expect(user.displayName, isNull); @@ -133,9 +173,7 @@ void main() { test('should copy with new values', () { const user = User(did: 'did:plc:abc123', handle: 'user.bsky.social'); - final newUser = user.copyWith(displayName: 'New Name', avatar: 'https://example.com/new-avatar.jpg'); - expect(newUser.did, equals('did:plc:abc123')); expect(newUser.displayName, equals('New Name')); expect(newUser.avatar, equals('https://example.com/new-avatar.jpg')); @@ -144,7 +182,6 @@ void main() { test('should support value equality', () { const user1 = User(did: 'did:plc:abc123', handle: 'user.bsky.social'); const user2 = User(did: 'did:plc:abc123', handle: 'user.bsky.social'); - expect(user1, equals(user2)); }); }); diff --git a/test/features/auth/presentation/login_screen_test.dart b/test/features/auth/presentation/login_screen_test.dart index 39a3f96..c8bbaea 100644 --- a/test/features/auth/presentation/login_screen_test.dart +++ b/test/features/auth/presentation/login_screen_test.dart @@ -17,6 +17,7 @@ import 'package:lazurite/features/auth/presentation/login_screen.dart'; 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/assertion_helpers.dart'; class MockAuthBloc extends MockBloc implements AuthBloc {} @@ -341,8 +342,7 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Saved accounts'), findsOneWidget); - expect(find.text('Alice'), findsOneWidget); - expect(find.text('@alice.bsky.social'), findsOneWidget); + expectAccountRow(displayName: 'Alice', handle: 'alice.bsky.social'); await tester.tap(find.byKey(const ValueKey('saved-account-did:plc:alice'))); await tester.pumpAndSettle(); diff --git a/test/features/compose/presentation/compose_screen_test.dart b/test/features/compose/presentation/compose_screen_test.dart index e580693..41cb01c 100644 --- a/test/features/compose/presentation/compose_screen_test.dart +++ b/test/features/compose/presentation/compose_screen_test.dart @@ -15,6 +15,7 @@ import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; import 'package:lazurite/features/profile/data/profile_repository.dart'; import 'package:lazurite/shared/presentation/widgets/profile_avatar.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/connectivity_helpers.dart'; class MockComposeBloc extends MockBloc implements ComposeBloc {} @@ -76,12 +77,7 @@ void main() { connectivityCubit = MockConnectivityCubit(); authBloc = MockAuthBloc(); profileRepository = MockProfileRepository(); - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); - whenListen( - connectivityCubit, - const Stream.empty(), - initialState: const ConnectivityState.online(), - ); + stubConnectivityCubit(connectivityCubit, state: const ConnectivityState.online()); when(() => authBloc.state).thenReturn(const AuthState.unauthenticated()); whenListen(authBloc, const Stream.empty(), initialState: const AuthState.unauthenticated()); }); diff --git a/test/features/devtools/cubit/dev_tools_cubit_test.dart b/test/features/devtools/cubit/dev_tools_cubit_test.dart index 9ef926b..4297580 100644 --- a/test/features/devtools/cubit/dev_tools_cubit_test.dart +++ b/test/features/devtools/cubit/dev_tools_cubit_test.dart @@ -7,6 +7,7 @@ import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/features/devtools/cubit/dev_tools_cubit.dart'; +import '../../../helpers/fixtures/network.dart'; class FakeDevToolsRepository implements DevToolsRepository { FakeDevToolsRepository({ @@ -157,13 +158,11 @@ void main() { expect(serviceHost, 'alice.host'); } describeRepoCalls++; - return const RepoDescribeRepoOutput( + return RepoDescribeRepoOutput( handle: 'alice.bsky.social', did: 'did:plc:alice', didDoc: { - 'service': [ - {'id': '#atproto_pds', 'type': 'AtprotoPersonalDataServer', 'serviceEndpoint': 'https://alice.host'}, - ], + 'service': [testPdsService(serviceEndpoint: 'https://alice.host')], }, collections: ['app.bsky.feed.post'], handleIsCorrect: true, @@ -229,13 +228,11 @@ void main() { expect(serviceHost, 'alice.host'); } describeRepoCalls++; - return const RepoDescribeRepoOutput( + return RepoDescribeRepoOutput( handle: 'alice.bsky.social', did: 'did:plc:alice', didDoc: { - 'service': [ - {'id': '#atproto_pds', 'type': 'AtprotoPersonalDataServer', 'serviceEndpoint': 'https://alice.host'}, - ], + 'service': [testPdsService(serviceEndpoint: 'https://alice.host')], }, collections: ['app.bsky.feed.post'], handleIsCorrect: true, diff --git a/test/features/feed/bloc/feed_bloc_test.dart b/test/features/feed/bloc/feed_bloc_test.dart index e3a31a2..9592fca 100644 --- a/test/features/feed/bloc/feed_bloc_test.dart +++ b/test/features/feed/bloc/feed_bloc_test.dart @@ -4,7 +4,7 @@ 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'; +import '../../../helpers/fixtures/feed.dart'; class MockFeedRepository extends Mock implements FeedRepository {} @@ -16,7 +16,10 @@ void main() { }); group('FeedBloc', () { - final samplePost = testFeedViewPost(cid: 'cid-123', record: testPostRecordJson(text: 'Hello world')); + 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 2f8536c..791ea70 100644 --- a/test/features/feed/cubit/post_thread_cubit_test.dart +++ b/test/features/feed/cubit/post_thread_cubit_test.dart @@ -1,11 +1,11 @@ -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_test/flutter_test.dart'; 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'; +import '../../../helpers/fixtures/feed.dart'; class MockPostThreadRepository extends Mock implements PostThreadRepository {} @@ -18,9 +18,7 @@ void main() { const testUri = 'at://did:plc:author/app.bsky.feed.post/abc'; - final sampleThread = ThreadViewPost( - post: testPostView(cid: 'cid-123'), - ); + final sampleThread = ThreadViewPost(post: testPostView(cid: 'cid-123')); group('PostThreadCubit', () { test('initial state is loading', () { diff --git a/test/features/feed/cubit/similar_posts_cubit_test.dart b/test/features/feed/cubit/similar_posts_cubit_test.dart index 34d4b7e..1a9fcc0 100644 --- a/test/features/feed/cubit/similar_posts_cubit_test.dart +++ b/test/features/feed/cubit/similar_posts_cubit_test.dart @@ -1,5 +1,5 @@ -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_test/flutter_test.dart'; import 'package:lazurite/core/network/constellation_client.dart'; import 'package:lazurite/core/network/poptart_client_adapter.dart'; @@ -7,7 +7,7 @@ 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'; +import '../../../helpers/fixtures/feed.dart'; class MockBluesky extends Mock implements Bluesky {} diff --git a/test/features/feed/data/feed_repository_cache_test.dart b/test/features/feed/data/feed_repository_cache_test.dart index d88ce97..d95bfb9 100644 --- a/test/features/feed/data/feed_repository_cache_test.dart +++ b/test/features/feed/data/feed_repository_cache_test.dart @@ -11,7 +11,8 @@ 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 '../../../helpers/test_utils.dart'; +import '../../../helpers/fixtures/auth.dart'; +import '../../../helpers/fixtures/network.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 e525b7a..4015ebf 100644 --- a/test/features/feed/data/feed_repository_test.dart +++ b/test/features/feed/data/feed_repository_test.dart @@ -7,7 +7,7 @@ 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'; +import '../../../helpers/fixtures/feed.dart'; class MockFeedRepository extends Mock implements FeedRepository {} diff --git a/test/features/feed/data/liked_posts_repository_test.dart b/test/features/feed/data/liked_posts_repository_test.dart index 6d0775e..1071318 100644 --- a/test/features/feed/data/liked_posts_repository_test.dart +++ b/test/features/feed/data/liked_posts_repository_test.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -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/get_actor_likes.dart'; import 'package:drift/drift.dart' show Value; @@ -13,6 +12,7 @@ import 'package:lazurite/features/feed/data/liked_posts_repository.dart'; import 'package:lazurite/features/search/data/semantic_indexer.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/fixtures/feed.dart'; import '../../../helpers/test_bluesky_client.dart'; class MockSemanticIndexer extends Mock implements SemanticIndexer {} @@ -458,7 +458,6 @@ void main() { ); await repo.removeLike(_accountDid, postUri); - final result = await database.getLikedPosts(_otherAccountDid); expect(result, hasLength(1)); }); @@ -490,7 +489,6 @@ void main() { ); await repo.syncLikes(_accountDid); - verify(() => mockIndexer.queueIndexPost(postUri, any(), _accountDid, 'liked')).called(1); }); @@ -541,7 +539,6 @@ void main() { ); await repo.removeLike(_accountDid, postUri); - verify(() => mockIndexer.removePost(postUri)).called(1); }); }); @@ -646,22 +643,12 @@ void main() { }); } -FeedViewPost _makeFeedViewPost(String uriStr, {DateTime? indexedAt, DateTime? createdAt}) { - final resolvedCreatedAt = createdAt ?? DateTime.utc(2026, 1, 1); - return FeedViewPost( - post: PostView( - uri: AtUri.parse(uriStr), - cid: 'cid-${uriStr.hashCode}', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social'), - record: { - r'$type': 'app.bsky.feed.post', - 'text': 'Test post', - 'createdAt': resolvedCreatedAt.toUtc().toIso8601String(), - }, - indexedAt: indexedAt ?? DateTime.utc(2026, 1, 1), - ), - ); -} +FeedViewPost _makeFeedViewPost(String uriStr, {DateTime? indexedAt, DateTime? createdAt}) => testFeedViewPost( + uri: uriStr, + cid: 'cid-${uriStr.hashCode}', + record: testPostRecordJson(text: 'Test post', createdAt: createdAt ?? DateTime.utc(2026, 1, 1)), + indexedAt: indexedAt ?? DateTime.utc(2026, 1, 1), +); Bluesky _testBluesky({required _FakeFeedService feed}) => testBluesky(getClient: feed.get); 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 463c9d3..0cc2006 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,8 @@ 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 '../../../helpers/test_utils.dart'; +import '../../../helpers/fixtures/auth.dart'; +import '../../../helpers/fixtures/network.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 6ba1b89..2124bf5 100644 --- a/test/features/feed/data/post_thread_repository_test.dart +++ b/test/features/feed/data/post_thread_repository_test.dart @@ -3,7 +3,7 @@ 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'; +import '../../../helpers/fixtures/feed.dart'; class MockPostThreadRepository extends Mock implements PostThreadRepository {} diff --git a/test/features/feed/data/similar_posts_repository_test.dart b/test/features/feed/data/similar_posts_repository_test.dart index 10c87ed..5f78fe3 100644 --- a/test/features/feed/data/similar_posts_repository_test.dart +++ b/test/features/feed/data/similar_posts_repository_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_test/flutter_test.dart'; import 'package:lazurite/core/network/constellation_client.dart'; @@ -6,6 +5,8 @@ import 'package:lazurite/core/network/poptart_client_adapter.dart'; import 'package:lazurite/features/feed/data/similar_posts_repository.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/fixtures/feed.dart'; + class MockBluesky extends Mock implements Bluesky {} void main() { @@ -102,14 +103,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/presentation/compact_post_card_test.dart b/test/features/feed/presentation/compact_post_card_test.dart index 9fb6c56..7e77ee8 100644 --- a/test/features/feed/presentation/compact_post_card_test.dart +++ b/test/features/feed/presentation/compact_post_card_test.dart @@ -9,6 +9,7 @@ import 'package:lazurite/core/theme/app_theme.dart'; import 'package:lazurite/features/feed/presentation/widgets/compact_post_card.dart'; import 'package:lazurite/shared/presentation/widgets/profile_avatar.dart'; import 'package:poptart_core/poptart_core.dart'; +import '../../../helpers/assertion_helpers.dart'; FeedViewPost _makePost({UFeedViewPostReason? reason, UPostViewEmbed? embed}) { final record = FeedPostRecord(text: 'Compact post', createdAt: DateTime.utc(2026, 3, 16)); @@ -110,8 +111,7 @@ void main() { (widget) => widget is RichText && widget.text.toPlainText() == 'Quoted context', ); - expect(find.text('Quoted User'), findsOneWidget); - expect(find.text('@quoted.bsky.social'), findsOneWidget); + expectAccountRow(displayName: 'Quoted User', handle: 'quoted.bsky.social'); expect(quotedText, findsOneWidget); final richText = tester.widget(quotedText); diff --git a/test/features/feed/presentation/feed_detail_screen_test.dart b/test/features/feed/presentation/feed_detail_screen_test.dart index 4c7b597..0090e3d 100644 --- a/test/features/feed/presentation/feed_detail_screen_test.dart +++ b/test/features/feed/presentation/feed_detail_screen_test.dart @@ -15,8 +15,9 @@ 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'; +import '../../../helpers/fixtures/feed.dart'; +import '../../../helpers/fixtures/settings.dart'; +import '../../../helpers/assertion_helpers.dart'; class MockFeedRepository extends Mock implements FeedRepository {} @@ -134,8 +135,7 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('Failed to load feed'), findsOneWidget); - expect(find.text('Retry'), findsOneWidget); + expectErrorState('Failed to load feed'); await tester.tap(find.text('Retry')); await tester.pumpAndSettle(); diff --git a/test/features/feed/presentation/home_feed_screen_test.dart b/test/features/feed/presentation/home_feed_screen_test.dart index 83285f2..5878a84 100644 --- a/test/features/feed/presentation/home_feed_screen_test.dart +++ b/test/features/feed/presentation/home_feed_screen_test.dart @@ -19,6 +19,7 @@ import 'package:lazurite/features/settings/bloc/settings_state.dart'; import 'package:lazurite/shared/presentation/widgets/animated_refresh_indicator.dart'; import 'package:lazurite/shared/presentation/widgets/app_screen_entrance.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/connectivity_helpers.dart'; class MockSettingsCubit extends MockCubit implements SettingsCubit {} @@ -81,8 +82,7 @@ void main() { final connectivityCubit = MockConnectivityCubit(); final settingsCubit = MockSettingsCubit(); final authBloc = MockAuthBloc(); - when(() => connectivityCubit.state).thenReturn(connectivityState); - whenListen(connectivityCubit, const Stream.empty(), initialState: connectivityState); + stubConnectivityCubit(connectivityCubit, state: connectivityState); when(() => settingsCubit.state).thenReturn(_settingsState(FeedLayout.comfortable)); whenListen( settingsCubit, diff --git a/test/features/feed/presentation/post_card_test.dart b/test/features/feed/presentation/post_card_test.dart index d07d70e..f6572a5 100644 --- a/test/features/feed/presentation/post_card_test.dart +++ b/test/features/feed/presentation/post_card_test.dart @@ -21,6 +21,9 @@ import 'package:poptart_core/poptart_core.dart'; import 'package:url_launcher_platform_interface/link.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +import '../../../helpers/assertion_helpers.dart'; +import '../../../helpers/fixtures/feed.dart'; + class _FakeUrlLauncher extends Fake with MockPlatformInterfaceMixin implements UrlLauncherPlatform { final List launchedUrls = []; @@ -40,19 +43,14 @@ class _FakeUrlLauncher extends Fake with MockPlatformInterfaceMixin implements U Future canLaunch(String url) async => true; } -FeedViewPost _makePost({String text = 'Hello', UFeedViewPostReason? reason}) { - final record = FeedPostRecord(text: text, createdAt: DateTime.utc(2026, 3, 16)); - return FeedViewPost( - reason: reason, - 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'), - record: record.toJson(), - indexedAt: DateTime.utc(2026, 3, 16), - ), - ); -} +FeedViewPost _makePost({String text = 'Hello', UFeedViewPostReason? reason}) => testFeedViewPost( + uri: 'at://did:plc:test/app.bsky.feed.post/xyz', + cid: 'cid-xyz', + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), + record: FeedPostRecord(text: text, createdAt: DateTime.utc(2026, 3, 16)).toJson(), + indexedAt: DateTime.utc(2026, 3, 16), + reason: reason, +); UFeedViewPostReason _makeRepostReason() { return UFeedViewPostReason.reasonRepost( @@ -98,14 +96,12 @@ FeedViewPost _makeReplyPost({String handle = 'test.bsky.social'}) { 'createdAt': DateTime.utc(2026, 3, 16).toIso8601String(), }; - return FeedViewPost( - post: PostView( - uri: const AtUri('at://did:plc:test/app.bsky.feed.post/reply'), - cid: 'cid-reply', - author: ProfileViewBasic(did: 'did:plc:test', handle: handle), - record: record, - indexedAt: DateTime.utc(2026, 3, 16), - ), + return testFeedViewPost( + uri: 'at://did:plc:test/app.bsky.feed.post/reply', + cid: 'cid-reply', + author: testProfileViewBasic(did: 'did:plc:test', handle: handle), + record: record, + indexedAt: DateTime.utc(2026, 3, 16), ); } @@ -143,14 +139,12 @@ void main() { ], ); - final post = 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'), - record: record.toJson(), - indexedAt: DateTime.utc(2026, 3, 16), - ), + final post = testFeedViewPost( + uri: 'at://did:plc:test/app.bsky.feed.post/xyz', + cid: 'cid-xyz', + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), + record: record.toJson(), + indexedAt: DateTime.utc(2026, 3, 16), ); await tester.pumpWidget(buildSubject(post)); @@ -164,20 +158,18 @@ void main() { testWidgets('renders external link card embeds', (tester) async { final record = FeedPostRecord(text: 'Read this', createdAt: DateTime.utc(2026, 3, 16)); - final post = 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'), - record: record.toJson(), - indexedAt: DateTime.utc(2026, 3, 16), - embed: const UPostViewEmbed.embedExternalView( - data: EmbedExternalView( - external: EmbedExternalViewExternal( - uri: 'https://example.com/article', - title: 'Example Article', - description: 'A useful external card', - ), + final post = testFeedViewPost( + uri: 'at://did:plc:test/app.bsky.feed.post/xyz', + cid: 'cid-xyz', + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), + record: record.toJson(), + indexedAt: DateTime.utc(2026, 3, 16), + embed: const UPostViewEmbed.embedExternalView( + data: EmbedExternalView( + external: EmbedExternalViewExternal( + uri: 'https://example.com/article', + title: 'Example Article', + description: 'A useful external card', ), ), ), @@ -193,20 +185,18 @@ void main() { testWidgets('tapping bsky external embed routes to profile in app', (tester) async { String? pushedRoute; final record = FeedPostRecord(text: 'Read this', createdAt: DateTime.utc(2026, 3, 16)); - final post = 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'), - record: record.toJson(), - indexedAt: DateTime.utc(2026, 3, 16), - embed: const UPostViewEmbed.embedExternalView( - data: EmbedExternalView( - external: EmbedExternalViewExternal( - uri: 'https://bsky.app/profile/alice.bsky.social', - title: 'Alice', - description: 'Profile link', - ), + final post = testFeedViewPost( + uri: 'at://did:plc:test/app.bsky.feed.post/xyz', + cid: 'cid-xyz', + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), + record: record.toJson(), + indexedAt: DateTime.utc(2026, 3, 16), + embed: const UPostViewEmbed.embedExternalView( + data: EmbedExternalView( + external: EmbedExternalViewExternal( + uri: 'https://bsky.app/profile/alice.bsky.social', + title: 'Alice', + description: 'Profile link', ), ), ), @@ -241,20 +231,18 @@ void main() { testWidgets('tapping non-matching external embed launches browser', (tester) async { final record = FeedPostRecord(text: 'Read this', createdAt: DateTime.utc(2026, 3, 16)); - final post = 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'), - record: record.toJson(), - indexedAt: DateTime.utc(2026, 3, 16), - embed: const UPostViewEmbed.embedExternalView( - data: EmbedExternalView( - external: EmbedExternalViewExternal( - uri: 'https://example.com/article', - title: 'External Article', - description: 'External card', - ), + final post = testFeedViewPost( + uri: 'at://did:plc:test/app.bsky.feed.post/xyz', + cid: 'cid-xyz', + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), + record: record.toJson(), + indexedAt: DateTime.utc(2026, 3, 16), + embed: const UPostViewEmbed.embedExternalView( + data: EmbedExternalView( + external: EmbedExternalViewExternal( + uri: 'https://example.com/article', + title: 'External Article', + description: 'External card', ), ), ), @@ -385,23 +373,21 @@ void main() { final quotedUri = AtUri.parse('at://did:plc:quoted/app.bsky.feed.post/quoted123'); final record = FeedPostRecord(text: 'Main post', createdAt: DateTime.utc(2026, 3, 16)); final quotedRecord = FeedPostRecord(text: 'Quoted text', createdAt: DateTime.utc(2026, 3, 15)); - final post = 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'), - record: record.toJson(), - indexedAt: DateTime.utc(2026, 3, 16), - embed: UPostViewEmbed.embedRecordView( - data: EmbedRecordView( - record: UEmbedRecordViewRecord.embedRecordViewRecord( - data: EmbedRecordViewRecord( - uri: quotedUri, - cid: 'cid-quoted', - author: const ProfileViewBasic(did: 'did:plc:quoted', handle: 'quoted.bsky.social'), - value: quotedRecord.toJson(), - indexedAt: DateTime.utc(2026, 3, 15), - ), + final post = testFeedViewPost( + uri: 'at://did:plc:test/app.bsky.feed.post/xyz', + cid: 'cid-xyz', + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), + record: record.toJson(), + indexedAt: DateTime.utc(2026, 3, 16), + embed: UPostViewEmbed.embedRecordView( + data: EmbedRecordView( + record: UEmbedRecordViewRecord.embedRecordViewRecord( + data: EmbedRecordViewRecord( + uri: quotedUri, + cid: 'cid-quoted', + author: const ProfileViewBasic(did: 'did:plc:quoted', handle: 'quoted.bsky.social'), + value: quotedRecord.toJson(), + indexedAt: DateTime.utc(2026, 3, 15), ), ), ), @@ -441,23 +427,21 @@ void main() { text: 'Quoted text that should fully expand inside the embed card', createdAt: DateTime.utc(2026, 3, 15), ); - final post = 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'), - record: FeedPostRecord(text: 'Main post', createdAt: DateTime.utc(2026, 3, 16)).toJson(), - indexedAt: DateTime.utc(2026, 3, 16), - embed: UPostViewEmbed.embedRecordView( - data: EmbedRecordView( - record: UEmbedRecordViewRecord.embedRecordViewRecord( - data: EmbedRecordViewRecord( - uri: AtUri.parse('at://did:plc:quoted/app.bsky.feed.post/quoted123'), - cid: 'cid-quoted', - author: const ProfileViewBasic(did: 'did:plc:quoted', handle: 'quoted.bsky.social'), - value: quotedRecord.toJson(), - indexedAt: DateTime.utc(2026, 3, 15), - ), + final post = testFeedViewPost( + uri: 'at://did:plc:test/app.bsky.feed.post/xyz', + cid: 'cid-xyz', + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), + record: FeedPostRecord(text: 'Main post', createdAt: DateTime.utc(2026, 3, 16)).toJson(), + indexedAt: DateTime.utc(2026, 3, 16), + embed: UPostViewEmbed.embedRecordView( + data: EmbedRecordView( + record: UEmbedRecordViewRecord.embedRecordViewRecord( + data: EmbedRecordViewRecord( + uri: AtUri.parse('at://did:plc:quoted/app.bsky.feed.post/quoted123'), + cid: 'cid-quoted', + author: const ProfileViewBasic(did: 'did:plc:quoted', handle: 'quoted.bsky.social'), + value: quotedRecord.toJson(), + indexedAt: DateTime.utc(2026, 3, 15), ), ), ), @@ -502,24 +486,22 @@ void main() { embeds: [deepest], ); final firstRecord = FeedPostRecord(text: 'First level quote', createdAt: DateTime.utc(2026, 3, 15)); - final post = 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'), - record: FeedPostRecord(text: 'Main post', createdAt: DateTime.utc(2026, 3, 16)).toJson(), - indexedAt: DateTime.utc(2026, 3, 16), - embed: UPostViewEmbed.embedRecordView( - data: EmbedRecordView( - record: UEmbedRecordViewRecord.embedRecordViewRecord( - data: EmbedRecordViewRecord( - uri: AtUri.parse('at://did:plc:first/app.bsky.feed.post/first'), - cid: 'cid-first', - author: const ProfileViewBasic(did: 'did:plc:first', handle: 'first.bsky.social', displayName: 'first'), - value: firstRecord.toJson(), - embeds: [second], - indexedAt: DateTime.utc(2026, 3, 15), - ), + final post = testFeedViewPost( + uri: 'at://did:plc:test/app.bsky.feed.post/xyz', + cid: 'cid-xyz', + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), + record: FeedPostRecord(text: 'Main post', createdAt: DateTime.utc(2026, 3, 16)).toJson(), + indexedAt: DateTime.utc(2026, 3, 16), + embed: UPostViewEmbed.embedRecordView( + data: EmbedRecordView( + record: UEmbedRecordViewRecord.embedRecordViewRecord( + data: EmbedRecordViewRecord( + uri: AtUri.parse('at://did:plc:first/app.bsky.feed.post/first'), + cid: 'cid-first', + author: const ProfileViewBasic(did: 'did:plc:first', handle: 'first.bsky.social', displayName: 'first'), + value: firstRecord.toJson(), + embeds: [second], + indexedAt: DateTime.utc(2026, 3, 15), ), ), ), @@ -566,48 +548,46 @@ void main() { testWidgets('uses unique image hero tags across record-with-media and quoted embeds', (tester) async { final quotedRecord = FeedPostRecord(text: 'Quoted with image', createdAt: DateTime.utc(2026, 3, 15)); - final post = 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'), - record: FeedPostRecord(text: 'Main post with media quote', createdAt: DateTime.utc(2026, 3, 16)).toJson(), - indexedAt: DateTime.utc(2026, 3, 16), - embed: UPostViewEmbed.embedRecordWithMediaView( - data: EmbedRecordWithMediaView( - media: const UEmbedRecordWithMediaViewMedia.embedImagesView( - data: EmbedImagesView( - images: [ - EmbedImagesViewImage( - thumb: 'https://example.com/main-thumb.jpg', - fullsize: 'https://example.com/main-full.jpg', - alt: 'main image', - ), - ], - ), + final post = testFeedViewPost( + uri: 'at://did:plc:test/app.bsky.feed.post/xyz', + cid: 'cid-xyz', + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), + record: FeedPostRecord(text: 'Main post with media quote', createdAt: DateTime.utc(2026, 3, 16)).toJson(), + indexedAt: DateTime.utc(2026, 3, 16), + embed: UPostViewEmbed.embedRecordWithMediaView( + data: EmbedRecordWithMediaView( + media: const UEmbedRecordWithMediaViewMedia.embedImagesView( + data: EmbedImagesView( + images: [ + EmbedImagesViewImage( + thumb: 'https://example.com/main-thumb.jpg', + fullsize: 'https://example.com/main-full.jpg', + alt: 'main image', + ), + ], ), - record: EmbedRecordView( - record: UEmbedRecordViewRecord.embedRecordViewRecord( - data: EmbedRecordViewRecord( - uri: AtUri.parse('at://did:plc:quoted/app.bsky.feed.post/quoted123'), - cid: 'cid-quoted', - author: const ProfileViewBasic(did: 'did:plc:quoted', handle: 'quoted.bsky.social'), - value: quotedRecord.toJson(), - embeds: [ - const UEmbedRecordViewRecordEmbeds.embedImagesView( - data: EmbedImagesView( - images: [ - EmbedImagesViewImage( - thumb: 'https://example.com/quoted-thumb.jpg', - fullsize: 'https://example.com/quoted-full.jpg', - alt: 'quoted image', - ), - ], - ), + ), + record: EmbedRecordView( + record: UEmbedRecordViewRecord.embedRecordViewRecord( + data: EmbedRecordViewRecord( + uri: AtUri.parse('at://did:plc:quoted/app.bsky.feed.post/quoted123'), + cid: 'cid-quoted', + author: const ProfileViewBasic(did: 'did:plc:quoted', handle: 'quoted.bsky.social'), + value: quotedRecord.toJson(), + embeds: [ + const UEmbedRecordViewRecordEmbeds.embedImagesView( + data: EmbedImagesView( + images: [ + EmbedImagesViewImage( + thumb: 'https://example.com/quoted-thumb.jpg', + fullsize: 'https://example.com/quoted-full.jpg', + alt: 'quoted image', + ), + ], ), - ], - indexedAt: DateTime.utc(2026, 3, 15), - ), + ), + ], + indexedAt: DateTime.utc(2026, 3, 15), ), ), ), @@ -666,7 +646,6 @@ void main() { await tester.pumpWidget(MaterialApp.router(routerConfig: router)); await tester.pumpAndSettle(); - expect(find.text('STARTER PACK'), findsOneWidget); expect(find.text('Starter Pack Picks'), findsOneWidget); expect(find.text('People worth following'), findsOneWidget); @@ -674,7 +653,6 @@ void main() { await tester.tap(find.text('Starter Pack Picks')); await tester.pumpAndSettle(); - expect(pushedRoute, isNotNull); expect(Uri.parse(pushedRoute!).path, '/starter-pack'); expect(Uri.decodeComponent(Uri.parse(pushedRoute!).queryParameters['uri']!), packUri.toString()); @@ -682,7 +660,6 @@ void main() { testWidgets('renders feed, list, labeler, and unknown record embeds', (tester) async { const creator = ProfileView(did: 'did:plc:creator', handle: 'creator.bsky.social', displayName: 'Creator'); - await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -756,12 +733,8 @@ void main() { ), ); - expect(find.text('FEED'), findsOneWidget); - expect(find.text('News Feed'), findsOneWidget); - expect(find.text('Fresh posts'), findsOneWidget); - expect(find.text('1.2K likes'), findsOneWidget); - expect(find.text('LIST'), findsOneWidget); - expect(find.text('Good Accounts'), findsOneWidget); + expectFeedEmbed(name: 'News Feed', description: 'Fresh posts', likeCount: '1.2K likes'); + expectListEmbed(name: 'Good Accounts'); expect(find.text('42 members'), findsOneWidget); expect(find.text('LABELER'), findsOneWidget); expect(find.text('Labeler Service'), findsOneWidget); @@ -850,16 +823,13 @@ void main() { await tester.tap(find.text('Labeler Service')); await tester.pumpAndSettle(); - expect(Uri.parse(pushedRoute!).path, '/settings/moderation/detail'); expect(Uri.parse(pushedRoute!).queryParameters['did'], 'did:plc:labeler'); }); testWidgets('renders unknown top-level embed fallback', (tester) async { final post = _makePostWithEmbed(const UPostViewEmbed.unknown(data: {r'$type': 'com.example.embed'})); - await tester.pumpWidget(buildSubject(post)); - expect(find.text('UNKNOWN'), findsOneWidget); }); } @@ -869,15 +839,13 @@ FeedViewPost _makePostWithRecordEmbed(UEmbedRecordViewRecord record, {String rke rkey: rkey, ); -FeedViewPost _makePostWithEmbed(UPostViewEmbed embed, {String rkey = 'record'}) => FeedViewPost( - post: PostView( - uri: AtUri.parse('at://did:plc:test/app.bsky.feed.post/$rkey'), - cid: 'cid-$rkey', - author: const ProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), - record: FeedPostRecord(text: 'Main post', createdAt: DateTime.utc(2026, 3, 16)).toJson(), - indexedAt: DateTime.utc(2026, 3, 16), - embed: embed, - ), +FeedViewPost _makePostWithEmbed(UPostViewEmbed embed, {String rkey = 'record'}) => testFeedViewPost( + uri: 'at://did:plc:test/app.bsky.feed.post/$rkey', + cid: 'cid-$rkey', + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), + record: FeedPostRecord(text: 'Main post', createdAt: DateTime.utc(2026, 3, 16)).toJson(), + indexedAt: DateTime.utc(2026, 3, 16), + embed: embed, ); Future _pumpRecordEmbedRouter( 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 cd40edb..91b4ec3 100644 --- a/test/features/feed/presentation/post_card_with_actions_test.dart +++ b/test/features/feed/presentation/post_card_with_actions_test.dart @@ -13,7 +13,8 @@ import 'package:lazurite/features/feed/presentation/widgets/post_card_with_actio import 'package:mocktail/mocktail.dart'; import 'package:poptart_core/poptart_core.dart'; -import '../../../helpers/feed_fixtures.dart'; +import '../../../helpers/fixtures/feed.dart'; +import '../../../helpers/connectivity_helpers.dart'; class MockPostActionRepository extends Mock implements PostActionRepository {} @@ -44,12 +45,7 @@ void main() { when(() => savedPostsCubit.state).thenReturn(const SavedPostsState()); whenListen(savedPostsCubit, const Stream.empty(), initialState: const SavedPostsState()); - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); - whenListen( - connectivityCubit, - const Stream.empty(), - initialState: const ConnectivityState.online(), - ); + stubConnectivityCubit(connectivityCubit, state: const ConnectivityState.online()); }); testWidgets('keeps optimistic like loading state across parent rebuilds', (tester) async { diff --git a/test/features/feed/presentation/post_quote_repost_sheet_test.dart b/test/features/feed/presentation/post_quote_repost_sheet_test.dart index 5b689c5..278a81b 100644 --- a/test/features/feed/presentation/post_quote_repost_sheet_test.dart +++ b/test/features/feed/presentation/post_quote_repost_sheet_test.dart @@ -10,6 +10,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/core/theme/app_theme.dart'; import 'package:lazurite/features/feed/data/post_action_repository.dart'; import 'package:lazurite/features/feed/presentation/widgets/post_quote_repost_sheet.dart'; +import '../../../helpers/assertion_helpers.dart'; class _FakeRepository implements PostActionRepository { _FakeRepository({this.quotes = const [], this.reposters = const []}); @@ -116,8 +117,7 @@ void main() { await tester.pump(); await tester.pump(); - expect(find.text('alice'), findsOneWidget); - expect(find.text('@alice.bsky.social'), findsOneWidget); + expectAccountRow(displayName: 'alice', handle: 'alice.bsky.social'); }); }); } diff --git a/test/features/feed/presentation/post_thread_edit_flow_test.dart b/test/features/feed/presentation/post_thread_edit_flow_test.dart index 857dd66..192148d 100644 --- a/test/features/feed/presentation/post_thread_edit_flow_test.dart +++ b/test/features/feed/presentation/post_thread_edit_flow_test.dart @@ -26,6 +26,7 @@ import 'package:lazurite/features/search/data/search_scope.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/connectivity_helpers.dart'; class MockPostThreadRepository extends Mock implements PostThreadRepository {} @@ -148,12 +149,7 @@ void main() { when(() => savedPostsCubit.state).thenReturn(savedState); whenListen(savedPostsCubit, const Stream.empty(), initialState: savedState); - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); - whenListen( - connectivityCubit, - const Stream.empty(), - initialState: const ConnectivityState.online(), - ); + stubConnectivityCubit(connectivityCubit, state: const ConnectivityState.online()); final settingsState = _settingsState(); when(() => settingsCubit.state).thenReturn(settingsState); diff --git a/test/features/feed/presentation/public_post_card_test.dart b/test/features/feed/presentation/public_post_card_test.dart index 6684c2d..68cebd8 100644 --- a/test/features/feed/presentation/public_post_card_test.dart +++ b/test/features/feed/presentation/public_post_card_test.dart @@ -7,7 +7,7 @@ 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 '../../../helpers/feed_fixtures.dart'; +import '../../../helpers/fixtures/feed.dart'; void main() { testWidgets('renders passive public counts and share action', (tester) async { diff --git a/test/features/feed/presentation/saved_posts_screen_test.dart b/test/features/feed/presentation/saved_posts_screen_test.dart index e5ab798..01daceb 100644 --- a/test/features/feed/presentation/saved_posts_screen_test.dart +++ b/test/features/feed/presentation/saved_posts_screen_test.dart @@ -16,6 +16,7 @@ import 'package:lazurite/features/feed/data/post_action_repository.dart'; import 'package:lazurite/features/feed/presentation/saved_posts_screen.dart'; import 'package:lazurite/features/feed/presentation/widgets/post_card_with_actions.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/connectivity_helpers.dart'; class MockAppDatabase extends Mock implements AppDatabase {} @@ -64,12 +65,7 @@ void main() { mockDatabase = MockAppDatabase(); mockPostActionRepository = MockPostActionRepository(); connectivityCubit = MockConnectivityCubit(); - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); - whenListen( - connectivityCubit, - const Stream.empty(), - initialState: const ConnectivityState.online(), - ); + stubConnectivityCubit(connectivityCubit, state: const ConnectivityState.online()); when(() => mockDatabase.watchSavedPostsWithType(testAccountDid)).thenAnswer((_) => Stream.value({})); when(() => mockDatabase.getSavedPosts(testAccountDid)).thenAnswer((_) => Future.value([])); diff --git a/test/features/feed/presentation/similar_posts_section_test.dart b/test/features/feed/presentation/similar_posts_section_test.dart index 54ab522..287dc52 100644 --- a/test/features/feed/presentation/similar_posts_section_test.dart +++ b/test/features/feed/presentation/similar_posts_section_test.dart @@ -9,7 +9,7 @@ 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'; +import '../../../helpers/fixtures/feed.dart'; class MockBluesky extends Mock implements Bluesky {} diff --git a/test/features/feed/presentation/trending_screen_test.dart b/test/features/feed/presentation/trending_screen_test.dart index 3a0e433..4c9e266 100644 --- a/test/features/feed/presentation/trending_screen_test.dart +++ b/test/features/feed/presentation/trending_screen_test.dart @@ -12,7 +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'; +import '../../../helpers/fixtures/settings.dart'; +import '../../../helpers/assertion_helpers.dart'; class MockFeedRepository extends Mock implements FeedRepository {} @@ -94,8 +95,7 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('Failed to load trending'), findsOneWidget); - expect(find.text('Retry'), findsOneWidget); + expectErrorState('Failed to load trending'); await tester.tap(find.text('Retry')); await tester.pumpAndSettle(); verify(() => feedRepository.getTrendingScreenData(limit: 10)).called(greaterThanOrEqualTo(2)); diff --git a/test/features/lists/cubit/add_to_list_cubit_test.dart b/test/features/lists/cubit/add_to_list_cubit_test.dart index a1b722a..d708aaa 100644 --- a/test/features/lists/cubit/add_to_list_cubit_test.dart +++ b/test/features/lists/cubit/add_to_list_cubit_test.dart @@ -1,13 +1,13 @@ import 'package:poptart_core/poptart_core.dart' show AtUri; import 'package:bloc_test/bloc_test.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; -import 'package:bluesky_poptart/app/bsky/graph/defs.dart'; import 'package:bluesky_poptart/app/bsky/graph/get_lists_with_membership.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/features/lists/cubit/add_to_list_cubit.dart'; import 'package:lazurite/features/lists/data/list_repository.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/fixtures/graph.dart'; + class MockListRepository extends Mock implements ListRepository {} void main() { @@ -16,18 +16,16 @@ void main() { final listUri = AtUri.parse('at://did:plc:creator/app.bsky.graph.list/list-1'); final listItemUri = AtUri.parse('at://did:plc:creator/app.bsky.graph.listitem/item-1'); - final listView = ListView( + final listView = testListView( uri: listUri, cid: 'cid-list', - creator: const ProfileView(did: 'did:plc:creator', handle: 'creator.bsky.social'), + creator: testProfileView(did: 'did:plc:creator', handle: 'creator.bsky.social'), name: 'My Feed', - purpose: const ListPurpose.knownValue(data: KnownListPurpose.appBskyGraphDefsCuratelist), - indexedAt: DateTime.utc(2026, 3, 21), ); - final listItem = ListItemView( + final listItem = testListItemView( uri: listItemUri, - subject: const ProfileView(did: 'did:plc:target', handle: 'target.bsky.social'), + subject: testProfileView(did: 'did:plc:target', handle: 'target.bsky.social'), ); final entryWithMember = ListWithMembership(list: listView, listItem: listItem); diff --git a/test/features/lists/data/list_repository_test.dart b/test/features/lists/data/list_repository_test.dart index 58ca4cc..9a07ba2 100644 --- a/test/features/lists/data/list_repository_test.dart +++ b/test/features/lists/data/list_repository_test.dart @@ -1,23 +1,25 @@ import 'dart:convert'; import 'dart:typed_data'; -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/lists/data/list_repository.dart'; import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/actor/search_actors_typeahead.dart'; -import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/get_list_feed.dart'; import 'package:bluesky_poptart/app/bsky/graph/defs.dart'; import 'package:bluesky_poptart/app/bsky/graph/get_list.dart'; import 'package:bluesky_poptart/app/bsky/graph/get_lists.dart'; import 'package:bluesky_poptart/app/bsky/graph/get_lists_with_membership.dart'; +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/lists/data/list_repository.dart'; import 'package:poptart_lex/com/atproto/repo/create_record.dart'; import 'package:poptart_lex/com/atproto/repo/delete_record.dart'; import 'package:poptart_lex/com/atproto/repo/put_record.dart'; import 'package:poptart_lex/com/atproto/repo/upload_blob.dart'; +import '../../../helpers/fixtures/feed.dart'; +import '../../../helpers/fixtures/graph.dart'; + void main() { late _FakeXrpcTransport transport; late ListRepository repository; @@ -44,18 +46,12 @@ void main() { uri: listItemUri, subject: const ProfileView(did: 'did:plc:member-1', handle: 'member1.bsky.social'), ); - final feedPost = FeedViewPost( - post: PostView( - uri: AtUri.parse('at://did:plc:member-1/app.bsky.feed.post/post-1'), - cid: 'cid-post', - author: const ProfileViewBasic(did: 'did:plc:member-1', handle: 'member1.bsky.social'), - record: { - r'$type': 'app.bsky.feed.post', - 'text': 'Hello from a list', - 'createdAt': DateTime.utc(2026, 3, 21).toIso8601String(), - }, - indexedAt: DateTime.utc(2026, 3, 21), - ), + final feedPost = testFeedViewPost( + uri: 'at://did:plc:member-1/app.bsky.feed.post/post-1', + cid: 'cid-post', + author: testProfileViewBasic(did: 'did:plc:member-1', handle: 'member1.bsky.social'), + record: testPostRecordJson(text: 'Hello from a list', createdAt: DateTime.utc(2026, 3, 21)), + indexedAt: DateTime.utc(2026, 3, 21), ); test('getLists requests curation and moderation lists by default', () async { @@ -72,9 +68,7 @@ void main() { test('getList returns the hydrated list and members', () async { transport.getListResult = GraphGetListOutput(list: listView, items: [listItem]); - final result = await repository.getList(listUri: listUri); - expect(result.list, listView); expect(result.items, [listItem]); expect(transport.lastGetListUri, listUri); @@ -82,9 +76,7 @@ void main() { test('getListFeed returns feed posts and cursor', () async { transport.getListFeedResult = FeedGetListFeedOutput(feed: [feedPost], cursor: 'cursor-2'); - final result = await repository.getListFeed(listUri: listUri); - expect(result.posts, [feedPost]); expect(result.cursor, 'cursor-2'); expect(transport.lastListUri, listUri); @@ -96,7 +88,6 @@ void main() { ); final result = await repository.getListsWithMembership(actor: 'did:plc:member-1'); - expect(result.lists.length, 1); expect(result.lists.single.listItem, listItem); expect(transport.lastGetListsWithMembershipPurposes?.map((purpose) => purpose.toJson()).toList(), [ @@ -111,7 +102,6 @@ void main() { ); final result = await repository.searchActorsTypeahead(query: 'member', limit: 5); - expect(result.single.did, 'did:plc:member-1'); expect(transport.lastQuery, 'member'); expect(transport.lastLimit, 5); @@ -122,7 +112,6 @@ void main() { final createdUri = await repository.addListItem(listUri: listUri, subjectDid: 'did:plc:member-1'); await repository.removeListItem(listItemUri: listItemUri); - expect(createdUri, listItemUri.toString()); expect(transport.lastCreateCollection, 'app.bsky.graph.listitem'); expect(transport.lastCreateRecord?['list'], listUri.toString()); @@ -134,17 +123,14 @@ void main() { test('mute and unmute list call graph endpoints', () async { await repository.muteList(listUri: listUri); await repository.unmuteList(listUri: listUri); - expect(transport.lastMutedList, listUri); expect(transport.lastUnmutedList, listUri); }); test('block and unblock list call listblock accessors', () async { transport.createdBlockUri = blockUri; - final createdUri = await repository.blockList(listUri: listUri); await repository.unblockList(blockUri: blockUri); - expect(createdUri, blockUri.toString()); expect(transport.lastCreateCollection, 'app.bsky.graph.listblock'); expect(transport.lastCreateRecord?['subject'], listUri.toString()); @@ -155,7 +141,6 @@ void main() { test('uploadListAvatar uploads bytes and returns Blob', () async { final bytes = [1, 2, 3, 4]; final blob = await repository.uploadListAvatar(bytes: bytes, mimeType: 'image/png'); - expect(blob, transport.uploadedBlob); expect(transport.lastUploadedBytes, Uint8List.fromList(bytes)); expect(transport.lastUploadHeaders, containsPair('Content-Type', 'image/png')); @@ -191,7 +176,6 @@ void main() { purpose: 'app.bsky.graph.defs#modlist', avatarBlob: avatarBlob, ); - expect(transport.lastCreateRecord?['avatar'], avatarBlob.toJson()); }); @@ -203,7 +187,6 @@ void main() { purpose: 'app.bsky.graph.defs#curatelist', description: 'Updated description', ); - expect(transport.lastPutRepo, 'did:plc:creator'); expect(transport.lastPutCollection, 'app.bsky.graph.list'); expect(transport.lastPutRkey, listUri.rkey); @@ -213,7 +196,6 @@ void main() { test('deleteList deletes the record by rkey', () async { await repository.deleteList(listUri: listUri, userDid: 'did:plc:creator'); - expect(transport.lastDeleteRepo, 'did:plc:creator'); expect(transport.lastDeleteCollection, 'app.bsky.graph.list'); expect(transport.lastDeleteRkey, listUri.rkey); @@ -228,16 +210,12 @@ const _session = Session( refreshJwt: 'refresh-token', ); -ListView _buildListView(AtUri uri) { - return ListView( - uri: uri, - cid: 'cid-${uri.rkey}', - creator: const ProfileView(did: 'did:plc:creator', handle: 'creator.bsky.social'), - name: 'Core List', - purpose: const ListPurpose.knownValue(data: KnownListPurpose.appBskyGraphDefsCuratelist), - indexedAt: DateTime.utc(2026, 3, 21), - ); -} +ListView _buildListView(AtUri uri) => testListView( + uri: uri, + cid: 'cid-${uri.rkey}', + creator: testProfileView(did: 'did:plc:creator', handle: 'creator.bsky.social'), + name: 'Core List', +); class _FakeXrpcTransport { GraphGetListsOutput? getListsResult; @@ -357,14 +335,12 @@ class _FakeXrpcTransport { } } - AtUri _createdUriFor(String? collection) { - return switch (collection) { - 'app.bsky.graph.list' => createdListUri, - 'app.bsky.graph.listitem' => createdListItemUri, - 'app.bsky.graph.listblock' => createdBlockUri, - _ => AtUri.parse('at://did:plc:creator/${collection ?? 'unknown'}/created'), - }; - } + AtUri _createdUriFor(String? collection) => switch (collection) { + 'app.bsky.graph.list' => createdListUri, + 'app.bsky.graph.listitem' => createdListItemUri, + 'app.bsky.graph.listblock' => createdBlockUri, + _ => AtUri.parse('at://did:plc:creator/${collection ?? 'unknown'}/created'), + }; List _getListPurposes(List values) { return values.map((value) => GraphGetListsPurposes.valueOf(value)!).toList(growable: false); @@ -386,12 +362,10 @@ class _FakeXrpcTransport { throw ArgumentError.value(body, 'body', 'Expected a JSON string body.'); } - http.Response _jsonResponse(Uri url, String method, Map body) { - return http.Response( - jsonEncode(body), - 200, - headers: {'content-type': 'application/json; charset=utf-8'}, - request: http.Request(method, url), - ); - } + http.Response _jsonResponse(Uri url, String method, Map body) => http.Response( + jsonEncode(body), + 200, + headers: {'content-type': 'application/json; charset=utf-8'}, + request: http.Request(method, url), + ); } diff --git a/test/features/lists/presentation/list_detail_screen_test.dart b/test/features/lists/presentation/list_detail_screen_test.dart index a599bf5..bcccebf 100644 --- a/test/features/lists/presentation/list_detail_screen_test.dart +++ b/test/features/lists/presentation/list_detail_screen_test.dart @@ -1,6 +1,5 @@ import 'package:poptart_core/poptart_core.dart' show AtUri; import 'package:bloc_test/bloc_test.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/graph/defs.dart' as bsky_graph; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -14,6 +13,9 @@ import 'package:lazurite/features/lists/presentation/list_detail_screen.dart'; import 'package:lazurite/features/moderation/data/moderation_service.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/fixtures/graph.dart'; +import '../../../helpers/assertion_helpers.dart'; + class MockAuthBloc extends MockBloc implements AuthBloc {} class MockListRepository extends Mock implements ListRepository {} @@ -37,20 +39,18 @@ void main() { final listUri = AtUri.parse('at://did:plc:creator/app.bsky.graph.list/list-1'); - final curationList = bsky_graph.ListView( + final curationList = testListView( uri: listUri, cid: 'cid-1', - creator: const ProfileView(did: 'did:plc:creator', handle: 'creator.bsky.social'), + creator: testProfileView(did: 'did:plc:creator', handle: 'creator.bsky.social'), name: 'Awesome Feed', description: 'A curated list of posts', - purpose: const bsky_graph.ListPurpose.knownValue(data: bsky_graph.KnownListPurpose.appBskyGraphDefsCuratelist), listItemCount: 5, - indexedAt: DateTime.utc(2026, 3, 21), ); - final member = bsky_graph.ListItemView( + final member = testListItemView( uri: AtUri.parse('at://did:plc:creator/app.bsky.graph.listitem/item-1'), - subject: const ProfileView(did: 'did:plc:member', handle: 'member.bsky.social', displayName: 'A Member'), + subject: testProfileView(did: 'did:plc:member', handle: 'member.bsky.social', displayName: 'A Member'), ); setUp(() { @@ -139,18 +139,16 @@ void main() { await tester.tap(find.text('MEMBERS')); await tester.pumpAndSettle(); - expect(find.text('A Member'), findsOneWidget); - expect(find.text('@member.bsky.social'), findsOneWidget); + expectListMember(displayName: 'A Member', handle: 'member.bsky.social'); }); testWidgets('shows feed unavailable message for moderation list in FEED tab', (tester) async { - final modList = bsky_graph.ListView( + final modList = testListView( uri: listUri, cid: 'cid-1', - creator: const ProfileView(did: 'did:plc:creator', handle: 'creator.bsky.social'), + creator: testProfileView(did: 'did:plc:creator', handle: 'creator.bsky.social'), name: 'Mod List', purpose: const bsky_graph.ListPurpose.knownValue(data: bsky_graph.KnownListPurpose.appBskyGraphDefsModlist), - indexedAt: DateTime.utc(2026, 3, 21), ); when( diff --git a/test/features/lists/presentation/list_members_screen_test.dart b/test/features/lists/presentation/list_members_screen_test.dart index f615546..5ea677d 100644 --- a/test/features/lists/presentation/list_members_screen_test.dart +++ b/test/features/lists/presentation/list_members_screen_test.dart @@ -1,7 +1,6 @@ import 'package:poptart_core/poptart_core.dart' show AtUri; import 'package:bloc_test/bloc_test.dart'; import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; -import 'package:bluesky_poptart/app/bsky/graph/defs.dart' as bsky_graph; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,6 +12,8 @@ import 'package:lazurite/features/lists/presentation/list_members_screen.dart'; 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/assertion_helpers.dart'; +import '../../../helpers/fixtures/graph.dart'; class MockAuthBloc extends MockBloc implements AuthBloc {} @@ -34,18 +35,16 @@ void main() { final listUri = AtUri.parse('at://did:plc:creator/app.bsky.graph.list/list-1'); - final curationList = bsky_graph.ListView( + final curationList = testListView( uri: listUri, cid: 'cid-1', - creator: const ProfileView(did: 'did:plc:creator', handle: 'creator.bsky.social'), + creator: testProfileView(did: 'did:plc:creator', handle: 'creator.bsky.social'), name: 'My List', - purpose: const bsky_graph.ListPurpose.knownValue(data: bsky_graph.KnownListPurpose.appBskyGraphDefsCuratelist), - indexedAt: DateTime.utc(2026, 3, 21), ); - final member = bsky_graph.ListItemView( + final member = testListItemView( uri: AtUri.parse('at://did:plc:creator/app.bsky.graph.listitem/item-1'), - subject: const ProfileView(did: 'did:plc:member', handle: 'member.bsky.social', displayName: 'Alice Member'), + subject: testProfileView(did: 'did:plc:member', handle: 'member.bsky.social', displayName: 'Alice Member'), ); setUp(() { @@ -108,8 +107,7 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('Alice Member'), findsOneWidget); - expect(find.text('@member.bsky.social'), findsOneWidget); + expectListMember(displayName: 'Alice Member', handle: 'member.bsky.social'); }); testWidgets('shows CURRENT MEMBERS heading', (tester) async { @@ -157,7 +155,6 @@ void main() { await tester.enterText(find.byType(TextField), 'newuser'); await tester.pumpAndSettle(); - expect(find.text('New User'), findsOneWidget); - expect(find.text('@newuser.bsky.social'), findsOneWidget); + expectListMember(displayName: 'New User', handle: 'newuser.bsky.social'); }); } diff --git a/test/features/lists/presentation/my_lists_screen_test.dart b/test/features/lists/presentation/my_lists_screen_test.dart index 3be3c1e..ea25dcb 100644 --- a/test/features/lists/presentation/my_lists_screen_test.dart +++ b/test/features/lists/presentation/my_lists_screen_test.dart @@ -1,6 +1,5 @@ import 'package:poptart_core/poptart_core.dart' show AtUri; import 'package:bloc_test/bloc_test.dart'; -import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/graph/defs.dart' as bsky_graph; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,6 +11,8 @@ import 'package:lazurite/features/lists/data/list_repository.dart'; import 'package:lazurite/features/lists/presentation/my_lists_screen.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/fixtures/graph.dart'; + class MockAuthBloc extends MockBloc implements AuthBloc {} class MockListRepository extends Mock implements ListRepository {} @@ -32,22 +33,19 @@ void main() { final listUri = AtUri.parse('at://did:plc:me/app.bsky.graph.list/list-1'); final modUri = AtUri.parse('at://did:plc:me/app.bsky.graph.list/mod-1'); - final curationList = bsky_graph.ListView( + final curationList = testListView( uri: listUri, cid: 'cid-1', - creator: const ProfileView(did: 'did:plc:me', handle: 'me.bsky.social'), + creator: testProfileView(did: 'did:plc:me', handle: 'me.bsky.social'), name: 'My Feed List', - purpose: const bsky_graph.ListPurpose.knownValue(data: bsky_graph.KnownListPurpose.appBskyGraphDefsCuratelist), - indexedAt: DateTime.utc(2026, 3, 21), ); - final moderationList = bsky_graph.ListView( + final moderationList = testListView( uri: modUri, cid: 'cid-2', - creator: const ProfileView(did: 'did:plc:me', handle: 'me.bsky.social'), + creator: testProfileView(did: 'did:plc:me', handle: 'me.bsky.social'), name: 'My Mod List', purpose: const bsky_graph.ListPurpose.knownValue(data: bsky_graph.KnownListPurpose.appBskyGraphDefsModlist), - indexedAt: DateTime.utc(2026, 3, 21), ); setUp(() { diff --git a/test/features/messages/presentation/convo_list_screen_test.dart b/test/features/messages/presentation/convo_list_screen_test.dart index d73fe5d..dc33e81 100644 --- a/test/features/messages/presentation/convo_list_screen_test.dart +++ b/test/features/messages/presentation/convo_list_screen_test.dart @@ -10,6 +10,9 @@ import 'package:lazurite/features/messages/data/convo_repository.dart'; import 'package:lazurite/features/messages/presentation/convo_list_screen.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/assertion_helpers.dart'; +import '../../../helpers/connectivity_helpers.dart'; + class MockConvoRepository extends Mock implements ConvoRepository {} class MockConnectivityCubit extends MockCubit implements ConnectivityCubit {} @@ -23,12 +26,7 @@ void main() { setUp(() { mockRepository = MockConvoRepository(); connectivityCubit = MockConnectivityCubit(); - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); - whenListen( - connectivityCubit, - const Stream.empty(), - initialState: const ConnectivityState.online(), - ); + stubConnectivityCubit(connectivityCubit); }); ProfileViewBasic makeProfile({String did = 'did:plc:other', String handle = 'other.bsky.social'}) => @@ -117,12 +115,7 @@ void main() { }); testWidgets('shows offline empty state when offline with no conversations', (tester) async { - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.offline()); - whenListen( - connectivityCubit, - const Stream.empty(), - initialState: const ConnectivityState.offline(), - ); + stubConnectivityCubit(connectivityCubit, state: const ConnectivityState.offline()); when( () => mockRepository.listConvos( cursor: any(named: 'cursor'), @@ -133,8 +126,7 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('No connection'), findsOneWidget); - expect(find.text('Reconnect to load messages.'), findsOneWidget); + expectOfflineState('No connection', message: 'Reconnect to load messages.'); }); testWidgets('shows error state on failure', (tester) async { @@ -148,8 +140,7 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('Failed to load messages'), findsOneWidget); - expect(find.text('Retry'), findsOneWidget); + expectErrorState('Failed to load messages'); }); testWidgets('retry button reloads conversations', (tester) async { diff --git a/test/features/messages/presentation/message_thread_screen_test.dart b/test/features/messages/presentation/message_thread_screen_test.dart index be2fcb6..7273b41 100644 --- a/test/features/messages/presentation/message_thread_screen_test.dart +++ b/test/features/messages/presentation/message_thread_screen_test.dart @@ -8,6 +8,7 @@ import 'package:lazurite/features/messages/data/convo_repository.dart'; import 'package:lazurite/features/messages/presentation/message_thread_screen.dart'; import 'package:lazurite/features/messages/presentation/widgets/message_bubble.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/assertion_helpers.dart'; class MockConvoRepository extends Mock implements ConvoRepository {} @@ -108,8 +109,7 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('Failed to load messages'), findsOneWidget); - expect(find.text('Retry'), findsOneWidget); + expectErrorState('Failed to load messages'); }); testWidgets('renders messages as bubbles', (tester) async { diff --git a/test/features/moderation/data/moderation_service_test.dart b/test/features/moderation/data/moderation_service_test.dart index ef3de8b..7a01598 100644 --- a/test/features/moderation/data/moderation_service_test.dart +++ b/test/features/moderation/data/moderation_service_test.dart @@ -1,10 +1,8 @@ import 'dart:convert'; -import 'package:poptart_lex/com/atproto/label/defs.dart'; import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/actor/get_preferences.dart'; import 'package:bluesky_poptart/app/bsky/actor/put_preferences.dart'; -import 'package:bluesky_poptart/app/bsky/feed/defs.dart' hide ViewerState; import 'package:bluesky_poptart/app/bsky/labeler/defs.dart'; import 'package:bluesky_poptart/app/bsky/labeler/get_services.dart'; import 'package:drift/native.dart'; @@ -14,7 +12,9 @@ import 'package:lazurite/core/database/app_database.dart'; import 'package:lazurite/core/network/poptart_client_adapter.dart'; import 'package:lazurite/features/moderation/data/moderation_service.dart'; import 'package:poptart_bluesky_moderation/poptart_bluesky_moderation.dart' as moderation; +import 'package:poptart_lex/com/atproto/label/defs.dart'; +import '../../../helpers/fixtures/feed.dart'; import '../../../helpers/test_bluesky_client.dart'; const _customLabelerDid = 'did:plc:custom-labeler'; @@ -75,16 +75,9 @@ void main() { await service.ensureInitialized(); - final labeledPost = PostView( - uri: AtUri.parse('at://did:plc:author/app.bsky.feed.post/abc'), + final labeledPost = testPostView( cid: 'cid-123', - author: const ProfileViewBasic(did: 'did:plc:author', handle: 'author.bsky.social'), - record: { - r'$type': 'app.bsky.feed.post', - 'text': 'sensitive', - 'createdAt': DateTime.utc(2026, 3, 15).toIso8601String(), - }, - indexedAt: DateTime.utc(2026, 3, 15), + record: testPostRecordJson(text: 'sensitive'), labels: [ Label( src: 'did:plc:ar7c4by46qjdydhdevvrndac', @@ -212,7 +205,6 @@ void main() { expect(fallbackService.currentPrefs, isNotNull); expect(fallbackService.currentHeaders['atproto-accept-labelers'], contains(_customLabelerDid)); - fallbackService.dispose(); }); @@ -239,7 +231,6 @@ void main() { contains(_customLabelerDid), ); expect(service.currentHeaders['atproto-accept-labelers'], contains(_customLabelerDid)); - service.dispose(); }); @@ -259,7 +250,6 @@ void main() { await service.subscribeToLabeler(_customLabelerDid); expect(actor.lastPutPreferencesHeaders?['atproto-proxy'], isNull); expect(actor.lastPutPreferencesHeaders?['atproto-accept-labelers'], contains(_customLabelerDid)); - service.dispose(); }); @@ -290,7 +280,6 @@ void main() { await service.subscribeToLabeler(_customLabelerDid); expect(actor.putPreferencesCallCount, 1); expect(actor.lastPutPreferencesHeaders?['atproto-proxy'], isNull); - service.dispose(); }); @@ -318,13 +307,11 @@ void main() { expect(contentPref.label, 'porn'); expect(contentPref.labelerDid, _customLabelerDid); expect(contentPref.visibility.toJson(), 'hide'); - service.dispose(); }); test('dispose is idempotent', () { final service = ModerationService(bluesky: _testBlueskyClient()); - expect(() => service.dispose(), returnsNormally); expect(() => service.dispose(), returnsNormally); }); @@ -413,21 +400,19 @@ Bluesky _testBlueskyClient({_FakeActorService? actor, _FakeLabelerService? label return testBluesky(getClient: transport.get, postClient: transport.post); } -ProfileViewDetailed _authGatedProfile() { - return ProfileViewDetailed( - did: _accountDid, - handle: 'test.bsky.social', - indexedAt: DateTime.utc(2026, 5, 16), - labels: [ - Label( - src: 'did:plc:ar7c4by46qjdydhdevvrndac', - uri: 'at://$_accountDid/app.bsky.actor.profile/self', - val: '!no-unauthenticated', - cts: DateTime.utc(2026, 5, 16), - ), - ], - ); -} +ProfileViewDetailed _authGatedProfile() => ProfileViewDetailed( + did: _accountDid, + handle: 'test.bsky.social', + indexedAt: DateTime.utc(2026, 5, 16), + labels: [ + Label( + src: 'did:plc:ar7c4by46qjdydhdevvrndac', + uri: 'at://$_accountDid/app.bsky.actor.profile/self', + val: '!no-unauthenticated', + cts: DateTime.utc(2026, 5, 16), + ), + ], +); class _FakeModerationTransport { _FakeModerationTransport({_FakeActorService? actor, _FakeLabelerService? labeler}) @@ -573,32 +558,30 @@ LabelerViewDetailed _buildLabeler({ required String description, required String definitionIdentifier, required String definitionName, -}) { - return LabelerViewDetailed( - uri: AtUri.parse('at://$did/app.bsky.labeler.service/self'), - cid: 'cid-$did', - creator: ProfileView( - did: did, - handle: handle, - displayName: displayName, - description: description, - avatar: 'https://example.com/$handle.png', - ), - policies: LabelerPolicies( - labelValues: [LabelValue.unknown(data: definitionIdentifier)], - labelValueDefinitions: [ - LabelValueDefinition( - identifier: definitionIdentifier, - severity: const LabelValueDefinitionSeverity.knownValue(data: KnownLabelValueDefinitionSeverity.alert), - blurs: const LabelValueDefinitionBlurs.knownValue(data: KnownLabelValueDefinitionBlurs.content), - defaultSetting: const LabelValueDefinitionDefaultSetting.knownValue( - data: KnownLabelValueDefinitionDefaultSetting.warn, - ), - adultOnly: false, - locales: [LabelValueDefinitionStrings(lang: 'en', name: definitionName, description: 'Example description')], +}) => LabelerViewDetailed( + uri: AtUri.parse('at://$did/app.bsky.labeler.service/self'), + cid: 'cid-$did', + creator: ProfileView( + did: did, + handle: handle, + displayName: displayName, + description: description, + avatar: 'https://example.com/$handle.png', + ), + policies: LabelerPolicies( + labelValues: [LabelValue.unknown(data: definitionIdentifier)], + labelValueDefinitions: [ + LabelValueDefinition( + identifier: definitionIdentifier, + severity: const LabelValueDefinitionSeverity.knownValue(data: KnownLabelValueDefinitionSeverity.alert), + blurs: const LabelValueDefinitionBlurs.knownValue(data: KnownLabelValueDefinitionBlurs.content), + defaultSetting: const LabelValueDefinitionDefaultSetting.knownValue( + data: KnownLabelValueDefinitionDefaultSetting.warn, ), - ], - ), - indexedAt: DateTime.utc(2026, 4, 30), - ); -} + adultOnly: false, + locales: [LabelValueDefinitionStrings(lang: 'en', name: definitionName, description: 'Example description')], + ), + ], + ), + indexedAt: DateTime.utc(2026, 4, 30), +); diff --git a/test/features/notifications/bloc/notification_bloc_test.dart b/test/features/notifications/bloc/notification_bloc_test.dart index e837ee2..1d6d586 100644 --- a/test/features/notifications/bloc/notification_bloc_test.dart +++ b/test/features/notifications/bloc/notification_bloc_test.dart @@ -6,7 +6,7 @@ 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'; +import '../../../helpers/fixtures/notification.dart'; class MockNotificationRepository extends Mock implements NotificationRepository {} diff --git a/test/features/notifications/data/notification_repository_test.dart b/test/features/notifications/data/notification_repository_test.dart index 24ae2b6..8007b7c 100644 --- a/test/features/notifications/data/notification_repository_test.dart +++ b/test/features/notifications/data/notification_repository_test.dart @@ -1,13 +1,13 @@ +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:http/http.dart' as http; import 'package:lazurite/core/network/poptart_client_adapter.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'; +import '../../../helpers/fixtures/notification.dart'; +import '../../../helpers/fixtures/auth.dart'; class MockNotificationRepository extends Mock implements NotificationRepository {} diff --git a/test/features/notifications/domain/notification_local_mappers_test.dart b/test/features/notifications/domain/notification_local_mappers_test.dart index 82647fe..43039f8 100644 --- a/test/features/notifications/domain/notification_local_mappers_test.dart +++ b/test/features/notifications/domain/notification_local_mappers_test.dart @@ -1,10 +1,10 @@ -import 'package:poptart_core/poptart_core.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 'package:poptart_core/poptart_core.dart'; -import '../../../helpers/notification_fixtures.dart'; +import '../../../helpers/fixtures/notification.dart'; void main() { group('NotificationLocalMapper', () { diff --git a/test/features/notifications/domain/push_registration_service_test.dart b/test/features/notifications/domain/push_registration_service_test.dart index 3bfdc57..d1fabff 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 '../../../helpers/test_utils.dart'; +import '../../../helpers/fixtures/network.dart'; import 'package:poptart_core/poptart_core.dart' show HttpMethod; import 'package:mocktail/mocktail.dart'; diff --git a/test/features/notifications/presentation/notifications_screen_test.dart b/test/features/notifications/presentation/notifications_screen_test.dart index 17e12ff..e2b579c 100644 --- a/test/features/notifications/presentation/notifications_screen_test.dart +++ b/test/features/notifications/presentation/notifications_screen_test.dart @@ -1,7 +1,6 @@ -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; -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'; @@ -13,6 +12,11 @@ import 'package:lazurite/features/notifications/presentation/notifications_scree import 'package:lazurite/features/notifications/presentation/widgets/grouped_notification_list_item.dart'; import 'package:lazurite/features/notifications/presentation/widgets/notification_list_item.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:poptart_core/poptart_core.dart'; + +import '../../../helpers/fixtures/notification.dart'; +import '../../../helpers/connectivity_helpers.dart'; +import '../../../helpers/assertion_helpers.dart'; class MockNotificationRepository extends Mock implements NotificationRepository {} @@ -26,12 +30,7 @@ void main() { setUp(() { mockNotificationRepository = MockNotificationRepository(); connectivityCubit = MockConnectivityCubit(); - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); - whenListen( - connectivityCubit, - const Stream.empty(), - initialState: const ConnectivityState.online(), - ); + stubConnectivityCubit(connectivityCubit, state: const ConnectivityState.online()); when( () => mockNotificationRepository.listNotifications( cursor: any(named: 'cursor'), @@ -42,22 +41,20 @@ void main() { when(() => mockNotificationRepository.updateSeen()).thenAnswer((_) async {}); }); - Widget buildSubject() { - return MaterialApp( - home: MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => NotificationBloc(notificationRepository: mockNotificationRepository), - ), - BlocProvider( - create: (_) => UnreadCountCubit(notificationRepository: mockNotificationRepository), - ), - BlocProvider.value(value: connectivityCubit), - ], - child: const NotificationsScreen(), - ), - ); - } + Widget buildSubject() => MaterialApp( + home: MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => NotificationBloc(notificationRepository: mockNotificationRepository), + ), + BlocProvider( + create: (_) => UnreadCountCubit(notificationRepository: mockNotificationRepository), + ), + BlocProvider.value(value: connectivityCubit), + ], + child: const NotificationsScreen(), + ), + ); testWidgets('displays loading indicator initially', (tester) async { await tester.pumpWidget(buildSubject()); @@ -66,15 +63,7 @@ void main() { }); testWidgets('displays notifications when loaded', (tester) async { - 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.like), - record: {'text': 'Test post'}, - isRead: false, - indexedAt: DateTime.now(), - ); + final notification = testNotification(record: const {'text': 'Test post'}, indexedAt: DateTime.now()); when( () => mockNotificationRepository.listNotifications( @@ -104,12 +93,7 @@ void main() { }); testWidgets('displays offline empty state when offline with no notifications', (tester) async { - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.offline()); - whenListen( - connectivityCubit, - const Stream.empty(), - initialState: const ConnectivityState.offline(), - ); + stubConnectivityCubit(connectivityCubit, state: const ConnectivityState.offline()); await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); @@ -129,8 +113,7 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('Failed to load notifications'), findsOneWidget); - expect(find.text('Retry'), findsOneWidget); + expectErrorState('Failed to load notifications'); }); testWidgets('tapping retry reloads notifications', (tester) async { @@ -170,22 +153,20 @@ void main() { }); testWidgets('groups notifications by day', (tester) async { - final todayNotification = bsky.Notification( - uri: AtUri.parse('at://did:plc:author/app.bsky.feed.post/1'), + final todayNotification = testNotification( + uri: 'at://did:plc:author/app.bsky.feed.post/1', cid: 'cid-1', - author: const ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.like), - record: {'text': 'Post 1'}, + record: const {'text': 'Post 1'}, isRead: true, indexedAt: DateTime.now(), ); - final yesterdayNotification = bsky.Notification( - uri: AtUri.parse('at://did:plc:author/app.bsky.feed.post/2'), + final yesterdayNotification = testNotification( + uri: 'at://did:plc:author/app.bsky.feed.post/2', cid: 'cid-2', 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.now().subtract(const Duration(days: 1)), ); @@ -208,23 +189,21 @@ void main() { testWidgets('groups repeated likes on the same post into one row', (tester) async { final postUri = AtUri.parse('at://did:plc:owner/app.bsky.feed.post/post1'); - final firstLike = bsky.Notification( - uri: AtUri.parse('at://did:plc:alice/app.bsky.feed.like/1'), + final firstLike = testNotification( + uri: 'at://did:plc:alice/app.bsky.feed.like/1', cid: 'cid-1', author: const ProfileView(did: 'did:plc:alice', handle: 'alice.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.like), reasonSubject: postUri, - record: {'text': 'Shared post'}, + record: const {'text': 'Shared post'}, isRead: true, indexedAt: DateTime.now(), ); - final secondLike = bsky.Notification( - uri: AtUri.parse('at://did:plc:bob/app.bsky.feed.like/2'), + final secondLike = testNotification( + uri: 'at://did:plc:bob/app.bsky.feed.like/2', cid: 'cid-2', author: const ProfileView(did: 'did:plc:bob', handle: 'bob.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.like), reasonSubject: postUri, - record: {'text': 'Shared post'}, + record: const {'text': 'Shared post'}, isRead: true, indexedAt: DateTime.now().subtract(const Duration(minutes: 1)), ); @@ -238,18 +217,15 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.byType(GroupedNotificationListItem), findsOneWidget); expect(find.byType(NotificationListItem), findsNothing); }); testWidgets('displays day header for older notifications', (tester) async { - final oldNotification = bsky.Notification( - uri: AtUri.parse('at://did:plc:author/app.bsky.feed.post/old'), + final oldNotification = testNotification( + uri: 'at://did:plc:author/app.bsky.feed.post/old', cid: 'cid-old', - author: const ProfileView(did: 'did:plc:author', handle: 'author.bsky.social'), - reason: const bsky.NotificationReason.knownValue(data: bsky.KnownNotificationReason.like), - record: {'text': 'Old post'}, + record: const {'text': 'Old post'}, isRead: true, indexedAt: DateTime(2026, 1, 15), ); @@ -263,7 +239,6 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('January 15'), findsOneWidget); }); }); diff --git a/test/features/profile/data/profile_repository_test.dart b/test/features/profile/data/profile_repository_test.dart index e07536b..35d826c 100644 --- a/test/features/profile/data/profile_repository_test.dart +++ b/test/features/profile/data/profile_repository_test.dart @@ -13,7 +13,8 @@ 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 '../../../helpers/test_utils.dart'; +import '../../../helpers/fixtures/auth.dart'; +import '../../../helpers/fixtures/network.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/profile/presentation/profile_connections_screen_test.dart b/test/features/profile/presentation/profile_connections_screen_test.dart index ed43c23..560461b 100644 --- a/test/features/profile/presentation/profile_connections_screen_test.dart +++ b/test/features/profile/presentation/profile_connections_screen_test.dart @@ -10,6 +10,7 @@ import 'package:lazurite/features/profile/data/profile_repository.dart'; import 'package:lazurite/features/profile/presentation/profile_connections_screen.dart'; import 'package:mocktail/mocktail.dart'; import 'package:poptart_core/poptart_core.dart'; +import '../../../helpers/connectivity_helpers.dart'; class MockProfileRepository extends Mock implements ProfileRepository {} @@ -44,12 +45,7 @@ void main() { profileActionRepository = MockProfileActionRepository(); connectivityCubit = MockConnectivityCubit(); - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); - whenListen( - connectivityCubit, - const Stream.empty(), - initialState: const ConnectivityState.online(), - ); + stubConnectivityCubit(connectivityCubit, state: const ConnectivityState.online()); }); Widget buildSubject({ProfileConnectionsTab initialTab = ProfileConnectionsTab.following}) { diff --git a/test/features/profile/presentation/profile_context_screen_test.dart b/test/features/profile/presentation/profile_context_screen_test.dart index 1140559..e395a76 100644 --- a/test/features/profile/presentation/profile_context_screen_test.dart +++ b/test/features/profile/presentation/profile_context_screen_test.dart @@ -10,6 +10,7 @@ import 'package:lazurite/features/profile/cubit/profile_context_cubit.dart'; import 'package:lazurite/features/profile/data/profile_context_repository.dart'; import 'package:lazurite/features/profile/presentation/profile_context_screen.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/assertion_helpers.dart'; class MockProfileContextCubit extends MockCubit implements ProfileContextCubit {} @@ -244,8 +245,7 @@ void main() { ); await tester.pumpWidget(buildSubject(state: state)); - expect(find.text('Something went wrong'), findsOneWidget); - expect(find.text('Retry'), findsOneWidget); + expectErrorState('Something went wrong'); }); testWidgets('retry calls loadBlockedBy on cubit', (tester) async { @@ -334,8 +334,7 @@ void main() { await tester.tap(find.text('Blocking')); await tester.pumpAndSettle(); - expect(find.text('Block error'), findsOneWidget); - expect(find.text('Retry'), findsOneWidget); + expectErrorState('Block error'); }); testWidgets('retry calls loadBlocking on cubit', (tester) async { @@ -456,8 +455,7 @@ void main() { await tester.tap(find.text('Lists')); await tester.pumpAndSettle(); - expect(find.text('List load error'), findsOneWidget); - expect(find.text('Retry'), findsOneWidget); + expectErrorState('List load error'); }); testWidgets('retry calls loadListsOn on cubit', (tester) async { diff --git a/test/features/profile/presentation/profile_edit_screen_test.dart b/test/features/profile/presentation/profile_edit_screen_test.dart index 3056d80..b128e79 100644 --- a/test/features/profile/presentation/profile_edit_screen_test.dart +++ b/test/features/profile/presentation/profile_edit_screen_test.dart @@ -6,7 +6,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.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/profile/bloc/profile_bloc.dart'; import 'package:lazurite/features/profile/data/profile_repository.dart'; import 'package:lazurite/features/profile/presentation/profile_edit_screen.dart'; @@ -14,6 +13,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/fixtures/auth.dart'; + class MockAuthBloc extends MockBloc implements AuthBloc {} class MockProfileBloc extends MockBloc implements ProfileBloc {} @@ -30,7 +31,7 @@ void main() { late MockProfileRepository profileRepository; late MockSettingsCubit settingsCubit; - const tokens = AuthTokens( + final tokens = testAuthTokens( accessToken: 'access', refreshToken: 'refresh', did: 'did:plc:me', @@ -64,7 +65,7 @@ void main() { profileRepository = MockProfileRepository(); settingsCubit = MockSettingsCubit(); - when(() => authBloc.state).thenReturn(const AuthState.authenticated(tokens)); + when(() => authBloc.state).thenReturn(AuthState.authenticated(tokens)); when(() => profileBloc.state).thenReturn(const ProfileState.loaded(profile: profile)); when(() => settingsCubit.state).thenReturn(settingsState); when( @@ -74,7 +75,7 @@ void main() { ), ).thenAnswer((_) async {}); - whenListen(authBloc, const Stream.empty(), initialState: const AuthState.authenticated(tokens)); + whenListen(authBloc, const Stream.empty(), initialState: AuthState.authenticated(tokens)); whenListen( profileBloc, const Stream.empty(), @@ -97,18 +98,16 @@ void main() { ); } - GoRouter buildRouter() { - return GoRouter( - initialLocation: '/profile/me/edit', - routes: [ - GoRoute( - path: '/profile/me', - builder: (_, _) => const Scaffold(body: Text('profile-me')), - ), - GoRoute(path: '/profile/me/edit', builder: (_, _) => const ProfileEditScreen()), - ], - ); - } + GoRouter buildRouter() => GoRouter( + initialLocation: '/profile/me/edit', + routes: [ + GoRoute( + path: '/profile/me', + builder: (_, _) => const Scaffold(body: Text('profile-me')), + ), + GoRoute(path: '/profile/me/edit', builder: (_, _) => const ProfileEditScreen()), + ], + ); testWidgets('hydrates the profile edit form from the loaded profile', (tester) async { final router = buildRouter(); diff --git a/test/features/profile/presentation/profile_screen_test.dart b/test/features/profile/presentation/profile_screen_test.dart index 745efec..110cad6 100644 --- a/test/features/profile/presentation/profile_screen_test.dart +++ b/test/features/profile/presentation/profile_screen_test.dart @@ -11,7 +11,6 @@ import 'package:go_router/go_router.dart'; import 'package:lazurite/core/theme/app_theme.dart'; import 'package:lazurite/core/theme/feed_layout.dart'; import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; -import 'package:lazurite/features/auth/data/models/auth_models.dart'; import 'package:lazurite/features/compose/presentation/compose_route_args.dart'; import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; import 'package:lazurite/features/feed/bloc/feed_bloc.dart'; @@ -27,7 +26,11 @@ import 'package:lazurite/features/settings/bloc/settings_cubit.dart'; import 'package:lazurite/features/settings/bloc/settings_state.dart'; import 'package:lazurite/shared/presentation/widgets/app_screen_entrance.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:poptart_core/poptart_core.dart'; + +import '../../../helpers/fixtures/feed.dart'; +import '../../../helpers/fixtures/auth.dart'; +import '../../../helpers/connectivity_helpers.dart'; +import '../../../helpers/assertion_helpers.dart'; class MockAuthBloc extends MockBloc implements AuthBloc {} @@ -59,7 +62,7 @@ void main() { late MockConnectivityCubit connectivityCubit; late MockProfileRepository profileRepository; - const tokens = AuthTokens( + final tokens = testAuthTokens( accessToken: 'access', refreshToken: 'refresh', did: 'did:plc:me', @@ -101,15 +104,15 @@ void main() { connectivityCubit = MockConnectivityCubit(); profileRepository = MockProfileRepository(); - when(() => authBloc.state).thenReturn(const AuthState.authenticated(tokens)); + when(() => authBloc.state).thenReturn(AuthState.authenticated(tokens)); when(() => profileBloc.state).thenReturn(ProfileState.loaded(profile: profile)); when(() => feedBloc.state).thenReturn( const FeedState.loaded(actor: 'did:plc:me', posts: [], filter: FeedFilter.postsNoReplies, hasMore: false), ); when(() => settingsCubit.state).thenReturn(defaultSettingsState()); - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); + stubConnectivityCubit(connectivityCubit); - whenListen(authBloc, const Stream.empty(), initialState: const AuthState.authenticated(tokens)); + whenListen(authBloc, const Stream.empty(), initialState: AuthState.authenticated(tokens)); whenListen(profileBloc, const Stream.empty(), initialState: ProfileState.loaded(profile: profile)); whenListen( feedBloc, @@ -129,20 +132,18 @@ void main() { ); }); - Widget buildSubject({String? publicProviderKey, String? actor}) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: authBloc), - BlocProvider.value(value: profileBloc), - BlocProvider.value(value: feedBloc), - BlocProvider.value(value: settingsCubit), - BlocProvider.value(value: connectivityCubit), - ], - child: MaterialApp( - home: ProfileScreen(actor: actor, publicProviderKey: publicProviderKey), - ), - ); - } + Widget buildSubject({String? publicProviderKey, String? actor}) => MultiBlocProvider( + providers: [ + BlocProvider.value(value: authBloc), + BlocProvider.value(value: profileBloc), + BlocProvider.value(value: feedBloc), + BlocProvider.value(value: settingsCubit), + BlocProvider.value(value: connectivityCubit), + ], + child: MaterialApp( + home: ProfileScreen(actor: actor, publicProviderKey: publicProviderKey), + ), + ); /// Sets the test viewport to a tall size so that the full profile header /// (cover + summary + tab bar) fits within the viewport. @@ -162,8 +163,7 @@ void main() { () => feedBloc.add(const FeedLoadRequested(actor: 'did:plc:me', filter: FeedFilter.postsNoReplies)), ).called(1); - expect(find.text('RIVER TAM'), findsOneWidget); - expect(find.text('@me.bsky.social'), findsOneWidget); + expectAccountRow(displayName: 'RIVER TAM', handle: 'me.bsky.social'); expect( find.byWidgetPredicate( (widget) => widget is RichText && widget.text.toPlainText().contains('Signal and signal boost.'), @@ -383,9 +383,7 @@ void main() { await tester.tap(find.byKey(const ValueKey('profile_known_followers_link'))); await tester.pump(); await tester.pump(const Duration(seconds: 1)); - expect(find.text('known-followers other.bsky.social'), findsOneWidget); - router.dispose(); }); @@ -403,7 +401,6 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('You know 2 followers'), findsNothing); expect(find.byKey(const ValueKey('profile_known_followers_link')), findsNothing); }); @@ -439,7 +436,6 @@ void main() { ); await tester.pumpWidget(widget); - expect(find.byKey(const Key('profile_edit_header_button')), findsNothing); expect(find.text('Edit Profile'), findsNothing); expect(find.text('Bookmarks'), findsNothing); @@ -819,18 +815,13 @@ void main() { }); group('Feed layout switching', () { - FeedViewPost makePost(String id) { - final record = FeedPostRecord(text: 'Post $id', createdAt: DateTime.utc(2026, 3, 1)); - return FeedViewPost( - post: PostView( - uri: AtUri('at://did:plc:me/app.bsky.feed.post/$id'), - cid: 'cid-$id', - author: const ProfileViewBasic(did: 'did:plc:me', handle: 'me.bsky.social', displayName: 'River Tam'), - record: record.toJson(), - indexedAt: DateTime.utc(2026, 3, 1), - ), - ); - } + FeedViewPost makePost(String id) => testFeedViewPost( + uri: 'at://did:plc:me/app.bsky.feed.post/$id', + cid: 'cid-$id', + author: testProfileViewBasic(did: 'did:plc:me', handle: 'me.bsky.social', displayName: 'River Tam'), + record: FeedPostRecord(text: 'Post $id', createdAt: DateTime.utc(2026, 3, 1)).toJson(), + indexedAt: DateTime.utc(2026, 3, 1), + ); final posts = List.generate(3, (i) => makePost('$i')); @@ -839,30 +830,28 @@ void main() { FeedViewPost makeReplyWithParent(String id) { final parentRecord = FeedPostRecord(text: 'Parent $id', createdAt: DateTime.utc(2026, 3, 1)); - final parentPost = PostView( - uri: AtUri('at://did:plc:parent/app.bsky.feed.post/parent-$id'), + final parentPost = testPostView( + uri: 'at://did:plc:parent/app.bsky.feed.post/parent-$id', cid: 'cid-parent-$id', - author: const ProfileViewBasic(did: 'did:plc:parent', handle: 'parent.bsky.social', displayName: 'Parent User'), + author: testProfileViewBasic(did: 'did:plc:parent', handle: 'parent.bsky.social', displayName: 'Parent User'), record: parentRecord.toJson(), indexedAt: DateTime.utc(2026, 3, 1), ); final replyRecord = FeedPostRecord(text: 'Reply $id', createdAt: DateTime.utc(2026, 3, 1, 0, 5), reply: null); - return FeedViewPost( - post: PostView( - uri: AtUri('at://did:plc:me/app.bsky.feed.post/reply-$id'), - cid: 'cid-reply-$id', - author: const ProfileViewBasic(did: 'did:plc:me', handle: 'me.bsky.social', displayName: 'River Tam'), - record: { - ...replyRecord.toJson(), - 'reply': { - r'$type': 'app.bsky.feed.post#replyRef', - 'root': {'uri': parentPost.uri.toString(), 'cid': parentPost.cid}, - 'parent': {'uri': parentPost.uri.toString(), 'cid': parentPost.cid}, - }, + return testFeedViewPost( + uri: 'at://did:plc:me/app.bsky.feed.post/reply-$id', + cid: 'cid-reply-$id', + author: testProfileViewBasic(did: 'did:plc:me', handle: 'me.bsky.social', displayName: 'River Tam'), + record: { + ...replyRecord.toJson(), + 'reply': { + r'$type': 'app.bsky.feed.post#replyRef', + 'root': {'uri': parentPost.uri.toString(), 'cid': parentPost.cid}, + 'parent': {'uri': parentPost.uri.toString(), 'cid': parentPost.cid}, }, - indexedAt: DateTime.utc(2026, 3, 1, 0, 5), - ), + }, + indexedAt: DateTime.utc(2026, 3, 1, 0, 5), reply: ReplyRef( root: UReplyRefRoot.postView(data: parentPost), parent: UReplyRefParent.postView(data: parentPost), @@ -1216,9 +1205,7 @@ void main() { await tester.tap(find.text('Clean Follows')); await tester.pumpAndSettle(); - expect(find.text('clean'), findsOneWidget); - router.dispose(); }); }); @@ -1281,26 +1268,19 @@ void main() { await tester.tap(find.byKey(const Key('profile_search_posts_button'))); await tester.pumpAndSettle(); - expect(find.text('search:other.bsky.social'), findsOneWidget); - router.dispose(); }); }); } -FeedViewPost _publicProfilePost() { - final record = FeedPostRecord(text: 'Public profile post', createdAt: DateTime.utc(2026, 5, 18)); - return FeedViewPost( - post: PostView( - uri: const AtUri('at://did:plc:public/app.bsky.feed.post/public'), - cid: 'cid-public', - author: const ProfileViewBasic(did: 'did:plc:public', handle: 'public.bsky.social', displayName: 'Public User'), - record: record.toJson(), - indexedAt: DateTime.utc(2026, 5, 18), - replyCount: 1, - repostCount: 2, - likeCount: 3, - ), - ); -} +FeedViewPost _publicProfilePost() => testFeedViewPost( + uri: 'at://did:plc:public/app.bsky.feed.post/public', + cid: 'cid-public', + author: testProfileViewBasic(did: 'did:plc:public', handle: 'public.bsky.social', displayName: 'Public User'), + record: FeedPostRecord(text: 'Public profile post', createdAt: DateTime.utc(2026, 5, 18)).toJson(), + indexedAt: DateTime.utc(2026, 5, 18), + replyCount: 1, + repostCount: 2, + likeCount: 3, +); diff --git a/test/features/profile/presentation/widgets/suggested_follows_sheet_test.dart b/test/features/profile/presentation/widgets/suggested_follows_sheet_test.dart index 96cb8c9..b41a3fd 100644 --- a/test/features/profile/presentation/widgets/suggested_follows_sheet_test.dart +++ b/test/features/profile/presentation/widgets/suggested_follows_sheet_test.dart @@ -1,14 +1,16 @@ import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; import 'package:lazurite/features/profile/cubit/suggested_follows_cubit.dart'; import 'package:lazurite/features/profile/data/profile_action_repository.dart'; import 'package:lazurite/features/profile/presentation/widgets/suggested_follows_sheet.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../../helpers/assertion_helpers.dart'; + class MockSuggestedFollowsCubit extends MockCubit implements SuggestedFollowsCubit {} class MockProfileActionRepository extends Mock implements ProfileActionRepository {} @@ -58,7 +60,6 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pump(); - expect(find.byType(CircularProgressIndicator), findsOneWidget); }); @@ -72,7 +73,6 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pump(); - expect(find.text('Something went wrong'), findsOneWidget); }); @@ -86,7 +86,6 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pump(); - expect(find.text('No suggestions found'), findsOneWidget); }); @@ -105,10 +104,8 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pump(); - expect(find.text('Bob Builder'), findsOneWidget); - expect(find.text('@did:plc:bob.bsky.social'), findsOneWidget); - expect(find.text('Carol Danvers'), findsOneWidget); - expect(find.text('@did:plc:carol.bsky.social'), findsOneWidget); + expectAccountRow(displayName: 'Bob Builder', handle: 'did:plc:bob.bsky.social'); + expectAccountRow(displayName: 'Carol Danvers', handle: 'did:plc:carol.bsky.social'); }); testWidgets('shows Follow button for unfollowed profiles', (tester) async { @@ -123,7 +120,6 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pump(); - expect(find.text('Follow'), findsOneWidget); }); @@ -175,7 +171,6 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pump(); - expect(find.text('Suggested Follows'), findsOneWidget); }); } diff --git a/test/features/public/data/public_content_repository_test.dart b/test/features/public/data/public_content_repository_test.dart index bdc06e2..a6a7890 100644 --- a/test/features/public/data/public_content_repository_test.dart +++ b/test/features/public/data/public_content_repository_test.dart @@ -8,7 +8,7 @@ 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'; +import '../../../helpers/fixtures/feed.dart'; class MockFeedRepository extends Mock implements FeedRepository {} diff --git a/test/features/public/presentation/public_home_screen_test.dart b/test/features/public/presentation/public_home_screen_test.dart index 3b90f4c..9b8aacb 100644 --- a/test/features/public/presentation/public_home_screen_test.dart +++ b/test/features/public/presentation/public_home_screen_test.dart @@ -14,7 +14,7 @@ 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'; +import '../../../helpers/fixtures/feed.dart'; class MockPublicContentRepository extends Mock implements PublicContentRepository {} diff --git a/test/features/search/bloc/search_bloc_test.dart b/test/features/search/bloc/search_bloc_test.dart index b2b7d3d..9afe4ef 100644 --- a/test/features/search/bloc/search_bloc_test.dart +++ b/test/features/search/bloc/search_bloc_test.dart @@ -1,8 +1,6 @@ -import 'package:poptart_core/poptart_core.dart' show AtUri; 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/graph/defs.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/core/database/app_database.dart'; import 'package:lazurite/features/search/bloc/search_bloc.dart'; @@ -11,8 +9,11 @@ import 'package:lazurite/features/search/data/search_repository.dart'; import 'package:lazurite/features/typeahead/data/typeahead_repository.dart'; import 'package:lazurite/features/typeahead/data/typeahead_result.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:poptart_core/poptart_core.dart' show AtUri; -import '../../../helpers/feed_fixtures.dart'; +import '../../../helpers/fixtures/feed.dart'; +import '../../../helpers/fixtures/graph.dart'; +import '../../../helpers/search_helpers.dart'; class MockSearchRepository extends Mock implements SearchRepository {} @@ -50,8 +51,7 @@ void main() { indexedAt: DateTime.utc(2026, 1, 1), ); - final sampleStarterPack = StarterPackViewBasic( - uri: AtUri.parse('at://did:plc:creator/app.bsky.graph.starterpack/pack-1'), + final sampleStarterPack = testStarterPackViewBasic( cid: 'cid-pack-1', record: const { r'$type': 'app.bsky.graph.starterpack', @@ -59,7 +59,6 @@ void main() { 'list': 'at://did:plc:creator/app.bsky.graph.list/list-1', 'createdAt': '2026-01-01T00:00:00.000Z', }, - creator: const ProfileViewBasic(did: 'did:plc:creator', handle: 'creator.bsky.social'), indexedAt: DateTime.utc(2026, 1, 1), ); @@ -80,15 +79,7 @@ void main() { accountDid: any(named: 'accountDid'), ), ).thenAnswer((_) async {}); - when( - () => mockRepository.searchPosts( - query: any(named: 'query'), - sort: any(named: 'sort'), - filters: any(named: 'filters'), - cursor: any(named: 'cursor'), - limit: any(named: 'limit'), - ), - ).thenAnswer((_) async => SearchPostsResult(posts: [])); + stubSearchPosts(mockRepository); when( () => mockRepository.searchActors( query: any(named: 'query'), @@ -479,16 +470,8 @@ void main() { build: buildBloc, act: (bloc) => bloc.add(const PostFiltersChanged(filters: PostSearchFilters(domain: 'example.com'))), verify: (_) { - final captured = verify( - () => mockRepository.searchPosts( - query: any(named: 'query'), - sort: any(named: 'sort'), - filters: captureAny(named: 'filters'), - cursor: any(named: 'cursor'), - limit: any(named: 'limit'), - ), - ).captured; - final filters = captured.last as PostSearchFilters; + final captured = captureSearchFilters(mockRepository); + final filters = captured.last; expect(filters.domain, 'example.com'); }, ); @@ -527,7 +510,7 @@ void main() { limit: 50, ), ).captured; - final filters = captured.last as PostSearchFilters; + final filters = captured.last; expect(filters.domain, 'example.com'); }, ); @@ -537,16 +520,8 @@ void main() { build: buildScopedBloc, act: (bloc) => bloc.add(const PostFiltersChanged(filters: PostSearchFilters(author: 'did:plc:other'))), verify: (_) { - final captured = verify( - () => mockRepository.searchPosts( - query: any(named: 'query'), - sort: any(named: 'sort'), - filters: captureAny(named: 'filters'), - cursor: any(named: 'cursor'), - limit: any(named: 'limit'), - ), - ).captured; - final filters = captured.last as PostSearchFilters; + final captured = captureSearchFilters(mockRepository); + final filters = captured.last; expect(filters.author, 'did:plc:scoped'); }, ); diff --git a/test/features/search/cubit/hashtag_cubit_test.dart b/test/features/search/cubit/hashtag_cubit_test.dart index cb37ea4..28cb86b 100644 --- a/test/features/search/cubit/hashtag_cubit_test.dart +++ b/test/features/search/cubit/hashtag_cubit_test.dart @@ -4,7 +4,7 @@ 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'; +import '../../../helpers/fixtures/feed.dart'; class MockSearchRepository extends Mock implements SearchRepository {} diff --git a/test/features/search/cubit/topic_cubit_test.dart b/test/features/search/cubit/topic_cubit_test.dart index 3ce662b..102f6a8 100644 --- a/test/features/search/cubit/topic_cubit_test.dart +++ b/test/features/search/cubit/topic_cubit_test.dart @@ -4,7 +4,7 @@ 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'; +import '../../../helpers/fixtures/feed.dart'; class MockSearchRepository extends Mock implements SearchRepository {} diff --git a/test/features/search/data/hashtag_utils_test.dart b/test/features/search/data/hashtag_utils_test.dart index a9178ac..f7e5e32 100644 --- a/test/features/search/data/hashtag_utils_test.dart +++ b/test/features/search/data/hashtag_utils_test.dart @@ -4,7 +4,7 @@ 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'; -import '../../../helpers/feed_fixtures.dart'; +import '../../../helpers/fixtures/feed.dart'; PostView _post(String uri, Map record) => testPostView(uri: uri, record: record, indexedAt: DateTime.utc(2026, 1, 1)); diff --git a/test/features/search/data/post_text_extractor_test.dart b/test/features/search/data/post_text_extractor_test.dart index b6f295c..7afb536 100644 --- a/test/features/search/data/post_text_extractor_test.dart +++ b/test/features/search/data/post_text_extractor_test.dart @@ -7,7 +7,7 @@ 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'; -import '../../../helpers/feed_fixtures.dart'; +import '../../../helpers/fixtures/feed.dart'; PostView _post({String text = '', UPostViewEmbed? embed}) => testPostView( uri: 'at://did:plc:test/app.bsky.feed.post/xyz', diff --git a/test/features/search/presentation/hashtag_screen_test.dart b/test/features/search/presentation/hashtag_screen_test.dart index b46b852..b690549 100644 --- a/test/features/search/presentation/hashtag_screen_test.dart +++ b/test/features/search/presentation/hashtag_screen_test.dart @@ -9,7 +9,7 @@ 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'; +import '../../../helpers/fixtures/feed.dart'; class MockSearchRepository extends Mock implements SearchRepository {} diff --git a/test/features/search/presentation/profile_post_search_screen_test.dart b/test/features/search/presentation/profile_post_search_screen_test.dart index d2bc719..a1ad779 100644 --- a/test/features/search/presentation/profile_post_search_screen_test.dart +++ b/test/features/search/presentation/profile_post_search_screen_test.dart @@ -1,6 +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'; import 'package:flutter_test/flutter_test.dart'; @@ -12,6 +9,9 @@ import 'package:lazurite/features/search/presentation/search_screen.dart'; import 'package:lazurite/features/typeahead/data/typeahead_repository.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../helpers/fixtures/feed.dart'; +import '../../../helpers/search_helpers.dart'; + class MockSearchRepository extends Mock implements SearchRepository {} class MockTypeaheadRepository extends Mock implements TypeaheadRepository {} @@ -32,22 +32,15 @@ void main() { typeaheadRepository = MockTypeaheadRepository(); database = MockAppDatabase(); - when( - () => searchRepository.searchPosts( - query: any(named: 'query'), - sort: any(named: 'sort'), - filters: any(named: 'filters'), - cursor: any(named: 'cursor'), - limit: any(named: 'limit'), - ), - ).thenAnswer( - (_) async => SearchPostsResult( + stubSearchPosts( + searchRepository, + result: SearchPostsResult( posts: [ - PostView( - uri: AtUri.parse('at://did:plc:test/app.bsky.feed.post/1'), + testPostView( + uri: 'at://did:plc:test/app.bsky.feed.post/1', cid: 'cid1', - author: const ProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), - record: const {r'$type': 'app.bsky.feed.post', 'text': 'hello', 'createdAt': '2026-01-01T00:00:00.000Z'}, + author: testProfileViewBasic(did: 'did:plc:test', handle: 'test.bsky.social'), + record: testPostRecordJson(text: 'hello', createdAt: DateTime.utc(2026, 1, 1)), indexedAt: DateTime.utc(2026, 1, 1), ), ], @@ -101,17 +94,7 @@ void main() { await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); - final captured = verify( - () => searchRepository.searchPosts( - query: any(named: 'query'), - sort: any(named: 'sort'), - filters: captureAny(named: 'filters'), - cursor: any(named: 'cursor'), - limit: any(named: 'limit'), - ), - ).captured; - - final PostSearchFilters filters = captured.last as PostSearchFilters; + final filters = captureSearchFilters(searchRepository).last; expect(filters.author, 'did:plc:fixed-author'); expect(filters.domain, 'example.com'); }); diff --git a/test/features/search/presentation/search_screen_test.dart b/test/features/search/presentation/search_screen_test.dart index ce28295..c5bb6e2 100644 --- a/test/features/search/presentation/search_screen_test.dart +++ b/test/features/search/presentation/search_screen_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/feed/defs.dart'; @@ -17,6 +16,10 @@ import 'package:lazurite/features/typeahead/data/typeahead_repository.dart'; import 'package:lazurite/features/typeahead/data/typeahead_result.dart'; import 'package:lazurite/shared/presentation/widgets/app_screen_entrance.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:poptart_core/poptart_core.dart'; + +import '../../../helpers/connectivity_helpers.dart'; +import '../../../helpers/search_helpers.dart'; class MockSearchRepository extends Mock implements SearchRepository {} @@ -47,12 +50,7 @@ void main() { mockDatabase = MockAppDatabase(); connectivityCubit = MockConnectivityCubit(); feedPreferencesCubit = MockFeedPreferencesCubit(); - when(() => connectivityCubit.state).thenReturn(const ConnectivityState.online()); - whenListen( - connectivityCubit, - const Stream.empty(), - initialState: const ConnectivityState.online(), - ); + stubConnectivityCubit(connectivityCubit, state: const ConnectivityState.online()); when(() => feedPreferencesCubit.state).thenReturn(const FeedPreferencesState.loaded(feeds: [])); whenListen( feedPreferencesCubit, @@ -67,15 +65,7 @@ void main() { ), ).thenAnswer((_) async {}); when(() => mockDatabase.getSearchHistory(any(), limit: any(named: 'limit'))).thenAnswer((_) async => []); - when( - () => mockSearchRepository.searchPosts( - query: any(named: 'query'), - sort: any(named: 'sort'), - filters: any(named: 'filters'), - cursor: any(named: 'cursor'), - limit: any(named: 'limit'), - ), - ).thenAnswer((_) async => SearchPostsResult(posts: [])); + stubSearchPosts(mockSearchRepository); when( () => mockSearchRepository.searchActors( query: any(named: 'query'), @@ -195,7 +185,6 @@ void main() { testWidgets('shows tab-aware empty state when no search history', (tester) async { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('Search posts'), findsWidgets); expect(find.textContaining('Find conversations and keywords across posts'), findsOneWidget); }); @@ -310,7 +299,6 @@ void main() { await tester.testTextInput.receiveAction(TextInputAction.search); await tester.pumpAndSettle(); - expect(find.text('Top'), findsOneWidget); expect(find.text('Latest'), findsOneWidget); }); @@ -389,7 +377,6 @@ void main() { await tester.tap(find.text('Jump to profile')); await tester.pumpAndSettle(); - expect(find.text('Jump to profile'), findsNWidgets(2)); expect(find.text('Handle'), findsOneWidget); }); @@ -397,15 +384,12 @@ void main() { testWidgets('jump to profile dialog hides typing hint after more than 3 characters', (tester) async { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - await tester.tap(find.text('Jump to profile')); await tester.pumpAndSettle(); - expect(find.text('Start typing to search handles.'), findsOneWidget); await tester.enterText(find.byType(TextFormField), 'rive'); await tester.pumpAndSettle(); - expect(find.text('Start typing to search handles.'), findsNothing); }); @@ -430,12 +414,10 @@ void main() { await tester.enterText(find.byType(TextFormField), 'river'); await tester.pump(const Duration(milliseconds: 350)); await tester.pumpAndSettle(); - expect(find.text('River Tam'), findsOneWidget); await tester.tap(find.text('River Tam')); await tester.pumpAndSettle(); - expect(find.text('profile:did:plc:river'), findsOneWidget); }); @@ -457,7 +439,6 @@ void main() { await tester.enterText(find.byType(TextField).first, '@river'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); - expect(find.text('River Tam'), findsNothing); }); @@ -471,14 +452,12 @@ void main() { await tester.enterText(find.byType(TextFormField), 'custom.bsky.social'); await tester.testTextInput.receiveAction(TextInputAction.search); await tester.pumpAndSettle(); - expect(find.text('profile:custom.bsky.social'), findsOneWidget); }); testWidgets('third Starter Packs tab renders', (tester) async { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('Starter Packs'), findsOneWidget); }); @@ -605,17 +584,14 @@ void main() { await tester.enterText(find.byType(TextField).first, 'fallback'); await tester.testTextInput.receiveAction(TextInputAction.search); await tester.pumpAndSettle(); - expect(find.text('fallback-name'), findsOneWidget); }); testWidgets('starter packs tab shows Bluesky issue link text', (tester) async { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - await tester.tap(find.text('Starter Packs')); await tester.pumpAndSettle(); - expect(find.text('Track API progress'), findsOneWidget); expect(find.textContaining('https://github.com/bluesky-social/bsky-docs/issues/306'), findsNothing); }); diff --git a/test/features/settings/data/video_repository_test.dart b/test/features/settings/data/video_repository_test.dart index 38de2f8..d5c520a 100644 --- a/test/features/settings/data/video_repository_test.dart +++ b/test/features/settings/data/video_repository_test.dart @@ -1,7 +1,9 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:bluesky_poptart/app/bsky/video/get_upload_limits.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/features/settings/data/video_repository.dart'; -import '../../../helpers/test_utils.dart'; + +import '../../../helpers/fixtures/auth.dart'; +import '../../../helpers/fixtures/network.dart'; class FakeVideoUploadLimitsApi implements VideoUploadLimitsApi { FakeVideoUploadLimitsApi({ @@ -110,7 +112,6 @@ void main() { ); final result = await repository.getUploadLimits(); - expect(result.canUpload, isTrue); expect(result.remainingDailyVideos, 8); expect(initialCalls, 1); @@ -124,7 +125,6 @@ void main() { getUploadLimitsAuthTokenHandler: () async => throw Exception('service auth unavailable'), ); final repository = VideoRepository(api: api); - await expectLater(repository.getUploadLimits(), throwsA(isA())); }); }); diff --git a/test/features/settings/presentation/search_settings_test.dart b/test/features/settings/presentation/search_settings_test.dart index d045276..3aa63a6 100644 --- a/test/features/settings/presentation/search_settings_test.dart +++ b/test/features/settings/presentation/search_settings_test.dart @@ -4,13 +4,13 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.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'; 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:mocktail/mocktail.dart'; -import '../../../helpers/settings_fixtures.dart'; +import '../../../helpers/fixtures/auth.dart'; +import '../../../helpers/fixtures/settings.dart'; class MockAuthBloc extends MockBloc implements AuthBloc {} @@ -22,13 +22,7 @@ SettingsState _baseSettings({bool semanticSearchEnabled = false, int maxResults testSettingsState(semanticSearchEnabled: semanticSearchEnabled, semanticSearchMaxResults: maxResults); void main() { - const tokens = AuthTokens( - accessToken: 'access', - refreshToken: 'refresh', - did: 'did:plc:test', - handle: 'test.bsky.social', - displayName: 'Test User', - ); + final tokens = testAuthTokens(accessToken: 'access', refreshToken: 'refresh', displayName: 'Test User'); late MockAuthBloc authBloc; late MockAccountSwitcherCubit accountSwitcherCubit; @@ -72,7 +66,7 @@ void main() { }); testWidgets('shows typeahead provider selector', (tester) async { - const authenticatedState = AuthState.authenticated(tokens); + final authenticatedState = AuthState.authenticated(tokens); when(() => authBloc.state).thenReturn(authenticatedState); whenListen(authBloc, const Stream.empty(), initialState: authenticatedState); @@ -90,7 +84,7 @@ void main() { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); - const authenticatedState = AuthState.authenticated(tokens); + final authenticatedState = AuthState.authenticated(tokens); when(() => authBloc.state).thenReturn(authenticatedState); whenListen(authBloc, const Stream.empty(), initialState: authenticatedState); diff --git a/test/features/settings/presentation/settings_screen_test.dart b/test/features/settings/presentation/settings_screen_test.dart index 867d3ad..0b434aa 100644 --- a/test/features/settings/presentation/settings_screen_test.dart +++ b/test/features/settings/presentation/settings_screen_test.dart @@ -17,8 +17,8 @@ import 'package:lazurite/features/settings/presentation/settings_screen.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'; +import '../../../helpers/fixtures/auth.dart'; +import '../../../helpers/fixtures/settings.dart'; class MockAccountSwitcherCubit extends MockCubit implements AccountSwitcherCubit {} @@ -102,22 +102,20 @@ void main() { when(() => cacheMaintenanceService.clearCaches()).thenAnswer((_) async {}); }); - Widget buildSubject() { - return MultiRepositoryProvider( + Widget buildSubject() => MultiRepositoryProvider( + providers: [ + RepositoryProvider.value(value: crashReportingService), + RepositoryProvider.value(value: cacheMaintenanceService), + ], + child: MultiBlocProvider( providers: [ - RepositoryProvider.value(value: crashReportingService), - RepositoryProvider.value(value: cacheMaintenanceService), + BlocProvider.value(value: authBloc), + BlocProvider.value(value: accountSwitcherCubit), + BlocProvider.value(value: settingsCubit), ], - child: MultiBlocProvider( - providers: [ - BlocProvider.value(value: authBloc), - BlocProvider.value(value: accountSwitcherCubit), - BlocProvider.value(value: settingsCubit), - ], - child: const MaterialApp(home: SettingsScreen()), - ), - ); - } + child: const MaterialApp(home: SettingsScreen()), + ), + ); Widget buildRoutedSubject() { final router = GoRouter( @@ -277,7 +275,7 @@ void main() { }); testWidgets('shows the AT Protocol connection card for the authenticated account', (tester) async { - final tokens = AuthTokens( + final tokens = testAuthTokens( accessToken: buildJwt( aud: 'shaggymane.us-west.host.bsky.network', sub: 'did:plc:lazurite123', @@ -333,7 +331,7 @@ void main() { }); testWidgets('tapping the DID row opens Dev Tools with the DID query', (tester) async { - final tokens = AuthTokens( + final tokens = testAuthTokens( accessToken: buildJwt( aud: 'shaggymane.us-west.host.bsky.network', sub: 'did:plc:lazurite123', @@ -752,7 +750,7 @@ void main() { }); } -AuthTokens _authenticatedTokens() => const AuthTokens( +AuthTokens _authenticatedTokens() => testAuthTokens( accessToken: 'access-token', refreshToken: 'refresh-token', did: 'did:plc:test', diff --git a/test/features/starter_packs/presentation/starter_pack_detail_screen_test.dart b/test/features/starter_packs/presentation/starter_pack_detail_screen_test.dart index 9247510..037cb70 100644 --- a/test/features/starter_packs/presentation/starter_pack_detail_screen_test.dart +++ b/test/features/starter_packs/presentation/starter_pack_detail_screen_test.dart @@ -1,4 +1,3 @@ -import 'package:poptart_core/poptart_core.dart' show AtUri; import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:bluesky_poptart/app/bsky/graph/defs.dart'; @@ -9,6 +8,9 @@ import 'package:go_router/go_router.dart'; import 'package:lazurite/features/starter_packs/data/starter_pack_repository.dart'; import 'package:lazurite/features/starter_packs/presentation/starter_pack_detail_screen.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:poptart_core/poptart_core.dart' show AtUri; + +import '../../../helpers/assertion_helpers.dart'; class MockStarterPackRepository extends Mock implements StarterPackRepository {} @@ -115,8 +117,7 @@ void main() { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle(); - expect(find.text('The Creator'), findsOneWidget); - expect(find.text('@creator.bsky.social'), findsOneWidget); + expectAccountRow(displayName: 'The Creator', handle: 'creator.bsky.social'); }); testWidgets('shows join stats', (tester) async { diff --git a/test/features/typeahead/presentation/typeahead_text_field_test.dart b/test/features/typeahead/presentation/typeahead_text_field_test.dart index bb65beb..5b5aa46 100644 --- a/test/features/typeahead/presentation/typeahead_text_field_test.dart +++ b/test/features/typeahead/presentation/typeahead_text_field_test.dart @@ -7,6 +7,8 @@ import 'package:lazurite/features/typeahead/data/typeahead_result.dart'; import 'package:lazurite/features/typeahead/presentation/typeahead_text_field.dart'; import 'package:lazurite/shared/presentation/widgets/global_tap_outside_unfocus.dart'; +import '../../../helpers/assertion_helpers.dart'; + void main() { group('TypeaheadTextField', () { testWidgets('overlay appears after typing and renders results', (tester) async { @@ -25,8 +27,7 @@ void main() { await tester.pump(const Duration(milliseconds: 20)); await tester.pumpAndSettle(); - expect(find.text('Alice'), findsOneWidget); - expect(find.text('@alice.bsky.social'), findsOneWidget); + expectAccountRow(displayName: 'Alice', handle: 'alice.bsky.social'); expect(find.byType(CircleAvatar), findsOneWidget); }); @@ -155,12 +156,10 @@ void main() { await tester.enterText(find.byType(TextFormField), 'alice'); await tester.pump(const Duration(milliseconds: 5)); - expect(find.byKey(const ValueKey('typeahead-input-loading-spinner')), findsOneWidget); completer.complete(const [TypeaheadResult(did: 'did:plc:alice', handle: 'alice.bsky.social')]); await tester.pumpAndSettle(); - expect(find.byKey(const ValueKey('typeahead-input-loading-spinner')), findsNothing); }); }); diff --git a/test/helpers/assertion_helpers.dart b/test/helpers/assertion_helpers.dart new file mode 100644 index 0000000..13481fc --- /dev/null +++ b/test/helpers/assertion_helpers.dart @@ -0,0 +1,54 @@ +import 'package:flutter_test/flutter_test.dart'; + +void expectAccountRow({required String handle, String? displayName}) { + if (displayName != null) { + expect(find.text(displayName), findsOneWidget); + } + expect(find.text('@$handle'), findsOneWidget); +} + +void expectErrorState(String title, {String? message, Finder? retryFinder}) { + expect(find.text(title), findsOneWidget); + if (message != null) { + expect(find.text(message), findsOneWidget); + } + expect(retryFinder ?? find.text('Retry'), findsOneWidget); +} + +Future tapRetry(WidgetTester tester, {Finder? retryFinder}) async { + await tester.tap(retryFinder ?? find.text('Retry')); + await tester.pumpAndSettle(); +} + +void expectOfflineState(String title, {String? message}) { + expect(find.text(title), findsOneWidget); + if (message != null) { + expect(find.text(message), findsOneWidget); + } +} + +void expectListDetailHeader({required String description, required String creatorHandle}) { + expect(find.text(description), findsOneWidget); + expect(find.text('by @$creatorHandle'), findsOneWidget); +} + +Future tapMembersTab(WidgetTester tester) async { + await tester.tap(find.text('MEMBERS')); + await tester.pumpAndSettle(); +} + +void expectListMember({required String handle, String? displayName}) => + expectAccountRow(handle: handle, displayName: displayName); + +void expectFeedEmbed({required String name, String? description, String? likeCount}) { + expect(find.text('FEED'), findsOneWidget); + expect(find.text(name), findsOneWidget); + if (description != null) expect(find.text(description), findsOneWidget); + if (likeCount != null) expect(find.text(likeCount), findsOneWidget); +} + +void expectListEmbed({required String name, String? description}) { + expect(find.text('LIST'), findsOneWidget); + expect(find.text(name), findsOneWidget); + if (description != null) expect(find.text(description), findsOneWidget); +} diff --git a/test/helpers/auth_fixtures.dart b/test/helpers/auth_fixtures.dart deleted file mode 100644 index 0696058..0000000 --- a/test/helpers/auth_fixtures.dart +++ /dev/null @@ -1,35 +0,0 @@ -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/connectivity_helpers.dart b/test/helpers/connectivity_helpers.dart new file mode 100644 index 0000000..3651358 --- /dev/null +++ b/test/helpers/connectivity_helpers.dart @@ -0,0 +1,12 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; +import 'package:mocktail/mocktail.dart'; + +void stubConnectivityCubit( + ConnectivityCubit cubit, { + ConnectivityState state = const ConnectivityState.online(), + Stream stream = const Stream.empty(), +}) { + when(() => cubit.state).thenReturn(state); + whenListen(cubit, stream, initialState: state); +} diff --git a/test/helpers/fixtures/auth.dart b/test/helpers/fixtures/auth.dart new file mode 100644 index 0000000..2b3cb84 --- /dev/null +++ b/test/helpers/fixtures/auth.dart @@ -0,0 +1,161 @@ +import 'dart:convert'; + +import 'package:lazurite/features/auth/data/models/auth_models.dart'; + +String base64UrlEncode(Map value) => + base64Url.encode(utf8.encode(jsonEncode(value))).replaceAll('=', ''); + +String buildJwt({ + required String sub, + String? aud, + String? clientId, + String? iss, + String? scope, + int? expEpochSeconds, + int? iatEpochSeconds, +}) { + final nowEpochSeconds = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000; + final header = base64UrlEncode(const {'alg': 'none', 'typ': 'JWT'}); + final payload = base64UrlEncode({ + 'sub': sub, + 'exp': expEpochSeconds ?? nowEpochSeconds + 3600, + 'iat': iatEpochSeconds ?? nowEpochSeconds, + 'aud': ?aud, + 'client_id': ?clientId, + 'iss': ?iss, + 'scope': scope ?? (clientId == null ? 'atproto' : 'atproto transition:generic'), + }); + + return '$header.$payload.signature'; +} + +const Object _expiresAtDefault = Object(); + +AuthTokens testAliceTokens({ + String accessToken = 'opaque-access-token', + String? refreshToken = 'refresh-token', + DateTime? expiresAt, +}) => testAuthTokens( + accessToken: accessToken, + refreshToken: refreshToken, + did: 'did:plc:alice', + handle: 'alice.bsky.social', + displayName: 'Alice', + expiresAt: expiresAt, +); + +AuthTokens testRiverTokens({String accessToken = 'access', String? refreshToken = 'refresh', DateTime? expiresAt}) => + testAuthTokens( + accessToken: accessToken, + refreshToken: refreshToken, + did: 'did:plc:me', + handle: 'me.bsky.social', + displayName: 'River Tam', + expiresAt: expiresAt, + ); + +AuthTokens testOAuthTokens({ + String accessToken = 'oauth-access-token', + String? refreshToken = 'oauth-refresh-token', + String did = 'did:plc:alice', + String handle = 'alice.bsky.social', + String? displayName = 'Alice', + String service = 'https://pds.example.com', + String oauthService = 'https://bsky.social', + String oauthClientId = 'client-id', + String oauthTokenType = 'DPoP', + String oauthScope = 'atproto transition:generic', + String? dpopNonce, + String? dpopPublicKey, + String? dpopPrivateKey, + DateTime? expiresAt, +}) => testAuthTokens( + accessToken: accessToken, + refreshToken: refreshToken, + did: did, + handle: handle, + displayName: displayName, + service: service, + oauthService: oauthService, + oauthClientId: oauthClientId, + oauthTokenType: oauthTokenType, + oauthScope: oauthScope, + dpopNonce: dpopNonce, + dpopPublicKey: dpopPublicKey, + dpopPrivateKey: dpopPrivateKey, + authMethod: AuthMethod.oauth, + expiresAt: expiresAt, +); + +AuthTokens testOpaqueOAuthTokens({ + String accessToken = 'opaque-access-token', + String? refreshToken = 'refresh-token', + String did = 'did:plc:alice', + String handle = 'alice.bsky.social', + String? displayName = 'Alice', + String service = 'https://pds.example.com', + String oauthClientId = 'client-id', + String oauthTokenType = 'DPoP', + String oauthScope = 'atproto transition:generic', + String? dpopNonce, + String? dpopPublicKey, + String? dpopPrivateKey, + DateTime? expiresAt, +}) => testOAuthTokens( + accessToken: accessToken, + refreshToken: refreshToken, + did: did, + handle: handle, + displayName: displayName, + service: service, + oauthClientId: oauthClientId, + oauthTokenType: oauthTokenType, + oauthScope: oauthScope, + dpopNonce: dpopNonce, + dpopPublicKey: dpopPublicKey, + dpopPrivateKey: dpopPrivateKey, + expiresAt: expiresAt, +); + +AuthTokens testPdsOAuthTokens({ + String accessToken = 'access', + String? refreshToken = 'refresh', + String service = 'https://pds.example.com', + DateTime? expiresAt, +}) => testOAuthTokens(accessToken: accessToken, refreshToken: refreshToken, service: service, expiresAt: expiresAt); + +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', + Object? expiresAt = _expiresAtDefault, + String? oauthService, + String? oauthClientId, + String? oauthTokenType, + String? oauthScope, + String? dpopNonce, + String? dpopPublicKey, + String? dpopPrivateKey, + AuthMethod authMethod = AuthMethod.appPassword, +}) => AuthTokens( + accessToken: accessToken, + refreshToken: refreshToken, + expiresAt: identical(expiresAt, _expiresAtDefault) + ? DateTime.now().toUtc().add(const Duration(hours: 1)) + : expiresAt as DateTime?, + 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/fixtures/feed.dart similarity index 89% rename from test/helpers/feed_fixtures.dart rename to test/helpers/fixtures/feed.dart index 4e9dced..d71119d 100644 --- a/test/helpers/feed_fixtures.dart +++ b/test/helpers/fixtures/feed.dart @@ -1,6 +1,7 @@ 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'; +import 'package:poptart_lex/com/atproto/label/defs.dart'; const testAuthorDid = 'did:plc:author'; const testAuthorHandle = 'author.bsky.social'; @@ -35,6 +36,7 @@ PostView testPostView({ int? likeCount, int? quoteCount, UPostViewEmbed? embed, + List