OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / AvaloniaShell.Tests / TimeViewTests.cs

151 lines · csharp ref:
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);
    }
}