// Author: fruit.com.cn import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/core/data/models/drama.dart'; import 'package:fruitshorts/core/data/models/page_result.dart'; import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; import 'package:fruitshorts/core/data/services/favorite_controller.dart'; Drama _card(String id) => Drama( id: id, title: 'Drama $id', synopsis: 'synopsis', posterUrl: '/files/$id.png', ); void main() { test('should_roll_back_when_toggle_fails', () async { final repository = InMemoryFavoriteRepository()..failToggle = true; final controller = FavoriteController(repository); final ok = await controller.toggle('signal'); expect(ok, isFalse); // 乐观更新必须回滚:不能"看起来收藏了,其实服务端没存" expect(controller.isSaved('signal'), isFalse); expect(controller.cards, isEmpty); }); test('should_keep_saved_state_when_toggle_succeeds', () async { final controller = FavoriteController(InMemoryFavoriteRepository()); final ok = await controller.toggle('signal', card: _card('signal')); expect(ok, isTrue); expect(controller.isSaved('signal'), isTrue); // 卡片随手带上:收藏完打开"我的收藏"就能看到 expect(controller.cards.map((drama) => drama.id), ['signal']); }); test('should_remove_card_and_state_when_toggled_off', () async { final repository = InMemoryFavoriteRepository(saved: {'signal'}); final controller = FavoriteController(repository); // 先按服务端状态对齐,再取消收藏 await controller.load(); await controller.toggle('signal'); expect(controller.isSaved('signal'), isFalse); expect(controller.cards, isEmpty); }); test('should_expose_error_and_keep_old_state_when_list_fails', () async { final repository = InMemoryFavoriteRepository()..failPage = true; final controller = FavoriteController(repository); await controller.load(); expect(controller.loading, isFalse); expect(controller.error, 'favoritesError'); }); test('should_load_cards_and_saved_ids_when_list_succeeds', () async { final repository = InMemoryFavoriteRepository( saved: {'signal'}, cards: [_card('signal'), _card('city')], ); final controller = FavoriteController(repository); await controller.load(); expect(controller.error, isNull); expect(controller.cards.map((drama) => drama.id), ['signal']); expect(controller.isSaved('signal'), isTrue); expect(controller.isSaved('city'), isFalse); }); test('should_roll_back_when_remove_fails', () async { final repository = InMemoryFavoriteRepository( saved: {'signal'}, cards: [_card('signal')], )..failRemove = true; final controller = FavoriteController(repository); await controller.load(); final ok = await controller.remove('signal'); expect(ok, isFalse); expect(controller.isSaved('signal'), isTrue); expect(controller.cards.map((drama) => drama.id), ['signal']); }); test( 'should_restore_removed_card_at_original_position_when_remove_fails', () async { final repository = InMemoryFavoriteRepository( saved: {'signal', 'city'}, cards: [_card('signal'), _card('city')], )..failRemove = true; final controller = FavoriteController(repository); await controller.load(); await controller.remove('signal'); expect(controller.cards.map((drama) => drama.id), ['signal', 'city']); }, ); test('should_clear_state_when_signing_out', () async { final controller = FavoriteController( InMemoryFavoriteRepository(saved: {'signal'}, cards: [_card('signal')]), ); await controller.load(); controller.clear(); expect(controller.savedIds, isEmpty); expect(controller.cards, isEmpty); }); test('should_append_unique_cards_and_stop_when_no_more_pages', () async { final repository = _PagedFavoriteRepository({ 1: PageResult(total: 3, list: [_card('a'), _card('b')]), 2: PageResult(total: 3, list: [_card('b'), _card('c')]), }); final controller = FavoriteController(repository, pageSize: 2); await controller.load(); await controller.loadMore(); await controller.loadMore(); expect(controller.cards.map((card) => card.id), ['a', 'b', 'c']); expect(controller.total, 3); expect(controller.hasMore, isFalse); expect(repository.pageRequests, [1, 2]); }); test('should_retry_next_page_when_load_more_fails', () async { final repository = _PagedFavoriteRepository({ 1: PageResult(total: 2, list: [_card('a')]), 2: PageResult(total: 2, list: [_card('b')]), })..failPages.add(2); final controller = FavoriteController(repository, pageSize: 1); await controller.load(); await controller.loadMore(); expect(controller.cards.map((card) => card.id), ['a']); expect(controller.loadMoreError, 'favoritesError'); expect(controller.hasMore, isTrue); repository.failPages.clear(); await controller.loadMore(); expect(controller.cards.map((card) => card.id), ['a', 'b']); expect(controller.loadMoreError, isNull); expect(repository.pageRequests, [1, 2, 2]); }); } class _PagedFavoriteRepository implements FavoriteRepository { _PagedFavoriteRepository(this.pages); final Map> pages; final List pageRequests = []; final Set failPages = {}; @override Future> page({int pageIndex = 1, int pageSize = 20}) async { pageRequests.add(pageIndex); if (failPages.contains(pageIndex)) { throw StateError('page unavailable'); } return pages[pageIndex] ?? const PageResult(); } @override Future remove(String dramaId) async {} @override Future removeMany(List dramaIds) async {} @override Future toggle(String dramaId) async => true; }