726 lines
27 KiB
C#
726 lines
27 KiB
C#
using System.Text.Json;
|
|
using Omika.App.Local.Abstractions;
|
|
using Omika.App.Local.Models;
|
|
using Omika.App.Local.Repositories;
|
|
using Omika.App.Local.Services;
|
|
using Omika.App.Navigation;
|
|
using Omika.App.Services;
|
|
using Omika.App.ViewModels;
|
|
using Omika.Core.Contracts.Clients;
|
|
using Omika.Core.Contracts.Common;
|
|
using Omika.Core.Contracts.Documents;
|
|
using Omika.Core.Contracts.Interventions;
|
|
using Omika.Core.Contracts.Planning;
|
|
using Omika.Core.Contracts.Sites;
|
|
|
|
namespace Omika.Tests.Mobile.Local;
|
|
|
|
/// <summary>
|
|
/// Tests du ViewModel Fiche Intervention : chargement, état introuvable,
|
|
/// indicateurs de relations et navigation vers un rendez-vous lié.
|
|
/// </summary>
|
|
public class InterventionDetailViewModelTests : LocalTestBase
|
|
{
|
|
private readonly InterventionDetailViewModel _viewModel;
|
|
private readonly NavigationService _navigation;
|
|
private readonly FakePhoneLauncher _phoneLauncher = new();
|
|
private readonly FakeEmailLauncher _emailLauncher = new();
|
|
private readonly PermissionService _permissions = new();
|
|
|
|
private sealed class FakeContactsService : IInterventionContactsService
|
|
{
|
|
public Task<IReadOnlyList<InterventionContact>> GetContactsForInterventionAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<IReadOnlyList<InterventionContact>>([]);
|
|
|
|
public Task<InterventionContact?> GetContactByIdAsync(Guid contactGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<InterventionContact?>(null);
|
|
|
|
public Task<IReadOnlyList<InterventionContact>> GetContactsDisponiblesAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<IReadOnlyList<InterventionContact>>([]);
|
|
}
|
|
|
|
private sealed class FakePhoneLauncher : IPhoneLauncher
|
|
{
|
|
public string? LastNumber { get; private set; }
|
|
|
|
public Task<bool> DialAsync(string phoneNumber)
|
|
{
|
|
LastNumber = phoneNumber;
|
|
return Task.FromResult(true);
|
|
}
|
|
}
|
|
|
|
private sealed class FakeEmailLauncher : IEmailLauncher
|
|
{
|
|
public string? LastEmail { get; private set; }
|
|
|
|
public Task<bool> ComposeAsync(string email)
|
|
{
|
|
LastEmail = email;
|
|
return Task.FromResult(true);
|
|
}
|
|
}
|
|
|
|
private sealed class FakeDocumentService : IDocumentLocalService
|
|
{
|
|
public Task<IReadOnlyList<InterventionDocumentItem>> GetDocumentsForInterventionAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<IReadOnlyList<InterventionDocumentItem>>([]);
|
|
|
|
public Task<bool> EnsureDownloadedAsync(Guid documentGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(false);
|
|
|
|
public Task<bool> OpenAsync(Guid documentGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(false);
|
|
|
|
public Task<string?> GetCachedFilePathAsync(Guid documentGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<string?>(null);
|
|
|
|
public Task<IReadOnlyList<DocumentNoteItem>> GetNotesForDocumentAsync(Guid documentGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<IReadOnlyList<DocumentNoteItem>>([]);
|
|
|
|
public Task<DocumentNoteItem> AddNoteAsync(Guid documentGuid, string contenu, CancellationToken cancellationToken = default)
|
|
=> throw new NotSupportedException();
|
|
|
|
public Task<InterventionDocumentItem?> AddPhotoAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<InterventionDocumentItem?>(null);
|
|
|
|
public Task<AddPhotosResult> AddPhotosAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(new AddPhotosResult());
|
|
|
|
public Task<InterventionDocumentItem?> AddPhotoFromCameraAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<InterventionDocumentItem?>(null);
|
|
|
|
public Task<InterventionDocumentItem?> AddDocumentAsync(Guid interventionGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<InterventionDocumentItem?>(null);
|
|
|
|
public Task<InterventionDocumentItem?> AddAnnotatedPhotoAsync(Guid interventionGuid, Guid sourceDocumentGuid, byte[] pngBytes, string fileName, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<InterventionDocumentItem?>(null);
|
|
}
|
|
|
|
private sealed class FakeMapLauncher : IMapLauncher
|
|
{
|
|
public Task<bool> OpenItineraireAsync(string address) => Task.FromResult(true);
|
|
}
|
|
|
|
public InterventionDetailViewModelTests()
|
|
{
|
|
IInterventionLocalService interventionService = new InterventionLocalService(
|
|
InterventionRepository, ClientRepository, SiteRepository, DossierRepository,
|
|
RendezVousRepository, DocumentMetadataRepository, CollaborateurRepository,
|
|
OutboxService, ReconciliationService, ApiClient, InterventionCacheRepository, ContextInterventionCacheRepository, Database);
|
|
|
|
var clientService = new ClientLocalService(
|
|
ClientRepository, DossierRepository, ContextInterventionCacheRepository, ClientStateRepository, ApiClient);
|
|
|
|
_navigation = new NavigationService();
|
|
_viewModel = new InterventionDetailViewModel(
|
|
interventionService, OutboxRepository, new FakeContactsService(), clientService, new FakeDocumentService(), _phoneLauncher, _emailLauncher, _navigation, _permissions,
|
|
new RapportBrouillonRepository(Database), new FakeMapLauncher(), Network);
|
|
|
|
}
|
|
|
|
private static LocalClient CreateClient(
|
|
Guid id,
|
|
string nom,
|
|
string? code = null,
|
|
string? phone = null,
|
|
string? mobile = null,
|
|
string? email = null)
|
|
{
|
|
bool aDesCoordonnees = phone is not null || mobile is not null || email is not null;
|
|
var dto = new ClientDetailsDto
|
|
{
|
|
Id = id.ToString(),
|
|
Nom = nom,
|
|
Code = code,
|
|
Coordonnees = aDesCoordonnees
|
|
? new ContactDetailsDto { Phone = phone, Mobile = mobile, Email = email }
|
|
: null
|
|
};
|
|
return new LocalClient
|
|
{
|
|
Id = id,
|
|
PayloadJson = JsonSerializer.Serialize(dto),
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
};
|
|
}
|
|
|
|
private static LocalIntervention CreateIntervention(Guid id, string reference, Guid? clientId = null)
|
|
{
|
|
var dto = new InterventionDetailsDto
|
|
{
|
|
Id = id.ToString(),
|
|
Reference = reference,
|
|
Titre = "Intervention",
|
|
ClientId = clientId?.ToString(),
|
|
Etat = new ReferenceValueDto { Label = "En cours" }
|
|
};
|
|
return new LocalIntervention
|
|
{
|
|
Id = id,
|
|
ClientId = clientId,
|
|
PayloadJson = JsonSerializer.Serialize(dto),
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
};
|
|
}
|
|
|
|
private static LocalSite CreateSite(Guid id, string nom)
|
|
{
|
|
var dto = new SiteDetailsDto { Id = id.ToString(), Nom = nom };
|
|
return new LocalSite
|
|
{
|
|
Id = id,
|
|
PayloadJson = JsonSerializer.Serialize(dto),
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
};
|
|
}
|
|
|
|
private static LocalRendezVous CreateRendezVous(Guid id, Guid interventionId, string titre)
|
|
{
|
|
var dto = new RendezVousDetailsDto
|
|
{
|
|
Id = id.ToString(),
|
|
Titre = titre,
|
|
Debut = new DateTime(2026, 8, 12, 9, 0, 0),
|
|
Fin = new DateTime(2026, 8, 12, 10, 0, 0),
|
|
InterventionId = interventionId.ToString(),
|
|
Statut = new ReferenceValueDto { Label = "Planifié" }
|
|
};
|
|
return new LocalRendezVous
|
|
{
|
|
Id = id,
|
|
InterventionId = interventionId,
|
|
PayloadJson = JsonSerializer.Serialize(dto),
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
};
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_InterventionTrouvee_PeutLaFiche()
|
|
{
|
|
// Arrange
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
|
|
// Act
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Assert
|
|
Assert.False(_viewModel.IsLoading);
|
|
Assert.Null(_viewModel.ErrorMessage);
|
|
Assert.NotNull(_viewModel.Intervention);
|
|
Assert.Equal("INT-001", _viewModel.Intervention.Reference);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_InterventionIntrouvable_MessageErreur()
|
|
{
|
|
// Act
|
|
await _viewModel.LoadCommand.ExecuteAsync(Guid.NewGuid());
|
|
|
|
// Assert
|
|
Assert.False(_viewModel.IsLoading);
|
|
Assert.NotNull(_viewModel.ErrorMessage);
|
|
Assert.Null(_viewModel.Intervention);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_AvecClient_HasClientVrai()
|
|
{
|
|
// Arrange
|
|
Guid clientId = Guid.NewGuid();
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
|
|
|
|
// Act
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Assert
|
|
Assert.True(_viewModel.HasClient);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_SansClient_HasClientFaux()
|
|
{
|
|
// Arrange
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
|
|
// Act
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Assert
|
|
Assert.False(_viewModel.HasClient);
|
|
Assert.False(_viewModel.HasSite);
|
|
Assert.False(_viewModel.HasDossier);
|
|
Assert.False(_viewModel.HasRendezVous);
|
|
Assert.False(_viewModel.HasDocuments);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_AvecSuiviPar_ExposeAppelEtEmailPersonnel()
|
|
{
|
|
// Arrange
|
|
Guid interventionId = Guid.NewGuid();
|
|
var dto = new InterventionDetailsDto
|
|
{
|
|
Id = interventionId.ToString(),
|
|
Reference = "INT-001",
|
|
Titre = "Intervention",
|
|
SuiviParNom = "Dupont",
|
|
SuiviParMobile = "0612345678",
|
|
SuiviParEmail = "dupont@example.com"
|
|
};
|
|
await InterventionRepository.UpsertAsync(new LocalIntervention
|
|
{
|
|
Id = interventionId,
|
|
PayloadJson = JsonSerializer.Serialize(dto),
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
});
|
|
|
|
// Act
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Assert : les boutons sont visibles et déclenchent le composeur système
|
|
// (appel vers le mobile + email via la messagerie personnelle).
|
|
Assert.True(_viewModel.HasSuiviParPhone);
|
|
Assert.True(_viewModel.HasSuiviParEmail);
|
|
|
|
await _viewModel.CallSuiviParCommand.ExecuteAsync(null);
|
|
Assert.Equal("0612345678", _phoneLauncher.LastNumber);
|
|
|
|
await _viewModel.EmailSuiviParCommand.ExecuteAsync(null);
|
|
Assert.Equal("dupont@example.com", _emailLauncher.LastEmail);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_AvecRendezVous_HasRendezVousVrai()
|
|
{
|
|
// Arrange
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
await RendezVousRepository.UpsertAsync(CreateRendezVous(Guid.NewGuid(), interventionId, "RDV"));
|
|
|
|
// Act
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Assert
|
|
Assert.True(_viewModel.HasRendezVous);
|
|
Assert.Single(_viewModel.Intervention!.RendezVous);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenRendezVous_NavigueVersDetail()
|
|
{
|
|
// Arrange
|
|
Guid interventionId = Guid.NewGuid();
|
|
Guid rdvId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
await RendezVousRepository.UpsertAsync(CreateRendezVous(rdvId, interventionId, "RDV"));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Act
|
|
_viewModel.OpenRendezVousCommand.Execute(_viewModel.Intervention!.RendezVous[0]);
|
|
|
|
// Assert : navigation vers le détail du rendez-vous avec le GUID public et le retour vers la fiche Intervention.
|
|
Assert.Equal(AppPage.RendezVousDetail, _navigation.CurrentPage);
|
|
var nav = Assert.IsType<DetailNavigation>(_navigation.CurrentParameter);
|
|
Assert.Equal(rdvId, nav.Id);
|
|
Assert.Equal(AppPage.InterventionDetail, nav.ReturnContext.Page);
|
|
Assert.Equal(interventionId, nav.ReturnContext.Parameter);
|
|
}
|
|
|
|
|
|
[Fact]
|
|
public async Task OpenRendezVous_Null_NeNaviguePas()
|
|
{
|
|
// Act
|
|
_viewModel.OpenRendezVousCommand.Execute(null);
|
|
|
|
// Assert
|
|
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Back_RetourneALAgenda()
|
|
{
|
|
// Act
|
|
_viewModel.BackCommand.Execute(null);
|
|
|
|
// Assert
|
|
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenDocuments_NavigueVersDocuments()
|
|
{
|
|
// Arrange
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Act
|
|
_viewModel.OpenDocumentsCommand.Execute(null);
|
|
|
|
// Assert : navigation vers la page « Contenus associés », onglet Documents.
|
|
Assert.Equal(AppPage.InterventionResources, _navigation.CurrentPage);
|
|
var nav = Assert.IsType<InterventionResourceNavigation>(_navigation.CurrentParameter);
|
|
Assert.Equal(interventionId, nav.InterventionGuid);
|
|
Assert.Equal(1, nav.TabIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenContacts_NavigueVersContacts()
|
|
{
|
|
// Arrange
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Act
|
|
_viewModel.OpenContactsCommand.Execute(null);
|
|
|
|
// Assert : navigation vers la page Contacts de l'intervention avec le GUID public.
|
|
Assert.Equal(AppPage.InterventionContacts, _navigation.CurrentPage);
|
|
Assert.Equal(interventionId, _navigation.CurrentParameter);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenContacts_InterventionNull_NeNaviguePas()
|
|
{
|
|
// Act
|
|
_viewModel.OpenContactsCommand.Execute(null);
|
|
|
|
// Assert
|
|
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_EnLigne_CartesCourrielsEtRapportsAccessibles()
|
|
{
|
|
// Arrange
|
|
Network.Online = true;
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
|
|
// Act
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Assert : en ligne, les deux cartes restent utilisables.
|
|
Assert.False(_viewModel.IsOffline);
|
|
Assert.True(_viewModel.PeutOuvrirCourriels);
|
|
Assert.True(_viewModel.PeutOuvrirRapports);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_HorsLigne_DesactiveLesCartesCourrielsEtRapports()
|
|
{
|
|
// Arrange
|
|
Network.Online = false;
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
|
|
// Act
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Assert : hors ligne les cartes restent VISIBLES (aucun masquage) mais
|
|
// sont désactivées : courriels et rapports passent par le serveur.
|
|
Assert.True(_viewModel.IsOffline);
|
|
Assert.False(_viewModel.PeutOuvrirCourriels);
|
|
Assert.False(_viewModel.PeutOuvrirRapports);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Messagerie EXTERNE : l'action « écrire un courriel » à un contact ouvre
|
|
/// DIRECTEMENT l'application de messagerie du téléphone, sans passer par le
|
|
/// composeur interne (le serveur ne lui expose d'ailleurs aucune configuration
|
|
/// SMTP).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task EmailContact_MessagerieExterne_OuvreLApplicationDeMessagerie()
|
|
{
|
|
// Arrange : droit d'envoi présent, mais messagerie externe.
|
|
_permissions.SetRights(new Dictionary<string, bool>
|
|
{
|
|
[Omika.Core.Constants.MobilePermissionKeys.EnvoiEmail] = true
|
|
});
|
|
_permissions.SetMessagerieExterne(true);
|
|
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
var contact = new InterventionContact { Email = "contact@exemple.test", DisplayName = "Contact" };
|
|
|
|
// Act
|
|
await _viewModel.EmailContactCommand.ExecuteAsync(contact);
|
|
|
|
// Assert : composeur externe utilisé, aucune navigation vers l'écran interne.
|
|
Assert.Equal("contact@exemple.test", _emailLauncher.LastEmail);
|
|
Assert.NotEqual(AppPage.NouveauEmail, _navigation.CurrentPage);
|
|
Assert.False(_viewModel.PeutEnvoyerEmail);
|
|
Assert.True(_viewModel.PeutEcrireCourriel);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenEmails_EnLigne_NavigueVersLesCourriels() {
|
|
// Arrange
|
|
Network.Online = true;
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Act
|
|
await _viewModel.OpenEmailsCommand.ExecuteAsync(null);
|
|
|
|
// Assert
|
|
Assert.Equal(AppPage.InterventionEmails, _navigation.CurrentPage);
|
|
Assert.Equal(interventionId, _navigation.CurrentParameter);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenEmails_CoupureApresChargement_NeNaviguePas()
|
|
{
|
|
// Arrange : la fiche est chargée en ligne puis le réseau est perdu.
|
|
Network.Online = true;
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
Network.Online = false;
|
|
|
|
// Act
|
|
await _viewModel.OpenEmailsCommand.ExecuteAsync(null);
|
|
|
|
// Assert : aucun écran inutilisable ouvert, l'état hors ligne est affiché
|
|
// (bandeau + cartes désactivées).
|
|
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
|
|
Assert.True(_viewModel.IsOffline);
|
|
Assert.False(_viewModel.PeutOuvrirCourriels);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenRapport_EnLigne_NavigueVersLesRapports()
|
|
{
|
|
// Arrange
|
|
Network.Online = true;
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Act
|
|
await _viewModel.OpenRapportCommand.ExecuteAsync(null);
|
|
|
|
// Assert
|
|
Assert.Equal(AppPage.RapportChamps, _navigation.CurrentPage);
|
|
Assert.Equal(interventionId, _navigation.CurrentParameter);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenRapport_CoupureApresChargement_NeNaviguePas()
|
|
{
|
|
// Arrange
|
|
Network.Online = true;
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
Network.Online = false;
|
|
|
|
// Act
|
|
await _viewModel.OpenRapportCommand.ExecuteAsync(null);
|
|
|
|
// Assert
|
|
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
|
|
Assert.True(_viewModel.IsOffline);
|
|
Assert.False(_viewModel.PeutOuvrirRapports);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_AvecClient_AfficheLeBlocClient()
|
|
{
|
|
// Arrange : client lié présent dans le snapshot local.
|
|
Guid clientId = Guid.NewGuid();
|
|
Guid interventionId = Guid.NewGuid();
|
|
await ClientRepository.UpsertAsync(CreateClient(clientId, "Boulangerie Martin", "CLI-042", "0240000000"));
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
|
|
|
|
// Act
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Assert : le bloc Client est affiché et porte les informations de base.
|
|
Assert.True(_viewModel.HasClient);
|
|
Assert.NotNull(_viewModel.Client);
|
|
Assert.Equal("Boulangerie Martin", _viewModel.ClientNomAffiche);
|
|
Assert.Equal("CLI-042", _viewModel.Client.Code);
|
|
Assert.True(_viewModel.Client.HasPhone);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_SansClient_BlocClientMasque()
|
|
{
|
|
// Arrange
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
|
|
// Act
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Assert : aucun bloc Client (ni bouton) sans client lié.
|
|
Assert.False(_viewModel.HasClient);
|
|
Assert.Null(_viewModel.Client);
|
|
Assert.Null(_viewModel.ClientNomAffiche);
|
|
Assert.False(_viewModel.PeutOuvrirClient);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenClient_NavigueVersLaFicheClient()
|
|
{
|
|
// Arrange
|
|
Guid clientId = Guid.NewGuid();
|
|
Guid interventionId = Guid.NewGuid();
|
|
await ClientRepository.UpsertAsync(CreateClient(clientId, "Boulangerie Martin"));
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Act
|
|
_viewModel.OpenClientCommand.Execute(null);
|
|
|
|
// Assert : navigation vers la fiche du client lié (GUID public).
|
|
Assert.Equal(AppPage.ClientDetail, _navigation.CurrentPage);
|
|
Assert.Equal(clientId, _navigation.CurrentParameter);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenClient_SansClient_NeNaviguePas()
|
|
{
|
|
// Arrange
|
|
Guid interventionId = Guid.NewGuid();
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001"));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Act
|
|
_viewModel.OpenClientCommand.Execute(null);
|
|
|
|
// Assert
|
|
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_ClientLie_SansDroitVoirClients_BoutonFicheClientMasque()
|
|
{
|
|
// Arrange : client lié mais aucun droit A.CLIENT.VOIR.
|
|
_permissions.SetRights(new Dictionary<string, bool>
|
|
{
|
|
[Omika.Core.Constants.MobilePermissionKeys.EnvoiEmail] = true
|
|
});
|
|
|
|
Guid clientId = Guid.NewGuid();
|
|
Guid interventionId = Guid.NewGuid();
|
|
await ClientRepository.UpsertAsync(CreateClient(clientId, "Boulangerie Martin"));
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
|
|
|
|
// Act
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Assert : le bloc reste visible (nom du client) mais l'ouverture est masquée,
|
|
// comme pour la fiche du lieu.
|
|
Assert.True(_viewModel.HasClient);
|
|
Assert.False(_viewModel.PeutOuvrirClient);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Les boutons de la ligne client se comportent comme ceux d'une ligne de
|
|
/// contact : appel vers le mobile (prioritaire sur le fixe), courriel via le
|
|
/// composeur interne ou la messagerie du téléphone.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CallClient_AppelleLeMobileDuClient()
|
|
{
|
|
// Arrange : le mobile est prioritaire sur le fixe.
|
|
Guid clientId = Guid.NewGuid();
|
|
Guid interventionId = Guid.NewGuid();
|
|
await ClientRepository.UpsertAsync(
|
|
CreateClient(clientId, "Boulangerie Martin", phone: "0240000000", mobile: "0612345678"));
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Act
|
|
await _viewModel.CallClientCommand.ExecuteAsync(null);
|
|
|
|
// Assert
|
|
Assert.Equal("0612345678", _phoneLauncher.LastNumber);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CallClient_SansNumero_NAppellePas()
|
|
{
|
|
// Arrange : client lié mais sans aucun numéro.
|
|
Guid clientId = Guid.NewGuid();
|
|
Guid interventionId = Guid.NewGuid();
|
|
await ClientRepository.UpsertAsync(CreateClient(clientId, "Boulangerie Martin"));
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Act
|
|
await _viewModel.CallClientCommand.ExecuteAsync(null);
|
|
|
|
// Assert : aucun appel composé et aucune erreur affichée.
|
|
Assert.Null(_phoneLauncher.LastNumber);
|
|
Assert.Null(_viewModel.ErrorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EmailClient_MessagerieInterne_OuvreLeComposeurAvecLAdresseDuClient()
|
|
{
|
|
// Arrange : messagerie interne + droit d'envoi.
|
|
_permissions.SetRights(new Dictionary<string, bool>
|
|
{
|
|
[Omika.Core.Constants.MobilePermissionKeys.EnvoiEmail] = true
|
|
});
|
|
|
|
Guid clientId = Guid.NewGuid();
|
|
Guid interventionId = Guid.NewGuid();
|
|
await ClientRepository.UpsertAsync(
|
|
CreateClient(clientId, "Boulangerie Martin", email: "contact@boulangerie.test"));
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Act
|
|
await _viewModel.EmailClientCommand.ExecuteAsync(null);
|
|
|
|
// Assert : composeur interne pré-rempli avec l'adresse du client.
|
|
Assert.Equal(AppPage.NouveauEmail, _navigation.CurrentPage);
|
|
var nav = Assert.IsType<NouveauEmailContactNavigation>(_navigation.CurrentParameter);
|
|
Assert.Equal(interventionId, nav.InterventionGuid);
|
|
Assert.Equal("contact@boulangerie.test", nav.DestinataireEmail);
|
|
Assert.Null(_emailLauncher.LastEmail);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EmailClient_MessagerieExterne_OuvreLApplicationDeMessagerie()
|
|
{
|
|
// Arrange : messagerie EXTERNE (application du téléphone).
|
|
_permissions.SetRights(new Dictionary<string, bool>
|
|
{
|
|
[Omika.Core.Constants.MobilePermissionKeys.EnvoiEmail] = true
|
|
});
|
|
_permissions.SetMessagerieExterne(true);
|
|
|
|
Guid clientId = Guid.NewGuid();
|
|
Guid interventionId = Guid.NewGuid();
|
|
await ClientRepository.UpsertAsync(
|
|
CreateClient(clientId, "Boulangerie Martin", email: "contact@boulangerie.test"));
|
|
await InterventionRepository.UpsertAsync(CreateIntervention(interventionId, "INT-001", clientId));
|
|
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
|
|
|
// Act
|
|
await _viewModel.EmailClientCommand.ExecuteAsync(null);
|
|
|
|
// Assert : ouverture directe de la messagerie, sans écran interne.
|
|
Assert.Equal("contact@boulangerie.test", _emailLauncher.LastEmail);
|
|
Assert.NotEqual(AppPage.NouveauEmail, _navigation.CurrentPage);
|
|
}
|
|
}
|
|
|
|
|
|
|