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

431 lines
17 KiB
C#

using System.Text.Json;
using Omika.App.Local.Abstractions;
using Omika.App.Local.Models;
using Omika.App.ViewModels;
using Omika.Core.Contracts.Documents;
namespace Omika.Tests.Mobile.Local;
/// <summary>
/// Tests de la correction UI « recette » : la sélection UNIQUE ($UN…) doit rendre
/// les MÊMES cartes que la sélection MULTIPLE ($TAB…) — ligne compacte si non
/// sélectionnée, développée si sélectionnée — seule la logique de sélection diffère
/// (max 1 pour UN, plusieurs pour TAB). Vérifie aussi la mutualisation des templates
/// par TYPE (EstTypePhotos/Contacts/RendezVous/CompteRendus vrais pour UN et TAB),
/// le résumé utilisateur (jamais CheminComplet) et la notification des états de
/// miniature (bug du premier affichage).
/// </summary>
public class RapportSelectionUiTests
{
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
// =====================================================================
// Helpers
// =====================================================================
private static string Serialiser(object valeur) => JsonSerializer.Serialize(valeur, Json);
private static RapportChampDto ChampUnique(
string rubrique,
string groupeApp,
string membreMob,
string listeDonnesBD,
string listeDonnesChoix)
=> new()
{
Id = "1",
Nom = "Rubrique",
Rubrique = rubrique,
Type = "ChpSimpleChaine",
GroupeApp = groupeApp,
MembreMob = membreMob,
ListeDonnesBD = listeDonnesBD,
ListeDonnesChoix = listeDonnesChoix
};
private static RapportChampDto Tableau(string rubrique, string groupeApp, string listeDonnesBD)
=> new()
{
Id = "1",
Nom = "Tableau",
Rubrique = rubrique,
Type = "Tableau",
GroupeApp = groupeApp,
ListeDonnesBD = listeDonnesBD
};
private static string ContactsJson(params string[] noms)
{
var items = noms.Select(n => new
{
id = Guid.NewGuid().ToString(),
nom = n,
role = "Occupant",
telFixe = "04 11 22 33 44",
telMobile = "06 00 00 00 00",
etage = "1"
});
return Serialiser(items);
}
private static string PhotosJson(params (string Nom, string Chemin)[] photos)
{
var items = photos.Select(p => new
{
guid = Guid.NewGuid().ToString(),
cheminComplet = p.Chemin,
nom = p.Nom,
date = "2026-01-30",
notes = (string?)null
});
return Serialiser(items);
}
private static string RdvJson(params (string Debut, string Fin, string Objet)[] rdvs)
{
var items = rdvs.Select(r => new
{
id = Guid.NewGuid().ToString(),
dateDebut = r.Debut,
dateFin = r.Fin,
objet = r.Objet,
note = (string?)null,
collaborateurs = "Tech 1"
});
return Serialiser(items);
}
private static string CrJson(params (string Debut, string Fin, string Collaborateur, string Signataire)[] crs)
{
var items = crs.Select(c => new
{
id = Guid.NewGuid().ToString(),
debut = c.Debut,
fin = c.Fin,
collaborateur = c.Collaborateur,
signataire = c.Signataire
});
return Serialiser(items);
}
private static TableauOptionEdition PhotoOption(string? guid = null, string? cheminComplet = null)
{
using JsonDocument doc = JsonDocument.Parse(Serialiser(
new { guid, cheminComplet, nom = "photo.jpg", date = "2026-01-30", notes = "Note" }));
return new TableauOptionEdition(doc.RootElement.Clone(), "photo.jpg", TypeTableau.Photos);
}
// =====================================================================
// Mutualisation par TYPE (UN et TAB partagent le même template)
// =====================================================================
[Fact]
public void UnCtsite_EstTypeContacts_True()
{
var edition = new RapportChampEdition(ChampUnique(
"$UNCTSITE.NOMPRENOM$", "CONTACT_SITE", "saNom",
"Jean Dupont", ContactsJson("Jean Dupont", "Marie Martin")));
Assert.True(edition.EstTypeContacts);
Assert.False(edition.EstTypePhotos);
}
[Fact]
public void TabCtsite_EstTypeContacts_True()
{
var edition = new RapportChampEdition(Tableau(
"$TABCONTACTSITE$", "CONTACT_SITE", ContactsJson("Jean Dupont", "Marie Martin")));
Assert.True(edition.EstTypeContacts);
}
[Fact]
public void UnPhoto_EstTypePhotos_True()
{
var edition = new RapportChampEdition(ChampUnique(
"$UNPHOTO.IMAGE$", "PHOTO_INTER", "cheminComplet",
"C:\\Documents_Demo\\photo1.jpg", PhotosJson(("photo1.jpg", "C:\\Documents_Demo\\photo1.jpg"))));
// UNPHOTO utilise le MÊME template que TABPHOTO (et déclenche le chargement
// des miniatures, qui n'est plus limité à AChoixMultiple).
Assert.True(edition.EstTypePhotos);
}
[Fact]
public void TabPhoto_EstTypePhotos_True()
{
var edition = new RapportChampEdition(Tableau(
"$TABPHOTO$", "PHOTO_INTER", PhotosJson(("photo1.jpg", "C:\\Documents_Demo\\photo1.jpg"))));
Assert.True(edition.EstTypePhotos);
}
[Fact]
public void UnRdv_EstTypeRendezVous_True()
{
var edition = new RapportChampEdition(ChampUnique(
"$UNRDV.OBJET$", "RDV_INTER", "saObjet",
"Objet", RdvJson(("2026-01-30 10:00:00", "2026-01-30 11:00:00", "Objet"))));
Assert.True(edition.EstTypeRendezVous);
}
[Fact]
public void UnCr_EstTypeCompteRendus_True()
{
var edition = new RapportChampEdition(ChampUnique(
"$UNCR.SIGNATAIRE$", "CR_INTER", "saSignataire",
"Signataire", CrJson(("2026-01-30 08:00:00", "2026-01-30 09:00:00", "Tech", "Signataire"))));
Assert.True(edition.EstTypeCompteRendus);
}
[Fact]
public void Consigne_Unique_SelectionnezUnElement()
{
var edition = new RapportChampEdition(ChampUnique(
"$UNCTSITE.NOMPRENOM$", "CONTACT_SITE", "saNom",
"Jean Dupont", ContactsJson("Jean Dupont")));
Assert.Equal("Sélectionnez un élément", edition.Consigne);
}
[Fact]
public void Consigne_MultiplePhotos_ConsignePhotos()
{
var edition = new RapportChampEdition(Tableau(
"$TABPHOTO$", "PHOTO_INTER", PhotosJson(("photo1.jpg", "C:\\photo1.jpg"))));
Assert.Equal("Sélectionnez les photos du rapport", edition.Consigne);
}
// =====================================================================
// Single vs Multi : une seule carte développée / plusieurs
// =====================================================================
[Fact]
public void UnCtsite_ChangementSelection_AncienneRepliee()
{
var edition = new RapportChampEdition(ChampUnique(
"$UNCTSITE.NOMPRENOM$", "CONTACT_SITE", "saNom",
"Renaud Oscar", ContactsJson("Renaud Oscar", "Alves Victoire", "Philippe Philippe")));
// Pré-sélection = premier élément (une seule carte développée).
Assert.True(edition.Options[0].EstSelectionne);
Assert.Equal(1, edition.NombreSelectionnes);
// Sélection « Alves Victoire » (index 1) : l'ancienne se replie.
edition.BasculerOptionCommand.Execute(edition.Options[1]);
Assert.True(edition.Options[1].EstSelectionne);
Assert.False(edition.Options[0].EstSelectionne);
Assert.False(edition.Options[2].EstSelectionne);
Assert.Equal(1, edition.NombreSelectionnes);
// Sélection « Philippe Philippe » (index 2) : Alves se replie.
edition.BasculerOptionCommand.Execute(edition.Options[2]);
Assert.True(edition.Options[2].EstSelectionne);
Assert.False(edition.Options[1].EstSelectionne);
Assert.False(edition.Options[0].EstSelectionne);
Assert.Equal(1, edition.NombreSelectionnes);
}
[Fact]
public void TabCtsite_DeuxSelectionnes_DeuxDeveloppes()
{
var edition = new RapportChampEdition(Tableau(
"$TABCONTACTSITE$", "CONTACT_SITE",
ContactsJson("Renaud Oscar", "Alves Victoire", "Philippe Philippe")));
edition.Options[0].EstSelectionne = true;
edition.Options[1].EstSelectionne = true;
Assert.True(edition.Options[0].EstSelectionne);
Assert.True(edition.Options[1].EstSelectionne);
Assert.False(edition.Options[2].EstSelectionne);
Assert.Equal(2, edition.NombreSelectionnes);
}
[Fact]
public void UnRdv_UneSeuleCarteDeveloppee()
{
var edition = new RapportChampEdition(ChampUnique(
"$UNRDV.OBJET$", "RDV_INTER", "saObjet",
"Objet 1",
RdvJson(
("2026-01-30 10:00:00", "2026-01-30 11:00:00", "Objet 1"),
("2026-01-30 12:00:00", "2026-01-30 13:00:00", "Objet 2"),
("2026-01-30 14:00:00", "2026-01-30 15:00:00", "Objet 3"))));
edition.BasculerOptionCommand.Execute(edition.Options[1]);
Assert.Equal(1, edition.NombreSelectionnes);
Assert.True(edition.Options[1].EstSelectionne);
Assert.False(edition.Options[0].EstSelectionne);
Assert.False(edition.Options[2].EstSelectionne);
}
[Fact]
public void UnCr_UneSeuleCarteDeveloppee()
{
var edition = new RapportChampEdition(ChampUnique(
"$UNCR.SIGNATAIRE$", "CR_INTER", "saSignataire",
"Signataire 1",
CrJson(
("2026-01-30 08:00:00", "2026-01-30 09:00:00", "Tech 1", "Signataire 1"),
("2026-01-30 09:00:00", "2026-01-30 10:00:00", "Tech 2", "Signataire 2"),
("2026-01-30 10:00:00", "2026-01-30 11:00:00", "Tech 3", "Signataire 3"))));
edition.BasculerOptionCommand.Execute(edition.Options[2]);
Assert.Equal(1, edition.NombreSelectionnes);
Assert.True(edition.Options[2].EstSelectionne);
Assert.False(edition.Options[0].EstSelectionne);
Assert.False(edition.Options[1].EstSelectionne);
}
// =====================================================================
// Résumé utilisateur (jamais CheminComplet / donnée technique)
// =====================================================================
[Fact]
public void UnPhoto_Resume_NomPhotoPasChemin()
{
string chemin = "C:\\Documents_Demo\\402 ORCHIDEES\\chambre_16020067.jpg";
var edition = new RapportChampEdition(ChampUnique(
"$UNPHOTO.IMAGE$", "PHOTO_INTER", "cheminComplet",
chemin, PhotosJson(("chambre_16020067.jpg", chemin))));
// La donnée TECHNIQUE (maître IMAGE) reste le chemin serveur pour la génération…
Assert.Equal(chemin, edition.Valeur);
// …mais le RÉSUMÉ UI affiche le nom de fichier, jamais le chemin.
var resume = new RapportRubriqueResume(edition);
Assert.Equal("chambre_16020067.jpg", resume.Resume);
Assert.DoesNotContain("C:\\", resume.Resume);
Assert.DoesNotContain("cheminComplet", resume.Resume);
}
[Fact]
public void TabPhoto_Resume_CompteSansChemin()
{
var edition = new RapportChampEdition(Tableau(
"$TABPHOTO$", "PHOTO_INTER",
PhotosJson(
("chambre_16020067.jpg", "C:\\Documents_Demo\\chambre_16020067.jpg"),
("peinture_16020986.jpg", "C:\\Documents_Demo\\peinture_16020986.jpg"))));
edition.Options[0].EstSelectionne = true;
edition.Options[1].EstSelectionne = true;
var resume = new RapportRubriqueResume(edition);
Assert.Equal("2 photos sélectionnées", resume.Resume);
Assert.DoesNotContain("C:\\", resume.Resume);
}
[Fact]
public void UnCtsite_Resume_NomContact()
{
var edition = new RapportChampEdition(ChampUnique(
"$UNCTSITE.NOMPRENOM$", "CONTACT_SITE", "saNom",
"Renaud Oscar", ContactsJson("Renaud Oscar", "Alves Victoire")));
var resume = new RapportRubriqueResume(edition);
Assert.Equal("Renaud Oscar", resume.Resume);
}
[Fact]
public void UnRdv_Resume_FourchetteHoraire()
{
var edition = new RapportChampEdition(ChampUnique(
"$UNRDV.OBJET$", "RDV_INTER", "saObjet",
"Objet",
RdvJson(("2026-01-30 10:00:00", "2026-01-30 11:00:00", "Objet"))));
var resume = new RapportRubriqueResume(edition);
Assert.Equal("30/01/2026 10:00 → 30/01/2026 11:00", resume.Resume);
}
// =====================================================================
// Miniatures : notification des états (bug du premier affichage)
// =====================================================================
[Fact]
public void Miniature_Etats_NotifientLeRafraichissementUi()
{
TableauOptionEdition option = PhotoOption(Guid.NewGuid().ToString(), null);
var notifications = new List<string?>();
option.PropertyChanged += (_, e) => notifications.Add(e.PropertyName);
// LOADING : le placeholder doit se masquer (spinner) — la propriété calculée
// AffichePlaceholderMiniature est re-notifiée (sans quoi l'UI reste figée
// jusqu'à la navigation suivante).
option.EstChargementMiniature = true;
Assert.Contains("AffichePlaceholderMiniature", notifications);
// UNAVAILABLE : la propriété calculée est également re-notifiée.
notifications.Clear();
option.MiniatureIndisponible = true;
Assert.Contains("AffichePlaceholderMiniature", notifications);
// Fin de chargement (offline) : retour au placeholder.
notifications.Clear();
option.EstChargementMiniature = false;
Assert.Contains("AffichePlaceholderMiniature", notifications);
Assert.True(option.AffichePlaceholderMiniature);
Assert.False(option.AfficheMiniature);
}
// =====================================================================
// Chargement des miniatures pour UNPHOTO (non limité à AChoixMultiple)
// =====================================================================
[Fact]
public async Task UnPhoto_ChargerMiniaturesAsync_ChargeLesOptions()
{
var edition = new RapportChampEdition(ChampUnique(
"$UNPHOTO.IMAGE$", "PHOTO_INTER", "cheminComplet",
"C:\\Documents_Demo\\photo1.jpg",
PhotosJson(
("photo1.jpg", "C:\\Documents_Demo\\photo1.jpg"),
("photo2.jpg", "C:\\Documents_Demo\\photo2.jpg"))));
var service = new CompteurDocumentService();
await edition.ChargerMiniaturesAsync(service, CancellationToken.None);
Assert.Equal(2, service.EnsureDownloadedCalls);
Assert.All(edition.Options, o => Assert.True(o.EstPhoto));
}
private sealed class CompteurDocumentService : IDocumentLocalService
{
public int EnsureDownloadedCalls { get; private set; }
public Task<bool> EnsureDownloadedAsync(Guid documentGuid, CancellationToken ct = default)
{
EnsureDownloadedCalls++;
return Task.FromResult(false);
}
public Task<string?> GetCachedFilePathAsync(Guid documentGuid, CancellationToken ct = default)
=> Task.FromResult<string?>(null);
public Task<IReadOnlyList<InterventionDocumentItem>> GetDocumentsForInterventionAsync(Guid interventionGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<bool> OpenAsync(Guid documentGuid, CancellationToken ct = default) => throw new NotImplementedException();
public Task<IReadOnlyList<DocumentNoteItem>> GetNotesForDocumentAsync(Guid documentGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<DocumentNoteItem> AddNoteAsync(Guid documentGuid, string contenu, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<InterventionDocumentItem?> AddPhotoAsync(Guid interventionGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<AddPhotosResult> AddPhotosAsync(Guid interventionGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<InterventionDocumentItem?> AddPhotoFromCameraAsync(Guid interventionGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<InterventionDocumentItem?> AddDocumentAsync(Guid interventionGuid, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<InterventionDocumentItem?> AddAnnotatedPhotoAsync(Guid interventionGuid, Guid sourceDocumentGuid, byte[] pngBytes, string fileName, CancellationToken ct = default)
=> throw new NotImplementedException();
}
}