diff --git a/src/PostIt/PostIt.Tests/MyFilesPageHeadlessTests.cs b/src/PostIt/PostIt.Tests/MyFilesPageHeadlessTests.cs new file mode 100644 index 000000000..72619f3c5 --- /dev/null +++ b/src/PostIt/PostIt.Tests/MyFilesPageHeadlessTests.cs @@ -0,0 +1,208 @@ +using System.Net.Http; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using Avalonia.VisualTree; +using CommunityToolkit.Mvvm.Input; +using PostIt.ViewModels; +using PostIt.Views; +using Yavsc.Api.Client; + +namespace PostIt.Tests; + +/// +/// Headless UI coverage for the "Mes fichiers" page +/// (). The user reported that once a file +/// is listed, "l'item est inerte au click, et ses boutons restent +/// grisés, désactivés" — neither the row nor its action buttons +/// (Télécharger / Supprimer) respond to a click. +/// +/// These tests mount a real under +/// the Avalonia headless platform with a stub +/// that lists one file, then walk the +/// realised visual tree to find the row's action buttons and +/// assert the two things that would silently break and reproduce +/// the "grayed / inert" symptom: +/// +/// The Command binding +/// ($parent[vm:MyFilesViewModel].DownloadFileCommand) +/// resolved to a non-null command — a null command leaves the +/// button disabled (grayed) and click-silent. +/// The command is armed +/// (CanExecute true) for the listed file — an +/// AsyncRelayCommand with +/// AllowConcurrentExecutions=false (the +/// +/// default) disables itself while running; a button stuck +/// "running" is exactly the grayed-and-inert report. +/// Executing the command actually invokes the +/// file download / delete on the API client — the row +/// "activates" on click instead of being inert. +/// +/// The OS save picker is absent under headless, so +/// FileSaveHelpers.SaveAsync throws or cancels and the +/// download command's catch sets an error status — but the +/// API download is awaited before the picker, so the call +/// is recorded regardless. We do not await the command's +/// ExecutionTask to completion, to avoid blocking on the +/// (platform-dependent) picker. +/// +public class MyFilesPageHeadlessTests +{ + private static (MyFilesPage page, MyFilesViewModel vm, StubFsApi api) CreatePage() + { + var api = new StubFsApi(); + var fsClient = new UserFilesApiClient(api, "https://blogs.example/api/v1/"); + var vm = new MyFilesViewModel(fsClient); + var page = new MyFilesPage { DataContext = vm }; + return (page, vm, api); + } + + /// + /// Mount the page the way the app does — pushed onto a + /// shown in a + /// (a realizes its content template via + /// the navigation host, cf. RdvPageHeadlessTests) — after + /// listing a single file by awaiting + /// . Populating + /// before the first layout + /// pass lets the ItemsControl realise its item templates + /// under headless. + /// + private static async Task<(MyFilesPage page, MyFilesViewModel vm, StubFsApi api)> MountAsync() + { + var (page, vm, api) = CreatePage(); + await vm.InitializeAsync(); + var nav = new NavigationPage(); + nav.PushAsync(page).GetAwaiter().GetResult(); + var window = new Window { Content = nav }; + window.Show(); + // Let bindings / the ItemsControl item templates apply. + await Task.Delay(50); + return (page, vm, api); + } + + /// + /// Find the first action button in a file row whose content + /// matches . The file rows live in an + /// ItemsControl DataTemplate (no x:Name), so we + /// walk the visual tree. + /// + private static Button? FindRowButton(MyFilesPage page, string content) + => page.GetVisualDescendants() + .OfType