Files
LehrerApp/LehrerApp.Desktop.Tests/ReportGradeCalculationTests.cs
T
adminandClaude Sonnet 5 a0f44f1b25 feat: Abwesenheits-Hinweise, Fehlquote bei Zeugnisnoten, Gruppen-Dokumentation
- Schnellbewerten-Dialog: abwesende Schüler werden gedimmt und mit ihrem
  Anwesenheitsstatus statt der Aspektbeschriftung angezeigt, damit keine
  Mitarbeitsnote für nicht anwesende Schüler vergeben wird.
- Zeugnisnoten-Dialog: zeigt je Schüler die Fehlquote im gewählten
  Zeitraum, ab 50 % hervorgehoben (informativ, keine automatische
  Notenänderung).
- Gruppen-Tab "Dokumentation" (bisher Platzhalter) implementiert: listet
  alle Dokumentationseinträge der Gruppen-Schüler, mit Schüler-Filter und
  optionalem "Nur dieser Unterricht"-Schalter. Einträge aus anderen
  Lerngruppen werden standardmäßig mitangezeigt, aber gedimmt. Der
  Dokumentationsdialog bekommt dafür einen optionalen Schüler-Picker.

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

222 lines
10 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 die Zeugnisnotenberechnung (2.4): ReportGradeDialogViewModel kombiniert
/// Klausuren, Mitarbeit und sonstige Noten gemäß Gewichtungsschema zu einer Zeugnisnote.
public class ReportGradeCalculationTests
{
private static Guid GroupId { get; } = Guid.NewGuid();
private static Student Anna { get; } = new() { Id = Guid.NewGuid(), FirstName = "Anna", LastName = "Beispiel" };
private static Student Ben { get; } = new() { Id = Guid.NewGuid(), FirstName = "Ben", LastName = "Muster" };
private static ReportGradeDialogViewModel BuildViewModel(
FakeGrades grades, FakeSchemes schemes, FakeReportGrades reportGrades,
FakeExams exams, FakeResults results, FakeSessions? sessions = null, FakeEntries? entries = null)
{
var students = new FakeStudents([Anna, Ben]);
var memberships = new FakeMemberships([]);
var grading = new GradingService();
return new ReportGradeDialogViewModel(grades, exams, results, students, memberships,
schemes, reportGrades, sessions ?? new FakeSessions([]), entries ?? new FakeEntries(),
new AttendanceBalanceService(), grading, GroupId, GroupType.Class, GradingSystem.Grades1To6,
"Testgruppe", "2025/26");
}
private static (Exam Exam1, Exam Exam2, FakeExams Exams, FakeResults Results) BuildExams()
{
var exam1 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 10), Title = "K1" };
var exam2 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 24), Title = "K2" };
var exams = new FakeExams([exam1, exam2]);
var results = new FakeResults();
results.Add(new ExamResult { ExamId = exam1.Id, StudentId = Anna.Id, Grade = "2" });
results.Add(new ExamResult { ExamId = exam2.Id, StudentId = Anna.Id, Grade = "4" });
return (exam1, exam2, exams, results);
}
[Fact]
public void CalculatedGrade_KombiniertKlausurenMitarbeitUndSonstigeGemaessSchema()
{
var (_, _, exams, results) = BuildExams();
var grades = new FakeGrades();
grades.Add(new Grade { StudentId = Anna.Id, GroupId = GroupId, Category = GradeCategory.Participation,
Date = new DateOnly(2025, 9, 5), Value = "3", Weight = 1.0 });
grades.Add(new Grade { StudentId = Anna.Id, GroupId = GroupId, Category = GradeCategory.Other,
Date = new DateOnly(2025, 9, 5), Value = "1", Weight = 1.0 });
var schemes = new FakeSchemes();
schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 });
var vm = BuildViewModel(grades, schemes, new FakeReportGrades(), exams, results);
// Klausuren Ø3 (Note 2 und 4), Mitarbeit 3, Sonstige 1 -> (3*50+3*40+1*10)/100 = 2.8 -> kaufmännisch 3
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
Assert.Equal("3", anna.CalculatedValue);
}
[Fact]
public void CalculatedGrade_SchuelerOhneJedeNote_ZeigtKeinenWert()
{
var (_, _, exams, results) = BuildExams();
var schemes = new FakeSchemes();
var vm = BuildViewModel(new FakeGrades(), schemes, new FakeReportGrades(), exams, results);
var ben = vm.Rows.Single(r => r.StudentId == Ben.Id);
Assert.Null(ben.CalculatedValue);
Assert.Equal("", ben.CalculatedDisplay);
}
[Fact]
public void AbsenceRatePercent_BerechnetFehlquoteAusSitzungenDieserGruppeImZeitraum()
{
var (_, _, exams, results) = BuildExams();
var session1 = new ParticipationSession { GroupId = GroupId, Date = new DateOnly(2025, 9, 5) };
var session2 = new ParticipationSession { GroupId = GroupId, Date = new DateOnly(2025, 9, 12) };
var sessions = new FakeSessions([session1, session2]);
var entries = new FakeEntries();
entries.Add(new ParticipationEntry { SessionId = session1.Id, StudentId = Anna.Id, Attendance = AttendanceStatus.Present });
entries.Add(new ParticipationEntry { SessionId = session2.Id, StudentId = Anna.Id, Attendance = AttendanceStatus.Unexcused });
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), new FakeReportGrades(), exams, results,
sessions, entries);
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
Assert.Equal(50.0, anna.AbsenceRatePercent);
Assert.True(anna.HasHighAbsenceRate);
var ben = vm.Rows.Single(r => r.StudentId == Ben.Id);
Assert.Equal(0.0, ben.AbsenceRatePercent);
Assert.False(ben.HasHighAbsenceRate);
}
[Fact]
public void ResolveScheme_BevorzugtGruppenspezifischesSchemaVorVoreinstellung()
{
var (_, _, exams, results) = BuildExams();
var grades = new FakeGrades();
var schemes = new FakeSchemes();
schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 100, ParticipationPercent = 0, OtherPercent = 0 });
schemes.SetDefault(GroupType.Class, new GradingScheme { ExamsPercent = 0, ParticipationPercent = 0, OtherPercent = 100 });
var vm = BuildViewModel(grades, schemes, new FakeReportGrades(), exams, results);
// Gruppenspezifisches Schema gewichtet nur Klausuren (Ø3 aus Note 2 und 4) -> exakt "3"
Assert.Contains("Klausuren 100", vm.SchemeSummary);
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
Assert.Equal("3", anna.CalculatedValue);
}
[Fact]
public void ResolveScheme_OhneGruppenschema_VerwendetVoreinstellungDesGruppentyps()
{
var (_, _, exams, results) = BuildExams();
var schemes = new FakeSchemes();
schemes.SetDefault(GroupType.Class, new GradingScheme { ExamsPercent = 70, ParticipationPercent = 20, OtherPercent = 10 });
var vm = BuildViewModel(new FakeGrades(), schemes, new FakeReportGrades(), exams, results);
Assert.Contains("Klausuren 70", vm.SchemeSummary);
}
[Fact]
public void ResolveScheme_OhneJedeVoreinstellung_VerwendetHartcodiertenFallback()
{
var (_, _, exams, results) = BuildExams();
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), new FakeReportGrades(), exams, results);
Assert.Contains("Klausuren 50", vm.SchemeSummary);
Assert.Contains("Mitarbeit 40", vm.SchemeSummary);
Assert.Contains("Sonstige 10", vm.SchemeSummary);
}
[Fact]
public void Save_UebersteuernOhneBegruendung_LiefertValidierungsfehlerUndSpeichertNicht()
{
var (_, _, exams, results) = BuildExams();
var reportGrades = new FakeReportGrades();
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, exams, results);
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
anna.OverrideValue = "1";
anna.SaveCommand.Execute(null);
Assert.False(string.IsNullOrEmpty(anna.ValidationMessage));
Assert.Equal(0, reportGrades.SavedCount);
}
[Fact]
public void Save_UebersteuernMitBegruendung_AktualisiertFinalDisplay()
{
var (_, _, exams, results) = BuildExams();
var reportGrades = new FakeReportGrades();
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, exams, results);
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
anna.OverrideValue = "1";
anna.OverrideReason = "Mündliche Steigerung im letzten Quartal";
anna.SaveCommand.Execute(null);
Assert.True(string.IsNullOrEmpty(anna.ValidationMessage));
Assert.Equal("1", anna.FinalDisplay);
Assert.Equal(1, reportGrades.SavedCount);
}
[Fact]
public void ToggleLock_FriertDenStandEinUndIgnoriertSpaetereNeuberechnung()
{
var (exam1, _, exams, results) = BuildExams();
var reportGrades = new FakeReportGrades();
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, exams, results);
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
anna.OverrideValue = "1";
anna.OverrideReason = "Testgrund";
anna.SaveCommand.Execute(null);
anna.ToggleLockCommand.Execute(null);
// Neue, bessere Klausurnote nachtragen -> würde die Berechnung ändern, wenn ungesperrt.
results.Add(new ExamResult { ExamId = exam1.Id, StudentId = Anna.Id, Grade = "1" });
vm.SelectedPeriod = vm.PeriodOptions.First(p => p.Period == ParticipationPeriod.H1);
vm.SelectedPeriod = vm.PeriodOptions.First(p => p.Period == ParticipationPeriod.FullYear);
var annaNachSperre = vm.Rows.Single(r => r.StudentId == Anna.Id);
Assert.True(annaNachSperre.IsLocked);
Assert.Equal("1", annaNachSperre.FinalDisplay);
}
[Fact]
public void ExportCsv_EnthaeltAlleSchuelerDerGruppe()
{
var (_, _, exams, results) = BuildExams();
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), new FakeReportGrades(), exams, results);
var csv = vm.ExportCsv();
Assert.Contains("Beispiel, Anna", csv);
Assert.Contains("Muster, Ben", csv);
}
[Fact]
public void RoundingRule_Paedagogisch_RundetGrenzfallZurBesserenNote()
{
var exam1 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 10), Title = "K1" };
var exam2 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 24), Title = "K2" };
var exams = new FakeExams([exam1, exam2]);
var results = new FakeResults();
// Ø exakt 2.5 -> kaufmännisch 3, pädagogisch 2 (bessere Note)
results.Add(new ExamResult { ExamId = exam1.Id, StudentId = Anna.Id, Grade = "2" });
results.Add(new ExamResult { ExamId = exam2.Id, StudentId = Anna.Id, Grade = "3" });
var schemes = new FakeSchemes();
schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 100, ParticipationPercent = 0, OtherPercent = 0 });
var vm = BuildViewModel(new FakeGrades(), schemes, new FakeReportGrades(), exams, results);
Assert.Equal("3", vm.Rows.Single(r => r.StudentId == Anna.Id).CalculatedValue);
vm.RoundingRuleName = "Pädagogisch";
Assert.Equal("2", vm.Rows.Single(r => r.StudentId == Anna.Id).CalculatedValue);
}
}