Files
LehrerApp/LehrerApp.Desktop.Tests/PlanningTabViewModelTests.cs
T
admin 8495e1b8d0 KI-gestützte Planungsunterstützung (4.5.9) + Kompetenzkatalog-Import (8.1.2)
KI-Unterstützung: neuer Einstellungen-Tab (Anmeldung, Guthaben) und Button im
Planungs-Tab, der Einheiten+Stunden als JSON an ein neues PHP-Backend (ai-backend/)
sendet und die Antwort als prüfbare Vorschlagsliste zurückbringt. Provider-Aufruf,
Guthabenverwaltung und Abrechnung nach echten Token-Kosten laufen serverseitig, der
Desktop-Client sieht nie einen LLM-API-Key. Zentral abgesichert: eine von der KI
zurückgegebene Stunden-Id, die zu keiner echten Lesson der Einheit passt, wird nie
als Update übernommen, sondern immer als neue Stunde behandelt.

Kompetenzkatalog-Import (8.1.2): JSON-Export/Import für Kompetenzkataloge.
2026-08-16 01:31:14 +02:00

231 lines
11 KiB
C#

using LehrerApp.Core.Models;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using Xunit;
namespace LehrerApp.Desktop.Tests;
/// Tests für die Unterrichtsplanung (4.1 Einheiten / 4.2 Einzelstunden): Fortschrittsberechnung,
/// Verschieben mit/ohne Nachrücken der Folgestunden und die Vorlage-Kopie in eine andere Gruppe.
public class PlanningTabViewModelTests
{
private static (PlanningTabViewModel Vm, FakeUnits Units, FakeLessons Lessons, Guid GroupId) BuildScenario()
{
var groupId = Guid.NewGuid();
var group = new LearningGroup { Id = groupId, Name = "Testgruppe" };
var groups = new FakeGroups([group]);
var subjects = new FakeSubjects([]);
var competencyDomains = new FakeCompetencyDomains();
var units = new FakeUnits();
var lessons = new FakeLessons();
var vm = new PlanningTabViewModel(units, lessons, groups, subjects, competencyDomains,
TestSupport.BuildAiSettingsService());
vm.Initialize(groupId);
return (vm, units, lessons, groupId);
}
[Fact]
public void LoadUnits_BerechnetFortschrittAusDurchgefuehrtenStunden()
{
var (vm, units, lessons, groupId) = BuildScenario();
var unit = new Unit { GroupId = groupId, Title = "Optik" };
units.Add(unit);
lessons.Add(new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Status = LessonStatus.Conducted, Topic = "A" });
lessons.Add(new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 8), Status = LessonStatus.Conducted, Topic = "B" });
lessons.Add(new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 15), Status = LessonStatus.Planned, Topic = "C" });
vm.Initialize(groupId);
var summary = vm.Units.Single(u => u.Id == unit.Id);
Assert.Equal(2, summary.ConductedCount);
Assert.Equal(3, summary.TotalCount);
Assert.Equal(2.0 / 3, summary.ProgressFraction, 3);
Assert.Contains("2 / 3", summary.ProgressText);
}
[Fact]
public async Task MoveLesson_MitNachrueckenVerschiebtNurGeplanteFolgestundenNachDemStichtag()
{
var (vm, units, lessons, groupId) = BuildScenario();
var unit = new Unit { GroupId = groupId, Title = "Mechanik" };
units.Add(unit);
var l1 = new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Status = LessonStatus.Planned, Topic = "1" };
var l2 = new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 8), Status = LessonStatus.Planned, Topic = "2" };
var l3 = new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 15), Status = LessonStatus.Planned, Topic = "3" };
var l4 = new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 22), Status = LessonStatus.Conducted, Topic = "4" };
lessons.Add(l1); lessons.Add(l2); lessons.Add(l3); lessons.Add(l4);
vm.Initialize(groupId);
vm.SelectedUnit = vm.Units.Single(u => u.Id == unit.Id);
vm.SelectedLesson = vm.Lessons.Single(l => l.Id == l1.Id);
vm.OnPickMoveTarget = _ => Task.FromResult<MoveLessonTarget?>(new MoveLessonTarget(new DateOnly(2025, 9, 3), true));
await vm.MoveLessonCommand.ExecuteAsync(null);
var byUnit = lessons.GetByUnit(unit.Id).ToDictionary(l => l.Id);
Assert.Equal(new DateOnly(2025, 9, 3), byUnit[l1.Id].Date);
Assert.Equal(new DateOnly(2025, 9, 10), byUnit[l2.Id].Date); // +2 Tage nachgerückt
Assert.Equal(new DateOnly(2025, 9, 17), byUnit[l3.Id].Date); // +2 Tage nachgerückt
Assert.Equal(new DateOnly(2025, 9, 22), byUnit[l4.Id].Date); // Conducted: unverändert
}
[Fact]
public async Task MoveLesson_OhneNachrueckenVerschiebtNurDieAusgewaehlteStunde()
{
var (vm, units, lessons, groupId) = BuildScenario();
var unit = new Unit { GroupId = groupId, Title = "Mechanik" };
units.Add(unit);
var l1 = new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Status = LessonStatus.Planned, Topic = "1" };
var l2 = new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 8), Status = LessonStatus.Planned, Topic = "2" };
lessons.Add(l1); lessons.Add(l2);
vm.Initialize(groupId);
vm.SelectedUnit = vm.Units.Single(u => u.Id == unit.Id);
vm.SelectedLesson = vm.Lessons.Single(l => l.Id == l1.Id);
vm.OnPickMoveTarget = _ => Task.FromResult<MoveLessonTarget?>(new MoveLessonTarget(new DateOnly(2025, 9, 5), false));
await vm.MoveLessonCommand.ExecuteAsync(null);
var byUnit = lessons.GetByUnit(unit.Id).ToDictionary(l => l.Id);
Assert.Equal(new DateOnly(2025, 9, 5), byUnit[l1.Id].Date);
Assert.Equal(new DateOnly(2025, 9, 8), byUnit[l2.Id].Date); // unverändert
}
[Fact]
public async Task CopyUnit_ErhaeltAbstaendeUndSetztGroupIdAufZielgruppe()
{
var (vm, units, lessons, sourceGroupId) = BuildScenario();
var targetGroupId = Guid.NewGuid();
var kurzversionId = Guid.NewGuid(); // Katalog-Referenz, bleibt beim Kopieren unverändert
var unit = new Unit
{
GroupId = sourceGroupId,
Title = "Kinetik",
StartDate = new DateOnly(2025, 9, 1),
EndDate = new DateOnly(2025, 10, 1),
Notes = "Original-Notiz",
};
units.Add(unit);
var l1 = new Lesson
{
UnitId = unit.Id, GroupId = sourceGroupId, Date = new DateOnly(2025, 9, 1),
Topic = "Einführung", Status = LessonStatus.Conducted, Reflection = "lief gut",
StartTime = new TimeOnly(11, 45),
Phases = [new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 10, Material = "AB01", Shorthand = "L->AB01", AlternativePathId = kurzversionId }],
};
var l2 = new Lesson
{
UnitId = unit.Id, GroupId = sourceGroupId, Date = new DateOnly(2025, 9, 8),
Topic = "Vertiefung", Status = LessonStatus.Planned,
};
lessons.Add(l1); lessons.Add(l2);
vm.Initialize(sourceGroupId);
vm.SelectedUnit = vm.Units.Single(u => u.Id == unit.Id);
var anchor = new DateOnly(2025, 10, 1);
vm.OnPickCopyTarget = _ => Task.FromResult<CopyUnitTarget?>(new CopyUnitTarget(targetGroupId, anchor));
await vm.CopyUnitCommand.ExecuteAsync(null);
var newUnit = units.GetByGroup(targetGroupId).Single();
Assert.Equal("Kinetik", newUnit.Title);
Assert.Equal(UnitStatus.Planned, newUnit.Status);
Assert.Equal(anchor, newUnit.StartDate);
Assert.Equal(anchor.AddDays(30), newUnit.EndDate); // ursprünglich 30 Tage nach StartDate
var copiedLessons = lessons.GetByUnit(newUnit.Id).OrderBy(l => l.Date).ToList();
Assert.Equal(2, copiedLessons.Count);
Assert.All(copiedLessons, l => Assert.Equal(targetGroupId, l.GroupId)); // nicht sourceGroupId
Assert.Equal(anchor, copiedLessons[0].Date);
Assert.Equal(anchor.AddDays(7), copiedLessons[1].Date); // 7-Tage-Abstand erhalten
Assert.All(copiedLessons, l => Assert.Equal(LessonStatus.Planned, l.Status));
Assert.Null(copiedLessons[0].Reflection); // Reflexion wird bei der Kopie geleert
Assert.Equal(new TimeOnly(11, 45), copiedLessons[0].StartTime); // Beginn wird übernommen
Assert.Single(copiedLessons[0].Phases);
Assert.Equal("Einstieg", copiedLessons[0].Phases[0].Name);
Assert.Equal("L->AB01", copiedLessons[0].Phases[0].Shorthand);
Assert.Equal(kurzversionId, copiedLessons[0].Phases[0].AlternativePathId);
Assert.NotEqual(l1.Phases[0].Id, copiedLessons[0].Phases[0].Id); // frische Id, keine geteilte Referenz
// Ursprüngliche Einheit/Stunden bleiben unverändert in der Quellgruppe.
Assert.Single(units.GetByGroup(sourceGroupId));
Assert.Equal(LessonStatus.Conducted, lessons.GetByUnit(unit.Id).Single(l => l.Id == l1.Id).Status);
}
[Fact]
public void LoadUnits_SammeltMaterialienAusAllenStundenphasenDerGruppe()
{
var (vm, units, lessons, groupId) = BuildScenario();
var unit1 = new Unit { GroupId = groupId, Title = "Einheit 1" };
var unit2 = new Unit { GroupId = groupId, Title = "Einheit 2" };
units.Add(unit1); units.Add(unit2);
lessons.Add(new Lesson
{
UnitId = unit1.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Topic = "A",
Phases = [new LessonPhaseStep { Material = "Arbeitsblatt" }],
});
lessons.Add(new Lesson
{
UnitId = unit2.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 2), Topic = "B",
Phases =
[
new LessonPhaseStep { Material = "Modell" },
new LessonPhaseStep { Material = "Arbeitsblatt" }, // Duplikat, soll nur einmal erscheinen
new LessonPhaseStep { Material = "" }, // leer, soll ignoriert werden
],
});
vm.Initialize(groupId);
Assert.Equal(["Arbeitsblatt", "Modell"], vm.KnownMaterials);
}
[Fact]
public async Task GenerateLessonSeries_RuftDelegateMitAusgewaehlterEinheitAufUndLaedtBeiErfolgNeu()
{
var (vm, units, lessons, groupId) = BuildScenario();
var unit = new Unit { GroupId = groupId, Title = "Optik" };
units.Add(unit);
vm.Initialize(groupId);
vm.SelectedUnit = vm.Units.Single(u => u.Id == unit.Id);
Unit? passedUnit = null;
vm.OnGenerateLessonSeries = u =>
{
passedUnit = u;
lessons.Add(new Lesson { UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Topic = "" });
return Task.FromResult<LessonSeriesResult?>(new LessonSeriesResult(1, 0, 0));
};
await vm.GenerateLessonSeriesCommand.ExecuteAsync(null);
Assert.Equal(unit.Id, passedUnit!.Id);
Assert.Single(vm.Lessons);
}
[Fact]
public void LoadUnits_SammeltKurzsymboleAusAllenStundenphasenDerGruppe()
{
var (vm, units, lessons, groupId) = BuildScenario();
var unit = new Unit { GroupId = groupId, Title = "Einheit 1" };
units.Add(unit);
lessons.Add(new Lesson
{
UnitId = unit.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Topic = "A",
Phases =
[
new LessonPhaseStep { Shorthand = "Plenum" },
new LessonPhaseStep { Shorthand = "L->AB01" },
new LessonPhaseStep { Shorthand = "" }, // leer, soll ignoriert werden
],
});
vm.Initialize(groupId);
Assert.Equal(["L->AB01", "Plenum"], vm.KnownShorthands);
}
}