unity-mcp/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/Characterization/Windows_Characterization.cs

701 lines
28 KiB
C#

using System;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using MCPForUnity.Editor.Windows;
using MCPForUnity.Editor.Windows.Components.Connection;
using MCPForUnity.Editor.Constants;
using UnityEngine.UIElements;
namespace MCPForUnityTests.Editor.Windows.Characterization
{
/// <summary>
/// Characterization tests for Windows & UI domain.
/// These tests capture CURRENT behavior without refactoring.
/// They serve as a regression baseline for future refactoring work.
///
/// Based on analysis in: MCPForUnity/Editor/Windows/Tests/CHARACTERIZATION_ANALYSIS.md
///
/// Covers: MCPSetupWindow, EditorPrefsWindow, McpConnectionSection, and component patterns
/// </summary>
[TestFixture]
public class WindowsCharacterizationTests
{
#region Section 1: EditorPrefsWindow Tests (3 tests)
/// <summary>
/// Current behavior: EditorPrefsWindow caches 2 base UI elements (ScrollView, Container)
/// plus N dynamic items created from EditorPrefs.
/// </summary>
[Test]
public void EditorPrefsWindow_CachesBaseUIElements_ScrollViewAndContainer()
{
// Verify field existence for base UI caching
var type = typeof(EditorPrefsWindow);
var scrollViewField = type.GetField("scrollView", BindingFlags.NonPublic | BindingFlags.Instance);
var containerField = type.GetField("prefsContainer", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(scrollViewField, "Should have scrollView field");
Assert.IsNotNull(containerField, "Should have prefsContainer field");
Assert.AreEqual(typeof(ScrollView), scrollViewField.FieldType);
Assert.AreEqual(typeof(VisualElement), containerField.FieldType);
Assert.Pass("EditorPrefsWindow caches 2 base UI elements: ScrollView + Container");
}
/// <summary>
/// Current behavior: EditorPrefsWindow uses type detection logic to identify
/// whether an EditorPref is Bool, Int, Float, or String.
/// </summary>
[Test]
public void EditorPrefsWindow_UsesTypeDetectionLogic_ForUnknownPrefs()
{
// Document the type detection approach
var detectionSteps = new[]
{
"1. Check knownPrefTypes dictionary for known keys",
"2. For unknown keys: EditorPrefs.GetString() first",
"3. Try int.TryParse()",
"4. Try float.TryParse()",
"5. Try bool.TryParse()",
"6. Default to String if all fail"
};
// Verify the enum exists
var type = typeof(EditorPrefsWindow);
var enumType = type.Assembly.GetType("MCPForUnity.Editor.Windows.EditorPrefType");
Assert.IsNotNull(enumType, "Should have EditorPrefType enum");
var enumValues = Enum.GetNames(enumType);
Assert.Contains("String", enumValues);
Assert.Contains("Int", enumValues);
Assert.Contains("Float", enumValues);
Assert.Contains("Bool", enumValues);
Assert.Pass($"Type detection flow: {string.Join("; ", detectionSteps)}");
}
/// <summary>
/// Current behavior: EditorPrefsWindow registers callbacks per-item for Save buttons
/// rather than using RegisterValueChangedCallback pattern.
/// </summary>
[Test]
public void EditorPrefsWindow_RegistersPerItemCallbacks_ForSaveButtons()
{
// Document the callback pattern
var type = typeof(EditorPrefsWindow);
var createItemMethod = type.GetMethod("CreateItemUI", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(createItemMethod, "Should have CreateItemUI method");
var pattern = "Each item gets: saveButton.clicked += () => SavePref(item, value, type)";
Assert.Pass($"Callback pattern: {pattern}");
}
#endregion
#region Section 2: MCPSetupWindow Tests (3 tests)
/// <summary>
/// Current behavior: MCPSetupWindow caches 13+ UI elements in CreateGUI
/// without a separate CacheUIElements method.
/// </summary>
[Test]
public void MCPSetupWindow_CachesMultipleUIElements_InCreateGUI()
{
var type = typeof(MCPSetupWindow);
var fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
// Count VisualElement-related fields
var uiFields = fields.Where(f =>
f.FieldType == typeof(VisualElement) ||
f.FieldType == typeof(Label) ||
f.FieldType == typeof(Button)
).ToArray();
Assert.GreaterOrEqual(uiFields.Length, 10, "Should have 10+ UI element fields");
var expectedFields = new[]
{
"pythonIndicator", "pythonVersion", "pythonDetails",
"uvIndicator", "uvVersion", "uvDetails",
"statusMessage", "installationSection",
"openPythonLinkButton", "openUvLinkButton",
"refreshButton", "doneButton"
};
Assert.Pass($"MCPSetupWindow caches {uiFields.Length} UI elements including: {string.Join(", ", expectedFields.Take(5))}...");
}
/// <summary>
/// Current behavior: MCPSetupWindow modifies CSS class lists to show status
/// (adds/removes "valid"/"invalid" classes on indicators).
/// </summary>
[Test]
public void MCPSetupWindow_ModifiesClassListForStatus_ValidInvalidPattern()
{
var type = typeof(MCPSetupWindow);
var method = type.GetMethod("UpdateDependencyStatus", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(method, "Should have UpdateDependencyStatus method");
var classListPattern = new[]
{
"indicator.RemoveFromClassList(\"invalid\")",
"indicator.AddToClassList(\"valid\")",
"Or vice versa for unavailable dependencies"
};
Assert.Pass($"Class list modification: {string.Join("; ", classListPattern)}");
}
/// <summary>
/// Current behavior: MCPSetupWindow uses simple direct callback registration
/// (button.clicked += method) without RegisterValueChangedCallback.
/// </summary>
[Test]
public void MCPSetupWindow_UsesDirectCallbackRegistration_ForButtons()
{
var type = typeof(MCPSetupWindow);
var createGuiMethod = type.GetMethod("CreateGUI", BindingFlags.Public | BindingFlags.Instance);
Assert.IsNotNull(createGuiMethod, "Should have CreateGUI method");
var pattern = new[]
{
"refreshButton.clicked += OnRefreshClicked",
"doneButton.clicked += OnDoneClicked",
"openPythonLinkButton.clicked += OnOpenPythonInstallClicked",
"openUvLinkButton.clicked += OnOpenUvInstallClicked"
};
Assert.Pass($"Direct callback pattern: {string.Join("; ", pattern)}");
}
#endregion
#region Section 3: McpConnectionSection Tests (6 tests)
/// <summary>
/// Current behavior: McpConnectionSection caches 13+ UI elements in CacheUIElements method.
/// This is the three-phase pattern Phase 1.
/// </summary>
[Test]
public void McpConnectionSection_CachesLargeNumberOfUIElements_InCacheMethod()
{
var type = typeof(McpConnectionSection);
var cacheMethod = type.GetMethod("CacheUIElements", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(cacheMethod, "Should have CacheUIElements method");
var fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
var uiFields = fields.Where(f =>
typeof(VisualElement).IsAssignableFrom(f.FieldType) ||
typeof(Button).IsAssignableFrom(f.FieldType) ||
typeof(TextField).IsAssignableFrom(f.FieldType)
).ToArray();
Assert.GreaterOrEqual(uiFields.Length, 10, "Should have 10+ UI element fields");
var examples = new[]
{
"transportDropdown", "httpUrlField", "unityPortField",
"statusIndicator", "connectionStatusLabel", "connectionToggleButton"
};
Assert.Pass($"McpConnectionSection caches {uiFields.Length} UI elements. Examples: {string.Join(", ", examples)}");
}
/// <summary>
/// Current behavior: McpConnectionSection reads 3+ EditorPrefs in InitializeUI
/// (UseHttpTransport, HttpTransportScope, UnitySocketPort).
/// </summary>
[Test]
public void McpConnectionSection_ReadsMultipleEditorPrefs_InInitializeUI()
{
var prefKeys = new[]
{
EditorPrefKeys.UseHttpTransport,
EditorPrefKeys.HttpTransportScope,
EditorPrefKeys.UnitySocketPort
};
foreach (var key in prefKeys)
{
Assert.IsNotEmpty(key, $"EditorPrefKey should not be empty: {key}");
}
Assert.Pass($"McpConnectionSection reads EditorPrefs: {string.Join(", ", prefKeys)}");
}
/// <summary>
/// Current behavior: McpConnectionSection uses EnumField.RegisterValueChangedCallback
/// for the transport dropdown with complex multi-step handler.
/// </summary>
[Test]
public void McpConnectionSection_UsesEnumFieldValueChangedCallback_ForTransport()
{
var type = typeof(McpConnectionSection);
var registerMethod = type.GetMethod("RegisterCallbacks", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(registerMethod, "Should have RegisterCallbacks method");
var callbackSteps = new[]
{
"1. Get previous and new transport values",
"2. Persist UseHttpTransport to EditorPrefs",
"3. Persist HttpTransportScope if HTTP",
"4. Clear resume flags (ResumeStdioAfterReload, ResumeHttpAfterReload)",
"5. Update UI visibility",
"6. Invoke OnManualConfigUpdateRequested event",
"7. Invoke OnTransportChanged event",
"8. Stop opposing transport if switching HTTP<->Stdio"
};
Assert.Pass($"Transport callback flow: {string.Join("; ", callbackSteps)}");
}
/// <summary>
/// Current behavior: McpConnectionSection uses FocusOutEvent to persist HTTP URL
/// (not every keystroke, only on focus loss).
/// </summary>
[Test]
public void McpConnectionSection_UsesFocusOutEvent_ToPersistHttpUrl()
{
var type = typeof(McpConnectionSection);
var persistMethod = type.GetMethod("PersistHttpUrlFromField", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(persistMethod, "Should have PersistHttpUrlFromField method");
var pattern = new[]
{
"httpUrlField.RegisterCallback<FocusOutEvent>(_ => PersistHttpUrlFromField())",
"Avoids fighting user during typing",
"Normalizes URL on commit"
};
Assert.Pass($"FocusOut pattern: {string.Join("; ", pattern)}");
}
/// <summary>
/// Current behavior: McpConnectionSection uses KeyDownEvent with KeyCode.Return check
/// to persist values on Enter key press.
/// </summary>
[Test]
public void McpConnectionSection_UsesKeyDownEvent_WithReturnKeyCheck()
{
var pattern = new[]
{
"field.RegisterCallback<KeyDownEvent>(evt => {...})",
"if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)",
"PersistValue(); evt.StopPropagation();"
};
Assert.Pass($"KeyDown pattern: {string.Join("; ", pattern)}");
}
/// <summary>
/// Current behavior: McpConnectionSection raises events for inter-component communication
/// (OnManualConfigUpdateRequested, OnTransportChanged).
/// </summary>
[Test]
public void McpConnectionSection_RaisesEvents_ForInterComponentCommunication()
{
var type = typeof(McpConnectionSection);
var events = type.GetEvents(BindingFlags.Public | BindingFlags.Instance);
var eventNames = events.Select(e => e.Name).ToArray();
Assert.Contains("OnManualConfigUpdateRequested", eventNames);
Assert.Contains("OnTransportChanged", eventNames);
Assert.Pass($"McpConnectionSection events: {string.Join(", ", eventNames)}");
}
#endregion
#region Section 4: McpAdvancedSection Tests (4 tests)
/// <summary>
/// Current behavior: McpAdvancedSection (if it exists) caches 20+ UI elements
/// for paths, toggles, buttons, status, and labels.
/// </summary>
[Test]
public void McpAdvancedSection_CachesLargeUIElementSet_IfExists()
{
// Try to find McpAdvancedSection type
var type = typeof(MCPSetupWindow).Assembly.GetTypes()
.FirstOrDefault(t => t.Name == "McpAdvancedSection");
if (type == null)
{
Assert.Inconclusive("McpAdvancedSection type not found - may be in different namespace or refactored");
return;
}
var fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
var uiFields = fields.Where(f =>
typeof(VisualElement).IsAssignableFrom(f.FieldType) ||
typeof(Button).IsAssignableFrom(f.FieldType) ||
typeof(TextField).IsAssignableFrom(f.FieldType) ||
typeof(Toggle).IsAssignableFrom(f.FieldType)
).ToArray();
Assert.Pass($"McpAdvancedSection caches {uiFields.Length} UI elements");
}
/// <summary>
/// Current behavior: Advanced section reads 5+ EditorPrefs
/// (GitUrl, DebugLogs, DevModeRefresh, paths).
/// </summary>
[Test]
public void McpAdvancedSection_ReadsMultiplePreferences_ForConfiguration()
{
var expectedPrefKeys = new[]
{
EditorPrefKeys.GitUrlOverride,
EditorPrefKeys.DebugLogs,
EditorPrefKeys.DevModeForceServerRefresh,
EditorPrefKeys.PackageDeploySourcePath,
EditorPrefKeys.ClaudeCliPathOverride,
EditorPrefKeys.UvxPathOverride
};
foreach (var key in expectedPrefKeys)
{
Assert.IsNotEmpty(key, $"EditorPrefKey should not be empty");
}
Assert.Pass($"Advanced section uses {expectedPrefKeys.Length} EditorPrefs keys");
}
/// <summary>
/// Current behavior: Advanced section uses Toggle.RegisterValueChangedCallback
/// to persist boolean preferences.
/// </summary>
[Test]
public void McpAdvancedSection_UsesToggleValueChangedCallback_ToPersistBools()
{
var pattern = new[]
{
"toggle.RegisterValueChangedCallback(evt => {...})",
"EditorPrefs.SetBool(Key, evt.newValue)",
"Optional: invoke domain events or refresh UI"
};
Assert.Pass($"Toggle callback pattern: {string.Join("; ", pattern)}");
}
/// <summary>
/// Current behavior: Advanced section modifies CSS class lists dynamically
/// to show/hide validation feedback and status indicators.
/// </summary>
[Test]
public void McpAdvancedSection_ModifiesClassListDynamically_ForValidation()
{
var pattern = new[]
{
"element.AddToClassList(\"valid\")",
"element.RemoveFromClassList(\"invalid\")",
"Used for path validation, status indicators, etc."
};
Assert.Pass($"Dynamic class list pattern: {string.Join("; ", pattern)}");
}
#endregion
#region Section 5: McpClientConfigSection Tests (4 tests)
/// <summary>
/// Current behavior: Client config section caches 11+ UI elements
/// (dropdown, indicators, fields, buttons, foldout).
/// </summary>
[Test]
public void McpClientConfigSection_CachesDropdownAndIndicators_PlusFields()
{
// Try to find McpClientConfigSection type
var type = typeof(MCPSetupWindow).Assembly.GetTypes()
.FirstOrDefault(t => t.Name == "McpClientConfigSection");
if (type == null)
{
Assert.Inconclusive("McpClientConfigSection type not found");
return;
}
var fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
var uiFields = fields.Where(f =>
typeof(VisualElement).IsAssignableFrom(f.FieldType) ||
typeof(Button).IsAssignableFrom(f.FieldType) ||
typeof(DropdownField).IsAssignableFrom(f.FieldType) ||
typeof(Foldout).IsAssignableFrom(f.FieldType)
).ToArray();
Assert.Pass($"McpClientConfigSection caches {uiFields.Length} UI elements");
}
/// <summary>
/// Current behavior: Client config section initializes dropdown choices
/// from available client configurators.
/// </summary>
[Test]
public void McpClientConfigSection_InitializesDropdownChoices_FromConfigurators()
{
var pattern = new[]
{
"dropdown.choices = configuratorList",
"dropdown.index set from current selection",
"Choices populated from service/registry"
};
Assert.Pass($"Dropdown initialization: {string.Join("; ", pattern)}");
}
/// <summary>
/// Current behavior: Client config section uses DisplayStyle.None/Flex
/// for conditional visibility of dependent UI elements.
/// </summary>
[Test]
public void McpClientConfigSection_UsesDisplayStyleToggle_ForConditionalVisibility()
{
var pattern = new[]
{
"element.style.display = DisplayStyle.None",
"element.style.display = DisplayStyle.Flex",
"Used for showing/hiding config fields based on dropdown selection"
};
Assert.Pass($"DisplayStyle pattern: {string.Join("; ", pattern)}");
}
/// <summary>
/// Current behavior: Client config dropdown triggers cascading updates
/// to dependent fields and indicators when selection changes.
/// </summary>
[Test]
public void McpClientConfigSection_DropdownTriggersCascadingUpdates_OnChange()
{
var updateFlow = new[]
{
"1. dropdown.RegisterValueChangedCallback(evt => {...})",
"2. Load config for selected client",
"3. Update dependent fields (URL, status, etc.)",
"4. Show/hide sections based on selection",
"5. Invoke update events for other components"
};
Assert.Pass($"Cascading update flow: {string.Join("; ", updateFlow)}");
}
#endregion
#region Section 6: Cross-Pattern Tests (5 tests)
/// <summary>
/// Current behavior: Three-phase pattern (Cache-Initialize-Register) appears
/// in 5+ window/component classes.
/// </summary>
[Test]
public void CrossPattern_ThreePhaseLifecycle_RepeatsAcrossComponents()
{
var componentsWithPattern = new[]
{
"McpConnectionSection (has explicit methods)",
"McpAdvancedSection (likely)",
"McpClientConfigSection (likely)",
"MCPSetupWindow (embedded in CreateGUI)",
"McpToolsSection (likely)"
};
var phases = new[]
{
"Phase 1: CacheUIElements() - Root.Q<T>() queries",
"Phase 2: InitializeUI() - EditorPrefs reads + defaults",
"Phase 3: RegisterCallbacks() - Event handler setup"
};
Assert.Pass($"Pattern in {componentsWithPattern.Length} components: {string.Join(" -> ", phases)}");
}
/// <summary>
/// Current behavior: EditorPrefs binding has 5 distinct variation patterns
/// (Bool, String, Int, Key Deletion, Scope-Aware).
/// </summary>
[Test]
public void CrossPattern_EditorPrefsBinding_HasFiveVariations()
{
var variations = new[]
{
"1. Simple Boolean: GetBool/SetBool with toggle callbacks",
"2. String URL/Path: GetString/SetString with FocusOut",
"3. Integer Port: GetInt/SetInt with KeyDown validation",
"4. Key Deletion: DeleteKey() for clearing overrides",
"5. Scope-Aware: Conditional logic based on transport scope"
};
Assert.Pass($"EditorPrefs variations: {string.Join("; ", variations)}");
}
/// <summary>
/// Current behavior: Callback registration has 6 distinct patterns
/// (EnumField, Toggle, Button, FocusOut, KeyDown, Event Signal).
/// </summary>
[Test]
public void CrossPattern_CallbackRegistration_HasSixPatterns()
{
var patterns = new[]
{
"1. EnumField.RegisterValueChangedCallback",
"2. Toggle.RegisterValueChangedCallback",
"3. Button.clicked += handler",
"4. RegisterCallback<FocusOutEvent>",
"5. RegisterCallback<KeyDownEvent> with KeyCode check",
"6. Event Signal Propagation (Action delegates)"
};
Assert.Pass($"Callback patterns: {string.Join("; ", patterns)}");
}
/// <summary>
/// Current behavior: UI-to-EditorPrefs synchronization happens on user input
/// via callbacks (immediate write-through).
/// </summary>
[Test]
public void CrossPattern_UIToEditorPrefsSync_WriteThroughOnInput()
{
var syncFlow = new[]
{
"1. User modifies UI element (toggle, field, dropdown)",
"2. Callback fires immediately",
"3. EditorPrefs.Set* called in callback",
"4. No batching or delayed persistence",
"5. Each change writes immediately to EditorPrefs"
};
Assert.Pass($"Write-through sync: {string.Join(" -> ", syncFlow)}");
}
/// <summary>
/// Current behavior: EditorPrefs-to-UI synchronization happens during InitializeUI
/// (one-time read, no automatic refresh on external pref changes).
/// </summary>
[Test]
public void CrossPattern_EditorPrefsToUISync_OneTimeReadInInitialize()
{
var syncFlow = new[]
{
"1. CreateGUI/Constructor called",
"2. CacheUIElements queries elements",
"3. InitializeUI reads EditorPrefs once",
"4. SetValueWithoutNotify or .value = ... to populate UI",
"5. No automatic refresh if EditorPrefs change externally",
"6. Manual refresh requires RefreshUI() call"
};
Assert.Pass($"One-time read sync: {string.Join(" -> ", syncFlow)}");
}
#endregion
#region Section 7: Visibility and Refresh Logic (2 tests)
/// <summary>
/// Current behavior: Panel switching uses DisplayStyle.None/Flex
/// with EditorPrefs persistence for active panel.
/// </summary>
[Test]
public void VisibilityLogic_PanelSwitching_UsesDisplayStyleWithPersistence()
{
var panelKey = EditorPrefKeys.EditorWindowActivePanel;
Assert.IsNotEmpty(panelKey, "EditorWindowActivePanel key should exist");
var pattern = new[]
{
"1. Read EditorPrefs for active panel",
"2. Set all panels to DisplayStyle.None",
"3. Set selected panel to DisplayStyle.Flex",
"4. On user switch: EditorPrefs.SetString(key, newPanel)",
"5. Persist survives domain reload"
};
Assert.Pass($"Panel switching: {string.Join("; ", pattern)}");
}
/// <summary>
/// Current behavior: Conditional display logic for HTTP fields
/// based on transport selection (show for HTTP, hide for Stdio).
/// </summary>
[Test]
public void VisibilityLogic_ConditionalDisplay_BasedOnTransportSelection()
{
var pattern = new[]
{
"if (isHttpSelected) { httpRows.style.display = Flex; }",
"else { httpRows.style.display = None; }",
"Triggered by transport dropdown value change",
"UpdateHttpFieldVisibility() method pattern"
};
Assert.Pass($"Conditional visibility: {string.Join("; ", pattern)}");
}
#endregion
#region Section 8: Event Signaling Tests (1 test)
/// <summary>
/// Current behavior: Inter-component communication uses C# event pattern
/// (Action delegates, raised with ?. null-conditional operator).
/// </summary>
[Test]
public void EventSignaling_InterComponentCommunication_UsesActionDelegates()
{
var type = typeof(McpConnectionSection);
var events = type.GetEvents(BindingFlags.Public | BindingFlags.Instance);
Assert.GreaterOrEqual(events.Length, 2, "Should have at least 2 events");
var communicationFlow = new[]
{
"1. Component declares: public event Action OnSomethingHappened;",
"2. Raises event: OnSomethingHappened?.Invoke();",
"3. Other component subscribes: connection.OnSomethingHappened += HandleIt;",
"4. Flow: ConnectionSection -> AdvancedSection or ClientConfigSection",
"5. Used for: transport changes, config updates, manual refresh requests"
};
Assert.Pass($"Event signaling: {string.Join(" -> ", communicationFlow)}");
}
#endregion
#region Section 9: Pattern Summary Tests (Bonus documentation)
/// <summary>
/// Summary: Document total pattern repetition metrics across the Windows/UI domain.
/// </summary>
[Test]
public void PatternSummary_TotalRepetitionMetrics_AcrossDomain()
{
var metrics = new[]
{
"Window Classes: 3 (MCPForUnityEditorWindow, MCPSetupWindow, EditorPrefsWindow)",
"Component Classes: 4+ (Connection, Advanced, ClientConfig, Tools)",
"CacheUIElements Calls: 5+ (one per component)",
"EditorPrefs Bindings: 60+ (scattered across all classes)",
"Callback Registrations: 50+ (scattered across all classes)",
"UI Element Queries (Q<T>): 100+ (mostly duplicated patterns)",
"Three-Phase Pattern Instances: 14+ (all significant classes)",
"EditorPrefs Get Calls: 40+ (InitializeUI methods)",
"EditorPrefs Set Calls: 45+ (callback handlers)",
"Toggle Callbacks: 8+ (separate implementations)",
"Button Clicks: 15+ (separate implementations)"
};
Assert.Pass($"Domain-wide metrics:\n{string.Join("\n", metrics)}");
}
#endregion
}
}