import 'package:flutter_test/flutter_test.dart'; import 'package:patbond_flutter/core/network/api_exception.dart'; import 'package:patbond_flutter/features/community/community_interaction_analytics.dart'; void main() { late List<(String, Map?)> events; late CommunityInteractionAnalytics analytics; setUp(() { events = []; analytics = CommunityInteractionAnalytics( (name, [props]) async => events.add((name, props)), ); }); List names() => events.map((e) => e.$1).toList(); List?> props() => events.map((e) => e.$2).toList(); test('互动四事件:source 逐字段(22 号白名单键集)', () { analytics.postLiked(source: InteractionSource.feed); analytics.postUnliked(source: InteractionSource.postDetail); analytics.postFavorited(source: InteractionSource.postDetail); analytics.postUnfavorited(source: InteractionSource.feed); expect(names(), [ 'post_liked', 'post_unliked', 'post_favorited', 'post_unfavorited', ]); expect(props(), [ {'source': 'feed'}, {'source': 'post_detail'}, {'source': 'post_detail'}, {'source': 'feed'}, ]); }); test('comment_create_succeeded:durationMs/isReply/textLengthBucket', () { analytics.commentCreateSucceeded( durationMs: 4200, isReply: false, textLength: 12, ); expect(names(), ['comment_create_succeeded']); expect(props().single, { 'durationMs': 4200, 'isReply': false, 'textLengthBucket': 'short', }); }); test('comment_create_failed:httpStatus 由五位业务码推导,网络错误缺席', () { analytics.commentCreateFailed( reason: CommentCreateFailureReason.validationError, attemptSeq: 2, errorCode: 40000, ); analytics.commentCreateFailed( reason: CommentCreateFailureReason.networkError, attemptSeq: 3, ); expect(names(), ['comment_create_failed', 'comment_create_failed']); expect(props(), [ { 'failureReason': 'validation_error', 'attemptSeq': 2, 'errorCode': 40000, 'httpStatus': 400, }, {'failureReason': 'network_error', 'attemptSeq': 3}, ]); }); test('关注对:user_followed / user_unfollowed', () { analytics.userFollowed(source: InteractionSource.postDetail); analytics.userUnfollowed(source: InteractionSource.postDetail); expect(names(), ['user_followed', 'user_unfollowed']); expect(props(), [ {'source': 'post_detail'}, {'source': 'post_detail'}, ]); }); test('textLengthBucketOf 分桶边界(06 §1.3 红线:不报精确字数)', () { expect(textLengthBucketOf(0), 'empty'); expect(textLengthBucketOf(1), 'short'); expect(textLengthBucketOf(50), 'short'); expect(textLengthBucketOf(51), 'medium'); expect(textLengthBucketOf(500), 'medium'); expect(textLengthBucketOf(501), 'long'); }); test('失败原因映射:网络归并口径 + 防枚举族归 not_found + 会话失效 null', () { expect( commentCreateFailureReasonOf(const ApiNetworkException()), CommentCreateFailureReason.networkError, ); expect( commentCreateFailureReasonOf(const ApiRateLimitException()), CommentCreateFailureReason.rateLimited, ); expect( commentCreateFailureReasonOf( const ApiBusinessException(code: ApiCodes.paramError, message: 'x'), ), CommentCreateFailureReason.validationError, ); expect( commentCreateFailureReasonOf( const ApiBusinessException(code: ApiCodes.postNotFound, message: 'x'), ), CommentCreateFailureReason.notFound, ); expect( commentCreateFailureReasonOf( const ApiBusinessException(code: 50000, message: 'x'), ), CommentCreateFailureReason.serverError, ); expect( commentCreateFailureReasonOf(const SessionExpiredException()), isNull, ); }); }