Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
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.ViewModels;
|
||||
using Omika.App.Views;
|
||||
using Omika.Core.Contracts.Documents;
|
||||
using Omika.Core.Contracts.Mobile.Sync;
|
||||
using Xunit;
|
||||
|
||||
namespace Omika.Tests.Mobile;
|
||||
|
||||
/// <summary>
|
||||
/// Vue de composition d'un courriel : la liste des destinataires affiche l'ORIGINE
|
||||
/// de chaque contact (« Intervention » ou « Téléphone »), afin qu'un contact
|
||||
/// personnel du carnet d'adresses ne soit jamais confondu avec un contact de
|
||||
/// l'intervention. Le texte d'aide annonce les deux sources.
|
||||
/// </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 NouveauEmailViewContactsTests : 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>
|
||||
/// Un contact du TÉLÉPHONE affiche son origine : le badge « Téléphone » est
|
||||
/// visible et le badge « Intervention » ne l'est pas.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ContactTelephone_AfficheLEBadgeTelephone()
|
||||
=> Executer(() =>
|
||||
{
|
||||
NouveauEmailView vue = Ouvrir();
|
||||
Window fenetre = RendreOption(vue, new EmailContactOption("Zoé Martin", "zoe@perso.fr", OrigineContactEmail.Telephone));
|
||||
try
|
||||
{
|
||||
Assert.True(Badge(fenetre, "Téléphone"), "Le badge d'origine « Téléphone » doit être visible.");
|
||||
Assert.False(
|
||||
Badge(fenetre, "Intervention"),
|
||||
"Le badge « Intervention » ne doit pas être affiché pour un contact du téléphone.");
|
||||
Assert.True(
|
||||
Texte(fenetre, "Zoé Martin — zoe@perso.fr"),
|
||||
"Le nom du contact et son adresse email doivent rester affichés.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
fenetre.Close();
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>Un contact de l'INTERVENTION affiche l'origine « Intervention ».</summary>
|
||||
[Fact]
|
||||
public void ContactIntervention_AfficheLEBadgeIntervention()
|
||||
=> Executer(() =>
|
||||
{
|
||||
NouveauEmailView vue = Ouvrir();
|
||||
Window fenetre = RendreOption(vue, new EmailContactOption("Alice Dupont", "alice@test.fr", OrigineContactEmail.Intervention));
|
||||
try
|
||||
{
|
||||
Assert.True(Badge(fenetre, "Intervention"), "Le badge d'origine « Intervention » doit être visible.");
|
||||
Assert.False(
|
||||
Badge(fenetre, "Téléphone"),
|
||||
"Le badge « Téléphone » ne doit pas être affiché pour un contact de l'intervention.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
fenetre.Close();
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>Le texte d'aide de la liste annonce les deux sources et l'origine affichée.</summary>
|
||||
[Fact]
|
||||
public void Liste_ExpliqueLesDeuxSourcesEtLOrigine()
|
||||
=> Executer(() =>
|
||||
{
|
||||
NouveauEmailView vue = Ouvrir();
|
||||
Window fenetre = Fenetre(vue);
|
||||
try
|
||||
{
|
||||
TextBlock aide = vue.GetVisualDescendants()
|
||||
.OfType<TextBlock>()
|
||||
.Single(t => t.Text?.StartsWith("Contacts de l'intervention et contacts du téléphone", StringComparison.Ordinal) == true);
|
||||
|
||||
Assert.True(aide.IsEffectivelyVisible, "L'explication des deux sources de contacts n'est pas affichée.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
fenetre.Close();
|
||||
}
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// Helpers
|
||||
// =====================================================================
|
||||
|
||||
/// <summary>Vrai si un texte visible porte exactement ce libellé.</summary>
|
||||
private static bool Texte(Window fenetre, string texte)
|
||||
=> fenetre.GetVisualDescendants()
|
||||
.OfType<TextBlock>()
|
||||
.Any(t => t.Text == texte && t.IsEffectivelyVisible);
|
||||
|
||||
/// <summary>Vrai si un badge visible porte exactement ce libellé d'origine.</summary>
|
||||
private static bool Badge(Window fenetre, string libelle) => Texte(fenetre, libelle);
|
||||
|
||||
/// <summary>
|
||||
/// Rend UNE entrée de la liste avec le gabarit réellement utilisé par les
|
||||
/// ComboBox de la vue (« ContactEmailOptionTemplate ») : c'est ce rendu qui
|
||||
/// affiche l'origine à côté du nom.
|
||||
/// </summary>
|
||||
private static Window RendreOption(NouveauEmailView vue, EmailContactOption option)
|
||||
{
|
||||
Assert.True(
|
||||
vue.TryFindResource("ContactEmailOptionTemplate", out object? ressource),
|
||||
"Le gabarit d'affichage des contacts est introuvable.");
|
||||
|
||||
var gabarit = Assert.IsAssignableFrom<IDataTemplate>(ressource);
|
||||
Control rendu = Assert.IsAssignableFrom<Control>(gabarit.Build(option));
|
||||
|
||||
// Build ne fixe pas le DataContext (c'est le presenter qui le fait) : on le
|
||||
// fixe ici pour que les liaisons de la ligne (libellé + badge d'origine)
|
||||
// résolvent réellement.
|
||||
rendu.DataContext = option;
|
||||
|
||||
return Fenetre(rendu);
|
||||
}
|
||||
|
||||
private static NouveauEmailView Ouvrir()
|
||||
{
|
||||
var modele = new NouveauEmailViewModel(
|
||||
new FakeInterventionService(),
|
||||
new FakeContactsService(),
|
||||
new FakeDocumentService(),
|
||||
new NavigationService(),
|
||||
new FakeSyncEngine(),
|
||||
Omika.Core.Observability.NullTelemetryService.Instance);
|
||||
|
||||
modele.InterventionGuid = Guid.NewGuid();
|
||||
modele.ContactsDisponibles = new ObservableCollection<EmailContactOption>
|
||||
{
|
||||
new("Alice Dupont", "alice@test.fr", OrigineContactEmail.Intervention),
|
||||
new("Zoé Martin", "zoe@perso.fr", OrigineContactEmail.Telephone)
|
||||
};
|
||||
|
||||
// Étape 2 : c'est elle qui contient la liste des destinataires.
|
||||
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 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 FakeInterventionService : IInterventionLocalService
|
||||
{
|
||||
public Task<IReadOnlyList<RapportChampDto>> GetMailChampsAsync(
|
||||
GetRapportChampsRequest request, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<RapportChampDto>>([]);
|
||||
|
||||
public Task<EmailModeleApercuDto?> GetEmailModeleApercuAsync(
|
||||
CreateEmailModeleRequest request, CancellationToken ct = default)
|
||||
=> Task.FromResult<EmailModeleApercuDto?>(null);
|
||||
|
||||
public Task<IReadOnlyList<SmtpConfigurationDto>> GetSmtpConfigurationsAsync(
|
||||
CancellationToken ct = default) => Task.FromResult<IReadOnlyList<SmtpConfigurationDto>>([]);
|
||||
|
||||
public Task<EmailInterventionDto> AddEmailAsync(
|
||||
Guid interventionGuid, CreateEmailInterventionRequest request, CancellationToken ct = default)
|
||||
=> Task.FromResult(new EmailInterventionDto { Id = Guid.NewGuid().ToString() });
|
||||
|
||||
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 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