using System; using System.Linq; using System.Threading; using Avalonia; using Avalonia.Controls; using Avalonia.Headless; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Threading; using Avalonia.VisualTree; using Omika.App.Controls; using Omika.App.ViewModels; using Omika.Core.Contracts.Documents; using Xunit; namespace Omika.Tests.Mobile; /// /// Champ TEXTE d'une rubrique dynamique (création de RAPPORT et composition /// d'EMAIL) : la saisie doit être TOUJOURS multiligne. /// /// /// Un champ sur une seule ligne masquait la fin d'une valeur longue : le texte /// défilait horizontalement au lieu de revenir à la ligne, et l'utilisateur ne /// voyait pas ce qu'il avait écrit. Le type RTF et la longueur de la valeur ne /// décident donc plus du contrôle affiché. /// /// /// /// Les deux écrans partagent le MÊME gabarit /// (avares://Omika.App/Views/ChampEditionTemplates.axaml, clé /// ChampEditionDetailTemplate) : ces tests portent donc sur le rendu réel /// du gabarit commun, pas sur une copie. /// /// /// /// 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 /// (un champ n'a de géométrie qu'une fois disposé). /// [Collection("ApplicationRuntimeContext")] public sealed class ChampTexteMultiligneTests : IDisposable { /// Gabarit partagé entre l'écran Rapport et l'écran de composition d'email. private const string GabaritPartage = "avares://Omika.App/Views/ChampEditionTemplates.axaml"; private const string CleGabarit = "ChampEditionDetailTemplate"; 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(); /// /// Champ modifiable, valeur courte (« REF ») : le contrôle affiché est un champ /// de saisie MULTILIGNE — retour à la ligne accepté et enroulement actif. /// [Fact] public void ChampTexteModifiable_ValeurCourte_ResteMultiligne() => Executer(() => { Window fenetre = Ouvrir(Texte(modifiable: true, valeur: "REF")); try { DictationTextField dictee = ChampDeSaisie(fenetre); TextBox saisie = dictee.FindControl("Saisie")!; Assert.True(dictee.Multiline, "Le champ texte n'est pas en mode multiligne."); // Deux marqueurs du multiligne : le retour à la ligne est accepté // (touche Entrée) et le texte revient à la ligne tout seul. Assert.True(saisie.AcceptsReturn, "Le champ n'accepte pas le retour à la ligne."); Assert.Equal(TextWrapping.Wrap, saisie.TextWrapping); // Et le champ est réellement plus haut qu'une ligne : la place des // lignes suivantes existe dès l'ouverture. Assert.True( saisie.Bounds.Height >= 90, $"Le champ ne fait que {saisie.Bounds.Height:0.##} px de haut."); } finally { fenetre.Close(); } }); /// /// Champ modifiable dont la valeur contient déjà un retour à la ligne : même /// contrôle multiligne (aucun basculement de gabarit selon le contenu). /// [Fact] public void ChampTexteModifiable_ValeurSurPlusieursLignes_ResteMultiligne() => Executer(() => { Window fenetre = Ouvrir(Texte(modifiable: true, valeur: "ligne 1\nligne 2")); try { TextBox saisie = ChampDeSaisie(fenetre).FindControl("Saisie")!; Assert.True(saisie.AcceptsReturn); Assert.Equal(TextWrapping.Wrap, saisie.TextWrapping); Assert.Equal("ligne 1\nligne 2", saisie.Text); } finally { fenetre.Close(); } }); /// /// Un seul contrôle de saisie est construit pour un champ texte : la vue ne /// peut pas basculer d'un champ simple vers un champ long selon la valeur. /// [Fact] public void ChampTexte_UnSeulControleDeSaisie() => Executer(() => { Window fenetre = Ouvrir(Texte(modifiable: true, valeur: "REF")); try { DictationTextField[] champs = fenetre.GetVisualDescendants() .OfType() .ToArray(); // Un seul champ de saisie est DESSINÉ (les autres branches du // gabarit restent masquées). DictationTextField dessine = Assert.Single(champs, c => c.IsVisible); Assert.True(dessine.Multiline); } finally { fenetre.Close(); } }); /// /// Champ NON modifiable : aucun contrôle de saisie, un texte qui revient à la /// ligne (la lecture suit la même largeur d'écran que la saisie). /// [Fact] public void ChampTexteNonModifiable_TexteQuiRevientALaLigne() => Executer(() => { Window fenetre = Ouvrir(Texte(modifiable: false, valeur: "REF")); try { Assert.DoesNotContain( fenetre.GetVisualDescendants().OfType(), c => c.IsVisible); TextBlock texte = fenetre.GetVisualDescendants() .OfType() .Single(t => t.IsVisible && t.Text == "REF"); Assert.Equal(TextWrapping.Wrap, texte.TextWrapping); } finally { fenetre.Close(); } }); // ===================================================================== // Aides // ===================================================================== /// Rubrique texte de test (type chaîne du modèle de rapport). private static RapportChampEdition Texte(bool modifiable, string valeur) => new(new RapportChampDto { Id = "1", Rubrique = "$REFINTERNE$", GroupeApp = "INTER", MembreMob = "IN_RefInterne", Type = "ChpSimpleChaine", ListeDonnesBD = valeur, Modifiable = modifiable }); /// /// Construit le gabarit PARTAGÉ (Rapport + Email) pour cette rubrique et /// l'affiche dans une fenêtre de largeur téléphone. /// private static Window Ouvrir(RapportChampEdition edition) { var dictionnaire = (ResourceDictionary)AvaloniaXamlLoader.Load(new Uri(GabaritPartage)); var gabarit = (DataTemplate)dictionnaire[CleGabarit]; Control contenu = gabarit.Build(edition)!; // Le contexte est posé explicitement : sans lui, aucun binding du gabarit // n'est évalué (les contrôleurs resteraient tous visibles, sans valeur), // et le test ne prouverait rien. contenu.DataContext = edition; var fenetre = new Window { Width = 400, Height = 900, RequestedThemeVariant = Avalonia.Styling.ThemeVariant.Light, Content = contenu }; fenetre.Show(); Dispatcher.UIThread.RunJobs(); Dispatcher.UIThread.RunJobs(); return fenetre; } /// Le champ de saisie dicté réellement construit par le gabarit. private static DictationTextField ChampDeSaisie(Window fenetre) => fenetre.GetVisualDescendants().OfType().Single(); }