Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
@@ -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);
}
}