From 6d835ddc834949d5389cb263f7c9cae3aa6730d3 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 30 Oct 2026 17:38:10 -0500 Subject: [PATCH 1/4] Let the user close the rescan dialog for background rescans, release the refresh lock on error --- .../sub_widgets/rescanning_dialog.dart | 32 +++++++-- .../wallet_network_settings_view.dart | 23 ++++-- .../intermediate/lib_monero_wallet.dart | 72 ++++++++++--------- .../intermediate/lib_salvium_wallet.dart | 22 +++--- .../intermediate/lib_wownero_wallet.dart | 22 +++--- 5 files changed, 107 insertions(+), 64 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart index 647eb2e331..241a7170af 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart @@ -9,6 +9,8 @@ */ import 'package:flutter/material.dart'; +import '../../../../../themes/stack_colors.dart'; +import '../../../../../utilities/text_styles.dart'; import '../../../../../utilities/util.dart'; import '../../../../../widgets/animated_widgets/rotating_arrows.dart'; import '../../../../../widgets/conditional_parent.dart'; @@ -18,9 +20,14 @@ import '../../../../../widgets/stack_dialog.dart'; class RescanningDialog extends StatefulWidget { const RescanningDialog({ super.key, + this.rescansInBackground = false, // required this.onCancel, }); + /// If true the rescan carries on in the background (see the sync status) + /// so the user is allowed to close this dialog and leave. + final bool rescansInBackground; + // final VoidCallback onCancel; @override @@ -65,22 +72,37 @@ class _RescanningDialogState extends State Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { - return false; + return widget.rescansInBackground; }, child: ConditionalParent( condition: isDesktop, builder: (child) => DesktopDialog( - maxHeight: 150, + maxHeight: widget.rescansInBackground ? 230 : 150, maxWidth: 500, child: child, ), - child: const StackDialog( + child: StackDialog( title: "Rescanning blockchain", - message: "This may take a while. Please do not exit this screen.", - icon: RotatingArrows( + message: widget.rescansInBackground + ? "This may take a while. You can close this dialog, the rescan " + "will continue in the background." + : "This may take a while. Please do not exit this screen.", + icon: const RotatingArrows( width: 24, height: 24, ), + rightButton: widget.rescansInBackground + ? TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Close", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () => Navigator.of(context).pop(), + ) + : null, // rightButton: TextButton( // style: Theme.of(context).textButtonTheme.style?.copyWith( // backgroundColor: MaterialStateProperty.all( diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index 3eccb79285..b60a2642d7 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -136,23 +136,32 @@ class _WalletNetworkSettingsViewState try { if (mounted) { + final wallet = ref.read(pWallets).getWallet(widget.walletId); + final rescansInBackground = + wallet is CryptonoteWallet || wallet is EpiccashWallet; + + // these wallets keep rescanning after recover is called so the user + // may close the dialog before recover returns (it can be held up by a + // refresh in progress) + bool rescanningDialogOpen = true; unawaited( showDialog( context: context, useSafeArea: false, barrierDismissible: false, - builder: (context) => const RescanningDialog(), - ), + builder: (context) => + RescanningDialog(rescansInBackground: rescansInBackground), + ).then((_) => rescanningDialogOpen = false), ); try { - final wallet = ref.read(pWallets).getWallet(widget.walletId); - await wallet.recover(isRescan: true); if (mounted) { // pop rescanning dialog - Navigator.of(context, rootNavigator: isDesktop).pop(); + if (rescanningDialogOpen) { + Navigator.of(context, rootNavigator: isDesktop).pop(); + } final String message; if (wallet is CryptonoteWallet || wallet is EpiccashWallet) { @@ -193,7 +202,9 @@ class _WalletNetworkSettingsViewState if (mounted) { // pop rescanning dialog - Navigator.of(context, rootNavigator: isDesktop).pop(); + if (rescanningDialogOpen) { + Navigator.of(context, rootNavigator: isDesktop).pop(); + } // show error await showDialog( diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index afc68c318c..0b4fcbc0f6 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -1124,45 +1124,51 @@ abstract class LibMoneroWallet "$runtimeType refresh() refreshMutex.acquire() acquired!", ); - Logging.instance.t("$runtimeType refresh() final node = getCurrentNode();"); - final node = getCurrentNode(); - - Logging.instance.i( - "$runtimeType refresh() await _torNodeMismatchGuard(node)", - ); - if (await _torNodeMismatchGuard(node)) { - throw Exception("TOR – clearnet mismatch"); - } + try { + Logging.instance.t( + "$runtimeType refresh() final node = getCurrentNode();", + ); + final node = getCurrentNode(); - Logging.instance.t( - "$runtimeType refresh() it csMonero.startSyncing(wallet!);", - ); - await csMonero.startSyncing(wallet!); - Logging.instance.t( - "$runtimeType refresh() _setSyncStatus(lib_monero_compat.StartingSyncStatus());", - ); - _setSyncStatus(lib_monero_compat.StartingSyncStatus()); + Logging.instance.i( + "$runtimeType refresh() await _torNodeMismatchGuard(node)", + ); + if (await _torNodeMismatchGuard(node)) { + throw Exception("TOR – clearnet mismatch"); + } - Logging.instance.t("$runtimeType refresh() await updateTransactions();"); - await updateTransactions(); - Logging.instance.t("$runtimeType refresh() await updateBalance();"); - await updateBalance(); + Logging.instance.t( + "$runtimeType refresh() it csMonero.startSyncing(wallet!);", + ); + await csMonero.startSyncing(wallet!); + Logging.instance.t( + "$runtimeType refresh() _setSyncStatus(lib_monero_compat.StartingSyncStatus());", + ); + _setSyncStatus(lib_monero_compat.StartingSyncStatus()); - Logging.instance.t( - "$runtimeType refresh() await checkReceivingAddressForTransactions();", - ); - if (info.otherData[WalletInfoKeys.reuseAddress] != true) { - await checkReceivingAddressForTransactions(); - } + Logging.instance.t("$runtimeType refresh() await updateTransactions();"); + await updateTransactions(); + Logging.instance.t("$runtimeType refresh() await updateBalance();"); + await updateBalance(); - Logging.instance.t( - "$runtimeType refresh() refreshMutex.isLocked=${refreshMutex.isLocked} pre release.", - ); - if (refreshMutex.isLocked) { - refreshMutex.release(); Logging.instance.t( - "$runtimeType refresh() refreshMutex.isLocked manually released.", + "$runtimeType refresh() await checkReceivingAddressForTransactions();", ); + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + await checkReceivingAddressForTransactions(); + } + } finally { + // always release, otherwise a failed refresh leaves the lock held + // forever and blocks rescan (recover) and every later refresh + Logging.instance.t( + "$runtimeType refresh() refreshMutex.isLocked=${refreshMutex.isLocked} pre release.", + ); + if (refreshMutex.isLocked) { + refreshMutex.release(); + Logging.instance.t( + "$runtimeType refresh() refreshMutex.isLocked manually released.", + ); + } } Logging.instance.t( diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart index 0d4c56bf18..67e81c106c 100644 --- a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart @@ -1101,18 +1101,20 @@ abstract class LibSalviumWallet // Slight possibility of race but should be irrelevant await refreshMutex.acquire(); - csSalvium.startSyncing(wallet!); - _setSyncStatus(StartingSyncStatus()); - - await updateTransactions(); - await updateBalance(); + try { + csSalvium.startSyncing(wallet!); + _setSyncStatus(StartingSyncStatus()); - if (info.otherData[WalletInfoKeys.reuseAddress] != true) { - await checkReceivingAddressForTransactions(); - } + await updateTransactions(); + await updateBalance(); - if (refreshMutex.isLocked) { - refreshMutex.release(); + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + await checkReceivingAddressForTransactions(); + } + } finally { + if (refreshMutex.isLocked) { + refreshMutex.release(); + } } final synced = wallet != null && await csSalvium.isSynced(wallet!); diff --git a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart index e568b2656a..9625f2501f 100644 --- a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart @@ -1138,18 +1138,20 @@ abstract class LibWowneroWallet // Slight possibility of race but should be irrelevant await refreshMutex.acquire(); - csWownero.startSyncing(wallet!); - _setSyncStatus(lib_monero_compat.StartingSyncStatus()); - - await updateTransactions(); - await updateBalance(); + try { + csWownero.startSyncing(wallet!); + _setSyncStatus(lib_monero_compat.StartingSyncStatus()); - if (info.otherData[WalletInfoKeys.reuseAddress] != true) { - await checkReceivingAddressForTransactions(); - } + await updateTransactions(); + await updateBalance(); - if (refreshMutex.isLocked) { - refreshMutex.release(); + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + await checkReceivingAddressForTransactions(); + } + } finally { + if (refreshMutex.isLocked) { + refreshMutex.release(); + } } final synced = wallet != null && await csWownero.isSynced(wallet!); From 382b8dc50c60bcb1c5ef8d0f76da0827080d1222 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 30 Oct 2026 20:19:00 -0500 Subject: [PATCH 2/4] Reset rescan progress guard after the worker accepts the rescan --- lib/wallets/wallet/intermediate/lib_monero_wallet.dart | 4 +++- lib/wallets/wallet/intermediate/lib_salvium_wallet.dart | 4 +++- lib/wallets/wallet/intermediate/lib_wownero_wallet.dart | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index 0b4fcbc0f6..37df5dab62 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -392,8 +392,10 @@ abstract class LibMoneroWallet // clear blockchain info await mainDB.deleteWalletBlockchainData(walletId); - highestPercentCached = 0; await csMonero.rescanBlockchain(wallet!); + // Reset after the worker has accepted the rescan so a progress reading sent + // before it cannot be ordered after the reset and pin the guard near 100%. + highestPercentCached = 0; await csMonero.startSyncing(wallet!); unawaited(save()); }); diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart index 67e81c106c..888c12691a 100644 --- a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart @@ -370,8 +370,10 @@ abstract class LibSalviumWallet // clear blockchain info await mainDB.deleteWalletBlockchainData(walletId); + await csSalvium.rescanBlockchain(wallet!); + // Reset after the worker has accepted the rescan so a progress reading sent + // before it cannot be ordered after the reset and pin the guard near 100%. highestPercentCached = 0; - unawaited(csSalvium.rescanBlockchain(wallet!)); csSalvium.startSyncing(wallet!); // unawaited(save()); }); diff --git a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart index 9625f2501f..17cbf8fc5f 100644 --- a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart @@ -394,8 +394,10 @@ abstract class LibWowneroWallet // clear blockchain info await mainDB.deleteWalletBlockchainData(walletId); + await csWownero.rescanBlockchain(wallet!); + // Reset after the worker has accepted the rescan so a progress reading sent + // before it cannot be ordered after the reset and pin the guard near 100%. highestPercentCached = 0; - unawaited(csWownero.rescanBlockchain(wallet!)); csWownero.startSyncing(wallet!); // unawaited(save()); }); From 6c9d898ac40c3f558752c2b2e247306329565703 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 30 Oct 2026 20:30:54 -0500 Subject: [PATCH 3/4] Keep Wownero and Salvium rescans unawaited Their rescanBlockchain runs the whole blocking rescan in an isolate, so awaiting it held the refresh lock and delayed startSyncing until the rescan finished. The Monero call returns once the async rescan starts, so only Monero keeps the reordered reset. --- lib/wallets/wallet/intermediate/lib_salvium_wallet.dart | 4 +--- lib/wallets/wallet/intermediate/lib_wownero_wallet.dart | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart index 888c12691a..67e81c106c 100644 --- a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart @@ -370,10 +370,8 @@ abstract class LibSalviumWallet // clear blockchain info await mainDB.deleteWalletBlockchainData(walletId); - await csSalvium.rescanBlockchain(wallet!); - // Reset after the worker has accepted the rescan so a progress reading sent - // before it cannot be ordered after the reset and pin the guard near 100%. highestPercentCached = 0; + unawaited(csSalvium.rescanBlockchain(wallet!)); csSalvium.startSyncing(wallet!); // unawaited(save()); }); diff --git a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart index 17cbf8fc5f..9625f2501f 100644 --- a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart @@ -394,10 +394,8 @@ abstract class LibWowneroWallet // clear blockchain info await mainDB.deleteWalletBlockchainData(walletId); - await csWownero.rescanBlockchain(wallet!); - // Reset after the worker has accepted the rescan so a progress reading sent - // before it cannot be ordered after the reset and pin the guard near 100%. highestPercentCached = 0; + unawaited(csWownero.rescanBlockchain(wallet!)); csWownero.startSyncing(wallet!); // unawaited(save()); }); From b690bacb687831c9fe22a954d9a165ebb51aac29 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sun, 1 Nov 2026 14:19:39 -0600 Subject: [PATCH 4/4] fix(rescan): keep mutex and dialog ownership local --- .../sub_widgets/rescanning_dialog.dart | 8 ++- .../wallet_network_settings_view.dart | 15 +++--- .../intermediate/lib_monero_wallet.dart | 14 ++---- .../intermediate/lib_salvium_wallet.dart | 8 +-- .../intermediate/lib_wownero_wallet.dart | 8 +-- test/widget_tests/rescanning_dialog_test.dart | 49 +++++++++++++++++++ 6 files changed, 70 insertions(+), 32 deletions(-) create mode 100644 test/widget_tests/rescanning_dialog_test.dart diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart index 241a7170af..44c4e4eca4 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart @@ -21,12 +21,14 @@ class RescanningDialog extends StatefulWidget { const RescanningDialog({ super.key, this.rescansInBackground = false, + this.onClose, // required this.onCancel, }); /// If true the rescan carries on in the background (see the sync status) /// so the user is allowed to close this dialog and leave. final bool rescansInBackground; + final VoidCallback? onClose; // final VoidCallback onCancel; @@ -72,6 +74,7 @@ class _RescanningDialogState extends State Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { + if (widget.rescansInBackground) widget.onClose?.call(); return widget.rescansInBackground; }, child: ConditionalParent( @@ -100,7 +103,10 @@ class _RescanningDialogState extends State "Close", style: STextStyles.itemSubtitle12(context), ), - onPressed: () => Navigator.of(context).pop(), + onPressed: () { + widget.onClose?.call(); + Navigator.of(context).pop(); + }, ) : null, // rightButton: TextButton( diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index b60a2642d7..6231db908e 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -137,20 +137,21 @@ class _WalletNetworkSettingsViewState try { if (mounted) { final wallet = ref.read(pWallets).getWallet(widget.walletId); - final rescansInBackground = - wallet is CryptonoteWallet || wallet is EpiccashWallet; + final rescansInBackground = wallet is CryptonoteWallet; - // these wallets keep rescanning after recover is called so the user - // may close the dialog before recover returns (it can be held up by a - // refresh in progress) + // Cryptonote wallets keep rescanning after recover is called, so the + // user may close the dialog before recover returns if a refresh is in + // progress. bool rescanningDialogOpen = true; unawaited( showDialog( context: context, useSafeArea: false, barrierDismissible: false, - builder: (context) => - RescanningDialog(rescansInBackground: rescansInBackground), + builder: (context) => RescanningDialog( + rescansInBackground: rescansInBackground, + onClose: () => rescanningDialogOpen = false, + ), ).then((_) => rescanningDialogOpen = false), ); diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index 37df5dab62..d3e57bec5f 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -883,10 +883,6 @@ abstract class LibMoneroWallet final _syncStatus = syncStatus; if (_syncStatus != null) { - if (_syncStatus.progress() == 1 && refreshMutex.isLocked) { - refreshMutex.release(); - } - WalletSyncStatus? status; xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(true); @@ -1165,12 +1161,10 @@ abstract class LibMoneroWallet Logging.instance.t( "$runtimeType refresh() refreshMutex.isLocked=${refreshMutex.isLocked} pre release.", ); - if (refreshMutex.isLocked) { - refreshMutex.release(); - Logging.instance.t( - "$runtimeType refresh() refreshMutex.isLocked manually released.", - ); - } + refreshMutex.release(); + Logging.instance.t( + "$runtimeType refresh() refreshMutex.isLocked manually released.", + ); } Logging.instance.t( diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart index 67e81c106c..aed9530de0 100644 --- a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart @@ -862,10 +862,6 @@ abstract class LibSalviumWallet final _syncStatus = syncStatus; if (_syncStatus != null) { - if (_syncStatus.progress() == 1 && refreshMutex.isLocked) { - refreshMutex.release(); - } - WalletSyncStatus? status; xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(true); @@ -1112,9 +1108,7 @@ abstract class LibSalviumWallet await checkReceivingAddressForTransactions(); } } finally { - if (refreshMutex.isLocked) { - refreshMutex.release(); - } + refreshMutex.release(); } final synced = wallet != null && await csSalvium.isSynced(wallet!); diff --git a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart index 9625f2501f..eb3b1cd0ea 100644 --- a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart @@ -898,10 +898,6 @@ abstract class LibWowneroWallet final _syncStatus = syncStatus; if (_syncStatus != null) { - if (_syncStatus.progress() == 1 && refreshMutex.isLocked) { - refreshMutex.release(); - } - WalletSyncStatus? status; xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(true); @@ -1149,9 +1145,7 @@ abstract class LibWowneroWallet await checkReceivingAddressForTransactions(); } } finally { - if (refreshMutex.isLocked) { - refreshMutex.release(); - } + refreshMutex.release(); } final synced = wallet != null && await csWownero.isSynced(wallet!); diff --git a/test/widget_tests/rescanning_dialog_test.dart b/test/widget_tests/rescanning_dialog_test.dart new file mode 100644 index 0000000000..0963274646 --- /dev/null +++ b/test/widget_tests/rescanning_dialog_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/models/isar/stack_theme.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; + +import '../sample_data/theme_json.dart'; + +void main() { + testWidgets("marks a background rescan closed before dismissing", ( + tester, + ) async { + final theme = StackTheme.fromJson(json: lightThemeJsonMap); + var closed = false; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(extensions: [StackColors.fromStackColorTheme(theme)]), + home: Builder( + builder: (context) => TextButton( + onPressed: () => showDialog( + context: context, + builder: (_) => RescanningDialog( + rescansInBackground: true, + onClose: () { + expect(find.byType(RescanningDialog), findsOneWidget); + closed = true; + }, + ), + ), + child: const Text("Open"), + ), + ), + ), + ); + await tester.tap(find.text("Open")); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); + + tester + .widget(find.widgetWithText(TextButton, "Close")) + .onPressed!(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); + + expect(closed, isTrue); + expect(find.byType(RescanningDialog), findsNothing); + }); +}