2026-03-27 19:00:26 -04:00
using System ;
using System.Collections.ObjectModel ;
using System.IO ;
using Avalonia.Controls ;
using Avalonia.Headless.XUnit ;
2026-06-24 18:35:51 -05:00
using LANCommander.Launcher.Services ;
2026-06-06 05:49:24 -05:00
using LANCommander.Launcher.Tests.Helpers ;
using LANCommander.Launcher.ViewModels ;
using LANCommander.Launcher.ViewModels.Components ;
using LANCommander.Launcher.Views ;
using LANCommander.Launcher.Views.Components ;
2026-03-27 19:00:26 -04:00
using Microsoft.Extensions.DependencyInjection ;
using Microsoft.Extensions.Logging ;
using Xunit ;
2026-06-06 05:49:24 -05:00
namespace LANCommander.Launcher.Tests.Tests ;
2026-03-27 19:00:26 -04:00
/// <summary>
/// Renders each major view in the headless Avalonia environment and compares the
/// resulting screenshot against a committed baseline PNG. A test fails when more
/// than <see cref="VisualComparer.DefaultMaxMismatchPercent"/> percent of pixels
/// differ from the baseline, indicating a visual regression.
///
/// FIRST RUN / NEW VIEWS
/// If no baseline PNG exists, the test saves the current screenshot and fails with
/// an informational message. Inspect the screenshot in the CI artifact, then commit
/// it to Baselines/ to establish the baseline for future runs. Alternatively, run
/// the "Update Visual Baselines" workflow dispatch to commit all baselines at once.
/// </summary>
public class ViewLayoutTests
{
private const int WindowWidth = 1200 ;
private const int WindowHeight = 800 ;
2026-06-24 02:03:09 -05:00
static ViewLayoutTests ( )
{
// The Login, Splash and ServerSelection views pick a random full-screen
// background on load. Disable that here so the captured screenshots — and the
// committed baselines — are deterministic; otherwise every run compares against
// a different photo and reports a spurious regression.
ViewBackground . Enabled = false ;
}
2026-03-27 19:00:26 -04:00
// ---------------------------------------------------------------------------
// Service provider shared by all tests that need ViewModels with DI dependencies.
2026-06-24 18:35:51 -05:00
// Minimal: logging plus navigation — GameDetailViewModel resolves INavigationService
// in its constructor. No real SDK services needed for layout-only rendering.
2026-03-27 19:00:26 -04:00
// ---------------------------------------------------------------------------
private static readonly IServiceProvider _testServices = new ServiceCollection ( )
. AddLogging ( b = > b . AddConsole ( ) . SetMinimumLevel ( LogLevel . Warning ) )
2026-06-24 18:35:51 -05:00
. AddSingleton < INavigationService , NavigationService > ( )
2026-03-27 19:00:26 -04:00
. BuildServiceProvider ( ) ;
// ---------------------------------------------------------------------------
// Helper: wrap a view in a sized window, show it, capture and compare.
// ---------------------------------------------------------------------------
private static void CaptureAndAssert ( Control view , string screenshotName )
{
var window = new Window
{
Width = WindowWidth ,
Height = WindowHeight ,
Content = view ,
} ;
window . Show ( ) ;
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 )
{
// First run: copy the actual as the baseline so next run can compare.
Directory . CreateDirectory ( ScreenshotHelper . BaselinesDirectory ) ;
File . Copy ( actualPath , baselinePath , overwrite : true ) ;
}
Assert . True ( result . Passed , result . Summary ) ;
}
// ---------------------------------------------------------------------------
// Login View
// ---------------------------------------------------------------------------
[AvaloniaFact]
public void LoginView_Layout ( )
{
// LoginViewModel requires SDK services — for a pure layout test we render
// the LoginView with a null DataContext and let the XAML show its skeleton.
// The view's bindings gracefully no-op when DataContext is null.
var view = new LoginView ( ) ;
CaptureAndAssert ( view , "LoginView" ) ;
}
// ---------------------------------------------------------------------------
// Server Selection View
// ---------------------------------------------------------------------------
[AvaloniaFact]
public void ServerSelectionView_Layout ( )
{
var view = new ServerSelectionView ( ) ;
CaptureAndAssert ( view , "ServerSelectionView" ) ;
}
// ---------------------------------------------------------------------------
// Splash / Loading View
// ---------------------------------------------------------------------------
[AvaloniaFact]
public void SplashView_Layout ( )
{
var view = new SplashView ( ) ;
CaptureAndAssert ( view , "SplashView" ) ;
}
// ---------------------------------------------------------------------------
// Games List (Depot) — populated with representative mock data
// ---------------------------------------------------------------------------
[AvaloniaFact]
public void GamesListView_Layout ( )
{
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 = "Unreal Tournament" , ReleasedOn = new DateTime ( 1999 , 11 , 16 ) , HasCover = false } ,
new ( ) { Title = "Counter-Strike" , ReleasedOn = new DateTime ( 2000 , 11 , 8 ) , HasCover = false , InLibrary = true } ,
new ( ) { Title = "Team Fortress 2" , ReleasedOn = new DateTime ( 2007 , 10 , 10 ) , HasCover = false } ,
new ( ) { Title = "Portal" , ReleasedOn = new DateTime ( 2007 , 10 , 10 ) , HasCover = false , InLibrary = true } ,
new ( ) { Title = "Left 4 Dead" , ReleasedOn = new DateTime ( 2008 , 11 , 17 ) , HasCover = false } ,
} ;
var view = new GamesListView { DataContext = vm } ;
CaptureAndAssert ( view , "GamesListView" ) ;
}
// ---------------------------------------------------------------------------
// Games List (Depot) — empty state
// ---------------------------------------------------------------------------
[AvaloniaFact]
public void GamesListView_EmptyState_Layout ( )
{
var vm = new GamesListViewModel ( _testServices )
{
StatusMessage = "No games found matching your filters." ,
} ;
var view = new GamesListView { DataContext = vm } ;
CaptureAndAssert ( view , "GamesListView_Empty" ) ;
}
// ---------------------------------------------------------------------------
// Game Detail View — installed game with metadata
// ---------------------------------------------------------------------------
[AvaloniaFact]
public void GameDetailView_Installed_Layout ( )
{
var vm = new GameDetailViewModel ( _testServices )
{
Title = "Half-Life" ,
Description = "You are Gordon Freeman, a theoretical physicist who must fight your way out of the Black Mesa Research Facility after a catastrophic accident opens a portal to an alien world." ,
Genres = "Action, First-Person Shooter" ,
Tags = "Classic, Single-player, Sci-fi" ,
Developers = "Valve" ,
Publishers = "Sierra Studios" ,
Singleplayer = true ,
} ;
vm . ActionBar . Title = "Half-Life" ;
vm . ActionBar . IsInstalled = true ;
vm . ActionBar . IsInLibrary = true ;
vm . ActionBar . PlayTime = "14h 22m" ;
vm . ActionBar . LastPlayed = "3 days ago" ;
var view = new GameDetailView { DataContext = vm } ;
CaptureAndAssert ( view , "GameDetailView_Installed" ) ;
}
// ---------------------------------------------------------------------------
// Game Detail View — not yet installed
// ---------------------------------------------------------------------------
[AvaloniaFact]
public void GameDetailView_NotInstalled_Layout ( )
{
var vm = new GameDetailViewModel ( _testServices )
{
Title = "Quake III Arena" ,
Description = "A pure multiplayer arena shooter featuring fast-paced frantic gameplay." ,
Genres = "Action, First-Person Shooter" ,
Developers = "id Software" ,
Publishers = "Activision" ,
} ;
vm . ActionBar . Title = "Quake III Arena" ;
vm . ActionBar . IsInstalled = false ;
vm . ActionBar . IsInLibrary = true ;
// CanInstall is computed: !IsOfflineMode && !IsInstalled && !IsInstalling
// With IsInstalled=false and IsOfflineMode=false (default), CanInstall will be true.
vm . ActionBar . PlayTime = "0h 0m" ;
vm . ActionBar . LastPlayed = "Never" ;
var view = new GameDetailView { DataContext = vm } ;
CaptureAndAssert ( view , "GameDetailView_NotInstalled" ) ;
}
// ---------------------------------------------------------------------------
// Game Action Bar — installing in progress
// ---------------------------------------------------------------------------
[AvaloniaFact]
public void GameActionBar_Installing_Layout ( )
{
var vm = new GameActionBarViewModel ( _testServices )
{
Title = "Portal" ,
IsInstalled = false ,
IsInstalling = true ,
// CanInstall is computed: will be false because IsInstalling=true
IsInLibrary = true ,
PlayTime = "0h 0m" ,
LastPlayed = "Never" ,
StatusMessage = "Downloading… 42%" ,
} ;
var view = new GameActionBarView { DataContext = vm } ;
CaptureAndAssert ( view , "GameActionBar_Installing" ) ;
}
// ---------------------------------------------------------------------------
// Game Action Bar — game running
// ---------------------------------------------------------------------------
[AvaloniaFact]
public void GameActionBar_Running_Layout ( )
{
var vm = new GameActionBarViewModel ( _testServices )
{
Title = "Half-Life" ,
IsInstalled = true ,
IsRunning = true ,
IsInLibrary = true ,
PlayTime = "14h 22m" ,
LastPlayed = "Now" ,
} ;
var view = new GameActionBarView { DataContext = vm } ;
CaptureAndAssert ( view , "GameActionBar_Running" ) ;
}
}