Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Headless;
|
||||
using Avalonia.VisualTree;
|
||||
using Omika.App.Local.Abstractions;
|
||||
using Omika.App.Local.Models;
|
||||
using Omika.App.Services;
|
||||
using Omika.App.ViewModels;
|
||||
using Omika.App.Views;
|
||||
using Xunit;
|
||||
|
||||
namespace Omika.Tests.Mobile;
|
||||
|
||||
/// <summary>
|
||||
/// Fiche intervention : les cartes « Courriels » et « Rapports » passent par le
|
||||
/// serveur (liste + création pour l'une, catalogue des modèles et champs calculés
|
||||
/// côté serveur pour l'autre). Hors connexion elles deviennent INACTIVES — sans
|
||||
/// jamais être masquées : l'action existe, elle est seulement momentanément
|
||||
/// impossible.
|
||||
///
|
||||
/// <para>
|
||||
/// Vérification faite sur la vue RÉELLE rendue (session Avalonia headless) avec le
|
||||
/// vrai ViewModel : les liaisons XAML sont compilées, un faux DataContext ne
|
||||
/// prouverait donc rien.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Collection("ApplicationRuntimeContext")]
|
||||
public sealed class InterventionDetailOfflineActionsTests : IDisposable
|
||||
{
|
||||
private readonly HeadlessUnitTestSession _session =
|
||||
HeadlessUnitTestSession.StartNew(typeof(LayoutTestAppBuilder));
|
||||
|
||||
public void Dispose() => _session.Dispose();
|
||||
|
||||
private sealed class FauxReseau : INetworkConnectivity
|
||||
{
|
||||
public bool Online { get; set; } = true;
|
||||
|
||||
public Task<bool> IsOnlineAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(Online);
|
||||
}
|
||||
|
||||
/// <summary>État observé d'une carte, lu sur le thread Avalonia puis comparé hors session.</summary>
|
||||
private sealed record EtatCarte(
|
||||
string Libelle,
|
||||
bool Actif,
|
||||
bool Visible,
|
||||
double Largeur,
|
||||
double Hauteur,
|
||||
double Opacite,
|
||||
bool IndiceOuvrir);
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel réel : seules les dépendances non utilisées par le rendu de la
|
||||
/// fiche restent nulles (aucun chargement, aucun appel réseau n'est déclenché).
|
||||
/// </summary>
|
||||
private static InterventionDetailViewModel CreerViewModel()
|
||||
=> new(
|
||||
null!, null!, null!, null!, null!, null!, null!, null!,
|
||||
new PermissionService(), null!, null!, new FauxReseau())
|
||||
{
|
||||
Intervention = new InterventionDetail
|
||||
{
|
||||
InterventionGuid = Guid.NewGuid(),
|
||||
Reference = "INT-001",
|
||||
Titre = "Intervention",
|
||||
Etat = "En cours"
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>Carte cliquable dont le libellé est exactement <paramref name="titre"/>.</summary>
|
||||
private static Button Carte(Visual racine, string titre)
|
||||
=> racine.GetVisualDescendants()
|
||||
.OfType<Button>()
|
||||
.Single(b => b.GetVisualDescendants()
|
||||
.OfType<TextBlock>()
|
||||
.Any(t => t.Text == titre));
|
||||
|
||||
private void Executer(Action scenario)
|
||||
=> _session.Dispatch(
|
||||
() =>
|
||||
{
|
||||
scenario();
|
||||
return true;
|
||||
},
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>Rend la fiche hors ligne ou en ligne et retourne l'état des deux cartes.</summary>
|
||||
private EtatCarte[] Rendre(bool horsLigne)
|
||||
{
|
||||
EtatCarte[] etats = [];
|
||||
|
||||
Executer(() =>
|
||||
{
|
||||
InterventionDetailViewModel vm = CreerViewModel();
|
||||
vm.IsOffline = horsLigne;
|
||||
|
||||
var fenetre = new Window
|
||||
{
|
||||
Width = 420,
|
||||
Height = 900,
|
||||
Content = new InterventionDetailView { DataContext = vm }
|
||||
};
|
||||
fenetre.Show();
|
||||
Avalonia.Threading.Dispatcher.UIThread.RunJobs();
|
||||
Avalonia.Threading.Dispatcher.UIThread.RunJobs();
|
||||
|
||||
etats = new[] { "Courriels", "Rapports" }
|
||||
.Select(titre =>
|
||||
{
|
||||
Button b = Carte(fenetre, titre);
|
||||
bool indiceOuvrir = b.GetVisualDescendants()
|
||||
.OfType<TextBlock>()
|
||||
.Any(t => t.Text == "Ouvrir ›" && t.IsVisible);
|
||||
return new EtatCarte(
|
||||
titre,
|
||||
b.IsEnabled,
|
||||
b.IsVisible,
|
||||
b.Bounds.Width,
|
||||
b.Bounds.Height,
|
||||
b.Opacity,
|
||||
indiceOuvrir);
|
||||
})
|
||||
.ToArray();
|
||||
});
|
||||
|
||||
return etats;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HorsLigne_CourrielsEtRapports_SontInactifsMaisToujoursVisibles()
|
||||
{
|
||||
foreach (EtatCarte carte in Rendre(horsLigne: true))
|
||||
{
|
||||
// Inactives : aucune navigation possible vers une page qui ne peut pas
|
||||
// fonctionner sans serveur.
|
||||
Assert.False(carte.Actif);
|
||||
|
||||
// Mais toujours présentes et à leur place (aucun masquage).
|
||||
Assert.True(carte.Visible);
|
||||
Assert.True(carte.Largeur > 0);
|
||||
Assert.True(carte.Hauteur > 0);
|
||||
|
||||
// Rendu « désactivé » visible : Button.secondary:disabled atténue la carte.
|
||||
Assert.Equal(0.45, carte.Opacite, 2);
|
||||
|
||||
// Aucun indice d'ouverture trompeur sur une carte inutilisable.
|
||||
Assert.False(carte.IndiceOuvrir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnLigne_CourrielsEtRapports_SontActifs()
|
||||
{
|
||||
foreach (EtatCarte carte in Rendre(horsLigne: false))
|
||||
{
|
||||
Assert.True(carte.Actif);
|
||||
Assert.True(carte.Visible);
|
||||
Assert.Equal(1.0, carte.Opacite, 2);
|
||||
Assert.True(carte.IndiceOuvrir);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Le bloc « Client » de la fiche intervention : présent UNIQUEMENT quand un
|
||||
/// client est lié, placé au-dessus du bloc « Informations », avec le nom du
|
||||
/// client, ses numéros, son adresse email, les boutons d'appel / courriel et le
|
||||
/// bouton « Fiche client ».
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ClientLie_AfficheLeBlocClientAuDessusDesInformations()
|
||||
{
|
||||
double yClient = double.NaN;
|
||||
double yInformations = double.NaN;
|
||||
bool boutonVisible = false;
|
||||
bool boutonActif = false;
|
||||
bool nomAffiche = false;
|
||||
bool mobileAffiche = false;
|
||||
bool fixeAffiche = false;
|
||||
bool emailAffiche = false;
|
||||
bool appelVisible = false;
|
||||
bool emailVisible = false;
|
||||
|
||||
Executer(() =>
|
||||
{
|
||||
var permissions = new PermissionService();
|
||||
permissions.SetRights(new Dictionary<string, bool>
|
||||
{
|
||||
[Omika.Core.Constants.MobilePermissionKeys.VoirClients] = true,
|
||||
[Omika.Core.Constants.MobilePermissionKeys.EnvoiEmail] = true
|
||||
});
|
||||
|
||||
Guid clientId = Guid.NewGuid();
|
||||
InterventionDetailViewModel vm = new(
|
||||
null!, null!, null!, null!, null!, null!, null!, null!,
|
||||
permissions, null!, null!, new FauxReseau())
|
||||
{
|
||||
Intervention = new InterventionDetail
|
||||
{
|
||||
InterventionGuid = Guid.NewGuid(),
|
||||
Reference = "INT-002",
|
||||
Titre = "Intervention avec client",
|
||||
Etat = "En cours",
|
||||
ClientGuid = clientId,
|
||||
ClientNom = "Boulangerie Martin"
|
||||
},
|
||||
Client = new ClientDetail
|
||||
{
|
||||
ClientGuid = clientId,
|
||||
Nom = "Boulangerie Martin",
|
||||
Code = "CLI-042",
|
||||
Mobile = "0612345678",
|
||||
Phone = "0240000000",
|
||||
Email = "contact@boulangerie.test"
|
||||
}
|
||||
};
|
||||
|
||||
var fenetre = new Window
|
||||
{
|
||||
Width = 900,
|
||||
Height = 1200,
|
||||
Content = new InterventionDetailView { DataContext = vm }
|
||||
};
|
||||
fenetre.Show();
|
||||
Avalonia.Threading.Dispatcher.UIThread.RunJobs();
|
||||
Avalonia.Threading.Dispatcher.UIThread.RunJobs();
|
||||
|
||||
Border CarteSection(string titre) => fenetre.GetVisualDescendants()
|
||||
.OfType<Border>()
|
||||
.Where(b => b.Classes.Contains("card"))
|
||||
.Single(b => b.GetVisualDescendants()
|
||||
.OfType<TextBlock>()
|
||||
.Any(t => t.Text == titre));
|
||||
|
||||
Border blocClient = CarteSection("Client");
|
||||
Border blocInformations = CarteSection("Informations");
|
||||
|
||||
Point? pointClient = blocClient.TranslatePoint(new Point(0, 0), fenetre);
|
||||
Point? pointInformations = blocInformations.TranslatePoint(new Point(0, 0), fenetre);
|
||||
yClient = pointClient?.Y ?? double.NaN;
|
||||
yInformations = pointInformations?.Y ?? double.NaN;
|
||||
|
||||
Button bouton = blocClient.GetVisualDescendants()
|
||||
.OfType<Button>()
|
||||
.Single(b => Equals(b.Content, "Fiche client"));
|
||||
boutonVisible = bouton.IsVisible && bouton.Bounds.Width > 0;
|
||||
boutonActif = bouton.IsEnabled;
|
||||
|
||||
var textes = blocClient.GetVisualDescendants()
|
||||
.OfType<TextBlock>()
|
||||
.Where(t => t.IsVisible)
|
||||
.Select(t => t.Text)
|
||||
.ToList();
|
||||
|
||||
nomAffiche = textes.Any(t => t == "Boulangerie Martin");
|
||||
mobileAffiche = textes.Any(t => t == "Mobile 0612345678");
|
||||
fixeAffiche = textes.Any(t => t == "Tél. 0240000000");
|
||||
emailAffiche = textes.Any(t => t == "contact@boulangerie.test");
|
||||
|
||||
appelVisible = blocClient.GetVisualDescendants()
|
||||
.OfType<Omika.App.Controls.CallButton>()
|
||||
.Any(c => c.IsVisible);
|
||||
emailVisible = blocClient.GetVisualDescendants()
|
||||
.OfType<Omika.App.Controls.EmailButton>()
|
||||
.Any(c => c.IsVisible);
|
||||
});
|
||||
|
||||
// Le bloc est placé entre l'en-tête et le bloc « Informations ».
|
||||
Assert.True(yClient < yInformations, $"bloc client ({yClient}) devrait précéder les informations ({yInformations})");
|
||||
|
||||
// Nom, numéros et adresse email sur des lignes distinctes.
|
||||
Assert.True(nomAffiche);
|
||||
Assert.True(mobileAffiche);
|
||||
Assert.True(fixeAffiche);
|
||||
Assert.True(emailAffiche);
|
||||
|
||||
// Actions : appel et courriel comme sur une ligne de contact, plus l'accès
|
||||
// à la fiche client.
|
||||
Assert.True(appelVisible);
|
||||
Assert.True(emailVisible);
|
||||
Assert.True(boutonVisible);
|
||||
Assert.True(boutonActif);
|
||||
}
|
||||
|
||||
/// <summary>Sans client lié, aucun bloc « Client » n'est rendu.</summary>
|
||||
[Fact]
|
||||
public void SansClient_AucunBlocClient()
|
||||
{
|
||||
bool blocPresent = true;
|
||||
|
||||
Executer(() =>
|
||||
{
|
||||
InterventionDetailViewModel vm = CreerViewModel();
|
||||
var fenetre = new Window
|
||||
{
|
||||
Width = 900,
|
||||
Height = 1200,
|
||||
Content = new InterventionDetailView { DataContext = vm }
|
||||
};
|
||||
fenetre.Show();
|
||||
Avalonia.Threading.Dispatcher.UIThread.RunJobs();
|
||||
Avalonia.Threading.Dispatcher.UIThread.RunJobs();
|
||||
|
||||
blocPresent = fenetre.GetVisualDescendants()
|
||||
.OfType<Button>()
|
||||
.Any(b => Equals(b.Content, "Fiche client") && b.IsVisible);
|
||||
});
|
||||
|
||||
Assert.False(blocPresent);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user