Skip to content

Commit

Permalink
Dont report description in browse mode with reportObjectDescriptions …
Browse files Browse the repository at this point in the history
…(PR #12917)

Historically the option "Object presentation: Report Object Descriptions" (default: true) has been limited to focus mode and object navigation.
In Report aria-description always #12500 this was changed to report descriptions always.
Historically, the word "object" in the settings category, and the name of this option was supposed to imply "focus mode / object nav specific behavior".

This change introduces a way to test braille (not dots, raw text) output.
Reverts changes from Report aria-description always #12500 that aimed to make the reportObjectDescriptions behavior consistent between browse and focus modes.
Updates the user docs to specify that options in the Object Presentation category don't apply to browse mode.
  • Loading branch information
feerrenrut committed Oct 13, 2021
1 parent 7dd53af commit c6aa771
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 11 deletions.
6 changes: 4 additions & 2 deletions source/braille.py
Expand Up @@ -702,8 +702,10 @@ def getControlFieldBraille( # noqa: C901
_descriptionIsContent: bool = field.get("descriptionIsContent", False)
if (
not _descriptionIsContent
and config.conf["presentation"]["reportObjectDescriptions"]
or (
# Note "reportObjectDescriptions" is not a reason to include description,
# "Object" implies focus/object nav, getControlFieldBraille calculates text for Browse mode.
# There is no way to identify getControlFieldBraille being called for reason focus, as is done in speech.
and (
config.conf["annotations"]["reportAriaDescription"]
and _descriptionFrom == controlTypes.DescriptionFrom.ARIA_DESCRIPTION
)
Expand Down
21 changes: 20 additions & 1 deletion source/gui/settingsDialogs.py
Expand Up @@ -46,6 +46,7 @@
import keyboardHandler
import characterProcessing
from . import guiHelper

try:
import updateCheck
except RuntimeError:
Expand All @@ -61,6 +62,10 @@
import keyLabels
from .dpiScalingHelper import DpiScalingHelperMixinWithoutInit

#: The size that settings panel text descriptions should be wrapped at.
# Ensure self.scaleSize is used to adjust for OS scaling adjustments.
PANEL_DESCRIPTION_WIDTH = 544

class SettingsDialog(
DpiScalingHelperMixinWithoutInit,
gui.contextHelp.ContextHelpMixin,
Expand Down Expand Up @@ -1908,6 +1913,14 @@ def onSave(self):


class ObjectPresentationPanel(SettingsPanel):

panelDescription = _(
# Translators: This is a label appearing on the Object Presentation settings panel.
"Configure how much information NVDA will present about controls."
" These options apply to focus reporting and NVDA object navigation,"
" but not when reading text content e.g. web content with browse mode."
)

# Translators: This is the label for the object presentation panel.
title = _("Object Presentation")
helpId = "ObjectPresentationSettings"
Expand All @@ -1932,6 +1945,12 @@ class ObjectPresentationPanel(SettingsPanel):

def makeSettings(self, settingsSizer):
sHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer)

self.windowText = sHelper.addItem(
wx.StaticText(self, label=self.panelDescription)
)
self.windowText.Wrap(self.scaleSize(PANEL_DESCRIPTION_WIDTH))

# Translators: This is the label for a checkbox in the
# object presentation settings panel.
reportToolTipsText = _("Report &tooltips")
Expand Down Expand Up @@ -2975,7 +2994,7 @@ def makeSettings(self, settingsSizer):
warningGroup.addItem(warningText)

self.windowText = warningGroup.addItem(wx.StaticText(warningBox, label=self.warningExplanation))
self.windowText.Wrap(self.scaleSize(544))
self.windowText.Wrap(self.scaleSize(PANEL_DESCRIPTION_WIDTH))

enableAdvancedControlslabel = _(
# Translators: This is the label for a checkbox in the Advanced settings panel.
Expand Down
4 changes: 4 additions & 0 deletions source/treeInterceptorHandler.py
Expand Up @@ -115,6 +115,10 @@ def __contains__(self, obj):
"""
raise NotImplementedError

#: Typing for autoproperty _get_passThrough
# Whether most scripts should temporarily pass through this interceptor without being intercepted.
passThrough: bool

def _get_passThrough(self):
"""Whether most scripts should temporarily pass through this interceptor without being intercepted.
"""
Expand Down
18 changes: 14 additions & 4 deletions tests/system/libraries/AssertsLib.py
Expand Up @@ -12,28 +12,38 @@
# In Robot libraries, class name must match the name of the module. Use caps for both.
class AssertsLib:
@staticmethod
def strings_match(actual, expected, ignore_case=False):
def strings_match(actual, expected, ignore_case=False, comparison="speech", message=""):
message += '\n' if message else ''
# Include expected text in robot test report so that the actual behavior
# can be determined entirely from the report, even when the test passes.
builtIn.log(
f"assert string matches (ignore case: {ignore_case}): '{expected}'",
f"{message}assert {comparison} string matches (ignore case: {ignore_case}): '{expected}'",
level="INFO"
)
try:
builtIn.should_be_equal_as_strings(
actual,
expected,
msg="Actual speech != Expected speech",
msg=f"{message}{comparison} Actual != Expected",
ignore_case=ignore_case
)
except AssertionError:
# Occasionally on assert failure the repr of the string makes it easier to determine the differences.
builtIn.log(
"repr of actual vs expected (ignore_case={}):\n{}\nvs\n{}".format(
"repr of ({}) actual vs expected (ignore_case={}):\n{}\nvs\n{}".format(
comparison,
ignore_case,
repr(actual),
repr(expected)
),
level="DEBUG"
)
raise

@staticmethod
def speech_matches(actual, expected, ignore_case=False, message=""):
AssertsLib.strings_match(actual, expected, ignore_case, comparison="speech", message=message)

@staticmethod
def braille_matches(actual, expected, ignore_case=False, message=""):
AssertsLib.strings_match(actual, expected, ignore_case, comparison="braille", message=message)
27 changes: 26 additions & 1 deletion tests/system/libraries/NvdaLib.py
Expand Up @@ -22,7 +22,10 @@
splitext as _splitext,
)
import tempfile as _tempFile
from typing import Optional as _Optional
from typing import (
Optional as _Optional,
Tuple as _Tuple,
)
from urllib.parse import quote as _quoteStr

from robotremoteserver import (
Expand Down Expand Up @@ -405,3 +408,25 @@ def getSpeechAfterKey(key) -> str:
spy.wait_for_speech_to_finish(speechStartedIndex=nextSpeechIndex)
speech = spy.get_speech_at_index_until_now(nextSpeechIndex)
return speech


def getSpeechAndBrailleAfterKey(key) -> _Tuple[str, str]:
"""Ensure speech has stopped, press key, and get speech until it stops, report the status of the
braille display.
@return: Tuple of Speech then Braille.
"""
spy = getSpyLib()
spy.wait_for_speech_to_finish()

nextSpeechIndex = spy.get_next_speech_index()
nextBrailleIndex = spy.get_next_braille_index()

spy.emulateKeyPress(key)

spy.wait_for_speech_to_finish(speechStartedIndex=nextSpeechIndex)
speech = spy.get_speech_at_index_until_now(nextSpeechIndex)

spy.wait_for_braille_update(nextBrailleIndex)
braille = spy.get_last_braille()

return speech, braille
Expand Up @@ -26,6 +26,7 @@ def _blockUntilConditionMet(
"""Repeatedly tries to get a value up until a time limit expires. Tries are separated by
a time interval. The call will block until shouldStopEvaluator returns True when given the value,
the default evaluator just returns the value converted to a boolean.
@param errorMessage Use 'None' to suppress the exception.
@return A tuple, (True, value) if evaluator condition is met, otherwise (False, None)
@raises RuntimeError if the time limit expires and an errorMessage is given.
"""
Expand Down
103 changes: 102 additions & 1 deletion tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py
Expand Up @@ -11,6 +11,7 @@
import typing
from typing import Optional

import extensionPoints
import globalPluginHandler
import threading
from .blockUntilConditionMet import _blockUntilConditionMet
Expand All @@ -37,6 +38,32 @@ def _importRobotRemoteServer() -> typing.Type:
return RobotRemoteServer


class BrailleViewerSpy:
postBrailleUpdate = extensionPoints.Action()

def __init__(self):
self._last = ""

def updateBrailleDisplayed(
self,
cells, # ignored
rawText,
currentCellCount, # ignored
):
rawText = rawText.strip()
if rawText and rawText != self._last:
self._last = rawText
self.postBrailleUpdate.notify(rawText=rawText)

isDestroyed: bool = False

def saveInfoAndDestroy(self):
if not self.isDestroyed:
self.isDestroyed = True
import brailleViewer
brailleViewer._onGuiDestroyed()


class NVDASpyLib:
""" Robot Framework Library to spy on NVDA during system tests.
Used to determine if NVDA has finished starting, and various ways of getting speech output.
Expand All @@ -50,10 +77,20 @@ def __init__(self):
[""], # initialise with an empty string, this allows for access via [-1]. This is equiv to no speech.
]
self._lastSpeechTime_requiresLock = _timer()
#: Lock to protect members written in _onNvdaSpeech.
#: Lock to protect members that are written to in _onNvdaSpeech.
self._speechLock = threading.RLock()

# braille raw text (not dots) cache is ordered temporally,
# oldest at low indexes, most recent at highest index.
self._nvdaBraille_requiresLock = [ # requires thread locking before read/write
"", # initialise with an empty string, this allows for access via [-1]. This is equiv to no braille.
]
#: Lock to protect members that are written to in _onNvdaBraille.
self._brailleLock = threading.RLock()

self._isNvdaStartupComplete = False
self._allSpeechStartIndex = self.get_last_speech_index()
self._allBrailleStartIndex = self.get_last_braille_index()
self._maxKeywordDuration = 30
self._registerWithExtensionPoints()

Expand All @@ -67,6 +104,9 @@ def _registerWithExtensionPoints(self):
from synthDrivers.speechSpySynthDriver import post_speech
post_speech.register(self._onNvdaSpeech)

self._brailleSpy = BrailleViewerSpy()
self._brailleSpy.postBrailleUpdate.register(self._onNvdaBraille)

def set_configValue(self, keyPath: typing.List[str], val: typing.Union[str, bool, int]):
import config
if not keyPath or len(keyPath) < 1:
Expand All @@ -92,6 +132,16 @@ def queueNVDAUIAHandlerThreadCrash(self):
# callbacks for extension points
def _onNvdaStartupComplete(self):
self._isNvdaStartupComplete = True
import brailleViewer
brailleViewer._brailleGui = self._brailleSpy
self.setBrailleCellCount(120)
brailleViewer.postBrailleViewerToolToggledAction.notify(created=True)

def _onNvdaBraille(self, rawText: str):
if not rawText:
return
with self._brailleLock:
self._nvdaBraille_requiresLock.append(rawText)

def _onNvdaSpeech(self, speechSequence=None):
if not speechSequence:
Expand Down Expand Up @@ -143,6 +193,27 @@ def _hasSpeechFinished(self, speechStartedIndex: Optional[int] = None):
finished = self.SPEECH_HAS_FINISHED_SECONDS < _timer() - self._lastSpeechTime_requiresLock
return started and finished

def setBrailleCellCount(self, brailleCellCount: int):
import brailleViewer
brailleViewer.DEFAULT_NUM_CELLS = brailleCellCount

def _getBrailleAtIndex(self, brailleIndex: int) -> str:
with self._brailleLock:
return self._nvdaBraille_requiresLock[brailleIndex]

def get_braille_at_index_until_now(self, brailleIndex: int) -> str:
""" All raw braille text from (and including) the index until now.
@param brailleIndex:
@return: The raw text, each update on a new line
"""
with self._brailleLock:
rangeOfInterest = self._nvdaBraille_requiresLock[brailleIndex:]
return "\n".join(rangeOfInterest)

def get_last_braille_index(self) -> int:
with self._brailleLock:
return len(self._nvdaBraille_requiresLock) - 1

def _devInfoToLog(self):
import api
obj = api.getNavigatorObject()
Expand All @@ -163,6 +234,14 @@ def dump_speech_to_log(self):
except Exception:
log.error("Unable to log speech")

def dump_braille_to_log(self):
log.debug("dump_braille_to_log.")
with self._brailleLock:
try:
log.debug(f"All braille:\n{repr(self._nvdaBraille_requiresLock)}")
except Exception:
log.error("Unable to log braille")

def _minTimeout(self, timeout: float) -> float:
"""Helper to get the minimum value, the timeout passed in, or self._maxKeywordDuration"""
return min(timeout, self._maxKeywordDuration)
Expand Down Expand Up @@ -237,6 +316,28 @@ def wait_for_speech_to_finish(
errorMessage="Speech did not finish before timeout"
)

def wait_for_braille_update(
self,
nextBrailleIndex: int,
maxWaitSeconds=5.0,
):
"""Wait until there is at least a single update.
@note there may be subsequent braille updates. This method does not confirm updates are finished.
"""
_blockUntilConditionMet(
getValue=lambda: self.get_last_braille_index() == nextBrailleIndex,
giveUpAfterSeconds=self._minTimeout(maxWaitSeconds),
errorMessage=None
)

def get_last_braille(self) -> str:
return self._getBrailleAtIndex(-1)

def get_next_braille_index(self) -> int:
""" @return: the next index that will be used.
"""
return self.get_last_braille_index() + 1

def emulateKeyPress(self, kbIdentifier: str, blockUntilProcessed=True):
"""
Emulates a key press using NVDA's input gesture framework.
Expand Down

0 comments on commit c6aa771

Please sign in to comment.