From f5063cf81e68d142a5af6209ef80fd29fd80fce2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 May 2017 17:00:23 -0400 Subject: [PATCH] add translation unit tests (#296) --- .../ModResolverTests.cs | 1 + .../StardewModdingAPI.Tests.csproj | 2 + .../TranslationTests.cs | 333 ++++++++++++++++++ src/StardewModdingAPI/Translation.cs | 7 +- src/crossplatform.targets | 1 - 5 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 src/StardewModdingAPI.Tests/TranslationTests.cs diff --git a/src/StardewModdingAPI.Tests/ModResolverTests.cs b/src/StardewModdingAPI.Tests/ModResolverTests.cs index c0545cd6..46c1c0cf 100644 --- a/src/StardewModdingAPI.Tests/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/ModResolverTests.cs @@ -13,6 +13,7 @@ using StardewModdingAPI.Tests.Framework; namespace StardewModdingAPI.Tests { + /// Unit tests for . [TestFixture] public class ModResolverTests { diff --git a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj b/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj index c84adbd7..3818ec9c 100644 --- a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj @@ -48,6 +48,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -61,5 +62,6 @@ StardewModdingAPI + \ No newline at end of file diff --git a/src/StardewModdingAPI.Tests/TranslationTests.cs b/src/StardewModdingAPI.Tests/TranslationTests.cs new file mode 100644 index 00000000..3e6e6499 --- /dev/null +++ b/src/StardewModdingAPI.Tests/TranslationTests.cs @@ -0,0 +1,333 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using StardewModdingAPI.Framework; +using StardewValley; + +namespace StardewModdingAPI.Tests +{ + /// Unit tests for and . + [TestFixture] + public class TranslationTests + { + /********* + ** Data + *********/ + /// Sample translation text for unit tests. + public static string[] Samples = { null, "", " ", "boop", " boop " }; + + /// A token structure type. + public enum TokenType + { + /// The tokens are passed in a dictionary. + Dictionary, + + /// The tokens are passed in an anonymous object. + AnonymousObject + } + + + /********* + ** Unit tests + *********/ + /**** + ** Translation helper + ****/ + [Test(Description = "Assert that the translation helper correctly handles no translations.")] + public void Helper_HandlesNoTranslations() + { + // arrange + var data = new Dictionary>(); + + // act + ITranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en, data); + Translation translation = helper.Translate("key"); + + // assert + Assert.AreEqual("en", helper.Locale, "The locale doesn't match the input value."); + Assert.AreEqual(LocalizedContentManager.LanguageCode.en, helper.LocaleEnum, "The locale enum doesn't match the input value."); + Assert.IsNotNull(helper.GetTranslations(), "The full list of translations is unexpectedly null."); + Assert.AreEqual(0, helper.GetTranslations().Count, "The full list of translations is unexpectedly not empty."); + + Assert.IsNotNull(translation, "The translation helper unexpectedly returned a null translation."); + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value."); + } + + [Test(Description = "Assert that the translation helper returns the expected translations correctly.")] + public void Helper_GetTranslations_ReturnsExpectedText() + { + // arrange + var data = this.GetSampleData(); + var expected = this.GetExpectedTranslations(); + + // act + var actual = new Dictionary>(); + TranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en, data); + foreach (string locale in expected.Keys) + { + this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); + actual[locale] = helper.GetTranslations(); + } + + // assert + foreach (string locale in expected.Keys) + { + Assert.IsNotNull(actual[locale], $"The translations for {locale} is unexpectedly null."); + Assert.That(actual[locale], Is.EquivalentTo(expected[locale]), $"The translations for {locale} don't match the expected values."); + } + } + + [Test(Description = "Assert that the translations returned by the helper has the expected text.")] + public void Helper_Translate_ReturnsExpectedText() + { + // arrange + var data = this.GetSampleData(); + var expected = this.GetExpectedTranslations(); + + // act + var actual = new Dictionary>(); + TranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en, data); + foreach (string locale in expected.Keys) + { + this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); + actual[locale] = new Dictionary(); + foreach (string key in expected[locale].Keys) + actual[locale][key] = helper.Translate(key); + } + + // assert + foreach (string locale in expected.Keys) + { + Assert.IsNotNull(actual[locale], $"The translations for {locale} is unexpectedly null."); + Assert.That(actual[locale], Is.EquivalentTo(expected[locale]), $"The translations for {locale} don't match the expected values."); + } + } + + /**** + ** Translation + ****/ + [Test(Description = "Assert that HasValue returns the expected result for various inputs.")] + [TestCase(null, ExpectedResult = false)] + [TestCase("", ExpectedResult = false)] + [TestCase(" ", ExpectedResult = true)] + [TestCase("boop", ExpectedResult = true)] + [TestCase(" boop ", ExpectedResult = true)] + public bool Translation_HasValue(string text) + { + return new Translation("ModName", "pt-BR", "key", text).HasValue(); + } + + [Test(Description = "Assert that the translation's ToString method returns the expected text for various inputs.")] + public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text); + + // assert + if (translation.HasValue()) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid input."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty input."); + } + + [Test(Description = "Assert that the translation's implicit string conversion returns the expected text for various inputs.")] + public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text); + + // assert + if (translation.HasValue()) + Assert.AreEqual(text, (string)translation, "The translation returned an unexpected value given a valid input."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), (string)translation, "The translation returned an unexpected value given a null or empty input."); + } + + [Test(Description = "Assert that the translation returns the expected text given a use-placeholder setting.")] + public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).UsePlaceholder(value); + + // assert + if (translation.HasValue()) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid input."); + else if (!value) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a null or empty input with the placeholder disabled."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty input with the placeholder enabled."); + } + + [Test(Description = "Assert that the translation's Assert method throws the expected exception.")] + public void Translation_Assert([ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text); + + // assert + if (translation.HasValue()) + Assert.That(() => translation.Assert(), Throws.Nothing, "The assert unexpected threw an exception for a valid input."); + else + Assert.That(() => translation.Assert(), Throws.Exception.TypeOf(), "The assert didn't throw an exception for invalid input."); + } + + [Test(Description = "Assert that the translation returns the expected text after setting the default.")] + public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string text, [ValueSource(nameof(TranslationTests.Samples))] string @default) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).Default(@default); + + // assert + if (!string.IsNullOrEmpty(text)) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid base text."); + else if (!string.IsNullOrEmpty(@default)) + Assert.AreEqual(@default, translation.ToString(), "The translation returned an unexpected value given a null or empty base text, but valid default."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty base and default text."); + } + + /**** + ** Translation tokens + ****/ + [Test(Description = "Assert that multiple translation tokens are replaced correctly regardless of the token structure.")] + public void Translation_Tokens([Values(TokenType.AnonymousObject, TokenType.Dictionary)] TokenType tokenType) + { + // arrange + string start = Guid.NewGuid().ToString("N"); + string middle = Guid.NewGuid().ToString("N"); + string end = Guid.NewGuid().ToString("N"); + const string input = "{{start}} tokens are properly replaced (including {{middle}} {{ MIDdlE}}) {{end}}"; + string expected = $"{start} tokens are properly replaced (including {middle} {middle}) {end}"; + + // act + Translation translation = new Translation("ModName", "pt-BR", "key", input); + switch (tokenType) + { + case TokenType.AnonymousObject: + translation = translation.Tokens(new { start, middle, end }); + break; + + case TokenType.Dictionary: + translation = translation.Tokens(new Dictionary { ["start"] = start, ["middle"] = middle, ["end"] = end }); + break; + + default: + throw new NotSupportedException($"Unknown token type {tokenType}."); + } + + // assert + Assert.AreEqual(expected, translation.ToString(), "The translation returned an unexpected text."); + } + + [Test(Description = "Assert that the translation can replace tokens in all valid formats.")] + [TestCase("{{value}}", "value")] + [TestCase("{{ value }}", "value")] + [TestCase("{{value }}", "value")] + [TestCase("{{ the_value }}", "the_value")] + [TestCase("{{ the.value_here }}", "the.value_here")] + [TestCase("{{ the_value-here.... }}", "the_value-here....")] + [TestCase("{{ tHe_vALuE-HEre.... }}", "tHe_vALuE-HEre....")] + public void Translation_Tokens_ValidFormats(string text, string key) + { + // arrange + string value = Guid.NewGuid().ToString("N"); + + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).Tokens(new Dictionary { [key] = value }); + + // assert + Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text."); + } + + [Test(Description = "Assert that translation tokens are case-insensitive and surrounding-whitespace-insensitive.")] + [TestCase("{{value}}", "value")] + [TestCase("{{VaLuE}}", "vAlUe")] + [TestCase("{{VaLuE }}", " vAlUe")] + public void Translation_Tokens_KeysAreNormalised(string text, string key) + { + // arrange + string value = Guid.NewGuid().ToString("N"); + + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).Tokens(new Dictionary { [key] = value }); + + // assert + Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text."); + } + + + /********* + ** Private methods + *********/ + /// Set a translation helper's locale and assert that it was set correctly. + /// The translation helper to change. + /// The expected locale. + /// The expected game language code. + private void AssertSetLocale(TranslationHelper helper, string locale, LocalizedContentManager.LanguageCode localeEnum) + { + helper.SetLocale(locale, localeEnum); + Assert.AreEqual(locale, helper.Locale, "The locale doesn't match the input value."); + Assert.AreEqual(localeEnum, helper.LocaleEnum, "The locale enum doesn't match the input value."); + } + + /// Get sample raw translations to input. + private IDictionary> GetSampleData() + { + return new Dictionary> + { + ["default"] = new Dictionary + { + ["key A"] = "default A", + ["key C"] = "default C" + }, + ["en"] = new Dictionary + { + ["key A"] = "en A", + ["key B"] = "en B" + }, + ["en-US"] = new Dictionary(), + ["zzz"] = new Dictionary + { + ["key A"] = "zzz A" + } + }; + } + + /// Get the expected translation output given , based on the expected locale fallback. + private IDictionary> GetExpectedTranslations() + { + return new Dictionary> + { + ["default"] = new Dictionary + { + ["key A"] = "default A", + ["key C"] = "default C" + }, + ["en"] = new Dictionary + { + ["key A"] = "en A", + ["key B"] = "en B", + ["key C"] = "default C" + }, + ["en-us"] = new Dictionary + { + ["key A"] = "en A", + ["key B"] = "en B", + ["key C"] = "default C" + }, + ["zzz"] = new Dictionary + { + ["key A"] = "zzz A", + ["key C"] = "default C" + } + }; + } + + /// Get the default placeholder text when a translation is missing. + /// The translation key. + private string GetPlaceholderText(string key) + { + return string.Format(Translation.PlaceholderText, key); + } + } +} diff --git a/src/StardewModdingAPI/Translation.cs b/src/StardewModdingAPI/Translation.cs index ae4b833c..868ee4fa 100644 --- a/src/StardewModdingAPI/Translation.cs +++ b/src/StardewModdingAPI/Translation.cs @@ -11,6 +11,9 @@ namespace StardewModdingAPI /********* ** Properties *********/ + /// The placeholder text when the translation is null or empty, where {0} is the translation key. + internal const string PlaceholderText = "(no translation:{0})"; + /// The name of the relevant mod for error messages. private readonly string ModName; @@ -36,7 +39,7 @@ namespace StardewModdingAPI /// The translation key. /// The underlying translation text. internal Translation(string modName, string locale, string key, string text) - : this(modName, locale, key, text, $"(no translation:{key})") { } + : this(modName, locale, key, text, string.Format(Translation.PlaceholderText, key)) { } /// Construct an isntance. /// The name of the relevant mod for error messages. @@ -75,7 +78,7 @@ namespace StardewModdingAPI /// Whether to return a placeholder. public Translation UsePlaceholder(bool use) { - return new Translation(this.ModName, this.Locale, this.Key, this.Text, use ? $"(no translation:{this.Key})" : null); + return new Translation(this.ModName, this.Locale, this.Key, this.Text, use ? string.Format(Translation.PlaceholderText, this.Key) : null); } /// Replace tokens in the text like {{value}} with the given values. Returns a new instance. diff --git a/src/crossplatform.targets b/src/crossplatform.targets index 00b731eb..31d4722d 100644 --- a/src/crossplatform.targets +++ b/src/crossplatform.targets @@ -33,7 +33,6 @@ $(GamePath)\Stardew Valley.exe - False $(GamePath)\xTile.dll