Files
tasq/test/accomplishment_report_data_test.dart
redz1029 87014d1656 fix(report): approverName uses only included days
Move reviewer tracking logic inside the non-empty rows guard in both
approved-only and include-unapproved modes. This ensures zero-row
approved sheets don't pollute the reviewer set, fixing the approverName
calculation to only consider reviewers from included ReportDays.

Add test: approved sheet with zero rows and different reviewer correctly
excluded from approver tracking.

All 9 tests passing.
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-09-28 18:47:18 +08:00

693 lines
20 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:tasq/models/programmer_day_sheet.model.dart';
import 'package:tasq/models/programmer_task.model.dart';
import 'package:tasq/models/profile.model.dart';
import 'package:tasq/providers/reports_provider.dart';
import 'package:tasq/screens/programmer_tasks/report/accomplishment_report_data.dart';
import 'package:tasq/utils/programmer_day_rows.dart';
void main() {
group('buildAccomplishmentReportData', () {
final programmer = Profile(
id: 'prog1',
role: 'programmer',
fullName: 'John Doe',
);
final admin = Profile(
id: 'admin1',
role: 'administrator',
fullName: 'Admin User',
);
final reviewer = Profile(
id: 'reviewer1',
role: 'reviewer',
fullName: 'Jane Reviewer',
);
test('approved-only uses snapshots and counts excluded days', () {
final day1 = DateTime.utc(2026, 9, 1);
final day2 = DateTime.utc(2026, 9, 2);
final day3 = DateTime.utc(2026, 9, 3);
final day4 = DateTime.utc(2026, 9, 4);
final row1 = DaySheetRow(
taskId: 't1',
title: 'Task 1',
category: 'Software Development',
kind: 'assignee',
seconds: 3600,
notes: [],
);
final row2 = DaySheetRow(
taskId: 't2',
title: 'Task 2',
category: 'Bug Fix',
kind: 'assignee',
seconds: 1800,
notes: [],
);
// 2 approved sheets
final sheets = [
ProgrammerDaySheet(
id: 'sheet1',
programmerId: 'prog1',
status: 'approved',
workDate: day1,
autoSubmitted: false,
resubmissions: 0,
reviewedBy: 'reviewer1',
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row1]),
createdAt: DateTime.utc(2026, 9, 1),
updatedAt: DateTime.utc(2026, 9, 1),
),
ProgrammerDaySheet(
id: 'sheet2',
programmerId: 'prog1',
status: 'approved',
workDate: day2,
autoSubmitted: false,
resubmissions: 0,
reviewedBy: 'reviewer1',
approvedSnapshot: DaySheetSnapshot(totalSeconds: 1800, rows: [row2]),
createdAt: DateTime.utc(2026, 9, 2),
updatedAt: DateTime.utc(2026, 9, 2),
),
// 1 pending
ProgrammerDaySheet(
id: 'sheet3',
programmerId: 'prog1',
status: 'pending',
workDate: day3,
autoSubmitted: false,
resubmissions: 0,
createdAt: DateTime.utc(2026, 9, 3),
updatedAt: DateTime.utc(2026, 9, 3),
),
// 1 disapproved
ProgrammerDaySheet(
id: 'sheet4',
programmerId: 'prog1',
status: 'disapproved',
workDate: day4,
autoSubmitted: false,
resubmissions: 1,
reviewedBy: 'reviewer1',
createdAt: DateTime.utc(2026, 9, 4),
updatedAt: DateTime.utc(2026, 9, 4),
),
];
final range = ReportDateRange(
start: DateTime.utc(2026, 9, 1),
end: DateTime.utc(2026, 9, 5),
label: 'Test Range',
);
final data = buildAccomplishmentReportData(
programmer: programmer,
range: range,
includeUnapproved: false,
sheets: sheets,
liveRowsByDay: {},
tasks: [],
profileNames: {'reviewer1': 'Jane Reviewer'},
);
expect(data.days.length, 2);
expect(data.excludedPending, 1);
expect(data.excludedDisapproved, 1);
expect(data.days[0].statusTag, null);
expect(data.days[1].statusTag, null);
expect(data.totalSeconds, 5400);
expect(data.daysReported, 2);
});
test('include unapproved tags days and uses live rows', () {
final day1 = DateTime.utc(2026, 9, 1);
final day2 = DateTime.utc(2026, 9, 2);
final day3 = DateTime.utc(2026, 9, 3);
final day4 = DateTime.utc(2026, 9, 4);
final approvedRow = DaySheetRow(
taskId: 't1',
title: 'Task 1',
category: 'Software Development',
kind: 'assignee',
seconds: 3600,
notes: [],
);
final pendingRow = DaySheetRow(
taskId: 't2',
title: 'Task 2',
category: 'Bug Fix',
kind: 'assignee',
seconds: 1800,
notes: [],
);
final disapprovedRow = DaySheetRow(
taskId: 't3',
title: 'Task 3',
category: 'Meeting',
kind: 'assignee',
seconds: 900,
notes: [],
);
final draftRow = DaySheetRow(
taskId: 't4',
title: 'Task 4',
category: 'Software Support',
kind: 'assignee',
seconds: 600,
notes: [],
);
final sheets = [
// approved
ProgrammerDaySheet(
id: 'sheet1',
programmerId: 'prog1',
status: 'approved',
workDate: day1,
autoSubmitted: false,
resubmissions: 0,
reviewedBy: 'reviewer1',
approvedSnapshot:
DaySheetSnapshot(totalSeconds: 3600, rows: [approvedRow]),
createdAt: DateTime.utc(2026, 9, 1),
updatedAt: DateTime.utc(2026, 9, 1),
),
// pending
ProgrammerDaySheet(
id: 'sheet2',
programmerId: 'prog1',
status: 'pending',
workDate: day2,
autoSubmitted: false,
resubmissions: 0,
createdAt: DateTime.utc(2026, 9, 2),
updatedAt: DateTime.utc(2026, 9, 2),
),
// disapproved
ProgrammerDaySheet(
id: 'sheet3',
programmerId: 'prog1',
status: 'disapproved',
workDate: day3,
autoSubmitted: false,
resubmissions: 1,
reviewedBy: 'reviewer1',
createdAt: DateTime.utc(2026, 9, 3),
updatedAt: DateTime.utc(2026, 9, 3),
),
// draft
ProgrammerDaySheet(
id: 'sheet4',
programmerId: 'prog1',
status: 'draft',
workDate: day4,
autoSubmitted: false,
resubmissions: 0,
createdAt: DateTime.utc(2026, 9, 4),
updatedAt: DateTime.utc(2026, 9, 4),
),
];
final range = ReportDateRange(
start: DateTime.utc(2026, 9, 1),
end: DateTime.utc(2026, 9, 5),
label: 'Test Range',
);
final liveRowsByDay = {
day2: [pendingRow],
day3: [disapprovedRow],
day4: [draftRow],
};
final data = buildAccomplishmentReportData(
programmer: programmer,
range: range,
includeUnapproved: true,
sheets: sheets,
liveRowsByDay: liveRowsByDay,
tasks: [],
profileNames: {'reviewer1': 'Jane Reviewer'},
);
expect(data.days.length, 4);
expect(data.excludedPending, 0);
expect(data.excludedDisapproved, 0);
expect(data.days[0].statusTag, null); // approved
expect(data.days[1].statusTag, 'Pending'); // pending
expect(data.days[2].statusTag, 'Disapproved'); // disapproved
expect(data.days[3].statusTag, 'Pending'); // draft
});
test('period end is inclusive', () {
final day1 = DateTime.utc(2026, 9, 1);
final row = DaySheetRow(
taskId: 't1',
title: 'Task 1',
category: 'Software Development',
kind: 'assignee',
seconds: 3600,
notes: [],
);
final sheets = [
ProgrammerDaySheet(
id: 'sheet1',
programmerId: 'prog1',
status: 'approved',
workDate: day1,
autoSubmitted: false,
resubmissions: 0,
reviewedBy: 'reviewer1',
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]),
createdAt: DateTime.utc(2026, 9, 1),
updatedAt: DateTime.utc(2026, 9, 1),
),
];
final range = ReportDateRange(
start: DateTime.utc(2026, 9, 1),
end: DateTime.utc(2026, 10, 1), // exclusive
label: 'Test Range',
);
final data = buildAccomplishmentReportData(
programmer: programmer,
range: range,
includeUnapproved: false,
sheets: sheets,
liveRowsByDay: {},
tasks: [],
profileNames: {'reviewer1': 'Jane Reviewer'},
);
expect(data.periodStart, DateTime.utc(2026, 9, 1));
expect(data.periodEndInclusive, DateTime.utc(2026, 9, 30));
});
test('approver name only when one reviewer approved every included day', () {
final day1 = DateTime.utc(2026, 9, 1);
final day2 = DateTime.utc(2026, 9, 2);
final day3 = DateTime.utc(2026, 9, 3);
final row = DaySheetRow(
taskId: 't1',
title: 'Task 1',
category: 'Software Development',
kind: 'assignee',
seconds: 3600,
notes: [],
);
// Test: same reviewer for all approved days
final sheets1 = [
ProgrammerDaySheet(
id: 'sheet1',
programmerId: 'prog1',
status: 'approved',
workDate: day1,
autoSubmitted: false,
resubmissions: 0,
reviewedBy: 'reviewer1',
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]),
createdAt: DateTime.utc(2026, 9, 1),
updatedAt: DateTime.utc(2026, 9, 1),
),
ProgrammerDaySheet(
id: 'sheet2',
programmerId: 'prog1',
status: 'approved',
workDate: day2,
autoSubmitted: false,
resubmissions: 0,
reviewedBy: 'reviewer1',
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]),
createdAt: DateTime.utc(2026, 9, 2),
updatedAt: DateTime.utc(2026, 9, 2),
),
];
final range = ReportDateRange(
start: DateTime.utc(2026, 9, 1),
end: DateTime.utc(2026, 9, 4),
label: 'Test Range',
);
final data1 = buildAccomplishmentReportData(
programmer: programmer,
range: range,
includeUnapproved: false,
sheets: sheets1,
liveRowsByDay: {},
tasks: [],
profileNames: {'reviewer1': 'Jane Reviewer'},
);
expect(data1.approverName, 'Jane Reviewer');
// Test: mixed reviewers
final sheets2 = [
ProgrammerDaySheet(
id: 'sheet1',
programmerId: 'prog1',
status: 'approved',
workDate: day1,
autoSubmitted: false,
resubmissions: 0,
reviewedBy: 'reviewer1',
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]),
createdAt: DateTime.utc(2026, 9, 1),
updatedAt: DateTime.utc(2026, 9, 1),
),
ProgrammerDaySheet(
id: 'sheet2',
programmerId: 'prog1',
status: 'approved',
workDate: day2,
autoSubmitted: false,
resubmissions: 0,
reviewedBy: 'reviewer2',
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]),
createdAt: DateTime.utc(2026, 9, 2),
updatedAt: DateTime.utc(2026, 9, 2),
),
];
final data2 = buildAccomplishmentReportData(
programmer: programmer,
range: range,
includeUnapproved: false,
sheets: sheets2,
liveRowsByDay: {},
tasks: [],
profileNames: {'reviewer1': 'Jane Reviewer', 'reviewer2': 'Bob Reviewer'},
);
expect(data2.approverName, null);
});
test('tasksCompleted counts tasks completed in range for the programmer', () {
final range = ReportDateRange(
start: DateTime.utc(2026, 9, 1),
end: DateTime.utc(2026, 9, 5),
label: 'Test Range',
);
final task1 = ProgrammerTask(
id: 't1',
title: 'Task 1',
category: 'Software Development',
status: 'completed',
assigneeId: 'prog1',
createdAt: DateTime.utc(2026, 9, 1),
completedAt: DateTime.utc(2026, 9, 3), // in range
updatedAt: DateTime.utc(2026, 9, 3),
);
final task2 = ProgrammerTask(
id: 't2',
title: 'Task 2',
category: 'Bug Fix',
status: 'completed',
assigneeId: 'prog1',
createdAt: DateTime.utc(2026, 9, 1),
completedAt: DateTime.utc(2026, 8, 31), // before range
updatedAt: DateTime.utc(2026, 8, 31),
);
final task3 = ProgrammerTask(
id: 't3',
title: 'Task 3',
category: 'Meeting',
status: 'completed',
assigneeId: 'prog2', // different assignee
createdAt: DateTime.utc(2026, 9, 1),
completedAt: DateTime.utc(2026, 9, 3),
updatedAt: DateTime.utc(2026, 9, 3),
);
final task4 = ProgrammerTask(
id: 't4',
title: 'Task 4',
category: 'Software Support',
status: 'in_progress', // not completed
assigneeId: 'prog1',
createdAt: DateTime.utc(2026, 9, 1),
updatedAt: DateTime.utc(2026, 9, 3),
);
final task5 = ProgrammerTask(
id: 't5',
title: 'Task 5',
category: 'Software Development',
status: 'completed',
assigneeId: 'prog1',
createdAt: DateTime.utc(2026, 9, 1),
completedAt: DateTime.utc(2026, 9, 5), // at end boundary (exclusive)
updatedAt: DateTime.utc(2026, 9, 5),
);
final data = buildAccomplishmentReportData(
programmer: programmer,
range: range,
includeUnapproved: false,
sheets: [],
liveRowsByDay: {},
tasks: [task1, task2, task3, task4, task5],
profileNames: {},
);
expect(data.tasksCompleted, 1); // only task1
});
test('empty period', () {
final range = ReportDateRange(
start: DateTime.utc(2026, 9, 1),
end: DateTime.utc(2026, 9, 2),
label: 'Test Range',
);
final data = buildAccomplishmentReportData(
programmer: programmer,
range: range,
includeUnapproved: false,
sheets: [],
liveRowsByDay: {},
tasks: [],
profileNames: {},
);
expect(data.days, isEmpty);
expect(data.daysReported, 0);
expect(data.totalSeconds, 0);
expect(data.excludedPending, 0);
expect(data.excludedDisapproved, 0);
});
test('positionLabel based on programmer role', () {
final range = ReportDateRange(
start: DateTime.utc(2026, 9, 1),
end: DateTime.utc(2026, 9, 2),
label: 'Test Range',
);
final dataProgrammer = buildAccomplishmentReportData(
programmer: programmer,
range: range,
includeUnapproved: false,
sheets: [],
liveRowsByDay: {},
tasks: [],
profileNames: {},
);
final dataAdmin = buildAccomplishmentReportData(
programmer: admin,
range: range,
includeUnapproved: false,
sheets: [],
liveRowsByDay: {},
tasks: [],
profileNames: {},
);
expect(dataProgrammer.positionLabel, 'Programmer');
expect(dataAdmin.positionLabel, 'Administrator');
});
test('helpSeconds and secondsByCategory calculations', () {
final day1 = DateTime.utc(2026, 9, 1);
final assigneeRow = DaySheetRow(
taskId: 't1',
title: 'Task 1',
category: 'Software Development',
kind: 'assignee',
seconds: 3600,
notes: [],
);
final helperRow = DaySheetRow(
taskId: 't2',
title: 'Task 2',
category: 'Bug Fix',
kind: 'helper',
seconds: 1800,
notes: [],
);
final anotherHelperRow = DaySheetRow(
taskId: 't3',
title: 'Task 3',
category: 'Software Development',
kind: 'helper',
seconds: 900,
notes: [],
);
final row = DaySheetRow(
taskId: 't4',
title: 'Task 4',
category: 'Meeting',
kind: 'assignee',
seconds: 600,
notes: [],
);
final sheets = [
ProgrammerDaySheet(
id: 'sheet1',
programmerId: 'prog1',
status: 'approved',
workDate: day1,
autoSubmitted: false,
resubmissions: 0,
reviewedBy: 'reviewer1',
approvedSnapshot: DaySheetSnapshot(
totalSeconds: 6900,
rows: [assigneeRow, helperRow, anotherHelperRow, row],
),
createdAt: DateTime.utc(2026, 9, 1),
updatedAt: DateTime.utc(2026, 9, 1),
),
];
final range = ReportDateRange(
start: DateTime.utc(2026, 9, 1),
end: DateTime.utc(2026, 9, 2),
label: 'Test Range',
);
final data = buildAccomplishmentReportData(
programmer: programmer,
range: range,
includeUnapproved: false,
sheets: sheets,
liveRowsByDay: {},
tasks: [],
profileNames: {'reviewer1': 'Jane Reviewer'},
);
expect(data.helpSeconds, 2700); // 1800 + 900
expect(data.secondsByCategory['Software Development'], 4500); // 3600 + 900
expect(data.secondsByCategory['Bug Fix'], 1800);
expect(data.secondsByCategory['Meeting'], 600);
expect(data.totalSeconds, 6900);
});
test('approved sheet with zero rows and different reviewer does not affect approverName',
() {
final day1 = DateTime.utc(2026, 9, 1);
final day2 = DateTime.utc(2026, 9, 2);
final day3 = DateTime.utc(2026, 9, 3);
final row = DaySheetRow(
taskId: 't1',
title: 'Task 1',
category: 'Software Development',
kind: 'assignee',
seconds: 3600,
notes: [],
);
// Two included approved sheets with same reviewer
// One zero-row approved sheet with different reviewer
final sheets = [
ProgrammerDaySheet(
id: 'sheet1',
programmerId: 'prog1',
status: 'approved',
workDate: day1,
autoSubmitted: false,
resubmissions: 0,
reviewedBy: 'reviewer1',
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]),
createdAt: DateTime.utc(2026, 9, 1),
updatedAt: DateTime.utc(2026, 9, 1),
),
// Zero-row approved sheet with different reviewer (should be ignored)
ProgrammerDaySheet(
id: 'sheet2',
programmerId: 'prog1',
status: 'approved',
workDate: day2,
autoSubmitted: false,
resubmissions: 0,
reviewedBy: 'reviewer2',
approvedSnapshot: DaySheetSnapshot(totalSeconds: 0, rows: []),
createdAt: DateTime.utc(2026, 9, 2),
updatedAt: DateTime.utc(2026, 9, 2),
),
ProgrammerDaySheet(
id: 'sheet3',
programmerId: 'prog1',
status: 'approved',
workDate: day3,
autoSubmitted: false,
resubmissions: 0,
reviewedBy: 'reviewer1',
approvedSnapshot: DaySheetSnapshot(totalSeconds: 3600, rows: [row]),
createdAt: DateTime.utc(2026, 9, 3),
updatedAt: DateTime.utc(2026, 9, 3),
),
];
final range = ReportDateRange(
start: DateTime.utc(2026, 9, 1),
end: DateTime.utc(2026, 9, 4),
label: 'Test Range',
);
final data = buildAccomplishmentReportData(
programmer: programmer,
range: range,
includeUnapproved: false,
sheets: sheets,
liveRowsByDay: {},
tasks: [],
profileNames: {
'reviewer1': 'Jane Reviewer',
'reviewer2': 'Bob Reviewer',
},
);
// Only 2 days included (day2 has zero rows)
expect(data.days.length, 2);
// Both included days were reviewed by reviewer1
expect(data.approverName, 'Jane Reviewer');
});
});
}