LANCommander/LANCommander.Launcher.Tests/Tests/ViewInteractionTests.cs
Pat Hartl b183e4b18c Remove old launcher, rename Avalonia launcher
Also adds ARM builds for Linux + Windows launcher
2026-06-06 05:49:24 -05:00

178 lines
6.9 KiB
C#

using System;
using System.Collections.ObjectModel;
using System.IO;
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.Threading;
using LANCommander.Launcher.Tests.Helpers;
using LANCommander.Launcher.ViewModels;
using LANCommander.Launcher.ViewModels.Components;
using LANCommander.Launcher.Views;
using LANCommander.Launcher.Views.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;
namespace LANCommander.Launcher.Tests.Tests;
/// <summary>
/// Renders a view, mutates ViewModel state to simulate a user action (or a service
/// callback firing), pumps the dispatcher, and screenshots the post-mutation frame.
/// Catches binding regressions that ViewLayoutTests' single-snapshot-per-state coverage
/// can miss — e.g. an [ObservableProperty] that no longer notifies, or a converter
/// that drops updates after the first render.
///
/// Each scenario emits its own baseline. First run saves the screenshot and fails;
/// inspect, accept, commit to Baselines/ and CI is green from then on.
/// </summary>
public class ViewInteractionTests
{
private const int WindowWidth = 1200;
private const int WindowHeight = 800;
private static readonly IServiceProvider _testServices = new ServiceCollection()
.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Warning))
.BuildServiceProvider();
private static void RenderInteractAndAssert(
Control view,
Action interact,
string screenshotName)
{
var window = new Window
{
Width = WindowWidth,
Height = WindowHeight,
Content = view,
};
window.Show();
// Initial layout pass so bindings settle before we mutate state.
Dispatcher.UIThread.RunJobs();
interact();
// Drain queued binding/render jobs the mutation generated.
Dispatcher.UIThread.RunJobs();
var actualPath = ScreenshotHelper.Capture(window, screenshotName);
var baselinePath = ScreenshotHelper.GetBaselinePath(screenshotName);
var diffPath = ScreenshotHelper.GetDiffPath(screenshotName);
var result = VisualComparer.Compare(actualPath, baselinePath, diffPath);
if (!result.BaselineExists)
{
Directory.CreateDirectory(ScreenshotHelper.BaselinesDirectory);
File.Copy(actualPath, baselinePath, overwrite: true);
}
Assert.True(result.Passed, result.Summary);
}
// -------------------------------------------------------------------------
// GameActionBar transitions Installed → Running when game launches
// -------------------------------------------------------------------------
[AvaloniaFact]
public void GameActionBar_transitions_to_Running_after_play()
{
var vm = new GameActionBarViewModel(_testServices)
{
Title = "Half-Life",
IsInstalled = true,
IsInLibrary = true,
PlayTime = "14h 22m",
LastPlayed = "3 days ago",
};
var view = new GameActionBarView { DataContext = vm };
RenderInteractAndAssert(
view,
interact: () =>
{
// Simulates the play command's effect — IsRunning flips after process starts.
vm.IsRunning = true;
vm.LastPlayed = "Now";
},
screenshotName: "GameActionBar_Interaction_BecomesRunning");
}
// -------------------------------------------------------------------------
// GameActionBar transitions Idle → Installing with progress message
// -------------------------------------------------------------------------
[AvaloniaFact]
public void GameActionBar_transitions_to_Installing_with_progress()
{
var vm = new GameActionBarViewModel(_testServices)
{
Title = "Portal",
IsInLibrary = true,
PlayTime = "0h 0m",
LastPlayed = "Never",
};
var view = new GameActionBarView { DataContext = vm };
RenderInteractAndAssert(
view,
interact: () =>
{
// Simulates InstallService.OnInstall firing into the action bar.
vm.IsInstalling = true;
vm.StatusMessage = "Downloading… 64%";
},
screenshotName: "GameActionBar_Interaction_BecomesInstalling");
}
// -------------------------------------------------------------------------
// GamesListView re-renders when filtered down to one item
// -------------------------------------------------------------------------
[AvaloniaFact]
public void GamesListView_rerenders_after_collection_filtered()
{
var vm = new GamesListViewModel(_testServices);
vm.Games = new ObservableCollection<GameItemViewModel>
{
new() { Title = "Half-Life", ReleasedOn = new DateTime(1998, 11, 19), HasCover = false },
new() { Title = "Quake III Arena", ReleasedOn = new DateTime(1999, 12, 2), HasCover = false },
new() { Title = "Doom", ReleasedOn = new DateTime(1993, 12, 10), HasCover = false, InLibrary = true },
new() { Title = "Counter-Strike", ReleasedOn = new DateTime(2000, 11, 8), HasCover = false, InLibrary = true },
};
var view = new GamesListView { DataContext = vm };
RenderInteractAndAssert(
view,
interact: () =>
{
// Simulates a user typing "Doom" — only one item should remain on screen.
vm.Games = new ObservableCollection<GameItemViewModel>
{
new() { Title = "Doom", ReleasedOn = new DateTime(1993, 12, 10), HasCover = false, InLibrary = true },
};
},
screenshotName: "GamesListView_Interaction_FilteredToOne");
}
// -------------------------------------------------------------------------
// GamesListView transitions populated → empty state
// -------------------------------------------------------------------------
[AvaloniaFact]
public void GamesListView_shows_empty_state_after_clear()
{
var vm = new GamesListViewModel(_testServices);
vm.Games = new ObservableCollection<GameItemViewModel>
{
new() { Title = "Half-Life", ReleasedOn = new DateTime(1998, 11, 19), HasCover = false },
new() { Title = "Doom", ReleasedOn = new DateTime(1993, 12, 10), HasCover = false },
};
var view = new GamesListView { DataContext = vm };
RenderInteractAndAssert(
view,
interact: () =>
{
vm.Games = new ObservableCollection<GameItemViewModel>();
vm.StatusMessage = "No games match your filters.";
},
screenshotName: "GamesListView_Interaction_EmptyAfterClear");
}
}