diff --git a/org.mixedrealitytoolkit.core/CHANGELOG.md b/org.mixedrealitytoolkit.core/CHANGELOG.md
index 95f237481..1b5c53d99 100644
--- a/org.mixedrealitytoolkit.core/CHANGELOG.md
+++ b/org.mixedrealitytoolkit.core/CHANGELOG.md
@@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
* Fixed "The type `MixedReality.Toolkit.Core MixedReality.Toolkit.Experimental.BubbleChildHoverEvents/TrickleChildHoverEvents/BubbleChildSelectEvents/TrickleChildSelectEvents` is being serialized by `[SerializeReference]`, but is missing the `[Serializable]` attribute." on Unity 6.3. [PR #1107](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1107)
* Fixed object identity API usages and obsolete sorting warnings for Unity 6.4 and newer in `FindObjectUtility`. [PR #1136](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1136)
+* Fixed PlayMode tests failing when XR is initialized by ensuring XR is disabled during test runs. [PR #1141](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1141)
## Deprecated
diff --git a/org.mixedrealitytoolkit.core/Tests/TestUtilities/BaseRuntimeTests.cs b/org.mixedrealitytoolkit.core/Tests/TestUtilities/BaseRuntimeTests.cs
index e136cbfe1..0ef495608 100644
--- a/org.mixedrealitytoolkit.core/Tests/TestUtilities/BaseRuntimeTests.cs
+++ b/org.mixedrealitytoolkit.core/Tests/TestUtilities/BaseRuntimeTests.cs
@@ -12,6 +12,8 @@ namespace MixedReality.Toolkit.Core.Tests
///
/// Base class to handle typical code setup/teardown and test utilities
///
+ [PrebuildSetup(typeof(PlayModeTestXRDisabler))]
+ [PostBuildCleanup(typeof(PlayModeTestXRDisabler))]
public abstract class BaseRuntimeTests
{
int originalTargetFrameRate = 0;
diff --git a/org.mixedrealitytoolkit.core/Tests/TestUtilities/MRTK.Core.TestUtilities.asmdef b/org.mixedrealitytoolkit.core/Tests/TestUtilities/MRTK.Core.TestUtilities.asmdef
index ac3cc28ee..8e7d3f29a 100644
--- a/org.mixedrealitytoolkit.core/Tests/TestUtilities/MRTK.Core.TestUtilities.asmdef
+++ b/org.mixedrealitytoolkit.core/Tests/TestUtilities/MRTK.Core.TestUtilities.asmdef
@@ -6,7 +6,9 @@
"Unity.InputSystem",
"UnityEditor.TestRunner",
"UnityEngine.TestRunner",
- "Unity.asset-store-validation.Editor"
+ "Unity.asset-store-validation.Editor",
+ "Unity.XR.Management",
+ "Unity.XR.Management.Editor"
],
"includePlatforms": [],
"excludePlatforms": [],
diff --git a/org.mixedrealitytoolkit.core/Tests/TestUtilities/PlayModeTestXRDisabler.cs b/org.mixedrealitytoolkit.core/Tests/TestUtilities/PlayModeTestXRDisabler.cs
new file mode 100644
index 000000000..a05fdc35e
--- /dev/null
+++ b/org.mixedrealitytoolkit.core/Tests/TestUtilities/PlayModeTestXRDisabler.cs
@@ -0,0 +1,150 @@
+// Copyright (c) Mixed Reality Toolkit Contributors
+// Licensed under the BSD 3-Clause
+
+// Disable "missing XML comment" warning for tests. While nice to have, this documentation is not required.
+#pragma warning disable CS1591
+
+using System;
+using UnityEngine;
+using UnityEngine.TestTools;
+
+#if UNITY_EDITOR
+using UnityEditor;
+using UnityEditor.XR.Management;
+using UnityEngine.XR.Management;
+#endif
+
+namespace MixedReality.Toolkit.Core.Tests
+{
+ ///
+ /// Ensures XR initialization on start is disabled during PlayMode tests so that input simulation
+ /// and synthetic hands function properly without being suppressed by active OpenXR or XR displays.
+ /// State is maintained in memory without dirtying project settings on disk, and restored to its original value upon test completion or exit.
+ ///
+ internal class PlayModeTestXRDisabler : IPrebuildSetup, IPostBuildCleanup
+ {
+ private const string WasXRDisabledByTestKey = "MRTK_PlayModeTest_XRDisabledByTest";
+ private static bool isSubscribed;
+
+ ///
+ void IPrebuildSetup.Setup()
+ {
+#if UNITY_EDITOR
+ SubscribePlayModeStateChanged();
+ DisableXRIfEnabled();
+#endif
+ }
+
+ ///
+ void IPostBuildCleanup.Cleanup()
+ {
+#if UNITY_EDITOR
+ UnsubscribePlayModeStateChanged();
+ RestoreXR();
+#endif
+ }
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
+ private static void OnSubsystemRegistration()
+ {
+#if UNITY_EDITOR
+ if (SessionState.GetBool(WasXRDisabledByTestKey, false))
+ {
+ ApplyDisableXR();
+ }
+#endif
+ }
+
+#if UNITY_EDITOR
+ private static void SubscribePlayModeStateChanged()
+ {
+ if (!isSubscribed)
+ {
+ EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
+ isSubscribed = true;
+ }
+ }
+
+ private static void UnsubscribePlayModeStateChanged()
+ {
+ if (isSubscribed)
+ {
+ EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
+ isSubscribed = false;
+ }
+ }
+
+ private static void OnPlayModeStateChanged(PlayModeStateChange state)
+ {
+ if (state == PlayModeStateChange.EnteredEditMode)
+ {
+ UnsubscribePlayModeStateChanged();
+ RestoreXR();
+ }
+ }
+
+ private static XRGeneralSettings GetTargetSettings()
+ {
+ try
+ {
+ BuildTargetGroup targetGroup = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget);
+ return XRGeneralSettingsPerBuildTarget.XRGeneralSettingsForBuildTarget(targetGroup);
+ }
+ catch (Exception e)
+ {
+ Debug.LogWarning($"[PlayModeTestXRDisabler] Failed to get XRGeneralSettings: {e.Message}");
+ return null;
+ }
+ }
+
+ private static void DisableXRIfEnabled()
+ {
+ XRGeneralSettings settings = GetTargetSettings();
+ bool isEnabledOnTarget = settings != null && settings.InitManagerOnStart;
+ bool isEnabledOnInstance = XRGeneralSettings.Instance != null && XRGeneralSettings.Instance.InitManagerOnStart;
+
+ if (isEnabledOnTarget || isEnabledOnInstance)
+ {
+ SessionState.SetBool(WasXRDisabledByTestKey, true);
+ ApplyDisableXR();
+ }
+ }
+
+ private static void ApplyDisableXR()
+ {
+ if (XRGeneralSettings.Instance != null)
+ {
+ XRGeneralSettings.Instance.InitManagerOnStart = false;
+ }
+
+ XRGeneralSettings settings = GetTargetSettings();
+ if (settings != null)
+ {
+ settings.InitManagerOnStart = false;
+ }
+ }
+
+ private static void RestoreXR()
+ {
+ if (!SessionState.GetBool(WasXRDisabledByTestKey, false))
+ {
+ return;
+ }
+
+ SessionState.EraseBool(WasXRDisabledByTestKey);
+
+ XRGeneralSettings settings = GetTargetSettings();
+ if (settings != null)
+ {
+ settings.InitManagerOnStart = true;
+ }
+
+ if (XRGeneralSettings.Instance != null)
+ {
+ XRGeneralSettings.Instance.InitManagerOnStart = true;
+ }
+ }
+#endif
+ }
+}
+#pragma warning restore CS1591
diff --git a/org.mixedrealitytoolkit.core/Tests/TestUtilities/PlayModeTestXRDisabler.cs.meta b/org.mixedrealitytoolkit.core/Tests/TestUtilities/PlayModeTestXRDisabler.cs.meta
new file mode 100644
index 000000000..3b3d4aeae
--- /dev/null
+++ b/org.mixedrealitytoolkit.core/Tests/TestUtilities/PlayModeTestXRDisabler.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 270301c6dca942958f7a6c42db4652fa
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/org.mixedrealitytoolkit.uxcore/CHANGELOG.md b/org.mixedrealitytoolkit.uxcore/CHANGELOG.md
index f5b02e2d5..b5ac52eb0 100644
--- a/org.mixedrealitytoolkit.uxcore/CHANGELOG.md
+++ b/org.mixedrealitytoolkit.uxcore/CHANGELOG.md
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
* Fixed "leaked managed shell" issue in `UGUIInputAdapter`. [PR #1096](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1096)
* Fixed "Attribute 'SerializeField' is not valid on this declaration type. It is only valid on 'field' declarations" error on `DialogButton` in Unity 6.3. [PR #1108](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1108)
+* Fixed `SeeItSayItLabelEnablerTests` failure when running on platforms without speech recognition support. [PR #1141](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1141)
## [3.3.0] - 2025-11-12
diff --git a/org.mixedrealitytoolkit.uxcore/Tests/Runtime/SeeItSayItLabelEnablerTests.cs b/org.mixedrealitytoolkit.uxcore/Tests/Runtime/SeeItSayItLabelEnablerTests.cs
index f92b9b7de..d4f54030e 100644
--- a/org.mixedrealitytoolkit.uxcore/Tests/Runtime/SeeItSayItLabelEnablerTests.cs
+++ b/org.mixedrealitytoolkit.uxcore/Tests/Runtime/SeeItSayItLabelEnablerTests.cs
@@ -34,10 +34,7 @@ public IEnumerator TestEnableAndSetLabel()
interactor.gameObject.SetActive(true);
yield return null;
- if (Application.isBatchMode)
- {
- LogAssert.Expect(LogType.Exception, new Regex("Speech recognition is not supported on this machine"));
- }
+ ExpectSpeechRecognitionNotSupportedException();
Transform sublabel = label.transform.GetChild(0);
Assert.IsTrue(label.gameObject.activeSelf, "Label is enabled");
@@ -64,10 +61,7 @@ public IEnumerator TestAutoUpdateLabel()
interactor.gameObject.SetActive(true);
yield return null;
- if (Application.isBatchMode)
- {
- LogAssert.Expect(LogType.Exception, new Regex("Speech recognition is not supported on this machine"));
- }
+ ExpectSpeechRecognitionNotSupportedException();
Transform sublabel = label.transform.GetChild(0);
TMP_Text text = label.gameObject.GetComponentInChildren(true);
@@ -84,10 +78,7 @@ public IEnumerator TestAutoUpdateLabel()
// Wait for a frame to give Unity a change to actually destroy the object
yield return null;
// The speech recognition keyword change will trigger this exception at next update when speech recognition is not supported
- if (Application.isBatchMode)
- {
- LogAssert.Expect(LogType.Exception, new Regex("Speech recognition is not supported on this machine"));
- }
+ ExpectSpeechRecognitionNotSupportedException();
}
[UnityTest]
@@ -115,10 +106,7 @@ public IEnumerator TestPositionCanvasLabel()
interactor.gameObject.SetActive(true);
yield return null;
- if (Application.isBatchMode)
- {
- LogAssert.Expect(LogType.Exception, new Regex("Speech recognition is not supported on this machine"));
- }
+ ExpectSpeechRecognitionNotSupportedException();
RectTransform sublabel = label.transform.GetChild(0) as RectTransform;
Assert.AreEqual(sublabel.anchoredPosition3D, new Vector3(10, -30, -10), "Label is positioned correctly");
@@ -142,10 +130,7 @@ public IEnumerator TestPositionNonCanvasLabel()
interactor.gameObject.SetActive(true);
yield return null;
- if (Application.isBatchMode)
- {
- LogAssert.Expect(LogType.Exception, new Regex("Speech recognition is not supported on this machine"));
- }
+ ExpectSpeechRecognitionNotSupportedException();
Assert.AreEqual(label.transform.localPosition, new Vector3(10f, -.504f, -.004f), "Label is positioned correctly");
#else
@@ -203,6 +188,16 @@ private GameObject SetUpButton(bool allowSelectByVoice, Control control)
return testButton;
}
+ private static void ExpectSpeechRecognitionNotSupportedException()
+ {
+#if MRTK_INPUT_PRESENT && MRTK_SPEECH_PRESENT && (UNITY_STANDALONE_WIN || UNITY_WSA || UNITY_EDITOR_WIN)
+ if (!UnityEngine.Windows.Speech.PhraseRecognitionSystem.isSupported)
+ {
+ LogAssert.Expect(LogType.Exception, new Regex("Speech recognition is not supported on this machine"));
+ }
+#endif
+ }
+
private enum Control
{
None,