627645056b
Separate feature for the `programmer` role to record dev work by category, distinct from the IT help-desk `tasks` feature. Modeled on the IT Service Requests pattern (Brick offline-first model -> Riverpod stream/controller -> list/detail screens); seed of a future Project Management workflow. - programmer_tasks + programmer_task_activity_logs (category enum, RLS, realtime) via supabase migration - single assignee, self-logged or lead-assigned; assign/reassign logged - pause-on-switch: starting a task while another runs prompts to pause the running one, keeping one active task per user for accurate time attribution - worked-duration math shared with help-desk tasks via a generic core in task_duration.dart (existing callers unchanged) - list distinguishes running vs paused with a per-task Resume shortcut - accessible dialog labels; route /programmer-tasks + nav gated to admin/programmer - unit/widget tests: controller lifecycle, duration math, pause-switch dialog Co-Authored-By: claude-flow <ruv@ruv.net>
86 lines
2.7 KiB
Dart
86 lines
2.7 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:tasq/utils/task_duration.dart';
|
|
|
|
DurationEvent _ev(DateTime at, String action) =>
|
|
(createdAt: at, actionType: action);
|
|
|
|
void main() {
|
|
final base = DateTime.utc(2026, 1, 1, 9, 0);
|
|
|
|
group('computeEffectiveDuration', () {
|
|
test('no pause events → full span from fallbackStartedAt to endAt', () {
|
|
final worked = computeEffectiveDuration(
|
|
fallbackStartedAt: base,
|
|
endAt: base.add(const Duration(hours: 2)),
|
|
events: const [],
|
|
);
|
|
expect(worked, const Duration(hours: 2));
|
|
});
|
|
|
|
test('resolves start from the "started" event over the fallback', () {
|
|
final started = base.add(const Duration(minutes: 30));
|
|
// events are newest-first (DESC)
|
|
final events = [_ev(started, 'started')];
|
|
final worked = computeEffectiveDuration(
|
|
fallbackStartedAt: base, // earlier fallback should be ignored
|
|
endAt: started.add(const Duration(hours: 1)),
|
|
events: events,
|
|
);
|
|
expect(worked, const Duration(hours: 1));
|
|
});
|
|
|
|
test('subtracts a completed paused interval', () {
|
|
final started = base;
|
|
final paused = base.add(const Duration(minutes: 20));
|
|
final resumed = base.add(const Duration(minutes: 50)); // 30m paused
|
|
final end = base.add(const Duration(hours: 2));
|
|
// DESC order
|
|
final events = [
|
|
_ev(resumed, 'resumed'),
|
|
_ev(paused, 'paused'),
|
|
_ev(started, 'started'),
|
|
];
|
|
final worked = computeEffectiveDuration(
|
|
fallbackStartedAt: started,
|
|
endAt: end,
|
|
events: events,
|
|
);
|
|
expect(worked, const Duration(hours: 2) - const Duration(minutes: 30));
|
|
});
|
|
|
|
test('counts an open pause interval still active at endAt', () {
|
|
final started = base;
|
|
final paused = base.add(const Duration(minutes: 40));
|
|
final end = base.add(const Duration(hours: 1)); // 20m still paused
|
|
final events = [
|
|
_ev(paused, 'paused'),
|
|
_ev(started, 'started'),
|
|
];
|
|
final worked = computeEffectiveDuration(
|
|
fallbackStartedAt: started,
|
|
endAt: end,
|
|
events: events,
|
|
);
|
|
expect(worked, const Duration(minutes: 40));
|
|
});
|
|
|
|
test('returns zero when there is no resolvable start', () {
|
|
final worked = computeEffectiveDuration(
|
|
fallbackStartedAt: null,
|
|
endAt: base,
|
|
events: const [],
|
|
);
|
|
expect(worked, Duration.zero);
|
|
});
|
|
|
|
test('returns zero when endAt is before start', () {
|
|
final worked = computeEffectiveDuration(
|
|
fallbackStartedAt: base,
|
|
endAt: base.subtract(const Duration(minutes: 5)),
|
|
events: const [],
|
|
);
|
|
expect(worked, Duration.zero);
|
|
});
|
|
});
|
|
}
|