feat: Dunkelmodus, Fenstergröße merken, Papierkorb für Löschvorgänge (12.4, 14.3, 14.6)
Dunkelmodus über neuen Einstellungen-Tab "Darstellung" (Systemvorgabe/Hell/Dunkel), Fenstergröße/Maximiert-Status wird über Sitzungen hinweg gemerkt (bewusst ohne Fensterposition), und ein generischer Snapshot-basierter Papierkorb (30 Tage) für Sitzpläne, Noten, Notenschlüssel-Vorlagen, Aufgaben und Zeiteinträge. Details und bewusste Scope-Entscheidungen (Spaltenbreiten zurückgestellt, Farb-Audit für Dunkelmodus offen, welche Entitäten der Papierkorb abdeckt) in TODO.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
using LehrerApp.Desktop.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class AppearanceSettingsServiceTests
|
||||
{
|
||||
private static string BuildTempPath()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"lehrerapp-appearance-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_OhneVorhandeneDatei_LiefertSystemvorgabe() =>
|
||||
Assert.Equal(AppTheme.System, new AppearanceSettingsService(BuildTempPath()).Load());
|
||||
|
||||
[Theory]
|
||||
[InlineData(AppTheme.Light)]
|
||||
[InlineData(AppTheme.Dark)]
|
||||
[InlineData(AppTheme.System)]
|
||||
public void SaveUndLoad_PersistiertUeberNeueInstanz(AppTheme theme)
|
||||
{
|
||||
var path = BuildTempPath();
|
||||
new AppearanceSettingsService(path).Save(theme);
|
||||
|
||||
var reloaded = new AppearanceSettingsService(path).Load();
|
||||
|
||||
Assert.Equal(theme, reloaded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_BeschaedigteDatei_FaelltAufSystemvorgabeZurueck()
|
||||
{
|
||||
var path = BuildTempPath();
|
||||
File.WriteAllText(Path.Combine(path, "appearancesettings.json"), "{ kein json");
|
||||
|
||||
Assert.Equal(AppTheme.System, new AppearanceSettingsService(path).Load());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AppThemeDisplayTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(AppTheme.System, "Systemvorgabe")]
|
||||
[InlineData(AppTheme.Light, "Hell")]
|
||||
[InlineData(AppTheme.Dark, "Dunkel")]
|
||||
public void Label_LiefertDeutscheBeschriftung(AppTheme theme, string expected) =>
|
||||
Assert.Equal(expected, AppThemeDisplay.Label(theme));
|
||||
|
||||
[Fact]
|
||||
public void FromLabel_UndLabel_SindZueinanderInvers()
|
||||
{
|
||||
foreach (var theme in Enum.GetValues<AppTheme>())
|
||||
Assert.Equal(theme, AppThemeDisplay.FromLabel(AppThemeDisplay.Label(theme)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromLabel_UnbekanntesLabel_LiefertSystemvorgabe() =>
|
||||
Assert.Equal(AppTheme.System, AppThemeDisplay.FromLabel("Nonsens"));
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Settings;
|
||||
using LehrerApp.Sync;
|
||||
using LehrerApp.Sync.Crypto;
|
||||
|
||||
@@ -69,6 +70,19 @@ public static class TestSupport
|
||||
/// Für Tests, die nicht speziell den "Schlüssel neu erzeugt"-Warnzustand prüfen.
|
||||
public static SyncKeyStatus BuildSyncKeyStatus(bool keyWasRegenerated = false) => new(keyWasRegenerated);
|
||||
|
||||
/// Neue, leere Fakes je Aufruf - für Tests, die nicht speziell TrashViewModel-Verhalten prüfen.
|
||||
public static TrashViewModel BuildTrashViewModel() => new(
|
||||
new FakeTrash(), new FakeGrades(), new FakeWorkTasks(), new FakeTimeEntries(),
|
||||
new FakeSeatingPlans(), new FakeGradingKeyTemplates());
|
||||
|
||||
/// Analog zu <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
||||
public static AppearanceSettingsService BuildAppearanceSettingsService()
|
||||
{
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-appearance-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempPath);
|
||||
return new AppearanceSettingsService(tempPath);
|
||||
}
|
||||
|
||||
/// Eigenes Temp-Verzeichnis je Aufruf, damit Tests sich nicht gegenseitig über dieselbe
|
||||
/// sync.key stören.
|
||||
public static SyncKeyRecoveryService BuildSyncKeyRecoveryService()
|
||||
@@ -123,6 +137,17 @@ public class FakeSeatingPlans(List<SeatingPlan>? initial = null) : ISeatingPlanR
|
||||
_all.Add(plan);
|
||||
}
|
||||
public void Delete(Guid id) => _all.RemoveAll(p => p.Id == id);
|
||||
// Trash-Mechanismus (14.3) lebt real in LiteDbContext.MoveToTrash/RestoreFromTrash - Fakes
|
||||
// bilden ihn bewusst nicht nach, ViewModel-Tests brauchen das nicht.
|
||||
public void Restore(Guid trashId) { }
|
||||
}
|
||||
|
||||
public class FakeTrash(List<TrashedItem>? initial = null) : ITrashRepository
|
||||
{
|
||||
private readonly List<TrashedItem> _all = initial ?? [];
|
||||
public void Add(TrashedItem item) => _all.Add(item);
|
||||
public List<TrashedItem> GetAll() => _all.OrderByDescending(t => t.DeletedAt).ToList();
|
||||
public void PurgeOlderThan(DateTime cutoffUtc) => _all.RemoveAll(t => t.DeletedAt < cutoffUtc);
|
||||
}
|
||||
|
||||
public class FakeSessions(List<ParticipationSession> all) : IParticipationSessionRepository
|
||||
@@ -190,6 +215,7 @@ public class FakeGrades : IGradeRepository
|
||||
_all.Add(grade);
|
||||
}
|
||||
public void Delete(Guid id) => _all.RemoveAll(g => g.Id == id);
|
||||
public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore
|
||||
}
|
||||
|
||||
public class FakeExams(List<Exam> all) : IExamRepository
|
||||
@@ -227,6 +253,7 @@ public class FakeGradingKeyTemplates : IGradingKeyTemplateRepository
|
||||
public GradingKeyTemplate? GetById(Guid id) => _all.FirstOrDefault(t => t.Id == id);
|
||||
public void Save(GradingKeyTemplate template) { _all.RemoveAll(t => t.Id == template.Id); _all.Add(template); }
|
||||
public void Delete(Guid id) => _all.RemoveAll(t => t.Id == id);
|
||||
public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore
|
||||
}
|
||||
|
||||
public class FakeSchemes : IGradingSchemeRepository
|
||||
@@ -407,6 +434,7 @@ public class FakeWorkTasks : IWorkTaskRepository
|
||||
public List<WorkTask> GetAll() => _all.ToList();
|
||||
public void Save(WorkTask task) { _all.RemoveAll(t => t.Id == task.Id); _all.Add(task); }
|
||||
public void Delete(Guid id) => _all.RemoveAll(t => t.Id == id);
|
||||
public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore
|
||||
}
|
||||
|
||||
public class FakeTimeEntries : ITimeEntryRepository
|
||||
@@ -419,6 +447,7 @@ public class FakeTimeEntries : ITimeEntryRepository
|
||||
public List<TimeEntry> GetByTask(Guid taskId) => _all.Where(e => e.TaskId == taskId).ToList();
|
||||
public void Save(TimeEntry entry) { _all.RemoveAll(e => e.Id == entry.Id); _all.Add(entry); }
|
||||
public void Delete(Guid id) => _all.RemoveAll(e => e.Id == id);
|
||||
public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore
|
||||
}
|
||||
|
||||
public class FakeReportGrades : IReportGradeRepository
|
||||
|
||||
@@ -16,7 +16,8 @@ public sealed class SettingsViewModelTests
|
||||
FakeSupervisionDuties? supervisionDuties = null,
|
||||
FakeSubjects? subjects = null, FakeCompetencyDomains? competencyDomains = null,
|
||||
EventQueue? eventQueue = null, SyncKeyStatus? syncKeyStatus = null,
|
||||
SyncKeyRecoveryService? syncKeyRecovery = null)
|
||||
SyncKeyRecoveryService? syncKeyRecovery = null, AppearanceSettingsService? appearance = null,
|
||||
TrashViewModel? trashTab = null)
|
||||
{
|
||||
// Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad erst bei SetState,
|
||||
// das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben.
|
||||
@@ -36,7 +37,37 @@ public sealed class SettingsViewModelTests
|
||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(),
|
||||
eventQueue ?? TestSupport.BuildEventQueue(), TestSupport.BuildAppLogger(),
|
||||
syncKeyStatus ?? TestSupport.BuildSyncKeyStatus(),
|
||||
syncKeyRecovery ?? TestSupport.BuildSyncKeyRecoveryService());
|
||||
syncKeyRecovery ?? TestSupport.BuildSyncKeyRecoveryService(),
|
||||
appearance ?? TestSupport.BuildAppearanceSettingsService(),
|
||||
trashTab ?? TestSupport.BuildTrashViewModel());
|
||||
}
|
||||
|
||||
// ── Darstellung (12.4) ────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SelectedTheme_SpiegeltDenGespeichertenStandBeimStartWider()
|
||||
{
|
||||
var appearance = TestSupport.BuildAppearanceSettingsService();
|
||||
appearance.Save(AppTheme.Dark);
|
||||
|
||||
var vm = BuildViewModel(appearance: appearance);
|
||||
|
||||
Assert.Equal(AppTheme.Dark, vm.SelectedTheme);
|
||||
Assert.Equal("Dunkel", vm.SelectedThemeName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectedThemeName_Aendern_PersistiertUndBenachrichtigtDenCodeBehind()
|
||||
{
|
||||
var appearance = TestSupport.BuildAppearanceSettingsService();
|
||||
var vm = BuildViewModel(appearance: appearance);
|
||||
AppTheme? notified = null;
|
||||
vm.OnThemeChanged = t => notified = t;
|
||||
|
||||
vm.SelectedThemeName = "Dunkel";
|
||||
|
||||
Assert.Equal(AppTheme.Dark, notified);
|
||||
Assert.Equal(AppTheme.Dark, appearance.Load());
|
||||
}
|
||||
|
||||
// ── Sync-Schlüssel: Warnung + Wiederherstellungscode (10.3.2) ────────────────────────────
|
||||
@@ -259,7 +290,8 @@ public sealed class SettingsViewModelTests
|
||||
new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath),
|
||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
||||
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService());
|
||||
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService(),
|
||||
TestSupport.BuildAppearanceSettingsService(), TestSupport.BuildTrashViewModel());
|
||||
|
||||
vm.SelectedStateName = "Bayern";
|
||||
|
||||
@@ -283,7 +315,8 @@ public sealed class SettingsViewModelTests
|
||||
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
||||
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService());
|
||||
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService(),
|
||||
TestSupport.BuildAppearanceSettingsService(), TestSupport.BuildTrashViewModel());
|
||||
|
||||
vm.PeriodTimes[0].StartText = "08:00";
|
||||
vm.PeriodTimes[0].EndText = "08:45";
|
||||
@@ -311,7 +344,8 @@ public sealed class SettingsViewModelTests
|
||||
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
||||
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
||||
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService());
|
||||
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService(),
|
||||
TestSupport.BuildAppearanceSettingsService(), TestSupport.BuildTrashViewModel());
|
||||
|
||||
vm.PeriodTimes[0].StartText = "08:45";
|
||||
vm.PeriodTimes[0].EndText = "08:00";
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Data;
|
||||
using LehrerApp.Data.Repositories;
|
||||
using LehrerApp.Desktop.ViewModels.Settings;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class TrashViewModelTests
|
||||
{
|
||||
private static TrashViewModel BuildViewModel(LiteDbContext db) => new(
|
||||
new TrashRepository(db), new GradeRepository(db), new WorkTaskRepository(db),
|
||||
new TimeEntryRepository(db), new SeatingPlanRepository(db), new GradingKeyTemplateRepository(db));
|
||||
|
||||
[Fact]
|
||||
public void Konstruktor_LaedtVorhandenePapierkorbEintraege()
|
||||
{
|
||||
using var db = new LiteDbContext(new MemoryStream());
|
||||
var taskRepo = new WorkTaskRepository(db);
|
||||
var task = new WorkTask { Title = "Elternbrief schreiben" };
|
||||
taskRepo.Save(task);
|
||||
taskRepo.Delete(task.Id);
|
||||
|
||||
var vm = BuildViewModel(db);
|
||||
|
||||
Assert.True(vm.HasItems);
|
||||
var item = Assert.Single(vm.Items);
|
||||
Assert.Equal("Aufgabe", item.TypeLabel);
|
||||
Assert.Equal("Elternbrief schreiben", item.Summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeererPapierkorb_HasItemsIstFalsch()
|
||||
{
|
||||
using var db = new LiteDbContext(new MemoryStream());
|
||||
|
||||
var vm = BuildViewModel(db);
|
||||
|
||||
Assert.False(vm.HasItems);
|
||||
Assert.Empty(vm.Items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RestoreCommand_StelltAufgabeWiederHerUndAktualisiertDieListe()
|
||||
{
|
||||
using var db = new LiteDbContext(new MemoryStream());
|
||||
var taskRepo = new WorkTaskRepository(db);
|
||||
var task = new WorkTask { Title = "Klausuren korrigieren" };
|
||||
taskRepo.Save(task);
|
||||
taskRepo.Delete(task.Id);
|
||||
var vm = BuildViewModel(db);
|
||||
var item = Assert.Single(vm.Items);
|
||||
|
||||
vm.RestoreCommand.Execute(item);
|
||||
|
||||
Assert.NotNull(db.Tasks.FindById(task.Id));
|
||||
Assert.False(vm.HasItems);
|
||||
Assert.Contains("Klausuren korrigieren", vm.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RestoreCommand_MitNullTutNichts()
|
||||
{
|
||||
using var db = new LiteDbContext(new MemoryStream());
|
||||
var vm = BuildViewModel(db);
|
||||
|
||||
vm.RestoreCommand.Execute(null);
|
||||
|
||||
Assert.Equal("", vm.Status);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(Grade), "Note")]
|
||||
[InlineData(nameof(WorkTask), "Aufgabe")]
|
||||
[InlineData(nameof(TimeEntry), "Zeiteintrag")]
|
||||
[InlineData(nameof(SeatingPlan), "Sitzplan")]
|
||||
[InlineData(nameof(GradingKeyTemplate), "Notenschlüssel-Vorlage")]
|
||||
public void TypeLabel_UebersetztDenEntityTypeInsDeutsche(string entityType, string expectedLabel)
|
||||
{
|
||||
using var db = new LiteDbContext(new MemoryStream());
|
||||
db.TrashedItems.Insert(new TrashedItem { EntityType = entityType, Summary = "Testeintrag" });
|
||||
|
||||
var vm = BuildViewModel(db);
|
||||
|
||||
var item = Assert.Single(vm.Items);
|
||||
Assert.Equal(expectedLabel, item.TypeLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeLabel_UnbekannterEntityTypeFaelltAufDenRohenNamenZurueck()
|
||||
{
|
||||
using var db = new LiteDbContext(new MemoryStream());
|
||||
db.TrashedItems.Insert(new TrashedItem { EntityType = "Sonstiges", Summary = "Testeintrag" });
|
||||
|
||||
var vm = BuildViewModel(db);
|
||||
|
||||
Assert.Equal("Sonstiges", Assert.Single(vm.Items).TypeLabel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using LehrerApp.Desktop.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class WindowSettingsServiceTests
|
||||
{
|
||||
private static string BuildTempPath()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"lehrerapp-windowsettings-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_OhneVorhandeneDatei_LiefertStandardgroesse()
|
||||
{
|
||||
var service = new WindowSettingsService(BuildTempPath());
|
||||
|
||||
var settings = service.Load();
|
||||
|
||||
Assert.Equal(1280, settings.Width);
|
||||
Assert.Equal(800, settings.Height);
|
||||
Assert.False(settings.IsMaximized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveUndLoad_PersistiertUeberNeueInstanz()
|
||||
{
|
||||
var path = BuildTempPath();
|
||||
new WindowSettingsService(path).Save(new WindowSettings { Width = 1600, Height = 950, IsMaximized = true });
|
||||
|
||||
var reloaded = new WindowSettingsService(path).Load();
|
||||
|
||||
Assert.Equal(1600, reloaded.Width);
|
||||
Assert.Equal(950, reloaded.Height);
|
||||
Assert.True(reloaded.IsMaximized);
|
||||
}
|
||||
|
||||
/// Eine winzige oder negative Größe (z.B. durch eine beschädigte/manuell editierte Datei)
|
||||
/// darf das Fenster beim nächsten Start nicht unbenutzbar klein machen.
|
||||
[Fact]
|
||||
public void Load_UnplausibelKleineGroesse_FaelltAufStandardgroesseZurueck()
|
||||
{
|
||||
var path = BuildTempPath();
|
||||
new WindowSettingsService(path).Save(new WindowSettings { Width = 5, Height = 5 });
|
||||
|
||||
var reloaded = new WindowSettingsService(path).Load();
|
||||
|
||||
Assert.Equal(1280, reloaded.Width);
|
||||
Assert.Equal(800, reloaded.Height);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_BeschaedigteDatei_FaelltAufStandardgroesseZurueck()
|
||||
{
|
||||
var path = BuildTempPath();
|
||||
File.WriteAllText(Path.Combine(path, "windowsettings.json"), "{ kein json");
|
||||
|
||||
var settings = new WindowSettingsService(path).Load();
|
||||
|
||||
Assert.Equal(1280, settings.Width);
|
||||
Assert.Equal(800, settings.Height);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user