420 lines
13 KiB
Python
420 lines
13 KiB
Python
"""Tests for the text editor."""
|
|
|
|
import pytest
|
|
|
|
from mudlib.editor import Editor
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_editor_with_empty_buffer():
|
|
"""Editor starts with empty buffer."""
|
|
editor = Editor()
|
|
assert editor.buffer == []
|
|
assert editor.cursor == 0
|
|
assert editor.dirty is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_editor_with_initial_content():
|
|
"""Editor can be initialized with content."""
|
|
editor = Editor(initial_content="line 1\nline 2\nline 3")
|
|
assert editor.buffer == ["line 1", "line 2", "line 3"]
|
|
assert editor.dirty is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_append_line():
|
|
"""Typing text that isn't a command appends to buffer."""
|
|
editor = Editor()
|
|
response = await editor.handle_input("hello world")
|
|
assert response.output == ""
|
|
assert response.done is False
|
|
assert editor.buffer == ["hello world"]
|
|
assert editor.dirty is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_append_multiple_lines():
|
|
"""Multiple lines can be appended."""
|
|
editor = Editor()
|
|
await editor.handle_input("line 1")
|
|
await editor.handle_input("line 2")
|
|
await editor.handle_input("line 3")
|
|
assert editor.buffer == ["line 1", "line 2", "line 3"]
|
|
assert editor.dirty is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_view_buffer():
|
|
"""':' shows the entire buffer with line numbers."""
|
|
editor = Editor(initial_content="line 1\nline 2\nline 3")
|
|
response = await editor.handle_input(":")
|
|
assert "1 line 1" in response.output
|
|
assert "2 line 2" in response.output
|
|
assert "3 line 3" in response.output
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_view_empty_buffer():
|
|
"""':' on empty buffer shows appropriate message."""
|
|
editor = Editor()
|
|
response = await editor.handle_input(":")
|
|
assert "empty" in response.output.lower() or response.output == ""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_view_specific_line():
|
|
"""':5' shows line 5 with context."""
|
|
editor = Editor(initial_content="\n".join([f"line {i}" for i in range(1, 11)]))
|
|
response = await editor.handle_input(":5")
|
|
# Should show lines 2-8 (3 lines of context on each side)
|
|
assert "5 line 5" in response.output
|
|
assert "2 line 2" in response.output
|
|
assert "8 line 8" in response.output
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_view_specific_line_at_start():
|
|
"""Viewing line near start shows available context."""
|
|
editor = Editor(initial_content="line 1\nline 2\nline 3")
|
|
response = await editor.handle_input(":1")
|
|
assert "1 line 1" in response.output
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_insert_at_line():
|
|
"""':i 5 text' inserts 'text' at line 5."""
|
|
editor = Editor(initial_content="\n".join([f"line {i}" for i in range(1, 6)]))
|
|
await editor.handle_input(":i 3 inserted text")
|
|
assert editor.buffer[2] == "inserted text"
|
|
assert editor.buffer[3] == "line 3"
|
|
assert len(editor.buffer) == 6
|
|
assert editor.dirty is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_insert_at_end():
|
|
"""':i' can insert at end of buffer."""
|
|
editor = Editor(initial_content="line 1\nline 2")
|
|
await editor.handle_input(":i 3 line 3")
|
|
assert editor.buffer[2] == "line 3"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_single_line():
|
|
"""':d 5' deletes line 5."""
|
|
editor = Editor(initial_content="\n".join([f"line {i}" for i in range(1, 6)]))
|
|
await editor.handle_input(":d 3")
|
|
assert editor.buffer == ["line 1", "line 2", "line 4", "line 5"]
|
|
assert editor.dirty is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_range():
|
|
"""':d 5 8' deletes lines 5 through 8."""
|
|
editor = Editor(initial_content="\n".join([f"line {i}" for i in range(1, 11)]))
|
|
await editor.handle_input(":d 3 6")
|
|
expected = ["line 1", "line 2", "line 7", "line 8", "line 9", "line 10"]
|
|
assert editor.buffer == expected
|
|
assert editor.dirty is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_invalid_line():
|
|
"""Deleting invalid line shows error."""
|
|
editor = Editor(initial_content="line 1\nline 2")
|
|
response = await editor.handle_input(":d 10")
|
|
assert "error" in response.output.lower() or "invalid" in response.output.lower()
|
|
assert editor.buffer == ["line 1", "line 2"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_replace_line():
|
|
"""':r 5 text' replaces line 5 with 'text'."""
|
|
editor = Editor(initial_content="\n".join([f"line {i}" for i in range(1, 6)]))
|
|
await editor.handle_input(":r 3 replaced text")
|
|
assert editor.buffer[2] == "replaced text"
|
|
assert len(editor.buffer) == 5
|
|
assert editor.dirty is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_and_replace_first():
|
|
"""':s old new' replaces first occurrence."""
|
|
editor = Editor(initial_content="hello world\nhello again\nhello there")
|
|
await editor.handle_input(":s hello hi")
|
|
assert editor.buffer[0] == "hi world"
|
|
assert editor.buffer[1] == "hello again"
|
|
assert editor.dirty is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_and_replace_all():
|
|
"""':sa old new' replaces all occurrences."""
|
|
editor = Editor(initial_content="hello world\nhello again\nhello there")
|
|
await editor.handle_input(":sa hello hi")
|
|
assert editor.buffer[0] == "hi world"
|
|
assert editor.buffer[1] == "hi again"
|
|
assert editor.buffer[2] == "hi there"
|
|
assert editor.dirty is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_replace_no_match():
|
|
"""Search/replace with no match shows message."""
|
|
editor = Editor(initial_content="hello world")
|
|
response = await editor.handle_input(":s foo bar")
|
|
assert (
|
|
"not found" in response.output.lower() or "no match" in response.output.lower()
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_undo():
|
|
"""':u' undoes last buffer change."""
|
|
editor = Editor(initial_content="line 1\nline 2")
|
|
await editor.handle_input("line 3")
|
|
assert len(editor.buffer) == 3
|
|
response = await editor.handle_input(":u")
|
|
assert editor.buffer == ["line 1", "line 2"]
|
|
assert "undo" in response.output.lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_undo_multiple_times():
|
|
"""Undo can be called multiple times."""
|
|
editor = Editor(initial_content="line 1")
|
|
await editor.handle_input("line 2")
|
|
await editor.handle_input("line 3")
|
|
await editor.handle_input(":u")
|
|
await editor.handle_input(":u")
|
|
assert editor.buffer == ["line 1"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_undo_when_empty():
|
|
"""Undo with no history shows message."""
|
|
editor = Editor()
|
|
response = await editor.handle_input(":u")
|
|
assert (
|
|
"nothing to undo" in response.output.lower()
|
|
or "no undo" in response.output.lower()
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_save():
|
|
"""':w' calls save callback."""
|
|
saved_content = None
|
|
|
|
async def save_callback(content: str):
|
|
nonlocal saved_content
|
|
saved_content = content
|
|
|
|
editor = Editor(initial_content="line 1\nline 2", save_callback=save_callback)
|
|
await editor.handle_input("line 3")
|
|
response = await editor.handle_input(":w")
|
|
assert response.saved is True
|
|
assert response.done is False
|
|
assert saved_content == "line 1\nline 2\nline 3"
|
|
assert editor.dirty is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_save_without_callback():
|
|
"""':w' without callback shows error."""
|
|
editor = Editor(initial_content="line 1")
|
|
response = await editor.handle_input(":w")
|
|
assert (
|
|
"cannot save" in response.output.lower() or "no save" in response.output.lower()
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_save_and_quit():
|
|
"""':wq' saves and quits."""
|
|
saved_content = None
|
|
|
|
async def save_callback(content: str):
|
|
nonlocal saved_content
|
|
saved_content = content
|
|
|
|
editor = Editor(initial_content="line 1", save_callback=save_callback)
|
|
response = await editor.handle_input(":wq")
|
|
assert response.saved is True
|
|
assert response.done is True
|
|
assert saved_content == "line 1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_quit_when_dirty_warns():
|
|
"""':q' when dirty warns and doesn't quit."""
|
|
editor = Editor(initial_content="line 1")
|
|
await editor.handle_input("line 2")
|
|
response = await editor.handle_input(":q")
|
|
assert response.done is False
|
|
assert "unsaved" in response.output.lower() or "save" in response.output.lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_quit_when_clean():
|
|
"""':q' when clean quits successfully."""
|
|
editor = Editor(initial_content="line 1")
|
|
response = await editor.handle_input(":q")
|
|
assert response.done is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_force_quit():
|
|
"""':q!' quits without saving."""
|
|
editor = Editor(initial_content="line 1")
|
|
await editor.handle_input("line 2")
|
|
response = await editor.handle_input(":q!")
|
|
assert response.done is True
|
|
assert response.saved is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dot_saves_and_quits():
|
|
"""'.' on its own line saves and quits (MOO convention)."""
|
|
saved_content = None
|
|
|
|
async def save_callback(content: str):
|
|
nonlocal saved_content
|
|
saved_content = content
|
|
|
|
editor = Editor(initial_content="line 1", save_callback=save_callback)
|
|
await editor.handle_input("line 2")
|
|
response = await editor.handle_input(".")
|
|
assert response.done is True
|
|
assert response.saved is True
|
|
assert saved_content == "line 1\nline 2"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dirty_flag_tracking():
|
|
"""Dirty flag is properly tracked."""
|
|
editor = Editor(initial_content="line 1")
|
|
assert editor.dirty is False
|
|
|
|
await editor.handle_input("line 2")
|
|
assert editor.dirty is True
|
|
|
|
# Mock save
|
|
saved_content = None
|
|
|
|
async def save_callback(content: str):
|
|
nonlocal saved_content
|
|
saved_content = content
|
|
|
|
editor.save_callback = save_callback
|
|
await editor.handle_input(":w")
|
|
assert editor.dirty is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_help():
|
|
"""':h' shows help."""
|
|
editor = Editor()
|
|
response = await editor.handle_input(":h")
|
|
assert ":w" in response.output
|
|
assert ":q" in response.output
|
|
assert ":i" in response.output
|
|
assert ":d" in response.output
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_undo_stack_size_limit():
|
|
"""Undo stack is capped at 50 entries."""
|
|
editor = Editor()
|
|
# Add 60 lines
|
|
for i in range(60):
|
|
await editor.handle_input(f"line {i}")
|
|
# Undo stack should be capped
|
|
assert len(editor.undo_stack) <= 50
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_line_numbers_padded():
|
|
"""Line numbers in view are left-padded."""
|
|
editor = Editor(initial_content="\n".join([f"line {i}" for i in range(1, 12)]))
|
|
response = await editor.handle_input(":")
|
|
# Line 1 should have padding, line 11 should not have extra padding
|
|
lines = response.output.split("\n")
|
|
# Find lines with numbers
|
|
assert any(" 1 line 1" in line for line in lines)
|
|
assert any("11 line 11" in line for line in lines)
|
|
|
|
|
|
# Syntax highlighting tests
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_view_buffer_with_python_includes_ansi():
|
|
"""Viewing buffer with content_type=python includes ANSI codes."""
|
|
editor = Editor(
|
|
initial_content="def foo():\n return 42",
|
|
content_type="python",
|
|
color_depth="16",
|
|
)
|
|
response = await editor.handle_input(":")
|
|
# Should contain ANSI escape codes from syntax highlighting
|
|
assert "\033[" in response.output
|
|
# Should contain the code
|
|
assert "def" in response.output
|
|
assert "foo" in response.output
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_view_buffer_with_text_no_ansi():
|
|
"""Viewing buffer with content_type=text does NOT include ANSI codes."""
|
|
editor = Editor(
|
|
initial_content="plain text line 1\nplain text line 2", content_type="text"
|
|
)
|
|
response = await editor.handle_input(":")
|
|
# Should NOT contain ANSI escape codes (plain line numbers)
|
|
assert "\033[" not in response.output
|
|
# Should contain plain line numbers
|
|
assert "1 plain text line 1" in response.output
|
|
assert "2 plain text line 2" in response.output
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_view_specific_line_with_python_includes_ansi():
|
|
"""Viewing a specific line with python content includes ANSI codes."""
|
|
content = "\n".join([f"def func{i}():\n return {i}" for i in range(10)])
|
|
editor = Editor(
|
|
initial_content=content,
|
|
content_type="python",
|
|
color_depth="16",
|
|
)
|
|
response = await editor.handle_input(":5")
|
|
# Should contain ANSI escape codes from syntax highlighting
|
|
assert "\033[" in response.output
|
|
# Should contain code from the context window
|
|
assert "def" in response.output
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_color_depth_passed_to_highlight():
|
|
"""Color depth is passed through to highlight function."""
|
|
editor = Editor(
|
|
initial_content="x = 1\ny = 2",
|
|
content_type="python",
|
|
color_depth="256",
|
|
)
|
|
response = await editor.handle_input(":")
|
|
# Should contain ANSI codes (256-color formatter)
|
|
assert "\033[" in response.output
|
|
# Should contain the code
|
|
assert "x" in response.output
|
|
assert "y" in response.output
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_buffer_with_python_content_type():
|
|
"""Empty buffer with content_type=python still works."""
|
|
editor = Editor(content_type="python", color_depth="16")
|
|
response = await editor.handle_input(":")
|
|
# Should show empty buffer message
|
|
assert "empty" in response.output.lower()
|