CI / build-and-test (push) Canceled after 0s
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>
389 lines
15 KiB
C#
389 lines
15 KiB
C#
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));
|
||
}
|
||
}
|