price per item is printed

This commit is contained in:
atseirjo
2026-10-28 12:32:49 +01:00
parent c32adbad50
commit 77fbe16867
31 changed files with 656 additions and 55 deletions
+47
View File
@@ -236,6 +236,53 @@ void main() {
expect(find.textContaining('nicht gefunden'), findsOneWidget);
});
testWidgets('Inventory marks saved profiles and updates after creating one', (
tester,
) async {
tester.view.physicalSize = const Size(1280, 1400);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
GetMaterialApp(
theme: AppTheme.light,
initialRoute: HomeView.routeName,
getPages: SampleRouts.samplePages,
),
);
await tester.pumpAndSettle();
final home = Get.find<HomeController>();
Finder badgeFor(String id) => find.descendant(
of: find.byKey(ValueKey(id)),
matching: find.text('Profil erstellt'),
);
expect(badgeFor('a'), findsNothing);
expect(badgeFor('b'), findsOneWidget);
await tester.tap(find.byKey(const ValueKey('a')));
await tester.pumpAndSettle();
final profileController = Get.find<AnwendungController>();
profileController.druckController.text = '220';
profileController.druckbettController.text = '60';
storage.failWrites = true;
await profileController.saveAnwendung();
expect(home.filamentIdsMitProfil, isNot(contains('a')));
storage.failWrites = false;
await profileController.saveAnwendung();
Get.back<void>();
await tester.pumpAndSettle();
expect(badgeFor('a'), findsOneWidget);
expect(badgeFor('b'), findsOneWidget);
// Reloads after imports must also clear badges for removed profiles.
await storage.saveModelList(AnwendungController.storageKey, []);
home.loadFilamente();
await tester.pumpAndSettle();
expect(find.text('Profil erstellt'), findsNothing);
expect(tester.takeException(), isNull);
});
testWidgets(
'Multicolor part persists both filaments and matching processing profile',
(tester) async {
+49
View File
@@ -7,6 +7,7 @@ import 'package:get/get.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/display_formats.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/pages/bauteil_view.dart';
@@ -143,6 +144,7 @@ void main() {
});
expect(legacy.filamente.single.verbrauchGramm, 0);
expect(legacy.fotoBytes, isNull);
expect(legacy.gesamtpreis, isNull);
},
);
@@ -151,6 +153,7 @@ void main() {
(tester) async {
await show(tester);
fill();
controller.gesamtpreisController.text = '12,50';
photos.result = png;
await controller.pickPhoto();
await controller.saveBauteil();
@@ -158,9 +161,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(
storage
.getModelList(BauteilController.storageKey)
.single['gesamtpreis'],
12.5,
);
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.verfuegbarGramm('a'), 1000);
controller.verbrauchController('a').text = '20';
controller.verbrauchController('b').text = '80';
@@ -180,6 +193,10 @@ void main() {
controller.bauteile.every((item) => item.fotoBase64 != null),
isTrue,
);
expect(
controller.bauteile.every((item) => item.gesamtpreis == 18.9),
isTrue,
);
expect(remaining('a'), 960);
expect(remaining('b'), 840);
await controller.deleteBauteil(originalId);
@@ -190,6 +207,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.filamente.last.verbrauchGramm, 80);
// Purchases retain the original weight; the persisted ledger is deducted.
expect(
@@ -218,6 +236,32 @@ void main() {
},
);
testWidgets(
'Total price rejects invalid amounts, accepts zero and can be cleared',
(tester) async {
await show(tester);
fill();
for (final value in ['-1', 'NaN', 'Infinity', 'abc']) {
controller.gesamtpreisController.text = value;
await controller.saveBauteil();
expect(storage.getModelList(BauteilController.storageKey), isEmpty);
}
controller.gesamtpreisController.text = '0';
await controller.saveBauteil();
expect(controller.bauteile.single.gesamtpreis, 0);
await edit(tester, controller.bauteile.single);
controller.gesamtpreisController.clear();
await controller.saveBauteil();
expect(controller.bauteile.single.gesamtpreis, isNull);
expect(
storage
.getModelList(BauteilController.storageKey)
.single['gesamtpreis'],
isNull,
);
},
);
testWidgets(
'Editing credits only the edited booking; duplicates need new stock',
(tester) async {
@@ -310,6 +354,7 @@ void main() {
(tester) async {
await show(tester, narrow: true);
fill();
controller.gesamtpreisController.text = '12,50';
photos.result = png;
await controller.pickPhoto();
await controller.saveBauteil();
@@ -320,6 +365,10 @@ void main() {
scrollable: find.byType(Scrollable).first,
);
await tester.pumpAndSettle();
expect(
find.text('Gesamtpreis ${DisplayFormats.currency(12.5)}'),
findsOneWidget,
);
expect(
find.descendant(
of: find.byType(BauteilCard),
+19
View File
@@ -88,6 +88,7 @@ void main() {
druckdauerMinuten: 60,
gewichtGramm: 40,
abfallGramm: 10,
gesamtpreis: 12.5,
filamente: [
const BauteilFilamentModel(
filamentId: 'f1',
@@ -129,6 +130,7 @@ void main() {
await controller.importData();
expect(storage.tables, fixture());
expect(home.materialien, ['PA12', 'PLA']);
expect(home.filamentIdsMitProfil, contains('f1'));
expect(home.restgewicht(home.filamente.single), 950);
expect(controller.hasError.value, isFalse);
},
@@ -174,6 +176,23 @@ void main() {
expect(storage.tables, fixture());
});
test('Backup imports accept legacy parts without price and reject negative prices', () {
final legacy = fixture();
legacy[StorageKeys.bauteile]!.single.remove('gesamtpreis');
final decoded = BackupModel.decode(BackupModel(legacy).encode());
expect(
BauteilModel.fromJson(decoded.data[StorageKeys.bauteile]!.single)
.gesamtpreis,
isNull,
);
final invalid = fixture();
invalid[StorageKeys.bauteile]!.single['gesamtpreis'] = -1;
expect(
() => BackupModel.decode(BackupModel(invalid).encode()),
throwsFormatException,
);
});
test('A failed import restores previously written tables', () async {
storage.tables.addAll(fixture());
files.source = BackupModel({for (final key in BackupModel.keys) key: []})
+136
View File
@@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:ertl_filament_verwaltung/controller/anwendung_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/storage_keys.dart';
import 'package:ertl_filament_verwaltung/main.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/pages/anwendung_view.dart';
import 'package:ertl_filament_verwaltung/pages/bauteil_view.dart';
import 'package:ertl_filament_verwaltung/pages/export_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/widgets/bauteil_card.dart';
import 'package:ertl_filament_verwaltung/widgets/filament_card.dart';
import 'helpers/memory_storage_service.dart';
void main() {
setUp(() {
Get.testMode = true;
final storage = MemoryStorageService();
storage.tables[StorageKeys.filamente] = [
KaufFilamentModel(
filamentId: 'filament',
kaufdatum: DateTime(2026),
haendler: 'Ein Händler mit einem besonders langen Namen',
marke: 'Eine besonders lange Filamentmarke',
material: 'Benutzerdefiniertes Material mit langem Namen',
preis: 12345.67,
farbe: 'Ein lang beschriebener Farbton',
gewicht: '1000',
).toJson(),
];
storage.tables[StorageKeys.bauteile] = [
BauteilModel(
bauteilId: 'part',
erstelltAm: DateTime(2026),
name: 'Ein besonders lang benanntes Bauteil',
verwendung: 'Eine ausführliche Beschreibung der Verwendung',
druckdauerMinuten: 120,
gewichtGramm: 40,
abfallGramm: 10,
filamente: [
const BauteilFilamentModel(
filamentId: 'filament',
verbrauchGramm: 50,
),
],
).toJson(),
];
Get.put<GetStorageService>(storage);
});
tearDown(() => Get.reset());
Future<void> resize(WidgetTester tester, double width) async {
tester.view.physicalSize = Size(width, 640);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull, reason: 'Width $width');
}
for (final route in [
HomeView.routeName,
BauteilView.routeName,
'${AnwendungView.routeName}/filament',
ExportView.routeName,
]) {
testWidgets(
'$route survives resizing across desktop and narrow browser widths',
(tester) async {
tester.view.physicalSize = const Size(1280, 640);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(const MyApp());
await tester.pumpAndSettle();
if (route != HomeView.routeName) {
Get.toNamed(route);
await tester.pumpAndSettle();
}
if (route == BauteilView.routeName) {
Get.find<BauteilController>().toggleFilament('filament', true);
}
if (route.startsWith(AnwendungView.routeName)) {
Get.find<AnwendungController>().sonstigesController.text = 'Entwurf';
}
for (final width in [
900.0,
899.0,
600.0,
360.0,
320.0,
240.0,
320.0,
1280.0,
]) {
await resize(tester, width);
expect(Get.currentRoute, route);
if (route == HomeView.routeName || route == BauteilView.routeName) {
final card = route == HomeView.routeName
? find.byType(FilamentCard)
: find.byType(BauteilCard);
final scroll = route == HomeView.routeName
? Get.find<HomeController>().pageScrollController
: Get.find<BauteilController>().scrollController;
scroll.jumpTo(scroll.position.maxScrollExtent);
await tester.pumpAndSettle();
expect(card, findsOneWidget);
expect(
tester.takeException(),
isNull,
reason: 'Card at width $width',
);
scroll.jumpTo(0);
await tester.pumpAndSettle();
}
}
if (route.startsWith(AnwendungView.routeName)) {
expect(
Get.find<AnwendungController>().sonstigesController.text,
'Entwurf',
);
}
if (route == BauteilView.routeName) {
expect(
Get.find<BauteilController>().selectedFilamente,
contains('filament'),
);
}
},
);
}
}
+19
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:ertl_filament_verwaltung/controller/home_controller.dart';
import 'package:ertl_filament_verwaltung/helper/storage_keys.dart';
@@ -17,6 +18,13 @@ void main() {
late HomeController controller;
setUp(() {
PackageInfo.setMockInitialValues(
appName: 'Filamentverwaltung',
packageName: 'com.example.ertl_filament_verwaltung',
version: '1.2.3',
buildNumber: '42',
buildSignature: '',
);
Get.testMode = true;
Get.put<GetStorageService>(MemoryStorageService());
controller = Get.put(HomeController());
@@ -46,6 +54,17 @@ void main() {
gewicht: '1000',
);
testWidgets('Hamburger menu displays the installed version and build', (
tester,
) async {
await showHome(tester, const Size(320, 640));
await tester.tap(find.byIcon(Icons.menu));
await tester.pumpAndSettle();
expect(find.text('Version 1.2.3'), findsOneWidget);
expect(find.text('Build 42'), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('Desktop shows both panes and edits PLA+', (tester) async {
controller.filamente.add(sample());
await showHome(tester, const Size(1280, 900));