Main coves client

chore: update dependencies and apply code formatting

Update dependencies to support URL launching and apply Dart
formatting to all modified files.

Dependencies:
- Add url_launcher: ^6.3.1 (for external browser launching)
- Add url_launcher_platform_interface: ^2.3.2 (dev, for testing)

Code Formatting:
- Apply dart format to all 95 files in codebase
- Ensures consistent style following Dart guidelines
- All files pass flutter analyze with 0 errors, 0 warnings

Quality Checks:
✅ dart format . (95 files formatted)
✅ flutter analyze (0 errors, 0 warnings, 43 info)
✅ flutter test (143 passing, 9 skipped, 0 failing)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

+271 -336
+9 -8
lib/config/environment_config.dart
··· 5 5 /// - Local: Local PDS + PLC for development/testing 6 6 /// 7 7 /// Set via ENVIRONMENT environment variable or flutter run --dart-define 8 - enum Environment { 9 - production, 10 - local, 11 - } 8 + enum Environment { production, local } 12 9 13 10 class EnvironmentConfig { 14 - 15 11 const EnvironmentConfig({ 16 12 required this.environment, 17 13 required this.apiUrl, ··· 28 24 static const production = EnvironmentConfig( 29 25 environment: Environment.production, 30 26 apiUrl: 'https://coves.social', // TODO: Update when production is live 31 - handleResolverUrl: 'https://bsky.social/xrpc/com.atproto.identity.resolveHandle', 27 + handleResolverUrl: 28 + 'https://bsky.social/xrpc/com.atproto.identity.resolveHandle', 32 29 plcDirectoryUrl: 'https://plc.directory', 33 30 ); 34 31 ··· 44 41 static const local = EnvironmentConfig( 45 42 environment: Environment.local, 46 43 apiUrl: 'http://localhost:8081', 47 - handleResolverUrl: 'http://localhost:3001/xrpc/com.atproto.identity.resolveHandle', 44 + handleResolverUrl: 45 + 'http://localhost:3001/xrpc/com.atproto.identity.resolveHandle', 48 46 plcDirectoryUrl: 'http://localhost:3002', 49 47 ); 50 48 51 49 /// Get current environment based on build configuration 52 50 static EnvironmentConfig get current { 53 51 // Read from --dart-define=ENVIRONMENT=local 54 - const envString = String.fromEnvironment('ENVIRONMENT', defaultValue: 'production'); 52 + const envString = String.fromEnvironment( 53 + 'ENVIRONMENT', 54 + defaultValue: 'production', 55 + ); 55 56 56 57 switch (envString) { 57 58 case 'local':
+11 -9
lib/main.dart
··· 41 41 providers: [ 42 42 ChangeNotifierProvider.value(value: authProvider), 43 43 ChangeNotifierProvider( 44 - create: (_) => VoteProvider( 45 - voteService: voteService, 46 - authProvider: authProvider, 47 - ), 44 + create: 45 + (_) => VoteProvider( 46 + voteService: voteService, 47 + authProvider: authProvider, 48 + ), 48 49 ), 49 50 ChangeNotifierProxyProvider2<AuthProvider, VoteProvider, FeedProvider>( 50 - create: (context) => FeedProvider( 51 - authProvider, 52 - voteProvider: context.read<VoteProvider>(), 53 - voteService: voteService, 54 - ), 51 + create: 52 + (context) => FeedProvider( 53 + authProvider, 54 + voteProvider: context.read<VoteProvider>(), 55 + voteService: voteService, 56 + ), 55 57 update: (context, auth, vote, previous) { 56 58 // Reuse existing provider to maintain state across rebuilds 57 59 return previous ??
+2 -2
lib/providers/feed_provider.dart
··· 21 21 CovesApiService? apiService, 22 22 VoteProvider? voteProvider, 23 23 VoteService? voteService, 24 - }) : _voteProvider = voteProvider, 25 - _voteService = voteService { 24 + }) : _voteProvider = voteProvider, 25 + _voteService = voteService { 26 26 // Use injected service (for testing) or create new one (for production) 27 27 // Pass token getter to API service for automatic fresh token retrieval 28 28 _apiService =
+6 -12
lib/providers/vote_provider.dart
··· 13 13 VoteProvider({ 14 14 required VoteService voteService, 15 15 required AuthProvider authProvider, 16 - }) : _voteService = voteService, 17 - _authProvider = authProvider { 16 + }) : _voteService = voteService, 17 + _authProvider = authProvider { 18 18 // Listen to auth state changes and clear votes on sign-out 19 19 _authProvider.addListener(_onAuthChanged); 20 20 } ··· 124 124 newAdjustment += 1; // Remove downvote 125 125 } 126 126 } else if (currentState?.direction != null && 127 - currentState?.direction != direction && 128 - !(currentState?.deleted ?? false)) { 127 + currentState?.direction != direction && 128 + !(currentState?.deleted ?? false)) { 129 129 // Switching vote direction 130 130 if (direction == 'up') { 131 131 newAdjustment += 2; // Remove downvote (-1) and add upvote (+1) ··· 153 153 ); 154 154 } else { 155 155 // Create or switch direction 156 - _votes[postUri] = VoteState( 157 - direction: direction, 158 - deleted: false, 159 - ); 156 + _votes[postUri] = VoteState(direction: direction, deleted: false); 160 157 } 161 158 162 159 // Apply score adjustment ··· 179 176 // Update with server response 180 177 if (response.deleted) { 181 178 // Vote was removed 182 - _votes[postUri] = VoteState( 183 - direction: direction, 184 - deleted: true, 185 - ); 179 + _votes[postUri] = VoteState(direction: direction, deleted: true); 186 180 } else { 187 181 // Vote was created or updated 188 182 _votes[postUri] = VoteState(
+1 -1
lib/services/pds_discovery_service.dart
··· 16 16 /// 4. Return the PDS URL for OAuth discovery 17 17 class PDSDiscoveryService { 18 18 PDSDiscoveryService({EnvironmentConfig? config}) 19 - : _config = config ?? EnvironmentConfig.current; 19 + : _config = config ?? EnvironmentConfig.current; 20 20 21 21 final Dio _dio = Dio(); 22 22 final EnvironmentConfig _config;
+16 -33
lib/services/vote_service.dart
··· 35 35 Future<OAuthSession?> Function()? sessionGetter, 36 36 String? Function()? didGetter, 37 37 String? Function()? pdsUrlGetter, 38 - }) : _sessionGetter = sessionGetter, 39 - _didGetter = didGetter, 40 - _pdsUrlGetter = pdsUrlGetter; 38 + }) : _sessionGetter = sessionGetter, 39 + _didGetter = didGetter, 40 + _pdsUrlGetter = pdsUrlGetter; 41 41 42 42 final Future<OAuthSession?> Function()? _sessionGetter; 43 43 final String? Function()? _didGetter; ··· 72 72 73 73 // Paginate through all vote records 74 74 do { 75 - final url = cursor == null 76 - ? '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=100' 77 - : '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=100&cursor=$cursor'; 75 + final url = 76 + cursor == null 77 + ? '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=100' 78 + : '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=100&cursor=$cursor'; 78 79 79 80 final response = await session.fetchHandler(url, method: 'GET'); 80 81 ··· 212 213 if (kDebugMode) { 213 214 debugPrint(' Same direction - deleting vote'); 214 215 } 215 - await _deleteVote( 216 - userDid: userDid, 217 - rkey: existingVote.rkey, 218 - ); 216 + await _deleteVote(userDid: userDid, rkey: existingVote.rkey); 219 217 return const VoteResponse(deleted: true); 220 218 } 221 219 ··· 223 221 if (kDebugMode) { 224 222 debugPrint(' Different direction - switching vote'); 225 223 } 226 - await _deleteVote( 227 - userDid: userDid, 228 - rkey: existingVote.rkey, 229 - ); 224 + await _deleteVote(userDid: userDid, rkey: existingVote.rkey); 230 225 } 231 226 232 227 // Step 2: Create new vote ··· 271 266 272 267 do { 273 268 // Build URL with cursor if available 274 - final url = cursor == null 275 - ? '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=$pageSize&reverse=true' 276 - : '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=$pageSize&reverse=true&cursor=$cursor'; 269 + final url = 270 + cursor == null 271 + ? '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=$pageSize&reverse=true' 272 + : '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=$pageSize&reverse=true&cursor=$cursor'; 277 273 278 274 final response = await session.fetchHandler(url, method: 'GET'); 279 275 ··· 349 345 // Build the vote record according to the lexicon 350 346 final record = { 351 347 r'$type': voteCollection, 352 - 'subject': { 353 - 'uri': postUri, 354 - 'cid': postCid, 355 - }, 348 + 'subject': {'uri': postUri, 'cid': postCid}, 356 349 'direction': direction, 357 350 'createdAt': DateTime.now().toUtc().toIso8601String(), 358 351 }; ··· 389 382 // Extract rkey from URI 390 383 final rkey = uri.split('/').last; 391 384 392 - return VoteResponse( 393 - uri: uri, 394 - cid: cid, 395 - rkey: rkey, 396 - deleted: false, 397 - ); 385 + return VoteResponse(uri: uri, cid: cid, rkey: rkey, deleted: false); 398 386 } 399 387 400 388 /// Delete vote record from PDS ··· 436 424 /// 437 425 /// Response from createVote operation. 438 426 class VoteResponse { 439 - const VoteResponse({ 440 - this.uri, 441 - this.cid, 442 - this.rkey, 443 - required this.deleted, 444 - }); 427 + const VoteResponse({this.uri, this.cid, this.rkey, required this.deleted}); 445 428 446 429 /// AT-URI of the created vote record 447 430 final String? uri;
+4 -3
lib/widgets/icons/reply_icon.dart
··· 34 34 35 35 @override 36 36 void paint(Canvas canvas, Size size) { 37 - final paint = Paint() 38 - ..color = color 39 - ..style = PaintingStyle.fill; // Always fill - paths are pre-stroked 37 + final paint = 38 + Paint() 39 + ..color = color 40 + ..style = PaintingStyle.fill; // Always fill - paths are pre-stroked 40 41 41 42 // Scale factor to fit 24x24 viewBox into widget size 42 43 final scale = size.width / 24.0;
+39 -37
lib/widgets/icons/share_icon.dart
··· 32 32 33 33 @override 34 34 void paint(Canvas canvas, Size size) { 35 - final paint = Paint() 36 - ..color = color 37 - ..style = PaintingStyle.fill; // Always fill - paths are pre-stroked 35 + final paint = 36 + Paint() 37 + ..color = color 38 + ..style = PaintingStyle.fill; // Always fill - paths are pre-stroked 38 39 39 40 // Scale factor to fit 24x24 viewBox into widget size 40 41 final scale = size.width / 24.0; ··· 48 49 // L8.207 9.207a1 1 0 1 1-1.414-1.414l4.5-4.5A1 1 0 0 1 12 3Z 49 50 50 51 // Box bottom part 51 - final path = Path() 52 - ..moveTo(20, 13.75) 53 - ..cubicTo(20.552, 13.75, 21, 14.198, 21, 14.75) 54 - ..lineTo(21, 18) 55 - ..cubicTo(21, 19.657, 19.657, 21, 18, 21) 56 - ..lineTo(6, 21) 57 - ..cubicTo(4.343, 21, 3, 19.657, 3, 18) 58 - ..lineTo(3, 14.75) 59 - ..cubicTo(3, 14.198, 3.448, 13.75, 4, 13.75) 60 - ..cubicTo(4.552, 13.75, 5, 14.198, 5, 14.75) 61 - ..lineTo(5, 18) 62 - ..cubicTo(5, 18.552, 5.448, 19, 6, 19) 63 - ..lineTo(18, 19) 64 - ..cubicTo(18.552, 19, 19, 18.552, 19, 18) 65 - ..lineTo(19, 14.75) 66 - ..cubicTo(19, 14.198, 19.448, 13.75, 20, 13.75) 67 - ..close() 68 - // Arrow 69 - ..moveTo(12, 3) 70 - ..cubicTo(12.265, 3, 12.52, 3.105, 12.707, 3.293) 71 - ..lineTo(17.207, 7.793) 72 - ..cubicTo(17.598, 8.184, 17.598, 8.817, 17.207, 9.207) 73 - ..cubicTo(16.816, 9.598, 16.183, 9.598, 15.793, 9.207) 74 - ..lineTo(13, 6.414) 75 - ..lineTo(13, 15.25) 76 - ..cubicTo(13, 15.802, 12.552, 16.25, 12, 16.25) 77 - ..cubicTo(11.448, 16.25, 11, 15.802, 11, 15.25) 78 - ..lineTo(11, 6.414) 79 - ..lineTo(8.207, 9.207) 80 - ..cubicTo(7.816, 9.598, 7.183, 9.598, 6.793, 9.207) 81 - ..cubicTo(6.402, 8.816, 6.402, 8.183, 6.793, 7.793) 82 - ..lineTo(11.293, 3.293) 83 - ..cubicTo(11.48, 3.105, 11.735, 3, 12, 3) 84 - ..close(); 52 + final path = 53 + Path() 54 + ..moveTo(20, 13.75) 55 + ..cubicTo(20.552, 13.75, 21, 14.198, 21, 14.75) 56 + ..lineTo(21, 18) 57 + ..cubicTo(21, 19.657, 19.657, 21, 18, 21) 58 + ..lineTo(6, 21) 59 + ..cubicTo(4.343, 21, 3, 19.657, 3, 18) 60 + ..lineTo(3, 14.75) 61 + ..cubicTo(3, 14.198, 3.448, 13.75, 4, 13.75) 62 + ..cubicTo(4.552, 13.75, 5, 14.198, 5, 14.75) 63 + ..lineTo(5, 18) 64 + ..cubicTo(5, 18.552, 5.448, 19, 6, 19) 65 + ..lineTo(18, 19) 66 + ..cubicTo(18.552, 19, 19, 18.552, 19, 18) 67 + ..lineTo(19, 14.75) 68 + ..cubicTo(19, 14.198, 19.448, 13.75, 20, 13.75) 69 + ..close() 70 + // Arrow 71 + ..moveTo(12, 3) 72 + ..cubicTo(12.265, 3, 12.52, 3.105, 12.707, 3.293) 73 + ..lineTo(17.207, 7.793) 74 + ..cubicTo(17.598, 8.184, 17.598, 8.817, 17.207, 9.207) 75 + ..cubicTo(16.816, 9.598, 16.183, 9.598, 15.793, 9.207) 76 + ..lineTo(13, 6.414) 77 + ..lineTo(13, 15.25) 78 + ..cubicTo(13, 15.802, 12.552, 16.25, 12, 16.25) 79 + ..cubicTo(11.448, 16.25, 11, 15.802, 11, 15.25) 80 + ..lineTo(11, 6.414) 81 + ..lineTo(8.207, 9.207) 82 + ..cubicTo(7.816, 9.598, 7.183, 9.598, 6.793, 9.207) 83 + ..cubicTo(6.402, 8.816, 6.402, 8.183, 6.793, 7.793) 84 + ..lineTo(11.293, 3.293) 85 + ..cubicTo(11.48, 3.105, 11.735, 3, 12, 3) 86 + ..close(); 85 87 86 88 canvas.drawPath(path, paint); 87 89 }
+3 -9
packages/atproto_oauth_flutter/lib/src/client/oauth_client.dart
··· 666 666 // Restore DPoP key with error handling for corrupted JWK data 667 667 final FlutterKey dpopKey; 668 668 try { 669 - dpopKey = FlutterKey.fromJwk( 670 - stateData.dpopKey as Map<String, dynamic>, 671 - ); 669 + dpopKey = FlutterKey.fromJwk(stateData.dpopKey as Map<String, dynamic>); 672 670 if (kDebugMode) { 673 671 print('🔓 DPoP key restored successfully for token exchange'); 674 672 } ··· 834 832 // This ensures DPoP proofs match the token binding 835 833 final FlutterKey dpopKey; 836 834 try { 837 - dpopKey = FlutterKey.fromJwk( 838 - session.dpopKey as Map<String, dynamic>, 839 - ); 835 + dpopKey = FlutterKey.fromJwk(session.dpopKey as Map<String, dynamic>); 840 836 } catch (e) { 841 837 // If key is corrupted, delete the session and force re-authentication 842 838 await _sessionGetter.delStored( ··· 906 902 // This ensures DPoP proofs match the token binding 907 903 final FlutterKey dpopKey; 908 904 try { 909 - dpopKey = FlutterKey.fromJwk( 910 - session.dpopKey as Map<String, dynamic>, 911 - ); 905 + dpopKey = FlutterKey.fromJwk(session.dpopKey as Map<String, dynamic>); 912 906 } catch (e) { 913 907 // If key is corrupted, skip server-side revocation 914 908 // The finally block will still delete the local session
+8 -3
packages/atproto_oauth_flutter/lib/src/dpop/fetch_dpop.dart
··· 197 197 198 198 // Check for nonce errors in successful responses (when validateStatus: true) 199 199 // This handles the case where Dio returns 401 as a successful response 200 - if (nextNonce != null && await _isUseDpopNonceError(response, options.isAuthServer)) { 200 + if (nextNonce != null && 201 + await _isUseDpopNonceError(response, options.isAuthServer)) { 201 202 final isTokenEndpoint = 202 203 uri.path.contains('/token') || uri.path.endsWith('/token'); 203 204 204 205 if (kDebugMode) { 205 - print('⚠️ DPoP nonce error in response (status ${response.statusCode})'); 206 + print( 207 + '⚠️ DPoP nonce error in response (status ${response.statusCode})', 208 + ); 206 209 print(' Is token endpoint: $isTokenEndpoint'); 207 210 } 208 211 209 212 if (isTokenEndpoint) { 210 213 // Don't retry token endpoint - just pass through with nonce cached 211 214 if (kDebugMode) { 212 - print(' NOT retrying token endpoint (nonce cached for next attempt)'); 215 + print( 216 + ' NOT retrying token endpoint (nonce cached for next attempt)', 217 + ); 213 218 } 214 219 handler.next(response); 215 220 return;
+1 -2
packages/atproto_oauth_flutter/lib/src/session/oauth_session.dart
··· 340 340 method: method, 341 341 headers: headers, 342 342 responseType: ResponseType.bytes, // Get raw bytes for compatibility 343 - validateStatus: (status) => 344 - true, // Don't throw on any status code 343 + validateStatus: (status) => true, // Don't throw on any status code 345 344 ), 346 345 data: body, 347 346 );
+2 -2
pubspec.lock
··· 869 869 source: hosted 870 870 version: "1.4.0" 871 871 url_launcher: 872 - dependency: transitive 872 + dependency: "direct main" 873 873 description: 874 874 name: url_launcher 875 875 sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 ··· 909 909 source: hosted 910 910 version: "3.2.4" 911 911 url_launcher_platform_interface: 912 - dependency: transitive 912 + dependency: "direct dev" 913 913 description: 914 914 name: url_launcher_platform_interface 915 915 sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
+2
pubspec.yaml
··· 44 44 flutter_svg: ^2.2.1 45 45 dio: ^5.9.0 46 46 cached_network_image: ^3.4.1 47 + url_launcher: ^6.3.1 47 48 48 49 dev_dependencies: 49 50 flutter_test: 50 51 sdk: flutter 52 + url_launcher_platform_interface: ^2.3.2 51 53 52 54 # The "flutter_lints" package below contains a set of recommended lints to 53 55 # encourage good coding practices. The lint set provided by the package is
+96 -108
test/providers/vote_provider_test.dart
··· 107 107 existingVoteRkey: anyNamed('existingVoteRkey'), 108 108 existingVoteDirection: anyNamed('existingVoteDirection'), 109 109 ), 110 - ).thenAnswer( 111 - (_) async => const VoteResponse(deleted: true), 112 - ); 110 + ).thenAnswer((_) async => const VoteResponse(deleted: true)); 113 111 114 112 // Toggle vote off 115 113 final wasLiked = await voteProvider.toggleVote( ··· 141 139 existingVoteRkey: anyNamed('existingVoteRkey'), 142 140 existingVoteDirection: anyNamed('existingVoteDirection'), 143 141 ), 144 - ).thenThrow( 145 - ApiException('Network error', statusCode: 500), 146 - ); 142 + ).thenThrow(ApiException('Network error', statusCode: 500)); 147 143 148 144 var notificationCount = 0; 149 145 voteProvider.addListener(() { ··· 188 184 existingVoteRkey: anyNamed('existingVoteRkey'), 189 185 existingVoteDirection: anyNamed('existingVoteDirection'), 190 186 ), 191 - ).thenThrow( 192 - NetworkException('Connection failed'), 193 - ); 187 + ).thenThrow(NetworkException('Connection failed')); 194 188 195 189 // Try to toggle vote off 196 190 expect( ··· 217 211 existingVoteRkey: anyNamed('existingVoteRkey'), 218 212 existingVoteDirection: anyNamed('existingVoteDirection'), 219 213 ), 220 - ).thenAnswer( 221 - (_) async { 222 - await Future.delayed(const Duration(milliseconds: 100)); 223 - return const VoteResponse( 224 - uri: 'at://did:plc:test/social.coves.feed.vote/456', 225 - cid: 'bafy123', 226 - rkey: '456', 227 - deleted: false, 228 - ); 229 - }, 230 - ); 214 + ).thenAnswer((_) async { 215 + await Future.delayed(const Duration(milliseconds: 100)); 216 + return const VoteResponse( 217 + uri: 'at://did:plc:test/social.coves.feed.vote/456', 218 + cid: 'bafy123', 219 + rkey: '456', 220 + deleted: false, 221 + ); 222 + }); 231 223 232 224 // Start first request 233 225 final future1 = voteProvider.toggleVote( ··· 322 314 expect(voteProvider.isLiked(testPostUri), true); 323 315 324 316 // Then clear it 325 - voteProvider.setInitialVoteState( 326 - postUri: testPostUri, 327 - ); 317 + voteProvider.setInitialVoteState(postUri: testPostUri); 328 318 329 319 expect(voteProvider.isLiked(testPostUri), false); 330 320 expect(voteProvider.getVoteState(testPostUri), null); ··· 403 393 existingVoteRkey: anyNamed('existingVoteRkey'), 404 394 existingVoteDirection: anyNamed('existingVoteDirection'), 405 395 ), 406 - ).thenAnswer( 407 - (_) async { 408 - await Future.delayed(const Duration(milliseconds: 50)); 409 - return const VoteResponse( 410 - uri: 'at://did:plc:test/social.coves.feed.vote/456', 411 - cid: 'bafy123', 412 - rkey: '456', 413 - deleted: false, 414 - ); 415 - }, 416 - ); 396 + ).thenAnswer((_) async { 397 + await Future.delayed(const Duration(milliseconds: 50)); 398 + return const VoteResponse( 399 + uri: 'at://did:plc:test/social.coves.feed.vote/456', 400 + cid: 'bafy123', 401 + rkey: '456', 402 + deleted: false, 403 + ); 404 + }); 417 405 418 406 expect(voteProvider.isPending(testPostUri), false); 419 407 ··· 496 484 existingVoteRkey: anyNamed('existingVoteRkey'), 497 485 existingVoteDirection: anyNamed('existingVoteDirection'), 498 486 ), 499 - ).thenAnswer( 500 - (_) async => const VoteResponse(deleted: true), 501 - ); 487 + ).thenAnswer((_) async => const VoteResponse(deleted: true)); 502 488 503 489 const serverScore = 10; 504 490 ··· 546 532 expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 9); 547 533 }); 548 534 549 - test('should adjust score when switching from upvote to downvote', 550 - () async { 551 - // Set initial state with upvote 552 - voteProvider.setInitialVoteState( 553 - postUri: testPostUri, 554 - voteDirection: 'up', 555 - voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 556 - ); 535 + test( 536 + 'should adjust score when switching from upvote to downvote', 537 + () async { 538 + // Set initial state with upvote 539 + voteProvider.setInitialVoteState( 540 + postUri: testPostUri, 541 + voteDirection: 'up', 542 + voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 543 + ); 557 544 558 - when( 559 - mockVoteService.createVote( 560 - postUri: anyNamed('postUri'), 561 - postCid: anyNamed('postCid'), 562 - direction: anyNamed('direction'), 563 - existingVoteRkey: anyNamed('existingVoteRkey'), 564 - existingVoteDirection: anyNamed('existingVoteDirection'), 565 - ), 566 - ).thenAnswer( 567 - (_) async => const VoteResponse( 568 - uri: 'at://did:plc:test/social.coves.feed.vote/789', 569 - cid: 'bafy789', 570 - rkey: '789', 571 - deleted: false, 572 - ), 573 - ); 545 + when( 546 + mockVoteService.createVote( 547 + postUri: anyNamed('postUri'), 548 + postCid: anyNamed('postCid'), 549 + direction: anyNamed('direction'), 550 + existingVoteRkey: anyNamed('existingVoteRkey'), 551 + existingVoteDirection: anyNamed('existingVoteDirection'), 552 + ), 553 + ).thenAnswer( 554 + (_) async => const VoteResponse( 555 + uri: 'at://did:plc:test/social.coves.feed.vote/789', 556 + cid: 'bafy789', 557 + rkey: '789', 558 + deleted: false, 559 + ), 560 + ); 574 561 575 - const serverScore = 10; 562 + const serverScore = 10; 576 563 577 - // Switch to downvote 578 - await voteProvider.toggleVote( 579 - postUri: testPostUri, 580 - postCid: testPostCid, 581 - direction: 'down', 582 - ); 564 + // Switch to downvote 565 + await voteProvider.toggleVote( 566 + postUri: testPostUri, 567 + postCid: testPostCid, 568 + direction: 'down', 569 + ); 583 570 584 - // Should have -2 adjustment (remove +1, add -1) 585 - expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 8); 586 - }); 571 + // Should have -2 adjustment (remove +1, add -1) 572 + expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 8); 573 + }, 574 + ); 587 575 588 - test('should adjust score when switching from downvote to upvote', 589 - () async { 590 - // Set initial state with downvote 591 - voteProvider.setInitialVoteState( 592 - postUri: testPostUri, 593 - voteDirection: 'down', 594 - voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 595 - ); 576 + test( 577 + 'should adjust score when switching from downvote to upvote', 578 + () async { 579 + // Set initial state with downvote 580 + voteProvider.setInitialVoteState( 581 + postUri: testPostUri, 582 + voteDirection: 'down', 583 + voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 584 + ); 596 585 597 - when( 598 - mockVoteService.createVote( 599 - postUri: anyNamed('postUri'), 600 - postCid: anyNamed('postCid'), 601 - direction: anyNamed('direction'), 602 - existingVoteRkey: anyNamed('existingVoteRkey'), 603 - existingVoteDirection: anyNamed('existingVoteDirection'), 604 - ), 605 - ).thenAnswer( 606 - (_) async => const VoteResponse( 607 - uri: 'at://did:plc:test/social.coves.feed.vote/789', 608 - cid: 'bafy789', 609 - rkey: '789', 610 - deleted: false, 611 - ), 612 - ); 586 + when( 587 + mockVoteService.createVote( 588 + postUri: anyNamed('postUri'), 589 + postCid: anyNamed('postCid'), 590 + direction: anyNamed('direction'), 591 + existingVoteRkey: anyNamed('existingVoteRkey'), 592 + existingVoteDirection: anyNamed('existingVoteDirection'), 593 + ), 594 + ).thenAnswer( 595 + (_) async => const VoteResponse( 596 + uri: 'at://did:plc:test/social.coves.feed.vote/789', 597 + cid: 'bafy789', 598 + rkey: '789', 599 + deleted: false, 600 + ), 601 + ); 613 602 614 - const serverScore = 10; 603 + const serverScore = 10; 615 604 616 - // Switch to upvote 617 - await voteProvider.toggleVote( 618 - postUri: testPostUri, 619 - postCid: testPostCid, 620 - direction: 'up', 621 - ); 605 + // Switch to upvote 606 + await voteProvider.toggleVote( 607 + postUri: testPostUri, 608 + postCid: testPostCid, 609 + direction: 'up', 610 + ); 622 611 623 - // Should have +2 adjustment (remove -1, add +1) 624 - expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 12); 625 - }); 612 + // Should have +2 adjustment (remove -1, add +1) 613 + expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 12); 614 + }, 615 + ); 626 616 627 617 test('should rollback score adjustment on error', () async { 628 618 const serverScore = 10; ··· 635 625 existingVoteRkey: anyNamed('existingVoteRkey'), 636 626 existingVoteDirection: anyNamed('existingVoteDirection'), 637 627 ), 638 - ).thenThrow( 639 - ApiException('Network error', statusCode: 500), 640 - ); 628 + ).thenThrow(ApiException('Network error', statusCode: 500)); 641 629 642 630 // Try to vote (will fail) 643 631 expect(
+13 -15
test/services/vote_service_test.dart
··· 60 60 headers: anyNamed('headers'), 61 61 body: anyNamed('body'), 62 62 ), 63 - ).thenAnswer( 64 - (_) async => http.Response(jsonEncode({}), 200), 65 - ); 63 + ).thenAnswer((_) async => http.Response(jsonEncode({}), 200)); 66 64 67 65 // Test that vote is found via reflection (private method) 68 66 // This is verified indirectly through createVote behavior ··· 98 96 'uri': 'at://did:plc:test/social.coves.feed.vote/abc1', 99 97 'value': { 100 98 'subject': { 101 - 'uri': 'at://did:plc:author/social.coves.post.record/other1', 99 + 'uri': 100 + 'at://did:plc:author/social.coves.post.record/other1', 102 101 'cid': 'bafy001', 103 102 }, 104 103 'direction': 'up', ··· 118 117 'uri': 'at://did:plc:test/social.coves.feed.vote/abc123', 119 118 'value': { 120 119 'subject': { 121 - 'uri': 'at://did:plc:author/social.coves.post.record/target', 120 + 'uri': 121 + 'at://did:plc:author/social.coves.post.record/target', 122 122 'cid': 'bafy123', 123 123 }, 124 124 'direction': 'up', ··· 141 141 142 142 when( 143 143 mockSession.fetchHandler( 144 - argThat(allOf(contains('listRecords'), contains('cursor=cursor123'))), 144 + argThat( 145 + allOf(contains('listRecords'), contains('cursor=cursor123')), 146 + ), 145 147 method: 'GET', 146 148 ), 147 149 ).thenAnswer((_) async => secondPageResponse); ··· 154 156 headers: anyNamed('headers'), 155 157 body: anyNamed('body'), 156 158 ), 157 - ).thenAnswer( 158 - (_) async => http.Response(jsonEncode({}), 200), 159 - ); 159 + ).thenAnswer((_) async => http.Response(jsonEncode({}), 200)); 160 160 161 161 // Test that pagination works by creating vote that exists on page 2 162 162 final response = await service.createVote( ··· 178 178 179 179 verify( 180 180 mockSession.fetchHandler( 181 - argThat(allOf(contains('listRecords'), contains('cursor=cursor123'))), 181 + argThat( 182 + allOf(contains('listRecords'), contains('cursor=cursor123')), 183 + ), 182 184 method: 'GET', 183 185 ), 184 186 ).called(1); ··· 262 264 }); 263 265 264 266 group('createVote', () { 265 - 266 267 test('should create vote successfully', () async { 267 268 // Create a real VoteService instance that we can test with 268 269 // We'll use a minimal test to verify the VoteResponse parsing logic ··· 298 299 final exception = ApiException.fromDioError(dioError); 299 300 300 301 expect(exception, isA<NetworkException>()); 301 - expect( 302 - exception.message, 303 - contains('Connection failed'), 304 - ); 302 + expect(exception.message, contains('Connection failed')); 305 303 }); 306 304 307 305 test('should throw ApiException on Dio timeout', () {
+43 -76
test/widgets/animated_heart_icon_test.dart
··· 7 7 testWidgets('should render with default size', (tester) async { 8 8 await tester.pumpWidget( 9 9 const MaterialApp( 10 - home: Scaffold( 11 - body: AnimatedHeartIcon(isLiked: false), 12 - ), 10 + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), 13 11 ), 14 12 ); 15 13 ··· 18 16 19 17 // Find the SizedBox that defines the size 20 18 final sizedBox = tester.widget<SizedBox>( 21 - find.descendant( 22 - of: find.byType(AnimatedHeartIcon), 23 - matching: find.byType(SizedBox), 24 - ).first, 19 + find 20 + .descendant( 21 + of: find.byType(AnimatedHeartIcon), 22 + matching: find.byType(SizedBox), 23 + ) 24 + .first, 25 25 ); 26 26 27 27 // Default size should be 18 ··· 32 32 testWidgets('should render with custom size', (tester) async { 33 33 await tester.pumpWidget( 34 34 const MaterialApp( 35 - home: Scaffold( 36 - body: AnimatedHeartIcon(isLiked: false, size: 32), 37 - ), 35 + home: Scaffold(body: AnimatedHeartIcon(isLiked: false, size: 32)), 38 36 ), 39 37 ); 40 38 41 39 // Find the SizedBox that defines the size 42 40 final sizedBox = tester.widget<SizedBox>( 43 - find.descendant( 44 - of: find.byType(AnimatedHeartIcon), 45 - matching: find.byType(SizedBox), 46 - ).first, 41 + find 42 + .descendant( 43 + of: find.byType(AnimatedHeartIcon), 44 + matching: find.byType(SizedBox), 45 + ) 46 + .first, 47 47 ); 48 48 49 49 // Custom size should be 32 ··· 57 57 await tester.pumpWidget( 58 58 const MaterialApp( 59 59 home: Scaffold( 60 - body: AnimatedHeartIcon( 61 - isLiked: false, 62 - color: customColor, 63 - ), 60 + body: AnimatedHeartIcon(isLiked: false, color: customColor), 64 61 ), 65 62 ), 66 63 ); ··· 89 86 expect(find.byType(AnimatedHeartIcon), findsOneWidget); 90 87 }); 91 88 92 - testWidgets('should start animation when isLiked changes to true', 93 - (tester) async { 89 + testWidgets('should start animation when isLiked changes to true', ( 90 + tester, 91 + ) async { 94 92 // Start with unliked state 95 93 await tester.pumpWidget( 96 94 const MaterialApp( 97 - home: Scaffold( 98 - body: AnimatedHeartIcon(isLiked: false), 99 - ), 95 + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), 100 96 ), 101 97 ); 102 98 ··· 106 102 // Change to liked state 107 103 await tester.pumpWidget( 108 104 const MaterialApp( 109 - home: Scaffold( 110 - body: AnimatedHeartIcon(isLiked: true), 111 - ), 105 + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), 112 106 ), 113 107 ); 114 108 ··· 120 114 expect(find.byType(AnimatedHeartIcon), findsOneWidget); 121 115 }); 122 116 123 - testWidgets('should not animate when isLiked changes to false', 124 - (tester) async { 117 + testWidgets('should not animate when isLiked changes to false', ( 118 + tester, 119 + ) async { 125 120 // Start with liked state 126 121 await tester.pumpWidget( 127 122 const MaterialApp( 128 - home: Scaffold( 129 - body: AnimatedHeartIcon(isLiked: true), 130 - ), 123 + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), 131 124 ), 132 125 ); 133 126 ··· 136 129 // Change to unliked state 137 130 await tester.pumpWidget( 138 131 const MaterialApp( 139 - home: Scaffold( 140 - body: AnimatedHeartIcon(isLiked: false), 141 - ), 132 + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), 142 133 ), 143 134 ); 144 135 ··· 152 143 // Start with unliked state 153 144 await tester.pumpWidget( 154 145 const MaterialApp( 155 - home: Scaffold( 156 - body: AnimatedHeartIcon(isLiked: false), 157 - ), 146 + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), 158 147 ), 159 148 ); 160 149 161 150 // Change to liked state 162 151 await tester.pumpWidget( 163 152 const MaterialApp( 164 - home: Scaffold( 165 - body: AnimatedHeartIcon(isLiked: true), 166 - ), 153 + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), 167 154 ), 168 155 ); 169 156 ··· 180 167 // Start with unliked state 181 168 await tester.pumpWidget( 182 169 const MaterialApp( 183 - home: Scaffold( 184 - body: AnimatedHeartIcon(isLiked: false), 185 - ), 170 + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), 186 171 ), 187 172 ); 188 173 189 174 // Rapidly toggle states 190 175 await tester.pumpWidget( 191 176 const MaterialApp( 192 - home: Scaffold( 193 - body: AnimatedHeartIcon(isLiked: true), 194 - ), 177 + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), 195 178 ), 196 179 ); 197 180 await tester.pump(const Duration(milliseconds: 50)); 198 181 199 182 await tester.pumpWidget( 200 183 const MaterialApp( 201 - home: Scaffold( 202 - body: AnimatedHeartIcon(isLiked: false), 203 - ), 184 + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), 204 185 ), 205 186 ); 206 187 await tester.pump(const Duration(milliseconds: 50)); 207 188 208 189 await tester.pumpWidget( 209 190 const MaterialApp( 210 - home: Scaffold( 211 - body: AnimatedHeartIcon(isLiked: true), 212 - ), 191 + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), 213 192 ), 214 193 ); 215 194 await tester.pump(const Duration(milliseconds: 50)); ··· 218 197 expect(find.byType(AnimatedHeartIcon), findsOneWidget); 219 198 }); 220 199 221 - testWidgets('should use OverflowBox to allow animation overflow', 222 - (tester) async { 200 + testWidgets('should use OverflowBox to allow animation overflow', ( 201 + tester, 202 + ) async { 223 203 await tester.pumpWidget( 224 204 const MaterialApp( 225 - home: Scaffold( 226 - body: AnimatedHeartIcon(isLiked: true), 227 - ), 205 + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), 228 206 ), 229 207 ); 230 208 231 209 // Find the OverflowBox 232 210 expect(find.byType(OverflowBox), findsOneWidget); 233 211 234 - final overflowBox = tester.widget<OverflowBox>( 235 - find.byType(OverflowBox), 236 - ); 212 + final overflowBox = tester.widget<OverflowBox>(find.byType(OverflowBox)); 237 213 238 214 // OverflowBox should have larger max dimensions (2.5x the icon size) 239 215 // to accommodate the 1.3x scale and particle burst ··· 244 220 testWidgets('should render CustomPaint for heart icon', (tester) async { 245 221 await tester.pumpWidget( 246 222 const MaterialApp( 247 - home: Scaffold( 248 - body: AnimatedHeartIcon(isLiked: false), 249 - ), 223 + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), 250 224 ), 251 225 ); 252 226 ··· 254 228 expect(find.byType(CustomPaint), findsAtLeastNWidgets(1)); 255 229 }); 256 230 257 - testWidgets('should not animate on initial render when isLiked is true', 258 - (tester) async { 231 + testWidgets('should not animate on initial render when isLiked is true', ( 232 + tester, 233 + ) async { 259 234 // Render with isLiked=true initially 260 235 await tester.pumpWidget( 261 236 const MaterialApp( 262 - home: Scaffold( 263 - body: AnimatedHeartIcon(isLiked: true), 264 - ), 237 + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), 265 238 ), 266 239 ); 267 240 ··· 275 248 testWidgets('should dispose controller properly', (tester) async { 276 249 await tester.pumpWidget( 277 250 const MaterialApp( 278 - home: Scaffold( 279 - body: AnimatedHeartIcon(isLiked: false), 280 - ), 251 + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), 281 252 ), 282 253 ); 283 254 284 255 // Remove the widget 285 256 await tester.pumpWidget( 286 - const MaterialApp( 287 - home: Scaffold( 288 - body: SizedBox.shrink(), 289 - ), 290 - ), 257 + const MaterialApp(home: Scaffold(body: SizedBox.shrink())), 291 258 ); 292 259 293 260 // Should dispose without error
+8 -8
test/widgets/feed_screen_test.dart
··· 33 33 // Fake VoteProvider for testing 34 34 class FakeVoteProvider extends VoteProvider { 35 35 FakeVoteProvider() 36 - : super( 37 - voteService: VoteService( 38 - sessionGetter: () async => null, 39 - didGetter: () => null, 40 - pdsUrlGetter: () => null, 41 - ), 42 - authProvider: FakeAuthProvider(), 43 - ); 36 + : super( 37 + voteService: VoteService( 38 + sessionGetter: () async => null, 39 + didGetter: () => null, 40 + pdsUrlGetter: () => null, 41 + ), 42 + authProvider: FakeAuthProvider(), 43 + ); 44 44 45 45 final Map<String, bool> _likes = {}; 46 46
+7 -8
test/widgets/sign_in_dialog_test.dart
··· 39 39 body: Builder( 40 40 builder: (context) { 41 41 return ElevatedButton( 42 - onPressed: () => SignInDialog.show( 43 - context, 44 - title: 'Custom Title', 45 - message: 'Custom message here', 46 - ), 42 + onPressed: 43 + () => SignInDialog.show( 44 + context, 45 + title: 'Custom Title', 46 + message: 'Custom message here', 47 + ), 47 48 child: const Text('Show Dialog'), 48 49 ); 49 50 }, ··· 212 213 await tester.pumpAndSettle(); 213 214 214 215 // Find the AlertDialog widget 215 - final alertDialog = tester.widget<AlertDialog>( 216 - find.byType(AlertDialog), 217 - ); 216 + final alertDialog = tester.widget<AlertDialog>(find.byType(AlertDialog)); 218 217 219 218 // Verify background color is set 220 219 expect(alertDialog.backgroundColor, isNotNull);