focus-flow/apps/focus_flow_flutter/test/widget_test.dart

248 lines
8.2 KiB
Dart

/// Tests Widget behavior in the FocusFlow Flutter app.
library;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:scheduler_core/scheduler_core.dart';
import 'package:focus_flow_flutter/app/demo_scheduler_composition.dart';
import 'package:focus_flow_flutter/app/focus_flow_app.dart';
import 'package:focus_flow_flutter/models/today_screen_models.dart';
import 'package:focus_flow_flutter/widgets/timeline/difficulty_bars.dart';
import 'package:focus_flow_flutter/widgets/timeline/task_timeline_card.dart';
/// Runs FocusFlow widget and visual adapter tests.
void main() {
testWidgets('app shell renders compact Today frame', (tester) async {
await _pumpPlanApp(tester);
expect(find.text('FocusFlow'), findsOneWidget);
expect(find.text('Today'), findsWidgets);
expect(find.text('Backlog'), findsOneWidget);
expect(find.text('Projects'), findsOneWidget);
expect(find.text('Reports'), findsOneWidget);
expect(find.text('Settings'), findsWidgets);
expect(find.text('Compact'), findsOneWidget);
expect(find.text('Normal'), findsOneWidget);
expect(find.byKey(const ValueKey('nav-today-active')), findsOneWidget);
});
testWidgets('seeded Today renders backend-backed compact mockup data', (
tester,
) async {
await _pumpPlanApp(tester);
expect(find.text('May 20, 2025'), findsOneWidget);
expect(
find.textContaining('Next required task:', findRichText: true),
findsOneWidget,
);
expect(find.text('Clean coffee maker'), findsOneWidget);
expect(find.text('Cleaned kitchen counter'), findsNWidgets(2));
expect(find.text('Pay bill'), findsOneWidget);
expect(find.text('Doctor appointment'), findsOneWidget);
expect(find.text('Free Slot'), findsOneWidget);
expect(find.text('Intentional rest'), findsOneWidget);
});
test(
'visual token adapter maps seeded items to expected visual kinds',
() async {
final state = await _readSeededToday();
final data = TodayScreenData.fromTodayState(state);
expect(_kindFor(data, 'clean-coffee-maker'), TaskVisualKind.flexible);
expect(_kindFor(data, 'pay-bill'), TaskVisualKind.required);
expect(_kindFor(data, 'doctor-appointment'), TaskVisualKind.appointment);
expect(_kindFor(data, 'free-slot'), TaskVisualKind.freeSlot);
expect(
_kindFor(data, 'cleaned-kitchen-counter-early'),
TaskVisualKind.completedSurprise,
);
},
);
test('difficulty bars map every difficulty level to five-bar fill count', () {
expect(DifficultyBars.fillCountForLevel(DifficultyLevel.notSet), 0);
expect(DifficultyBars.fillCountForLevel(DifficultyLevel.veryEasy), 1);
expect(DifficultyBars.fillCountForLevel(DifficultyLevel.easy), 2);
expect(DifficultyBars.fillCountForLevel(DifficultyLevel.medium), 3);
expect(DifficultyBars.fillCountForLevel(DifficultyLevel.hard), 4);
expect(DifficultyBars.fillCountForLevel(DifficultyLevel.veryHard), 5);
});
testWidgets('task modal opens closes and keeps action buttons inert', (
tester,
) async {
await _pumpPlanApp(tester);
expect(find.byKey(const ValueKey('task-selection-modal')), findsNothing);
await tester.tap(find.byKey(const ValueKey('pay-bill')));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('task-selection-modal')), findsOneWidget);
expect(find.text('Pay bill'), findsNWidgets(2));
expect(find.textContaining('Required'), findsWidgets);
expect(find.text('Reward level 2'), findsOneWidget);
expect(find.text('Medium effort'), findsOneWidget);
expect(find.text('Done'), findsOneWidget);
expect(find.text('Push'), findsOneWidget);
expect(find.text('Move to Backlog'), findsOneWidget);
expect(find.text('Break up'), findsOneWidget);
await tester.tap(find.text('Done'));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('task-selection-modal')), findsOneWidget);
expect(find.text('Pay bill'), findsNWidgets(2));
await tester.tap(find.byKey(const ValueKey('close-task-modal')));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('task-selection-modal')), findsNothing);
await tester.tap(find.byKey(const ValueKey('pay-bill')));
await tester.pumpAndSettle();
await tester.tapAt(const Offset(1450, 140));
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('task-selection-modal')), findsNothing);
expect(find.text('Pay bill'), findsOneWidget);
});
testWidgets('task card controls scale inside a narrow task bubble', (
tester,
) async {
await _pumpTimelineCard(
tester,
card: _timelineCard(
id: 'narrow-actions',
title: 'A task title that would otherwise crowd the controls',
),
size: const Size(320, 88),
);
expect(tester.takeException(), isNull);
expect(
tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity)).opacity,
0,
);
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(
location: tester.getCenter(find.byKey(const ValueKey('narrow-actions'))),
);
await tester.pumpAndSettle();
expect(
tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity)).opacity,
1,
);
final button = tester.widget<IconButton>(
find.widgetWithIcon(IconButton, Icons.arrow_forward),
);
expect(button.iconSize, lessThan(18));
expect(button.constraints?.maxWidth, lessThan(32));
await gesture.removePointer();
});
testWidgets('free slot text scales inside a short task bubble', (
tester,
) async {
await _pumpTimelineCard(
tester,
card: _timelineCard(
id: 'short-free-slot',
title: 'Free Slot',
subtitle: 'Intentional rest',
timeText: '6:15 PM - 6:30 PM',
visualKind: TaskVisualKind.freeSlot,
showsQuickActions: false,
),
size: const Size(360, 88),
);
expect(tester.takeException(), isNull);
expect(find.text('Intentional rest'), findsOneWidget);
expect(find.text('6:15 PM - 6:30 PM'), findsOneWidget);
});
}
Future<void> _pumpPlanApp(WidgetTester tester) async {
await tester.binding.setSurfaceSize(const Size(1586, 992));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(FocusFlowApp(composition: _composition()));
await tester.pumpAndSettle();
}
DemoSchedulerComposition _composition() {
return DemoSchedulerComposition.seeded(selectedDate: CivilDate(2025, 5, 20));
}
Future<TodayState> _readSeededToday() async {
final composition = _composition();
final result = await composition.todayQuery.execute(
GetTodayStateRequest(
context: composition.context('test-read-today'),
date: composition.date,
),
);
return result.requireValue;
}
TaskVisualKind _kindFor(TodayScreenData data, String id) {
return data.cards.singleWhere((card) => card.id == id).visualKind;
}
Future<void> _pumpTimelineCard(
WidgetTester tester, {
required TimelineCardModel card,
required Size size,
}) async {
await tester.binding.setSurfaceSize(Size(size.width + 40, size.height + 40));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: size.width,
height: size.height,
child: TaskTimelineCard(card: card, onTap: () {}),
),
),
),
),
);
await tester.pumpAndSettle();
}
TimelineCardModel _timelineCard({
required String id,
required String title,
String subtitle = '30 min',
String timeText = '6:00 PM - 6:30 PM',
TaskVisualKind visualKind = TaskVisualKind.flexible,
bool showsQuickActions = true,
}) {
return TimelineCardModel(
id: id,
title: title,
typeLabel: 'Flexible',
subtitle: subtitle,
timeText: timeText,
startMinutes: 18 * 60,
endMinutes: 18 * 60 + 30,
visualKind: visualKind,
rewardIconToken: TimelineRewardIconToken.medium,
difficultyIconToken: TimelineDifficultyIconToken.medium,
isCompleted: false,
isSelectable: true,
showsQuickActions: showsQuickActions,
durationMinutes: 30,
);
}