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 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 23:27:19 +02:00
parent 9fcaae9687
commit 65214ac34e
8 changed files with 703 additions and 259 deletions
@@ -61,5 +61,14 @@ void main() {
expect(entry.rawToCooked(0), 0.0); expect(entry.rawToCooked(0), 0.0);
expect(entry.cookedToRaw(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);
});
}); });
} }
@@ -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);
});
});
});
}
@@ -1,88 +1,140 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.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/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() { void main() {
setUp(() { setUp(() {
SharedPreferences.setMockInitialValues({}); SharedPreferences.setMockInitialValues({});
}); });
test('inizia con piano vuoto', () async { group('MealPlannerProvider', () {
final provider = MealPlannerProvider(); test('inizia con piano vuoto', () async {
await provider.load(); final provider = MealPlannerProvider();
expect(provider.plan.hasAnyMeal, false); await provider.load();
}); expect(provider.plan.hasAnyMeal, false);
});
test('addItem aggiunge nello slot corretto', () async { test('addItem aggiunge nello slot corretto', () async {
final provider = MealPlannerProvider(); final provider = MealPlannerProvider();
await provider.load(); await provider.load();
provider.addItem('lunedi', 'colazione', 'latte'); provider.addItem('lunedi', 'colazione', 'latte');
expect(provider.plan.days['lunedi']!.colazione, ['latte']); expect(provider.plan.days['lunedi']!.colazione, ['latte']);
expect(provider.plan.days['lunedi']!.pranzo, isEmpty); expect(provider.plan.days['lunedi']!.pranzo, isEmpty);
}); });
test('addItem aggiunge in tutti e tre gli slot', () async { test('addItem aggiunge in tutti e tre gli slot', () async {
final provider = MealPlannerProvider(); final provider = MealPlannerProvider();
await provider.load(); await provider.load();
provider.addItem('martedi', 'colazione', 'yogurt'); provider.addItem('martedi', 'colazione', 'yogurt');
provider.addItem('martedi', 'pranzo', 'pasta'); provider.addItem('martedi', 'pranzo', 'pasta');
provider.addItem('martedi', 'cena', 'pollo'); provider.addItem('martedi', 'cena', 'pollo');
expect(provider.plan.days['martedi']!.colazione, ['yogurt']); expect(provider.plan.days['martedi']!.colazione, ['yogurt']);
expect(provider.plan.days['martedi']!.pranzo, ['pasta']); expect(provider.plan.days['martedi']!.pranzo, ['pasta']);
expect(provider.plan.days['martedi']!.cena, ['pollo']); expect(provider.plan.days['martedi']!.cena, ['pollo']);
}); });
test('addItem ignora testo vuoto o solo spazi', () async { test('addItem ignora testo vuoto o solo spazi', () async {
final provider = MealPlannerProvider(); final provider = MealPlannerProvider();
await provider.load(); await provider.load();
provider.addItem('lunedi', 'colazione', ' '); provider.addItem('lunedi', 'colazione', ' ');
provider.addItem('lunedi', 'colazione', ''); provider.addItem('lunedi', 'colazione', '');
expect(provider.plan.days['lunedi']!.colazione, isEmpty); expect(provider.plan.days['lunedi']!.colazione, isEmpty);
}); });
test('addItem esegue trim del testo', () async { test('addItem esegue trim del testo', () async {
final provider = MealPlannerProvider(); final provider = MealPlannerProvider();
await provider.load(); await provider.load();
provider.addItem('lunedi', 'pranzo', ' pasta '); provider.addItem('lunedi', 'pranzo', ' pasta ');
expect(provider.plan.days['lunedi']!.pranzo, ['pasta']); expect(provider.plan.days['lunedi']!.pranzo, ['pasta']);
}); });
test('removeItem rimuove dalla posizione corretta', () async { test('removeItem rimuove dalla posizione corretta', () async {
final provider = MealPlannerProvider(); final provider = MealPlannerProvider();
await provider.load(); await provider.load();
provider.addItem('lunedi', 'pranzo', 'pasta'); provider.addItem('lunedi', 'pranzo', 'pasta');
provider.addItem('lunedi', 'pranzo', 'insalata'); provider.addItem('lunedi', 'pranzo', 'insalata');
provider.addItem('lunedi', 'pranzo', 'frutta'); provider.addItem('lunedi', 'pranzo', 'frutta');
provider.removeItem('lunedi', 'pranzo', 1); provider.removeItem('lunedi', 'pranzo', 1);
expect(provider.plan.days['lunedi']!.pranzo, ['pasta', 'frutta']); expect(provider.plan.days['lunedi']!.pranzo, ['pasta', 'frutta']);
}); });
test('clearAll svuota tutti i giorni', () async { test('clearAll svuota tutti i giorni', () async {
final provider = MealPlannerProvider(); final provider = MealPlannerProvider();
await provider.load(); await provider.load();
provider.addItem('lunedi', 'colazione', 'latte'); provider.addItem('lunedi', 'colazione', 'latte');
provider.addItem('venerdi', 'cena', 'pesce'); provider.addItem('venerdi', 'cena', 'pesce');
provider.clearAll(); provider.clearAll();
expect(provider.plan.hasAnyMeal, false); expect(provider.plan.hasAnyMeal, false);
}); });
test('persiste e ricarica i dati', () async { test('persiste e ricarica i dati', () async {
final provider1 = MealPlannerProvider(); final provider1 = MealPlannerProvider();
await provider1.load(); await provider1.load();
provider1.addItem('lunedi', 'cena', 'pollo'); provider1.addItem('lunedi', 'cena', 'pollo');
await Future.delayed(const Duration(milliseconds: 50)); await Future.delayed(const Duration(milliseconds: 50));
final provider2 = MealPlannerProvider(); final provider2 = MealPlannerProvider();
await provider2.load(); await provider2.load();
expect(provider2.plan.days['lunedi']!.cena, ['pollo']); expect(provider2.plan.days['lunedi']!.cena, ['pollo']);
}); });
test('notifica i listener dopo addItem', () async { test('notifica i listener dopo addItem', () async {
final provider = MealPlannerProvider(); final provider = MealPlannerProvider();
await provider.load(); await provider.load();
var notified = false; var notified = false;
provider.addListener(() => notified = true); provider.addListener(() => notified = true);
provider.addItem('lunedi', 'colazione', 'latte'); provider.addItem('lunedi', 'colazione', 'latte');
expect(notified, true); 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);
});
}); });
} }
+104
View File
@@ -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);
});
});
}
@@ -24,79 +24,81 @@ Widget _card({
); );
void main() { void main() {
testWidgets('mostra il nome del giorno', (tester) async { group('MealCard', () {
await tester.pumpApp(_card()); testWidgets('mostra il nome del giorno', (tester) async {
expect(find.text('Lunedì'), findsOneWidget); await tester.pumpApp(_card());
}); expect(find.text('Lunedì'), findsOneWidget);
});
testWidgets('mostra il conteggio voci nel sottotitolo', (tester) async { testWidgets('mostra il conteggio voci nel sottotitolo', (tester) async {
await tester.pumpApp(_card( await tester.pumpApp(_card(
dayPlan: DayPlan(colazione: ['latte', 'caffè'], pranzo: ['pasta']), dayPlan: DayPlan(colazione: ['latte', 'caffè'], pranzo: ['pasta']),
)); ));
expect(find.text('3 voci'), findsOneWidget); expect(find.text('3 voci'), findsOneWidget);
}); });
testWidgets('usa forma singolare per 1 voce', (tester) async { testWidgets('usa forma singolare per 1 voce', (tester) async {
await tester.pumpApp(_card(dayPlan: DayPlan(colazione: ['latte']))); await tester.pumpApp(_card(dayPlan: DayPlan(colazione: ['latte'])));
expect(find.text('1 voce'), findsOneWidget); expect(find.text('1 voce'), findsOneWidget);
}); });
testWidgets('nessun sottotitolo quando il giorno è vuoto', (tester) async { testWidgets('nessun sottotitolo quando il giorno è vuoto', (tester) async {
await tester.pumpApp(_card()); await tester.pumpApp(_card());
expect(find.text('0 voci'), findsNothing); expect(find.text('0 voci'), findsNothing);
expect(find.text('voce'), findsNothing); expect(find.text('voce'), findsNothing);
}); });
testWidgets('mostra gli elementi quando espanso', (tester) async { testWidgets('mostra gli elementi quando espanso', (tester) async {
await tester.pumpApp(_card( await tester.pumpApp(_card(
dayPlan: DayPlan(colazione: ['latte'], cena: ['pollo al forno']), dayPlan: DayPlan(colazione: ['latte'], cena: ['pollo al forno']),
initiallyExpanded: true, initiallyExpanded: true,
)); ));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('latte'), findsOneWidget); expect(find.text('latte'), findsOneWidget);
expect(find.text('pollo al forno'), findsOneWidget); expect(find.text('pollo al forno'), findsOneWidget);
}); });
testWidgets('chiama onAdd con slot e testo corretti', (tester) async { testWidgets('chiama onAdd con slot e testo corretti', (tester) async {
String? capturedSlot; String? capturedSlot;
String? capturedText; String? capturedText;
await tester.pumpApp(_card( await tester.pumpApp(_card(
initiallyExpanded: true, initiallyExpanded: true,
onAdd: (slot, text) { onAdd: (slot, text) {
capturedSlot = slot; capturedSlot = slot;
capturedText = text; capturedText = text;
}, },
)); ));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).first, 'yogurt'); await tester.enterText(find.byType(TextField).first, 'yogurt');
await tester.testTextInput.receiveAction(TextInputAction.done); await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump(); await tester.pump();
expect(capturedSlot, 'colazione'); expect(capturedSlot, 'colazione');
expect(capturedText, 'yogurt'); expect(capturedText, 'yogurt');
}); });
testWidgets('chiama onRemove quando si preme il pulsante elimina', testWidgets('chiama onRemove quando si preme il pulsante elimina',
(tester) async { (tester) async {
String? removedSlot; String? removedSlot;
int? removedIndex; int? removedIndex;
await tester.pumpApp(_card( await tester.pumpApp(_card(
dayPlan: DayPlan(colazione: ['latte']), dayPlan: DayPlan(colazione: ['latte']),
initiallyExpanded: true, initiallyExpanded: true,
onRemove: (slot, index) { onRemove: (slot, index) {
removedSlot = slot; removedSlot = slot;
removedIndex = index; removedIndex = index;
}, },
)); ));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.close).first); await tester.tap(find.byIcon(Icons.close).first);
await tester.pump(); await tester.pump();
expect(removedSlot, 'colazione'); expect(removedSlot, 'colazione');
expect(removedIndex, 0); expect(removedIndex, 0);
});
}); });
} }
@@ -8,6 +8,11 @@ void main() {
expect(item.checked, false); 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', () { test('copyWith() aggiorna solo il campo specificato', () {
final item = ShoppingItem(id: '1', name: 'Pollo'); final item = ShoppingItem(id: '1', name: 'Pollo');
final checked = item.copyWith(checked: true); final checked = item.copyWith(checked: true);
@@ -16,18 +21,26 @@ void main() {
expect(checked.id, '1'); 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', () { test('copyWith() non modifica lorigine', () {
final item = ShoppingItem(id: '1', name: 'Pollo'); final item = ShoppingItem(id: '1', name: 'Pollo');
item.copyWith(checked: true); item.copyWith(checked: true);
expect(item.checked, false); expect(item.checked, false);
}); });
test('JSON round-trip preserva tutti i campi', () { test('JSON round-trip preserva tutti i campi incluso quantity', () {
const item = ShoppingItem(id: '42', name: 'Riso', checked: true); const item = ShoppingItem(id: '42', name: 'Riso', checked: true, quantity: 3);
final restored = ShoppingItem.fromJson(item.toJson()); final restored = ShoppingItem.fromJson(item.toJson());
expect(restored.id, '42'); expect(restored.id, '42');
expect(restored.name, 'Riso'); expect(restored.name, 'Riso');
expect(restored.checked, true); expect(restored.checked, true);
expect(restored.quantity, 3);
}); });
test('fromJson gestisce campo checked assente (default false)', () { test('fromJson gestisce campo checked assente (default false)', () {
@@ -35,12 +48,18 @@ void main() {
expect(item.checked, false); expect(item.checked, false);
}); });
test('toJson produce la mappa corretta', () { test('fromJson gestisce campo quantity assente (default 1)', () {
const item = ShoppingItem(id: '5', name: 'Zucchine', checked: false); 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(); final json = item.toJson();
expect(json['id'], '5'); expect(json['id'], '5');
expect(json['name'], 'Zucchine'); expect(json['name'], 'Zucchine');
expect(json['checked'], false); expect(json['checked'], false);
expect(json['quantity'], 2);
}); });
}); });
} }
@@ -7,103 +7,151 @@ void main() {
SharedPreferences.setMockInitialValues({}); SharedPreferences.setMockInitialValues({});
}); });
test('inizia vuota', () async { group('ShoppingListProvider', () {
final provider = ShoppingListProvider(); test('inizia vuota', () async {
await provider.load(); final provider = ShoppingListProvider();
expect(provider.items, isEmpty); await provider.load();
}); expect(provider.items, isEmpty);
});
test('add() aggiunge un elemento', () async { group('add()', () {
final provider = ShoppingListProvider(); test('aggiunge un elemento', () async {
await provider.load(); final provider = ShoppingListProvider();
provider.add('Pollo'); await provider.load();
expect(provider.items.length, 1); provider.add('Pollo');
expect(provider.items.first.name, 'Pollo'); expect(provider.items.length, 1);
expect(provider.items.first.checked, false); expect(provider.items.first.name, 'Pollo');
}); expect(provider.items.first.checked, false);
});
test('add() ignora stringa vuota o solo spazi', () async { test('ignora stringa vuota o solo spazi', () async {
final provider = ShoppingListProvider(); final provider = ShoppingListProvider();
await provider.load(); await provider.load();
provider.add(' '); provider.add(' ');
provider.add(''); provider.add('');
expect(provider.items, isEmpty); expect(provider.items, isEmpty);
}); });
test('add() esegue trim del testo', () async { test('esegue trim del testo', () async {
final provider = ShoppingListProvider(); final provider = ShoppingListProvider();
await provider.load(); await provider.load();
provider.add(' Riso '); provider.add(' Riso ');
expect(provider.items.first.name, 'Riso'); expect(provider.items.first.name, 'Riso');
}); });
});
test('toggle() inverte lo stato checked', () async { group('toggle()', () {
final provider = ShoppingListProvider(); test('inverte lo stato checked', () async {
await provider.load(); final provider = ShoppingListProvider();
provider.add('Riso'); await provider.load();
final id = provider.items.first.id; provider.add('Riso');
provider.toggle(id); final id = provider.items.first.id;
expect(provider.items.first.checked, true); provider.toggle(id);
provider.toggle(id); expect(provider.items.first.checked, true);
expect(provider.items.first.checked, false); provider.toggle(id);
}); expect(provider.items.first.checked, false);
});
});
test('pendingItems e checkedItems separano correttamente', () async { group('pendingItems / checkedItems', () {
final provider = ShoppingListProvider(); test('separano correttamente', () async {
await provider.load(); final provider = ShoppingListProvider();
provider.add('Riso'); await provider.load();
provider.add('Pasta'); provider.add('Riso');
provider.add('Pollo'); provider.add('Pasta');
provider.toggle(provider.items[0].id); provider.add('Pollo');
provider.toggle(provider.items[2].id); provider.toggle(provider.items[0].id);
expect(provider.pendingItems.length, 1); provider.toggle(provider.items[2].id);
expect(provider.checkedItems.length, 2); expect(provider.pendingItems.length, 1);
}); expect(provider.checkedItems.length, 2);
});
});
test('remove() elimina per id', () async { group('remove()', () {
final provider = ShoppingListProvider(); test('elimina per id', () async {
await provider.load(); final provider = ShoppingListProvider();
provider.add('Pollo'); await provider.load();
provider.add('Riso'); provider.add('Pollo');
final id = provider.items.first.id; provider.add('Riso');
provider.remove(id); final id = provider.items.first.id;
expect(provider.items.length, 1); provider.remove(id);
expect(provider.items.first.name, 'Riso'); expect(provider.items.length, 1);
}); expect(provider.items.first.name, 'Riso');
});
});
test('addAll() non aggiunge duplicati (case-insensitive)', () async { group('addAll()', () {
final provider = ShoppingListProvider(); test('non aggiunge duplicati (case-insensitive)', () async {
await provider.load(); final provider = ShoppingListProvider();
provider.add('Pollo'); await provider.load();
provider.addAll(['pollo', 'Riso', 'RISO', 'Pasta']); provider.add('Pollo');
expect(provider.items.length, 3); // Pollo, Riso, Pasta provider.addAll(['pollo', 'Riso', 'RISO', 'Pasta']);
}); expect(provider.items.length, 3); // Pollo, Riso, Pasta
});
test('addAll() ignora elementi già presenti', () async { test('ignora elementi già presenti', () async {
final provider = ShoppingListProvider(); final provider = ShoppingListProvider();
await provider.load(); await provider.load();
provider.add('Riso'); provider.add('Riso');
provider.addAll(['Riso', 'Pasta']); provider.addAll(['Riso', 'Pasta']);
expect(provider.items.length, 2); expect(provider.items.length, 2);
}); });
test('clearAll() svuota la lista', () async { test('aggrega duplicati con quantity', () async {
final provider = ShoppingListProvider(); final provider = ShoppingListProvider();
await provider.load(); await provider.load();
provider.add('Pollo'); provider.addAll(['Zucchine', 'Pollo', 'Zucchine', 'Pollo', 'Pollo']);
provider.add('Riso'); final zucchine = provider.items.firstWhere((i) => i.name.toLowerCase() == 'zucchine');
provider.clearAll(); final pollo = provider.items.firstWhere((i) => i.name.toLowerCase() == 'pollo');
expect(provider.items, isEmpty); expect(zucchine.quantity, 2);
}); expect(pollo.quantity, 3);
});
test('persiste e ricarica i dati', () async { test('imposta quantity=1 per elementi non duplicati', () async {
final provider1 = ShoppingListProvider(); final provider = ShoppingListProvider();
await provider1.load(); await provider.load();
provider1.add('Merluzzo'); provider.addAll(['Pasta', 'Riso']);
await Future.delayed(const Duration(milliseconds: 50)); for (final item in provider.items) {
expect(item.quantity, 1);
}
});
final provider2 = ShoppingListProvider(); test('non aggiunge se tutti già presenti', () async {
await provider2.load(); final provider = ShoppingListProvider();
expect(provider2.items.first.name, 'Merluzzo'); 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);
});
}); });
} }
@@ -18,48 +18,67 @@ Widget _tile({
); );
void main() { void main() {
testWidgets('mostra il nome dell\'elemento', (tester) async { group('ShoppingItemTile', () {
await tester.pumpApp(_tile(item: const ShoppingItem(id: '1', name: 'Pollo'))); testWidgets('mostra il nome dell\'elemento', (tester) async {
expect(find.text('Pollo'), findsOneWidget); 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 { testWidgets('checkbox non spuntato quando checked è false', (tester) async {
await tester.pumpApp(_tile(item: const ShoppingItem(id: '1', name: 'Riso'))); await tester.pumpApp(_tile(item: const ShoppingItem(id: '1', name: 'Riso')));
final checkbox = tester.widget<Checkbox>(find.byType(Checkbox)); final checkbox = tester.widget<Checkbox>(find.byType(Checkbox));
expect(checkbox.value, false); expect(checkbox.value, false);
}); });
testWidgets('checkbox spuntato quando checked è true', (tester) async { testWidgets('checkbox spuntato quando checked è true', (tester) async {
await tester.pumpApp( await tester.pumpApp(
_tile(item: const ShoppingItem(id: '1', name: 'Riso', checked: true))); _tile(item: const ShoppingItem(id: '1', name: 'Riso', checked: true)));
final checkbox = tester.widget<Checkbox>(find.byType(Checkbox)); final checkbox = tester.widget<Checkbox>(find.byType(Checkbox));
expect(checkbox.value, true); expect(checkbox.value, true);
}); });
testWidgets('chiama onToggle al tap sul checkbox', (tester) async { testWidgets('chiama onToggle al tap sul checkbox', (tester) async {
var toggled = false; var toggled = false;
await tester.pumpApp(_tile( await tester.pumpApp(_tile(
item: const ShoppingItem(id: '1', name: 'Pollo'), item: const ShoppingItem(id: '1', name: 'Pollo'),
onToggle: () => toggled = true, onToggle: () => toggled = true,
)); ));
await tester.tap(find.byType(Checkbox)); await tester.tap(find.byType(Checkbox));
expect(toggled, true); expect(toggled, true);
}); });
testWidgets('chiama onRemove al tap sul pulsante chiudi', (tester) async { testWidgets('chiama onRemove al tap sul pulsante chiudi', (tester) async {
var removed = false; var removed = false;
await tester.pumpApp(_tile( await tester.pumpApp(_tile(
item: const ShoppingItem(id: '1', name: 'Pasta'), item: const ShoppingItem(id: '1', name: 'Pasta'),
onRemove: () => removed = true, onRemove: () => removed = true,
)); ));
await tester.tap(find.byIcon(Icons.close)); await tester.tap(find.byIcon(Icons.close));
expect(removed, true); expect(removed, true);
}); });
testWidgets('testo barrato quando elemento è completato', (tester) async { testWidgets('testo barrato quando elemento è completato', (tester) async {
await tester.pumpApp( await tester.pumpApp(
_tile(item: const ShoppingItem(id: '1', name: 'Tonno', checked: true))); _tile(item: const ShoppingItem(id: '1', name: 'Tonno', checked: true)));
final textWidget = tester.widget<Text>(find.text('Tonno')); final textWidget = tester.widget<Text>(find.text('Tonno'));
expect(textWidget.style?.decoration, TextDecoration.lineThrough); 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);
});
}); });
} }