696 lines
28 KiB
C#
696 lines
28 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
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.Common;
|
|
using Omika.Core.Contracts.Mobile.Sync;
|
|
using Omika.Core.Contracts.Planning;
|
|
|
|
|
|
namespace Omika.Tests.Mobile.Local;
|
|
|
|
/// <summary>
|
|
/// Tests du SuiviTempsCreateViewModel (formulaire de création offline-first).
|
|
/// Vérifie le chargement du référentiel TypeTemps via
|
|
/// <see cref="IParametreMetierLocalService"/> (Key = PA_CLE, aucun libellé
|
|
/// hardcodé), le préremplissage Intervention depuis le contexte, l'appel
|
|
/// exclusif à <see cref="ISuiviTempsLocalService.CreateAsync"/>, la protection
|
|
/// contre le double clic (TaskCompletionSource), l'absence de champ Valide et
|
|
/// l'absence de toute commande UPDATE / ARCHIVE / DELETE.
|
|
/// </summary>
|
|
public class SuiviTempsCreateViewModelTests
|
|
{
|
|
private sealed class FakeSuiviTempsService : ISuiviTempsLocalService
|
|
{
|
|
public int CreateCallCount { get; private set; }
|
|
public int CreateDraftCallCount { get; private set; }
|
|
public SuiviTempsSyncPayload? LastPayload { get; private set; }
|
|
public SuiviTempsDraftEntry? LastDraft { get; private set; }
|
|
public bool ThrowOnCreate { get; set; }
|
|
|
|
public Task<Guid> CreateAsync(SuiviTempsSyncPayload payload, CancellationToken ct = default)
|
|
{
|
|
CreateCallCount++;
|
|
LastPayload = payload;
|
|
|
|
if (ThrowOnCreate)
|
|
{
|
|
throw new InvalidOperationException("Erreur locale simulée");
|
|
}
|
|
|
|
return Task.FromResult(Guid.NewGuid());
|
|
}
|
|
|
|
public Task<SuiviTempsDto?> GetByGuidAsync(Guid suiviTempsGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<SuiviTempsDto?>(null);
|
|
|
|
public Task<IReadOnlyList<SuiviTempsDto>> GetAllAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<SuiviTempsDto>>([]);
|
|
|
|
public Task<IReadOnlyList<SuiviTempsDto>> GetByInterventionAsync(Guid interventionGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<SuiviTempsDto>>([]);
|
|
|
|
public Task<IReadOnlyList<SuiviTempsDto>> GetByRendezVousAsync(Guid rendezVousGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<SuiviTempsDto>>([]);
|
|
|
|
public Task<IReadOnlyList<SuiviTempsDraftEntry>> GetDraftsByRendezVousAsync(Guid rendezVousGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<SuiviTempsDraftEntry>>([]);
|
|
|
|
public Task CreateDraftAsync(SuiviTempsDraftEntry draft, CancellationToken ct = default)
|
|
{
|
|
CreateDraftCallCount++;
|
|
LastDraft = draft;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task DeleteDraftAsync(Guid rendezVousGuid, string typeTempsKey, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
|
|
public Task<bool> HasTypeTempsAsync(Guid rendezVousGuid, string typeTempsKey, CancellationToken ct = default)
|
|
=> Task.FromResult(false);
|
|
|
|
public Task<bool> IsValideAsync(Guid rendezVousGuid, CancellationToken ct = default)
|
|
=> Task.FromResult(false);
|
|
|
|
public Task ValidateRendezVousAsync(Guid rendezVousGuid, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
|
|
public Task SaveRendezVousPourBrouillonAsync(AgendaItem rendezVous, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousAvecBrouillonAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<AgendaItem>>([]);
|
|
|
|
public Task DeleteRendezVousPourBrouillonAsync(Guid rendezVousId, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fake local bloquant pour le test double Save : CreateAsync signale son
|
|
/// démarrage via <see cref="Started"/> puis attend la libération via
|
|
/// <see cref="Gate"/>. Les deux TaskCompletionSource utilisent
|
|
/// RunContinuationsAsynchronously pour éviter tout deadlock.
|
|
/// </summary>
|
|
private sealed class BlockingSuiviTempsLocalService : ISuiviTempsLocalService
|
|
{
|
|
public int CallCount { get; private set; }
|
|
public TaskCompletionSource<bool> Started { get; } =
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
public TaskCompletionSource<bool> Gate { get; } =
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
public Task<Guid> CreateAsync(SuiviTempsSyncPayload payload, CancellationToken ct = default)
|
|
{
|
|
CallCount++;
|
|
Started.TrySetResult(true);
|
|
return AwaitGateAsync(ct);
|
|
}
|
|
|
|
private async Task<Guid> AwaitGateAsync(CancellationToken ct)
|
|
{
|
|
await Gate.Task.WaitAsync(ct);
|
|
return Guid.NewGuid();
|
|
}
|
|
|
|
public void Release() => Gate.TrySetResult(true);
|
|
|
|
public Task<SuiviTempsDto?> GetByGuidAsync(Guid suiviTempsGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<SuiviTempsDto?>(null);
|
|
|
|
public Task<IReadOnlyList<SuiviTempsDto>> GetAllAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<SuiviTempsDto>>([]);
|
|
|
|
public Task<IReadOnlyList<SuiviTempsDto>> GetByInterventionAsync(Guid interventionGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<SuiviTempsDto>>([]);
|
|
|
|
public Task<IReadOnlyList<SuiviTempsDto>> GetByRendezVousAsync(Guid rendezVousGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<SuiviTempsDto>>([]);
|
|
|
|
public Task<IReadOnlyList<SuiviTempsDraftEntry>> GetDraftsByRendezVousAsync(Guid rendezVousGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<SuiviTempsDraftEntry>>([]);
|
|
|
|
public Task CreateDraftAsync(SuiviTempsDraftEntry draft, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
|
|
public Task DeleteDraftAsync(Guid rendezVousGuid, string typeTempsKey, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
|
|
public Task<bool> HasTypeTempsAsync(Guid rendezVousGuid, string typeTempsKey, CancellationToken ct = default)
|
|
=> Task.FromResult(false);
|
|
|
|
public Task<bool> IsValideAsync(Guid rendezVousGuid, CancellationToken ct = default)
|
|
=> Task.FromResult(false);
|
|
|
|
public Task ValidateRendezVousAsync(Guid rendezVousGuid, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
|
|
public Task SaveRendezVousPourBrouillonAsync(AgendaItem rendezVous, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousAvecBrouillonAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<AgendaItem>>([]);
|
|
|
|
public Task DeleteRendezVousPourBrouillonAsync(Guid rendezVousId, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
}
|
|
|
|
private sealed class FakeParametreMetierService : IParametreMetierLocalService
|
|
{
|
|
public IReadOnlyList<ReferenceValueDto> TypeTemps { get; set; } = [];
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetTypeTempsAsync(CancellationToken ct = default)
|
|
=> Task.FromResult(TypeTemps);
|
|
|
|
public Task<Omika.Core.Contracts.Configuration.TypeTempsParametresDto?> GetTypeTempsParametresAsync(string typeTempsKey, CancellationToken ct = default)
|
|
=> Task.FromResult<Omika.Core.Contracts.Configuration.TypeTempsParametresDto?>(null);
|
|
|
|
public Task<Omika.Core.Contracts.Configuration.TypeRdvParametresDto?> GetTypeRdvParametresAsync(string typeRdvKey, CancellationToken ct = default)
|
|
=> Task.FromResult<Omika.Core.Contracts.Configuration.TypeRdvParametresDto?>(null);
|
|
|
|
public Task<Omika.Core.Contracts.Configuration.StatutRdvParametresDto?> GetStatutRdvParametresAsync(string statutRdvKey, CancellationToken ct = default)
|
|
=> Task.FromResult<Omika.Core.Contracts.Configuration.StatutRdvParametresDto?>(null);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetInterventionTypesAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetInterventionEtatsAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetDossierTypesAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetDossierStatutsAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetClientTypesAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetClientStatutsAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetSiteTypesAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
public Task<IReadOnlyList<ReferenceValueDto>> GetTypeSignatairesAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
}
|
|
|
|
private sealed class FakeCollaborateurService : ICollaborateurLocalService
|
|
{
|
|
public IReadOnlyList<CollaborateurListItem> Collaborateurs { get; set; } = [];
|
|
|
|
public Task<IReadOnlyList<CollaborateurListItem>> GetCollaborateursAsync(CancellationToken ct = default)
|
|
=> Task.FromResult(Collaborateurs);
|
|
|
|
public Task<CollaborateurDetail?> GetCollaborateurByIdAsync(Guid collaborateurGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<CollaborateurDetail?>(null);
|
|
}
|
|
|
|
private sealed class FakeInterventionService : IInterventionLocalService
|
|
{
|
|
public IReadOnlyList<InterventionListItem> Interventions { get; set; } = [];
|
|
|
|
public Task<Guid> CreateAsync(InterventionSyncPayload payload, CancellationToken ct = default)
|
|
=> Task.FromResult(Guid.NewGuid());
|
|
|
|
public Task<Guid> UpdateAsync(Guid interventionGuid, InterventionSyncPayload payload, CancellationToken ct = default)
|
|
=> Task.FromResult(interventionGuid);
|
|
|
|
public Task<Guid> ArchiveAsync(Guid interventionGuid, CancellationToken ct = default)
|
|
=> Task.FromResult(interventionGuid);
|
|
|
|
public Task<InterventionDetail?> GetInterventionByIdAsync(Guid interventionGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<InterventionDetail?>(null);
|
|
|
|
public Task<IReadOnlyList<InterventionListItem>> GetInterventionsAsync(CancellationToken ct = default)
|
|
=> Task.FromResult(Interventions);
|
|
}
|
|
|
|
private sealed class FakeAgendaService : IAgendaService
|
|
{
|
|
public IReadOnlyList<AgendaItem> RendezVous { get; set; } = [];
|
|
public AgendaItem? ById { get; set; }
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousForDayAsync(DateTime date, CancellationToken ct = default)
|
|
=> Task.FromResult(RendezVous);
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousForRangeAsync(DateTime startInclusive, DateTime endExclusive, CancellationToken ct = default)
|
|
=> Task.FromResult(RendezVous);
|
|
|
|
public Task<AgendaItem?> GetRendezVousByIdAsync(Guid rendezVousGuid, CancellationToken ct = default, string? affectationId = null)
|
|
=> Task.FromResult(ById);
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousForCollaborateursAsync(IReadOnlyList<Guid> collaborateurGuids, DateTime startInclusive, DateTime endExclusive, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<AgendaItem>>(RendezVous);
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousForCollaborateurAsync(Guid collaborateurGuid, DateTime startInclusive, DateTime endExclusive, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<AgendaItem>>(RendezVous);
|
|
|
|
public Task EnrichSiteAccessAsync(AgendaItem item, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
}
|
|
|
|
private static ReferenceValueDto TypeTemps(string key, string label)
|
|
=> new() { Key = key, Label = label };
|
|
|
|
private static CollaborateurListItem Collaborateur(Guid guid, string name)
|
|
=> new() { CollaborateurGuid = guid, DisplayName = name };
|
|
|
|
private static InterventionListItem Intervention(Guid guid, string reference)
|
|
=> new() { InterventionGuid = guid, Reference = reference };
|
|
|
|
private static AgendaItem RendezVous(Guid guid, string titre)
|
|
=> new() { RendezVousGuid = guid, Titre = titre };
|
|
|
|
private readonly FakeSuiviTempsService _suiviTempsService = new();
|
|
private readonly FakeParametreMetierService _parametreMetierService = new();
|
|
private readonly FakeCollaborateurService _collaborateurService = new();
|
|
private readonly FakeInterventionService _interventionService = new();
|
|
private readonly FakeAgendaService _agendaService = new();
|
|
private readonly StubSessionService _sessionService = new();
|
|
private readonly NavigationService _navigation = new();
|
|
private readonly SuiviTempsCreateViewModel _vm;
|
|
|
|
public SuiviTempsCreateViewModelTests()
|
|
{
|
|
_vm = new SuiviTempsCreateViewModel(
|
|
_suiviTempsService,
|
|
_parametreMetierService,
|
|
_collaborateurService,
|
|
_interventionService,
|
|
_agendaService,
|
|
_sessionService,
|
|
_navigation,
|
|
new PermissionService());
|
|
}
|
|
|
|
private static SuiviTempsCreateNavigation CreateNavigation(Guid? interventionId = null, Guid? rendezVousId = null)
|
|
=> new(interventionId, rendezVousId, new NavigationContext(AppPage.Home, null));
|
|
|
|
[Fact]
|
|
public async Task Load_ChargeLesTypeTemps()
|
|
{
|
|
// Arrange
|
|
_parametreMetierService.TypeTemps =
|
|
[
|
|
TypeTemps("18", "Travail"),
|
|
TypeTemps("19", "Trajet")
|
|
];
|
|
|
|
// Act
|
|
await _vm.InitializeAsync(CreateNavigation());
|
|
|
|
// Assert
|
|
Assert.Equal(2, _vm.TypesTemps.Count);
|
|
Assert.False(_vm.HasNoTypeTemps);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TypeTemps_KeyEstLaCleMetier()
|
|
{
|
|
// Arrange
|
|
_parametreMetierService.TypeTemps = [TypeTemps("18", "Travail")];
|
|
|
|
// Act
|
|
await _vm.InitializeAsync(CreateNavigation());
|
|
_vm.SelectedTypeTemps = _vm.TypesTemps[0];
|
|
_vm.DureeMinutes = 60;
|
|
|
|
// Assert : la clé envoyée est la clé métier (PA_CLE), jamais le libellé.
|
|
await _vm.SaveCommand.ExecuteAsync(null);
|
|
Assert.Equal("18", _suiviTempsService.LastPayload?.TypeTempsKey);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AucunTypeTempsFictif()
|
|
{
|
|
// Arrange : aucun type de temps hardcodé dans le ViewModel.
|
|
_parametreMetierService.TypeTemps = [];
|
|
|
|
// Act
|
|
await _vm.InitializeAsync(CreateNavigation());
|
|
|
|
// Assert : la liste est vide, aucun libellé inventé.
|
|
Assert.Empty(_vm.TypesTemps);
|
|
Assert.True(_vm.HasNoTypeTemps);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TypeTempsVide_SaveImpossible()
|
|
{
|
|
// Arrange : référentiel vide.
|
|
_parametreMetierService.TypeTemps = [];
|
|
await _vm.InitializeAsync(CreateNavigation());
|
|
_vm.DureeMinutes = 60;
|
|
|
|
// Act
|
|
await _vm.SaveCommand.ExecuteAsync(null);
|
|
|
|
// Assert : aucun CreateAsync, message utilisateur cohérent.
|
|
Assert.Equal(0, _suiviTempsService.CreateCallCount);
|
|
Assert.NotNull(_vm.ErrorMessage);
|
|
Assert.Contains("type de temps", _vm.ErrorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Collaborateurs_Charges()
|
|
{
|
|
// Arrange
|
|
_collaborateurService.Collaborateurs =
|
|
[
|
|
Collaborateur(Guid.Parse("11111111-1111-1111-1111-111111111111"), "Jean Dupont")
|
|
];
|
|
|
|
// Act
|
|
await _vm.InitializeAsync(CreateNavigation());
|
|
|
|
// Assert
|
|
Assert.Single(_vm.Collaborateurs);
|
|
Assert.Equal("Jean Dupont", _vm.Collaborateurs[0].DisplayName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Interventions_Chargees()
|
|
{
|
|
// Arrange
|
|
_interventionService.Interventions =
|
|
[
|
|
Intervention(Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), "INT-001")
|
|
];
|
|
|
|
// Act
|
|
await _vm.InitializeAsync(CreateNavigation());
|
|
|
|
// Assert
|
|
Assert.Single(_vm.Interventions);
|
|
Assert.Equal("INT-001", _vm.Interventions[0].Reference);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RendezVous_Charges()
|
|
{
|
|
// Arrange
|
|
_agendaService.RendezVous =
|
|
[
|
|
RendezVous(Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), "RDV Client")
|
|
];
|
|
|
|
// Act
|
|
await _vm.InitializeAsync(CreateNavigation());
|
|
|
|
// Assert
|
|
Assert.Single(_vm.RendezVous);
|
|
Assert.Equal("RDV Client", _vm.RendezVous[0].Titre);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InterventionId_PrerempliDepuisContexte()
|
|
{
|
|
// Arrange
|
|
Guid interventionGuid = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
|
_interventionService.Interventions =
|
|
[
|
|
Intervention(interventionGuid, "INT-001"),
|
|
Intervention(Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"), "INT-002")
|
|
];
|
|
|
|
// Act
|
|
await _vm.InitializeAsync(CreateNavigation(interventionGuid));
|
|
|
|
// Assert : le sélecteur Intervention est prérempli (aucune inférence).
|
|
Assert.True(_vm.IsFilteredByIntervention);
|
|
Assert.NotNull(_vm.SelectedIntervention);
|
|
Assert.Equal(interventionGuid, _vm.SelectedIntervention!.InterventionGuid);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Save_AppelleCreateAsync()
|
|
{
|
|
// Arrange
|
|
_parametreMetierService.TypeTemps = [TypeTemps("18", "Travail")];
|
|
await _vm.InitializeAsync(CreateNavigation());
|
|
_vm.SelectedTypeTemps = _vm.TypesTemps[0];
|
|
_vm.DureeMinutes = 45;
|
|
|
|
// Act
|
|
await _vm.SaveCommand.ExecuteAsync(null);
|
|
|
|
// Assert
|
|
Assert.Equal(1, _suiviTempsService.CreateCallCount);
|
|
Assert.NotNull(_suiviTempsService.LastPayload);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Save_ValeursExactesPassees()
|
|
{
|
|
// Arrange
|
|
Guid collaborateurGuid = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
|
Guid interventionGuid = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
|
Guid rendezVousGuid = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
|
|
var date = new DateTime(2026, 8, 14, 10, 0, 0);
|
|
|
|
_parametreMetierService.TypeTemps = [TypeTemps("18", "Travail")];
|
|
_collaborateurService.Collaborateurs = [Collaborateur(collaborateurGuid, "Jean Dupont")];
|
|
_interventionService.Interventions = [Intervention(interventionGuid, "INT-001")];
|
|
_agendaService.RendezVous = [RendezVous(rendezVousGuid, "RDV Client")];
|
|
|
|
await _vm.InitializeAsync(CreateNavigation());
|
|
_vm.SelectedTypeTemps = _vm.TypesTemps[0];
|
|
_vm.SelectedCollaborateur = _vm.Collaborateurs[0];
|
|
_vm.SelectedIntervention = _vm.Interventions[0];
|
|
_vm.SelectedRendezVous = _vm.RendezVous[0];
|
|
_vm.Date = date;
|
|
_vm.DureeMinutes = 30;
|
|
_vm.Description = "Saisie de test";
|
|
|
|
// Act
|
|
await _vm.SaveCommand.ExecuteAsync(null);
|
|
|
|
// Assert : valeurs exactes transmises au service local.
|
|
var payload = _suiviTempsService.LastPayload;
|
|
Assert.NotNull(payload);
|
|
Assert.Equal(collaborateurGuid.ToString(), payload!.CollaborateurId);
|
|
Assert.Equal(interventionGuid.ToString(), payload.InterventionId);
|
|
Assert.Equal(rendezVousGuid.ToString(), payload.RendezVousId);
|
|
Assert.Equal(date, payload.Date);
|
|
Assert.Equal(30, payload.DureeMinutes);
|
|
Assert.Equal("18", payload.TypeTempsKey);
|
|
Assert.Equal("Saisie de test", payload.Description);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DureeMinutes_SaisieManuelle()
|
|
{
|
|
// Arrange : aucune durée calculée depuis un RendezVous ou une Intervention.
|
|
_parametreMetierService.TypeTemps = [TypeTemps("18", "Travail")];
|
|
_agendaService.RendezVous =
|
|
[
|
|
new AgendaItem
|
|
{
|
|
RendezVousGuid = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
|
Titre = "RDV 09:00-11:00",
|
|
Debut = new DateTime(2026, 8, 14, 9, 0, 0),
|
|
Fin = new DateTime(2026, 8, 14, 11, 0, 0)
|
|
}
|
|
];
|
|
|
|
await _vm.InitializeAsync(CreateNavigation());
|
|
_vm.SelectedTypeTemps = _vm.TypesTemps[0];
|
|
_vm.SelectedRendezVous = _vm.RendezVous[0];
|
|
_vm.DureeMinutes = 15; // saisie manuelle, pas 120 (différence RDV)
|
|
|
|
// Act
|
|
await _vm.SaveCommand.ExecuteAsync(null);
|
|
|
|
// Assert : la durée saisie manuellement est transmise telle quelle.
|
|
Assert.Equal(15, _suiviTempsService.LastPayload?.DureeMinutes);
|
|
}
|
|
|
|
[Fact]
|
|
public void Valide_NonExpose()
|
|
{
|
|
// Assert : le champ Valide n'est pas exposé par le formulaire.
|
|
Assert.Null(_vm.GetType().GetProperty("Valide"));
|
|
Assert.Null(_vm.GetType().GetProperty("IsValide"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Cancel_ZeroCreateAsync()
|
|
{
|
|
// Arrange
|
|
_parametreMetierService.TypeTemps = [TypeTemps("18", "Travail")];
|
|
await _vm.InitializeAsync(CreateNavigation());
|
|
|
|
// Act
|
|
_vm.CancelCommand.Execute(null);
|
|
|
|
// Assert : aucune mutation locale ni Outbox.
|
|
Assert.Equal(0, _suiviTempsService.CreateCallCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SaveCommand_PendantExecution_CanExecuteFalse_EtUneSeuleMutation()
|
|
{
|
|
// Arrange : fake local bloquant basé sur le comportement réel de la
|
|
// commande/UI. AsyncRelayCommand (AllowConcurrentExecutions=false) fait
|
|
// que CanExecute retourne false pendant que ExecutionTask est en cours,
|
|
// ce qui désactive le bouton Avalonia : aucun second Execute n'est possible.
|
|
var fake = new BlockingSuiviTempsLocalService();
|
|
var vm = new SuiviTempsCreateViewModel(
|
|
fake,
|
|
_parametreMetierService,
|
|
_collaborateurService,
|
|
_interventionService,
|
|
_agendaService,
|
|
_sessionService,
|
|
_navigation,
|
|
new PermissionService());
|
|
_parametreMetierService.TypeTemps = [TypeTemps("18", "Travail")];
|
|
await vm.InitializeAsync(CreateNavigation());
|
|
vm.SelectedTypeTemps = vm.TypesTemps[0];
|
|
vm.DureeMinutes = 30;
|
|
|
|
try
|
|
{
|
|
// Act : premier Save reste en cours (CreateAsync bloqué sur le gate).
|
|
var firstSave = vm.SaveCommand.ExecuteAsync(null);
|
|
|
|
// Attendre que le premier CreateAsync soit réellement démarré.
|
|
await fake.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
Assert.Equal(1, fake.CallCount);
|
|
Assert.True(vm.IsBusy);
|
|
|
|
// Comportement commande : le bouton Avalonia est désactivé pendant
|
|
// l'exécution (CanExecute == false).
|
|
Assert.False(vm.SaveCommand.CanExecute(null));
|
|
|
|
// Simulation d'un command source UI normal : le bloc n'est PAS exécuté
|
|
// puisque CanExecute == false. Aucun second Execute n'est déclenché.
|
|
if (vm.SaveCommand.CanExecute(null))
|
|
{
|
|
vm.SaveCommand.Execute(null);
|
|
}
|
|
|
|
Assert.Equal(1, fake.CallCount);
|
|
|
|
// Fin de la première exécution : libération du gate.
|
|
fake.Release();
|
|
|
|
await firstSave.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
Assert.Equal(1, fake.CallCount);
|
|
Assert.False(vm.IsBusy);
|
|
Assert.True(vm.SaveCommand.CanExecute(null));
|
|
}
|
|
finally
|
|
{
|
|
// Le gate DOIT toujours être libéré, même si une assertion échoue,
|
|
// afin qu'aucun firstSave ne reste suspendu.
|
|
fake.Release();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ErreurService_ErrorMessageEtPasDeNavigation()
|
|
{
|
|
|
|
// Arrange
|
|
_suiviTempsService.ThrowOnCreate = true;
|
|
_parametreMetierService.TypeTemps = [TypeTemps("18", "Travail")];
|
|
await _vm.InitializeAsync(CreateNavigation());
|
|
_vm.SelectedTypeTemps = _vm.TypesTemps[0];
|
|
_vm.DureeMinutes = 30;
|
|
|
|
// Act
|
|
await _vm.SaveCommand.ExecuteAsync(null);
|
|
|
|
// Assert : IsBusy revient false, ErrorMessage renseigné, pas d'exception brute.
|
|
Assert.False(_vm.IsBusy);
|
|
Assert.NotNull(_vm.ErrorMessage);
|
|
Assert.Contains("Erreur locale simulée", _vm.ErrorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public void AucuneCommandeDeMutation()
|
|
{
|
|
// Assert : le formulaire CREATE n'expose aucune commande UPDATE / ARCHIVE / DELETE.
|
|
Assert.Null(_vm.GetType().GetProperty("EditCommand"));
|
|
Assert.Null(_vm.GetType().GetProperty("ArchiveCommand"));
|
|
Assert.Null(_vm.GetType().GetProperty("DeleteCommand"));
|
|
Assert.Null(_vm.GetType().GetProperty("UpdateCommand"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ContexteRendezVous_RendezVousIdPrerempli()
|
|
{
|
|
// Arrange
|
|
Guid rendezVousGuid = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
|
|
_parametreMetierService.TypeTemps = [TypeTemps("18", "Travail")];
|
|
_agendaService.ById = new AgendaItem
|
|
{
|
|
RendezVousGuid = rendezVousGuid,
|
|
Titre = "RDV Client",
|
|
Debut = new DateTime(2026, 8, 14, 9, 0, 0),
|
|
Fin = new DateTime(2026, 8, 14, 10, 0, 0)
|
|
};
|
|
|
|
// Act : ouverture CREATE depuis un contexte RendezVous X.
|
|
await _vm.InitializeAsync(CreateNavigation(rendezVousId: rendezVousGuid));
|
|
|
|
// Assert : le sélecteur RendezVous est prérempli (aucune inférence).
|
|
Assert.True(_vm.IsRendezVousCentric);
|
|
Assert.True(_vm.IsFilteredByRendezVous);
|
|
Assert.NotNull(_vm.SelectedRendezVous);
|
|
Assert.Equal(rendezVousGuid, _vm.SelectedRendezVous!.RendezVousGuid);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ContexteRendezVous_PrefilleDateMaisPasInterventionNiDuree()
|
|
{
|
|
// Arrange : RendezVous X possède InterventionId=I, DateDebut=D1, DateFin=D2.
|
|
Guid rendezVousGuid = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
|
|
Guid interventionGuid = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
|
var dateDebut = new DateTime(2026, 8, 14, 9, 0, 0);
|
|
var dateFin = new DateTime(2026, 8, 14, 11, 0, 0);
|
|
|
|
_parametreMetierService.TypeTemps = [TypeTemps("18", "Travail")];
|
|
_interventionService.Interventions = [Intervention(interventionGuid, "INT-001")];
|
|
_agendaService.ById = new AgendaItem
|
|
{
|
|
RendezVousGuid = rendezVousGuid,
|
|
Titre = "RDV Client",
|
|
InterventionGuid = interventionGuid,
|
|
Debut = dateDebut,
|
|
Fin = dateFin
|
|
};
|
|
|
|
// Act : ouverture CREATE depuis un contexte RendezVous X.
|
|
await _vm.InitializeAsync(CreateNavigation(rendezVousId: rendezVousGuid));
|
|
|
|
// Assert : RendezVousId prérempli.
|
|
Assert.True(_vm.IsRendezVousCentric);
|
|
Assert.NotNull(_vm.SelectedRendezVous);
|
|
Assert.Equal(rendezVousGuid, _vm.SelectedRendezVous!.RendezVousGuid);
|
|
|
|
// Assert : la date est préremplie sur le début du rendez-vous.
|
|
Assert.Equal(new DateTimeOffset(dateDebut), _vm.Date);
|
|
|
|
// Assert : l'intervention n'est pas un sélecteur déduit (portée par le contexte).
|
|
Assert.Null(_vm.SelectedIntervention);
|
|
|
|
// Assert : la durée reste à saisir (non déduite de D1/D2 pour un type ajouté).
|
|
Assert.Equal(0, _vm.DureeMinutes);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|