// Author: fruit.com.cn import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/core/data/models/watch_progress.dart'; import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; /// 进度仓库从"本地按 profile+剧+集"换成"服务端每剧一条": /// 这里钉住续看点换算、空记录、字符串数值解析、失败透传与清理。 void main() { test('should_clamp_invalid_and_completed_values_when_resolving_resume_position', () { const duration = Duration(seconds: 8); expect( clampResumePosition(const Duration(seconds: -1), duration), Duration.zero, ); expect( clampResumePosition(const Duration(seconds: 3), duration), const Duration(seconds: 3), ); expect( clampResumePosition(const Duration(seconds: 9), duration), Duration.zero, ); expect( clampResumePosition(const Duration(milliseconds: 7700), duration), Duration.zero, ); }); test('should_return_resume_point_when_server_has_progress', () async { final history = InMemoryWatchHistoryRepository( seed: const [ WatchProgress( dramaId: 'drama-a', episodeId: 'episode-3', position: Duration(seconds: 42), duration: Duration(minutes: 3), ), ], ); final point = await history.resumePointFor('drama-a'); expect(point?.episodeId, 'episode-3'); expect(point?.position, const Duration(seconds: 42)); }); test('should_return_no_resume_point_when_server_has_no_record', () async { final history = InMemoryWatchHistoryRepository(); expect(await history.resumePointFor('drama-a'), isNull); }); test('should_return_no_resume_point_when_record_has_no_episode', () async { final history = InMemoryWatchHistoryRepository( seed: const [ WatchProgress( dramaId: 'drama-a', episodeId: '', position: Duration(seconds: 5), duration: Duration(minutes: 3), ), ], ); expect(await history.resumePointFor('drama-a'), isNull); }); test('should_expose_fraction_when_duration_is_known', () { const record = WatchProgress( dramaId: 'd', episodeId: 'e', position: Duration(seconds: 30), duration: Duration(seconds: 120), ); expect(record.fraction, closeTo(0.25, 0.0001)); expect(record.hasPosition, isTrue); const unknown = WatchProgress( dramaId: 'd', episodeId: 'e', position: Duration(seconds: 30), duration: Duration.zero, ); expect(unknown.fraction, 0); }); test('should_parse_string_numbers_when_server_sends_ids_and_ms_as_strings', () { final record = WatchProgress.fromJson({ 'dramaId': '2111012753262120962', 'episodeId': '2111012753262120963', 'positionMs': '42000', 'durationMs': '180000', 'finished': false, 'playCount': '2', }); expect(record.dramaId, '2111012753262120962'); expect(record.episodeId, '2111012753262120963'); expect(record.position, const Duration(seconds: 42)); expect(record.duration, const Duration(seconds: 180)); expect(record.playCount, 2); }); test('should_throw_when_reading_progress_fails', () async { final history = InMemoryWatchHistoryRepository()..failReads = 1; expect(() => history.progressFor('drama-a'), throwsStateError); }); test('should_drop_record_when_removing_progress', () async { final history = InMemoryWatchHistoryRepository( seed: const [ WatchProgress( dramaId: 'drama-a', episodeId: 'e1', position: Duration(seconds: 5), duration: Duration(minutes: 1), ), ], ); await history.remove('drama-a'); expect(await history.progressFor('drama-a'), isNull); }); }