106 lines
4.0 KiB
C#
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();
|
|
}
|
|
}
|