From 4554886e332eb5ddd7096bda6406d9f29393cb99 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Fri, 25 Sep 2026 21:17:33 +0100 Subject: [PATCH] PostIt: fix grayed/inert file-row action buttons (wrong $parent binding) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The action buttons inside ItemsControl item templates (Télécharger / Supprimer / Joindre on MyFiles; download/detach on the attachment lists of BillingQueryDetailsPage and EstimateEditionPage) were bound with $parent[vm:XxxViewModel].Cmd, which searches for an ancestor CONTROL of type XxxViewModel — a viewmodel, not a control — so the binding resolved to null and the Button stayed disabled (grayed) and inert on click. Switch to the working CirclesPage/EstimateListPage syntax: $parent[ContentPage].((vm:XxxViewModel)DataContext).Cmd. Adds MyFilesPageHeadlessTests: mounts MyFilesPage via NavigationPage + Window.Show() and asserts each row button's Command is bound, armed (CanExecute), and fires the API call on click. 136/136 pass. Co-Authored-By: Claude Code --- .../PostIt.Tests/MyFilesPageHeadlessTests.cs | 208 ++++++++++++++++++ .../Activity/BillingQueryDetailsPage.axaml | 6 +- .../Views/Activity/EstimateEditionPage.axaml | 4 +- .../PostIt/Views/MyFiles/MyFilesPage.axaml | 10 +- 4 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 src/PostIt/PostIt.Tests/MyFilesPageHeadlessTests.cs 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