add two views
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
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/input_validators.dart';
|
||||
import 'package:ertl_filament_verwaltung/helper/sample_routes.dart';
|
||||
import 'package:ertl_filament_verwaltung/models/anwendung_model.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/home_view.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/filament_card.dart';
|
||||
import 'package:ertl_filament_verwaltung/widgets/entry_panel.dart';
|
||||
import 'package:ertl_filament_verwaltung/widgets/bauteil_card.dart';
|
||||
|
||||
import 'helpers/memory_storage_service.dart';
|
||||
|
||||
void main() {
|
||||
late MemoryStorageService storage;
|
||||
final uuidPattern = RegExp(
|
||||
r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
|
||||
);
|
||||
|
||||
KaufFilamentModel filament(String id, String color) => KaufFilamentModel(
|
||||
filamentId: id,
|
||||
kaufdatum: DateTime(2026, 10, 27),
|
||||
haendler: 'Shop',
|
||||
marke: 'Marke $id',
|
||||
material: 'PLA',
|
||||
preis: 20,
|
||||
farbe: color,
|
||||
gewicht: '1000',
|
||||
);
|
||||
|
||||
AnwendungModel profile(String id, String filamentId) => AnwendungModel(
|
||||
anwendungId: id,
|
||||
filamentId: filamentId,
|
||||
erstelltAm: DateTime(2026, 10, 26),
|
||||
druckbettTemperatur: 60,
|
||||
druckTemperatur: 210,
|
||||
bauraumGeschlossen: false,
|
||||
);
|
||||
|
||||
setUp(() {
|
||||
Get.testMode = true;
|
||||
storage = MemoryStorageService();
|
||||
storage.tables[HomeController.storageKey] = [
|
||||
filament('a', 'Rot').toJson(),
|
||||
filament('b', 'Blau').toJson(),
|
||||
];
|
||||
storage.tables[AnwendungController.storageKey] = [
|
||||
profile('profile-b', 'b').toJson(),
|
||||
];
|
||||
Get.put<GetStorageService>(storage);
|
||||
});
|
||||
tearDown(() => Get.reset());
|
||||
|
||||
Future<void> show(
|
||||
WidgetTester tester,
|
||||
Widget page, {
|
||||
bool narrow = false,
|
||||
}) async {
|
||||
tester.view.physicalSize = narrow
|
||||
? const Size(320, 640)
|
||||
: const Size(1100, 1400);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
await tester.pumpWidget(GetMaterialApp(theme: AppTheme.light, home: page));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void fillPart(BauteilController controller) {
|
||||
controller.nameController.text = 'Zweifarbiger Halter';
|
||||
controller.verwendungController.text = 'Werkstatt';
|
||||
controller.stundenController.text = '2';
|
||||
controller.minutenController.text = '35';
|
||||
controller.gewichtController.text = '42,5';
|
||||
controller.abfallController.text = '8,2';
|
||||
controller.verbrauchController('a').text = '50,7';
|
||||
controller.verbrauchController('b').text = '20,7';
|
||||
}
|
||||
|
||||
test(
|
||||
'Numeric validation accepts comma decimals and rejects invalid values',
|
||||
() {
|
||||
expect(InputValidators.number('42,5'), 42.5);
|
||||
for (final value in ['NaN', 'Infinity', '-1', 'abc']) {
|
||||
expect(InputValidators.nonNegative(value), isNotNull);
|
||||
}
|
||||
expect(InputValidators.positive('0'), isNotNull);
|
||||
expect(InputValidators.nonNegative('0'), isNull);
|
||||
expect(InputValidators.nonNegative('', optional: true), isNull);
|
||||
expect(InputValidators.minutes('60'), isNotNull);
|
||||
expect(InputValidators.hours('1,5'), isNotNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Profiles persist selected filament UUID and preserve other profiles',
|
||||
(tester) async {
|
||||
final controller = Get.put(AnwendungController(filamentId: 'a'));
|
||||
await show(tester, const AnwendungView(), narrow: true);
|
||||
controller.druckController.text = '220,5';
|
||||
controller.druckbettController.text = '65';
|
||||
controller.bauraumController.text = '40';
|
||||
controller.bauraumGeschlossen.value = true;
|
||||
controller.sonstigesController.text = 'Luefter 50 %';
|
||||
await controller.saveAnwendung();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final stored = storage.getModelList(AnwendungController.storageKey);
|
||||
expect(stored, hasLength(2));
|
||||
final saved = AnwendungModel.fromJson(stored.last);
|
||||
expect(saved.anwendungId, matches(uuidPattern));
|
||||
expect(saved.filamentId, 'a');
|
||||
expect(saved.druckTemperatur, 220.5);
|
||||
expect(saved.druckbettTemperatur, 65);
|
||||
expect(saved.bauraumGeschlossen, isTrue);
|
||||
expect(saved.bauraumTemperatur, 40);
|
||||
expect(saved.sonstiges, 'Luefter 50 %');
|
||||
expect(controller.anwendungen, hasLength(1));
|
||||
final reloaded = Get.put(
|
||||
AnwendungController(filamentId: 'a'),
|
||||
tag: 'reload',
|
||||
);
|
||||
expect(reloaded.anwendungen.single.anwendungId, saved.anwendungId);
|
||||
expect(storage.getModelList(HomeController.storageKey), hasLength(2));
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(SavedEntryCard),
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Bauraum geschlossen: Ja'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('Profile write failure retains input and does not add history', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = Get.put(AnwendungController(filamentId: 'a'));
|
||||
await show(tester, const AnwendungView());
|
||||
controller.druckController.text = '220';
|
||||
controller.druckbettController.text = '0';
|
||||
storage.failWrites = true;
|
||||
await controller.saveAnwendung();
|
||||
expect(controller.hasError.value, isTrue);
|
||||
expect(controller.druckController.text, '220');
|
||||
expect(controller.anwendungen, isEmpty);
|
||||
expect(controller.isSaving.value, isFalse);
|
||||
storage.failWrites = false;
|
||||
await controller.saveAnwendung();
|
||||
expect(controller.anwendungen.single.bauraumTemperatur, isNull);
|
||||
expect(controller.anwendungen.single.bauraumGeschlossen, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Missing filament cannot create orphan profiles', (tester) async {
|
||||
final controller = Get.put(AnwendungController(filamentId: 'missing'));
|
||||
await show(tester, const AnwendungView());
|
||||
await controller.saveAnwendung();
|
||||
expect(controller.anwendungen, isEmpty);
|
||||
expect(storage.getModelList(AnwendungController.storageKey), hasLength(1));
|
||||
expect(find.textContaining('nicht gefunden'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Multicolor part persists both filaments and matching processing profile',
|
||||
(tester) async {
|
||||
final controller = Get.put(BauteilController());
|
||||
await show(tester, const BauteilView(), narrow: true);
|
||||
fillPart(controller);
|
||||
controller.toggleFilament('a', true);
|
||||
controller.toggleFilament('b', true);
|
||||
controller.verbrauchController('a').text = '30';
|
||||
controller.selectProfile('a', 'profile-b');
|
||||
expect(controller.selectedFilamente['a'], isNull);
|
||||
controller.selectProfile('b', 'profile-b');
|
||||
await tester.pumpAndSettle();
|
||||
await controller.saveBauteil();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final stored = storage.getModelList(BauteilController.storageKey);
|
||||
final saved = BauteilModel.fromJson(stored.single);
|
||||
expect(saved.bauteilId, matches(uuidPattern));
|
||||
expect(saved.name, 'Zweifarbiger Halter');
|
||||
expect(saved.verwendung, 'Werkstatt');
|
||||
expect(saved.druckdauerMinuten, 155);
|
||||
expect(saved.gewichtGramm, 42.5);
|
||||
expect(saved.abfallGramm, 8.2);
|
||||
expect(saved.gesamtverbrauchGramm, closeTo(50.7, 0.001));
|
||||
expect(saved.filamente.map((item) => item.filamentId), ['a', 'b']);
|
||||
expect(saved.filamente.last.anwendungId, 'profile-b');
|
||||
expect(controller.selectedFilamente, isEmpty);
|
||||
final reloaded = Get.put(BauteilController(), tag: 'reload');
|
||||
expect(reloaded.bauteile.single.filamente, hasLength(2));
|
||||
expect(
|
||||
storage.getModelList(AnwendungController.storageKey),
|
||||
hasLength(1),
|
||||
);
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(BauteilCard),
|
||||
250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Zweifarbiger Halter'), findsOneWidget);
|
||||
expect(find.text('Mehrfarbdruck · 2 Filamente'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('Part requires filament, positive duration and valid weights', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = Get.put(BauteilController());
|
||||
await show(tester, const BauteilView());
|
||||
fillPart(controller);
|
||||
await controller.saveBauteil();
|
||||
expect(controller.message.value, contains('mindestens ein Filament'));
|
||||
controller.toggleFilament('a', true);
|
||||
controller.stundenController.text = '0';
|
||||
controller.minutenController.text = '0';
|
||||
await controller.saveBauteil();
|
||||
expect(controller.message.value, contains('mindestens eine Minute'));
|
||||
controller.minutenController.text = '30';
|
||||
controller.abfallController.text = '-1';
|
||||
await controller.saveBauteil();
|
||||
expect(storage.getModelList(BauteilController.storageKey), isEmpty);
|
||||
expect(controller.nameController.text, isNotEmpty);
|
||||
});
|
||||
|
||||
testWidgets('Part write failure keeps selection and inputs for retry', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = Get.put(BauteilController());
|
||||
await show(tester, const BauteilView());
|
||||
fillPart(controller);
|
||||
controller.toggleFilament('a', true);
|
||||
storage.failWrites = true;
|
||||
await controller.saveBauteil();
|
||||
expect(controller.hasError.value, isTrue);
|
||||
expect(controller.bauteile, isEmpty);
|
||||
expect(controller.nameController.text, 'Zweifarbiger Halter');
|
||||
expect(controller.selectedFilamente.keys, ['a']);
|
||||
expect(controller.isSaving.value, isFalse);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Navigation opens a fresh controller for each selected filament',
|
||||
(tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 900);
|
||||
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();
|
||||
await tester.tap(find.byType(FilamentCard).first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(Get.find<AnwendungController>().filamentId, 'a');
|
||||
Get.find<AnwendungController>().sonstigesController.text =
|
||||
'Nicht uebernehmen';
|
||||
Get.back<void>();
|
||||
await tester.pumpAndSettle();
|
||||
expect(Get.isRegistered<AnwendungController>(), isFalse);
|
||||
await tester.tap(find.byType(FilamentCard).last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(Get.find<AnwendungController>().filamentId, 'b');
|
||||
expect(Get.find<AnwendungController>().sonstigesController.text, isEmpty);
|
||||
expect(Get.find<AnwendungController>().anwendungen, hasLength(1));
|
||||
Get.back<void>();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('Bauteile verwalten'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(BauteilView), findsOneWidget);
|
||||
expect(Get.find<BauteilController>().filamente, hasLength(2));
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
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/home_controller.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';
|
||||
import 'package:ertl_filament_verwaltung/pages/home_view.dart';
|
||||
import 'package:ertl_filament_verwaltung/services/bauteil_photo_service.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_card.dart';
|
||||
import 'package:ertl_filament_verwaltung/widgets/bauteil_photo.dart';
|
||||
|
||||
import 'helpers/memory_storage_service.dart';
|
||||
|
||||
class TestPhotoService extends BauteilPhotoService {
|
||||
Uint8List? result;
|
||||
bool fail = false;
|
||||
|
||||
@override
|
||||
Future<Uint8List?> pickPhoto() async {
|
||||
if (fail) throw const FormatException('Ungültiges Foto');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
late MemoryStorageService storage;
|
||||
late HomeController home;
|
||||
late BauteilController controller;
|
||||
late TestPhotoService photos;
|
||||
late Uint8List png;
|
||||
|
||||
Future<Uint8List> createPng(int width, int height) async {
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = ui.Canvas(recorder);
|
||||
canvas.drawRect(
|
||||
ui.Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()),
|
||||
ui.Paint()..color = const Color(0xFF007F78),
|
||||
);
|
||||
final picture = recorder.endRecording();
|
||||
final image = await picture.toImage(width, height);
|
||||
final data = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
image.dispose();
|
||||
picture.dispose();
|
||||
return data!.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
|
||||
}
|
||||
|
||||
setUpAll(() async {
|
||||
png = await createPng(2, 2);
|
||||
});
|
||||
|
||||
KaufFilamentModel spool(String id, String weight) => KaufFilamentModel(
|
||||
filamentId: id,
|
||||
kaufdatum: DateTime(2026, 10, 27),
|
||||
haendler: 'Shop',
|
||||
marke: 'Marke $id',
|
||||
material: 'PLA',
|
||||
preis: 20,
|
||||
farbe: id,
|
||||
gewicht: weight,
|
||||
);
|
||||
|
||||
setUp(() {
|
||||
Get.testMode = true;
|
||||
storage = MemoryStorageService();
|
||||
storage.tables[HomeController.storageKey] = [
|
||||
spool('a', '1kg').toJson(),
|
||||
spool('b', '1000').toJson(),
|
||||
];
|
||||
Get.put<GetStorageService>(storage);
|
||||
home = Get.put(HomeController());
|
||||
photos = TestPhotoService();
|
||||
controller = Get.put(BauteilController(photoService: photos));
|
||||
});
|
||||
tearDown(() => Get.reset());
|
||||
|
||||
Future<void> show(WidgetTester tester, {bool narrow = false}) async {
|
||||
tester.view.physicalSize = narrow
|
||||
? const Size(320, 640)
|
||||
: const Size(1100, 1600);
|
||||
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({double a = 60, double b = 40}) {
|
||||
controller.nameController.text = 'Testbauteil';
|
||||
controller.verwendungController.text = 'Werkstatt';
|
||||
controller.stundenController.text = '1';
|
||||
controller.minutenController.text = '10';
|
||||
controller.gewichtController.text = '${a + b - 10}';
|
||||
controller.abfallController.text = '10';
|
||||
if (a > 0) {
|
||||
controller.toggleFilament('a', true);
|
||||
controller.verbrauchController('a').text = '$a';
|
||||
}
|
||||
if (b > 0) {
|
||||
controller.toggleFilament('b', true);
|
||||
controller.verbrauchController('b').text = '$b';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> edit(WidgetTester tester, BauteilModel entry) async {
|
||||
final pending = controller.editBauteil(entry);
|
||||
await tester.pumpAndSettle();
|
||||
await pending;
|
||||
}
|
||||
|
||||
double remaining(String id) => home.restgewicht(
|
||||
home.filamente.firstWhere((item) => item.filamentId == id),
|
||||
)!;
|
||||
|
||||
test(
|
||||
'Legacy records retain unknown usage and old weight units are supported',
|
||||
() {
|
||||
expect(spool('a', '1kg').gewichtGramm, 1000);
|
||||
expect(spool('a', '0,75 KG').gewichtGramm, 750);
|
||||
expect(spool('a', '500 g').gewichtGramm, 500);
|
||||
expect(spool('a', 'NaN').gewichtGramm, isNull);
|
||||
final legacy = BauteilModel.fromJson({
|
||||
'bauteilId': 'old',
|
||||
'erstelltAm': DateTime.now().toIso8601String(),
|
||||
'name': 'Altbestand',
|
||||
'verwendung': 'Test',
|
||||
'druckdauerMinuten': 30,
|
||||
'gewichtGramm': 10,
|
||||
'abfallGramm': 2,
|
||||
'filamente': [
|
||||
{'filamentId': 'a', 'anwendungId': null},
|
||||
],
|
||||
});
|
||||
expect(legacy.filamente.single.verbrauchGramm, 0);
|
||||
expect(legacy.fotoBytes, isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Create, edit, duplicate and delete reconcile stock and survive reload',
|
||||
(tester) async {
|
||||
await show(tester);
|
||||
fill();
|
||||
photos.result = png;
|
||||
await controller.pickPhoto();
|
||||
await controller.saveBauteil();
|
||||
expect(remaining('a'), 940);
|
||||
expect(remaining('b'), 960);
|
||||
final originalId = controller.bauteile.single.bauteilId;
|
||||
final originalDate = controller.bauteile.single.erstelltAm;
|
||||
|
||||
await edit(tester, controller.bauteile.single);
|
||||
expect(controller.foto.value, png);
|
||||
expect(controller.verfuegbarGramm('a'), 1000);
|
||||
controller.verbrauchController('a').text = '20';
|
||||
controller.verbrauchController('b').text = '80';
|
||||
await controller.saveBauteil();
|
||||
expect(controller.bauteile.single.bauteilId, originalId);
|
||||
expect(controller.bauteile.single.erstelltAm, originalDate);
|
||||
expect(remaining('a'), 980);
|
||||
expect(remaining('b'), 920);
|
||||
|
||||
await controller.duplicateBauteil(controller.bauteile.single);
|
||||
expect(controller.bauteile, hasLength(2));
|
||||
expect(
|
||||
controller.bauteile.map((item) => item.bauteilId).toSet(),
|
||||
hasLength(2),
|
||||
);
|
||||
expect(
|
||||
controller.bauteile.every((item) => item.fotoBase64 != null),
|
||||
isTrue,
|
||||
);
|
||||
expect(remaining('a'), 960);
|
||||
expect(remaining('b'), 840);
|
||||
await controller.deleteBauteil(originalId);
|
||||
expect(remaining('a'), 980);
|
||||
expect(remaining('b'), 920);
|
||||
|
||||
home.loadFilamente();
|
||||
expect(remaining('a'), 980);
|
||||
final reloaded = Get.put(BauteilController(), tag: 'reload');
|
||||
expect(reloaded.bauteile.single.fotoBytes, png);
|
||||
expect(reloaded.bauteile.single.filamente.last.verbrauchGramm, 80);
|
||||
// Purchases retain the original weight; the persisted ledger is deducted.
|
||||
expect(
|
||||
storage.getModelList(HomeController.storageKey).first['gewicht'],
|
||||
'1kg',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Overdraw, missing usage and mismatched totals cannot change stock',
|
||||
(tester) async {
|
||||
await show(tester);
|
||||
fill(a: 1001, b: 0);
|
||||
await controller.saveBauteil();
|
||||
expect(controller.hasError.value, isTrue);
|
||||
expect(controller.message.value, contains('1000 g verfügbar'));
|
||||
expect(remaining('a'), 1000);
|
||||
controller.verbrauchController('a').text = '1';
|
||||
await controller.saveBauteil();
|
||||
expect(controller.message.value, contains('Bauteilgewicht + Abfall'));
|
||||
controller.verbrauchController('a').text = '';
|
||||
await controller.saveBauteil();
|
||||
expect(controller.bauteile, isEmpty);
|
||||
expect(storage.getModelList(BauteilController.storageKey), isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Editing credits only the edited booking; duplicates need new stock',
|
||||
(tester) async {
|
||||
await show(tester);
|
||||
fill(a: 900, b: 0);
|
||||
await controller.saveBauteil();
|
||||
final first = controller.bauteile.single;
|
||||
fill(a: 60, b: 0);
|
||||
await controller.saveBauteil();
|
||||
expect(remaining('a'), 40);
|
||||
await edit(tester, first);
|
||||
expect(controller.verfuegbarGramm('a'), 940);
|
||||
controller.gewichtController.text = '940';
|
||||
controller.verbrauchController('a').text = '950';
|
||||
await controller.saveBauteil();
|
||||
expect(controller.message.value, contains('940 g verfügbar'));
|
||||
expect(remaining('a'), 40);
|
||||
await controller.duplicateBauteil(first);
|
||||
expect(controller.bauteile, hasLength(2));
|
||||
expect(remaining('a'), 40);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Failed edit, duplicate and delete leave ledger and stocks unchanged',
|
||||
(tester) async {
|
||||
await show(tester);
|
||||
fill();
|
||||
await controller.saveBauteil();
|
||||
final entry = controller.bauteile.single;
|
||||
await edit(tester, entry);
|
||||
controller.verbrauchController('a').text = '50';
|
||||
controller.verbrauchController('b').text = '50';
|
||||
storage.failWrites = true;
|
||||
await controller.saveBauteil();
|
||||
expect(controller.hasError.value, isTrue);
|
||||
expect(controller.editingId.value, entry.bauteilId);
|
||||
expect(controller.verbrauchController('a').text, '50');
|
||||
expect(remaining('a'), 940);
|
||||
await controller.duplicateBauteil(entry);
|
||||
expect(controller.bauteile, hasLength(1));
|
||||
await controller.deleteBauteil(entry.bauteilId);
|
||||
expect(controller.bauteile, hasLength(1));
|
||||
expect(remaining('b'), 960);
|
||||
expect(
|
||||
storage
|
||||
.getModelList(BauteilController.storageKey)
|
||||
.single['filamente'][0]['verbrauchGramm'],
|
||||
60,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Photo selection, cancellation, errors and removal preserve the draft',
|
||||
(tester) async {
|
||||
await show(tester);
|
||||
photos.result = png;
|
||||
await controller.pickPhoto();
|
||||
expect(controller.foto.value, png);
|
||||
photos.result = null;
|
||||
await controller.pickPhoto();
|
||||
expect(controller.foto.value, png);
|
||||
photos.fail = true;
|
||||
await controller.pickPhoto();
|
||||
expect(controller.foto.value, png);
|
||||
expect(controller.hasError.value, isTrue);
|
||||
expect(controller.isPickingPhoto.value, isFalse);
|
||||
controller.removePhoto();
|
||||
expect(controller.foto.value, isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Native image processing converts valid images and rejects non-images',
|
||||
(tester) async {
|
||||
final service = BauteilPhotoService();
|
||||
// Codec operations need real asynchronous work rather than the fake clock.
|
||||
final converted = await tester.runAsync(() => service.preparePhoto(png));
|
||||
expect(converted, isNotEmpty);
|
||||
await expectLater(
|
||||
service.preparePhoto(Uint8List.fromList([1, 2, 3])),
|
||||
throwsA(anything),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Narrow card shows thumbnail and menu; cancel and confirm deletion work',
|
||||
(tester) async {
|
||||
await show(tester, narrow: true);
|
||||
fill();
|
||||
photos.result = png;
|
||||
await controller.pickPhoto();
|
||||
await controller.saveBauteil();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(BauteilCard),
|
||||
250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(BauteilCard),
|
||||
matching: find.byType(BauteilPhoto),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.ensureVisible(find.byTooltip('Bauteil verwalten'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('Bauteil verwalten'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Löschen'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Abbrechen'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(remaining('a'), 940);
|
||||
await tester.tap(find.byTooltip('Bauteil verwalten'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Löschen'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Löschen'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.bauteile, isEmpty);
|
||||
expect(remaining('a'), 1000);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('Home displays current remaining grams after saving a part', (
|
||||
tester,
|
||||
) async {
|
||||
await show(tester);
|
||||
fill();
|
||||
await controller.saveBauteil();
|
||||
await tester.pumpWidget(
|
||||
GetMaterialApp(theme: AppTheme.light, home: const HomeView()),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('940 g verbleibend'), findsOneWidget);
|
||||
expect(find.text('960 g verbleibend'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:ertl_filament_verwaltung/services/get_storage_services.dart';
|
||||
|
||||
class MemoryStorageService extends GetxService implements GetStorageService {
|
||||
final tables = <String, List<Map<String, dynamic>>>{};
|
||||
bool failWrites = false;
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
|
||||
@override
|
||||
List<Map<String, dynamic>> getModelList(String key) =>
|
||||
(jsonDecode(jsonEncode(tables[key] ?? [])) as List)
|
||||
.map((item) => Map<String, dynamic>.from(item as Map))
|
||||
.toList();
|
||||
|
||||
@override
|
||||
Future<void> saveModelList(
|
||||
String key,
|
||||
List<Map<String, dynamic>> items,
|
||||
) async {
|
||||
if (failWrites) throw StateError('Storage unavailable');
|
||||
tables[key] = items;
|
||||
}
|
||||
}
|
||||
+107
-20
@@ -1,30 +1,117 @@
|
||||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:ertl_filament_verwaltung/main.dart';
|
||||
import 'package:ertl_filament_verwaltung/controller/home_controller.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 'package:ertl_filament_verwaltung/widgets/filament_card.dart';
|
||||
import 'package:ertl_filament_verwaltung/widgets/filament_form.dart';
|
||||
|
||||
import 'helpers/memory_storage_service.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
late HomeController controller;
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
setUp(() {
|
||||
Get.testMode = true;
|
||||
Get.put<GetStorageService>(MemoryStorageService());
|
||||
controller = Get.put(HomeController());
|
||||
});
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
tearDown(() => Get.reset());
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
Future<void> showHome(WidgetTester tester, Size size) async {
|
||||
tester.view.physicalSize = size;
|
||||
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();
|
||||
}
|
||||
|
||||
KaufFilamentModel sample() => KaufFilamentModel(
|
||||
filamentId: 'sample',
|
||||
kaufdatum: DateTime(2026, 10, 27),
|
||||
haendler: 'Filament Shop',
|
||||
marke: 'Testmarke',
|
||||
material: 'PLA+',
|
||||
preis: 24.90,
|
||||
farbe: 'Ozeanblau',
|
||||
gewicht: '1000',
|
||||
);
|
||||
|
||||
testWidgets('Desktop shows both panes and edits PLA+', (tester) async {
|
||||
controller.filamente.add(sample());
|
||||
await showHome(tester, const Size(1280, 900));
|
||||
|
||||
expect(
|
||||
tester.getTopLeft(find.byType(FilamentCard)).dx,
|
||||
greaterThan(tester.getTopRight(find.byType(FilamentForm)).dx),
|
||||
);
|
||||
expect(find.text('1000 g'), findsOneWidget);
|
||||
expect(find.text('Testmarke'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Filament verwalten'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Bearbeiten'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Filament bearbeiten'), findsOneWidget);
|
||||
expect(controller.markeController.text, 'Testmarke');
|
||||
expect(controller.material.value, 'PLA+');
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('Small phone scrolls to save and shows the purchase', (
|
||||
tester,
|
||||
) async {
|
||||
await showHome(tester, const Size(320, 640));
|
||||
controller.haendlerController.text = 'Filament Shop';
|
||||
controller.markeController.text = 'Testmarke';
|
||||
controller.farbeController.text = 'Ozeanblau';
|
||||
controller.preisController.text = '24,90';
|
||||
controller.gewichtController.text = '1000';
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Filament speichern'),
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Filament speichern'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.filamente, hasLength(1));
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(FilamentCard),
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Testmarke'), findsOneWidget);
|
||||
expect(find.text('1000 g'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('Empty inventory is accessible on a narrow screen', (
|
||||
tester,
|
||||
) async {
|
||||
await showHome(tester, const Size(320, 640));
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Hier beginnt dein Bestand'),
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text('Hier beginnt dein Bestand').hitTestable(),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user