Adding upstream version 0.15.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
0184169650
commit
c6da052ee9
47 changed files with 6799 additions and 0 deletions
9
tests/conftest.py
Normal file
9
tests/conftest.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data_dir() -> Path:
|
||||
here = Path(__file__)
|
||||
return here.parent / "data"
|
0
tests/data/test_open/empty.py
Normal file
0
tests/data/test_open/empty.py
Normal file
2
tests/data/test_open/foo.py
Normal file
2
tests/data/test_open/foo.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
def foo(bar: str, baz: int) -> None:
|
||||
return
|
0
tests/data/test_validator/bar/.gitkeep
Normal file
0
tests/data/test_validator/bar/.gitkeep
Normal file
0
tests/data/test_validator/foo/baz.txt
Normal file
0
tests/data/test_validator/foo/baz.txt
Normal file
0
tests/functional_tests/__init__.py
Normal file
0
tests/functional_tests/__init__.py
Normal file
62
tests/functional_tests/conftest.py
Normal file
62
tests/functional_tests/conftest.py
Normal 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
|
279
tests/functional_tests/test_autocomplete.py
Normal file
279
tests/functional_tests/test_autocomplete.py
Normal 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
|
29
tests/functional_tests/test_comments.py
Normal file
29
tests/functional_tests/test_comments.py
Normal 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}"
|
143
tests/functional_tests/test_find.py
Normal file
143
tests/functional_tests/test_find.py
Normal 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)
|
36
tests/functional_tests/test_goto.py
Normal file
36
tests/functional_tests/test_goto.py
Normal 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")
|
70
tests/functional_tests/test_open.py
Normal file
70
tests/functional_tests/test_open.py
Normal 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
|
462
tests/functional_tests/test_textarea.py
Normal file
462
tests/functional_tests/test_textarea.py
Normal 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"
|
84
tests/unit_tests/test_path_validator.py
Normal file
84
tests/unit_tests/test_path_validator.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue