"""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()