import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:ertl_filament_verwaltung/controller/anwendung_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/input_validators.dart'; import 'package:ertl_filament_verwaltung/helper/sample_routes.dart'; import 'package:ertl_filament_verwaltung/models/anwendung_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/anwendung_view.dart'; import 'package:ertl_filament_verwaltung/pages/bauteil_view.dart'; import 'package:ertl_filament_verwaltung/pages/home_view.dart'; import 'package:ertl_filament_verwaltung/services/get_storage_services.dart'; import 'package:ertl_filament_verwaltung/theme/app_theme.dart'; import 'package:ertl_filament_verwaltung/widgets/filament_card.dart'; import 'package:ertl_filament_verwaltung/widgets/entry_panel.dart'; import 'package:ertl_filament_verwaltung/widgets/bauteil_card.dart'; import 'helpers/memory_storage_service.dart'; void main() { late MemoryStorageService storage; final uuidPattern = RegExp( r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', ); KaufFilamentModel filament(String id, String color) => KaufFilamentModel( filamentId: id, kaufdatum: DateTime(2026, 10, 27), haendler: 'Shop', marke: 'Marke $id', material: 'PLA', preis: 20, farbe: color, gewicht: '1000', ); AnwendungModel profile(String id, String filamentId) => AnwendungModel( anwendungId: id, filamentId: filamentId, erstelltAm: DateTime(2026, 10, 26), druckbettTemperatur: 60, druckTemperatur: 210, bauraumGeschlossen: false, ); setUp(() { Get.testMode = true; storage = MemoryStorageService(); storage.tables[HomeController.storageKey] = [ filament('a', 'Rot').toJson(), filament('b', 'Blau').toJson(), ]; storage.tables[AnwendungController.storageKey] = [ profile('profile-b', 'b').toJson(), ]; Get.put(storage); }); tearDown(() => Get.reset()); Future show( WidgetTester tester, Widget page, { bool narrow = false, }) async { tester.view.physicalSize = narrow ? const Size(320, 640) : const Size(1100, 1400); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(GetMaterialApp(theme: AppTheme.light, home: page)); await tester.pumpAndSettle(); } void fillPart(BauteilController controller) { controller.nameController.text = 'Zweifarbiger Halter'; controller.verwendungController.text = 'Werkstatt'; controller.stundenController.text = '2'; controller.minutenController.text = '35'; controller.gewichtController.text = '42,5'; controller.abfallController.text = '8,2'; controller.verbrauchController('a').text = '50,7'; controller.verbrauchController('b').text = '20,7'; } test( 'Numeric validation accepts comma decimals and rejects invalid values', () { expect(InputValidators.number('42,5'), 42.5); for (final value in ['NaN', 'Infinity', '-1', 'abc']) { expect(InputValidators.nonNegative(value), isNotNull); } expect(InputValidators.positive('0'), isNotNull); expect(InputValidators.nonNegative('0'), isNull); expect(InputValidators.nonNegative('', optional: true), isNull); expect(InputValidators.minutes('60'), isNotNull); expect(InputValidators.hours('1,5'), isNotNull); }, ); testWidgets( 'Profiles persist selected filament UUID and preserve other profiles', (tester) async { final controller = Get.put(AnwendungController(filamentId: 'a')); await show(tester, const AnwendungView(), narrow: true); controller.druckController.text = '220,5'; controller.druckbettController.text = '65'; controller.bauraumController.text = '40'; controller.bauraumGeschlossen.value = true; controller.sonstigesController.text = 'Luefter 50 %'; await controller.saveAnwendung(); await tester.pumpAndSettle(); final stored = storage.getModelList(AnwendungController.storageKey); expect(stored, hasLength(2)); final saved = AnwendungModel.fromJson(stored.last); expect(saved.anwendungId, matches(uuidPattern)); expect(saved.filamentId, 'a'); expect(saved.druckTemperatur, 220.5); expect(saved.druckbettTemperatur, 65); expect(saved.bauraumGeschlossen, isTrue); expect(saved.bauraumTemperatur, 40); expect(saved.sonstiges, 'Luefter 50 %'); expect(controller.anwendungen, hasLength(1)); final reloaded = Get.put( AnwendungController(filamentId: 'a'), tag: 'reload', ); expect(reloaded.anwendungen.single.anwendungId, saved.anwendungId); expect(storage.getModelList(HomeController.storageKey), hasLength(2)); await tester.scrollUntilVisible( find.byType(SavedEntryCard), 200, scrollable: find.byType(Scrollable).first, ); await tester.pumpAndSettle(); expect(find.text('Bauraum geschlossen: Ja'), findsOneWidget); expect(tester.takeException(), isNull); }, ); testWidgets('Profile write failure retains input and does not add history', ( tester, ) async { final controller = Get.put(AnwendungController(filamentId: 'a')); await show(tester, const AnwendungView()); controller.druckController.text = '220'; controller.druckbettController.text = '0'; storage.failWrites = true; await controller.saveAnwendung(); expect(controller.hasError.value, isTrue); expect(controller.druckController.text, '220'); expect(controller.anwendungen, isEmpty); expect(controller.isSaving.value, isFalse); storage.failWrites = false; await controller.saveAnwendung(); expect(controller.anwendungen.single.bauraumTemperatur, isNull); expect(controller.anwendungen.single.bauraumGeschlossen, isFalse); }); testWidgets('Missing filament cannot create orphan profiles', (tester) async { final controller = Get.put(AnwendungController(filamentId: 'missing')); await show(tester, const AnwendungView()); await controller.saveAnwendung(); expect(controller.anwendungen, isEmpty); expect(storage.getModelList(AnwendungController.storageKey), hasLength(1)); expect(find.textContaining('nicht gefunden'), findsOneWidget); }); testWidgets( 'Multicolor part persists both filaments and matching processing profile', (tester) async { final controller = Get.put(BauteilController()); await show(tester, const BauteilView(), narrow: true); fillPart(controller); controller.toggleFilament('a', true); controller.toggleFilament('b', true); controller.verbrauchController('a').text = '30'; controller.selectProfile('a', 'profile-b'); expect(controller.selectedFilamente['a'], isNull); controller.selectProfile('b', 'profile-b'); await tester.pumpAndSettle(); await controller.saveBauteil(); await tester.pumpAndSettle(); final stored = storage.getModelList(BauteilController.storageKey); final saved = BauteilModel.fromJson(stored.single); expect(saved.bauteilId, matches(uuidPattern)); expect(saved.name, 'Zweifarbiger Halter'); expect(saved.verwendung, 'Werkstatt'); expect(saved.druckdauerMinuten, 155); expect(saved.gewichtGramm, 42.5); expect(saved.abfallGramm, 8.2); expect(saved.gesamtverbrauchGramm, closeTo(50.7, 0.001)); expect(saved.filamente.map((item) => item.filamentId), ['a', 'b']); expect(saved.filamente.last.anwendungId, 'profile-b'); expect(controller.selectedFilamente, isEmpty); final reloaded = Get.put(BauteilController(), tag: 'reload'); expect(reloaded.bauteile.single.filamente, hasLength(2)); expect( storage.getModelList(AnwendungController.storageKey), hasLength(1), ); await tester.scrollUntilVisible( find.byType(BauteilCard), 250, scrollable: find.byType(Scrollable).first, ); await tester.pumpAndSettle(); expect(find.text('Zweifarbiger Halter'), findsOneWidget); expect(find.text('Mehrfarbdruck ยท 2 Filamente'), findsOneWidget); expect(tester.takeException(), isNull); }, ); testWidgets('Part requires filament, positive duration and valid weights', ( tester, ) async { final controller = Get.put(BauteilController()); await show(tester, const BauteilView()); fillPart(controller); await controller.saveBauteil(); expect(controller.message.value, contains('mindestens ein Filament')); controller.toggleFilament('a', true); controller.stundenController.text = '0'; controller.minutenController.text = '0'; await controller.saveBauteil(); expect(controller.message.value, contains('mindestens eine Minute')); controller.minutenController.text = '30'; controller.abfallController.text = '-1'; await controller.saveBauteil(); expect(storage.getModelList(BauteilController.storageKey), isEmpty); expect(controller.nameController.text, isNotEmpty); }); testWidgets('Part write failure keeps selection and inputs for retry', ( tester, ) async { final controller = Get.put(BauteilController()); await show(tester, const BauteilView()); fillPart(controller); controller.toggleFilament('a', true); storage.failWrites = true; await controller.saveBauteil(); expect(controller.hasError.value, isTrue); expect(controller.bauteile, isEmpty); expect(controller.nameController.text, 'Zweifarbiger Halter'); expect(controller.selectedFilamente.keys, ['a']); expect(controller.isSaving.value, isFalse); }); testWidgets( 'Navigation opens a fresh controller for each selected filament', (tester) async { tester.view.physicalSize = const Size(1280, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget( GetMaterialApp( theme: AppTheme.light, initialRoute: HomeView.routeName, getPages: SampleRouts.samplePages, ), ); await tester.pumpAndSettle(); await tester.tap(find.byType(FilamentCard).first); await tester.pumpAndSettle(); expect(Get.find().filamentId, 'a'); Get.find().sonstigesController.text = 'Nicht uebernehmen'; Get.back(); await tester.pumpAndSettle(); expect(Get.isRegistered(), isFalse); await tester.tap(find.byType(FilamentCard).last); await tester.pumpAndSettle(); expect(Get.find().filamentId, 'b'); expect(Get.find().sonstigesController.text, isEmpty); expect(Get.find().anwendungen, hasLength(1)); Get.back(); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Bauteile verwalten')); await tester.pumpAndSettle(); expect(find.byType(BauteilView), findsOneWidget); expect(Get.find().filamente, hasLength(2)); expect(tester.takeException(), isNull); }, ); }