diff --git a/src/cthulhu/scripts/web/script.py b/src/cthulhu/scripts/web/script.py index ccc5071..caf3e1e 100644 --- a/src/cthulhu/scripts/web/script.py +++ b/src/cthulhu/scripts/web/script.py @@ -1083,7 +1083,7 @@ class Script(default.Script): """Speaks the specified contents.""" utterances = self.speechGenerator.generateContents(contents, **args) - speech.speak(utterances) + speech.speak(utterances, interrupt=args.get("interrupt", True)) def sayCharacter(self, obj): """Speaks the character at the current caret position.""" @@ -1874,11 +1874,15 @@ class Script(default.Script): debug.printMessage(debug.LEVEL_INFO, msg, True) return True + if self.utilities.shouldInterruptForLocusOfFocusChange(oldFocus, newFocus, event): + self.presentationInterrupt() + + args["interrupt"] = False if contents: self.speakContents(contents, **args) else: utterances = self.speechGenerator.generateSpeech(newFocus, **args) - speech.speak(utterances) + speech.speak(utterances, interrupt=False) self._saveFocusedObjectInfo(newFocus) @@ -2650,6 +2654,44 @@ class Script(default.Script): self._lastCommandWasMouseButton = True return False + def onDescriptionChanged(self, event): + """Callback for object:property-change:accessible-description events.""" + + if self.utilities.eventIsBrowserUINoise(event): + msg = "WEB: Ignoring event believed to be browser UI noise" + debug.printMessage(debug.LEVEL_INFO, msg, True) + return True + + obj = event.source + if not self.utilities.inDocumentContent(obj): + return False + + descriptions = self.pointOfReference.get('descriptions', {}) + oldDescription = descriptions.get(hash(obj)) + if oldDescription == event.any_data: + tokens = ["WEB: Old description (", oldDescription, ") is the same as new one"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return True + + descriptions[hash(obj)] = event.any_data + self.pointOfReference['descriptions'] = descriptions + if obj != cthulhu_state.locusOfFocus: + msg = "WEB: Description change is for object other than locusOfFocus" + debug.printMessage(debug.LEVEL_INFO, msg, True) + return True + + if not event.any_data: + return True + + name = AXObject.get_name(obj) + if self.utilities.stringsAreRedundant(name, event.any_data): + tokens = ["WEB: Description change is redundant with name for", obj] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return True + + self.presentMessage(event.any_data, interrupt=False) + return True + def onNameChanged(self, event): """Callback for object:property-change:accessible-name events.""" diff --git a/tests/test_web_input_regressions.py b/tests/test_web_input_regressions.py index 8add719..a20e9c6 100644 --- a/tests/test_web_input_regressions.py +++ b/tests/test_web_input_regressions.py @@ -163,6 +163,104 @@ class WebPresentationModeSpeechRegressionTests(unittest.TestCase): testScript.presentMessage.assert_called_once_with(messages.MODE_FOCUS, interrupt=True) +class WebFocusSpeechInterruptionRegressionTests(unittest.TestCase): + def _make_script(self): + testScript = web_script.Script.__new__(web_script.Script) + testScript._navSuspended = False + testScript._lastCommandWasCaretNav = False + testScript._lastCommandWasStructNav = False + testScript._lastCommandWasMouseButton = False + testScript._inFocusMode = True + testScript._focusModeIsSticky = True + testScript._browseModeIsSticky = False + testScript.flatReviewPresenter = mock.Mock() + testScript.flatReviewPresenter.is_active.return_value = False + testScript.utilities = mock.Mock() + testScript.utilities.isZombie.return_value = False + testScript.utilities.isDocument.return_value = False + testScript.utilities.getTopLevelDocumentForObject.return_value = "document" + testScript.utilities.inFindContainer.return_value = False + testScript.utilities.queryNonEmptyText.return_value = None + testScript.utilities.isContentEditableWithEmbeddedObjects.return_value = False + testScript.utilities.isAnchor.return_value = False + testScript.utilities.lastInputEventWasPageNav.return_value = False + testScript.utilities.isFocusedWithMathChild.return_value = False + testScript.utilities.caretMovedToSamePageFragment.return_value = False + testScript.utilities.lastInputEventWasLineNav.return_value = False + testScript.utilities.inDocumentContent.return_value = True + testScript.utilities.shouldInterruptForLocusOfFocusChange.return_value = True + testScript.speechGenerator = mock.Mock() + testScript.speechGenerator.generateSpeech.return_value = ["focus speech"] + testScript.updateBraille = mock.Mock() + testScript.presentationInterrupt = mock.Mock() + testScript._saveFocusedObjectInfo = mock.Mock() + testScript.refreshKeyGrabs = mock.Mock() + return testScript + + def test_focus_generated_speech_uses_explicit_interrupt_then_appends(self): + testScript = self._make_script() + oldFocus = object() + newFocus = object() + event = mock.Mock(type="object:state-changed:focused", source=newFocus) + + with ( + mock.patch.object(web_script.AXObject, "is_dead", return_value=False), + mock.patch.object(web_script.AXUtilities, "is_unknown_or_redundant", return_value=False), + mock.patch.object(web_script.AXUtilities, "is_heading", return_value=False), + mock.patch.object(web_script.speech, "speak") as speak, + mock.patch.object(web_script.cthulhu, "emitRegionChanged"), + ): + result = web_script.Script.locus_of_focus_changed( + testScript, event, oldFocus, newFocus + ) + + self.assertTrue(result) + testScript.presentationInterrupt.assert_called_once_with() + speak.assert_called_once_with(["focus speech"], interrupt=False) + + +class WebDescriptionChangeRegressionTests(unittest.TestCase): + def _make_script(self): + testScript = web_script.Script.__new__(web_script.Script) + testScript.pointOfReference = {} + testScript.utilities = mock.Mock() + testScript.utilities.eventIsBrowserUINoise.return_value = False + testScript.utilities.inDocumentContent.return_value = True + testScript.utilities.stringsAreRedundant.side_effect = ( + lambda first, second: first == second + ) + testScript.presentMessage = mock.Mock() + return testScript + + def test_document_description_change_matching_name_is_ignored(self): + testScript = self._make_script() + source = object() + event = mock.Mock(source=source, any_data="More") + + with ( + mock.patch.object(web_script.cthulhu_state, "locusOfFocus", source), + mock.patch.object(web_script.AXObject, "get_name", return_value="More"), + ): + result = web_script.Script.onDescriptionChanged(testScript, event) + + self.assertTrue(result) + testScript.presentMessage.assert_not_called() + + def test_document_description_change_appends_to_focus_presentation(self): + testScript = self._make_script() + source = object() + event = mock.Mock(source=source, any_data="Helpful tooltip") + + with ( + mock.patch.object(web_script.cthulhu_state, "locusOfFocus", source), + mock.patch.object(web_script.AXObject, "get_name", return_value="Button"), + ): + result = web_script.Script.onDescriptionChanged(testScript, event) + + self.assertTrue(result) + testScript.presentMessage.assert_called_once_with("Helpful tooltip", interrupt=False) + + if __name__ == "__main__": unittest.main()