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, ); }); }); }