add two views

This commit is contained in:
atseirjo
2026-10-27 15:00:16 +01:00
parent 864da57537
commit 07a1bb02fd
42 changed files with 3582 additions and 346 deletions
+295
View File
@@ -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);
},
);
}
+365
View File
@@ -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);
});
}
+27
View File
@@ -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
View File
@@ -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);
});
}