diff --git a/lib/screens/livetv/live_tv_screen.dart b/lib/screens/livetv/live_tv_screen.dart index e1961a5c..f26e9c15 100644 --- a/lib/screens/livetv/live_tv_screen.dart +++ b/lib/screens/livetv/live_tv_screen.dart @@ -82,6 +82,9 @@ class _LiveTvScreenState extends State int _favoritesLoadGeneration = 0; Future? _favoritesLoadFuture; final SerialFutureQueue _favoritesMutationQueue = SerialFutureQueue(); + + /// True while [_favoriteChannels] holds an authoritative set. A refresh keeps the previous set live until the new + /// one commits, so the favorites filter never widens mid-load. bool _favoritesLoaded = false; bool _favoritesWritable = false; @@ -423,7 +426,6 @@ class _LiveTvScreenState extends State Future _loadFavorites(MultiServerProvider multiServer) async { final loadGeneration = ++_favoritesLoadGeneration; - _favoritesLoaded = false; _favoritesWritable = false; final previousStoreBySource = Map.of(_favoriteStoreBySource); final scopeByLiveServer = Map.of(_favoriteScopeByLiveServer); diff --git a/test/screens/livetv/live_tv_screen_test.dart b/test/screens/livetv/live_tv_screen_test.dart index 2b7841a9..0ab672ca 100644 --- a/test/screens/livetv/live_tv_screen_test.dart +++ b/test/screens/livetv/live_tv_screen_test.dart @@ -52,6 +52,30 @@ void main() { expect(_guideChannels(tester), isEmpty); }); + testWidgets('refresh keeps the favorites filter narrow while favorites reload', (tester) async { + final harness = await _pumpLiveTvScreen(tester, channelKeys: const ['channel-a', 'channel-b']); + addTearDown(() async { + await tester.pumpWidget(const SizedBox.shrink()); + harness.dispose(); + }); + + final favorite = FavoriteChannel(id: 'channel-a', source: 'server://server-a/provider-a'); + harness.liveTv.favorites.complete([favorite]); + await tester.pumpAndSettle(); + + expect(_guideChannels(tester).map((channel) => channel.key), ['channel-a']); + + await tester.tap(find.byIcon(Symbols.refresh_rounded)); + await tester.pumpAndSettle(); + + expect(_guideChannels(tester).map((channel) => channel.key), ['channel-a']); + + harness.liveTv.favorites.complete([favorite]); + await tester.pumpAndSettle(); + + expect(_guideChannels(tester).map((channel) => channel.key), ['channel-a']); + }); + testWidgets('favorite read failure preserves raw Guide channels', (tester) async { final harness = await _pumpLiveTvScreen(tester); addTearDown(() async { @@ -144,8 +168,8 @@ void main() { List _guideChannels(WidgetTester tester) => tester.widget(find.byType(GuideTab)).channels; -Future<_LiveTvHarness> _pumpLiveTvScreen(WidgetTester tester) async { - final liveTv = _FakeLiveTvSupport(); +Future<_LiveTvHarness> _pumpLiveTvScreen(WidgetTester tester, {List? channelKeys}) async { + final liveTv = _FakeLiveTvSupport(channelKeys: channelKeys); final client = _FakeMediaServerClient(liveTv); final manager = MultiServerManager()..debugRegisterClientForTesting(client); final provider = testMultiServerProvider(manager); @@ -207,11 +231,26 @@ class _FakeMediaServerClient implements MediaServerClient { } class _FakeLiveTvSupport implements LiveTvSupport { - _FakeLiveTvSupport({this.serverId = 'server-a', this.storeKey = 'test-store'}); + _FakeLiveTvSupport({this.serverId = 'server-a', this.storeKey = 'test-store', List? channelKeys}) + : channelKeys = channelKeys ?? [serverId == 'server-a' ? 'channel-a' : 'channel-$serverId']; final String serverId; final String storeKey; - final Completer> favorites = Completer>(); + final List channelKeys; + final List>> _favoriteRequests = []; + int _servedFavoriteRequests = 0; + + Completer> get favorites { + if (_favoriteRequests.length > _servedFavoriteRequests) { + return _favoriteRequests[_servedFavoriteRequests]; + } + if (_servedFavoriteRequests > 0 && !_favoriteRequests[_servedFavoriteRequests - 1].isCompleted) { + return _favoriteRequests[_servedFavoriteRequests - 1]; + } + final request = Completer>(); + _favoriteRequests.add(request); + return request; + } @override LiveTvDvrSupport? get dvr => null; @@ -227,18 +266,24 @@ class _FakeLiveTvSupport implements LiveTvSupport { @override Future> fetchChannels({String? lineup}) async => [ - LiveTvChannel( - key: serverId == 'server-a' ? 'channel-a' : 'channel-$serverId', - title: serverId == 'server-a' ? 'Unique Channel A' : 'Unique Channel $serverId', - serverId: serverId, - ), + for (final key in channelKeys) + LiveTvChannel( + key: key, + title: key == 'channel-a' ? 'Unique Channel A' : 'Unique Channel $key', + serverId: serverId, + ), ]; @override Future> fetchSchedule({DateTime? from, DateTime? to}) async => const []; @override - Future> fetchFavoriteChannels() => favorites.future; + Future> fetchFavoriteChannels() { + if (_favoriteRequests.length == _servedFavoriteRequests) { + _favoriteRequests.add(Completer>()); + } + return _favoriteRequests[_servedFavoriteRequests++].future; + } final List writeFailures = []; final List> writes = [];