add import export and items
This commit is contained in:
@@ -16,7 +16,6 @@ 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';
|
||||
@@ -131,19 +130,84 @@ void main() {
|
||||
tag: 'reload',
|
||||
);
|
||||
expect(reloaded.anwendungen.single.anwendungId, saved.anwendungId);
|
||||
expect(reloaded.druckController.text, '220.5');
|
||||
expect(reloaded.druckbettController.text, '65.0');
|
||||
expect(reloaded.bauraumController.text, '40.0');
|
||||
expect(reloaded.bauraumGeschlossen.value, isTrue);
|
||||
expect(reloaded.sonstigesController.text, 'Luefter 50 %');
|
||||
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(controller.druckController.text, '220,5');
|
||||
expect(find.text('Gespeicherte Profile'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('Editing replaces the profile and keeps its identity', (
|
||||
tester,
|
||||
) async {
|
||||
final otherProfile = storage
|
||||
.getModelList(AnwendungController.storageKey)
|
||||
.single;
|
||||
final original = profile('profile-a', 'a');
|
||||
storage.tables[AnwendungController.storageKey]!.add(original.toJson());
|
||||
final controller = Get.put(AnwendungController(filamentId: 'a'));
|
||||
await show(tester, const AnwendungView());
|
||||
expect(controller.druckController.text, '210.0');
|
||||
expect(controller.druckbettController.text, '60.0');
|
||||
expect(controller.bauraumController.text, isEmpty);
|
||||
|
||||
controller.druckController.text = '225';
|
||||
controller.sonstigesController.text = 'Aktualisiert';
|
||||
storage.failWrites = true;
|
||||
await controller.saveAnwendung();
|
||||
expect(controller.hasError.value, isTrue);
|
||||
expect(controller.anwendungen.single.druckTemperatur, 210);
|
||||
expect(controller.druckController.text, '225');
|
||||
|
||||
storage.failWrites = false;
|
||||
await controller.saveAnwendung();
|
||||
controller.druckController.text = '230';
|
||||
await controller.saveAnwendung();
|
||||
final stored = storage.getModelList(AnwendungController.storageKey);
|
||||
expect(stored, hasLength(2));
|
||||
expect(stored.first, otherProfile);
|
||||
final updated = AnwendungModel.fromJson(stored.last);
|
||||
expect(updated.anwendungId, original.anwendungId);
|
||||
expect(updated.erstelltAm, original.erstelltAm);
|
||||
expect(updated.druckTemperatur, 230);
|
||||
expect(updated.sonstiges, 'Aktualisiert');
|
||||
expect(controller.anwendungen, hasLength(1));
|
||||
});
|
||||
|
||||
testWidgets('Legacy profiles load the latest and save one per filament', (
|
||||
tester,
|
||||
) async {
|
||||
final latest = AnwendungModel(
|
||||
anwendungId: 'latest-a',
|
||||
filamentId: 'a',
|
||||
erstelltAm: DateTime(2026, 10, 27),
|
||||
druckbettTemperatur: 70,
|
||||
druckTemperatur: 240,
|
||||
bauraumGeschlossen: true,
|
||||
bauraumTemperatur: 45,
|
||||
sonstiges: 'Neuestes Profil',
|
||||
);
|
||||
storage.tables[AnwendungController.storageKey]!.addAll([
|
||||
latest.toJson(),
|
||||
profile('older-a', 'a').toJson(),
|
||||
]);
|
||||
final controller = Get.put(AnwendungController(filamentId: 'a'));
|
||||
await show(tester, const AnwendungView());
|
||||
expect(controller.druckController.text, '240.0');
|
||||
expect(controller.bauraumGeschlossen.value, isTrue);
|
||||
expect(controller.sonstigesController.text, 'Neuestes Profil');
|
||||
await controller.saveAnwendung();
|
||||
final stored = storage.getModelList(AnwendungController.storageKey);
|
||||
expect(stored, hasLength(2));
|
||||
expect(stored.where((item) => item['filamentId'] == 'a'), hasLength(1));
|
||||
expect(stored.last['anwendungId'], 'latest-a');
|
||||
});
|
||||
|
||||
testWidgets('Profile write failure retains input and does not add history', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:ertl_filament_verwaltung/controller/export_controller.dart';
|
||||
import 'package:ertl_filament_verwaltung/controller/home_controller.dart';
|
||||
import 'package:ertl_filament_verwaltung/helper/sample_routes.dart';
|
||||
import 'package:ertl_filament_verwaltung/helper/storage_keys.dart';
|
||||
import 'package:ertl_filament_verwaltung/models/anwendung_model.dart';
|
||||
import 'package:ertl_filament_verwaltung/models/backup_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/export_view.dart';
|
||||
import 'package:ertl_filament_verwaltung/pages/home_view.dart';
|
||||
import 'package:ertl_filament_verwaltung/services/backup_file_service.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';
|
||||
|
||||
class FakeBackupFiles extends BackupFileService {
|
||||
String? source;
|
||||
String? exported;
|
||||
bool cancelSave = false;
|
||||
|
||||
@override
|
||||
Future<String?> open() async => source;
|
||||
|
||||
@override
|
||||
Future<bool> save(String json, String name) async {
|
||||
if (cancelSave) return false;
|
||||
exported = json;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class FailOnceStorage extends MemoryStorageService {
|
||||
String? failKey;
|
||||
|
||||
@override
|
||||
Future<void> saveModelList(
|
||||
String key,
|
||||
List<Map<String, dynamic>> items,
|
||||
) async {
|
||||
if (failKey == key) {
|
||||
failKey = null;
|
||||
throw StateError('Write failed');
|
||||
}
|
||||
await super.saveModelList(key, items);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late FailOnceStorage storage;
|
||||
late FakeBackupFiles files;
|
||||
late ExportController controller;
|
||||
|
||||
Map<String, List<Map<String, dynamic>>> fixture() => {
|
||||
StorageKeys.filamente: [
|
||||
KaufFilamentModel(
|
||||
filamentId: 'f1',
|
||||
kaufdatum: DateTime(2026),
|
||||
haendler: 'Shop',
|
||||
marke: 'Marke',
|
||||
material: 'PA12',
|
||||
preis: 20,
|
||||
farbe: 'Grün',
|
||||
gewicht: '1000',
|
||||
).toJson(),
|
||||
],
|
||||
StorageKeys.anwendungen: [
|
||||
AnwendungModel(
|
||||
anwendungId: 'p1',
|
||||
filamentId: 'f1',
|
||||
erstelltAm: DateTime(2026),
|
||||
druckbettTemperatur: 60,
|
||||
druckTemperatur: 220,
|
||||
bauraumGeschlossen: true,
|
||||
).toJson(),
|
||||
],
|
||||
StorageKeys.bauteile: [
|
||||
BauteilModel(
|
||||
bauteilId: 'b1',
|
||||
erstelltAm: DateTime(2026),
|
||||
name: 'Halter',
|
||||
verwendung: 'Werkstatt',
|
||||
druckdauerMinuten: 60,
|
||||
gewichtGramm: 40,
|
||||
abfallGramm: 10,
|
||||
filamente: [
|
||||
const BauteilFilamentModel(
|
||||
filamentId: 'f1',
|
||||
anwendungId: 'p1',
|
||||
verbrauchGramm: 50,
|
||||
),
|
||||
],
|
||||
fotoBase64: 'aGVsbG8=',
|
||||
).toJson(),
|
||||
],
|
||||
StorageKeys.materialien: [
|
||||
{
|
||||
'werte': ['PA12', 'PLA'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
setUp(() {
|
||||
Get.testMode = true;
|
||||
storage = FailOnceStorage();
|
||||
Get.put<GetStorageService>(storage);
|
||||
files = FakeBackupFiles();
|
||||
controller = Get.put(ExportController(fileService: files));
|
||||
});
|
||||
tearDown(() => Get.reset());
|
||||
|
||||
test(
|
||||
'Complete JSON backup restores photos, IDs, stock and custom materials',
|
||||
() async {
|
||||
storage.tables.addAll(fixture());
|
||||
await controller.exportData();
|
||||
expect(files.exported, isNotNull);
|
||||
expect(BackupModel.decode(files.exported!).data, fixture());
|
||||
storage.tables.clear();
|
||||
final home = Get.put(HomeController());
|
||||
files.source = files.exported;
|
||||
await controller.selectImport();
|
||||
expect(storage.tables, isEmpty);
|
||||
await controller.importData();
|
||||
expect(storage.tables, fixture());
|
||||
expect(home.materialien, ['PA12', 'PLA']);
|
||||
expect(home.restgewicht(home.filamente.single), 950);
|
||||
expect(controller.hasError.value, isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'Malformed, incomplete, future-version and duplicate data do not write',
|
||||
() async {
|
||||
storage.tables.addAll(fixture());
|
||||
final valid =
|
||||
jsonDecode(BackupModel(fixture()).encode()) as Map<String, dynamic>;
|
||||
final badData = fixture();
|
||||
badData[StorageKeys.filamente]!.add(
|
||||
badData[StorageKeys.filamente]!.single,
|
||||
);
|
||||
for (final source in [
|
||||
'not json',
|
||||
'{}',
|
||||
jsonEncode({...valid, 'version': 2}),
|
||||
jsonEncode({
|
||||
...valid,
|
||||
'daten': {StorageKeys.filamente: []},
|
||||
}),
|
||||
BackupModel(badData).encode(),
|
||||
]) {
|
||||
files.source = source;
|
||||
await controller.selectImport();
|
||||
await controller.importData();
|
||||
expect(controller.pendingBackup.value, isNull);
|
||||
expect(controller.hasError.value, isTrue);
|
||||
expect(storage.tables, fixture());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test('Cancelled dialogs leave data unchanged', () async {
|
||||
storage.tables.addAll(fixture());
|
||||
files.cancelSave = true;
|
||||
await controller.exportData();
|
||||
await controller.selectImport();
|
||||
expect(controller.message.value, isNull);
|
||||
expect(controller.pendingBackup.value, isNull);
|
||||
expect(storage.tables, fixture());
|
||||
});
|
||||
|
||||
test('A failed import restores previously written tables', () async {
|
||||
storage.tables.addAll(fixture());
|
||||
files.source = BackupModel({for (final key in BackupModel.keys) key: []})
|
||||
.encode();
|
||||
await controller.selectImport();
|
||||
storage.failKey = StorageKeys.bauteile;
|
||||
await controller.importData();
|
||||
expect(storage.tables, fixture());
|
||||
expect(controller.hasError.value, isTrue);
|
||||
expect(controller.isBusy.value, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Import preview requires confirmation and works on a phone', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(320, 640);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
storage.tables.addAll(fixture());
|
||||
files.source = BackupModel({for (final key in BackupModel.keys) key: []})
|
||||
.encode();
|
||||
await tester.pumpWidget(
|
||||
GetMaterialApp(theme: AppTheme.light, home: const ExportView()),
|
||||
);
|
||||
await controller.selectImport();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('Daten ersetzen und importieren'));
|
||||
await tester.tap(find.text('Daten ersetzen und importieren'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Abbrechen'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(storage.tables, fixture());
|
||||
await tester.tap(find.text('Daten ersetzen und importieren'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Daten ersetzen'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(storage.tables.values.every((value) => value.isEmpty), isTrue);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('Hamburger menu opens the backup route', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
GetMaterialApp(
|
||||
theme: AppTheme.light,
|
||||
initialRoute: HomeView.routeName,
|
||||
getPages: SampleRouts.samplePages,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byIcon(Icons.menu));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Daten exportieren / importieren'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ExportView), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:ertl_filament_verwaltung/controller/home_controller.dart';
|
||||
import 'package:ertl_filament_verwaltung/helper/storage_keys.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';
|
||||
@@ -114,4 +115,106 @@ void main() {
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('Custom material can be typed, selected and removed on a phone', (
|
||||
tester,
|
||||
) async {
|
||||
await showHome(tester, const Size(320, 640));
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is TextFormField &&
|
||||
widget.controller == controller.materialController,
|
||||
);
|
||||
await tester.ensureVisible(field);
|
||||
await tester.enterText(field, 'PA12');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.ensureVisible(find.text('Art hinzufügen'));
|
||||
await tester.tap(find.text('Art hinzufügen'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.materialien, contains('PA12'));
|
||||
|
||||
await Scrollable.ensureVisible(
|
||||
tester.element(find.byTooltip('Material auswählen')),
|
||||
alignment: 0.5,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('Material auswählen'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('PETG').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.materialController.text, 'PETG');
|
||||
|
||||
await tester.ensureVisible(find.text('Arten verwalten'));
|
||||
await tester.tap(find.text('Arten verwalten'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(
|
||||
find.byTooltip('PA12 aus Liste entfernen'),
|
||||
180,
|
||||
scrollable: find.descendant(
|
||||
of: find.byType(AlertDialog),
|
||||
matching: find.byType(Scrollable),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byTooltip('PA12 aus Liste entfernen'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.materialien, isNot(contains('PA12')));
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.tap(find.text('Schließen'));
|
||||
await tester.pumpAndSettle();
|
||||
final reloaded = Get.put(HomeController(), tag: 'materials-reload');
|
||||
expect(reloaded.materialien, isNot(contains('PA12')));
|
||||
});
|
||||
|
||||
testWidgets('Saving adds custom material and removal preserves purchases', (
|
||||
tester,
|
||||
) async {
|
||||
await showHome(tester, const Size(1280, 900));
|
||||
controller.haendlerController.text = 'Shop';
|
||||
controller.markeController.text = 'Marke';
|
||||
controller.farbeController.text = 'Rot';
|
||||
controller.preisController.text = '20';
|
||||
controller.gewichtController.text = '1000';
|
||||
controller.materialController.text = ' PA12 ';
|
||||
await controller.saveFilament();
|
||||
expect(controller.filamente.single.material, 'PA12');
|
||||
final reloaded = Get.put(HomeController(), tag: 'custom-reload');
|
||||
expect(reloaded.materialien, contains('PA12'));
|
||||
await controller.removeMaterial('PA12');
|
||||
await controller.editFilament(controller.filamente.single);
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.materialController.text, 'PA12');
|
||||
expect(controller.materialien, isNot(contains('PA12')));
|
||||
expect(controller.filamente.single.material, 'PA12');
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'An empty material list survives reload and failed writes keep the list',
|
||||
() async {
|
||||
final storage = Get.find<GetStorageService>() as MemoryStorageService;
|
||||
controller.materialController.text = 'pa12';
|
||||
expect(await controller.addMaterial(), isTrue);
|
||||
controller.materialController.text = ' PA12 ';
|
||||
expect(await controller.addMaterial(), isTrue);
|
||||
expect(
|
||||
controller.materialien.where((item) => item.toLowerCase() == 'pa12'),
|
||||
hasLength(1),
|
||||
);
|
||||
storage.failWrites = true;
|
||||
expect(await controller.removeMaterial('pa12'), isFalse);
|
||||
expect(controller.materialien, contains('pa12'));
|
||||
expect(controller.materialMessage.value, isNotNull);
|
||||
storage.failWrites = false;
|
||||
for (final value in controller.materialien.toList()) {
|
||||
await controller.removeMaterial(value);
|
||||
}
|
||||
final reloaded = Get.put(HomeController(), tag: 'empty-reload');
|
||||
expect(reloaded.materialien, isEmpty);
|
||||
expect(reloaded.materialController.text, isEmpty);
|
||||
expect(
|
||||
storage.getModelList(StorageKeys.materialien).single['werte'],
|
||||
isEmpty,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user