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 ft/monetization 348 lines 15 kB view raw
1import 'package:atproto/com_atproto_identity_resolvehandle.dart'; 2import 'package:atproto/com_atproto_repo_describerepo.dart'; 3import 'package:atproto/com_atproto_repo_getrecord.dart'; 4import 'package:atproto/com_atproto_repo_listrecords.dart'; 5import 'package:atproto_core/atproto_core.dart'; 6import 'package:bloc_test/bloc_test.dart'; 7import 'package:flutter_test/flutter_test.dart'; 8import 'package:lazurite/features/devtools/cubit/dev_tools_cubit.dart'; 9 10class FakeDevToolsRepository implements DevToolsRepository { 11 FakeDevToolsRepository({ 12 this.resolveHandleHandler, 13 this.describeRepoHandler, 14 this.listRecordsHandler, 15 this.getRecordHandler, 16 }); 17 18 Future<IdentityResolveHandleOutput> Function({required String handle})? resolveHandleHandler; 19 Future<RepoDescribeRepoOutput> Function({required String repo})? describeRepoHandler; 20 Future<RepoListRecordsOutput> Function({ 21 required String repo, 22 required String collection, 23 int? limit, 24 String? cursor, 25 bool? reverse, 26 })? 27 listRecordsHandler; 28 Future<RepoGetRecordOutput> Function({required String repo, required String collection, required String rkey})? 29 getRecordHandler; 30 31 @override 32 Future<RepoDescribeRepoOutput> describeRepo({required String repo}) { 33 return describeRepoHandler!.call(repo: repo); 34 } 35 36 @override 37 Future<RepoGetRecordOutput> getRecord({required String repo, required String collection, required String rkey}) { 38 return getRecordHandler!.call(repo: repo, collection: collection, rkey: rkey); 39 } 40 41 @override 42 Future<RepoListRecordsOutput> listRecords({ 43 required String repo, 44 required String collection, 45 int? limit, 46 String? cursor, 47 bool? reverse, 48 }) { 49 return listRecordsHandler!.call(repo: repo, collection: collection, limit: limit, cursor: cursor, reverse: reverse); 50 } 51 52 @override 53 Future<IdentityResolveHandleOutput> resolveHandle({required String handle}) { 54 return resolveHandleHandler!.call(handle: handle); 55 } 56} 57 58void main() { 59 group('DevToolsCubit', () { 60 test('initial state is DevToolsState with initial status', () { 61 final cubit = DevToolsCubit(repository: FakeDevToolsRepository()); 62 expect(cubit.state, const DevToolsState()); 63 }); 64 65 blocTest<DevToolsCubit, DevToolsState>( 66 'clearInput resets to the initial state', 67 build: () => DevToolsCubit(repository: FakeDevToolsRepository()), 68 seed: () => const DevToolsState(status: DevToolsStatus.repoLoaded, did: 'did:plc:test'), 69 act: (cubit) => cubit.clearInput(), 70 expect: () => [const DevToolsState()], 71 ); 72 73 blocTest<DevToolsCubit, DevToolsState>( 74 'resolve handle loads repo and progressive collection counts', 75 build: () { 76 final repository = FakeDevToolsRepository( 77 resolveHandleHandler: ({required String handle}) async => 78 const IdentityResolveHandleOutput(did: 'did:plc:alice'), 79 describeRepoHandler: ({required String repo}) async => const RepoDescribeRepoOutput( 80 handle: 'alice.bsky.social', 81 did: 'did:plc:alice', 82 didDoc: {}, 83 collections: ['app.bsky.feed.post'], 84 handleIsCorrect: true, 85 ), 86 listRecordsHandler: 87 ({required String repo, required String collection, int? limit, String? cursor, bool? reverse}) async { 88 expect(repo, 'did:plc:alice'); 89 expect(collection, 'app.bsky.feed.post'); 90 return RepoListRecordsOutput( 91 cursor: cursor == null ? 'next' : null, 92 records: [ 93 RepoListRecordsRecord( 94 uri: AtUri('at://did:plc:alice/app.bsky.feed.post/${cursor == null ? '1' : '2'}'), 95 cid: 'cid-${cursor ?? 'first'}', 96 value: {'text': 'post ${cursor ?? 'first'}'}, 97 ), 98 ], 99 ); 100 }, 101 ); 102 103 return DevToolsCubit(repository: repository); 104 }, 105 act: (cubit) => cubit.resolve('alice.bsky.social'), 106 wait: const Duration(milliseconds: 10), 107 expect: () => [ 108 const DevToolsState(status: DevToolsStatus.loading), 109 isA<DevToolsState>() 110 .having((state) => state.status, 'status', DevToolsStatus.repoLoaded) 111 .having((state) => state.did, 'did', 'did:plc:alice') 112 .having((state) => state.repoHandle, 'repoHandle', 'alice.bsky.social') 113 .having((state) => state.collections.first.recordCount, 'initial count', isNull) 114 .having((state) => state.isCollectionCountsLoading, 'count loading', isTrue), 115 isA<DevToolsState>() 116 .having((state) => state.status, 'status', DevToolsStatus.repoLoaded) 117 .having((state) => state.collections.first.recordCount, 'resolved count', 2) 118 .having((state) => state.totalRepoRecords, 'total repo records', 2) 119 .having((state) => state.isCollectionCountsLoading, 'count loading', isFalse), 120 ], 121 ); 122 123 blocTest<DevToolsCubit, DevToolsState>( 124 'resolve AT-URI loads collection and full record JSON', 125 build: () { 126 final repository = FakeDevToolsRepository( 127 resolveHandleHandler: ({required String handle}) async => 128 const IdentityResolveHandleOutput(did: 'did:plc:alice'), 129 describeRepoHandler: ({required String repo}) async => const RepoDescribeRepoOutput( 130 handle: 'alice.bsky.social', 131 did: 'did:plc:alice', 132 didDoc: {}, 133 collections: ['app.bsky.feed.post'], 134 handleIsCorrect: true, 135 ), 136 listRecordsHandler: 137 ({required String repo, required String collection, int? limit, String? cursor, bool? reverse}) async { 138 if (limit == 100) { 139 return const RepoListRecordsOutput( 140 records: [ 141 RepoListRecordsRecord( 142 uri: AtUri('at://did:plc:alice/app.bsky.feed.post/3kz'), 143 cid: 'cid-count', 144 value: {'text': 'count me'}, 145 ), 146 ], 147 ); 148 } 149 150 return const RepoListRecordsOutput( 151 records: [ 152 RepoListRecordsRecord( 153 uri: AtUri('at://did:plc:alice/app.bsky.feed.post/3kz'), 154 cid: 'cid-list', 155 value: {'text': 'summary'}, 156 ), 157 ], 158 ); 159 }, 160 getRecordHandler: ({required String repo, required String collection, required String rkey}) async { 161 expect(repo, 'did:plc:alice'); 162 expect(collection, 'app.bsky.feed.post'); 163 expect(rkey, '3kz'); 164 return const RepoGetRecordOutput( 165 uri: AtUri('at://did:plc:alice/app.bsky.feed.post/3kz'), 166 cid: 'cid-full', 167 value: { 168 'text': 'full record', 169 'nested': {'ok': true}, 170 }, 171 ); 172 }, 173 ); 174 175 return DevToolsCubit(repository: repository); 176 }, 177 act: (cubit) => cubit.resolve('at://alice.bsky.social/app.bsky.feed.post/3kz'), 178 wait: const Duration(milliseconds: 10), 179 expect: () => [ 180 const DevToolsState(status: DevToolsStatus.loading), 181 isA<DevToolsState>() 182 .having((state) => state.status, 'status', DevToolsStatus.recordLoaded) 183 .having((state) => state.did, 'did', 'did:plc:alice') 184 .having((state) => state.selectedCollection, 'selectedCollection', 'app.bsky.feed.post') 185 .having((state) => state.records?.length, 'list records', 1) 186 .having((state) => state.selectedRecord?.cid, 'full cid', 'cid-full') 187 .having((state) => state.selectedRecord?.value['nested'], 'nested JSON', {'ok': true}), 188 isA<DevToolsState>() 189 .having((state) => state.collections.first.recordCount, 'collection count', 1) 190 .having((state) => state.totalRepoRecords, 'total repo records', 1), 191 ], 192 ); 193 194 blocTest<DevToolsCubit, DevToolsState>( 195 'loadRecord replaces list preview with getRecord response', 196 build: () { 197 final repository = FakeDevToolsRepository( 198 getRecordHandler: ({required String repo, required String collection, required String rkey}) async { 199 return const RepoGetRecordOutput( 200 uri: AtUri('at://did:plc:test/app.bsky.feed.post/123'), 201 cid: 'cid123', 202 value: { 203 'text': 'Expanded', 204 'reply': {'root': 'abc'}, 205 }, 206 ); 207 }, 208 ); 209 210 return DevToolsCubit(repository: repository); 211 }, 212 seed: () => const DevToolsState( 213 status: DevToolsStatus.collectionLoaded, 214 did: 'did:plc:test', 215 collections: [CollectionSummary('app.bsky.feed.post', recordCount: 1)], 216 selectedCollection: 'app.bsky.feed.post', 217 records: [ 218 RepoListRecordsRecord( 219 uri: AtUri('at://did:plc:test/app.bsky.feed.post/123'), 220 cid: 'cid-list', 221 value: {'text': 'Summary'}, 222 ), 223 ], 224 ), 225 act: (cubit) => cubit.loadRecord( 226 const RepoListRecordsRecord( 227 uri: AtUri('at://did:plc:test/app.bsky.feed.post/123'), 228 cid: 'cid-list', 229 value: {'text': 'Summary'}, 230 ), 231 ), 232 expect: () => [ 233 isA<DevToolsState>() 234 .having((state) => state.status, 'status', DevToolsStatus.collectionLoaded) 235 .having((state) => state.isRecordLoading, 'isRecordLoading', isTrue), 236 isA<DevToolsState>() 237 .having((state) => state.status, 'status', DevToolsStatus.recordLoaded) 238 .having((state) => state.isRecordLoading, 'isRecordLoading', isFalse) 239 .having((state) => state.selectedRecord?.cid, 'cid', 'cid123') 240 .having((state) => state.selectedRecord?.value['reply'], 'expanded value', {'root': 'abc'}), 241 ], 242 ); 243 244 blocTest<DevToolsCubit, DevToolsState>( 245 'loadCollection keeps repo view active while records load', 246 build: () { 247 final repository = FakeDevToolsRepository( 248 listRecordsHandler: 249 ({required String repo, required String collection, int? limit, String? cursor, bool? reverse}) async { 250 return const RepoListRecordsOutput( 251 records: [ 252 RepoListRecordsRecord( 253 uri: AtUri('at://did:plc:test/app.bsky.feed.post/123'), 254 cid: 'cid123', 255 value: {'text': 'Summary'}, 256 ), 257 ], 258 ); 259 }, 260 ); 261 262 return DevToolsCubit(repository: repository); 263 }, 264 seed: () => const DevToolsState( 265 status: DevToolsStatus.repoLoaded, 266 did: 'did:plc:test', 267 repoHandle: 'test.bsky.social', 268 collections: [CollectionSummary('app.bsky.feed.post', recordCount: 1)], 269 ), 270 act: (cubit) => cubit.loadCollection('app.bsky.feed.post'), 271 expect: () => [ 272 isA<DevToolsState>() 273 .having((state) => state.status, 'status', DevToolsStatus.repoLoaded) 274 .having((state) => state.isCollectionLoading, 'isCollectionLoading', isTrue), 275 isA<DevToolsState>() 276 .having((state) => state.status, 'status', DevToolsStatus.collectionLoaded) 277 .having((state) => state.isCollectionLoading, 'isCollectionLoading', isFalse) 278 .having((state) => state.selectedCollection, 'selectedCollection', 'app.bsky.feed.post') 279 .having((state) => state.records?.length, 'records', 1), 280 ], 281 ); 282 283 blocTest<DevToolsCubit, DevToolsState>( 284 'invalid AT-URI surfaces a clear error', 285 build: () => DevToolsCubit(repository: FakeDevToolsRepository()), 286 act: (cubit) => cubit.resolve('at://bad uri'), 287 expect: () => [ 288 const DevToolsState(status: DevToolsStatus.loading), 289 isA<DevToolsState>() 290 .having((state) => state.status, 'status', DevToolsStatus.error) 291 .having((state) => state.errorMessage, 'error', 'Invalid AT-URI'), 292 ], 293 ); 294 295 blocTest<DevToolsCubit, DevToolsState>( 296 'goBackToCollection clears selectedRecord', 297 build: () => DevToolsCubit(repository: FakeDevToolsRepository()), 298 seed: () => const DevToolsState( 299 status: DevToolsStatus.recordLoaded, 300 did: 'did:plc:test', 301 collections: [CollectionSummary('app.bsky.feed.post', recordCount: 1)], 302 selectedCollection: 'app.bsky.feed.post', 303 records: [ 304 RepoListRecordsRecord( 305 uri: AtUri('at://did:plc:test/app.bsky.feed.post/123'), 306 cid: 'cid', 307 value: {'text': 'Summary'}, 308 ), 309 ], 310 selectedRecord: RecordInfo(uri: 'at://did:plc:test/app.bsky.feed.post/123', value: {'text': 'Full'}), 311 ), 312 act: (cubit) => cubit.goBackToCollection(), 313 expect: () => [ 314 isA<DevToolsState>() 315 .having((state) => state.status, 'status', DevToolsStatus.collectionLoaded) 316 .having((state) => state.selectedRecord, 'selectedRecord', isNull) 317 .having((state) => state.records?.length, 'records length', 1), 318 ], 319 ); 320 321 blocTest<DevToolsCubit, DevToolsState>( 322 'goBackToRepo clears collection and record state', 323 build: () => DevToolsCubit(repository: FakeDevToolsRepository()), 324 seed: () => const DevToolsState( 325 status: DevToolsStatus.recordLoaded, 326 did: 'did:plc:test', 327 collections: [CollectionSummary('app.bsky.feed.post', recordCount: 1)], 328 selectedCollection: 'app.bsky.feed.post', 329 records: [ 330 RepoListRecordsRecord( 331 uri: AtUri('at://did:plc:test/app.bsky.feed.post/123'), 332 cid: 'cid', 333 value: {'text': 'Summary'}, 334 ), 335 ], 336 selectedRecord: RecordInfo(uri: 'at://did:plc:test/app.bsky.feed.post/123', value: {'text': 'Full'}), 337 ), 338 act: (cubit) => cubit.goBackToRepo(), 339 expect: () => [ 340 isA<DevToolsState>() 341 .having((state) => state.status, 'status', DevToolsStatus.repoLoaded) 342 .having((state) => state.selectedCollection, 'selectedCollection', isNull) 343 .having((state) => state.records, 'records', isNull) 344 .having((state) => state.selectedRecord, 'selectedRecord', isNull), 345 ], 346 ); 347 }); 348}