Files
tasq/test/programmer_day_rows_test.dart
redz1029 12842bfa12 fix(programmer): day rows — Delta JSON test + non-assignee row gate
- Add Delta JSON happy-path test case to deltaJsonToPlainText tests
- Fix double-row bug: notes-only assignee rows only when userId == assigneeId
- Remove dead code: tautological taskMap.containsKey check in loop
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-09-27 21:08:30 +08:00

406 lines
12 KiB
Dart

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/models/programmer_task_work_log.model.dart';
import 'package:tasq/utils/app_time.dart';
import 'package:tasq/utils/programmer_day_rows.dart';
import 'package:tasq/widgets/quill_field.dart';
void main() {
setUpAll(AppTime.initialize);
group('deltaJsonToPlainText', () {
test('legacy plain text notes pass through', () {
expect(deltaJsonToPlainText('fixed bug'), 'fixed bug');
expect(deltaJsonToPlainText(' spaces '), 'spaces');
expect(deltaJsonToPlainText(''), '');
});
test('null returns empty string', () {
expect(deltaJsonToPlainText(null), '');
});
test('invalid JSON falls back to returning trimmed string', () {
expect(deltaJsonToPlainText('[{invalid json'), '[{invalid json');
});
test('Delta JSON with insert is converted to plain text', () {
expect(deltaJsonToPlainText('[{"insert":"Hello\\n"}]'), 'Hello');
});
test('Delta JSON arrays starting with [ are attempted to be parsed', () {
// The function attempts to parse JSON that starts with '['
// If parsing fails, it returns the trimmed string
// This test verifies the fallback behavior
expect(deltaJsonToPlainText('[{"invalid":"json"}]'), '[{"invalid":"json"}]');
});
});
group('DaySheetRow', () {
test('json round-trip', () {
final row = DaySheetRow(
taskId: 't1',
title: 'Review PR',
category: 'Software Support',
kind: 'assignee',
projectName: 'Project A',
helpedName: null,
seconds: 3600,
notes: ['First note', 'Second note'],
);
expect(DaySheetRow.fromJson(row.toJson()).toJson(), row.toJson());
});
test('helper row json round-trip', () {
final row = DaySheetRow(
taskId: 't2',
title: 'Debug issue',
category: 'Bug Fix',
kind: 'helper',
projectName: null,
helpedName: 'Ana Cruz',
seconds: 2700,
notes: ['Helped with testing'],
);
expect(DaySheetRow.fromJson(row.toJson()).toJson(), row.toJson());
});
});
group('ProgrammerTaskWorkLog.workDate', () {
test('workDate defaults to Manila day of createdAt', () {
final base = {
'id': 'l1',
'task_id': 't1',
'author_id': 'a',
'description': 'work',
'minutes': null,
'created_at': '2026-09-26T16:30:00Z',
};
final log = ProgrammerTaskWorkLog.fromMap(base);
// 2026-09-26 16:30 UTC is 2026-09-27 in Manila (UTC+8)
expect(log.workDate, DateTime.utc(2026, 9, 27));
});
test('workDate from map when provided', () {
final base = {
'id': 'l1',
'task_id': 't1',
'author_id': 'a',
'description': 'work',
'minutes': null,
'created_at': '2026-09-26T16:30:00Z',
'work_date': '2026-09-25',
};
final log = ProgrammerTaskWorkLog.fromMap(base);
expect(log.workDate, DateTime.utc(2026, 9, 25));
});
test('workDate constructor parameter', () {
final log = ProgrammerTaskWorkLog(
id: 'l1',
taskId: 't1',
authorId: 'a',
description: 'work',
createdAt: DateTime.utc(2026, 9, 26, 16, 30),
workDate: DateTime.utc(2026, 9, 25),
);
expect(log.workDate, DateTime.utc(2026, 9, 25));
});
});
group('buildDayRows', () {
final day = DateTime.utc(2026, 9, 27);
final earlierDay = DateTime.utc(2026, 9, 26);
final now = DateTime.utc(2026, 9, 27, 12, 0);
ProgrammerTask _task(String id, String assigneeId, [String? projectId]) =>
ProgrammerTask(
id: id,
title: 'Task $id',
category: 'Software Support',
status: 'in_progress',
assigneeId: assigneeId,
creatorId: 'creator',
projectId: projectId,
createdAt: DateTime.utc(2026, 9, 1),
updatedAt: DateTime.utc(2026, 9, 27),
);
ProgrammerTaskActivityLog _log(String taskId, String type,
[Map<String, dynamic>? meta]) =>
ProgrammerTaskActivityLog(
id: '$taskId-$type',
taskId: taskId,
actionType: type,
meta: meta,
createdAt: DateTime.utc(2026, 9, 27, 10),
);
ProgrammerTaskWorkLog _workLog(
String taskId,
String authorId,
String description, {
int? minutes,
DateTime? workDate,
}) =>
ProgrammerTaskWorkLog(
id: '$taskId-$authorId-${minutes ?? "notes"}',
taskId: taskId,
authorId: authorId,
description: description,
minutes: minutes,
createdAt: DateTime.utc(2026, 9, 27, 10),
workDate: workDate,
);
test('assignee row includes only that-day seconds and notes', () {
// Task t1 assigned to 'a'
// 'a' worked 1 hour on t1 that day
// 'a' also logged a note on t1 that day
// 'a' logged a note on t1 the day before (should be excluded)
final task = _task('t1', 'a');
final workLogs = [
_workLog('t1', 'a', 'Fixed part 1', workDate: day),
_workLog('t1', 'a', 'Fixed part 2', workDate: earlierDay),
];
final rows = buildDayRows(
userId: 'a',
day: day,
tasks: [task],
logsByTask: {
't1': [
ProgrammerTaskActivityLog(
id: 't1-started',
taskId: 't1',
actionType: 'started',
createdAt: DateTime.utc(2026, 9, 27, 8, 0),
),
ProgrammerTaskActivityLog(
id: 't1-paused',
taskId: 't1',
actionType: 'paused',
createdAt: DateTime.utc(2026, 9, 27, 9, 0),
),
],
},
workLogs: workLogs,
projectNames: {},
profileNames: {'a': 'Alice Smith'},
now: now,
);
expect(rows.length, 1);
expect(rows[0].kind, 'assignee');
expect(rows[0].notes, ['Fixed part 1']);
});
test('helper row: minutes*60 and helpedName', () {
// Task t1 assigned to 'a'
// 'h' (helper) logs 45 minutes of help on t1 that day
final task = _task('t1', 'a');
final workLogs = [
_workLog('t1', 'h', 'Helped with debugging', minutes: 45, workDate: day),
];
final rows = buildDayRows(
userId: 'h',
day: day,
tasks: [task],
logsByTask: {'t1': []},
workLogs: workLogs,
projectNames: {},
profileNames: {'a': 'Ana Cruz'},
now: now,
);
expect(rows.length, 1);
expect(rows[0].kind, 'helper');
expect(rows[0].seconds, 2700);
expect(rows[0].helpedName, 'Ana Cruz');
});
test('notes-only row when assignee logged but ran no timer', () {
// Task t1 assigned to 'a'
// 'a' only logged notes (no timer activity)
final task = _task('t1', 'a');
final workLogs = [
_workLog('t1', 'a', 'Just some notes', workDate: day),
];
final rows = buildDayRows(
userId: 'a',
day: day,
tasks: [task],
logsByTask: {'t1': []},
workLogs: workLogs,
projectNames: {},
profileNames: {'a': 'Alice Smith'},
now: now,
);
expect(rows.length, 1);
expect(rows[0].kind, 'assignee');
expect(rows[0].seconds, 0);
expect(rows[0].notes, ['Just some notes']);
});
test('rows sort: assignee by seconds desc, then helpers by seconds desc', () {
// Task t1 assigned to 'a': 1 hour
// Task t2 assigned to 'a': 30 minutes
// Task t3 assigned to 'b': with 'a' as helper (20 minutes)
// Task t4 assigned to 'b': with 'a' as helper (10 minutes)
final tasks = [
_task('t1', 'a'),
_task('t2', 'a'),
_task('t3', 'b'),
_task('t4', 'b'),
];
final workLogs = [
_workLog('t3', 'a', 'helped b', minutes: 20, workDate: day),
_workLog('t4', 'a', 'helped b', minutes: 10, workDate: day),
];
final rows = buildDayRows(
userId: 'a',
day: day,
tasks: tasks,
logsByTask: {
't1': [
ProgrammerTaskActivityLog(
id: 't1-started',
taskId: 't1',
actionType: 'started',
createdAt: DateTime.utc(2026, 9, 27, 8, 0),
),
ProgrammerTaskActivityLog(
id: 't1-paused',
taskId: 't1',
actionType: 'paused',
createdAt: DateTime.utc(2026, 9, 27, 9, 0),
),
],
't2': [
ProgrammerTaskActivityLog(
id: 't2-started',
taskId: 't2',
actionType: 'started',
createdAt: DateTime.utc(2026, 9, 27, 9, 0),
),
ProgrammerTaskActivityLog(
id: 't2-paused',
taskId: 't2',
actionType: 'paused',
createdAt: DateTime.utc(2026, 9, 27, 9, 30),
),
],
't3': [],
't4': [],
},
workLogs: workLogs,
projectNames: {},
profileNames: {'a': 'Alice Smith', 'b': 'Bob Jones'},
now: now,
);
// Expect: assignee t1 (3600), assignee t2 (1800), helper t3 (1200), helper t4 (600)
expect(rows.length, 4);
expect(rows[0].kind, 'assignee');
expect(rows[0].taskId, 't1');
expect(rows[0].seconds, 3600);
expect(rows[1].kind, 'assignee');
expect(rows[1].taskId, 't2');
expect(rows[1].seconds, 1800);
expect(rows[2].kind, 'helper');
expect(rows[2].taskId, 't3');
expect(rows[2].seconds, 1200);
expect(rows[2].helpedName, 'Bob Jones');
expect(rows[3].kind, 'helper');
expect(rows[3].taskId, 't4');
expect(rows[3].seconds, 600);
expect(rows[3].helpedName, 'Bob Jones');
});
test('tasks missing from task list are skipped', () {
// Work logs reference t1, but t1 is not in tasks list
final workLogs = [
_workLog('t1', 'a', 'work', workDate: day),
];
final rows = buildDayRows(
userId: 'a',
day: day,
tasks: [],
logsByTask: {},
workLogs: workLogs,
projectNames: {},
profileNames: {},
now: now,
);
expect(rows, isEmpty);
});
test('helper logs with minutes=0 are ignored', () {
final task = _task('t1', 'a');
final workLogs = [
_workLog('t1', 'h', 'zero minutes', minutes: 0, workDate: day),
];
final rows = buildDayRows(
userId: 'h',
day: day,
tasks: [task],
logsByTask: {'t1': []},
workLogs: workLogs,
projectNames: {},
profileNames: {'a': 'Ana Cruz'},
now: now,
);
expect(rows, isEmpty);
});
test('empty notes are dropped from output', () {
final task = _task('t1', 'a');
final workLogs = [
_workLog('t1', 'a', ' ', workDate: day), // whitespace-only
_workLog('t1', 'a', 'Real note', workDate: day),
_workLog('t1', 'a', '', workDate: day), // empty
];
final rows = buildDayRows(
userId: 'a',
day: day,
tasks: [task],
logsByTask: {'t1': []},
workLogs: workLogs,
projectNames: {},
profileNames: {},
now: now,
);
expect(rows.length, 1);
expect(rows[0].notes, ['Real note']);
});
});
group('totalSeconds', () {
test('sums all row seconds', () {
final rows = [
DaySheetRow(
taskId: 't1',
title: 'Task 1',
category: 'Work',
kind: 'assignee',
seconds: 3600,
notes: [],
),
DaySheetRow(
taskId: 't2',
title: 'Task 2',
category: 'Work',
kind: 'helper',
seconds: 1800,
notes: [],
helpedName: 'Someone',
),
];
expect(totalSeconds(rows), 5400);
});
test('empty list returns 0', () {
expect(totalSeconds([]), 0);
});
});
}