diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index 000fd4b85ba..1cf435277d1 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -2591,13 +2591,24 @@ def _get_states(self) -> set[controlTypes.State]: ) return states - 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 index b7958a42e00..5261218d0aa 100644 --- a/tests/unit/test_NVDAObjects_UIA.py +++ b/tests/unit/test_NVDAObjects_UIA.py @@ -6,7 +6,7 @@ """Unit tests for NVDAObjects.UIA.""" import unittest -from unittest.mock import patch +from unittest.mock import Mock, patch import controlTypes from NVDAObjects.UIA import MenuItem, UIA @@ -14,6 +14,77 @@ import UIAHandler +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() + + class TestMenuItemStates(unittest.TestCase): def test_legacyCheckedStateFallback(self) -> None: menuItem = object.__new__(MenuItem) diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 2c1cb56644d..e1d90379580 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -56,6 +56,7 @@ Previously these keys had no function when pressed on their own. (#20366, @fla-r * 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) * NVDA now reports checked ToolStrip menu items in .NET Framework Windows Forms applications using UI Automation. (#19335, @Cary-rowen) +* Object descriptions are now reported for .NET Framework Windows Forms ToolStrip menu items exposed through UI Automation. (#20486, @Cary-rowen) ### Changes for Developers