Files
LehrerApp/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs
T
adminandClaude Sonnet 5 fd509fc921 feat: Komfort-Features für Unterrichtsplanung, Zeiterfassung und Dashboard
- Neue-Stunde-Dialog: Datum wird beim Anlegen anhand des Stundenplans
  und der letzten Stunde der Einheit vorbelegt statt auf "heute"
  (behebt eine falsch erkannte Doppelstunde, wenn "heute" nicht auf
  den passenden Wochentag fiel).
- Zeiterfassung: Button "Unterrichtszeit heute übernehmen" schlägt
  Start/Ende aus dem heutigen Stundenplan inkl. Puffer davor/danach vor.
- Dashboard: neue Kachel "Ungeplante Stunden" erinnert an Stunden ohne
  Thema für heute/morgen, mit Opt-out je Gruppe ("Benötigt
  Unterrichtsplanung"), Doppelstunden-Erkennung (keine doppelte Meldung
  für die zweite Periode) und Berücksichtigung von Stundenausfall.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 11:30:50 +02:00

357 lines
16 KiB
C#

using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels;
using Xunit;
namespace LehrerApp.Desktop.Tests;
/// Tests für 9.1 (heutige Stunden mit Uhrzeit und Raum im Dashboard) und 9.2 (Absprung von einer
/// Stunde). Deckt nur die neue Zeit-/Raum-Auflösung ab, nicht die übrigen, unveränderten
/// Dashboard-Bausteine (Kalender, offene Aufgaben, Fehlzeiten-Warnungen, ...).
public sealed class DashboardViewModelTests
{
private static PeriodScheduleService NewPeriodSchedule()
{
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-dashboardvm-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
return new PeriodScheduleService(tempPath);
}
private static DashboardSettingsService NewDashboardSettings()
{
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-dashboardsettings-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
return new DashboardSettingsService(tempPath);
}
private static SchoolCalendarSettingsService NewCalendarSettings()
{
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-schoolcalendar-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
return new SchoolCalendarSettingsService(tempPath);
}
private static DashboardViewModel BuildVm(LearningGroup group, Lesson lesson,
FakeTimetableSlots? slots = null, PeriodScheduleService? periodSchedule = null,
FakeExams? exams = null, FakeResults? results = null, FakeGrades? grades = null,
FakeReportGrades? reportGrades = null, FakeMemberships? memberships = null,
FakeWorkTasks? tasks = null, FakeStudents? students = null, FakeDocumentation? documentation = null,
DashboardSettingsService? dashboardSettings = null, FakeSchoolHolidays? schoolHolidays = null,
FakeLessons? lessons = null, FakeSubstitutionEntries? substitutions = null)
{
lessons ??= new FakeLessons();
lessons.Add(lesson);
return new DashboardViewModel(
new FakeGroups([group]), new FakeSubjects([]), lessons,
exams ?? new FakeExams([]), results ?? new FakeResults(), grades ?? new FakeGrades(),
reportGrades ?? new FakeReportGrades(), memberships ?? new FakeMemberships([]),
tasks ?? new FakeWorkTasks(), new FakeSessions([]), new FakeEntries(),
students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(),
slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(),
new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings(),
schoolHolidays ?? new FakeSchoolHolidays(), new PublicHolidayService(), NewCalendarSettings(),
substitutions ?? new FakeSubstitutionEntries());
}
[Fact]
public void TodaysLessons_LoestRaumUeberPassendenStundenplanSlotAuf()
{
var group = new LearningGroup { Name = "9c" };
var today = DateOnly.FromDateTime(DateTime.Today);
var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 3, Topic = "Redox" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 3, Room = "R204" });
var vm = BuildVm(group, lesson, slots: slots);
var item = Assert.Single(vm.TodaysLessons);
Assert.Equal("R204", item.Room);
Assert.True(item.HasRoom);
}
[Fact]
public void TodaysLessons_OhnePassendenSlot_HatKeinenRaum()
{
var group = new LearningGroup { Name = "9c" };
var today = DateOnly.FromDateTime(DateTime.Today);
var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 3, Topic = "Redox" };
var vm = BuildVm(group, lesson);
var item = Assert.Single(vm.TodaysLessons);
Assert.False(item.HasRoom);
Assert.Equal("", item.Room);
}
[Fact]
public void TodaysLessons_ZeitKommtAusLessonStartTimeWennGesetzt()
{
var group = new LearningGroup { Name = "9c" };
var today = DateOnly.FromDateTime(DateTime.Today);
var lesson = new Lesson
{
GroupId = group.Id, Date = today, LessonNumber = 3, Topic = "Redox",
StartTime = new TimeOnly(9, 15),
};
var periodSchedule = NewPeriodSchedule();
periodSchedule.SetPeriods([new PeriodTimeEntry { PeriodNumber = 3, Start = new TimeOnly(10, 0), End = new TimeOnly(10, 45) }]);
var vm = BuildVm(group, lesson, periodSchedule: periodSchedule);
Assert.Equal("09:15", Assert.Single(vm.TodaysLessons).TimeDisplay);
}
[Fact]
public void TodaysLessons_OhneStartTime_FaelltAufStundenrasterZurueck()
{
var group = new LearningGroup { Name = "9c" };
var today = DateOnly.FromDateTime(DateTime.Today);
var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 3, Topic = "Redox" };
var periodSchedule = NewPeriodSchedule();
periodSchedule.SetPeriods([new PeriodTimeEntry { PeriodNumber = 3, Start = new TimeOnly(10, 0), End = new TimeOnly(10, 45) }]);
var vm = BuildVm(group, lesson, periodSchedule: periodSchedule);
Assert.Equal("10:00", Assert.Single(vm.TodaysLessons).TimeDisplay);
}
[Fact]
public void OpenLesson_RuftNavigationMitGruppenIdAuf()
{
var group = new LearningGroup { Name = "9c" };
var today = DateOnly.FromDateTime(DateTime.Today);
var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 3, Topic = "Redox" };
var vm = BuildVm(group, lesson);
Guid? navigatedTo = null;
vm.OnNavigateToLesson = id => navigatedTo = id;
vm.OpenLessonCommand.Execute(vm.TodaysLessons[0]);
Assert.Equal(group.Id, navigatedTo);
}
[Fact]
public void UpcomingDates_BuendeltKlausurenFristenUndFoerderplanPruefungen()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
var exams = new FakeExams([
new Exam { GroupId = group.Id, Title = "Chemie-Test", Date = today.AddDays(5) },
]);
var tasks = new FakeWorkTasks();
tasks.Add(new WorkTask { Title = "Notenschluss", DueDate = today.AddDays(10) });
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
var documentation = new FakeDocumentation();
documentation.Add(new Documentation
{
StudentId = student.Id, Type = DocumentationType.SupportPlan, Title = "Leseförderung",
SupportData = new SupportData { Status = SupportStatus.Active, ReviewDate = today.AddDays(3) },
});
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, exams: exams,
tasks: tasks, students: new FakeStudents([student]), documentation: documentation);
Assert.Contains(vm.UpcomingDates, i => i.Kind == UpcomingDateKind.Exam && i.Title == "Chemie-Test");
Assert.Contains(vm.UpcomingDates, i => i.Kind == UpcomingDateKind.Deadline && i.Title == "Notenschluss");
Assert.Contains(vm.UpcomingDates, i => i.Kind == UpcomingDateKind.SupportPlan && i.StudentId == student.Id);
}
[Fact]
public void OpenCorrections_ZeigtBewertungsfortschrittJeKlausur()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
var exam = new Exam
{ GroupId = group.Id, Title = "Klausur 1", Date = today.AddDays(-2), Status = ExamStatus.Conducted };
var anna = new Student { FirstName = "Anna", LastName = "A" };
var ben = new Student { FirstName = "Ben", LastName = "B" };
var memberships = new FakeMemberships([
new GroupMembership { GroupId = group.Id, StudentId = anna.Id },
new GroupMembership { GroupId = group.Id, StudentId = ben.Id },
]);
var results = new FakeResults();
results.Add(new ExamResult { ExamId = exam.Id, StudentId = anna.Id, Grade = "2" });
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
exams: new FakeExams([exam]), results: results, memberships: memberships,
students: new FakeStudents([anna, ben]));
var correction = Assert.Single(vm.OpenCorrections);
Assert.Equal(1, correction.Completed);
Assert.Equal(2, correction.Total);
Assert.Equal(50, correction.Percent);
}
[Fact]
public void Alerts_ErkenntDeutlichenNotenabfall()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c", GradingSystem = GradingSystem.Grades1To6 };
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
var memberships = new FakeMemberships([
new GroupMembership { GroupId = group.Id, StudentId = student.Id },
]);
var grades = new FakeGrades();
grades.Add(new Grade { GroupId = group.Id, StudentId = student.Id, Date = today.AddDays(-20), Value = "2" });
grades.Add(new Grade { GroupId = group.Id, StudentId = student.Id, Date = today.AddDays(-15), Value = "2" });
grades.Add(new Grade { GroupId = group.Id, StudentId = student.Id, Date = today.AddDays(-10), Value = "4" });
grades.Add(new Grade { GroupId = group.Id, StudentId = student.Id, Date = today.AddDays(-5), Value = "4" });
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, grades: grades,
memberships: memberships, students: new FakeStudents([student]));
Assert.Contains(vm.Alerts, a => a.StudentId == student.Id && a.KindLabel == "Notenabfall");
}
[Fact]
public void Kalenderauswahl_ZeigtTermineDesGeklicktenTages()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
var examDate = today.AddDays(2);
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
exams: new FakeExams([new Exam { GroupId = group.Id, Title = "Test", Date = examDate }]));
var day = vm.CalendarDays.Single(d => d.Date == examDate);
vm.SelectCalendarDayCommand.Execute(day);
Assert.True(day.IsSelected);
Assert.Contains(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.Exam && e.Title == "Test");
}
[Fact]
public void DashboardKacheln_SichtbarkeitUndReihenfolgeWerdenGespeichert()
{
var settings = NewDashboardSettings();
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, dashboardSettings: settings);
vm.TodayCard.IsVisible = false;
vm.MoveCardDownCommand.Execute(vm.TasksCard);
var saved = settings.Load();
Assert.False(saved.Single(c => c.Key == "today").IsVisible);
Assert.True(saved.FindIndex(c => c.Key == "tasks") > 1);
}
// ── Ungeplante Stunden (Nutzer-Feedback) ──────────────────────────────────
[Fact]
public void UnplannedLessons_StundeplanSlotOhneLessonMitThema_ErscheintInDerListe()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
// "lesson" liegt bewusst weit in der Vergangenheit, damit sie nicht in den heute/morgen-
// Vorgriff fällt und den Test verfälscht.
var pastLesson = new Lesson { GroupId = group.Id, Date = today.AddYears(-1), LessonNumber = 1, Topic = "Alt" };
var vm = BuildVm(group, pastLesson, slots: slots);
var item = Assert.Single(vm.UnplannedLessons);
Assert.Equal(group.Id, item.GroupId);
Assert.Equal(1, item.PeriodNumber);
Assert.Equal("Heute", item.DateDisplay);
}
[Fact]
public void UnplannedLessons_LessonMitThemaVorhanden_ErscheintNicht()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 1, Topic = "Redoxreaktionen" };
var vm = BuildVm(group, lesson, slots: slots);
Assert.Empty(vm.UnplannedLessons);
}
[Fact]
public void UnplannedLessons_GruppeOhnePlanungsbedarf_WirdIgnoriert()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "Klassenrat", RequiresLessonPlanning = false };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
var pastLesson = new Lesson { GroupId = group.Id, Date = today.AddYears(-1), LessonNumber = 1, Topic = "Alt" };
var vm = BuildVm(group, pastLesson, slots: slots);
Assert.Empty(vm.UnplannedLessons);
}
[Fact]
public void UnplannedLessons_Feiertag_WirdUebersprungen()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
var pastLesson = new Lesson { GroupId = group.Id, Date = today.AddYears(-1), LessonNumber = 1, Topic = "Alt" };
var schoolHolidays = new FakeSchoolHolidays();
schoolHolidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = today, EndDate = today.AddDays(1) });
var vm = BuildVm(group, pastLesson, slots: slots, schoolHolidays: schoolHolidays);
Assert.Empty(vm.UnplannedLessons);
}
[Fact]
public void UnplannedLessons_DoppelstundeMitThemaAmErstenSlot_ZweiterSlotWirdNichtGemeldet()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 3 });
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 4 });
var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 3, Topic = "Redox" };
var vm = BuildVm(group, lesson, slots: slots);
Assert.Empty(vm.UnplannedLessons);
}
[Fact]
public void UnplannedLessons_DoppelstundeOhneThema_BeideSlotsWerdenGemeldet()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 3 });
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 4 });
var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 3, Topic = "" };
var vm = BuildVm(group, lesson, slots: slots);
Assert.Equal(2, vm.UnplannedLessons.Count);
Assert.Contains(vm.UnplannedLessons, i => i.PeriodNumber == 3);
Assert.Contains(vm.UnplannedLessons, i => i.PeriodNumber == 4);
}
[Fact]
public void UnplannedLessons_StundenausfallEingetragen_WirdNichtGemeldet()
{
var today = DateOnly.FromDateTime(DateTime.Today);
var group = new LearningGroup { Name = "9c" };
var slots = new FakeTimetableSlots();
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 5 });
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 6 });
var pastLesson = new Lesson { GroupId = group.Id, Date = today.AddYears(-1), LessonNumber = 5, Topic = "Alt" };
var substitutions = new FakeSubstitutionEntries();
substitutions.Add(new SubstitutionEntry { Date = today, Kind = SubstitutionKind.Cancelled, PeriodNumber = 5 });
substitutions.Add(new SubstitutionEntry { Date = today, Kind = SubstitutionKind.Cancelled, PeriodNumber = 6 });
var vm = BuildVm(group, pastLesson, slots: slots, substitutions: substitutions);
Assert.Empty(vm.UnplannedLessons);
}
}