import sys import unittest from pathlib import Path from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from cthulhu import where_am_i_presenter from cthulhu.plugins.WindowTitleReader.plugin import WindowTitleReader class WindowTitleFallbackRegressionTests(unittest.TestCase): def test_activate_registers_reader_api_and_starts_fallback_tracking(self): plugin = WindowTitleReader() plugin.app = mock.Mock() with ( mock.patch.object(plugin, "_register_keybinding"), mock.patch.object(plugin, "_start_tracking") as startTracking, ): plugin.activate(plugin) plugin.app.getDynamicApiManager.return_value.registerAPI.assert_called_once_with( "WindowTitleReader", plugin, overwrite=True, ) startTracking.assert_called_once_with() def test_poll_schedules_fallback_after_active_window_changes(self): plugin = WindowTitleReader() plugin._pollSourceId = 1 plugin._lastActiveWindowId = 100 activeWindow = mock.Mock(id=200) with ( mock.patch.object(plugin, "_ensure_x11_display", return_value=True), mock.patch.object(plugin, "_get_active_window", return_value=activeWindow), mock.patch.object(plugin, "_get_current_title", return_value="XTerm"), mock.patch.object(plugin, "_schedule_fallback_title") as scheduleFallback, ): self.assertTrue(plugin._poll_window_title()) scheduleFallback.assert_called_once_with() def test_poll_keeps_previous_window_across_transient_missing_active_window(self): plugin = WindowTitleReader() plugin._pollSourceId = 1 plugin._lastActiveWindowId = 100 with ( mock.patch.object(plugin, "_ensure_x11_display", return_value=True), mock.patch.object(plugin, "_get_active_window", return_value=None), ): self.assertTrue(plugin._poll_window_title()) self.assertEqual(plugin._lastActiveWindowId, 100) def test_fallback_is_empty_when_atspi_exposes_same_title(self): plugin = WindowTitleReader() plugin._pollSourceId = 1 activeWindow = mock.Mock() with ( mock.patch.object(plugin, "_ensure_x11_display", return_value=True), mock.patch.object(plugin, "_get_active_window", return_value=activeWindow), mock.patch.object(plugin, "_get_current_title", return_value="Terminal"), ): self.assertEqual(plugin.get_fallback_title("Terminal"), "") def test_fallback_replaces_wine_desktop_title(self): plugin = WindowTitleReader() plugin._pollSourceId = 1 activeWindow = mock.Mock() with ( mock.patch.object(plugin, "_ensure_x11_display", return_value=True), mock.patch.object(plugin, "_get_active_window", return_value=activeWindow), mock.patch.object(plugin, "_get_current_title", return_value="Game Window"), ): self.assertEqual(plugin.get_fallback_title("Wine Desktop"), "Game Window") def test_fallback_prefers_i3_title_and_window_id(self): plugin = WindowTitleReader() plugin.app = mock.Mock() snapshot = mock.Mock( backend_name="i3-ipc", focused_workspace_empty=False, focused_window_id=0x200000C, focused_window_title="Accurate i3 title", ) plugin.app.getCompositorStateAdapter.return_value.get_snapshot.return_value = snapshot with mock.patch.object(plugin, "_get_active_window") as getX11Window: self.assertEqual(plugin.get_fallback_title("Incomplete title"), "Accurate i3 title") getX11Window.assert_not_called() def test_i3_wine_desktop_title_uses_existing_child_window_resolution(self): plugin = WindowTitleReader() plugin.app = mock.Mock() snapshot = mock.Mock( backend_name="i3-ipc", focused_workspace_empty=False, focused_window_id=0x2400007, focused_window_title="Wine Desktop", ) desktopWindow = mock.Mock() plugin.app.getCompositorStateAdapter.return_value.get_snapshot.return_value = snapshot plugin._display = mock.Mock() plugin._display.create_resource_object.return_value = desktopWindow with ( mock.patch.object(plugin, "_ensure_x11_display", return_value=True), mock.patch.object( plugin, "_get_window_title", return_value="Wine Desktop", ), mock.patch.object( plugin, "_get_wine_desktop_title", return_value="Actual Game Window", ) as getWineTitle, ): self.assertEqual( plugin.get_fallback_title("Wine Desktop"), "Actual Game Window", ) plugin._display.create_resource_object.assert_called_once_with("window", 0x2400007) getWineTitle.assert_called_once_with(desktopWindow) def test_wine_title_resolution_ignores_unrelated_x11_focus(self): plugin = WindowTitleReader() desktopWindow = mock.Mock(id=10) unrelatedFocus = mock.Mock(id=20) with ( mock.patch.object(plugin, "_get_focus_window", return_value=unrelatedFocus), mock.patch.object( plugin, "_window_is_descendant_of", return_value=False, ) as isDescendant, mock.patch.object(plugin, "_get_window_title", return_value="Unrelated Terminal"), mock.patch.object( plugin, "_find_child_title", return_value="Actual Game Window", ) as findChildTitle, ): self.assertEqual( plugin._get_wine_desktop_title(desktopWindow), "Actual Game Window", ) isDescendant.assert_called_once_with(unrelatedFocus, desktopWindow) findChildTitle.assert_called_once_with(desktopWindow) def test_wine_child_title_filter_rejects_infrastructure_windows(self): plugin = WindowTitleReader() helperWindow = mock.Mock() helperWindow.get_wm_class.return_value = ( "cthulhu-wine-access.exe", "cthulhu-wine-access.exe", ) gameWindow = mock.Mock() gameWindow.get_wm_class.return_value = ("game.exe", "game.exe") self.assertFalse(plugin._is_usable_wine_child_title(gameWindow, "Default IME")) self.assertFalse(plugin._is_usable_wine_child_title(helperWindow, "NVDA")) self.assertTrue(plugin._is_usable_wine_child_title(gameWindow, "Actual Game Window")) def test_confirmed_empty_i3_workspace_does_not_fall_back_to_stale_x11_title(self): plugin = WindowTitleReader() plugin.app = mock.Mock() snapshot = mock.Mock( backend_name="i3-ipc", focused_workspace_empty=True, focused_window_id=None, focused_window_title="", ) plugin.app.getCompositorStateAdapter.return_value.get_snapshot.return_value = snapshot with mock.patch.object(plugin, "_get_active_window") as getX11Window: self.assertEqual(plugin.get_fallback_title("Browser"), "") getX11Window.assert_not_called() def test_unknown_i3_context_falls_back_to_generic_x11_title(self): plugin = WindowTitleReader() plugin.app = mock.Mock() snapshot = mock.Mock( backend_name="i3-ipc", focused_workspace_empty=None, focused_window_id=None, focused_window_title="", ) plugin.app.getCompositorStateAdapter.return_value.get_snapshot.return_value = snapshot plugin._pollSourceId = 1 activeWindow = mock.Mock() with ( mock.patch.object(plugin, "_ensure_x11_display", return_value=True) as ensureX11, mock.patch.object(plugin, "_get_active_window", return_value=activeWindow), mock.patch.object(plugin, "_get_current_title", return_value="Generic X title"), ): self.assertEqual(plugin.get_fallback_title("Incomplete"), "Generic X title") ensureX11.assert_called_once_with() def test_known_i3_xid_with_missing_title_uses_exact_x11_title(self): plugin = WindowTitleReader() plugin.app = mock.Mock() snapshot = mock.Mock( backend_name="i3-ipc", focused_workspace_empty=False, focused_window_id=0x2600003, focused_window_title="", ) activeWindow = mock.Mock() plugin.app.getCompositorStateAdapter.return_value.get_snapshot.return_value = snapshot plugin._display = mock.Mock() plugin._display.create_resource_object.return_value = activeWindow with ( mock.patch.object(plugin, "_ensure_x11_display", return_value=True), mock.patch.object( plugin, "_get_current_title", return_value="Exact X11 title", ) as getCurrentTitle, ): self.assertEqual( plugin.get_fallback_title("Incomplete"), "Exact X11 title", ) plugin._display.create_resource_object.assert_called_once_with("window", 0x2600003) getCurrentTitle.assert_called_once_with(activeWindow) def test_present_title_uses_fallback_instead_of_atspi_title(self): presenter = where_am_i_presenter.WhereAmIPresenter() script = mock.Mock() script.speechGenerator.generateTitle.return_value = [("Wine Desktop", None)] with ( mock.patch.object(where_am_i_presenter.cthulhu_state, "locusOfFocus", object()), mock.patch.object(where_am_i_presenter.AXObject, "is_dead", return_value=False), mock.patch.object( presenter, "_get_fallback_title", return_value="Game Window", ), ): self.assertTrue(presenter.present_title(script)) script.presentMessage.assert_called_once_with("Game Window") if __name__ == "__main__": unittest.main()