using System; using System.Globalization; using System.Linq; using System.Threading; using Avalonia; using Avalonia.Controls; using Avalonia.Headless; using Avalonia.Threading; using Avalonia.VisualTree; using Omika.App.Views; using Xunit; namespace Omika.Tests.Mobile; /// /// Rangée d'actions de l'écran « Rapport » — dans l'ordre demandé : Annuler, /// Enregistrer, Créer, Envoyer un email. Sur un téléphone, les quatre boutons /// doivent rester ATTEIGNABLES et porter le MÊME style que les autres boutons de /// l'application. /// /// /// Deux défauts constatés à l'usage, et corrigés ici : /// /// /// les quatre boutons étaient alignés sur UNE seule ligne, de largeur /// illimitée : en petit écran les deux derniers sortaient du cadre, deux actions /// seulement restaient accessibles ; /// « Annuler » et « Enregistrer » ne portaient que la classe de TAILLE /// (« compact »), sans aucune variante de couleur : ils conservaient le rendu /// natif Fluent et ne ressemblaient à aucun autre bouton de l'application. /// /// /// /// Le bouton d'email, lui, ne doit plus afficher d'icône enveloppe : le libellé /// seul porte le sens, comme sur tous les autres boutons de l'application. /// /// /// /// La classe rejoint la collection « ApplicationRuntimeContext » : le localisateur /// d'Avalonia est global au processus, deux sessions headless ne doivent jamais /// s'exécuter en parallèle. Les mesures sont lues À L'INTÉRIEUR du fil d'interface /// (une fenêtre n'a de géométrie qu'une fois disposée). /// [Collection("ApplicationRuntimeContext")] public sealed class RapportActionsTests : IDisposable { /// Libellés attendus des quatre actions, dans l'ordre de la vue. private static readonly string[] Libelles = ["Annuler", "Enregistrer", "Créer", "Envoyer un email"]; /// Variantes de couleur réelles de l'application (styles Controls.axaml). private static readonly string[] Variantes = ["primary", "secondary", "ghost", "danger"]; private readonly HeadlessUnitTestSession _session = HeadlessUnitTestSession.StartNew(typeof(LayoutTestAppBuilder)); public void Dispose() => _session.Dispose(); private void Executer(Action scenario) => _session.Dispatch( () => { scenario(); return true; }, CancellationToken.None).GetAwaiter().GetResult(); /// /// Les quatre actions sont présentes et ENTIÈREMENT dans le cadre, à la largeur /// d'un téléphone (320 px : le plus étroit courant) comme à 420 px. Sans le /// retour à la ligne, les derniers boutons étaient disposés au-delà du bord /// droit (rect.Right very largement supérieur à la largeur de l'écran). /// [Theory] [InlineData(420)] [InlineData(360)] [InlineData(320)] public void LesQuatreActionsRestentDansLEcran(double largeur) => Executer(() => { Window fenetre = OuvrirRapport(largeur); // Mesure APRÈS disposition : une vue non affichée n'a aucune géométrie. var boutons = BoutonsDAction(fenetre); Assert.Equal(Libelles, boutons.Select(b => b.Content as string).ToArray()); foreach (Button bouton in boutons) { Rect rect = RectDe(bouton, fenetre); Assert.True( rect.Width > 0 && rect.Height > 0, $"« {bouton.Content} » n'est pas disposé (taille {rect.Width}×{rect.Height})."); Assert.True( rect.X >= -0.5, $"« {bouton.Content} » déborde à gauche : x={Fmt(rect.X)}."); Assert.True( rect.Right <= largeur + 0.5, $"« {bouton.Content} » dépasse l'écran : droite={Fmt(rect.Right)} " + $"pour une largeur de {Fmt(largeur)} px."); Assert.True(bouton.IsVisible, $"« {bouton.Content} » n'est pas visible."); } fenetre.Close(); }); /// /// Les quatre actions sont dans l'ORDRE DEMANDÉ, lu de gauche à droite et de /// haut en bas dans la page réellement disposée (Annuler, Enregistrer, Créer, /// Envoyer un email). Ce test est distinct de la recherche par libellé : il /// mesure les POSITIONS, donc il détecte un ordre inversé dans la vue. /// [Fact] public void LesActionsSontDansLordreDemande() => Executer(() => { Window fenetre = OuvrirRapport(420); string[] ordre = fenetre.GetVisualDescendants() .OfType