Skip to content
Merged
24 changes: 18 additions & 6 deletions bot/extensions/translator_cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ async def language_autocomplete(_: Interaction, current: str) -> list[Choice[str

return [
Choice(name=language.capitalize(), value=language)
for language in languages[:25]
for language in languages
if current.lower() in language.lower()
]
][:25]


class TranslatorCog(
Expand All @@ -36,31 +36,43 @@ class TranslatorCog(
usage="sentence={sentence}",
)
@autocomplete(translate_into=language_autocomplete)
async def translator(self, ctx: Context, *, sentence: str, translate_into: str):
@autocomplete(translate_from=language_autocomplete)
async def translator(
self,
ctx: Context,
*,
sentence: str,
translate_from: str = "auto",
translate_into: str = "english",
):
"""Translate a sentence or word from any language into any languages.

:param ctx: The context object.
:type ctx: Context
:param sentence: The sentence or word to be translated.
:type sentence: str
:param translate_from: The language code for the source language.
:type translate_from: str
:param translate_into: The language code for the target language.
:type translate_into: str
:return: Embed with original input and its translation
"""
await ctx.defer()

text_translator = Translator()
translated_text = text_translator.translate(sentence, dest=translate_into)
translated_text = text_translator.translate(
sentence, src=translate_from, dest=translate_into
)

embed = Embed(color=self.bot.default_color)

embed.add_field(
name=f"{LANGUAGES[translated_text.src].capitalize()} Original",
value=sentence.capitalize(),
value=sentence,
inline=False,
)
embed.add_field(
name=f"{translate_into} Translation",
name=f"{translate_into.capitalize()} Translation",
value=translated_text.text,
inline=False,
)
Expand Down
13 changes: 13 additions & 0 deletions tests/extensions/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from unittest.mock import MagicMock

import pytest


@pytest.fixture
def mock_bot():
"""Create a mock Discord bot instance."""
bot = MagicMock()
bot.default_color = 0xFFFFFF
bot.app.config.get = MagicMock(return_value=None)
bot.scheduler = MagicMock()
return bot
12 changes: 1 addition & 11 deletions tests/extensions/test_reminder_cog.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re
from datetime import UTC, datetime, timedelta
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import AsyncMock

import pytest
from dateutil.tz import tzlocal
Expand All @@ -10,16 +10,6 @@
from bot.extensions.reminder_cog import ReminderCog


@pytest.fixture
def mock_bot():
"""Create a mock Discord bot instance."""
bot = MagicMock()
bot.default_color = 0xFFFFFF
bot.app.config.get = MagicMock(return_value=None)
bot.scheduler = MagicMock()
return bot


@pytest.fixture
def reminder_cog(mock_bot):
"""Instantiate the ReminderCog with a mock bot."""
Expand Down
10 changes: 0 additions & 10 deletions tests/extensions/test_threads_cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,6 @@
from bot.models.extensions.thread import Thread


@pytest.fixture
def mock_bot():
"""Create a mock Discord bot instance."""
bot = MagicMock()
bot.default_color = 0xFFFFFF
bot.app.config.get = MagicMock(return_value=None)
bot.scheduler = MagicMock()
return bot


@pytest.fixture
def threads_cog(mock_bot):
"""Instantiate the ThreadsCog with a mock bot."""
Expand Down
6 changes: 0 additions & 6 deletions tests/extensions/test_time_cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@
from bot.extensions.time_cog import TimeCog


@pytest.fixture
def mock_bot():
"""Create a mock Discord bot instance."""
return MagicMock()


@pytest.fixture
def time_cog(mock_bot):
"""Instantiate the TimeCog with a mock bot."""
Expand Down
252 changes: 252 additions & 0 deletions tests/extensions/test_translator_cog.py
Comment thread
PenguinBoi12 marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You added defaults for translate_from ("auto") and translate_into ("english") earlier in review, but there's no test covering that path. Add one that omits both kwargs and confirms they get passed through to Translator().translate(), one test that passes each and one that passes both.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might worth to make a fixture for the ctx:

@pytest.fixture
def mock_ctx():
    """Create a mock command context."""
    ctx = MagicMock()
    ctx.defer = AsyncMock()
    ctx.send = AsyncMock()
    return ctx

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no case sensitivity test of language_autocomplete

@pytest.mark.asyncio
async def test_language_autocomplete__with_uppercase_input__expect_case_insensitive_match():
    result = await language_autocomplete(None, "ARA")
    assert result == [
        Choice(name="Arabic", value="arabic"),
        Choice(name="Gujarati", value="gujarati"),
        Choice(name="Marathi", value="marathi"),
    ]

Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
from unittest.mock import AsyncMock, MagicMock, patch
from types import SimpleNamespace

import pytest
from discord import Embed
from discord.app_commands import Choice
from discord.ext.commands import CommandInvokeError

from bot.extensions.translator_cog import TranslatorCog, language_autocomplete


@pytest.fixture
def translator_cog(mock_bot):
"""Instantiate the TranslatorCog with a mock bot."""

return TranslatorCog(mock_bot)


@pytest.fixture
def mock_ctx():
"""Create a mock command context."""
ctx = MagicMock()
ctx.defer = AsyncMock()
ctx.send = AsyncMock()
return ctx


@pytest.fixture
def embed_factory(mock_bot):
def _make_embed(*, sentence, translate_from, translate_into, translated_text):
embed = Embed(color=mock_bot.default_color)

embed.add_field(
name=f"{translate_from.capitalize()} Original",
value=sentence,
inline=False,
)
embed.add_field(
name=f"{translate_into.capitalize()} Translation",
value=translated_text,
inline=False,
)

return embed

return _make_embed


@pytest.mark.asyncio
@patch("bot.extensions.translator_cog.Translator")
async def test_translator__with_all_input__expect_translation_with_embed(
mock_translator, translator_cog, mock_ctx, embed_factory
):
instance = mock_translator.return_value
instance.translate.return_value = SimpleNamespace(
text="مضحك", src="en"
) # translated text

await translator_cog.translator(
translator_cog,
mock_ctx,
sentence="funny",
translate_from="english",
translate_into="arabic",
)

result = embed_factory(
sentence="funny",
translate_from="english",
translate_into="arabic",
translated_text="مضحك",
)

instance.translate.assert_called_once_with("funny", src="english", dest="arabic")
mock_ctx.defer.assert_awaited_once()
mock_ctx.send.assert_awaited_once_with(embed=result)


@pytest.mark.asyncio
@patch("bot.extensions.translator_cog.Translator")
async def test_translator__with_translate_into_input__expect_translation_with_embed(
mock_translator, translator_cog, mock_ctx, embed_factory
):
instance = mock_translator.return_value
instance.translate.return_value = SimpleNamespace(text="مضحك", src="en")

await translator_cog.translator(
translator_cog,
mock_ctx,
sentence="funny",
translate_into="arabic",
)

result = embed_factory(
sentence="funny",
translate_from="english",
translate_into="arabic",
translated_text="مضحك",
)

instance.translate.assert_called_once_with("funny", src="auto", dest="arabic")
mock_ctx.defer.assert_awaited_once()
mock_ctx.send.assert_awaited_once_with(embed=result)


@pytest.mark.asyncio
@patch("bot.extensions.translator_cog.Translator")
async def test_translator__with_translate_from_input__expect_translation_with_embed(
mock_translator, translator_cog, mock_ctx, embed_factory
):
instance = mock_translator.return_value
instance.translate.return_value = SimpleNamespace(text="funny", src="ar")

await translator_cog.translator(
translator_cog,
mock_ctx,
sentence="مضحك",
translate_from="arabic",
)

result = embed_factory(
sentence="مضحك",
translate_from="arabic",
translate_into="english",
translated_text="funny",
)

instance.translate.assert_called_once_with("مضحك", src="arabic", dest="english")
mock_ctx.defer.assert_awaited_once()
mock_ctx.send.assert_awaited_once_with(embed=result)


@pytest.mark.asyncio
@patch("bot.extensions.translator_cog.Translator")
async def test_translator__with_no_langugae_input__expect_translation_with_embed(
mock_translator, translator_cog, mock_ctx, embed_factory
):
instance = mock_translator.return_value
instance.translate.return_value = SimpleNamespace(text="funny", src="ar")

await translator_cog.translator(
translator_cog,
mock_ctx,
sentence="مضحك",
)

result = embed_factory(
sentence="مضحك",
translate_from="arabic",
translate_into="english",
translated_text="funny",
)

instance.translate.assert_called_once_with("مضحك", src="auto", dest="english")
mock_ctx.defer.assert_awaited_once()
mock_ctx.send.assert_awaited_once_with(embed=result)


@pytest.mark.asyncio
@patch("bot.extensions.translator_cog.Translator")
async def test_translator__with_invalid_input__expect_error(
mock_translator, translator_cog, mock_ctx
):
instance = mock_translator.return_value
instance.translate.side_effect = ValueError("invalid language code")

with pytest.raises(ValueError, match="invalid language code"):
await translator_cog.translator(
translator_cog,
mock_ctx,
sentence="Funny",
translate_from="1234",
translate_into="1234",
)

mock_ctx.defer.assert_awaited_once()
mock_ctx.send.assert_not_called()


@pytest.mark.asyncio
async def test_translator_error_handler__expect_error(translator_cog, mock_ctx):
error = CommandInvokeError(ValueError("Error"))

await translator_cog.translator_error(mock_ctx, error)

mock_ctx.defer.assert_not_called()
mock_ctx.send.assert_called_once_with(
"Please enter a valid language code.", ephemeral=True
)


@pytest.mark.asyncio
async def test_language_autocomplete__with_invalid_input__expect_empty_list():
invalid = await language_autocomplete(None, "1232")
assert invalid == []


@pytest.mark.asyncio
@pytest.mark.parametrize(
"input_str",
["ara", "Ara", "ARA"],
)
async def test_language_autocomplete__with_partial_input__expect_matching_output(
input_str,
):
ara = await language_autocomplete(None, input_str)

assert ara == [
Choice(name="Arabic", value="arabic"),
Choice(name="Gujarati", value="gujarati"),
Choice(name="Marathi", value="marathi"),
]


@pytest.mark.asyncio
async def test_language_autocomplete__with_partial_input__expect_last_language():
zu = await language_autocomplete(None, "zu")

assert zu == [Choice(name="Zulu", value="zulu")]


@pytest.mark.asyncio
async def test_language_autocomplete__with_no_input__expect_first_25_languages():
empty = await language_autocomplete(None, "")
assert len(empty) == 25
assert empty == [
Choice(name="Afrikaans", value="afrikaans"),
Choice(name="Albanian", value="albanian"),
Choice(name="Amharic", value="amharic"),
Choice(name="Arabic", value="arabic"),
Choice(name="Armenian", value="armenian"),
Choice(name="Azerbaijani", value="azerbaijani"),
Choice(name="Basque", value="basque"),
Choice(name="Belarusian", value="belarusian"),
Choice(name="Bengali", value="bengali"),
Choice(name="Bosnian", value="bosnian"),
Choice(name="Bulgarian", value="bulgarian"),
Choice(name="Catalan", value="catalan"),
Choice(name="Cebuano", value="cebuano"),
Choice(name="Chichewa", value="chichewa"),
Choice(name="Chinese (simplified)", value="chinese (simplified)"),
Choice(name="Chinese (traditional)", value="chinese (traditional)"),
Choice(name="Corsican", value="corsican"),
Choice(name="Croatian", value="croatian"),
Choice(name="Czech", value="czech"),
Choice(name="Danish", value="danish"),
Choice(name="Dutch", value="dutch"),
Choice(name="English", value="english"),
Choice(name="Esperanto", value="esperanto"),
Choice(name="Estonian", value="estonian"),
Choice(name="Filipino", value="filipino"),
]
Loading