Ctrl K
ComunidadGuías…

ClipmanLinux/test_context_help.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import pathlib
import hashlib
import io
import shutil
import tarfile
import tempfile
import unittest
import ast
from unittest import mock

import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Gdk", "4.0")
from gi.repository import Gdk, GLib, Gtk

import clipman
import context_help
import update_service
from test_content_features import image_rich_text, small_png


class ContextHelpTests(unittest.TestCase):
    def test_open_link_hides_history_only_after_success(self):
        app = mock.Mock()
        app.section = "links"
        app.selected_entry.return_value = {"text": "https://example.com/test"}
        app.selected_entries.return_value = [app.selected_entry.return_value]
        clipman.ClipmanApplication.open_selected_link(app)
        app._open_uri.assert_called_once_with("https://example.com/test")
        app.window.set_visible.assert_called_once_with(False)
        app.reset_mock()
        app._open_uri.side_effect = OSError("no default browser")
        clipman.ClipmanApplication.open_selected_link(app)
        app.window.set_visible.assert_not_called()
        app.sounds.play.assert_called_once_with("skip")

    def test_open_files_reveals_location_without_executing_content(self):
        app = mock.Mock()
        app.section = "files"
        app.selected_entries.return_value = [{"files": ["test.txt"]}]
        app.go_to_selected_file.return_value = True
        clipman.ClipmanApplication.open_selected_link(app)
        app.go_to_selected_file.assert_called_once()
        app._open_uri.assert_not_called()
        app.window.set_visible.assert_called_once_with(False)
        app.reset_mock()
        app.go_to_selected_file.return_value = False
        clipman.ClipmanApplication.open_selected_link(app)
        app.window.set_visible.assert_not_called()

    def test_open_image_preserves_bytes_history_and_clipboard(self):
        with tempfile.TemporaryDirectory() as scratch:
            entry = {"text": "original image", "rich_text": image_rich_text(),
                     "created_unix_ms": 1700000000000, "source_machine": "Test device"}
            app = mock.Mock()
            app.section = "rich"
            app.selected_entries.return_value = [entry]
            app.selected_entry.return_value = entry
            app.image_viewer_cache = clipman.ClipboardImageFileCache(pathlib.Path(scratch))
            clipman.ClipmanApplication.open_selected_link(app)
            path = app.image_viewer_cache.current
            self.assertEqual(path.read_bytes(), small_png())
            self.assertEqual(path.stat().st_mtime, 1700000000)
            self.assertEqual(entry["text"], "original image")
            app._set_clipboard.assert_not_called()
            app._open_uri.assert_called_once_with(path.as_uri())
            app.window.set_visible.assert_called_once_with(False)
            app.reset_mock()
            app._open_uri.side_effect = OSError("no image viewer")
            clipman.ClipmanApplication.open_selected_link(app)
            app.window.set_visible.assert_not_called()

    def test_open_multiple_items_keeps_history_open(self):
        app = mock.Mock()
        app.section = "links"
        app.selected_entries.return_value = [{"text": "https://example.com/a"}, {"text": "https://example.com/b"}]
        clipman.ClipmanApplication.open_selected_link(app)
        app._open_uri.assert_not_called()
        app.window.set_visible.assert_not_called()

    def test_quick_clip_restores_hidden_history_on_cancel(self):
        with tempfile.TemporaryDirectory() as scratch, mock.patch("clipman.config_home", return_value=pathlib.Path(scratch)):
            app = clipman.ClipmanApplication()
            app.window = Gtk.Window()
            self.assertFalse(app.window.get_visible())
            app.handle_global_hotkey("quick-clip")
            self.pump()
            dialog = next(window for window in Gtk.Window.get_toplevels() if window.get_title() == "Quick Clip")
            self.assertTrue(app.window.get_visible())
            dialog.response(Gtk.ResponseType.CANCEL)
            self.assertFalse(app.window.get_visible())
            app.window.present()
            app.handle_global_hotkey("quick-clip")
            self.pump()
            dialog = next(window for window in Gtk.Window.get_toplevels() if window.get_title() == "Quick Clip")
            dialog.response(Gtk.ResponseType.CANCEL)
            self.assertTrue(app.window.get_visible())

    def test_explanatory_descriptions_use_retained_help_in_all_dialogs(self):
        source = ast.parse(pathlib.Path(clipman.__file__).read_text())
        bypasses = []
        for node in ast.walk(source):
            if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
                if node.func.attr == "update_property" and any(
                    isinstance(value, ast.Attribute) and value.attr == "DESCRIPTION"
                    for value in ast.walk(node)
                ):
                    bypasses.append(node.lineno)
        self.assertEqual(bypasses, [], "Descriptions bypass the retained-help helper")

    def test_dynamic_help_clears_automatic_speech_and_preserves_states(self):
        widget = mock.Mock()
        context_help.set_properties(widget,
            [Gtk.AccessibleProperty.LABEL, Gtk.AccessibleProperty.DESCRIPTION],
            ["Remove channel", "Select a channel to remove."])
        widget.update_property.assert_called_with(
            [Gtk.AccessibleProperty.LABEL, Gtk.AccessibleProperty.DESCRIPTION],
            ["Remove channel", ""])
        context_help.set_properties(widget, [Gtk.AccessibleProperty.DESCRIPTION],
            ["Remove this channel without deleting its clips."])
        widget.update_property.assert_called_with([Gtk.AccessibleProperty.DESCRIPTION], [""])
        self.assertEqual(widget._clipman_help_name, "Remove channel")
        self.assertEqual(widget._clipman_help, "Remove this channel without deleting its clips.")
        widget.update_state.assert_not_called()

    def tearDown(self):
        for window in Gtk.Window.get_toplevels():
            window.destroy()
        self.pump()

    @staticmethod
    def pump():
        context = GLib.MainContext.default()
        for _ in range(30):
            if not context.pending():
                break
            context.iteration(False)

    def test_help_uses_metadata_not_private_values(self):
        field = Gtk.PasswordEntry()
        field.set_text("private-value-not-help")
        context_help.set_properties(field,
            [Gtk.AccessibleProperty.LABEL, Gtk.AccessibleProperty.DESCRIPTION],
            ["History password", "Unlock this device's encrypted history."])
        name, text = context_help.focused_content(field)
        self.assertEqual(name, "History password")
        self.assertEqual(text, "Unlock this device's encrypted history.")
        self.assertNotIn(field.get_text(), text)

    def test_private_history_row_uses_its_list_help(self):
        listing = Gtk.ListBox()
        listing._clipman_help_name = "Text history"
        row = Gtk.ListBoxRow()
        context_help.set_properties(row, [Gtk.AccessibleProperty.LABEL], ["private clip contents"])
        listing.append(row)
        self.assertEqual(context_help.focused_content(row)[0], "Text history")

    def test_help_is_read_only_closes_and_restores_focus(self):
        owner = Gtk.Window()
        field = Gtk.Entry()
        context_help.set_properties(field, [Gtk.AccessibleProperty.LABEL], ["Device name"])
        owner.set_child(field)
        owner.present()
        field.grab_focus()
        self.pump()
        focused = owner.get_focus()
        manual = mock.Mock()
        context_help.attach(owner, manual)
        controller = owner._clipman_help_controller
        self.assertTrue(controller.emit("key-pressed", Gdk.KEY_F1, 0, Gdk.ModifierType(0)))
        dialog = owner._clipman_active_help
        self.assertEqual(dialog.get_title(), "Help: Device name")
        view = dialog.get_content_area().get_first_child().get_child()
        self.assertFalse(view.get_editable())
        self.assertFalse(view.get_accepts_tab())
        context_help.show(owner, manual)
        self.assertIs(owner._clipman_active_help, dialog)
        dialog.response(Gtk.ResponseType.HELP)
        manual.assert_called_once()
        dialog.close()
        self.pump()
        self.assertIsNone(owner._clipman_active_help)
        self.assertIs(owner.get_focus(), focused)
        self.assertFalse(controller.emit("key-pressed", Gdk.KEY_F1, 0, Gdk.ModifierType.SHIFT_MASK))
        context_help.show(owner, manual)
        owner._clipman_active_help.response(Gtk.ResponseType.CLOSE)
        self.assertIsNone(owner._clipman_active_help)

    def test_plain_f1_is_not_a_shortcut(self):
        field = clipman.HotkeyEntry("<Control><Alt>h", "Show history hotkey")
        original = field.get_accelerator()
        self.assertFalse(field._key_pressed(None, Gdk.KEY_F1, 0, Gdk.ModifierType(0)))
        self.assertEqual(field.get_accelerator(), original)
        self.assertTrue(field._key_pressed(None, Gdk.KEY_F1, 0, Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.ALT_MASK))
        self.assertNotEqual(field.get_accelerator(), original)

    def test_every_preference_control_has_tailored_help(self):
        with tempfile.TemporaryDirectory() as scratch, mock.patch("clipman.config_home", return_value=pathlib.Path(scratch)):
            application = clipman.ClipmanApplication()
            application.window = Gtk.Window()
            application.show_preferences()
            dialog = next(window for window in Gtk.Window.get_toplevels() if window.get_title() == "Clipman Preferences")
            missing = []
            def inspect(widget):
                if isinstance(widget, (Gtk.Entry, Gtk.TextView, Gtk.DropDown, Gtk.SpinButton, Gtk.CheckButton, Gtk.Button, Gtk.Notebook)):
                    name, text = context_help.focused_content(widget)
                    if text.startswith("Use the focused control"):
                        missing.append(name)
                child = widget.get_first_child()
                while child:
                    inspect(child)
                    child = child.get_next_sibling()
            inspect(dialog.get_content_area())
            self.assertEqual(sorted(set(missing)), [])
            dialog.destroy()

    def test_every_application_dialog_uses_the_help_presenter(self):
        source = pathlib.Path(clipman.__file__).read_text()
        # The only direct presentation is inside the central presenter itself.
        self.assertEqual(source.count(".present()"), 1)
        for filename in ("install.sh", "build.sh"):
            self.assertIn("context_help.py", pathlib.Path(clipman.__file__).with_name(filename).read_text())

    def test_update_rejects_a_package_without_context_help(self):
        files = ("clipman.py", "clipman-hotkeys.py", "clipman-updater.py", "update_service.py",
                 "install.sh", "libexec/clipman-gui-backend", "Manual.html", "LICENSE.txt", "BUILD_STAMP")
        for include_help in (False, True):
            payload = io.BytesIO()
            with tarfile.open(fileobj=payload, mode="w:gz") as archive:
                for name in (*files, "VERSION", *(("context_help.py",) if include_help else ())):
                    data = b"3.1.9\n" if name == "VERSION" else b"test\n"
                    member = tarfile.TarInfo("Clipman-Linux-GUI/" + name)
                    member.size = len(data)
                    archive.addfile(member, io.BytesIO(data))
            data = payload.getvalue()
            candidate = update_service.UpdateCandidate("3.1.9", "https://github.com/OnjLouis/Clipman",
                "https://github.com/OnjLouis/Clipman/update.tar.gz", "update.tar.gz",
                "sha256:" + hashlib.sha256(data).hexdigest(), len(data))
            opener = lambda *_args, **_kwargs: io.BytesIO(data)
            if not include_help:
                with self.assertRaisesRegex(update_service.UpdateError, "incomplete"):
                    update_service.stage_update(candidate, opener)
            else:
                package, temporary = update_service.stage_update(candidate, opener)
                try:
                    self.assertTrue((package / "context_help.py").is_file())
                finally:
                    shutil.rmtree(temporary)


if __name__ == "__main__":
    unittest.main()