Files
LehrerApp/LehrerApp.Desktop.Tests/ReportGradeAssistantViewModelTests.cs
T
adminandClaude Opus 5.5 d9eac0d1d2
CI / build-and-test (push) Canceled after 0s
Zwischenstand: vorläufige Noten zu Stichtag und Anlass
Ein Zwischenstand ist ein Durchgang aus Stichtag, Anlass und
Schülerauswahl (ganze Gruppe, Teilgruppe oder Einzelfall, mit
Schnellfilter für gefährdete Schüler). Der Zeugnisnoten-Assistent geht
die gewählten Schüler im Zwischenstand-Modus durch und notiert
vorläufige schriftliche, mündliche und Gesamtnote samt Notiz.

Gespeichert wird das als eigenes Modell InterimGrade, getrennt von der
Zeugnisnote. Die Berechnung endet am Stichtag und rechnet den laufenden
Mitarbeits-Abschnitt mit ein. Zwischenstände erscheinen im Assistenten
als Liste und Marker im Verlauf sowie als graue Spalte in der
Notenübersicht (nicht in "Gesamt", nicht im PDF).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:01:34 +02:00

576 lines
23 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.Groups;
using Xunit;
namespace LehrerApp.Desktop.Tests;
/// Tests für den Zeugnisnoten-Assistenten: eigenständige Halbjahre (H1 nur als Kontext in H2),
/// Endjahresnote aus den beiden Halbjahresnoten, Speichern/Festschreiben über dieselben
/// ReportGrade-Datensätze wie der Zeugnisnoten-Dialog sowie die beiden Graph-Modi.
public sealed class ReportGradeAssistantViewModelTests
{
private static readonly Guid GroupId = Guid.NewGuid();
private static readonly Student Anna = new() { Id = Guid.NewGuid(), FirstName = "Anna", LastName = "Beispiel" };
private static readonly Student Ben = new() { Id = Guid.NewGuid(), FirstName = "Ben", LastName = "Muster" };
private sealed class Fixture
{
public List<Exam> Exams { get; } = [];
public FakeResults Results { get; } = new();
public FakeGrades Grades { get; } = new();
public FakeReportGrades ReportGrades { get; } = new();
public List<ParticipationSession> Sessions { get; } = [];
public FakeEntries Entries { get; } = new();
public List<ParticipationSection> Sections { get; } = [];
public FakeInterimGrades InterimGrades { get; } = new();
public ReportGradeAssistantViewModel Build(DateOnly today, int gradeLevel = 8, InterimRunOptions? interim = null)
{
var schemes = new FakeSchemes();
schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 });
return new ReportGradeAssistantViewModel(
Grades, new FakeExams(Exams), Results, new FakeStudents([Anna, Ben]), new FakeMemberships([]),
schemes, ReportGrades, new FakeSessions(Sessions), Entries, new FakeAspects(),
new FakeSections(Sections), InterimGrades, new AttendanceBalanceService(), new GradingService(),
GroupId, GroupType.Class, GradingSystem.Grades1To6, "Testkurs", "2025/26", gradeLevel, today, interim);
}
public InterimRunDialogViewModel BuildRunDialog(DateOnly today, Guid? preselected = null)
{
var schemes = new FakeSchemes();
schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 });
return new InterimRunDialogViewModel(
Grades, new FakeExams(Exams), Results, new FakeStudents([Anna, Ben]), new FakeMemberships([]),
schemes, ReportGrades, new FakeSessions(Sessions), Entries, new FakeAspects(),
new FakeSections(Sections), InterimGrades, new GradingService(),
GroupId, GroupType.Class, GradingSystem.Grades1To6, "Testkurs", "2025/26", preselected, today);
}
public void AddSession(DateOnly date, int rating)
{
var session = new ParticipationSession { Id = Guid.NewGuid(), GroupId = GroupId, Date = date };
Sessions.Add(session);
Entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = Anna.Id,
Ratings = [new AspectRating { Key = "quality", Value = rating }] });
}
public Exam AddExam(string title, DateOnly date, string annaGrade)
{
var exam = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Title = title, Date = date };
Exams.Add(exam);
Results.Add(new ExamResult { ExamId = exam.Id, StudentId = Anna.Id, Grade = annaGrade });
return exam;
}
public void AddParticipation(DateOnly date, string value, string? note = null) =>
Grades.Add(new Grade { StudentId = Anna.Id, GroupId = GroupId, Category = GradeCategory.Participation,
Date = date, Value = value, Note = note });
}
private static readonly DateOnly InH1 = new(2025, 11, 10);
private static readonly DateOnly InH2 = new(2026, 4, 20);
[Fact]
public void Vorbelegung_WaehltDasLaufendeHalbjahr()
{
Assert.Equal(ParticipationPeriod.H1, new Fixture().Build(InH1).SelectedPeriod.Period);
Assert.Equal(ParticipationPeriod.H2, new Fixture().Build(InH2).SelectedPeriod.Period);
}
[Fact]
public void Halbjahr_BerechnetNoteUndZeigtTeilbereiche()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "2");
f.AddParticipation(new DateOnly(2025, 10, 15), "3");
var vm = f.Build(InH1);
// (2*50 + 3*40) / 90 = 2,44 -> 2
Assert.Equal("2", vm.CalculatedDisplay);
Assert.Contains("2,44", vm.CalculationDetail);
Assert.Equal(["2,00", "3,00", "–"], vm.Tiles.Select(t => t.Value));
Assert.Equal("2", vm.Students[0].GradeDisplay);
Assert.Equal(AssistantGradeStatus.Open, vm.Students[0].Status);
}
[Fact]
public void ZweitesHalbjahr_ErstesZaehltNichtWirdAberAlsKontextGezeigt()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "5");
f.AddExam("K2", new DateOnly(2026, 3, 1), "2");
f.AddParticipation(new DateOnly(2025, 12, 1), "5");
var vm = f.Build(InH2);
Assert.Equal("2", vm.CalculatedDisplay);
Assert.Equal(2, vm.Works.Count);
Assert.True(vm.Works[0].IsContext);
Assert.False(vm.Works[1].IsContext);
Assert.True(Assert.Single(vm.ParticipationGrades).IsContext);
vm.ShowPreviousHalf = false;
Assert.Equal("K2", Assert.Single(vm.Works).Title);
Assert.Empty(vm.ParticipationGrades);
Assert.Equal("2", vm.CalculatedDisplay);
}
[Fact]
public void Schuljahr_VerrechnetHalbjahresnotenUndBevorzugtGespeicherteEntscheidung()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "2");
f.AddExam("K2", new DateOnly(2026, 3, 1), "4");
var vm = f.Build(InH2);
vm.SelectedPeriod = vm.PeriodOptions.Single(p => p.Period == ParticipationPeriod.FullYear);
// (2 + 4) / 2 = 3
Assert.Equal("3", vm.CalculatedDisplay);
Assert.Equal(["2", "4"], vm.Tiles.Select(t => t.Value));
// Pädagogisch übersteuerte Halbjahresnote geht in die Verrechnung ein, nicht die berechnete.
f.ReportGrades.Save(new ReportGrade { StudentId = Anna.Id, GroupId = GroupId, Period = "1. Halbjahr",
CalculatedValue = "2", OverrideValue = "1", OverrideReason = "starke Entwicklung" });
vm.SelectedPeriod = vm.PeriodOptions.Single(p => p.Period == ParticipationPeriod.H2);
vm.SelectedPeriod = vm.PeriodOptions.Single(p => p.Period == ParticipationPeriod.FullYear);
// (1 + 4) / 2 = 2,5 -> kaufmännisch 3, pädagogisch 2
Assert.Equal("3", vm.CalculatedDisplay);
Assert.Contains("2,50", vm.CalculationDetail);
vm.RoundingRule = RoundingRule.Pedagogical;
Assert.Equal("2", vm.CalculatedDisplay);
}
[Fact]
public void Klasse10_PruefungsnotenZaehlenWieWeitereHalbjahre()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "2");
f.AddExam("K2", new DateOnly(2026, 3, 1), "2");
var vm = f.Build(InH2, gradeLevel: 10);
vm.SelectedPeriod = vm.PeriodOptions.Single(p => p.Period == ParticipationPeriod.FullYear);
Assert.True(vm.CanEnterExamGrades);
Assert.Equal("2", vm.CalculatedDisplay);
// (2 + 2 + 5) / 3 = 3
vm.ProjectExamGrade = "5";
Assert.Equal("3", vm.CalculatedDisplay);
Assert.Equal(3, vm.Tiles.Count);
// (2 + 2 + 5 + 5) / 4 = 3,5 -> 4
vm.OralExamGrade = "5";
Assert.Equal("4", vm.CalculatedDisplay);
Assert.Contains("4 Teilnoten", vm.CalculationDetail);
vm.SaveCommand.Execute(null);
var record = f.ReportGrades.GetByStudentGroupPeriod(Anna.Id, GroupId, "Gesamtes Schuljahr");
Assert.Equal("5", record!.ProjectExamGrade);
Assert.Equal("5", record.OralExamGrade);
Assert.Equal("4", record.CalculatedValue);
Assert.Equal("4", vm.Students[0].GradeDisplay);
}
private static ReportGradeAssistantViewModel Klasse10Schuljahr(Fixture f, string h1, string h2)
{
f.AddExam("K1", new DateOnly(2025, 10, 1), h1);
f.AddExam("K2", new DateOnly(2026, 3, 1), h2);
var vm = f.Build(InH2, gradeLevel: 10);
vm.SelectedPeriod = vm.PeriodOptions.Single(p => p.Period == ParticipationPeriod.FullYear);
return vm;
}
[Fact]
public void Klasse10_SchriftlichePruefungGehtMitEinemDrittelEin()
{
var vm = Klasse10Schuljahr(new Fixture(), "2", "2");
// § 14 Abs. 3 ObSchVO: (2 * 2 + 5) / 3 = 3,0
vm.WrittenExamGrade = "5";
Assert.Equal("3", vm.CalculatedDisplay);
}
[Fact]
public void Klasse10_ZusaetzlicheMuendlicheImSchriftlichenFach_EinsZuEinsOhneRundung()
{
var f = new Fixture();
var vm = Klasse10Schuljahr(f, "4", "4");
vm.WrittenExamGrade = "6";
// (4 + 4 + 6) / 3 = 4,67 -> 5
Assert.Equal("5", vm.CalculatedDisplay);
// § 13 Abs. 2 PrüfV: (6 + 3) / 2 = 4,5 — bleibt ungerundet; (4 + 4 + 4,5) / 3 = 4,17 -> 4
vm.AdditionalOralExamGrade = "3";
Assert.Equal("4", vm.CalculatedDisplay);
Assert.Contains("4,17", vm.CalculationDetail);
var tile = vm.Tiles.Single(t => t.Label == "Prüfungsleistung");
Assert.Equal("4,5", tile.Value);
Assert.Contains("zwischen 4 und 5", tile.Hint);
vm.SaveCommand.Execute(null);
var record = f.ReportGrades.GetByStudentGroupPeriod(Anna.Id, GroupId, "Gesamtes Schuljahr");
Assert.Equal("6", record!.WrittenExamGrade);
Assert.Equal("3", record.AdditionalOralExamGrade);
}
[Fact]
public void GenauZwischenZweiNoten_WeistAufPaedagogischeEntscheidungHin()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "2");
f.AddExam("K2", new DateOnly(2026, 3, 1), "3");
var vm = f.Build(InH2);
Assert.Empty(vm.BorderlineHint);
vm.SelectedPeriod = vm.PeriodOptions.Single(p => p.Period == ParticipationPeriod.FullYear);
// (2 + 3) / 2 = 2,5
Assert.Contains("zwischen 2 und 3", vm.BorderlineHint);
vm.ToggleLockCommand.Execute(null);
Assert.Empty(vm.BorderlineHint);
}
[Fact]
public void Klasse10_NachpruefungOhneAbschlusspruefung_ZweiZuEins()
{
var vm = Klasse10Schuljahr(new Fixture(), "5", "5");
// § 13 Abs. 3 PrüfV: (2 * 5 + 3) / 3 = 4,33 -> 4
vm.AdditionalOralExamGrade = "3";
Assert.Equal("4", vm.CalculatedDisplay);
}
[Fact]
public void Klasse10_NurEinHalbjahr_UnterrichtlicheLeistungZaehltTrotzdemDoppelt()
{
var f = new Fixture();
f.AddExam("K2", new DateOnly(2026, 3, 1), "2");
var vm = f.Build(InH2, gradeLevel: 10);
vm.SelectedPeriod = vm.PeriodOptions.Single(p => p.Period == ParticipationPeriod.FullYear);
// (2 * 2 + 5) / 3 = 3 — nicht (2 + 5) / 2 = 3,5 -> 4
vm.OralExamGrade = "5";
Assert.Equal("3", vm.CalculatedDisplay);
}
[Fact]
public void Klasse10_WiderspruechlichePruefungen_WerdenAbgelehnt()
{
var f = new Fixture();
var vm = Klasse10Schuljahr(f, "3", "3");
vm.WrittenExamGrade = "3";
vm.OralExamGrade = "2";
vm.SaveCommand.Execute(null);
Assert.NotEmpty(vm.OralExamGradeError);
vm.WrittenExamGrade = "";
vm.AdditionalOralExamGrade = "4";
vm.SaveCommand.Execute(null);
Assert.NotEmpty(vm.AdditionalOralExamGradeError);
Assert.Equal(0, f.ReportGrades.SavedCount);
}
[Fact]
public void Klasse10_UngueltigePruefungsnote_WirdNichtGespeichert()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "2");
var vm = f.Build(InH2, gradeLevel: 10);
vm.SelectedPeriod = vm.PeriodOptions.Single(p => p.Period == ParticipationPeriod.FullYear);
vm.ProjectExamGrade = "gut";
vm.SaveCommand.Execute(null);
Assert.NotEmpty(vm.ProjectExamGradeError);
Assert.Equal(0, f.ReportGrades.SavedCount);
}
[Fact]
public void PruefungsnotenNurInKlasse10UndNurFuerDasSchuljahr()
{
var klasse8 = new Fixture().Build(InH2);
klasse8.SelectedPeriod = klasse8.PeriodOptions.Single(p => p.Period == ParticipationPeriod.FullYear);
Assert.False(klasse8.CanEnterExamGrades);
Assert.False(new Fixture().Build(InH2, gradeLevel: 10).CanEnterExamGrades);
}
[Fact]
public void Speichern_UebersteuernOhneBegruendung_WirdAbgelehnt()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "3");
var vm = f.Build(InH1);
vm.OverrideValue = "2";
vm.SaveCommand.Execute(null);
Assert.NotEmpty(vm.OverrideReasonError);
Assert.Equal(0, f.ReportGrades.SavedCount);
vm.OverrideReason = "mündlich deutlich stärker";
vm.SaveCommand.Execute(null);
var record = f.ReportGrades.GetByStudentGroupPeriod(Anna.Id, GroupId, "1. Halbjahr");
Assert.NotNull(record);
Assert.Equal("3", record.CalculatedValue);
Assert.Equal("2", record.OverrideValue);
Assert.Equal("2", vm.Students[0].GradeDisplay);
Assert.Equal(AssistantGradeStatus.Saved, vm.Students[0].Status);
}
[Fact]
public void SpeichernUndWeiter_GehtZumNaechstenSchueler()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "3");
var vm = f.Build(InH1);
vm.SaveAndNextCommand.Execute(null);
Assert.Equal(Ben.Id, vm.SelectedStudent!.StudentId);
Assert.Equal("2 / 2", vm.ProgressText);
Assert.False(vm.NextStudentCommand.CanExecute(null));
}
[Fact]
public void Festschreiben_FriertBerechnetenStandEin()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "3");
var vm = f.Build(InH1);
vm.ToggleLockCommand.Execute(null);
Assert.True(vm.IsLocked);
Assert.Equal(AssistantGradeStatus.Locked, vm.Students[0].Status);
// Eine später eingetragene Arbeit ändert den festgeschriebenen Stand nicht mehr.
f.AddExam("K2", new DateOnly(2025, 12, 1), "1");
vm.SelectedPeriod = vm.PeriodOptions.Single(p => p.Period == ParticipationPeriod.H2);
vm.SelectedPeriod = vm.PeriodOptions.Single(p => p.Period == ParticipationPeriod.H1);
Assert.Equal("3", vm.CalculatedDisplay);
vm.ToggleLockCommand.Execute(null);
Assert.False(vm.IsLocked);
Assert.Equal("2", vm.CalculatedDisplay);
}
[Fact]
public void Mitarbeitsgraph_AbschnitteBlaetternUndVerlaufAmStueck()
{
var f = new Fixture();
f.Sections.Add(new ParticipationSection { GroupId = GroupId, Label = "Abschnitt 1",
StartDate = new DateOnly(2025, 8, 20), EndDate = new DateOnly(2025, 9, 30) });
f.Sections.Add(new ParticipationSection { GroupId = GroupId, Label = "Abschnitt 2",
StartDate = new DateOnly(2025, 10, 1), EndDate = new DateOnly(2025, 10, 31) });
f.AddParticipation(new DateOnly(2025, 9, 30), "2", "Abschnitt: Abschnitt 1");
foreach (var date in new[] { new DateOnly(2025, 9, 2), new DateOnly(2025, 10, 7), new DateOnly(2025, 11, 4) })
{
var session = new ParticipationSession { Id = Guid.NewGuid(), GroupId = GroupId, Date = date };
f.Sessions.Add(session);
f.Entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = Anna.Id,
Ratings = [new AspectRating { Key = "quality", Value = 1 }] });
}
var vm = f.Build(InH1);
Assert.True(vm.IsSectionMode);
Assert.Equal("1 / 3", vm.SectionPosition);
Assert.Contains("Abschnittsnote 2", vm.SectionTitle);
Assert.Single(Assert.Single(vm.ChartGroups).Points);
Assert.False(vm.PreviousSectionCommand.CanExecute(null));
vm.NextSectionCommand.Execute(null);
Assert.Contains("keine Abschnittsnote", vm.SectionTitle);
vm.NextSectionCommand.Execute(null);
Assert.Contains("vorläufig", vm.SectionTitle);
Assert.True(vm.ChartGroups[0].IsOpen);
Assert.False(vm.NextSectionCommand.CanExecute(null));
vm.IsSectionMode = false;
Assert.Equal(3, vm.ChartGroups.Count);
Assert.Equal(3, vm.ChartGroups.Sum(g => g.Points.Count));
}
// ── Zwischenstand (2.7.5) ────────────────────────────────────────────────
private static InterimRunOptions Sprechtag(params Guid[] students) =>
new(new DateOnly(2025, 11, 12), "Sprechtag", students);
[Fact]
public void Zwischenstand_RechnetNurBisZumStichtagUndMitLaufenderMitarbeit()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "2");
f.AddExam("K2", new DateOnly(2025, 12, 1), "6"); // nach dem Stichtag
f.AddSession(new DateOnly(2025, 10, 20), -2); // laufender Abschnitt, schwache Mitarbeit
var vm = f.Build(InH1, interim: Sprechtag(Anna.Id));
Assert.True(vm.IsInterimMode);
Assert.Equal(Anna.Id, Assert.Single(vm.Students).StudentId);
Assert.Equal("K1", Assert.Single(vm.Works).Title);
Assert.Equal("2", vm.InterimWritten);
Assert.NotNull(vm.InterimOral);
Assert.Contains("laufender Abschnitt", vm.Tiles.Single(t => t.Label == "Mitarbeit").Hint);
// Die schwache laufende Mitarbeit zieht den Gesamtstand unter die reine Klausurnote.
Assert.True(int.Parse(vm.InterimOverall!) > 2);
Assert.Contains("12.11.2025", vm.CalculationDetail);
// Die Zeugnisnoten-Berechnung selbst bleibt ohne laufenden Abschnitt.
var report = f.Build(InH1);
Assert.Equal("–", report.Tiles.Single(t => t.Label == "Mitarbeit").Value);
}
[Fact]
public void Zwischenstand_NotierenSpeichertGetrenntVonDerZeugnisnote()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "3");
var vm = f.Build(InH1, interim: Sprechtag(Anna.Id, Ben.Id));
vm.InterimOverall = "";
vm.NoteInterimCommand.Execute(null);
Assert.NotEmpty(vm.InterimOverallError);
Assert.Empty(f.InterimGrades.All);
vm.InterimOverall = "3-";
vm.InterimNote = "Mitarbeit muss besser werden";
vm.NoteInterimAndNextCommand.Execute(null);
var entry = Assert.Single(f.InterimGrades.All);
Assert.Equal(Anna.Id, entry.StudentId);
Assert.Equal(new DateOnly(2025, 11, 12), entry.Date);
Assert.Equal("Sprechtag", entry.Occasion);
Assert.Equal("3", entry.WrittenValue);
Assert.Equal("3-", entry.OverallValue);
Assert.Equal(0, f.ReportGrades.SavedCount);
Assert.Equal(AssistantGradeStatus.Noted, vm.Students[0].Status);
Assert.Equal("3-", vm.Students[0].GradeDisplay);
Assert.Equal(Ben.Id, vm.SelectedStudent!.StudentId);
// Erneutes Notieren im selben Durchgang überschreibt statt zu verdoppeln.
vm.SelectedStudent = vm.Students[0];
Assert.True(vm.HasInterimEntry);
Assert.Equal("3-", vm.InterimOverall);
vm.InterimOverall = "4";
vm.NoteInterimCommand.Execute(null);
Assert.Equal("4", Assert.Single(f.InterimGrades.All).OverallValue);
vm.RemoveInterimCommand.Execute(null);
Assert.Empty(f.InterimGrades.All);
Assert.Equal(AssistantGradeStatus.Open, vm.Students[0].Status);
}
[Fact]
public void Zwischenstaende_ErscheinenImZeugnisnotenModusAlsListeUndMarker()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "3");
f.InterimGrades.Save(new InterimGrade { StudentId = Anna.Id, GroupId = GroupId,
Date = new DateOnly(2025, 11, 12), Occasion = "Sprechtag", WrittenValue = "3", OverallValue = "3" });
var vm = f.Build(new DateOnly(2025, 12, 10));
var row = Assert.Single(vm.InterimEntries);
Assert.Equal("12.11.2025 · Sprechtag", row.Title);
Assert.Equal("schriftlich 3", row.Detail);
Assert.Contains(vm.ChartGroups.SelectMany(g => g.Points), p => p.ExamLabel == "Zwischenstand: 3");
Assert.True(vm.IsReportMode);
}
[Fact]
public void Startdialog_VorauswahlGefaehrdeteUndBestaetigen()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "5");
f.Results.Add(new ExamResult { ExamId = f.Exams[0].Id, StudentId = Ben.Id, Grade = "2" });
var vm = f.BuildRunDialog(new DateOnly(2025, 11, 12));
Assert.All(vm.Students, s => Assert.True(s.IsSelected));
Assert.Equal(["5", "2"], vm.Students.Select(s => s.ProvisionalDisplay));
vm.SelectAtRiskCommand.Execute(null);
Assert.Equal([Anna.Id], vm.Students.Where(s => s.IsSelected).Select(s => s.StudentId));
vm.OccasionName = "Blauer Brief";
Assert.True(vm.Confirm());
Assert.Equal(new DateOnly(2025, 11, 12), vm.Result!.Date);
Assert.Equal("Blauer Brief", vm.Result.Occasion);
Assert.Equal([Anna.Id], vm.Result.StudentIds);
}
[Fact]
public void Startdialog_EinzelfallUndValidierung()
{
var f = new Fixture();
var vm = f.BuildRunDialog(new DateOnly(2025, 11, 12), preselected: Ben.Id);
Assert.Equal([Ben.Id], vm.Students.Where(s => s.IsSelected).Select(s => s.StudentId));
vm.OccasionName = "Elternanfrage";
vm.OccasionDetail = "Frau Muster";
Assert.Equal("Elternanfrage: Frau Muster", vm.Occasion);
vm.SelectNoneCommand.Execute(null);
Assert.False(vm.Confirm());
Assert.NotEmpty(vm.SelectionError);
vm.DateText = "12.11.2030";
Assert.False(vm.Confirm());
Assert.NotEmpty(vm.DateTextError);
}
[Fact]
public void Startdialog_BegonnenenDurchgangFortsetzen()
{
var f = new Fixture();
f.InterimGrades.Save(new InterimGrade { StudentId = Anna.Id, GroupId = GroupId,
Date = new DateOnly(2025, 11, 12), Occasion = "Sprechtag: 1. Elternsprechtag", OverallValue = "3" });
var vm = f.BuildRunDialog(new DateOnly(2025, 12, 1));
vm.SelectedExistingRun = Assert.Single(vm.ExistingRuns);
Assert.Equal("12.11.2025", vm.DateText);
Assert.Equal("Sprechtag: 1. Elternsprechtag", vm.Occasion);
Assert.Equal("notiert: 3", vm.Students.Single(s => s.StudentId == Anna.Id).NotedDisplay);
Assert.Equal("", vm.Students.Single(s => s.StudentId == Ben.Id).NotedDisplay);
}
[Fact]
public void Notenuebersicht_ZeigtZwischenstaendeAlsEigeneSpalteOhneEinflussAufGesamt()
{
var f = new Fixture();
f.AddExam("K1", new DateOnly(2025, 10, 1), "2");
f.InterimGrades.Save(new InterimGrade { StudentId = Anna.Id, GroupId = GroupId,
Date = new DateOnly(2025, 11, 12), Occasion = "Sprechtag", OverallValue = "5" });
var vm = new GradeOverviewTabViewModel(f.Grades, new FakeExams(f.Exams), f.Results,
new FakeStudents([Anna, Ben]), new FakeMemberships([]), f.InterimGrades, new GradingService());
vm.Initialize(GroupId, GradingSystem.Grades1To6, GroupType.Class, "Testkurs", "2025/26");
var column = vm.Columns.Last();
Assert.True(column.IsInterim);
Assert.Equal("Zwischenstand 12.11. Sprechtag", column.Header);
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
Assert.Equal("5", anna.Cells.Last());
Assert.Equal("2.00", anna.TotalDisplay);
Assert.Equal("", vm.Rows.Single(r => r.StudentId == Ben.Id).Cells.Last());
}
}