Files
Omika/tests/Omika.Tests/Mobile/DetailViewBackHeaderTests.cs
2026-10-23 18:46:50 +02:00

106 lines
4.0 KiB
C#

using System;
using System.Linq;
using System.Threading;
using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Omika.App.Views;
using Xunit;
namespace Omika.Tests.Mobile;
/// <summary>
/// Régression « bouton retour toujours accessible » : dans chaque page de détail,
/// le bouton « ← Retour » est construit dans un en-tête FIXE, donc HORS de la zone
/// de défilement — sinon il disparaît dès que l'utilisateur descend dans la page
/// et l'oblige à remonter pour revenir en arrière.
/// </summary>
/// <remarks>
/// Deux pièges de mesure sont évités ici :
/// <list type="bullet">
/// <item>la surcharge <c>Dispatch(Action)</c> n'attend pas la fin de l'action
/// (elle s'exécute après le retour de l'appel) : les assertions ne seraient
/// jamais évaluées dans le test — d'où l'appel à la surcharge générique, qui est
/// attendue ;</item>
/// <item>une vue non attachée à une fenêtre n'a pas d'arbre visuel : la vue est
/// donc réellement affichée dans une fenêtre avant d'être inspectée.</item>
/// </list>
/// La classe rejoint la collection « ApplicationRuntimeContext » : une session
/// Avalonia headless ne doit jamais s'exécuter en parallèle d'une autre, sous
/// peine de corrompre le localisateur global d'Avalonia.
/// </remarks>
[Collection("ApplicationRuntimeContext")]
public sealed class DetailViewBackHeaderTests : IDisposable
{
private readonly HeadlessUnitTestSession _session;
public DetailViewBackHeaderTests()
=> _session = HeadlessUnitTestSession.StartNew(typeof(LayoutTestAppBuilder));
public void Dispose() => _session.Dispose();
/// <summary>Toutes les pages de détail exposant un bouton retour.</summary>
public static TheoryData<Type> VuesDeDetail => new()
{
typeof(ClientDetailView),
typeof(DossierDetailView),
typeof(SiteDetailView),
typeof(ContactDetailView),
typeof(CollaborateurDetailView),
typeof(EquipeDetailView),
typeof(InterventionDetailView),
typeof(InterventionContactsView),
typeof(InterventionEmailsView),
typeof(InterventionNotesView),
typeof(InterventionNotesAdministrativesView),
typeof(InterventionResourcesView),
typeof(InterventionDocumentsView),
typeof(CompteRenduCreateView),
typeof(RendezVousDetailView),
typeof(RapportChampsView),
typeof(NouveauEmailView),
typeof(SynchronisationView),
typeof(AgendaEquipeView)
};
[Theory]
[MemberData(nameof(VuesDeDetail))]
public void BoutonRetour_EstHorsZoneDeDefilement(Type typeVue)
{
_session.Dispatch(
() =>
{
var vue = (UserControl)Activator.CreateInstance(typeVue)!;
// Vue réellement affichée : sans cela l'arbre visuel est vide et
// le test passerait sans rien vérifier.
var fenetre = new Window { Width = 420, Height = 800, Content = vue };
fenetre.Show();
Dispatcher.UIThread.RunJobs();
Control[] descendants = vue.GetVisualDescendants().OfType<Control>().ToArray();
Button[] boutonsRetour = descendants
.OfType<Button>()
.Where(b => (b.Content as string)?.Contains("Retour", StringComparison.Ordinal) == true)
.ToArray();
Assert.NotEmpty(boutonsRetour);
// Au moins un bouton retour est FIXE : aucun ScrollViewer au-dessus
// de lui, il ne défile donc pas avec le contenu.
Assert.Contains(
boutonsRetour,
b => !b.GetVisualAncestors().Any(a => a is ScrollViewer));
// Le contenu de la page, lui, reste défilable.
Assert.Contains(descendants, c => c is ScrollViewer);
fenetre.Close();
return true;
},
CancellationToken.None).GetAwaiter().GetResult();
}
}