Files
ReelShort/test/playback/watch_history_test.dart
T
Vicky d7eff4ca59 feat(client): 对接详情/播放/进度续看/收藏(K1 看剧闭环)+ 三态 UI
后端接口从 6 个扩到 13 个,全部走真数据:

- 新增 api:playback_api(/app/episodes/{id}/play)、watch_api(/app/watch/*)、
  library_api(/app/favorites*);票据从 CatalogApi 搬到 PlaybackApi
- 新增 repository:PlaybackRepository(含票据 TTL 缓存,默认 10 分钟,时钟可注入)、
  FavoriteRepository、WatchHistoryRepository 换成服务端模型(每剧一条续看记录,删掉 demoProfileId)
- PlayerLogic:起播前取票(拿不到给三态 UI,失败可重试);accessType 驱动锁定文案,
  不在前端猜;签名地址原样交给播放器(只补 host,不动 path/query);
  进度上报节流 5s → 10s,切集/退出/切后台各补一次,播完带 finished
- 续看定位:服务端记录里的那一集才 seek(其它集从头播),续看位置进详情/播放页时读一次
- FavoriteController:收藏以服务端为准,乐观更新 + 失败回滚;退出登录清空
- UI 三态:详情页(加载/失败可重试/空剧集 + 续看进度条 + 收藏按钮)、
  播放页(票据加载中/失败可重试/后端判定不可播)、我的页(续看失败可重试、收藏三态、最近观看)
- l10n 新增 8 个键(en/zh/id 三份):网络/进度/收藏失败、锁定提示、续看文案等

测试:flutter test 188 全过(存量 27 个测试文件未跑红);新增票据过期重取、进度节流、
续看定位、收藏回滚、签名 query 原样保留、真实后端端到端联调(live_watch_flow)
2026-10-16 17:39:05 +08:00

128 lines
3.8 KiB
Dart
Raw 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.
// 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);
});
}