Files
LlamaCasty/ytLive.Tests/ZoomFollowModeTests.cs
T
gramps 15734a5477 feat(zoom): Ctrl closes the box, and a Follow pill that re-anchors the capture
Two creator directives, 2026-11-01.

**Ctrl-down dismisses an open zoom box.** Window-level PreviewKeyDown, so the same hand that
opened it can put it away. It cannot cancel itself at creation: Ctrl is already held before
the right-click that makes the box, so the keydown fires while nothing is open.

**Follow mode, default Anchor.** A PillToggle in App Settings ("Follow the box"), persisted
as Zoom.FollowBox. Off — the default — keeps the 2026-10-08 model: the capture stays on the
clicked point and dragging only moves the box over it. On, UpdateZoomMove re-anchors the
capture to the box centre (half the box's size, clamped per axis), so the magnifier shows
whatever it hovers. Flipping to Follow while the box is open snaps it immediately.

Three tests: anchor drags leave CaptureX alone, follow moves it to the expected quarter-offset
centre, and the setting round-trips through LayoutStore (tested against the store, not via two
view models — two live VMs fight over the temp DB, which is a fixture problem, not a bug).

Both new test classes share a collection tag: MainViewModel.LayoutPathOverride is static, and
xUnit runs classes in parallel, so without it the two view models collided (one failure in a
combined run, clean when run apart).

Release build 0 warnings; 14 tests pass across zoom, entitlement and preview history. Debug
build is blocked by the running app (PID 47388) — close it and the exe updates.
2026-11-01 08:51:28 -08:00

106 lines
3.5 KiB
C#

using System;
using System.IO;
using Xunit;
using ytLive.ViewModels;
namespace ytLive.Tests;
/// <summary>
/// Zoom box follow mode (creator ruling 2026-11-01): the Settings pill toggles between **Anchor**
/// — the capture stays on the clicked point, which is the default and the 2026-10-08 model — and
/// **Follow**, where dragging the box re-anchors the capture to the box centre so the magnifier
/// shows whatever it hovers over.
/// </summary>
// Shares the static MainViewModel.LayoutPathOverride with the sibling class; without a
// shared collection xUnit runs them in parallel and the two view models fight over one temp DB.
[Collection("MainViewModelLayoutPath")]
public class ZoomFollowModeTests
{
private static MainViewModel NewVm(out string dbPath)
{
dbPath = Path.Combine(Path.GetTempPath(), $"llamacasty-zoom-{Guid.NewGuid():N}.db");
MainViewModel.LayoutPathOverride = dbPath;
return new MainViewModel();
}
[Fact]
public void Anchor_is_the_default_and_dragging_leaves_the_capture_alone()
{
var vm = NewVm(out var dbPath);
try
{
Assert.False(vm.ZoomFollowsBox);
vm.ActivateZoom(1000, 500);
var captureX = vm.ZoomState.CaptureX;
vm.StartZoomMove(1000, 500);
vm.UpdateZoomMove(1200, 700);
Assert.Equal(captureX, vm.ZoomState.CaptureX);
}
finally
{
MainViewModel.LayoutPathOverride = null;
try { File.Delete(dbPath); } catch (IOException) { }
}
}
[Fact]
public void Follow_moves_the_capture_with_the_box()
{
var vm = NewVm(out var dbPath);
try
{
vm.ActivateZoom(1000, 500);
var anchoredCaptureX = vm.ZoomState.CaptureX;
vm.ZoomFollowsBox = true;
vm.StartZoomMove(1000, 500);
vm.UpdateZoomMove(1200, 700);
Assert.NotEqual(anchoredCaptureX, vm.ZoomState.CaptureX);
// The capture is half the box, centred on the box: box centre minus a quarter of it.
var expected = Math.Clamp(
vm.ZoomState.X + vm.ZoomState.Width / 2 - vm.ZoomState.Width / 4,
0, 1920 - vm.ZoomState.Width / 2);
Assert.Equal(expected, vm.ZoomState.CaptureX, 3);
}
finally
{
MainViewModel.LayoutPathOverride = null;
try { File.Delete(dbPath); } catch (IOException) { }
}
}
[Fact]
public void The_choice_persists_in_settings()
{
// Persistence is the store's job, tested against the store directly: two live view models
// in one process fight over the SQLite file and the frame pump, which is a property of the
// fixture, not of the setting.
var dbPath = Path.Combine(Path.GetTempPath(), $"llamacasty-zoom-{Guid.NewGuid():N}.db");
try
{
using (var store = new ytLive.Services.LayoutStore(dbPath))
{
Assert.False(store.LoadZoomFollowBox());
store.SaveZoomFollowBox(true);
}
using (var reopened = new ytLive.Services.LayoutStore(dbPath))
{
Assert.True(reopened.LoadZoomFollowBox());
reopened.SaveZoomFollowBox(false);
}
using (var again = new ytLive.Services.LayoutStore(dbPath))
Assert.False(again.LoadZoomFollowBox());
}
finally
{
try { File.Delete(dbPath); } catch (IOException) { }
}
}
}