303 lines
8.9 KiB
Python
303 lines
8.9 KiB
Python
"""Tests for editor integration with the shell and command system."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from mudlib import commands
|
|
from mudlib.commands.edit import cmd_edit
|
|
from mudlib.editor import Editor
|
|
from mudlib.player import Player
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_writer():
|
|
writer = MagicMock()
|
|
writer.write = MagicMock()
|
|
writer.drain = AsyncMock()
|
|
return writer
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_reader():
|
|
return MagicMock()
|
|
|
|
|
|
@pytest.fixture
|
|
def player(mock_reader, mock_writer):
|
|
return Player(name="TestPlayer", x=5, y=5, reader=mock_reader, writer=mock_writer)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_command_enters_editor_mode(player):
|
|
"""Test that edit command sets up editor mode."""
|
|
assert player.mode == "normal"
|
|
assert player.editor is None
|
|
|
|
await cmd_edit(player, "")
|
|
|
|
assert player.mode == "editor"
|
|
assert player.editor is not None
|
|
assert isinstance(player.editor, Editor)
|
|
assert player.mode_stack == ["normal", "editor"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_command_sends_welcome_message(player, mock_writer):
|
|
"""Test that edit command sends welcome message."""
|
|
await cmd_edit(player, "")
|
|
|
|
assert mock_writer.write.called
|
|
output = "".join([call[0][0] for call in mock_writer.write.call_args_list])
|
|
assert "editor" in output.lower()
|
|
assert ":h" in output
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_command_sets_save_callback(player):
|
|
"""Test that edit command sets up a save callback."""
|
|
await cmd_edit(player, "")
|
|
|
|
assert player.editor is not None
|
|
assert player.editor.save_callback is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_editor_handle_input_preserves_whitespace(player):
|
|
"""Test that editor receives input with whitespace preserved."""
|
|
await cmd_edit(player, "")
|
|
|
|
# Simulate code with indentation
|
|
response = await player.editor.handle_input(" def foo():")
|
|
|
|
assert player.editor.buffer == [" def foo():"]
|
|
assert response.done is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_editor_done_clears_editor_and_pops_mode(player):
|
|
"""Test that when editor returns done=True, mode is popped and editor cleared."""
|
|
await cmd_edit(player, "")
|
|
|
|
assert player.mode == "editor"
|
|
assert player.editor is not None
|
|
|
|
# Quit the editor
|
|
response = await player.editor.handle_input(":q")
|
|
assert response.done is True
|
|
|
|
# Simulate what the shell loop should do
|
|
player.editor = None
|
|
player.mode_stack.pop()
|
|
|
|
assert player.mode == "normal"
|
|
assert player.editor is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_editor_save_callback_sends_message(player, mock_writer):
|
|
"""Test that save callback sends confirmation to player."""
|
|
await cmd_edit(player, "")
|
|
mock_writer.reset_mock()
|
|
|
|
await player.editor.handle_input("test line")
|
|
response = await player.editor.handle_input(":w")
|
|
|
|
assert response.saved is True
|
|
# Save callback should have sent a message
|
|
assert mock_writer.write.called
|
|
output = "".join([call[0][0] for call in mock_writer.write.call_args_list])
|
|
assert "saved" in output.lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_command_only_works_in_normal_mode(player):
|
|
"""Test that edit command has mode='normal' restriction."""
|
|
# Push a different mode onto the stack
|
|
player.mode_stack.append("combat")
|
|
|
|
# Try to invoke edit command through dispatch
|
|
await commands.dispatch(player, "edit")
|
|
|
|
# Should be blocked by mode check
|
|
assert player.mode == "combat"
|
|
assert player.editor is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mode_stack_push_and_pop(player):
|
|
"""Test mode stack mechanics for editor mode."""
|
|
assert player.mode_stack == ["normal"]
|
|
assert player.mode == "normal"
|
|
|
|
# Enter editor mode
|
|
player.mode_stack.append("editor")
|
|
assert player.mode == "editor"
|
|
assert player.mode_stack == ["normal", "editor"]
|
|
|
|
# Exit editor mode
|
|
player.mode_stack.pop()
|
|
assert player.mode == "normal"
|
|
assert player.mode_stack == ["normal"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_input_allowed_in_editor(player):
|
|
"""Test that empty lines are valid in editor mode (for blank lines in code)."""
|
|
await cmd_edit(player, "")
|
|
|
|
# Empty line should be appended to buffer
|
|
response = await player.editor.handle_input("")
|
|
assert response.done is False
|
|
assert player.editor.buffer == [""]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_editor_prompt_uses_cursor(player):
|
|
"""Test that editor prompt should show line number from cursor."""
|
|
await cmd_edit(player, "")
|
|
|
|
# Add some lines
|
|
await player.editor.handle_input("line 1")
|
|
await player.editor.handle_input("line 2")
|
|
|
|
# After appending, cursor should be at the last line (1 in 0-indexed)
|
|
# This test verifies the cursor field exists and can be used for prompts
|
|
assert player.editor.cursor == 1
|
|
# Shell loop prompt: f" {player.editor.cursor + 1}> " = " 2> "
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_no_args_opens_blank_editor(player):
|
|
"""Test that edit with no args opens a blank editor."""
|
|
await cmd_edit(player, "")
|
|
|
|
assert player.editor is not None
|
|
assert player.editor.buffer == []
|
|
assert player.mode == "editor"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_combat_move_opens_toml(player, tmp_path):
|
|
"""Test that edit roundhouse opens the TOML file for editing."""
|
|
from mudlib.combat import commands as combat_commands
|
|
|
|
# Create a test TOML file
|
|
toml_content = """name = "roundhouse"
|
|
aliases = ["rh"]
|
|
move_type = "attack"
|
|
stamina_cost = 8.0
|
|
hit_time_ms = 2000
|
|
"""
|
|
toml_file = tmp_path / "roundhouse.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
# Set up combat moves
|
|
combat_commands.combat_moves = {"roundhouse": MagicMock(command="roundhouse")}
|
|
combat_commands.combat_content_dir = tmp_path
|
|
|
|
await cmd_edit(player, "roundhouse")
|
|
|
|
assert player.editor is not None
|
|
assert player.mode == "editor"
|
|
assert toml_content in "\n".join(player.editor.buffer)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_combat_move_saves_to_disk(player, tmp_path, mock_writer):
|
|
"""Test that saving in editor writes back to the TOML file."""
|
|
from mudlib.combat import commands as combat_commands
|
|
|
|
# Create a test TOML file
|
|
original_content = """name = "roundhouse"
|
|
aliases = ["rh"]
|
|
move_type = "attack"
|
|
"""
|
|
toml_file = tmp_path / "roundhouse.toml"
|
|
toml_file.write_text(original_content)
|
|
|
|
# Set up combat moves
|
|
combat_commands.combat_moves = {"roundhouse": MagicMock(command="roundhouse")}
|
|
combat_commands.combat_content_dir = tmp_path
|
|
|
|
await cmd_edit(player, "roundhouse")
|
|
mock_writer.reset_mock()
|
|
|
|
# Modify the buffer
|
|
player.editor.buffer = [
|
|
'name = "roundhouse"',
|
|
'aliases = ["rh"]',
|
|
'move_type = "attack"',
|
|
"stamina_cost = 9.0",
|
|
]
|
|
|
|
# Save
|
|
await player.editor.handle_input(":w")
|
|
|
|
# Check that file was written
|
|
saved_content = toml_file.read_text()
|
|
assert "stamina_cost = 9.0" in saved_content
|
|
assert mock_writer.write.called
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_variant_base_opens_toml(player, tmp_path):
|
|
"""Test that edit punch opens punch.toml (variant base name)."""
|
|
from mudlib.combat import commands as combat_commands
|
|
|
|
# Create punch.toml with variants
|
|
toml_content = """name = "punch"
|
|
move_type = "attack"
|
|
|
|
[variants.left]
|
|
aliases = ["pl"]
|
|
"""
|
|
toml_file = tmp_path / "punch.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
# Set up combat moves with variant
|
|
combat_commands.combat_moves = {
|
|
"punch left": MagicMock(command="punch", variant="left")
|
|
}
|
|
combat_commands.combat_content_dir = tmp_path
|
|
|
|
await cmd_edit(player, "punch")
|
|
|
|
assert player.editor is not None
|
|
assert player.mode == "editor"
|
|
assert "[variants.left]" in "\n".join(player.editor.buffer)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_unknown_content_shows_error(player, mock_writer, tmp_path):
|
|
"""Test that edit nonexistent shows an error."""
|
|
from mudlib.combat import commands as combat_commands
|
|
|
|
combat_commands.combat_moves = {}
|
|
combat_commands.combat_content_dir = tmp_path
|
|
|
|
await cmd_edit(player, "nonexistent")
|
|
|
|
assert player.editor is None
|
|
assert player.mode == "normal"
|
|
assert mock_writer.write.called
|
|
output = "".join([call[0][0] for call in mock_writer.write.call_args_list])
|
|
assert "unknown" in output.lower()
|
|
assert "nonexistent" in output.lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_combat_move_uses_toml_content_type(player, tmp_path):
|
|
"""Test that editor for combat moves uses toml content type."""
|
|
from mudlib.combat import commands as combat_commands
|
|
|
|
toml_file = tmp_path / "roundhouse.toml"
|
|
toml_file.write_text("name = 'roundhouse'\n")
|
|
|
|
combat_commands.combat_moves = {"roundhouse": MagicMock(command="roundhouse")}
|
|
combat_commands.combat_content_dir = tmp_path
|
|
|
|
await cmd_edit(player, "roundhouse")
|
|
|
|
assert player.editor is not None
|
|
assert player.editor.content_type == "toml"
|