Files
LehrerApp/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs
T
admin edf7510765
CI / build-and-test (push) Canceled after 0s
Datepicker update
2026-09-01 15:31:16 +02:00

784 lines
34 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.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.Views.Groups;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public sealed class SeatingPlanViewModelTests
{
[Fact]
public void TafelpositionWirdAusPlanGeladenUndBeimBearbeitenGespeichert()
{
var groupId = Guid.NewGuid();
var plan = new SeatingPlan
{
GroupId = groupId,
Name = "Standard",
Rows = 1,
Columns = 1,
IsBoardAtBottom = true,
};
var plans = new FakeSeatingPlans([plan]);
var tabVm = new SeatingPlanTabViewModel(
plans, new FakeStudents([]), new FakeMemberships([]),
new FakeSessions([]), new FakeEntries(), new FakeAspects());
tabVm.Initialize(groupId, isReadOnly: false);
Assert.True(tabVm.IsBoardAtBottom);
Assert.False(tabVm.IsBoardAtTop);
var dialogVm = tabVm.CreateDialogViewModel(plan);
dialogVm.IsBoardAtBottom = false;
dialogVm.SaveCommand.Execute(null);
Assert.False(plans.GetById(plan.Id)!.IsBoardAtBottom);
}
[Fact]
public void AuswahlEinesBereitsZugeordnetenSchuelers_VerschiebtIhnAufDenNeuenPlatz()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
var students = new FakeStudents([student]);
var memberships = new FakeMemberships([
new GroupMembership { GroupId = groupId, StudentId = student.Id },
]);
var plan = new SeatingPlan
{
GroupId = groupId,
Name = "Standard",
Rows = 1,
Columns = 2,
Assignments = [new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id }],
};
var plans = new FakeSeatingPlans([plan]);
var vm = new SeatingPlanTabViewModel(plans, students, memberships,
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.IsEditMode = true;
vm.Seats[1].SelectedOption = vm.StudentOptions.Single(o => o.StudentId == student.Id);
Assert.Null(vm.Seats[0].SelectedOption.StudentId);
var assignment = Assert.Single(plans.GetById(plan.Id)!.Assignments);
Assert.Equal((0, 1, student.Id), (assignment.Row, assignment.Column, assignment.StudentId));
}
[Fact]
public void ArchivierteGruppe_DeaktiviertSitzplatzbearbeitung()
{
var groupId = Guid.NewGuid();
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
var vm = new SeatingPlanTabViewModel(
new FakeSeatingPlans([plan]), new FakeStudents([]), new FakeMemberships([]),
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: true);
Assert.False(vm.IsEditable);
Assert.False(vm.Seats.Single().CanEdit);
Assert.False(vm.AddPlanCommand.CanExecute(null));
}
[Fact]
public void DragDrop_ZwischenBelegtenPlaetzen_TauschtDieSchueler()
{
var groupId = Guid.NewGuid();
var anna = new Student { FirstName = "Anna", LastName = "A" };
var ben = new Student { FirstName = "Ben", LastName = "B" };
var students = new FakeStudents([anna, ben]);
var memberships = new FakeMemberships([
new GroupMembership { GroupId = groupId, StudentId = anna.Id },
new GroupMembership { GroupId = groupId, StudentId = ben.Id },
]);
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 2,
Assignments =
[
new SeatAssignment { Row = 0, Column = 0, StudentId = anna.Id },
new SeatAssignment { Row = 0, Column = 1, StudentId = ben.Id },
],
};
var plans = new FakeSeatingPlans([plan]);
var vm = new SeatingPlanTabViewModel(plans, students, memberships,
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.IsEditMode = true;
vm.MoveSeat(vm.Seats[0], vm.Seats[1]);
Assert.Equal(ben.Id, vm.Seats[0].SelectedOption.StudentId);
Assert.Equal(anna.Id, vm.Seats[1].SelectedOption.StudentId);
Assert.Equal(2, plans.GetById(plan.Id)!.Assignments.Count);
}
// ── Ausgeblendete Plätze (fehlender Tisch in einer Reihe) ─────────────────────────────
[Fact]
public void LoadPlan_LaedtAusgeblendetePlaetzeAusDemPlan()
{
var groupId = Guid.NewGuid();
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 2,
HiddenSeats = [new HiddenSeat { Row = 0, Column = 1 }],
};
var vm = new SeatingPlanTabViewModel(
new FakeSeatingPlans([plan]), new FakeStudents([]), new FakeMemberships([]),
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
Assert.False(vm.Seats[0].IsHidden);
Assert.True(vm.Seats[1].IsHidden);
}
[Fact]
public void ToggleSeatHidden_LeererPlatz_BlendetAusUndSpeichertUndKannWiederEingeblendetWerden()
{
var groupId = Guid.NewGuid();
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 2 };
var plans = new FakeSeatingPlans([plan]);
var vm = new SeatingPlanTabViewModel(plans, new FakeStudents([]), new FakeMemberships([]),
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.IsEditMode = true;
vm.ToggleSeatHidden(vm.Seats[1]);
Assert.True(vm.Seats[1].IsHidden);
var hidden = Assert.Single(plans.GetById(plan.Id)!.HiddenSeats);
Assert.Equal((0, 1), (hidden.Row, hidden.Column));
vm.ToggleSeatHidden(vm.Seats[1]);
Assert.False(vm.Seats[1].IsHidden);
Assert.Empty(plans.GetById(plan.Id)!.HiddenSeats);
}
[Fact]
public void ToggleSeatHidden_BelegterPlatz_TutNichts()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var memberships = new FakeMemberships([
new GroupMembership { GroupId = groupId, StudentId = student.Id },
]);
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
Assignments = [new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id }],
};
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
memberships, new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.IsEditMode = true;
vm.ToggleSeatHidden(vm.Seats[0]);
Assert.False(vm.Seats[0].IsHidden);
}
/// Ein ausgeblendeter Platz darf nie ein Ziel für Drag&Drop/Zuordnung sein - sonst hätte man
/// gleichzeitig "kein Tisch hier" und einen dort sitzenden Schüler.
[Fact]
public void AssignStudent_AufAusgeblendetenPlatz_WirdIgnoriert()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var memberships = new FakeMemberships([
new GroupMembership { GroupId = groupId, StudentId = student.Id },
]);
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
HiddenSeats = [new HiddenSeat { Row = 0, Column = 0 }],
};
var plans = new FakeSeatingPlans([plan]);
var vm = new SeatingPlanTabViewModel(plans, new FakeStudents([student]), memberships,
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.IsEditMode = true;
vm.AssignStudent(vm.StudentOptions.Single(o => o.StudentId == student.Id), vm.Seats[0]);
Assert.False(vm.Seats[0].IsOccupied);
Assert.Empty(plans.GetById(plan.Id)!.Assignments);
}
[Fact]
public void MoveSeat_AufAusgeblendetenPlatz_WirdIgnoriert()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var memberships = new FakeMemberships([
new GroupMembership { GroupId = groupId, StudentId = student.Id },
]);
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 2,
HiddenSeats = [new HiddenSeat { Row = 0, Column = 1 }],
Assignments = [new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id }],
};
var plans = new FakeSeatingPlans([plan]);
var vm = new SeatingPlanTabViewModel(plans, new FakeStudents([student]), memberships,
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.IsEditMode = true;
vm.MoveSeat(vm.Seats[0], vm.Seats[1]);
Assert.Equal(student.Id, vm.Seats[0].SelectedOption.StudentId);
Assert.False(vm.Seats[1].IsOccupied);
}
// ── Unterrichtsmodus (14.x): Sitzung mit einer konkreten Stunde verknüpfen ────────────────
[Fact]
public void SelectOrCreateSessionForLesson_BereitsVerknuepfteSitzungVorhanden_WaehltSieAus()
{
var groupId = Guid.NewGuid();
var lesson = new Lesson { GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" };
var existingSession = new ParticipationSession
{ GroupId = groupId, Date = lesson.Date, LessonId = lesson.Id, Comment = "Redox" };
var sessions = new FakeSessions([existingSession]);
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([]),
new FakeMemberships([]), sessions, new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.SelectOrCreateSessionForLesson(lesson);
Assert.Equal(existingSession.Id, vm.SelectedSession?.Id);
Assert.Single(sessions.GetByGroup(groupId));
}
[Fact]
public void SelectOrCreateSessionForLesson_AndereStundeAmSelbenTagBereitsVerknuepft_LegtKeineZweiteSitzungAn()
{
// Nutzer-Feedback: eine dritte Stunde am selben Tag (z.B. durch Vertretung, eigene Lesson-
// Id) soll die bereits bestehende Sitzung der Doppelstunde weiterverwenden statt eine
// zweite, unabhängige Sitzung für denselben Tag anzulegen.
var groupId = Guid.NewGuid();
var today = DateOnly.FromDateTime(DateTime.Today);
var doubleLesson = new Lesson { GroupId = groupId, Date = today, Topic = "Redox" };
var thirdLesson = new Lesson { GroupId = groupId, Date = today, Topic = "Vertretung" };
var existingSession = new ParticipationSession
{ GroupId = groupId, Date = today, LessonId = doubleLesson.Id, Comment = "Redox" };
var sessions = new FakeSessions([existingSession]);
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([]),
new FakeMemberships([]), sessions, new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.SelectOrCreateSessionForLesson(thirdLesson);
Assert.Equal(existingSession.Id, vm.SelectedSession?.Id);
Assert.Single(sessions.GetByGroup(groupId));
}
[Fact]
public void SelectOrCreateSessionForLesson_KeineSitzungVorhanden_LegtVerknuepfteAnUndWaehltSieAus()
{
var groupId = Guid.NewGuid();
var lesson = new Lesson
{ GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" };
var sessions = new FakeSessions([]);
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([]),
new FakeMemberships([]), sessions, new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.SelectOrCreateSessionForLesson(lesson);
var created = Assert.Single(sessions.GetByGroup(groupId));
Assert.Equal(lesson.Id, created.LessonId);
Assert.Equal(lesson.Topic, created.Comment);
Assert.Equal(vm.SelectedSession?.Id, created.Id);
}
[Fact]
public void SelectOrCreateSessionForLesson_ArchivierteGruppe_LegtKeineSitzungAn()
{
var groupId = Guid.NewGuid();
var lesson = new Lesson
{ GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" };
var sessions = new FakeSessions([]);
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([]),
new FakeMemberships([]), sessions, new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: true);
vm.SelectOrCreateSessionForLesson(lesson);
Assert.Empty(sessions.GetByGroup(groupId));
Assert.Null(vm.SelectedSession);
}
[Fact]
public void ShowSeatUndCanToggleHidden_HaengenVonBearbeitungsmodusUndBelegungAb()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var memberships = new FakeMemberships([
new GroupMembership { GroupId = groupId, StudentId = student.Id },
]);
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 2,
HiddenSeats = [new HiddenSeat { Row = 0, Column = 1 }],
Assignments = [new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id }],
};
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
memberships, new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
// Ansichtsmodus: der ausgeblendete Platz wird nicht gerendert, belegte Plätze können
// ohnehin nicht ausgeblendet werden.
Assert.False(vm.Seats[1].ShowSeat);
Assert.False(vm.Seats[0].CanToggleHidden);
Assert.False(vm.Seats[1].CanToggleHidden);
vm.IsEditMode = true;
// Bearbeitungsmodus: der ausgeblendete Platz bleibt sichtbar (abgeblendet), um ihn wieder
// einblenden zu können - der belegte Platz kann weiterhin nicht ausgeblendet werden.
Assert.True(vm.Seats[1].ShowSeat);
Assert.False(vm.Seats[0].CanToggleHidden);
Assert.True(vm.Seats[1].CanToggleHidden);
}
[Fact]
public void Ansichtsmodus_VerhindertVerschiebenUndBlendetBearbeitungsbefehleAus()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 2,
Assignments = [new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id }],
};
var vm = new SeatingPlanTabViewModel(
new FakeSeatingPlans([plan]), new FakeStudents([student]),
new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.MoveSeat(vm.Seats[0], vm.Seats[1]);
Assert.False(vm.IsEditMode);
Assert.False(vm.CanEditLayout);
Assert.False(vm.EditPlanCommand.CanExecute(null));
Assert.Equal(student.Id, vm.Seats[0].SelectedOption.StudentId);
Assert.Null(vm.Seats[1].SelectedOption.StudentId);
vm.IsEditMode = true;
Assert.True(vm.CanEditLayout);
Assert.True(vm.EditPlanCommand.CanExecute(null));
Assert.All(vm.Seats, seat => Assert.True(seat.CanEdit));
}
[Fact]
public void Ansichtsmodus_ZeigtAnwesenheitUndHausaufgabeUndBlendetAbwesendeAb()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var session = new ParticipationSession { GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today) };
var entries = new FakeEntries();
entries.Add(new ParticipationEntry
{
SessionId = session.Id, StudentId = student.Id,
Attendance = AttendanceStatus.Excused, Homework = HomeworkStatus.MissingOpen,
});
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
Assignments = [new SeatAssignment { StudentId = student.Id }],
};
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
new FakeSessions([session]), entries, new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
var seat = Assert.Single(vm.Seats);
Assert.True(seat.HasAttendanceBadge);
Assert.True(seat.HasHomeworkBadge);
Assert.Equal(0.42, seat.LessonOpacity);
}
[Fact]
public void SituationsTags_WerdenProSchuelerUndSitzungInEinemEntwurfZusammengefuehrt()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var session = new ParticipationSession
{
GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today), LessonId = Guid.NewGuid(),
};
var docs = new FakeDocumentation();
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
Assignments = [new SeatAssignment { StudentId = student.Id }],
};
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
new FakeSessions([session]), new FakeEntries(), new FakeAspects(), docs);
vm.Initialize(groupId, isReadOnly: false);
var seat = Assert.Single(vm.Seats);
seat.SituationTags.Single(t => t.Text == "Mitarbeit verweigert").ToggleCommand.Execute(null);
seat.SituationTags.Single(t => t.Text == "Material vergessen").ToggleCommand.Execute(null);
var draft = Assert.Single(docs.GetByStudent(student.Id));
Assert.True(draft.IsDraft);
Assert.Equal(session.Id, draft.ParticipationSessionId);
Assert.Equal(session.LessonId, draft.LessonId);
Assert.Equal(["Mitarbeit verweigert", "Material vergessen"], draft.Tags);
}
[Fact]
public void Tischabstaende_WerdenBeimBearbeitenProSpaltengrenzeGespeichert()
{
var groupId = Guid.NewGuid();
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 2, Columns = 4,
ColumnGapWidths = [0, 80, 20],
};
var plans = new FakeSeatingPlans([plan]);
var vm = new SeatingPlanDialogViewModel(plans, groupId, plan);
Assert.Equal([0m, 80m, 20m], vm.ColumnGaps.Select(g => g.Width));
vm.ColumnGaps[0].Width = 40;
vm.SaveCommand.Execute(null);
Assert.Equal([40d, 80d, 20d], plans.GetById(plan.Id)!.ColumnGapWidths);
}
[Fact]
public void SitzplanPanel_FuegtAbstandNurAnKonfigurierterSpaltengrenzeEin()
{
var panel = new SeatingPlanPanel
{
Columns = 3,
ColumnGapWidths = [0, 70],
};
for (var i = 0; i < 6; i++)
panel.Children.Add(new Avalonia.Controls.Border { Width = 100, Height = 50 });
panel.Measure(Avalonia.Size.Infinity);
panel.Arrange(new Avalonia.Rect(panel.DesiredSize));
Assert.Equal(370, panel.DesiredSize.Width);
Assert.Equal(100, panel.DesiredSize.Height);
Assert.Equal(0, panel.Children[0].Bounds.X);
Assert.Equal(100, panel.Children[1].Bounds.X);
Assert.Equal(270, panel.Children[2].Bounds.X);
Assert.Equal(270, panel.Children[5].Bounds.X);
}
[Fact]
public void SitzplatzBewertung_ErstelltHeutigeSitzungUndSpeichertAlleDreiBereiche()
{
var groupId = Guid.NewGuid();
var studentId = Guid.NewGuid();
var sessions = new FakeSessions([]);
var entries = new FakeEntries();
var vm = new SeatAssessmentViewModel(sessions, entries, new FakeAspects(),
groupId, studentId, "Beispiel, Anna", canEdit: true);
vm.SetRatingByNumber(5);
vm.ApplyAttendanceShortcut(1, clear: false);
vm.ApplyHomeworkShortcut(7, clear: false);
vm.ApplyDayHighlightShortcut(1, clear: false);
var session = Assert.Single(sessions.GetByGroup(groupId));
Assert.Equal(DateOnly.FromDateTime(DateTime.Today), session.Date);
Assert.Equal("Sitzplan", session.Comment);
var entry = entries.GetBySessionAndStudent(session.Id, studentId)!;
Assert.Equal(2, entry.Ratings.Single(r => r.Key == "quality").Value);
Assert.Equal(AttendanceStatus.Present, entry.Attendance);
Assert.Equal(HomeworkStatus.MissingOpen, entry.Homework);
Assert.True(entry.HomeworkMissing);
Assert.Equal(DayHighlightKind.Standout, entry.DayHighlight);
}
[Fact]
public void SitzplatzBewertung_TagesflaggeKannWiederGeloeschtWerden()
{
var groupId = Guid.NewGuid();
var studentId = Guid.NewGuid();
var sessions = new FakeSessions([]);
var entries = new FakeEntries();
var vm = new SeatAssessmentViewModel(sessions, entries, new FakeAspects(),
groupId, studentId, "Beispiel, Anna", canEdit: true);
vm.ApplyDayHighlightShortcut(3, clear: false); // aus Versehen "Schlechter Tag" gesetzt
vm.ApplyDayHighlightShortcut(null, clear: true);
var session = Assert.Single(sessions.GetByGroup(groupId));
var entry = entries.GetBySessionAndStudent(session.Id, studentId)!;
Assert.Null(entry.DayHighlight);
Assert.Equal("Keine Markierung", vm.DayHighlightLabel);
Assert.All(vm.DayHighlightChoices, c => Assert.Equal(c.Kind is null, c.IsSelected));
}
[Fact]
public void SitzplatzBewertung_SchlaegtQuantitaetAusStrichlisteVor()
{
var groupId = Guid.NewGuid();
var studentId = Guid.NewGuid();
var today = DateOnly.FromDateTime(DateTime.Today);
var session = new ParticipationSession { GroupId = groupId, Date = today };
var sessions = new FakeSessions([session]);
var entries = new FakeEntries();
entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = studentId, RaisedHandCount = 3 });
var vm = new SeatAssessmentViewModel(sessions, entries, new FakeAspects(),
groupId, studentId, "Beispiel, Anna", canEdit: true);
var quantityRow = vm.AspectRows.Single(r => r.Key == "quantity");
Assert.True(quantityRow.HasSuggestion);
Assert.Equal(0, quantityRow.SuggestedValue); // 3 gemeldet -> "" laut ParticipationCountSuggestion
Assert.Contains("3× gemeldet", quantityRow.SuggestionLabel);
var qualityRow = vm.AspectRows.Single(r => r.Key == "quality");
Assert.False(qualityRow.HasSuggestion);
quantityRow.ApplySuggestionCommand.Execute(null);
Assert.Equal(0, quantityRow.Value);
var updatedEntry = entries.GetBySessionAndStudent(session.Id, studentId)!;
Assert.Equal(0, updatedEntry.Ratings.Single(r => r.Key == "quantity").Value);
}
[Fact]
public void SitzplatzBewertung_ArchiviertOhneSitzung_LegtKeineNeueSitzungAn()
{
var groupId = Guid.NewGuid();
var sessions = new FakeSessions([]);
var vm = new SeatAssessmentViewModel(sessions, new FakeEntries(), new FakeAspects(),
groupId, Guid.NewGuid(), "Beispiel, Anna", canEdit: false);
vm.SetRatingByNumber(5);
Assert.False(vm.CanEdit);
Assert.Empty(sessions.GetByGroup(groupId));
}
[Fact]
public void Initialize_LegtOhneVorhandeneSitzungKeineGeistersitzungAn()
{
// Regressionstest: Initialize() läuft schon beim bloßen Navigieren zur Kursübersicht
// (GroupDetailViewModel.LoadGroup ruft SeatingPlanTab.Initialize für jede Gruppe auf),
// unabhängig davon, ob der Sitzplan-Tab überhaupt angesehen wird. Vorher legte
// LoadTodaySessions() dabei sofort eine "Sitzplan"-Sitzung für heute an.
var groupId = Guid.NewGuid();
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
var sessions = new FakeSessions([]);
var vm = new SeatingPlanTabViewModel(
new FakeSeatingPlans([plan]), new FakeStudents([]), new FakeMemberships([]),
sessions, new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
Assert.Empty(sessions.GetByGroup(groupId));
Assert.Null(vm.SelectedSession);
}
[Fact]
public void TallyRaisedHand_ErhoehtNurGemeldetZaehler()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
Assignments = [new SeatAssignment { StudentId = student.Id }],
};
var sessions = new FakeSessions([]);
var entries = new FakeEntries();
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
sessions, entries, new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
var seat = vm.Seats.Single();
seat.TallyRaisedHandCommand.Execute(null);
seat.TallyRaisedHandCommand.Execute(null);
Assert.Equal(2, seat.RaisedHandCount);
Assert.Equal(0, seat.CalledOnCount);
var session = Assert.Single(sessions.GetByGroup(groupId));
var entry = entries.GetBySessionAndStudent(session.Id, student.Id)!;
Assert.Equal(2, entry.RaisedHandCount);
Assert.Equal(0, entry.CalledOnCount);
}
[Fact]
public void TallyCalledOn_ErhoehtBeideZaehler()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
Assignments = [new SeatAssignment { StudentId = student.Id }],
};
var sessions = new FakeSessions([]);
var entries = new FakeEntries();
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
sessions, entries, new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
var seat = vm.Seats.Single();
seat.TallyRaisedHandCommand.Execute(null); // erste Meldung, nicht drangekommen
seat.TallyCalledOnCommand.Execute(null); // zweite Meldung, drangekommen
Assert.Equal(2, seat.RaisedHandCount);
Assert.Equal(1, seat.CalledOnCount);
}
[Fact]
public async Task AssessStudent_LegtErstBeiTatsaechlicherBewertungEineSitzungAn()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
Assignments = [new SeatAssignment { StudentId = student.Id }],
};
var sessions = new FakeSessions([]);
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
sessions, new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.OnAssessStudent = _ => Task.CompletedTask;
Assert.Empty(sessions.GetByGroup(groupId));
await vm.AssessStudent(vm.Seats.Single());
var session = Assert.Single(sessions.GetByGroup(groupId));
Assert.Equal(DateOnly.FromDateTime(DateTime.Today), session.Date);
Assert.Equal("Sitzplan", session.Comment);
Assert.Equal(session.Id, vm.SelectedSession?.Id);
}
[Fact]
public async Task AssessStudent_SitzungBereitsUeberAndereInstanzVerknuepft_LegtKeineZweiteAn()
{
// Regressionstest: Der Unterrichtsmodus läuft auf einer eigenen, per DI frisch aufgelösten
// SeatingPlanTabViewModel-Instanz (TimetableView.ShowTeachingMode), getrennt von der
// Instanz im Sitzplan-Tab der Kursübersicht — beide teilen sich nur das Repository. Wurde
// (wie hier simuliert) die Sitzung bereits über den Unterrichtsmodus mit der Stunde
// verknüpft, bevor im echten Sitzplan-Tab bewertet wird, darf letzterer keine zweite,
// unverknüpfte "Sitzplan"-Sitzung für denselben Tag anlegen.
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
Assignments = [new SeatAssignment { StudentId = student.Id }],
};
var sessions = new FakeSessions([]);
var memberships = new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]);
// Instanz A: Sitzplan-Tab der Kursübersicht, bereits initialisiert (kein SelectedSession).
var tabVm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
memberships, sessions, new FakeEntries(), new FakeAspects());
tabVm.Initialize(groupId, isReadOnly: false);
tabVm.OnAssessStudent = _ => Task.CompletedTask;
// Instanz B: eigene, frische Instanz wie im Unterrichtsmodus — verknüpft die Sitzung mit
// der Stunde, ohne dass Instanz A davon erfährt.
var lesson = new Lesson { GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" };
var teachingModeVm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
memberships, sessions, new FakeEntries(), new FakeAspects());
teachingModeVm.Initialize(groupId, isReadOnly: false);
teachingModeVm.SelectOrCreateSessionForLesson(lesson);
var linked = Assert.Single(sessions.GetByGroup(groupId));
Assert.Equal(lesson.Id, linked.LessonId);
await tabVm.AssessStudent(tabVm.Seats.Single());
var session = Assert.Single(sessions.GetByGroup(groupId));
Assert.Equal(linked.Id, session.Id);
Assert.Equal(lesson.Id, session.LessonId);
Assert.Equal(session.Id, tabVm.SelectedSession?.Id);
}
[Fact]
public void ShuffleSeats_ErzeugtNeuenPlanMitVertauschtenPlaetzenUndBehaeltDenUrsprungsplan()
{
var groupId = Guid.NewGuid();
var anna = new Student { FirstName = "Anna", LastName = "A" };
var ben = new Student { FirstName = "Ben", LastName = "B" };
var cara = new Student { FirstName = "Cara", LastName = "C" };
var students = new FakeStudents([anna, ben, cara]);
var memberships = new FakeMemberships([
new GroupMembership { GroupId = groupId, StudentId = anna.Id },
new GroupMembership { GroupId = groupId, StudentId = ben.Id },
new GroupMembership { GroupId = groupId, StudentId = cara.Id },
]);
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Klausur", Room = "B204", Rows = 1, Columns = 3,
Assignments =
[
new SeatAssignment { Row = 0, Column = 0, StudentId = anna.Id },
new SeatAssignment { Row = 0, Column = 1, StudentId = ben.Id },
new SeatAssignment { Row = 0, Column = 2, StudentId = cara.Id },
],
};
var plans = new FakeSeatingPlans([plan]);
var vm = new SeatingPlanTabViewModel(plans, students, memberships,
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.IsEditMode = true;
vm.ShuffleSeatsCommand.Execute(null);
var all = plans.GetByGroup(groupId);
Assert.Equal(2, all.Count);
var shuffled = Assert.Single(all, p => p.Id != plan.Id);
Assert.Equal("B204", shuffled.Room);
Assert.Equal(1, shuffled.Rows);
Assert.Equal(3, shuffled.Columns);
// Dieselben drei Schüler auf denselben drei Koordinaten, nur die Zuordnung untereinander
// wurde neu gewürfelt.
Assert.Equal(new[] { anna.Id, ben.Id, cara.Id }.OrderBy(id => id),
shuffled.Assignments.Select(a => a.StudentId).OrderBy(id => id));
Assert.Equal(new[] { (0, 0), (0, 1), (0, 2) },
shuffled.Assignments.Select(a => (a.Row, a.Column)).OrderBy(t => t.Item2));
// Ursprungsplan bleibt unverändert erhalten.
Assert.Equal(3, plans.GetById(plan.Id)!.Assignments.Count);
Assert.Equal(vm.SelectedPlan?.Id, shuffled.Id);
}
[Fact]
public void ShuffleSeatsCommand_OhneBearbeitungsmodus_NichtAusfuehrbar()
{
var groupId = Guid.NewGuid();
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
var vm = new SeatingPlanTabViewModel(
new FakeSeatingPlans([plan]), new FakeStudents([]), new FakeMemberships([]),
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
Assert.False(vm.ShuffleSeatsCommand.CanExecute(null));
vm.IsEditMode = true;
Assert.True(vm.ShuffleSeatsCommand.CanExecute(null));
}
}