317 lines
11 KiB
C#
317 lines
11 KiB
C#
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);
|
||
}
|
||
}
|