diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index 5f9ff822901..4fe0aedb7c6 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -2565,13 +2565,24 @@ def _get_positionInfo(self): class MenuItem(UIA): - def _get_description(self): + def _get_description(self) -> str | None: name = self.name - description = super(MenuItem, self)._get_description() - if description != name: - return description - else: - return None + description = super()._get_description() + if not description or description == name: + providerDescription = self.UIAElement.cachedProviderDescription or "" + if ( + self.UIAElement.cachedFrameworkID == "WinForm" + and "System.Windows.Forms, Version=4.0.0.0" in providerDescription + and "ToolStripMenuItem" in providerDescription + ): + legacyDescription = self._getUIACacheablePropertyValue_handlesCOMErrors( + UIAHandler.UIA_LegacyIAccessibleDescriptionPropertyId, + ignoreDefault=True, + onError=None, + ) + if isinstance(legacyDescription, str): + description = legacyDescription + return description if description != name else None class UIColumnHeader(UIA): diff --git a/tests/unit/test_NVDAObjects_UIA.py b/tests/unit/test_NVDAObjects_UIA.py new file mode 100644 index 00000000000..8c099111dc8 --- /dev/null +++ b/tests/unit/test_NVDAObjects_UIA.py @@ -0,0 +1,82 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2026 NV Access Limited, Cary-rowen +# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license. +# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt + +"""Unit tests for NVDAObjects.UIA.""" + +import unittest +from unittest.mock import Mock, patch + +from NVDAObjects.UIA import MenuItem, UIA + + +class TestMenuItemDescription(unittest.TestCase): + def test_legacyDescriptionFallback(self) -> None: + menuItem = object.__new__(MenuItem) + menuItem.name = "Name" + menuItem.UIAElement = Mock( + cachedFrameworkID="WinForm", + cachedProviderDescription=( + "managed:System.Windows.Forms.ToolStripMenuItem+ToolStripMenuItemAccessibleObject, " + "System.Windows.Forms, Version=4.0.0.0" + ), + ) + notSupportedValue = object() + testCases = ( + ("UIA description", "Legacy description", "UIA description", False), + ("", "Legacy description", "Legacy description", True), + ("Name", "Legacy description", "Legacy description", True), + ("Name", "Name", None, True), + ("Name", notSupportedValue, None, True), + ) + for uiaDescription, legacyDescription, expectedDescription, shouldReadLegacyDescription in testCases: + with ( + self.subTest( + uiaDescription=uiaDescription, + legacyDescription=legacyDescription, + ), + patch.object(UIA, "_get_description", return_value=uiaDescription), + patch.object( + MenuItem, + "_getUIACacheablePropertyValue_handlesCOMErrors", + return_value=legacyDescription, + ) as getLegacyDescription, + ): + self.assertEqual(expectedDescription, menuItem._get_description()) + if shouldReadLegacyDescription: + getLegacyDescription.assert_called_once() + else: + getLegacyDescription.assert_not_called() + + def test_legacyDescriptionFallbackIsLimitedToNetFrameworkWinFormsToolStripMenuItems(self) -> None: + menuItem = object.__new__(MenuItem) + menuItem.name = "Name" + for frameworkID, providerDescription in ( + ( + "WPF", + "managed:System.Windows.Forms.ToolStripMenuItem+ToolStripMenuItemAccessibleObject, " + "System.Windows.Forms, Version=4.0.0.0", + ), + ( + "WinForm", + "managed:System.Windows.Forms.ToolStripMenuItem+ToolStripMenuItemAccessibleObject, " + "System.Windows.Forms, Version=8.0.0.0", + ), + ("WinForm", "System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0"), + ): + with ( + self.subTest(frameworkID=frameworkID, providerDescription=providerDescription), + patch.object(UIA, "_get_description", return_value=""), + patch.object( + MenuItem, + "_getUIACacheablePropertyValue_handlesCOMErrors", + return_value="Legacy description", + ) as getLegacyDescription, + ): + menuItem.UIAElement = Mock( + cachedFrameworkID=frameworkID, + cachedProviderDescription=providerDescription, + ) + self.assertEqual("", menuItem._get_description()) + getLegacyDescription.assert_not_called() diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 6befac7d53e..ac1108f6714 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -52,6 +52,7 @@ Previously these keys had no function when pressed on their own. (#20366, @fla-rion) * The HID keyboard input simulation setting for ALVA braille displays is now remembered across reconnects and restarts. (#20455, @Cary-rowen) * Braille now follows the spoken text during say all in browse mode when braille is tethered to focus. (#3287, @LeonarddeR) +* Object descriptions are now reported for .NET Framework Windows Forms ToolStrip menu items exposed through UI Automation. (#20486, @Cary-rowen) ### Changes for Developers