OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / AvaloniaShell.Tests / ScrollBarTests.cs

117 lines · csharp ref:
using System.Linq;

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Headless.XUnit;
using Avalonia.Threading;
using Avalonia.VisualTree;

using Xunit;

namespace IFSRenderer.AvaloniaShell.Tests;

/// <summary>
/// "this scroll bar is weird" and, after a first attempted fix, "they still overlap other
/// windows, and shouldn't be so fat".
/// </summary>
/// <remarks>
/// <para>
/// The measured mechanism, which the first fix got wrong: Avalonia's ScrollViewer template
/// lets the ScrollContentPresenter span the scrollbar's column, so the bar is an OVERLAY.
/// <c>AllowAutoHide="False"</c> — the first fix — only stops it collapsing to a hairline; it
/// reserves no space whatsoever. Material's own bar measures 16px, the panes left a 12px
/// gutter, so content ran 4px underneath it.
/// </para>
/// <para>
/// The fix is therefore two numbers that have to agree: the bar is pinned narrow, and every
/// scrolled pane leaves a gutter wider than it. These tests measure the real window, because
/// the previous attempt was reasoned rather than measured and shipped broken.
/// </para>
/// </remarks>
public class ScrollBarTests
{
    /// <summary>Widest a vertical bar may be before it reads as "fat". Material's default is 16.</summary>
    private const double MaxBarWidth = 12;

    private static readonly string[] ScrolledPanes = ["LeftPanel", "PropertiesPanel"];

    private static (MainWindow Window, Fakes.FakeRenderQueue Queue) LaidOutWindow()
    {
        (MainWindow window, Fakes.FakeRenderQueue queue) = MainWindowHarness.Start();
        window.Measure(new Size(1500, 900));
        window.Arrange(new Rect(0, 0, 1500, 900));
        Dispatcher.UIThread.RunJobs();
        return (window, queue);
    }

    private static (ScrollBar Bar, Control Content) PaneParts(MainWindow window, string paneName)
    {
        var pane = window.FindControl<Border>(paneName);
        Assert.NotNull(pane);

        ScrollViewer viewer = pane.GetVisualDescendants().OfType<ScrollViewer>().First();
        ScrollBar bar = viewer.GetVisualDescendants().OfType<ScrollBar>()
            .FirstOrDefault(b => b.Orientation == Avalonia.Layout.Orientation.Vertical);
        return (bar, viewer.Content as Control);
    }

    [AvaloniaFact]
    public void The_scrollbar_is_not_fat()
    {
        (MainWindow window, _) = LaidOutWindow();

        foreach (string pane in ScrolledPanes)
        {
            (ScrollBar bar, _) = PaneParts(window, pane);
            Assert.True(bar is not null && bar.IsVisible, $"{pane}: expected a visible scrollbar");
            Assert.True(bar.Bounds.Width <= MaxBarWidth,
                $"{pane}: scrollbar is {bar.Bounds.Width}px wide, expected <= {MaxBarWidth}px");
        }
    }

    /// <summary>The reported defect: the bar is drawn on top of the controls.</summary>
    [AvaloniaFact]
    public void Scrolled_content_never_runs_under_the_scrollbar()
    {
        (MainWindow window, _) = LaidOutWindow();

        foreach (string paneName in ScrolledPanes)
        {
            (ScrollBar bar, Control content) = PaneParts(window, paneName);
            Assert.NotNull(content);
            Assert.True(bar is not null && bar.IsVisible, $"{paneName}: expected a visible scrollbar");

            double contentRight = content.Bounds.Right;
            double barLeft = bar.Bounds.Left;
            Assert.True(contentRight <= barLeft,
                $"{paneName}: content ends at {contentRight}px but the bar starts at {barLeft}px "
                + $"— overlapping by {contentRight - barLeft}px");
        }
    }

    /// <summary>
    /// Keeps the two numbers honest. The bar is an overlay, so the gutter is the ONLY thing
    /// keeping content clear of it — if the gutter ever shrinks below the bar width again, the
    /// assertion above starts failing rather than the overlap going unnoticed.
    /// </summary>
    [AvaloniaFact]
    public void The_presenter_really_does_span_the_scrollbar()
    {
        (MainWindow window, _) = LaidOutWindow();

        var pane = window.FindControl<Border>("LeftPanel");
        ScrollViewer viewer = pane.GetVisualDescendants().OfType<ScrollViewer>().First();
        var presenter = viewer.GetVisualDescendants().OfType<ScrollContentPresenter>().First();
        ScrollBar bar = viewer.GetVisualDescendants().OfType<ScrollBar>()
            .First(b => b.Orientation == Avalonia.Layout.Orientation.Vertical);

        // If this ever stops being true, the template changed and the gutter is no longer
        // needed — but until then, removing the gutter reintroduces the reported bug.
        Assert.True(presenter.Bounds.Right > bar.Bounds.Left,
            "the ScrollContentPresenter no longer spans the scrollbar column; re-derive the "
            + "gutter from the new template instead of assuming an overlay");
    }
}