287 lines
10 KiB
Dart
287 lines
10 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/abfall_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/abfall_berechnung.dart';
|
|
import 'package:ertl_filament_verwaltung/helper/storage_keys.dart';
|
|
import 'package:ertl_filament_verwaltung/models/abfall_model.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/abfall_view.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;
|
|
KaufFilamentModel rolle(String id, double preis, String startgewicht) =>
|
|
KaufFilamentModel(
|
|
filamentId: id,
|
|
kaufdatum: DateTime(2026),
|
|
haendler: 'Shop',
|
|
marke: id,
|
|
material: 'PLA',
|
|
preis: preis,
|
|
farbe: id,
|
|
gewicht: startgewicht,
|
|
);
|
|
BauteilModel teil(
|
|
String id,
|
|
double abfall,
|
|
List<BauteilFilamentModel> filamente, {
|
|
double? kosten,
|
|
}) => BauteilModel(
|
|
bauteilId: id,
|
|
erstelltAm: DateTime(2026, 10, 29),
|
|
name: id,
|
|
verwendung: 'Test',
|
|
druckdauerMinuten: 30,
|
|
gewichtGramm: 20,
|
|
abfallGramm: abfall,
|
|
filamente: filamente,
|
|
abfallKosten: kosten,
|
|
);
|
|
|
|
setUp(() {
|
|
Get.testMode = true;
|
|
storage = MemoryStorageService();
|
|
storage.tables[StorageKeys.filamente] = [rolle('a', 15, '750').toJson()];
|
|
Get.put<GetStorageService>(storage);
|
|
});
|
|
tearDown(() => Get.reset());
|
|
|
|
Future<BauteilController> showParts(WidgetTester tester) async {
|
|
final controller = Get.put(BauteilController());
|
|
await tester.pumpWidget(
|
|
GetMaterialApp(theme: AppTheme.light, home: const BauteilView()),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
return controller;
|
|
}
|
|
|
|
void fill(BauteilController controller, {double waste = 5}) {
|
|
controller.nameController.text = 'Halter';
|
|
controller.verwendungController.text = 'Werkstatt';
|
|
controller.minutenController.text = '30';
|
|
controller.gewichtController.text = '20';
|
|
controller.abfallController.text = '$waste';
|
|
controller.toggleFilament('a', true);
|
|
controller.verbrauchController('a').text = '${20 + waste}';
|
|
}
|
|
|
|
Future<void> edit(
|
|
WidgetTester tester,
|
|
BauteilController controller,
|
|
BauteilModel part,
|
|
) async {
|
|
final pending = controller.editBauteil(part);
|
|
await tester.pumpAndSettle();
|
|
await pending;
|
|
}
|
|
|
|
testWidgets(
|
|
'Part waste uses original 750g roll despite previous consumption; lifecycle reconciles once',
|
|
(tester) async {
|
|
// 600g already consumed: only 150g remain before the next print.
|
|
storage.tables[StorageKeys.bauteile] = [
|
|
teil('vorher', 0, [
|
|
const BauteilFilamentModel(filamentId: 'a', verbrauchGramm: 600),
|
|
]).toJson(),
|
|
];
|
|
storage.tables[StorageKeys.abfaelle] = [
|
|
AbfallModel(
|
|
abfallId: 'manual',
|
|
datumZeit: DateTime(2026),
|
|
gewichtGramm: 3,
|
|
kosten: 1,
|
|
).toJson(),
|
|
];
|
|
final home = Get.put(HomeController());
|
|
final waste = Get.put(AbfallController());
|
|
waste.kilopreisController.text =
|
|
'999'; // Manual rate must never price a part.
|
|
final controller = await showParts(tester);
|
|
fill(controller);
|
|
await controller.saveBauteil();
|
|
await tester.pumpAndSettle();
|
|
final saved = controller.bauteile.firstWhere(
|
|
(entry) => entry.name == 'Halter',
|
|
);
|
|
expect(saved.abfallKosten, .1); // 15 / 750 * 5, not 15 / 150 * 5.
|
|
expect(home.restgewicht(home.filamente.single), 125);
|
|
expect(waste.summeAbfall, 8);
|
|
expect(waste.summeKosten, closeTo(1.1, 1e-9));
|
|
waste.loadAbfaelle();
|
|
waste.loadAbfaelle();
|
|
expect(waste.abfaelle, hasLength(2));
|
|
expect(storage.getModelList(StorageKeys.abfaelle), hasLength(1));
|
|
final linked = waste.abfaelle.firstWhere((entry) => entry.ausBauteil);
|
|
await waste.deleteAbfall(linked.abfallId);
|
|
await waste.editAbfall(linked);
|
|
expect(waste.abfaelle, hasLength(2));
|
|
expect(waste.editingId.value, isNull);
|
|
|
|
await edit(tester, controller, saved);
|
|
controller.abfallController.text = '10';
|
|
controller.verbrauchController('a').text = '30';
|
|
await controller.saveBauteil();
|
|
await tester.pumpAndSettle();
|
|
expect(waste.summeAbfall, 13);
|
|
expect(waste.summeKosten, closeTo(1.2, 1e-9));
|
|
final changed = controller.bauteile.firstWhere(
|
|
(entry) => entry.bauteilId == saved.bauteilId,
|
|
);
|
|
await controller.duplicateBauteil(changed);
|
|
await tester.pumpAndSettle();
|
|
expect(waste.summeAbfall, 23);
|
|
expect(waste.summeKosten, closeTo(1.4, 1e-9));
|
|
final copy = controller.bauteile.firstWhere(
|
|
(entry) => entry.bauteilId != saved.bauteilId && entry.name == 'Halter',
|
|
);
|
|
await controller.deleteBauteil(copy.bauteilId);
|
|
await tester.pumpAndSettle();
|
|
expect(waste.summeAbfall, 13);
|
|
await edit(tester, controller, changed);
|
|
controller.abfallController.text = '0';
|
|
controller.verbrauchController('a').text = '20';
|
|
await controller.saveBauteil();
|
|
await tester.pumpAndSettle();
|
|
expect(waste.abfaelle.single.abfallId, 'manual');
|
|
expect(waste.summeAbfall, 3);
|
|
await controller.deleteBauteil(saved.bauteilId);
|
|
await tester.pumpAndSettle();
|
|
expect(waste.abfaelle.single.abfallId, 'manual');
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Frozen waste cost survives price changes, metadata edits, removed rolls and backups',
|
|
(tester) async {
|
|
final waste = Get.put(AbfallController());
|
|
final controller = await showParts(tester);
|
|
fill(controller, waste: 25);
|
|
await controller.saveBauteil();
|
|
await tester.pumpAndSettle();
|
|
expect(waste.summeKosten, .5);
|
|
storage.tables[StorageKeys.filamente] = [rolle('a', 30, '750').toJson()];
|
|
await edit(tester, controller, controller.bauteile.single);
|
|
controller.nameController.text = 'Neuer Name';
|
|
await controller.saveBauteil();
|
|
await tester.pumpAndSettle();
|
|
expect(waste.summeKosten, .5);
|
|
storage.tables[StorageKeys.filamente] = [];
|
|
waste.loadAbfaelle();
|
|
expect(waste.summeKosten, .5);
|
|
expect(waste.abfaelle.single.kostenUnbekannt, isFalse);
|
|
final backup = BackupModel({
|
|
for (final key in BackupModel.keys) key: storage.getModelList(key),
|
|
});
|
|
final restored = BackupModel.decode(backup.encode());
|
|
expect(
|
|
BauteilModel.fromJson(restored.data[StorageKeys.bauteile]!.single)
|
|
.abfallKosten,
|
|
.5,
|
|
);
|
|
restored.data[StorageKeys.bauteile]!.single['abfallKosten'] = -1;
|
|
expect(
|
|
() => BackupModel.decode(BackupModel(restored.data).encode()),
|
|
throwsFormatException,
|
|
);
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Failed part writes create no waste and failed delete keeps linked waste',
|
|
(tester) async {
|
|
final waste = Get.put(AbfallController());
|
|
final controller = await showParts(tester);
|
|
fill(controller);
|
|
storage.failWrites = true;
|
|
await controller.saveBauteil();
|
|
await tester.pumpAndSettle();
|
|
expect(waste.abfaelle, isEmpty);
|
|
expect(controller.nameController.text, 'Halter');
|
|
storage.failWrites = false;
|
|
await controller.saveBauteil();
|
|
await tester.pumpAndSettle();
|
|
expect(waste.summeKosten, .1);
|
|
storage.failWrites = true;
|
|
final deleting = controller.deleteBauteil(
|
|
controller.bauteile.single.bauteilId,
|
|
);
|
|
await tester.pumpAndSettle();
|
|
await deleting;
|
|
expect(waste.summeKosten, .1);
|
|
expect(waste.summeAbfall, 5);
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'Multi-filament waste uses weighted roll prices and original weights',
|
|
() {
|
|
final part = teil('multi', 10, [
|
|
const BauteilFilamentModel(filamentId: 'a', verbrauchGramm: 60),
|
|
const BauteilFilamentModel(filamentId: 'b', verbrauchGramm: 40),
|
|
]);
|
|
// 6g at 20€/kg and 4g at 40€/kg = 0.28€.
|
|
expect(
|
|
AbfallBerechnung.kosten(part, [
|
|
rolle('a', 15, '0,75 kg'),
|
|
rolle('b', 20, '500'),
|
|
]),
|
|
.28,
|
|
);
|
|
expect(AbfallBerechnung.kosten(part, [rolle('a', 15, '750')]), isNull);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Existing parts appear with origin and unknown historical prices are labelled',
|
|
(tester) async {
|
|
storage.tables[StorageKeys.bauteile] = [
|
|
teil('Altbestand', 25, [
|
|
const BauteilFilamentModel(filamentId: 'a'),
|
|
]).toJson(),
|
|
teil('Fehlende Rolle', 5, [
|
|
const BauteilFilamentModel(filamentId: 'deleted'),
|
|
]).toJson(),
|
|
];
|
|
final waste = Get.put(AbfallController());
|
|
expect(waste.summeAbfall, 30);
|
|
expect(waste.summeKosten, .5);
|
|
expect(
|
|
waste.abfaelle.where((entry) => entry.kostenUnbekannt),
|
|
hasLength(1),
|
|
);
|
|
expect(
|
|
waste.abfaelle.where((entry) => entry.kostenNachtraeglich),
|
|
hasLength(1),
|
|
);
|
|
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 AbfallView()),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
waste.scrollController.jumpTo(
|
|
waste.scrollController.position.maxScrollExtent,
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Abfallkosten: nicht ermittelbar'), findsOneWidget);
|
|
expect(find.byTooltip('Abfalleintrag verwalten'), findsNothing);
|
|
expect(tester.takeException(), isNull);
|
|
},
|
|
);
|
|
}
|