import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:krow_worker_app/core/theme.dart'; import 'package:krow_worker_app/widgets/common.dart'; import 'package:krow_worker_app/widgets/glass_segments.dart'; import 'package:krow_worker_app/widgets/krow_nav_bar.dart'; import 'package:krow_worker_app/widgets/motion.dart'; import 'package:krow_worker_app/widgets/swipe_action.dart'; Widget wrap(Widget child) => MaterialApp( theme: buildKrowTheme(), home: Scaffold(body: child), ); void main() { group('KrowNavBar', () { const items = [ KrowNavItem(icon: Icons.home, label: 'Today'), KrowNavItem(icon: Icons.work, label: 'Shifts'), KrowNavItem(icon: Icons.timer, label: 'Clock', showDot: true), ]; testWidgets('renders every label', (tester) async { await tester.pumpWidget( wrap(KrowNavBar(items: items, currentIndex: 0, onSelect: (_) {})), ); expect(find.text('Today'), findsOneWidget); expect(find.text('Shifts'), findsOneWidget); expect(find.text('Clock'), findsOneWidget); }); testWidgets('reports the tapped index', (tester) async { var selected = -1; await tester.pumpWidget( wrap( KrowNavBar( items: items, currentIndex: 0, onSelect: (i) => selected = i, ), ), ); await tester.tap(find.text('Clock')); expect(selected, 2); }); testWidgets('announces a tab that needs attention', (tester) async { await tester.pumpWidget( wrap(KrowNavBar(items: items, currentIndex: 0, onSelect: (_) {})), ); // The dot is not readable, so the state has to reach the semantics tree. expect(find.bySemanticsLabel('Clock, needs attention'), findsOneWidget); expect(find.bySemanticsLabel('Today'), findsOneWidget); }); testWidgets('the indicator travels rather than jumping', (tester) async { var index = 0; await tester.pumpWidget( wrap( StatefulBuilder( builder: (context, setState) => KrowNavBar( items: items, currentIndex: index, onSelect: (i) => setState(() => index = i), ), ), ), ); Rect pillRect() => tester.getRect(find.byKey(KrowNavBar.indicatorKey)); final start = pillRect().center.dx; await tester.tap(find.text('Clock')); await tester.pump(); await tester.pump(const Duration(milliseconds: 120)); // Part way through it should be between the two tabs, not already there. final middle = pillRect().center.dx; expect(middle, greaterThan(start)); await tester.pumpAndSettle(); final end = pillRect().center.dx; expect(end, greaterThan(middle)); }); }); group('StatSlabRow', () { // Every real use sits in a scrolling column, where the height is // unbounded. Testing it against a bounded Scaffold body hides exactly the // layout errors this row is prone to. Widget inScrollable(Widget child) => wrap(ListView(children: [const SizedBox(height: 8), child])); testWidgets('lays out inside a scrollable', (tester) async { await tester.pumpWidget( inScrollable( const StatSlabRow( slabs: [ StatSlab('Clock in', '6:01 PM'), StatSlab('Clock out', '11:50 PM'), StatSlab('Hours', '5h 19m'), ], ), ), ); expect(tester.takeException(), isNull); // A row that tried to fill unbounded height would be enormous. expect(tester.getSize(find.byType(StatSlabRow)).height, lessThan(120)); }); testWidgets('keeps every card the same height, however long the text', ( tester, ) async { await tester.pumpWidget( inScrollable( const StatSlabRow( slabs: [ // The middle one is what used to wrap on both lines and push // its card taller than the two beside it. StatSlab('Clock in', '6:01 PM'), StatSlab('Clock out', '11:50 PM'), StatSlab('Hours', '5h 19m'), ], ), ), ); final heights = tester .widgetList(find.byType(StatSlab)) .map((slab) => tester.getSize(find.byWidget(slab)).height) .toSet(); expect(heights.length, 1, reason: 'cards should be one height'); }); testWidgets('scales long values down instead of wrapping', (tester) async { await tester.pumpWidget( inScrollable( const StatSlabRow( slabs: [ StatSlab('Paid', r'$1,234.56'), StatSlab('Rate', r'$21.00/hr'), StatSlab('Hours', '5h 19m'), ], ), ), ); for (final text in tester.widgetList(find.byType(Text))) { expect(text.maxLines, 1); } }); }); group('GlassSegments', () { testWidgets('reports the tapped segment', (tester) async { String? picked; await tester.pumpWidget( wrap( GlassSegments( segments: const [('a', 'Week'), ('b', 'Month'), ('c', 'Year')], value: 'a', onChanged: (v) => picked = v, ), ), ); await tester.tap(find.text('Year')); expect(picked, 'c'); }); testWidgets('ignores a tap on the segment already selected', ( tester, ) async { var calls = 0; await tester.pumpWidget( wrap( GlassSegments( segments: const [('a', 'Week'), ('b', 'Month')], value: 'a', onChanged: (_) => calls++, ), ), ); await tester.tap(find.text('Week')); expect(calls, 0); await tester.tap(find.text('Month')); expect(calls, 1); }); }); group('Entrance', () { testWidgets('settles opaque and in place', (tester) async { await tester.pumpWidget( wrap( Column( children: Entrance.stagger(const [Text('one'), Text('two')]), ), ), ); await tester.pumpAndSettle(); for (final o in tester.widgetList(find.byType(Opacity))) { expect(o.opacity, 1); } }); testWidgets('leaves no timer behind when disposed mid-flight', ( tester, ) async { await tester.pumpWidget( wrap(Column(children: Entrance.stagger(const [Text('one')]))), ); // Torn down before the entrance finishes. A delay implemented as a // stray timer survives this and fails the test at teardown. await tester.pump(const Duration(milliseconds: 20)); await tester.pumpWidget(wrap(const SizedBox())); await tester.pumpAndSettle(); }); }); group('SwipeAction', () { Widget swipe({ required VoidCallback onConfirm, bool enabled = true, }) => wrap( SizedBox( width: 320, child: SwipeAction( icon: Icons.login, label: 'Swipe to clock in', enabled: enabled, onConfirm: onConfirm, ), ), ); testWidgets('confirms once dragged across the track', (tester) async { var confirms = 0; await tester.pumpWidget(swipe(onConfirm: () => confirms++)); await tester.drag(find.byIcon(Icons.login), const Offset(320, 0)); await tester.pumpAndSettle(); expect(confirms, 1); }); testWidgets('snaps back when the drag stops short', (tester) async { var confirms = 0; await tester.pumpWidget(swipe(onConfirm: () => confirms++)); // Well under the commit fraction: a brush, not a deliberate swipe. await tester.drag(find.byIcon(Icons.login), const Offset(60, 0)); await tester.pumpAndSettle(); expect(confirms, 0); }); testWidgets('ignores the drag while disabled', (tester) async { var confirms = 0; await tester.pumpWidget( swipe(onConfirm: () => confirms++, enabled: false), ); await tester.drag(find.byIcon(Icons.login), const Offset(320, 0)); await tester.pumpAndSettle(); expect(confirms, 0); }); }); }