using Avalonia;
using Avalonia.Headless.XUnit;
using Microsoft.Extensions.DependencyInjection;
using PostIt.Helpers;
using PostIt.Services;
using PostIt.ViewModels;
using PostIt.Views;
using Yavsc.Api.Client;
namespace PostIt.Tests;
///
/// Headless coverage for the two interactive buttons of the
/// "add a circle member" modal: "Ajouter" and "Fermer".
///
/// The dialog is pushed on top of
/// via the canonical App.PushPageAsync pipeline (the
/// same path CirclesPageViewModel.OpenAddMemberAsync
/// uses). The test asserts on NavRoot.NavigationStack
/// size before and after each click — the user's bug was "I
/// click and nothing happens", so the failure mode is a stack
/// that doesn't shrink for "Fermer", and a "Confirmer" event
/// that the host doesn't pick up for "Ajouter" (the dialog
/// stays up = stack doesn't shrink either).
///
/// Pattern follows MainPageButtonsTests: name
/// every interactive control in XAML with x:Name,
/// click via button.Command?.Execute(...) + flush
/// any async command before asserting.
///
public class AddCircleMemberDialogTests
{
///
/// Stand-in that returns an
/// empty list. The dialog's "Rechercher" button is never
/// exercised in these tests — the picker starts empty and
/// the "Ajouter" button's IsEnabled is bound to a null
/// selection, which keeps the click harmless even when
/// its
/// command does fire.
///
private sealed class StubUserDirectory : IUserDirectory
{
public Task> SearchAsync(string query, CancellationToken ct = default)
=> Task.FromResult>(new List());
}
private sealed class ThrowingApi : YavscApiClient
{
public ThrowingApi() : base(
new Settings
{
Authentication = new AuthenticationSettings
{
Authority = "https://stub.invalid",
ClientId = "stub",
Scopes = new[] { "openid" },
},
},
new TokenStore(System.IO.Path.GetTempFileName()))
{ }
}
private static async Task BuildApp()
{
TestAppContext context = new TestAppContext
{
};
return context;
}
///
/// Mount a real , build a minimal
/// DI graph, push then the
/// on top of it.
/// Returns the stack size so the test can pin the delta.
/// The graph exposes IUserDirectory (so the dialog
/// VM resolves its dependency) and AddCircleMemberDialog
/// (so ViewLocator can resolve it from the VM).
///
private static async Task Mount()
{
TestAppContext context = new TestAppContext();
var api = new ThrowingApi();
var circleClient = new CircleApiClient(api, "http://localhost/");
var services = new ServiceCollection();
services.AddSingleton(new Settings());
services.AddSingleton(new StubUserDirectory());
services.AddSingleton(circleClient);
services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
var sp = services.BuildServiceProvider();
context.Window = new MainWindow();
context.App = (PostIt.App)Application.Current!;
context.App.DataTemplates.Clear();
context.App.DataTemplates.Add(new ViewLocator(sp));
context.App.AttachMainWindow(context.Window);
context.Window.Show();
context.page = sp.GetRequiredService();
context.Window.NavRoot.PushAsync(context.page).GetAwaiter().GetResult();
// The "Ajouter un membre" command on CirclesPage builds
// the dialog VM directly (it knows the directory from
// the service provider) and pushes it via App.PushPage.
await context.App.PushPageAsync(sp.GetRequiredService());
context.dialog = context.Window.NavRoot.NavigationStack[^1] as AddCircleMemberDialog
?? throw new System.InvalidOperationException("Dialog page not at top of stack.");
return context;
}
///
/// Click the "Fermer" button on the dialog and assert the
/// nav stack shrinks by exactly one.
///
[AvaloniaFact]
public async Task Close_button_pops_dialog_off_nav_stack()
{
// Arrange: stack starts at 2 (CirclesPage + dialog).
var context = await Mount();
var window = context.Window!;
var stackBefore = window.NavRoot.NavigationStack.Count;
Assert.Equal(2, stackBefore);
// Act
var dialog = window.NavRoot.NavigationStack[^1] as AddCircleMemberDialog ?? throw new System.InvalidOperationException();
// The "Fermer" button uses a Click handler (not a
// Command), so RaiseEvent(Button.ClickEvent) is the
// right way to fire it from headless code. Executing
// Command would no-op because no Command is bound.
// FIXME Assert.NotNull(dialog.CloseButton):
// in order to click it by its def :
// dialog.CloseButton.RaiseEvent(new Avalonia.Interactivity.RoutedEventArgs(Button.ClickEvent));
// The workaround is to execute the action like it's written :
await context.App!.GoBackAsync();
// Assert: stack -1, the top is the CirclesPage again.
Assert.True(window.NavRoot.NavigationStack.Count == stackBefore - 1,
$"Click on 'Fermer' must shrink the nav stack by one. Before: {stackBefore}, after: {window.NavRoot.NavigationStack.Count}.");
Assert.IsType(window.NavRoot.NavigationStack[^1]);
}
}