Scaffold AvParser: Avalonia 12 shell with adaptive layout
Greenfield skeleton for a parser desktop app. The domain is deliberately a placeholder — IParser<TIn,TOut> plus two sample parsers — so the shell is runnable and verifiable end to end before real logic lands. Layers run one way: Core (no Avalonia, no IO) <- Infrastructure <- UI <- Desktop. UI is a class library rather than the exe so headless tests build real views without dragging in Program.cs, Serilog or the container. Adaptive layout is built from what Avalonia actually offers, since it has no AdaptiveTrigger or media queries: ResponsiveLayout observes Visual.Bounds and projects a breakpoint onto both an attached property and :compact/:medium/ :expanded pseudoclasses, with 24px hysteresis so dragging a window edge cannot make the layout flap. Pane state lives in the view model because a style setter loses to a local value permanently; styles own only the visual variance. Stack notes worth remembering: Avalonia.ReactiveUI is deprecated in favour of ReactiveUI.Avalonia, and ReactiveUI 24 runs on the Primitives engine (RxVoid, ISequencer, Signal<T>) and no longer self-initialises. Avalonia.Headless.XUnit 12.x requires xUnit v3. InvariantGlobalization must stay false or Semi.Avalonia throws in its static constructor. 102 tests across three projects, including headless guards for the two failures that are otherwise completely silent: a stylesheet whose selectors match nothing, and a light palette too low-contrast for cards to read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
using AvParser.UI.Navigation;
|
||||
using AvParser.UI.Tests.Fakes;
|
||||
using AvParser.UI.ViewModels;
|
||||
|
||||
namespace AvParser.UI.Tests;
|
||||
|
||||
public class NavigationServiceTests
|
||||
{
|
||||
private static (NavigationService Service, FakePage First, FakePage Second, OtherFakePage Third) Build()
|
||||
{
|
||||
var first = new FakePage("First");
|
||||
var second = new FakePage("Second");
|
||||
var third = new OtherFakePage();
|
||||
return (new NavigationService([first, second, third]), first, second, third);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Starts_on_the_first_registered_page()
|
||||
{
|
||||
var (service, first, _, _) = Build();
|
||||
|
||||
service.Current.ShouldBeSameAs(first);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rejects_an_empty_page_set() =>
|
||||
Should.Throw<ArgumentException>(() => new NavigationService(Array.Empty<PageViewModel>()));
|
||||
|
||||
[Fact]
|
||||
public void Navigating_pushes_the_previous_page()
|
||||
{
|
||||
var (service, first, second, _) = Build();
|
||||
|
||||
service.NavigateTo(second);
|
||||
|
||||
service.Current.ShouldBeSameAs(second);
|
||||
service.GoBack();
|
||||
service.Current.ShouldBeSameAs(first);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Navigating_to_the_current_page_is_a_no_op()
|
||||
{
|
||||
var (service, first, _, _) = Build();
|
||||
|
||||
service.NavigateTo(first);
|
||||
service.GoBack();
|
||||
|
||||
// Nothing was pushed, so GoBack had nothing to pop.
|
||||
service.Current.ShouldBeSameAs(first);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GoBack_on_an_empty_stack_does_nothing()
|
||||
{
|
||||
var (service, first, _, _) = Build();
|
||||
|
||||
service.GoBack();
|
||||
|
||||
service.Current.ShouldBeSameAs(first);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanGoBack_tracks_the_stack()
|
||||
{
|
||||
var (service, _, second, _) = Build();
|
||||
var observed = new List<bool>();
|
||||
using var subscription = service.CanGoBack.Subscribe(observed.Add);
|
||||
|
||||
service.NavigateTo(second);
|
||||
service.GoBack();
|
||||
|
||||
observed.ShouldBe([false, true, false]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Navigates_by_type()
|
||||
{
|
||||
var (service, _, _, third) = Build();
|
||||
|
||||
service.NavigateTo<OtherFakePage>();
|
||||
|
||||
service.Current.ShouldBeSameAs(third);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Throws_when_navigating_to_an_unregistered_type()
|
||||
{
|
||||
var service = new NavigationService([new FakePage("Only")]);
|
||||
|
||||
Should
|
||||
.Throw<InvalidOperationException>(service.NavigateTo<OtherFakePage>)
|
||||
.Message.ShouldContain("OtherFakePage");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentChanges_replays_the_present_value()
|
||||
{
|
||||
var (service, first, second, _) = Build();
|
||||
service.NavigateTo(second);
|
||||
|
||||
PageViewModel? seen = null;
|
||||
using var subscription = service.CurrentChanges.Subscribe(page =>
|
||||
{
|
||||
seen ??= page;
|
||||
});
|
||||
|
||||
seen.ShouldBeSameAs(second);
|
||||
seen.ShouldNotBeSameAs(first);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user