import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/core/data/models/drama.dart'; import 'package:fruitshorts/core/data/models/home_feed.dart'; import 'package:fruitshorts/core/data/models/play_ticket.dart'; import 'package:fruitshorts/core/data/repositories/catalog_repository.dart'; import 'package:fruitshorts/core/data/services/catalog_controller.dart'; void main() { test('should_distinguish_empty_catalog_from_load_failure_when_retry_succeeds', () async { final repository = ScriptedCatalogRepository(); final controller = CatalogController(repository); await controller.load(); expect(controller.dramas, isEmpty); expect(controller.error, isNull); repository.fail = true; await controller.load(); expect(controller.error, isNotNull); repository.fail = false; repository.feed = HomeFeed( sections: [ FeedSection(code: 'hot', title: '热门推荐', dramas: [_card('1')]), ], ); await controller.load(); expect(controller.error, isNull); expect(controller.dramas, hasLength(1)); expect(controller.sections.single.title, '热门推荐'); }); test('should_request_drama_detail_once_when_it_is_loaded_on_demand', () async { final repository = ScriptedCatalogRepository(); final controller = CatalogController(repository); repository.feed = HomeFeed( sections: [ FeedSection(code: 'hot', title: '热门推荐', dramas: [_card('1')]), ], ); await controller.load(); final card = controller.dramas.single; expect(card.episodes, isEmpty); final detail = await controller.requireDetail(card); expect(detail.episodes, hasLength(2)); expect(repository.detailCalls, 1); // 第二次已经是详情,直接复用,不再请求 await controller.requireDetail(detail); expect(repository.detailCalls, 1); expect(controller.dramas.single.episodes, hasLength(2)); }); test('should_write_play_url_for_free_episode_and_keep_locked_episode_empty_when_ticket_is_issued', () async { final repository = ScriptedCatalogRepository(); final controller = CatalogController(repository); repository.tickets['10'] = const PlayTicket( episodeId: '10', accessible: true, accessType: PlayTicket.free, playUrl: '/files/episode/10.mp4', ); const free = Episode(id: '10', number: 1, title: '免费集', isFree: true); final resolved = await controller.resolvePlay(free); expect(resolved.videoUrl, '/files/episode/10.mp4'); const locked = Episode( id: '11', number: 2, title: '锁定集', isFree: false, locked: true, ); final lockedResolved = await controller.resolvePlay(locked); expect(lockedResolved.videoUrl, isEmpty); }); } Drama _card(String id) => Drama( id: id, title: 'Drama $id', synopsis: '简介 $id', posterUrl: '/files/$id.jpg', ); Drama _detail(String id) => Drama( id: id, title: 'Drama $id', synopsis: '简介 $id', posterUrl: '/files/$id.jpg', detailLoaded: true, episodes: const [ Episode(id: '1', number: 1, title: '第一集', isFree: true), Episode(id: '2', number: 2, title: '第二集', isFree: false), ], ); class ScriptedCatalogRepository implements CatalogRepository { bool fail = false; HomeFeed feed = const HomeFeed(); int detailCalls = 0; final Map tickets = {}; @override Future fetchHomeFeed() async { if (fail) throw StateError('offline'); return feed; } @override Future fetchDiscoverFeed() async => feed; @override Future fetchDrama(String dramaId) async { detailCalls++; return _detail(dramaId); } @override Future resolvePlay(String episodeId) async { return tickets[episodeId] ?? PlayTicket( episodeId: episodeId, accessible: false, accessType: PlayTicket.noMedia, ); } }