using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Collections.Generic; using System.Globalization; namespace mustache.test { /// /// Tests the Formatter class. /// [TestClass] public class FormatterTester { #region Real World Example /// /// The Formatter class is especially useful when performing simple mail merge operations. /// Like String.Format, Formatter will substitute placeholders for actual values. In the case /// of Formatter, placeholders are indicated by name, rather than index and are wrapped with /// double curly braces: {{name}}. The name within the curly brace can include any characters, /// including whitespace, except for two or more adjacent right curly braces (}}). /// [TestMethod] public void TestFormatter_ReplaceNamedPlaceholdersWithFormats() { const string format = "Hello {{name}}! It is {{date:MM-dd-yyyy}}. You make {{income:C}} an hour."; Formatter formatter = new Formatter(format); string result1 = formatter.Format(new Dictionary() { { "name", "Bob" }, { "date", new DateTime(2012, 03, 11) }, { "income", 32.8 } }); Assert.AreEqual("Hello Bob! It is 03-11-2012. You make $32.80 an hour.", result1); } /// /// If we want to work with objects, rather than raw dictionaries, we can wrap the objects with /// property dictionaries. /// [TestMethod] public void TestFormatter_UseObject() { var person = new { Name = "Bob", Date = new DateTime(2012, 03, 11), Income = 32.8 }; const string format = "Hello {{Name}}! It is {{Date:MM-dd-yyyy}}. You make {{Income:C}} an hour."; Formatter formatter = new Formatter(format); string result1 = formatter.Format(person); Assert.AreEqual("Hello Bob! It is 03-11-2012. You make $32.80 an hour.", result1); } /// /// We can the Formatter to print out a list of items following a format. /// [TestMethod] public void TestFormatter_PrintList() { List values = new List() { 0, 1, 2, 3, 4 }; const string format = "{{#each this}}{{this}} {{/each}}"; Formatter formatter = new Formatter(format); string result = formatter.Format(values); Assert.AreEqual("0 1 2 3 4 ", result); } /// /// We can include some text conditionally. /// [TestMethod] public void TestFormatter_ConditionallyIncludeText() { Random random = new Random(); int value = random.Next(); bool isEven = value % 2 == 0; var data = new { Value = value, IsEven = isEven, }; const string format = "{{Value}} {{#if IsEven}}is even{{#else}}is odd{{/if}}."; Formatter formatter = new Formatter(format); string result = formatter.Format(data); string expected = String.Format("{0}", value) + (isEven ? " is even." : " is odd."); Assert.AreEqual(expected, result); } /// /// Multiple cases can be handled using if/elif/else. /// [TestMethod] public void TestFormatter_HandleCases() { const string format = @"{{#if No}}No{{#elif Yes}}Yes{{#else}}Maybe{{/if}}"; Formatter formatter = new Formatter(format); var data = new { Yes = true, No = false, }; string result = formatter.Format(data); Assert.AreEqual("Yes", result); } /// /// We should be able to combine tags anyway we want. /// [TestMethod] public void TestFormatter_Compound() { const string format = @"{{#with Customer}} Hello{{#if FirstName}} {{FirstName}}{{/if}}: {{/with}} {{#! We only want to print out purchases if they have some. }} {{#if Purchases}} You recently purchased: {{#each Purchases}} {{Name}}: {{Quantity}} x {{Price:C}} {{/each}} Your total was: {{Total:C}} {{/if}} We thought you might be interested in buying: {{PromotionProduct}}. Thank you, {{#with Agent}} {{Name}} {{/with}}"; Formatter formatter = new Formatter(format); var data = new { Customer = new { FirstName = "Bob", }, Purchases = new object[] { new { Name = "Donkey", Quantity = 8, Price = 1.23m, }, new { Name = "Hammer", Quantity = 1, Price = 8.32m, }, }, Total = 18.16m, PromotionProduct = "Sneakers", Agent = new { Name = "Tom", }, }; string result = formatter.Format(data); Assert.AreEqual(@"Hello Bob: You recently purchased: Donkey: 8 x $1.23 Hammer: 1 x $8.32 Your total was: $18.16 We thought you might be interested in buying: Sneakers. Thank you, Tom ", result); } #endregion #region Argument Checking /// /// An exception should be thrown if the format string is null. /// [TestMethod] [ExpectedException(typeof(ArgumentNullException))] public void TestCtor_NullFormat_ThrowsException() { string format = null; new Formatter(format); } /// /// If we try to replace a placeholder that we do not have a lookup key for, /// an exception should be thrown. /// [TestMethod] [ExpectedException(typeof(KeyNotFoundException))] public void TestFormat_MissingKey_ThrowsException() { Formatter formatter = new Formatter("{{unknown}}"); IDictionary lookup = new Dictionary(); formatter.Format(lookup); } /// /// A format exception should be thrown if there is not a matching closing if tag. /// [TestMethod] [ExpectedException(typeof(FormatException))] public void TestFormat_MissingClosingIfTag_ThrowsException() { new Formatter("{{#if Bob}}Hello"); } /// /// A format exception should be thrown if the matching closing tag is wrong. /// [TestMethod] [ExpectedException(typeof(FormatException))] public void TestFormat_WrongClosingIfTag_ThrowsException() { new Formatter("{{#with this}}{{#if Bob}}Hello{{/with}}{{/if}}"); } #endregion /// /// If we specify a right alignment, the output should be aligned to the right. /// [TestMethod] public void TestFormatter_WithRightAlignment_AlignsToRight() { string format = "{{Name,10}}"; var instance = new { Name = "Bob" }; PropertyDictionary dictionary = new PropertyDictionary(instance); string result = Formatter.Format(format, dictionary); Assert.AreEqual(" Bob", result, "The text was not aligned."); } /// /// If we specify a left alignment, the output should be aligned to the left. /// [TestMethod] public void TestFormatter_WithLeftAlignment_AlignsToLeft() { string format = "{{Name,-10}}"; var instance = new { Name = "Bob" }; PropertyDictionary dictionary = new PropertyDictionary(instance); string result = Formatter.Format(null, format, dictionary); Assert.AreEqual("Bob ", result, "The text was not aligned."); } /// /// If we try to format an empty string, an empty string should be returned. /// [TestMethod] public void TestFormatter_EmptyFormat_ReturnsEmpty() { Formatter formatter = new Formatter(String.Empty); Dictionary lookup = new Dictionary(); string result = formatter.Format(lookup); Assert.AreEqual(String.Empty, result, "The result should have been empty."); } /// /// If our format string is just a placeholder, than just the replacement value should be returned. /// [TestMethod] public void TestFormatter_FormatIsSinglePlaceholder_ReturnsReplaced() { Formatter formatter = new Formatter("{{name}}"); Dictionary lookup = new Dictionary() { { "name", "test" } }; string result = formatter.Format(lookup); Assert.AreEqual("test", result, "The result was wrong."); } /// /// We should be able to put just about anything inside of a placeholder, but it will /// not be treated like a placeholder. /// [TestMethod] public void TestFormatter_PlaceholderContainsSpecialCharacters_ReturnsUnreplaced() { Formatter formatter = new Formatter("{{ \\_@#$%^ }1233 abc}}"); Dictionary lookup = new Dictionary() { { " \\_@#$%^ }1233 abc", "test" } }; string result = formatter.Format(lookup); Assert.AreEqual("{{ \\_@#$%^ }1233 abc}}", result, "The result was wrong."); } /// /// If a lookup value is null, it should be replaced with an empty string. /// [TestMethod] public void TestFormatter_NullValue_ReplacesWithBlank() { Formatter formatter = new Formatter("These quotes should be empty '{{name}}'."); Dictionary lookup = new Dictionary() { { "name", null } }; string result = formatter.Format(lookup); Assert.AreEqual("These quotes should be empty ''.", result, "The result was wrong."); } /// /// If a replacement value contains a placeholder, it should NOT be evaluated. /// [TestMethod] public void TestFormatter_ReplacementContainsPlaceholder_IgnoresPlaceholder() { Formatter formatter = new Formatter("The length of {{name}} is {{length}}."); Dictionary lookup = new Dictionary() { { "name", "Bob" }, { "length", "{{name}}" } }; string result = formatter.Format(lookup); Assert.AreEqual("The length of Bob is {{name}}.", result, "The result was wrong."); } /// /// If we pass null to as the format provider to the Format function, /// the current culture is used. /// [TestMethod] public void TestFormatter_NullFormatter_UsesCurrentCulture() { string format = "{0:C}"; Formatter formatter = new Formatter("{" + format + "}"); string result = formatter.Format((IFormatProvider)null, new Dictionary() { { "0", 28.30m } }); string expected = String.Format(CultureInfo.CurrentCulture, format, 28.30m); Assert.AreEqual(expected, result, "The wrong format provider was used."); } /// /// If we put a tag on a line by itself, it shouldn't result in any whitespace. /// [TestMethod] public void TestFormatter_TagOnLineByItself_NoNewlineGenerated() { const string format = @"Hello {{#if Name}} {{Name}} {{/if}} Goodbye "; var data = new { Name = "George" }; Formatter formatter = new Formatter(format); string result = formatter.Format(data); const string expected = @"Hello George Goodbye "; Assert.AreEqual(expected, result); } /// /// If a key is not found at the current level, it is looked for at the parent level. /// [TestMethod] public void TestFormatter_NameAtHigherScope_Finds() { const string format = "{{#with Child}}{{TopLevel}} and {{ChildLevel}}{{/with}}"; Formatter formatter = new Formatter(format); var data = new { TopLevel = "Parent", Child = new { ChildLevel = "Child" }, }; string result = formatter.Format(data); Assert.AreEqual("Parent and Child", result); } /// /// Null values are considered false by if statements. /// [TestMethod] public void TestFormatter_ConditionOnNull_ConsideredFalse() { const string format = "{{#if this}}Bad{{#else}}Good{{/if}}"; Formatter formatter = new Formatter(format); string result = formatter.Format(null); Assert.AreEqual("Good", result); } /// /// Empty collections are considered false by if statements. /// [TestMethod] public void TestFormatter_ConditionOnEmptyCollection_ConsideredFalse() { const string format = "{{#if this}}Bad{{#else}}Good{{/if}}"; Formatter formatter = new Formatter(format); string result = formatter.Format(new object[0]); Assert.AreEqual("Good", result); } /// /// Non-empty collections are considered true by if statements. /// [TestMethod] public void TestFormatter_ConditionOnNonEmptyCollection_ConsideredTrue() { const string format = "{{#if this}}Good{{#else}}Bad{{/if}}"; Formatter formatter = new Formatter(format); string result = formatter.Format(new object[1]); Assert.AreEqual("Good", result); } /// /// Null-char is considered false by if statements. /// [TestMethod] public void TestFormatter_ConditionOnNullChar_ConsideredFalse() { const string format = "{{#if this}}Bad{{#else}}Good{{/if}}"; Formatter formatter = new Formatter(format); string result = formatter.Format('\0'); Assert.AreEqual("Good", result); } /// /// Zero is considered false by if statements. /// [TestMethod] public void TestFormatter_ConditionOnZero_ConsideredFalse() { const string format = "{{#if this}}Bad{{#else}}Good{{/if}}"; Formatter formatter = new Formatter(format); int? value = 0; string result = formatter.Format(value); Assert.AreEqual("Good", result); } /// /// Everything else is considered true by if statements. /// [TestMethod] public void TestFormatter_ConditionOnDateTime_ConsideredTrue() { const string format = "{{#if this}}Good{{#else}}Bad{{/if}}"; Formatter formatter = new Formatter(format); string result = formatter.Format(DateTime.Now); Assert.AreEqual("Good", result); } /// /// Instead of requiring deeply nested "with" statements, members /// can be separated by dots. /// [TestMethod] public void TestFormatter_NestedMembers_SearchesMembers() { const string format = "{{Customer.Name}}"; Formatter formatter = new Formatter(format); var data = new { Customer = new { Name = "Bob" } }; string result = formatter.Format(data); Assert.AreEqual("Bob", result); } /// /// Keys should cause newlines to be respected, since they are considered content. /// [TestMethod] public void TestFormatter_KeyBetweenTags_RespectsTrailingNewline() { string format = "{{#each this}}{{this}} {{/each}}" + Environment.NewLine; Formatter formatter = new Formatter(format); string result = formatter.Format("Hello"); Assert.AreEqual("H e l l o " + Environment.NewLine, result); } /// /// If someone tries to loop on a non-enumerable, it should do nothing. /// [TestMethod] public void TestFormatter_EachOnNonEnumerable_PrintsNothing() { const string format = "{{#each this}}Bad{{/each}}"; Formatter formatter = new Formatter(format); string result = formatter.Format(123); Assert.AreEqual(String.Empty, result); } /// /// If a tag header is on the same line as it's footer, the new-line should not be removed. /// [TestMethod] public void TestFormatter_InlineTags_RespectNewLine() { const string format = @"{{#if this}}{{/if}} "; Formatter formatter = new Formatter(format); string result = formatter.Format(true); Assert.AreEqual(Environment.NewLine, result); } /// /// If a tag header is on the same line as it's footer, the new-line should not be removed. /// [TestMethod] public void TestFormatter_TagFooterFollowedByTagHeader_RemovesNewLine() { const string format = @"{{#if this}} {{/if}}{{#if this}} Hello{{/if}}"; Formatter formatter = new Formatter(format); string result = formatter.Format(true); Assert.AreEqual("Hello", result); } } }