add some new features
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
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/helper/storage_keys.dart';
|
||||
import 'package:ertl_filament_verwaltung/main.dart';
|
||||
import 'package:ertl_filament_verwaltung/models/abfall_model.dart';
|
||||
import 'package:ertl_filament_verwaltung/models/backup_model.dart';
|
||||
import 'package:ertl_filament_verwaltung/pages/abfall_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 'helpers/memory_storage_service.dart';
|
||||
|
||||
void main() {
|
||||
late MemoryStorageService storage;
|
||||
setUp(() {
|
||||
Get.testMode = true;
|
||||
storage = MemoryStorageService();
|
||||
Get.put<GetStorageService>(storage);
|
||||
});
|
||||
tearDown(() => Get.reset());
|
||||
|
||||
Future<AbfallController> show(WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(320, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final controller = Get.put(AbfallController());
|
||||
await tester.pumpWidget(
|
||||
GetMaterialApp(theme: AppTheme.light, home: const AbfallView()),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
return controller;
|
||||
}
|
||||
|
||||
testWidgets('Home opens waste management and back returns home at 320px', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(320, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
await tester.pumpWidget(const MyApp());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byTooltip('Bauteile verwalten'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Abfall verwalten'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(Get.currentRoute, AbfallView.routeName);
|
||||
expect(find.text('Abfallverwaltung'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Zurück'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(Get.currentRoute, HomeView.routeName);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Save, edit, reload and delete keep totals and timestamps consistent',
|
||||
(tester) async {
|
||||
final controller = await show(tester);
|
||||
final date = DateTime(2026, 10, 28, 14, 35);
|
||||
controller.setDatum(date);
|
||||
controller.setUhrzeit(const TimeOfDay(hour: 14, minute: 35));
|
||||
controller.gewichtController.text = '12,5';
|
||||
controller.kostenController.text = '1,25';
|
||||
await controller.saveAbfall();
|
||||
await tester.pumpAndSettle();
|
||||
final first = controller.abfaelle.single;
|
||||
expect(first.datumZeit, date);
|
||||
expect(controller.summeAbfall, 12.5);
|
||||
expect(controller.summeKosten, 1.25);
|
||||
|
||||
controller.datumZeit.value = date.add(const Duration(days: 1));
|
||||
controller.gewichtController.text = '7,5';
|
||||
controller.kostenController.text = '0,75';
|
||||
await controller.saveAbfall();
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.abfaelle.first.datumZeit.isAfter(date), isTrue);
|
||||
expect(controller.summeAbfall, 20);
|
||||
expect(controller.summeKosten, 2);
|
||||
controller.scrollController.jumpTo(
|
||||
controller.scrollController.position.maxScrollExtent,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Summe Abfall'), findsOneWidget);
|
||||
expect(find.text('20 g'), findsOneWidget);
|
||||
|
||||
final editing = controller.editAbfall(first);
|
||||
await tester.pumpAndSettle();
|
||||
await editing;
|
||||
controller.gewichtController.text = '15';
|
||||
controller.kostenController.text = '1,50';
|
||||
await controller.saveAbfall();
|
||||
await tester.pumpAndSettle();
|
||||
controller.loadAbfaelle();
|
||||
expect(controller.abfaelle.length, 2);
|
||||
expect(controller.summeAbfall, 22.5);
|
||||
expect(controller.summeKosten, 2.25);
|
||||
expect(controller.abfaelle.last.abfallId, first.abfallId);
|
||||
await controller.deleteAbfall(first.abfallId);
|
||||
await tester.pumpAndSettle();
|
||||
final reloaded = AbfallController()..onInit();
|
||||
expect(reloaded.abfaelle.single.gewichtGramm, 7.5);
|
||||
expect(reloaded.summeKosten, .75);
|
||||
reloaded.onClose();
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('Kilopreis calculates from grams while costs remain editable', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = await show(tester);
|
||||
Future<void> enter(TextEditingController field, String value) async {
|
||||
final finder = find.byWidgetPredicate(
|
||||
(widget) => widget is TextFormField && widget.controller == field,
|
||||
);
|
||||
await tester.ensureVisible(finder);
|
||||
await tester.enterText(finder, value);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
await enter(controller.kilopreisController, '20');
|
||||
await enter(controller.gewichtController, '125');
|
||||
expect(controller.kostenController.text, '2,50');
|
||||
await enter(controller.kostenController, '3,10');
|
||||
controller.setUhrzeit(const TimeOfDay(hour: 10, minute: 30));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.kostenController.text, '3,10');
|
||||
await enter(controller.gewichtController, '200');
|
||||
expect(controller.kostenController.text, '4,00');
|
||||
await enter(controller.kilopreisController, '30,5');
|
||||
expect(controller.kostenController.text, '6,10');
|
||||
await enter(controller.kostenController, '5,55');
|
||||
await controller.saveAbfall();
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.abfaelle.single.kosten, 5.55);
|
||||
expect(controller.kilopreisController.text, '30,5');
|
||||
final editing = controller.editAbfall(controller.abfaelle.single);
|
||||
await tester.pumpAndSettle();
|
||||
await editing;
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.kostenController.text, '5,55');
|
||||
await enter(controller.kilopreisController, '0');
|
||||
expect(controller.kostenController.text, '0,00');
|
||||
await enter(controller.kilopreisController, '');
|
||||
await enter(controller.gewichtController, '250');
|
||||
await enter(controller.kostenController, '2,75');
|
||||
await controller.saveAbfall();
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.abfaelle.single.kosten, 2.75);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
testWidgets('Invalid inputs and failed writes preserve entries and form', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = await show(tester);
|
||||
for (final weight in ['', '-1', '0', 'NaN', 'Infinity']) {
|
||||
controller.gewichtController.text = weight;
|
||||
controller.kostenController.text = '1';
|
||||
await controller.saveAbfall();
|
||||
expect(controller.abfaelle, isEmpty);
|
||||
}
|
||||
controller.gewichtController.text = '10';
|
||||
for (final cost in ['', '-1', 'NaN', 'Infinity']) {
|
||||
controller.kostenController.text = cost;
|
||||
await controller.saveAbfall();
|
||||
expect(controller.abfaelle, isEmpty);
|
||||
}
|
||||
controller.kostenController.text = '0';
|
||||
storage.failWrites = true;
|
||||
await controller.saveAbfall();
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.hasError.value, isTrue);
|
||||
expect(controller.gewichtController.text, '10');
|
||||
expect(controller.abfaelle, isEmpty);
|
||||
storage.failWrites = false;
|
||||
await controller.saveAbfall();
|
||||
await tester.pumpAndSettle();
|
||||
final saved = controller.abfaelle.single;
|
||||
storage.failWrites = true;
|
||||
await controller.deleteAbfall(saved.abfallId);
|
||||
expect(controller.abfaelle.single.abfallId, saved.abfallId);
|
||||
expect(controller.summeAbfall, 10);
|
||||
expect(storage.getModelList(StorageKeys.abfaelle), hasLength(1));
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'Backups round-trip waste, accept older backups and reject invalid waste',
|
||||
() {
|
||||
final entry = AbfallModel(
|
||||
abfallId: 'w1',
|
||||
datumZeit: DateTime(2026, 10, 29, 9, 5),
|
||||
gewichtGramm: 25,
|
||||
kosten: 1.5,
|
||||
);
|
||||
final data = {
|
||||
for (final key in BackupModel.keys) key: <Map<String, dynamic>>[],
|
||||
};
|
||||
data[StorageKeys.abfaelle] = [entry.toJson()];
|
||||
final restored = BackupModel.decode(BackupModel(data).encode());
|
||||
expect(restored.data[StorageKeys.abfaelle], [entry.toJson()]);
|
||||
final legacy = {...data}..remove(StorageKeys.abfaelle);
|
||||
expect(
|
||||
BackupModel.decode(BackupModel(legacy).encode())
|
||||
.data[StorageKeys.abfaelle],
|
||||
isEmpty,
|
||||
);
|
||||
data[StorageKeys.abfaelle] = [
|
||||
{...entry.toJson(), 'kosten': -1},
|
||||
];
|
||||
expect(
|
||||
() => BackupModel.decode(BackupModel(data).encode()),
|
||||
throwsFormatException,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
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);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
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/controller/bauteil_auswertung_controller.dart';
|
||||
import 'package:ertl_filament_verwaltung/helper/storage_keys.dart';
|
||||
import 'package:ertl_filament_verwaltung/main.dart';
|
||||
import 'package:ertl_filament_verwaltung/models/bauteil_auswertung_model.dart';
|
||||
import 'package:ertl_filament_verwaltung/models/bauteil_model.dart';
|
||||
import 'package:ertl_filament_verwaltung/pages/bauteil_auswertung_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;
|
||||
BauteilModel part(String id, DateTime date, double? price) => BauteilModel(
|
||||
bauteilId: id,
|
||||
erstelltAm: date,
|
||||
name: 'Bauteil $id',
|
||||
verwendung: 'Werkstatt',
|
||||
druckdauerMinuten: 30,
|
||||
gewichtGramm: 100,
|
||||
abfallGramm: 10,
|
||||
gesamtpreis: price,
|
||||
filamente: [
|
||||
const BauteilFilamentModel(filamentId: 'rolle', verbrauchGramm: 110),
|
||||
],
|
||||
);
|
||||
setUp(() {
|
||||
Get.testMode = true;
|
||||
storage = MemoryStorageService();
|
||||
storage.tables[StorageKeys.bauteile] = [
|
||||
part('alt', DateTime(2025, 12, 31, 23), 10).toJson(),
|
||||
part('vormittag', DateTime(2026, 1, 2, 8), 2.5).toJson(),
|
||||
part('nachmittag', DateTime(2026, 1, 2, 16), 3.25).toJson(),
|
||||
part('januar', DateTime(2026, 1, 3, 12), 0).toJson(),
|
||||
part('ohne', DateTime(2026, 2, 1, 8), null).toJson(),
|
||||
];
|
||||
Get.put<GetStorageService>(storage);
|
||||
});
|
||||
tearDown(() => Get.reset());
|
||||
|
||||
test('Groups and sums all stored parts by year, month and day with unknown prices labelled', () {
|
||||
final controller = Get.put(BauteilAuswertungController());
|
||||
expect(controller.zeitraum.value, BauteilZeitraum.monat);
|
||||
expect(controller.bauteile.map((item) => item.bauteilId), [
|
||||
'ohne',
|
||||
'januar',
|
||||
'nachmittag',
|
||||
'vormittag',
|
||||
'alt',
|
||||
]);
|
||||
expect(controller.gesamtsumme, 15.75);
|
||||
expect(controller.ohnePreis, 1);
|
||||
expect(controller.gruppen.map((group) => group.summe), [0, 5.75, 10]);
|
||||
expect(controller.gruppen.first.ohnePreis, 1);
|
||||
expect(
|
||||
controller.gruppenTitel(controller.gruppen[1].beginn),
|
||||
'Monat 01.2026',
|
||||
);
|
||||
controller.waehleZeitraum(BauteilZeitraum.jahr);
|
||||
expect(controller.gruppen.map((group) => group.summe), [5.75, 10]);
|
||||
expect(controller.gruppen.first.bauteile, hasLength(4));
|
||||
expect(
|
||||
controller.gruppenTitel(controller.gruppen.first.beginn),
|
||||
'Jahr 2026',
|
||||
);
|
||||
controller.waehleZeitraum(BauteilZeitraum.tag);
|
||||
expect(controller.gruppen, hasLength(4));
|
||||
final sameDay = controller.gruppen.firstWhere(
|
||||
(group) => group.beginn == DateTime(2026, 1, 2),
|
||||
);
|
||||
expect(sameDay.summe, 5.75);
|
||||
expect(sameDay.bauteile.map((item) => item.bauteilId), [
|
||||
'nachmittag',
|
||||
'vormittag',
|
||||
]);
|
||||
expect(controller.gruppenTitel(sameDay.beginn), '02.01.2026');
|
||||
expect(
|
||||
controller.gruppen.fold<int>(
|
||||
0,
|
||||
(sum, group) => sum + group.bauteile.length,
|
||||
),
|
||||
5,
|
||||
);
|
||||
expect(storage.getModelList(StorageKeys.bauteile), hasLength(5));
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Appbar opens report, selection works at 320px and back retains part draft',
|
||||
(tester) async {
|
||||
tester.view.physicalSize = const Size(320, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
await tester.pumpWidget(const MyApp());
|
||||
await tester.pumpAndSettle();
|
||||
Get.toNamed(BauteilView.routeName);
|
||||
await tester.pumpAndSettle();
|
||||
final parts = Get.find<BauteilController>();
|
||||
parts.nameController.text = 'Mein Entwurf';
|
||||
await tester.tap(find.byTooltip('Bauteile auswerten'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(Get.currentRoute, BauteilAuswertungView.routeName);
|
||||
expect(find.text('Gesamtsumme'), findsOneWidget);
|
||||
expect(find.text('Bauteilauswertung'), findsOneWidget);
|
||||
final report = Get.find<BauteilAuswertungController>();
|
||||
expect(report.gruppen, hasLength(3));
|
||||
await tester.tap(find.text('Jahr'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(report.gruppen, hasLength(2));
|
||||
expect(find.text('Jahr 2026'), findsOneWidget);
|
||||
await tester.tap(find.text('Tag'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(report.gruppen, hasLength(4));
|
||||
tester.view.physicalSize = const Size(1280, 900);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.tap(find.byTooltip('Zurück zu den Bauteilen'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(Get.currentRoute, BauteilView.routeName);
|
||||
expect(parts.nameController.text, 'Mein Entwurf');
|
||||
expect(Get.isRegistered<BauteilAuswertungController>(), isFalse);
|
||||
storage.tables[StorageKeys.bauteile]!.add(
|
||||
part('neu', DateTime(2027), 4.25).toJson(),
|
||||
);
|
||||
await tester.tap(find.byTooltip('Bauteile auswerten'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(Get.find<BauteilAuswertungController>().gesamtsumme, 20);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Every part is reachable by scrolling and grouping never changes total',
|
||||
(tester) async {
|
||||
tester.view.physicalSize = const Size(320, 640);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final controller = Get.put(BauteilAuswertungController());
|
||||
await tester.pumpWidget(
|
||||
GetMaterialApp(
|
||||
theme: AppTheme.light,
|
||||
home: const BauteilAuswertungView(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
for (final period in BauteilZeitraum.values) {
|
||||
controller.waehleZeitraum(period);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Bauteil alt'),
|
||||
350,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Bauteil alt'), findsOneWidget);
|
||||
expect(controller.gesamtsumme, 15.75);
|
||||
expect(tester.takeException(), isNull);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Empty report shows zero and an empty message for all groupings',
|
||||
(tester) async {
|
||||
storage.tables.clear();
|
||||
final controller = Get.put(BauteilAuswertungController());
|
||||
await tester.pumpWidget(
|
||||
GetMaterialApp(
|
||||
theme: AppTheme.light,
|
||||
home: const BauteilAuswertungView(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
for (final period in BauteilZeitraum.values) {
|
||||
controller.waehleZeitraum(period);
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.gesamtsumme, 0);
|
||||
expect(controller.gruppen, isEmpty);
|
||||
expect(find.text('Noch keine Bauteile gespeichert.'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -143,6 +143,12 @@ void main() {
|
||||
Future<void> add(String id) async {
|
||||
await tester.tap(find.byType(DropdownButtonFormField<String>));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text(
|
||||
'Restgewicht: ${DisplayFormats.number(controller.restgewichtGramm(id)!)} g',
|
||||
),
|
||||
findsWidgets,
|
||||
);
|
||||
await tester.tap(find.text(controller.filamentLabel(id)).last);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
@@ -201,7 +207,6 @@ void main() {
|
||||
(tester) async {
|
||||
await show(tester);
|
||||
fill();
|
||||
controller.gesamtpreisController.text = '12,50';
|
||||
photos.result = png;
|
||||
await controller.pickPhoto();
|
||||
await controller.saveBauteil();
|
||||
@@ -209,19 +214,19 @@ void main() {
|
||||
expect(remaining('b'), 960);
|
||||
final originalId = controller.bauteile.single.bauteilId;
|
||||
final originalDate = controller.bauteile.single.erstelltAm;
|
||||
expect(controller.bauteile.single.gesamtpreis, 12.5);
|
||||
expect(controller.bauteile.single.gesamtpreis, 3);
|
||||
expect(
|
||||
storage
|
||||
.getModelList(BauteilController.storageKey)
|
||||
.single['gesamtpreis'],
|
||||
12.5,
|
||||
3,
|
||||
);
|
||||
expect(controller.gesamtpreisController.text, isEmpty);
|
||||
|
||||
await edit(tester, controller.bauteile.single);
|
||||
expect(controller.foto.value, png);
|
||||
expect(controller.gesamtpreisController.text, '12,5');
|
||||
controller.gesamtpreisController.text = '18,90';
|
||||
expect(controller.gesamtpreisController.text, '3,00');
|
||||
controller.aufschlagController.text = '20';
|
||||
expect(controller.verfuegbarGramm('a'), 1000);
|
||||
controller.verbrauchController('a').text = '20';
|
||||
controller.verbrauchController('b').text = '80';
|
||||
@@ -242,7 +247,7 @@ void main() {
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
controller.bauteile.every((item) => item.gesamtpreis == 18.9),
|
||||
controller.bauteile.every((item) => item.gesamtpreis == 2.4),
|
||||
isTrue,
|
||||
);
|
||||
expect(remaining('a'), 960);
|
||||
@@ -255,7 +260,7 @@ void main() {
|
||||
expect(remaining('a'), 980);
|
||||
final reloaded = Get.put(BauteilController(), tag: 'reload');
|
||||
expect(reloaded.bauteile.single.fotoBytes, png);
|
||||
expect(reloaded.bauteile.single.gesamtpreis, 18.9);
|
||||
expect(reloaded.bauteile.single.gesamtpreis, 2.4);
|
||||
expect(reloaded.bauteile.single.filamente.last.verbrauchGramm, 80);
|
||||
// Purchases retain the original weight; the persisted ledger is deducted.
|
||||
expect(
|
||||
@@ -285,27 +290,27 @@ void main() {
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Total price rejects invalid amounts, accepts zero and can be cleared',
|
||||
'Markup rejects invalid amounts, accepts zero and price is always calculated',
|
||||
(tester) async {
|
||||
await show(tester);
|
||||
fill();
|
||||
for (final value in ['-1', 'NaN', 'Infinity', 'abc']) {
|
||||
controller.gesamtpreisController.text = value;
|
||||
controller.aufschlagController.text = value;
|
||||
await controller.saveBauteil();
|
||||
expect(storage.getModelList(BauteilController.storageKey), isEmpty);
|
||||
}
|
||||
controller.gesamtpreisController.text = '0';
|
||||
controller.aufschlagController.text = '0';
|
||||
await controller.saveBauteil();
|
||||
expect(controller.bauteile.single.gesamtpreis, 0);
|
||||
expect(controller.bauteile.single.gesamtpreis, 2);
|
||||
await edit(tester, controller.bauteile.single);
|
||||
controller.gesamtpreisController.clear();
|
||||
controller.aufschlagController.text = '25';
|
||||
await controller.saveBauteil();
|
||||
expect(controller.bauteile.single.gesamtpreis, isNull);
|
||||
expect(controller.bauteile.single.gesamtpreis, 2.5);
|
||||
expect(
|
||||
storage
|
||||
.getModelList(BauteilController.storageKey)
|
||||
.single['gesamtpreis'],
|
||||
isNull,
|
||||
2.5,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -322,6 +327,7 @@ void main() {
|
||||
expect(remaining('a'), 40);
|
||||
await edit(tester, first);
|
||||
expect(controller.verfuegbarGramm('a'), 940);
|
||||
expect(controller.restgewichtGramm('a'), 40);
|
||||
controller.gewichtController.text = '940';
|
||||
controller.verbrauchController('a').text = '950';
|
||||
await controller.saveBauteil();
|
||||
@@ -402,7 +408,6 @@ void main() {
|
||||
(tester) async {
|
||||
await show(tester, narrow: true);
|
||||
fill();
|
||||
controller.gesamtpreisController.text = '12,50';
|
||||
photos.result = png;
|
||||
await controller.pickPhoto();
|
||||
await controller.saveBauteil();
|
||||
@@ -414,7 +419,7 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text('Gesamtpreis ${DisplayFormats.currency(12.5)}'),
|
||||
find.text('Gesamtpreis ${DisplayFormats.currency(3)}'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -57,6 +57,7 @@ void main() {
|
||||
late ExportController controller;
|
||||
|
||||
Map<String, List<Map<String, dynamic>>> fixture() => {
|
||||
StorageKeys.abfaelle: [],
|
||||
StorageKeys.filamente: [
|
||||
KaufFilamentModel(
|
||||
filamentId: 'f1',
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:ertl_filament_verwaltung/controller/home_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/home_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 HomeController controller;
|
||||
setUp(() {
|
||||
Get.testMode = true;
|
||||
storage = MemoryStorageService();
|
||||
Get.put<GetStorageService>(storage);
|
||||
controller = Get.put(HomeController());
|
||||
});
|
||||
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 HomeView()),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void fill() {
|
||||
controller.haendlerController.text = 'Shop';
|
||||
controller.markeController.text = 'Marke';
|
||||
controller.farbeController.text = 'Blau';
|
||||
controller.preisController.text = '20';
|
||||
controller.gewichtController.text = '1000';
|
||||
}
|
||||
|
||||
Future<void> edit(WidgetTester tester, KaufFilamentModel filament) async {
|
||||
final pending = controller.editFilament(filament);
|
||||
await tester.pumpAndSettle();
|
||||
await pending;
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'Gross weight saves, reloads, edits, duplicates and can be cleared independently',
|
||||
(tester) async {
|
||||
await show(tester);
|
||||
fill();
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is TextFormField &&
|
||||
widget.controller == controller.bruttoGewichtController,
|
||||
);
|
||||
await tester.ensureVisible(field);
|
||||
await tester.enterText(field, '1250,5');
|
||||
await controller.saveFilament();
|
||||
await tester.pumpAndSettle();
|
||||
var saved = controller.filamente.single;
|
||||
expect(saved.gewichtGramm, 1000);
|
||||
expect(saved.bruttoGewichtGramm, 1250.5);
|
||||
expect(controller.bruttoGewichtController.text, isEmpty);
|
||||
controller.loadFilamente();
|
||||
expect(controller.filamente.single.bruttoGewichtGramm, 1250.5);
|
||||
await edit(tester, saved);
|
||||
expect(controller.bruttoGewichtController.text, '1250,5');
|
||||
controller.bruttoGewichtController.text = '1300';
|
||||
await controller.saveFilament();
|
||||
await tester.pumpAndSettle();
|
||||
saved = controller.filamente.single;
|
||||
await controller.duplicateFilament(saved);
|
||||
expect(controller.filamente, hasLength(2));
|
||||
expect(
|
||||
controller.filamente.map((item) => item.filamentId).toSet(),
|
||||
hasLength(2),
|
||||
);
|
||||
expect(
|
||||
controller.filamente.every((item) => item.bruttoGewichtGramm == 1300),
|
||||
isTrue,
|
||||
);
|
||||
await edit(tester, saved);
|
||||
controller.bruttoGewichtController.clear();
|
||||
await controller.saveFilament();
|
||||
await tester.pumpAndSettle();
|
||||
final changed = controller.filamente.firstWhere(
|
||||
(item) => item.filamentId == saved.filamentId,
|
||||
);
|
||||
expect(changed.bruttoGewichtGramm, isNull);
|
||||
expect(changed.gewichtGramm, 1000);
|
||||
expect(
|
||||
controller.filamente
|
||||
.firstWhere((item) => item.filamentId != saved.filamentId)
|
||||
.bruttoGewichtGramm,
|
||||
1300,
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Gross weight must be finite, positive and at least net weight; leaving it empty is allowed',
|
||||
(tester) async {
|
||||
await show(tester);
|
||||
fill();
|
||||
for (final invalid in ['999', '0', '-1', 'NaN', 'Infinity', 'abc']) {
|
||||
controller.bruttoGewichtController.text = invalid;
|
||||
await controller.saveFilament();
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.filamente, isEmpty);
|
||||
expect(storage.getModelList(StorageKeys.filamente), isEmpty);
|
||||
}
|
||||
controller.bruttoGewichtController.clear();
|
||||
await controller.saveFilament();
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.filamente.single.bruttoGewichtGramm, isNull);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test('Gross weight is backed up while old backups without gross weight still load', () {
|
||||
final rolle = KaufFilamentModel(
|
||||
filamentId: 'a',
|
||||
kaufdatum: DateTime(2026),
|
||||
haendler: 'Shop',
|
||||
marke: 'Marke',
|
||||
material: 'PLA',
|
||||
preis: 20,
|
||||
farbe: 'Blau',
|
||||
gewicht: '1kg',
|
||||
bruttoGewichtGramm: 1250,
|
||||
);
|
||||
final data = {
|
||||
for (final key in BackupModel.keys) key: <Map<String, dynamic>>[],
|
||||
};
|
||||
data[StorageKeys.filamente] = [rolle.toJson()];
|
||||
final restored = BackupModel.decode(BackupModel(data).encode());
|
||||
final model = KaufFilamentModel.fromJson(
|
||||
restored.data[StorageKeys.filamente]!.single,
|
||||
);
|
||||
expect(model.bruttoGewichtGramm, 1250);
|
||||
expect(model.gewichtGramm, 1000);
|
||||
data[StorageKeys.filamente]!.single.remove('bruttoGewichtGramm');
|
||||
expect(
|
||||
KaufFilamentModel.fromJson(
|
||||
BackupModel.decode(BackupModel(data).encode())
|
||||
.data[StorageKeys.filamente]!
|
||||
.single,
|
||||
).bruttoGewichtGramm,
|
||||
isNull,
|
||||
);
|
||||
for (final invalid in [999, 0, -1, 'invalid']) {
|
||||
data[StorageKeys.filamente]!.single['bruttoGewichtGramm'] = invalid;
|
||||
expect(
|
||||
() => BackupModel.decode(BackupModel(data).encode()),
|
||||
throwsFormatException,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'Stock and all price calculations use only original net material weight',
|
||||
() {
|
||||
final rolle = KaufFilamentModel(
|
||||
filamentId: 'a',
|
||||
kaufdatum: DateTime(2026),
|
||||
haendler: 'Shop',
|
||||
marke: 'Marke',
|
||||
material: 'PLA',
|
||||
preis: 20,
|
||||
farbe: 'Blau',
|
||||
gewicht: '1000',
|
||||
bruttoGewichtGramm: 1500,
|
||||
);
|
||||
controller.verbrauch['a'] = 200;
|
||||
expect(controller.restgewicht(rolle), 800);
|
||||
expect(
|
||||
controller.restgewicht(rolle.copyWith(bruttoGewichtGramm: 2000)),
|
||||
800,
|
||||
);
|
||||
final part = BauteilModel(
|
||||
bauteilId: 'part',
|
||||
erstelltAm: DateTime(2026),
|
||||
name: 'Teil',
|
||||
verwendung: 'Test',
|
||||
druckdauerMinuten: 1,
|
||||
gewichtGramm: 100,
|
||||
abfallGramm: 10,
|
||||
filamente: [
|
||||
const BauteilFilamentModel(filamentId: 'a', verbrauchGramm: 110),
|
||||
],
|
||||
);
|
||||
final price = BauteilPreisBerechnung.berechne(part, [rolle], 50)!;
|
||||
expect(price.materialKosten, 2);
|
||||
expect(price.abfallKosten, .2);
|
||||
expect(price.gesamtpreis, 3.3);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
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/storage_keys.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/services/get_storage_services.dart';
|
||||
import 'package:ertl_filament_verwaltung/theme/app_theme.dart';
|
||||
import 'package:ertl_filament_verwaltung/widgets/bauteil_filament_auswahl.dart';
|
||||
|
||||
import 'helpers/memory_storage_service.dart';
|
||||
|
||||
void main() {
|
||||
late MemoryStorageService storage;
|
||||
late BauteilController controller;
|
||||
setUp(() {
|
||||
Get.testMode = true;
|
||||
storage = MemoryStorageService();
|
||||
storage.tables[StorageKeys.filamente] = [
|
||||
for (final entry in {'a': '100', 'b': '150'}.entries)
|
||||
KaufFilamentModel(
|
||||
filamentId: entry.key,
|
||||
kaufdatum: DateTime(2026),
|
||||
haendler: 'Shop',
|
||||
marke: 'Rolle ${entry.key}',
|
||||
material: 'PLA',
|
||||
preis: 10,
|
||||
farbe: 'Blau',
|
||||
gewicht: entry.value,
|
||||
).toJson(),
|
||||
];
|
||||
storage.tables[StorageKeys.bauteile] = [
|
||||
BauteilModel(
|
||||
bauteilId: 'vorher',
|
||||
erstelltAm: DateTime(2026),
|
||||
name: 'Vorher',
|
||||
verwendung: 'Test',
|
||||
druckdauerMinuten: 1,
|
||||
gewichtGramm: 40,
|
||||
abfallGramm: 0,
|
||||
filamente: [
|
||||
const BauteilFilamentModel(filamentId: 'a', verbrauchGramm: 40),
|
||||
],
|
||||
).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: Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: BauteilFilamentAuswahl(controller: controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> select(WidgetTester tester, String id) async {
|
||||
await tester.ensureVisible(find.byType(DropdownButtonFormField<String>));
|
||||
await tester.tap(find.byType(DropdownButtonFormField<String>));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(controller.filamentLabel(id)).last);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'Insufficient roll warns with quantities and requires OK; second roll covers combined demand',
|
||||
(tester) async {
|
||||
controller.gewichtController.text = '150';
|
||||
controller.abfallController.text = '10';
|
||||
await show(tester);
|
||||
await select(tester, 'a');
|
||||
expect(find.byType(AlertDialog), findsOneWidget);
|
||||
expect(find.text('Achtung: zu wenig Filament'), findsOneWidget);
|
||||
expect(
|
||||
find.textContaining('Benötigt (inklusive Abfall): 160 g'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.textContaining('Verfügbar: 60 g'), findsOneWidget);
|
||||
expect(find.textContaining('Es fehlen: 100 g'), findsOneWidget);
|
||||
await tester.tapAt(const Offset(2, 2));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(AlertDialog), findsOneWidget);
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(AlertDialog), findsOneWidget);
|
||||
await tester.tap(find.text('OK'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(AlertDialog), findsNothing);
|
||||
expect(controller.selectedFilamente.keys, ['a']);
|
||||
await select(tester, 'b'); // 60g + 150g cover 160g; b alone would not.
|
||||
expect(find.byType(AlertDialog), findsNothing);
|
||||
expect(controller.selectedFilamente.keys, ['a', 'b']);
|
||||
expect(storage.getModelList(StorageKeys.bauteile), hasLength(1));
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Exact remaining amount produces no warning and selecting does not book consumption',
|
||||
(tester) async {
|
||||
controller.gewichtController.text = '50';
|
||||
controller.abfallController.text = '10';
|
||||
await show(tester);
|
||||
await select(tester, 'a');
|
||||
expect(find.byType(AlertDialog), findsNothing);
|
||||
expect(controller.restgewichtGramm('a'), 60);
|
||||
expect(storage.getModelList(StorageKeys.bauteile), hasLength(1));
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test('Combined stock, decimal commas and editing credit use the correct available amount', () {
|
||||
controller.gewichtController.text = '200,5';
|
||||
controller.abfallController.text = '9,5';
|
||||
controller.toggleFilament('a', true);
|
||||
controller.toggleFilament('b', true);
|
||||
expect(controller.pruefeFilamentVorrat(), isNull); // Exactly 210g.
|
||||
controller.abfallController.text = '10';
|
||||
expect(
|
||||
controller.pruefeFilamentVorrat(),
|
||||
contains('ausgewählten Rollen zusammen'),
|
||||
);
|
||||
expect(controller.pruefeFilamentVorrat(), contains('Es fehlen: 0,5 g'));
|
||||
controller.toggleFilament('b', false);
|
||||
controller.gewichtController.text = '90';
|
||||
controller.abfallController.text = '0';
|
||||
expect(controller.pruefeFilamentVorrat(), isNotNull);
|
||||
controller.editingId.value =
|
||||
'vorher'; // Replace existing 40g booking, not a new print.
|
||||
expect(controller.pruefeFilamentVorrat(), isNull);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Without a valid requirement selection shows no shortage warning',
|
||||
(tester) async {
|
||||
await show(tester);
|
||||
await select(tester, 'a');
|
||||
expect(find.byType(AlertDialog), findsNothing);
|
||||
for (final value in ['', 'NaN', 'Infinity', '-1', '0', 'abc']) {
|
||||
controller.gewichtController.text = value;
|
||||
expect(controller.pruefeFilamentVorrat(), isNull);
|
||||
}
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -73,7 +73,7 @@ void main() {
|
||||
tester.getTopLeft(find.byType(FilamentCard)).dx,
|
||||
greaterThan(tester.getTopRight(find.byType(FilamentForm)).dx),
|
||||
);
|
||||
expect(find.text('1000 g'), findsOneWidget);
|
||||
expect(find.text('Netto: 1000 g'), findsOneWidget);
|
||||
expect(find.text('Testmarke'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Filament verwalten'));
|
||||
@@ -114,7 +114,7 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Testmarke'), findsOneWidget);
|
||||
expect(find.text('1000 g'), findsOneWidget);
|
||||
expect(find.text('Netto: 1000 g'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user