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

225 lines
8.1 KiB
C#

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;
/// <summary>
/// Champ TEXTE d'une rubrique dynamique (création de RAPPORT et composition
/// d'EMAIL) : la saisie doit être TOUJOURS multiligne.
///
/// <para>
/// 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é.
/// </para>
///
/// <para>
/// Les deux écrans partagent le MÊME gabarit
/// (<c>avares://Omika.App/Views/ChampEditionTemplates.axaml</c>, clé
/// <c>ChampEditionDetailTemplate</c>) : ces tests portent donc sur le rendu réel
/// du gabarit commun, pas sur une copie.
/// </para>
/// </summary>
/// <remarks>
/// 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é).
/// </remarks>
[Collection("ApplicationRuntimeContext")]
public sealed class ChampTexteMultiligneTests : IDisposable
{
/// <summary>Gabarit partagé entre l'écran Rapport et l'écran de composition d'email.</summary>
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();
/// <summary>
/// Champ modifiable, valeur courte (« REF ») : le contrôle affiché est un champ
/// de saisie MULTILIGNE — retour à la ligne accepté et enroulement actif.
/// </summary>
[Fact]
public void ChampTexteModifiable_ValeurCourte_ResteMultiligne()
=> Executer(() =>
{
Window fenetre = Ouvrir(Texte(modifiable: true, valeur: "REF"));
try
{
DictationTextField dictee = ChampDeSaisie(fenetre);
TextBox saisie = dictee.FindControl<TextBox>("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();
}
});
/// <summary>
/// Champ modifiable dont la valeur contient déjà un retour à la ligne : même
/// contrôle multiligne (aucun basculement de gabarit selon le contenu).
/// </summary>
[Fact]
public void ChampTexteModifiable_ValeurSurPlusieursLignes_ResteMultiligne()
=> Executer(() =>
{
Window fenetre = Ouvrir(Texte(modifiable: true, valeur: "ligne 1\nligne 2"));
try
{
TextBox saisie = ChampDeSaisie(fenetre).FindControl<TextBox>("Saisie")!;
Assert.True(saisie.AcceptsReturn);
Assert.Equal(TextWrapping.Wrap, saisie.TextWrapping);
Assert.Equal("ligne 1\nligne 2", saisie.Text);
}
finally
{
fenetre.Close();
}
});
/// <summary>
/// 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.
/// </summary>
[Fact]
public void ChampTexte_UnSeulControleDeSaisie()
=> Executer(() =>
{
Window fenetre = Ouvrir(Texte(modifiable: true, valeur: "REF"));
try
{
DictationTextField[] champs = fenetre.GetVisualDescendants()
.OfType<DictationTextField>()
.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();
}
});
/// <summary>
/// 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).
/// </summary>
[Fact]
public void ChampTexteNonModifiable_TexteQuiRevientALaLigne()
=> Executer(() =>
{
Window fenetre = Ouvrir(Texte(modifiable: false, valeur: "REF"));
try
{
Assert.DoesNotContain(
fenetre.GetVisualDescendants().OfType<DictationTextField>(),
c => c.IsVisible);
TextBlock texte = fenetre.GetVisualDescendants()
.OfType<TextBlock>()
.Single(t => t.IsVisible && t.Text == "REF");
Assert.Equal(TextWrapping.Wrap, texte.TextWrapping);
}
finally
{
fenetre.Close();
}
});
// =====================================================================
// Aides
// =====================================================================
/// <summary>Rubrique texte de test (type chaîne du modèle de rapport).</summary>
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
});
/// <summary>
/// Construit le gabarit PARTAGÉ (Rapport + Email) pour cette rubrique et
/// l'affiche dans une fenêtre de largeur téléphone.
/// </summary>
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;
}
/// <summary>Le champ de saisie dicté réellement construit par le gabarit.</summary>
private static DictationTextField ChampDeSaisie(Window fenetre)
=> fenetre.GetVisualDescendants().OfType<DictationTextField>().Single();
}