diff --git a/bot/extensions/translator_cog.py b/bot/extensions/translator_cog.py index c807299..26f2c65 100644 --- a/bot/extensions/translator_cog.py +++ b/bot/extensions/translator_cog.py @@ -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( @@ -36,13 +36,23 @@ 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 @@ -50,17 +60,19 @@ async def translator(self, ctx: Context, *, sentence: str, translate_into: str): 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, ) diff --git a/tests/extensions/conftest.py b/tests/extensions/conftest.py new file mode 100644 index 0000000..46ac231 --- /dev/null +++ b/tests/extensions/conftest.py @@ -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 diff --git a/tests/extensions/test_reminder_cog.py b/tests/extensions/test_reminder_cog.py index bb57999..eb44dc6 100644 --- a/tests/extensions/test_reminder_cog.py +++ b/tests/extensions/test_reminder_cog.py @@ -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 @@ -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.""" diff --git a/tests/extensions/test_threads_cog.py b/tests/extensions/test_threads_cog.py index 8fddf69..c71f67e 100644 --- a/tests/extensions/test_threads_cog.py +++ b/tests/extensions/test_threads_cog.py @@ -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.""" diff --git a/tests/extensions/test_time_cog.py b/tests/extensions/test_time_cog.py index ea06e3a..e1e5f5e 100644 --- a/tests/extensions/test_time_cog.py +++ b/tests/extensions/test_time_cog.py @@ -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.""" diff --git a/tests/extensions/test_translator_cog.py b/tests/extensions/test_translator_cog.py new file mode 100644 index 0000000..f4a867e --- /dev/null +++ b/tests/extensions/test_translator_cog.py @@ -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"), + ]