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; /// /// 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). /// 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(); 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 EnsureDownloadedAsync(Guid documentGuid, CancellationToken ct = default) { EnsureDownloadedCalls++; return Task.FromResult(false); } public Task GetCachedFilePathAsync(Guid documentGuid, CancellationToken ct = default) => Task.FromResult(null); public Task> GetDocumentsForInterventionAsync(Guid interventionGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task OpenAsync(Guid documentGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task> GetNotesForDocumentAsync(Guid documentGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task AddNoteAsync(Guid documentGuid, string contenu, CancellationToken ct = default) => throw new NotImplementedException(); public Task AddPhotoAsync(Guid interventionGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task AddPhotosAsync(Guid interventionGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task AddPhotoFromCameraAsync(Guid interventionGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task AddDocumentAsync(Guid interventionGuid, CancellationToken ct = default) => throw new NotImplementedException(); public Task AddAnnotatedPhotoAsync(Guid interventionGuid, Guid sourceDocumentGuid, byte[] pngBytes, string fileName, CancellationToken ct = default) => throw new NotImplementedException(); } }