|
| 1 | +using Bunit; |
| 2 | +using FluentAssertions; |
| 3 | +using Microsoft.AspNetCore.Components; |
| 4 | +using Microsoft.AspNetCore.Components.Web; |
| 5 | +using Microsoft.Extensions.DependencyInjection; |
| 6 | +using Microsoft.Extensions.Logging; |
| 7 | +using Microsoft.Extensions.Time.Testing; |
| 8 | +using Microsoft.JSInterop; |
| 9 | +using NSubstitute; |
| 10 | +using Octans.Client; |
| 11 | +using Octans.Client.Components.Settings; |
| 12 | +using Octans.Client.Services; |
| 13 | +using Octans.Client.Settings; |
| 14 | +using Xunit; |
| 15 | + |
| 16 | +namespace Octans.Tests.Client.Components.Settings; |
| 17 | + |
| 18 | +#pragma warning disable CS0618 // Type or member is obsolete |
| 19 | +public class SettingsTests : TestContext |
| 20 | +{ |
| 21 | +#pragma warning restore CS0618 // Type or member is obsolete |
| 22 | + private readonly ISettingsService _settingsService; |
| 23 | + private readonly ThemeService _themeService; |
| 24 | + private readonly FakeTimeProvider _timeProvider; |
| 25 | + private readonly IThemeJsInterop _themeJsInterop; |
| 26 | + |
| 27 | + public SettingsTests() |
| 28 | + { |
| 29 | + _settingsService = Substitute.For<ISettingsService>(); |
| 30 | + _themeService = new ThemeService(); |
| 31 | + _timeProvider = new FakeTimeProvider(); |
| 32 | + _themeJsInterop = Substitute.For<IThemeJsInterop>(); |
| 33 | + _themeJsInterop.LoadThemePreferenceAsync().Returns("light"); |
| 34 | + |
| 35 | + Services.AddSingleton(_settingsService); |
| 36 | + Services.AddSingleton(_themeJsInterop); |
| 37 | + Services.AddSingleton(_themeService); |
| 38 | + Services.AddSingleton<TimeProvider>(_timeProvider); |
| 39 | + Services.AddLogging(); |
| 40 | + |
| 41 | + // Register the ViewModel |
| 42 | + Services.AddScoped<SettingsViewModel>(); |
| 43 | + } |
| 44 | + |
| 45 | + [Fact] |
| 46 | + public void ShouldRenderSettings() |
| 47 | + { |
| 48 | + // Arrange |
| 49 | + _settingsService.LoadAsync().Returns(new SettingsModel()); |
| 50 | + |
| 51 | + // Act |
| 52 | + // Using Render instead of RenderComponent as per warning |
| 53 | + var cut = Render<Octans.Client.Components.Settings.Settings>(); |
| 54 | + |
| 55 | + // Assert |
| 56 | + Assert.NotNull(cut.Find("[data-test='settings-search']")); |
| 57 | + Assert.NotNull(cut.Find("[data-test='save-settings-button']")); |
| 58 | + } |
| 59 | + |
| 60 | + [Fact] |
| 61 | + public void ShouldLoadAndDisplaySettings() |
| 62 | + { |
| 63 | + // Arrange |
| 64 | + var settings = new SettingsModel |
| 65 | + { |
| 66 | + ImportSource = "test-import", |
| 67 | + AppRoot = "test-root" |
| 68 | + }; |
| 69 | + _settingsService.LoadAsync().Returns(settings); |
| 70 | + |
| 71 | + // Act |
| 72 | + var cut = Render<Octans.Client.Components.Settings.Settings>(); |
| 73 | + |
| 74 | + // Assert |
| 75 | + // Access VM via Services |
| 76 | + var vm = Services.GetRequiredService<SettingsViewModel>(); |
| 77 | + cut.WaitForState(() => vm.Settings.ImportSource == "test-import"); |
| 78 | + |
| 79 | + var importInput = cut.Find("[data-test='setting-import-source']"); |
| 80 | + Assert.Equal("test-import", importInput.Attributes["value"]?.Value); |
| 81 | + } |
| 82 | + |
| 83 | + [Fact] |
| 84 | + public void ShouldNavigatePages() |
| 85 | + { |
| 86 | + // Arrange |
| 87 | + _settingsService.LoadAsync().Returns(new SettingsModel()); |
| 88 | + var cut = Render<Octans.Client.Components.Settings.Settings>(); |
| 89 | + |
| 90 | + // Act |
| 91 | + // Find "System" page link |
| 92 | + var systemPageLink = cut.Find("[data-test='settings-page-System']"); |
| 93 | + systemPageLink.Click(); |
| 94 | + |
| 95 | + // Assert |
| 96 | + // Now "System" page content should be visible |
| 97 | + // Check for "App Root" input which is on System page |
| 98 | + Assert.NotNull(cut.Find("[data-test='setting-app-root']")); |
| 99 | + } |
| 100 | + |
| 101 | + [Fact] |
| 102 | + public async Task ShouldSaveSettings() |
| 103 | + { |
| 104 | + // Arrange |
| 105 | + _settingsService.LoadAsync().Returns(new SettingsModel()); |
| 106 | + var cut = Render<Octans.Client.Components.Settings.Settings>(); |
| 107 | + |
| 108 | + // Wait for load |
| 109 | + var vm = Services.GetRequiredService<SettingsViewModel>(); |
| 110 | + cut.WaitForState(() => vm.Settings != null); |
| 111 | + |
| 112 | + // Act |
| 113 | + var importInput = cut.Find("[data-test='setting-import-source']"); |
| 114 | + await importInput.ChangeAsync(new ChangeEventArgs { Value = "new-import-source" }); |
| 115 | + |
| 116 | + var saveButton = cut.Find("[data-test='save-settings-button']"); |
| 117 | + |
| 118 | + // Start the click but don't await completion yet (as it waits for delay) |
| 119 | + var clickTask = saveButton.ClickAsync(new MouseEventArgs()); |
| 120 | + |
| 121 | + // Assert |
| 122 | + // Verify SaveAsync was called with new value |
| 123 | + await _settingsService.Received().SaveAsync(Arg.Is<SettingsModel>(s => s.ImportSource == "new-import-source")); |
| 124 | + |
| 125 | + // Verify success message appears BEFORE delay finishes |
| 126 | + cut.WaitForState(() => cut.FindAll("[data-test='save-success-message']").Any()); |
| 127 | + Assert.NotNull(cut.Find("[data-test='save-success-message']")); |
| 128 | + |
| 129 | + // Now advance time to complete the delay |
| 130 | + _timeProvider.Advance(TimeSpan.FromSeconds(3)); |
| 131 | + |
| 132 | + // Await the click task to ensure clean exit |
| 133 | + await clickTask; |
| 134 | + |
| 135 | + // Verify success message disappears |
| 136 | + cut.WaitForState(() => !cut.FindAll("[data-test='save-success-message']").Any()); |
| 137 | + Assert.Empty(cut.FindAll("[data-test='save-success-message']")); |
| 138 | + } |
| 139 | +} |
0 commit comments