import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:tane/data/export_import/inventory_json_codec.dart'; import 'package:tane/data/export_import/inventory_snapshot.dart'; import 'package:tane/db/database.dart'; import 'package:tane/i18n/strings.g.dart'; import 'package:tane/services/export_import_service.dart'; import 'package:tane/services/file_service.dart'; import 'package:tane/ui/backup_section.dart'; import 'package:tane/ui/settings_screen.dart'; import '../support/test_support.dart'; /// Records calls instead of opening real dialogs. class FakeFileService implements FileService { String? savedName; Uint8List? savedBytes; Uint8List? bytesToPick; @override Future saveFile({ required String suggestedName, required Uint8List bytes, }) async { savedName = suggestedName; savedBytes = bytes; return '/picked/$suggestedName'; } @override Future pickFileBytes({List? allowedExtensions}) async => bytesToPick; } void main() { late AppDatabase db; late FakeFileService files; late ExportImportService service; setUp(() { db = newTestDatabase(); files = FakeFileService(); service = ExportImportService( repository: newTestRepository(db), files: files, ); }); tearDown(() => db.close()); Widget wrap(Widget child) { LocaleSettings.setLocaleSync(AppLocale.en); return TranslationProvider( child: MaterialApp( supportedLocales: AppLocaleUtils.supportedLocales, localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], home: child, ), ); } testWidgets('settings shows the backup section with its three actions', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(800, 1400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(wrap(SettingsScreen(exportImport: service))); expect(find.text('Backup'), findsOneWidget); expect(find.text('Export as CSV'), findsOneWidget); expect(find.text('Export as JSON'), findsOneWidget); expect(find.text('Import from JSON'), findsOneWidget); }); testWidgets('export CSV saves a .csv file and confirms', (tester) async { await tester.pumpWidget( wrap(Scaffold(body: BackupSection(service: service))), ); await tester.tap(find.text('Export as CSV')); await tester.pumpAndSettle(); expect(files.savedName, endsWith('.csv')); expect(utf8.decode(files.savedBytes!), startsWith('variety_id,')); expect(find.text('Copy saved'), findsOneWidget); }); testWidgets('export JSON saves a versioned .json file', (tester) async { await tester.pumpWidget( wrap(Scaffold(body: BackupSection(service: service))), ); await tester.tap(find.text('Export as JSON')); await tester.pumpAndSettle(); expect(files.savedName, endsWith('.json')); final root = jsonDecode(utf8.decode(files.savedBytes!)) as Map; expect(root['formatVersion'], inventoryFormatVersion); expect(find.text('Copy saved'), findsOneWidget); }); testWidgets('import asks for confirmation, imports and reports counts', ( tester, ) async { // A valid one-variety export produced by another repository. final otherDb = newTestDatabase(); addTearDown(otherDb.close); final other = newTestRepository(otherDb, nodeId: 'node-other'); await other.addQuickVariety(label: 'Imported bean'); files.bytesToPick = utf8.encode( const InventoryJsonCodec().encode(await other.exportInventory()), ); await tester.pumpWidget( wrap(Scaffold(body: BackupSection(service: service))), ); await tester.tap(find.text('Import from JSON')); await tester.pumpAndSettle(); expect(find.text('Import a saved copy?'), findsOneWidget); await tester.tap(find.text('Import')); await tester.pumpAndSettle(); expect(find.text('Imported: 1 new, 0 updated'), findsOneWidget); final varieties = await db.select(db.varieties).get(); expect(varieties.single.label, 'Imported bean'); }); testWidgets('an unreadable file reports a friendly error', (tester) async { files.bytesToPick = utf8.encode('this is not json'); await tester.pumpWidget( wrap(Scaffold(body: BackupSection(service: service))), ); await tester.tap(find.text('Import from JSON')); await tester.pumpAndSettle(); await tester.tap(find.text('Import')); await tester.pumpAndSettle(); expect( find.text('This file could not be read as a Tanemaki copy'), findsOneWidget, ); }); }