230 lines
7.7 KiB
Dart
230 lines
7.7 KiB
Dart
/// Tests Widget behavior in the FocusFlow Flutter app.
|
|
library;
|
|
|
|
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);
|
|
|
|
final button = tester.widget<IconButton>(
|
|
find.widgetWithIcon(IconButton, Icons.arrow_forward),
|
|
);
|
|
expect(button.iconSize, lessThan(18));
|
|
expect(button.constraints?.maxWidth, lessThan(32));
|
|
});
|
|
|
|
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,
|
|
);
|
|
}
|