581 lines
27 KiB
C#
581 lines
27 KiB
C#
using Omika.App.Local.Abstractions;
|
|
using Omika.Core.Contracts.Documents;
|
|
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.Common;
|
|
using Omika.Core.Contracts.Mobile.License;
|
|
using Omika.Core.Contracts.Mobile.Sync;
|
|
using Omika.Core.Contracts.Planning;
|
|
|
|
namespace Omika.Tests.Mobile.Local;
|
|
|
|
/// <summary>
|
|
/// Tests ciblés de la sélection de couleur d'accent de la page Paramètres.
|
|
/// Vérifie que le clic sur un preset transmet sa couleur à la commande,
|
|
/// met à jour <see cref="ParametresViewModel.SelectedPrimaryColor"/> et
|
|
/// délègue l'application/persistance à <see cref="ThemeService"/>.
|
|
/// </summary>
|
|
public class ParametresViewModelTests : LocalTestBase
|
|
{
|
|
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 sealed class FakeDocumentService : IDocumentLocalService
|
|
{
|
|
public Task<IReadOnlyList<InterventionDocumentItem>> GetDocumentsForInterventionAsync(
|
|
Guid interventionGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<InterventionDocumentItem>>([]);
|
|
|
|
public Task<bool> EnsureDownloadedAsync(Guid documentGuid, CancellationToken ct = default)
|
|
=> Task.FromResult(false);
|
|
|
|
public Task<bool> OpenAsync(Guid documentGuid, CancellationToken ct = default)
|
|
=> Task.FromResult(false);
|
|
|
|
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)
|
|
=> Task.FromResult<InterventionDocumentItem?>(null);
|
|
|
|
public Task<AddPhotosResult> AddPhotosAsync(Guid interventionGuid, CancellationToken ct = default)
|
|
=> Task.FromResult(new AddPhotosResult());
|
|
|
|
public Task<InterventionDocumentItem?> AddPhotoFromCameraAsync(Guid interventionGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<InterventionDocumentItem?>(null);
|
|
|
|
public Task<InterventionDocumentItem?> AddDocumentAsync(Guid interventionGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<InterventionDocumentItem?>(null);
|
|
|
|
public Task<InterventionDocumentItem?> AddAnnotatedPhotoAsync(Guid interventionGuid, Guid sourceDocumentGuid, byte[] pngBytes, string fileName, CancellationToken ct = default)
|
|
=> Task.FromResult<InterventionDocumentItem?>(null);
|
|
}
|
|
|
|
private sealed class FakeContactsService : IInterventionContactsService
|
|
{
|
|
public Task<IReadOnlyList<InterventionContact>> GetContactsForInterventionAsync(
|
|
Guid interventionGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<InterventionContact>>([]);
|
|
|
|
public Task<InterventionContact?> GetContactByIdAsync(Guid contactGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<InterventionContact?>(null);
|
|
|
|
public Task<IReadOnlyList<InterventionContact>> GetContactsDisponiblesAsync(Guid interventionGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<InterventionContact>>([]);
|
|
}
|
|
|
|
private sealed class FakeDossierService : IDossierLocalService
|
|
{
|
|
public Task<IReadOnlyList<DossierListItem>> GetDossiersAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<DossierListItem>>([]);
|
|
|
|
public Task<IReadOnlyList<DossierListItem>> SearchDossiersAsync(string? refInterne, string? titre, string? client, string? statut, string? type, string? refExterne, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<DossierListItem>>([]);
|
|
|
|
public Task<IReadOnlyList<DossierListItem>> GetLastSearchAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<DossierListItem>>([]);
|
|
|
|
public Task ClearLastSearchAsync(CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
|
|
public Task<DossierDetail?> GetDossierByIdAsync(Guid dossierGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<DossierDetail?>(null);
|
|
|
|
public Task SetPinnedAsync(Guid dossierGuid, bool isPinned, DossierListItem? item, CancellationToken ct = default)
|
|
=> Task.CompletedTask;
|
|
|
|
public Task<IReadOnlyList<DossierListItem>> GetPinnedAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<DossierListItem>>([]);
|
|
|
|
public Task<IReadOnlyList<DossierIntervention>> GetDossierInterventionsAsync(Guid dossierGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<DossierIntervention>>([]);
|
|
|
|
public Task<IReadOnlyList<DossierIntervention>> RefreshDossierInterventionsAsync(Guid dossierGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<DossierIntervention>>([]);
|
|
}
|
|
|
|
private sealed class FakeClientService : IClientLocalService
|
|
{
|
|
public Task<IReadOnlyList<ClientListItem>> GetClientsAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ClientListItem>>([]);
|
|
|
|
public Task<ClientDetail?> GetClientByIdAsync(Guid clientGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<ClientDetail?>(null);
|
|
}
|
|
|
|
private sealed class FakeSiteService : ISiteLocalService
|
|
{
|
|
public Task<IReadOnlyList<SiteListItem>> GetSitesAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<SiteListItem>>([]);
|
|
|
|
public Task<SiteDetail?> GetSiteByIdAsync(Guid siteGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<SiteDetail?>(null);
|
|
}
|
|
|
|
private sealed class FakeCollaborateurService : ICollaborateurLocalService
|
|
{
|
|
public Task<IReadOnlyList<CollaborateurListItem>> GetCollaborateursAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<CollaborateurListItem>>([]);
|
|
|
|
public Task<CollaborateurDetail?> GetCollaborateurByIdAsync(Guid collaborateurGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<CollaborateurDetail?>(null);
|
|
}
|
|
|
|
private sealed class FakeEquipeService : IEquipeLocalService
|
|
{
|
|
public Task<IReadOnlyList<EquipeListItem>> GetEquipesAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<EquipeListItem>>([]);
|
|
|
|
public Task<IReadOnlyList<EquipeListItem>> GetAllEquipesAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<EquipeListItem>>([]);
|
|
}
|
|
|
|
private sealed class FakeSuiviTempsService : ISuiviTempsLocalService
|
|
{
|
|
public Task<Guid> CreateAsync(SuiviTempsSyncPayload payload, CancellationToken ct = default)
|
|
=> 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)
|
|
=> 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 Task<IReadOnlyList<ReferenceValueDto>> GetTypeTempsAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<ReferenceValueDto>>([]);
|
|
|
|
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 FakePhoneLauncher : IPhoneLauncher
|
|
{
|
|
public Task<bool> DialAsync(string phoneNumber) => Task.FromResult(true);
|
|
}
|
|
|
|
private sealed class FakeEmailLauncher : IEmailLauncher
|
|
{
|
|
public string? LastEmail { get; private set; }
|
|
public IReadOnlyList<string>? LastAttachments { get; private set; }
|
|
public bool ComposeResult { get; set; } = true;
|
|
|
|
public Task<bool> ComposeAsync(string email)
|
|
{
|
|
LastEmail = email;
|
|
LastAttachments = null;
|
|
return Task.FromResult(ComposeResult);
|
|
}
|
|
|
|
public Task<bool> ComposeAsync(string email, string? subject, string? body, IReadOnlyList<string>? attachmentPaths)
|
|
{
|
|
LastEmail = email;
|
|
LastAttachments = attachmentPaths;
|
|
return Task.FromResult(ComposeResult);
|
|
}
|
|
}
|
|
|
|
private sealed class FakeSupportLogExporter : ISupportLogExporter
|
|
{
|
|
public string? ExportResult { get; set; }
|
|
|
|
public string? ExportLatestLogs() => ExportResult;
|
|
}
|
|
|
|
private sealed class FakeMapLauncher : IMapLauncher
|
|
{
|
|
public Task<bool> OpenItineraireAsync(string address) => Task.FromResult(true);
|
|
}
|
|
|
|
private sealed class FakeSessionService : IMobileSessionService
|
|
{
|
|
public Task<bool> HasValidSessionAsync(CancellationToken ct = default) => Task.FromResult(false);
|
|
public Task<bool> HasExpiredSessionAsync(CancellationToken ct = default) => Task.FromResult(false);
|
|
public Task<LicenseResponse> ActivateAsync(
|
|
string codeClient, string numeroLicence, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
public Task<LicenseResponse> ValidateAsync(CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
public Task<LocalBootstrapState?> GetBootstrapAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<LocalBootstrapState?>(null);
|
|
public Task<string?> GetAccessTokenAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<string?>(null);
|
|
public Task ClearSessionAsync(CancellationToken ct = default) => Task.CompletedTask;
|
|
public Task EnsureDevelopmentSessionAsync(CancellationToken ct = default) => Task.CompletedTask;
|
|
}
|
|
|
|
private readonly UiPreferencesRepository _preferencesRepository;
|
|
private readonly ThemeService _themeService;
|
|
private readonly FakeSessionService _sessionService = new();
|
|
private readonly FakeEmailLauncher _emailLauncher = new();
|
|
private readonly FakeSupportLogExporter _supportLogExporter = new();
|
|
private readonly ParametresViewModel _viewModel;
|
|
|
|
public ParametresViewModelTests()
|
|
{
|
|
_preferencesRepository = new UiPreferencesRepository(Database);
|
|
_themeService = new ThemeService(_preferencesRepository);
|
|
|
|
var navigation = new NavigationService();
|
|
var syncEngine = new FakeSyncEngine();
|
|
|
|
IAgendaService agendaService = new AgendaService(
|
|
RendezVousRepository, ClientRepository, SiteRepository, InterventionRepository, CollaborateurRepository, ApiClient, SessionService);
|
|
IInterventionLocalService interventionService = new InterventionLocalService(
|
|
InterventionRepository, ClientRepository, SiteRepository, DossierRepository,
|
|
RendezVousRepository, DocumentMetadataRepository, CollaborateurRepository,
|
|
OutboxService, ReconciliationService, ApiClient, InterventionCacheRepository, ContextInterventionCacheRepository, Database);
|
|
|
|
var sessionService = _sessionService;
|
|
var pauseDejeunerPreference = new PauseDejeunerPreferenceService(
|
|
PreferencesUtilisateurRepository, sessionService);
|
|
|
|
var pageFactory = new PageViewModelFactory(
|
|
_themeService, agendaService, interventionService, new FakeDocumentService(),
|
|
new FakeContactsService(), new FakeDossierService(), new FakeClientService(),
|
|
new FakeSiteService(), new FakeCollaborateurService(), new FakeEquipeService(),
|
|
new FakeSuiviTempsService(), new FakeParametreMetierService(), InterventionRepository, OutboxRepository, null!,
|
|
new FakePhoneLauncher(), new FakeEmailLauncher(), new FakeMapLauncher(), null!, navigation, sessionService, new PermissionService(),
|
|
syncEngine,
|
|
null!,
|
|
new FakePhotoPolicyProvider(),
|
|
pauseDejeunerPreference,
|
|
new RendezVousAnnulationLocalService(RendezVousRepository, OutboxService, new RendezVousStateNotifier()),
|
|
new RendezVousFinalisationLocalService(RendezVousRepository, new RendezVousStateNotifier()),
|
|
new CompteRenduScopeService(interventionService),
|
|
new RendezVousStateNotifier(),
|
|
null!,
|
|
Omika.Core.Observability.NullTelemetryService.Instance,
|
|
Network,
|
|
_langueSecondaire,
|
|
_languagePacks,
|
|
_traduction);
|
|
|
|
var shell = new ShellViewModel(navigation, _themeService, syncEngine, pageFactory, sessionService, new PermissionService(), OutboxService, null!);
|
|
pageFactory.SetShell(shell);
|
|
|
|
_viewModel = new ParametresViewModel(
|
|
_themeService, shell, sessionService, null!, new FakePhotoPolicyProvider(), pauseDejeunerPreference,
|
|
_emailLauncher, _supportLogExporter,
|
|
_langueSecondaire, _languagePacks, _traduction);
|
|
}
|
|
|
|
/// <summary>Préférence de langue secondaire simulée (aucun stockage réel).</summary>
|
|
private readonly FakeLangueSecondairePreferenceService _langueSecondaire = new();
|
|
|
|
/// <summary>Packs de langue simulés (aucun réseau, aucun fichier).</summary>
|
|
private readonly FakeLanguagePackService _languagePacks = new();
|
|
|
|
/// <summary>Traduction simulée : la disponibilité est pilotée par les tests.</summary>
|
|
private readonly FakeTranslationService _traduction = new();
|
|
|
|
[Fact]
|
|
public async Task ApplyPrimaryColorCommand_CouleurCliquee_EstAppliqueeEtPersistee()
|
|
{
|
|
// Arrange : couleur initiale = défaut du ThemeService.
|
|
Assert.Equal(ThemeService.DefaultPrimaryColor, _viewModel.SelectedPrimaryColor);
|
|
|
|
// Act : clic sur un preset (couleur transmise via CommandParameter).
|
|
await _viewModel.ApplyPrimaryColorCommand.ExecuteAsync("#2563eb");
|
|
|
|
// Assert : la couleur cliquée devient la sélection…
|
|
Assert.Equal("#2563eb", _viewModel.SelectedPrimaryColor);
|
|
// … est appliquée au ThemeService…
|
|
Assert.Equal("#2563eb", _themeService.PrimaryColor);
|
|
// … et persistée via UiPreferences.
|
|
UiPreferences saved = await _preferencesRepository.GetAsync();
|
|
Assert.Equal("#2563eb", saved.PrimaryColor);
|
|
|
|
// Le mode de thème n'est pas modifié.
|
|
Assert.Equal(ThemeMode.System, _viewModel.SelectedThemeMode);
|
|
Assert.Equal(ThemeMode.System, _themeService.CurrentMode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ApplyPrimaryColorCommand_DeuxCouleursSuccessives_LaDerniereGagne()
|
|
{
|
|
// Act : deux couleurs successives.
|
|
await _viewModel.ApplyPrimaryColorCommand.ExecuteAsync("#2563eb");
|
|
await _viewModel.ApplyPrimaryColorCommand.ExecuteAsync("#0d9488");
|
|
|
|
// Assert : la valeur finale est la dernière couleur appliquée.
|
|
Assert.Equal("#0d9488", _viewModel.SelectedPrimaryColor);
|
|
Assert.Equal("#0d9488", _themeService.PrimaryColor);
|
|
UiPreferences saved = await _preferencesRepository.GetAsync();
|
|
Assert.Equal("#0d9488", saved.PrimaryColor);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ApplyPrimaryColorCommand_ParametreNullOuVide_EstIgnoreSansEffet()
|
|
{
|
|
// Arrange : couleur initiale.
|
|
string initial = _viewModel.SelectedPrimaryColor;
|
|
|
|
// Act : paramètres invalides.
|
|
await _viewModel.ApplyPrimaryColorCommand.ExecuteAsync((string?)null);
|
|
await _viewModel.ApplyPrimaryColorCommand.ExecuteAsync(string.Empty);
|
|
await _viewModel.ApplyPrimaryColorCommand.ExecuteAsync(" ");
|
|
|
|
// Assert : aucun crash, aucun changement, aucune persistance invalide.
|
|
Assert.Equal(initial, _viewModel.SelectedPrimaryColor);
|
|
Assert.Equal(ThemeService.DefaultPrimaryColor, _themeService.PrimaryColor);
|
|
UiPreferences saved = await _preferencesRepository.GetAsync();
|
|
Assert.Equal(ThemeService.DefaultPrimaryColor, saved.PrimaryColor);
|
|
}
|
|
|
|
// ===== Pause déjeuner (préférence locale) =====
|
|
|
|
[Fact]
|
|
public async Task LoadPauseDejeuner_SansPreference_AfficheLesValeursParDefaut()
|
|
{
|
|
// Act
|
|
await _viewModel.LoadAsync();
|
|
|
|
// Assert : 12:00 → 13:00, aucun message d'erreur.
|
|
Assert.Equal(PauseDejeunerPreference.DebutParDefaut, _viewModel.PauseDejeunerDebut);
|
|
Assert.Equal(PauseDejeunerPreference.FinParDefaut, _viewModel.PauseDejeunerFin);
|
|
Assert.False(_viewModel.PauseDejeunerHasError);
|
|
Assert.Null(_viewModel.PauseDejeunerStatusText);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SavePauseDejeuner_EnregistreEtPersisteLocalement()
|
|
{
|
|
// Arrange
|
|
_viewModel.PauseDejeunerDebut = new TimeSpan(12, 15, 0);
|
|
_viewModel.PauseDejeunerFin = new TimeSpan(13, 15, 0);
|
|
|
|
// Act
|
|
await _viewModel.SavePauseDejeunerCommand.ExecuteAsync(null);
|
|
|
|
// Assert : confirmation affichée, aucune erreur…
|
|
Assert.False(_viewModel.PauseDejeunerHasError);
|
|
Assert.Equal("Horaires enregistrés.", _viewModel.PauseDejeunerStatusText);
|
|
|
|
// … et valeur réellement persistée dans SQLite (aucun appel réseau).
|
|
PauseDejeunerPreference? stockee = await PreferencesUtilisateurRepository.GetPauseDejeunerAsync(
|
|
PauseDejeunerPreferenceService.CleSansUtilisateur);
|
|
Assert.NotNull(stockee);
|
|
Assert.Equal(new TimeSpan(12, 15, 0), stockee!.Debut);
|
|
Assert.Equal(new TimeSpan(13, 15, 0), stockee.Fin);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SavePauseDejeuner_FinAnterieureAuDebut_RefuseEtNEnregistreRien()
|
|
{
|
|
// Arrange : 13:30 → 12:00, configuration incohérente.
|
|
_viewModel.PauseDejeunerDebut = new TimeSpan(13, 30, 0);
|
|
_viewModel.PauseDejeunerFin = new TimeSpan(12, 0, 0);
|
|
|
|
// Act
|
|
await _viewModel.SavePauseDejeunerCommand.ExecuteAsync(null);
|
|
|
|
// Assert : message clair, aucune écriture silencieuse.
|
|
Assert.True(_viewModel.PauseDejeunerHasError);
|
|
Assert.Equal(
|
|
"L'heure de fin doit être postérieure à l'heure de début.",
|
|
_viewModel.PauseDejeunerStatusText);
|
|
Assert.Null(await PreferencesUtilisateurRepository.GetPauseDejeunerAsync(
|
|
PauseDejeunerPreferenceService.CleSansUtilisateur));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SavePauseDejeuner_FinEgaleAuDebut_RefuseEtNEnregistreRien()
|
|
{
|
|
// Arrange : durée nulle, configuration incohérente.
|
|
_viewModel.PauseDejeunerDebut = new TimeSpan(12, 0, 0);
|
|
_viewModel.PauseDejeunerFin = new TimeSpan(12, 0, 0);
|
|
|
|
// Act
|
|
await _viewModel.SavePauseDejeunerCommand.ExecuteAsync(null);
|
|
|
|
// Assert
|
|
Assert.True(_viewModel.PauseDejeunerHasError);
|
|
Assert.Null(await PreferencesUtilisateurRepository.GetPauseDejeunerAsync(
|
|
PauseDejeunerPreferenceService.CleSansUtilisateur));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LoadPauseDejeuner_ApresEnregistrement_RestitueLesHoraires()
|
|
{
|
|
// Arrange : l'utilisateur enregistre 11:45 → 12:45.
|
|
_viewModel.PauseDejeunerDebut = new TimeSpan(11, 45, 0);
|
|
_viewModel.PauseDejeunerFin = new TimeSpan(12, 45, 0);
|
|
await _viewModel.SavePauseDejeunerCommand.ExecuteAsync(null);
|
|
|
|
// Act : réouverture de la page Paramètres (simule un redémarrage).
|
|
_viewModel.PauseDejeunerDebut = TimeSpan.Zero;
|
|
_viewModel.PauseDejeunerFin = TimeSpan.Zero;
|
|
await _viewModel.LoadAsync();
|
|
|
|
// Assert
|
|
Assert.Equal(new TimeSpan(11, 45, 0), _viewModel.PauseDejeunerDebut);
|
|
Assert.Equal(new TimeSpan(12, 45, 0), _viewModel.PauseDejeunerFin);
|
|
}
|
|
|
|
// ===== Masquage du numéro de licence (affichage uniquement) =====
|
|
|
|
[Theory]
|
|
[InlineData("fgts12345654sf", "fgts…54sf")]
|
|
[InlineData("12345678", "1234…5678")]
|
|
[InlineData("1234567", "1…7")]
|
|
[InlineData("12", "…")]
|
|
[InlineData("1", "…")]
|
|
[InlineData(null, null)]
|
|
[InlineData("", null)]
|
|
[InlineData(" ", null)]
|
|
public void MasquerNumeroLicence_MasqueSansModifierLaValeurReelle(string? input, string? expected)
|
|
{
|
|
// Le masquage est purement visuel : la valeur d'entrée n'est pas altérée,
|
|
// et la sortie ne contient jamais plus de 4 premiers + 4 derniers caractères.
|
|
Assert.Equal(expected, ParametresViewModel.MasquerNumeroLicence(input));
|
|
}
|
|
|
|
// ===== Support (email avec journaux de diagnostic en pièce jointe) =====
|
|
|
|
[Fact]
|
|
public async Task SendSupportEmail_SansAdresseSupport_NeComposePas()
|
|
{
|
|
// Arrange : aucune adresse de support (licence absente).
|
|
_viewModel.SupportEmail = null;
|
|
|
|
// Act
|
|
await _viewModel.SendSupportEmailCommand.ExecuteAsync(null);
|
|
|
|
// Assert : aucun lancement du client email, message explicite.
|
|
Assert.Null(_emailLauncher.LastEmail);
|
|
Assert.Equal("Aucune adresse de support configurée.", _viewModel.SupportStatusText);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendSupportEmail_AvecLogs_ComposeAvecPieceJointe()
|
|
{
|
|
// Arrange : adresse de support + journaux exportés dans un fichier .txt.
|
|
_viewModel.SupportEmail = "support@example.test";
|
|
_supportLogExporter.ExportResult = "/tmp/omika-support-logs.txt";
|
|
|
|
// Act
|
|
await _viewModel.SendSupportEmailCommand.ExecuteAsync(null);
|
|
|
|
// Assert : le destinataire est l'adresse de support et le fichier .txt
|
|
// est passé en pièce jointe.
|
|
Assert.Equal("support@example.test", _emailLauncher.LastEmail);
|
|
Assert.NotNull(_emailLauncher.LastAttachments);
|
|
Assert.Single(_emailLauncher.LastAttachments!);
|
|
Assert.Equal("/tmp/omika-support-logs.txt", _emailLauncher.LastAttachments![0]);
|
|
Assert.Null(_viewModel.SupportStatusText);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendSupportEmail_SansLogs_ComposeSansPieceJointe()
|
|
{
|
|
// Arrange : adresse de support mais aucun journal exportable.
|
|
_viewModel.SupportEmail = "support@example.test";
|
|
_supportLogExporter.ExportResult = null;
|
|
|
|
// Act
|
|
await _viewModel.SendSupportEmailCommand.ExecuteAsync(null);
|
|
|
|
// Assert : composeur ouvert vers le support, sans pièce jointe.
|
|
Assert.Equal("support@example.test", _emailLauncher.LastEmail);
|
|
Assert.Null(_emailLauncher.LastAttachments);
|
|
Assert.Null(_viewModel.SupportStatusText);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendSupportEmail_ComposeEchoue_AfficheUnMessage()
|
|
{
|
|
// Arrange : le client email refuse le lancement.
|
|
_viewModel.SupportEmail = "support@example.test";
|
|
_supportLogExporter.ExportResult = null;
|
|
_emailLauncher.ComposeResult = false;
|
|
|
|
// Act
|
|
await _viewModel.SendSupportEmailCommand.ExecuteAsync(null);
|
|
|
|
// Assert
|
|
Assert.Equal("Aucune application de messagerie disponible.", _viewModel.SupportStatusText);
|
|
}
|
|
}
|