From 648bde44b1756485d95ca3c3a8714cd2015d5be4 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 1 Nov 2026 19:21:06 +0100 Subject: [PATCH] sync_settings_ui 0.5.2: cancelled backup is not success Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WzxdWXTUM9dpnCS2wtFkx4 --- sync_settings_ui/lib/src/backup_slot.dart | 18 +++++++++-- .../lib/src/sync_settings_screen.dart | 4 +++ sync_settings_ui/pubspec.yaml | 2 +- sync_settings_ui/test/backup_slot_test.dart | 17 ++++++++-- .../sync_settings_screen_backup_test.dart | 32 +++++++++++++++++++ 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/sync_settings_ui/lib/src/backup_slot.dart b/sync_settings_ui/lib/src/backup_slot.dart index dd39034..34f2500 100644 --- a/sync_settings_ui/lib/src/backup_slot.dart +++ b/sync_settings_ui/lib/src/backup_slot.dart @@ -1,5 +1,14 @@ import 'package:data_transfer_flutter/data_transfer_flutter.dart'; +/// Thrown by a [BackupSlot] callback when the user dismissed the file dialog. +/// +/// Not a failure: [SyncSettingsScreen] clears its status line instead of +/// claiming success or showing an error. +class BackupCancelled implements Exception { + /// Creates the signal. + const BackupCancelled(); +} + /// An injectable local export/import delegate for [SyncSettingsScreen]'s /// optional "Backup" section. /// @@ -24,7 +33,8 @@ class BackupSlot { /// Export saves `build()` as [fileName] to device storage (system Save-as /// on Android/iOS, a save dialog on desktop, a download on web). Import /// lets the user pick a JSON file and hands the decoded object to - /// [restore]; a cancelled picker restores nothing. + /// [restore]. A dismissed dialog throws [BackupCancelled] (and a cancelled + /// picker restores nothing). factory BackupSlot.json({ required String label, required String fileName, @@ -33,11 +43,13 @@ class BackupSlot { }) => BackupSlot( label: label, export: () async { - await exportJson(fileName: fileName, json: await build()); + final saved = await exportJson(fileName: fileName, json: await build()); + if (!saved) throw const BackupCancelled(); }, import: () async { final json = await pickJson(); - if (json != null) await restore(json); + if (json == null) throw const BackupCancelled(); + await restore(json); }, ); diff --git a/sync_settings_ui/lib/src/sync_settings_screen.dart b/sync_settings_ui/lib/src/sync_settings_screen.dart index 3329012..607376d 100644 --- a/sync_settings_ui/lib/src/sync_settings_screen.dart +++ b/sync_settings_ui/lib/src/sync_settings_screen.dart @@ -123,6 +123,8 @@ class _SyncSettingsScreenState extends State { try { await backup.export(); if (mounted) setState(() => _status = 'Exported ${backup.label}.'); + } on BackupCancelled { + if (mounted) setState(() => _status = null); } on Exception catch (error) { if (mounted) setState(() => _status = 'Export failed: $error'); } @@ -132,6 +134,8 @@ class _SyncSettingsScreenState extends State { try { await backup.import(); if (mounted) setState(() => _status = 'Imported ${backup.label}.'); + } on BackupCancelled { + if (mounted) setState(() => _status = null); } on Exception catch (error) { if (mounted) setState(() => _status = 'Import failed: $error'); } diff --git a/sync_settings_ui/pubspec.yaml b/sync_settings_ui/pubspec.yaml index 6f5161f..2992599 100644 --- a/sync_settings_ui/pubspec.yaml +++ b/sync_settings_ui/pubspec.yaml @@ -5,7 +5,7 @@ description: >- injected closures -- no keystore or platform plugin access lives in this package itself. publish_to: 'none' -version: 0.5.1 +version: 0.5.2 environment: sdk: ^3.12.2 diff --git a/sync_settings_ui/test/backup_slot_test.dart b/sync_settings_ui/test/backup_slot_test.dart index 1ce2655..34e0856 100644 --- a/sync_settings_ui/test/backup_slot_test.dart +++ b/sync_settings_ui/test/backup_slot_test.dart @@ -70,15 +70,28 @@ void main() { restore: (_) async => called = true, ); - await slot.import(); + await expectLater(slot.import(), throwsA(isA())); expect(called, isFalse); }); + + test('a cancelled save dialog reports the cancellation', () async { + fake.acceptSave = false; + final slot = BackupSlot.json( + label: 'inventory', + fileName: 'inv.json', + build: () async => {'items': 3}, + restore: (_) async {}, + ); + + await expectLater(slot.export(), throwsA(isA())); + }); }); } class _FakePlatform implements DataTransferPlatform { String? picked; + bool acceptSave = true; final saved = <(String, String)>[]; @override @@ -88,7 +101,7 @@ class _FakePlatform implements DataTransferPlatform { String mimeType = 'application/json', }) async { saved.add((fileName, text)); - return true; + return acceptSave; } @override diff --git a/sync_settings_ui/test/sync_settings_screen_backup_test.dart b/sync_settings_ui/test/sync_settings_screen_backup_test.dart index dfb3713..16ec9a1 100644 --- a/sync_settings_ui/test/sync_settings_screen_backup_test.dart +++ b/sync_settings_ui/test/sync_settings_screen_backup_test.dart @@ -17,6 +17,38 @@ Widget _wrap(Widget child) => MaterialApp(home: child); void main() { group('Backup section', () { + testWidgets('a cancelled export or import claims no success', ( + tester, + ) async { + await tester.pumpWidget( + _wrap( + SyncSettingsScreen( + accountLoader: () async => null, + accountSaver: (_) async {}, + accountClearer: () async {}, + sessionProbe: () async => false, + firebaseFactory: () async => null, + backup: BackupSlot( + label: 'notes', + export: () async => throw const BackupCancelled(), + import: () async => throw const BackupCancelled(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Export notes')); + await tester.pumpAndSettle(); + expect(find.text('Exported notes.'), findsNothing); + expect(find.textContaining('failed'), findsNothing); + + await tester.tap(find.text('Import notes')); + await tester.pumpAndSettle(); + expect(find.text('Imported notes.'), findsNothing); + expect(find.textContaining('failed'), findsNothing); + }); + testWidgets('omitted entirely when backup is null', (tester) async { await tester.pumpWidget( _wrap(