diff --git a/lib/config/environment_config.dart b/lib/config/environment_config.dart --- a/lib/config/environment_config.dart +++ b/lib/config/environment_config.dart @@ -5,13 +5,9 @@ /// - Production: Real Bluesky infrastructure /// - Local: Local PDS + PLC for development/testing /// /// Set via ENVIRONMENT environment variable or flutter run --dart-define -enum Environment { - production, - local, -} +enum Environment { production, local } class EnvironmentConfig { - const EnvironmentConfig({ required this.environment, required this.apiUrl, @@ -28,7 +24,8 @@ /// Uses real Bluesky infrastructure static const production = EnvironmentConfig( environment: Environment.production, apiUrl: 'https://coves.social', // TODO: Update when production is live - handleResolverUrl: 'https://bsky.social/xrpc/com.atproto.identity.resolveHandle', + handleResolverUrl: + 'https://bsky.social/xrpc/com.atproto.identity.resolveHandle', plcDirectoryUrl: 'https://plc.directory', ); @@ -44,14 +41,18 @@ /// Note: For physical devices not connected via USB, use ngrok URLs instead static const local = EnvironmentConfig( environment: Environment.local, apiUrl: 'http://localhost:8081', - handleResolverUrl: 'http://localhost:3001/xrpc/com.atproto.identity.resolveHandle', + handleResolverUrl: + 'http://localhost:3001/xrpc/com.atproto.identity.resolveHandle', plcDirectoryUrl: 'http://localhost:3002', ); /// Get current environment based on build configuration static EnvironmentConfig get current { // Read from --dart-define=ENVIRONMENT=local - const envString = String.fromEnvironment('ENVIRONMENT', defaultValue: 'production'); + const envString = String.fromEnvironment( + 'ENVIRONMENT', + defaultValue: 'production', + ); switch (envString) { case 'local': diff --git a/lib/main.dart b/lib/main.dart --- a/lib/main.dart +++ b/lib/main.dart @@ -41,17 +41,19 @@ MultiProvider( providers: [ ChangeNotifierProvider.value(value: authProvider), ChangeNotifierProvider( - create: (_) => VoteProvider( - voteService: voteService, - authProvider: authProvider, - ), + create: + (_) => VoteProvider( + voteService: voteService, + authProvider: authProvider, + ), ), ChangeNotifierProxyProvider2( - create: (context) => FeedProvider( - authProvider, - voteProvider: context.read(), - voteService: voteService, - ), + create: + (context) => FeedProvider( + authProvider, + voteProvider: context.read(), + voteService: voteService, + ), update: (context, auth, vote, previous) { // Reuse existing provider to maintain state across rebuilds return previous ?? diff --git a/lib/providers/feed_provider.dart b/lib/providers/feed_provider.dart --- a/lib/providers/feed_provider.dart +++ b/lib/providers/feed_provider.dart @@ -21,8 +21,8 @@ this._authProvider, { CovesApiService? apiService, VoteProvider? voteProvider, VoteService? voteService, - }) : _voteProvider = voteProvider, - _voteService = voteService { + }) : _voteProvider = voteProvider, + _voteService = voteService { // Use injected service (for testing) or create new one (for production) // Pass token getter to API service for automatic fresh token retrieval _apiService = diff --git a/lib/providers/vote_provider.dart b/lib/providers/vote_provider.dart --- a/lib/providers/vote_provider.dart +++ b/lib/providers/vote_provider.dart @@ -13,8 +13,8 @@ class VoteProvider with ChangeNotifier { VoteProvider({ required VoteService voteService, required AuthProvider authProvider, - }) : _voteService = voteService, - _authProvider = authProvider { + }) : _voteService = voteService, + _authProvider = authProvider { // Listen to auth state changes and clear votes on sign-out _authProvider.addListener(_onAuthChanged); } @@ -124,8 +124,8 @@ } else { newAdjustment += 1; // Remove downvote } } else if (currentState?.direction != null && - currentState?.direction != direction && - !(currentState?.deleted ?? false)) { + currentState?.direction != direction && + !(currentState?.deleted ?? false)) { // Switching vote direction if (direction == 'up') { newAdjustment += 2; // Remove downvote (-1) and add upvote (+1) @@ -153,10 +153,7 @@ deleted: true, ); } else { // Create or switch direction - _votes[postUri] = VoteState( - direction: direction, - deleted: false, - ); + _votes[postUri] = VoteState(direction: direction, deleted: false); } // Apply score adjustment @@ -179,10 +176,7 @@ // Update with server response if (response.deleted) { // Vote was removed - _votes[postUri] = VoteState( - direction: direction, - deleted: true, - ); + _votes[postUri] = VoteState(direction: direction, deleted: true); } else { // Vote was created or updated _votes[postUri] = VoteState( diff --git a/lib/services/pds_discovery_service.dart b/lib/services/pds_discovery_service.dart --- a/lib/services/pds_discovery_service.dart +++ b/lib/services/pds_discovery_service.dart @@ -16,7 +16,7 @@ /// 3. Extract the PDS endpoint from the service array /// 4. Return the PDS URL for OAuth discovery class PDSDiscoveryService { PDSDiscoveryService({EnvironmentConfig? config}) - : _config = config ?? EnvironmentConfig.current; + : _config = config ?? EnvironmentConfig.current; final Dio _dio = Dio(); final EnvironmentConfig _config; diff --git a/lib/services/vote_service.dart b/lib/services/vote_service.dart --- a/lib/services/vote_service.dart +++ b/lib/services/vote_service.dart @@ -35,9 +35,9 @@ VoteService({ Future Function()? sessionGetter, String? Function()? didGetter, String? Function()? pdsUrlGetter, - }) : _sessionGetter = sessionGetter, - _didGetter = didGetter, - _pdsUrlGetter = pdsUrlGetter; + }) : _sessionGetter = sessionGetter, + _didGetter = didGetter, + _pdsUrlGetter = pdsUrlGetter; final Future Function()? _sessionGetter; final String? Function()? _didGetter; @@ -72,9 +72,10 @@ String? cursor; // Paginate through all vote records do { - final url = cursor == null - ? '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=100' - : '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=100&cursor=$cursor'; + final url = + cursor == null + ? '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=100' + : '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=100&cursor=$cursor'; final response = await session.fetchHandler(url, method: 'GET'); @@ -212,10 +213,7 @@ if (existingVote.direction == direction) { if (kDebugMode) { debugPrint(' Same direction - deleting vote'); } - await _deleteVote( - userDid: userDid, - rkey: existingVote.rkey, - ); + await _deleteVote(userDid: userDid, rkey: existingVote.rkey); return const VoteResponse(deleted: true); } @@ -223,10 +221,7 @@ // Different direction - delete old vote first if (kDebugMode) { debugPrint(' Different direction - switching vote'); } - await _deleteVote( - userDid: userDid, - rkey: existingVote.rkey, - ); + await _deleteVote(userDid: userDid, rkey: existingVote.rkey); } // Step 2: Create new vote @@ -271,9 +266,10 @@ const pageSize = 100; do { // Build URL with cursor if available - final url = cursor == null - ? '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=$pageSize&reverse=true' - : '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=$pageSize&reverse=true&cursor=$cursor'; + final url = + cursor == null + ? '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=$pageSize&reverse=true' + : '/xrpc/com.atproto.repo.listRecords?repo=$userDid&collection=$voteCollection&limit=$pageSize&reverse=true&cursor=$cursor'; final response = await session.fetchHandler(url, method: 'GET'); @@ -349,10 +345,7 @@ // Build the vote record according to the lexicon final record = { r'$type': voteCollection, - 'subject': { - 'uri': postUri, - 'cid': postCid, - }, + 'subject': {'uri': postUri, 'cid': postCid}, 'direction': direction, 'createdAt': DateTime.now().toUtc().toIso8601String(), }; @@ -389,12 +382,7 @@ // Extract rkey from URI final rkey = uri.split('/').last; - return VoteResponse( - uri: uri, - cid: cid, - rkey: rkey, - deleted: false, - ); + return VoteResponse(uri: uri, cid: cid, rkey: rkey, deleted: false); } /// Delete vote record from PDS @@ -436,12 +424,7 @@ /// Vote Response /// /// Response from createVote operation. class VoteResponse { - const VoteResponse({ - this.uri, - this.cid, - this.rkey, - required this.deleted, - }); + const VoteResponse({this.uri, this.cid, this.rkey, required this.deleted}); /// AT-URI of the created vote record final String? uri; diff --git a/lib/widgets/icons/reply_icon.dart b/lib/widgets/icons/reply_icon.dart --- a/lib/widgets/icons/reply_icon.dart +++ b/lib/widgets/icons/reply_icon.dart @@ -34,9 +34,10 @@ final bool filled; @override void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..style = PaintingStyle.fill; // Always fill - paths are pre-stroked + final paint = + Paint() + ..color = color + ..style = PaintingStyle.fill; // Always fill - paths are pre-stroked // Scale factor to fit 24x24 viewBox into widget size final scale = size.width / 24.0; diff --git a/lib/widgets/icons/share_icon.dart b/lib/widgets/icons/share_icon.dart --- a/lib/widgets/icons/share_icon.dart +++ b/lib/widgets/icons/share_icon.dart @@ -32,9 +32,10 @@ final Color color; @override void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..style = PaintingStyle.fill; // Always fill - paths are pre-stroked + final paint = + Paint() + ..color = color + ..style = PaintingStyle.fill; // Always fill - paths are pre-stroked // Scale factor to fit 24x24 viewBox into widget size final scale = size.width / 24.0; @@ -48,40 +49,41 @@ // a1 1 0 1 1-1.414 1.414L13 6.414v8.836a1 1 0 1 1-2 0V6.414 // L8.207 9.207a1 1 0 1 1-1.414-1.414l4.5-4.5A1 1 0 0 1 12 3Z // Box bottom part - final path = Path() - ..moveTo(20, 13.75) - ..cubicTo(20.552, 13.75, 21, 14.198, 21, 14.75) - ..lineTo(21, 18) - ..cubicTo(21, 19.657, 19.657, 21, 18, 21) - ..lineTo(6, 21) - ..cubicTo(4.343, 21, 3, 19.657, 3, 18) - ..lineTo(3, 14.75) - ..cubicTo(3, 14.198, 3.448, 13.75, 4, 13.75) - ..cubicTo(4.552, 13.75, 5, 14.198, 5, 14.75) - ..lineTo(5, 18) - ..cubicTo(5, 18.552, 5.448, 19, 6, 19) - ..lineTo(18, 19) - ..cubicTo(18.552, 19, 19, 18.552, 19, 18) - ..lineTo(19, 14.75) - ..cubicTo(19, 14.198, 19.448, 13.75, 20, 13.75) - ..close() - // Arrow - ..moveTo(12, 3) - ..cubicTo(12.265, 3, 12.52, 3.105, 12.707, 3.293) - ..lineTo(17.207, 7.793) - ..cubicTo(17.598, 8.184, 17.598, 8.817, 17.207, 9.207) - ..cubicTo(16.816, 9.598, 16.183, 9.598, 15.793, 9.207) - ..lineTo(13, 6.414) - ..lineTo(13, 15.25) - ..cubicTo(13, 15.802, 12.552, 16.25, 12, 16.25) - ..cubicTo(11.448, 16.25, 11, 15.802, 11, 15.25) - ..lineTo(11, 6.414) - ..lineTo(8.207, 9.207) - ..cubicTo(7.816, 9.598, 7.183, 9.598, 6.793, 9.207) - ..cubicTo(6.402, 8.816, 6.402, 8.183, 6.793, 7.793) - ..lineTo(11.293, 3.293) - ..cubicTo(11.48, 3.105, 11.735, 3, 12, 3) - ..close(); + final path = + Path() + ..moveTo(20, 13.75) + ..cubicTo(20.552, 13.75, 21, 14.198, 21, 14.75) + ..lineTo(21, 18) + ..cubicTo(21, 19.657, 19.657, 21, 18, 21) + ..lineTo(6, 21) + ..cubicTo(4.343, 21, 3, 19.657, 3, 18) + ..lineTo(3, 14.75) + ..cubicTo(3, 14.198, 3.448, 13.75, 4, 13.75) + ..cubicTo(4.552, 13.75, 5, 14.198, 5, 14.75) + ..lineTo(5, 18) + ..cubicTo(5, 18.552, 5.448, 19, 6, 19) + ..lineTo(18, 19) + ..cubicTo(18.552, 19, 19, 18.552, 19, 18) + ..lineTo(19, 14.75) + ..cubicTo(19, 14.198, 19.448, 13.75, 20, 13.75) + ..close() + // Arrow + ..moveTo(12, 3) + ..cubicTo(12.265, 3, 12.52, 3.105, 12.707, 3.293) + ..lineTo(17.207, 7.793) + ..cubicTo(17.598, 8.184, 17.598, 8.817, 17.207, 9.207) + ..cubicTo(16.816, 9.598, 16.183, 9.598, 15.793, 9.207) + ..lineTo(13, 6.414) + ..lineTo(13, 15.25) + ..cubicTo(13, 15.802, 12.552, 16.25, 12, 16.25) + ..cubicTo(11.448, 16.25, 11, 15.802, 11, 15.25) + ..lineTo(11, 6.414) + ..lineTo(8.207, 9.207) + ..cubicTo(7.816, 9.598, 7.183, 9.598, 6.793, 9.207) + ..cubicTo(6.402, 8.816, 6.402, 8.183, 6.793, 7.793) + ..lineTo(11.293, 3.293) + ..cubicTo(11.48, 3.105, 11.735, 3, 12, 3) + ..close(); canvas.drawPath(path, paint); } diff --git a/packages/atproto_oauth_flutter/lib/src/client/oauth_client.dart b/packages/atproto_oauth_flutter/lib/src/client/oauth_client.dart --- a/packages/atproto_oauth_flutter/lib/src/client/oauth_client.dart +++ b/packages/atproto_oauth_flutter/lib/src/client/oauth_client.dart @@ -666,9 +666,7 @@ // Restore dpopKey from stored private JWK // Restore DPoP key with error handling for corrupted JWK data final FlutterKey dpopKey; try { - dpopKey = FlutterKey.fromJwk( - stateData.dpopKey as Map, - ); + dpopKey = FlutterKey.fromJwk(stateData.dpopKey as Map); if (kDebugMode) { print('🔓 DPoP key restored successfully for token exchange'); } @@ -834,9 +832,7 @@ // CRITICAL FIX: Use the stored key instead of generating a new one // This ensures DPoP proofs match the token binding final FlutterKey dpopKey; try { - dpopKey = FlutterKey.fromJwk( - session.dpopKey as Map, - ); + dpopKey = FlutterKey.fromJwk(session.dpopKey as Map); } catch (e) { // If key is corrupted, delete the session and force re-authentication await _sessionGetter.delStored( @@ -906,9 +902,7 @@ // CRITICAL FIX: Use the stored key instead of generating a new one // This ensures DPoP proofs match the token binding final FlutterKey dpopKey; try { - dpopKey = FlutterKey.fromJwk( - session.dpopKey as Map, - ); + dpopKey = FlutterKey.fromJwk(session.dpopKey as Map); } catch (e) { // If key is corrupted, skip server-side revocation // The finally block will still delete the local session diff --git a/packages/atproto_oauth_flutter/lib/src/dpop/fetch_dpop.dart b/packages/atproto_oauth_flutter/lib/src/dpop/fetch_dpop.dart --- a/packages/atproto_oauth_flutter/lib/src/dpop/fetch_dpop.dart +++ b/packages/atproto_oauth_flutter/lib/src/dpop/fetch_dpop.dart @@ -197,19 +197,24 @@ } // Check for nonce errors in successful responses (when validateStatus: true) // This handles the case where Dio returns 401 as a successful response - if (nextNonce != null && await _isUseDpopNonceError(response, options.isAuthServer)) { + if (nextNonce != null && + await _isUseDpopNonceError(response, options.isAuthServer)) { final isTokenEndpoint = uri.path.contains('/token') || uri.path.endsWith('/token'); if (kDebugMode) { - print('⚠️ DPoP nonce error in response (status ${response.statusCode})'); + print( + '⚠️ DPoP nonce error in response (status ${response.statusCode})', + ); print(' Is token endpoint: $isTokenEndpoint'); } if (isTokenEndpoint) { // Don't retry token endpoint - just pass through with nonce cached if (kDebugMode) { - print(' NOT retrying token endpoint (nonce cached for next attempt)'); + print( + ' NOT retrying token endpoint (nonce cached for next attempt)', + ); } handler.next(response); return; diff --git a/packages/atproto_oauth_flutter/lib/src/session/oauth_session.dart b/packages/atproto_oauth_flutter/lib/src/session/oauth_session.dart --- a/packages/atproto_oauth_flutter/lib/src/session/oauth_session.dart +++ b/packages/atproto_oauth_flutter/lib/src/session/oauth_session.dart @@ -340,8 +340,7 @@ options: Options( method: method, headers: headers, responseType: ResponseType.bytes, // Get raw bytes for compatibility - validateStatus: (status) => - true, // Don't throw on any status code + validateStatus: (status) => true, // Don't throw on any status code ), data: body, ); diff --git a/pubspec.lock b/pubspec.lock --- a/pubspec.lock +++ b/pubspec.lock @@ -869,7 +869,7 @@ url: "https://pub.dev" source: hosted version: "1.4.0" url_launcher: - dependency: transitive + dependency: "direct main" description: name: url_launcher sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 @@ -909,7 +909,7 @@ url: "https://pub.dev" source: hosted version: "3.2.4" url_launcher_platform_interface: - dependency: transitive + dependency: "direct dev" description: name: url_launcher_platform_interface sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" diff --git a/pubspec.yaml b/pubspec.yaml --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,10 +44,12 @@ provider: ^6.1.5+1 flutter_svg: ^2.2.1 dio: ^5.9.0 cached_network_image: ^3.4.1 + url_launcher: ^6.3.1 dev_dependencies: flutter_test: sdk: flutter + url_launcher_platform_interface: ^2.3.2 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is diff --git a/test/providers/vote_provider_test.dart b/test/providers/vote_provider_test.dart --- a/test/providers/vote_provider_test.dart +++ b/test/providers/vote_provider_test.dart @@ -107,9 +107,7 @@ direction: anyNamed('direction'), existingVoteRkey: anyNamed('existingVoteRkey'), existingVoteDirection: anyNamed('existingVoteDirection'), ), - ).thenAnswer( - (_) async => const VoteResponse(deleted: true), - ); + ).thenAnswer((_) async => const VoteResponse(deleted: true)); // Toggle vote off final wasLiked = await voteProvider.toggleVote( @@ -141,9 +139,7 @@ direction: anyNamed('direction'), existingVoteRkey: anyNamed('existingVoteRkey'), existingVoteDirection: anyNamed('existingVoteDirection'), ), - ).thenThrow( - ApiException('Network error', statusCode: 500), - ); + ).thenThrow(ApiException('Network error', statusCode: 500)); var notificationCount = 0; voteProvider.addListener(() { @@ -188,9 +184,7 @@ direction: anyNamed('direction'), existingVoteRkey: anyNamed('existingVoteRkey'), existingVoteDirection: anyNamed('existingVoteDirection'), ), - ).thenThrow( - NetworkException('Connection failed'), - ); + ).thenThrow(NetworkException('Connection failed')); // Try to toggle vote off expect( @@ -217,17 +211,15 @@ direction: anyNamed('direction'), existingVoteRkey: anyNamed('existingVoteRkey'), existingVoteDirection: anyNamed('existingVoteDirection'), ), - ).thenAnswer( - (_) async { - await Future.delayed(const Duration(milliseconds: 100)); - return const VoteResponse( - uri: 'at://did:plc:test/social.coves.feed.vote/456', - cid: 'bafy123', - rkey: '456', - deleted: false, - ); - }, - ); + ).thenAnswer((_) async { + await Future.delayed(const Duration(milliseconds: 100)); + return const VoteResponse( + uri: 'at://did:plc:test/social.coves.feed.vote/456', + cid: 'bafy123', + rkey: '456', + deleted: false, + ); + }); // Start first request final future1 = voteProvider.toggleVote( @@ -322,9 +314,7 @@ expect(voteProvider.isLiked(testPostUri), true); // Then clear it - voteProvider.setInitialVoteState( - postUri: testPostUri, - ); + voteProvider.setInitialVoteState(postUri: testPostUri); expect(voteProvider.isLiked(testPostUri), false); expect(voteProvider.getVoteState(testPostUri), null); @@ -403,17 +393,15 @@ direction: anyNamed('direction'), existingVoteRkey: anyNamed('existingVoteRkey'), existingVoteDirection: anyNamed('existingVoteDirection'), ), - ).thenAnswer( - (_) async { - await Future.delayed(const Duration(milliseconds: 50)); - return const VoteResponse( - uri: 'at://did:plc:test/social.coves.feed.vote/456', - cid: 'bafy123', - rkey: '456', - deleted: false, - ); - }, - ); + ).thenAnswer((_) async { + await Future.delayed(const Duration(milliseconds: 50)); + return const VoteResponse( + uri: 'at://did:plc:test/social.coves.feed.vote/456', + cid: 'bafy123', + rkey: '456', + deleted: false, + ); + }); expect(voteProvider.isPending(testPostUri), false); @@ -496,9 +484,7 @@ direction: anyNamed('direction'), existingVoteRkey: anyNamed('existingVoteRkey'), existingVoteDirection: anyNamed('existingVoteDirection'), ), - ).thenAnswer( - (_) async => const VoteResponse(deleted: true), - ); + ).thenAnswer((_) async => const VoteResponse(deleted: true)); const serverScore = 10; @@ -546,83 +532,87 @@ // Should have -1 adjustment (downvote added) expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 9); }); - test('should adjust score when switching from upvote to downvote', - () async { - // Set initial state with upvote - voteProvider.setInitialVoteState( - postUri: testPostUri, - voteDirection: 'up', - voteUri: 'at://did:plc:test/social.coves.feed.vote/456', - ); + test( + 'should adjust score when switching from upvote to downvote', + () async { + // Set initial state with upvote + voteProvider.setInitialVoteState( + postUri: testPostUri, + voteDirection: 'up', + voteUri: 'at://did:plc:test/social.coves.feed.vote/456', + ); - when( - mockVoteService.createVote( - postUri: anyNamed('postUri'), - postCid: anyNamed('postCid'), - direction: anyNamed('direction'), - existingVoteRkey: anyNamed('existingVoteRkey'), - existingVoteDirection: anyNamed('existingVoteDirection'), - ), - ).thenAnswer( - (_) async => const VoteResponse( - uri: 'at://did:plc:test/social.coves.feed.vote/789', - cid: 'bafy789', - rkey: '789', - deleted: false, - ), - ); + when( + mockVoteService.createVote( + postUri: anyNamed('postUri'), + postCid: anyNamed('postCid'), + direction: anyNamed('direction'), + existingVoteRkey: anyNamed('existingVoteRkey'), + existingVoteDirection: anyNamed('existingVoteDirection'), + ), + ).thenAnswer( + (_) async => const VoteResponse( + uri: 'at://did:plc:test/social.coves.feed.vote/789', + cid: 'bafy789', + rkey: '789', + deleted: false, + ), + ); - const serverScore = 10; + const serverScore = 10; - // Switch to downvote - await voteProvider.toggleVote( - postUri: testPostUri, - postCid: testPostCid, - direction: 'down', - ); + // Switch to downvote + await voteProvider.toggleVote( + postUri: testPostUri, + postCid: testPostCid, + direction: 'down', + ); - // Should have -2 adjustment (remove +1, add -1) - expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 8); - }); + // Should have -2 adjustment (remove +1, add -1) + expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 8); + }, + ); - test('should adjust score when switching from downvote to upvote', - () async { - // Set initial state with downvote - voteProvider.setInitialVoteState( - postUri: testPostUri, - voteDirection: 'down', - voteUri: 'at://did:plc:test/social.coves.feed.vote/456', - ); + test( + 'should adjust score when switching from downvote to upvote', + () async { + // Set initial state with downvote + voteProvider.setInitialVoteState( + postUri: testPostUri, + voteDirection: 'down', + voteUri: 'at://did:plc:test/social.coves.feed.vote/456', + ); - when( - mockVoteService.createVote( - postUri: anyNamed('postUri'), - postCid: anyNamed('postCid'), - direction: anyNamed('direction'), - existingVoteRkey: anyNamed('existingVoteRkey'), - existingVoteDirection: anyNamed('existingVoteDirection'), - ), - ).thenAnswer( - (_) async => const VoteResponse( - uri: 'at://did:plc:test/social.coves.feed.vote/789', - cid: 'bafy789', - rkey: '789', - deleted: false, - ), - ); + when( + mockVoteService.createVote( + postUri: anyNamed('postUri'), + postCid: anyNamed('postCid'), + direction: anyNamed('direction'), + existingVoteRkey: anyNamed('existingVoteRkey'), + existingVoteDirection: anyNamed('existingVoteDirection'), + ), + ).thenAnswer( + (_) async => const VoteResponse( + uri: 'at://did:plc:test/social.coves.feed.vote/789', + cid: 'bafy789', + rkey: '789', + deleted: false, + ), + ); - const serverScore = 10; + const serverScore = 10; - // Switch to upvote - await voteProvider.toggleVote( - postUri: testPostUri, - postCid: testPostCid, - direction: 'up', - ); + // Switch to upvote + await voteProvider.toggleVote( + postUri: testPostUri, + postCid: testPostCid, + direction: 'up', + ); - // Should have +2 adjustment (remove -1, add +1) - expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 12); - }); + // Should have +2 adjustment (remove -1, add +1) + expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 12); + }, + ); test('should rollback score adjustment on error', () async { const serverScore = 10; @@ -635,9 +625,7 @@ direction: anyNamed('direction'), existingVoteRkey: anyNamed('existingVoteRkey'), existingVoteDirection: anyNamed('existingVoteDirection'), ), - ).thenThrow( - ApiException('Network error', statusCode: 500), - ); + ).thenThrow(ApiException('Network error', statusCode: 500)); // Try to vote (will fail) expect( diff --git a/test/services/vote_service_test.dart b/test/services/vote_service_test.dart --- a/test/services/vote_service_test.dart +++ b/test/services/vote_service_test.dart @@ -60,9 +60,7 @@ method: 'POST', headers: anyNamed('headers'), body: anyNamed('body'), ), - ).thenAnswer( - (_) async => http.Response(jsonEncode({}), 200), - ); + ).thenAnswer((_) async => http.Response(jsonEncode({}), 200)); // Test that vote is found via reflection (private method) // This is verified indirectly through createVote behavior @@ -98,7 +96,8 @@ { 'uri': 'at://did:plc:test/social.coves.feed.vote/abc1', 'value': { 'subject': { - 'uri': 'at://did:plc:author/social.coves.post.record/other1', + 'uri': + 'at://did:plc:author/social.coves.post.record/other1', 'cid': 'bafy001', }, 'direction': 'up', @@ -118,7 +117,8 @@ { 'uri': 'at://did:plc:test/social.coves.feed.vote/abc123', 'value': { 'subject': { - 'uri': 'at://did:plc:author/social.coves.post.record/target', + 'uri': + 'at://did:plc:author/social.coves.post.record/target', 'cid': 'bafy123', }, 'direction': 'up', @@ -141,7 +141,9 @@ ).thenAnswer((_) async => firstPageResponse); when( mockSession.fetchHandler( - argThat(allOf(contains('listRecords'), contains('cursor=cursor123'))), + argThat( + allOf(contains('listRecords'), contains('cursor=cursor123')), + ), method: 'GET', ), ).thenAnswer((_) async => secondPageResponse); @@ -154,9 +156,7 @@ method: 'POST', headers: anyNamed('headers'), body: anyNamed('body'), ), - ).thenAnswer( - (_) async => http.Response(jsonEncode({}), 200), - ); + ).thenAnswer((_) async => http.Response(jsonEncode({}), 200)); // Test that pagination works by creating vote that exists on page 2 final response = await service.createVote( @@ -178,7 +178,9 @@ ).called(1); verify( mockSession.fetchHandler( - argThat(allOf(contains('listRecords'), contains('cursor=cursor123'))), + argThat( + allOf(contains('listRecords'), contains('cursor=cursor123')), + ), method: 'GET', ), ).called(1); @@ -262,7 +264,6 @@ }); }); group('createVote', () { - test('should create vote successfully', () async { // Create a real VoteService instance that we can test with // We'll use a minimal test to verify the VoteResponse parsing logic @@ -298,10 +299,7 @@ final exception = ApiException.fromDioError(dioError); expect(exception, isA()); - expect( - exception.message, - contains('Connection failed'), - ); + expect(exception.message, contains('Connection failed')); }); test('should throw ApiException on Dio timeout', () { diff --git a/test/widgets/animated_heart_icon_test.dart b/test/widgets/animated_heart_icon_test.dart --- a/test/widgets/animated_heart_icon_test.dart +++ b/test/widgets/animated_heart_icon_test.dart @@ -7,9 +7,7 @@ group('AnimatedHeartIcon', () { testWidgets('should render with default size', (tester) async { await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: false), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), ), ); @@ -18,10 +16,12 @@ expect(find.byType(AnimatedHeartIcon), findsOneWidget); // Find the SizedBox that defines the size final sizedBox = tester.widget( - find.descendant( - of: find.byType(AnimatedHeartIcon), - matching: find.byType(SizedBox), - ).first, + find + .descendant( + of: find.byType(AnimatedHeartIcon), + matching: find.byType(SizedBox), + ) + .first, ); // Default size should be 18 @@ -32,18 +32,18 @@ testWidgets('should render with custom size', (tester) async { await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: false, size: 32), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: false, size: 32)), ), ); // Find the SizedBox that defines the size final sizedBox = tester.widget( - find.descendant( - of: find.byType(AnimatedHeartIcon), - matching: find.byType(SizedBox), - ).first, + find + .descendant( + of: find.byType(AnimatedHeartIcon), + matching: find.byType(SizedBox), + ) + .first, ); // Custom size should be 32 @@ -57,10 +57,7 @@ await tester.pumpWidget( const MaterialApp( home: Scaffold( - body: AnimatedHeartIcon( - isLiked: false, - color: customColor, - ), + body: AnimatedHeartIcon(isLiked: false, color: customColor), ), ), ); @@ -89,14 +86,13 @@ // Widget should render with custom liked color expect(find.byType(AnimatedHeartIcon), findsOneWidget); }); - testWidgets('should start animation when isLiked changes to true', - (tester) async { + testWidgets('should start animation when isLiked changes to true', ( + tester, + ) async { // Start with unliked state await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: false), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), ), ); @@ -106,9 +102,7 @@ // Change to liked state await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: true), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), ), ); @@ -120,14 +114,13 @@ // Widget should still be present and animating expect(find.byType(AnimatedHeartIcon), findsOneWidget); }); - testWidgets('should not animate when isLiked changes to false', - (tester) async { + testWidgets('should not animate when isLiked changes to false', ( + tester, + ) async { // Start with liked state await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: true), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), ), ); @@ -136,9 +129,7 @@ // Change to unliked state await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: false), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), ), ); @@ -152,18 +143,14 @@ testWidgets('should complete animation after duration', (tester) async { // Start with unliked state await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: false), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), ), ); // Change to liked state await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: true), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), ), ); @@ -180,36 +167,28 @@ testWidgets('should handle rapid state changes', (tester) async { // Start with unliked state await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: false), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), ), ); // Rapidly toggle states await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: true), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), ), ); await tester.pump(const Duration(milliseconds: 50)); await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: false), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), ), ); await tester.pump(const Duration(milliseconds: 50)); await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: true), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), ), ); await tester.pump(const Duration(milliseconds: 50)); @@ -218,22 +197,19 @@ // Widget should handle rapid changes without error expect(find.byType(AnimatedHeartIcon), findsOneWidget); }); - testWidgets('should use OverflowBox to allow animation overflow', - (tester) async { + testWidgets('should use OverflowBox to allow animation overflow', ( + tester, + ) async { await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: true), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), ), ); // Find the OverflowBox expect(find.byType(OverflowBox), findsOneWidget); - final overflowBox = tester.widget( - find.byType(OverflowBox), - ); + final overflowBox = tester.widget(find.byType(OverflowBox)); // OverflowBox should have larger max dimensions (2.5x the icon size) // to accommodate the 1.3x scale and particle burst @@ -244,9 +220,7 @@ testWidgets('should render CustomPaint for heart icon', (tester) async { await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: false), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), ), ); @@ -254,14 +228,13 @@ // Find the CustomPaint widget (used for rendering the heart) expect(find.byType(CustomPaint), findsAtLeastNWidgets(1)); }); - testWidgets('should not animate on initial render when isLiked is true', - (tester) async { + testWidgets('should not animate on initial render when isLiked is true', ( + tester, + ) async { // Render with isLiked=true initially await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: true), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: true)), ), ); @@ -275,19 +248,13 @@ testWidgets('should dispose controller properly', (tester) async { await tester.pumpWidget( const MaterialApp( - home: Scaffold( - body: AnimatedHeartIcon(isLiked: false), - ), + home: Scaffold(body: AnimatedHeartIcon(isLiked: false)), ), ); // Remove the widget await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: SizedBox.shrink(), - ), - ), + const MaterialApp(home: Scaffold(body: SizedBox.shrink())), ); // Should dispose without error diff --git a/test/widgets/feed_screen_test.dart b/test/widgets/feed_screen_test.dart --- a/test/widgets/feed_screen_test.dart +++ b/test/widgets/feed_screen_test.dart @@ -33,14 +33,14 @@ // Fake VoteProvider for testing class FakeVoteProvider extends VoteProvider { FakeVoteProvider() - : super( - voteService: VoteService( - sessionGetter: () async => null, - didGetter: () => null, - pdsUrlGetter: () => null, - ), - authProvider: FakeAuthProvider(), - ); + : super( + voteService: VoteService( + sessionGetter: () async => null, + didGetter: () => null, + pdsUrlGetter: () => null, + ), + authProvider: FakeAuthProvider(), + ); final Map _likes = {}; diff --git a/test/widgets/sign_in_dialog_test.dart b/test/widgets/sign_in_dialog_test.dart --- a/test/widgets/sign_in_dialog_test.dart +++ b/test/widgets/sign_in_dialog_test.dart @@ -39,11 +39,12 @@ home: Scaffold( body: Builder( builder: (context) { return ElevatedButton( - onPressed: () => SignInDialog.show( - context, - title: 'Custom Title', - message: 'Custom message here', - ), + onPressed: + () => SignInDialog.show( + context, + title: 'Custom Title', + message: 'Custom message here', + ), child: const Text('Show Dialog'), ); }, @@ -212,9 +213,7 @@ await tester.tap(find.text('Show Dialog')); await tester.pumpAndSettle(); // Find the AlertDialog widget - final alertDialog = tester.widget( - find.byType(AlertDialog), - ); + final alertDialog = tester.widget(find.byType(AlertDialog)); // Verify background color is set expect(alertDialog.backgroundColor, isNotNull);