diff --git a/flutter/run_tests.bat b/flutter/run_tests.bat new file mode 100644 index 00000000..9dce2b84 --- /dev/null +++ b/flutter/run_tests.bat @@ -0,0 +1,5 @@ +@echo off +cd /d "C:\Users\Nils-JohanGynther\dev\recipe-app\flutter" +echo Running from: %CD% +flutter test test\admin_aliases_panel_test.dart +pause \ No newline at end of file diff --git a/flutter/test/admin_aliases_panel_test.dart b/flutter/test/admin_aliases_panel_test.dart new file mode 100644 index 00000000..6be53a0f --- /dev/null +++ b/flutter/test/admin_aliases_panel_test.dart @@ -0,0 +1,243 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:recipe_flutter/features/admin/data/admin_repository.dart'; +import 'package:recipe_flutter/features/admin/domain/admin_product.dart'; +import 'package:recipe_flutter/features/admin/domain/receipt_alias.dart'; +import 'package:recipe_flutter/features/admin/presentation/admin_aliases_panel.dart'; + +// Test wrapper that implements AdminRepository with minimal methods +class TestAdminRepositoryWrapper implements AdminRepository { + final FakeAdminRepository _fakeRepo; + + TestAdminRepositoryWrapper(this._fakeRepo); + + @override + Future> listReceiptAliases() => _fakeRepo.listReceiptAliases(); + + @override + Future> listGlobalProducts() => _fakeRepo.listGlobalProducts(); + + @override + Future updateReceiptAlias(int id, {String? receiptName, int? productId, bool? isGlobal}) => _fakeRepo.updateReceiptAlias(id, receiptName: receiptName, productId: productId, isGlobal: isGlobal); + + // Stub implementations for other required methods + @override + Future> aiCategorizeBulk({List? productIds}) => throw UnimplementedError(); + @override + Future bulkSetCategory(List ids, {required int? categoryId}) => throw UnimplementedError(); + @override + Future createAdminInventory({int? userId, required int productId, required double quantity, required String unit, String? location, String? brand, String? receiptName, String? suitableFor, String? comment}) => throw UnimplementedError(); + @override + Future createAdminPantry({int? userId, required int productId, String? location}) => throw UnimplementedError(); + @override + Future> createProduct(String name, {int? categoryId}) => throw UnimplementedError(); + @override + Future createUser({required String username, required String email, required String password, String role = 'user'}) => throw UnimplementedError(); + @override + Future deleteUser(int userId) => throw UnimplementedError(); + @override + Future> listAdminInventory({int? userId, String? sort}) => throw UnimplementedError(); + @override + Future> listAdminPantry({int? userId}) => throw UnimplementedError(); + @override + Future> listAiModels() => throw UnimplementedError(); + @override + Future> listCategoryTree() => throw UnimplementedError(); + @override + Future> listDeletedProducts() => throw UnimplementedError(); + @override + Future> listPendingProducts() => throw UnimplementedError(); + @override + Future> listPrivateProducts() => throw UnimplementedError(); + @override + Future> listProducts() => throw UnimplementedError(); + @override + Future> listSelectableProductsForAdmin({bool forceRefresh = false}) => throw UnimplementedError(); + @override + Future> listUsers() => throw UnimplementedError(); + @override + Future mergeAdminInventory({required int sourceInventoryId, required int targetInventoryId}) => throw UnimplementedError(); + @override + Future mergeProducts({required int sourceProductId, required int targetProductId}) => throw UnimplementedError(); + @override + Future mergeProductsPrivate({required int sourceProductId, required int targetProductId}) => throw UnimplementedError(); + @override + Future moveAdminInventoryToPantry(int inventoryId) => throw UnimplementedError(); + @override + Future moveAdminPantryToInventory(int pantryItemId, Map body) => throw UnimplementedError(); + @override + Future> previewAdminInventoryMerge({required int sourceInventoryId, required int targetInventoryId}) => throw UnimplementedError(); + @override + Future> previewMerge({required int sourceProductId, required int targetProductId}) => throw UnimplementedError(); + @override + Future promotePrivateProduct(int productId) => throw UnimplementedError(); + @override + Future removeAdminInventory(int inventoryId) => throw UnimplementedError(); + @override + Future removeAdminPantryItem(int pantryItemId) => throw UnimplementedError(); + @override + Future removeProduct(int productId) => throw UnimplementedError(); + @override + Future removeReceiptAlias(int id) => throw UnimplementedError(); + @override + Future> resetPassword(int userId) => throw UnimplementedError(); + @override + Future restoreProduct(int productId) => throw UnimplementedError(); + @override + Future setPremium(int userId, {required bool isPremium}) => throw UnimplementedError(); + @override + Future setProductCategory(int productId, {required int? categoryId}) => throw UnimplementedError(); + @override + Future setProductStatus(int productId, String status) => throw UnimplementedError(); + @override + Future setRecipeSharing(int userId, {required bool canShareRecipes}) => throw UnimplementedError(); + @override + Future setRole(int userId, String newRole) => throw UnimplementedError(); + @override + Future updateAdminInventory(int inventoryId, {int? productId, double? quantity, String? unit, String? location, String? brand, String? receiptName, String? suitableFor, String? comment}) => throw UnimplementedError(); + @override + Future updateAdminPantry(int pantryItemId, {int? productId, String? location}) => throw UnimplementedError(); + @override + Future updateCanonicalName(int productId, String canonicalName) => throw UnimplementedError(); + @override + Future updateCanonicalNamePrivate(int productId, String canonicalName) => throw UnimplementedError(); + @override + Future updateEmail(int userId, String email) => throw UnimplementedError(); + @override + Future upsertReceiptAlias({required String receiptName, required int productId, bool isGlobal = false}) => throw UnimplementedError(); +} + +// Simple fake that only implements the methods we need +class FakeAdminRepository { + List _aliases = []; + List _products = []; + + Future> listReceiptAliases() async { + return _aliases; + } + + Future> listGlobalProducts() async { + return _products; + } + + Future updateReceiptAlias(int id, {String? receiptName, int? productId, bool? isGlobal}) async { + // Find and update alias + final index = _aliases.indexWhere((a) => a.id == id); + if (index >= 0) { + _aliases[index] = ReceiptAlias( + id: id, + receiptName: receiptName ?? _aliases[index].receiptName, + productId: productId ?? _aliases[index].productId, + displayProductName: _products.firstWhere((p) => p.id == (productId ?? _aliases[index].productId)).displayName, + isGlobal: isGlobal ?? _aliases[index].isGlobal, + ); + } + } + + void setAliases(List aliases) { + _aliases = aliases; + } + + void setProducts(List products) { + _products = products; + } +} + +void main() { + group('AdminAliasesPanel - Switch Tests', () { + late FakeAdminRepository fakeRepo; + late TestAdminRepositoryWrapper wrapper; + late List mockAliases; + late List mockProducts; + + setUp(() { + fakeRepo = FakeAdminRepository(); + wrapper = TestAdminRepositoryWrapper(fakeRepo); + mockProducts = [ + AdminProduct(id: 1, name: 'Test Product', displayName: 'Test Product', categoryPath: 'Test > Category'), + ]; + fakeRepo.setProducts(mockProducts); + }); + + testWidgets('Switch should be disabled for global alias', (tester) async { + mockAliases = [ + ReceiptAlias( + id: 1, + receiptName: 'Global Alias', + productId: 1, + displayProductName: 'Test Product', + isGlobal: true, + ), + ]; + fakeRepo.setAliases(mockAliases); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + adminRepositoryProvider.overrideWithValue(wrapper), + ], + child: MaterialApp( + home: Scaffold( + body: AdminAliasesPanel(embedded: true), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Find the switch in the first alias card + final switchFinder = find.byType(Switch); + expect(switchFinder, findsOneWidget); + + final switchWidget = tester.widget(switchFinder); + expect(switchWidget.value, isTrue); + expect(switchWidget.onChanged, isNull); // Disabled + + // Verify subtitle text + expect(find.text('Aliaset är redan globalt.'), findsOneWidget); + }); + + testWidgets('Switch should be enabled for private alias', (tester) async { + mockAliases = [ + ReceiptAlias( + id: 1, + receiptName: 'Private Alias', + productId: 1, + displayProductName: 'Test Product', + isGlobal: false, + ), + ]; + fakeRepo.setAliases(mockAliases); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + adminRepositoryProvider.overrideWithValue(wrapper), + ], + child: MaterialApp( + home: Scaffold( + body: AdminAliasesPanel(embedded: true), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Find the switch in the first alias card + final switchFinder = find.byType(Switch); + expect(switchFinder, findsOneWidget); + + final switchWidget = tester.widget(switchFinder); + expect(switchWidget.value, isFalse); + expect(switchWidget.onChanged, isNotNull); // Enabled + + // Verify subtitle text + expect(find.text('Du kan göra privata alias globala.'), findsOneWidget); + }); + }); +} + + diff --git a/kilo.json b/kilo.json new file mode 100644 index 00000000..c185b77a --- /dev/null +++ b/kilo.json @@ -0,0 +1,5 @@ +{ + "env": { + "MISTRAL_API_KEY": "process.env.MISTRAL_API_KEY" + } +}