264 lines
10 KiB
Python
264 lines
10 KiB
Python
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()
|