129 lines
3.8 KiB
Dart
129 lines
3.8 KiB
Dart
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',
|
|
);
|
|
});
|
|
});
|
|
}
|