diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart new file mode 100644 index 0000000..eea5947 --- /dev/null +++ b/integration_test/app_test.dart @@ -0,0 +1,225 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:provider/provider.dart'; +import 'package:biteplan/app.dart'; +import 'package:biteplan/features/meal_planner/providers/meal_planner_provider.dart'; +import 'package:biteplan/features/converter/providers/converter_provider.dart'; +import 'package:biteplan/features/shopping_list/providers/shopping_list_provider.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + }); + + Widget buildApp() => MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => MealPlannerProvider()..load()), + ChangeNotifierProvider( + create: (_) => ConverterProvider()..loadDb()), + ChangeNotifierProvider( + create: (_) => ShoppingListProvider()..load()), + ], + child: const BitePlanApp(), + ); + + // ── Navigazione ──────────────────────────────────────────────────────────── + + group('Navigazione', () { + testWidgets('il tab iniziale è Piano Pasti', (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + expect(find.text('Piano Pasti'), findsOneWidget); + }); + + testWidgets('passa al tab Convertitore', (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + await tester.tap(find.text('Converti')); + await tester.pumpAndSettle(); + expect(find.text('Convertitore'), findsOneWidget); + }); + + testWidgets('passa al tab Lista della spesa', (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + await tester.tap(find.text('Spesa')); + await tester.pumpAndSettle(); + expect(find.text('Lista della spesa'), findsOneWidget); + }); + + testWidgets('naviga tra tutti e tre i tab', (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Converti')); + await tester.pumpAndSettle(); + expect(find.text('Convertitore'), findsOneWidget); + + await tester.tap(find.text('Spesa')); + await tester.pumpAndSettle(); + expect(find.text('Lista della spesa'), findsOneWidget); + + await tester.tap(find.text('Pasti')); + await tester.pumpAndSettle(); + expect(find.text('Piano Pasti'), findsOneWidget); + }); + }); + + // ── Lista della spesa ────────────────────────────────────────────────────── + + group('Lista della spesa', () { + testWidgets('aggiunge un elemento e lo mostra', (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + await tester.tap(find.text('Spesa')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), 'Pollo'); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + expect(find.text('Pollo'), findsOneWidget); + }); + + testWidgets('rimuove un elemento con il pulsante chiudi', (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + await tester.tap(find.text('Spesa')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), 'Riso'); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.close).first); + await tester.pumpAndSettle(); + + expect(find.text('Riso'), findsNothing); + }); + + testWidgets('spuntare un elemento lo sposta in Completati', (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + await tester.tap(find.text('Spesa')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), 'Pasta'); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(Checkbox).first); + await tester.pumpAndSettle(); + + expect(find.text('COMPLETATI (1)'), findsOneWidget); + }); + + testWidgets('aggiorna il contatore completati/totale', (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + await tester.tap(find.text('Spesa')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), 'Pollo'); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextField), 'Riso'); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(Checkbox).first); + await tester.pumpAndSettle(); + + expect(find.text('1 / 2 completato'), findsOneWidget); + }); + }); + + // ── Convertitore ────────────────────────────────────────────────────────── + + group('Convertitore', () { + testWidgets('mostra hint iniziale', (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + await tester.tap(find.text('Converti')); + await tester.pumpAndSettle(); + + expect(find.text('Cerca un alimento per iniziare'), findsOneWidget); + }); + + testWidgets('la ricerca mostra risultati pertinenti', (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + await tester.tap(find.text('Converti')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), 'pollo'); + await tester.pumpAndSettle(); + + expect(find.textContaining('Pollo'), findsWidgets); + }); + + testWidgets('selezionare un alimento mostra la card del convertitore', + (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + await tester.tap(find.text('Converti')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), 'riso basmati'); + await tester.pumpAndSettle(); + + await tester.tap(find.textContaining('Riso basmati').first); + await tester.pumpAndSettle(); + + expect(find.text('CRUDO'), findsOneWidget); + expect(find.text('COTTO'), findsOneWidget); + expect(find.text('Cambia'), findsOneWidget); + }); + + testWidgets('il pulsante Cambia resetta la selezione', (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + await tester.tap(find.text('Converti')); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), 'riso basmati'); + await tester.pumpAndSettle(); + await tester.tap(find.textContaining('Riso basmati').first); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Cambia')); + await tester.pumpAndSettle(); + + expect(find.text('Cerca un alimento per iniziare'), findsOneWidget); + }); + }); + + // ── Meal Planner ─────────────────────────────────────────────────────────── + + group('Piano Pasti', () { + testWidgets('genera lista della spesa e naviga al tab Spesa', + (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + + // Trova il giorno corrente espanso e aggiungi un elemento + final addButtons = find.byIcon(Icons.add); + if (addButtons.evaluate().isNotEmpty) { + final firstTextField = find.byType(TextField).first; + await tester.enterText(firstTextField, 'Broccoli'); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + } + + await tester.tap(find.text('Genera lista della spesa')); + await tester.pumpAndSettle(); + + // Deve essere atterrato sul tab Spesa + expect(find.text('Lista della spesa'), findsOneWidget); + }); + }); +} diff --git a/test/features/converter/models/conversion_entry_test.dart b/test/features/converter/models/conversion_entry_test.dart new file mode 100644 index 0000000..eb2c9e2 --- /dev/null +++ b/test/features/converter/models/conversion_entry_test.dart @@ -0,0 +1,65 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:biteplan/features/converter/models/conversion_entry.dart'; + +void main() { + group('ConversionEntry', () { + test('rawToCooked moltiplica per il fattore di resa', () { + const entry = ConversionEntry( + food: 'riso basmati', + method: 'bollitura', + yieldFactor: 3.0, + ); + expect(entry.rawToCooked(100), closeTo(300.0, 0.001)); + expect(entry.rawToCooked(70), closeTo(210.0, 0.001)); + }); + + test('cookedToRaw divide per il fattore di resa', () { + const entry = ConversionEntry( + food: 'riso basmati', + method: 'bollitura', + yieldFactor: 3.0, + ); + expect(entry.cookedToRaw(300), closeTo(100.0, 0.001)); + expect(entry.cookedToRaw(150), closeTo(50.0, 0.001)); + }); + + test('conversione bidirezionale è consistente', () { + const entry = ConversionEntry( + food: 'pollo petto', + method: 'forno', + yieldFactor: 0.67, + ); + final cooked = entry.rawToCooked(140); + final backToRaw = entry.cookedToRaw(cooked); + expect(backToRaw, closeTo(140, 0.001)); + }); + + test('yield > 1: i cereali aumentano di peso con la cottura', () { + const entry = ConversionEntry( + food: 'pasta', + method: 'bollitura', + yieldFactor: 2.1, + ); + expect(entry.rawToCooked(80), greaterThan(80)); + }); + + test('yield < 1: la carne perde peso con la cottura', () { + const entry = ConversionEntry( + food: 'pollo petto', + method: 'forno', + yieldFactor: 0.67, + ); + expect(entry.rawToCooked(100), lessThan(100)); + }); + + test('valore limite: 0 grammi restituisce 0', () { + const entry = ConversionEntry( + food: 'riso', + method: 'bollitura', + yieldFactor: 3.0, + ); + expect(entry.rawToCooked(0), 0.0); + expect(entry.cookedToRaw(0), 0.0); + }); + }); +} diff --git a/test/features/meal_planner/models/meal_plan_test.dart b/test/features/meal_planner/models/meal_plan_test.dart new file mode 100644 index 0000000..209f986 --- /dev/null +++ b/test/features/meal_planner/models/meal_plan_test.dart @@ -0,0 +1,100 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:biteplan/features/meal_planner/models/meal_plan.dart'; + +void main() { + group('DayPlan', () { + test('crea con liste vuote di default', () { + final day = DayPlan(); + expect(day.colazione, isEmpty); + expect(day.pranzo, isEmpty); + expect(day.cena, isEmpty); + }); + + test('slot() ritorna la lista corretta', () { + final day = DayPlan( + colazione: ['latte'], + pranzo: ['pasta'], + cena: ['pollo'], + ); + expect(day.slot('colazione'), ['latte']); + expect(day.slot('pranzo'), ['pasta']); + expect(day.slot('cena'), ['pollo']); + expect(day.slot('unknown'), isEmpty); + }); + + test('copyWith() non modifica lorigine', () { + final day = DayPlan(colazione: ['latte']); + final updated = day.copyWith(colazione: ['latte', 'caffè']); + expect(updated.colazione, ['latte', 'caffè']); + expect(day.colazione, ['latte']); + }); + + test('copyWith() preserva i campi non aggiornati', () { + final day = DayPlan(colazione: ['latte'], pranzo: ['pasta']); + final updated = day.copyWith(colazione: ['caffè']); + expect(updated.pranzo, ['pasta']); + }); + + test('JSON round-trip preserva i dati', () { + final day = DayPlan(colazione: ['latte', 'biscotti'], cena: ['pollo']); + final restored = DayPlan.fromJson(day.toJson()); + expect(restored.colazione, ['latte', 'biscotti']); + expect(restored.cena, ['pollo']); + expect(restored.pranzo, isEmpty); + }); + }); + + group('MealPlan', () { + test('empty() crea piano con tutti i giorni', () { + const days = ['lunedi', 'martedi', 'mercoledi']; + final plan = MealPlan.empty(days); + expect(plan.days.keys, containsAll(days)); + for (final d in days) { + expect(plan.days[d]!.colazione, isEmpty); + } + }); + + test('hasAnyMeal è false con piano vuoto', () { + final plan = MealPlan.empty(['lunedi', 'martedi']); + expect(plan.hasAnyMeal, false); + }); + + test('hasAnyMeal è true con almeno un elemento', () { + final plan = MealPlan({'lunedi': DayPlan(colazione: ['latte'])}); + expect(plan.hasAnyMeal, true); + }); + + test('allItems ritorna lista appiattita', () { + final plan = MealPlan({ + 'lunedi': DayPlan(colazione: ['latte'], pranzo: ['pasta']), + 'martedi': DayPlan(cena: ['pollo']), + }); + expect(plan.allItems, containsAll(['latte', 'pasta', 'pollo'])); + expect(plan.allItems.length, 3); + }); + + test('allItems è vuota con piano vuoto', () { + final plan = MealPlan.empty(['lunedi']); + expect(plan.allItems, isEmpty); + }); + + test('withUpdatedDay() aggiorna solo il giorno specificato', () { + final plan = MealPlan.empty(['lunedi', 'martedi']); + final updated = + plan.withUpdatedDay('lunedi', DayPlan(colazione: ['latte'])); + expect(updated.days['lunedi']!.colazione, ['latte']); + expect(updated.days['martedi']!.colazione, isEmpty); + }); + + test('JSON round-trip preserva tutti i dati', () { + final original = MealPlan({ + 'lunedi': DayPlan(colazione: ['latte', 'biscotti'], cena: ['pollo']), + 'martedi': DayPlan(pranzo: ['pasta al pomodoro']), + }); + final restored = MealPlan.fromJsonString(original.toJsonString()); + expect(restored.days['lunedi']!.colazione, ['latte', 'biscotti']); + expect(restored.days['lunedi']!.cena, ['pollo']); + expect(restored.days['martedi']!.pranzo, ['pasta al pomodoro']); + }); + }); +} diff --git a/test/features/meal_planner/providers/meal_planner_provider_test.dart b/test/features/meal_planner/providers/meal_planner_provider_test.dart new file mode 100644 index 0000000..3ea06b6 --- /dev/null +++ b/test/features/meal_planner/providers/meal_planner_provider_test.dart @@ -0,0 +1,88 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:biteplan/features/meal_planner/providers/meal_planner_provider.dart'; + +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + test('inizia con piano vuoto', () async { + final provider = MealPlannerProvider(); + await provider.load(); + expect(provider.plan.hasAnyMeal, false); + }); + + test('addItem aggiunge nello slot corretto', () async { + final provider = MealPlannerProvider(); + await provider.load(); + provider.addItem('lunedi', 'colazione', 'latte'); + expect(provider.plan.days['lunedi']!.colazione, ['latte']); + expect(provider.plan.days['lunedi']!.pranzo, isEmpty); + }); + + test('addItem aggiunge in tutti e tre gli slot', () async { + final provider = MealPlannerProvider(); + await provider.load(); + provider.addItem('martedi', 'colazione', 'yogurt'); + provider.addItem('martedi', 'pranzo', 'pasta'); + provider.addItem('martedi', 'cena', 'pollo'); + expect(provider.plan.days['martedi']!.colazione, ['yogurt']); + expect(provider.plan.days['martedi']!.pranzo, ['pasta']); + expect(provider.plan.days['martedi']!.cena, ['pollo']); + }); + + test('addItem ignora testo vuoto o solo spazi', () async { + final provider = MealPlannerProvider(); + await provider.load(); + provider.addItem('lunedi', 'colazione', ' '); + provider.addItem('lunedi', 'colazione', ''); + expect(provider.plan.days['lunedi']!.colazione, isEmpty); + }); + + test('addItem esegue trim del testo', () async { + final provider = MealPlannerProvider(); + await provider.load(); + provider.addItem('lunedi', 'pranzo', ' pasta '); + expect(provider.plan.days['lunedi']!.pranzo, ['pasta']); + }); + + test('removeItem rimuove dalla posizione corretta', () async { + final provider = MealPlannerProvider(); + await provider.load(); + provider.addItem('lunedi', 'pranzo', 'pasta'); + provider.addItem('lunedi', 'pranzo', 'insalata'); + provider.addItem('lunedi', 'pranzo', 'frutta'); + provider.removeItem('lunedi', 'pranzo', 1); + expect(provider.plan.days['lunedi']!.pranzo, ['pasta', 'frutta']); + }); + + test('clearAll svuota tutti i giorni', () async { + final provider = MealPlannerProvider(); + await provider.load(); + provider.addItem('lunedi', 'colazione', 'latte'); + provider.addItem('venerdi', 'cena', 'pesce'); + provider.clearAll(); + expect(provider.plan.hasAnyMeal, false); + }); + + test('persiste e ricarica i dati', () async { + final provider1 = MealPlannerProvider(); + await provider1.load(); + provider1.addItem('lunedi', 'cena', 'pollo'); + await Future.delayed(const Duration(milliseconds: 50)); + + final provider2 = MealPlannerProvider(); + await provider2.load(); + expect(provider2.plan.days['lunedi']!.cena, ['pollo']); + }); + + test('notifica i listener dopo addItem', () async { + final provider = MealPlannerProvider(); + await provider.load(); + var notified = false; + provider.addListener(() => notified = true); + provider.addItem('lunedi', 'colazione', 'latte'); + expect(notified, true); + }); +} diff --git a/test/features/meal_planner/widgets/meal_card_test.dart b/test/features/meal_planner/widgets/meal_card_test.dart new file mode 100644 index 0000000..542eeed --- /dev/null +++ b/test/features/meal_planner/widgets/meal_card_test.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:biteplan/features/meal_planner/presentation/widgets/meal_card.dart'; +import 'package:biteplan/features/meal_planner/models/meal_plan.dart'; +import '../../../helpers/pump_app.dart'; + +Widget _card({ + DayPlan? dayPlan, + bool initiallyExpanded = false, + void Function(String, String)? onAdd, + void Function(String, int)? onRemove, +}) => + Scaffold( + body: SingleChildScrollView( + child: MealCard( + dayId: 'lunedi', + dayLabel: 'Lunedì', + dayPlan: dayPlan ?? DayPlan(), + initiallyExpanded: initiallyExpanded, + onAdd: onAdd ?? (_, __) {}, + onRemove: onRemove ?? (_, __) {}, + ), + ), + ); + +void main() { + testWidgets('mostra il nome del giorno', (tester) async { + await tester.pumpApp(_card()); + expect(find.text('Lunedì'), findsOneWidget); + }); + + testWidgets('mostra il conteggio voci nel sottotitolo', (tester) async { + await tester.pumpApp(_card( + dayPlan: DayPlan(colazione: ['latte', 'caffè'], pranzo: ['pasta']), + )); + expect(find.text('3 voci'), findsOneWidget); + }); + + testWidgets('usa forma singolare per 1 voce', (tester) async { + await tester.pumpApp(_card(dayPlan: DayPlan(colazione: ['latte']))); + expect(find.text('1 voce'), findsOneWidget); + }); + + testWidgets('nessun sottotitolo quando il giorno è vuoto', (tester) async { + await tester.pumpApp(_card()); + expect(find.text('0 voci'), findsNothing); + expect(find.text('voce'), findsNothing); + }); + + testWidgets('mostra gli elementi quando espanso', (tester) async { + await tester.pumpApp(_card( + dayPlan: DayPlan(colazione: ['latte'], cena: ['pollo al forno']), + initiallyExpanded: true, + )); + await tester.pumpAndSettle(); + expect(find.text('latte'), findsOneWidget); + expect(find.text('pollo al forno'), findsOneWidget); + }); + + testWidgets('chiama onAdd con slot e testo corretti', (tester) async { + String? capturedSlot; + String? capturedText; + + await tester.pumpApp(_card( + initiallyExpanded: true, + onAdd: (slot, text) { + capturedSlot = slot; + capturedText = text; + }, + )); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField).first, 'yogurt'); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pump(); + + expect(capturedSlot, 'colazione'); + expect(capturedText, 'yogurt'); + }); + + testWidgets('chiama onRemove quando si preme il pulsante elimina', + (tester) async { + String? removedSlot; + int? removedIndex; + + await tester.pumpApp(_card( + dayPlan: DayPlan(colazione: ['latte']), + initiallyExpanded: true, + onRemove: (slot, index) { + removedSlot = slot; + removedIndex = index; + }, + )); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.close).first); + await tester.pump(); + + expect(removedSlot, 'colazione'); + expect(removedIndex, 0); + }); +} diff --git a/test/features/shopping_list/models/shopping_item_test.dart b/test/features/shopping_list/models/shopping_item_test.dart new file mode 100644 index 0000000..72080ba --- /dev/null +++ b/test/features/shopping_list/models/shopping_item_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:biteplan/features/shopping_list/models/shopping_item.dart'; + +void main() { + group('ShoppingItem', () { + test('checked è false di default', () { + final item = ShoppingItem(id: '1', name: 'Pollo'); + expect(item.checked, false); + }); + + test('copyWith() aggiorna solo il campo specificato', () { + final item = ShoppingItem(id: '1', name: 'Pollo'); + final checked = item.copyWith(checked: true); + expect(checked.checked, true); + expect(checked.name, 'Pollo'); + expect(checked.id, '1'); + }); + + test('copyWith() non modifica lorigine', () { + final item = ShoppingItem(id: '1', name: 'Pollo'); + item.copyWith(checked: true); + expect(item.checked, false); + }); + + test('JSON round-trip preserva tutti i campi', () { + const item = ShoppingItem(id: '42', name: 'Riso', checked: true); + final restored = ShoppingItem.fromJson(item.toJson()); + expect(restored.id, '42'); + expect(restored.name, 'Riso'); + expect(restored.checked, true); + }); + + test('fromJson gestisce campo checked assente (default false)', () { + final item = ShoppingItem.fromJson({'id': '1', 'name': 'Pasta'}); + expect(item.checked, false); + }); + + test('toJson produce la mappa corretta', () { + const item = ShoppingItem(id: '5', name: 'Zucchine', checked: false); + final json = item.toJson(); + expect(json['id'], '5'); + expect(json['name'], 'Zucchine'); + expect(json['checked'], false); + }); + }); +} diff --git a/test/features/shopping_list/providers/shopping_list_provider_test.dart b/test/features/shopping_list/providers/shopping_list_provider_test.dart new file mode 100644 index 0000000..42c5d46 --- /dev/null +++ b/test/features/shopping_list/providers/shopping_list_provider_test.dart @@ -0,0 +1,109 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:biteplan/features/shopping_list/providers/shopping_list_provider.dart'; + +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + test('inizia vuota', () async { + final provider = ShoppingListProvider(); + await provider.load(); + expect(provider.items, isEmpty); + }); + + test('add() aggiunge un elemento', () async { + final provider = ShoppingListProvider(); + await provider.load(); + provider.add('Pollo'); + expect(provider.items.length, 1); + expect(provider.items.first.name, 'Pollo'); + expect(provider.items.first.checked, false); + }); + + test('add() ignora stringa vuota o solo spazi', () async { + final provider = ShoppingListProvider(); + await provider.load(); + provider.add(' '); + provider.add(''); + expect(provider.items, isEmpty); + }); + + test('add() esegue trim del testo', () async { + final provider = ShoppingListProvider(); + await provider.load(); + provider.add(' Riso '); + expect(provider.items.first.name, 'Riso'); + }); + + test('toggle() inverte lo stato checked', () async { + final provider = ShoppingListProvider(); + await provider.load(); + provider.add('Riso'); + final id = provider.items.first.id; + provider.toggle(id); + expect(provider.items.first.checked, true); + provider.toggle(id); + expect(provider.items.first.checked, false); + }); + + test('pendingItems e checkedItems separano correttamente', () async { + final provider = ShoppingListProvider(); + await provider.load(); + provider.add('Riso'); + provider.add('Pasta'); + provider.add('Pollo'); + provider.toggle(provider.items[0].id); + provider.toggle(provider.items[2].id); + expect(provider.pendingItems.length, 1); + expect(provider.checkedItems.length, 2); + }); + + test('remove() elimina per id', () async { + final provider = ShoppingListProvider(); + await provider.load(); + provider.add('Pollo'); + provider.add('Riso'); + final id = provider.items.first.id; + provider.remove(id); + expect(provider.items.length, 1); + expect(provider.items.first.name, 'Riso'); + }); + + test('addAll() non aggiunge duplicati (case-insensitive)', () async { + final provider = ShoppingListProvider(); + await provider.load(); + provider.add('Pollo'); + provider.addAll(['pollo', 'Riso', 'RISO', 'Pasta']); + expect(provider.items.length, 3); // Pollo, Riso, Pasta + }); + + test('addAll() ignora elementi già presenti', () async { + final provider = ShoppingListProvider(); + await provider.load(); + provider.add('Riso'); + provider.addAll(['Riso', 'Pasta']); + expect(provider.items.length, 2); + }); + + test('clearAll() svuota la lista', () async { + final provider = ShoppingListProvider(); + await provider.load(); + provider.add('Pollo'); + provider.add('Riso'); + provider.clearAll(); + expect(provider.items, isEmpty); + }); + + test('persiste e ricarica i dati', () async { + final provider1 = ShoppingListProvider(); + await provider1.load(); + provider1.add('Merluzzo'); + await Future.delayed(const Duration(milliseconds: 50)); + + final provider2 = ShoppingListProvider(); + await provider2.load(); + expect(provider2.items.first.name, 'Merluzzo'); + }); +} diff --git a/test/features/shopping_list/widgets/shopping_item_tile_test.dart b/test/features/shopping_list/widgets/shopping_item_tile_test.dart new file mode 100644 index 0000000..4bfee35 --- /dev/null +++ b/test/features/shopping_list/widgets/shopping_item_tile_test.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:biteplan/features/shopping_list/presentation/widgets/shopping_item_tile.dart'; +import 'package:biteplan/features/shopping_list/models/shopping_item.dart'; +import '../../../helpers/pump_app.dart'; + +Widget _tile({ + required ShoppingItem item, + VoidCallback? onToggle, + VoidCallback? onRemove, +}) => + Scaffold( + body: ShoppingItemTile( + item: item, + onToggle: onToggle ?? () {}, + onRemove: onRemove ?? () {}, + ), + ); + +void main() { + testWidgets('mostra il nome dell\'elemento', (tester) async { + await tester.pumpApp(_tile(item: const ShoppingItem(id: '1', name: 'Pollo'))); + expect(find.text('Pollo'), findsOneWidget); + }); + + testWidgets('checkbox non spuntato quando checked è false', (tester) async { + await tester.pumpApp(_tile(item: const ShoppingItem(id: '1', name: 'Riso'))); + final checkbox = tester.widget(find.byType(Checkbox)); + expect(checkbox.value, false); + }); + + testWidgets('checkbox spuntato quando checked è true', (tester) async { + await tester.pumpApp( + _tile(item: const ShoppingItem(id: '1', name: 'Riso', checked: true))); + final checkbox = tester.widget(find.byType(Checkbox)); + expect(checkbox.value, true); + }); + + testWidgets('chiama onToggle al tap sul checkbox', (tester) async { + var toggled = false; + await tester.pumpApp(_tile( + item: const ShoppingItem(id: '1', name: 'Pollo'), + onToggle: () => toggled = true, + )); + await tester.tap(find.byType(Checkbox)); + expect(toggled, true); + }); + + testWidgets('chiama onRemove al tap sul pulsante chiudi', (tester) async { + var removed = false; + await tester.pumpApp(_tile( + item: const ShoppingItem(id: '1', name: 'Pasta'), + onRemove: () => removed = true, + )); + await tester.tap(find.byIcon(Icons.close)); + expect(removed, true); + }); + + testWidgets('testo barrato quando elemento è completato', (tester) async { + await tester.pumpApp( + _tile(item: const ShoppingItem(id: '1', name: 'Tonno', checked: true))); + final textWidget = tester.widget(find.text('Tonno')); + expect(textWidget.style?.decoration, TextDecoration.lineThrough); + }); +} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart new file mode 100644 index 0000000..d451979 --- /dev/null +++ b/test/helpers/pump_app.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:biteplan/core/theme/app_theme.dart'; + +extension PumpApp on WidgetTester { + Future pumpApp(Widget widget) => pumpWidget( + MaterialApp(theme: AppTheme.light(), home: widget), + ); +}