add some new features
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user