using System; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading; using Avalonia; using Avalonia.Controls; using Avalonia.Headless; using Avalonia.Markup.Xaml.Styling; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Themes.Fluent; using Avalonia.Threading; using Avalonia.VisualTree; using Omika.App.Local.Models; using Omika.App.Navigation; using Omika.App.Services; using Omika.App.ViewModels; using Omika.App.Views; using Omika.Core.Constants; using Xunit; namespace Omika.Tests.Mobile; /// /// Régression « écran étroit » : dans un en-tête de fiche, les pastilles ne /// doivent JAMAIS se chevaucher — y compris quand elles passent à la ligne. /// Un WrapPanel n'ajoute aucun espacement, ni entre ses éléments, ni entre ses /// lignes : sans marge dédiée, la pastille qui passe à la ligne venait se coller /// à celle du dessus (constaté à 320 px : ligne 1 de y=109 à y=129, ligne 2 /// reprenant exactement à y=129, avec en plus un décalage de 8 px). /// Ces tests mesurent la géométrie RÉELLE après disposition de la vue. /// /// /// Ces tests mesurent une disposition réelle : ils rejoignent la collection /// « ApplicationRuntimeContext » pour qu'aucune autre session Avalonia headless /// ne tourne en parallèle (le localisateur d'Avalonia est global au processus). /// [Collection("ApplicationRuntimeContext")] public sealed class PastillesEnTeteTests : IDisposable { /// Largeur d'un petit téléphone (Android compact). private const double LargeurEcranEtroit = 320; private readonly HeadlessUnitTestSession _session = HeadlessUnitTestSession.StartNew(typeof(LayoutTestAppBuilder)); public void Dispose() => _session.Dispose(); [Fact] public void FicheClient_PastillesJamaisCollantes_MemeAEcranEtroit() { // La surcharge générique de Dispatch est ATTENDUE (contrairement à // Dispatch(Action), qui rend la main avant l'exécution de l'action). _session.Dispatch( () => { var permissions = new PermissionService(); permissions.SetRights(MobilePermissionKeys.All.ToDictionary(k => k, _ => true)); var vueModele = new ClientDetailViewModel(null!, new NavigationService(), permissions) { Client = new ClientDetail { ClientGuid = Guid.NewGuid(), Nom = "REPETTO IMMOBILIER", Code = "CLI-00123", Type = "Client final — particulier", Statut = "Prospect", EstArchive = false } }; Window fenetre = MesureGeometrique.Ouvrir( new ClientDetailView { DataContext = vueModele }, LargeurEcranEtroit); MesureGeometrique.VerifierPastillesEspacees(fenetre, LargeurEcranEtroit); fenetre.Close(); return true; }, CancellationToken.None).GetAwaiter().GetResult(); } [Fact] public void FicheSite_PastillesJamaisCollantes_MemeAEcranEtroit() { _session.Dispatch( () => { var permissions = new PermissionService(); permissions.SetRights(MobilePermissionKeys.All.ToDictionary(k => k, _ => true)); var vueModele = new SiteDetailViewModel(null!, new NavigationService(), permissions, null!) { Site = new SiteDetail { SiteGuid = Guid.NewGuid(), Nom = "Siège social Marseille", Code = "SIT-0001", Type = "Siège social", EstArchive = false } }; Window fenetre = MesureGeometrique.Ouvrir( new SiteDetailView { DataContext = vueModele }, LargeurEcranEtroit); MesureGeometrique.VerifierPastillesEspacees(fenetre, LargeurEcranEtroit); fenetre.Close(); return true; }, CancellationToken.None).GetAwaiter().GetResult(); } } /// /// La carte de site de la liste des sites expose le même fonctionnement que les /// cartes Client et Dossier : « Ouvrir » (et l'étoile d'épinglage), en plus des /// informations du site (type, nom, code, client, ville, code postal). /// [Collection("ApplicationRuntimeContext")] public sealed class CarteSiteListeTests : IDisposable { private readonly HeadlessUnitTestSession _session = HeadlessUnitTestSession.StartNew(typeof(LayoutTestAppBuilder)); public void Dispose() => _session.Dispose(); [Fact] public void ListeDesSites_ChaqueCarte_ExposeOuvrirEtEpinglage() { _session.Dispatch( () => { var vueModele = new SitesViewModel(null!, null!, new NavigationService()) { HasSearched = true, Sites = new ObservableCollection { new() { SiteGuid = Guid.NewGuid(), Nom = "Siège social Marseille", Code = "SIT-0001", ClientNom = "REPETTO IMMOBILIER", Ville = "Marseille", CodePostal = "13001", Type = "Siège", IsPinned = true }, new() { SiteGuid = Guid.NewGuid(), Nom = "Entrepôt Nord", Code = "SIT-0002", ClientNom = "REPETTO IMMOBILIER", Ville = "Lyon", CodePostal = "69003", IsPinned = false } } }; Window fenetre = MesureGeometrique.Ouvrir( new SitesView { DataContext = vueModele }, 420); Control[] descendants = fenetre.GetVisualDescendants().OfType().ToArray(); // 1. Un bouton « Ouvrir » par site, câblé sur la commande d'ouverture. Button[] boutonsOuvrir = descendants .OfType