mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 294 lines 12 kB view raw
1import 'package:atproto/com_atproto_repo_listrecords.dart'; 2import 'package:atproto_core/atproto_core.dart'; 3import 'package:bluesky/app_bsky_actor_defs.dart'; 4import 'package:bloc_test/bloc_test.dart'; 5import 'package:flutter/material.dart'; 6import 'package:flutter_bloc/flutter_bloc.dart'; 7import 'package:flutter_test/flutter_test.dart'; 8import 'package:lazurite/features/devtools/cubit/dev_tools_cubit.dart'; 9import 'package:lazurite/features/devtools/presentation/dev_tools_screen.dart'; 10import 'package:mocktail/mocktail.dart'; 11 12class MockDevToolsCubit extends MockCubit<DevToolsState> implements DevToolsCubit {} 13 14class FakeDevToolsState extends Fake implements DevToolsState {} 15 16late MockDevToolsCubit mockDevToolsCubit; 17 18void main() { 19 setUpAll(() { 20 registerFallbackValue(FakeDevToolsState()); 21 registerFallbackValue( 22 const RepoListRecordsRecord( 23 uri: AtUri('at://did:plc:test/app.bsky.feed.post/123'), 24 cid: 'cid123', 25 value: {'text': 'Test'}, 26 ), 27 ); 28 }); 29 30 setUp(() { 31 mockDevToolsCubit = MockDevToolsCubit(); 32 33 when(() => mockDevToolsCubit.state).thenReturn(const DevToolsState()); 34 when(() => mockDevToolsCubit.resolve(any())).thenAnswer((_) async {}); 35 when(() => mockDevToolsCubit.queryTypeahead(any())).thenAnswer((_) async {}); 36 when(() => mockDevToolsCubit.loadCollection(any())).thenAnswer((_) async {}); 37 when(() => mockDevToolsCubit.loadRecord(any())).thenAnswer((_) async {}); 38 when(() => mockDevToolsCubit.loadMoreRecords()).thenAnswer((_) async {}); 39 when(() => mockDevToolsCubit.goBackToRepo()).thenReturn(null); 40 when(() => mockDevToolsCubit.goBackToCollection()).thenReturn(null); 41 when(() => mockDevToolsCubit.clearTypeahead()).thenReturn(null); 42 when(() => mockDevToolsCubit.clearInput()).thenReturn(null); 43 44 whenListen(mockDevToolsCubit, const Stream<DevToolsState>.empty(), initialState: const DevToolsState()); 45 }); 46 47 Widget buildSubject({String? initialQuery}) { 48 return MaterialApp( 49 home: BlocProvider<DevToolsCubit>.value( 50 value: mockDevToolsCubit, 51 child: DevToolsScreen(initialQuery: initialQuery), 52 ), 53 ); 54 } 55 56 group('DevToolsScreen', () { 57 testWidgets('renders empty state initially', (tester) async { 58 await tester.pumpWidget(buildSubject()); 59 60 expect(find.text('PDS Explorer'), findsAtLeastNWidgets(1)); 61 expect(find.textContaining('Enter a handle, DID, or AT-URI'), findsOneWidget); 62 expect(find.text('Inspired by pds.ls'), findsOneWidget); 63 }); 64 65 testWidgets('renders loading state', (tester) async { 66 when(() => mockDevToolsCubit.state).thenReturn(const DevToolsState(status: DevToolsStatus.loading)); 67 whenListen( 68 mockDevToolsCubit, 69 const Stream<DevToolsState>.empty(), 70 initialState: const DevToolsState(status: DevToolsStatus.loading), 71 ); 72 73 await tester.pumpWidget(buildSubject()); 74 75 expect(find.byType(CircularProgressIndicator), findsOneWidget); 76 }); 77 78 testWidgets('renders error state', (tester) async { 79 when( 80 () => mockDevToolsCubit.state, 81 ).thenReturn(const DevToolsState(status: DevToolsStatus.error, errorMessage: 'Test error')); 82 whenListen( 83 mockDevToolsCubit, 84 const Stream<DevToolsState>.empty(), 85 initialState: const DevToolsState(status: DevToolsStatus.error, errorMessage: 'Test error'), 86 ); 87 88 await tester.pumpWidget(buildSubject()); 89 90 expect(find.text('Error'), findsOneWidget); 91 expect(find.text('Test error'), findsOneWidget); 92 }); 93 94 testWidgets('renders repo overview with collection counts', (tester) async { 95 const state = DevToolsState( 96 status: DevToolsStatus.repoLoaded, 97 did: 'did:plc:test', 98 handle: 'test.bsky.social', 99 repoHandle: 'test.bsky.social', 100 collections: [ 101 CollectionSummary('app.bsky.feed.post', recordCount: 2), 102 CollectionSummary('app.bsky.feed.like', recordCount: 3), 103 ], 104 ); 105 106 when(() => mockDevToolsCubit.state).thenReturn(state); 107 whenListen(mockDevToolsCubit, const Stream<DevToolsState>.empty(), initialState: state); 108 109 await tester.pumpWidget(buildSubject()); 110 111 expect(find.text('test.bsky.social'), findsAtLeastNWidgets(1)); 112 expect(find.text('did:plc:test'), findsOneWidget); 113 expect(find.text('2 collections'), findsOneWidget); 114 expect(find.text('5 records'), findsOneWidget); 115 expect(find.text('app.bsky.feed.post'), findsOneWidget); 116 expect(find.text('app.bsky.feed.like'), findsOneWidget); 117 }); 118 119 testWidgets('submitting search calls cubit resolve', (tester) async { 120 await tester.pumpWidget(buildSubject()); 121 122 await tester.enterText(find.byType(TextField), 'alice.bsky.social'); 123 await tester.tap(find.text('Resolve')); 124 125 verify(() => mockDevToolsCubit.resolve('alice.bsky.social')).called(1); 126 }); 127 128 testWidgets('typing @handle triggers typeahead query', (tester) async { 129 await tester.pumpWidget(buildSubject()); 130 131 await tester.enterText(find.byType(TextField), '@ali'); 132 await tester.pump(); 133 134 verify(() => mockDevToolsCubit.queryTypeahead('@ali')).called(1); 135 }); 136 137 testWidgets('tapping a typeahead suggestion resolves the selected handle', (tester) async { 138 const state = DevToolsState( 139 typeaheadActors: [ProfileViewBasic(did: 'did:plc:alice', handle: 'alice.bsky.social', displayName: 'Alice')], 140 ); 141 when(() => mockDevToolsCubit.state).thenReturn(state); 142 whenListen(mockDevToolsCubit, const Stream<DevToolsState>.empty(), initialState: state); 143 144 await tester.pumpWidget(buildSubject()); 145 await tester.enterText(find.byType(TextField), '@a'); 146 await tester.pump(); 147 148 await tester.tap(find.text('Alice')); 149 150 verify(() => mockDevToolsCubit.resolve('alice.bsky.social')).called(1); 151 verify(() => mockDevToolsCubit.clearTypeahead()).called(1); 152 }); 153 154 testWidgets('initial query prefills the input and resolves automatically', (tester) async { 155 await tester.pumpWidget(buildSubject(initialQuery: 'did:plc:test')); 156 await tester.pump(); 157 158 expect(find.widgetWithText(TextField, 'did:plc:test'), findsOneWidget); 159 verify(() => mockDevToolsCubit.resolve('did:plc:test')).called(1); 160 }); 161 162 testWidgets('tapping a record calls cubit loadRecord', (tester) async { 163 const record = RepoListRecordsRecord( 164 uri: AtUri('at://did:plc:test/app.bsky.feed.post/123'), 165 cid: 'cid123', 166 value: {'text': 'Summary'}, 167 ); 168 const state = DevToolsState( 169 status: DevToolsStatus.collectionLoaded, 170 did: 'did:plc:test', 171 handle: 'test.bsky.social', 172 repoHandle: 'test.bsky.social', 173 collections: [CollectionSummary('app.bsky.feed.post', recordCount: 1)], 174 selectedCollection: 'app.bsky.feed.post', 175 records: [record], 176 ); 177 178 when(() => mockDevToolsCubit.state).thenReturn(state); 179 whenListen(mockDevToolsCubit, const Stream<DevToolsState>.empty(), initialState: state); 180 181 await tester.pumpWidget(buildSubject()); 182 await tester.tap(find.text('123')); 183 184 verify(() => mockDevToolsCubit.loadRecord(record)).called(1); 185 }); 186 187 testWidgets('renders breadcrumbs for record navigation', (tester) async { 188 const state = DevToolsState( 189 status: DevToolsStatus.recordLoaded, 190 did: 'did:plc:test', 191 handle: 'test.bsky.social', 192 repoHandle: 'test.bsky.social', 193 collections: [CollectionSummary('app.bsky.feed.post', recordCount: 1)], 194 selectedCollection: 'app.bsky.feed.post', 195 selectedRecord: RecordInfo( 196 uri: 'at://did:plc:test/app.bsky.feed.post/123', 197 cid: 'cid123', 198 value: {'text': 'Summary'}, 199 ), 200 ); 201 202 when(() => mockDevToolsCubit.state).thenReturn(state); 203 whenListen(mockDevToolsCubit, const Stream<DevToolsState>.empty(), initialState: state); 204 205 await tester.pumpWidget(buildSubject()); 206 207 expect(find.byKey(const ValueKey('dev-tools-breadcrumb-repo')), findsOneWidget); 208 expect(find.byKey(const ValueKey('dev-tools-breadcrumb-collection')), findsOneWidget); 209 expect(find.byKey(const ValueKey('dev-tools-breadcrumb-record')), findsOneWidget); 210 expect(find.text('test.bsky.social'), findsAtLeastNWidgets(1)); 211 expect(find.text('app.bsky.feed.post'), findsAtLeastNWidgets(1)); 212 expect(find.text('123'), findsAtLeastNWidgets(1)); 213 }); 214 215 testWidgets('tapping repo breadcrumb calls cubit goBackToRepo', (tester) async { 216 const state = DevToolsState( 217 status: DevToolsStatus.collectionLoaded, 218 did: 'did:plc:test', 219 handle: 'test.bsky.social', 220 repoHandle: 'test.bsky.social', 221 collections: [CollectionSummary('app.bsky.feed.post', recordCount: 1)], 222 selectedCollection: 'app.bsky.feed.post', 223 records: [ 224 RepoListRecordsRecord( 225 uri: AtUri('at://did:plc:test/app.bsky.feed.post/123'), 226 cid: 'cid123', 227 value: {'text': 'Summary'}, 228 ), 229 ], 230 ); 231 232 when(() => mockDevToolsCubit.state).thenReturn(state); 233 whenListen(mockDevToolsCubit, const Stream<DevToolsState>.empty(), initialState: state); 234 235 await tester.pumpWidget(buildSubject()); 236 await tester.tap(find.byKey(const ValueKey('dev-tools-breadcrumb-repo'))); 237 238 verify(() => mockDevToolsCubit.goBackToRepo()).called(1); 239 }); 240 241 testWidgets('tapping collection breadcrumb calls cubit goBackToCollection', (tester) async { 242 const state = DevToolsState( 243 status: DevToolsStatus.recordLoaded, 244 did: 'did:plc:test', 245 handle: 'test.bsky.social', 246 repoHandle: 'test.bsky.social', 247 collections: [CollectionSummary('app.bsky.feed.post', recordCount: 1)], 248 selectedCollection: 'app.bsky.feed.post', 249 selectedRecord: RecordInfo( 250 uri: 'at://did:plc:test/app.bsky.feed.post/123', 251 cid: 'cid123', 252 value: {'text': 'Summary'}, 253 ), 254 ); 255 256 when(() => mockDevToolsCubit.state).thenReturn(state); 257 whenListen(mockDevToolsCubit, const Stream<DevToolsState>.empty(), initialState: state); 258 259 await tester.pumpWidget(buildSubject()); 260 await tester.tap(find.byKey(const ValueKey('dev-tools-breadcrumb-collection'))); 261 262 verify(() => mockDevToolsCubit.goBackToCollection()).called(1); 263 }); 264 265 testWidgets('shows breadcrumb progress without full-screen spinner during record navigation', (tester) async { 266 const state = DevToolsState( 267 status: DevToolsStatus.collectionLoaded, 268 did: 'did:plc:test', 269 handle: 'test.bsky.social', 270 repoHandle: 'test.bsky.social', 271 collections: [CollectionSummary('app.bsky.feed.post', recordCount: 1)], 272 selectedCollection: 'app.bsky.feed.post', 273 records: [ 274 RepoListRecordsRecord( 275 uri: AtUri('at://did:plc:test/app.bsky.feed.post/123'), 276 cid: 'cid123', 277 value: {'text': 'Summary'}, 278 ), 279 ], 280 isRecordLoading: true, 281 ); 282 283 when(() => mockDevToolsCubit.state).thenReturn(state); 284 whenListen(mockDevToolsCubit, const Stream<DevToolsState>.empty(), initialState: state); 285 286 await tester.pumpWidget(buildSubject()); 287 288 expect(find.byKey(const ValueKey('app-breadcrumbs-loading')), findsOneWidget); 289 expect(find.byType(LinearProgressIndicator), findsOneWidget); 290 expect(find.byType(CircularProgressIndicator), findsNothing); 291 expect(find.text('123'), findsOneWidget); 292 }); 293 }); 294}