diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3cca5a0..8f69577 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,7 +25,7 @@ jobs:
run: dotnet build AndiGenerator.slnx -c Release -nologo
- name: Testen (mit Messung der Testabdeckung)
- run: dotnet test AndiGenerator.slnx -c Release --no-build -nologo --logger "trx" --results-directory TestResults --collect "Code Coverage;Format=cobertura"
+ run: dotnet test AndiGenerator.slnx -c Release --no-build -nologo --logger "trx" --results-directory TestResults --filter "Kategorie!=Messung" --settings .runsettings --collect "Code Coverage;Format=cobertura"
- name: Testabdeckung zusammenfassen
if: always()
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4cd09d5..f7846de 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -37,7 +37,7 @@ jobs:
- name: Bauen und testen
run: |
dotnet build AndiGenerator.slnx -c Release -nologo -p:Version=${{ steps.version.outputs.version }}
- dotnet test AndiGenerator.slnx -c Release --no-build -nologo
+ dotnet test AndiGenerator.slnx -c Release --no-build -nologo --filter "Kategorie!=Messung"
- name: Veröffentlichen
run: dotnet publish src/AndiGenerator.Desktop -c Release -r win-x64 --self-contained -nologo -o publish -p:Version=${{ steps.version.outputs.version }}
diff --git a/.gitignore b/.gitignore
index 23b47e5..6bdd97c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,6 +65,7 @@ build-log.txt
build-messung.txt
optimierung-log.txt
abdeckung-log.txt
+messen-log.txt
# Private Originaldaten (Klarnamen) – öffentlich nur die anonymisierte Kopie unter testdaten/
/Referenz/
diff --git a/.runsettings b/.runsettings
new file mode 100644
index 0000000..82b17b7
--- /dev/null
+++ b/.runsettings
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+ .*[\\/]AndiGenerator\.[^\\/]*\.dll$
+
+
+ .*\.Tests\.dll$
+
+
+
+
+
+
+
+
diff --git a/AndiGenerator.slnx b/AndiGenerator.slnx
index 53651a2..6a7cebe 100644
--- a/AndiGenerator.slnx
+++ b/AndiGenerator.slnx
@@ -13,6 +13,7 @@
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 398a8af..a0afaf0 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,6 +9,7 @@
+
diff --git a/Doku/MIGRATIONSPLAN.md b/Doku/MIGRATIONSPLAN.md
index ecb8bd0..fa03813 100644
--- a/Doku/MIGRATIONSPLAN.md
+++ b/Doku/MIGRATIONSPLAN.md
@@ -682,3 +682,6 @@ Die **TT-Spielplan Engine** (Stephan Rüb, v1.1.2) ist ein neues, unabhängiges
| 01.10.2026 | Vorbereitung SignPath: `CODE_SIGNING.md` (Signierung, Ablauf, Rollen, Datenschutz, englische Kurzfassung; aus README und SECURITY.md verlinkt); Programmeinstellung „Beim Start nach neuen Versionen suchen“ im Dialog „Über“ (Standard an, `programm-einstellungen.txt` im eigenen Ordner), die Update-Suche richtet sich danach; Anleitung ergänzt |
| 01.10.2026 | Architekturtests als eigenes Testprojekt `AndiGenerator.Architektur.Tests`: liest die gebauten DLLs mit System.Reflection.Metadata (ohne sie zu laden) und prüft (1) Schichten verwenden nur tieferliegende Schichten (transitive Hülle der erlaubten Verweise), (2) Domain, Engine, Persistence, Rendering und Application nur .NET selbst, (3) Avalonia, Skia, Dock.Avalonia und Velopack nur in UI und Desktop, (4) Rechenkern (Domain, Engine, Rendering) ohne Datei-, Netz-, Prozess- und Konsolenzugriff, (5) Namensräume folgen dem Projekt, (6) ViewModels nur in der Presentation, (7) jede Quelldatei mit SPDX-Kopf; die bisherigen Prüfungen der Projektdateien (`SchichtenTests`) sind als `ProjektverweisTests` dorthin umgezogen |
| 01.10.2026 | Weitere Qualitätsmaßnahmen: Dependabot für Versions-Updates (NuGet und GitHub Actions, monatlich, gebündelt; Velopack ausgenommen, weil es mit `vpk` zusammen aktualisiert wird); Testabdeckung im CI (Microsoft Code Coverage, Zusammenfassung je Schicht über `tools/abdeckung/zusammenfassen.ps1` in der Zusammenfassung des Laufs, Berichte als Artefakt); Release mit Stückliste (SBOM, SPDX) und GitHub-Herkunftsnachweisen für Setup, ZIP und Pakete |
+| 01.10.2026 | Oberflächentests als eigenes Testprojekt `AndiGenerator.UI.Tests` mit Avalonia.Headless (MIT, nur für Tests): echte Anwendung mit Stilen und Skia-Zeichnung ohne Bildschirm; Rauchtest des Hauptfensters (Staffel öffnen, Einrichtung, alle Ansichten mit dem click-TT-Plan zeigen und zeichnen), der einfachen Dialoge und aller Seiten von „Spielplandaten bearbeiten“; Regressionstest Doppelklick auf einen Wunschtermin; Avalonia-Fehlermeldungen (Protokoll) lassen die Tests scheitern. `THIRD-PARTY-NOTICES.md` ohne Versionsnummern (maßgeblich sind `Directory.Packages.props` und die Workflows) |
+| 01.10.2026 | Testabdeckung ohne Testprojekte: `.runsettings` im Lösungsordner misst nur die Programmschichten – Visual Studio erkennt die Datei selbst, `abdeckung.cmd` und CI geben sie mit `--settings` an |
+| 01.10.2026 | Schnellerer Testlauf: die Geschwindigkeitsmessungen (`Messungen`, rund 20 s, laufen allein und halten alle anderen Tests auf) tragen die Kategorie `Messung` und laufen nicht mehr in `bauen.cmd`, `abdeckung.cmd`, CI und Release, sondern mit dem neuen `messen.cmd`; der Test der Spezial-Insel rechnet in Schritten von 10 000 Durchläufen nur noch so lange, bis die Spezial-Insel eine Insel neu gestartet hat (vorher fest 200 000, rund 14 s) |
diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md
index 01ad519..ce05121 100644
--- a/THIRD-PARTY-NOTICES.md
+++ b/THIRD-PARTY-NOTICES.md
@@ -1,21 +1,22 @@
# Drittbibliotheken
-Für alles, was mit dem Programm ausgeliefert oder im Quellcode als Bibliothek verwendet wird, sind nur MIT-, Apache-2.0-, BSD- oder LGPL-lizenzierte Pakete zulässig (MIGRATIONSPLAN E11). Reine Entwicklungswerkzeuge sind davon ausgenommen (siehe unten). Jede neue Abhängigkeit wird hier eingetragen.
+Für alles, was mit dem Programm ausgeliefert oder im Quellcode als Bibliothek verwendet wird, sind nur MIT-, Apache-2.0-, BSD- oder LGPL-lizenzierte Pakete zulässig (MIGRATIONSPLAN E11). Reine Entwicklungswerkzeuge sind davon ausgenommen (siehe unten). Jede neue Abhängigkeit wird hier eingetragen. Die gültigen Versionen der NuGet-Pakete stehen in [`Directory.Packages.props`](Directory.Packages.props) (Dependabot hält sie aktuell), die der GitHub Actions in den Workflows unter `.github/workflows`.
Ausnahme Schriften: Die SIL Open Font License (OFL 1.1) erlaubt das Einbetten und Weitergeben mit GPL-Software; der Lizenztext liegt neben den Schriftdateien, der reservierte Name „Plex“ wird nicht für veränderte Schriften verwendet.
-| Paket | Version | Lizenz | Verwendung |
-|---|---|---|---|
-| .NET 10 Runtime/BCL | 10.0 | MIT | Laufzeit |
-| Avalonia, Avalonia.Desktop, Avalonia.Themes.Fluent | 12.1.3 | MIT | Oberfläche (E2) |
-| Avalonia.Skia (mit SkiaSharp, HarfBuzzSharp) | 12.1.3 | MIT | PDF-Ausdruck (`SKDocument`, Phase 7) |
-| Dock.Avalonia, Dock.Avalonia.Themes.Fluent, Dock.Model.Mvvm | 12.1.0.6 | MIT | Andock-Layout der Ansichten (E15) |
-| CommunityToolkit.Mvvm | 8.4.0 | MIT | ViewModels (E3) |
-| Velopack | 0.0.1298 | MIT | Installation und Updates über GitHub Releases (E12) |
-| Schriften IBM Plex Sans und IBM Plex Mono (je Regular, Medium, SemiBold, Bold) | 2017 IBM Corp. | SIL OFL 1.1 | Schrift der Oberfläche, eingebettet unter `src/AndiGenerator.UI/Assets/Fonts` mit `OFL.txt` |
-| xunit | 2.9.3 | Apache-2.0 | nur Tests |
-| xunit.runner.visualstudio | 3.1.0 | Apache-2.0 | nur Tests |
-| Microsoft.NET.Test.Sdk | 17.14.1 | MIT | nur Tests |
+| Paket | Lizenz | Verwendung |
+|---|---|---|
+| .NET 10 Runtime/BCL | MIT | Laufzeit |
+| Avalonia, Avalonia.Desktop, Avalonia.Themes.Fluent | MIT | Oberfläche (E2) |
+| Avalonia.Skia (mit SkiaSharp, HarfBuzzSharp) | MIT | PDF-Ausdruck (`SKDocument`, Phase 7) |
+| Dock.Avalonia, Dock.Avalonia.Themes.Fluent, Dock.Model.Mvvm | MIT | Andock-Layout der Ansichten (E15) |
+| CommunityToolkit.Mvvm | MIT | ViewModels (E3) |
+| Velopack | MIT | Installation und Updates über GitHub Releases (E12); Version gemeinsam mit `VPK_VERSION` in `release.yml` |
+| Schriften IBM Plex Sans und IBM Plex Mono (je Regular, Medium, SemiBold, Bold), © 2017 IBM Corp. | SIL OFL 1.1 | Schrift der Oberfläche, eingebettet unter `src/AndiGenerator.UI/Assets/Fonts` mit `OFL.txt` |
+| xunit | Apache-2.0 | nur Tests |
+| xunit.runner.visualstudio | Apache-2.0 | nur Tests |
+| Microsoft.NET.Test.Sdk | MIT | nur Tests |
+| Avalonia.Headless | MIT | nur Tests (Oberflächentests ohne Bildschirm) |
## Werkzeuge nur zur Build-Zeit (werden nicht ausgeliefert)
@@ -23,11 +24,11 @@ Analyzer laufen nur beim Kompilieren und landen nicht in den Programmdateien (`P
Geprüft vor der Veröffentlichung (01.10.2026): SonarAnalyzer.CSharp steht seit Version 10.33 unter der Sonar Source-Available License. Wir halten das für unproblematisch, weil der Analyzer ein reines Entwicklungswerkzeug ist – wie ein Compiler: Er wird beim Bauen von NuGet geladen, ist nicht Teil des Quellcodes im Repository und wird weder ausgeliefert noch signiert. E11 nimmt solche Werkzeuge seit der Präzisierung vom 01.10.2026 ausdrücklich von der Lizenzregel aus; die Bedingung der SignPath Foundation (kein proprietärer Code in den signierten Dateien) betrifft ebenfalls nur ausgelieferte Bestandteile. Wer das Projekt ohne Sonar bauen möchte, entfernt die Zeile in `Directory.Packages.props`; StyleCop (MIT) bleibt.
-| Paket | Version | Lizenz | Verwendung |
-|---|---|---|---|
-| StyleCop.Analyzers | 1.2.0-beta.556 | MIT | Stilprüfung (Konfiguration: `stylecop.json`, `.editorconfig`) |
-| anchore/sbom-action (GitHub Action, nutzt Syft) | v0 | Apache-2.0 | Stückliste (SBOM) im Release-Workflow |
-| actions/attest-build-provenance, actions/attest-sbom (GitHub Actions) | v2 | MIT | Herkunftsnachweise im Release-Workflow |
-| SonarAnalyzer.CSharp | 10.33.0.1635 | Sonar Source-Available License v1 (kein OSI-Open-Source; bis 10.32 LGPL-3.0) | Qualitätsprüfung |
+| Paket | Lizenz | Verwendung |
+|---|---|---|
+| StyleCop.Analyzers | MIT | Stilprüfung (Konfiguration: `stylecop.json`, `.editorconfig`) |
+| anchore/sbom-action (GitHub Action, nutzt Syft) | Apache-2.0 | Stückliste (SBOM) im Release-Workflow |
+| actions/attest-build-provenance, actions/attest-sbom (GitHub Actions) | MIT | Herkunftsnachweise im Release-Workflow |
+| SonarAnalyzer.CSharp | Sonar Source-Available License v1 (kein OSI-Open-Source; bis 10.32 LGPL-3.0) | Qualitätsprüfung |
Geplant: Avalonia.Controls.DataGrid (MIT). **Nicht** zulässig: Avalonia TreeDataGrid/Accelerate, QuestPDF.
diff --git a/abdeckung.cmd b/abdeckung.cmd
index b86a628..4f31b06 100644
--- a/abdeckung.cmd
+++ b/abdeckung.cmd
@@ -4,7 +4,7 @@ chcp 65001 >nul
cd /d "%~dp0"
set "LOG=%~dp0abdeckung-log.txt"
echo ===== START %date% %time% > "%LOG%"
-dotnet test AndiGenerator.slnx -c Release --no-build -nologo --collect "Code Coverage;Format=cobertura" --results-directory TestResults\Abdeckung >> "%LOG%" 2>&1
+dotnet test AndiGenerator.slnx -c Release --no-build -nologo --filter "Kategorie!=Messung" --settings .runsettings --collect "Code Coverage;Format=cobertura" --results-directory TestResults\Abdeckung >> "%LOG%" 2>&1
echo ----- TEST Exitcode %ERRORLEVEL% >> "%LOG%"
echo ----- ZUSAMMENFASSUNG >> "%LOG%"
powershell -NoProfile -ExecutionPolicy Bypass -File tools\abdeckung\zusammenfassen.ps1 -Ordner TestResults\Abdeckung >> "%LOG%" 2>&1
diff --git a/bauen.cmd b/bauen.cmd
index bd7db19..1182842 100644
--- a/bauen.cmd
+++ b/bauen.cmd
@@ -21,7 +21,7 @@ echo ----- BUILD Exitcode %BUILDRC% >> "%LOG%"
if not "%BUILDRC%"=="0" goto ende
echo Teste ...
echo ----- TEST >> "%LOG%"
-dotnet test AndiGenerator.slnx -c Release --no-build -nologo --logger "console;verbosity=normal" >> "%LOG%" 2>&1
+dotnet test AndiGenerator.slnx -c Release --no-build -nologo --filter "Kategorie!=Messung" --logger "console;verbosity=normal" >> "%LOG%" 2>&1
set TESTRC=%ERRORLEVEL%
echo ----- TEST Exitcode %TESTRC% >> "%LOG%"
:ende
diff --git a/messen.cmd b/messen.cmd
new file mode 100644
index 0000000..1afa12c
--- /dev/null
+++ b/messen.cmd
@@ -0,0 +1,14 @@
+@echo off
+rem Geschwindigkeitsmessungen der Engine (Tests der Kategorie "Messung"). Ergebnisse werden an build-messung.txt
+rem angehaengt, das Protokoll steht in messen-log.txt. Vorher bauen.cmd ausfuehren.
+chcp 65001 >nul
+cd /d "%~dp0"
+set "LOG=%~dp0messen-log.txt"
+echo ===== START %date% %time% > "%LOG%"
+echo Messe ...
+dotnet test tests\AndiGenerator.Persistence.Tests\AndiGenerator.Persistence.Tests.csproj -c Release --no-build -nologo --filter "Kategorie=Messung" --logger "console;verbosity=normal" >> "%LOG%" 2>&1
+echo ----- TEST Exitcode %ERRORLEVEL% >> "%LOG%"
+echo ===== ENDE %date% %time% >> "%LOG%"
+echo.
+echo Fertig. Ergebnisse in build-messung.txt - Claude kann sie jetzt lesen.
+timeout /t 5 >nul
diff --git a/src/AndiGenerator.UI/AndiGenerator.UI.csproj b/src/AndiGenerator.UI/AndiGenerator.UI.csproj
index 1eeacc5..a866c0a 100644
--- a/src/AndiGenerator.UI/AndiGenerator.UI.csproj
+++ b/src/AndiGenerator.UI/AndiGenerator.UI.csproj
@@ -17,6 +17,10 @@
+
+
+
+
diff --git a/tests/AndiGenerator.Persistence.Tests/InseloptimiererTests.cs b/tests/AndiGenerator.Persistence.Tests/InseloptimiererTests.cs
index 22f55f3..2fb480a 100644
--- a/tests/AndiGenerator.Persistence.Tests/InseloptimiererTests.cs
+++ b/tests/AndiGenerator.Persistence.Tests/InseloptimiererTests.cs
@@ -77,7 +77,11 @@ Optimierungseinstellungen.Standard with
MindestDurchlaeufeSpezialInsel = 3_000,
});
- optimierer.Rechnen(200_000);
+ // In Schritten rechnen und aufhören, sobald die Spezial-Insel eine Insel neu gestartet hat (statt immer 200 000).
+ for (long durchlaeufe = 10_000; optimierer.SpezialVerwendet == 0 && durchlaeufe <= 200_000; durchlaeufe += 10_000)
+ {
+ optimierer.Rechnen(durchlaeufe);
+ }
Assert.NotNull(optimierer.SpezialKostenart);
Assert.True(optimierer.SpezialVerwendet > 0);
diff --git a/tests/AndiGenerator.Persistence.Tests/Messungen.cs b/tests/AndiGenerator.Persistence.Tests/Messungen.cs
index baf8b6c..46daeec 100644
--- a/tests/AndiGenerator.Persistence.Tests/Messungen.cs
+++ b/tests/AndiGenerator.Persistence.Tests/Messungen.cs
@@ -16,9 +16,11 @@ namespace AndiGenerator.Persistence.Tests;
///
/// Geschwindigkeitsmessungen (Ausgabe in build-messung.txt). Sie prüfen nichts Fachliches und laufen allein,
-/// damit andere Tests die Kerne nicht mitbenutzen.
+/// damit andere Tests die Kerne nicht mitbenutzen. Wegen ihrer Laufzeit gehören sie nicht zum normalen Testlauf
+/// (bauen.cmd, abdeckung.cmd, CI filtern Kategorie!=Messung), sondern laufen mit messen.cmd.
///
[Collection(MessungenSammlung.Name)]
+[Trait("Kategorie", "Messung")]
public class Messungen
{
private static readonly string[] Strategien = ["R", "15", "5", "S1,25", "M2,10"];
diff --git a/tests/AndiGenerator.UI.Tests/AndiGenerator.UI.Tests.csproj b/tests/AndiGenerator.UI.Tests/AndiGenerator.UI.Tests.csproj
new file mode 100644
index 0000000..9ece89b
--- /dev/null
+++ b/tests/AndiGenerator.UI.Tests/AndiGenerator.UI.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_Parameter1>Referenz
+ <_Parameter2>$(MSBuildThisFileDirectory)../../testdaten/referenz
+
+
+
diff --git a/tests/AndiGenerator.UI.Tests/DialogTests.cs b/tests/AndiGenerator.UI.Tests/DialogTests.cs
new file mode 100644
index 0000000..2b71720
--- /dev/null
+++ b/tests/AndiGenerator.UI.Tests/DialogTests.cs
@@ -0,0 +1,122 @@
+// SPDX-FileCopyrightText: 2017 Andreas Hofmann
+// SPDX-FileCopyrightText: 2026 Peter Buchmann
+// SPDX-License-Identifier: GPL-3.0-only
+
+using AndiGenerator.Domain.Optionen;
+using AndiGenerator.Presentation;
+using AndiGenerator.Presentation.Tests;
+using AndiGenerator.UI.Seiten;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Headless;
+using Avalonia.Input;
+using Avalonia.VisualTree;
+using Xunit.Abstractions;
+
+namespace AndiGenerator.UI.Tests;
+
+/// Rauchtests der Dialoge: öffnen, alle Seiten zeigen, zeichnen, schließen – ohne Fehler von Avalonia.
+[Collection(OberflaechenSammlung.Name)]
+public sealed class DialogTests
+{
+ private readonly Oberflaeche oberflaeche;
+ private readonly ITestOutputHelper ausgabe;
+
+ public DialogTests(Oberflaeche oberflaeche, ITestOutputHelper ausgabe)
+ {
+ this.oberflaeche = oberflaeche;
+ this.ausgabe = ausgabe;
+ oberflaeche.Protokoll.Leeren();
+ }
+
+ [Fact]
+ public Task Einfache_Dialoge_werden_ohne_Fehler_angezeigt() => oberflaeche.Ausfuehren(() =>
+ {
+ var o = new TestOberflaeche();
+ Anzeigen(new Ueberdialog(new UeberViewModel(o, "1.0")));
+ Anzeigen(new Druckauswahldialog(new DruckauswahlViewModel([PlanQuelle.ClickTt])));
+ Anzeigen(new Dialogfenster("Frage", "Wirklich?", null, mitAbbrechen: true));
+ Anzeigen(new Dialogfenster("Name", "Name des Plans:", "Plan 1", mitAbbrechen: true));
+ Anzeigen(new Dialogfenster("Entscheidung", "Was tun?", ["Erster Punkt", "Zweiter Punkt"], "Ja", "Nein"));
+ Anzeigen(new Gewichtungsdialog("Gewichtung", "Wunschtermine", Gewichtung.Normal, ["Ein Hinweis"]));
+ oberflaeche.KeineFehler(ausgabe);
+ return Task.CompletedTask;
+ });
+
+ [Fact]
+ public Task Spielplandaten_zeigen_alle_Seiten_ohne_Fehler() => oberflaeche.Ausfuehren(() =>
+ {
+ using var a = new Arbeitsplatz("datendialoge/D01_Allgemein.xml");
+ var fenster = new Datendialog(a.Dialog);
+ fenster.Show();
+ try
+ {
+ ListBox liste = Seitenliste(fenster);
+ foreach (DatenSeiteViewModel seite in a.Dialog.Seiten)
+ {
+ liste.SelectedItem = seite;
+ Oberflaeche.Zeichnen(fenster);
+ Assert.Same(seite, a.Dialog.Seite);
+ if (seite is MehrMannschaftenSeiteViewModel mehr && mehr.Namen.Count > 0)
+ {
+ mehr.Mannschaft = mehr.Namen[^1];
+ Oberflaeche.Zeichnen(fenster);
+ Assert.NotNull(mehr.Detail);
+ }
+ }
+ }
+ finally
+ {
+ fenster.Close();
+ }
+
+ oberflaeche.KeineFehler(ausgabe);
+ return Task.CompletedTask;
+ });
+
+ [Fact]
+ public Task Doppelklick_auf_einen_Wunschtermin_oeffnet_den_Dialog() => oberflaeche.Ausfuehren(() =>
+ {
+ using var a = new Arbeitsplatz("datendialoge/D01_Allgemein.xml");
+ var fenster = new Datendialog(a.Dialog);
+ fenster.Show();
+ try
+ {
+ WunschtermineSeiteViewModel seite = a.Dialog.Seiten.OfType().Single();
+ Seitenliste(fenster).SelectedItem = seite;
+ seite.Mannschaft = Namen.RheinduerkheimIV;
+ Oberflaeche.Zeichnen(fenster);
+
+ // Regression: Das Textfeld schluckte den Doppelklick (Wort markieren), der Dialog ging nicht auf.
+ WunschterminDetail detail = Assert.Single(fenster.GetVisualDescendants().OfType());
+ Point punkt = Assert.IsType(detail.GetVisualDescendants()
+ .OfType()
+ .Where(t => t.DataContext is WunschterminZelle)
+ .Select(t => t.TranslatePoint(new Point(t.Bounds.Width / 2, t.Bounds.Height / 2), fenster))
+ .FirstOrDefault(p => p is Point q && new Rect(fenster.ClientSize).Contains(q)));
+ fenster.MouseDown(punkt, MouseButton.Left);
+ fenster.MouseUp(punkt, MouseButton.Left);
+ fenster.MouseDown(punkt, MouseButton.Left);
+ fenster.MouseUp(punkt, MouseButton.Left);
+ Oberflaeche.Zeichnen(fenster);
+ }
+ finally
+ {
+ fenster.Close();
+ }
+
+ Assert.Contains("Wunschtermin", a.Oberflaeche.Aufrufe);
+ oberflaeche.KeineFehler(ausgabe);
+ return Task.CompletedTask;
+ });
+
+ private static ListBox Seitenliste(Window fenster) =>
+ fenster.GetVisualDescendants().OfType().Single(l => l.Classes.Contains("seitenliste"));
+
+ private static void Anzeigen(Window dialog)
+ {
+ dialog.Show();
+ Oberflaeche.Zeichnen(dialog);
+ dialog.Close();
+ }
+}
diff --git a/tests/AndiGenerator.UI.Tests/HauptfensterTests.cs b/tests/AndiGenerator.UI.Tests/HauptfensterTests.cs
new file mode 100644
index 0000000..d8d0959
--- /dev/null
+++ b/tests/AndiGenerator.UI.Tests/HauptfensterTests.cs
@@ -0,0 +1,102 @@
+// SPDX-FileCopyrightText: 2017 Andreas Hofmann
+// SPDX-FileCopyrightText: 2026 Peter Buchmann
+// SPDX-License-Identifier: GPL-3.0-only
+
+using AndiGenerator.Presentation;
+using AndiGenerator.Presentation.Tests;
+using AndiGenerator.UI.Ansichten;
+using Avalonia.Controls;
+using Avalonia.VisualTree;
+using CommunityToolkit.Mvvm.Input;
+using Dock.Avalonia.Controls;
+using Xunit.Abstractions;
+
+namespace AndiGenerator.UI.Tests;
+
+///
+/// Rauchtest des Hauptfensters: Staffel öffnen, Einrichtung abschließen und jede Ansicht einmal mit dem Plan aus click-TT
+/// anzeigen und zeichnen. Die Inhalte prüfen die ViewModel-Tests; hier geht es um XAML, Bindungen, Vorlagen und Zeichnen.
+///
+[Collection(OberflaechenSammlung.Name)]
+public sealed class HauptfensterTests : IDisposable
+{
+ private readonly string ordner = Path.Combine(Path.GetTempPath(), "andigen-ui-" + Guid.NewGuid().ToString("N"));
+ private readonly Oberflaeche oberflaeche;
+ private readonly ITestOutputHelper ausgabe;
+
+ public HauptfensterTests(Oberflaeche oberflaeche, ITestOutputHelper ausgabe)
+ {
+ this.oberflaeche = oberflaeche;
+ this.ausgabe = ausgabe;
+ oberflaeche.Protokoll.Leeren();
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ Directory.Delete(ordner, recursive: true);
+ }
+ catch (IOException)
+ {
+ // Aufräumen ist nicht Teil des Tests.
+ }
+ }
+
+ [Fact]
+ public Task Alle_Ansichten_werden_ohne_Fehler_angezeigt() => oberflaeche.Ausfuehren(async () =>
+ {
+ Directory.CreateDirectory(Path.Combine(ordner, "daten"));
+ string datei = Path.Combine(ordner, "daten", "staffel.xml");
+ File.Copy(Path.Combine(Testdaten.Referenz, "datendialoge", "D01_Allgemein.xml"), datei);
+ var o = new TestOberflaeche { DateiZumOeffnen = datei, Sammeln = true };
+ using var modell = new HauptfensterViewModel(o, () => new HostWindow(), Path.Combine(ordner, "basis"), null);
+ var fenster = new HauptfensterView { DataContext = modell, Width = 1400, Height = 900 };
+ fenster.Show();
+ try
+ {
+ Oberflaeche.Zeichnen(fenster);
+ await modell.OeffnenCommand.ExecuteAsync(null);
+ o.Abarbeiten();
+ Oberflaeche.Zeichnen(fenster);
+ Assert.NotEmpty(fenster.GetVisualDescendants().OfType());
+
+ EinrichtungViewModel einrichtung = Assert.IsType(modell.Einrichtung);
+ await einrichtung.AbschliessenCommand.ExecuteAsync(null);
+ o.Abarbeiten();
+ Oberflaeche.Zeichnen(fenster);
+
+ Zeigen(fenster, o, modell.KostenansichtCommand);
+ Zeigen(fenster, o, modell.QualitaetsansichtCommand);
+ Zeigen(fenster, o, modell.MeldungsansichtCommand);
+ Zeigen(fenster, o, modell.TerminplanansichtCommand);
+ Zeigen(fenster, o, modell.DiagrammansichtCommand);
+ Zeigen(fenster, o, modell.TerminwunschansichtCommand);
+ Zeigen(fenster, o, modell.NachbarterminansichtCommand);
+ Zeigen(fenster, o, modell.EinstellungenCommand);
+ }
+ finally
+ {
+ fenster.Close();
+ }
+
+ Assert.Empty(o.Meldungen);
+ oberflaeche.KeineFehler(ausgabe);
+ });
+
+ private static void Zeigen(Window fenster, TestOberflaeche o, IRelayCommand befehl)
+ where T : Control
+ {
+ Assert.True(befehl.CanExecute(null));
+ befehl.Execute(null);
+ o.Abarbeiten();
+ Oberflaeche.Zeichnen(fenster);
+ T ansicht = Assert.Single(fenster.GetVisualDescendants().OfType());
+ if (ansicht.DataContext is AnsichtViewModel plan)
+ {
+ plan.Quelle = PlanQuelle.ClickTt;
+ o.Abarbeiten();
+ Oberflaeche.Zeichnen(fenster);
+ }
+ }
+}
diff --git a/tests/AndiGenerator.UI.Tests/Oberflaeche.cs b/tests/AndiGenerator.UI.Tests/Oberflaeche.cs
new file mode 100644
index 0000000..20d2e8f
--- /dev/null
+++ b/tests/AndiGenerator.UI.Tests/Oberflaeche.cs
@@ -0,0 +1,72 @@
+// SPDX-FileCopyrightText: 2017 Andreas Hofmann
+// SPDX-FileCopyrightText: 2026 Peter Buchmann
+// SPDX-License-Identifier: GPL-3.0-only
+
+using Avalonia.Controls;
+using Avalonia.Headless;
+using Avalonia.Logging;
+using Avalonia.Media.Imaging;
+using Avalonia.Threading;
+using Xunit.Abstractions;
+
+namespace AndiGenerator.UI.Tests;
+
+///
+/// Eine Avalonia-Sitzung ohne Bildschirm für alle Oberflächentests. Avalonia erlaubt nur einen UI-Thread je Prozess,
+/// deshalb teilen sich die Tests die Sitzung und laufen nacheinander (Sammlung „Oberfläche“).
+///
+public sealed class Oberflaeche : IDisposable
+{
+ private readonly HeadlessUnitTestSession sitzung = HeadlessUnitTestSession.StartNew(typeof(TestAnwendung));
+
+ /// Initialisiert die Sitzung und das Protokoll.
+ public Oberflaeche()
+ {
+ Logger.Sink = Protokoll;
+ }
+
+ /// Holt das Protokoll der Avalonia-Meldungen.
+ internal Protokoll Protokoll { get; } = new();
+
+ /// Arbeitet die anstehenden Aufträge des UI-Threads ab und zeichnet das Fenster einmal vollständig.
+ /// Das Fenster.
+ public static void Zeichnen(Window fenster)
+ {
+ Dispatcher.UIThread.RunJobs();
+ using WriteableBitmap? bild = fenster.CaptureRenderedFrame();
+ Assert.NotNull(bild);
+ Dispatcher.UIThread.RunJobs();
+ }
+
+ /// Führt einen Testablauf auf dem UI-Thread aus.
+ /// Der Ablauf.
+ /// Der laufende Ablauf.
+ public Task Ausfuehren(Func ablauf) =>
+ sitzung.Dispatch(
+ async () =>
+ {
+ await ablauf();
+ return true;
+ },
+ CancellationToken.None);
+
+ ///
+ /// Gibt die Warnungen von Avalonia (z. B. Bindungsfehler) aus und lässt den Test bei Fehlern scheitern. Danach ist
+ /// das Protokoll leer.
+ ///
+ /// Ausgabe des Tests.
+ public void KeineFehler(ITestOutputHelper ausgabe)
+ {
+ var eintraege = Protokoll.Eintraege.ToList();
+ Protokoll.Leeren();
+ foreach ((LogEventLevel stufe, string bereich, string text) in eintraege)
+ {
+ ausgabe.WriteLine($"{stufe} [{bereich}] {text}");
+ }
+
+ Assert.DoesNotContain(eintraege, e => e.Stufe >= LogEventLevel.Error);
+ }
+
+ ///
+ public void Dispose() => sitzung.Dispose();
+}
diff --git a/tests/AndiGenerator.UI.Tests/OberflaechenSammlung.cs b/tests/AndiGenerator.UI.Tests/OberflaechenSammlung.cs
new file mode 100644
index 0000000..b0c3cb4
--- /dev/null
+++ b/tests/AndiGenerator.UI.Tests/OberflaechenSammlung.cs
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: 2017 Andreas Hofmann
+// SPDX-FileCopyrightText: 2026 Peter Buchmann
+// SPDX-License-Identifier: GPL-3.0-only
+
+namespace AndiGenerator.UI.Tests;
+
+/// Alle Oberflächentests teilen sich eine und laufen nacheinander.
+[CollectionDefinition(Name)]
+public sealed class OberflaechenSammlung : ICollectionFixture
+{
+ /// Name der Sammlung.
+ public const string Name = "Oberfläche";
+}
diff --git a/tests/AndiGenerator.UI.Tests/Protokoll.cs b/tests/AndiGenerator.UI.Tests/Protokoll.cs
new file mode 100644
index 0000000..6065721
--- /dev/null
+++ b/tests/AndiGenerator.UI.Tests/Protokoll.cs
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: 2017 Andreas Hofmann
+// SPDX-FileCopyrightText: 2026 Peter Buchmann
+// SPDX-License-Identifier: GPL-3.0-only
+
+using System.Collections.Concurrent;
+using Avalonia.Logging;
+
+namespace AndiGenerator.UI.Tests;
+
+///
+/// Sammelt die Meldungen von Avalonia während der Tests, z. B. Bindungsfehler oder Ausnahmen beim Layout. Fehler lassen
+/// die Tests scheitern, Warnungen werden nur ausgegeben.
+///
+internal sealed class Protokoll : ILogSink
+{
+ private readonly ConcurrentQueue<(LogEventLevel Stufe, string Bereich, string Text)> eintraege = new();
+
+ /// Holt die gesammelten Meldungen.
+ public IReadOnlyCollection<(LogEventLevel Stufe, string Bereich, string Text)> Eintraege => eintraege;
+
+ ///
+ public bool IsEnabled(LogEventLevel level, string area) => level >= LogEventLevel.Warning;
+
+ ///
+ public void Log(LogEventLevel level, string area, object? source, string messageTemplate) =>
+ Log(level, area, source, messageTemplate, []);
+
+ ///
+ public void Log(LogEventLevel level, string area, object? source, string messageTemplate, params object?[] propertyValues)
+ {
+ if (IsEnabled(level, area))
+ {
+ string werte = propertyValues.Length == 0 ? string.Empty : " [" + string.Join(", ", propertyValues) + "]";
+ eintraege.Enqueue((level, area, messageTemplate + werte + " (" + source?.GetType().Name + ")"));
+ }
+ }
+
+ /// Leert das Protokoll.
+ public void Leeren() => eintraege.Clear();
+}
diff --git a/tests/AndiGenerator.UI.Tests/TestAnwendung.cs b/tests/AndiGenerator.UI.Tests/TestAnwendung.cs
new file mode 100644
index 0000000..3377e68
--- /dev/null
+++ b/tests/AndiGenerator.UI.Tests/TestAnwendung.cs
@@ -0,0 +1,20 @@
+// SPDX-FileCopyrightText: 2017 Andreas Hofmann
+// SPDX-FileCopyrightText: 2026 Peter Buchmann
+// SPDX-License-Identifier: GPL-3.0-only
+
+using Avalonia;
+using Avalonia.Headless;
+
+namespace AndiGenerator.UI.Tests;
+
+///
+/// Die echte Anwendung (Stile, Schriften, Ansichten-Zuordnung), aber auf der Headless-Plattform ohne Bildschirm. Gezeichnet
+/// wird wirklich mit Skia, damit auch Fehler in den eigenen Zeichenflächen (Diagramme, Terminwunschraster) auffallen.
+///
+public static class TestAnwendung
+{
+ /// Konfiguriert die Anwendung; aufgerufen von .
+ /// Der Builder.
+ public static AppBuilder BuildAvaloniaApp() =>
+ AppBuilder.Configure().UseSkia().UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false });
+}
diff --git a/tools/tests-wsl.sh b/tools/tests-wsl.sh
index 7754280..bfb29a1 100755
--- a/tools/tests-wsl.sh
+++ b/tools/tests-wsl.sh
@@ -20,7 +20,7 @@ export DOTNET_ROOT="${DOTNET_ROOT:-$HOME/.dotnet}"
echo "----- BUILD Exitcode $rc"
if [ "$rc" -eq 0 ]; then
echo "----- TEST"
- dotnet test AndiGenerator.slnx -c Release --no-build -nologo --logger "console;verbosity=normal"
+ dotnet test AndiGenerator.slnx -c Release --no-build -nologo --filter "Kategorie!=Messung" --logger "console;verbosity=normal"
echo "----- TEST Exitcode $?"
fi
echo "===== ENDE $(date '+%d.%m.%Y %H:%M:%S')"