From 92e3fa94ec7f31d99fcb84591ff751f0649b3df5 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:00:14 +0700 Subject: [PATCH] fix(network): discard stale folder browsing responses --- lib/screens/network_storage_screen.dart | 22 ++++++-- test/network_storage_screen_test.dart | 69 +++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/lib/screens/network_storage_screen.dart b/lib/screens/network_storage_screen.dart index 0b2dc328..56697c38 100644 --- a/lib/screens/network_storage_screen.dart +++ b/lib/screens/network_storage_screen.dart @@ -46,6 +46,7 @@ class _NetworkStorageScreenState extends ConsumerState { bool _adding = false; bool _added = false; bool _selectingDestination = false; + int _loadGeneration = 0; Future _useForDownloads() async { final connection = widget.connection; @@ -123,7 +124,20 @@ class _NetworkStorageScreenState extends ConsumerState { _load(); } + @override + void didUpdateWidget(covariant NetworkStorageScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.connection != widget.connection || + oldWidget.path != widget.path || + oldWidget.service != widget.service) { + _added = false; + _load(); + } + } + Future _load() async { + final generation = ++_loadGeneration; + bool isCurrent() => mounted && generation == _loadGeneration; setState(() { _loading = true; _failed = false; @@ -132,18 +146,18 @@ class _NetworkStorageScreenState extends ConsumerState { final connection = widget.connection; if (connection == null) { final connections = await service.connections(); - if (mounted) _connections = connections; + if (isCurrent()) _connections = connections; } else { final entries = await service.list(connection, widget.path); - if (mounted) _entries = entries; + if (isCurrent()) _entries = entries; } } catch (error) { - if (mounted) { + if (isCurrent()) { _failed = true; _failure = classifyNetworkStorageError(error); } } finally { - if (mounted) setState(() => _loading = false); + if (isCurrent()) setState(() => _loading = false); } } diff --git a/test/network_storage_screen_test.dart b/test/network_storage_screen_test.dart index 85a84b58..3d2de9ed 100644 --- a/test/network_storage_screen_test.dart +++ b/test/network_storage_screen_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/cupertino.dart'; @@ -12,6 +13,59 @@ import 'package:spotiflac_android/theme/mornye_theme.dart'; import 'package:spotiflac_android/widgets/app_action_button.dart'; void main() { + for (final olderFails in [false, true]) { + testWidgets('latest folder refresh wins (older fails: $olderFails)', ( + tester, + ) async { + final service = _DelayedListings(); + await tester.pumpWidget( + ProviderScope( + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: NetworkStorageScreen( + service: service, + connection: const NetworkConnection( + id: 'nas', + name: 'NAS', + protocol: NetworkProtocol.webdav, + address: 'https://nas.test/music/', + ), + ), + ), + ), + ); + service.requests.single.complete([ + const NetworkEntry('Initial.flac', 'Initial.flac'), + ]); + await tester.pumpAndSettle(); + final refresh = find.byTooltip('Refresh'); + // Two taps before the disabled state is painted can overlap requests. + await tester.tap(refresh); + await tester.tap(refresh); + expect(service.requests, hasLength(3)); + service.requests.last.complete([ + const NetworkEntry('Latest.flac', 'Latest.flac'), + ]); + await tester.pumpAndSettle(); + if (olderFails) { + service.requests[1].completeError(const SocketException('offline')); + } else { + service.requests[1].complete([ + const NetworkEntry('Stale.flac', 'Stale.flac'), + ]); + } + await tester.pumpAndSettle(); + expect(find.text('Latest.flac'), findsOneWidget); + expect(find.text('Stale.flac'), findsNothing); + expect( + find.textContaining('The server could not be reached.'), + findsNothing, + ); + expect(tester.takeException(), isNull); + }); + } + testWidgets('failed sign-in stays on the form with a specific error', ( tester, ) async { @@ -243,3 +297,18 @@ class _NetworkFixture extends NetworkStorageService { : [const NetworkEntry('Album/Song.flac', 'Song.flac')]; } } + +class _DelayedListings extends NetworkStorageService { + _DelayedListings() : super(read: () async => null, write: (_) async {}); + final requests = >>[]; + + @override + Future> list( + NetworkConnection connection, [ + String path = '', + ]) { + final request = Completer>(); + requests.add(request); + return request.future; + } +}