Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Introduce braille system tests for reportObjectDescription
  • Loading branch information
feerrenrut committed Oct 11, 2021
commit 9426199bb66c4db17ca59d40eabf1320869410c1
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
119 changes: 118 additions & 1 deletion tests/system/robot/chromeTests.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
# adds a slight pause to the synthesizer.
SPEECH_SEP = " "
SPEECH_CALL_SEP = '\n'

#: single space is used to separate semantics in braille output.
BRAILLE_SEP = " "

ARIAExamplesDir = os.path.join(
_NvdaLib._locations.repoRoot, "include", "w3c-aria-practices", "examples"
Expand Down Expand Up @@ -1011,3 +1012,119 @@ def preventDuplicateSpeechFromDescription_focus():
actualSpeech,
"banana link"
)


def test_ensureNoBrowseModeDescription():
"""
Test that option (speech.reportObjectDescriptions default:True)
does not result in description in browse mode.
"""
REPORT_OBJ_DESC_KEY = ["presentation", "reportObjectDescriptions"]
spy = _NvdaLib.getSpyLib()
# prevent browse / focus mode messages from interfering, 0 means don't show.
spy.set_configValue(["braille", "messageTimeout"], 0)

_chrome.prepareChrome(
"\n".join([
r'<button>something for focus</button>'
r'<a href="#" style="display:block" title="Cat">Apple</a>',
# second link to make testing second focus mode tab easier
r'<a href="#" style="display:block" title="Fish">Banana</a>',
])
)

actualSpeech = _NvdaLib.getSpeechAfterKey('tab')
_builtIn.should_contain(actualSpeech, "something for focus")

# Test Browse mode
spy.set_configValue(REPORT_OBJ_DESC_KEY, True)
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey('downArrow')
_asserts.speech_matches(
actualSpeech,
SPEECH_SEP.join([
"link", # role description
"Apple", # link name / contents
]),
message="Test browse mode with reportObjectDescriptions=True"
)
_asserts.braille_matches(
actualBraille,
BRAILLE_SEP.join([
"lnk", # role description
"Cat", # link description (from title)
"Apple", # link name / contents
]),
message="Test browse mode with reportObjectDescriptions=True"
)

# move virtual cursor back up to reset to start position
actualSpeech = _NvdaLib.getSpeechAfterKey('upArrow')
_builtIn.should_contain(actualSpeech, "something for focus")
spy.set_configValue(REPORT_OBJ_DESC_KEY, False)

actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey('downArrow')
_asserts.speech_matches(
actualSpeech,
SPEECH_SEP.join([
"link", # role description
"Apple", # link name / contents
]),
message="Test browse mode with reportObjectDescriptions=False"
)
_asserts.braille_matches(
actualBraille,
BRAILLE_SEP.join([
"lnk", # role description
"Apple", # link name / contents
]),
message="Test browse mode with reportObjectDescriptions=False"
)

# move virtual cursor back up to reset to start position
actualSpeech = _NvdaLib.getSpeechAfterKey('upArrow')
_builtIn.should_contain(actualSpeech, "something for focus")
spy.set_configValue(REPORT_OBJ_DESC_KEY, True)

# Test focus mode
actualSpeech = _NvdaLib.getSpeechAfterKey("nvda+space")
_asserts.speech_matches(actualSpeech, "Focus mode")

actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("tab")
_asserts.speech_matches(
actualSpeech,
SPEECH_SEP.join([
"Apple", # link name / contents
"link", # role description
"Cat", # link description (from title)
]),
message="Test focus mode with reportObjectDescriptions=True"
)
_asserts.braille_matches(
actualBraille,
BRAILLE_SEP.join([
"Apple", # link name / contents
"lnk", # role description
"Cat", # link description (from title)
]),
message="Test focus mode with reportObjectDescriptions=True"
)

# Use second link to test focus mode when 'reportObjectDescriptions' is off.
spy.set_configValue(REPORT_OBJ_DESC_KEY, False)
actualSpeech, actualBraille = _NvdaLib.getSpeechAndBrailleAfterKey("tab")
_asserts.speech_matches(
actualSpeech,
SPEECH_SEP.join([
"Banana", # link name / contents
"link", # role description
]),
message="Test focus mode with reportObjectDescriptions=False"
)
_asserts.braille_matches(
actualBraille,
BRAILLE_SEP.join([
"Banana", # link name / contents
"lnk", # role description
]),
message="Test focus mode with reportObjectDescriptions=False"
)
5 changes: 5 additions & 0 deletions tests/system/robot/chromeTests.robot
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Test Teardown default teardown
default teardown
${screenshotName}= create_preserved_test_output_filename failedTest.png
Run Keyword If Test Failed Take Screenshot ${screenShotName}
dump_speech_to_log
dump_braille_to_log
exit chrome
quit NVDA

Expand Down Expand Up @@ -96,3 +98,6 @@ Prevent Duplicate Speech From Description while in Focus mode
preventDuplicateSpeechFromDescription_focus
Prevent Duplicate Speech From Description while in Browse mode with tab nav
test_preventDuplicateSpeechFromDescription_browse_tab
Only report description in focus mode due to reportObjectDescriptions
[Documentation] The term object in reportObjectDescriptions (essentially) means focus mode.
test_ensureNoBrowseModeDescription