Files
LehrerApp/LehrerApp.Desktop.Tests/PdfExportServiceTests.cs
T
adminandClaude Sonnet 5 038337997f feat: Anhänge je Stunde, Klausur-Sitzplan mischen, Gefährdungsbeurteilungs-Assistent
Lesson bekommt dieselbe Anhang-Infrastruktur wie Documentation (Material,
Arbeitsblätter, Experimentunterlagen), samt Fix einer Sync-Lücke, die Anhang-
Dateibytes bisher nur für Documentation statt generisch übertragen hat
(IHasAttachments). Sitzplan-Tab bekommt einen "Plätze mischen"-Button für
Klausursitzpläne. Neu: mehrschrittiger Gefährdungsbeurteilungs-Assistent mit
optionalem KI-Entwurf (ai-backend/gbu.php) und PDF-Export, Format bewusst als
JSON-Anhang statt eigener Datenbank-Entität. Details und Architekturentscheidungen
in TODO.md (4.2, 7.1.5, 10.1.8).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 17:13:14 +02:00

186 lines
8.0 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.Desktop.Services;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public sealed class PdfExportServiceTests
{
private static readonly PdfExportService Service = new();
private static void AssertIsPdf(byte[] bytes)
{
Assert.True(bytes.Length > 500, "PDF ist verdächtig klein.");
Assert.Equal("%PDF"u8.ToArray(), bytes[..4]);
}
[Fact]
public void BuildSeatingPlanPdf_VollstaendigerPlan_LiefertGueltigesPdf()
{
var data = new SeatingPlanPrintData("Standard", "Raum B204 · 2 × 3 Plätze",
Columns: 3, IsBoardAtBottom: false, ColumnGapWidths: [0, 90],
Seats:
[
new SeatPrintCell(0, 0, "Beispiel, Anna", false),
new SeatPrintCell(0, 1, null, false),
new SeatPrintCell(0, 2, "Muster, Ben", false),
new SeatPrintCell(1, 0, null, false),
new SeatPrintCell(1, 1, "Probe, Cem", false),
new SeatPrintCell(1, 2, null, false),
]);
AssertIsPdf(Service.BuildSeatingPlanPdf(data));
}
/// Ausgeblendete Plätze (kein Tisch im Raum, siehe SeatingPlan.HiddenSeats) müssen als leere
/// Rasterposition mitgedruckt werden, ohne die Erzeugung zu stören.
[Fact]
public void BuildSeatingPlanPdf_MitAusgeblendetenPlaetzenUndTafelUnten_LiefertGueltigesPdf()
{
var data = new SeatingPlanPrintData("Physikraum", "",
Columns: 2, IsBoardAtBottom: true, ColumnGapWidths: [],
Seats:
[
new SeatPrintCell(0, 0, "Beispiel, Anna", false),
new SeatPrintCell(0, 1, null, true),
new SeatPrintCell(1, 0, null, true),
new SeatPrintCell(1, 1, null, false),
]);
AssertIsPdf(Service.BuildSeatingPlanPdf(data));
}
[Fact]
public void BuildGradeListPdf_MatrixMitMehrerenSpalten_LiefertGueltigesPdf()
{
var data = new GradeListPrintData("8a Chemie", "2026/27", "1. Halbjahr",
ColumnHeaders: ["12.09. Klausur 1", "Mündlich 20.09.", "Hausaufgaben 01.10."],
Rows:
[
new GradeListPrintRow("Beispiel, Anna", ["2", "1", ""], "1.67"),
new GradeListPrintRow("Muster, Ben", ["abwesend", "3", "2"], "2.50"),
new GradeListPrintRow("Probe, Cem", ["", "", ""], ""),
]);
AssertIsPdf(Service.BuildGradeListPdf(data));
}
/// Ein Kurs kann übers Jahr viele Notenspalten ansammeln - die Tabelle muss auch dann noch
/// erzeugbar sein (QuestPDF staucht die Spalten, statt zu werfen).
[Fact]
public void BuildGradeListPdf_VieleSpalten_LiefertGueltigesPdf()
{
var headers = Enumerable.Range(1, 20).Select(i => $"Note {i}").ToList();
var rows = Enumerable.Range(1, 30).Select(i => new GradeListPrintRow(
$"Schüler {i}", [.. Enumerable.Repeat("2", 20)], "2.00")).ToList();
AssertIsPdf(Service.BuildGradeListPdf(new GradeListPrintData("LK Chemie", "2026/27",
"Gesamtes Schuljahr", headers, rows)));
}
[Fact]
public void BuildExamStatisticsPdf_MitVerteilungUndAufgabenanalyse_LiefertGueltigesPdf()
{
var data = new ExamStatisticsPrintData("Klausur 1 - Säuren und Basen", "12.09.2026", "E",
Stats:
[
new LabelValue("Bewertet", "24"),
new LabelValue("Abwesend", "2"),
new LabelValue("Durchschnitt", "2.71"),
new LabelValue("Median", "3"),
new LabelValue("Anteil nicht ausreichend (Note 5/6)", "8.3 %"),
],
Distribution:
[
new GradeDistributionPrintRow("1", 3, 12.5),
new GradeDistributionPrintRow("2", 8, 33.3),
new GradeDistributionPrintRow("3", 7, 29.2),
new GradeDistributionPrintRow("4", 4, 16.7),
new GradeDistributionPrintRow("5", 2, 8.3),
new GradeDistributionPrintRow("6", 0, 0),
],
TaskAnalysis:
[
new TaskAnalysisPrintRow("1. Titration", 78.4, false),
new TaskAnalysisPrintRow("2. pH-Berechnung", 42.1, true),
]);
AssertIsPdf(Service.BuildExamStatisticsPdf(data));
}
/// Auch ohne Aufgabenstruktur (Klausur ohne Teilaufgaben) und ohne Niveau muss der
/// Notenspiegel druckbar sein - der Abschnitt Aufgabenanalyse entfällt dann einfach.
[Fact]
public void BuildExamStatisticsPdf_OhneAufgabenUndNiveau_LiefertGueltigesPdf()
{
var data = new ExamStatisticsPrintData("Test", "01.10.2026", "",
[new LabelValue("Bewertet", "0")], [], []);
AssertIsPdf(Service.BuildExamStatisticsPdf(data));
}
[Fact]
public void BuildCompetencyReportPdf_LiefertGueltigesPdf()
{
var data = new CompetencyReportPrintData("8a Chemie", "Beispiel, Anna",
"3 von 5 behandelt · 2 geprüft · 2 mit Ergebnisdaten", "Wiederholungsbedarf unter 60 %",
[
new CompetencyReportPrintRow("Stoff-Teilchen (ST)", "ST1",
"Aggregatzustände auf Teilchenebene erklären", "2× behandelt", "1× geprüft",
"72.5 % · 24 Schüler · 3 Aufgabenwerte", "85 % · 3 Aufgabenwerte", "Stand solide"),
new CompetencyReportPrintRow("Chemische Reaktion (CR)", "CR2",
"Reaktionsgleichungen aufstellen", "1× behandelt · 1× geplant", "1× geprüft",
"48.3 % · 24 Schüler · 2 Aufgabenwerte", "40 % · 2 Aufgabenwerte", "Wiederholungsbedarf"),
new CompetencyReportPrintRow("Energie (EN)", "", "Energiediagramme deuten",
"", "1× geplant", "", "", "Noch keine Bewertung"),
]);
AssertIsPdf(Service.BuildCompetencyReportPdf(data));
}
[Fact]
public void BuildStudentDocumentationPdf_MitVertraulichenUndEntwurfsEintraegen_LiefertGueltigesPdf()
{
var data = new StudentDocumentationPrintData("Beispiel, Anna",
[
new DocumentationPrintEntry("12.09.2026", "Beobachtung", "Unterricht gestört",
"Wiederholt dazwischengerufen, nach Ermahnung gebessert.",
["Kritisch"], IsConfidential: false, IsDraft: false),
new DocumentationPrintEntry("20.09.2026", "Elterngespräch", "Telefonat Mutter",
"Absprache: wöchentliche Rückmeldung im Hausaufgabenheft.",
[], IsConfidential: true, IsDraft: false),
new DocumentationPrintEntry("01.10.2026", "Beobachtung", "Schnellnotiz",
"", ["Nacharbeiten"], IsConfidential: false, IsDraft: true),
]);
AssertIsPdf(Service.BuildStudentDocumentationPdf(data));
}
[Fact]
public void BuildHazardAssessmentPdf_MitStoffenUndKiHinweis_LiefertGueltigesPdf()
{
var data = new HazardAssessmentPrintData(
"Natrium in Wasser", "10c", "12.09.2026", "Lehrerversuch",
"Ein kleines Stück Natrium wird in eine Schale mit Wasser gegeben.",
[
new HazardSubstancePrintRow("Natrium", "ca. 0,5 g", "GHS02, GHS05",
"H260 - Reagiert mit Wasser unter Bildung entzündbarer Gase", "P223, P231+P232, P370+P378"),
],
["Wasserstoffbildung, Verpuffungsgefahr", "Verätzungsgefahr durch Natronlauge"],
["Schutzbrille", "Sicherheitsabstand einhalten", "Abzug/Freiluftversuch"],
"Betroffene Stellen mit viel Wasser spülen, Arzt hinzuziehen.",
"Reste unter Aufsicht vollständig abreagieren lassen.",
"", IsAiAssisted: true);
AssertIsPdf(Service.BuildHazardAssessmentPdf(data));
}
[Fact]
public void BuildHazardAssessmentPdf_OhneStoffeUndOhneKiHinweis_LiefertGueltigesPdf()
{
var data = new HazardAssessmentPrintData(
"Papierchromatographie", "8b", "", "Schülerversuch", "", [], [], [], "", "", "", IsAiAssisted: false);
AssertIsPdf(Service.BuildHazardAssessmentPdf(data));
}
}