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,
);
},
);
}