import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:patbond_flutter/analytics/session_tracker.dart'; void main() { final uuidV7 = RegExp( r'^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$', ); group('SessionTracker', () { test('冷启动生成 UUIDv7 格式的 sessionId', () { final tracker = SessionTracker(); expect(tracker.sessionId, matches(uuidV7)); }); test('短后台(≤30 分钟)resume 后沿用原 sessionId', () { var clock = DateTime.utc(2026, 9, 7, 10); final tracker = SessionTracker(now: () => clock); final original = tracker.sessionId; tracker.didChangeAppLifecycleState(AppLifecycleState.paused); clock = clock.add(const Duration(minutes: 30)); tracker.didChangeAppLifecycleState(AppLifecycleState.resumed); expect(tracker.sessionId, original); }); test('长后台(>30 分钟)resume 后生成新 sessionId', () { var clock = DateTime.utc(2026, 9, 7, 10); final tracker = SessionTracker(now: () => clock); final original = tracker.sessionId; tracker.didChangeAppLifecycleState(AppLifecycleState.paused); clock = clock.add(const Duration(minutes: 31)); tracker.didChangeAppLifecycleState(AppLifecycleState.resumed); expect(tracker.sessionId, isNot(original)); expect(tracker.sessionId, matches(uuidV7)); }); test('真实生命周期级联下退后台时刻不被 inactive 覆盖', () { // 前台恢复时状态机级联 paused → hidden → inactive → resumed, // 若 inactive 覆盖记时,31 分钟长后台会被误判为 0 分钟。 var clock = DateTime.utc(2026, 9, 7, 10); final tracker = SessionTracker(now: () => clock); final original = tracker.sessionId; tracker.didChangeAppLifecycleState(AppLifecycleState.inactive); tracker.didChangeAppLifecycleState(AppLifecycleState.hidden); tracker.didChangeAppLifecycleState(AppLifecycleState.paused); clock = clock.add(const Duration(minutes: 31)); tracker.didChangeAppLifecycleState(AppLifecycleState.hidden); tracker.didChangeAppLifecycleState(AppLifecycleState.inactive); tracker.didChangeAppLifecycleState(AppLifecycleState.resumed); expect(tracker.sessionId, isNot(original)); }); test('连续多次短后台幂等,仅超阈值才换新', () { var clock = DateTime.utc(2026, 9, 7, 10); final tracker = SessionTracker(now: () => clock); final original = tracker.sessionId; for (var i = 0; i < 3; i++) { tracker.didChangeAppLifecycleState(AppLifecycleState.paused); clock = clock.add(const Duration(minutes: 10)); tracker.didChangeAppLifecycleState(AppLifecycleState.resumed); expect(tracker.sessionId, original); } tracker.didChangeAppLifecycleState(AppLifecycleState.paused); clock = clock.add(const Duration(minutes: 35)); tracker.didChangeAppLifecycleState(AppLifecycleState.resumed); expect(tracker.sessionId, isNot(original)); }); }); }