// Author: fruit.com.cn import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/core/data/models/auth_session.dart'; import 'package:fruitshorts/core/data/remote/api_client.dart'; import 'package:fruitshorts/core/data/remote/catalog_api.dart'; import 'package:fruitshorts/core/data/remote/library_api.dart'; import 'package:fruitshorts/core/data/remote/playback_api.dart'; import 'package:fruitshorts/core/data/remote/watch_api.dart'; /// K1 端到端联调:直连本机客户端接口服务(8081),把"首页 → 详情 → 播放票据 → /// 上报进度 → 读回续看位置 → 收藏 → 收藏列表 → 取消收藏"整条链路走一遍。 /// /// 用一个一次性邮箱注册(需要登录态的接口才会通);服务没起时自动跳过。 void main() { const baseUrl = 'http://127.0.0.1:8081'; final email = 'k1.selftest.${DateTime.now().millisecondsSinceEpoch}@example.com'; const password = 'Passw0rd!23'; AuthSession? session; final client = ApiClient( baseUrl: baseUrl, accessTokenReader: () => session?.accessToken, deviceIdReader: () async => 'k1-live-device', ); final catalog = CatalogApi(client); final playback = PlaybackApi(client); final watch = WatchApi(client); final library = LibraryApi(client); tearDownAll(client.close); Future register() async { try { final raw = await client.post( '/app/auth/register', body: { 'targetType': 'EMAIL', 'target': email, 'password': password, 'nickname': 'K1 联调', 'deviceId': 'k1-live-device', }, ); if (raw is! Map) return false; session = AuthSession.fromJson(raw); return true; } catch (_) { return false; } } test('should_complete_the_watch_flow_when_backend_is_running', () async { if (!await register()) { markTestSkipped('客户端接口服务未启动,已跳过联调测试'); return; } expect(session?.user.uid, isNotEmpty); final recentProbe = await client.get( '/app/watch/recent', query: {'pageIndex': '1', 'pageSize': '1'}, ); final favoriteProbe = await client.get( '/app/favorites', query: {'pageIndex': '1', 'pageSize': '1'}, ); if (recentProbe is! Map || favoriteProbe is! Map) { markTestSkipped('运行中的后端仍返回旧版 List,分页契约联调已跳过'); return; } // 1) 首页 → 取第一部剧 final feed = await catalog.homeFeed(); expect(feed.dramas, isNotEmpty); final card = feed.dramas.first; expect(card.id, isNotEmpty); // 2) 详情:剧集列表按集号升序,可播剧集带已签名地址 final detail = await catalog.dramaDetail(card.id); expect(detail.id, card.id); expect(detail.episodes, isNotEmpty); final playable = detail.episodes .where((episode) => episode.videoUrl.isNotEmpty) .toList(); if (playable.isEmpty) { markTestSkipped('这部演示剧没有可播剧集,已跳过播放相关断言'); return; } // 3) 播放票据:签名地址带 e/s,客户端原样使用 final ticket = await playback.ticket(playable.first.id); expect(ticket.accessible, isTrue); expect(ticket.accessType, isNotEmpty); expect(ticket.playUrl, startsWith('/files/')); expect(ticket.playUrl, contains('e=')); expect(ticket.playUrl, contains('s=')); // 4) 上报进度 → 读回续看位置 const position = Duration(seconds: 42); final saved = await watch.saveProgress( episodeId: playable.first.id, position: position, duration: Duration(seconds: ticket.durationSec), ); expect(saved?.episodeId, playable.first.id); expect(saved?.position, position); final resume = await watch.progressOfDrama(card.id); expect(resume?.episodeId, playable.first.id); expect(resume?.position, position); // 最近观看里能看到这部剧 final recent = await watch.recent(pageSize: 20); expect(recent.list.map((item) => item.dramaId), contains(card.id)); // 5) 收藏 → 出现在收藏列表 → 取消收藏 final savedState = await library.toggleFavorite(card.id); expect(savedState, isTrue); final favorites = await library.favorites(); expect(favorites.list.map((drama) => drama.id), contains(card.id)); await library.removeFavorite(card.id); final afterRemove = await library.favorites(); expect(afterRemove.list.map((drama) => drama.id), isNot(contains(card.id))); }); }