235 lines
7.4 KiB
Dart
235 lines
7.4 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:ertl_filament_verwaltung/controller/export_controller.dart';
|
|
import 'package:ertl_filament_verwaltung/controller/home_controller.dart';
|
|
import 'package:ertl_filament_verwaltung/helper/sample_routes.dart';
|
|
import 'package:ertl_filament_verwaltung/helper/storage_keys.dart';
|
|
import 'package:ertl_filament_verwaltung/models/anwendung_model.dart';
|
|
import 'package:ertl_filament_verwaltung/models/backup_model.dart';
|
|
import 'package:ertl_filament_verwaltung/models/bauteil_model.dart';
|
|
import 'package:ertl_filament_verwaltung/models/kauf_filament_model.dart';
|
|
import 'package:ertl_filament_verwaltung/pages/export_view.dart';
|
|
import 'package:ertl_filament_verwaltung/pages/home_view.dart';
|
|
import 'package:ertl_filament_verwaltung/services/backup_file_service.dart';
|
|
import 'package:ertl_filament_verwaltung/services/get_storage_services.dart';
|
|
import 'package:ertl_filament_verwaltung/theme/app_theme.dart';
|
|
|
|
import 'helpers/memory_storage_service.dart';
|
|
|
|
class FakeBackupFiles extends BackupFileService {
|
|
String? source;
|
|
String? exported;
|
|
bool cancelSave = false;
|
|
|
|
@override
|
|
Future<String?> open() async => source;
|
|
|
|
@override
|
|
Future<bool> save(String json, String name) async {
|
|
if (cancelSave) return false;
|
|
exported = json;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class FailOnceStorage extends MemoryStorageService {
|
|
String? failKey;
|
|
|
|
@override
|
|
Future<void> saveModelList(
|
|
String key,
|
|
List<Map<String, dynamic>> items,
|
|
) async {
|
|
if (failKey == key) {
|
|
failKey = null;
|
|
throw StateError('Write failed');
|
|
}
|
|
await super.saveModelList(key, items);
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
late FailOnceStorage storage;
|
|
late FakeBackupFiles files;
|
|
late ExportController controller;
|
|
|
|
Map<String, List<Map<String, dynamic>>> fixture() => {
|
|
StorageKeys.filamente: [
|
|
KaufFilamentModel(
|
|
filamentId: 'f1',
|
|
kaufdatum: DateTime(2026),
|
|
haendler: 'Shop',
|
|
marke: 'Marke',
|
|
material: 'PA12',
|
|
preis: 20,
|
|
farbe: 'Grün',
|
|
gewicht: '1000',
|
|
).toJson(),
|
|
],
|
|
StorageKeys.anwendungen: [
|
|
AnwendungModel(
|
|
anwendungId: 'p1',
|
|
filamentId: 'f1',
|
|
erstelltAm: DateTime(2026),
|
|
druckbettTemperatur: 60,
|
|
druckTemperatur: 220,
|
|
bauraumGeschlossen: true,
|
|
).toJson(),
|
|
],
|
|
StorageKeys.bauteile: [
|
|
BauteilModel(
|
|
bauteilId: 'b1',
|
|
erstelltAm: DateTime(2026),
|
|
name: 'Halter',
|
|
verwendung: 'Werkstatt',
|
|
druckdauerMinuten: 60,
|
|
gewichtGramm: 40,
|
|
abfallGramm: 10,
|
|
filamente: [
|
|
const BauteilFilamentModel(
|
|
filamentId: 'f1',
|
|
anwendungId: 'p1',
|
|
verbrauchGramm: 50,
|
|
),
|
|
],
|
|
fotoBase64: 'aGVsbG8=',
|
|
).toJson(),
|
|
],
|
|
StorageKeys.materialien: [
|
|
{
|
|
'werte': ['PA12', 'PLA'],
|
|
},
|
|
],
|
|
};
|
|
|
|
setUp(() {
|
|
Get.testMode = true;
|
|
storage = FailOnceStorage();
|
|
Get.put<GetStorageService>(storage);
|
|
files = FakeBackupFiles();
|
|
controller = Get.put(ExportController(fileService: files));
|
|
});
|
|
tearDown(() => Get.reset());
|
|
|
|
test(
|
|
'Complete JSON backup restores photos, IDs, stock and custom materials',
|
|
() async {
|
|
storage.tables.addAll(fixture());
|
|
await controller.exportData();
|
|
expect(files.exported, isNotNull);
|
|
expect(BackupModel.decode(files.exported!).data, fixture());
|
|
storage.tables.clear();
|
|
final home = Get.put(HomeController());
|
|
files.source = files.exported;
|
|
await controller.selectImport();
|
|
expect(storage.tables, isEmpty);
|
|
await controller.importData();
|
|
expect(storage.tables, fixture());
|
|
expect(home.materialien, ['PA12', 'PLA']);
|
|
expect(home.restgewicht(home.filamente.single), 950);
|
|
expect(controller.hasError.value, isFalse);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'Malformed, incomplete, future-version and duplicate data do not write',
|
|
() async {
|
|
storage.tables.addAll(fixture());
|
|
final valid =
|
|
jsonDecode(BackupModel(fixture()).encode()) as Map<String, dynamic>;
|
|
final badData = fixture();
|
|
badData[StorageKeys.filamente]!.add(
|
|
badData[StorageKeys.filamente]!.single,
|
|
);
|
|
for (final source in [
|
|
'not json',
|
|
'{}',
|
|
jsonEncode({...valid, 'version': 2}),
|
|
jsonEncode({
|
|
...valid,
|
|
'daten': {StorageKeys.filamente: []},
|
|
}),
|
|
BackupModel(badData).encode(),
|
|
]) {
|
|
files.source = source;
|
|
await controller.selectImport();
|
|
await controller.importData();
|
|
expect(controller.pendingBackup.value, isNull);
|
|
expect(controller.hasError.value, isTrue);
|
|
expect(storage.tables, fixture());
|
|
}
|
|
},
|
|
);
|
|
|
|
test('Cancelled dialogs leave data unchanged', () async {
|
|
storage.tables.addAll(fixture());
|
|
files.cancelSave = true;
|
|
await controller.exportData();
|
|
await controller.selectImport();
|
|
expect(controller.message.value, isNull);
|
|
expect(controller.pendingBackup.value, isNull);
|
|
expect(storage.tables, fixture());
|
|
});
|
|
|
|
test('A failed import restores previously written tables', () async {
|
|
storage.tables.addAll(fixture());
|
|
files.source = BackupModel({for (final key in BackupModel.keys) key: []})
|
|
.encode();
|
|
await controller.selectImport();
|
|
storage.failKey = StorageKeys.bauteile;
|
|
await controller.importData();
|
|
expect(storage.tables, fixture());
|
|
expect(controller.hasError.value, isTrue);
|
|
expect(controller.isBusy.value, isFalse);
|
|
});
|
|
|
|
testWidgets('Import preview requires confirmation and works on a phone', (
|
|
tester,
|
|
) async {
|
|
tester.view.physicalSize = const Size(320, 640);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
storage.tables.addAll(fixture());
|
|
files.source = BackupModel({for (final key in BackupModel.keys) key: []})
|
|
.encode();
|
|
await tester.pumpWidget(
|
|
GetMaterialApp(theme: AppTheme.light, home: const ExportView()),
|
|
);
|
|
await controller.selectImport();
|
|
await tester.pumpAndSettle();
|
|
await tester.ensureVisible(find.text('Daten ersetzen und importieren'));
|
|
await tester.tap(find.text('Daten ersetzen und importieren'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('Abbrechen'));
|
|
await tester.pumpAndSettle();
|
|
expect(storage.tables, fixture());
|
|
await tester.tap(find.text('Daten ersetzen und importieren'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('Daten ersetzen'));
|
|
await tester.pumpAndSettle();
|
|
expect(storage.tables.values.every((value) => value.isEmpty), isTrue);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('Hamburger menu opens the backup route', (tester) async {
|
|
await tester.pumpWidget(
|
|
GetMaterialApp(
|
|
theme: AppTheme.light,
|
|
initialRoute: HomeView.routeName,
|
|
getPages: SampleRouts.samplePages,
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.byIcon(Icons.menu));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('Daten exportieren / importieren'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byType(ExportView), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|