Files
LehrerApp/LehrerApp.Desktop.Tests/TimetableViewModelTests.cs
T
adminandClaude Sonnet 5 99b2de6519 Stundenplan: Wochenraster, Wochennavigation, Ferien/Feiertage (Kapitel 4.3)
Neuer Stundenplan mit "Heute"-Standardansicht (Tagesliste unten angedockt,
gruppenübergreifendes Wochenraster mit Fach/Klasse/Raum/Thema, Vor-/Zurück-
Navigation zwischen Kalenderwochen) und separatem Bearbeiten-Raster für die
wöchentliche Zuordnung. Badges für Ferien-/Klausur-Nähe und ausgegraute
Ferientage direkt im Plan statt einer separaten Liste. Ferien-/Feiertage-
Pflege (Bundesland, Schulferien) sitzt jetzt in den Einstellungen statt im
Stundenplan selbst.

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

414 lines
19 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 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)
{
// 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());
}
/// 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 ShowEditor_SchaltetAufBearbeiten_Tab()
{
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
vm.ShowEditorCommand.Execute(null);
Assert.Equal(1, vm.ActiveTabIndex);
}
// ── "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);
}
}