glp-companion/test/features/meal_planner/meal_planner_screen_test.dart

129 lines
4.8 KiB
Dart

import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:glp1_companion/core/database/app_database.dart';
import 'package:glp1_companion/features/meal_planner/presentation/screens/meal_planner_screen.dart';
/// Regression tests for the desktop resize overflow (yellow/black hatched
/// stripes): the meal planner must reflow into scrollable layouts at short
/// window heights instead of overflowing a rigid `Column`.
///
/// RenderFlex overflows raise a [FlutterError] inside widget tests, so a
/// clean run also proves the stripes are gone.
void main() {
late AppDatabase database;
setUp(() {
database = AppDatabase.forTesting(NativeDatabase.memory());
});
tearDown(() => database.close());
Future<void> pumpScreen(
WidgetTester tester, {
required Size logicalSize,
}) async {
tester.view.physicalSize = logicalSize;
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
ProviderScope(
overrides: [appDatabaseProvider.overrideWithValue(database)],
child: const MaterialApp(home: MealPlannerScreen()),
),
);
// Let the initial drift loads (watch stream + microtask refresh) settle.
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
await tester.pump(const Duration(milliseconds: 50));
}
/// Unmounts the tree and flushes the zero-duration timer drift schedules
/// when its query streams are cancelled during container disposal
/// (`StreamQueryStore.markAsClosed`), which would otherwise trip the test
/// framework's pending-timers invariant.
Future<void> unmountAndFlushTimers(WidgetTester tester) async {
await tester.pumpWidget(const SizedBox());
await tester.pump(const Duration(milliseconds: 1));
}
testWidgets(
'short compact window collapses into a scrollable page without overflow',
(tester) async {
await pumpScreen(tester, logicalSize: const Size(480, 320));
expect(find.byType(MealPlannerScreen), findsOneWidget);
expect(find.text('Today'), findsOneWidget);
expect(tester.takeException(), isNull);
// The whole page scrolls: drag upward and verify the content moved up.
final before = tester.getTopLeft(find.text('Today')).dy;
await tester.drag(find.text('Today'), const Offset(0, -120));
await tester.pump();
final after = tester.getTopLeft(find.text('Today')).dy;
expect(after, lessThan(before),
reason: 'the page should scroll vertically when the window is short');
expect(tester.takeException(), isNull);
await unmountAndFlushTimers(tester);
});
testWidgets(
'short wide window keeps the 7-day grid with scrollable day columns',
(tester) async {
await pumpScreen(tester, logicalSize: const Size(1100, 420));
// Wide breakpoint: all seven weekday headers render at once.
for (final label in const [
'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday',
]) {
expect(find.textContaining(label), findsWidgets);
}
expect(tester.takeException(), isNull);
await unmountAndFlushTimers(tester);
});
testWidgets('tall window uses the pinned layout without overflow',
(tester) async {
await pumpScreen(tester, logicalSize: const Size(1000, 900));
expect(find.text('Today'), findsOneWidget);
// 1000 dp is above the 900 dp wide breakpoint, so the full seven-column
// grid renders; empty slots surface the "+ Assign Recipe" affordance.
expect(find.text('Assign Recipe'), findsWidgets);
for (final label in const [
'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday',
]) {
expect(find.textContaining(label), findsWidgets);
}
expect(tester.takeException(), isNull);
await unmountAndFlushTimers(tester);
});
testWidgets('extremely short window still renders without overflow',
(tester) async {
await pumpScreen(tester, logicalSize: const Size(1280, 320));
expect(find.byType(MealPlannerScreen), findsOneWidget);
expect(tester.takeException(), isNull);
await unmountAndFlushTimers(tester);
});
testWidgets('slot tap opens the searchable assign sheet from the vault',
(tester) async {
await pumpScreen(tester, logicalSize: const Size(1400, 900));
// Wide grid: tap the first "+ Assign Recipe" affordance.
await tester.tap(find.text('Assign Recipe').first);
await tester.pumpAndSettle();
expect(find.text('Search Recipe Vault'), findsOneWidget);
expect(find.text('Custom entry (no recipe)'), findsOneWidget);
expect(tester.takeException(), isNull);
await unmountAndFlushTimers(tester);
});
}