904 lines
30 KiB
C#
904 lines
30 KiB
C#
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<bool>("success"));
|
|
}
|
|
|
|
[Test]
|
|
public void HandleCommand_UnknownAction_ReturnsError()
|
|
{
|
|
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
|
{
|
|
["action"] = "explode"
|
|
}));
|
|
Assert.IsFalse(result.Value<bool>("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<bool>("success"));
|
|
Assert.AreEqual("pong", result.Value<string>("message"));
|
|
}
|
|
|
|
// ---- Create file ----
|
|
|
|
[Test]
|
|
public void Create_Uxml_CreatesFile()
|
|
{
|
|
string path = $"{TempRoot}/Test_{Guid.NewGuid():N}.uxml";
|
|
string content = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"Hi\" /></ui:UXML>";
|
|
|
|
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
|
{
|
|
["action"] = "create",
|
|
["path"] = path,
|
|
["contents"] = content,
|
|
}));
|
|
|
|
Assert.IsTrue(result.Value<bool>("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<bool>("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<bool>("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<bool>("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 = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />";
|
|
|
|
// 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<bool>("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 = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />";
|
|
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<bool>("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 = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />";
|
|
|
|
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<bool>("success"), result.ToString());
|
|
var data = result["data"] as JObject;
|
|
Assert.IsNotNull(data);
|
|
// EnsureEditorExtensionMode may inject editor-extension-mode attribute
|
|
Assert.That(data.Value<string>("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<bool>("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<bool>("success"), result.ToString());
|
|
|
|
// Verify content was updated
|
|
var readResult = ToJObject(ManageUI.HandleCommand(new JObject
|
|
{
|
|
["action"] = "read",
|
|
["path"] = path,
|
|
}));
|
|
Assert.AreEqual(updated, readResult["data"].Value<string>("contents"));
|
|
}
|
|
|
|
[Test]
|
|
public void Update_NonExistentFile_ReturnsError()
|
|
{
|
|
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
|
{
|
|
["action"] = "update",
|
|
["path"] = $"{TempRoot}/Missing.uxml",
|
|
["contents"] = "<ui:UXML />",
|
|
}));
|
|
|
|
Assert.IsFalse(result.Value<bool>("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<bool>("success"), result.ToString());
|
|
|
|
var ps = AssetDatabase.LoadAssetAtPath<PanelSettings>(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<bool>("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"] = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"Test\" /></ui:UXML>",
|
|
});
|
|
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<bool>("success"), result.ToString());
|
|
|
|
var uiDoc = go.GetComponent<UIDocument>();
|
|
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<bool>("success"));
|
|
}
|
|
|
|
[Test]
|
|
public void AttachUIDocument_MissingSourceAsset_ReturnsError()
|
|
{
|
|
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
|
{
|
|
["action"] = "attach_ui_document",
|
|
["target"] = "SomeObject",
|
|
}));
|
|
|
|
Assert.IsFalse(result.Value<bool>("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<bool>("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<bool>("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<bool>("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<bool>("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<bool>("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"] = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />",
|
|
});
|
|
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<bool>("success"), result.ToString());
|
|
var data = result["data"] as JObject;
|
|
Assert.IsNotNull(data);
|
|
int total = data.Value<int>("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"] = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />",
|
|
});
|
|
|
|
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
|
{
|
|
["action"] = "list",
|
|
["path"] = TempRoot,
|
|
["filterType"] = "uxml",
|
|
}));
|
|
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
var assets = result["data"]["assets"] as JArray;
|
|
Assert.IsNotNull(assets);
|
|
foreach (var asset in assets)
|
|
{
|
|
Assert.AreEqual("uxml", asset.Value<string>("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"] = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"Test\" /></ui:UXML>",
|
|
});
|
|
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>(), "UIDocument should be attached");
|
|
|
|
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
|
{
|
|
["action"] = "detach_ui_document",
|
|
["target"] = go.name,
|
|
}));
|
|
|
|
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
|
|
Assert.IsNull(go.GetComponent<UIDocument>(), "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<bool>("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<bool>("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<bool>("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<bool>("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<bool>("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 = "<ui:UXML><ui:Label text=\"unclosed\">";
|
|
|
|
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
|
{
|
|
["action"] = "create",
|
|
["path"] = path,
|
|
["contents"] = badContent,
|
|
}));
|
|
|
|
Assert.IsFalse(result.Value<bool>("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 = "<ui:UXML><ui:Label text=\"hi\" /></ui:UXML>";
|
|
|
|
// 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<bool>("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 = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"ok\" /></ui:UXML>";
|
|
|
|
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
|
{
|
|
["action"] = "create",
|
|
["path"] = path,
|
|
["contents"] = content,
|
|
}));
|
|
|
|
Assert.IsTrue(result.Value<bool>("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 = "<div xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"hi\" /></div>";
|
|
|
|
// 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<bool>("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<bool>("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 = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />";
|
|
|
|
ManageUI.HandleCommand(new JObject
|
|
{
|
|
["action"] = "create",
|
|
["path"] = path,
|
|
["contents"] = original,
|
|
});
|
|
|
|
string badContent = "<ui:UXML><broken>";
|
|
var result = ToJObject(ManageUI.HandleCommand(new JObject
|
|
{
|
|
["action"] = "update",
|
|
["path"] = path,
|
|
["contents"] = badContent,
|
|
}));
|
|
|
|
Assert.IsFalse(result.Value<bool>("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("<broken>"), "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 <broken>";
|
|
|
|
// 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<bool>("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"] = "<ui:UXML />",
|
|
}));
|
|
|
|
Assert.IsFalse(result.Value<bool>("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"] = "<ui:UXML />",
|
|
}));
|
|
|
|
Assert.IsFalse(result.Value<bool>("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<bool>("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<bool>("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<bool>("success"));
|
|
Assert.That(result["error"].ToString(), Does.Contain("traversal"));
|
|
}
|
|
}
|
|
}
|