ApplyCardLayout() zaehlte bisher IsVisible statt EffectiveIsVisible, wodurch eine eingeschaltete aber leere HideWhenEmpty-Kachel weiterhin einen Rasterplatz belegte. Da im Alltag meist mehrere der sieben HideWhenEmpty-Kacheln leer sind, war das der Normalfall, nicht die Ausnahme. UpdateDashboardSummary() setzte zudem alle IsEmpty- Werte, ohne das Layout danach neu zu berechnen. Zusaetzlich hatte jede Kachel ihren Grid-Margin fest im XAML verdrahtet (links/rechts), obwohl Spalte und Zeile erst zur Laufzeit aus Sichtbarkeit und Reihenfolge berechnet werden - beim Ausblenden einer Kachel wanderten die Nachbarn in die andere Spalte, der Rinnstein sass dann auf der falschen Seite. DashboardCardOption.Margin leitet den Wert jetzt aus Column ab; das XAML bindet darauf statt fixer Werte. Drei Regressionstests decken beide Faelle ab.
811 lines
37 KiB
C#
811 lines
37 KiB
C#
using LehrerApp.Core.Models;
|
|
using LehrerApp.Core.Services;
|
|
using LehrerApp.Desktop.ViewModels;
|
|
using System.Globalization;
|
|
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), außerdem die Kalender-Sitzungsanzeige (3.3.4) und die Mindeststichprobe der
|
|
/// Fehlzeiten-Warnung (5.2.3). Deckt nicht die übrigen, unveränderten Dashboard-Bausteine
|
|
/// (offene Aufgaben, ...).
|
|
public sealed class DashboardViewModelTests
|
|
{
|
|
[Fact]
|
|
public void LeereHinweisbereiche_WerdenAusgeblendetUndZusammenfassungBleibtKompakt()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today });
|
|
|
|
Assert.False(vm.ExcusesCard.EffectiveIsVisible);
|
|
Assert.False(vm.CorrectionsCard.EffectiveIsVisible);
|
|
Assert.False(vm.AlertsCard.EffectiveIsVisible);
|
|
Assert.Equal("0 offene Punkte", vm.AttentionSummary);
|
|
Assert.True(vm.TodayCard.EffectiveIsVisible);
|
|
Assert.True(vm.CalendarCard.EffectiveIsVisible);
|
|
}
|
|
|
|
/// Regression: ApplyCardLayout zaehlte frueher IsVisible statt EffectiveIsVisible. Eine
|
|
/// eingeschaltete, aber leere HideWhenEmpty-Kachel belegte damit einen Rasterplatz, den das
|
|
/// Grid nie fuellt — im Alltag der Normalfall, weil meist mehrere Hinweiskacheln leer sind.
|
|
[Fact]
|
|
public void Kachelraster_LeereAusgeblendeteKacheln_HinterlassenKeineLuecke()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today });
|
|
|
|
// Ohne mindestens eine leer ausgeblendete Kachel wuerde der Test nichts pruefen.
|
|
Assert.False(vm.ExcusesCard.EffectiveIsVisible);
|
|
|
|
var belegtePlaetze = vm.DashboardCards.Where(c => c.EffectiveIsVisible)
|
|
.Select(c => c.Row * 2 + c.Column).OrderBy(slot => slot).ToList();
|
|
Assert.Equal(Enumerable.Range(0, belegtePlaetze.Count), belegtePlaetze);
|
|
}
|
|
|
|
[Fact]
|
|
public void KachelMargin_FolgtDerBerechnetenSpalte()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today });
|
|
|
|
foreach (var card in vm.DashboardCards.Where(c => c.EffectiveIsVisible))
|
|
Assert.Equal(card.Column == 0
|
|
? new Avalonia.Thickness(0, 0, 8, 8)
|
|
: new Avalonia.Thickness(8, 0, 0, 8), card.Margin);
|
|
}
|
|
|
|
/// Regression: der Margin hing fest im XAML an der Kachel. Wandert sie durch Aus-/Einblenden
|
|
/// einer vorherigen Kachel in die andere Spalte, sass der Rinnstein auf der falschen Seite.
|
|
[Fact]
|
|
public void KachelAusblenden_DrehtDenMarginDerNachfolgendenKachel()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today });
|
|
Assert.Equal(1, vm.TasksCard.Column);
|
|
Assert.Equal(new Avalonia.Thickness(8, 0, 0, 8), vm.TasksCard.Margin);
|
|
|
|
vm.TodayCard.IsVisible = false;
|
|
|
|
Assert.Equal(0, vm.TasksCard.Column);
|
|
Assert.Equal(new Avalonia.Thickness(0, 0, 8, 8), vm.TasksCard.Margin);
|
|
}
|
|
|
|
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,
|
|
FakeSessions? sessions = null, FakeEntries? entries = null,
|
|
FakeAnnualPlanEvents? annualPlanEvents = null, List<LearningGroup>? allGroups = null,
|
|
FakeTimeEntries? timeEntries = null)
|
|
{
|
|
lessons ??= new FakeLessons();
|
|
lessons.Add(lesson);
|
|
return new DashboardViewModel(
|
|
new FakeGroups(allGroups ?? [group]), new FakeSubjects([]), lessons,
|
|
exams ?? new FakeExams([]), results ?? new FakeResults(), grades ?? new FakeGrades(),
|
|
reportGrades ?? new FakeReportGrades(), memberships ?? new FakeMemberships([]),
|
|
tasks ?? new FakeWorkTasks(), sessions ?? new FakeSessions([]), entries ?? 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(), timeEntries ?? new FakeTimeEntries(),
|
|
TestSupport.BuildUntisHubService(), TestSupport.BuildWebUntisIntegrationService(), annualPlanEvents);
|
|
}
|
|
|
|
/// Montag einer Woche, die garantiert in der Zukunft liegt und innerhalb der
|
|
/// Klausurwochen-Vorausschau — unabhängig davon, welcher Wochentag "heute" gerade ist.
|
|
private static DateOnly NextIsoWeekMonday()
|
|
{
|
|
var reference = DateTime.Today.AddDays(7);
|
|
return DateOnly.FromDateTime(
|
|
ISOWeek.ToDateTime(ISOWeek.GetYear(reference), ISOWeek.GetWeekOfYear(reference), DayOfWeek.Monday));
|
|
}
|
|
|
|
[Fact]
|
|
public void ExamWeekLoads_DreiGeplanteKlausurenInDerselbenWoche_ErzeugtEintrag()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var monday = NextIsoWeekMonday();
|
|
var exams = new FakeExams([
|
|
new Exam { GroupId = group.Id, Title = "a", Date = monday, Status = ExamStatus.Planned },
|
|
new Exam { GroupId = group.Id, Title = "b", Date = monday.AddDays(1), Status = ExamStatus.Planned },
|
|
new Exam { GroupId = group.Id, Title = "c", Date = monday.AddDays(2), Status = ExamStatus.Planned },
|
|
]);
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, exams: exams);
|
|
|
|
var entry = Assert.Single(vm.ExamWeekLoads);
|
|
Assert.Equal(3, entry.ExamCount);
|
|
Assert.True(vm.ExamLoadCard.EffectiveIsVisible);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExamWeekLoads_WenigeKlausurenProWoche_BleibtLeer()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var monday = NextIsoWeekMonday();
|
|
var exams = new FakeExams([
|
|
new Exam { GroupId = group.Id, Title = "a", Date = monday, Status = ExamStatus.Planned },
|
|
new Exam { GroupId = group.Id, Title = "b", Date = monday.AddDays(1), Status = ExamStatus.Planned },
|
|
]);
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, exams: exams);
|
|
|
|
Assert.Empty(vm.ExamWeekLoads);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExamWeekLoads_BereitsDurchgefuehrteKlausurenZaehlenNicht()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var monday = NextIsoWeekMonday();
|
|
var exams = new FakeExams([
|
|
new Exam { GroupId = group.Id, Title = "a", Date = monday, Status = ExamStatus.Conducted },
|
|
new Exam { GroupId = group.Id, Title = "b", Date = monday.AddDays(1), Status = ExamStatus.Graded },
|
|
new Exam { GroupId = group.Id, Title = "c", Date = monday.AddDays(2), Status = ExamStatus.Planned },
|
|
]);
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, exams: exams);
|
|
|
|
Assert.Empty(vm.ExamWeekLoads);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingTeachingTime_VergangenerUnterrichtstagOhneErfassung_WirdGemeldet()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var pastDay = today.AddDays(-1);
|
|
var slots = new FakeTimetableSlots();
|
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = pastDay.DayOfWeek, PeriodNumber = 1 });
|
|
var periodSchedule = NewPeriodSchedule();
|
|
periodSchedule.SetPeriods([new PeriodTimeEntry
|
|
{ PeriodNumber = 1, Start = new TimeOnly(8, 0), End = new TimeOnly(8, 45) }]);
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
|
|
|
|
Assert.Contains(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
|
|
Assert.True(vm.MissingTeachingTimeCard.EffectiveIsVisible);
|
|
Assert.Equal(2, vm.AttentionCount); // fehlende Unterrichtszeit + bereits bestehende ungeplante Stunde
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingTeachingTime_BereitsErfassterTag_WirdNichtGemeldet()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var pastDay = today.AddDays(-1);
|
|
var slots = new FakeTimetableSlots();
|
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = pastDay.DayOfWeek, PeriodNumber = 1 });
|
|
var periodSchedule = NewPeriodSchedule();
|
|
periodSchedule.SetPeriods([new PeriodTimeEntry
|
|
{ PeriodNumber = 1, Start = new TimeOnly(8, 0), End = new TimeOnly(8, 45) }]);
|
|
var timeEntries = new FakeTimeEntries();
|
|
timeEntries.Add(new TimeEntry { Date = pastDay, Category = "Unterricht", DurationMinutes = 45 });
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
|
slots: slots, periodSchedule: periodSchedule, timeEntries: timeEntries);
|
|
|
|
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingTeachingTime_KomplettAusgefallenerTag_WirdNichtGemeldet()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var pastDay = today.AddDays(-1);
|
|
var slots = new FakeTimetableSlots();
|
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = pastDay.DayOfWeek, PeriodNumber = 1 });
|
|
var periodSchedule = NewPeriodSchedule();
|
|
periodSchedule.SetPeriods([new PeriodTimeEntry
|
|
{ PeriodNumber = 1, Start = new TimeOnly(8, 0), End = new TimeOnly(8, 45) }]);
|
|
var substitutions = new FakeSubstitutionEntries();
|
|
substitutions.Add(new SubstitutionEntry { Date = pastDay, Kind = SubstitutionKind.Cancelled, PeriodNumber = 1 });
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
|
slots: slots, periodSchedule: periodSchedule, substitutions: substitutions);
|
|
|
|
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingTeachingTime_SchulferienTagWirdNichtGemeldet()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var pastDay = today.AddDays(-1);
|
|
var slots = new FakeTimetableSlots();
|
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = pastDay.DayOfWeek, PeriodNumber = 1 });
|
|
var periodSchedule = NewPeriodSchedule();
|
|
periodSchedule.SetPeriods([new PeriodTimeEntry
|
|
{ PeriodNumber = 1, Start = new TimeOnly(8, 0), End = new TimeOnly(8, 45) }]);
|
|
var schoolHolidays = new FakeSchoolHolidays();
|
|
schoolHolidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = pastDay, EndDate = pastDay });
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
|
slots: slots, periodSchedule: periodSchedule, schoolHolidays: schoolHolidays);
|
|
|
|
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingTeachingTime_HeuteVorAblaufDerWartezeit_WirdNichtGemeldet()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var slots = new FakeTimetableSlots();
|
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
|
var periodSchedule = NewPeriodSchedule();
|
|
// Unterrichtsende liegt garantiert noch keine 30 Minuten zurück.
|
|
var futureEnd = TimeOnly.FromDateTime(DateTime.Now).AddHours(2);
|
|
periodSchedule.SetPeriods([new PeriodTimeEntry
|
|
{ PeriodNumber = 1, Start = futureEnd.AddHours(-1), End = futureEnd }]);
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
|
|
|
|
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == today);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingTeachingTime_HeuteNachAblaufDerWartezeit_WirdGemeldet()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var slots = new FakeTimetableSlots();
|
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
|
var periodSchedule = NewPeriodSchedule();
|
|
// Unterrichtsende liegt garantiert mehr als 30 Minuten zurück.
|
|
var pastEnd = TimeOnly.FromDateTime(DateTime.Now).AddHours(-2);
|
|
periodSchedule.SetPeriods([new PeriodTimeEntry
|
|
{ PeriodNumber = 1, Start = pastEnd.AddHours(-1), End = pastEnd }]);
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
|
|
|
|
Assert.Contains(vm.MissingTeachingTimeEntries, i => i.Date == today);
|
|
}
|
|
|
|
[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 Kalenderauswahl_SortiertKurseDesTagesVorDenAlphabetischenRest()
|
|
{
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var selectedDate = today.AddDays(1);
|
|
var alphabeticallyFirst = new LearningGroup { Name = "10a" };
|
|
var selectedDayGroup = new LearningGroup { Name = "WAT 10c" };
|
|
var lessons = new FakeLessons();
|
|
lessons.Add(new Lesson { GroupId = selectedDayGroup.Id, Date = selectedDate });
|
|
var vm = BuildVm(alphabeticallyFirst,
|
|
new Lesson { GroupId = alphabeticallyFirst.Id, Date = today }, lessons: lessons,
|
|
allGroups: [alphabeticallyFirst, selectedDayGroup]);
|
|
|
|
vm.SelectCalendarDayCommand.Execute(vm.CalendarDays.Single(d => d.Date == selectedDate));
|
|
|
|
Assert.Equal(selectedDayGroup.Id, vm.CurrentGroups[0].GroupId);
|
|
Assert.True(vm.CurrentGroups[0].IsOnSelectedDay);
|
|
Assert.Equal(alphabeticallyFirst.Id, vm.CurrentGroups[1].GroupId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Kalender_ZeigtMitarbeitssitzungenAlsEigenenTermintyp()
|
|
{
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var sessionDate = today.AddDays(3);
|
|
var session = new ParticipationSession { GroupId = group.Id, Date = sessionDate, Comment = "Aufsatz" };
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
|
sessions: new FakeSessions([session]));
|
|
|
|
var day = vm.CalendarDays.Single(d => d.Date == sessionDate);
|
|
Assert.True(day.HasSession);
|
|
|
|
vm.SelectCalendarDayCommand.Execute(day);
|
|
|
|
Assert.Contains(vm.SelectedDayEvents,
|
|
e => e.Kind == CalendarEventKind.ParticipationSession && e.Subtitle == "Aufsatz");
|
|
}
|
|
|
|
[Fact]
|
|
public void Kalender_ZeigtMehrtaegigenJahresplanParallelZumUnterrichtAn()
|
|
{
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var annualPlan = new FakeAnnualPlanEvents();
|
|
annualPlan.Add(new AnnualPlanEvent
|
|
{
|
|
ExternalId = "fahrt", Title = "Klassenfahrt 6a",
|
|
Description = "Aushang beachten", Location = "Jugendherberge",
|
|
CalendarGroup = "Lehrkräfte", StartDate = today, EndDate = today.AddDays(2),
|
|
StartTime = new TimeOnly(13, 30), EndTime = new TimeOnly(15, 0), IsAllDay = false,
|
|
});
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today, Topic = "Redox" },
|
|
annualPlanEvents: annualPlan);
|
|
|
|
var day = vm.CalendarDays.Single(d => d.Date == today);
|
|
Assert.True(day.HasLesson);
|
|
Assert.True(day.HasAnnualPlanEvent);
|
|
|
|
vm.SelectCalendarDayCommand.Execute(day);
|
|
|
|
Assert.Contains(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.Lesson);
|
|
var annual = Assert.Single(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.AnnualPlan);
|
|
Assert.Equal("Klassenfahrt 6a", annual.Title);
|
|
Assert.Equal("Aushang beachten", annual.Description);
|
|
Assert.Contains("13:30", annual.Subtitle);
|
|
Assert.Null(annual.GroupId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Kalender_AusStundeErzeugteSitzungErzeugtKeinenDoppelteintrag()
|
|
{
|
|
// Regressionstest (Nutzer-Feedback): "Sitzung erzeugen" (3.3.1) verknüpft die Sitzung über
|
|
// LessonId mit der Stunde — dieselbe Unterrichtsstunde darf im Kalender nicht zusätzlich
|
|
// als eigener Sitzungstermin auftauchen.
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var lesson = new Lesson { GroupId = group.Id, Date = today, Topic = "Brechung" };
|
|
var session = new ParticipationSession { GroupId = group.Id, Date = today, LessonId = lesson.Id, Comment = "Brechung" };
|
|
var vm = BuildVm(group, lesson, sessions: new FakeSessions([session]));
|
|
|
|
var day = vm.CalendarDays.Single(d => d.Date == today);
|
|
Assert.True(day.HasLesson);
|
|
Assert.False(day.HasSession);
|
|
|
|
vm.SelectCalendarDayCommand.Execute(day);
|
|
|
|
Assert.Single(vm.SelectedDayEvents);
|
|
Assert.DoesNotContain(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.ParticipationSession);
|
|
}
|
|
|
|
[Fact]
|
|
public void Kalender_EigeneKlasseAmStundenplanTagOhneLessonZeigtDenRingTrotzdem()
|
|
{
|
|
// Regressionstest (Nutzer-Feedback): der "Meine Klasse"-Ring soll schon laut Stundenplan
|
|
// gelten, nicht erst, sobald für den Tag eine Lesson angelegt wurde.
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var tomorrow = today.AddDays(1);
|
|
var group = new LearningGroup { Name = "9c", IsOwnClass = true };
|
|
var slots = new FakeTimetableSlots();
|
|
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = tomorrow.DayOfWeek, PeriodNumber = 3 });
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots);
|
|
|
|
var day = vm.CalendarDays.Single(d => d.Date == tomorrow);
|
|
Assert.True(day.IsOwnClassDay);
|
|
}
|
|
|
|
[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);
|
|
}
|
|
|
|
[Fact]
|
|
public void AttendanceWarnings_KleineStichprobeMitHoherQuoteWirdNichtGemeldet()
|
|
{
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
|
var sessions = new FakeSessions([]);
|
|
var entries = new FakeEntries();
|
|
|
|
// Nur 3 erfasste Termine, alle unentschuldigt (100 %) — Stichprobe zu klein für eine Meldung.
|
|
for (var i = 0; i < 3; i++)
|
|
{
|
|
var session = new ParticipationSession { GroupId = group.Id, Date = today.AddDays(-i - 1) };
|
|
sessions.Save(session);
|
|
entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = student.Id, Attendance = AttendanceStatus.Unexcused });
|
|
}
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
|
students: new FakeStudents([student]), sessions: sessions, entries: entries);
|
|
|
|
Assert.Empty(vm.AttendanceWarnings);
|
|
}
|
|
|
|
[Fact]
|
|
public void AttendanceWarnings_MeldetHoheQuoteAbMindeststichprobe()
|
|
{
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
|
var sessions = new FakeSessions([]);
|
|
var entries = new FakeEntries();
|
|
|
|
// 10 erfasste Termine, 3 davon unentschuldigt (30 % > 20 % Schwelle) — genug Stichprobe.
|
|
for (var i = 0; i < 10; i++)
|
|
{
|
|
var session = new ParticipationSession { GroupId = group.Id, Date = today.AddDays(-i - 1) };
|
|
sessions.Save(session);
|
|
var status = i < 3 ? AttendanceStatus.Unexcused : AttendanceStatus.Present;
|
|
entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = student.Id, Attendance = status });
|
|
}
|
|
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
|
students: new FakeStudents([student]), sessions: sessions, entries: entries);
|
|
|
|
var item = Assert.Single(vm.AttendanceWarnings);
|
|
Assert.Equal(student.FullName, item.StudentName);
|
|
Assert.Equal(30.0, item.AbsenceRatePercent);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddTask_SpeichertErgebnisUndAktualisiertOffeneAufgaben()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var tasks = new FakeWorkTasks();
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, tasks: tasks);
|
|
vm.OnAddTask = startAsReminder => Task.FromResult<WorkTask?>(
|
|
new WorkTask { Title = "Ansage an die Klasse", Kind = startAsReminder ? TaskKind.Reminder : TaskKind.WorkItem });
|
|
|
|
await vm.AddReminderCommand.ExecuteAsync(null);
|
|
|
|
Assert.Single(tasks.GetAll());
|
|
Assert.Contains(vm.OpenTasks, t => t.Title == "Ansage an die Klasse" && t.IsReminder);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddTask_OhneDelegatMachtNichts()
|
|
{
|
|
var group = new LearningGroup { Name = "9c" };
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var tasks = new FakeWorkTasks();
|
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, tasks: tasks);
|
|
|
|
await vm.AddTaskCommand.ExecuteAsync(null);
|
|
|
|
Assert.Empty(tasks.GetAll());
|
|
}
|
|
|
|
[Fact]
|
|
public void WeatherWarningItem_BereitetAmtlicheWarnungFuerDashboardAuf()
|
|
{
|
|
var warning = new WeatherWarning
|
|
{
|
|
Event = "GEWITTER", Headline = "Amtliche Warnung vor Gewitter",
|
|
Description = "Es treten Gewitter auf.", Instruction = "Gebäude aufsuchen.",
|
|
Severity = "Severe", Onset = new DateTime(2026, 8, 23, 16, 0, 0, DateTimeKind.Utc),
|
|
Expires = new DateTime(2026, 8, 23, 18, 0, 0, DateTimeKind.Utc),
|
|
};
|
|
|
|
var item = new DashboardWeatherWarningItem(warning);
|
|
|
|
Assert.Equal("Amtliche Warnung vor Gewitter", item.Headline);
|
|
Assert.Equal("#D32F2F", item.SeverityColor);
|
|
Assert.Contains("Uhr", item.PeriodDisplay);
|
|
}
|
|
}
|