Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
using Omika.App.Local.Abstractions;
|
||||
using Omika.App.Local.Models;
|
||||
using Omika.App.Navigation;
|
||||
using Omika.App.Services;
|
||||
using Omika.App.ViewModels;
|
||||
using Omika.Core.Contracts.Documents;
|
||||
|
||||
namespace Omika.Tests.Mobile.Local;
|
||||
|
||||
/// <summary>
|
||||
/// Tests du ViewModel Documents / Photos : chargement local, états vide/erreur,
|
||||
/// téléchargement à la demande, ouverture, ajout photo/document et navigation.
|
||||
/// </summary>
|
||||
public class InterventionDocumentsViewModelTests
|
||||
{
|
||||
private sealed class FakeDocumentService : IDocumentLocalService
|
||||
{
|
||||
public IReadOnlyList<InterventionDocumentItem> ItemsToReturn { get; set; } = [];
|
||||
public bool ThrowOnLoad { get; set; }
|
||||
public bool DownloadResult { get; set; } = true;
|
||||
public int DownloadCalls { get; private set; }
|
||||
public bool OpenResult { get; set; } = true;
|
||||
public int OpenCalls { get; private set; }
|
||||
public InterventionDocumentItem? PhotoToReturn { get; set; }
|
||||
public InterventionDocumentItem? DocumentToReturn { get; set; }
|
||||
public AddPhotosResult PhotosBatchToReturn { get; set; } = new();
|
||||
public InterventionDocumentItem? CameraPhotoToReturn { get; set; }
|
||||
public Exception? CameraExceptionToThrow { get; set; }
|
||||
public int AddPhotosCalls { get; private set; }
|
||||
|
||||
public Task<IReadOnlyList<InterventionDocumentItem>> GetDocumentsForInterventionAsync(
|
||||
Guid interventionGuid, CancellationToken ct = default)
|
||||
{
|
||||
if (ThrowOnLoad)
|
||||
{
|
||||
throw new InvalidOperationException("Erreur de chargement");
|
||||
}
|
||||
return Task.FromResult(ItemsToReturn);
|
||||
}
|
||||
|
||||
public Task<bool> EnsureDownloadedAsync(Guid documentGuid, CancellationToken ct = default)
|
||||
{
|
||||
DownloadCalls++;
|
||||
return Task.FromResult(DownloadResult);
|
||||
}
|
||||
|
||||
public Task<bool> OpenAsync(Guid documentGuid, CancellationToken ct = default)
|
||||
{
|
||||
OpenCalls++;
|
||||
return Task.FromResult(OpenResult);
|
||||
}
|
||||
|
||||
public Task<string?> GetCachedFilePathAsync(Guid documentGuid, CancellationToken ct = default)
|
||||
=> Task.FromResult<string?>(null);
|
||||
|
||||
public Task<IReadOnlyList<DocumentNoteItem>> GetNotesForDocumentAsync(Guid documentGuid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<DocumentNoteItem>>([]);
|
||||
|
||||
public Task<DocumentNoteItem> AddNoteAsync(Guid documentGuid, string contenu, CancellationToken ct = default)
|
||||
=> Task.FromResult(new DocumentNoteItem());
|
||||
|
||||
public Task<InterventionDocumentItem?> AddPhotoAsync(Guid interventionGuid, CancellationToken ct = default)
|
||||
{
|
||||
if (PhotoToReturn is not null)
|
||||
{
|
||||
var items = ItemsToReturn.ToList();
|
||||
items.Add(PhotoToReturn);
|
||||
ItemsToReturn = items;
|
||||
}
|
||||
return Task.FromResult(PhotoToReturn);
|
||||
}
|
||||
|
||||
public Task<InterventionDocumentItem?> AddDocumentAsync(Guid interventionGuid, CancellationToken ct = default)
|
||||
{
|
||||
if (DocumentToReturn is not null)
|
||||
{
|
||||
var items = ItemsToReturn.ToList();
|
||||
items.Add(DocumentToReturn);
|
||||
ItemsToReturn = items;
|
||||
}
|
||||
return Task.FromResult(DocumentToReturn);
|
||||
}
|
||||
|
||||
public Task<AddPhotosResult> AddPhotosAsync(Guid interventionGuid, CancellationToken ct = default)
|
||||
{
|
||||
AddPhotosCalls++;
|
||||
foreach (InterventionDocumentItem item in PhotosBatchToReturn.Added)
|
||||
{
|
||||
var items = ItemsToReturn.ToList();
|
||||
items.Add(item);
|
||||
ItemsToReturn = items;
|
||||
}
|
||||
return Task.FromResult(PhotosBatchToReturn);
|
||||
}
|
||||
|
||||
public Task<InterventionDocumentItem?> AddPhotoFromCameraAsync(Guid interventionGuid, CancellationToken ct = default)
|
||||
{
|
||||
if (CameraExceptionToThrow is not null)
|
||||
{
|
||||
return Task.FromException<InterventionDocumentItem?>(CameraExceptionToThrow);
|
||||
}
|
||||
|
||||
if (CameraPhotoToReturn is not null)
|
||||
{
|
||||
var items = ItemsToReturn.ToList();
|
||||
items.Add(CameraPhotoToReturn);
|
||||
ItemsToReturn = items;
|
||||
}
|
||||
return Task.FromResult(CameraPhotoToReturn);
|
||||
}
|
||||
|
||||
public Task<InterventionDocumentItem?> AddAnnotatedPhotoAsync(Guid interventionGuid, Guid sourceDocumentGuid, byte[] pngBytes, string fileName, CancellationToken ct = default)
|
||||
=> Task.FromResult<InterventionDocumentItem?>(null);
|
||||
}
|
||||
|
||||
private static InterventionDocumentItem CreateItem(Guid id, string nom, bool isPhoto = false, bool isCached = false)
|
||||
=> new()
|
||||
{
|
||||
DocumentGuid = id,
|
||||
Nom = nom,
|
||||
Titre = nom,
|
||||
IsPhoto = isPhoto,
|
||||
IsCached = isCached
|
||||
};
|
||||
|
||||
private sealed class PermissivePermissionService : IPermissionService
|
||||
{
|
||||
public IReadOnlyDictionary<string, bool> Rights => new Dictionary<string, bool>();
|
||||
public void SetRights(IReadOnlyDictionary<string, bool> rights) { }
|
||||
public bool HasRight(string key) => true;
|
||||
public bool MessagerieExterne => false;
|
||||
public void SetMessagerieExterne(bool externe) { }
|
||||
public bool PeutRechercherInterventions => true;
|
||||
public bool VoirRdvSansRestriction => true;
|
||||
public bool PeutVoirAgendaEquipe => true;
|
||||
public bool PeutEnvoyerEmail => true;
|
||||
public bool PeutComposerEmail => true;
|
||||
public bool PeutCreerRapport => true;
|
||||
public bool PeutGererLicence => true;
|
||||
public bool PeutSaisirTempsAutrui => true;
|
||||
public bool PeutVoirNotesAdmin => true;
|
||||
public bool PeutVoirClients => true;
|
||||
public bool PeutVoirSites => true;
|
||||
public bool PeutVoirDossiers => true;
|
||||
}
|
||||
|
||||
private sealed class FakeSyncEngine : ISyncEngine
|
||||
{
|
||||
public Task<SyncCycleResult> SyncAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult(new SyncCycleResult { Executed = true });
|
||||
|
||||
public Task<SyncCycleResult> PushAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new SyncCycleResult { Executed = true });
|
||||
}
|
||||
|
||||
private readonly FakeDocumentService _service = new();
|
||||
private readonly NavigationService _navigation = new();
|
||||
private readonly InterventionDocumentsViewModel _viewModel;
|
||||
|
||||
public InterventionDocumentsViewModelTests()
|
||||
{
|
||||
_viewModel = new InterventionDocumentsViewModel(
|
||||
_service, _navigation, new PermissivePermissionService(), new FakeSyncEngine());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_ChargeLesDocuments()
|
||||
{
|
||||
// Arrange
|
||||
Guid interventionId = Guid.NewGuid();
|
||||
Guid docId = Guid.NewGuid();
|
||||
_service.ItemsToReturn = [CreateItem(docId, "doc.pdf")];
|
||||
|
||||
// Act
|
||||
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
||||
|
||||
// Assert
|
||||
Assert.False(_viewModel.IsLoading);
|
||||
Assert.Null(_viewModel.ErrorMessage);
|
||||
Assert.Single(_viewModel.Documents);
|
||||
Assert.Equal(docId, _viewModel.Documents[0].DocumentGuid);
|
||||
Assert.True(_viewModel.HasDocuments);
|
||||
Assert.False(_viewModel.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_AucunDocument_IsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
_service.ItemsToReturn = [];
|
||||
|
||||
// Act
|
||||
await _viewModel.LoadCommand.ExecuteAsync(Guid.NewGuid());
|
||||
|
||||
// Assert
|
||||
Assert.Empty(_viewModel.Documents);
|
||||
Assert.False(_viewModel.HasDocuments);
|
||||
Assert.True(_viewModel.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_Erreur_MessageAffiché()
|
||||
{
|
||||
// Arrange
|
||||
_service.ThrowOnLoad = true;
|
||||
|
||||
// Act
|
||||
await _viewModel.LoadCommand.ExecuteAsync(Guid.NewGuid());
|
||||
|
||||
// Assert
|
||||
Assert.False(_viewModel.IsLoading);
|
||||
Assert.NotNull(_viewModel.ErrorMessage);
|
||||
Assert.Empty(_viewModel.Documents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Download_AppelleLeService()
|
||||
{
|
||||
// Arrange
|
||||
Guid interventionId = Guid.NewGuid();
|
||||
Guid docId = Guid.NewGuid();
|
||||
var item = CreateItem(docId, "doc.pdf", isCached: false);
|
||||
_service.ItemsToReturn = [item];
|
||||
|
||||
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
||||
|
||||
// Act
|
||||
await _viewModel.DownloadCommand.ExecuteAsync(item);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, _service.DownloadCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Download_DejaEnCache_NeReappellePas()
|
||||
{
|
||||
// Arrange
|
||||
Guid interventionId = Guid.NewGuid();
|
||||
Guid docId = Guid.NewGuid();
|
||||
var item = CreateItem(docId, "doc.pdf", isCached: true);
|
||||
_service.ItemsToReturn = [item];
|
||||
|
||||
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
||||
|
||||
// Act
|
||||
await _viewModel.DownloadCommand.ExecuteAsync(item);
|
||||
|
||||
// Assert : déjà en cache, aucun appel de téléchargement.
|
||||
Assert.Equal(0, _service.DownloadCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Open_AppelleLeService()
|
||||
{
|
||||
// Arrange
|
||||
Guid interventionId = Guid.NewGuid();
|
||||
Guid docId = Guid.NewGuid();
|
||||
var item = CreateItem(docId, "doc.pdf", isCached: true);
|
||||
_service.ItemsToReturn = [item];
|
||||
|
||||
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
||||
|
||||
// Act
|
||||
await _viewModel.OpenCommand.ExecuteAsync(item);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, _service.OpenCalls);
|
||||
Assert.Null(_viewModel.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Open_NonDisponible_MessageErreur()
|
||||
{
|
||||
// Arrange
|
||||
Guid interventionId = Guid.NewGuid();
|
||||
Guid docId = Guid.NewGuid();
|
||||
var item = CreateItem(docId, "doc.pdf", isCached: false);
|
||||
_service.ItemsToReturn = [item];
|
||||
_service.OpenResult = false;
|
||||
|
||||
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
||||
|
||||
// Act
|
||||
await _viewModel.OpenCommand.ExecuteAsync(item);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, _service.OpenCalls);
|
||||
Assert.NotNull(_viewModel.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddPhoto_AjouteALaListe()
|
||||
{
|
||||
// Arrange
|
||||
Guid interventionId = Guid.NewGuid();
|
||||
var photo = CreateItem(Guid.NewGuid(), "photo.jpg", isPhoto: true, isCached: true);
|
||||
_service.PhotosBatchToReturn = new AddPhotosResult { Added = [photo] };
|
||||
|
||||
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
||||
|
||||
// Act
|
||||
await _viewModel.AddPhotoCommand.ExecuteAsync(null);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(_viewModel.Documents, d => d.DocumentGuid == photo.DocumentGuid);
|
||||
Assert.True(_viewModel.HasDocuments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddDocument_AjouteALaListe()
|
||||
{
|
||||
// Arrange
|
||||
Guid interventionId = Guid.NewGuid();
|
||||
var doc = CreateItem(Guid.NewGuid(), "doc.pdf", isCached: true);
|
||||
_service.DocumentToReturn = doc;
|
||||
|
||||
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
||||
|
||||
// Act
|
||||
await _viewModel.AddDocumentCommand.ExecuteAsync(null);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(_viewModel.Documents, d => d.DocumentGuid == doc.DocumentGuid);
|
||||
Assert.True(_viewModel.HasDocuments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddPhoto_Annule_NeRienAjoute()
|
||||
{
|
||||
// Arrange : sélection multiple annulée → lot vide (Added vide, 0 échec).
|
||||
Guid interventionId = Guid.NewGuid();
|
||||
_service.PhotosBatchToReturn = new AddPhotosResult();
|
||||
|
||||
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
||||
|
||||
// Act
|
||||
await _viewModel.AddPhotoCommand.ExecuteAsync(null);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(_viewModel.Documents);
|
||||
Assert.Null(_viewModel.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddPhoto_Multiple_ToutesAjoutees()
|
||||
{
|
||||
// Arrange : 3 photos sélectionnées.
|
||||
Guid interventionId = Guid.NewGuid();
|
||||
var p1 = CreateItem(Guid.NewGuid(), "p1.jpg", isPhoto: true, isCached: true);
|
||||
var p2 = CreateItem(Guid.NewGuid(), "p2.jpg", isPhoto: true, isCached: true);
|
||||
var p3 = CreateItem(Guid.NewGuid(), "p3.jpg", isPhoto: true, isCached: true);
|
||||
_service.PhotosBatchToReturn = new AddPhotosResult { Added = [p1, p2, p3] };
|
||||
|
||||
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
||||
|
||||
// Act
|
||||
await _viewModel.AddPhotoCommand.ExecuteAsync(null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, _viewModel.Documents.Count);
|
||||
Assert.Null(_viewModel.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddPhoto_Partiel_MessageErreurEtSuccesConserves()
|
||||
{
|
||||
// Arrange : 2 succès + 1 échec.
|
||||
Guid interventionId = Guid.NewGuid();
|
||||
var p1 = CreateItem(Guid.NewGuid(), "p1.jpg", isPhoto: true, isCached: true);
|
||||
var p2 = CreateItem(Guid.NewGuid(), "p2.jpg", isPhoto: true, isCached: true);
|
||||
_service.PhotosBatchToReturn = new AddPhotosResult { Added = [p1, p2], FailedCount = 1 };
|
||||
|
||||
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
||||
|
||||
// Act
|
||||
await _viewModel.AddPhotoCommand.ExecuteAsync(null);
|
||||
|
||||
// Assert : les 2 succès sont conservés + message utilisateur cohérent.
|
||||
Assert.Equal(2, _viewModel.Documents.Count);
|
||||
Assert.NotNull(_viewModel.ErrorMessage);
|
||||
Assert.Contains("2 images ajoutées", _viewModel.ErrorMessage);
|
||||
Assert.Contains("1 image n'a pas pu être ajoutée", _viewModel.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddPhotoFromCamera_Annule_AucunAjout()
|
||||
{
|
||||
// Arrange
|
||||
Guid interventionId = Guid.NewGuid();
|
||||
_service.CameraPhotoToReturn = null;
|
||||
|
||||
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
||||
|
||||
// Act
|
||||
await _viewModel.AddPhotoFromCameraCommand.ExecuteAsync(null);
|
||||
|
||||
// Assert : aucun document, aucune erreur.
|
||||
Assert.Empty(_viewModel.Documents);
|
||||
Assert.Null(_viewModel.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddPhotoFromCamera_Erreur_MessageAffiché()
|
||||
{
|
||||
// Arrange : échec du launcher caméra → exception contrôlée.
|
||||
Guid interventionId = Guid.NewGuid();
|
||||
_service.CameraExceptionToThrow = new PhotoCaptureException("Caméra indisponible.");
|
||||
|
||||
await _viewModel.LoadCommand.ExecuteAsync(interventionId);
|
||||
|
||||
// Act
|
||||
await _viewModel.AddPhotoFromCameraCommand.ExecuteAsync(null);
|
||||
|
||||
// Assert : l'application ne plante pas, message utilisateur affiché.
|
||||
Assert.Empty(_viewModel.Documents);
|
||||
Assert.Equal("Caméra indisponible.", _viewModel.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Back_RetourneALaFicheIntervention()
|
||||
{
|
||||
// Arrange
|
||||
Guid interventionId = Guid.NewGuid();
|
||||
_viewModel.InterventionGuid = interventionId;
|
||||
|
||||
// Act
|
||||
_viewModel.BackCommand.Execute(null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AppPage.InterventionDetail, _navigation.CurrentPage);
|
||||
Assert.Equal(interventionId, _navigation.CurrentParameter);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user