diff --git a/docs/knowledgebase/gui-architecture-deferred.md b/docs/knowledgebase/gui-architecture-deferred.md index f765e78a..bf4c46ff 100644 --- a/docs/knowledgebase/gui-architecture-deferred.md +++ b/docs/knowledgebase/gui-architecture-deferred.md @@ -62,6 +62,7 @@ Plans: `docs/plans/2026-06-03-012`, `021`–`029`, `033`–`057`. Surface refere | Directory picker init/sync → `SettingsService` | Done | plan `075`, PR #123 | | Mod context menu + global flyout → `MenuBuilderService` | Done | plan `072`, PR #130 | +**Headless tests:** `dotnet test src/ModSync.Tests/ModSync.Tests.csproj --filter SettingsService` (plan `077`); `--filter MenuBuilderService` (plan `072`); `--filter ValidationDisplayUiHelper` (plan `094`). **Headless tests:** `dotnet test src/ModSync.Tests/ModSync.Tests.csproj --filter SettingsService` (plan `077`); `--filter MenuBuilderService` (plan `072`); `--filter DownloadOrchestrationService` (plan `088`). **Headless tests:** `dotnet test src/ModSync.Tests/ModSync.Tests.csproj --filter SettingsService` (plan `077`); `--filter MenuBuilderService` (plan `072`); `--filter DialogServiceTests` / `FileSystemServiceTests` (plan `113`). **Headless tests:** `dotnet test src/ModSync.Tests/ModSync.Tests.csproj --filter SettingsService` (plan `077`); `--filter MenuBuilderService` (plan `072`); `--filter FilterItemModel` (plan `112`). diff --git a/docs/plans/2026-06-04-094-refactor-validation-display-ui-helper-plan.md b/docs/plans/2026-06-04-094-refactor-validation-display-ui-helper-plan.md new file mode 100644 index 00000000..9d5bb7a1 --- /dev/null +++ b/docs/plans/2026-06-04-094-refactor-validation-display-ui-helper-plan.md @@ -0,0 +1,19 @@ +--- +title: "refactor: ValidationDisplayUiHelper for Getting Started summaries" +status: shipped +pr: pending +--- + +# refactor: ValidationDisplayUiHelper for Getting Started summaries + +## Problem + +`ValidationDisplayService` duplicated validation summary strings and error-navigation bounds inline in Getting Started validation UI. + +## Solution + +Add `ValidationDisplayUiHelper` for summary/error-counter formatting, invalid-component collection, and navigation bounds; wire `ValidationDisplayService` to delegate. + +## Verification + +`dotnet test src/ModSync.Tests/ModSync.Tests.csproj --filter ValidationDisplayUiHelper` diff --git a/src/ModSync.GUI/Services/ValidationDisplayService.cs b/src/ModSync.GUI/Services/ValidationDisplayService.cs index 592d491b..ad6e4b3d 100644 --- a/src/ModSync.GUI/Services/ValidationDisplayService.cs +++ b/src/ModSync.GUI/Services/ValidationDisplayService.cs @@ -47,14 +47,8 @@ public void ShowValidationResults( List mainComponents = _getMainComponents(); var selectedComponents = mainComponents.Where(c => c.IsSelected).ToList(); _validationErrors.Clear(); - - foreach (ModComponent component in selectedComponents) - { - if (!isComponentValid(component)) - { - _validationErrors.Add(component); - } - } + _validationErrors.AddRange( + ValidationDisplayUiHelper.CollectInvalidSelectedComponents(selectedComponents, isComponentValid)); if (validationResultsArea is null) { @@ -68,7 +62,7 @@ public void ShowValidationResults( if (validationSummaryText != null) { - validationSummaryText.Text = $"✅ All {selectedComponents.Count} mods validated successfully!"; + validationSummaryText.Text = ValidationDisplayUiHelper.FormatAllValidSummary(selectedComponents.Count); } if (errorNavigationArea != null) @@ -92,7 +86,9 @@ public void ShowValidationResults( int validCount = selectedComponents.Count - _validationErrors.Count; if (validationSummaryText != null) { - validationSummaryText.Text = $"⚠️ {validCount}/{selectedComponents.Count} mods validated successfully"; + validationSummaryText.Text = ValidationDisplayUiHelper.FormatPartialValidSummary( + validCount, + selectedComponents.Count); } if (errorNavigationArea != null) @@ -161,7 +157,9 @@ public void UpdateErrorDisplay( if (errorCounterText != null) { - errorCounterText.Text = $"Error {_currentErrorIndex + 1} of {_validationErrors.Count}"; + errorCounterText.Text = ValidationDisplayUiHelper.FormatErrorCounter( + _currentErrorIndex, + _validationErrors.Count); } if (errorModNameText != null) @@ -171,12 +169,14 @@ public void UpdateErrorDisplay( if (prevErrorButton != null) { - prevErrorButton.IsEnabled = _currentErrorIndex > 0; + prevErrorButton.IsEnabled = ValidationDisplayUiHelper.CanNavigateToPreviousError(_currentErrorIndex); } if (nextErrorButton != null) { - nextErrorButton.IsEnabled = _currentErrorIndex < _validationErrors.Count - 1; + nextErrorButton.IsEnabled = ValidationDisplayUiHelper.CanNavigateToNextError( + _currentErrorIndex, + _validationErrors.Count); } (string ErrorType, string Description, bool CanAutoFix) = _validationService.GetComponentErrorDetails(currentError); @@ -211,7 +211,7 @@ public void NavigateToPreviousError( Button prevErrorButton, Button nextErrorButton) { - if (_currentErrorIndex > 0) + if (ValidationDisplayUiHelper.CanNavigateToPreviousError(_currentErrorIndex)) { _currentErrorIndex--; UpdateErrorDisplay( @@ -234,7 +234,7 @@ public void NavigateToNextError( Button prevErrorButton, Button nextErrorButton) { - if (_currentErrorIndex < _validationErrors.Count - 1) + if (ValidationDisplayUiHelper.CanNavigateToNextError(_currentErrorIndex, _validationErrors.Count)) { _currentErrorIndex++; UpdateErrorDisplay( diff --git a/src/ModSync.GUI/Services/ValidationDisplayUiHelper.cs b/src/ModSync.GUI/Services/ValidationDisplayUiHelper.cs new file mode 100644 index 00000000..ef2f5760 --- /dev/null +++ b/src/ModSync.GUI/Services/ValidationDisplayUiHelper.cs @@ -0,0 +1,53 @@ +// Copyright 2021-2025 ModSync +// Licensed under the Business Source License 1.1 (BSL 1.1). +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; +using ModSync.Core; + +namespace ModSync.Services +{ + public static class ValidationDisplayUiHelper + { + public static string FormatAllValidSummary(int selectedCount) => + $"✅ All {selectedCount} mods validated successfully!"; + + public static string FormatPartialValidSummary(int validCount, int selectedCount) => + $"⚠️ {validCount}/{selectedCount} mods validated successfully"; + + public static string FormatErrorCounter(int currentIndex, int totalErrors) => + $"Error {currentIndex + 1} of {totalErrors}"; + + public static List CollectInvalidSelectedComponents( + IEnumerable selectedComponents, + Func isComponentValid) + { + if (selectedComponents is null) + { + throw new ArgumentNullException(nameof(selectedComponents)); + } + + if (isComponentValid is null) + { + throw new ArgumentNullException(nameof(isComponentValid)); + } + + var validationErrors = new List(); + foreach (ModComponent component in selectedComponents) + { + if (!isComponentValid(component)) + { + validationErrors.Add(component); + } + } + + return validationErrors; + } + + public static bool CanNavigateToPreviousError(int currentErrorIndex) => currentErrorIndex > 0; + + public static bool CanNavigateToNextError(int currentErrorIndex, int errorCount) => + currentErrorIndex < errorCount - 1; + } +} diff --git a/src/ModSync.Tests/ValidationDisplayUiHelperTests.cs b/src/ModSync.Tests/ValidationDisplayUiHelperTests.cs new file mode 100644 index 00000000..d8e2a1cd --- /dev/null +++ b/src/ModSync.Tests/ValidationDisplayUiHelperTests.cs @@ -0,0 +1,72 @@ +// Copyright 2021-2025 ModSync +// Licensed under the Business Source License 1.1 (BSL 1.1). +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; +using ModSync.Core; +using ModSync.Services; +using Xunit; + +namespace ModSync.Tests +{ + public sealed class ValidationDisplayUiHelperTests + { + [Fact(DisplayName = "FormatAllValidSummary uses success phrasing")] + public void FormatAllValidSummary_UsesSuccessPhrasing() + { + Assert.Equal("✅ All 3 mods validated successfully!", ValidationDisplayUiHelper.FormatAllValidSummary(3)); + } + + [Fact(DisplayName = "FormatPartialValidSummary uses warning ratio phrasing")] + public void FormatPartialValidSummary_UsesWarningRatio() + { + Assert.Equal( + "⚠️ 2/5 mods validated successfully", + ValidationDisplayUiHelper.FormatPartialValidSummary(validCount: 2, selectedCount: 5)); + } + + [Fact(DisplayName = "FormatErrorCounter uses one-based index")] + public void FormatErrorCounter_UsesOneBasedIndex() + { + Assert.Equal("Error 2 of 4", ValidationDisplayUiHelper.FormatErrorCounter(currentIndex: 1, totalErrors: 4)); + } + + [Fact(DisplayName = "CollectInvalidSelectedComponents returns only failing mods")] + public void CollectInvalidSelectedComponents_ReturnsInvalidOnly() + { + var components = new List + { + new ModComponent { Name = "Valid" }, + new ModComponent { Name = "Invalid" }, + }; + + List errors = ValidationDisplayUiHelper.CollectInvalidSelectedComponents( + components, + component => string.Equals(component.Name, "Valid", StringComparison.Ordinal)); + + Assert.Single(errors); + Assert.Equal("Invalid", errors[0].Name); + } + + [Fact(DisplayName = "Navigation helpers respect bounds")] + public void NavigationHelpers_RespectBounds() + { + Assert.False(ValidationDisplayUiHelper.CanNavigateToPreviousError(0)); + Assert.True(ValidationDisplayUiHelper.CanNavigateToPreviousError(1)); + Assert.True(ValidationDisplayUiHelper.CanNavigateToNextError(0, errorCount: 3)); + Assert.False(ValidationDisplayUiHelper.CanNavigateToNextError(2, errorCount: 3)); + } + + [Fact(DisplayName = "CollectInvalidSelectedComponents rejects null inputs")] + public void CollectInvalidSelectedComponents_RejectsNullInputs() + { + Assert.Throws(() => + ValidationDisplayUiHelper.CollectInvalidSelectedComponents(null, _ => true)); + Assert.Throws(() => + ValidationDisplayUiHelper.CollectInvalidSelectedComponents( + new List(), + null)); + } + } +}