sync_settings_ui 0.5.2: cancelled backup is not success
Some checks failed
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
music_theory tests / test (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
web_ui tests / test (push) Waiting to run
data_transfer_flutter tests / test (push) Has been cancelled
sync_settings_ui tests / test (push) Has been cancelled

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWXTUM9dpnCS2wtFkx4
This commit is contained in:
Krzysztof Rudnicki 2026-11-01 19:21:06 +01:00
parent bbb7f0f0c0
commit 648bde44b1
5 changed files with 67 additions and 6 deletions

View File

@ -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);
},
);

View File

@ -123,6 +123,8 @@ class _SyncSettingsScreenState extends State<SyncSettingsScreen> {
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<SyncSettingsScreen> {
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');
}

View File

@ -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

View File

@ -70,15 +70,28 @@ void main() {
restore: (_) async => called = true,
);
await slot.import();
await expectLater(slot.import(), throwsA(isA<BackupCancelled>()));
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<BackupCancelled>()));
});
});
}
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

View File

@ -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(