diet-guard/app/test/widgets/today_progress_card_motion_test.dart
Krzysztof kuhy Rudnicki 97af520fdd
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Add log sound cue and progress card motion
- lightImpact + 160 ms confirm cue on a validated log (audioplayers,
  no audio focus); device-local opt-out switch in Settings, on by default
- post-log card: confirmation line (slot + the meal's kcal), entrance and
  kcal/streak count-up on motion tokens transcribed into ui/theme.dart;
  durations collapse to zero under disableAnimations
- test/flutter_test_config.dart mutes the cue suite-wide

Todo: 8e66c1b9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWVHMLzeojNP4AaPoEj
2026-11-02 21:12:46 +01:00

154 lines
5.1 KiB
Dart

import 'package:diet_guard_app/widgets/today_progress_card.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
TodayProgress _progress({
double consumedKcal = 1450,
double? previousKcal,
int adherenceStreak = 3,
int? previousAdherenceStreak,
int? loggedSlotMinute,
double? loggedKcal,
}) => TodayProgress(
consumedKcal: consumedKcal,
budgetKcal: 2200,
proteinG: 90,
carbsG: 120,
fatG: 40,
adherenceStreak: adherenceStreak,
previousKcal: previousKcal,
previousAdherenceStreak: previousAdherenceStreak,
loggedSlotMinute: loggedSlotMinute,
loggedKcal: loggedKcal,
);
Future<void> _pump(
WidgetTester tester,
TodayProgress progress, {
bool reduced = false,
}) => tester.pumpWidget(
MediaQuery(
data: MediaQueryData(disableAnimations: reduced),
child: MaterialApp(
home: Scaffold(body: TodayProgressCard(progress: progress)),
),
),
);
double _entranceOpacity(WidgetTester tester) => tester
.widget<Opacity>(
find
.descendant(
of: find.byType(TodayProgressCard),
matching: find.byType(Opacity),
)
.first,
)
.opacity;
void main() {
group('confirmation line', () {
testWidgets('prints whole kcal without decimals', (tester) async {
await _pump(tester, _progress(loggedSlotMinute: 720, loggedKcal: 450));
expect(find.text('✓ 12:00 · 450 kcal'), findsOneWidget);
expect(find.textContaining('Logged'), findsNothing);
});
testWidgets('keeps fractional kcal exact', (tester) async {
await _pump(tester, _progress(loggedSlotMinute: 495, loggedKcal: 450.5));
expect(find.text('✓ 08:15 · 450.5 kcal'), findsOneWidget);
});
testWidgets('is absent when the logged fields are null', (tester) async {
await _pump(tester, _progress());
expect(find.textContaining('✓'), findsNothing);
await _pump(tester, _progress(loggedSlotMinute: 720));
expect(find.textContaining('✓'), findsNothing);
await _pump(tester, _progress(loggedKcal: 450));
expect(find.textContaining('✓'), findsNothing);
});
});
group('count-up', () {
testWidgets('runs from previousKcal to consumedKcal', (tester) async {
await _pump(tester, _progress(previousKcal: 1000));
expect(find.text('1000 / 2200'), findsOneWidget);
expect(find.text('1200 left'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 160));
final mid = tester.widget<Text>(find.textContaining(' / 2200')).data!;
final value = int.parse(mid.split(' / ').first);
expect(value, greaterThan(1000));
expect(value, lessThan(1450));
await tester.pumpAndSettle();
expect(find.text('1450 / 2200'), findsOneWidget);
expect(find.text('750 left'), findsOneWidget);
});
testWidgets('is static without previousKcal', (tester) async {
await _pump(tester, _progress());
expect(find.text('1450 / 2200'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 160));
expect(find.text('1450 / 2200'), findsOneWidget);
});
testWidgets('over-budget label and colour follow the animation', (
tester,
) async {
await _pump(tester, _progress(consumedKcal: 2500, previousKcal: 2000));
expect(find.text('200 left'), findsOneWidget);
await tester.pumpAndSettle();
expect(find.text('2500 / 2200'), findsOneWidget);
expect(find.text('300 over'), findsOneWidget);
});
testWidgets('streak counts up when it changed', (tester) async {
await _pump(tester, _progress(previousAdherenceStreak: 2));
expect(find.text('Adherence streak: 2 days'), findsOneWidget);
await tester.pumpAndSettle();
expect(find.text('Adherence streak: 3 days'), findsOneWidget);
});
testWidgets('streak is static when unchanged', (tester) async {
await _pump(tester, _progress(previousAdherenceStreak: 3));
expect(find.text('Adherence streak: 3 days'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 160));
expect(find.text('Adherence streak: 3 days'), findsOneWidget);
});
});
group('reduced motion', () {
testWidgets('first frame shows the final values at full opacity', (
tester,
) async {
await _pump(
tester,
_progress(previousKcal: 1000, previousAdherenceStreak: 2),
reduced: true,
);
expect(find.text('1450 / 2200'), findsOneWidget);
expect(find.text('750 left'), findsOneWidget);
expect(find.text('Adherence streak: 3 days'), findsOneWidget);
expect(_entranceOpacity(tester), 1);
});
});
group('entrance', () {
testWidgets('starts transparent and ends opaque', (tester) async {
await _pump(tester, _progress());
expect(_entranceOpacity(tester), lessThan(1));
await tester.pumpAndSettle();
expect(_entranceOpacity(tester), 1);
});
testWidgets('replays for a new TodayProgress instance', (tester) async {
await _pump(tester, _progress());
await tester.pumpAndSettle();
await _pump(tester, _progress());
expect(_entranceOpacity(tester), lessThan(1));
await tester.pumpAndSettle();
});
});
}