add some new features

This commit is contained in:
2026-10-29 15:03:48 +01:00
parent c876405a8f
commit 20012db615
39 changed files with 2908 additions and 97 deletions
+250
View File
@@ -0,0 +1,250 @@
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<GetStorageService>(storage);
controller = Get.put(BauteilController());
});
tearDown(() => Get.reset());
Future<void> 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<void> 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<TextField>(
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: <Map<String, dynamic>>[],
};
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,
);
}
});
}