Files
LehrerApp/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs
T
adminandClaude Sonnet 5 f8058b537b feat: Geräte-Pairing-UI + Einstellungen-Neustrukturierung
- Sync-Settings-Tab: neuer Bereich "Gerät koppeln" (10.3.1), nutzt den
  bereits vorhandenen, bisher nirgends verdrahteten SnapshotService.
  Ein Gerät erzeugt per CreatePairingCode einen verschlüsselten
  DB-Snapshot + Einmal-Code, das zweite Gerät übernimmt per
  RedeemPairingCode Datenbank und Sync-Schlüssel. Bestätigungsdialog
  vor dem Einlösen, da die lokale Datenbank dabei vollständig ersetzt
  wird (alter Stand wird automatisch als Backup gesichert).
- Einstellungen: TabPlacement von Top auf Left umgestellt (vertikale
  Liste statt langem horizontalem Balken) und die 13 Tabs in drei
  logische Gruppen sortiert (Fachliches / Zeitplanung / System).
- Neuer SettingsTab-Enum ersetzt rohe int-Tab-Indizes bei
  MainWindowViewModel.NavigateToSettings/
  TimetableViewModel.OnNavigateToSettings. Dabei einen bestehenden Bug
  gefunden und mitbehoben: das Zahnrad im Stundenplan öffnete über den
  hartcodierten Index 7 tatsächlich "Datenschutz" statt des laut
  Kommentar/Tooltip beabsichtigten "Ferien & Feiertage".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 01:09:25 +02:00

813 lines
38 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.) <paramref name="from"/>, 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 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_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);
}
// ── 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_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);
}
}