using Avalonia;
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.VisualTree;
using AvParser.Core.Settings;
using AvParser.UI.Localization;
using AvParser.UI.Navigation;
using AvParser.UI.Responsive;
using AvParser.UI.Services;
using AvParser.UI.ViewModels;
using AvParser.UI.Views;
using ReactiveUI.Primitives.Concurrency;
namespace AvParser.UI.HeadlessTests;
public class ShellViewTests
{
///
/// Shows the shell inside a window of the requested width.
///
///
/// A real is required — a detached control never builds its visual tree,
/// so there would be no to assert on. The width is set on the window
/// rather than by calling Measure/Arrange by hand: a manual arrange is undone by the window's
/// own next layout pass, which made these assertions depend on pump ordering.
///
private static (ShellView View, ShellViewModel ViewModel, Window Window) ShowShell(double width)
{
var navigation = new NavigationService([
new FakePage("Page.Dashboard"),
new FakePage("Page.Parse", "IconDocument"),
]);
// ImmediateSequencer, not the real main-thread one: derived properties then settle within
// the same layout pass, so a single RunJobs() is enough for the bindings to catch up.
var viewModel = new ShellViewModel(navigation, new FakeThemeService(), ImmediateSequencer.Instance);
var view = new ShellView { DataContext = viewModel };
var window = new Window
{
Width = width,
Height = 800,
Content = view,
};
window.Show();
Dispatcher.UIThread.RunJobs();
return (view, viewModel, window);
}
private static void Resize(Window window, double width)
{
window.Width = width;
Dispatcher.UIThread.RunJobs();
}
private static SplitView NavPaneOf(Visual view) => view.GetVisualDescendants().OfType().Single();
[AvaloniaTheory]
[InlineData(500, SplitViewDisplayMode.Overlay)]
[InlineData(900, SplitViewDisplayMode.CompactInline)]
[InlineData(1400, SplitViewDisplayMode.Inline)]
public void The_navigation_pane_adapts_to_the_shell_width(double width, SplitViewDisplayMode expected)
{
var (view, _, _) = ShowShell(width);
NavPaneOf(view).DisplayMode.ShouldBe(expected);
}
[AvaloniaTheory]
[InlineData(500, Breakpoint.Compact)]
[InlineData(900, Breakpoint.Medium)]
[InlineData(1400, Breakpoint.Expanded)]
public void The_shell_hands_its_breakpoint_to_the_view_model(double width, Breakpoint expected)
{
var (_, viewModel, _) = ShowShell(width);
viewModel.Breakpoint.ShouldBe(expected);
}
[AvaloniaFact]
public void Narrowing_the_shell_closes_the_pane_and_widening_reopens_it()
{
var (_, viewModel, window) = ShowShell(1400);
viewModel.IsPaneOpen.ShouldBeTrue();
Resize(window, 480);
viewModel.IsPaneOpen.ShouldBeFalse();
Resize(window, 1400);
viewModel.IsPaneOpen.ShouldBeTrue();
}
[AvaloniaFact]
public void The_rail_lists_every_registered_page()
{
var (view, _, _) = ShowShell(1400);
view.GetVisualDescendants().OfType().Single().ItemCount.ShouldBe(2);
}
[AvaloniaFact]
public void Selecting_a_rail_entry_swaps_the_hosted_page()
{
var (view, viewModel, _) = ShowShell(1400);
var second = viewModel.Pages[1];
viewModel.SelectedPage = second;
Dispatcher.UIThread.RunJobs();
var host = view.GetVisualDescendants().OfType().Single();
host.Content.ShouldBeSameAs(second);
viewModel.Title.ShouldBe(Localizer.Instance["Page.Parse"]);
}
///
/// Guards against the shell stylesheet silently matching nothing.
///
///
/// Avalonia type selectors match the exact type, so UserControl.shell does not match
/// (which derives from ReactiveUserControl<T>). That
/// failure is completely silent — the app renders, just with default metrics — so it needs an
/// explicit assertion on a value only the stylesheet can produce.
///
[AvaloniaFact]
public void The_shell_stylesheet_is_actually_applied()
{
var (view, _, _) = ShowShell(1400);
// 248 comes from the NavPaneWidth token; SplitView's own default is 320.
NavPaneOf(view).OpenPaneLength.ShouldBe(248d);
NavPaneOf(view).CompactPaneLength.ShouldBe(56d);
var pageHost = view.GetVisualDescendants().OfType().Single(b => b.Name == "PageHost");
pageHost.Padding.ShouldBe(new Thickness(24));
var paneToggle = view.GetVisualDescendants().OfType