OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / AvaloniaShell.Tests / MainWindowRegressionTests.cs

173 lines · csharp ref:
using System;
using System.Diagnostics;
using System.IO;
using System.Numerics;
using System.Threading;

using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.Interactivity;
using Avalonia.Threading;

using IFSRenderer.AvaloniaShell.Controls;
using IFSRenderer.AvaloniaShell.Tests.Fakes;
using IFSRenderer.Shared;

using Xunit;

namespace IFSRenderer.AvaloniaShell.Tests;

/// <summary>
/// One test per bug reported against the <c>linux-port-ab998cd</c> test build.
/// </summary>
/// <remarks>
/// <para>
/// Each of these fails on the code as shipped, which is the only reason to trust it. The
/// gesture is performed through the real controls, then the queue is drained and the
/// dispatcher pumped — mirroring what the render thread and the UI thread actually do.
/// </para>
/// </remarks>
public class MainWindowRegressionTests
{
    /// <summary>
    /// Brings up the window against a GPU-less queue and waits for startup to settle.
    /// </summary>
    /// <remarks>
    /// Settings are redirected to a scratch directory: the harness must never read or write
    /// the developer's real <c>~/.config/ifsrenderer</c>, and the Welcome window has to stay
    /// shut or startup would block on a dialog.
    /// </remarks>
    // ------------------------------------------------------------------ the bugs

    /// <summary>
    /// "I don't think it's normal behavior that when adding a node, it doesn't appear until
    /// you add or remove another node too."
    /// </summary>
    /// <remarks>
    /// The rebuild used to run on the line after <c>Post</c>, so it read the fractal before the
    /// queued mutation had been applied and rendered the PREVIOUS state. Draining the queue
    /// first is what this asserts: after the engine has run the edit, the list must show it.
    /// </remarks>
    [AvaloniaFact]
    public void Adding_an_iterator_shows_it_without_a_further_edit()
    {
        (MainWindow window, FakeRenderQueue queue) = MainWindowHarness.Start();
        var list = window.FindControl<ListBox>("IteratorList");
        int before = queue.Current.Iterators.Count;

        MainWindowHarness.Click(window, "BtnAddIterator");
        MainWindowHarness.Settle(queue);

        Assert.Equal(before + 1, queue.Current.Iterators.Count);
        Assert.Equal(before + 1, list.ItemCount);
    }

    /// <summary>Removing has the same ordering hazard as adding.</summary>
    [AvaloniaFact]
    public void Removing_an_iterator_updates_the_list_immediately()
    {
        (MainWindow window, FakeRenderQueue queue) = MainWindowHarness.Start();
        MainWindowHarness.Click(window, "BtnAddIterator");
        MainWindowHarness.Settle(queue);

        var list = window.FindControl<ListBox>("IteratorList");
        int before = queue.Current.Iterators.Count;
        Assert.True(before > 1, "need more than one iterator to remove");

        MainWindowHarness.Click(window, "BtnRemoveIterator");
        MainWindowHarness.Settle(queue);

        Assert.Equal(before - 1, queue.Current.Iterators.Count);
        Assert.Equal(before - 1, list.ItemCount);
    }

    /// <summary>
    /// "this also applies to post processing on the right side" — the same ordering bug in
    /// <c>AddPostFx</c>, reported as the worst of the set.
    /// </summary>
    [AvaloniaFact]
    public void Adding_a_post_effect_shows_the_layer_immediately()
    {
        (MainWindow window, FakeRenderQueue queue) = MainWindowHarness.Start();
        var combo = window.FindControl<ComboBox>("PostFxAddCombo");
        var layers = window.FindControl<StackPanel>("PostFxLayers");
        Assert.True(combo.ItemCount > 0, "no post effects were loaded from the library");

        int before = queue.Current.PostEffects.Count;
        combo.SelectedIndex = 0;
        MainWindowHarness.Settle(queue);

        Assert.Equal(before + 1, queue.Current.PostEffects.Count);
        Assert.Equal(before + 1, layers.Children.Count);
    }

    /// <summary>
    /// "Orientation sliders don't do anything."
    /// </summary>
    /// <remarks>
    /// The port wrote all four quaternion components and then normalised. Normalising projects
    /// the value back onto the unit sphere, so a purely RADIAL change cancels exactly — and
    /// from the identity pose (0,0,0,1) the W slider is precisely that, making it a no-op. The
    /// canonical <c>CameraSettingsViewModel.WOrientation</c> writes one component and does not
    /// normalise.
    /// </remarks>
    [AvaloniaFact]
    public void Orientation_w_slider_actually_rotates_the_camera()
    {
        (MainWindow window, FakeRenderQueue queue) = MainWindowHarness.Start();
        queue.Current.Camera.Orientation = Quaternion.Identity;

        var oriW = window.FindControl<ValueSlider>("OriW");
        Assert.NotNull(oriW);
        oriW.Value = queue.Current.Camera.Orientation.W;

        MainWindowHarness.Click(oriW, "UpButton");
        MainWindowHarness.Settle(queue);

        Assert.NotEqual(Quaternion.Identity, queue.Current.Camera.Orientation);
        Assert.Equal(1.001f, queue.Current.Camera.Orientation.W, 4);
    }

    /// <summary>Each orientation axis must move only its own component.</summary>
    [AvaloniaFact]
    public void Orientation_x_slider_leaves_the_other_components_alone()
    {
        (MainWindow window, FakeRenderQueue queue) = MainWindowHarness.Start();
        queue.Current.Camera.Orientation = Quaternion.Identity;

        var oriX = window.FindControl<ValueSlider>("OriX");
        oriX.Value = 0;
        MainWindowHarness.Click(oriX, "UpButton");
        MainWindowHarness.Settle(queue);

        Quaternion q = queue.Current.Camera.Orientation;
        Assert.Equal(0.001f, q.X, 4);
        Assert.Equal(0f, q.Y);
        Assert.Equal(0f, q.Z);
        Assert.Equal(1f, q.W);
    }

    /// <summary>
    /// "I somehow broke the renderer, it stopped rendering."
    /// </summary>
    /// <remarks>
    /// A throw from the dispatch loop pauses rendering. That was reported through the status
    /// bar, which the twice-a-second ticker overwrote within half a second, so the stall had no
    /// visible cause. The error is now sticky until rendering is resumed.
    /// </remarks>
    [AvaloniaFact]
    public void A_render_failure_stays_reported_and_clears_on_resume()
    {
        (MainWindow window, FakeRenderQueue queue) = MainWindowHarness.Start();

        queue.SimulateRenderFailure(new InvalidOperationException("compute blew up"));
        Dispatcher.UIThread.RunJobs();

        Assert.NotNull(queue.LastError);
        Assert.True(queue.Paused);

        queue.Paused = false;
        Assert.Null(queue.LastError);
    }
}