Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user