diff --git a/UnityProjects/MRTKDevTemplate/Packages/manifest.json b/UnityProjects/MRTKDevTemplate/Packages/manifest.json
index 16cea28ea..fc096fc1a 100644
--- a/UnityProjects/MRTKDevTemplate/Packages/manifest.json
+++ b/UnityProjects/MRTKDevTemplate/Packages/manifest.json
@@ -82,6 +82,7 @@
"org.mixedrealitytoolkit.input",
"org.mixedrealitytoolkit.spatialmanipulation",
"org.mixedrealitytoolkit.standardassets",
+ "org.mixedrealitytoolkit.theming",
"org.mixedrealitytoolkit.uxcomponents",
"org.mixedrealitytoolkit.uxcomponents.noncanvas",
"org.mixedrealitytoolkit.uxcore",
diff --git a/org.mixedrealitytoolkit.theming/CHANGELOG.md b/org.mixedrealitytoolkit.theming/CHANGELOG.md
index 52880eb4c..febb779dc 100644
--- a/org.mixedrealitytoolkit.theming/CHANGELOG.md
+++ b/org.mixedrealitytoolkit.theming/CHANGELOG.md
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
* Added serializable theme item data types for booleans. [PR #1130](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1130)
* Added built-in binders for skybox materials and behaviour enabled states. [PR #1130](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1130)
+* Added edit mode tests for validating custom theming types and their dependencies. [PR #1133](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1133)
## [1.0.0-pre.1] - 2026-05-20
diff --git a/org.mixedrealitytoolkit.theming/Tests.meta b/org.mixedrealitytoolkit.theming/Tests.meta
new file mode 100644
index 000000000..0aa4b53d4
--- /dev/null
+++ b/org.mixedrealitytoolkit.theming/Tests.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f15c71cd4449f464592b9317d8df9037
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/org.mixedrealitytoolkit.theming/Tests/Editor.meta b/org.mixedrealitytoolkit.theming/Tests/Editor.meta
new file mode 100644
index 000000000..28c724e82
--- /dev/null
+++ b/org.mixedrealitytoolkit.theming/Tests/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 05341ca71d86b4749837802d9272b087
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/org.mixedrealitytoolkit.theming/Tests/Editor/AssemblyInfo.cs b/org.mixedrealitytoolkit.theming/Tests/Editor/AssemblyInfo.cs
new file mode 100644
index 000000000..a01411c1b
--- /dev/null
+++ b/org.mixedrealitytoolkit.theming/Tests/Editor/AssemblyInfo.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Mixed Reality Toolkit Contributors
+// Licensed under the BSD 3-Clause
+
+using System.Reflection;
+
+[assembly: AssemblyProduct("Mixed Reality Toolkit UX Theming Editor Tests")]
+[assembly: AssemblyCopyright("Copyright (c) Mixed Reality Toolkit Contributors")]
+
+// The AssemblyVersion attribute is checked-in and is recommended not to be changed often.
+// https://docs.microsoft.com/troubleshoot/visualstudio/general/assembly-version-assembly-file-version
+// AssemblyFileVersion and AssemblyInformationalVersion are added by pack-upm.ps1 to match the current MRTK build version.
+[assembly: AssemblyVersion("1.0.0.0")]
diff --git a/org.mixedrealitytoolkit.theming/Tests/Editor/AssemblyInfo.cs.meta b/org.mixedrealitytoolkit.theming/Tests/Editor/AssemblyInfo.cs.meta
new file mode 100644
index 000000000..d5ad5babc
--- /dev/null
+++ b/org.mixedrealitytoolkit.theming/Tests/Editor/AssemblyInfo.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7b447f4a2c57e514ca8ccb9ee04e1d45
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/org.mixedrealitytoolkit.theming/Tests/Editor/BaseThemeBinderTests.cs b/org.mixedrealitytoolkit.theming/Tests/Editor/BaseThemeBinderTests.cs
new file mode 100644
index 000000000..dad7846e6
--- /dev/null
+++ b/org.mixedrealitytoolkit.theming/Tests/Editor/BaseThemeBinderTests.cs
@@ -0,0 +1,125 @@
+// Copyright (c) Mixed Reality Toolkit Contributors
+// Licensed under the BSD 3-Clause
+
+using NUnit.Framework;
+using System;
+using System.Linq;
+using UnityEditor;
+
+namespace MixedReality.Toolkit.Theming.Tests.EditMode
+{
+ ///
+ /// Tests to evaluate the architectural integrity of Theme Binders.
+ ///
+ public class BaseThemeBinderTests
+ {
+ ///
+ /// Verifies that all non-abstract theme binders have fully resolved their generic arguments (T and K).
+ ///
+ [Test]
+ public void ConcreteBindersFullyResolveGenericArguments()
+ {
+ // Quickly grab every class in the project that implements IBinder
+ var binderTypes = TypeCache.GetTypesDerivedFrom(typeof(IBinder))
+ .Where(t => !t.IsAbstract && !t.IsInterface);
+
+ int testedCount = 0;
+
+ foreach (Type type in binderTypes)
+ {
+ Assert.IsFalse(type.ContainsGenericParameters,
+ $"Binder type '{type.Name}' is not abstract, but contains open generic parameters. " +
+ "Concrete binders must fully resolve their BaseThemeBinder data types.");
+
+ Type baseBinderType = type;
+ while (baseBinderType != null && (!baseBinderType.IsGenericType || baseBinderType.GetGenericTypeDefinition() != typeof(BaseThemeBinder<,>)))
+ {
+ baseBinderType = baseBinderType.BaseType;
+ }
+
+ if (baseBinderType != null)
+ {
+ Type valueType = baseBinderType.GenericTypeArguments[0];
+ Type targetType = baseBinderType.GenericTypeArguments[1];
+
+ Assert.IsNotNull(valueType, $"Value type (T) for '{type.Name}' could not be resolved.");
+ Assert.IsNotNull(targetType, $"Target type (K) for '{type.Name}' could not be resolved.");
+
+ testedCount++;
+ }
+ }
+
+ Assert.Greater(testedCount, 0, "No concrete BaseThemeBinder implementations were found to test.");
+ }
+
+ ///
+ /// Verifies that all non-abstract theme item data classes have the [Serializable] attribute.
+ ///
+ [Test]
+ public void ConcreteThemeItemDataClassesAreSerializable()
+ {
+ var itemDataTypes = TypeCache.GetTypesDerivedFrom(typeof(BaseThemeItemData<>))
+ .Where(t => !t.IsAbstract && !t.IsInterface);
+
+ int testedCount = 0;
+ foreach (Type type in itemDataTypes)
+ {
+ Assert.IsTrue(type.IsSerializable,
+ $"Theme item data type '{type.Name}' must have the [Serializable] attribute to be saved in a Theme asset.");
+ testedCount++;
+ }
+
+ Assert.Greater(testedCount, 0, "No concrete BaseThemeItemData implementations were found to test.");
+ }
+
+ ///
+ /// Verifies that every generic data type targeted by a binder has a corresponding concrete ThemeItemData class to hold it.
+ ///
+ [Test]
+ public void BindersHaveCorrespondingThemeItemData()
+ {
+ var binderTypes = TypeCache.GetTypesDerivedFrom(typeof(IBinder))
+ .Where(t => !t.IsAbstract && !t.IsInterface);
+
+ var itemDataTypes = TypeCache.GetTypesDerivedFrom(typeof(BaseThemeItemData<>))
+ .Where(t => !t.IsAbstract && !t.IsInterface).ToList();
+
+ int testedCount = 0;
+ foreach (Type binderType in binderTypes)
+ {
+ Type baseBinderType = binderType;
+ while (baseBinderType != null && (!baseBinderType.IsGenericType || baseBinderType.GetGenericTypeDefinition() != typeof(BaseThemeBinder<,>)))
+ {
+ baseBinderType = baseBinderType.BaseType;
+ }
+
+ if (baseBinderType != null)
+ {
+ Type valueType = baseBinderType.GenericTypeArguments[0];
+
+ bool hasMatchingItemData = false;
+ foreach (Type itemDataType in itemDataTypes)
+ {
+ Type baseItemDataType = itemDataType;
+ while (baseItemDataType != null && (!baseItemDataType.IsGenericType || baseItemDataType.GetGenericTypeDefinition() != typeof(BaseThemeItemData<>)))
+ {
+ baseItemDataType = baseItemDataType.BaseType;
+ }
+
+ if (baseItemDataType != null && baseItemDataType.GenericTypeArguments[0] == valueType)
+ {
+ hasMatchingItemData = true;
+ break;
+ }
+ }
+
+ Assert.IsTrue(hasMatchingItemData,
+ $"Binder '{binderType.Name}' binds to '{valueType.Name}', but no concrete BaseThemeItemData<{valueType.Name}> class exists to serialize it in the Theme asset.");
+ testedCount++;
+ }
+ }
+
+ Assert.Greater(testedCount, 0, "No concrete BaseThemeBinder implementations were found to test.");
+ }
+ }
+}
diff --git a/org.mixedrealitytoolkit.theming/Tests/Editor/BaseThemeBinderTests.cs.meta b/org.mixedrealitytoolkit.theming/Tests/Editor/BaseThemeBinderTests.cs.meta
new file mode 100644
index 000000000..98239b300
--- /dev/null
+++ b/org.mixedrealitytoolkit.theming/Tests/Editor/BaseThemeBinderTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 033e10d7e483c314386bb9b93fb16ed9
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/org.mixedrealitytoolkit.theming/Tests/Editor/MRTK.Theming.EditorTests.asmdef b/org.mixedrealitytoolkit.theming/Tests/Editor/MRTK.Theming.EditorTests.asmdef
new file mode 100644
index 000000000..0aaf086e0
--- /dev/null
+++ b/org.mixedrealitytoolkit.theming/Tests/Editor/MRTK.Theming.EditorTests.asmdef
@@ -0,0 +1,31 @@
+{
+ "name": "MixedReality.Toolkit.Theming.Editor.Tests",
+ "rootNamespace": "MixedReality.Toolkit.Theming.Tests.EditMode",
+ "references": [
+ "MixedReality.Toolkit.Theming",
+ "MixedReality.Toolkit.UXCore",
+ "UnityEngine.TestRunner",
+ "UnityEditor.TestRunner"
+ ],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": true,
+ "precompiledReferences": [
+ "nunit.framework.dll"
+ ],
+ "autoReferenced": false,
+ "defineConstraints": [
+ "UNITY_INCLUDE_TESTS"
+ ],
+ "versionDefines": [
+ {
+ "name": "com.unity.asset-store-validation",
+ "expression": "",
+ "define": "HAS_ASSET_STORE_VALIDATION"
+ }
+ ],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/org.mixedrealitytoolkit.theming/Tests/Editor/MRTK.Theming.EditorTests.asmdef.meta b/org.mixedrealitytoolkit.theming/Tests/Editor/MRTK.Theming.EditorTests.asmdef.meta
new file mode 100644
index 000000000..a0bda6f68
--- /dev/null
+++ b/org.mixedrealitytoolkit.theming/Tests/Editor/MRTK.Theming.EditorTests.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 07bb2e8be351e584b80703fb9df9603d
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/org.mixedrealitytoolkit.theming/Tests/Editor/PackageValidationTest.cs b/org.mixedrealitytoolkit.theming/Tests/Editor/PackageValidationTest.cs
new file mode 100644
index 000000000..4c9561e3f
--- /dev/null
+++ b/org.mixedrealitytoolkit.theming/Tests/Editor/PackageValidationTest.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Mixed Reality Toolkit Contributors
+// Licensed under the BSD 3-Clause
+
+#if HAS_ASSET_STORE_VALIDATION
+
+using MixedReality.Toolkit.Core.Tests.EditMode;
+using NUnit.Framework;
+using System;
+
+namespace MixedReality.Toolkit.Theming.Tests.EditMode
+{
+ ///
+ /// This class is used to validate the package for the Mixed Reality Toolkit UX Theming package, verifying that all
+ /// requirements are met for publishing the package to the Unity Asset Store.
+ ///
+ internal class PackageValidationTest
+ {
+ ///
+ /// Test to validate the package for the Mixed Reality Toolkit UX Theming package, verifying that all
+ /// requirements are met for publishing the package to the Unity Asset Store.
+ ///
+ [Test]
+ public void PackageTest()
+ {
+ PackageValidatorResults results = PackageValidator.Validate("org.mixedrealitytoolkit.theming");
+ Assert.AreEqual(0, results.FailedCount, $"Failed tests found.{Environment.NewLine}{results.ToString(PackageValidatorResults.MessageType.Failed)}");
+ Assert.IsTrue(0 < results.SucceededCount, "No tests succeeded");
+ }
+ }
+}
+
+#endif // HAS_ASSET_STORE_VALIDATION
diff --git a/org.mixedrealitytoolkit.theming/Tests/Editor/PackageValidationTest.cs.meta b/org.mixedrealitytoolkit.theming/Tests/Editor/PackageValidationTest.cs.meta
new file mode 100644
index 000000000..4a16aba46
--- /dev/null
+++ b/org.mixedrealitytoolkit.theming/Tests/Editor/PackageValidationTest.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 78b18fd01cb9ed64a97e55d09ba534e6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: