using LehrerApp.Core.Models; using LehrerApp.Core.Services; using LehrerApp.Desktop.ViewModels.Planning; using LehrerApp.Desktop.ViewModels.Settings; using Xunit; namespace LehrerApp.Desktop.Tests; public sealed class TimetableViewModelTests { private static TimetableViewModel BuildViewModel( FakeTimetableSlots slots, FakeGroups groups, FakeSchoolHolidays? holidays = null, FakeSubjects? subjects = null, FakeLessons? lessons = null, FakeExams? exams = null, SchoolCalendarSettingsService? calendarSettings = null, FakeSupervisionDuties? supervisionDuties = null, FakeSubstitutionEntries? substitutions = null) { // Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad nur bei Bedarf // (SetState), das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben. var tempPath = System.IO.Path.Combine( System.IO.Path.GetTempPath(), $"lehrerapp-timetablevm-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(tempPath); return new TimetableViewModel( slots, groups, subjects ?? new FakeSubjects([]), lessons ?? new FakeLessons(), exams ?? new FakeExams([]), holidays ?? new FakeSchoolHolidays(), calendarSettings ?? new SchoolCalendarSettingsService(tempPath), new PublicHolidayService(), new SchoolYearService(), supervisionDuties ?? new FakeSupervisionDuties(), substitutions ?? new FakeSubstitutionEntries()); } /// Nächstes Datum ab (inkl.) , das auf einen Wochentag Mo-Fr fällt — /// nur diese Tage haben eine Spalte im Raster. Für Badge-Tests reicht ein beliebiger /// Mo-Fr-Wochentag: die Badges hängen nur am Wochentag (nicht am konkreten Kalenderdatum), /// da `TimetableSlot` ein wiederkehrendes Muster ohne Datum ist. private static DateOnly NextGridWeekday(DateOnly from) { var d = from; while (d.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday) d = d.AddDays(1); return d; } /// Datum des angegebenen Wochentags in der laufenden Kalenderwoche — exakt wie /// `TimetableViewModel.BuildWeekOverview` es berechnet, für Tests der Wochenkachel-Inhalte /// (Thema, Klausur am Tag), die anders als die Badges an ein konkretes Datum gebunden sind. private static DateOnly DateInCurrentWeek(DayOfWeek weekday) { var today = DateOnly.FromDateTime(DateTime.Today); var monday = today.AddDays(-((int)today.DayOfWeek + 6) % 7); return monday.AddDays((int)weekday - (int)DayOfWeek.Monday); } [Fact] public void Load_BautEineZelleProWochentagUndStundePlusKopfzeilen() { var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([])); // 1 Ecke + 5 Wochentage + 10 Stunden × (1 Label + 5 Zellen) Assert.Equal(6 + 10 * 6, vm.Cells.Count); Assert.Equal(6 + 10 * 6, vm.WeekItems.Count); } [Fact] public void Load_ZeigtZugewieseneGruppeInDerPassendenZelle() { var weekday = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today)); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 1, Room = "R204" }); var vm = BuildViewModel(slots, new FakeGroups([group])); var cell = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 1); Assert.True(cell.IsAssigned); Assert.Equal("Q1 Chemie · R204", cell.Text); } [Fact] public void HoursWarnings_WeichtDieZugewieseneStundenzahlAb_ZeigtWarnung() { var group = new LearningGroup { Name = "Q1 Chemie", SchoolYear = new SchoolYearService().CurrentSchoolYear(), HoursPerWeek = 3 }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); var vm = BuildViewModel(slots, new FakeGroups([group])); Assert.Single(vm.HoursWarnings); Assert.Contains("1 von 3", vm.HoursWarnings[0].Text); } [Fact] public void HoursWarnings_PassendeStundenzahl_KeineWarnung() { var group = new LearningGroup { Name = "Q1 Chemie", SchoolYear = new SchoolYearService().CurrentSchoolYear(), HoursPerWeek = 1 }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); var vm = BuildViewModel(slots, new FakeGroups([group])); Assert.Empty(vm.HoursWarnings); } // ── "Heute"-Ansicht: Tagesliste (Nutzer-Feedback: nicht-editierende Standardansicht) ──── [Fact] public void Load_ZeigtHeutigeStundeMitGruppeUndRaum() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 3, Room = "R204" }); var vm = BuildViewModel(slots, new FakeGroups([group])); var item = Assert.Single(vm.TodayItems); Assert.Equal("Q1 Chemie", item.GroupName); Assert.Equal("R204", item.Room); Assert.Equal(3, item.PeriodNumber); } [Fact] public void Load_HeutigeStundeMitLektionUndKlausur_ZeigtBeides() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 }); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = group.Id, Date = today, Topic = "Redoxreaktionen" }); var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today, Title = "Klausur Nr. 2" }]); var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons, exams: exams); var item = Assert.Single(vm.TodayItems); Assert.Equal("Redoxreaktionen", item.LessonTopic); Assert.Equal("Klausur Nr. 2", item.ExamTitle); Assert.True(item.HasExam); } [Fact] public void Load_HeutigeStunde_VorherigeUnkontrollierteHausaufgabe_ZeigtBadge() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 }); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = group.Id, Date = today.AddDays(-3), Homework = "S. 42, Aufgabe 3" }); var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons); var item = Assert.Single(vm.TodayItems); Assert.True(item.HasUnhandledHomework); } [Fact] public void Load_HeutigeStunde_HausaufgabeKontrolliert_KeinBadge() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 }); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = group.Id, Date = today.AddDays(-3), Homework = "S. 42, Aufgabe 3", HomeworkChecked = true, }); var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons); var item = Assert.Single(vm.TodayItems); Assert.False(item.HasUnhandledHomework); } [Fact] public void Load_HeutigeStunde_HausaufgabenKontrolleBewusstAbgeschaltet_KeinBadge() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 }); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = group.Id, Date = today.AddDays(-3), Homework = "S. 42, Aufgabe 3", HomeworkCheckDismissed = true, }); var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons); var item = Assert.Single(vm.TodayItems); Assert.False(item.HasUnhandledHomework); } [Fact] public void Load_HeutigeStunde_VorherigeStundeOhneHausaufgabe_KeinBadge() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 }); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = group.Id, Date = today.AddDays(-3) }); var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons); var item = Assert.Single(vm.TodayItems); Assert.False(item.HasUnhandledHomework); } [Fact] public void Load_Wochenkachel_VorherigeUnkontrollierteHausaufgabe_ZeigtBadge() { var group = new LearningGroup { Name = "Q1 Chemie" }; var date = DateInCurrentWeek(DayOfWeek.Wednesday); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Wednesday, PeriodNumber = 1 }); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = group.Id, Date = date.AddDays(-2), Homework = "Vokabeln lernen" }); var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Wednesday && c.PeriodNumber == 1); Assert.True(cell.HasUnhandledHomework); } [Fact] public void OpenGroup_RuftOnNavigateToGroupMitDerGroupIdAuf() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 }); var vm = BuildViewModel(slots, new FakeGroups([group])); Guid? navigatedTo = null; vm.OnNavigateToGroup = id => navigatedTo = id; vm.OpenGroupCommand.Execute(vm.TodayItems[0].GroupId); Assert.Equal(group.Id, navigatedTo); } [Fact] public void OpenSettings_RuftOnNavigateToSettingsAuf() { var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([])); SettingsTab? navigatedTab = null; vm.OnNavigateToSettings = tab => navigatedTab = tab; vm.OpenSettingsCommand.Execute(null); Assert.Equal(SettingsTab.Holidays, navigatedTab); } // ── "Heute"-Ansicht: Wochenraster (Nutzer-Feedback, zweite Iteration) ──────────────────── [Fact] public void Load_Wochenkachel_ZeigtFachKlasseRaumUndThema() { var subject = new Subject { Name = "Chemie", ShortName = "Ch" }; var group = new LearningGroup { Name = "Q1 Chemie", SubjectId = subject.Id }; var date = DateInCurrentWeek(DayOfWeek.Monday); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1, Room = "R204" }); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = group.Id, Date = date, Topic = "Redoxreaktionen" }); var vm = BuildViewModel(slots, new FakeGroups([group]), subjects: new FakeSubjects([subject]), lessons: lessons); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.Equal("Ch", cell.SubjectLabel); Assert.Equal("Q1 Chemie", cell.GroupName); Assert.Equal("R204", cell.Room); Assert.Equal("Redoxreaktionen", cell.Topic); } [Fact] public void Load_Wochenkachel_UnbelegteZelle_IstNichtZugewiesen() { var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([])); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.False(cell.IsAssigned); } [Fact] public void Load_Wochenkachel_KlausurAmTag_ZeigtKlausurIcon() { var group = new LearningGroup { Name = "Q1 Chemie" }; var date = DateInCurrentWeek(DayOfWeek.Monday); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); var exams = new FakeExams([new Exam { GroupId = group.Id, Date = date, Title = "Klausur" }]); var vm = BuildViewModel(slots, new FakeGroups([group]), exams: exams); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.True(cell.HasExam); } [Fact] public void Load_Wochenkachel_TagInSchulferien_WirdAusgegraut() { var group = new LearningGroup { Name = "Q1 Chemie" }; var date = DateInCurrentWeek(DayOfWeek.Monday); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); var holidays = new FakeSchoolHolidays(); holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = date, EndDate = date.AddDays(5) }); var vm = BuildViewModel(slots, new FakeGroups([group]), holidays); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.True(cell.IsHoliday); Assert.Equal("#BDBDBD", cell.ColorHex); } [Fact] public void Load_Wochenkachel_TagAusserhalbVonFerien_WirdNichtAusgegraut() { var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); var vm = BuildViewModel(slots, new FakeGroups([group])); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.False(cell.IsHoliday); } [Fact] public void Load_Wochenkachel_ExperimentInPhaseGeplant_ZeigtExperimentIcon() { var group = new LearningGroup { Name = "Q1 Chemie" }; var date = DateInCurrentWeek(DayOfWeek.Monday); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = group.Id, Date = date, Phases = [new LessonPhaseStep { Activity = "Experiment: Redoxreihe" }] }); var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.True(cell.HasExperiment); } [Fact] public void Load_Wochenkachel_OhneExperimentErwaehnung_KeinExperimentIcon() { var group = new LearningGroup { Name = "Q1 Chemie" }; var date = DateInCurrentWeek(DayOfWeek.Monday); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = group.Id, Date = date, Phases = [new LessonPhaseStep { Activity = "Stillarbeit" }] }); var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.False(cell.HasExperiment); } [Fact] public void Load_Wochenkachel_UnvollstaendigePhasendaten_VerursachenKeinenFehler() { var group = new LearningGroup { Name = "Q1 Chemie" }; var date = DateInCurrentWeek(DayOfWeek.Monday); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); var lessons = new FakeLessons(); lessons.Add(new Lesson { GroupId = group.Id, Date = date, Phases = [new LessonPhaseStep { Name = null!, Activity = null!, Material = "Experimentiermaterial" }], }); var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.True(cell.HasExperiment); } [Fact] public void Load_Wochenkachel_LetzteStundeVorKlausur_ZeigtIcon() { var date = DateInCurrentWeek(DayOfWeek.Monday); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); var exams = new FakeExams([new Exam { GroupId = group.Id, Date = date.AddDays(1), Title = "Klausur" }]); var vm = BuildViewModel(slots, new FakeGroups([group]), exams: exams); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.True(cell.IsLastBeforeExam); } [Fact] public void Load_Wochenkachel_KeineAnstehendeKlausur_KeinIcon() { var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); var vm = BuildViewModel(slots, new FakeGroups([group])); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.False(cell.IsLastBeforeExam); } [Fact] public void PreviousWeek_ZeigtVorherigeKalenderwoche() { var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([])); var initialLabel = vm.WeekRangeLabel; vm.PreviousWeekCommand.Execute(null); Assert.NotEqual(initialLabel, vm.WeekRangeLabel); Assert.False(vm.IsCurrentWeek); } [Fact] public void NextWeek_GefolgtVonCurrentWeek_KehrtZurLaufendenWocheZurueck() { var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([])); var initialLabel = vm.WeekRangeLabel; vm.NextWeekCommand.Execute(null); Assert.NotEqual(initialLabel, vm.WeekRangeLabel); vm.CurrentWeekCommand.Execute(null); Assert.Equal(initialLabel, vm.WeekRangeLabel); Assert.True(vm.IsCurrentWeek); } [Fact] public void NextWeek_BadgeGehoertZurAngezeigtenWoche_NichtZuHeute() { // Klausur liegt in der übernächsten Woche relativ zu "heute" — im Wochenraster einer // Woche dahinter (also der übernächsten Woche selbst) muss "letzte Stunde vor Klausur" // erscheinen, nicht bereits in der aktuellen Woche. var group = new LearningGroup { Name = "Q1 Chemie" }; var currentWeekMonday = DateInCurrentWeek(DayOfWeek.Monday); var examWeekMonday = currentWeekMonday.AddDays(14); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); var exams = new FakeExams([new Exam { GroupId = group.Id, Date = examWeekMonday.AddDays(1), Title = "Klausur" }]); var vm = BuildViewModel(slots, new FakeGroups([group]), exams: exams); var currentWeekCell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.False(currentWeekCell.IsLastBeforeExam); vm.NextWeekCommand.Execute(null); vm.NextWeekCommand.Execute(null); var examWeekCell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.True(examWeekCell.IsLastBeforeExam); } // ── Badge: letzte/vorletzte Stunde vor Ferien (Bearbeiten-Raster) ──────────────────────── [Fact] public void Load_LetzteStundeVorFerien_ZeigtBadge1() { var weekday = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today)); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 1 }); var holidays = new FakeSchoolHolidays(); holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = weekday.AddDays(1), EndDate = weekday.AddDays(10) }); var vm = BuildViewModel(slots, new FakeGroups([group]), holidays); var cell = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 1); Assert.Equal("1", cell.BadgeText); } [Fact] public void Load_Wochenkachel_LetzteStundeVorFerien_ZeigtBadge1() { var date = DateInCurrentWeek(DayOfWeek.Monday); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); var holidays = new FakeSchoolHolidays(); holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = date.AddDays(1), EndDate = date.AddDays(10) }); var vm = BuildViewModel(slots, new FakeGroups([group]), holidays); var weekCell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.Equal("1", weekCell.HolidayBadge); } [Fact] public void Load_VorletzteStundeVorFerien_ZeigtBadge2() { var firstOccurrence = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today)); var secondOccurrence = firstOccurrence.AddDays(7); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = firstOccurrence.DayOfWeek, PeriodNumber = 1 }); var holidays = new FakeSchoolHolidays(); holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = secondOccurrence.AddDays(1), EndDate = secondOccurrence.AddDays(10) }); var vm = BuildViewModel(slots, new FakeGroups([group]), holidays); var cell = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == firstOccurrence.DayOfWeek && c.PeriodNumber == 1); Assert.Equal("2", cell.BadgeText); } [Fact] public void Load_Doppelstunde_BeideStundenZeigenDasselbeBadge() { var weekday = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today)); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 1 }); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 2 }); var holidays = new FakeSchoolHolidays(); holidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = weekday.AddDays(1), EndDate = weekday.AddDays(10) }); var vm = BuildViewModel(slots, new FakeGroups([group]), holidays); var period1 = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 1); var period2 = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 2); Assert.Equal("1", period1.BadgeText); Assert.Equal("1", period2.BadgeText); } [Fact] public void Load_KeineAnstehendenFerien_KeinBadge() { var weekday = NextGridWeekday(DateOnly.FromDateTime(DateTime.Today)); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = weekday.DayOfWeek, PeriodNumber = 1 }); var vm = BuildViewModel(slots, new FakeGroups([group])); var cell = vm.Cells.Single(c => c.IsSlotCell && c.Weekday == weekday.DayOfWeek && c.PeriodNumber == 1); Assert.False(cell.HasBadge); } // ── Aufsicht: wiederkehrend (Bearbeiten-Raster) ────────────────────────── [Fact] public void Load_AufsichtEingetragen_ZeigtSupervisionRowMitOrt() { var duties = new FakeSupervisionDuties(); duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" }); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), supervisionDuties: duties); var label = vm.Cells.Single(c => c.IsSupervisionRow && c.Text.Contains("n. 2.")); Assert.NotNull(label); var mondayCell = vm.Cells .SkipWhile(c => c != label).Skip(1) // erste Zelle nach dem Label = Montag .First(); Assert.True(mondayCell.IsSupervisionCell); Assert.Equal("Pausenhof", mondayCell.SupervisionLocation); } [Fact] public void Load_KeineAufsichtEingetragen_KeineSupervisionRow() { var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([])); Assert.DoesNotContain(vm.Cells, c => c.IsSupervisionRow); } [Fact] public void Load_AufsichtEingetragen_BekommtSchmalereZeilenhoeheAlsStundenzeilen() { var duties = new FakeSupervisionDuties(); duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" }); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), supervisionDuties: duties); var row = vm.GridRows.Single(r => r.Cells.Any(c => c.IsSupervisionRow && c.Text.Contains("n. 2."))); Assert.Equal(22, row.RowHeight); Assert.Contains(vm.GridRows, r => r.RowHeight == 46); // normale Stundenzeilen bleiben unverändert } // ── Aufsicht + Vertretung im Wochenraster ──────────────────────────────── [Fact] public void Load_Wochenraster_ZeigtWiederkehrendeAufsicht() { var duties = new FakeSupervisionDuties(); duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" }); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), supervisionDuties: duties); var label = vm.WeekItems.Single(c => c.IsSupervisionRow && c.Text.Contains("n. 2.")); var mondayCell = vm.WeekItems.SkipWhile(c => c != label).Skip(1).First(); Assert.Equal("Pausenhof", mondayCell.SupervisionLocation); Assert.False(mondayCell.IsSubstitutionSupervision); } [Fact] public void Load_Wochenraster_AufsichtBekommtSchmalereZeilenhoeheAlsStundenzeilen() { var duties = new FakeSupervisionDuties(); duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" }); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), supervisionDuties: duties); var row = vm.WeekRows.Single(r => r.Cells.Any(c => c.IsSupervisionRow && c.Text.Contains("n. 2."))); Assert.Equal(22, row.RowHeight); Assert.Contains(vm.WeekRows, r => r.RowHeight == 76); } [Fact] public void Load_Wochenraster_VertretungsaufsichtUeberschreibtRegulaereAnzeige() { var date = DateInCurrentWeek(DayOfWeek.Monday); var duties = new FakeSupervisionDuties(); duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" }); var substitutions = new FakeSubstitutionEntries(); substitutions.Add(new SubstitutionEntry { Date = date, Kind = SubstitutionKind.Supervision, AfterPeriod = 2, Description = "Vertretung für Hr. Müller", }); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), supervisionDuties: duties, substitutions: substitutions); var label = vm.WeekItems.Single(c => c.IsSupervisionRow && c.Text.Contains("n. 2.")); var mondayCell = vm.WeekItems.SkipWhile(c => c != label).Skip(1).First(); Assert.Equal("Vertretung für Hr. Müller", mondayCell.SupervisionLocation); Assert.True(mondayCell.IsSubstitutionSupervision); } [Fact] public void Load_Wochenraster_VertretungsaufsichtOhneRegulaereAufsicht_ZeigtEigeneRow() { var date = DateInCurrentWeek(DayOfWeek.Monday); var substitutions = new FakeSubstitutionEntries(); substitutions.Add(new SubstitutionEntry { Date = date, Kind = SubstitutionKind.Supervision, AfterPeriod = 3, Description = "Vertretung für Fr. Schmidt", }); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions); var label = vm.WeekItems.Single(c => c.IsSupervisionRow && c.Text.Contains("n. 3.")); var mondayCell = vm.WeekItems.SkipWhile(c => c != label).Skip(1).First(); Assert.Equal("Vertretung für Fr. Schmidt", mondayCell.SupervisionLocation); } [Fact] public void Load_Wochenraster_VertretungsstundeUeberschreibtNormaleAnzeige() { var group = new LearningGroup { Name = "Q1 Chemie" }; var date = DateInCurrentWeek(DayOfWeek.Monday); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 1 }); var substitutions = new FakeSubstitutionEntries(); substitutions.Add(new SubstitutionEntry { Date = date, Kind = SubstitutionKind.Lesson, PeriodNumber = 1, GroupLabel = "8a", Description = "Vertretung Erdkunde", }); var vm = BuildViewModel(slots, new FakeGroups([group]), substitutions: substitutions); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.True(cell.IsSubstitutionLesson); Assert.Equal("8a", cell.GroupName); Assert.Equal("Vertretung Erdkunde", cell.Topic); } [Fact] public void Load_Wochenraster_VertretungsstundeOhnePassendenSlot_ErscheintTrotzdem() { var date = DateInCurrentWeek(DayOfWeek.Monday); var substitutions = new FakeSubstitutionEntries(); substitutions.Add(new SubstitutionEntry { Date = date, Kind = SubstitutionKind.Lesson, PeriodNumber = 5, GroupLabel = "8a", Description = "Vertretung Erdkunde", }); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 5); Assert.True(cell.IsSubstitutionLesson); Assert.True(cell.IsAssigned); } // ── Aufsicht + Vertretung in der Tagesliste ("Heute") ──────────────────── [Fact] public void Load_Heute_ZeigtHeutigeAufsicht() { var today = DateOnly.FromDateTime(DateTime.Today); var duties = new FakeSupervisionDuties(); duties.Add(new SupervisionDuty { Weekday = today.DayOfWeek, AfterPeriod = 2, Location = "Pausenhof" }); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), supervisionDuties: duties); var item = Assert.Single(vm.TodaySupervisions); Assert.Equal("Pausenhof", item.Description); Assert.False(item.IsSubstitution); } [Fact] public void Load_Heute_VertretungsstundeErsetztNormaleStundenanzeige() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "Q1 Chemie" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 }); var substitutions = new FakeSubstitutionEntries(); substitutions.Add(new SubstitutionEntry { Date = today, Kind = SubstitutionKind.Lesson, PeriodNumber = 1, GroupLabel = "8a", Description = "Vertretung Erdkunde", }); var vm = BuildViewModel(slots, new FakeGroups([group]), substitutions: substitutions); var item = Assert.Single(vm.TodayItems); Assert.True(item.IsSubstitution); Assert.Equal("8a", item.GroupName); Assert.Equal("Vertretung Erdkunde", item.LessonTopic); } [Fact] public void OpenGroup_MitLeererGroupId_RuftDelegateNichtAuf() { var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([])); var called = false; vm.OnNavigateToGroup = _ => called = true; vm.OpenGroupCommand.Execute(Guid.Empty); Assert.False(called); } // ── Sondereinsätze (Ausflüge, Berufsmessen, ...) ───────────────────────── [Fact] public void Load_Wochenraster_GanztaegigerSondereinsatz_UeberdecktAllePeriodenDesTages() { var date = DateInCurrentWeek(DayOfWeek.Monday); var substitutions = new FakeSubstitutionEntries(); substitutions.Add(new SubstitutionEntry { Date = date, Kind = SubstitutionKind.SpecialAssignment, IsAllDay = true, GroupLabel = "8a", Description = "Ausflug ins Museum", }); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions); var period1 = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); var period7 = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 7); Assert.True(period1.IsSpecialAssignment); Assert.True(period7.IsSpecialAssignment); Assert.Equal("Ausflug ins Museum", period1.Topic); Assert.Equal("8a", period1.GroupName); } [Fact] public void Load_Wochenraster_SondereinsatzMitStundenbereich_NurDortSichtbar() { var date = DateInCurrentWeek(DayOfWeek.Monday); var substitutions = new FakeSubstitutionEntries(); substitutions.Add(new SubstitutionEntry { Date = date, Kind = SubstitutionKind.SpecialAssignment, IsAllDay = false, FromPeriod = 3, ToPeriod = 4, Description = "Berufsmesse", }); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions); var period2 = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 2); var period3 = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 3); var period4 = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 4); var period5 = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 5); Assert.False(period2.IsSpecialAssignment); Assert.True(period3.IsSpecialAssignment); Assert.True(period4.IsSpecialAssignment); Assert.False(period5.IsSpecialAssignment); } [Fact] public void Load_Wochenraster_SondereinsatzOhneGruppe_ZeigtLeeresGroupName() { var date = DateInCurrentWeek(DayOfWeek.Monday); var substitutions = new FakeSubstitutionEntries(); substitutions.Add(new SubstitutionEntry { Date = date, Kind = SubstitutionKind.SpecialAssignment, IsAllDay = true, Description = "Berufsmesse", }); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 1); Assert.Equal("", cell.GroupName); } [Fact] public void Load_Heute_ZeigtGanztaegigenSondereinsatz() { var today = DateOnly.FromDateTime(DateTime.Today); var substitutions = new FakeSubstitutionEntries(); substitutions.Add(new SubstitutionEntry { Date = today, Kind = SubstitutionKind.SpecialAssignment, IsAllDay = true, GroupLabel = "8a", Description = "Ausflug ins Museum", }); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions); var item = Assert.Single(vm.TodaySpecialAssignments); Assert.Equal("Ganztägig", item.PeriodLabel); Assert.Equal("Ausflug ins Museum", item.Description); Assert.Equal("8a", item.GroupLabel); } [Fact] public void Load_Heute_ZeigtSondereinsatzMitStundenbereich() { var today = DateOnly.FromDateTime(DateTime.Today); var substitutions = new FakeSubstitutionEntries(); substitutions.Add(new SubstitutionEntry { Date = today, Kind = SubstitutionKind.SpecialAssignment, IsAllDay = false, FromPeriod = 3, ToPeriod = 4, Description = "Berufsmesse", }); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), substitutions: substitutions); var item = Assert.Single(vm.TodaySpecialAssignments); Assert.Equal("3.–4. Stunde", item.PeriodLabel); } // ── Stundenausfall ──────────────────────────────────────────────────────── [Fact] public void Load_Wochenraster_Ausfall_ZeigtGruppeUndFachAusDemStundenplan() { var subject = new Subject { Name = "Naturwissenschaften", ShortName = "NAT" }; var group = new LearningGroup { Name = "6a", SubjectId = subject.Id }; var date = DateInCurrentWeek(DayOfWeek.Monday); var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 2 }); var substitutions = new FakeSubstitutionEntries(); substitutions.Add(new SubstitutionEntry { Date = date, Kind = SubstitutionKind.Cancelled, PeriodNumber = 2, Description = "6a auf Klassenfahrt", }); var vm = BuildViewModel(slots, new FakeGroups([group]), subjects: new FakeSubjects([subject]), substitutions: substitutions); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 2); Assert.True(cell.IsCancelled); Assert.Equal("NAT", cell.SubjectLabel); Assert.Equal("6a", cell.GroupName); Assert.Equal("6a auf Klassenfahrt", cell.Topic); } [Fact] public void Load_Wochenraster_KeinAusfall_ZeigtNormaleStunde() { var group = new LearningGroup { Name = "6a" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = DayOfWeek.Monday, PeriodNumber = 2 }); var vm = BuildViewModel(slots, new FakeGroups([group])); var cell = vm.WeekItems.Single(c => c.IsSlotCell && c.Weekday == DayOfWeek.Monday && c.PeriodNumber == 2); Assert.False(cell.IsCancelled); } [Fact] public void Load_Heute_Ausfall_ErsetztNormaleStundenanzeige() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "6a" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 2 }); var substitutions = new FakeSubstitutionEntries(); substitutions.Add(new SubstitutionEntry { Date = today, Kind = SubstitutionKind.Cancelled, PeriodNumber = 2, Description = "6a auf Klassenfahrt", }); var vm = BuildViewModel(slots, new FakeGroups([group]), substitutions: substitutions); var item = Assert.Single(vm.TodayItems); Assert.True(item.IsCancelled); Assert.Equal("6a", item.GroupName); Assert.Equal("6a auf Klassenfahrt", item.LessonTopic); Assert.Equal(group.Id, item.GroupId); } [Fact] public void Load_Heute_AusfallOhneGrund_LessonTopicBleibtLeer() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "6a" }; var slots = new FakeTimetableSlots(); slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 2 }); var substitutions = new FakeSubstitutionEntries(); substitutions.Add(new SubstitutionEntry { Date = today, Kind = SubstitutionKind.Cancelled, PeriodNumber = 2 }); var vm = BuildViewModel(slots, new FakeGroups([group]), substitutions: substitutions); var item = Assert.Single(vm.TodayItems); Assert.False(item.HasLessonTopic); } // ── Anstehende Klausurtermine (4.4.3) ──────────────────────────────────── [Fact] public void Load_ZeigtKlausurInnerhalbDesVorausblicks() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "Q1 Chemie" }; var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(10), Title = "Klausur Nr. 2" }]); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams); var item = Assert.Single(vm.UpcomingExams); Assert.Equal("Q1 Chemie", item.GroupName); Assert.Equal("Klausur Nr. 2", item.Title); Assert.Equal(today.AddDays(10), item.Date); } [Fact] public void Load_KlausurAusserhalbDesVorausblicks_WirdNichtGezeigt() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "Q1 Chemie" }; var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(30), Title = "Klausur Nr. 3" }]); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams); Assert.Empty(vm.UpcomingExams); } [Fact] public void Load_VergangeneKlausur_WirdNichtGezeigt() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "Q1 Chemie" }; var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(-1), Title = "Alte Klausur" }]); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams); Assert.Empty(vm.UpcomingExams); } [Fact] public void Load_MehrereKlausuren_SindNachDatumSortiert() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "Q1 Chemie" }; var exams = new FakeExams( [ new Exam { GroupId = group.Id, Date = today.AddDays(15), Title = "Später" }, new Exam { GroupId = group.Id, Date = today.AddDays(3), Title = "Früher" }, ]); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams); Assert.Equal(["Früher", "Später"], vm.UpcomingExams.Select(e => e.Title)); } [Fact] public void Load_HeutigeKlausur_IstImVorausblickEnthalten() { var today = DateOnly.FromDateTime(DateTime.Today); var group = new LearningGroup { Name = "Q1 Chemie" }; var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today, Title = "Heute" }]); var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([group]), exams: exams); Assert.Single(vm.UpcomingExams); } }