feat: expand teaching mode with live timeline and seating quick checks
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class TeachingTimelineViewModelTests
|
||||
{
|
||||
private readonly DateTime _start = new(2026, 9, 14, 8, 0, 0, DateTimeKind.Utc);
|
||||
private DateTime _now;
|
||||
private readonly FakeLessons _lessons = new();
|
||||
|
||||
private (Lesson Lesson, TeachingTimelineViewModel Vm) Build(bool scheduled = true)
|
||||
{
|
||||
_now = _start.AddMinutes(5);
|
||||
var local = _start.ToLocalTime();
|
||||
var lesson = new Lesson
|
||||
{
|
||||
Date = DateOnly.FromDateTime(local),
|
||||
StartTime = scheduled ? TimeOnly.FromDateTime(local) : null,
|
||||
Phases = [new() { Name = "Einstieg", DurationMinutes = 10 },
|
||||
new() { Name = "Arbeit", DurationMinutes = 10 },
|
||||
new() { Name = "Sicherung", DurationMinutes = 10 }]
|
||||
};
|
||||
_lessons.Add(lesson);
|
||||
return (lesson, new TeachingTimelineViewModel(lesson, _lessons, utcNow: () => _now));
|
||||
}
|
||||
|
||||
[Fact] public void Clock_HighlightsExactlyOnePhaseAtBoundary()
|
||||
{
|
||||
var (_, vm) = Build();
|
||||
Assert.True(vm.Phases[0].IsActive);
|
||||
Assert.Equal(50, vm.Phases[0].Progress);
|
||||
_now = _start.AddMinutes(10);
|
||||
vm.Refresh();
|
||||
Assert.True(vm.Phases[0].IsCompleted);
|
||||
Assert.True(vm.Phases[1].IsActive);
|
||||
Assert.Single(vm.Phases, p => p.IsActive);
|
||||
}
|
||||
|
||||
[Fact] public void NoStartTime_RequiresExplicitStart()
|
||||
{
|
||||
var (_, vm) = Build(scheduled: false);
|
||||
Assert.True(vm.NeedsStart);
|
||||
Assert.DoesNotContain(vm.Phases, p => p.IsActive);
|
||||
vm.StartNowCommand.Execute(null);
|
||||
Assert.False(vm.NeedsStart);
|
||||
Assert.True(vm.Phases[0].IsActive);
|
||||
Assert.Equal(_now, vm.Phases[0].StartUtc);
|
||||
}
|
||||
|
||||
[Fact] public void Extend_ShiftsLaterPhasesAndPreservesOriginalPlan()
|
||||
{
|
||||
var (lesson, vm) = Build();
|
||||
vm.Phases[0].ExtendTenCommand.Execute(null);
|
||||
Assert.Equal(_start.AddMinutes(20), vm.Phases[1].StartUtc);
|
||||
Assert.True(vm.Phases[2].IsOverflow);
|
||||
Assert.Equal(10, lesson.Phases[0].DurationMinutes);
|
||||
Assert.NotNull(_lessons.GetById(lesson.Id)!.TeachingTimeline);
|
||||
}
|
||||
|
||||
[Fact] public void FinishEarly_StartsNextPhaseNow()
|
||||
{
|
||||
var (_, vm) = Build();
|
||||
vm.Phases[0].FinishCommand.Execute(null);
|
||||
Assert.True(vm.Phases[0].IsCompleted);
|
||||
Assert.True(vm.Phases[1].IsActive);
|
||||
Assert.Equal(_now, vm.Phases[1].StartUtc);
|
||||
}
|
||||
|
||||
[Fact] public void Hold_SurvivesReopenAndContinuesOnlyOnNext()
|
||||
{
|
||||
var (lesson, vm) = Build();
|
||||
vm.Phases[0].HoldCommand.Execute(null);
|
||||
_now = _start.AddMinutes(65);
|
||||
var reopened = new TeachingTimelineViewModel(lesson, _lessons, utcNow: () => _now);
|
||||
Assert.True(reopened.Phases[0].IsHeld);
|
||||
Assert.True(reopened.Phases[0].IsActive);
|
||||
Assert.Single(reopened.Phases, p => p.IsActive);
|
||||
reopened.Phases[0].FinishCommand.Execute(null);
|
||||
Assert.True(reopened.Phases[1].IsActive);
|
||||
Assert.False(reopened.Phases[0].IsHeld);
|
||||
Assert.Equal(_now, reopened.Phases[1].StartUtc);
|
||||
}
|
||||
|
||||
[Fact] public void BringForward_KeepsSkippedPendingPhasesBelowChosenPhase()
|
||||
{
|
||||
var (_, vm) = Build();
|
||||
var chosen = vm.Phases[2];
|
||||
chosen.BringForwardCommand.Execute(null);
|
||||
Assert.Equal(new[] { "Einstieg", "Sicherung", "Arbeit" }, vm.Phases.Select(p => p.Source.Name));
|
||||
Assert.True(chosen.IsActive);
|
||||
Assert.Equal(_now, chosen.StartUtc);
|
||||
Assert.False(vm.Phases[2].IsCompleted);
|
||||
}
|
||||
|
||||
[Fact] public void Overflow_RemainsPendingTheNextDayAndCanBeStartedNow()
|
||||
{
|
||||
var (lesson, vm) = Build();
|
||||
vm.Phases[0].ExtendTenCommand.Execute(null);
|
||||
_now = _start.AddDays(1);
|
||||
var reopened = new TeachingTimelineViewModel(lesson, _lessons, utcNow: () => _now);
|
||||
var remainder = reopened.Phases[2];
|
||||
Assert.True(remainder.IsOverflow);
|
||||
Assert.False(remainder.IsCompleted);
|
||||
Assert.False(remainder.IsActive);
|
||||
Assert.True(reopened.HasRemainder);
|
||||
remainder.BringForwardCommand.Execute(null);
|
||||
Assert.True(remainder.IsActive);
|
||||
Assert.Equal(_now, remainder.StartUtc);
|
||||
}
|
||||
|
||||
[Fact] public void Transfer_CopiesIntoSelectedLessonOnlyOnceAndKeepsSource()
|
||||
{
|
||||
var (lesson, _) = Build();
|
||||
var target = new Lesson { GroupId = lesson.GroupId, Date = lesson.Date.AddDays(1), Topic = "Folgestunde" };
|
||||
_lessons.Add(target);
|
||||
var vm = new TeachingTimelineViewModel(lesson, _lessons, utcNow: () => _now);
|
||||
vm.Phases[0].ExtendTenCommand.Execute(null);
|
||||
vm.TransferRemainderCommand.Execute(null);
|
||||
vm.TransferRemainderCommand.Execute(null);
|
||||
var copy = Assert.Single(target.Phases);
|
||||
Assert.Equal("Sicherung", copy.Name);
|
||||
Assert.NotEqual(lesson.Phases[2].Id, copy.Id);
|
||||
Assert.Equal(3, lesson.Phases.Count);
|
||||
Assert.True(vm.Phases[2].IsTransferred);
|
||||
}
|
||||
|
||||
[Fact] public void PartiallyOverflowingPhase_PreservesOnlyUnfinishedMinutesAfterLessonEnds()
|
||||
{
|
||||
var (lesson, vm) = Build();
|
||||
vm.Phases[0].ExtendFiveCommand.Execute(null);
|
||||
_now = _start.AddMinutes(28);
|
||||
vm.Refresh();
|
||||
Assert.True(vm.Phases[2].IsActive);
|
||||
_now = _start.AddDays(1);
|
||||
var reopened = new TeachingTimelineViewModel(lesson, _lessons, utcNow: () => _now);
|
||||
var remainder = reopened.Phases[2];
|
||||
Assert.False(remainder.IsCompleted);
|
||||
Assert.False(remainder.IsActive);
|
||||
Assert.True(reopened.HasRemainder);
|
||||
Assert.Equal(5, remainder.RemainingMinutes);
|
||||
remainder.BringForwardCommand.Execute(null);
|
||||
Assert.True(remainder.IsActive);
|
||||
Assert.Equal(_now.AddMinutes(5), remainder.EndUtc);
|
||||
}
|
||||
|
||||
[Fact] public void ReadOnly_DoesNotChangeTimingOrStartClock()
|
||||
{
|
||||
var (lesson, _) = Build(scheduled: false);
|
||||
var vm = new TeachingTimelineViewModel(lesson, _lessons, readOnly: true, utcNow: () => _now);
|
||||
vm.StartNowCommand.Execute(null);
|
||||
Assert.Null(lesson.TeachingTimeline);
|
||||
Assert.True(vm.NeedsStart);
|
||||
}
|
||||
|
||||
[Fact] public void AlternativePhases_AreNotRunAlongsideMainPath()
|
||||
{
|
||||
var (lesson, _) = Build();
|
||||
lesson.Phases.Add(new LessonPhaseStep { AlternativePathId = Guid.NewGuid(), DurationMinutes = 30 });
|
||||
var vm = new TeachingTimelineViewModel(lesson, _lessons, utcNow: () => _now);
|
||||
Assert.Equal(3, vm.Phases.Count);
|
||||
Assert.Equal(_start.AddMinutes(30), vm.Phases[^1].EndUtc);
|
||||
}
|
||||
|
||||
[Fact] public void HeldTiming_RoundTripsThroughLiteDbWithoutTimezoneShift()
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using var db = new LehrerApp.Data.LiteDbContext(stream);
|
||||
var repository = new LehrerApp.Data.Repositories.LessonRepository(db);
|
||||
var (lesson, _) = Build();
|
||||
repository.Save(lesson);
|
||||
var vm = new TeachingTimelineViewModel(lesson, repository, utcNow: () => _now);
|
||||
vm.Phases[0].HoldCommand.Execute(null);
|
||||
_now = _start.AddMinutes(40);
|
||||
var loaded = repository.GetById(lesson.Id)!;
|
||||
var reopened = new TeachingTimelineViewModel(loaded, repository, utcNow: () => _now);
|
||||
Assert.True(reopened.Phases[0].IsActive);
|
||||
Assert.True(reopened.Phases[0].IsHeld);
|
||||
Assert.Equal(_start, reopened.Phases[0].StartUtc);
|
||||
Assert.Equal(_start.AddMinutes(45), reopened.Phases[0].EndUtc);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user