119 lines
4 KiB
C#
119 lines
4 KiB
C#
|
|
using ClosedXML.Excel;
|
||
|
|
using WorkTracker.Domain;
|
||
|
|
using WorkTracker.Services.Exports;
|
||
|
|
using Xunit;
|
||
|
|
|
||
|
|
namespace WorkTracker.Tests;
|
||
|
|
|
||
|
|
public sealed class MonthlyTimesheetExcelExporterTests
|
||
|
|
{
|
||
|
|
[Fact]
|
||
|
|
public void Export_ForApril2026EmptyTimesheet_MatchesExpectedWorkbook()
|
||
|
|
{
|
||
|
|
var exporter = new MonthlyTimesheetExcelExporter();
|
||
|
|
var timesheet = CreateTimesheet(new DateOnly(2026, 4, 1), new HashSet<DateOnly>
|
||
|
|
{
|
||
|
|
new(2026, 4, 6),
|
||
|
|
new(2026, 4, 25)
|
||
|
|
});
|
||
|
|
var templatePath = GetTemplatePath();
|
||
|
|
|
||
|
|
using var templateStream = File.OpenRead(templatePath);
|
||
|
|
var workbookBytes = exporter.Export(timesheet, templateStream);
|
||
|
|
|
||
|
|
WorkbookAssert.Equivalent(GetExpectedWorkbookPath(), workbookBytes);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Export_ForThirtyOneDayMonth_ShiftsTotalColumnAfterLastDay()
|
||
|
|
{
|
||
|
|
var exporter = new MonthlyTimesheetExcelExporter();
|
||
|
|
var timesheet = CreateTimesheet(new DateOnly(2026, 5, 1), new HashSet<DateOnly>());
|
||
|
|
var templatePath = GetTemplatePath();
|
||
|
|
|
||
|
|
using var templateStream = File.OpenRead(templatePath);
|
||
|
|
var workbookBytes = exporter.Export(timesheet, templateStream);
|
||
|
|
|
||
|
|
using var workbook = new XLWorkbook(new MemoryStream(workbookBytes));
|
||
|
|
var worksheet = workbook.Worksheet(1);
|
||
|
|
|
||
|
|
Assert.Equal(31d, worksheet.Cell("AG1").GetDouble());
|
||
|
|
Assert.Equal("SUM(C2:AG2)", worksheet.Cell("AH2").FormulaA1);
|
||
|
|
Assert.Equal("TOTALE", worksheet.Cell("AH1").GetString());
|
||
|
|
}
|
||
|
|
|
||
|
|
private static MonthlyTimesheetModel CreateTimesheet(DateOnly monthStart, ISet<DateOnly> holidays)
|
||
|
|
{
|
||
|
|
var lastDay = monthStart.AddMonths(1).AddDays(-1);
|
||
|
|
var days = new List<MonthlyTimesheetDayModel>();
|
||
|
|
for (var date = monthStart; date <= lastDay; date = date.AddDays(1))
|
||
|
|
{
|
||
|
|
days.Add(new MonthlyTimesheetDayModel
|
||
|
|
{
|
||
|
|
Date = date,
|
||
|
|
IsWeekend = date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday,
|
||
|
|
IsHoliday = holidays.Contains(date)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return new MonthlyTimesheetModel
|
||
|
|
{
|
||
|
|
Year = monthStart.Year,
|
||
|
|
Month = monthStart.Month,
|
||
|
|
Days = days,
|
||
|
|
Rows =
|
||
|
|
[
|
||
|
|
CreateRow("office", days.Count),
|
||
|
|
CreateRow("home", days.Count),
|
||
|
|
CreateRow("overtime", days.Count),
|
||
|
|
CreateRow("weekend", days.Count),
|
||
|
|
CreateRow("night", days.Count),
|
||
|
|
CreateRow("vacation", days.Count),
|
||
|
|
CreateRow("permit", days.Count),
|
||
|
|
CreateRow("compensatory-rest", days.Count),
|
||
|
|
CreateRow("sick", days.Count),
|
||
|
|
CreateRow("holiday", days.Count)
|
||
|
|
]
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
private static MonthlyTimesheetRowModel CreateRow(string key, int dayCount)
|
||
|
|
{
|
||
|
|
return new MonthlyTimesheetRowModel
|
||
|
|
{
|
||
|
|
Key = key,
|
||
|
|
DailyValues = Enumerable.Repeat<decimal?>(null, dayCount).ToList()
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
private static string GetExpectedWorkbookPath()
|
||
|
|
{
|
||
|
|
var repositoryRoot = FindRepositoryRoot();
|
||
|
|
var candidate = Path.Combine(repositoryRoot, "tests", "WorkTracker.Tests", "Expected", "monthly-timesheet-2026-04-empty.expected.xlsx");
|
||
|
|
return File.Exists(candidate)
|
||
|
|
? candidate
|
||
|
|
: GetTemplatePath();
|
||
|
|
}
|
||
|
|
|
||
|
|
private static string GetTemplatePath()
|
||
|
|
{
|
||
|
|
var repositoryRoot = FindRepositoryRoot();
|
||
|
|
return Path.Combine(repositoryRoot, "Templates", "monthly-timesheet-template.xlsx");
|
||
|
|
}
|
||
|
|
|
||
|
|
private static string FindRepositoryRoot()
|
||
|
|
{
|
||
|
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||
|
|
while (directory is not null)
|
||
|
|
{
|
||
|
|
if (File.Exists(Path.Combine(directory.FullName, "WorkTracker.sln")))
|
||
|
|
{
|
||
|
|
return directory.FullName;
|
||
|
|
}
|
||
|
|
|
||
|
|
directory = directory.Parent;
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new DirectoryNotFoundException("Unable to locate the WorkTracker repository root.");
|
||
|
|
}
|
||
|
|
}
|