All checks were successful
Ruff Lint / lint (pull_request) Successful in 21s
Replace plain ASCII progress bars and text badges with a polished embed: - Unicode block progress bars (▰▱) replacing ASCII [===---] - Tier-specific symbols (○ ◈ ◆ ✦ ★) instead of [BC]/[R]/[GR]/[SF] badges - Team-branded embeds via get_team_embed (color, logo, season footer) - Tier distribution summary header in code block - Percentage display and backtick-wrapped values - Tier-specific accent colors for single-tier filtered views - Sparkle treatment for fully evolved cards (✧ FULLY EVOLVED ✧) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
299 lines
11 KiB
Python
299 lines
11 KiB
Python
"""
|
|
Tests for WP-12: Tier Badge on Card Embed.
|
|
|
|
Verifies that get_card_embeds() prepends a tier badge to the card title when a
|
|
card has Refractor tier progression, and falls back gracefully when the Refractor
|
|
API is unavailable or returns no state.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import discord
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_card(card_id=1, player_name="Mike Trout", rarity_color="FFD700"):
|
|
"""Minimal card dict matching the API shape consumed by get_card_embeds."""
|
|
return {
|
|
"id": card_id,
|
|
"player": {
|
|
"player_id": 101,
|
|
"p_name": player_name,
|
|
"rarity": {"name": "MVP", "value": 5, "color": rarity_color},
|
|
"cost": 500,
|
|
"image": "https://example.com/card.png",
|
|
"image2": None,
|
|
"mlbclub": "Los Angeles Angels",
|
|
"franchise": "Los Angeles Angels",
|
|
"headshot": "https://example.com/headshot.jpg",
|
|
"cardset": {"name": "2023 Season"},
|
|
"pos_1": "CF",
|
|
"pos_2": None,
|
|
"pos_3": None,
|
|
"pos_4": None,
|
|
"pos_5": None,
|
|
"pos_6": None,
|
|
"pos_7": None,
|
|
"bbref_id": "troutmi01",
|
|
"strat_code": "420420",
|
|
"fangr_id": None,
|
|
"vanity_card": None,
|
|
},
|
|
"team": {
|
|
"id": 10,
|
|
"lname": "Paper Dynasty",
|
|
"logo": "https://example.com/logo.png",
|
|
"season": 7,
|
|
},
|
|
}
|
|
|
|
|
|
def _make_paperdex():
|
|
"""Minimal paperdex response."""
|
|
return {"count": 0, "paperdex": []}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers to patch the async dependencies of get_card_embeds
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _patch_db_get(evo_response=None, paperdex_response=None):
|
|
"""
|
|
Return a side_effect callable that routes db_get calls to the right mock
|
|
responses, so other get_card_embeds internals still behave.
|
|
"""
|
|
if paperdex_response is None:
|
|
paperdex_response = _make_paperdex()
|
|
|
|
async def _side_effect(endpoint, *args, **kwargs):
|
|
if str(endpoint).startswith("refractor/cards/"):
|
|
return evo_response
|
|
if endpoint == "paperdex":
|
|
return paperdex_response
|
|
# Fallback for any other endpoint (e.g. plays/batting, plays/pitching)
|
|
return None
|
|
|
|
return _side_effect
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestTierBadgeFormat:
|
|
"""Unit: tier badge string format for each tier level."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_zero_no_badge(self):
|
|
"""T0 evolution state (current_tier=0) should produce no badge in title."""
|
|
card = _make_card()
|
|
evo_state = {"current_tier": 0, "card_id": 1}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "Mike Trout"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_one_badge(self):
|
|
"""current_tier=1 should prefix title with [BC] (Base Chrome)."""
|
|
card = _make_card()
|
|
evo_state = {"current_tier": 1, "card_id": 1}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "[BC] Mike Trout"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_two_badge(self):
|
|
"""current_tier=2 should prefix title with [R] (Refractor)."""
|
|
card = _make_card()
|
|
evo_state = {"current_tier": 2, "card_id": 1}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "[R] Mike Trout"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_three_badge(self):
|
|
"""current_tier=3 should prefix title with [GR] (Gold Refractor)."""
|
|
card = _make_card()
|
|
evo_state = {"current_tier": 3, "card_id": 1}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "[GR] Mike Trout"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_four_superfractor_badge(self):
|
|
"""current_tier=4 (Superfractor) should prefix title with [SF]."""
|
|
card = _make_card()
|
|
evo_state = {"current_tier": 4, "card_id": 1}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "[SF] Mike Trout"
|
|
|
|
|
|
class TestTierBadgeInTitle:
|
|
"""Unit: badge appears correctly in the embed title."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_badge_prepended_to_player_name(self):
|
|
"""Badge should be prepended so title reads '[Tx] <player_name>'."""
|
|
card = _make_card(player_name="Juan Soto")
|
|
evo_state = {"current_tier": 2, "card_id": 1}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title.startswith("[R] ")
|
|
assert "Juan Soto" in embeds[0].title
|
|
|
|
|
|
class TestFullyEvolvedBadge:
|
|
"""Unit: fully evolved card shows [SF] badge (Superfractor)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fully_evolved_badge(self):
|
|
"""T4 card should show [SF] prefix, not [T4]."""
|
|
card = _make_card()
|
|
evo_state = {"current_tier": 4}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title.startswith("[SF] ")
|
|
assert "[T4]" not in embeds[0].title
|
|
|
|
|
|
class TestNoBadgeGracefulFallback:
|
|
"""Unit: embed renders correctly when evolution state is absent or API fails."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_evolution_state_no_badge(self):
|
|
"""When evolution API returns None (404), title has no badge."""
|
|
card = _make_card()
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=None)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "Mike Trout"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_exception_no_badge(self):
|
|
"""When evolution API raises an exception, card display is unaffected."""
|
|
card = _make_card()
|
|
|
|
async def _failing_db_get(endpoint, *args, **kwargs):
|
|
if str(endpoint).startswith("refractor/cards/"):
|
|
raise ConnectionError("API unreachable")
|
|
if endpoint == "paperdex":
|
|
return _make_paperdex()
|
|
return None
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _failing_db_get
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "Mike Trout"
|
|
|
|
|
|
class TestEmbedColorUnchanged:
|
|
"""Unit: embed color comes from card rarity, not affected by evolution state."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_embed_color_from_rarity_with_evolution(self):
|
|
"""Color is still derived from rarity even when a tier badge is present."""
|
|
rarity_color = "FF0000"
|
|
card = _make_card(rarity_color=rarity_color)
|
|
evo_state = {"current_tier": 2}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].color == discord.Color(int(rarity_color, 16))
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_embed_color_from_rarity_without_evolution(self):
|
|
"""Color is derived from rarity when no evolution state exists."""
|
|
rarity_color = "00FF00"
|
|
card = _make_card(rarity_color=rarity_color)
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=None)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].color == discord.Color(int(rarity_color, 16))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# T1-7: TIER_BADGES format consistency check across modules
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestTierSymbolsCompleteness:
|
|
"""
|
|
T1-7: Assert that TIER_SYMBOLS in cogs.refractor covers all tiers 0-4
|
|
and that helpers.main TIER_BADGES still covers tiers 1-4 for card embeds.
|
|
|
|
Why: The refractor status command uses Unicode TIER_SYMBOLS for display,
|
|
while card embed titles use helpers.main TIER_BADGES in bracket format.
|
|
Both must cover the full tier range for their respective contexts.
|
|
"""
|
|
|
|
def test_tier_symbols_covers_all_tiers(self):
|
|
"""TIER_SYMBOLS must have entries for T0 through T4."""
|
|
from cogs.refractor import TIER_SYMBOLS
|
|
|
|
for tier in range(5):
|
|
assert tier in TIER_SYMBOLS, f"TIER_SYMBOLS missing tier {tier}"
|
|
|
|
def test_tier_badges_covers_evolved_tiers(self):
|
|
"""helpers.main TIER_BADGES must have entries for T1 through T4."""
|
|
from helpers.main import TIER_BADGES
|
|
|
|
for tier in range(1, 5):
|
|
assert tier in TIER_BADGES, f"TIER_BADGES missing tier {tier}"
|
|
|
|
def test_tier_symbols_are_unique(self):
|
|
"""Each tier must have a distinct symbol."""
|
|
from cogs.refractor import TIER_SYMBOLS
|
|
|
|
values = list(TIER_SYMBOLS.values())
|
|
assert len(values) == len(set(values)), f"Duplicate symbols found: {values}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper: call get_card_embeds and return embed list
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
async def _call_get_card_embeds(card):
|
|
"""Import and call get_card_embeds, returning the list of embeds."""
|
|
from helpers.main import get_card_embeds
|
|
|
|
result = await get_card_embeds(card)
|
|
if isinstance(result, list):
|
|
return result
|
|
return [result]
|