import os import sys import unittest from pathlib import Path from unittest import mock import gi os.environ.setdefault("GSETTINGS_BACKEND", "memory") gi.require_version("Gdk", "3.0") gi.require_version("Gtk", "3.0") sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) soundGeneratorModule = sys.modules.get("cthulhu.sound_generator") if soundGeneratorModule is not None and not hasattr(soundGeneratorModule, "SoundGenerator"): class _StubSoundGenerator: pass soundGeneratorModule.SoundGenerator = _StubSoundGenerator from cthulhu import messages from cthulhu.scripts.web import script as web_script from cthulhu.scripts.web import script_utilities as web_script_utilities class WebKeyGrabRegressionTests(unittest.TestCase): def _make_partial_script(self): testScript = web_script.Script.__new__(web_script.Script) testScript._lastCommandWasCaretNav = True testScript._lastCommandWasStructNav = True testScript._lastCommandWasMouseButton = True testScript._lastMouseButtonContext = ("old", 7) testScript._madeFindAnnouncement = True testScript._inFocusMode = True testScript._navSuspended = False testScript.removeKeyGrabs = mock.Mock() testScript.refreshKeyGrabs = mock.Mock() testScript._setNavigationSuspended = mock.Mock() testScript.utilities = mock.Mock() testScript.utilities.isZombie.return_value = False testScript.utilities.isDocument.return_value = False testScript.utilities.getTopLevelDocumentForObject.return_value = None return testScript def test_window_deactivate_does_not_drop_key_grabs(self): testScript = self._make_partial_script() result = web_script.Script.onWindowDeactivated(testScript, mock.Mock()) self.assertFalse(result) self.assertFalse(testScript._lastCommandWasCaretNav) self.assertFalse(testScript._lastCommandWasStructNav) self.assertFalse(testScript._lastCommandWasMouseButton) self.assertEqual(testScript._lastMouseButtonContext, (None, -1)) testScript.removeKeyGrabs.assert_not_called() def test_non_document_focus_change_without_prior_document_does_not_refresh_grabs(self): testScript = self._make_partial_script() oldFocus = object() newFocus = object() with mock.patch("cthulhu.scripts.web.script.AXObject.is_dead", return_value=False): result = web_script.Script.locus_of_focus_changed(testScript, None, oldFocus, newFocus) self.assertFalse(result) self.assertFalse(testScript._madeFindAnnouncement) self.assertFalse(testScript._inFocusMode) testScript._setNavigationSuspended.assert_called_once_with( True, "focus left document content", ) testScript.refreshKeyGrabs.assert_not_called() def test_non_document_focus_change_refreshes_grabs_after_leaving_document(self): testScript = self._make_partial_script() oldFocus = object() newFocus = object() oldDocument = object() testScript.utilities.getTopLevelDocumentForObject.side_effect = ( lambda obj: oldDocument if obj is oldFocus else None ) with mock.patch("cthulhu.scripts.web.script.AXObject.is_dead", return_value=False): result = web_script.Script.locus_of_focus_changed(testScript, None, oldFocus, newFocus) self.assertFalse(result) testScript.refreshKeyGrabs.assert_called_once_with() def test_non_document_focus_change_refreshes_grabs_when_old_focus_missing(self): testScript = self._make_partial_script() newFocus = object() with mock.patch("cthulhu.scripts.web.script.AXObject.is_dead", return_value=False): result = web_script.Script.locus_of_focus_changed(testScript, None, None, newFocus) self.assertFalse(result) testScript.refreshKeyGrabs.assert_called_once_with() class WebActiveWindowRegressionTests(unittest.TestCase): def test_sanity_check_recovers_missing_script_app_from_active_window(self): testScript = mock.Mock(app=None) utilities = web_script_utilities.Utilities.__new__(web_script_utilities.Utilities) utilities._script = testScript activeWindow = object() app = object() with ( mock.patch.object(web_script_utilities.cthulhu_state, "activeWindow", activeWindow), mock.patch.object(web_script_utilities.cthulhu_state, "locusOfFocus", None), mock.patch.object( web_script_utilities.AXObject, "get_application", side_effect=lambda obj: app if obj is activeWindow else None, ), mock.patch.object( web_script_utilities.AXObject, "get_parent", side_effect=lambda obj: app if obj is activeWindow else None, ), mock.patch.object(web_script_utilities.cthulhu, "setActiveWindow") as setActiveWindow, ): result = web_script_utilities.Utilities.sanityCheckActiveWindow(utilities) self.assertTrue(result) self.assertIs(testScript.app, app) setActiveWindow.assert_not_called() def test_sanity_check_preserves_current_window_when_recovery_is_inconclusive(self): oldApp = object() testScript = mock.Mock(app=oldApp) utilities = web_script_utilities.Utilities.__new__(web_script_utilities.Utilities) utilities._script = testScript activeWindow = object() with ( mock.patch.object(web_script_utilities.cthulhu_state, "activeWindow", activeWindow), mock.patch.object( web_script_utilities.AXObject, "get_parent", return_value=object(), ), mock.patch.object(utilities, "activeWindow", return_value=None), mock.patch.object(web_script_utilities.cthulhu, "setActiveWindow") as setActiveWindow, ): result = web_script_utilities.Utilities.sanityCheckActiveWindow(utilities) self.assertTrue(result) self.assertIs(testScript.app, oldApp) setActiveWindow.assert_not_called() class WebPresentationModeSpeechRegressionTests(unittest.TestCase): def _make_script(self, inFocusMode): testScript = web_script.Script.__new__(web_script.Script) testScript._inFocusMode = inFocusMode testScript._focusModeIsSticky = True testScript._browseModeIsSticky = True testScript._loadingDocumentContent = False testScript._lastCommandWasCaretNav = False testScript._lastCommandWasStructNav = False testScript.utilities = mock.Mock() testScript.utilities.getCaretContext.return_value = ("button", 0) testScript.utilities.grabFocusWhenSettingCaret.return_value = True testScript.presentMessage = mock.Mock() testScript.refreshKeyGrabs = mock.Mock() testScript._shouldSuppressBrowseModeSound = mock.Mock(return_value=False) return testScript def test_automatic_browse_mode_announcement_does_not_interrupt_focus_presentation(self): testScript = self._make_script(inFocusMode=True) soundManager = mock.Mock() with ( mock.patch.object(web_script.AXObject, "get_parent", return_value=None), mock.patch.object(web_script.AXUtilities, "is_list_box", return_value=False), mock.patch.object(web_script.AXUtilities, "is_menu", return_value=False), mock.patch.object(web_script.sound_theme_manager, "getManager", return_value=soundManager), ): web_script.Script.togglePresentationMode(testScript, None, "document") testScript.presentMessage.assert_called_once_with(messages.MODE_BROWSE) soundManager.playBrowseModeSound.assert_called_once_with() self.assertFalse(testScript._inFocusMode) self.assertFalse(testScript._focusModeIsSticky) self.assertFalse(testScript._browseModeIsSticky) testScript.refreshKeyGrabs.assert_called_once_with() def test_automatic_focus_mode_announcement_does_not_interrupt_focus_presentation(self): testScript = self._make_script(inFocusMode=False) soundManager = mock.Mock() with mock.patch.object( web_script.sound_theme_manager, "getManager", return_value=soundManager, ): web_script.Script.togglePresentationMode(testScript, None, "document") testScript.presentMessage.assert_called_once_with(messages.MODE_FOCUS) soundManager.playFocusModeSound.assert_called_once_with() self.assertTrue(testScript._inFocusMode) self.assertFalse(testScript._focusModeIsSticky) self.assertFalse(testScript._browseModeIsSticky) testScript.refreshKeyGrabs.assert_called_once_with() def test_manual_presentation_mode_toggle_still_interrupts(self): testScript = self._make_script(inFocusMode=False) with mock.patch.object(web_script.sound_theme_manager, "getManager", return_value=mock.Mock()): web_script.Script.togglePresentationMode(testScript, object(), "document") 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") class WebDynamicContentRecoveryRegressionTests(unittest.TestCase): def _make_dynamic_script(self): testScript = web_script.Script.__new__(web_script.Script) testScript._loadingDocumentContent = False testScript._lastCommandWasCaretNav = False testScript._lastCommandWasStructNav = False testScript._lastMouseButtonContext = (None, -1) testScript.lastMouseRoutingTime = 0 testScript.utilities = mock.Mock() testScript.utilities.eventIsBrowserUINoise.return_value = False testScript.utilities.isLiveRegion.return_value = False testScript.utilities.getTopLevelDocumentForObject.return_value = "document" testScript.utilities.isZombie.return_value = False testScript.utilities.handleEventFromContextReplicant.return_value = False testScript.utilities.handleEventForRemovedChild.return_value = False testScript.utilities.inDocumentContent.return_value = True return testScript def test_children_added_to_live_focus_preserves_caret_context(self): testScript = self._make_dynamic_script() focus = object() event = mock.Mock(source=focus, any_data=object()) with ( mock.patch.object(web_script.cthulhu_state, "locusOfFocus", focus), mock.patch.object(web_script.AXObject, "is_dead", return_value=False), mock.patch.object(web_script.AXUtilities, "is_busy", return_value=False), mock.patch.object(web_script.AXUtilities, "is_alert", return_value=False), ): result = web_script.Script.onChildrenAdded(testScript, event) self.assertFalse(result) testScript.utilities.dumpCache.assert_called_once_with( "document", preserveContext=True, ) def test_children_removed_from_live_focus_preserves_caret_context(self): testScript = self._make_dynamic_script() focus = object() event = mock.Mock(source=focus, any_data=object()) with ( mock.patch.object(web_script.cthulhu_state, "locusOfFocus", focus), mock.patch.object(web_script.AXObject, "is_dead", return_value=False), ): result = web_script.Script.onChildrenRemoved(testScript, event) self.assertFalse(result) testScript.utilities.dumpCache.assert_called_once_with( "document", preserveContext=True, ) def test_focused_event_source_becomes_context_when_search_fails(self): testScript = self._make_dynamic_script() source = object() oldFocus = object() event = mock.Mock(detail1=1, source=source) testScript._lastCommandWasCaretNav = True testScript.utilities.getDocumentForObject.return_value = "document" testScript.utilities.isWebAppDescendant.return_value = False testScript.utilities.handleEventFromContextReplicant.return_value = False testScript.utilities.getCaretContext.return_value = (None, -1) testScript.utilities.searchForCaretContext.return_value = (None, -1) testScript.utilities.inFindContainer.return_value = False with ( mock.patch.object(web_script.cthulhu_state, "locusOfFocus", oldFocus), mock.patch.object(web_script.AXUtilities, "is_editable", return_value=False), mock.patch.object(web_script.AXUtilities, "is_dialog_or_alert", return_value=False), mock.patch.object(web_script.AXUtilities, "is_focusable", return_value=True), mock.patch.object(web_script.AXUtilities, "is_focused", return_value=True), mock.patch.object(web_script.cthulhu, "setLocusOfFocus") as setLocusOfFocus, ): result = web_script.Script.onFocusedChanged(testScript, event) self.assertTrue(result) setLocusOfFocus.assert_called_once_with(event, source, False) testScript.utilities.setCaretContext.assert_called_once_with(source, 0) class WebContextReplicantRegressionTests(unittest.TestCase): def test_same_object_replicant_can_recover_before_old_focus_is_dead(self): utilities = web_script_utilities.Utilities.__new__(web_script_utilities.Utilities) document = object() parent = object() oldFocus = object() replicant = object() event = mock.Mock() utilities._caretContexts = {hash(parent): (oldFocus, 0)} utilities.getCaretContextPathRoleAndName = mock.Mock( return_value=([1, 2, 3], "button", "storm, microphone on") ) utilities.documentFrame = mock.Mock(return_value=document) utilities.isSameObject = mock.Mock(return_value=True) utilities.setCaretContext = mock.Mock() with ( mock.patch.object(web_script_utilities.cthulhu_state, "locusOfFocus", oldFocus), mock.patch.object(web_script_utilities.AXObject, "is_dead", return_value=False), mock.patch.object(web_script_utilities.AXObject, "get_path", return_value=[1, 2, 3]), mock.patch.object(web_script_utilities.AXObject, "get_role", return_value="button"), mock.patch.object(web_script_utilities.AXObject, "get_name", return_value="storm, microphone on"), mock.patch.object(web_script_utilities.AXObject, "get_parent", return_value=parent), mock.patch.object(web_script_utilities.cthulhu, "setLocusOfFocus") as setLocusOfFocus, ): result = web_script_utilities.Utilities.handleEventFromContextReplicant( utilities, event, replicant, ) self.assertTrue(result) setLocusOfFocus.assert_called_once_with(event, replicant, False) utilities.setCaretContext.assert_called_once_with(replicant, 0, document) if __name__ == "__main__": unittest.main() class WebSelectionRegressionTests(unittest.TestCase): def _make_script(self): testScript = web_script.Script.__new__(web_script.Script) testScript.pointOfReference = {} testScript.utilities = mock.Mock() testScript.utilities.sanityCheckActiveWindow = mock.Mock() testScript.utilities.isZombie.return_value = False testScript.utilities.eventIsBrowserUINoise.return_value = False testScript.utilities.eventIsBrowserUIAutocompleteNoise.return_value = False testScript.utilities.lastInputEventWasTab.return_value = False testScript.utilities.inFindContainer.return_value = False testScript.utilities.eventIsFromLocusOfFocusDocument.return_value = True testScript.utilities.textEventIsDueToInsertion.return_value = False testScript.utilities.textEventIsDueToDeletion.return_value = False testScript.utilities.isItemForEditableComboBox.return_value = False testScript.utilities.eventIsAutocompleteNoise.return_value = False testScript.utilities.eventIsSpinnerNoise.return_value = False testScript.utilities.caretMovedOutsideActiveGrid.return_value = False testScript.utilities.treatEventAsSpinnerValueChange.return_value = False testScript.utilities.queryNonEmptyText.return_value = "Darkest Dungeon" testScript.utilities.lastInputEventWasCaretNavWithSelection.return_value = True testScript.utilities.getCharacterContentsAtOffset.return_value = [[None, 0, 1, "D"]] testScript.utilities.pathComparison.return_value = -1 testScript.updateBraille = mock.Mock() testScript.speakContents = mock.Mock() testScript.speakMessage = mock.Mock() testScript._lastCommandWasCaretNav = True testScript._lastCommandWasStructNav = False testScript._lastCommandWasMouseButton = False testScript._lastMouseButtonContext = (None, -1) testScript._inFocusMode = False testScript._focusModeIsSticky = False testScript._browseModeIsSticky = False return testScript def test_broken_web_caret_selection_synthesizes_selection_from_caret_context(self): testScript = self._make_script() document = object() section = object() event = mock.Mock(source=section, detail1=-1) manager = mock.Mock() manager.last_event_was_forward_caret_selection.return_value = True manager.last_event_was_backward_caret_selection.return_value = False testScript.utilities.getTopLevelDocumentForObject.return_value = document testScript.utilities.getCaretContext.return_value = (section, 0) testScript.utilities.nextContext.return_value = (section, 1) testScript.utilities.getCharacterContentsAtOffset.return_value = [[section, 0, 1, "D"]] with ( mock.patch.object(web_script.cthulhu_state, "locusOfFocus", section), mock.patch.object(web_script.input_event_manager, "get_manager", return_value=manager), mock.patch.object(web_script.cthulhu, "setLocusOfFocus") as setLocusOfFocus, ): result = web_script.Script.onCaretMoved(testScript, event) self.assertTrue(result) testScript.utilities.setCaretContext.assert_called_once_with(section, 1, document) setLocusOfFocus.assert_called_once_with(event, section, False, True) testScript.speakContents.assert_called_once_with([[section, 0, 1, "D"]]) testScript.speakMessage.assert_called_once_with(messages.TEXT_SELECTED) self.assertEqual( testScript.pointOfReference["syntheticWebSelection"]["string"], "D", ) def test_broken_web_text_selection_event_synthesizes_selection_from_event_source(self): testScript = self._make_script() document = object() anchor = object() link = object() event = mock.Mock(source=link) testScript.utilities.getTopLevelDocumentForObject.return_value = document testScript.utilities.getCaretContext.return_value = (anchor, 0) testScript.utilities.nextContext.side_effect = [(anchor, 0)] testScript.utilities.getCharacterContentsAtOffset.side_effect = ( lambda obj, _offset: [[link, 0, 1, "S"]] if obj is link else [] ) testScript.utilities.textEventIsForNonNavigableTextObject.return_value = False testScript.utilities.isContentEditableWithEmbeddedObjects.return_value = False testScript._compareCaretContexts = mock.Mock(return_value=1) with ( mock.patch.object(web_script.cthulhu_state, "locusOfFocus", anchor), mock.patch.object(web_script.AXText, "get_caret_offset", return_value=0), mock.patch.object(web_script.AXText, "get_selected_ranges", return_value=[]), mock.patch.object(web_script.AXText, "get_substring", return_value="S"), mock.patch.object(web_script.cthulhu, "setLocusOfFocus") as setLocusOfFocus, ): result = web_script.Script.onTextSelectionChanged(testScript, event) self.assertTrue(result) testScript.utilities.setCaretContext.assert_called_once_with(link, 0, document) setLocusOfFocus.assert_called_once_with(event, link, False, True) testScript.speakContents.assert_called_once_with([[link, 0, 1, "S"]]) testScript.speakMessage.assert_called_once_with(messages.TEXT_SELECTED) self.assertEqual( testScript.pointOfReference["syntheticWebSelection"]["string"], "S", ) def test_web_utilities_all_selected_text_falls_back_to_synthetic_selection(self): testScript = mock.Mock(pointOfReference={}) utilities = web_script_utilities.Utilities.__new__(web_script_utilities.Utilities) utilities._script = testScript document = object() obj = object() testScript.pointOfReference["syntheticWebSelection"] = { "document": document, "string": "Dark", } with ( mock.patch.object( web_script_utilities.script_utilities.Utilities, "allSelectedText", return_value=["", 0, 0], ), mock.patch.object(utilities, "inDocumentContent", return_value=True), mock.patch.object(utilities, "getTopLevelDocumentForObject", return_value=document), ): result = web_script_utilities.Utilities.allSelectedText(utilities, obj) self.assertEqual(result, ["Dark", 0, 4])