v1.1.3
Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai> Co-Authored-By: Michel Thomazo <michel.thomazo@mistral.ai> Co-Authored-By: Vincent Guilloux <vincent.guilloux@mistral.ai>
This commit is contained in:
parent
a340a721ea
commit
661588de0c
48 changed files with 1679 additions and 467 deletions
228
tests/cli/test_clipboard.py
Normal file
228
tests/cli/test_clipboard.py
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from unittest.mock import MagicMock, mock_open, patch
|
||||
|
||||
import pytest
|
||||
from textual.app import App
|
||||
|
||||
from vibe.cli.clipboard import _copy_osc52, copy_selection_to_clipboard
|
||||
|
||||
|
||||
class MockWidget:
|
||||
def __init__(
|
||||
self,
|
||||
text_selection: object | None = None,
|
||||
get_selection_result: tuple[str, object] | None = None,
|
||||
get_selection_raises: Exception | None = None,
|
||||
) -> None:
|
||||
self.text_selection = text_selection
|
||||
self._get_selection_result = get_selection_result
|
||||
self._get_selection_raises = get_selection_raises
|
||||
|
||||
def get_selection(self, selection: object) -> tuple[str, object]:
|
||||
if self._get_selection_raises:
|
||||
raise self._get_selection_raises
|
||||
if self._get_selection_result is None:
|
||||
return ("", None)
|
||||
return self._get_selection_result
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_app() -> App:
|
||||
app = MagicMock(spec=App)
|
||||
app.query = MagicMock(return_value=[])
|
||||
app.notify = MagicMock()
|
||||
app.copy_to_clipboard = MagicMock()
|
||||
return cast(App, app)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"widgets,description",
|
||||
[
|
||||
([], "no widgets"),
|
||||
([MockWidget(text_selection=None)], "no selection"),
|
||||
([MockWidget()], "widget without text_selection attr"),
|
||||
(
|
||||
[
|
||||
MockWidget(
|
||||
text_selection=SimpleNamespace(),
|
||||
get_selection_raises=ValueError("Error getting selection"),
|
||||
)
|
||||
],
|
||||
"get_selection raises",
|
||||
),
|
||||
(
|
||||
[MockWidget(text_selection=SimpleNamespace(), get_selection_result=None)],
|
||||
"empty result",
|
||||
),
|
||||
(
|
||||
[
|
||||
MockWidget(
|
||||
text_selection=SimpleNamespace(), get_selection_result=(" ", None)
|
||||
)
|
||||
],
|
||||
"empty text",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_copy_selection_to_clipboard_no_notification(
|
||||
mock_app: MagicMock, widgets: list[MockWidget], description: str
|
||||
) -> None:
|
||||
if description == "widget without text_selection attr":
|
||||
del widgets[0].text_selection
|
||||
mock_app.query.return_value = widgets
|
||||
|
||||
copy_selection_to_clipboard(mock_app)
|
||||
mock_app.notify.assert_not_called()
|
||||
|
||||
|
||||
@patch("vibe.cli.clipboard._copy_osc52")
|
||||
@patch("vibe.cli.clipboard.pyperclip.copy")
|
||||
def test_copy_selection_to_clipboard_success_with_osc52(
|
||||
mock_pyperclip_copy: MagicMock, mock_osc52_copy: MagicMock, mock_app: MagicMock
|
||||
) -> None:
|
||||
widget = MockWidget(
|
||||
text_selection=SimpleNamespace(), get_selection_result=("selected text", None)
|
||||
)
|
||||
mock_app.query.return_value = [widget]
|
||||
|
||||
copy_selection_to_clipboard(mock_app)
|
||||
|
||||
mock_osc52_copy.assert_called_once_with("selected text")
|
||||
mock_pyperclip_copy.assert_not_called()
|
||||
mock_app.copy_to_clipboard.assert_not_called()
|
||||
mock_app.notify.assert_called_once_with(
|
||||
'"selected text" copied to clipboard', severity="information", timeout=2
|
||||
)
|
||||
|
||||
|
||||
@patch("vibe.cli.clipboard._copy_osc52")
|
||||
@patch("vibe.cli.clipboard.pyperclip.copy")
|
||||
def test_copy_selection_to_clipboard_osc52_fails_success_with_pyperclip(
|
||||
mock_pyperclip_copy: MagicMock, mock_osc52_copy: MagicMock, mock_app: MagicMock
|
||||
) -> None:
|
||||
widget = MockWidget(
|
||||
text_selection=SimpleNamespace(),
|
||||
get_selection_result=(" selected text ", None),
|
||||
)
|
||||
mock_app.query.return_value = [widget]
|
||||
mock_osc52_copy.side_effect = Exception("osc52 failed")
|
||||
|
||||
copy_selection_to_clipboard(mock_app)
|
||||
|
||||
mock_osc52_copy.assert_called_once_with(" selected text ")
|
||||
mock_pyperclip_copy.assert_called_once_with(" selected text ")
|
||||
mock_app.notify.assert_called_once_with(
|
||||
'" selected text " copied to clipboard', severity="information", timeout=2
|
||||
)
|
||||
mock_app.copy_to_clipboard.assert_not_called()
|
||||
|
||||
|
||||
@patch("vibe.cli.clipboard._copy_osc52")
|
||||
@patch("vibe.cli.clipboard.pyperclip.copy")
|
||||
def test_copy_selection_to_clipboard_osc52_and_pyperclip_fail_success_with_app_copy(
|
||||
mock_pyperclip_copy: MagicMock, mock_osc52_copy: MagicMock, mock_app: MagicMock
|
||||
) -> None:
|
||||
widget = MockWidget(
|
||||
text_selection=SimpleNamespace(), get_selection_result=("selected text", None)
|
||||
)
|
||||
mock_app.query.return_value = [widget]
|
||||
mock_osc52_copy.side_effect = Exception("osc52 failed")
|
||||
mock_pyperclip_copy.side_effect = Exception("pyperclip failed")
|
||||
|
||||
copy_selection_to_clipboard(mock_app)
|
||||
|
||||
mock_osc52_copy.assert_called_once_with("selected text")
|
||||
mock_pyperclip_copy.assert_called_once_with("selected text")
|
||||
mock_app.copy_to_clipboard.assert_called_once_with("selected text")
|
||||
mock_app.notify.assert_called_once_with(
|
||||
'"selected text" copied to clipboard', severity="information", timeout=2
|
||||
)
|
||||
|
||||
|
||||
@patch("vibe.cli.clipboard._copy_osc52")
|
||||
@patch("vibe.cli.clipboard.pyperclip.copy")
|
||||
def test_copy_selection_to_clipboard_all_methods_fail(
|
||||
mock_pyperclip_copy: MagicMock, mock_osc52_copy: MagicMock, mock_app: MagicMock
|
||||
) -> None:
|
||||
widget = MockWidget(
|
||||
text_selection=SimpleNamespace(), get_selection_result=("selected text", None)
|
||||
)
|
||||
mock_app.query.return_value = [widget]
|
||||
mock_osc52_copy.side_effect = Exception("osc52 failed")
|
||||
mock_pyperclip_copy.side_effect = Exception("pyperclip failed")
|
||||
mock_app.copy_to_clipboard.side_effect = Exception("app copy failed")
|
||||
|
||||
copy_selection_to_clipboard(mock_app)
|
||||
|
||||
mock_osc52_copy.assert_called_once_with("selected text")
|
||||
mock_pyperclip_copy.assert_called_once_with("selected text")
|
||||
mock_app.copy_to_clipboard.assert_called_once_with("selected text")
|
||||
mock_app.notify.assert_called_once_with(
|
||||
"Failed to copy - no clipboard method available", severity="warning", timeout=3
|
||||
)
|
||||
|
||||
|
||||
def test_copy_selection_to_clipboard_multiple_widgets(mock_app: MagicMock) -> None:
|
||||
widget1 = MockWidget(
|
||||
text_selection=SimpleNamespace(), get_selection_result=("first selection", None)
|
||||
)
|
||||
widget2 = MockWidget(
|
||||
text_selection=SimpleNamespace(),
|
||||
get_selection_result=("second selection", None),
|
||||
)
|
||||
widget3 = MockWidget(text_selection=None)
|
||||
mock_app.query.return_value = [widget1, widget2, widget3]
|
||||
|
||||
with patch("vibe.cli.clipboard._copy_osc52") as mock_osc52_copy:
|
||||
copy_selection_to_clipboard(mock_app)
|
||||
|
||||
mock_osc52_copy.assert_called_once_with("first selection\nsecond selection")
|
||||
mock_app.notify.assert_called_once_with(
|
||||
'"first selection⏎second selection" copied to clipboard',
|
||||
severity="information",
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
|
||||
def test_copy_selection_to_clipboard_preview_shortening(mock_app: MagicMock) -> None:
|
||||
long_text = "a" * 100
|
||||
widget = MockWidget(
|
||||
text_selection=SimpleNamespace(), get_selection_result=(long_text, None)
|
||||
)
|
||||
mock_app.query.return_value = [widget]
|
||||
|
||||
with (
|
||||
patch("vibe.cli.clipboard._copy_osc52") as mock_osc52_copy,
|
||||
patch("vibe.cli.clipboard.pyperclip.copy") as mock_pyperclip_copy,
|
||||
):
|
||||
mock_osc52_copy.side_effect = Exception("osc52 failed")
|
||||
copy_selection_to_clipboard(mock_app)
|
||||
|
||||
mock_osc52_copy.assert_called_once_with(long_text)
|
||||
mock_pyperclip_copy.assert_called_once_with(long_text)
|
||||
notification_call = mock_app.notify.call_args
|
||||
assert notification_call is not None
|
||||
assert '"' in notification_call[0][0]
|
||||
assert "copied to clipboard" in notification_call[0][0]
|
||||
assert len(notification_call[0][0]) < len(long_text) + 30
|
||||
|
||||
|
||||
@patch("builtins.open", new_callable=mock_open)
|
||||
def test_copy_osc52_writes_correct_sequence(
|
||||
mock_file: MagicMock, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.delenv("TMUX", raising=False)
|
||||
test_text = "héllo wörld 🎉"
|
||||
|
||||
_copy_osc52(test_text)
|
||||
|
||||
encoded = base64.b64encode(test_text.encode("utf-8")).decode("ascii")
|
||||
expected_seq = f"\033]52;c;{encoded}\a"
|
||||
mock_file.assert_called_once_with("/dev/tty", "w")
|
||||
handle = mock_file()
|
||||
handle.write.assert_called_once_with(expected_seq)
|
||||
handle.flush.assert_called_once()
|
||||
Loading…
Add table
Add a link
Reference in a new issue