8aac8c52cc
CI / flutter-gates (push) Successful in 2m41s
- home_page Feed segment 改接 CommunityController:骨架屏首载、空态 CTA、 错误横幅重试、下拉刷新(失败保留旧列表 + SnackBar)、触底游标翻页 (失败态只走显式重试,不随滚动风暴自动重打) - PostCard 三形态(单图通栏 4:3 / 多图折叠封面 +N 角标 / 纯文字 6 行)+ PostMediaGrid(列数规则 + ink 80% scrim 精算)+ LikeButton(error 族修订 Colors.red,T3-14 纯展示禁用态,ToggleSync 接线留 T3-15/16)+ FeedSkeleton (呼吸动效尊重减弱动态设置);降级作者统一「宠友」占位 - SignedNetworkImage:预签名 URL 缓存 key 剥离 X-Amz-* 签名参数, RemoteImage 全仓换用(同图不同签名命中同一缓存条目) - feed 域埋点:feed_viewed 聚合曝光(浏览段开/结算于切 Tab、切分段、 退后台、销毁;≥50% 可见 ≥500ms 段内按帖去重,postId 不上报)+ feed_load_failed(refresh/load_more 双路,会话失效不报) - integration_test/feed_live_test.dart:compose 六容器 Linux 桌面真链路 实测入口(PATBOND_FEED_LIVE=1 门控,默认跳过) - 测试 379 → 421(+42):四态/尾部三态/翻页不丢不重/降级作者/曝光结算 时机/缓存 key/事件属性全覆盖;analyze 0,format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
78 lines
2.8 KiB
Dart
78 lines
2.8 KiB
Dart
import 'package:fake_async/fake_async.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:patbond_flutter/features/community/feed_exposure.dart';
|
|
|
|
void main() {
|
|
test('曝光判定:≥50% 驻留满 500ms 记一次,段内按帖去重', () {
|
|
fakeAsync((async) {
|
|
final segment = FeedViewSegment(now: async.getClock(DateTime(2026)).now);
|
|
segment.updateVisibility('p-1', 0.8);
|
|
async.elapse(const Duration(milliseconds: 600));
|
|
expect(segment.impressionCount, 1);
|
|
|
|
// 再次可见不重复计。
|
|
segment.updateVisibility('p-1', 0.9);
|
|
async.elapse(const Duration(milliseconds: 600));
|
|
expect(segment.impressionCount, 1);
|
|
});
|
|
});
|
|
|
|
test('快速滑过(<500ms 跌破阈值)不计曝光', () {
|
|
fakeAsync((async) {
|
|
final segment = FeedViewSegment(now: async.getClock(DateTime(2026)).now);
|
|
segment.updateVisibility('p-1', 0.8);
|
|
async.elapse(const Duration(milliseconds: 300));
|
|
segment.updateVisibility('p-1', 0.2); // 滚出视口
|
|
async.elapse(const Duration(seconds: 1));
|
|
expect(segment.impressionCount, 0);
|
|
});
|
|
});
|
|
|
|
test('可见面积不足 50% 不起计时', () {
|
|
fakeAsync((async) {
|
|
final segment = FeedViewSegment(now: async.getClock(DateTime(2026)).now);
|
|
segment.updateVisibility('p-1', 0.49);
|
|
async.elapse(const Duration(seconds: 1));
|
|
expect(segment.impressionCount, 0);
|
|
});
|
|
});
|
|
|
|
test('结算:冻结计数、取消在途驻留、幂等只出一份', () {
|
|
fakeAsync((async) {
|
|
final clock = async.getClock(DateTime(2026));
|
|
final segment = FeedViewSegment(now: clock.now);
|
|
segment.updateVisibility('p-1', 1);
|
|
async.elapse(const Duration(milliseconds: 600));
|
|
segment.updateVisibility('p-2', 1); // 在途驻留,结算时未满 500ms
|
|
async.elapse(const Duration(milliseconds: 100));
|
|
segment.recordRefresh();
|
|
segment.recordLoadMore();
|
|
segment.recordLoadMore();
|
|
|
|
final summary = segment.settle();
|
|
expect(summary, isNotNull);
|
|
expect(summary!.impressionCount, 1);
|
|
expect(summary.refreshCount, 1);
|
|
expect(summary.loadMoreCount, 2);
|
|
expect(summary.durationMs, 700);
|
|
|
|
// 幂等:二次结算无第二条;结算后计数与曝光全部作废。
|
|
expect(segment.settle(), isNull);
|
|
segment.updateVisibility('p-3', 1);
|
|
async.elapse(const Duration(seconds: 1));
|
|
expect(segment.impressionCount, 1);
|
|
});
|
|
});
|
|
|
|
test('durationMs 上限截断 30 分钟(防挂机污染 H8)', () {
|
|
fakeAsync((async) {
|
|
final segment = FeedViewSegment(now: async.getClock(DateTime(2026)).now);
|
|
async.elapse(const Duration(minutes: 45));
|
|
expect(
|
|
segment.settle()!.durationMs,
|
|
const Duration(minutes: 30).inMilliseconds,
|
|
);
|
|
});
|
|
});
|
|
}
|