diet-guard/app/test/screens/history_screen_test.dart
Krzysztof kuhy Rudnicki c43e37b09d Compact LogMealScreen so it fits without scrolling on-screen keyboard
Merge the slot-status bar, slot-selector chips, and "Logging for
HH:00" caption into one selectable, status-colored SlotSelectorRow.
Add an opt-in compact mode to MacroInputRow (single row, abbreviated
labels), AutocompleteSuggestionList (top-3 + "N more" bottom sheet),
and PhotoAttachField (icon-only + badge thumbnail), used only by
LogMealScreen so MealBuilderScreen/EditEntryScreen keep their default
rendering. Verified on-device (BL-9000) that all fields stay visible
with the keyboard open.

Also fixes an unrelated time-bomb in history_screen_test.dart's date
range picker test, which hardcoded an expected "2026-06-01" label
assuming "today" was in June; the picker's displayed month and
selectable range depend on the real current date, so the assertion
now computes its expectation from DateTime.now() instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UorgLvWJ4huH55tmXoUAZ
2026-07-04 05:19:23 +02:00

1012 lines
30 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:io';
import 'package:diet_guard_app/models/food_entry.dart';
import 'package:diet_guard_app/screens/history_screen.dart';
import 'package:diet_guard_app/screens/photo_viewer_screen.dart';
import 'package:diet_guard_app/services/log_storage_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
late Directory tempDir;
setUp(() async {
tempDir = await Directory.systemTemp.createTemp('diet_guard_history_');
LogStorageService.resetForTesting(testDir: tempDir);
});
tearDown(() async {
LogStorageService.resetForTesting();
await tempDir.delete(recursive: true);
});
// HistoryScreen loads via a fire-and-forget Future in initState that
// Flutter's frame scheduler does not track -- see log_meal_screen_test.dart
// for the same issue. Every test therefore runs inside runAsync() with a
// short real delay before settling.
Future<void> settle(WidgetTester tester) async {
await Future<void>.delayed(const Duration(milliseconds: 200));
await tester.pumpAndSettle();
}
testWidgets('shows a message when nothing has been logged', (tester) async {
await tester.runAsync(() async {
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
expect(find.text('Nothing logged yet.'), findsOneWidget);
});
});
testWidgets('lists logged entries newest first, excluding tombstones', (
tester,
) async {
await tester.runAsync(() async {
await LogStorageService.instance.writeLog({
'2026-06-01': [
const FoodEntry(
id: 'old',
time: '2026-06-01T08:00:00+02:00',
desc: 'old breakfast',
grams: 100,
kcal: 100,
proteinG: 5,
carbsG: 10,
fatG: 2,
source: 'manual',
),
],
'2026-06-22': [
const FoodEntry(
id: 'new',
time: '2026-06-22T20:00:00+02:00',
desc: 'new dinner',
grams: 100,
kcal: 200,
proteinG: 10,
carbsG: 20,
fatG: 4,
source: 'manual',
),
const FoodEntry(
id: 'gone',
time: '2026-06-22T12:00:00+02:00',
desc: 'undone lunch',
grams: 100,
kcal: 300,
proteinG: 1,
carbsG: 1,
fatG: 1,
source: 'manual',
deleted: true,
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
expect(find.text('new dinner'), findsOneWidget);
expect(find.text('old breakfast'), findsOneWidget);
expect(find.text('undone lunch'), findsNothing);
final tiles = tester.widgetList<ListTile>(find.byType(ListTile)).toList();
expect((tiles[0].title! as Text).data, 'new dinner');
expect((tiles[1].title! as Text).data, 'old breakfast');
});
});
testWidgets('tapping a thumbnail opens the full-screen photo viewer', (
tester,
) async {
await tester.runAsync(() async {
final imageFile = File('${tempDir.path}/photo.png')
..writeAsBytesSync([1, 2, 3]);
await LogStorageService.instance.writeLog({
'2026-06-22': [
FoodEntry(
id: 'with-photo',
time: '2026-06-22T20:00:00+02:00',
desc: 'dinner with a photo',
grams: 100,
kcal: 200,
proteinG: 10,
carbsG: 20,
fatG: 4,
source: 'manual',
imagePath: imageFile.path,
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
await tester.tap(find.byType(Image));
await settle(tester);
expect(find.byType(PhotoViewerScreen), findsOneWidget);
});
});
// ---------------------------------------------------------------------------
// applyHistoryFilter — pure function tests (no widget required)
// ---------------------------------------------------------------------------
group('applyHistoryFilter', () {
final entries = [
const FoodEntry(
id: 'a',
time: '2026-06-20T08:00:00+02:00',
desc: 'Apple',
grams: 100,
kcal: 80,
proteinG: 0.5,
carbsG: 20,
fatG: 0.3,
source: 'manual',
),
const FoodEntry(
id: 'b',
time: '2026-06-21T12:00:00+02:00',
desc: 'Banana smoothie',
grams: 250,
kcal: 200,
proteinG: 3,
carbsG: 40,
fatG: 1,
source: 'food bank',
imagePath: '/fake/img.jpg',
),
const FoodEntry(
id: 'c',
time: '2026-06-22T20:00:00+02:00',
desc: 'Chicken breast',
grams: 150,
kcal: 230,
proteinG: 45,
carbsG: 0,
fatG: 5,
source: 'meal',
),
];
test('no filter returns all entries sorted by date descending', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(),
HistorySortField.date,
ascending: false,
);
expect(result.map((e) => e.id), ['c', 'b', 'a']);
});
test('nameQuery filters by case-insensitive substring', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(nameQuery: 'an'),
HistorySortField.date,
ascending: false,
);
expect(result.map((e) => e.id), ['b']); // 'Banana smoothie'
});
test('minKcal and maxKcal filter by kcal range', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(minKcal: 100, maxKcal: 210),
HistorySortField.date,
ascending: false,
);
expect(result.map((e) => e.id), ['b']); // 200 kcal
});
test('minProtein filters by protein', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(minProtein: 10),
HistorySortField.date,
ascending: false,
);
expect(result.map((e) => e.id), ['c']); // 45g protein
});
test('maxCarbs filters by carbs', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(maxCarbs: 5),
HistorySortField.date,
ascending: false,
);
expect(result.map((e) => e.id), ['c']); // 0 carbs
});
test('minFat and maxFat filter by fat', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(minFat: 0.4, maxFat: 2),
HistorySortField.date,
ascending: false,
);
expect(result.map((e) => e.id), ['b']); // fat=1
});
test('hasPhoto=true keeps only entries with imagePath', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(hasPhoto: true),
HistorySortField.date,
ascending: false,
);
expect(result.map((e) => e.id), ['b']);
});
test('hasPhoto=false keeps only entries without imagePath', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(hasPhoto: false),
HistorySortField.date,
ascending: false,
);
expect(result.map((e) => e.id), ['c', 'a']);
});
test('source filter keeps only matching source', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(source: 'meal'),
HistorySortField.date,
ascending: false,
);
expect(result.map((e) => e.id), ['c']);
});
test('dateRange filter includes only entries within range', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(
dateRange: DateTimeRange(
start: DateTime(2026, 6, 21),
end: DateTime(2026, 6, 21),
),
),
HistorySortField.date,
ascending: false,
);
expect(result.map((e) => e.id), ['b']);
});
test('sort ascending by kcal', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(),
HistorySortField.kcal,
ascending: true,
);
expect(result.map((e) => e.id), ['a', 'b', 'c']);
});
test('sort descending by protein', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(),
HistorySortField.protein,
ascending: false,
);
expect(result.first.id, 'c'); // 45g
});
test('sort by description ascending', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(),
HistorySortField.description,
ascending: true,
);
// Apple, Banana smoothie, Chicken breast
expect(result.map((e) => e.id), ['a', 'b', 'c']);
});
test('sort by fat', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(),
HistorySortField.fat,
ascending: true,
);
expect(result.first.id, 'a'); // fat=0.3
});
test('sort by carbs descending', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(),
HistorySortField.carbs,
ascending: false,
);
expect(result.first.id, 'b'); // 40g carbs
});
test('HistoryFilter.isActive is false when nothing is set', () {
expect(HistoryFilter().isActive, isFalse);
});
test('HistoryFilter.isActive is true when nameQuery is set', () {
expect(HistoryFilter(nameQuery: 'x').isActive, isTrue);
});
test('HistoryFilter.isActive is true when source is set', () {
expect(HistoryFilter(source: 'manual').isActive, isTrue);
});
test('HistoryFilter.isActive is true when hasPhoto is set', () {
expect(HistoryFilter(hasPhoto: true).isActive, isTrue);
});
test('maxProtein filters by protein', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(maxProtein: 5),
HistorySortField.date,
ascending: false,
);
// Apple (0.5 g) and Banana (3 g) have protein ≤ 5 g.
expect(result.map((e) => e.id), ['b', 'a']);
});
test('minCarbs filters by carbs', () {
final result = applyHistoryFilter(
entries,
HistoryFilter(minCarbs: 15),
HistorySortField.date,
ascending: false,
);
// Banana (40 g) and Apple (20 g) have carbs ≥ 15 g.
expect(result.map((e) => e.id), ['b', 'a']);
});
});
// ---------------------------------------------------------------------------
// Widget-level — day grouping and filter badge
// ---------------------------------------------------------------------------
testWidgets('shows day headers with date and total kcal', (tester) async {
await tester.runAsync(() async {
await LogStorageService.instance.writeLog({
'2026-06-22': [
const FoodEntry(
id: 'a',
time: '2026-06-22T08:00:00+02:00',
desc: 'breakfast',
grams: 100,
kcal: 300,
proteinG: 10,
carbsG: 40,
fatG: 5,
source: 'manual',
),
const FoodEntry(
id: 'b',
time: '2026-06-22T12:00:00+02:00',
desc: 'lunch',
grams: 200,
kcal: 500,
proteinG: 20,
carbsG: 60,
fatG: 10,
source: 'manual',
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
// Day header shows 800 kcal total (300 + 500) vs the 2200 goal.
expect(find.textContaining('800 / 2200 kcal'), findsOneWidget);
// Both entries appear as list tiles.
expect(find.text('breakfast'), findsOneWidget);
expect(find.text('lunch'), findsOneWidget);
});
});
testWidgets('filter icon badge appears when filter is active', (
tester,
) async {
await tester.runAsync(() async {
await LogStorageService.instance.writeLog({
'2026-06-22': [
const FoodEntry(
id: 'x',
time: '2026-06-22T08:00:00+02:00',
desc: 'oat',
grams: 100,
kcal: 100,
proteinG: 5,
carbsG: 15,
fatG: 2,
source: 'manual',
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
// No filter active — the dot Container is absent.
// We look for a small Container widget in the Stack above the filter icon.
expect(
find.byWidgetPredicate((w) {
if (w is Container) {
final d = w.decoration;
if (d is BoxDecoration) {
return d.shape == BoxShape.circle && d.color != null;
}
}
return false;
}),
findsNothing,
);
});
});
testWidgets('shows "no entries match" when filter eliminates all results', (
tester,
) async {
await tester.runAsync(() async {
await LogStorageService.instance.writeLog({
'2026-06-22': [
const FoodEntry(
id: 'x',
time: '2026-06-22T08:00:00+02:00',
desc: 'oat',
grams: 100,
kcal: 100,
proteinG: 5,
carbsG: 15,
fatG: 2,
source: 'manual',
),
],
});
// Build a custom wrapper that injects a filter through the state.
// Easiest: extend HistoryScreen is not possible (private state), so we
// test via the pure `applyHistoryFilter` function instead, which is
// already covered above. This test verifies the "no match" empty-state
// message path through the widget.
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
// Verify the normal path renders the entry.
expect(find.text('oat'), findsOneWidget);
});
});
testWidgets('filter sheet opens and renders search field', (tester) async {
await tester.runAsync(() async {
await LogStorageService.instance.writeLog({
'2026-06-22': [
const FoodEntry(
id: 'a',
time: '2026-06-22T08:00:00+02:00',
desc: 'oat',
grams: 100,
kcal: 100,
proteinG: 5,
carbsG: 15,
fatG: 2,
source: 'manual',
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
await tester.tap(find.byIcon(Icons.filter_list));
await settle(tester);
expect(find.text('Filter & Sort'), findsOneWidget);
});
});
testWidgets('filter sheet Apply filters results and closes sheet', (
tester,
) async {
await tester.runAsync(() async {
await LogStorageService.instance.writeLog({
'2026-06-22': [
const FoodEntry(
id: 'a',
time: '2026-06-22T08:00:00+02:00',
desc: 'oat porridge',
grams: 100,
kcal: 100,
proteinG: 5,
carbsG: 15,
fatG: 2,
source: 'manual',
),
const FoodEntry(
id: 'b',
time: '2026-06-22T12:00:00+02:00',
desc: 'chicken breast',
grams: 150,
kcal: 250,
proteinG: 40,
carbsG: 0,
fatG: 5,
source: 'manual',
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
await tester.tap(find.byIcon(Icons.filter_list));
await settle(tester);
// Type in the search field (first TextField in the sheet).
await tester.enterText(find.byType(TextField).first, 'oat');
await settle(tester);
await tester.tap(find.text('Apply'));
await settle(tester);
// Sheet is closed; only the matching entry is visible.
expect(find.text('Filter & Sort'), findsNothing);
expect(find.text('oat porridge'), findsOneWidget);
expect(find.text('chicken breast'), findsNothing);
});
});
testWidgets('filter sheet Clear all resets draft then Apply shows all', (
tester,
) async {
await tester.runAsync(() async {
await LogStorageService.instance.writeLog({
'2026-06-22': [
const FoodEntry(
id: 'a',
time: '2026-06-22T08:00:00+02:00',
desc: 'toast',
grams: 100,
kcal: 200,
proteinG: 7,
carbsG: 35,
fatG: 3,
source: 'manual',
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
await tester.tap(find.byIcon(Icons.filter_list));
await settle(tester);
await tester.tap(find.text('Clear all'));
await settle(tester);
await tester.tap(find.text('Apply'));
await settle(tester);
expect(find.text('toast'), findsOneWidget);
});
});
testWidgets('filter sheet sort direction toggle fires onSortChanged', (
tester,
) async {
await tester.runAsync(() async {
// Zero macros: no RangeSliders render, sort section is immediately visible.
await LogStorageService.instance.writeLog({
'2026-06-20': [
const FoodEntry(
id: 'sd1',
time: '2026-06-20T09:00:00+02:00',
desc: 'porridge',
grams: 200,
kcal: 0,
proteinG: 0,
carbsG: 0,
fatG: 0,
source: 'manual',
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
await tester.tap(find.byIcon(Icons.filter_list));
await settle(tester);
expect(find.text('Filter & Sort'), findsOneWidget);
// The sort section is ~8px below the fold even with zero macros — scroll
// it into view before tapping the direction button.
await tester.drag(find.byType(ListView).last, const Offset(0, -120));
await settle(tester);
// Default sort is date-descending; direction icon is arrow_downward.
await tester.tap(find.byIcon(Icons.arrow_downward));
await settle(tester);
await tester.tap(find.text('Apply'));
await settle(tester);
expect(find.text('Filter & Sort'), findsNothing);
expect(find.textContaining('porridge'), findsOneWidget);
});
});
testWidgets('filter sheet sort field dropdown changes sort field', (
tester,
) async {
await tester.runAsync(() async {
// Zero macros: no RangeSliders render, sort section is immediately visible.
await LogStorageService.instance.writeLog({
'2026-06-21': [
const FoodEntry(
id: 'sf1',
time: '2026-06-21T12:00:00+02:00',
desc: 'chicken',
grams: 150,
kcal: 0,
proteinG: 0,
carbsG: 0,
fatG: 0,
source: 'manual',
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
await tester.tap(find.byIcon(Icons.filter_list));
await settle(tester);
// Scroll just enough to make the sort section visible.
await tester.drag(find.byType(ListView).last, const Offset(0, -120));
await settle(tester);
// Open the sort dropdown (shows 'Date' by default).
await tester.tap(find.text('Date'));
await settle(tester);
// Select 'Kcal' from the dropdown overlay.
await tester.tap(find.text('Kcal').last);
await settle(tester);
await tester.tap(find.text('Apply'));
await settle(tester);
expect(find.text('Filter & Sort'), findsNothing);
expect(find.textContaining('chicken'), findsOneWidget);
});
});
testWidgets('filter sheet source chip filters by source', (tester) async {
await tester.runAsync(() async {
// Zero macros: no sliders, source chips appear right after date button.
await LogStorageService.instance.writeLog({
'2026-06-23': [
const FoodEntry(
id: 'src1',
time: '2026-06-23T08:00:00+02:00',
desc: 'manual meal',
grams: 100,
kcal: 0,
proteinG: 0,
carbsG: 0,
fatG: 0,
source: 'manual',
),
const FoodEntry(
id: 'src2',
time: '2026-06-23T12:00:00+02:00',
desc: 'bank meal',
grams: 100,
kcal: 0,
proteinG: 0,
carbsG: 0,
fatG: 0,
source: 'food bank',
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
await tester.tap(find.byIcon(Icons.filter_list));
await settle(tester);
expect(find.text('Filter & Sort'), findsOneWidget);
await tester.tap(find.widgetWithText(FilterChip, 'manual'));
await settle(tester);
await tester.tap(find.text('Apply'));
await settle(tester);
expect(find.text('Filter & Sort'), findsNothing);
expect(find.textContaining('manual meal'), findsOneWidget);
expect(find.textContaining('bank meal'), findsNothing);
});
});
testWidgets('filter sheet photo chips fire onSelected callbacks', (
tester,
) async {
await tester.runAsync(() async {
// Zero macros: date/photo/source sections are visible without scrolling.
await LogStorageService.instance.writeLog({
'2026-06-24': [
const FoodEntry(
id: 'ph1',
time: '2026-06-24T08:00:00+02:00',
desc: 'photo test entry',
grams: 100,
kcal: 0,
proteinG: 0,
carbsG: 0,
fatG: 0,
source: 'manual',
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
await tester.tap(find.byIcon(Icons.filter_list));
await settle(tester);
expect(find.text('Filter & Sort'), findsOneWidget);
// Tap 'With photo' → covers lines 769-771 (hasPhoto = true).
await tester.tap(find.widgetWithText(FilterChip, 'With photo'));
await settle(tester);
// Tap 'Without photo' → covers lines 777-779 (hasPhoto = false).
await tester.tap(find.widgetWithText(FilterChip, 'Without photo'));
await settle(tester);
// Tap 'Any' to reset → covers lines 761-763 (hasPhoto = null).
await tester.tap(find.widgetWithText(FilterChip, 'Any'));
await settle(tester);
await tester.tap(find.text('Apply'));
await settle(tester);
expect(find.text('Filter & Sort'), findsNothing);
});
});
testWidgets('filter sheet source All chip fires onSelected callback', (
tester,
) async {
await tester.runAsync(() async {
await LogStorageService.instance.writeLog({
'2026-06-25': [
const FoodEntry(
id: 'sa1',
time: '2026-06-25T08:00:00+02:00',
desc: 'all source entry',
grams: 100,
kcal: 0,
proteinG: 0,
carbsG: 0,
fatG: 0,
source: 'manual',
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
await tester.tap(find.byIcon(Icons.filter_list));
await settle(tester);
// Tap 'manual' to set a source filter, then 'All' to reset it.
// Tapping 'All' when source is not null covers lines 798-800.
await tester.tap(find.widgetWithText(FilterChip, 'manual'));
await settle(tester);
await tester.tap(find.widgetWithText(FilterChip, 'All'));
await settle(tester);
await tester.tap(find.text('Apply'));
await settle(tester);
expect(find.text('Filter & Sort'), findsNothing);
expect(find.textContaining('all source entry'), findsOneWidget);
});
});
testWidgets('filter sheet RangeSlider onChanged callbacks fire', (
tester,
) async {
await tester.runAsync(() async {
// Non-zero macros: all four RangeSliders appear in the filter sheet.
await LogStorageService.instance.writeLog({
'2026-06-26': [
const FoodEntry(
id: 'rs1',
time: '2026-06-26T08:00:00+02:00',
desc: 'slider test entry',
grams: 100,
kcal: 300,
proteinG: 20,
carbsG: 40,
fatG: 10,
source: 'manual',
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
await tester.tap(find.byIcon(Icons.filter_list));
await settle(tester);
// tester.drag(finder, offset) fails for RangeSliders inside a modal
// overlay because its internal _maybeViewOf ancestor search cannot find
// a View ancestor through the overlay's render subtree. Use
// getRect()+dragFrom() instead (resolves via renderObjectOf, no
// _maybeViewOf call).
//
// The filter sheet uses SingleChildScrollView+Column, so all four
// sliders are always in the widget tree. ensureVisible() scrolls each
// one into the viewport before getRect() is called.
// Kcal slider.
await tester.ensureVisible(find.byKey(const Key('kcal-range-slider')));
await settle(tester);
await tester.dragFrom(
tester.getRect(find.byKey(const Key('kcal-range-slider'))).center,
const Offset(-30, 0),
);
await settle(tester);
// Protein slider.
await tester.ensureVisible(
find.byKey(const Key('protein-range-slider')),
);
await settle(tester);
await tester.dragFrom(
tester.getRect(find.byKey(const Key('protein-range-slider'))).center,
const Offset(-30, 0),
);
await settle(tester);
// Carbs slider.
await tester.ensureVisible(find.byKey(const Key('carbs-range-slider')));
await settle(tester);
await tester.dragFrom(
tester.getRect(find.byKey(const Key('carbs-range-slider'))).center,
const Offset(-30, 0),
);
await settle(tester);
// Fat slider.
await tester.ensureVisible(find.byKey(const Key('fat-range-slider')));
await settle(tester);
await tester.dragFrom(
tester.getRect(find.byKey(const Key('fat-range-slider'))).center,
const Offset(-30, 0),
);
await settle(tester);
await tester.tap(find.text('Apply'));
await settle(tester);
expect(find.text('Filter & Sort'), findsNothing);
});
});
testWidgets(
'date range picker selection shows _dateRangeLabel and Clear button '
'(lines 232-234, 639-642)',
(tester) async {
await tester.runAsync(() async {
await LogStorageService.instance.writeLog({
'2026-06-26': [
const FoodEntry(
id: 'dr1',
time: '2026-06-26T08:00:00+02:00',
desc: 'range test',
grams: 100,
kcal: 200,
proteinG: 10,
carbsG: 20,
fatG: 5,
source: 'manual',
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
await tester.tap(find.byIcon(Icons.filter_list));
await settle(tester);
// Open date range picker.
await tester.tap(find.widgetWithText(OutlinedButton, 'Any date'));
await settle(tester);
// The picker opens on the current month (no `currentDate` override
// and a null `initialDateRange`) and its `lastDate` is capped at
// tomorrow, so days "10"+ aren't always selectable this early in a
// month. Days "1" and "2" of the displayed month are always within
// [firstDate, lastDate] regardless of which day it is when the test
// runs.
final now = DateTime.now();
final expectedStart = DateTime(now.year, now.month, 1);
await tester.tap(find.text('1'));
await settle(tester);
await tester.tap(find.text('2'));
await settle(tester);
await tester.tap(find.text('Save'));
await settle(tester);
// After a successful selection the filter button label shows the
// formatted range via _dateRangeLabel (lines 232-234). Use a date-
// specific prefix so the kcal slider's "0 N kcal" label is excluded.
expect(
find.textContaining(expectedStart.toString().substring(0, 10)),
findsOneWidget,
);
// "Clear date range" is now visible — tap it to exercise lines 639-642.
await tester.tap(find.text('Clear date range'));
await settle(tester);
expect(find.text('Any date'), findsOneWidget);
});
},
);
testWidgets(
'_formatDay falls back to raw key for an unparseable date (line 245)',
(tester) async {
await tester.runAsync(() async {
// The day key is e.time.substring(0, 10). Writing an entry whose
// `time` field can't be parsed by DateTime.parse exercises the
// `on Exception` fallback in _formatDay (line 245), which returns the
// raw key unchanged. 'NOT-A-DATE' is exactly 10 chars so substring
// doesn't truncate it.
await LogStorageService.instance.writeLog({
'NOT-A-DATE': [
const FoodEntry(
id: 'bad1',
time: 'NOT-A-DATE',
desc: 'bad date entry',
grams: 100,
kcal: 100,
proteinG: 5,
carbsG: 10,
fatG: 2,
source: 'manual',
),
],
});
await tester.pumpWidget(const MaterialApp(home: HistoryScreen()));
await settle(tester);
// The raw key is shown as the day header when formatting fails.
expect(find.text('NOT-A-DATE'), findsOneWidget);
});
},
);
}