Files
LehrerApp/LehrerApp.Desktop.Tests/ReportGradeAssistantViewModelTests.cs
T
adminandClaude Opus 5.5 67c5a906d1
CI / build-and-test (push) Canceled after 0s
Zeugnisnoten-Assistent mit Mitarbeitsverlauf und Prüfungsnoten Klasse 10
Neuer Assistent (Einstieg im Zeugnisnoten-Dialog) geht die Lerngruppe
Schüler für Schüler durch: berechnete Note mit ungerundetem Wert und
Teilbereichen, Mitarbeitsgraph abschnittsweise oder als Verlauf, Arbeiten
und Einzelnoten. Das 1. Halbjahr erscheint im 2. Halbjahr nur als Kontext.

Die Endjahresnote wird jetzt in Dialog und Assistent aus den
Halbjahresnoten verrechnet (gemeinsamer ReportGradeCalculator). In Klasse 10
kommen Projektprüfung, schriftliche/mündliche Abschlussprüfung und
zusätzliche mündliche Prüfung hinzu, gewichtet nach Oberschulverordnung und
PrüfV Sek. I; gerundet wird dabei bewusst nicht automatisch.

Die Zeitleisten-Erzeugung des Mitarbeits-Assistenten liegt dafür im
gemeinsamen ParticipationTimelineBuilder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:09:33 +02:00

389 lines
15 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 ReportGradeAssistantViewModel Build(DateOnly today, int gradeLevel = 8)
{
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), new AttendanceBalanceService(), new GradingService(),
GroupId, GroupType.Class, GradingSystem.Grades1To6, "Testkurs", "2025/26", gradeLevel, today);
}
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));
}
}