using System; using System.IO; using Newtonsoft.Json.Linq; using NUnit.Framework; using UnityEditor; using UnityEngine; using UnityEngine.TestTools; using UnityEngine.UIElements; using MCPForUnity.Editor.Tools; using static MCPForUnityTests.Editor.TestUtilities; namespace MCPForUnityTests.Editor.Tools { public class ManageUITests { private const string TempRoot = "Assets/Temp/ManageUITests"; [SetUp] public void SetUp() { EnsureFolder(TempRoot); } [TearDown] public void TearDown() { if (AssetDatabase.IsValidFolder(TempRoot)) { AssetDatabase.DeleteAsset(TempRoot); } CleanupEmptyParentFolders(TempRoot); } // ---- Action validation ---- [Test] public void HandleCommand_MissingAction_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject())); Assert.IsFalse(result.Value("success")); } [Test] public void HandleCommand_UnknownAction_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "explode" })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("Unknown action")); } [Test] public void Ping_ReturnsPong() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "ping" })); Assert.IsTrue(result.Value("success")); Assert.AreEqual("pong", result.Value("message")); } // ---- Create file ---- [Test] public void Create_Uxml_CreatesFile() { string path = $"{TempRoot}/Test_{Guid.NewGuid():N}.uxml"; string content = ""; var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = content, })); Assert.IsTrue(result.Value("success"), result.ToString()); // Verify file was created on disk string fullPath = Path.Combine(Application.dataPath, path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar); Assert.IsTrue(File.Exists(fullPath), $"File should exist at {fullPath}"); // EnsureEditorExtensionMode may inject editor-extension-mode attribute string actual = File.ReadAllText(fullPath); Assert.That(actual, Does.Contain("ui:UXML")); Assert.That(actual, Does.Contain("ui:Label")); } [Test] public void Create_Uss_CreatesFile() { string path = $"{TempRoot}/Test_{Guid.NewGuid():N}.uss"; string content = ".root { background-color: red; }"; var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = content, })); Assert.IsTrue(result.Value("success"), result.ToString()); } [Test] public void Create_InvalidExtension_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = $"{TempRoot}/Test.txt", ["contents"] = "hello", })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain(".uxml or .uss")); } [Test] public void Create_MissingContents_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = $"{TempRoot}/Test.uxml", })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("contents")); } [Test] public void Create_AlreadyExists_ReturnsError() { string path = $"{TempRoot}/Exists_{Guid.NewGuid():N}.uxml"; string content = ""; // Create first time ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = content, }); // Try to create again var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = content, })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("already exists")); } [Test] public void Create_WithBase64EncodedContents_Decodes() { string path = $"{TempRoot}/Encoded_{Guid.NewGuid():N}.uxml"; string content = ""; string encoded = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(content)); var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["encodedContents"] = encoded, ["contentsEncoded"] = true, })); Assert.IsTrue(result.Value("success"), result.ToString()); string fullPath = Path.Combine(Application.dataPath, path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar); string actual = File.ReadAllText(fullPath); // EnsureEditorExtensionMode may inject editor-extension-mode attribute Assert.That(actual, Does.Contain("ui:UXML")); Assert.That(actual, Does.Contain("UnityEngine.UIElements")); } // ---- Read file ---- [Test] public void Read_ExistingFile_ReturnsContents() { string path = $"{TempRoot}/ReadTest_{Guid.NewGuid():N}.uxml"; string content = ""; ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = content, }); var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "read", ["path"] = path, })); Assert.IsTrue(result.Value("success"), result.ToString()); var data = result["data"] as JObject; Assert.IsNotNull(data); // EnsureEditorExtensionMode may inject editor-extension-mode attribute Assert.That(data.Value("contents"), Does.Contain("ui:UXML")); } [Test] public void Read_NonExistentFile_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "read", ["path"] = $"{TempRoot}/DoesNotExist.uxml", })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("not found")); } // ---- Update file ---- [Test] public void Update_ExistingFile_OverwritesContents() { string path = $"{TempRoot}/UpdateTest_{Guid.NewGuid():N}.uss"; string original = ".root { color: red; }"; string updated = ".root { color: blue; font-size: 20px; }"; ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = original, }); var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "update", ["path"] = path, ["contents"] = updated, })); Assert.IsTrue(result.Value("success"), result.ToString()); // Verify content was updated var readResult = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "read", ["path"] = path, })); Assert.AreEqual(updated, readResult["data"].Value("contents")); } [Test] public void Update_NonExistentFile_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "update", ["path"] = $"{TempRoot}/Missing.uxml", ["contents"] = "", })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("not found")); } // ---- Create PanelSettings ---- [Test] public void CreatePanelSettings_CreatesAsset() { string path = $"{TempRoot}/TestPanel_{Guid.NewGuid():N}.asset"; var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create_panel_settings", ["path"] = path, })); Assert.IsTrue(result.Value("success"), result.ToString()); var ps = AssetDatabase.LoadAssetAtPath(path); Assert.IsNotNull(ps, "PanelSettings should exist at the path"); } [Test] public void CreatePanelSettings_AlreadyExists_ReturnsError() { string path = $"{TempRoot}/ExistingPanel_{Guid.NewGuid():N}.asset"; ManageUI.HandleCommand(new JObject { ["action"] = "create_panel_settings", ["path"] = path, }); var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create_panel_settings", ["path"] = path, })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("already exists")); } // ---- Attach UIDocument ---- [Test] public void AttachUIDocument_AddsComponent() { // Create a UXML file first string uxmlPath = $"{TempRoot}/Attach_{Guid.NewGuid():N}.uxml"; ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = uxmlPath, ["contents"] = "", }); AssetDatabase.Refresh(); // Create a test GameObject var go = new GameObject("UITestObject_Attach"); try { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "attach_ui_document", ["target"] = go.name, ["source_asset"] = uxmlPath, })); Assert.IsTrue(result.Value("success"), result.ToString()); var uiDoc = go.GetComponent(); Assert.IsNotNull(uiDoc, "UIDocument component should be attached"); Assert.IsNotNull(uiDoc.visualTreeAsset, "VisualTreeAsset should be assigned"); Assert.IsNotNull(uiDoc.panelSettings, "PanelSettings should be assigned (auto-created)"); } finally { UnityEngine.Object.DestroyImmediate(go); } } [Test] public void AttachUIDocument_MissingTarget_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "attach_ui_document", ["source_asset"] = "Assets/UI/Test.uxml", })); Assert.IsFalse(result.Value("success")); } [Test] public void AttachUIDocument_MissingSourceAsset_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "attach_ui_document", ["target"] = "SomeObject", })); Assert.IsFalse(result.Value("success")); } // ---- Get Visual Tree ---- [Test] public void GetVisualTree_MissingTarget_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "get_visual_tree", })); Assert.IsFalse(result.Value("success")); } [Test] public void GetVisualTree_NoUIDocument_ReturnsError() { var go = new GameObject("UITestObject_NoDoc"); try { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "get_visual_tree", ["target"] = go.name, })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("UIDocument")); } finally { UnityEngine.Object.DestroyImmediate(go); } } // ---- Delete file ---- [Test] public void Delete_ExistingFile_DeletesFile() { string path = $"{TempRoot}/Delete_{Guid.NewGuid():N}.uss"; ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = ".root { color: red; }", }); var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "delete", ["path"] = path, })); Assert.IsTrue(result.Value("success"), result.ToString()); string fullPath = Path.Combine(Application.dataPath, path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar); Assert.IsFalse(File.Exists(fullPath), "File should be deleted"); } [Test] public void Delete_NonExistentFile_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "delete", ["path"] = $"{TempRoot}/Missing.uxml", })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("not found")); } [Test] public void Delete_InvalidExtension_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "delete", ["path"] = $"{TempRoot}/File.txt", })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain(".uxml or .uss")); } // ---- List UI assets ---- [Test] public void List_ReturnsUIAssets() { string uxmlPath = $"{TempRoot}/ListTest_{Guid.NewGuid():N}.uxml"; string ussPath = $"{TempRoot}/ListTest_{Guid.NewGuid():N}.uss"; ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = uxmlPath, ["contents"] = "", }); ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = ussPath, ["contents"] = ".root { }", }); var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "list", ["path"] = TempRoot, })); Assert.IsTrue(result.Value("success"), result.ToString()); var data = result["data"] as JObject; Assert.IsNotNull(data); int total = data.Value("total"); Assert.GreaterOrEqual(total, 2, "Should find at least 2 UI assets"); } [Test] public void List_WithFilterType_FiltersResults() { string uxmlPath = $"{TempRoot}/FilterTest_{Guid.NewGuid():N}.uxml"; ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = uxmlPath, ["contents"] = "", }); var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "list", ["path"] = TempRoot, ["filterType"] = "uxml", })); Assert.IsTrue(result.Value("success"), result.ToString()); var assets = result["data"]["assets"] as JArray; Assert.IsNotNull(assets); foreach (var asset in assets) { Assert.AreEqual("uxml", asset.Value("type")); } } // ---- Detach UIDocument ---- [Test] public void DetachUIDocument_RemovesComponent() { string uxmlPath = $"{TempRoot}/Detach_{Guid.NewGuid():N}.uxml"; ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = uxmlPath, ["contents"] = "", }); AssetDatabase.Refresh(); var go = new GameObject("UITestObject_Detach"); try { ManageUI.HandleCommand(new JObject { ["action"] = "attach_ui_document", ["target"] = go.name, ["source_asset"] = uxmlPath, }); Assert.IsNotNull(go.GetComponent(), "UIDocument should be attached"); var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "detach_ui_document", ["target"] = go.name, })); Assert.IsTrue(result.Value("success"), result.ToString()); Assert.IsNull(go.GetComponent(), "UIDocument should be removed"); } finally { UnityEngine.Object.DestroyImmediate(go); } } [Test] public void DetachUIDocument_NoUIDocument_ReturnsError() { var go = new GameObject("UITestObject_DetachNoDoc"); try { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "detach_ui_document", ["target"] = go.name, })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("UIDocument")); } finally { UnityEngine.Object.DestroyImmediate(go); } } [Test] public void DetachUIDocument_MissingTarget_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "detach_ui_document", })); Assert.IsFalse(result.Value("success")); } // ---- Modify visual element ---- [Test] public void ModifyVisualElement_MissingTarget_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "modify_visual_element", ["elementName"] = "test", })); Assert.IsFalse(result.Value("success")); } [Test] public void ModifyVisualElement_MissingElementName_ReturnsError() { var go = new GameObject("UITestObject_ModifyNoName"); try { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "modify_visual_element", ["target"] = go.name, })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("element_name")); } finally { UnityEngine.Object.DestroyImmediate(go); } } [Test] public void ModifyVisualElement_NoUIDocument_ReturnsError() { var go = new GameObject("UITestObject_ModifyNoDoc"); try { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "modify_visual_element", ["target"] = go.name, ["elementName"] = "test", })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("UIDocument")); } finally { UnityEngine.Object.DestroyImmediate(go); } } // ---- UXML validation ---- [Test] public void Create_MalformedXml_ReturnsError_FileNotWritten() { string path = $"{TempRoot}/Malformed_{Guid.NewGuid():N}.uxml"; string badContent = ""; var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = badContent, })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("Malformed XML")); // Verify file was NOT written string fullPath = Path.Combine(Application.dataPath, path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar); Assert.IsFalse(File.Exists(fullPath), "Malformed UXML should not be written to disk"); } [Test] public void Create_MissingNamespace_WritesWithWarning() { string path = $"{TempRoot}/NoNs_{Guid.NewGuid():N}.uxml"; string content = ""; // Unity's UXML importer logs an error for undeclared 'ui' prefix LogAssert.ignoreFailingMessages = true; try { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = content, })); Assert.IsTrue(result.Value("success"), result.ToString()); var data = result["data"] as JObject; Assert.IsNotNull(data); var warnings = data["validationWarnings"] as JArray; Assert.IsNotNull(warnings, "Should have validationWarnings"); Assert.That(warnings.ToString(), Does.Contain("Missing namespace")); } finally { LogAssert.ignoreFailingMessages = false; } } [Test] public void Create_ValidUxml_NoWarnings() { string path = $"{TempRoot}/Valid_{Guid.NewGuid():N}.uxml"; string content = ""; var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = content, })); Assert.IsTrue(result.Value("success"), result.ToString()); var data = result["data"] as JObject; Assert.IsNull(data?["validationWarnings"], "Fully valid UXML should not have validationWarnings"); } [Test] public void Create_WrongRootElement_WritesWithWarning() { string path = $"{TempRoot}/WrongRoot_{Guid.NewGuid():N}.uxml"; string content = "
"; // Unity's UXML importer logs an error about expected root element LogAssert.ignoreFailingMessages = true; try { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = content, })); Assert.IsTrue(result.Value("success"), result.ToString()); var data = result["data"] as JObject; var warnings = data["validationWarnings"] as JArray; Assert.IsNotNull(warnings, "Should have validationWarnings"); Assert.That(warnings.ToString(), Does.Contain("Root element")); } finally { LogAssert.ignoreFailingMessages = false; } } [Test] public void Create_EmptyContent_ReturnsError() { string path = $"{TempRoot}/Empty_{Guid.NewGuid():N}.uxml"; var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = " ", })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("empty")); } [Test] public void Update_MalformedXml_ReturnsError_FileNotChanged() { string path = $"{TempRoot}/UpdateMalformed_{Guid.NewGuid():N}.uxml"; string original = ""; ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = original, }); string badContent = ""; var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "update", ["path"] = path, ["contents"] = badContent, })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("Malformed XML")); // Verify original content was preserved (EnsureEditorExtensionMode may have injected attribute) string fullPath = Path.Combine(Application.dataPath, path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar); string actual = File.ReadAllText(fullPath); Assert.That(actual, Does.Contain("ui:UXML"), "Original file content should be preserved"); Assert.That(actual, Does.Not.Contain(""), "Malformed content should not be written"); } [Test] public void Create_Uss_SkipsUxmlValidation() { string path = $"{TempRoot}/NoValidation_{Guid.NewGuid():N}.uss"; // USS is CSS-like, not XML — validation should be skipped string content = "This is not valid XML "; // Unity 6000.4+'s USS importer logs an error on this content; the test only // verifies that ManageUI itself doesn't pre-validate .uss as UXML, not the // downstream importer's behavior. LogAssert.ignoreFailingMessages = true; try { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, ["contents"] = content, })); Assert.IsTrue(result.Value("success"), result.ToString()); } finally { LogAssert.ignoreFailingMessages = false; } } // ---- Path traversal validation ---- [Test] public void Create_TraversalPath_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = "Assets/../etc/evil.uxml", ["contents"] = "", })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("traversal")); } [Test] public void Create_DotDotInMiddle_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = "Assets/UI/../../secret.uxml", ["contents"] = "", })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("traversal")); } [Test] public void Read_TraversalPath_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "read", ["path"] = "Assets/../secret.uxml", })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("traversal")); } [Test] public void Update_TraversalPath_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "update", ["path"] = "Assets/../../etc/passwd.uxml", ["contents"] = "overwrite", })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("traversal")); } [Test] public void Delete_TraversalPath_ReturnsError() { var result = ToJObject(ManageUI.HandleCommand(new JObject { ["action"] = "delete", ["path"] = "Assets/../outside.uxml", })); Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("traversal")); } } }