/// 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/theme/focus_flow_theme.dart'; import 'package:focus_flow_flutter/widgets/required_banner.dart'; import 'package:focus_flow_flutter/widgets/timeline/difficulty_bars.dart'; import 'package:focus_flow_flutter/widgets/timeline/task_timeline_card.dart'; import 'package:focus_flow_flutter/widgets/top_bar.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('top bar controls scale inside a compact header width', ( tester, ) async { await _pumpConstrainedWidget( tester, size: const Size(520, 72), child: const TopBar(dateLabel: 'June 30, 2026'), ); expect(tester.takeException(), isNull); expect(find.text('Compact'), findsOneWidget); expect(find.text('Normal'), findsOneWidget); expect(find.text('Settings'), findsOneWidget); expect(tester.getSize(find.byType(OutlinedButton)).width, lessThan(136)); }); testWidgets('required banner scales inside a compact header width', ( tester, ) async { await _pumpConstrainedWidget( tester, size: const Size(440, 72), child: const RequiredBanner( model: RequiredBannerModel(title: 'Pay bill', timeText: '6:00 PM'), ), ); expect(tester.takeException(), isNull); expect( find.textContaining('Next required task:', findRichText: true), findsOneWidget, ); expect(find.text('Show upcoming'), findsOneWidget); expect(tester.getSize(find.byType(OutlinedButton)).width, lessThan(176)); }); 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(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(find.byType(AnimatedOpacity)).opacity, 1, ); final button = tester.widget( 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 _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 _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 _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(); } Future _pumpConstrainedWidget( WidgetTester tester, { required Size size, required Widget child, }) async { await tester.binding.setSurfaceSize(Size(size.width + 40, size.height + 40)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget( MaterialApp( theme: FocusFlowTheme.dark(), home: Scaffold( body: Center( child: SizedBox(width: size.width, height: size.height, child: child), ), ), ), ); 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, ); }