Initial commit: CookIT recipe collection and week planner
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
7d7a0bc60b
173 files changed
+22803
No files matched your search
@@ -0,0 +1,232 @@
|
||||
import 'package:cookit/src/recipes/models/recipe.dart';
|
||||
import 'package:cookit/src/recipes/views/widgets/recipe_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../support/fixtures.dart';
|
||||
import '../support/test_app.dart';
|
||||
|
||||
/// A Recipe name as shown on a grid card (the suggestion card may show it too).
|
||||
Finder inGrid(String text) => find.descendant(of: find.byType(RecipeCard), matching: find.text(text));
|
||||
|
||||
void main() {
|
||||
testWidgets('home grid shows Recipes and filters them by search, Course and review', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
final dessert = (await tester.runAsync(() => app.courses.findOrCreate('Dessert')))!;
|
||||
await tester.runAsync(() async {
|
||||
await app.recipes.saveRecipe(recipe(id: 'a', name: 'Käsespätzle', tags: ['schwäbisch']));
|
||||
await app.recipes.saveRecipe(
|
||||
recipe(id: 'b', name: 'Tiramisu', courseId: dessert.id, reviewReasons: ['Portionen fehlen.']),
|
||||
);
|
||||
});
|
||||
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
expect(inGrid('Käsespätzle'), findsOneWidget);
|
||||
expect(inGrid('Tiramisu'), findsOneWidget);
|
||||
expect(find.text('IDEE FÜR HEUTE'), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'kase');
|
||||
await tester.pumpAndSettle();
|
||||
expect(inGrid('Käsespätzle'), findsOneWidget);
|
||||
expect(inGrid('Tiramisu'), findsNothing);
|
||||
|
||||
await tester.enterText(find.byType(TextField), '');
|
||||
await tester.tap(find.text('1 Rezept wartet auf deinen Blick'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(inGrid('Käsespätzle'), findsNothing);
|
||||
expect(inGrid('Tiramisu'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Nur Rezepte zum Prüfen – alle zeigen'));
|
||||
await tester.pumpAndSettle();
|
||||
final chips = find.byWidgetPredicate((w) => w is ListView && w.scrollDirection == Axis.horizontal);
|
||||
final dessertChip = find.descendant(of: chips, matching: find.text('Dessert'));
|
||||
await tester.scrollUntilVisible(
|
||||
dessertChip,
|
||||
200,
|
||||
scrollable: find.descendant(of: chips, matching: find.byType(Scrollable)),
|
||||
);
|
||||
await tester.tap(dessertChip);
|
||||
await tester.pumpAndSettle();
|
||||
expect(inGrid('Tiramisu'), findsOneWidget);
|
||||
expect(inGrid('Käsespätzle'), findsNothing);
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('Cooking Mode walks through the ingredients and every step', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
await tester.runAsync(() => app.recipes.saveRecipe(recipe()));
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
await tester.tap(inGrid('Focaccia'));
|
||||
await app.settle(tester);
|
||||
|
||||
await tester.tap(find.text('Kochmodus starten'));
|
||||
await app.settle(tester);
|
||||
expect(find.text('Bereit zum Kochen?'), findsOneWidget);
|
||||
expect(find.text('300 g'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Mehr Portionen'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('5'), findsOneWidget);
|
||||
expect(find.text('375 g'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Los geht’s'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Schritt 1 von 2'), findsOneWidget);
|
||||
expect(find.text('Kneten.'), findsOneWidget);
|
||||
expect(find.text('Teig'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Weiter'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Gehen lassen.'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Fertig'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Guten Appetit!'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Zurück zum Rezept'));
|
||||
await app.settle(tester);
|
||||
expect(find.text('Kochmodus starten'), findsOneWidget);
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('the heart on a card toggles the favourite', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
await tester.runAsync(() => app.recipes.saveRecipe(recipe()));
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
|
||||
await tester.tap(find.descendant(of: find.byType(RecipeCard), matching: find.byTooltip('Favorit')));
|
||||
await app.settle(tester);
|
||||
expect((await tester.runAsync(app.recipes.allRecipes))!.single.favourite, isTrue);
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('empty collection explains how to start', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
expect(find.text('Noch keine Rezepte'), findsOneWidget);
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('detail shows the Recipe, and "Erledigt" clears its Review Flag', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
await tester.runAsync(() => app.recipes.saveRecipe(recipe(reviewReasons: ['Portionen fehlen.'])));
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
|
||||
await tester.tap(inGrid('Focaccia'));
|
||||
await app.settle(tester);
|
||||
expect(find.text('Bitte prüfen'), findsOneWidget);
|
||||
expect(find.text('• Portionen fehlen.'), findsOneWidget);
|
||||
expect(find.text('300 g'), findsOneWidget);
|
||||
expect(find.text('2–3 EL'), findsOneWidget);
|
||||
expect(find.text('Topping'), findsOneWidget);
|
||||
expect(find.text('775 kcal'), findsOneWidget);
|
||||
expect(find.text('pro Portion'), findsOneWidget);
|
||||
expect(find.text('50 Min.'), findsOneWidget);
|
||||
|
||||
expect(find.text('22x22 cm Form'), findsOneWidget, reason: 'Notes show under the Zutaten tab too');
|
||||
await tester.tap(find.text('Zubereitung · 2'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Teig'), findsOneWidget);
|
||||
expect(find.text('Kneten.'), findsOneWidget);
|
||||
expect(find.text('22x22 cm Form'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Erledigt'));
|
||||
await app.settle(tester);
|
||||
expect(find.text('Bitte prüfen'), findsNothing);
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('a new Recipe can be created with ingredients and steps', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
|
||||
await tester.tap(find.byTooltip('Neues Rezept'));
|
||||
await app.settle(tester);
|
||||
|
||||
await tester.tap(find.text('Speichern'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Bitte gib dem Rezept einen Namen.'), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.widgetWithText(TextFormField, 'Name'), 'Pfannkuchen');
|
||||
await tester.enterText(find.widgetWithText(TextFormField, 'Portionen'), '2');
|
||||
final ingredientName = find.widgetWithText(TextFormField, 'Zutat');
|
||||
await tester.enterText(find.widgetWithText(TextFormField, 'Menge'), '1,5');
|
||||
await tester.enterText(ingredientName, 'Mehl');
|
||||
await tester.tap(find.text('–'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Bund'), findsNothing);
|
||||
await tester.tap(find.text('Weitere Einheiten …'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(ChoiceChip, 'Bund'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Bund'), findsOneWidget);
|
||||
await tester.enterText(find.widgetWithText(TextFormField, 'Schritt 1'), 'Alles verrühren.');
|
||||
|
||||
await tester.tap(find.text('Speichern'));
|
||||
await app.settle(tester);
|
||||
|
||||
expect(inGrid('Pfannkuchen'), findsOneWidget);
|
||||
final saved = (await tester.runAsync(app.recipes.allRecipes))!.single;
|
||||
expect(saved.servings, 2);
|
||||
expect(saved.ingredientGroups.single.lines.single.amount, 1.5);
|
||||
expect(saved.ingredientGroups.single.lines.single.name, 'Mehl');
|
||||
expect(saved.ingredientGroups.single.lines.single.unitKey, 'bunch');
|
||||
expect(saved.stepSections.single.steps, ['Alles verrühren.']);
|
||||
expect(saved.calories, isNull);
|
||||
expect(saved.needsReview, isFalse);
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('an invalid amount blocks saving', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
await tester.tap(find.byTooltip('Neues Rezept'));
|
||||
await app.settle(tester);
|
||||
|
||||
await tester.enterText(find.widgetWithText(TextFormField, 'Name'), 'Test');
|
||||
await tester.enterText(find.widgetWithText(TextFormField, 'Menge'), 'viel');
|
||||
await tester.tap(find.text('Speichern'));
|
||||
await app.settle(tester);
|
||||
expect(find.text('z. B. 2, 1,5, ½ oder 2-3'), findsOneWidget);
|
||||
expect(await tester.runAsync(app.recipes.allRecipes), isEmpty);
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('editing keeps favourite and Review Flag', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
await tester.runAsync(() => app.recipes.saveRecipe(recipe(favourite: true, reviewReasons: ['x'])));
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
await tester.tap(inGrid('Focaccia'));
|
||||
await app.settle(tester);
|
||||
await tester.tap(find.byTooltip('Bearbeiten'));
|
||||
await app.settle(tester);
|
||||
|
||||
await tester.enterText(find.widgetWithText(TextFormField, 'Name'), 'Focaccia mit Rosmarin');
|
||||
await tester.tap(find.text('Speichern'));
|
||||
await app.settle(tester);
|
||||
|
||||
final saved = (await tester.runAsync(app.recipes.allRecipes))!.single;
|
||||
expect(saved.name, 'Focaccia mit Rosmarin');
|
||||
expect(saved.favourite, isTrue);
|
||||
expect(saved.reviewReasons, ['x']);
|
||||
expect(saved.calories!.basis, CaloriesBasis.perServing);
|
||||
expect(saved.ingredientGroups.map((g) => g.name), [null, 'Topping']);
|
||||
expect(saved.stepSections.single.title, 'Teig');
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import 'package:cookit/src/recipes/views/widgets/recipe_card.dart';
|
||||
import 'package:cookit/src/recipes/views/widgets/recipe_photo.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';
|
||||
|
||||
Finder inSheet(Finder finder) => find.descendant(of: find.byType(BottomSheet), matching: finder);
|
||||
|
||||
void main() {
|
||||
testWidgets('"Hilf mir entscheiden" narrows down by Course and time', (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');
|
||||
await tester.runAsync(() async {
|
||||
await app.recipes.saveRecipe(
|
||||
recipe(
|
||||
id: 'a',
|
||||
name: 'Kaiserschmarrn',
|
||||
courseId: dessert.id,
|
||||
).copyWith(prepMinutes: () => 10, cookMinutes: () => 10),
|
||||
);
|
||||
await app.recipes.saveRecipe(recipe(id: 'b', name: 'Tiramisu', courseId: dessert.id)); // 50 min
|
||||
await app.recipes.saveRecipe(recipe(id: 'c', name: 'Rinderrouladen', courseId: main.id));
|
||||
});
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
|
||||
await tester.tap(find.byTooltip('Hilf mir entscheiden'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Worauf hast du Lust?'), findsOneWidget);
|
||||
await tester.tap(inSheet(find.text('Dessert')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Weiter'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Wie viel Zeit hast du?'), findsOneWidget);
|
||||
await tester.tap(find.text('bis 30 Min.'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Vorschlag zeigen'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('1 Rezept passt'), findsOneWidget);
|
||||
expect(inSheet(find.text('Kaiserschmarrn')), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Rezept öffnen'));
|
||||
await app.settle(tester);
|
||||
expect(find.text('Kochmodus starten'), findsOneWidget);
|
||||
expect(find.text('Kaiserschmarrn'), findsOneWidget);
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('the decision helper offers a looser choice when nothing fits', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
await tester.runAsync(() => app.recipes.saveRecipe(recipe())); // 50 min, no Course
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
|
||||
await tester.tap(find.byTooltip('Hilf mir entscheiden'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Weiter'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('bis 30 Min.'));
|
||||
await tester.tap(find.text('Vorschlag zeigen'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Dazu passt leider nichts.'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Ohne Zeitlimit: 1'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('1 Rezept passt'), findsOneWidget);
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('Darstellung switches between light and dark', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
ThemeMode mode() => tester.widget<MaterialApp>(find.byType(MaterialApp)).themeMode!;
|
||||
expect(mode(), ThemeMode.light);
|
||||
|
||||
await tester.tap(find.byTooltip('Einstellungen'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Dunkel'));
|
||||
await app.settle(tester);
|
||||
expect(mode(), ThemeMode.dark);
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('an own unit added in the settings is offered in the unit picker', (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('Maßeinheiten bearbeiten'));
|
||||
await app.settle(tester);
|
||||
await tester.tap(find.text('Einheit hinzufügen'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextFormField), 'Würfel');
|
||||
await tester.tap(find.text('Speichern'));
|
||||
await app.settle(tester);
|
||||
expect(find.text('Würfel'), findsOneWidget);
|
||||
expect(find.text('Eigene Einheit'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Zurück'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('Zurück'));
|
||||
await app.settle(tester);
|
||||
await tester.tap(find.byTooltip('Neues Rezept'));
|
||||
await app.settle(tester);
|
||||
await tester.tap(find.text('–'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.widgetWithText(ChoiceChip, 'Würfel'), findsOneWidget);
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('tapping a photo can make it the main Photo', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
final (first, second) = (await tester.runAsync(() async {
|
||||
final a = await app.photos.saveBytes(Uint8List.fromList([1]));
|
||||
final b = await app.photos.saveBytes(Uint8List.fromList([2]));
|
||||
await app.recipes.saveRecipe(recipe(photos: [a, b]));
|
||||
return (a, b);
|
||||
}))!;
|
||||
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('Bearbeiten'));
|
||||
await app.settle(tester);
|
||||
|
||||
expect(find.textContaining('das erste Foto ist das Hauptfoto'), findsOneWidget);
|
||||
// The photo is covered by its own tap layer, which is what receives the tap.
|
||||
await tester.tap(find.byType(RecipePhoto).at(1), warnIfMissed: false);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Als Hauptfoto festlegen'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Speichern'));
|
||||
await app.settle(tester);
|
||||
|
||||
expect((await tester.runAsync(() => app.recipes.getRecipe('r1')))!.photos, [second, first]);
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('"Zutaten teilen" shares the ingredients for the chosen servings', (tester) async {
|
||||
final shared = <Map<Object?, Object?>>[];
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
const MethodChannel('dev.fluttercommunity.plus/share'),
|
||||
(call) async {
|
||||
shared.add(call.arguments as Map<Object?, Object?>);
|
||||
return 'dev.fluttercommunity.plus/share/unavailable';
|
||||
},
|
||||
);
|
||||
final app = await TestApp.create(tester);
|
||||
await tester.runAsync(() => app.recipes.saveRecipe(recipe()));
|
||||
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.scrollUntilVisible(find.text('Zutaten teilen'), 200, scrollable: find.byType(Scrollable).first);
|
||||
await tester.tap(find.text('Zutaten teilen'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(inSheet(find.byTooltip('Mehr Portionen')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Teilen'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(shared, hasLength(1));
|
||||
expect(shared.single['text'], '375 g Mehl\n2½–3¾ EL Olivenöl (gutes)\n\nRosmarin');
|
||||
expect(shared.single['subject'], 'Focaccia – Zutaten für 5 Portionen');
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:cookit/src/planning/day.dart';
|
||||
import 'package:cookit/src/planning/plan_share_service.dart';
|
||||
import 'package:cookit/src/planning/views/shared_week_view.dart';
|
||||
import 'package:cookit/src/recipes/views/widgets/recipe_card.dart';
|
||||
import 'package:cookit/src/sharing/recipe_code.dart';
|
||||
import 'package:cookit/src/sharing/views/shared_recipe_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import '../support/fixtures.dart';
|
||||
import '../support/test_app.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('"Rezept teilen" shows the Recipe Code and can leave the notes out', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
await tester.runAsync(() => app.recipes.saveRecipe(recipe()));
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
expect(find.byTooltip('Rezept scannen'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.descendant(of: find.byType(RecipeCard), matching: find.text('Focaccia')));
|
||||
await app.settle(tester);
|
||||
await tester.tap(find.byTooltip('Rezept teilen'));
|
||||
await app.settle(tester);
|
||||
|
||||
expect(find.byType(QrImageView), findsOneWidget);
|
||||
expect(find.text('Als Datei teilen'), findsOneWidget);
|
||||
await tester.tap(find.text('Notizen mitschicken'));
|
||||
await app.settle(tester);
|
||||
expect(tester.widget<SwitchListTile>(find.byType(SwitchListTile)).value, isFalse);
|
||||
expect(find.byType(QrImageView), findsOneWidget);
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('a received Recipe is previewed, flagged as a double and saved as a copy', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
await tester.runAsync(() => app.recipes.saveRecipe(recipe()));
|
||||
final shared = RecipeCode.decode(RecipeCode.encode(recipe(), courseName: 'Brot'), now: DateTime(2026));
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
|
||||
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed(SharedRecipeView.routeName, arguments: shared);
|
||||
await app.settle(tester);
|
||||
expect(find.text('Rezept empfangen'), findsOneWidget);
|
||||
expect(find.text('Brot · 4 Portionen · 50 Min.'), findsOneWidget);
|
||||
expect(find.text('Mehl'), findsOneWidget);
|
||||
expect(find.text('1. Kneten.'), findsOneWidget);
|
||||
expect(find.textContaining('Du hast schon ein Rezept „Focaccia“'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Speichern'));
|
||||
await app.settle(tester);
|
||||
expect(find.text('„Focaccia“ gespeichert'), findsOneWidget);
|
||||
expect(find.text('Kochmodus starten'), findsOneWidget);
|
||||
final all = (await tester.runAsync(app.recipes.allRecipes))!;
|
||||
expect(all, hasLength(2));
|
||||
final courses = (await tester.runAsync(app.courses.allCourses))!;
|
||||
expect(courses.map((c) => c.name), contains('Brot'));
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
|
||||
testWidgets('a received week is previewed and adopted next to what is planned', (tester) async {
|
||||
final app = await TestApp.create(tester);
|
||||
final monday = DateTime.utc(2026, 10, 19);
|
||||
final week = (await tester.runAsync(() async {
|
||||
await app.recipes.saveRecipe(recipe());
|
||||
final meals = await app.plan.allMeals();
|
||||
final abend = meals.firstWhere((m) => m.name == 'Abend');
|
||||
await app.plan.addEntry(monday, abend.id, recipeId: 'r1');
|
||||
await app.plan.addEntry(addDays(monday, 2), abend.id, note: 'Essen gehen');
|
||||
final shares = PlanShareService(app.recipes, app.courses, app.units, app.photos, app.plan, app.bundles);
|
||||
return SharedWeek(await app.bundles.read(await shares.weekFile(monday)));
|
||||
}))!;
|
||||
await tester.pumpWidget(app.build());
|
||||
await app.settle(tester);
|
||||
|
||||
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed(SharedWeekView.routeName, arguments: week);
|
||||
await app.settle(tester);
|
||||
expect(find.text('Wochenplan empfangen'), findsOneWidget);
|
||||
expect(find.text('2 Einträge · keine neuen Rezepte'), findsOneWidget);
|
||||
expect(find.textContaining('schon 2 Einträge'), findsOneWidget);
|
||||
expect(find.text('Focaccia'), findsOneWidget);
|
||||
expect(find.text('Essen gehen'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Übernehmen'));
|
||||
await app.settle(tester);
|
||||
expect(find.text('Wochenplan 19.–25. Okt. übernommen'), findsOneWidget);
|
||||
expect(await tester.runAsync(app.plan.allEntries), hasLength(4));
|
||||
expect(await tester.runAsync(app.recipes.allRecipes), hasLength(1));
|
||||
|
||||
await app.dispose(tester);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
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<void> 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<void> 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 = <Map<Object?, Object?>>[];
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
const MethodChannel('dev.fluttercommunity.plus/share'),
|
||||
(call) async {
|
||||
shared.add(call.arguments as Map<Object?, Object?>);
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user