import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:ertl_filament_verwaltung/controller/bauteil_controller.dart'; import 'package:ertl_filament_verwaltung/helper/bauteil_preis_berechnung.dart'; import 'package:ertl_filament_verwaltung/helper/storage_keys.dart'; import 'package:ertl_filament_verwaltung/models/bauteil_model.dart'; import 'package:ertl_filament_verwaltung/models/backup_model.dart'; import 'package:ertl_filament_verwaltung/models/kauf_filament_model.dart'; import 'package:ertl_filament_verwaltung/pages/bauteil_view.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'; void main() { late MemoryStorageService storage; late BauteilController controller; KaufFilamentModel rolle(String id, double preis, String gewicht) => KaufFilamentModel( filamentId: id, kaufdatum: DateTime(2026), haendler: 'Shop', marke: id, material: 'PLA', preis: preis, farbe: id, gewicht: gewicht, ); setUp(() { Get.testMode = true; storage = MemoryStorageService(); storage.tables[StorageKeys.filamente] = [ rolle('a', 15, '750').toJson(), rolle('b', 20, '500').toJson(), ]; Get.put(storage); controller = Get.put(BauteilController()); }); tearDown(() => Get.reset()); Future show(WidgetTester tester) async { tester.view.physicalSize = const Size(320, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget( GetMaterialApp(theme: AppTheme.light, home: const BauteilView()), ); await tester.pumpAndSettle(); } void fill() { controller.nameController.text = 'Halter'; controller.verwendungController.text = 'Werkstatt'; controller.minutenController.text = '30'; controller.gewichtController.text = '100'; controller.abfallController.text = '25'; controller.toggleFilament('a', true); controller.verbrauchController('a').text = '125'; } Future edit(WidgetTester tester) async { final pending = controller.editBauteil(controller.bauteile.single); await tester.pumpAndSettle(); await pending; } testWidgets( 'Single filament price updates live before usage entry; markup changes recalculate', (tester) async { await show(tester); expect(controller.aufschlagController.text, '50'); controller.gewichtController.text = '100'; controller.abfallController.text = '25'; controller.toggleFilament('a', true); await tester.pumpAndSettle(); expect(controller.preisDetails.value!.materialKosten, 2); expect(controller.preisDetails.value!.abfallKosten, .5); expect(controller.gesamtpreisController.text, '3,75'); await tester.ensureVisible( find.widgetWithText(TextFormField, 'Gesamtpreis (automatisch)'), ); await tester.pumpAndSettle(); expect(find.textContaining('Filamentkosten (Bauteil):'), findsOneWidget); final total = tester.widget( find.descendant( of: find.widgetWithText(TextFormField, 'Gesamtpreis (automatisch)'), matching: find.byType(TextField), ), ); expect(total.readOnly, isTrue); controller.gewichtController.text = '200'; await tester.pumpAndSettle(); expect(controller.gesamtpreisController.text, '6,75'); final markup = find.byWidgetPredicate( (widget) => widget is TextFormField && widget.controller == controller.aufschlagController, ); await tester.ensureVisible(markup); await tester.enterText(markup, '20'); await tester.pumpAndSettle(); expect(controller.gesamtpreisController.text, '5,40'); controller.toggleFilament('a', false); controller.toggleFilament('b', true); expect(controller.gesamtpreisController.text, '10,80'); controller.toggleFilament('b', false); expect(controller.preisDetails.value, isNull); expect(controller.gesamtpreisController.text, isEmpty); expect(tester.takeException(), isNull); }, ); testWidgets( 'Save, edit, duplicate and backup preserve markup and calculated totals', (tester) async { await show(tester); fill(); await controller.saveBauteil(); await tester.pumpAndSettle(); var saved = controller.bauteile.single; expect(saved.materialKosten, 2); expect(saved.abfallKosten, .5); expect(saved.aufschlagProzent, 50); expect(saved.gesamtpreis, 3.75); expect(controller.aufschlagController.text, '50'); storage.tables[StorageKeys.filamente]![0] = rolle( 'a', 30, '750', ).toJson(); await edit(tester); expect( controller.gesamtpreisController.text, '3,75', ); // Recorded material cost stays stable. controller.aufschlagController.text = '25'; expect(controller.gesamtpreisController.text, '3,13'); await controller.saveBauteil(); await tester.pumpAndSettle(); saved = controller.bauteile.single; expect(saved.aufschlagProzent, 25); expect(saved.gesamtpreis, 3.13); final backup = BackupModel({ for (final key in BackupModel.keys) key: storage.getModelList(key), }); final roundtrip = BauteilModel.fromJson( BackupModel.decode(backup.encode()).data[StorageKeys.bauteile]!.single, ); expect(roundtrip.gesamtpreis, 3.13); expect(roundtrip.materialKosten, 2); expect(roundtrip.aufschlagProzent, 25); await controller.duplicateBauteil( saved, ); // New print uses current roll price, same markup. await tester.pumpAndSettle(); expect(controller.bauteile, hasLength(2)); final copy = controller.bauteile.firstWhere( (item) => item.bauteilId != saved.bauteilId, ); expect(copy.materialKosten, 4); expect(copy.abfallKosten, 1); expect(copy.aufschlagProzent, 25); expect(copy.gesamtpreis, 6.25); expect(tester.takeException(), isNull); }, ); testWidgets( 'Multiple filaments use usage shares and count waste exactly once', (tester) async { await show(tester); fill(); controller.gewichtController.text = '90'; controller.abfallController.text = '10'; controller.toggleFilament('b', true); expect( controller.preisDetails.value, isNull, ); // Need an allocation for both roles. controller.verbrauchController('a').text = '60'; controller.verbrauchController('b').text = '40'; expect(controller.preisDetails.value!.materialKosten, 2.52); expect(controller.preisDetails.value!.abfallKosten, .28); expect(controller.gesamtpreisController.text, '4,20'); await controller.saveBauteil(); await tester.pumpAndSettle(); expect(controller.bauteile.single.gesamtpreis, 4.2); expect(controller.bauteile.single.abfallKosten, .28); expect(tester.takeException(), isNull); }, ); testWidgets('Failed writes retain calculation and edited markup for retry', ( tester, ) async { await show(tester); fill(); controller.aufschlagController.text = '12,5'; storage.failWrites = true; await controller.saveBauteil(); await tester.pumpAndSettle(); expect(controller.bauteile, isEmpty); expect(controller.aufschlagController.text, '12,5'); expect(controller.gesamtpreisController.text, '2,81'); storage.failWrites = false; await controller.saveBauteil(); await tester.pumpAndSettle(); expect(controller.bauteile.single.gesamtpreis, 2.81); expect(tester.takeException(), isNull); }); test('Unknown prices and invalid allocation do not produce a plausible total; old backups remain valid', () { final entry = BauteilModel( bauteilId: 'x', erstelltAm: DateTime(2026), name: 'Alt', verwendung: 'Test', druckdauerMinuten: 1, gewichtGramm: 100, abfallGramm: 10, filamente: [ const BauteilFilamentModel(filamentId: 'missing', verbrauchGramm: 110), ], ); expect(BauteilPreisBerechnung.berechne(entry, [], 10), isNull); final oldJson = entry.toJson() ..remove('aufschlagProzent') ..remove('materialKosten'); final data = { for (final key in BackupModel.keys) key: >[], }; data[StorageKeys.bauteile] = [oldJson]; expect( BackupModel.decode(BackupModel(data).encode()).data[StorageKeys.bauteile], hasLength(1), ); for (final key in ['aufschlagProzent', 'materialKosten']) { data[StorageKeys.bauteile] = [ {...oldJson, key: -1}, ]; expect( () => BackupModel.decode(BackupModel(data).encode()), throwsFormatException, ); } }); }