diff --git a/src/Lanceur.Core/Models/AliasQueryResult.cs b/src/Lanceur.Core/Models/AliasQueryResult.cs index 8557ba4a..5a0fd8dd 100644 --- a/src/Lanceur.Core/Models/AliasQueryResult.cs +++ b/src/Lanceur.Core/Models/AliasQueryResult.cs @@ -81,21 +81,21 @@ public string Synonyms set { Set(ref _synonyms, value); - OnPropertyChanged(nameof(SynonymsNextState)); + OnPropertyChanged(nameof(SynonymsToAdd)); } } /// /// New synonyms added when updated /// - public string SynonymsNextState => (from n in Synonyms.SplitCsv() - where !SynonymsPreviousState.SplitCsv().Contains(n) + public string SynonymsToAdd => (from n in Synonyms.SplitCsv() + where !SynonymsWhenLoaded.SplitCsv().Contains(n) select n).ToArray().JoinCsv(); /// /// Synonyms present when the entity was loaded /// - public string SynonymsPreviousState { get; set; } + public string SynonymsWhenLoaded { get; set; } public string WorkingDirectory { get; set; } @@ -105,7 +105,5 @@ public string Synonyms public static AliasQueryResult FromName(string aliasName) => new() { Name = aliasName, Synonyms = aliasName }; - public override int GetHashCode() => (base.GetHashCode(), (Parameters?.GetHashCode() ?? 0)).GetHashCode(); - #endregion Methods } \ No newline at end of file diff --git a/src/Lanceur.Core/Models/AliasQueryResultMixin.cs b/src/Lanceur.Core/Models/AliasQueryResultMixin.cs index d37f6471..f5770e4d 100644 --- a/src/Lanceur.Core/Models/AliasQueryResultMixin.cs +++ b/src/Lanceur.Core/Models/AliasQueryResultMixin.cs @@ -1,3 +1,5 @@ +using Lanceur.SharedKernel.Mixins; + namespace Lanceur.Core.Models; public static class AliasQueryResultMixin @@ -16,5 +18,16 @@ public static void UpdateIcon(this AliasQueryResult alias) } } + /// + /// Set first names defined in the synonyms as the name of the alias + /// + /// The alias + public static void SetName(this AliasQueryResult alias) + { + alias.Name = alias.Synonyms + .SplitCsv() + .FirstOrDefault(); + } + #endregion Methods } \ No newline at end of file diff --git a/src/Lanceur.Core/Models/ExistingNameResponse.cs b/src/Lanceur.Core/Models/ExistingNameResponse.cs index f0ba4c72..7e5f207b 100644 --- a/src/Lanceur.Core/Models/ExistingNameResponse.cs +++ b/src/Lanceur.Core/Models/ExistingNameResponse.cs @@ -7,9 +7,7 @@ public record ExistingNameResponse { public ExistingNameResponse(string[] existingNames) { - ExistingNames = existingNames is null - ? Array.Empty() - : existingNames; + ExistingNames = existingNames ?? Array.Empty(); } /// diff --git a/src/Lanceur.Core/Models/QueryResult.cs b/src/Lanceur.Core/Models/QueryResult.cs index 4b66ca74..75aa2655 100644 --- a/src/Lanceur.Core/Models/QueryResult.cs +++ b/src/Lanceur.Core/Models/QueryResult.cs @@ -25,7 +25,7 @@ public abstract class QueryResult : ObservableModel protected static Task> NoResultAsync => Task.FromResult(NoResult); public static IEnumerable NoResult => new List(); - public int Count { get; set; } = 0; + public int Count { get; set; } public virtual string Description { get; set; } /// @@ -46,7 +46,7 @@ public abstract class QueryResult : ObservableModel /// public virtual bool IsResult => true; - public virtual string Name { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; public Cmdline Query { get; set; } = Cmdline.Empty; /// @@ -66,11 +66,6 @@ public object Thumbnail #region Methods - /// - /// This hashcode is calculated on not readonly properties and can be outdated if properties are updated! - /// - public override int GetHashCode() => (Count, Description, Icon, Id, IsResult, Name, Query, Thumbnail).GetHashCode(); - public virtual string ToQuery() => $"{Name}"; #endregion Methods diff --git a/src/Lanceur.Infra.SQLite/DbActions/AliasDbAction.cs b/src/Lanceur.Infra.SQLite/DbActions/AliasDbAction.cs index c341a898..3913943e 100644 --- a/src/Lanceur.Infra.SQLite/DbActions/AliasDbAction.cs +++ b/src/Lanceur.Infra.SQLite/DbActions/AliasDbAction.cs @@ -382,7 +382,7 @@ update alias UpdateName(alias, tx); }); - alias.SynonymsPreviousState = alias.Synonyms; + alias.SynonymsWhenLoaded = alias.Synonyms; return alias.Id; } diff --git a/src/Lanceur.Infra.SQLite/DbActions/AliasSearchDbAction.cs b/src/Lanceur.Infra.SQLite/DbActions/AliasSearchDbAction.cs index 0ec0dfad..7a9e7056 100644 --- a/src/Lanceur.Infra.SQLite/DbActions/AliasSearchDbAction.cs +++ b/src/Lanceur.Infra.SQLite/DbActions/AliasSearchDbAction.cs @@ -43,7 +43,7 @@ public IEnumerable Search(string name, long idSession) a.lua_script as {nameof(AliasQueryResult.LuaScript)}, c.exec_count as {nameof(AliasQueryResult.Count)}, s.synonyms as {nameof(AliasQueryResult.Synonyms)}, - s.Synonyms as {nameof(AliasQueryResult.SynonymsPreviousState)} + s.Synonyms as {nameof(AliasQueryResult.SynonymsWhenLoaded)} from alias a left join alias_name an on a.id = an.id_alias @@ -80,7 +80,7 @@ public IEnumerable SearchAliasWithAdditionalParameters(string a.lua_script as {nameof(AliasQueryResult.LuaScript)}, c.exec_count as {nameof(AliasQueryResult.Count)}, s.synonyms as {nameof(AliasQueryResult.Synonyms)}, - s.Synonyms as {nameof(AliasQueryResult.SynonymsPreviousState)} + s.Synonyms as {nameof(AliasQueryResult.SynonymsWhenLoaded)} from alias a left join alias_name an on a.id = an.id_alias diff --git a/src/Lanceur.Infra.SQLite/DbActions/GetAllAliasDbAction.cs b/src/Lanceur.Infra.SQLite/DbActions/GetAllAliasDbAction.cs index c893a26b..aac48767 100644 --- a/src/Lanceur.Infra.SQLite/DbActions/GetAllAliasDbAction.cs +++ b/src/Lanceur.Infra.SQLite/DbActions/GetAllAliasDbAction.cs @@ -41,7 +41,7 @@ public IEnumerable GetAll(long? idSession = null) a.lua_script as {nameof(AliasQueryResult.LuaScript)}, c.exec_count as {nameof(AliasQueryResult.Count)}, s.synonyms as {nameof(AliasQueryResult.Synonyms)}, - s.Synonyms as {nameof(AliasQueryResult.SynonymsPreviousState)} + s.Synonyms as {nameof(AliasQueryResult.SynonymsWhenLoaded)} from alias a left join alias_name an on a.id = an.id_alias @@ -81,7 +81,7 @@ public IEnumerable GetAllAliasWithAdditionalParameters(long id a.lua_script as {nameof(AliasQueryResult.LuaScript)}, c.exec_count as {nameof(AliasQueryResult.Count)}, s.synonyms as {nameof(AliasQueryResult.Synonyms)}, - s.Synonyms as {nameof(AliasQueryResult.SynonymsPreviousState)} + s.Synonyms as {nameof(AliasQueryResult.SynonymsWhenLoaded)} from alias a left join alias_name an on a.id = an.id_alias diff --git a/src/Lanceur.Infra.SQLite/SQLiteRepository.cs b/src/Lanceur.Infra.SQLite/SQLiteRepository.cs index c3a3cd7f..2dddf83a 100644 --- a/src/Lanceur.Infra.SQLite/SQLiteRepository.cs +++ b/src/Lanceur.Infra.SQLite/SQLiteRepository.cs @@ -309,6 +309,9 @@ public void SaveOrUpdate(ref AliasQueryResult alias, long? idSession = null) _aliasDbAction.Update(alias); break; } + + // Reset state after save + alias.SynonymsWhenLoaded = alias.Synonyms; } /// diff --git a/src/Lanceur/AssemblyInfo.cs b/src/Lanceur/AssemblyInfo.cs index 5cb6b839..7fc2e629 100644 --- a/src/Lanceur/AssemblyInfo.cs +++ b/src/Lanceur/AssemblyInfo.cs @@ -1,4 +1,5 @@ -using System.Windows; +using System.Runtime.CompilerServices; +using System.Windows; [assembly: ThemeInfo( ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located @@ -7,4 +8,6 @@ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located //(used if a resource is not found in the page, // app, or any theme specific resource dictionaries) -)] \ No newline at end of file +)] + +[assembly:InternalsVisibleTo("Lanceur.Tests")] \ No newline at end of file diff --git a/src/Lanceur/Views/KeywordsView.xaml.cs b/src/Lanceur/Views/KeywordsView.xaml.cs index 048f0b96..65138308 100644 --- a/src/Lanceur/Views/KeywordsView.xaml.cs +++ b/src/Lanceur/Views/KeywordsView.xaml.cs @@ -49,11 +49,16 @@ public KeywordsView() this.OneWayBind(ViewModel, vm => vm.BusyMessage, v => v.BusyMessage.Text).DisposeWith(d); this.OneWayBind(ViewModel, vm => vm.IsBusy, v => v.BusyControl.Visibility).DisposeWith(d); this.OneWayBind(ViewModel, vm => vm.IsBusy, v => v.AliasList.Visibility, val => val.ToVisibilityInverted()).DisposeWith(d); + this.OneWayBind( + ViewModel, + vm => vm.SelectedAlias, + v => v.BtnDeleteAlias.Content, + val => val is null ? "Delete" : val.Id == 0 ? "Discard" : "Delete").DisposeWith(d); this.Bind(ViewModel, vm => vm.SearchQuery, v => v.QueryBox.Text).DisposeWith(d); this.Bind(ViewModel, vm => vm.SelectedAlias, v => v.Aliases.SelectedItem).DisposeWith(d); - this.BindCommand(ViewModel, vm => vm.CreateAlias, v => v.BtnCreateAlias).DisposeWith(d); + this.BindCommand(ViewModel, vm => vm.CreatingAlias, v => v.BtnCreateAlias).DisposeWith(d); this.BindCommand(ViewModel, vm => vm.RemoveAlias, v => v.BtnDeleteAlias, v => v.SelectedAlias).DisposeWith(d); this.BindCommand(ViewModel, vm => vm.SaveOrUpdateAlias, v => v.BtnSaveOrUpdateAlias, v => v.SelectedAlias).DisposeWith(d); diff --git a/src/Lanceur/Views/KeywordsViewModel.cs b/src/Lanceur/Views/KeywordsViewModel.cs index a943453e..986b5608 100644 --- a/src/Lanceur/Views/KeywordsViewModel.cs +++ b/src/Lanceur/Views/KeywordsViewModel.cs @@ -34,13 +34,13 @@ public class KeywordsViewModel : RoutableViewModel, IValidatableViewModel, IActi private readonly SourceList _aliases = new(); private readonly IDbRepository _aliasService; - private readonly Interaction _askLuaEditor; private readonly Scope _busyScope; - private readonly Interaction _confirmRemove; private readonly IAppLogger _log; private readonly INotification _notification; private readonly IPackagedAppValidator _packagedAppValidator; private readonly IThumbnailManager _thumbnailManager; + private readonly IUserNotification _notify; + private readonly ISchedulerProvider _schedulers; #endregion Fields @@ -56,59 +56,67 @@ public KeywordsViewModel( INotification notification = null) { _busyScope = new(b => IsBusy = b, true, false); - schedulers ??= Locator.Current.GetService(); + _schedulers = schedulers ?? Locator.Current.GetService(); var l = Locator.Current; - notify ??= l.GetService(); + _notify = notify?? l.GetService(); _packagedAppValidator = packagedAppValidator ?? l.GetService(); _notification = notification ?? l.GetService(); _log = l.GetLogger(logFactory); _thumbnailManager = thumbnailManager ?? l.GetService(); _aliasService = searchService ?? l.GetService(); - _confirmRemove = Interactions.YesNoQuestion(schedulers.MainThreadScheduler); - _askLuaEditor = new(); - this.WhenActivated(d => - { - Disposable + ConfirmRemove = Interactions.YesNoQuestion(_schedulers.MainThreadScheduler); + AskLuaEditor = new(); + + this.WhenActivated(Activate); + } + + /// + /// The purpose of this method is for unit test only. You can manually mimic + /// WhenActivated + /// + /// + /// Ensures the provided disposable is disposed with the specified . + /// + internal void Activate(CompositeDisposable d) + { + Disposable .Create(() => { Aliases.Clear(); AliasToCreate = null; }).DisposeWith(d); - SetupValidations(d); - SetupCommands(schedulers.MainThreadScheduler, notify, d); - SetupBindings(schedulers.MainThreadScheduler, d); - }); + SetupValidations(d); + SetupCommands(_schedulers.MainThreadScheduler, _notify, d); + SetupBindings(_schedulers.MainThreadScheduler, d); } - #endregion Constructors #region Properties private ReactiveCommand> Search { get; set; } + public ViewModelActivator Activator { get; } = new(); public IObservableCollection Aliases { get; } = new ObservableCollectionExtended(); [Reactive] public AliasQueryResult AliasToCreate { get; set; } - public Interaction AskLuaEditor => _askLuaEditor; + public Interaction AskLuaEditor { get; } [Reactive] public string BusyMessage { get; set; } - public Interaction ConfirmRemove => _confirmRemove; - - public ReactiveCommand CreateAlias { get; private set; } + public Interaction ConfirmRemove { get; } - [Reactive] public bool IsBusy { get; set; } + public ReactiveCommand CreatingAlias { get; private set; } public ReactiveCommand EditLuaScript { get; private set; } - + [Reactive] public bool IsBusy { get; set; } public ReactiveCommand RemoveAlias { get; private set; } - public ReactiveCommand SaveOrUpdateAlias { get; private set; } + public ReactiveCommand SaveOrUpdateAlias { get; private set; } [Reactive] public string SearchQuery { get; set; } [Reactive] public AliasQueryResult SelectedAlias { get; set; } @@ -123,18 +131,27 @@ public KeywordsViewModel( #region Methods - private void OnCreateAlias() + private void OnCreatingAlias() { + if (Aliases.Any(x => x.Id == 0)) return; + var newAlias = AliasQueryResult.EmptyForCreation; _aliases.Insert(0, newAlias); SelectedAlias = newAlias; } + private async Task OnEditLuaScript() + { + var output = await AskLuaEditor.Handle(SelectedAlias.ToScript()); + SelectedAlias.LuaScript = output; + return output; + } + private async Task OnRemoveAliasAsync(AliasQueryResult alias) { if (alias == null) return Unit.Default; - var remove = await _confirmRemove.Handle(alias.Name); + var remove = await ConfirmRemove.Handle(alias.Name); if (remove) { _log.Info($"User removed alias '{alias.Name}'"); @@ -152,22 +169,19 @@ private async Task OnRemoveAliasAsync(AliasQueryResult alias) return Unit.Default; } - private async Task OnSaveOrUpdateAliasAsync(AliasQueryResult alias) + private async Task OnSaveOrUpdateAliasAsync(AliasQueryResult alias) { var created = alias.Id == 0; BusyMessage = "Saving alias..."; using (_busyScope.Open()) { + alias.SetName(); alias = await _packagedAppValidator.FixAsync(alias); _aliasService.SaveOrUpdate(ref alias); } _notification.Information($"Alias '{alias.Name}' {(created ? "created" : "updated")}."); - // Returns updated results - return new() - { - Aliases = _aliasService.GetAll() - }; + return alias; } private IEnumerable OnSearch(SearchRequest request) @@ -185,8 +199,14 @@ private IEnumerable OnSearch(SearchRequest request) private void SetAliases(IEnumerable x) { + var selected = Aliases.FirstOrDefault(a => a.Id == SelectedAlias?.Id); _aliases.Clear(); _aliases.AddRange(x); + + if (selected is AliasQueryResult alias) + { + SelectedAlias = alias; + } } private void SetupBindings(IScheduler uiThread, CompositeDisposable d) @@ -212,6 +232,16 @@ private void SetupBindings(IScheduler uiThread, CompositeDisposable d) .BindTo(this, vm => vm.SelectedAlias) .DisposeWith(d); + this.WhenAnyObservable(vm => vm.SaveOrUpdateAlias) + .Where(alias => alias is not null) + .Subscribe(alias => + { + var toDel = _aliases.Items.Single(a => a.Id == alias.Id); + _aliases.Remove(toDel); + _aliases.Add(alias); + }) + .DisposeWith(d); + this.WhenAnyValue(vm => vm.SearchQuery) .DistinctUntilChanged() .Throttle(TimeSpan.FromMilliseconds(10), scheduler: uiThread) @@ -219,10 +249,6 @@ private void SetupBindings(IScheduler uiThread, CompositeDisposable d) .Log(this, $"Invoking search.", c => $"With criterion '{c.Query}' and alias to create '{(c.AliasToCreate?.Name ?? "")}'") .InvokeCommand(Search) .DisposeWith(d); - - this.WhenAnyObservable(vm => vm.SaveOrUpdateAlias) - .Select(x => x.Aliases.ToObservableCollection()) - .Subscribe(SetAliases); } private void SetupCommands(IScheduler uiThread, IUserNotification notify, CompositeDisposable d) @@ -230,62 +256,51 @@ private void SetupCommands(IScheduler uiThread, IUserNotification notify, Compos Search = ReactiveCommand.Create>(OnSearch, outputScheduler: uiThread).DisposeWith(d); Search.ThrownExceptions.Subscribe(ex => notify.Error(ex.Message, ex)); - CreateAlias = ReactiveCommand.Create(OnCreateAlias, outputScheduler: uiThread).DisposeWith(d); - CreateAlias.ThrownExceptions.Subscribe(ex => notify.Error(ex.Message, ex)); + CreatingAlias = ReactiveCommand.Create(OnCreatingAlias, outputScheduler: uiThread).DisposeWith(d); + CreatingAlias.ThrownExceptions.Subscribe(ex => notify.Error(ex.Message, ex)); RemoveAlias = ReactiveCommand.CreateFromTask(OnRemoveAliasAsync, outputScheduler: uiThread).DisposeWith(d); RemoveAlias.ThrownExceptions.Subscribe(ex => notify.Error(ex.Message, ex)); - SaveOrUpdateAlias = ReactiveCommand.CreateFromTask(OnSaveOrUpdateAliasAsync, this.IsValid(), outputScheduler: uiThread).DisposeWith(d); + SaveOrUpdateAlias = ReactiveCommand.CreateFromTask( + OnSaveOrUpdateAliasAsync, + this.IsValid(), + outputScheduler: uiThread + ).DisposeWith(d); SaveOrUpdateAlias.ThrownExceptions.Subscribe(ex => notify.Error(ex.Message, ex)); EditLuaScript = ReactiveCommand.CreateFromTask(OnEditLuaScript, outputScheduler: uiThread).DisposeWith(d); EditLuaScript.ThrownExceptions.Subscribe(ex => notify.Error(ex.Message, ex)); } - private async Task OnEditLuaScript() - { - var output = await _askLuaEditor.Handle(SelectedAlias.ToScript()); - SelectedAlias.LuaScript = output; - return output; - } - private void SetupValidations(CompositeDisposable d) { - var fileNameExists = this.WhenAnyValue( + var validateFileNameExists = this.WhenAnyValue( x => x.SelectedAlias.FileName, x => !x.IsNullOrEmpty() ); - var nameExists = this.WhenAnyValue( - x => x.SelectedAlias.SynonymsNextState, + var validateNameExists = this.WhenAnyValue( + x => x.SelectedAlias.SynonymsToAdd, x => _aliasService.CheckNamesExist(x.SplitCsv()) ); ValidationFileName = this.ValidationRule( vm => vm.SelectedAlias.FileName, - fileNameExists, + validateFileNameExists, "The path to the file shouldn't be empty." ); ValidationFileName.DisposeWith(d); ValidationAliasExists = this.ValidationRule( vm => vm.SelectedAlias.Synonyms, - nameExists, + validateNameExists, response => !response.Exists, - response => (response.Exists == false && !response.ExistingNames.Any()) + response => !response.Exists && !response.ExistingNames.Any() ? "The names should not be empty" : $"'{response.ExistingNames.JoinCsv()}' {(response.ExistingNames.Length <= 1 ? "is" : "are")} already used as alias."); ValidationAliasExists.DisposeWith(d); } - public async Task Clear() - { - SearchQuery = null; - SelectedAlias = null; - Aliases.Clear(); - await Task.Delay(50); - } - public void HydrateSelectedAlias() => _aliasService.HydrateAlias(SelectedAlias); #endregion Methods @@ -312,15 +327,6 @@ public SearchRequest(string query, AliasQueryResult alias = null) #endregion Properties } - public class SaveOrUpdateAliasResponse - { - #region Properties - - public IEnumerable Aliases { get; init; } - - #endregion Properties - } - #endregion Classes } } \ No newline at end of file diff --git a/src/Tests/Lanceur.Tests/Utils/Builders/KeywordsViewModelBuilder.cs b/src/Tests/Lanceur.Tests/Utils/Builders/KeywordsViewModelBuilder.cs new file mode 100644 index 00000000..2798fb58 --- /dev/null +++ b/src/Tests/Lanceur.Tests/Utils/Builders/KeywordsViewModelBuilder.cs @@ -0,0 +1,65 @@ +using Lanceur.Core.Managers; +using Lanceur.Core.Repositories; +using Lanceur.Core.Services; +using Lanceur.Schedulers; +using Lanceur.Tests.Logging; +using Lanceur.Ui; +using Lanceur.Views; +using NSubstitute; +using System.Reactive.Concurrency; +using Xunit.Abstractions; + +namespace Lanceur.Tests.Utils.Builders; + +internal class KeywordsViewModelBuilder +{ + #region Fields + + private IDbRepository _dbRepository; + private ITestOutputHelper _output; + private IPackagedAppValidator _packagedAppValidator; + private TestSchedulerProvider _schedulerProvider; + + #endregion Fields + + #region Methods + + public KeywordsViewModel Build() + { + return new( + logFactory: new XUnitLoggerFactory(_output), + searchService: _dbRepository ?? Substitute.For(), + schedulers: _schedulerProvider ?? throw new ArgumentNullException($"No scheduler configured for the ViewModel to test."), + notify: Substitute.For(), + thumbnailManager: Substitute.For(), + packagedAppValidator: _packagedAppValidator ?? Substitute.For(), + notification: Substitute.For() + ); + } + + public KeywordsViewModelBuilder With(IPackagedAppValidator packagedAppValidator) + { + _packagedAppValidator = packagedAppValidator; + return this; + } + + public KeywordsViewModelBuilder With(IDbRepository dbRepository) + { + _dbRepository = dbRepository; + return this; + } + + public KeywordsViewModelBuilder With(IScheduler scheduler) + { + _schedulerProvider = new TestSchedulerProvider(scheduler); + return this; + } + + public KeywordsViewModelBuilder With(ITestOutputHelper output) + { + _output = output; + return this; + } + + #endregion Methods +} \ No newline at end of file diff --git a/src/Tests/Lanceur.Tests/Utils/Builders/MainViewModelBuilder.cs b/src/Tests/Lanceur.Tests/Utils/Builders/MainViewModelBuilder.cs index 861859b2..a2f63825 100644 --- a/src/Tests/Lanceur.Tests/Utils/Builders/MainViewModelBuilder.cs +++ b/src/Tests/Lanceur.Tests/Utils/Builders/MainViewModelBuilder.cs @@ -41,8 +41,8 @@ public MainViewModel Build() var settingsFacade = Substitute.For(); settingsFacade.Application.Returns(new AppConfig()); - return new MainViewModel( - schedulerProvider: _schedulerProvider, + return new( + schedulerProvider: _schedulerProvider ?? throw new ArgumentNullException($"No scheduler configured for the ViewModel to test."), logFactory: new XUnitLoggerFactory(_output), searchService: _searchService ?? Substitute.For(), cmdlineService: new CmdlineManager(), diff --git a/src/Tests/Lanceur.Tests/ViewModels/KeywordViewModelShould.cs b/src/Tests/Lanceur.Tests/ViewModels/KeywordViewModelShould.cs new file mode 100644 index 00000000..307bcb71 --- /dev/null +++ b/src/Tests/Lanceur.Tests/ViewModels/KeywordViewModelShould.cs @@ -0,0 +1,79 @@ +using System.Reactive.Disposables; +using System.Windows.Forms.Design; +using FluentAssertions; +using Lanceur.Core.Managers; +using Lanceur.Core.Models; +using Lanceur.Core.Repositories; +using Lanceur.Tests.Utils.Builders; +using Microsoft.Reactive.Testing; +using NSubstitute; +using ReactiveUI.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Lanceur.Tests.ViewModels; + +public class KeywordViewModelShould +{ + #region Fields + + private readonly ITestOutputHelper _output; + + #endregion Fields + + #region Constructors + + public KeywordViewModelShould(ITestOutputHelper output) + { + _output = output; + } + + #endregion Constructors + + #region Methods + + [Fact] + public void AbleToAddMultipleTimesAlias() + { + new TestScheduler().With(scheduler => + { + // ARRANGE + var dbRepository = Substitute.For(); + dbRepository.CheckNamesExist(Arg.Any()) + .Returns(new ExistingNameResponse(Array.Empty())); + + var packageValidator = Substitute.For(); + + var vm = new KeywordsViewModelBuilder() + .With(scheduler) + .With(_output) + .With(dbRepository) + .With(packageValidator) + .Build(); + + var synonyms = Guid.NewGuid().ToString(); + var fileName = Guid.NewGuid().ToString(); + + // ACT + + vm.Activate(new()); + scheduler.Start(); + vm.CreatingAlias.Execute().Subscribe(); + var hash = vm.SelectedAlias.GetHashCode(); + + vm.SelectedAlias.Synonyms = synonyms; + vm.SelectedAlias.FileName = fileName; + + packageValidator.FixAsync(Arg.Any()) + .Returns(vm.SelectedAlias); + + vm.SaveOrUpdateAlias.Execute(vm.SelectedAlias).Subscribe(); + + // ASSERT + vm.SelectedAlias.GetHashCode().Should().Be(hash); + + }); + } + + #endregion Methods +} \ No newline at end of file