From 65214ac34e5a2011d00e32db30077e6155c6cd52 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Sun, 17 May 2026 23:27:19 +0200 Subject: [PATCH] test: espandi copertura a 110 test e organizza in gruppi - Aggiunto converter_provider_test.dart (nuovo): stato iniziale, select, setGrams, result, swapDirection, reset, search - Aggiunto qr_test.dart (nuovo): buildQrPayload e parseMealPlanFromQr con round-trip e casi di errore - meal_planner_provider_test: group(), importPlan(), fallback JSON corrotto - shopping_list_provider_test: group(), addAll con aggregazione quantity, fallback JSON corrotto - shopping_item_test: campi quantity (default, copyWith, JSON round-trip) - shopping_item_tile_test: group(), visualizzazione (xN) - conversion_entry_test: caso limite yieldFactor=0 - meal_card_test: group() - Rimosso widget_test.dart (boilerplate obsoleto) Co-Authored-By: Claude Sonnet 4.6 --- .../models/conversion_entry_test.dart | 9 + .../providers/converter_provider_test.dart | 191 +++++++++++++++ .../providers/meal_planner_provider_test.dart | 188 +++++++++------ test/features/meal_planner/qr_test.dart | 104 ++++++++ .../meal_planner/widgets/meal_card_test.dart | 126 +++++----- .../models/shopping_item_test.dart | 27 ++- .../shopping_list_provider_test.dart | 222 +++++++++++------- .../widgets/shopping_item_tile_test.dart | 95 +++++--- 8 files changed, 703 insertions(+), 259 deletions(-) create mode 100644 test/features/converter/providers/converter_provider_test.dart create mode 100644 test/features/meal_planner/qr_test.dart diff --git a/test/features/converter/models/conversion_entry_test.dart b/test/features/converter/models/conversion_entry_test.dart index eb2c9e2..fa2feb4 100644 --- a/test/features/converter/models/conversion_entry_test.dart +++ b/test/features/converter/models/conversion_entry_test.dart @@ -61,5 +61,14 @@ void main() { expect(entry.rawToCooked(0), 0.0); expect(entry.cookedToRaw(0), 0.0); }); + + test('yield = 0: rawToCooked restituisce 0', () { + const entry = ConversionEntry( + food: 'test', + method: 'test', + yieldFactor: 0.0, + ); + expect(entry.rawToCooked(100), 0.0); + }); }); } diff --git a/test/features/converter/providers/converter_provider_test.dart b/test/features/converter/providers/converter_provider_test.dart new file mode 100644 index 0000000..f5bec36 --- /dev/null +++ b/test/features/converter/providers/converter_provider_test.dart @@ -0,0 +1,191 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:biteplan/features/converter/providers/converter_provider.dart'; +import 'package:biteplan/features/converter/models/conversion_entry.dart'; + +void main() { + group('ConverterProvider', () { + late ConverterProvider provider; + + setUp(() { + provider = ConverterProvider(); + }); + + group('stato iniziale', () { + test('results è vuoto', () { + expect(provider.results, isEmpty); + }); + + test('selected è null', () { + expect(provider.selected, isNull); + }); + + test('grams è null', () { + expect(provider.grams, isNull); + }); + + test('rawToCooked è true', () { + expect(provider.rawToCooked, true); + }); + + test('result è null', () { + expect(provider.result, isNull); + }); + }); + + group('select()', () { + const entry = ConversionEntry(food: 'riso', method: 'bollitura', yieldFactor: 3.0); + + test('imposta l\'alimento selezionato', () { + provider.select(entry); + expect(provider.selected, entry); + }); + + test('azzera i grammi', () { + provider.setGrams(100); + provider.select(entry); + expect(provider.grams, isNull); + }); + + test('svuota i risultati', () { + provider.select(entry); + expect(provider.results, isEmpty); + }); + + test('notifica i listener', () { + var notified = false; + provider.addListener(() => notified = true); + provider.select(entry); + expect(notified, true); + }); + }); + + group('setGrams()', () { + test('aggiorna il valore', () { + provider.setGrams(150.0); + expect(provider.grams, 150.0); + }); + + test('accetta null', () { + provider.setGrams(100.0); + provider.setGrams(null); + expect(provider.grams, isNull); + }); + + test('notifica i listener', () { + var notified = false; + provider.addListener(() => notified = true); + provider.setGrams(100.0); + expect(notified, true); + }); + }); + + group('result', () { + const entry = ConversionEntry(food: 'riso', method: 'bollitura', yieldFactor: 3.0); + + test('null se nessun alimento selezionato', () { + provider.setGrams(100.0); + expect(provider.result, isNull); + }); + + test('null se grammi non impostati', () { + provider.select(entry); + expect(provider.result, isNull); + }); + + test('null se grammi sono 0', () { + provider.select(entry); + provider.setGrams(0.0); + expect(provider.result, isNull); + }); + + test('null se grammi sono negativi', () { + provider.select(entry); + provider.setGrams(-10.0); + expect(provider.result, isNull); + }); + + test('calcola crudo→cotto', () { + provider.select(entry); + provider.setGrams(100.0); + expect(provider.result, closeTo(300.0, 0.05)); + }); + + test('calcola cotto→crudo dopo swapDirection', () { + provider.select(entry); + provider.swapDirection(); + provider.setGrams(300.0); + expect(provider.result, closeTo(100.0, 0.05)); + }); + + test('arrotonda a 1 decimale', () { + const e = ConversionEntry(food: 'pollo', method: 'forno', yieldFactor: 0.67); + provider.select(e); + provider.setGrams(33.0); + // 33 * 0.67 = 22.11 → arrotondato a 22.1 + expect(provider.result, closeTo(22.1, 0.05)); + }); + }); + + group('swapDirection()', () { + test('inverte la direzione', () { + expect(provider.rawToCooked, true); + provider.swapDirection(); + expect(provider.rawToCooked, false); + provider.swapDirection(); + expect(provider.rawToCooked, true); + }); + + test('azzera i grammi', () { + provider.setGrams(100.0); + provider.swapDirection(); + expect(provider.grams, isNull); + }); + + test('notifica i listener', () { + var notified = false; + provider.addListener(() => notified = true); + provider.swapDirection(); + expect(notified, true); + }); + }); + + group('reset()', () { + test('azzera tutti i campi', () { + provider.select(const ConversionEntry(food: 'riso', method: 'bollitura', yieldFactor: 3.0)); + provider.setGrams(100.0); + provider.swapDirection(); + provider.reset(); + expect(provider.selected, isNull); + expect(provider.grams, isNull); + expect(provider.rawToCooked, true); + expect(provider.results, isEmpty); + }); + + test('notifica i listener', () { + var notified = false; + provider.addListener(() => notified = true); + provider.reset(); + expect(notified, true); + }); + }); + + group('search()', () { + test('risultati vuoti con query vuota', () { + provider.search(''); + expect(provider.results, isEmpty); + }); + + test('risultati vuoti con solo spazi', () { + provider.search(' '); + expect(provider.results, isEmpty); + }); + + test('notifica i listener', () { + var notified = false; + provider.addListener(() => notified = true); + provider.search('riso'); + expect(notified, true); + }); + }); + }); +} diff --git a/test/features/meal_planner/providers/meal_planner_provider_test.dart b/test/features/meal_planner/providers/meal_planner_provider_test.dart index 3ea06b6..d53fddd 100644 --- a/test/features/meal_planner/providers/meal_planner_provider_test.dart +++ b/test/features/meal_planner/providers/meal_planner_provider_test.dart @@ -1,88 +1,140 @@ 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'; +import 'package:biteplan/features/meal_planner/models/meal_plan.dart'; +import 'package:biteplan/core/constants/app_constants.dart'; void main() { setUp(() { SharedPreferences.setMockInitialValues({}); }); - test('inizia con piano vuoto', () async { - final provider = MealPlannerProvider(); - await provider.load(); - expect(provider.plan.hasAnyMeal, false); - }); + group('MealPlannerProvider', () { + 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 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 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 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('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('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('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)); + 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']); - }); + 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); + 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); + }); + + group('importPlan()', () { + test('sovrascrive il piano corrente', () async { + final provider = MealPlannerProvider(); + await provider.load(); + provider.addItem('lunedi', 'colazione', 'latte'); + + final newPlan = MealPlan({ + ...Map.fromEntries(kDayIds.map((d) => MapEntry(d, DayPlan()))), + 'martedi': DayPlan(pranzo: ['pasta']), + }); + provider.importPlan(newPlan); + + expect(provider.plan.days['lunedi']!.colazione, isEmpty); + expect(provider.plan.days['martedi']!.pranzo, ['pasta']); + }); + + test('persiste il piano importato', () async { + final provider1 = MealPlannerProvider(); + await provider1.load(); + final newPlan = MealPlan({ + ...Map.fromEntries(kDayIds.map((d) => MapEntry(d, DayPlan()))), + 'mercoledi': DayPlan(cena: ['pesce']), + }); + provider1.importPlan(newPlan); + await Future.delayed(const Duration(milliseconds: 50)); + + final provider2 = MealPlannerProvider(); + await provider2.load(); + expect(provider2.plan.days['mercoledi']!.cena, ['pesce']); + }); + + test('notifica i listener', () async { + final provider = MealPlannerProvider(); + await provider.load(); + var notified = false; + provider.addListener(() => notified = true); + provider.importPlan(MealPlan.empty(kDayIds)); + expect(notified, true); + }); + }); + + test('load gestisce JSON corrotto con piano vuoto', () async { + SharedPreferences.setMockInitialValues({'meals': 'json non valido {'}); + final provider = MealPlannerProvider(); + await provider.load(); + expect(provider.plan.hasAnyMeal, false); + }); }); } diff --git a/test/features/meal_planner/qr_test.dart b/test/features/meal_planner/qr_test.dart new file mode 100644 index 0000000..bc3fd7a --- /dev/null +++ b/test/features/meal_planner/qr_test.dart @@ -0,0 +1,104 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:biteplan/features/meal_planner/qr_codec.dart'; +import 'package:biteplan/features/meal_planner/models/meal_plan.dart'; +import 'package:biteplan/core/constants/app_constants.dart'; + +void main() { + group('buildQrPayload', () { + test('genera payload non nullo per piano vuoto', () { + final plan = MealPlan.empty(kDayIds); + expect(buildQrPayload(plan), isNotNull); + }); + + test('il payload contiene versione e dati pasti', () { + final plan = MealPlan.empty(kDayIds); + final payload = buildQrPayload(plan)!; + expect(payload, contains('"v":1')); + expect(payload, contains('"meals"')); + }); + + test('il payload include i dati inseriti', () { + final plan = MealPlan({ + ...Map.fromEntries(kDayIds.map((d) => MapEntry(d, DayPlan()))), + 'lunedi': DayPlan(colazione: ['latte', 'biscotti'], pranzo: ['pasta']), + }); + final payload = buildQrPayload(plan)!; + expect(payload, contains('latte')); + expect(payload, contains('biscotti')); + expect(payload, contains('pasta')); + }); + + test('restituisce null se payload supera 2953 byte', () { + final longName = 'a' * 400; + final plan = MealPlan(Map.fromEntries(kDayIds.map((d) => MapEntry( + d, + DayPlan( + colazione: List.generate(5, (_) => longName), + pranzo: List.generate(5, (_) => longName), + cena: List.generate(5, (_) => longName), + ), + )))); + expect(buildQrPayload(plan), isNull); + }); + }); + + group('parseMealPlanFromQr', () { + test('round-trip: parse di payload generato da buildQrPayload', () { + final plan = MealPlan.empty(kDayIds); + final payload = buildQrPayload(plan)!; + final (parsed, error) = parseMealPlanFromQr(payload); + expect(error, isNull); + expect(parsed, isNotNull); + for (final day in kDayIds) { + expect(parsed!.days.containsKey(day), true); + } + }); + + test('round-trip preserva tutti i pasti', () { + final original = MealPlan({ + ...Map.fromEntries(kDayIds.map((d) => MapEntry(d, DayPlan()))), + 'lunedi': DayPlan(colazione: ['latte'], pranzo: ['pasta'], cena: ['pollo']), + 'martedi': DayPlan(cena: ['pesce']), + }); + final payload = buildQrPayload(original)!; + final (parsed, error) = parseMealPlanFromQr(payload); + + expect(error, isNull); + expect(parsed!.days['lunedi']!.colazione, ['latte']); + expect(parsed.days['lunedi']!.pranzo, ['pasta']); + expect(parsed.days['lunedi']!.cena, ['pollo']); + expect(parsed.days['martedi']!.cena, ['pesce']); + expect(parsed.days['mercoledi']!.colazione, isEmpty); + }); + + test('errore su stringa non JSON', () { + final (plan, error) = parseMealPlanFromQr('testo non json'); + expect(plan, isNull); + expect(error, isNotNull); + }); + + test('errore se versione non è 1', () { + final (plan, error) = parseMealPlanFromQr('{"v":2,"meals":{}}'); + expect(plan, isNull); + expect(error, contains('non valido')); + }); + + test('errore se meals è una lista invece di una mappa', () { + final (plan, error) = parseMealPlanFromQr('{"v":1,"meals":[]}'); + expect(plan, isNull); + expect(error, isNotNull); + }); + + test('errore se struttura di un giorno non è valida', () { + final (plan, error) = parseMealPlanFromQr('{"v":1,"meals":{"lunedi":"stringa"}}'); + expect(plan, isNull); + expect(error, isNotNull); + }); + + test('errore se campo v è assente', () { + final (plan, error) = parseMealPlanFromQr('{"meals":{}}'); + expect(plan, isNull); + expect(error, isNotNull); + }); + }); +} diff --git a/test/features/meal_planner/widgets/meal_card_test.dart b/test/features/meal_planner/widgets/meal_card_test.dart index 542eeed..431c0ea 100644 --- a/test/features/meal_planner/widgets/meal_card_test.dart +++ b/test/features/meal_planner/widgets/meal_card_test.dart @@ -24,79 +24,81 @@ Widget _card({ ); void main() { - testWidgets('mostra il nome del giorno', (tester) async { - await tester.pumpApp(_card()); - expect(find.text('Lunedì'), findsOneWidget); - }); + group('MealCard', () { + 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('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('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('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('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; + 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.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(); + await tester.enterText(find.byType(TextField).first, 'yogurt'); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pump(); - expect(capturedSlot, 'colazione'); - expect(capturedText, 'yogurt'); - }); + expect(capturedSlot, 'colazione'); + expect(capturedText, 'yogurt'); + }); - testWidgets('chiama onRemove quando si preme il pulsante elimina', - (tester) async { - String? removedSlot; - int? removedIndex; + 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.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(); + await tester.tap(find.byIcon(Icons.close).first); + await tester.pump(); - expect(removedSlot, 'colazione'); - expect(removedIndex, 0); + 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 index 72080ba..ab52367 100644 --- a/test/features/shopping_list/models/shopping_item_test.dart +++ b/test/features/shopping_list/models/shopping_item_test.dart @@ -8,6 +8,11 @@ void main() { expect(item.checked, false); }); + test('quantity è 1 di default', () { + final item = ShoppingItem(id: '1', name: 'Pollo'); + expect(item.quantity, 1); + }); + test('copyWith() aggiorna solo il campo specificato', () { final item = ShoppingItem(id: '1', name: 'Pollo'); final checked = item.copyWith(checked: true); @@ -16,18 +21,26 @@ void main() { expect(checked.id, '1'); }); + test('copyWith() aggiorna quantity', () { + final item = ShoppingItem(id: '1', name: 'Zucchine'); + final updated = item.copyWith(quantity: 3); + expect(updated.quantity, 3); + expect(updated.name, 'Zucchine'); + }); + 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); + test('JSON round-trip preserva tutti i campi incluso quantity', () { + const item = ShoppingItem(id: '42', name: 'Riso', checked: true, quantity: 3); final restored = ShoppingItem.fromJson(item.toJson()); expect(restored.id, '42'); expect(restored.name, 'Riso'); expect(restored.checked, true); + expect(restored.quantity, 3); }); test('fromJson gestisce campo checked assente (default false)', () { @@ -35,12 +48,18 @@ void main() { expect(item.checked, false); }); - test('toJson produce la mappa corretta', () { - const item = ShoppingItem(id: '5', name: 'Zucchine', checked: false); + test('fromJson gestisce campo quantity assente (default 1)', () { + final item = ShoppingItem.fromJson({'id': '1', 'name': 'Pasta', 'checked': false}); + expect(item.quantity, 1); + }); + + test('toJson produce la mappa corretta con quantity', () { + const item = ShoppingItem(id: '5', name: 'Zucchine', checked: false, quantity: 2); final json = item.toJson(); expect(json['id'], '5'); expect(json['name'], 'Zucchine'); expect(json['checked'], false); + expect(json['quantity'], 2); }); }); } diff --git a/test/features/shopping_list/providers/shopping_list_provider_test.dart b/test/features/shopping_list/providers/shopping_list_provider_test.dart index 42c5d46..d2b1f1d 100644 --- a/test/features/shopping_list/providers/shopping_list_provider_test.dart +++ b/test/features/shopping_list/providers/shopping_list_provider_test.dart @@ -7,103 +7,151 @@ void main() { SharedPreferences.setMockInitialValues({}); }); - test('inizia vuota', () async { - final provider = ShoppingListProvider(); - await provider.load(); - expect(provider.items, isEmpty); - }); + group('ShoppingListProvider', () { + 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); - }); + group('add()', () { + test('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('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('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); - }); + group('toggle()', () { + test('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); - }); + group('pendingItems / checkedItems', () { + test('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'); - }); + group('remove()', () { + test('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 - }); + group('addAll()', () { + test('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('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('aggrega duplicati con quantity', () async { + final provider = ShoppingListProvider(); + await provider.load(); + provider.addAll(['Zucchine', 'Pollo', 'Zucchine', 'Pollo', 'Pollo']); + final zucchine = provider.items.firstWhere((i) => i.name.toLowerCase() == 'zucchine'); + final pollo = provider.items.firstWhere((i) => i.name.toLowerCase() == 'pollo'); + expect(zucchine.quantity, 2); + expect(pollo.quantity, 3); + }); - test('persiste e ricarica i dati', () async { - final provider1 = ShoppingListProvider(); - await provider1.load(); - provider1.add('Merluzzo'); - await Future.delayed(const Duration(milliseconds: 50)); + test('imposta quantity=1 per elementi non duplicati', () async { + final provider = ShoppingListProvider(); + await provider.load(); + provider.addAll(['Pasta', 'Riso']); + for (final item in provider.items) { + expect(item.quantity, 1); + } + }); - final provider2 = ShoppingListProvider(); - await provider2.load(); - expect(provider2.items.first.name, 'Merluzzo'); + test('non aggiunge se tutti già presenti', () async { + final provider = ShoppingListProvider(); + await provider.load(); + provider.add('Riso'); + provider.addAll(['riso']); + expect(provider.items.length, 1); + }); + }); + + group('clearAll()', () { + test('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'); + }); + + test('load gestisce JSON corrotto con lista vuota', () async { + SharedPreferences.setMockInitialValues({'shopping_list': 'json non valido {'}); + final provider = ShoppingListProvider(); + await provider.load(); + expect(provider.items, isEmpty); + }); }); } diff --git a/test/features/shopping_list/widgets/shopping_item_tile_test.dart b/test/features/shopping_list/widgets/shopping_item_tile_test.dart index 4bfee35..589a244 100644 --- a/test/features/shopping_list/widgets/shopping_item_tile_test.dart +++ b/test/features/shopping_list/widgets/shopping_item_tile_test.dart @@ -18,48 +18,67 @@ Widget _tile({ ); 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); - }); + group('ShoppingItemTile', () { + 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 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('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 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('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); + 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); + }); + + testWidgets('non mostra indicatore quantità quando quantity è 1', (tester) async { + await tester.pumpApp(_tile(item: const ShoppingItem(id: '1', name: 'Riso'))); + expect(find.text('(x1)'), findsNothing); + }); + + testWidgets('mostra (x2) quando quantity è 2', (tester) async { + await tester.pumpApp( + _tile(item: const ShoppingItem(id: '1', name: 'Zucchine', quantity: 2))); + expect(find.text('(x2)'), findsOneWidget); + }); + + testWidgets('mostra (xN) per quantità maggiori', (tester) async { + await tester.pumpApp( + _tile(item: const ShoppingItem(id: '1', name: 'Pollo', quantity: 5))); + expect(find.text('(x5)'), findsOneWidget); + }); }); }