using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Headless.XUnit; using Microsoft.Extensions.DependencyInjection; 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())) { } } /// /// 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 (MainWindow window, CirclesPage page, AddCircleMemberDialog dialog) Mount() { 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(); var window = new MainWindow(); var app = (PostIt.App)Application.Current!; app.DataTemplates.Clear(); app.DataTemplates.Add(new ViewLocator(sp)); app.AttachMainWindow(window); window.Show(); var circlesPage = sp.GetRequiredService(); window.NavRoot.PushAsync(circlesPage).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. var dialogVm = new AddCircleMemberDialogViewModel(sp.GetRequiredService()); ((App)Application.Current!).PushPageAsync(dialogVm).GetAwaiter().GetResult(); var dialog = window.NavRoot.NavigationStack[^1] as AddCircleMemberDialog ?? throw new System.InvalidOperationException("Dialog page not at top of stack."); return (window, circlesPage, dialog); } /// /// Click the "Fermer" button on the dialog and assert the /// nav stack shrinks by exactly one. /// [AvaloniaFact] public void Close_button_pops_dialog_off_nav_stack() { // Arrange: stack starts at 2 (CirclesPage + dialog). var (window, _, _) = Mount(); 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. dialog.CloseButton.RaiseEvent(new Avalonia.Interactivity.RoutedEventArgs(Button.ClickEvent)); // 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]); } }