Files
patbond-flutter/test/features/community/community_interaction_analytics_test.dart
lixi 92524da8e2 新增:互动埋点与乐观更新视觉基建——字典 v3 互动八事件封装 + ToggleSync 成功上报与触点归因 + LikeButton 三层闪烁抑制动画(T3-16)
- community_interaction_analytics:post_liked/unliked、post_favorited/unfavorited、
  comment_create_succeeded/failed、user_followed/unfollowed 强类型封装(22 号
  白名单键集逐一对齐;textLengthBucket 分桶、httpStatus 由五位码推导、
  会话失效不上报);comment_create_started 锁死 unknown 不封装
- CommunityController:toggleLike/toggleBookmark 增 source 触点归因
 (feed / post_detail),成功响应后经注入的 interactionAnalytics 上报
 (乐观翻转与失败不报);adjustCommentCount 评论计数同源写入
 (详情副本与 Feed 卡片一并更新)
- LikeButton 升 Stateful:点按激活 240ms 弹性缩放 + 120ms 图标淡入、
  取消仅颜色渐出;回滚/对账零动画直接跳变、计数与状态成对更新、
  激活动画未播完等播完再跳(05 号 §3.5/§4);减弱动态设置降级瞬变

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-09 13:38:50 +08:00

127 lines
3.9 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<String, dynamic>?)> events;
late CommunityInteractionAnalytics analytics;
setUp(() {
events = [];
analytics = CommunityInteractionAnalytics(
(name, [props]) async => events.add((name, props)),
);
});
List<String> names() => events.map((e) => e.$1).toList();
List<Map<String, dynamic>?> 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_succeededdurationMs/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_failedhttpStatus 由五位业务码推导,网络错误缺席', () {
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,
);
});
}