Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/knowledgebase/gui-architecture-deferred.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
Original file line number Diff line number Diff line change
@@ -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`
30 changes: 15 additions & 15 deletions src/ModSync.GUI/Services/ValidationDisplayService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,8 @@ public void ShowValidationResults(
List<ModComponent> 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)
{
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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);
Expand Down Expand Up @@ -211,7 +211,7 @@ public void NavigateToPreviousError(
Button prevErrorButton,
Button nextErrorButton)
{
if (_currentErrorIndex > 0)
if (ValidationDisplayUiHelper.CanNavigateToPreviousError(_currentErrorIndex))
{
_currentErrorIndex--;
UpdateErrorDisplay(
Expand All @@ -234,7 +234,7 @@ public void NavigateToNextError(
Button prevErrorButton,
Button nextErrorButton)
{
if (_currentErrorIndex < _validationErrors.Count - 1)
if (ValidationDisplayUiHelper.CanNavigateToNextError(_currentErrorIndex, _validationErrors.Count))
{
_currentErrorIndex++;
UpdateErrorDisplay(
Expand Down
53 changes: 53 additions & 0 deletions src/ModSync.GUI/Services/ValidationDisplayUiHelper.cs
Original file line number Diff line number Diff line change
@@ -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<ModComponent> CollectInvalidSelectedComponents(
IEnumerable<ModComponent> selectedComponents,
Func<ModComponent, bool> isComponentValid)
{
if (selectedComponents is null)
{
throw new ArgumentNullException(nameof(selectedComponents));
}

if (isComponentValid is null)
{
throw new ArgumentNullException(nameof(isComponentValid));
}

var validationErrors = new List<ModComponent>();
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;
}
}
72 changes: 72 additions & 0 deletions src/ModSync.Tests/ValidationDisplayUiHelperTests.cs
Original file line number Diff line number Diff line change
@@ -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<ModComponent>
{
new ModComponent { Name = "Valid" },
new ModComponent { Name = "Invalid" },
};

List<ModComponent> 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<ArgumentNullException>(() =>
ValidationDisplayUiHelper.CollectInvalidSelectedComponents(null, _ => true));
Assert.Throws<ArgumentNullException>(() =>
ValidationDisplayUiHelper.CollectInvalidSelectedComponents(
new List<ModComponent>(),
null));
}
}
}
Loading