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 open() async => source; @override Future save(String json, String name) async { if (cancelSave) return false; exported = json; return true; } } class FailOnceStorage extends MemoryStorageService { String? failKey; @override Future saveModelList( String key, List> 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>> fixture() => { StorageKeys.abfaelle: [], 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, gesamtpreis: 12.5, 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(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.filamentIdsMitProfil, contains('f1')); 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; 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('Backup imports accept legacy parts without price and reject negative prices', () { final legacy = fixture(); legacy[StorageKeys.bauteile]!.single.remove('gesamtpreis'); final decoded = BackupModel.decode(BackupModel(legacy).encode()); expect( BauteilModel.fromJson(decoded.data[StorageKeys.bauteile]!.single) .gesamtpreis, isNull, ); final invalid = fixture(); invalid[StorageKeys.bauteile]!.single['gesamtpreis'] = -1; expect( () => BackupModel.decode(BackupModel(invalid).encode()), throwsFormatException, ); }); 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); }); }