import 'package:flutter_test/flutter_test.dart'; import 'package:krow_worker_app/core/formatters.dart'; import 'package:krow_worker_app/models/shift.dart'; import 'package:krow_worker_app/models/timecard.dart'; Shift buildShift({ ShiftStatus status = ShiftStatus.booked, DateTime? clockIn, DateTime? clockOut, int breakMinutes = 30, }) { final start = DateTime(2026, 8, 10, 17, 50); return Shift( id: 's1', role: 'Barista', business: 'Zoox', venue: 'Zoox - San Jose Hub', address: '848 E Gish Rd', start: start, end: start.add(const Duration(hours: 6)), rate: 21.00, status: status, breakMinutes: breakMinutes, clockIn: clockIn, clockOut: clockOut, ); } void main() { group('Shift pay', () { test('scheduled pay deducts the unpaid break', () { final shift = buildShift(); expect(shift.scheduledDuration, const Duration(hours: 5, minutes: 30)); expect(shift.scheduledPay, closeTo(115.50, 0.001)); }); test('worked hours replace the schedule once clocked out', () { final shift = buildShift( status: ShiftStatus.completed, clockIn: DateTime(2026, 8, 10, 18, 1), clockOut: DateTime(2026, 8, 10, 23, 50), ); // 5h49m on the clock, less the 30 minute break. expect(shift.workedDuration, const Duration(hours: 5, minutes: 19)); expect(shift.pay, closeTo(111.65, 0.01)); }); test('a clocked window shorter than the break never goes negative', () { final shift = buildShift( status: ShiftStatus.completed, clockIn: DateTime(2026, 8, 10, 18, 0), clockOut: DateTime(2026, 8, 10, 18, 5), ); expect(shift.workedDuration, Duration.zero); expect(shift.pay, 0); }); test('cancelled and no-show shifts generate no pay', () { expect(buildShift(status: ShiftStatus.cancelled).pay, 0); expect(buildShift(status: ShiftStatus.noShow).pay, 0); }); }); group('Timecard', () { Timecard card({required DateTime clockIn}) { final start = DateTime(2026, 8, 10, 17, 50); return Timecard( id: 'tc1', shiftId: 's1', role: 'Barista', business: 'Zoox', scheduledStart: start, scheduledEnd: start.add(const Duration(hours: 6)), clockIn: clockIn, clockOut: DateTime(2026, 8, 10, 23, 50), breakMinutes: 30, rate: 21.00, status: TimecardStatus.needsReview, submittedAt: DateTime(2026, 8, 11, 0, 2), ); } test('variance is positive when you clock in late', () { expect(card(clockIn: DateTime(2026, 8, 10, 18, 1)).varianceMinutes, 11); }); test('variance is negative when you clock in early', () { expect(card(clockIn: DateTime(2026, 8, 10, 17, 45)).varianceMinutes, -5); }); test('amount follows the clocked hours', () { expect( card(clockIn: DateTime(2026, 8, 10, 18, 1)).amount, closeTo(111.65, 0.01), ); }); }); group('Formatters', () { test('duration reads as hours and minutes', () { expect(Fmt.duration(const Duration(hours: 5, minutes: 19)), '5h 19m'); expect(Fmt.duration(const Duration(minutes: 45)), '45m'); }); test('stopwatch pads every field', () { expect( Fmt.stopwatch(const Duration(hours: 1, minutes: 2, seconds: 3)), '01:02:03', ); }); test('countdown switches unit as the shift approaches', () { final now = DateTime(2026, 8, 10, 12, 0); expect( Fmt.untilStart(now.add(const Duration(minutes: 41)), now: now), 'In 41 min', ); expect( Fmt.untilStart(now.add(const Duration(hours: 3)), now: now), 'In 3 hr', ); expect( Fmt.untilStart(now.subtract(const Duration(minutes: 5)), now: now), 'Started', ); }); }); }