Files
LehrerApp/LehrerApp.Desktop.Tests/WorkloadViewModelTests.cs
T
admin 52bb9c2c2f Arbeitszeit: Auswertung nach Kategorie/Gruppe + Pflichtstunden-Abgleich (Kapitel 6.3)
Dritter Tab "Auswertung": Zeitraum wahlweise Monat oder Schuljahr, Balken je Kategorie
und Gruppe. Neuer WorkloadSettingsService (gleiches JSON-Muster wie PeriodScheduleService)
für die Pflichtstundenzahl pro Woche, bewusst direkt im Auswertungs-Tab editierbar statt
in den Einstellungen, da das Feld nur dort gebraucht wird. Export (6.3.3) bleibt offen,
da er an das noch fehlende Kapitel 11 (Export-Infrastruktur) hängt.
2026-08-15 23:26:06 +02:00

449 lines
16 KiB
C#

using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Workload;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public sealed class WorkTaskListViewModelTests
{
[Fact]
public void Load_BlendetErledigteAufgabenStandardmaessigAus()
{
var tasks = new FakeWorkTasks();
tasks.Add(new WorkTask { Title = "Offen", Status = WorkTaskStatus.Open });
tasks.Add(new WorkTask { Title = "Erledigt", Status = WorkTaskStatus.Done });
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries());
Assert.Single(vm.Tasks);
Assert.Equal("Offen", vm.Tasks[0].Title);
}
[Fact]
public void StatusFilter_Alle_ZeigtAuchErledigteAufgaben()
{
var tasks = new FakeWorkTasks();
tasks.Add(new WorkTask { Title = "Erledigt", Status = WorkTaskStatus.Done });
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries())
{
StatusFilter = WorkTaskListViewModel.AllFilter,
};
Assert.Single(vm.Tasks);
}
[Fact]
public void CategoryFilter_FiltertNachKategorie()
{
var tasks = new FakeWorkTasks();
tasks.Add(new WorkTask { Title = "Korrektur", Category = TaskCategory.Correction });
tasks.Add(new WorkTask { Title = "Vorbereitung", Category = TaskCategory.Preparation });
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries())
{
CategoryFilter = TaskCategoryDisplay.Label(TaskCategory.Correction),
};
Assert.Single(vm.Tasks);
Assert.Equal("Korrektur", vm.Tasks[0].Title);
}
[Fact]
public void GroupFilter_OhneGruppe_ZeigtNurAufgabenOhneGroupId()
{
var group = new LearningGroup { Name = "8a" };
var tasks = new FakeWorkTasks();
tasks.Add(new WorkTask { Title = "Mit Gruppe", GroupId = group.Id });
tasks.Add(new WorkTask { Title = "Ohne Gruppe", GroupId = null });
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([group]), new FakeTimeEntries())
{
GroupFilter = WorkTaskListViewModel.NoGroupFilter,
};
Assert.Single(vm.Tasks);
Assert.Equal("Ohne Gruppe", vm.Tasks[0].Title);
}
[Fact]
public void CycleStatus_WechseltVonOffenNachInBearbeitung()
{
var tasks = new FakeWorkTasks();
tasks.Add(new WorkTask { Title = "T", Status = WorkTaskStatus.Open });
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries())
{
StatusFilter = WorkTaskListViewModel.AllFilter,
};
vm.CycleStatusCommand.Execute(vm.Tasks[0]);
Assert.Equal(WorkTaskStatus.InProgress, vm.Tasks[0].Model.Status);
}
[Fact]
public void CycleStatus_VonErledigtSpringtZurueckZuOffen()
{
var tasks = new FakeWorkTasks();
tasks.Add(new WorkTask { Title = "T", Status = WorkTaskStatus.Done });
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries())
{
StatusFilter = WorkTaskListViewModel.AllFilter,
};
vm.CycleStatusCommand.Execute(vm.Tasks[0]);
Assert.Equal(WorkTaskStatus.Open, vm.Tasks[0].Model.Status);
}
[Fact]
public void DeleteTask_EntferntAufgabeAusDerListe()
{
var tasks = new FakeWorkTasks();
tasks.Add(new WorkTask { Title = "T" });
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries());
vm.DeleteTaskCommand.Execute(vm.Tasks[0]);
Assert.Empty(vm.Tasks);
}
[Fact]
public async Task AddTask_RuftOnEditTaskAufUndSpeichertErgebnis()
{
var tasks = new FakeWorkTasks();
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries());
vm.OnEditTask = _ => Task.FromResult<WorkTask?>(new WorkTask { Title = "Neu" });
await vm.AddTaskCommand.ExecuteAsync(null);
Assert.Single(vm.Tasks);
Assert.Equal("Neu", vm.Tasks[0].Title);
}
[Fact]
public void Tasks_SindNachFaelligkeitSortiertUnbefristeteAmEnde()
{
var tasks = new FakeWorkTasks();
tasks.Add(new WorkTask { Title = "Ohne Frist", DueDate = null });
tasks.Add(new WorkTask { Title = "Spät fällig", DueDate = DateOnly.FromDateTime(DateTime.Today.AddDays(10)) });
tasks.Add(new WorkTask { Title = "Bald fällig", DueDate = DateOnly.FromDateTime(DateTime.Today.AddDays(1)) });
var vm = new WorkTaskListViewModel(tasks, new FakeGroups([]), new FakeTimeEntries());
Assert.Equal(["Bald fällig", "Spät fällig", "Ohne Frist"], vm.Tasks.Select(t => t.Title));
}
}
public sealed class AddEditWorkTaskDialogViewModelTests
{
[Fact]
public void Save_OhneTitel_SetztFehlerUndErzeugtKeinResult()
{
var vm = new AddEditWorkTaskDialogViewModel(null, []);
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.NotEmpty(vm.TitleError);
}
[Fact]
public void Save_MitGueltigenDaten_ErzeugtWorkTask()
{
var group = new LearningGroup { Name = "8a" };
var vm = new AddEditWorkTaskDialogViewModel(null, [group])
{
Title = "Klausur korrigieren",
SelectedCategory = TaskCategoryDisplay.Label(TaskCategory.Correction),
SelectedGroup = group,
DueDateText = "24.12.2026",
EstimatedMinutesText = "90",
};
vm.SaveCommand.Execute(null);
Assert.NotNull(vm.Result);
Assert.Equal("Klausur korrigieren", vm.Result!.Title);
Assert.Equal(TaskCategory.Correction, vm.Result.Category);
Assert.Equal(group.Id, vm.Result.GroupId);
Assert.Equal(new DateOnly(2026, 12, 24), vm.Result.DueDate);
Assert.Equal(90, vm.Result.EstimatedMinutes);
}
[Fact]
public void Save_MitUngueltigemDatum_SetztFehler()
{
var vm = new AddEditWorkTaskDialogViewModel(null, [])
{
Title = "T",
DueDateText = "nicht-ein-datum",
};
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.NotEmpty(vm.DueDateError);
}
[Fact]
public void Save_BestehendeAufgabe_BehaeltIdUndStatus()
{
var source = new WorkTask { Title = "Alt", Status = WorkTaskStatus.InProgress };
var vm = new AddEditWorkTaskDialogViewModel(source, []) { Title = "Neu" };
vm.SaveCommand.Execute(null);
Assert.Equal(source.Id, vm.Result!.Id);
Assert.Equal(WorkTaskStatus.InProgress, vm.Result.Status);
}
}
public sealed class TimeTrackingViewModelTests
{
[Fact]
public void StartStopTimer_SpeichertZeiteintragMitMindestensEinerMinute()
{
var entries = new FakeTimeEntries();
var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks())
{
SelectedTimerCategory = TaskCategoryDisplay.Label(TaskCategory.Correction),
};
vm.StartTimerCommand.Execute(null);
Assert.True(vm.IsTimerRunning);
vm.StopTimerCommand.Execute(null);
Assert.False(vm.IsTimerRunning);
var saved = Assert.Single(entries.GetByDate(DateOnly.FromDateTime(DateTime.Today)));
Assert.True(saved.DurationMinutes >= 1);
Assert.Equal(TaskCategoryDisplay.Label(TaskCategory.Correction), saved.Category);
}
[Fact]
public void SelectedTimerTask_UebernimmtDessenKategorie()
{
var task = new WorkTask { Title = "T", Category = TaskCategory.Meeting };
var tasks = new FakeWorkTasks();
tasks.Add(task);
var vm = new TimeTrackingViewModel(new FakeTimeEntries(), tasks);
vm.SelectedTimerTask = task;
Assert.Equal(TaskCategoryDisplay.Label(TaskCategory.Meeting), vm.SelectedTimerCategory);
}
[Fact]
public void Load_ZeigtNurEintraegeDerAktuellenWoche()
{
var entries = new FakeTimeEntries();
entries.Add(new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today), DurationMinutes = 30 });
entries.Add(new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today).AddDays(-30), DurationMinutes = 45 });
var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks());
Assert.Single(vm.WeekEntries);
Assert.Equal(30, vm.WeekEntries[0].Model.DurationMinutes);
}
[Fact]
public void CategorySummaries_GruppiertUndSummiertProKategorie()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var entries = new FakeTimeEntries();
entries.Add(new TimeEntry { Category = "Korrektur", Date = today, DurationMinutes = 30 });
entries.Add(new TimeEntry { Category = "Korrektur", Date = today, DurationMinutes = 20 });
entries.Add(new TimeEntry { Category = "Verwaltung", Date = today, DurationMinutes = 10 });
var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks());
var korrektur = vm.CategorySummaries.Single(s => s.Category == "Korrektur");
Assert.Equal("50 min", korrektur.MinutesDisplay);
Assert.Equal(1.0, korrektur.BarFraction);
Assert.Equal("60 min diese Woche", vm.TotalWeekMinutesDisplay);
}
[Fact]
public void DeleteEntry_EntferntEintragAusDerWoche()
{
var entries = new FakeTimeEntries();
entries.Add(new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today), DurationMinutes = 30 });
var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks());
vm.DeleteEntryCommand.Execute(vm.WeekEntries[0]);
Assert.Empty(vm.WeekEntries);
}
[Fact]
public async Task AddEntry_RuftOnAddEntryAufUndSpeichertErgebnis()
{
var entries = new FakeTimeEntries();
var vm = new TimeTrackingViewModel(entries, new FakeWorkTasks());
vm.OnAddEntry = () => Task.FromResult<TimeEntry?>(
new TimeEntry { Category = "X", Date = DateOnly.FromDateTime(DateTime.Today), DurationMinutes = 15 });
await vm.AddEntryCommand.ExecuteAsync(null);
Assert.Single(vm.WeekEntries);
}
}
public sealed class AddTimeEntryDialogViewModelTests
{
[Fact]
public void Save_MitDauer_ErzeugtEintrag()
{
var vm = new AddTimeEntryDialogViewModel([])
{
DateText = "10.01.2026",
DurationText = "45",
};
vm.SaveCommand.Execute(null);
Assert.NotNull(vm.Result);
Assert.Equal(45, vm.Result!.DurationMinutes);
Assert.Equal(new DateOnly(2026, 1, 10), vm.Result.Date);
}
[Fact]
public void Save_MitVonBis_BerechnetDauer()
{
var vm = new AddTimeEntryDialogViewModel([])
{
DateText = "10.01.2026",
StartTimeText = "14:00",
EndTimeText = "14:30",
};
vm.SaveCommand.Execute(null);
Assert.NotNull(vm.Result);
Assert.Equal(30, vm.Result!.DurationMinutes);
}
[Fact]
public void Save_OhneDauerUndOhneZeiten_SetztFehler()
{
var vm = new AddTimeEntryDialogViewModel([]) { DateText = "10.01.2026" };
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.NotEmpty(vm.TimeError);
}
[Fact]
public void Save_EndeVorBeginn_SetztFehler()
{
var vm = new AddTimeEntryDialogViewModel([])
{
DateText = "10.01.2026",
StartTimeText = "14:30",
EndTimeText = "14:00",
};
vm.SaveCommand.Execute(null);
Assert.Null(vm.Result);
Assert.NotEmpty(vm.TimeError);
}
}
public sealed class WorkloadEvaluationViewModelTests
{
private static WorkloadSettingsService BuildSettingsService()
{
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-workloadsettings-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
return new WorkloadSettingsService(tempPath);
}
[Fact]
public void MonatModus_ZeigtNurEintraegeDesGewaehltenMonats()
{
var entries = new FakeTimeEntries();
entries.Add(new TimeEntry { Category = "Korrektur", Date = new DateOnly(2026, 3, 10), DurationMinutes = 30 });
entries.Add(new TimeEntry { Category = "Korrektur", Date = new DateOnly(2026, 4, 1), DurationMinutes = 45 });
var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([]), BuildSettingsService(), new SchoolYearService())
{
SelectedYear = 2026,
};
vm.SelectedMonth = vm.MonthOptions[2]; // März
Assert.Equal("30 min (0.5 h)", vm.TotalMinutesDisplay);
}
[Fact]
public void CategorySummaries_GruppiertProKategorie()
{
var entries = new FakeTimeEntries();
entries.Add(new TimeEntry { Category = "Korrektur", Date = new DateOnly(2026, 3, 5), DurationMinutes = 30 });
entries.Add(new TimeEntry { Category = "Verwaltung", Date = new DateOnly(2026, 3, 6), DurationMinutes = 20 });
var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([]), BuildSettingsService(), new SchoolYearService())
{
SelectedYear = 2026,
};
vm.SelectedMonth = vm.MonthOptions[2];
Assert.Equal(2, vm.CategorySummaries.Count);
Assert.Contains(vm.CategorySummaries, s => s.Category == "Korrektur" && s.MinutesDisplay == "30 min");
}
[Fact]
public void GroupSummaries_ZeigtNurEintraegeMitGruppe()
{
var group = new LearningGroup { Name = "8a" };
var entries = new FakeTimeEntries();
entries.Add(new TimeEntry { Category = "X", GroupId = group.Id, Date = new DateOnly(2026, 3, 5), DurationMinutes = 30 });
entries.Add(new TimeEntry { Category = "X", GroupId = null, Date = new DateOnly(2026, 3, 6), DurationMinutes = 20 });
var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([group]), BuildSettingsService(), new SchoolYearService())
{
SelectedYear = 2026,
};
vm.SelectedMonth = vm.MonthOptions[2];
var summary = Assert.Single(vm.GroupSummaries);
Assert.Equal("8a", summary.GroupName);
Assert.Equal("30 min", summary.MinutesDisplay);
}
[Fact]
public void SchuljahrModus_UmfasstAugustBisJuli()
{
var entries = new FakeTimeEntries();
entries.Add(new TimeEntry { Category = "X", Date = new DateOnly(2025, 9, 1), DurationMinutes = 30 }); // im Schuljahr 2025/26
entries.Add(new TimeEntry { Category = "X", Date = new DateOnly(2026, 7, 30), DurationMinutes = 15 }); // im Schuljahr 2025/26
entries.Add(new TimeEntry { Category = "X", Date = new DateOnly(2026, 8, 5), DurationMinutes = 99 }); // schon Schuljahr 2026/27
var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([]), BuildSettingsService(), new SchoolYearService())
{
SelectedYear = 2025,
PeriodMode = WorkloadEvaluationViewModel.SchoolYearMode,
};
Assert.Equal("45 min (0.8 h)", vm.TotalMinutesDisplay);
}
[Fact]
public void SaveRequiredWeeklyHours_AktualisiertSollIstVergleich()
{
var entries = new FakeTimeEntries();
entries.Add(new TimeEntry { Category = "X", Date = new DateOnly(2026, 3, 2), DurationMinutes = 60 });
var vm = new WorkloadEvaluationViewModel(entries, new FakeGroups([]), BuildSettingsService(), new SchoolYearService())
{
SelectedYear = 2026,
RequiredWeeklyHoursText = "10",
};
vm.SelectedMonth = vm.MonthOptions[2];
vm.SaveRequiredWeeklyHoursCommand.Execute(null);
Assert.Contains("Soll:", vm.RequiredVsActualDisplay);
Assert.Contains("Ist: 1 h", vm.RequiredVsActualDisplay);
}
}