251 lines
8.9 KiB
Dart
251 lines
8.9 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/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,
|
|
);
|
|
}
|
|
});
|
|
}
|