1
0
Fork 0

Adding upstream version 0.15.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-24 11:29:34 +01:00
parent 0184169650
commit c6da052ee9
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
47 changed files with 6799 additions and 0 deletions

9
tests/conftest.py Normal file
View file

@ -0,0 +1,9 @@
from pathlib import Path
import pytest
@pytest.fixture
def data_dir() -> Path:
here = Path(__file__)
return here.parent / "data"

View file

View file

@ -0,0 +1,2 @@
def foo(bar: str, baz: int) -> None:
return

View file

View file

View file

View file

@ -0,0 +1,62 @@
from typing import Type, Union
from unittest.mock import MagicMock
import pytest
from textual.app import App, ComposeResult
from textual.driver import Driver
from textual.types import CSSPathType
from textual_textarea.text_editor import TextEditor
class TextEditorApp(App, inherit_bindings=False):
def __init__(
self,
driver_class: Union[Type[Driver], None] = None,
css_path: Union[CSSPathType, None] = None,
watch_css: bool = False,
language: Union[str, None] = None,
use_system_clipboard: bool = True,
):
self.language = language
self.use_system_clipboard = use_system_clipboard
super().__init__(driver_class, css_path, watch_css)
def compose(self) -> ComposeResult:
self.editor = TextEditor(
language=self.language,
use_system_clipboard=self.use_system_clipboard,
id="ta",
)
yield self.editor
def on_mount(self) -> None:
self.editor.focus()
@pytest.fixture
def app() -> App:
app = TextEditorApp(language="python")
return app
@pytest.fixture(
params=[False, True],
ids=["no_sys_clipboard", "default"],
)
def app_all_clipboards(request: pytest.FixtureRequest) -> App:
app = TextEditorApp(use_system_clipboard=request.param)
return app
@pytest.fixture(autouse=True)
def mock_pyperclip(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
mock = MagicMock()
mock.determine_clipboard.return_value = mock.copy, mock.paste
def set_paste(x: str) -> None:
mock.paste.return_value = x
mock.copy.side_effect = set_paste
monkeypatch.setattr("textual_textarea.text_editor.pyperclip", mock)
return mock

View file

@ -0,0 +1,279 @@
from __future__ import annotations
from pathlib import Path
from time import monotonic
from typing import Callable
from unittest.mock import MagicMock
import pytest
from textual.app import App
from textual.message import Message
from textual.widgets.text_area import Selection
from textual_textarea import TextEditor
@pytest.fixture
def word_completer() -> Callable[[str], list[tuple[str, str]]]:
def _completer(prefix: str) -> list[tuple[str, str]]:
words = [
"satisfy",
"season",
"second",
"seldom",
"select",
"self",
"separate",
"set",
"space",
"super",
]
return [(w, w) for w in words if w.startswith(prefix)]
return _completer
@pytest.fixture
def word_completer_with_types() -> Callable[[str], list[tuple[tuple[str, str], str]]]:
def _completer(prefix: str) -> list[tuple[tuple[str, str], str]]:
words = [
"satisfy",
"season",
"second",
"seldom",
"select",
"self",
"separate",
"set",
"space",
"super",
]
return [((w, "word"), w) for w in words if w.startswith(prefix)]
return _completer
@pytest.fixture
def member_completer() -> Callable[[str], list[tuple[str, str]]]:
mock = MagicMock()
mock.return_value = [("completion", "completion")]
return mock
@pytest.mark.asyncio
async def test_autocomplete(
app: App, word_completer: Callable[[str], list[tuple[str, str]]]
) -> None:
messages: list[Message] = []
async with app.run_test(message_hook=messages.append) as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ta.word_completer = word_completer
ta.focus()
while ta.word_completer is None:
await pilot.pause()
start_time = monotonic()
await pilot.press("s")
while ta.completion_list.is_open is False:
if monotonic() - start_time > 10:
print("MESSAGES:")
print("\n".join([str(m) for m in messages]))
break
await pilot.pause()
assert ta.text_input
assert ta.text_input.completer_active == "word"
assert ta.completion_list.is_open is True
assert ta.completion_list.option_count == 10
first_offset = ta.completion_list.styles.offset
await pilot.press("e")
await app.workers.wait_for_complete()
await pilot.pause()
assert ta.text_input.completer_active == "word"
assert ta.completion_list.is_open is True
assert ta.completion_list.option_count == 7
assert ta.completion_list.styles.offset == first_offset
await pilot.press("z") # sez, no matches
await app.workers.wait_for_complete()
await pilot.pause()
assert ta.text_input.completer_active is None
assert ta.completion_list.is_open is False
# backspace when the list is not open doesn't re-open it
await pilot.press("backspace")
await app.workers.wait_for_complete()
await pilot.pause()
assert ta.text_input.completer_active is None
assert ta.completion_list.is_open is False
await pilot.press("l") # sel
await app.workers.wait_for_complete()
await pilot.pause()
assert ta.text_input.completer_active == "word"
assert ta.completion_list.is_open is True
assert ta.completion_list.option_count == 3
assert ta.completion_list.styles.offset == first_offset
await pilot.press("backspace") # se
await app.workers.wait_for_complete()
await pilot.pause()
assert ta.text_input.completer_active == "word"
assert ta.completion_list.is_open is True
assert ta.completion_list.option_count == 7
assert ta.completion_list.styles.offset == first_offset
await pilot.press("enter")
await app.workers.wait_for_complete()
await pilot.pause()
assert ta.text_input.completer_active is None
assert ta.completion_list.is_open is False
assert ta.text == "season"
assert ta.selection.end[1] == 6
@pytest.mark.asyncio
async def test_autocomplete_with_types(
app: App,
word_completer_with_types: Callable[[str], list[tuple[tuple[str, str], str]]],
) -> None:
messages: list[Message] = []
word_completer = word_completer_with_types
async with app.run_test(message_hook=messages.append) as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ta.word_completer = word_completer
ta.focus()
while ta.word_completer is None:
await pilot.pause()
start_time = monotonic()
await pilot.press("s")
while ta.completion_list.is_open is False:
if monotonic() - start_time > 10:
print("MESSAGES:")
print("\n".join([str(m) for m in messages]))
break
await pilot.pause()
assert ta.text_input
assert ta.text_input.completer_active == "word"
assert ta.completion_list.is_open is True
assert ta.completion_list.option_count == 10
first_offset = ta.completion_list.styles.offset
await pilot.press("e")
await app.workers.wait_for_complete()
await pilot.pause()
assert ta.text_input.completer_active == "word"
assert ta.completion_list.is_open is True
assert ta.completion_list.option_count == 7
assert ta.completion_list.styles.offset == first_offset
await pilot.press("z") # sez, no matches
await app.workers.wait_for_complete()
await pilot.pause()
assert ta.text_input.completer_active is None
assert ta.completion_list.is_open is False
# backspace when the list is not open doesn't re-open it
await pilot.press("backspace")
await app.workers.wait_for_complete()
await pilot.pause()
assert ta.text_input.completer_active is None
assert ta.completion_list.is_open is False
await pilot.press("l") # sel
await app.workers.wait_for_complete()
await pilot.pause()
assert ta.text_input.completer_active == "word"
assert ta.completion_list.is_open is True
assert ta.completion_list.option_count == 3
assert ta.completion_list.styles.offset == first_offset
await pilot.press("backspace") # se
await app.workers.wait_for_complete()
await pilot.pause()
assert ta.text_input.completer_active == "word"
assert ta.completion_list.is_open is True
assert ta.completion_list.option_count == 7
assert ta.completion_list.styles.offset == first_offset
await pilot.press("enter")
await app.workers.wait_for_complete()
await pilot.pause()
assert ta.text_input.completer_active is None
assert ta.completion_list.is_open is False
assert ta.text == "season"
assert ta.selection.end[1] == 6
@pytest.mark.asyncio
async def test_autocomplete_paths(app: App, data_dir: Path) -> None:
messages: list[Message] = []
async with app.run_test(message_hook=messages.append) as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ta.focus()
test_path = str(data_dir / "test_validator")
ta.text = test_path
await pilot.pause()
ta.selection = Selection((0, len(test_path)), (0, len(test_path)))
start_time = monotonic()
await pilot.press("slash")
while ta.completion_list.is_open is False:
if monotonic() - start_time > 10:
print("MESSAGES:")
print("\n".join([str(m) for m in messages]))
break
await pilot.pause()
assert ta.text_input
assert ta.text_input.completer_active == "path"
assert ta.completion_list.is_open is True
assert ta.completion_list.option_count == 2
@pytest.mark.parametrize(
"text,keys,expected_prefix",
[
("foo bar", ["full_stop"], "bar."),
("foo 'bar'", ["full_stop"], "'bar'."),
("foo `bar`", ["full_stop"], "`bar`."),
('foo "bar"', ["full_stop"], '"bar".'),
("foo bar", ["colon"], "bar:"),
("foo bar", ["colon", "colon"], "bar::"),
('foo "bar"', ["colon", "colon"], '"bar"::'),
("foo bar", ["full_stop", "quotation_mark"], 'bar."'),
('foo "bar"', ["full_stop", "quotation_mark"], '"bar"."'),
],
)
@pytest.mark.asyncio
async def test_autocomplete_members(
app: App,
member_completer: MagicMock,
text: str,
keys: list[str],
expected_prefix: str,
) -> None:
messages: list[Message] = []
async with app.run_test(message_hook=messages.append) as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ta.member_completer = member_completer
ta.focus()
while ta.member_completer is None:
await pilot.pause()
ta.text = text
ta.selection = Selection((0, len(text)), (0, len(text)))
await pilot.pause()
for key in keys:
await pilot.press(key)
start_time = monotonic()
while ta.completion_list.is_open is False:
if monotonic() - start_time > 10:
print("MESSAGES:")
print("\n".join([str(m) for m in messages]))
break
await pilot.pause()
member_completer.assert_called_with(expected_prefix)
assert ta.text_input is not None
assert ta.text_input.completer_active == "member"
assert ta.completion_list.is_open is True

View file

@ -0,0 +1,29 @@
import pytest
from textual.app import App
from textual.widgets.text_area import Selection
from textual_textarea import TextEditor
@pytest.mark.parametrize(
"language,expected_marker",
[
("python", "# "),
("sql", "-- "),
# ("mysql", "# "),
# ("c", "// "),
],
)
@pytest.mark.asyncio
async def test_comments(app: App, language: str, expected_marker: str) -> None:
async with app.run_test() as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ta.language = language
original_text = "foo bar baz"
ta.text = original_text
ta.selection = Selection((0, 0), (0, 0))
await pilot.press("ctrl+underscore") # alias for ctrl+/
assert ta.text == f"{expected_marker}{original_text}"
await pilot.press("ctrl+underscore") # alias for ctrl+/
assert ta.text == f"{original_text}"

View file

@ -0,0 +1,143 @@
import pytest
from textual.app import App
from textual_textarea import TextEditor
from textual_textarea.find_input import FindInput
@pytest.mark.asyncio
async def test_find(app: App) -> None:
async with app.run_test() as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ta.text = "foo bar\n" * 50
await pilot.pause()
assert ta.selection.start == ta.selection.end == (0, 0)
await pilot.press("ctrl+f")
find_input = app.query_one(FindInput)
assert find_input
assert find_input.has_focus
assert "Find" in find_input.placeholder
await pilot.press("b")
assert find_input.has_focus
assert ta.selection.start == (0, 4)
assert ta.selection.end == (0, 5)
await pilot.press("a")
assert find_input.has_focus
assert ta.selection.start == (0, 4)
assert ta.selection.end == (0, 6)
await pilot.press("enter")
assert find_input.has_focus
assert ta.selection.start == (1, 4)
assert ta.selection.end == (1, 6)
await pilot.press("escape")
assert ta.text_input
assert ta.text_input.has_focus
assert ta.selection.start == (1, 4)
assert ta.selection.end == (1, 6)
await pilot.press("ctrl+f")
find_input = app.query_one(FindInput)
await pilot.press("f")
assert find_input.has_focus
assert ta.selection.start == (2, 0)
assert ta.selection.end == (2, 1)
@pytest.mark.asyncio
async def test_find_history(app: App) -> None:
async with app.run_test() as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ta.text = "foo bar\n" * 50
await pilot.pause()
# add an item to the history by pressing enter
await pilot.press("ctrl+f")
await pilot.press("a")
await pilot.press("enter")
await pilot.press("escape")
# re-open the find input and navigate the one-item
# history
await pilot.press("ctrl+f")
find_input = app.query_one(FindInput)
assert find_input.value == ""
await pilot.press("up")
assert find_input.value == "a"
await pilot.press("down")
assert find_input.value == ""
await pilot.press("up")
await pilot.press("up")
assert find_input.value == "a"
await pilot.press("down")
assert find_input.value == ""
# add an item to the history by closing the find input
await pilot.press("b")
await pilot.press("escape")
# navigate the two-item history
await pilot.press("ctrl+f")
find_input = app.query_one(FindInput)
assert find_input.value == ""
await pilot.press("up")
assert find_input.value == "b"
await pilot.press("down")
assert find_input.value == ""
await pilot.press("up")
assert find_input.value == "b"
await pilot.press("up")
assert find_input.value == "a"
await pilot.press("up")
assert find_input.value == "a"
await pilot.press("down")
assert find_input.value == "b"
await pilot.press("down")
assert find_input.value == ""
@pytest.mark.asyncio
async def test_find_with_f3(app: App) -> None:
async with app.run_test() as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ta.text = "foo bar\n" * 50
await pilot.pause()
assert ta.selection.start == ta.selection.end == (0, 0)
# pressing f3 with no history brings up an empty find box
await pilot.press("f3")
find_input = app.query_one(FindInput)
assert find_input
assert find_input.has_focus
assert find_input.value == ""
await pilot.press("b")
assert find_input.has_focus
assert ta.selection.start == (0, 4)
assert ta.selection.end == (0, 5)
# pressing f3 from the find input finds the next match
await pilot.press("f3")
assert find_input.has_focus
assert ta.selection.start == (1, 4)
assert ta.selection.end == (1, 5)
# close the find input and navigate up one line
await pilot.press("escape")
await pilot.press("up")
# pressing f3 with history prepopulates the find input
await pilot.press("f3")
find_input = app.query_one(FindInput)
assert find_input.value == "b"
assert ta.selection.start == (1, 4)
assert ta.selection.end == (1, 5)
# pressing again advances to the next match
await pilot.press("f3")
assert ta.selection.start == (2, 4)
assert ta.selection.end == (2, 5)

View file

@ -0,0 +1,36 @@
import pytest
from textual.app import App
from textual_textarea import TextEditor
from textual_textarea.goto_input import GotoLineInput
@pytest.mark.asyncio
async def test_goto_line(app: App) -> None:
async with app.run_test() as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ta.text = "\n" * 50
await pilot.pause()
assert ta.selection.start == ta.selection.end == (0, 0)
await pilot.press("ctrl+g")
goto_input = app.query_one(GotoLineInput)
assert goto_input
assert goto_input.has_focus
assert "51" in goto_input.placeholder
await pilot.press("1")
await pilot.press("2")
await pilot.press("enter")
assert ta.text_input
assert ta.text_input.has_focus
assert ta.selection.start == ta.selection.end == (11, 0)
# ensure pressing ctrl+g twice doesn't crash
await pilot.press("ctrl+g")
goto_input = app.query_one(GotoLineInput)
assert goto_input.has_focus
await pilot.press("2")
await pilot.press("ctrl+g")

View file

@ -0,0 +1,70 @@
from pathlib import Path
from typing import List
import pytest
from textual.app import App
from textual.message import Message
from textual.widgets import Input
from textual_textarea import TextAreaSaved, TextEditor
@pytest.mark.parametrize("filename", ["foo.py", "empty.py"])
@pytest.mark.asyncio
async def test_open(data_dir: Path, app: App, filename: str) -> None:
p = data_dir / "test_open" / filename
with open(p, "r") as f:
contents = f.read()
async with app.run_test() as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
assert ta.text == ""
starting_text = "123"
for key in starting_text:
await pilot.press(key)
assert ta.text == starting_text
await pilot.press("ctrl+o")
open_input = ta.query_one(Input)
assert open_input.id and "open" in open_input.id
assert open_input.has_focus
for key in str(p):
await pilot.press(key)
await pilot.press("enter")
assert ta.text == contents
assert ta.text_input is not None
assert ta.text_input.has_focus
# make sure the end of the buffer is formatted properly.
# these previously caused a crash.
await pilot.press("ctrl+end")
assert ta.selection.end[1] >= 0
await pilot.press("enter")
@pytest.mark.asyncio
async def test_save(app: App, tmp_path: Path) -> None:
TEXT = "select\n 1 as a,\n 2 as b,\n 'c' as c"
p = tmp_path / "text.sql"
print(p)
messages: List[Message] = []
async with app.run_test(message_hook=messages.append) as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ta.text = TEXT
await pilot.press("ctrl+s")
save_input = ta.query_one(Input)
assert save_input.id and "save" in save_input.id
assert save_input.has_focus
save_input.value = str(p)
await pilot.press("enter")
await pilot.pause()
assert len(messages) > 1
assert Input.Submitted in [msg.__class__ for msg in messages]
assert TextAreaSaved in [msg.__class__ for msg in messages]
with open(p, "r") as f:
saved_text = f.read()
assert saved_text == TEXT

View file

@ -0,0 +1,462 @@
from __future__ import annotations
from typing import List
import pytest
from textual.app import App
from textual.widgets.text_area import Selection
from textual_textarea import TextEditor
@pytest.mark.parametrize(
"keys,text,selection,expected_text,expected_selection",
[
(
["ctrl+a"],
"select\n foo",
Selection(start=(1, 2), end=(1, 2)),
"select\n foo",
Selection(start=(0, 0), end=(1, 4)),
),
(
["ctrl+shift+right"],
"select\n foo",
Selection(start=(0, 0), end=(0, 0)),
"select\n foo",
Selection(start=(0, 0), end=(0, 6)),
),
(
["right"],
"select\n foo",
Selection(start=(0, 0), end=(0, 6)),
"select\n foo",
Selection(start=(1, 0), end=(1, 0)),
),
(
["a"],
"select\n foo",
Selection(start=(1, 4), end=(1, 4)),
"select\n fooa",
Selection(start=(1, 5), end=(1, 5)),
),
(
["a"],
"select\n foo",
Selection(start=(1, 0), end=(1, 4)),
"select\na",
Selection(start=(1, 1), end=(1, 1)),
),
(
["enter"],
"a\na",
Selection(start=(1, 0), end=(1, 0)),
"a\n\na",
Selection(start=(2, 0), end=(2, 0)),
),
(
["enter"],
"a\na",
Selection(start=(1, 1), end=(1, 1)),
"a\na\n",
Selection(start=(2, 0), end=(2, 0)),
),
(
["enter", "b"],
"a()",
Selection(start=(0, 2), end=(0, 2)),
"a(\n b\n)",
Selection(start=(1, 5), end=(1, 5)),
),
(
["enter", "b"],
" a()",
Selection(start=(0, 3), end=(0, 3)),
" a(\n b\n )",
Selection(start=(1, 5), end=(1, 5)),
),
(
["delete"],
"0\n1\n2\n3",
Selection(start=(2, 1), end=(2, 1)),
"0\n1\n23",
Selection(start=(2, 1), end=(2, 1)),
),
(
["shift+delete"],
"0\n1\n2\n3",
Selection(start=(2, 1), end=(2, 1)),
"0\n1\n3",
Selection(start=(2, 0), end=(2, 0)),
),
(
["shift+delete"],
"0\n1\n2\n3",
Selection(start=(2, 0), end=(2, 1)),
"0\n1\n\n3",
Selection(start=(2, 0), end=(2, 0)),
),
(
["shift+delete"],
"0\n1\n2\n3",
Selection(start=(3, 1), end=(3, 1)),
"0\n1\n2",
Selection(start=(2, 1), end=(2, 1)),
),
(
["shift+delete"],
"foo",
Selection(start=(3, 1), end=(3, 1)),
"",
Selection(start=(0, 0), end=(0, 0)),
),
(
["ctrl+home"],
"foo\nbar",
Selection(start=(1, 2), end=(1, 2)),
"foo\nbar",
Selection(start=(0, 0), end=(0, 0)),
),
(
["ctrl+end"],
"foo\nbar",
Selection(start=(0, 1), end=(0, 1)),
"foo\nbar",
Selection(start=(1, 3), end=(1, 3)),
),
(
["("],
"foo",
Selection(start=(0, 3), end=(0, 3)),
"foo()",
Selection(start=(0, 4), end=(0, 4)),
),
(
["("],
"foo",
Selection(start=(0, 2), end=(0, 2)),
"fo(o",
Selection(start=(0, 3), end=(0, 3)),
),
(
["("],
"foo.",
Selection(start=(0, 3), end=(0, 3)),
"foo().",
Selection(start=(0, 4), end=(0, 4)),
),
(
["("],
"foo-",
Selection(start=(0, 3), end=(0, 3)),
"foo(-",
Selection(start=(0, 4), end=(0, 4)),
),
(
["'"],
"foo",
Selection(start=(0, 3), end=(0, 3)),
"foo'",
Selection(start=(0, 4), end=(0, 4)),
),
(
["'"],
"ba r",
Selection(start=(0, 3), end=(0, 3)),
"ba '' r",
Selection(start=(0, 4), end=(0, 4)),
),
(
["'"],
"foo-",
Selection(start=(0, 3), end=(0, 3)),
"foo'-",
Selection(start=(0, 4), end=(0, 4)),
),
(
["'"],
"fo--",
Selection(start=(0, 3), end=(0, 3)),
"fo-'-",
Selection(start=(0, 4), end=(0, 4)),
),
(
["'"],
"fo-.",
Selection(start=(0, 3), end=(0, 3)),
"fo-''.",
Selection(start=(0, 4), end=(0, 4)),
),
(
["'"],
"fo()",
Selection(start=(0, 3), end=(0, 3)),
"fo('')",
Selection(start=(0, 4), end=(0, 4)),
),
(
["tab"],
"bar",
Selection(start=(0, 1), end=(0, 1)),
"b ar",
Selection(start=(0, 4), end=(0, 4)),
),
(
["tab"],
"bar",
Selection(start=(0, 0), end=(0, 0)),
" bar",
Selection(start=(0, 4), end=(0, 4)),
),
(
["shift+tab"],
"bar",
Selection(start=(0, 0), end=(0, 0)),
"bar",
Selection(start=(0, 0), end=(0, 0)),
),
(
["shift+tab"],
" bar",
Selection(start=(0, 7), end=(0, 7)),
"bar",
Selection(start=(0, 3), end=(0, 3)),
),
(
["tab"],
"bar\n baz",
Selection(start=(0, 2), end=(1, 1)),
" bar\n baz",
Selection(start=(0, 6), end=(1, 4)),
),
(
["tab"],
"bar\n baz",
Selection(start=(0, 0), end=(1, 1)),
" bar\n baz",
Selection(start=(0, 0), end=(1, 4)),
),
(
["shift+tab"],
" bar\n baz",
Selection(start=(0, 0), end=(1, 1)),
"bar\nbaz",
Selection(start=(0, 0), end=(1, 0)),
),
],
)
@pytest.mark.asyncio
async def test_keys(
app: App,
keys: List[str],
text: str,
selection: Selection,
expected_text: str,
expected_selection: Selection,
) -> None:
async with app.run_test() as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ta.text = text
ta.selection = selection
for key in keys:
await pilot.press(key)
assert ta.text == expected_text
assert ta.selection == expected_selection
@pytest.mark.parametrize(
"starting_selection,expected_clipboard,expected_paste_loc",
[
(Selection((0, 5), (1, 5)), "56789\n01234", (1, 5)),
(Selection((0, 0), (1, 0)), "0123456789\n", (1, 0)),
],
)
@pytest.mark.asyncio
async def test_copy_paste(
app_all_clipboards: App,
starting_selection: Selection,
expected_clipboard: str,
expected_paste_loc: tuple[int, int],
) -> None:
original_text = "0123456789\n0123456789\n0123456789"
def eq(a: str, b: str) -> bool:
return a.replace("\r\n", "\n") == b.replace("\r\n", "\n")
async with app_all_clipboards.run_test() as pilot:
ta = app_all_clipboards.query_one("#ta", expect_type=TextEditor)
while ta.text_input is None:
await pilot.pause(0.1)
ti = ta.text_input
assert ti is not None
ta.text = original_text
ta.selection = starting_selection
await pilot.press("ctrl+c")
await pilot.pause()
assert eq(ti.clipboard, expected_clipboard)
assert ta.selection == starting_selection
assert ta.text == original_text
await pilot.press("ctrl+u")
await pilot.pause()
assert eq(ti.clipboard, expected_clipboard)
assert ta.selection == Selection(starting_selection.end, starting_selection.end)
assert ta.text == original_text
await pilot.press("ctrl+a")
assert ta.selection == Selection(
(0, 0),
(len(original_text.splitlines()) - 1, len(original_text.splitlines()[-1])),
)
assert eq(ti.clipboard, expected_clipboard)
assert ta.text == original_text
await pilot.press("ctrl+u")
await pilot.pause()
assert ta.selection == Selection(expected_paste_loc, expected_paste_loc)
assert eq(ti.clipboard, expected_clipboard)
assert ta.text == expected_clipboard
await pilot.press("ctrl+a")
await pilot.press("ctrl+x")
await pilot.pause()
assert ta.selection == Selection((0, 0), (0, 0))
assert eq(ti.clipboard, expected_clipboard)
assert ta.text == ""
await pilot.press("ctrl+v")
await pilot.pause()
assert eq(ti.clipboard, expected_clipboard)
assert ta.text == expected_clipboard
assert ta.selection == Selection(expected_paste_loc, expected_paste_loc)
@pytest.mark.asyncio
async def test_undo_redo(app: App) -> None:
async with app.run_test() as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ti = ta.text_input
assert ti
assert ti.has_focus
for char in "foo":
await pilot.press(char)
await pilot.pause(0.6)
await pilot.press("enter")
for char in "bar":
await pilot.press(char)
await pilot.pause(0.6)
await pilot.press("ctrl+z")
assert ta.text == "foo\n"
assert ta.selection == Selection((1, 0), (1, 0))
await pilot.press("ctrl+z")
assert ta.text == "foo"
assert ta.selection == Selection((0, 3), (0, 3))
await pilot.press("ctrl+z")
assert ta.text == ""
assert ta.selection == Selection((0, 0), (0, 0))
await pilot.press("ctrl+y")
assert ta.text == "foo"
assert ta.selection == Selection((0, 3), (0, 3))
await pilot.press("z")
assert ta.text == "fooz"
@pytest.mark.parametrize(
"start_text,insert_text,selection,expected_text",
[
(
"select ",
'"main"."drivers"."driverId"',
Selection((0, 7), (0, 7)),
'select "main"."drivers"."driverId"',
),
(
"select , foo",
'"main"."drivers"."driverId"',
Selection((0, 7), (0, 7)),
'select "main"."drivers"."driverId", foo',
),
(
"aaa\naaa\naaa\naaa",
"bb",
Selection((2, 2), (2, 2)),
"aaa\naaa\naabba\naaa",
),
(
"aaa\naaa\naaa\naaa",
"bb",
Selection((2, 2), (1, 1)),
"aaa\nabba\naaa",
),
(
"01234",
"\nabc\n",
Selection((0, 2), (0, 2)),
"01\nabc\n234",
),
],
)
@pytest.mark.asyncio
async def test_insert_text(
app: App,
start_text: str,
insert_text: str,
selection: Selection,
expected_text: str,
) -> None:
async with app.run_test() as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ta.text = start_text
ta.selection = selection
await pilot.pause()
ta.insert_text_at_selection(insert_text)
await pilot.pause()
assert ta.text == expected_text
@pytest.mark.asyncio
async def test_toggle_comment(app: App) -> None:
async with app.run_test() as pilot:
ta = app.query_one("#ta", expect_type=TextEditor)
ta.text = "one\ntwo\n\nthree"
ta.selection = Selection((0, 0), (0, 0))
await pilot.pause()
await pilot.press("ctrl+underscore")
assert ta.text == "# one\ntwo\n\nthree"
await pilot.press("down")
await pilot.press("ctrl+underscore")
assert ta.text == "# one\n# two\n\nthree"
await pilot.press("ctrl+a")
await pilot.press("ctrl+underscore")
assert ta.text == "# # one\n# # two\n\n# three"
await pilot.press("ctrl+underscore")
assert ta.text == "# one\n# two\n\nthree"
await pilot.press("up")
await pilot.press("up")
await pilot.press("ctrl+underscore")
assert ta.text == "# one\ntwo\n\nthree"
await pilot.press("shift+down")
await pilot.press("shift+down")
await pilot.press("ctrl+underscore")
assert ta.text == "# one\n# two\n\n# three"
await pilot.press("ctrl+a")
await pilot.press("ctrl+underscore")
assert ta.text == "one\ntwo\n\nthree"

View file

@ -0,0 +1,84 @@
from __future__ import annotations
from pathlib import Path
import pytest
from textual_textarea.path_input import PathValidator, path_completer
@pytest.mark.parametrize(
"relpath,expected_matches",
[
("", ["foo", "bar"]),
("f", ["foo"]),
("fo", ["foo"]),
("foo", ["baz.txt"]),
("foo/", ["baz.txt"]),
("b", ["bar"]),
("c", []),
],
)
def test_path_completer(
data_dir: Path,
relpath: str,
expected_matches: list[str],
) -> None:
test_path = data_dir / "test_validator" / relpath
test_dir = test_path if test_path.is_dir() else test_path.parent
prefix = str(test_path)
print(prefix)
matches = path_completer(prefix)
assert sorted(matches) == sorted(
[(str(test_dir / m), str(test_dir / m)) for m in expected_matches]
)
@pytest.mark.parametrize(
"relpath,dir_okay,file_okay,must_exist,expected_result",
[
("foo", True, True, True, True),
("foo", True, True, False, True),
("foo", True, False, True, True),
("foo", True, False, False, True),
("foo", False, True, True, False),
("foo", False, True, False, False),
("foo", False, False, True, False),
("foo", False, False, False, False),
("bar", True, True, True, True),
("bar", True, True, False, True),
("bar", True, False, True, True),
("bar", True, False, False, True),
("bar", False, True, True, False),
("bar", False, True, False, False),
("bar", False, False, True, False),
("bar", False, False, False, False),
("baz", True, True, True, False),
("baz", True, True, False, True),
("baz", True, False, True, False),
("baz", True, False, False, True),
("baz", False, True, True, False),
("baz", False, True, False, True),
("baz", False, False, True, False),
("baz", False, False, False, True),
("foo/baz.txt", True, True, True, True),
("foo/baz.txt", True, True, False, True),
("foo/baz.txt", True, False, True, False),
("foo/baz.txt", True, False, False, False),
("foo/baz.txt", False, True, True, True),
("foo/baz.txt", False, True, False, True),
("foo/baz.txt", False, False, True, False),
("foo/baz.txt", False, False, False, False),
],
)
def test_path_validator(
data_dir: Path,
relpath: str,
dir_okay: bool,
file_okay: bool,
must_exist: bool,
expected_result: bool,
) -> None:
p = data_dir / "test_validator" / relpath
validator = PathValidator(dir_okay, file_okay, must_exist)
result = validator.validate(str(p))
assert result.is_valid == expected_result