413 lines
16 KiB
Dart
413 lines
16 KiB
Dart
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/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<GetStorageService>(storage);
|
||
});
|
||
tearDown(() => Get.reset());
|
||
|
||
Future<void> 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.empfohleneDrucktemperaturController.text = '220–260 °C';
|
||
controller.empfohlenerHeizbettbereichController.text = '0–60 °C';
|
||
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.empfohleneDrucktemperatur, '220–260 °C');
|
||
expect(saved.empfohlenerHeizbettbereich, '0–60 °C');
|
||
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(reloaded.druckController.text, '220.5');
|
||
expect(reloaded.empfohleneDrucktemperaturController.text, '220–260 °C');
|
||
expect(reloaded.empfohlenerHeizbettbereichController.text, '0–60 °C');
|
||
expect(reloaded.druckbettController.text, '65.0');
|
||
expect(reloaded.bauraumController.text, '40.0');
|
||
expect(reloaded.bauraumGeschlossen.value, isTrue);
|
||
expect(reloaded.sonstigesController.text, 'Luefter 50 %');
|
||
expect(storage.getModelList(HomeController.storageKey), hasLength(2));
|
||
expect(controller.druckController.text, '220,5');
|
||
expect(find.text('Gespeicherte Profile'), findsNothing);
|
||
expect(tester.takeException(), isNull);
|
||
},
|
||
);
|
||
|
||
testWidgets('Editing replaces the profile and keeps its identity', (
|
||
tester,
|
||
) async {
|
||
final otherProfile = storage
|
||
.getModelList(AnwendungController.storageKey)
|
||
.single;
|
||
final original = profile('profile-a', 'a');
|
||
storage.tables[AnwendungController.storageKey]!.add(original.toJson());
|
||
final controller = Get.put(AnwendungController(filamentId: 'a'));
|
||
await show(tester, const AnwendungView());
|
||
expect(controller.druckController.text, '210.0');
|
||
expect(controller.druckbettController.text, '60.0');
|
||
expect(controller.bauraumController.text, isEmpty);
|
||
|
||
controller.druckController.text = '225';
|
||
controller.sonstigesController.text = 'Aktualisiert';
|
||
storage.failWrites = true;
|
||
await controller.saveAnwendung();
|
||
expect(controller.hasError.value, isTrue);
|
||
expect(controller.anwendungen.single.druckTemperatur, 210);
|
||
expect(controller.druckController.text, '225');
|
||
|
||
storage.failWrites = false;
|
||
await controller.saveAnwendung();
|
||
controller.druckController.text = '230';
|
||
await controller.saveAnwendung();
|
||
final stored = storage.getModelList(AnwendungController.storageKey);
|
||
expect(stored, hasLength(2));
|
||
expect(stored.first, otherProfile);
|
||
final updated = AnwendungModel.fromJson(stored.last);
|
||
expect(updated.anwendungId, original.anwendungId);
|
||
expect(updated.erstelltAm, original.erstelltAm);
|
||
expect(updated.druckTemperatur, 230);
|
||
expect(updated.sonstiges, 'Aktualisiert');
|
||
expect(controller.anwendungen, hasLength(1));
|
||
});
|
||
|
||
testWidgets('Legacy profiles load the latest and save one per filament', (
|
||
tester,
|
||
) async {
|
||
final latest = AnwendungModel(
|
||
anwendungId: 'latest-a',
|
||
filamentId: 'a',
|
||
erstelltAm: DateTime(2026, 10, 27),
|
||
druckbettTemperatur: 70,
|
||
druckTemperatur: 240,
|
||
bauraumGeschlossen: true,
|
||
bauraumTemperatur: 45,
|
||
sonstiges: 'Neuestes Profil',
|
||
);
|
||
storage.tables[AnwendungController.storageKey]!.addAll([
|
||
latest.toJson(),
|
||
profile('older-a', 'a').toJson(),
|
||
]);
|
||
final controller = Get.put(AnwendungController(filamentId: 'a'));
|
||
await show(tester, const AnwendungView());
|
||
expect(controller.druckController.text, '240.0');
|
||
expect(controller.bauraumGeschlossen.value, isTrue);
|
||
expect(controller.sonstigesController.text, 'Neuestes Profil');
|
||
await controller.saveAnwendung();
|
||
final stored = storage.getModelList(AnwendungController.storageKey);
|
||
expect(stored, hasLength(2));
|
||
expect(stored.where((item) => item['filamentId'] == 'a'), hasLength(1));
|
||
expect(stored.last['anwendungId'], 'latest-a');
|
||
});
|
||
|
||
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('Inventory marks saved profiles and updates after creating one', (
|
||
tester,
|
||
) async {
|
||
tester.view.physicalSize = const Size(1280, 1400);
|
||
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();
|
||
final home = Get.find<HomeController>();
|
||
Finder badgeFor(String id) => find.descendant(
|
||
of: find.byKey(ValueKey(id)),
|
||
matching: find.text('Profil erstellt'),
|
||
);
|
||
expect(badgeFor('a'), findsNothing);
|
||
expect(badgeFor('b'), findsOneWidget);
|
||
|
||
await tester.tap(find.byKey(const ValueKey('a')));
|
||
await tester.pumpAndSettle();
|
||
final profileController = Get.find<AnwendungController>();
|
||
profileController.druckController.text = '220';
|
||
profileController.druckbettController.text = '60';
|
||
storage.failWrites = true;
|
||
await profileController.saveAnwendung();
|
||
expect(home.filamentIdsMitProfil, isNot(contains('a')));
|
||
|
||
storage.failWrites = false;
|
||
await profileController.saveAnwendung();
|
||
Get.back<void>();
|
||
await tester.pumpAndSettle();
|
||
expect(badgeFor('a'), findsOneWidget);
|
||
expect(badgeFor('b'), findsOneWidget);
|
||
|
||
// Reloads after imports must also clear badges for removed profiles.
|
||
await storage.saveModelList(AnwendungController.storageKey, []);
|
||
home.loadFilamente();
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('Profil erstellt'), findsNothing);
|
||
expect(tester.takeException(), isNull);
|
||
});
|
||
|
||
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<AnwendungController>().filamentId, 'a');
|
||
Get.find<AnwendungController>().sonstigesController.text =
|
||
'Nicht uebernehmen';
|
||
Get.back<void>();
|
||
await tester.pumpAndSettle();
|
||
expect(Get.isRegistered<AnwendungController>(), isFalse);
|
||
await tester.tap(find.byType(FilamentCard).last);
|
||
await tester.pumpAndSettle();
|
||
expect(Get.find<AnwendungController>().filamentId, 'b');
|
||
expect(Get.find<AnwendungController>().sonstigesController.text, isEmpty);
|
||
expect(Get.find<AnwendungController>().anwendungen, hasLength(1));
|
||
Get.back<void>();
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.byTooltip('Bauteile verwalten'));
|
||
await tester.pumpAndSettle();
|
||
expect(find.byType(BauteilView), findsOneWidget);
|
||
expect(Get.find<BauteilController>().filamente, hasLength(2));
|
||
expect(tester.takeException(), isNull);
|
||
},
|
||
);
|
||
}
|