mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 19:07:16 +01:00
workout_app: cover export/import to 100%
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
This commit is contained in:
parent
ba3a8a293d
commit
6cd21296a9
@ -3,9 +3,6 @@ import 'dart:convert';
|
||||
import 'package:data_transfer_flutter/data_transfer_flutter.dart';
|
||||
import 'package:workout_app/services/storage_service.dart';
|
||||
|
||||
String _exportFileName(DateTime now) =>
|
||||
'workout_app-export-${now.toIso8601String().substring(0, 10)}.json';
|
||||
|
||||
/// Parses [source] and merges it; throws [FormatException] on a bad file.
|
||||
Future<ImportReport> importExportText(String source) async {
|
||||
final decoded = jsonDecode(source);
|
||||
@ -17,6 +14,9 @@ Future<ImportReport> importExportText(String source) async {
|
||||
|
||||
// coverage:ignore-start
|
||||
|
||||
String _exportFileName(DateTime now) =>
|
||||
'workout_app-export-${now.toIso8601String().substring(0, 10)}.json';
|
||||
|
||||
/// Saves an export via the system Save-as picker; false when cancelled.
|
||||
Future<bool> exportUserData() async {
|
||||
final json = const JsonEncoder.withIndent(
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
// The export/import buttons in Settings reload the screen after an import.
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:workout_app/screens/settings_screen.dart';
|
||||
import 'package:workout_app/services/progression_sync_service.dart';
|
||||
import 'package:workout_app/services/storage_service.dart';
|
||||
import 'package:workout_app/ui/theme.dart';
|
||||
import 'package:workout_app/widgets/data_transfer_buttons.dart';
|
||||
|
||||
import '../fake_pc_pairing.dart';
|
||||
import '../fake_secure_storage.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
StorageService.resetForTesting();
|
||||
await StorageService.init();
|
||||
installFakeSecureStorage();
|
||||
});
|
||||
|
||||
testWidgets('Settings shows the buttons and reloads after an import', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.runAsync(() async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: buildAppTheme(),
|
||||
home: SettingsScreen(
|
||||
pcPairingLoader: fakePcPairing,
|
||||
firebaseFactory: () async => null,
|
||||
sessionProbe: () async => false,
|
||||
storageChecker: () async => false,
|
||||
progressionPuller: () async =>
|
||||
const ProgressionSyncResult(changed: false, reason: 'stubbed'),
|
||||
),
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||
});
|
||||
await tester.pump();
|
||||
final buttons = find.byType(DataTransferButtons);
|
||||
await tester.scrollUntilVisible(buttons, 300);
|
||||
expect(find.bySemanticsLabel('Export data'), findsOneWidget);
|
||||
expect(find.bySemanticsLabel('Import data'), findsOneWidget);
|
||||
|
||||
tester.widget<DataTransferButtons>(buttons).onImported!();
|
||||
await tester.runAsync(
|
||||
() => Future<void>.delayed(const Duration(milliseconds: 200)),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(DataTransferButtons), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:workout_app/services/data_transfer.dart';
|
||||
import 'package:workout_app/services/storage_service.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
StorageService.resetForTesting();
|
||||
await StorageService.init();
|
||||
});
|
||||
|
||||
test('importExportText merges a valid export and reports it', () async {
|
||||
final report = await importExportText(
|
||||
'{"format":"$kExportFormat","version":2,"settings":[]}',
|
||||
);
|
||||
expect(report.rows, 0);
|
||||
expect(report.manualSkipped, false);
|
||||
});
|
||||
|
||||
test('a JSON array is rejected with a FormatException', () async {
|
||||
await expectLater(
|
||||
importExportText('[1, 2]'),
|
||||
throwsA(isA<FormatException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('text that is not JSON is rejected with a FormatException', () async {
|
||||
await expectLater(
|
||||
importExportText('not json'),
|
||||
throwsA(isA<FormatException>()),
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
// Import edge cases: junk sections, duplicate manual ids, a throwing port.
|
||||
import 'package:crdt_sync/crdt_sync.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:workout_app/services/manual_workout_port.dart';
|
||||
import 'package:workout_app/services/storage_service.dart';
|
||||
|
||||
import '_sync_test_fixtures.dart' show manualRecord;
|
||||
|
||||
class _Port implements ManualWorkoutPort {
|
||||
_Port({this.throwOnRead = false});
|
||||
|
||||
final bool throwOnRead;
|
||||
Log pushed = {};
|
||||
|
||||
@override
|
||||
Future<Log?> read() async {
|
||||
if (throwOnRead) throw StateError('network down');
|
||||
return <String, Record>{};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> push(Log records) async {
|
||||
pushed = mergeLogs(pushed, records);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _freshStore(ManualWorkoutPort port) async {
|
||||
StorageService.resetForTesting();
|
||||
await StorageService.init();
|
||||
StorageService.manualWorkouts = port;
|
||||
}
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
});
|
||||
|
||||
const id = 'manual:2026-07-13T10:00';
|
||||
|
||||
test('a manual_workouts section that is not a list is skipped', () async {
|
||||
await _freshStore(_Port());
|
||||
final written = await StorageService.instance.importData({
|
||||
'settings': <Object>[],
|
||||
'manual_workouts': 'junk',
|
||||
});
|
||||
expect(written, 0);
|
||||
});
|
||||
|
||||
test('a table section that is not a list is skipped', () async {
|
||||
await _freshStore(_Port());
|
||||
final written = await StorageService.instance.importData({
|
||||
'settings': <Object>[],
|
||||
'exercise_state': 'junk',
|
||||
});
|
||||
expect(written, 0);
|
||||
});
|
||||
|
||||
test('a manual id repeated in one file merges to the newer copy', () async {
|
||||
final port = _Port();
|
||||
await _freshStore(port);
|
||||
final older = manualRecord(id, 'OLD', 1000);
|
||||
final newer = manualRecord(id, 'NEW', 2000);
|
||||
|
||||
final report = await StorageService.instance.importDataReport({
|
||||
'manual_workouts': [older.toJson(), newer.toJson()],
|
||||
});
|
||||
|
||||
expect(report.manualRecords, 1);
|
||||
expect((port.pushed[id]!.fields['payload']!.$1 as Map)['cost'], 'NEW');
|
||||
});
|
||||
|
||||
test('a port that throws on import marks manual workouts skipped', () async {
|
||||
await _freshStore(_Port(throwOnRead: true));
|
||||
final report = await StorageService.instance.importDataReport({
|
||||
'manual_workouts': [manualRecord(id, 'X', 1000).toJson()],
|
||||
});
|
||||
expect(report.manualSkipped, true);
|
||||
expect(report.manualRecords, 0);
|
||||
});
|
||||
}
|
||||
@ -91,4 +91,29 @@ void main() {
|
||||
expect(await port.push({manual.id: manual}), false);
|
||||
expect(putCalls, isEmpty);
|
||||
});
|
||||
|
||||
test('read mirrors through Firebase and returns an empty log', () async {
|
||||
installFakeSecureStorage(initial: {'sync.token': 'tok'});
|
||||
final (:httpClient, putCalls: _) = mockGitHub(
|
||||
contentResponses: {'screen-locker-sync/devices': syncResponse(200, [])},
|
||||
);
|
||||
final firebase = fakeFirebase().client;
|
||||
final port = SyncManualWorkoutPort(
|
||||
syncService(
|
||||
httpClient: httpClient,
|
||||
firebaseFactory: () async => firebase,
|
||||
),
|
||||
);
|
||||
expect(await port.read(), isEmpty);
|
||||
});
|
||||
|
||||
test('the default port builds its own sync service', () {
|
||||
expect(SyncManualWorkoutPort(), isA<ManualWorkoutPort>());
|
||||
});
|
||||
|
||||
test('the offline port reads null and refuses pushes', () async {
|
||||
const port = OfflineManualWorkoutPort();
|
||||
expect(await port.read(), isNull);
|
||||
expect(await port.push({manual.id: manual}), false);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user