using System;
using Xunit;
namespace IFSRenderer.AvaloniaShell.Tests;
/// <summary>
/// The shared visible time window behind timeline zoom and pan.
/// </summary>
/// <remarks>
/// The wiki documents three timeline interactions the port had none of: "scroll to zoom",
/// "use the bottom scrollbar to pan", and detaching the panel. The first two both come down to
/// this type - the panes used to map the whole clip onto their width, which left zoom nowhere
/// to put a magnified span.
/// </remarks>
public class TimeViewTests
{
private static TimeView Clip(double length = 10)
{
var view = new TimeView();
view.Length = length;
view.Reset();
return view;
}
[Fact]
public void StartsShowingTheWholeClip()
{
TimeView view = Clip();
Assert.Equal(0, view.Start);
Assert.Equal(10, view.Span);
Assert.True(view.IsFullyZoomedOut);
}
[Fact]
public void ZoomKeepsTheAnchorUnderTheCursor()
{
TimeView view = Clip();
const double width = 808; // 800 track px after the pads
double anchor = 7.5;
double before = view.TimeToX(anchor, width);
view.ZoomAt(anchor, 1 / 4.0);
Assert.Equal(2.5, view.Span, 6);
Assert.Equal(before, view.TimeToX(anchor, width), 6);
}
[Fact]
public void ZoomStopsAtTheWholeClipAndNeverScrollsPastTheEnds()
{
TimeView view = Clip();
for (int i = 0; i < 40; i++)
view.ZoomAt(5, 1.25);
Assert.Equal(10, view.Span, 6);
Assert.Equal(0, view.Start, 6);
Assert.True(view.IsFullyZoomedOut);
}
[Fact]
public void ZoomingInFarNeverCollapsesTheSpan()
{
TimeView view = Clip();
for (int i = 0; i < 200; i++)
view.ZoomAt(5, 1 / 1.25);
Assert.True(view.Span > 0, "a zero span would make the pixel mapping degenerate");
Assert.True(view.Span >= 10.0 / 400 - 1e-9);
}
[Fact]
public void PanIsClampedToTheClip()
{
TimeView view = Clip();
view.ZoomAt(5, 1 / 5.0); // 2s visible
view.PanBy(-100);
Assert.Equal(0, view.Start, 6);
view.PanBy(100);
Assert.Equal(8, view.Start, 6); // length 10 minus the 2s window
Assert.Equal(10, view.End, 6);
}
[Fact]
public void PixelAndTimeRoundTripWithinTheWindow()
{
TimeView view = Clip();
view.ZoomAt(6, 1 / 4.0);
const double width = 808;
foreach (double t in new[] { view.Start, view.Start + view.Span / 3, view.End })
Assert.Equal(t, view.XToTime(view.TimeToX(t, width), width), 6);
}
[Fact]
public void PlaybackScrollsTheWindowToFollowThePlayhead()
{
TimeView view = Clip();
view.ZoomAt(0, 1 / 5.0); // 2s window at the start
Assert.Equal(0, view.Start, 6);
view.EnsureVisible(5);
Assert.True(view.Start <= 5 && 5 <= view.End,
$"playhead at 5s fell outside the window [{view.Start}, {view.End}]");
}
[Fact]
public void AlreadyVisibleTimesDoNotMoveTheWindow()
{
TimeView view = Clip();
view.ZoomAt(5, 1 / 2.0);
double start = view.Start;
view.EnsureVisible(5);
Assert.Equal(start, view.Start, 9);
}
[Fact]
public void ChangingTheClipLengthReClampsTheWindow()
{
TimeView view = Clip(60);
view.ZoomAt(55, 1 / 10.0);
Assert.True(view.End <= 60 + 1e-9);
view.Length = 5; // clip shortened under a window near its old end
Assert.True(view.End <= 5 + 1e-9, $"window ran past the new clip end: {view.End}");
Assert.True(view.Start >= 0);
}
[Fact]
public void ObserversAreNotifiedSoBothPanesRepaintTogether()
{
TimeView view = Clip();
int changes = 0;
view.Changed += () => changes++;
view.ZoomAt(5, 1 / 2.0);
view.PanBy(0.5);
view.Reset();
Assert.Equal(3, changes);
}
}