304 lines
12 KiB
C#
304 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Headless;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using Omika.App.Local.Abstractions;
|
|
using Omika.App.Local.Models;
|
|
using Omika.App.Navigation;
|
|
using Omika.App.Services;
|
|
using Omika.App.ViewModels;
|
|
using Omika.App.Views;
|
|
using Omika.Core.Contracts.Documents;
|
|
using Omika.Core.Contracts.Mobile.Sync;
|
|
using Xunit;
|
|
|
|
namespace Omika.Tests.Mobile;
|
|
|
|
/// <summary>
|
|
/// Écran de composition d'un courriel en MESSAGERIE EXTERNE : le choix de
|
|
/// configuration SMTP disparaît (l'envoi ne passe pas par Omika), une explication
|
|
/// le remplace, et le panneau « Le courriel a-t-il été envoyé ? » (Oui / Non)
|
|
/// apparaît après le retour de l'application de messagerie.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Session headless partagée au niveau du processus (localisateur Avalonia global) :
|
|
/// la classe rejoint la collection « ApplicationRuntimeContext ».
|
|
/// </remarks>
|
|
[Collection("ApplicationRuntimeContext")]
|
|
public sealed class NouveauEmailViewExterneTests : IDisposable
|
|
{
|
|
private readonly HeadlessUnitTestSession _session =
|
|
HeadlessUnitTestSession.StartNew(typeof(LayoutTestAppBuilder));
|
|
|
|
public void Dispose() => _session.Dispose();
|
|
|
|
private void Executer(Action scenario)
|
|
=> _session.Dispatch(
|
|
() =>
|
|
{
|
|
scenario();
|
|
return true;
|
|
},
|
|
CancellationToken.None).GetAwaiter().GetResult();
|
|
|
|
/// <summary>
|
|
/// Messagerie externe : la configuration SMTP est MASQUÉE et l'explication
|
|
/// affichée (l'utilisateur doit comprendre que l'envoi se fera ailleurs).
|
|
/// </summary>
|
|
[Fact]
|
|
public void MessagerieExterne_MasqueLaConfigurationSmtpEtExplique()
|
|
=> Executer(() =>
|
|
{
|
|
NouveauEmailView vue = Ouvrir(messagerieExterne: true);
|
|
Window fenetre = Fenetre(vue);
|
|
try
|
|
{
|
|
ComboBox? configuration = vue.FindControl<ComboBox>("ConfigurationSmtp");
|
|
Assert.NotNull(configuration);
|
|
Assert.False(
|
|
configuration!.IsEffectivelyVisible,
|
|
"La configuration SMTP ne doit pas être proposée en messagerie externe.");
|
|
|
|
TextBlock explication = vue.GetVisualDescendants()
|
|
.OfType<TextBlock>()
|
|
.Single(t => t.Text?.StartsWith("Messagerie externe", StringComparison.Ordinal) == true);
|
|
Assert.True(explication.IsEffectivelyVisible, "L'explication de la messagerie externe n'est pas affichée.");
|
|
}
|
|
finally
|
|
{
|
|
fenetre.Close();
|
|
}
|
|
});
|
|
|
|
/// <summary>Messagerie interne (Back-Office) : rien ne change, la configuration reste proposée.</summary>
|
|
[Fact]
|
|
public void MessagerieInterne_GardeLaConfigurationSmtp()
|
|
=> Executer(() =>
|
|
{
|
|
NouveauEmailView vue = Ouvrir(messagerieExterne: false);
|
|
Window fenetre = Fenetre(vue);
|
|
try
|
|
{
|
|
ComboBox? configuration = vue.FindControl<ComboBox>("ConfigurationSmtp");
|
|
Assert.NotNull(configuration);
|
|
Assert.True(configuration!.IsEffectivelyVisible);
|
|
|
|
TextBlock? explication = vue.GetVisualDescendants()
|
|
.OfType<TextBlock>()
|
|
.FirstOrDefault(t => t.Text?.StartsWith("Messagerie externe", StringComparison.Ordinal) == true);
|
|
Assert.True(explication is null || !explication.IsEffectivelyVisible);
|
|
}
|
|
finally
|
|
{
|
|
fenetre.Close();
|
|
}
|
|
});
|
|
|
|
/// <summary>
|
|
/// La question n'est PAS affichée avant l'envoi, puis apparaît avec ses deux
|
|
/// réponses « Oui » (enregistre comme envoyé) et « Non » (conserve en brouillon).
|
|
/// </summary>
|
|
[Fact]
|
|
public void QuestionEnvoiExterne_SafficheApresLeRetourAvecOuiEtNon()
|
|
=> Executer(() =>
|
|
{
|
|
NouveauEmailView vue = Ouvrir(messagerieExterne: true);
|
|
Window fenetre = Fenetre(vue);
|
|
try
|
|
{
|
|
var modele = (NouveauEmailViewModel)vue.DataContext!;
|
|
|
|
Button oui = Bouton(vue, "Oui");
|
|
Button non = Bouton(vue, "Non");
|
|
|
|
Assert.False(
|
|
oui.IsEffectivelyVisible,
|
|
"La question ne doit pas apparaître avant l'ouverture de la messagerie.");
|
|
|
|
modele.AfficherQuestionEnvoiExterne = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
Assert.True(oui.IsEffectivelyVisible, "Le bouton « Oui » n'est pas affiché.");
|
|
Assert.True(non.IsEffectivelyVisible, "Le bouton « Non » n'est pas affiché.");
|
|
Assert.NotNull(oui.Command);
|
|
Assert.NotNull(non.Command);
|
|
Assert.NotSame(oui.Command, non.Command);
|
|
}
|
|
finally
|
|
{
|
|
fenetre.Close();
|
|
}
|
|
});
|
|
|
|
// =====================================================================
|
|
// Aides
|
|
// =====================================================================
|
|
|
|
private static Button Bouton(Control vue, string libelle)
|
|
=> vue.GetVisualDescendants()
|
|
.OfType<Button>()
|
|
.Single(b => b.Content is string texte && texte == libelle);
|
|
|
|
/// <summary>
|
|
/// Construit la vue réelle avec un ViewModel minimal : seule la préférence de
|
|
/// messagerie influence les éléments testés (aucun appel réseau n'est déclenché).
|
|
/// </summary>
|
|
private static NouveauEmailView Ouvrir(bool messagerieExterne)
|
|
{
|
|
var modele = new NouveauEmailViewModel(
|
|
new FakeInterventionService(),
|
|
new FakeContactsService(),
|
|
new FakeDocumentService(),
|
|
new NavigationService(),
|
|
new FakeSyncEngine(),
|
|
Omika.Core.Observability.NullTelemetryService.Instance,
|
|
new FakeEmailLauncher(),
|
|
new FakeAppRetourService(),
|
|
new FakePermissionService(messagerieExterne));
|
|
|
|
// Étape 2 : c'est elle qui contient le bloc d'envoi (configuration SMTP /
|
|
// explication de la messagerie externe).
|
|
modele.Etape = 2;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
return new NouveauEmailView { DataContext = modele };
|
|
}
|
|
|
|
private static Window Fenetre(Control contenu)
|
|
{
|
|
var fenetre = new Window
|
|
{
|
|
Width = 400,
|
|
Height = 900,
|
|
RequestedThemeVariant = Avalonia.Styling.ThemeVariant.Light,
|
|
Content = contenu
|
|
};
|
|
|
|
fenetre.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
Dispatcher.UIThread.RunJobs();
|
|
return fenetre;
|
|
}
|
|
|
|
// =====================================================================
|
|
// Fakes
|
|
// =====================================================================
|
|
|
|
private sealed class FakePermissionService : IPermissionService
|
|
{
|
|
public FakePermissionService(bool messagerieExterne) => MessagerieExterneValue = messagerieExterne;
|
|
|
|
private bool MessagerieExterneValue { get; }
|
|
|
|
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 => MessagerieExterneValue;
|
|
public void SetMessagerieExterne(bool externe) { }
|
|
public bool PeutRechercherInterventions => true;
|
|
public bool VoirRdvSansRestriction => true;
|
|
public bool PeutVoirAgendaEquipe => true;
|
|
public bool PeutEnvoyerEmail => !MessagerieExterneValue;
|
|
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 FakeEmailLauncher : IEmailLauncher
|
|
{
|
|
public Task<bool> ComposeAsync(string email) => Task.FromResult(true);
|
|
}
|
|
|
|
private sealed class FakeAppRetourService : IAppRetourService
|
|
{
|
|
public IAppRetourAttente Armer() => new RetourImmediat();
|
|
|
|
private sealed class RetourImmediat : IAppRetourAttente
|
|
{
|
|
public Task AttendreAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
public void Dispose() { }
|
|
}
|
|
}
|
|
|
|
private sealed class FakeInterventionService : IInterventionLocalService
|
|
{
|
|
public Task<Guid> CreateAsync(InterventionSyncPayload payload, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<Guid> UpdateAsync(Guid interventionGuid, InterventionSyncPayload payload, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<Guid> ArchiveAsync(Guid interventionGuid, CancellationToken ct = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<InterventionDetail?> GetInterventionByIdAsync(Guid interventionGuid, CancellationToken ct = default)
|
|
=> Task.FromResult<InterventionDetail?>(null);
|
|
|
|
public Task<IReadOnlyList<InterventionListItem>> GetInterventionsAsync(CancellationToken ct = default)
|
|
=> Task.FromResult<IReadOnlyList<InterventionListItem>>([]);
|
|
}
|
|
|
|
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 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(true);
|
|
public Task<bool> OpenAsync(Guid documentGuid, CancellationToken ct = default) => Task.FromResult(true);
|
|
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 FakeSyncEngine : ISyncEngine
|
|
{
|
|
public Task<SyncCycleResult> SyncAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(new SyncCycleResult());
|
|
|
|
public Task<SyncCycleResult> PushAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(new SyncCycleResult());
|
|
}
|
|
}
|