Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions source/braille.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading