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,75 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Headless.XUnit;
|
||||
using AvParser.UI;
|
||||
using AvParser.UI.ViewModels;
|
||||
using AvParser.UI.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace AvParser.UI.HeadlessTests;
|
||||
|
||||
public class ViewLocatorTests
|
||||
{
|
||||
private static ViewLocator Locator(Action<IServiceCollection>? configure = null)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
configure?.Invoke(services);
|
||||
return new ViewLocator(services.BuildServiceProvider());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Matches_view_models_only()
|
||||
{
|
||||
var locator = Locator();
|
||||
|
||||
locator.Match(new FakePage("x")).ShouldBeTrue();
|
||||
locator.Match("a string").ShouldBeFalse();
|
||||
locator.Match(null).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Resolves_a_view_by_naming_convention()
|
||||
{
|
||||
var locator = Locator();
|
||||
var viewModel = new AboutViewModel(new TestPaths());
|
||||
|
||||
var view = locator.Build(viewModel);
|
||||
|
||||
view.ShouldBeOfType<AboutView>();
|
||||
view.DataContext.ShouldBeSameAs(viewModel);
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Prefers_a_registered_view_over_activation()
|
||||
{
|
||||
var registered = new AboutView();
|
||||
var locator = Locator(services => services.AddSingleton(registered));
|
||||
|
||||
var view = locator.Build(new AboutViewModel(new TestPaths()));
|
||||
|
||||
view.ShouldBeSameAs(registered);
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Reports_a_missing_view_instead_of_throwing()
|
||||
{
|
||||
var locator = Locator();
|
||||
|
||||
// "FakePage" matches neither half of the convention, so the locator must report a miss
|
||||
// rather than resolving the view model as its own view and failing to activate it.
|
||||
var view = locator.Build(new FakePage("x"));
|
||||
|
||||
view.ShouldBeOfType<TextBlock>().Text!.ShouldContain("View not found");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builds_a_placeholder_for_a_null_view_model() => Locator().Build(null).ShouldBeOfType<TextBlock>();
|
||||
|
||||
private sealed class TestPaths : Infrastructure.Storage.IAppPaths
|
||||
{
|
||||
public string DataDirectory => Path.Combine(Path.GetTempPath(), "AvParserTests");
|
||||
|
||||
public string SettingsFile => Path.Combine(DataDirectory, "settings.json");
|
||||
|
||||
public string LogDirectory => Path.Combine(DataDirectory, "logs");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user