407 lines
16 KiB
Dart
407 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.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(reloaded.druckController.text, '220.5');
|
|
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);
|
|
},
|
|
);
|
|
}
|