237 lines
7.1 KiB
Dart
237 lines
7.1 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:tasq/providers/tasks_provider.dart';
|
|
|
|
// Lightweight in-memory fake Supabase client used only for this test.
|
|
class _FakeClient {
|
|
final Map<String, List<Map<String, dynamic>>> tables = {
|
|
'tickets': [],
|
|
'tasks': [],
|
|
'teams': [],
|
|
'team_members': [],
|
|
'duty_schedules': [],
|
|
'task_assignments': [],
|
|
'task_activity_logs': [],
|
|
'notifications': [],
|
|
};
|
|
|
|
_FakeQuery from(String table) => _FakeQuery(this, table);
|
|
}
|
|
|
|
class _FakeQuery {
|
|
final _FakeClient client;
|
|
final String table;
|
|
Map<String, dynamic>? _eq;
|
|
List? _in;
|
|
Map<String, dynamic>? _insertPayload;
|
|
Map<String, dynamic>? _updatePayload;
|
|
|
|
_FakeQuery(this.client, this.table);
|
|
|
|
_FakeQuery select([String? _]) => this;
|
|
_FakeQuery eq(String field, dynamic value) {
|
|
_eq = {field: value};
|
|
return this;
|
|
}
|
|
|
|
_FakeQuery inFilter(String field, List values) {
|
|
_in = values;
|
|
return this;
|
|
}
|
|
|
|
Future<Map<String, dynamic>?> maybeSingle() async {
|
|
final rows = client.tables[table] ?? [];
|
|
if (_eq != null) {
|
|
final field = _eq!.keys.first;
|
|
final value = _eq![field];
|
|
Map<String, dynamic>? found;
|
|
for (final r in rows) {
|
|
if (r[field] == value) {
|
|
found = Map<String, dynamic>.from(r);
|
|
break;
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
return rows.isEmpty ? null : Map<String, dynamic>.from(rows.first);
|
|
}
|
|
|
|
Future<List<Map<String, dynamic>>> execute() async {
|
|
final rows = client.tables[table] ?? [];
|
|
if (_in != null) {
|
|
// return rows where the field (assume first) is in the list
|
|
return rows
|
|
.where((r) => _in!.contains(r.values.first))
|
|
.map((r) => Map<String, dynamic>.from(r))
|
|
.toList();
|
|
}
|
|
return rows.map((r) => Map<String, dynamic>.from(r)).toList();
|
|
}
|
|
|
|
_FakeQuery insert(Map<String, dynamic> payload) {
|
|
_insertPayload = Map<String, dynamic>.from(payload);
|
|
return this;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> single() async {
|
|
if (_insertPayload != null) {
|
|
// emulate DB-generated id
|
|
final id = 'tsk-${client.tables['tasks']!.length + 1}';
|
|
final row = Map<String, dynamic>.from(_insertPayload!);
|
|
row['id'] = id;
|
|
client.tables[table]!.add(row);
|
|
return Map<String, dynamic>.from(row);
|
|
}
|
|
if (_updatePayload != null && _eq != null) {
|
|
final field = _eq!.keys.first;
|
|
final value = _eq![field];
|
|
final idx = client.tables[table]!.indexWhere((r) => r[field] == value);
|
|
if (idx >= 0) {
|
|
client.tables[table]![idx] = {
|
|
...client.tables[table]![idx],
|
|
..._updatePayload!,
|
|
};
|
|
return Map<String, dynamic>.from(client.tables[table]![idx]);
|
|
}
|
|
}
|
|
throw Exception('single() called in unsupported context in fake');
|
|
}
|
|
|
|
Future<void> update(Map<String, dynamic> payload) async {
|
|
_updatePayload = payload;
|
|
if (_eq != null) {
|
|
final field = _eq!.keys.first;
|
|
final value = _eq![field];
|
|
final idx = client.tables[table]!.indexWhere((r) => r[field] == value);
|
|
if (idx >= 0) {
|
|
client.tables[table]![idx] = {
|
|
...client.tables[table]![idx],
|
|
...payload,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> delete() async {
|
|
if (_eq != null) {
|
|
final field = _eq!.keys.first;
|
|
final value = _eq![field];
|
|
client.tables[table]!.removeWhere((r) => r[field] == value);
|
|
}
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
test(
|
|
'ticket promotion creates task and auto-assigns late-comer',
|
|
() async {
|
|
final fake = _FakeClient();
|
|
|
|
// ticket row
|
|
fake.tables['tickets']!.add({
|
|
'id': 'TCK-1',
|
|
'subject': 'Printer down',
|
|
'description': 'Paper jam',
|
|
'office_id': 'office-1',
|
|
});
|
|
|
|
// team covering office-1
|
|
fake.tables['teams']!.add({
|
|
'id': 'team-1',
|
|
'office_ids': ['office-1'],
|
|
});
|
|
|
|
// two team members
|
|
fake.tables['team_members']!.add({'team_id': 'team-1', 'user_id': 'u1'});
|
|
fake.tables['team_members']!.add({'team_id': 'team-1', 'user_id': 'u2'});
|
|
|
|
// both checked in today; u2 arrived later -> should be chosen
|
|
fake.tables['duty_schedules']!.add({
|
|
'user_id': 'u1',
|
|
'check_in_at': DateTime(2026, 2, 18, 8, 0).toIso8601String(),
|
|
});
|
|
fake.tables['duty_schedules']!.add({
|
|
'user_id': 'u2',
|
|
'check_in_at': DateTime(2026, 2, 18, 9, 0).toIso8601String(),
|
|
});
|
|
|
|
// No existing tasks for ticket
|
|
|
|
// We'll reuse production controllers but point them at our fake client
|
|
// by creating minimal adapter wrappers. For this test we only need to
|
|
// exercise selection+assignment via TasksController.createTask.
|
|
|
|
// Create TicketsController-like flow manually (simulate promoted path):
|
|
// 1) create task row (simulate TasksController.createTask)
|
|
final taskPayload = {
|
|
'title': 'Printer down',
|
|
'description': 'Paper jam',
|
|
'office_id': 'office-1',
|
|
'ticket_id': 'TCK-1',
|
|
};
|
|
// emulate insert -> returns row with id
|
|
final insertedTask = await fake
|
|
.from('tasks')
|
|
.insert(taskPayload)
|
|
.single();
|
|
final taskId = insertedTask['id'] as String;
|
|
|
|
// 2) run simplified autoAssign logic (mirror of production selection)
|
|
// Gather team ids covering office
|
|
final teams = fake.tables['teams']!;
|
|
final teamIds = teams
|
|
.where((t) => (t['office_ids'] as List).contains('office-1'))
|
|
.map((t) => t['id'] as String)
|
|
.toList();
|
|
|
|
final memberRows = fake.tables['team_members']!
|
|
.where((m) => teamIds.contains(m['team_id']))
|
|
.toList();
|
|
final candidateIds = memberRows
|
|
.map((r) => r['user_id'] as String)
|
|
.toList();
|
|
|
|
// read duty_schedules for candidates
|
|
final onDuty = <String, DateTime>{};
|
|
for (final s in fake.tables['duty_schedules']!) {
|
|
final uid = s['user_id'] as String;
|
|
if (!candidateIds.contains(uid)) continue;
|
|
final checkIn = DateTime.parse(s['check_in_at'] as String);
|
|
onDuty[uid] = checkIn;
|
|
}
|
|
|
|
// compute completedToday as 0 (no completed tasks in fake DB)
|
|
final candidates = onDuty.entries
|
|
.map(
|
|
(e) => AutoAssignCandidate(
|
|
userId: e.key,
|
|
checkInAt: e.value,
|
|
completedToday: 0,
|
|
),
|
|
)
|
|
.toList();
|
|
|
|
final chosen = chooseAutoAssignCandidate(candidates);
|
|
expect(chosen, equals('u2'), reason: 'Late-comer u2 should be chosen');
|
|
|
|
// Insert assignment row (what _autoAssignTask would do)
|
|
await fake.from('task_assignments').insert({
|
|
'task_id': taskId,
|
|
'user_id': chosen,
|
|
}).single();
|
|
|
|
// Assert task exists and assignment was created
|
|
final createdTask = fake.tables['tasks']!.firstWhere(
|
|
(t) => t['id'] == taskId,
|
|
);
|
|
expect(createdTask['ticket_id'], equals('TCK-1'));
|
|
|
|
final assignment = fake.tables['task_assignments']!.firstWhere(
|
|
(a) => a['task_id'] == taskId,
|
|
orElse: () => {},
|
|
);
|
|
expect(assignment['user_id'], equals('u2'));
|
|
},
|
|
timeout: Timeout(Duration(seconds: 2)),
|
|
);
|
|
}
|