Files
tasq/test/programmer_day_data_test.dart
2026-09-28 14:08:28 +08:00

360 lines
11 KiB
Dart

import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:tasq/models/programmer_task.model.dart';
import 'package:tasq/models/programmer_task_activity_log.model.dart';
import 'package:tasq/providers/programmer_day_data_provider.dart';
import 'package:tasq/utils/app_time.dart';
// ---------------------------------------------------------------------------
// Minimal fake Supabase client — only the surface fetchDayActivityData uses.
// ---------------------------------------------------------------------------
class _FakeClient {
_FakeClient({required this.tables});
final Map<String, List<Map<String, dynamic>>> tables;
_FakeQuery from(String table) => _FakeQuery(this, table);
}
class _FakeQuery implements Future<List<Map<String, dynamic>>> {
_FakeQuery(this._client, this._table);
final _FakeClient _client;
final String _table;
final List<MapEntry<String, dynamic>> _eqs = [];
String? _inField;
List<String>? _inValues;
String? _orderField;
bool _orderAsc = true;
String? _gteField;
dynamic _gteValue;
String? _ltField;
dynamic _ltValue;
List<Map<String, dynamic>> get _rows {
var rows = List<Map<String, dynamic>>.from(_client.tables[_table] ?? []);
for (final eq in _eqs) {
rows = rows.where((r) => r[eq.key] == eq.value).toList();
}
if (_inField != null && _inValues != null) {
rows = rows
.where((r) => _inValues!.contains(r[_inField]?.toString()))
.toList();
}
if (_gteField != null) {
rows = rows.where((r) {
final v = r[_gteField];
return v != null && v.toString().compareTo(_gteValue.toString()) >= 0;
}).toList();
}
if (_ltField != null) {
rows = rows.where((r) {
final v = r[_ltField];
return v != null && v.toString().compareTo(_ltValue.toString()) < 0;
}).toList();
}
if (_orderField != null) {
rows.sort((a, b) {
final av = '${a[_orderField]}';
final bv = '${b[_orderField]}';
return _orderAsc ? av.compareTo(bv) : bv.compareTo(av);
});
}
return rows;
}
Future<List<Map<String, dynamic>>> get _future async => _rows;
_FakeQuery select([String? _]) => this;
_FakeQuery eq(String field, dynamic value) {
_eqs.add(MapEntry(field, value));
return this;
}
_FakeQuery inFilter(String field, List<dynamic> values) {
_inField = field;
_inValues = values.map((v) => v.toString()).toList();
return this;
}
_FakeQuery gte(String field, dynamic value) {
_gteField = field;
_gteValue = value;
return this;
}
_FakeQuery lt(String field, dynamic value) {
_ltField = field;
_ltValue = value;
return this;
}
_FakeQuery order(String field, {bool ascending = true}) {
_orderField = field;
_orderAsc = ascending;
return this;
}
@override
Stream<List<Map<String, dynamic>>> asStream() => _future.asStream();
@override
Future<List<Map<String, dynamic>>> catchError(Function onError,
{bool Function(Object error)? test}) =>
_future.catchError(onError, test: test);
@override
Future<R> then<R>(
FutureOr<R> Function(List<Map<String, dynamic>>) onValue,
{Function? onError}) =>
_future.then(onValue, onError: onError);
@override
Future<List<Map<String, dynamic>>> timeout(Duration timeLimit,
{FutureOr<List<Map<String, dynamic>>> Function()? onTimeout}) =>
_future.timeout(timeLimit, onTimeout: onTimeout);
@override
Future<List<Map<String, dynamic>>> whenComplete(
FutureOr<void> Function() action) =>
_future.whenComplete(action);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
ProgrammerTask _task({
required String id,
required String status,
DateTime? startedAt,
DateTime? completedAt,
DateTime? cancelledAt,
}) {
return ProgrammerTask(
id: id,
title: 'Task $id',
category: ProgrammerTaskCategory.others,
status: status,
createdAt: DateTime.utc(2026, 9, 1),
updatedAt: DateTime.utc(2026, 9, 1),
startedAt: startedAt,
completedAt: completedAt,
cancelledAt: cancelledAt,
);
}
ProgrammerTaskActivityLog _log({
required String taskId,
required String actionType,
required DateTime createdAt,
}) {
return ProgrammerTaskActivityLog(
id: '${taskId}_${actionType}_${createdAt.millisecondsSinceEpoch}',
taskId: taskId,
actionType: actionType,
createdAt: createdAt,
);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
void main() {
setUpAll(AppTime.initialize);
group('computeRunStates', () {
test(
'queued->notStarted; latest paused->paused since pause time; '
'resumed->running; completed', () {
final startedAt = DateTime.utc(2026, 9, 27, 8, 0);
final pauseAt = DateTime.utc(2026, 9, 27, 10, 0);
final resumeAt = DateTime.utc(2026, 9, 27, 11, 0);
final tasks = [
_task(id: 'q', status: ProgrammerTaskStatus.queued),
_task(
id: 'p',
status: ProgrammerTaskStatus.inProgress,
startedAt: startedAt,
),
_task(
id: 'r',
status: ProgrammerTaskStatus.inProgress,
startedAt: DateTime.utc(2026, 9, 27, 9, 0),
),
_task(
id: 'c',
status: ProgrammerTaskStatus.completed,
startedAt: startedAt,
completedAt: DateTime.utc(2026, 9, 27, 12, 0),
),
];
final logsByTask = {
'p': [
_log(taskId: 'p', actionType: 'started', createdAt: startedAt),
_log(taskId: 'p', actionType: 'paused', createdAt: pauseAt),
],
'r': [
_log(taskId: 'r', actionType: 'started', createdAt: DateTime.utc(2026, 9, 27, 9, 0)),
_log(taskId: 'r', actionType: 'paused', createdAt: DateTime.utc(2026, 9, 27, 10, 30)),
_log(taskId: 'r', actionType: 'resumed', createdAt: resumeAt),
],
};
final states = computeRunStates(tasks, logsByTask);
// queued -> notStarted
expect(states['q']!.state, ProgrammerRunState.notStarted);
expect(states['q']!.since, isNull);
// latest action is paused -> paused since pause time
expect(states['p']!.state, ProgrammerRunState.paused);
expect(states['p']!.since, pauseAt);
// latest action is resumed -> running since resume time
expect(states['r']!.state, ProgrammerRunState.running);
expect(states['r']!.since, resumeAt);
// completed -> completed
expect(states['c']!.state, ProgrammerRunState.completed);
expect(states['c']!.since, isNull);
});
});
group('fetchDayActivityData', () {
// Target day: 2026-09-27 Manila.
// dayStart (Manila midnight) = 2026-09-26T16:00:00Z UTC.
// dayEnd = 2026-09-27T16:00:00Z UTC.
final day = DateTime.utc(2026, 9, 27);
final now = DateTime.utc(2026, 9, 27, 12, 0);
test('includes in-progress tasks and tasks completed that day', () async {
final tActive = _task(
id: 'active',
status: ProgrammerTaskStatus.inProgress,
startedAt: DateTime.utc(2026, 9, 27, 0, 0),
);
// Completed at 11:00 UTC on the 27th — still within the Manila day
final tDone = _task(
id: 'done',
status: ProgrammerTaskStatus.completed,
startedAt: DateTime.utc(2026, 9, 27, 0, 0),
completedAt: DateTime.utc(2026, 9, 27, 3, 0),
);
final client = _FakeClient(tables: {
'programmer_task_work_logs': [],
'programmer_task_activity_logs': [
{
'id': 'log-1',
'task_id': 'active',
'action_type': 'started',
'created_at': '2026-09-27T00:00:00Z',
},
],
});
final result = await fetchDayActivityData(
client,
day: day,
tasks: [tActive, tDone],
now: now,
);
expect(result.tasks.map((t) => t.id), containsAll(['active', 'done']));
});
test('excludes a task completed two days earlier', () async {
// Completed well before the Manila day boundary (2026-09-26T16:00Z).
final tOld = _task(
id: 'old',
status: ProgrammerTaskStatus.completed,
startedAt: DateTime.utc(2026, 9, 24, 8, 0),
completedAt: DateTime.utc(2026, 9, 25, 10, 0),
);
final client = _FakeClient(tables: {
'programmer_task_work_logs': [],
'programmer_task_activity_logs': [],
});
final result = await fetchDayActivityData(
client,
day: day,
tasks: [tOld],
now: now,
);
expect(result.tasks, isEmpty);
});
test('includes a task referenced only by a helper work log that day',
() async {
// Task was completed long before; not active on 2026-09-27 by dates alone.
final tHelper = _task(
id: 'helper-task',
status: ProgrammerTaskStatus.completed,
startedAt: DateTime.utc(2026, 9, 1, 8, 0),
completedAt: DateTime.utc(2026, 9, 1, 17, 0),
);
final client = _FakeClient(tables: {
'programmer_task_work_logs': [
{
'id': 'wl-1',
'task_id': 'helper-task',
'author_id': 'u2',
'description': 'helped out',
'minutes': 30,
'created_at': '2026-09-27T01:00:00Z',
'work_date': '2026-09-27',
},
],
'programmer_task_activity_logs': [],
});
final result = await fetchDayActivityData(
client,
day: day,
tasks: [tHelper],
now: now,
);
expect(result.tasks.map((t) => t.id), contains('helper-task'));
expect(result.workLogs, hasLength(1));
expect(result.workLogs.first.taskId, 'helper-task');
});
test('excludes work logs with a different work_date', () async {
final tTask = _task(
id: 'some-task',
status: ProgrammerTaskStatus.inProgress,
startedAt: DateTime.utc(2026, 9, 27, 0, 0),
);
final client = _FakeClient(tables: {
'programmer_task_work_logs': [
{
'id': 'wl-old',
'task_id': 'some-task',
'author_id': 'u1',
'description': 'yesterday',
'minutes': 60,
'created_at': '2026-09-26T10:00:00Z',
'work_date': '2026-09-26',
},
],
'programmer_task_activity_logs': [],
});
final result = await fetchDayActivityData(
client,
day: day,
tasks: [tTask],
now: now,
);
expect(result.workLogs, isEmpty);
});
});
}