import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:ertl_filament_verwaltung/controller/abfall_controller.dart'; import 'package:ertl_filament_verwaltung/controller/bauteil_controller.dart'; import 'package:ertl_filament_verwaltung/controller/home_controller.dart'; import 'package:ertl_filament_verwaltung/helper/abfall_berechnung.dart'; import 'package:ertl_filament_verwaltung/helper/storage_keys.dart'; import 'package:ertl_filament_verwaltung/models/abfall_model.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/abfall_view.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; KaufFilamentModel rolle(String id, double preis, String startgewicht) => KaufFilamentModel( filamentId: id, kaufdatum: DateTime(2026), haendler: 'Shop', marke: id, material: 'PLA', preis: preis, farbe: id, gewicht: startgewicht, ); BauteilModel teil( String id, double abfall, List filamente, { double? kosten, }) => BauteilModel( bauteilId: id, erstelltAm: DateTime(2026, 10, 29), name: id, verwendung: 'Test', druckdauerMinuten: 30, gewichtGramm: 20, abfallGramm: abfall, filamente: filamente, abfallKosten: kosten, ); setUp(() { Get.testMode = true; storage = MemoryStorageService(); storage.tables[StorageKeys.filamente] = [rolle('a', 15, '750').toJson()]; Get.put(storage); }); tearDown(() => Get.reset()); Future showParts(WidgetTester tester) async { final controller = Get.put(BauteilController()); await tester.pumpWidget( GetMaterialApp(theme: AppTheme.light, home: const BauteilView()), ); await tester.pumpAndSettle(); return controller; } void fill(BauteilController controller, {double waste = 5}) { controller.nameController.text = 'Halter'; controller.verwendungController.text = 'Werkstatt'; controller.minutenController.text = '30'; controller.gewichtController.text = '20'; controller.abfallController.text = '$waste'; controller.toggleFilament('a', true); controller.verbrauchController('a').text = '${20 + waste}'; } Future edit( WidgetTester tester, BauteilController controller, BauteilModel part, ) async { final pending = controller.editBauteil(part); await tester.pumpAndSettle(); await pending; } testWidgets( 'Part waste uses original 750g roll despite previous consumption; lifecycle reconciles once', (tester) async { // 600g already consumed: only 150g remain before the next print. storage.tables[StorageKeys.bauteile] = [ teil('vorher', 0, [ const BauteilFilamentModel(filamentId: 'a', verbrauchGramm: 600), ]).toJson(), ]; storage.tables[StorageKeys.abfaelle] = [ AbfallModel( abfallId: 'manual', datumZeit: DateTime(2026), gewichtGramm: 3, kosten: 1, ).toJson(), ]; final home = Get.put(HomeController()); final waste = Get.put(AbfallController()); waste.kilopreisController.text = '999'; // Manual rate must never price a part. final controller = await showParts(tester); fill(controller); await controller.saveBauteil(); await tester.pumpAndSettle(); final saved = controller.bauteile.firstWhere( (entry) => entry.name == 'Halter', ); expect(saved.abfallKosten, .1); // 15 / 750 * 5, not 15 / 150 * 5. expect(home.restgewicht(home.filamente.single), 125); expect(waste.summeAbfall, 8); expect(waste.summeKosten, closeTo(1.1, 1e-9)); waste.loadAbfaelle(); waste.loadAbfaelle(); expect(waste.abfaelle, hasLength(2)); expect(storage.getModelList(StorageKeys.abfaelle), hasLength(1)); final linked = waste.abfaelle.firstWhere((entry) => entry.ausBauteil); await waste.deleteAbfall(linked.abfallId); await waste.editAbfall(linked); expect(waste.abfaelle, hasLength(2)); expect(waste.editingId.value, isNull); await edit(tester, controller, saved); controller.abfallController.text = '10'; controller.verbrauchController('a').text = '30'; await controller.saveBauteil(); await tester.pumpAndSettle(); expect(waste.summeAbfall, 13); expect(waste.summeKosten, closeTo(1.2, 1e-9)); final changed = controller.bauteile.firstWhere( (entry) => entry.bauteilId == saved.bauteilId, ); await controller.duplicateBauteil(changed); await tester.pumpAndSettle(); expect(waste.summeAbfall, 23); expect(waste.summeKosten, closeTo(1.4, 1e-9)); final copy = controller.bauteile.firstWhere( (entry) => entry.bauteilId != saved.bauteilId && entry.name == 'Halter', ); await controller.deleteBauteil(copy.bauteilId); await tester.pumpAndSettle(); expect(waste.summeAbfall, 13); await edit(tester, controller, changed); controller.abfallController.text = '0'; controller.verbrauchController('a').text = '20'; await controller.saveBauteil(); await tester.pumpAndSettle(); expect(waste.abfaelle.single.abfallId, 'manual'); expect(waste.summeAbfall, 3); await controller.deleteBauteil(saved.bauteilId); await tester.pumpAndSettle(); expect(waste.abfaelle.single.abfallId, 'manual'); expect(tester.takeException(), isNull); }, ); testWidgets( 'Frozen waste cost survives price changes, metadata edits, removed rolls and backups', (tester) async { final waste = Get.put(AbfallController()); final controller = await showParts(tester); fill(controller, waste: 25); await controller.saveBauteil(); await tester.pumpAndSettle(); expect(waste.summeKosten, .5); storage.tables[StorageKeys.filamente] = [rolle('a', 30, '750').toJson()]; await edit(tester, controller, controller.bauteile.single); controller.nameController.text = 'Neuer Name'; await controller.saveBauteil(); await tester.pumpAndSettle(); expect(waste.summeKosten, .5); storage.tables[StorageKeys.filamente] = []; waste.loadAbfaelle(); expect(waste.summeKosten, .5); expect(waste.abfaelle.single.kostenUnbekannt, isFalse); final backup = BackupModel({ for (final key in BackupModel.keys) key: storage.getModelList(key), }); final restored = BackupModel.decode(backup.encode()); expect( BauteilModel.fromJson(restored.data[StorageKeys.bauteile]!.single) .abfallKosten, .5, ); restored.data[StorageKeys.bauteile]!.single['abfallKosten'] = -1; expect( () => BackupModel.decode(BackupModel(restored.data).encode()), throwsFormatException, ); expect(tester.takeException(), isNull); }, ); testWidgets( 'Failed part writes create no waste and failed delete keeps linked waste', (tester) async { final waste = Get.put(AbfallController()); final controller = await showParts(tester); fill(controller); storage.failWrites = true; await controller.saveBauteil(); await tester.pumpAndSettle(); expect(waste.abfaelle, isEmpty); expect(controller.nameController.text, 'Halter'); storage.failWrites = false; await controller.saveBauteil(); await tester.pumpAndSettle(); expect(waste.summeKosten, .1); storage.failWrites = true; final deleting = controller.deleteBauteil( controller.bauteile.single.bauteilId, ); await tester.pumpAndSettle(); await deleting; expect(waste.summeKosten, .1); expect(waste.summeAbfall, 5); expect(tester.takeException(), isNull); }, ); test( 'Multi-filament waste uses weighted roll prices and original weights', () { final part = teil('multi', 10, [ const BauteilFilamentModel(filamentId: 'a', verbrauchGramm: 60), const BauteilFilamentModel(filamentId: 'b', verbrauchGramm: 40), ]); // 6g at 20€/kg and 4g at 40€/kg = 0.28€. expect( AbfallBerechnung.kosten(part, [ rolle('a', 15, '0,75 kg'), rolle('b', 20, '500'), ]), .28, ); expect(AbfallBerechnung.kosten(part, [rolle('a', 15, '750')]), isNull); }, ); testWidgets( 'Existing parts appear with origin and unknown historical prices are labelled', (tester) async { storage.tables[StorageKeys.bauteile] = [ teil('Altbestand', 25, [ const BauteilFilamentModel(filamentId: 'a'), ]).toJson(), teil('Fehlende Rolle', 5, [ const BauteilFilamentModel(filamentId: 'deleted'), ]).toJson(), ]; final waste = Get.put(AbfallController()); expect(waste.summeAbfall, 30); expect(waste.summeKosten, .5); expect( waste.abfaelle.where((entry) => entry.kostenUnbekannt), hasLength(1), ); expect( waste.abfaelle.where((entry) => entry.kostenNachtraeglich), hasLength(1), ); 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 AbfallView()), ); await tester.pumpAndSettle(); waste.scrollController.jumpTo( waste.scrollController.position.maxScrollExtent, ); await tester.pumpAndSettle(); expect(find.text('Abfallkosten: nicht ermittelbar'), findsOneWidget); expect(find.byTooltip('Abfalleintrag verwalten'), findsNothing); expect(tester.takeException(), isNull); }, ); }