Kapitel 7 abgeschlossen.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
using System.IO.Compression;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class CreateLetterDialogViewModelTests : IDisposable
|
||||
{
|
||||
private readonly string _directory = Path.Combine(Path.GetTempPath(), $"lehrerapp-letter-vm-tests-{Guid.NewGuid():N}");
|
||||
|
||||
public CreateLetterDialogViewModelTests() => Directory.CreateDirectory(_directory);
|
||||
|
||||
[Fact]
|
||||
public void OhneVorlage_KannKeinenBriefErzeugen()
|
||||
{
|
||||
var student = StudentWithContact("Sehr geehrte Frau Muster,");
|
||||
|
||||
var vm = Build(student, new LetterTemplateService(_directory));
|
||||
|
||||
Assert.False(vm.CanGenerate);
|
||||
Assert.Contains(vm.Issues, i => i.Message.Contains("Vorlage", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerwendeteBriefanredeFehlt_BlockiertErzeugung()
|
||||
{
|
||||
var service = ServiceWithTemplate("Letter.Salutation");
|
||||
var vm = Build(StudentWithContact(null), service);
|
||||
|
||||
Assert.False(vm.CanGenerate);
|
||||
Assert.Contains(vm.Issues, i => i.Message.Contains("Briefanrede"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VollstaendigeDaten_ErzeugenEditierbareDocxKopie()
|
||||
{
|
||||
var service = ServiceWithTemplate("Letter.Salutation", "Student.FirstName", "CurrentDate");
|
||||
var vm = Build(StudentWithContact("Sehr geehrte Frau Muster,"), service);
|
||||
var output = Path.Combine(_directory, "Brief.docx");
|
||||
|
||||
var generated = vm.Generate(output);
|
||||
|
||||
Assert.True(generated);
|
||||
Assert.True(File.Exists(output));
|
||||
using var archive = ZipFile.OpenRead(output);
|
||||
using var reader = new StreamReader(archive.GetEntry("word/document.xml")!.Open());
|
||||
var xml = reader.ReadToEnd();
|
||||
Assert.Contains("Sehr geehrte Frau Muster,", xml);
|
||||
Assert.Contains("Lena", xml);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KontaktBearbeiten_SpeichertExpliziteBriefanrede()
|
||||
{
|
||||
var vm = new ContactEditDialogViewModel(new Contact { Name = "Frau Muster" });
|
||||
vm.LetterSalutation = "Sehr geehrte Frau Muster,";
|
||||
|
||||
vm.SaveCommand.Execute(null);
|
||||
|
||||
Assert.Equal("Sehr geehrte Frau Muster,", vm.Result!.LetterSalutation);
|
||||
}
|
||||
|
||||
private CreateLetterDialogViewModel Build(Student student, LetterTemplateService service) =>
|
||||
new(student, service, new FakeMemberships([]), new FakeGroups([]));
|
||||
|
||||
private LetterTemplateService ServiceWithTemplate(params string[] tags)
|
||||
{
|
||||
var source = Path.Combine(_directory, $"{Guid.NewGuid():N}.docx");
|
||||
using (var archive = ZipFile.Open(source, ZipArchiveMode.Create))
|
||||
{
|
||||
var entry = archive.CreateEntry("word/document.xml");
|
||||
using var writer = new StreamWriter(entry.Open());
|
||||
var controls = string.Join("", tags.Select(tag =>
|
||||
$"<w:sdt><w:sdtPr><w:tag w:val=\"{tag}\"/></w:sdtPr><w:sdtContent>" +
|
||||
"<w:p><w:r><w:t>Platzhalter</w:t></w:r></w:p></w:sdtContent></w:sdt>"));
|
||||
writer.Write("<w:document xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">" +
|
||||
$"<w:body>{controls}</w:body></w:document>");
|
||||
}
|
||||
var service = new LetterTemplateService(Path.Combine(_directory, $"service-{Guid.NewGuid():N}"));
|
||||
service.Import(source, "Elternbrief");
|
||||
return service;
|
||||
}
|
||||
|
||||
private static Student StudentWithContact(string? salutation) => new()
|
||||
{
|
||||
FirstName = "Lena",
|
||||
LastName = "Beispiel",
|
||||
Contacts =
|
||||
[
|
||||
new Contact
|
||||
{
|
||||
Name = "Frau Muster",
|
||||
Relation = "Mutter",
|
||||
LetterSalutation = salutation,
|
||||
Street = "Hauptstraße 1",
|
||||
PostalCode = "12345",
|
||||
City = "Musterstadt",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true);
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,20 @@ namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public class FakeStudents(List<Student> all) : IStudentRepository
|
||||
{
|
||||
private readonly Dictionary<Guid, StudentReferenceSummary> _references = [];
|
||||
public Student? GetById(Guid id) => all.FirstOrDefault(s => s.Id == id);
|
||||
public List<Student> GetAll(bool includeInactive = false) => all;
|
||||
public List<Student> GetAll(bool includeInactive = false) =>
|
||||
includeInactive ? all.ToList() : all.Where(s => s.IsActive).ToList();
|
||||
public List<Student> GetByGroup(Guid groupId) => all;
|
||||
public void Save(Student student) { }
|
||||
public void Delete(Guid id) { }
|
||||
public StudentReferenceSummary GetReferenceSummary(Guid studentId) =>
|
||||
_references.GetValueOrDefault(studentId, new StudentReferenceSummary(0, 0, 0, 0, 0, 0));
|
||||
public void SetReferenceSummary(Guid studentId, StudentReferenceSummary summary) => _references[studentId] = summary;
|
||||
public void Save(Student student) { all.RemoveAll(s => s.Id == student.Id); all.Add(student); }
|
||||
public void Delete(Guid id)
|
||||
{
|
||||
if (GetReferenceSummary(id).HasReferences) throw new InvalidOperationException();
|
||||
all.RemoveAll(s => s.Id == id);
|
||||
}
|
||||
}
|
||||
|
||||
public class FakeMemberships(List<GroupMembership> all) : IGroupMembershipRepository
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public class GenderAvatarDisplayTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(Gender.M, "M")]
|
||||
[InlineData(Gender.W, "W")]
|
||||
[InlineData(Gender.D, "D")]
|
||||
public void VerwendetEindeutigeBeschriftung(Gender gender, string expected) =>
|
||||
Assert.Equal(expected, GenderAvatarDisplay.Label(gender));
|
||||
|
||||
[Fact]
|
||||
public void UnbekanntesGeschlechtHatNeutralenFallback()
|
||||
{
|
||||
Assert.Equal("?", GenderAvatarDisplay.Label(null));
|
||||
Assert.Contains("nicht angegeben", GenderAvatarDisplay.Tooltip(null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Gender.M)]
|
||||
[InlineData(Gender.W)]
|
||||
[InlineData(Gender.D)]
|
||||
public void AuswahlwertKannVerlustfreiZurueckgewandeltWerden(Gender gender) =>
|
||||
Assert.Equal(gender, GenderAvatarDisplay.FromOption(GenderAvatarDisplay.ToOption(gender)));
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class GroupRolloverDialogViewModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Vorauswahl_NimmtNurAktiveSchuelerAmSchuljahresende()
|
||||
{
|
||||
var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26", GradeLevel = 8 };
|
||||
var fullYearStudent = Student("Ganzjahr");
|
||||
var h1Student = Student("Halbjahr");
|
||||
var withdrawnStudent = Student("Ausgetreten");
|
||||
var inactiveStudent = Student("Inaktiv", active: false);
|
||||
var membershipList = new List<GroupMembership>
|
||||
{
|
||||
Membership(group, fullYearStudent, MembershipPeriod.FullYear),
|
||||
Membership(group, h1Student, MembershipPeriod.H1Only),
|
||||
Membership(group, withdrawnStudent, MembershipPeriod.FullYear, new DateOnly(2026, 5, 1)),
|
||||
Membership(group, inactiveStudent, MembershipPeriod.FullYear),
|
||||
};
|
||||
var groups = new FakeGroups([group]);
|
||||
var memberships = new FakeMemberships(membershipList);
|
||||
var schemes = new FakeSchemes();
|
||||
var schoolYears = new SchoolYearService();
|
||||
var vm = new GroupRolloverDialogViewModel(group,
|
||||
new FakeStudents([fullYearStudent, h1Student, withdrawnStudent, inactiveStudent]),
|
||||
memberships, schemes, schoolYears,
|
||||
new GroupRolloverService(groups, memberships, schemes, schoolYears));
|
||||
|
||||
Assert.True(vm.Students.Single(s => s.FullName.Contains("Ganzjahr")).IsSelected);
|
||||
Assert.False(vm.Students.Single(s => s.FullName.Contains("Halbjahr")).IsSelected);
|
||||
Assert.False(vm.Students.Single(s => s.FullName.Contains("Ausgetreten")).IsSelected);
|
||||
var inactive = vm.Students.Single(s => s.FullName.Contains("Inaktiv"));
|
||||
Assert.False(inactive.IsSelected);
|
||||
Assert.False(inactive.CanSelect);
|
||||
Assert.Equal(1, vm.SelectedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dialog_SchlaegtNaechstesSchuljahrUndNaechsteStufeVor()
|
||||
{
|
||||
var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26", GradeLevel = 8 };
|
||||
var student = Student("Ganzjahr");
|
||||
var membershipList = new List<GroupMembership> { Membership(group, student, MembershipPeriod.FullYear) };
|
||||
var groups = new FakeGroups([group]);
|
||||
var memberships = new FakeMemberships(membershipList);
|
||||
var schemes = new FakeSchemes();
|
||||
var schoolYears = new SchoolYearService();
|
||||
|
||||
var vm = new GroupRolloverDialogViewModel(group, new FakeStudents([student]), memberships,
|
||||
schemes, schoolYears, new GroupRolloverService(groups, memberships, schemes, schoolYears));
|
||||
|
||||
Assert.Equal("2026/27", vm.TargetSchoolYear);
|
||||
Assert.Equal(9, vm.GradeLevel);
|
||||
Assert.True(vm.ArchiveSource);
|
||||
}
|
||||
|
||||
private static Student Student(string lastName, bool active = true) =>
|
||||
new() { FirstName = "Test", LastName = lastName, IsActive = active };
|
||||
|
||||
private static GroupMembership Membership(LearningGroup group, Student student,
|
||||
MembershipPeriod period, DateOnly? leftAt = null) => new()
|
||||
{
|
||||
GroupId = group.Id,
|
||||
StudentId = student.Id,
|
||||
Period = period,
|
||||
JoinedAt = new DateOnly(2025, 8, 1),
|
||||
LeftAt = leftAt,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public class ManageStudentDialogViewModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void VerknuepfteDatenSperrenEndgueltigesLoeschenAberNichtDeaktivieren()
|
||||
{
|
||||
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var students = new FakeStudents([student]);
|
||||
students.SetReferenceSummary(student.Id, new StudentReferenceSummary(1, 2, 0, 0, 3, 1));
|
||||
var vm = new ManageStudentDialogViewModel(students, student);
|
||||
|
||||
Assert.False(vm.CanDelete);
|
||||
Assert.Equal(4, vm.ReferenceItems.Count);
|
||||
|
||||
vm.DeactivateCommand.Execute(null);
|
||||
|
||||
Assert.False(student.IsActive);
|
||||
Assert.Equal(StudentManagementResult.Deactivated, vm.Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnverknuepfterSchuelerKannEndgueltigGeloeschtWerden()
|
||||
{
|
||||
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var students = new FakeStudents([student]);
|
||||
var vm = new ManageStudentDialogViewModel(students, student);
|
||||
|
||||
vm.DeletePermanentlyCommand.Execute(null);
|
||||
|
||||
Assert.Equal(StudentManagementResult.Deleted, vm.Result);
|
||||
Assert.Null(students.GetById(student.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeaktivierterSchuelerKannWiederAktiviertWerden()
|
||||
{
|
||||
var student = new Student { FirstName = "Anna", LastName = "Beispiel", IsActive = false };
|
||||
var vm = new ManageStudentDialogViewModel(new FakeStudents([student]), student);
|
||||
|
||||
vm.ReactivateCommand.Execute(null);
|
||||
|
||||
Assert.True(student.IsActive);
|
||||
Assert.Equal(StudentManagementResult.Reactivated, vm.Result);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,8 @@ public sealed class SettingsViewModelTests
|
||||
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
|
||||
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||
holidays ?? new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath),
|
||||
new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties());
|
||||
new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties(),
|
||||
new LetterTemplateService(tempPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -100,7 +101,7 @@ public sealed class SettingsViewModelTests
|
||||
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
|
||||
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||
new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath),
|
||||
new FakeSupervisionDuties());
|
||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath));
|
||||
|
||||
vm.SelectedStateName = "Bayern";
|
||||
|
||||
@@ -122,7 +123,7 @@ public sealed class SettingsViewModelTests
|
||||
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
|
||||
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
||||
new FakeSupervisionDuties());
|
||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath));
|
||||
|
||||
vm.PeriodTimes[0].StartText = "08:00";
|
||||
vm.PeriodTimes[0].EndText = "08:45";
|
||||
@@ -148,7 +149,7 @@ public sealed class SettingsViewModelTests
|
||||
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
|
||||
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
||||
new FakeSupervisionDuties());
|
||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath));
|
||||
|
||||
vm.PeriodTimes[0].StartText = "08:45";
|
||||
vm.PeriodTimes[0].EndText = "08:00";
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public class StudentListViewModelTests
|
||||
{
|
||||
private readonly Student _anna = new() { FirstName = "Anna", LastName = "Beispiel", Gender = Gender.W };
|
||||
private readonly Student _ben = new() { FirstName = "Ben", LastName = "Muster", Gender = Gender.M };
|
||||
private readonly LearningGroup _group = new() { Name = "Mathematik 7a" };
|
||||
|
||||
[Fact]
|
||||
public void SucheFindetSchuelerWeiterhinUeberNamen()
|
||||
{
|
||||
var vm = BuildViewModel();
|
||||
|
||||
vm.SearchText = "Anna";
|
||||
|
||||
Assert.Single(vm.Students);
|
||||
Assert.Equal(_anna.Id, vm.Students[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SucheFindetSchuelerUeberLerngruppe()
|
||||
{
|
||||
var vm = BuildViewModel();
|
||||
|
||||
vm.SearchText = "7a";
|
||||
|
||||
Assert.Single(vm.Students);
|
||||
Assert.Equal(_ben.Id, vm.Students[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeereSucheZeigtAlleSchueler()
|
||||
{
|
||||
var vm = BuildViewModel();
|
||||
|
||||
vm.SearchText = " ";
|
||||
|
||||
Assert.Equal(2, vm.Students.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListeneintraegeZeigenAvatarAusGespeichertemGeschlecht()
|
||||
{
|
||||
var vm = BuildViewModel();
|
||||
|
||||
Assert.Equal("W", vm.Students.Single(s => s.Id == _anna.Id).AvatarLabel);
|
||||
Assert.Equal("M", vm.Students.Single(s => s.Id == _ben.Id).AvatarLabel);
|
||||
}
|
||||
|
||||
private StudentListViewModel BuildViewModel() => new(
|
||||
new FakeStudents([_anna, _ben]),
|
||||
new FakeGroups([_group]),
|
||||
new FakeMemberships([
|
||||
new GroupMembership { StudentId = _ben.Id, GroupId = _group.Id },
|
||||
]));
|
||||
}
|
||||
Reference in New Issue
Block a user