// Author: fruit.com.cn import 'dart:async'; 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/repositories/catalog_repository.dart'; import 'package:fruitshorts/core/data/services/catalog_controller.dart'; import 'package:fruitshorts/core/config/app_config.dart'; void main() { setUp(() { AppConfig.clearRuntimeBaseUrl(); AppConfig.ready = Future.value(); }); tearDown(() { AppConfig.clearRuntimeBaseUrl(); AppConfig.ready = Future.value(); }); test( 'should_distinguish_empty_catalog_from_load_failure_when_retry_succeeds', () async { final repository = ScriptedCatalogRepository(); final controller = CatalogController(repository); expect(controller.settled, isFalse); await controller.load(); expect(controller.settled, isTrue); 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_wait_for_ready_and_use_final_endpoint_before_loading', () async { const finalBaseUrl = 'http://final.test:8081'; final ready = Completer(); AppConfig.ready = ready.future; final repository = ScriptedCatalogRepository(); final controller = CatalogController(repository); final loading = controller.load(); await Future.delayed(Duration.zero); expect(controller.loading, isTrue); expect(repository.homeCalls, 0); AppConfig.applyRuntimeBaseUrl(finalBaseUrl); ready.complete(); await loading; expect(repository.homeCalls, 1); expect(repository.requestBaseUrl, finalBaseUrl); expect(controller.loading, isFalse); expect(controller.settled, isTrue); }); } 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; int homeCalls = 0; String? requestBaseUrl; @override Future fetchHomeFeed() async { homeCalls++; requestBaseUrl = AppConfig.apiBaseUrl; if (fail) throw StateError('offline'); return feed; } @override Future fetchDiscoverFeed() async => feed; @override Future fetchDrama(String dramaId) async { detailCalls++; return _detail(dramaId); } }