major-domo-v2/tests/test_views_transaction_embed.py
Cal Corum 1dd930e4b3 CLAUDE: Complete dice command system with fielding mechanics
Implements comprehensive dice rolling system for gameplay:

## New Features
- `/roll` and `!roll` commands for XdY dice notation with multiple roll support
- `/ab` and `!atbat` commands for baseball at-bat dice shortcuts (1d6;2d6;1d20)
- `/fielding` and `!f` commands for Super Advanced fielding with full position charts

## Technical Implementation
- Complete dice command package in commands/dice/
- Full range and error charts for all 8 defensive positions (1B,2B,3B,SS,LF,RF,CF,C)
- Pre-populated position choices for user-friendly slash command interface
- Backwards compatibility with prefix commands (!roll, !r, !dice, !ab, !atbat, !f, !fielding, !saf)
- Type-safe implementation following "Raise or Return" pattern

## Testing & Quality
- 30 comprehensive tests with 100% pass rate
- Complete test coverage for all dice functionality, parsing, validation, and error handling
- Integration with bot.py command loading system
- Maintainable data structures replacing verbose original implementation

## User Experience
- Consistent embed formatting across all commands
- Detailed fielding results with range and error analysis
- Support for complex dice combinations and multiple roll formats
- Clear error messages for invalid inputs

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 22:30:31 -05:00

596 lines
24 KiB
Python

"""
Tests for Transaction Embed Views
Validates Discord UI components, modals, and interactive elements.
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
import discord
from views.transaction_embed import (
TransactionEmbedView,
RemoveMoveView,
RemoveMoveSelect,
PlayerSelectionModal,
SubmitConfirmationModal,
create_transaction_embed,
create_preview_embed
)
from services.transaction_builder import (
TransactionBuilder,
TransactionMove,
RosterValidationResult
)
from models.team import Team, RosterType
from models.player import Player
class TestTransactionEmbedView:
"""Test TransactionEmbedView Discord UI component."""
@pytest.fixture
def mock_builder(self):
"""Create mock TransactionBuilder."""
team = Team(id=499, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', season=12)
builder = MagicMock(spec=TransactionBuilder)
builder.team = team
builder.user_id = 123456789
builder.season = 12
builder.is_empty = False
builder.move_count = 2
builder.moves = []
builder.created_at = MagicMock()
builder.created_at.strftime.return_value = "10:30:15"
return builder
# Don't create view as fixture - create in test methods to ensure event loop is running
@pytest.fixture
def mock_interaction(self):
"""Create mock Discord interaction."""
interaction = AsyncMock()
interaction.user = MagicMock()
interaction.user.id = 123456789
interaction.response = AsyncMock()
interaction.followup = AsyncMock()
interaction.client = MagicMock()
interaction.channel = MagicMock()
return interaction
@pytest.mark.asyncio
async def test_interaction_check_correct_user(self, mock_builder, mock_interaction):
"""Test interaction check passes for correct user."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
result = await view.interaction_check(mock_interaction)
assert result is True
@pytest.mark.asyncio
async def test_interaction_check_wrong_user(self, mock_builder, mock_interaction):
"""Test interaction check fails for wrong user."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
mock_interaction.user.id = 999999999 # Different user
result = await view.interaction_check(mock_interaction)
assert result is False
mock_interaction.response.send_message.assert_called_once()
call_args = mock_interaction.response.send_message.call_args
assert "don't have permission" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_add_move_button_click(self, mock_builder, mock_interaction):
"""Test add move button click opens modal."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
await view.add_move_button.callback(mock_interaction)
# Should send modal
mock_interaction.response.send_modal.assert_called_once()
# Check that modal is PlayerSelectionModal
modal_arg = mock_interaction.response.send_modal.call_args[0][0]
assert isinstance(modal_arg, PlayerSelectionModal)
@pytest.mark.asyncio
async def test_remove_move_button_empty_builder(self, mock_builder, mock_interaction):
"""Test remove move button with empty builder."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
view.builder.is_empty = True
await view.remove_move_button.callback(mock_interaction)
mock_interaction.response.send_message.assert_called_once()
call_args = mock_interaction.response.send_message.call_args
assert "No moves to remove" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_remove_move_button_with_moves(self, mock_builder, mock_interaction):
"""Test remove move button with moves available."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
view.builder.is_empty = False
with patch('views.transaction_embed.create_transaction_embed') as mock_create_embed:
mock_create_embed.return_value = MagicMock()
await view.remove_move_button.callback(mock_interaction)
mock_interaction.response.edit_message.assert_called_once()
# Check that view is RemoveMoveView
call_args = mock_interaction.response.edit_message.call_args
view_arg = call_args[1]['view']
assert isinstance(view_arg, RemoveMoveView)
@pytest.mark.asyncio
async def test_preview_button_empty_builder(self, mock_builder, mock_interaction):
"""Test preview button with empty builder."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
view.builder.is_empty = True
await view.preview_button.callback(mock_interaction)
mock_interaction.response.send_message.assert_called_once()
call_args = mock_interaction.response.send_message.call_args
assert "No moves to preview" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_preview_button_with_moves(self, mock_builder, mock_interaction):
"""Test preview button with moves available."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
view.builder.is_empty = False
with patch('views.transaction_embed.create_preview_embed') as mock_create_preview:
mock_create_preview.return_value = MagicMock()
await view.preview_button.callback(mock_interaction)
mock_interaction.response.send_message.assert_called_once()
call_args = mock_interaction.response.send_message.call_args
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_submit_button_empty_builder(self, mock_builder, mock_interaction):
"""Test submit button with empty builder."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
view.builder.is_empty = True
await view.submit_button.callback(mock_interaction)
mock_interaction.response.send_message.assert_called_once()
call_args = mock_interaction.response.send_message.call_args
assert "Cannot submit empty transaction" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_submit_button_illegal_transaction(self, mock_builder, mock_interaction):
"""Test submit button with illegal transaction."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
view.builder.is_empty = False
view.builder.validate_transaction = AsyncMock(return_value=RosterValidationResult(
is_legal=False,
major_league_count=26,
minor_league_count=10,
warnings=[],
errors=["Too many players"],
suggestions=["Drop 1 player"]
))
await view.submit_button.callback(mock_interaction)
mock_interaction.response.send_message.assert_called_once()
call_args = mock_interaction.response.send_message.call_args
message = call_args[0][0]
assert "Cannot submit illegal transaction" in message
assert "Too many players" in message
assert "Drop 1 player" in message
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_submit_button_legal_transaction(self, mock_builder, mock_interaction):
"""Test submit button with legal transaction."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
view.builder.is_empty = False
view.builder.validate_transaction = AsyncMock(return_value=RosterValidationResult(
is_legal=True,
major_league_count=25,
minor_league_count=10,
warnings=[],
errors=[],
suggestions=[]
))
await view.submit_button.callback(mock_interaction)
# Should send confirmation modal
mock_interaction.response.send_modal.assert_called_once()
modal_arg = mock_interaction.response.send_modal.call_args[0][0]
assert isinstance(modal_arg, SubmitConfirmationModal)
@pytest.mark.asyncio
async def test_cancel_button(self, mock_builder, mock_interaction):
"""Test cancel button clears moves and disables view."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
with patch('views.transaction_embed.create_transaction_embed') as mock_create_embed:
mock_create_embed.return_value = MagicMock()
await view.cancel_button.callback(mock_interaction)
# Should clear moves
view.builder.clear_moves.assert_called_once()
# Should edit message with disabled view
mock_interaction.response.edit_message.assert_called_once()
call_args = mock_interaction.response.edit_message.call_args
assert "Transaction cancelled" in call_args[1]['content']
class TestPlayerSelectionModal:
"""Test PlayerSelectionModal functionality."""
@pytest.fixture
def mock_builder(self):
"""Create mock TransactionBuilder."""
team = Team(id=499, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', season=12)
builder = MagicMock(spec=TransactionBuilder)
builder.team = team
builder.season = 12
builder.add_move.return_value = True
return builder
# Don't create modal as fixture - create in test methods to ensure event loop is running
@pytest.fixture
def mock_interaction(self):
"""Create mock Discord interaction."""
interaction = AsyncMock()
interaction.user = MagicMock()
interaction.user.id = 123456789
interaction.response = AsyncMock()
interaction.followup = AsyncMock()
interaction.client = MagicMock()
interaction.channel = MagicMock()
# Mock message history
mock_message = MagicMock()
mock_message.author = interaction.client.user
mock_message.embeds = [MagicMock()]
mock_message.embeds[0].title = "📋 Transaction Builder"
mock_message.edit = AsyncMock()
interaction.channel.history.return_value.__aiter__ = AsyncMock(return_value=iter([mock_message]))
return interaction
@pytest.mark.asyncio
async def test_modal_initialization(self, mock_builder):
"""Test modal initialization."""
modal = PlayerSelectionModal(mock_builder)
assert modal.title == f"Add Move - {mock_builder.team.abbrev}"
assert len(modal.children) == 3 # player_name, action, destination
@pytest.mark.asyncio
async def test_modal_submit_success(self, mock_builder, mock_interaction):
"""Test successful modal submission."""
modal = PlayerSelectionModal(mock_builder)
# Mock the TextInput values
modal.player_name = MagicMock()
modal.player_name.value = 'Mike Trout'
modal.action = MagicMock()
modal.action.value = 'add'
modal.destination = MagicMock()
modal.destination.value = 'ml'
mock_player = Player(id=123, name='Mike Trout', wara=2.5, season=12, pos_1='CF')
with patch('services.player_service.player_service') as mock_service:
mock_service.get_players_by_name.return_value = [mock_player]
await modal.on_submit(mock_interaction)
# Should defer response
mock_interaction.response.defer.assert_called_once()
# Should search for player
mock_service.get_players_by_name.assert_called_once_with('Mike Trout', 12)
# Should add move to builder
modal.builder.add_move.assert_called_once()
# Should send success message
mock_interaction.followup.send.assert_called_once()
call_args = mock_interaction.followup.send.call_args
assert "✅ Added:" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_modal_submit_invalid_action(self, mock_builder, mock_interaction):
"""Test modal submission with invalid action."""
modal = PlayerSelectionModal(mock_builder)
# Mock the TextInput values
modal.player_name = MagicMock()
modal.player_name.value = 'Mike Trout'
modal.action = MagicMock()
modal.action.value = 'invalid'
modal.destination = MagicMock()
modal.destination.value = 'ml'
await modal.on_submit(mock_interaction)
mock_interaction.followup.send.assert_called_once()
call_args = mock_interaction.followup.send.call_args
assert "Invalid action 'invalid'" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_modal_submit_player_not_found(self, mock_builder, mock_interaction):
"""Test modal submission when player not found."""
modal = PlayerSelectionModal(mock_builder)
# Mock the TextInput values
modal.player_name = MagicMock()
modal.player_name.value = 'Nonexistent Player'
modal.action = MagicMock()
modal.action.value = 'add'
modal.destination = MagicMock()
modal.destination.value = 'ml'
with patch('services.player_service.player_service') as mock_service:
mock_service.get_players_by_name.return_value = []
await modal.on_submit(mock_interaction)
mock_interaction.followup.send.assert_called_once()
call_args = mock_interaction.followup.send.call_args
assert "No players found matching" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_modal_submit_move_add_fails(self, mock_builder, mock_interaction):
"""Test modal submission when move addition fails."""
modal = PlayerSelectionModal(mock_builder)
# Mock the TextInput values
modal.player_name = MagicMock()
modal.player_name.value = 'Mike Trout'
modal.action = MagicMock()
modal.action.value = 'add'
modal.destination = MagicMock()
modal.destination.value = 'ml'
modal.builder.add_move.return_value = False # Simulate failure
mock_player = Player(id=123, name='Mike Trout', wara=2.5, season=12, pos_1='CF')
with patch('services.player_service.player_service') as mock_service:
mock_service.get_players_by_name.return_value = [mock_player]
await modal.on_submit(mock_interaction)
mock_interaction.followup.send.assert_called_once()
call_args = mock_interaction.followup.send.call_args
assert "Could not add move" in call_args[0][0]
assert "already be in this transaction" in call_args[0][0]
class TestSubmitConfirmationModal:
"""Test SubmitConfirmationModal functionality."""
@pytest.fixture
def mock_builder(self):
"""Create mock TransactionBuilder."""
team = Team(id=499, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', season=12)
builder = MagicMock(spec=TransactionBuilder)
builder.team = team
builder.moves = []
return builder
@pytest.fixture
def modal(self, mock_builder):
"""Create SubmitConfirmationModal instance."""
return SubmitConfirmationModal(mock_builder)
@pytest.fixture
def mock_interaction(self):
"""Create mock Discord interaction."""
interaction = AsyncMock()
interaction.user = MagicMock()
interaction.user.id = 123456789
interaction.response = AsyncMock()
interaction.followup = AsyncMock()
interaction.client = MagicMock()
interaction.channel = MagicMock()
# Mock message history
mock_message = MagicMock()
mock_message.author = interaction.client.user
mock_message.embeds = [MagicMock()]
mock_message.embeds[0].title = "📋 Transaction Builder"
mock_message.edit = AsyncMock()
interaction.channel.history.return_value.__aiter__ = AsyncMock(return_value=iter([mock_message]))
return interaction
@pytest.mark.asyncio
async def test_modal_submit_wrong_confirmation(self, mock_builder, mock_interaction):
"""Test modal submission with wrong confirmation text."""
modal = SubmitConfirmationModal(mock_builder)
# Mock the TextInput values
modal.confirmation = MagicMock()
modal.confirmation.value = 'WRONG'
await modal.on_submit(mock_interaction)
mock_interaction.response.send_message.assert_called_once()
call_args = mock_interaction.response.send_message.call_args
assert "must type 'CONFIRM' exactly" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_modal_submit_correct_confirmation(self, mock_builder, mock_interaction):
"""Test modal submission with correct confirmation."""
modal = SubmitConfirmationModal(mock_builder)
# Mock the TextInput values
modal.confirmation = MagicMock()
modal.confirmation.value = 'CONFIRM'
mock_transaction = MagicMock()
mock_transaction.moveid = 'Season-012-Week-11-123456789'
mock_transaction.week = 11
with patch('services.league_service.LeagueService') as mock_league_service_class:
mock_league_service = MagicMock()
mock_league_service_class.return_value = mock_league_service
mock_current_state = MagicMock()
mock_current_state.week = 10
mock_league_service.get_current_state.return_value = mock_current_state
modal.builder.submit_transaction.return_value = [mock_transaction]
with patch('services.transaction_builder.clear_transaction_builder') as mock_clear:
await modal.on_submit(mock_interaction)
# Should defer response
mock_interaction.response.defer.assert_called_once_with(ephemeral=True)
# Should get current state
mock_league_service.get_current_state.assert_called_once()
# Should submit transaction for next week
modal.builder.submit_transaction.assert_called_once_with(week=11)
# Should clear builder
mock_clear.assert_called_once_with(123456789)
# Should send success message
mock_interaction.followup.send.assert_called_once()
call_args = mock_interaction.followup.send.call_args
assert "Transaction Submitted Successfully" in call_args[0][0]
assert mock_transaction.moveid in call_args[0][0]
@pytest.mark.asyncio
async def test_modal_submit_no_current_state(self, mock_builder, mock_interaction):
"""Test modal submission when current state unavailable."""
modal = SubmitConfirmationModal(mock_builder)
# Mock the TextInput values
modal.confirmation = MagicMock()
modal.confirmation.value = 'CONFIRM'
with patch('services.league_service.LeagueService') as mock_league_service_class:
mock_league_service = MagicMock()
mock_league_service_class.return_value = mock_league_service
mock_league_service.get_current_state.return_value = None
await modal.on_submit(mock_interaction)
mock_interaction.followup.send.assert_called_once()
call_args = mock_interaction.followup.send.call_args
assert "Could not get current league state" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
class TestEmbedCreation:
"""Test embed creation functions."""
@pytest.fixture
def mock_builder_empty(self):
"""Create empty mock TransactionBuilder."""
team = Team(id=499, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', season=12)
builder = MagicMock(spec=TransactionBuilder)
builder.team = team
builder.is_empty = True
builder.move_count = 0
builder.moves = []
builder.created_at = MagicMock()
builder.created_at.strftime.return_value = "10:30:15"
builder.validate_transaction = AsyncMock(return_value=RosterValidationResult(
is_legal=True,
major_league_count=24,
minor_league_count=10,
warnings=[],
errors=[],
suggestions=["Add player moves to build your transaction"]
))
return builder
@pytest.fixture
def mock_builder_with_moves(self):
"""Create mock TransactionBuilder with moves."""
team = Team(id=499, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', season=12)
builder = MagicMock(spec=TransactionBuilder)
builder.team = team
builder.is_empty = False
builder.move_count = 2
mock_moves = []
for i in range(2):
move = MagicMock()
move.description = f"Move {i+1}: Player → Team"
mock_moves.append(move)
builder.moves = mock_moves
builder.created_at = MagicMock()
builder.created_at.strftime.return_value = "10:30:15"
builder.validate_transaction = AsyncMock(return_value=RosterValidationResult(
is_legal=False,
major_league_count=26,
minor_league_count=10,
warnings=["Warning message"],
errors=["Error message"],
suggestions=["Suggestion message"]
))
return builder
@pytest.mark.asyncio
async def test_create_transaction_embed_empty(self, mock_builder_empty):
"""Test creating embed for empty transaction."""
embed = await create_transaction_embed(mock_builder_empty)
assert isinstance(embed, discord.Embed)
assert "Transaction Builder - WV" in embed.title
assert "📋" in embed.title
# Should have fields for empty state
field_names = [field.name for field in embed.fields]
assert "Current Moves" in field_names
assert "Roster Status" in field_names
assert "Suggestions" in field_names
# Check empty moves message
moves_field = next(field for field in embed.fields if field.name == "Current Moves")
assert "No moves yet" in moves_field.value
@pytest.mark.asyncio
async def test_create_transaction_embed_with_moves(self, mock_builder_with_moves):
"""Test creating embed for transaction with moves."""
embed = await create_transaction_embed(mock_builder_with_moves)
assert isinstance(embed, discord.Embed)
assert "Transaction Builder - WV" in embed.title
# Should have all fields
field_names = [field.name for field in embed.fields]
assert "Current Moves (2)" in field_names
assert "Roster Status" in field_names
assert "❌ Errors" in field_names
assert "Suggestions" in field_names
# Check moves content
moves_field = next(field for field in embed.fields if "Current Moves" in field.name)
assert "Move 1: Player → Team" in moves_field.value
assert "Move 2: Player → Team" in moves_field.value
@pytest.mark.asyncio
async def test_create_preview_embed(self, mock_builder_with_moves):
"""Test creating preview embed."""
embed = await create_preview_embed(mock_builder_with_moves)
assert isinstance(embed, discord.Embed)
assert "Transaction Preview - WV" in embed.title
assert "📋" in embed.title
# Should have preview-specific fields
field_names = [field.name for field in embed.fields]
assert "All Moves (2)" in field_names
assert "Final Roster Status" in field_names
assert "❌ Validation Issues" in field_names