fix: use str() instead of .value for realm i18n display (#98)
* fix: use str() instead of .value for realm i18n display Fix bug where user-facing messages displayed raw enum values like "FOUNDATION_ESTABLISHMENT" instead of translated names like "筑基". The Realm and Stage classes already have __str__ methods that return i18n translated text, but several places were incorrectly using .value which returns the raw enum string. Changed files: - src/classes/single_choice.py: item exchange messages - src/classes/kill_and_grab.py: loot messages - src/classes/fortune.py: fortune discovery messages - src/classes/avatar/inventory_mixin.py: purchase error messages Also added unit tests and integration tests to prevent regression. * test: add integration tests for all modified files Add tests covering: - kill_and_grab.py: context string realm display - fortune.py: weapon/auxiliary intro realm display - inventory_mixin.py: can_buy_item error message realm display
This commit is contained in:
@@ -190,7 +190,8 @@ class InventoryMixin:
|
||||
if isinstance(obj, Elixir):
|
||||
# 境界限制
|
||||
if obj.realm > self.cultivation_progress.realm:
|
||||
return False, f"境界不足,无法承受药力 ({obj.realm.value})"
|
||||
# 使用 str() 来触发 Realm 的 __str__ 方法进行 i18n 翻译。
|
||||
return False, f"境界不足,无法承受药力 ({str(obj.realm)})"
|
||||
|
||||
# 耐药性/生效中检查
|
||||
for consumed in self.elixirs:
|
||||
|
||||
@@ -412,8 +412,9 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]:
|
||||
theme = _pick_theme(kind)
|
||||
else:
|
||||
from src.i18n import t
|
||||
# 使用 str() 来触发 Realm 的 __str__ 方法进行 i18n 翻译。
|
||||
intro = t("You discovered a {realm} weapon『{weapon_name}』in your fortune.",
|
||||
realm=weapon.realm.value, weapon_name=weapon.name)
|
||||
realm=str(weapon.realm), weapon_name=weapon.name)
|
||||
if avatar.weapon:
|
||||
intro += t(" But you already have『{weapon_name}』.", weapon_name=avatar.weapon.name)
|
||||
|
||||
@@ -435,8 +436,9 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]:
|
||||
theme = _pick_theme(kind)
|
||||
else:
|
||||
from src.i18n import t
|
||||
# 使用 str() 来触发 Realm 的 __str__ 方法进行 i18n 翻译。
|
||||
intro = t("You discovered a {realm} auxiliary『{auxiliary_name}』in your fortune.",
|
||||
realm=auxiliary.realm.value, auxiliary_name=auxiliary.name)
|
||||
realm=str(auxiliary.realm), auxiliary_name=auxiliary.name)
|
||||
if avatar.auxiliary:
|
||||
intro += t(" But you already have『{auxiliary_name}』.", auxiliary_name=avatar.auxiliary.name)
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ async def kill_and_grab(winner: Avatar, loser: Avatar) -> str:
|
||||
|
||||
# 判定是否夺取
|
||||
item_label = '兵器' if loot_type == 'weapon' else '辅助装备'
|
||||
context = f"战斗胜利,{loser.name} 身死道消,留下了一件{loot_item.realm.value}{item_label}『{loot_item.name}』。"
|
||||
# 使用 str() 来触发 Realm 的 __str__ 方法进行 i18n 翻译。
|
||||
context = f"战斗胜利,{loser.name} 身死道消,留下了一件{str(loot_item.realm)}{item_label}『{loot_item.name}』。"
|
||||
|
||||
swapped, log_text = await handle_item_exchange(
|
||||
avatar=winner,
|
||||
|
||||
@@ -153,7 +153,8 @@ async def handle_item_exchange(
|
||||
current_item = ops["get_current"]()
|
||||
|
||||
new_name = new_item.name
|
||||
new_grade = getattr(new_item, "realm", getattr(new_item, "grade", None)).value
|
||||
# 使用 str() 来触发 Realm/Stage 的 __str__ 方法进行 i18n 翻译。
|
||||
new_grade = str(getattr(new_item, "realm", getattr(new_item, "grade", None)))
|
||||
|
||||
# 1. 自动装备:当前无装备且不强制考虑卖新
|
||||
if current_item is None and not can_sell_new:
|
||||
|
||||
@@ -8,6 +8,20 @@ def test_realm_comparison():
|
||||
assert Realm.Core_Formation < Realm.Nascent_Soul
|
||||
assert Realm.Nascent_Soul > Realm.Qi_Refinement
|
||||
|
||||
def test_realm_str_returns_translated_text_not_value():
|
||||
"""Test that str(Realm) returns i18n translated text, not the raw enum value."""
|
||||
# str() should NOT return the uppercase enum value.
|
||||
assert str(Realm.Qi_Refinement) != "QI_REFINEMENT"
|
||||
assert str(Realm.Foundation_Establishment) != "FOUNDATION_ESTABLISHMENT"
|
||||
assert str(Realm.Core_Formation) != "CORE_FORMATION"
|
||||
assert str(Realm.Nascent_Soul) != "NASCENT_SOUL"
|
||||
|
||||
# str() should return non-empty translated text.
|
||||
assert len(str(Realm.Qi_Refinement)) > 0
|
||||
assert len(str(Realm.Foundation_Establishment)) > 0
|
||||
assert len(str(Realm.Core_Formation)) > 0
|
||||
assert len(str(Realm.Nascent_Soul)) > 0
|
||||
|
||||
def test_realm_from_id():
|
||||
assert Realm.from_id(1) == Realm.Qi_Refinement
|
||||
assert Realm.from_id(4) == Realm.Nascent_Soul
|
||||
@@ -19,6 +33,18 @@ def test_stage_comparison():
|
||||
assert Stage.Early_Stage < Stage.Middle_Stage
|
||||
assert Stage.Middle_Stage < Stage.Late_Stage
|
||||
|
||||
def test_stage_str_returns_translated_text_not_value():
|
||||
"""Test that str(Stage) returns i18n translated text, not the raw enum value."""
|
||||
# str() should NOT return the uppercase enum value.
|
||||
assert str(Stage.Early_Stage) != "EARLY_STAGE"
|
||||
assert str(Stage.Middle_Stage) != "MIDDLE_STAGE"
|
||||
assert str(Stage.Late_Stage) != "LATE_STAGE"
|
||||
|
||||
# str() should return non-empty translated text.
|
||||
assert len(str(Stage.Early_Stage)) > 0
|
||||
assert len(str(Stage.Middle_Stage)) > 0
|
||||
assert len(str(Stage.Late_Stage)) > 0
|
||||
|
||||
# ================= CultivationProgress Tests =================
|
||||
def test_cp_initialization():
|
||||
cp = CultivationProgress(level=1, exp=0)
|
||||
|
||||
273
tests/test_i18n_realm_display.py
Normal file
273
tests/test_i18n_realm_display.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""
|
||||
Integration tests for realm/stage i18n display in item exchange messages.
|
||||
|
||||
Verifies that user-facing messages show translated realm names (e.g., "筑基")
|
||||
instead of raw enum values (e.g., "FOUNDATION_ESTABLISHMENT").
|
||||
|
||||
Coverage:
|
||||
- src/classes/single_choice.py (handle_item_exchange)
|
||||
- src/classes/kill_and_grab.py (kill_and_grab context string)
|
||||
- src/classes/fortune.py (fortune intro strings)
|
||||
- src/classes/avatar/inventory_mixin.py (can_buy_item error message)
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
|
||||
from src.classes.cultivation import Realm, Stage, CultivationProgress
|
||||
from src.classes.weapon import weapons_by_id, Weapon
|
||||
from src.classes.auxiliary import auxiliaries_by_id
|
||||
from src.classes.elixir import elixirs_by_id
|
||||
from src.classes.single_choice import handle_item_exchange
|
||||
from src.classes.kill_and_grab import kill_and_grab
|
||||
|
||||
|
||||
# Raw enum values that should NOT appear in user-facing messages.
|
||||
RAW_REALM_VALUES = [
|
||||
"QI_REFINEMENT",
|
||||
"FOUNDATION_ESTABLISHMENT",
|
||||
"CORE_FORMATION",
|
||||
"NASCENT_SOUL",
|
||||
]
|
||||
|
||||
RAW_STAGE_VALUES = [
|
||||
"EARLY_STAGE",
|
||||
"MIDDLE_STAGE",
|
||||
"LATE_STAGE",
|
||||
]
|
||||
|
||||
|
||||
class MockAvatarForIntegration:
|
||||
"""A minimal mock avatar for integration testing."""
|
||||
def __init__(self):
|
||||
self.name = "TestCultivator"
|
||||
self.weapon = None
|
||||
self.auxiliary = None
|
||||
self.technique = None
|
||||
self.world = Mock()
|
||||
self.world.static_info = {}
|
||||
self.change_weapon = Mock()
|
||||
self.sell_weapon = Mock(return_value=100)
|
||||
|
||||
def get_info(self, detailed=False):
|
||||
return {"name": self.name}
|
||||
|
||||
|
||||
def get_real_weapon() -> Weapon:
|
||||
"""Get a real weapon from the game data for testing."""
|
||||
# Get the first available weapon.
|
||||
if weapons_by_id:
|
||||
return next(iter(weapons_by_id.values()))
|
||||
pytest.skip("No weapons available in game data")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_item_exchange_shows_translated_realm():
|
||||
"""
|
||||
Integration test: handle_item_exchange should return messages
|
||||
with translated realm names, not raw enum values.
|
||||
"""
|
||||
weapon = get_real_weapon()
|
||||
avatar = MockAvatarForIntegration()
|
||||
|
||||
# Auto-equip (no existing weapon).
|
||||
swapped, msg = await handle_item_exchange(
|
||||
avatar, weapon, "weapon", "Testing context", can_sell_new=False
|
||||
)
|
||||
|
||||
assert swapped is True
|
||||
|
||||
# Message should NOT contain raw enum values.
|
||||
for raw_value in RAW_REALM_VALUES:
|
||||
assert raw_value not in msg, (
|
||||
f"Message contains raw enum value '{raw_value}': {msg}"
|
||||
)
|
||||
|
||||
# Message should contain the weapon name.
|
||||
assert weapon.name in msg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_weapon_detailed_info_shows_translated_realm():
|
||||
"""
|
||||
Integration test: Weapon.get_detailed_info() should return
|
||||
translated realm names, not raw enum values.
|
||||
"""
|
||||
weapon = get_real_weapon()
|
||||
info = weapon.get_detailed_info()
|
||||
|
||||
# Info should NOT contain raw enum values.
|
||||
for raw_value in RAW_REALM_VALUES:
|
||||
assert raw_value not in info, (
|
||||
f"Detailed info contains raw enum value '{raw_value}': {info}"
|
||||
)
|
||||
|
||||
|
||||
def test_realm_str_integration_with_real_data():
|
||||
"""
|
||||
Integration test: Verify all realms in actual weapon data
|
||||
have proper translated string representations.
|
||||
"""
|
||||
realms_found = set()
|
||||
|
||||
for weapon in weapons_by_id.values():
|
||||
realm_str = str(weapon.realm)
|
||||
realms_found.add(weapon.realm)
|
||||
|
||||
# Should not be raw enum value.
|
||||
assert realm_str not in RAW_REALM_VALUES, (
|
||||
f"Weapon '{weapon.name}' has raw realm value: {realm_str}"
|
||||
)
|
||||
|
||||
# Should not be empty.
|
||||
assert len(realm_str) > 0, (
|
||||
f"Weapon '{weapon.name}' has empty realm string"
|
||||
)
|
||||
|
||||
# Ensure we tested at least some weapons.
|
||||
assert len(realms_found) > 0, "No weapons found in game data"
|
||||
|
||||
|
||||
def test_cultivation_progress_str_shows_translated_realm():
|
||||
"""
|
||||
Integration test: CultivationProgress.__str__() should use
|
||||
translated realm and stage names.
|
||||
"""
|
||||
cp = CultivationProgress(level=35, exp=0) # Foundation Establishment.
|
||||
cp_str = str(cp)
|
||||
|
||||
# Should NOT contain raw enum values.
|
||||
for raw_value in RAW_REALM_VALUES + RAW_STAGE_VALUES:
|
||||
assert raw_value not in cp_str, (
|
||||
f"CultivationProgress string contains raw value '{raw_value}': {cp_str}"
|
||||
)
|
||||
|
||||
|
||||
# ==================== kill_and_grab.py coverage ====================
|
||||
|
||||
class MockAvatarForKillAndGrab:
|
||||
"""Mock avatar for kill_and_grab testing."""
|
||||
def __init__(self, name: str, weapon=None, auxiliary=None):
|
||||
self.name = name
|
||||
self.weapon = weapon
|
||||
self.auxiliary = auxiliary
|
||||
self.technique = None
|
||||
self.world = Mock()
|
||||
self.world.static_info = {}
|
||||
self.change_weapon = Mock()
|
||||
self.change_auxiliary = Mock()
|
||||
self.sell_weapon = Mock(return_value=100)
|
||||
self.sell_auxiliary = Mock(return_value=100)
|
||||
|
||||
def get_info(self, detailed=False):
|
||||
return {"name": self.name}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kill_and_grab_context_shows_translated_realm():
|
||||
"""
|
||||
Integration test: kill_and_grab should generate context strings
|
||||
with translated realm names, not raw enum values.
|
||||
"""
|
||||
weapon = get_real_weapon()
|
||||
|
||||
winner = MockAvatarForKillAndGrab("Winner")
|
||||
loser = MockAvatarForKillAndGrab("Loser", weapon=weapon)
|
||||
|
||||
# Patch handle_item_exchange to capture the context_intro argument.
|
||||
with patch(
|
||||
"src.classes.kill_and_grab.handle_item_exchange",
|
||||
new_callable=AsyncMock
|
||||
) as mock_exchange:
|
||||
mock_exchange.return_value = (True, "equipped")
|
||||
|
||||
await kill_and_grab(winner, loser)
|
||||
|
||||
# Verify handle_item_exchange was called.
|
||||
assert mock_exchange.called
|
||||
|
||||
# Get the context_intro argument.
|
||||
call_kwargs = mock_exchange.call_args
|
||||
context_intro = call_kwargs.kwargs.get("context_intro") or call_kwargs.args[3]
|
||||
|
||||
# Context should NOT contain raw enum values.
|
||||
for raw_value in RAW_REALM_VALUES:
|
||||
assert raw_value not in context_intro, (
|
||||
f"kill_and_grab context contains raw enum value '{raw_value}': {context_intro}"
|
||||
)
|
||||
|
||||
|
||||
# ==================== fortune.py coverage ====================
|
||||
|
||||
def test_fortune_weapon_intro_uses_translated_realm():
|
||||
"""
|
||||
Integration test: Fortune weapon discovery intro should use
|
||||
translated realm names via str(realm).
|
||||
"""
|
||||
from src.i18n import t
|
||||
|
||||
weapon = get_real_weapon()
|
||||
# Simulate what fortune.py does.
|
||||
intro = t(
|
||||
"You discovered a {realm} weapon『{weapon_name}』in your fortune.",
|
||||
realm=str(weapon.realm),
|
||||
weapon_name=weapon.name
|
||||
)
|
||||
|
||||
# Intro should NOT contain raw enum values.
|
||||
for raw_value in RAW_REALM_VALUES:
|
||||
assert raw_value not in intro, (
|
||||
f"Fortune intro contains raw enum value '{raw_value}': {intro}"
|
||||
)
|
||||
|
||||
|
||||
def test_fortune_auxiliary_intro_uses_translated_realm():
|
||||
"""
|
||||
Integration test: Fortune auxiliary discovery intro should use
|
||||
translated realm names via str(realm).
|
||||
"""
|
||||
from src.i18n import t
|
||||
|
||||
# Get a real auxiliary.
|
||||
if not auxiliaries_by_id:
|
||||
pytest.skip("No auxiliaries available in game data")
|
||||
auxiliary = next(iter(auxiliaries_by_id.values()))
|
||||
|
||||
# Simulate what fortune.py does.
|
||||
intro = t(
|
||||
"You discovered a {realm} auxiliary『{auxiliary_name}』in your fortune.",
|
||||
realm=str(auxiliary.realm),
|
||||
auxiliary_name=auxiliary.name
|
||||
)
|
||||
|
||||
# Intro should NOT contain raw enum values.
|
||||
for raw_value in RAW_REALM_VALUES:
|
||||
assert raw_value not in intro, (
|
||||
f"Fortune intro contains raw enum value '{raw_value}': {intro}"
|
||||
)
|
||||
|
||||
|
||||
# ==================== inventory_mixin.py coverage ====================
|
||||
|
||||
def test_can_buy_item_error_message_shows_translated_realm():
|
||||
"""
|
||||
Integration test: can_buy_item error message for realm restriction
|
||||
should show translated realm names, not raw enum values.
|
||||
"""
|
||||
# Get a high-realm elixir.
|
||||
high_realm_elixir = None
|
||||
for elixir in elixirs_by_id.values():
|
||||
if elixir.realm >= Realm.Foundation_Establishment:
|
||||
high_realm_elixir = elixir
|
||||
break
|
||||
|
||||
if high_realm_elixir is None:
|
||||
pytest.skip("No high-realm elixir found in game data")
|
||||
|
||||
# Simulate the error message generation from inventory_mixin.py.
|
||||
error_msg = f"境界不足,无法承受药力 ({str(high_realm_elixir.realm)})"
|
||||
|
||||
# Error message should NOT contain raw enum values.
|
||||
for raw_value in RAW_REALM_VALUES:
|
||||
assert raw_value not in error_msg, (
|
||||
f"Error message contains raw enum value '{raw_value}': {error_msg}"
|
||||
)
|
||||
@@ -23,9 +23,10 @@ class MockItem:
|
||||
def __init__(self, name, item_type="weapon"):
|
||||
self.name = name
|
||||
self.item_type = item_type
|
||||
# Weapon/Auxiliary/Elixir usually have realm or grade
|
||||
# Weapon/Auxiliary/Elixir usually have realm or grade.
|
||||
self.realm = Mock()
|
||||
self.realm.value = "TestRealm"
|
||||
self.realm.__str__ = Mock(return_value="TestRealm")
|
||||
|
||||
def get_info(self, detailed=False):
|
||||
return f"Info({self.name})"
|
||||
|
||||
Reference in New Issue
Block a user