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
+221
View File
@@ -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,
);
},
);
}
+286
View File
@@ -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);
},
);
}
+191
View File
@@ -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);
}
},
);
}
+21 -16
View File
@@ -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(
+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,
);
}
});
}
+1
View File
@@ -57,6 +57,7 @@ void main() {
late ExportController controller;
Map<String, List<Map<String, dynamic>>> fixture() => {
StorageKeys.abfaelle: [],
StorageKeys.filamente: [
KaufFilamentModel(
filamentId: 'f1',
+206
View File
@@ -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);
},
);
}
+160
View File
@@ -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);
},
);
}
+2 -2
View File
@@ -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);
});