import 'package:cookit/src/planning/day.dart'; import 'package:cookit/src/planning/models/meal.dart'; import 'package:cookit/src/recipes/views/widgets/recipe_card.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../support/fixtures.dart'; import '../support/test_app.dart'; final today = dayOf(DateTime.now()); /// The grid cell of [meal] on [day]. Finder cell(DateTime day, Meal meal) => find.byKey(ValueKey('${dayKey(day)}-${meal.id}')); Finder inCell(DateTime day, Meal meal, Finder finder) => find.descendant(of: cell(day, meal), matching: finder); Finder inSheet(Finder finder) => find.descendant(of: find.byType(BottomSheet), matching: finder); Future openWeekPlan(WidgetTester tester, TestApp app) async { await tester.tap(find.byTooltip('Wochenplan')); await app.settle(tester); } Future<(Meal, Meal)> meals(WidgetTester tester, TestApp app) async { final all = (await tester.runAsync(app.plan.allMeals))!; return (all.firstWhere((m) => m.name == 'Mittag'), all.firstWhere((m) => m.name == 'Abend')); } /// Long-presses [from] and drops it on [to]. Future drag(WidgetTester tester, TestApp app, Finder from, Finder to) async { final start = tester.getCenter(from); final gesture = await tester.startGesture(start); await tester.pump(kLongPressTimeout + const Duration(milliseconds: 100)); // While lifted, the entry's place shows an empty outline, so [from] finds nothing. await gesture.moveTo(start + const Offset(0, 20)); await tester.pump(); await gesture.moveTo(tester.getCenter(to)); await tester.pump(); await gesture.up(); await app.settle(tester); } void main() { testWidgets('"Einplanen" puts a Recipe on today\'s Abend, shown in the grid and the banners', (tester) async { final app = await TestApp.create(tester); await tester.runAsync(() => app.recipes.saveRecipe(recipe())); final (_, abend) = await meals(tester, app); await tester.pumpWidget(app.build()); await app.settle(tester); await tester.tap(find.descendant(of: find.byType(RecipeCard), matching: find.text('Focaccia'))); await app.settle(tester); await tester.tap(find.byTooltip('Einplanen')); await tester.pumpAndSettle(); await tester.tap(inSheet(find.text('Einplanen'))); await app.settle(tester); expect(find.text('Eingeplant: Heute · Abend'), findsOneWidget); await tester.tap(find.byTooltip('Zurück')); await app.settle(tester); expect(find.text('HEUTE GEPLANT'), findsOneWidget); expect(find.text('Abend · 50 Min.'), findsOneWidget); await tester.tap(find.byTooltip('Lieber eine Idee')); await tester.pumpAndSettle(); expect(find.text('IDEE FÜR HEUTE'), findsOneWidget); await openWeekPlan(tester, app); expect(find.text('Diese Woche'), findsOneWidget); expect(inCell(today, abend, find.text('Focaccia')), findsOneWidget); expect(find.textContaining('HEUTE ·'), findsOneWidget); expect(find.text('Kochen'), findsOneWidget); await app.dispose(tester); }); testWidgets('a note is added with "+", removed from its cell and brought back', (tester) async { final app = await TestApp.create(tester); final (mittag, _) = await meals(tester, app); await tester.pumpWidget(app.build()); await app.settle(tester); await openWeekPlan(tester, app); await tester.tap(cell(today, mittag)); await tester.pumpAndSettle(); await tester.enterText(inSheet(find.byType(TextField)), 'Essen gehen'); await tester.pumpAndSettle(); await tester.tap(find.text('Als Notiz: „Essen gehen“')); await app.settle(tester); expect(inCell(today, mittag, find.text('Essen gehen')), findsOneWidget); await tester.tap(cell(today, mittag)); await tester.pumpAndSettle(); await tester.tap(inSheet(find.text('Entfernen'))); await app.settle(tester); expect(inCell(today, mittag, find.text('Essen gehen')), findsNothing); expect(find.text('„Essen gehen“ entfernt'), findsOneWidget); await tester.tap(find.text('Rückgängig')); await app.settle(tester); expect(inCell(today, mittag, find.text('Essen gehen')), findsOneWidget); await app.dispose(tester); }); testWidgets('dragging an entry onto another cell adds it there, and each entry drags on its own', (tester) async { final app = await TestApp.create(tester); final (mittag, abend) = await meals(tester, app); await tester.runAsync(() async { await app.recipes.saveRecipe(recipe()); await app.plan.addEntry(today, abend.id, recipeId: 'r1'); await app.plan.addEntry(today, mittag.id, note: 'Salat'); }); await tester.pumpWidget(app.build()); await app.settle(tester); await openWeekPlan(tester, app); await drag(tester, app, cell(today, abend), cell(today, mittag)); final entries = (await tester.runAsync(app.plan.allEntries))!; expect(entries.map((e) => (e.mealId, e.recipeId ?? e.note)), [(mittag.id, 'Salat'), (mittag.id, 'r1')]); // Both show in the cell, one slim line each. expect(inCell(today, mittag, find.text('Salat')), findsOneWidget); expect(inCell(today, mittag, find.text('Focaccia')), findsOneWidget); // The navigation bar is back once the finger is up. expect(find.text('Hier ablegen zum Entfernen'), findsNothing); // The second one can be dragged on its own. await drag(tester, app, inCell(today, mittag, find.text('Focaccia')), cell(today, abend)); final after = (await tester.runAsync(app.plan.allEntries))!; expect( after.map((e) => (e.mealId, e.recipeId ?? e.note)), unorderedEquals([(mittag.id, 'Salat'), (abend.id, 'r1')]), ); await app.dispose(tester); }); testWidgets('dropping an entry on "Nächste Woche" moves it seven days on', (tester) async { final app = await TestApp.create(tester); final (_, abend) = await meals(tester, app); await tester.runAsync(() async { await app.recipes.saveRecipe(recipe()); await app.plan.addEntry(today, abend.id, recipeId: 'r1'); }); await tester.pumpWidget(app.build()); await app.settle(tester); await openWeekPlan(tester, app); final gesture = await tester.startGesture(tester.getCenter(cell(today, abend))); await tester.pump(kLongPressTimeout + const Duration(milliseconds: 100)); await gesture.moveBy(const Offset(0, 20)); await tester.pump(); expect(find.text('Hier ablegen zum Entfernen'), findsOneWidget); await gesture.moveTo(tester.getCenter(find.text('Nächste Woche'))); await tester.pump(); await gesture.up(); await app.settle(tester); expect((await tester.runAsync(app.plan.allEntries))!.single.day, addDays(today, 7)); expect(find.text('Nächste Woche'), findsOneWidget); // the grid followed it expect(inCell(addDays(today, 7), abend, find.text('Focaccia')), findsOneWidget); await app.dispose(tester); }); testWidgets('dropping an entry on the remove zone removes it, with undo', (tester) async { final app = await TestApp.create(tester); final (_, abend) = await meals(tester, app); await tester.runAsync(() async { await app.recipes.saveRecipe(recipe()); await app.plan.addEntry(today, abend.id, recipeId: 'r1'); }); await tester.pumpWidget(app.build()); await app.settle(tester); await openWeekPlan(tester, app); final gesture = await tester.startGesture(tester.getCenter(cell(today, abend))); await tester.pump(kLongPressTimeout + const Duration(milliseconds: 100)); await gesture.moveBy(const Offset(0, 20)); await tester.pump(); await gesture.moveTo(tester.getCenter(find.text('Hier ablegen zum Entfernen'))); await tester.pump(); await gesture.up(); await app.settle(tester); expect(await tester.runAsync(app.plan.allEntries), isEmpty); await tester.tap(find.text('Rückgängig')); await app.settle(tester); expect((await tester.runAsync(app.plan.allEntries))!.single.recipeId, 'r1'); await app.dispose(tester); }); testWidgets('"Woche füllen" gives every Abend from today on a different Hauptgericht', (tester) async { final app = await TestApp.create(tester); final courses = (await tester.runAsync(app.courses.allCourses))!; final main = courses.firstWhere((c) => c.name == 'Hauptgericht'); final dessert = courses.firstWhere((c) => c.name == 'Dessert'); await tester.runAsync(() async { for (var i = 0; i < 7; i++) { await app.recipes.saveRecipe(recipe(id: 'm$i', name: 'Gericht $i', courseId: main.id)); } await app.recipes.saveRecipe(recipe(id: 'd', name: 'Pudding', courseId: dessert.id)); }); await tester.pumpWidget(app.build()); await app.settle(tester); await openWeekPlan(tester, app); await tester.tap(find.byTooltip('Woche füllen')); await tester.pumpAndSettle(); await tester.tap(find.text('Füllen')); await app.settle(tester); final daysLeft = addDays(weekStartOf(today, DateTime.monday), 6).difference(today).inDays + 1; final entries = (await tester.runAsync(app.plan.allEntries))!; final (_, abend) = await meals(tester, app); expect(entries, hasLength(daysLeft)); expect(entries.map((e) => e.recipeId).toSet(), hasLength(daysLeft)); expect(entries.every((e) => e.mealId == abend.id && e.recipeId!.startsWith('m')), isTrue); expect(find.text(daysLeft == 1 ? '1 Tag gefüllt.' : '$daysLeft Tage gefüllt.'), findsOneWidget); // Cells show the Course's icon. expect(inCell(today, abend, find.text('🍝')), findsOneWidget); await app.dispose(tester); }); testWidgets('"Zutaten der Woche teilen" adds up what is planned', (tester) async { final shared = >[]; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( const MethodChannel('dev.fluttercommunity.plus/share'), (call) async { shared.add(call.arguments as Map); return 'dev.fluttercommunity.plus/share/unavailable'; }, ); final app = await TestApp.create(tester); await tester.runAsync(() async { await app.recipes.saveRecipe(recipe()); for (final meal in await app.plan.allMeals()) { await app.plan.addEntry(today, meal.id, recipeId: 'r1'); } }); await tester.pumpWidget(app.build()); await app.settle(tester); await openWeekPlan(tester, app); await tester.tap(find.byTooltip('Teilen')); await tester.pumpAndSettle(); await tester.tap(find.text('Zutaten der Woche teilen')); await tester.pumpAndSettle(); await tester.tap(inSheet(find.text('Teilen'))); await tester.pumpAndSettle(); expect(shared.single['text'], '600 g Mehl\n4–6 EL Olivenöl (gutes)\nRosmarin'); await app.dispose(tester); }); testWidgets('a Meal added in the settings becomes a column of the grid', (tester) async { final app = await TestApp.create(tester); await tester.pumpWidget(app.build()); await app.settle(tester); await tester.tap(find.byTooltip('Einstellungen')); await tester.pumpAndSettle(); await tester.tap(find.text('Mahlzeiten bearbeiten')); await app.settle(tester); await tester.tap(find.text('Mahlzeit hinzufügen')); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextFormField), 'Frühstück'); await tester.tap(find.text('Speichern')); await app.settle(tester); await tester.tap(find.byTooltip('Zurück')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Zurück')); await app.settle(tester); await openWeekPlan(tester, app); expect(find.text('Frühstück'), findsOneWidget); final fruehstueck = (await tester.runAsync(app.plan.allMeals))!.last; expect(cell(today, fruehstueck), findsOneWidget); await app.dispose(tester); }); testWidgets('the add sheet filters Recipes by Course', (tester) async { final app = await TestApp.create(tester); final courses = (await tester.runAsync(app.courses.allCourses))!; final dessert = courses.firstWhere((c) => c.name == 'Dessert'); final main = courses.firstWhere((c) => c.name == 'Hauptgericht'); final (mittag, _) = await meals(tester, app); await tester.runAsync(() async { await app.recipes.saveRecipe(recipe(id: 'a', name: 'Tiramisu', courseId: dessert.id)); await app.recipes.saveRecipe(recipe(id: 'b', name: 'Rinderrouladen', courseId: main.id)); }); await tester.pumpWidget(app.build()); await app.settle(tester); await openWeekPlan(tester, app); await tester.tap(cell(today, mittag)); await tester.pumpAndSettle(); expect(inSheet(find.text('Tiramisu')), findsOneWidget); expect(inSheet(find.text('Rinderrouladen')), findsOneWidget); // The Course chips scroll sideways; Dessert is far to the right. await tester.dragUntilVisible( inSheet(find.text(dessert.label)), inSheet(find.byType(ListView)).first, const Offset(-200, 0), ); await tester.ensureVisible(inSheet(find.text(dessert.label))); await tester.pumpAndSettle(); await tester.tap(inSheet(find.text(dessert.label))); await tester.pumpAndSettle(); expect(inSheet(find.text('Tiramisu')), findsOneWidget); expect(inSheet(find.text('Rinderrouladen')), findsNothing); await tester.tap(inSheet(find.text('Tiramisu'))); await app.settle(tester); expect(inCell(today, mittag, find.text('Tiramisu')), findsOneWidget); await app.dispose(tester); }); testWidgets('a note can be edited from its cell', (tester) async { final app = await TestApp.create(tester); final (mittag, _) = await meals(tester, app); await tester.runAsync(() => app.plan.addEntry(today, mittag.id, note: 'Essen gehen')); await tester.pumpWidget(app.build()); await app.settle(tester); await openWeekPlan(tester, app); await tester.tap(cell(today, mittag)); await tester.pumpAndSettle(); await tester.tap(inSheet(find.text('Bearbeiten'))); await tester.pumpAndSettle(); await tester.enterText( find.descendant(of: find.byType(AlertDialog), matching: find.byType(TextField)), 'Pizza bestellen', ); await tester.tap(find.text('Speichern')); await app.settle(tester); expect(inCell(today, mittag, find.text('Pizza bestellen')), findsOneWidget); expect((await tester.runAsync(app.plan.allEntries))!.single.note, 'Pizza bestellen'); await app.dispose(tester); }); }