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
@@ -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);
});
});
}
+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() {
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);
});
});
}