// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/base/template_expressions.h" #include "base/test/gtest_util.h" #include "testing/gtest/include/gtest/gtest.h" namespace ui { // Tip: ui_base_unittests --gtest_filter='TemplateExpressionsTest.*' to run // these tests. TEST(TemplateExpressionsTest, ReplaceTemplateExpressionsPieces) { TemplateReplacements substitutions; substitutions["test"] = "word"; substitutions["5"] = "number"; EXPECT_EQ("", ReplaceTemplateExpressions("", substitutions)); EXPECT_EQ("word", ReplaceTemplateExpressions("$i18n{test}", substitutions)); EXPECT_EQ("number ", ReplaceTemplateExpressions("$i18n{5} ", substitutions)); EXPECT_EQ("multiple: word, number.", ReplaceTemplateExpressions("multiple: $i18n{test}, $i18n{5}.", substitutions)); } TEST(TemplateExpressionsTest, ReplaceTemplateExpressionsConsecutiveDollarSignsPieces) { TemplateReplacements substitutions; substitutions["a"] = "x"; EXPECT_EQ("$ $$ $$$", ReplaceTemplateExpressions("$ $$ $$$", substitutions)); EXPECT_EQ("$x", ReplaceTemplateExpressions("$$i18n{a}", substitutions)); EXPECT_EQ("$$x", ReplaceTemplateExpressions("$$$i18n{a}", substitutions)); EXPECT_EQ("$i1812", ReplaceTemplateExpressions("$i1812", substitutions)); } TEST(TemplateExpressionsTest, ReplaceTemplateExpressionsEscaping) { static TemplateReplacements substitutions; substitutions["punctuationSample"] = "a\"b'ce&f"; substitutions["htmlSample"] = "
hello
"; EXPECT_EQ( "a"b'c<d>e&f", ReplaceTemplateExpressions("$i18n{punctuationSample}", substitutions)); EXPECT_EQ("<div>hello</div>", ReplaceTemplateExpressions("$i18n{htmlSample}", substitutions)); EXPECT_EQ( "multiple: <div>hello</div>, a"b'c<d>e&f.", ReplaceTemplateExpressions( "multiple: $i18n{htmlSample}, $i18n{punctuationSample}.", substitutions)); } TEST(TemplateExpressionsTest, ReplaceTemplateExpressionsRaw) { static TemplateReplacements substitutions; substitutions["rawSample"] = "hello"; EXPECT_EQ("hello", ReplaceTemplateExpressions("$i18nRaw{rawSample}", substitutions)); } TEST(TemplateExpressionsTest, ReplaceTemplateExpressionsSkipPlaceholderCheck) { static TemplateReplacements substitutions; substitutions["rawSample"] = "$1"; // Skips DCHECK if |skip_unexpected_placeholder_check| is true. ReplaceTemplateExpressions("$i18nRaw{rawSample}", substitutions, /* skip_unexpected_placeholder_check= */ true); EXPECT_DCHECK_DEATH(ReplaceTemplateExpressions( "$i18nRaw{rawSample}", substitutions, /* skip_unexpected_placeholder_check= */ false)); // |skip_unexpected_placeholder_check|'s default value is false. EXPECT_DCHECK_DEATH( ReplaceTemplateExpressions("$i18nRaw{rawSample}", substitutions)); } TEST(TemplateExpressionsTest, ReplaceTemplateExpressionsPolymerQuoting) { static TemplateReplacements substitutions; substitutions["singleSample"] = "don't do it"; substitutions["doubleSample"] = "\"moo\" said the cow"; // This resolves |Call('don\'t do it')| to Polymer, which is presented as // |don't do it| to the user. EXPECT_EQ("
[[Call('don\\'t do it')]]", ReplaceTemplateExpressions( "
[[Call('$i18nPolymer{singleSample}')]]", substitutions)); // This resolves |Call('\"moo\" said the cow')| to Polymer, which is // presented as |"moo" said the cow| to the user. EXPECT_EQ("
[[Call('"moo" said the cow')]]", ReplaceTemplateExpressions( "
[[Call('$i18nPolymer{doubleSample}')]]", substitutions)); } TEST(TemplateExpressionsTest, ReplaceTemplateExpressionsPolymerMixed) { static TemplateReplacements substitutions; substitutions["punctuationSample"] = R"(a"b'ce&f,g)"; substitutions["htmlSample"] = "
hello
"; EXPECT_EQ(R"(a"b\'ce&f\,g)", ReplaceTemplateExpressions("$i18nPolymer{punctuationSample}", substitutions)); EXPECT_EQ("
hello
", ReplaceTemplateExpressions( "$i18nPolymer{htmlSample}", substitutions)); EXPECT_EQ(R"(multiple:
hello
, a"b\'ce&f\,g.)", ReplaceTemplateExpressions("multiple: $i18nPolymer{htmlSample}, " "$i18nPolymer{punctuationSample}.", substitutions)); } struct TestCase { const char* js_in; const char* expected_out; }; TEST(TemplateExpressionsTest, JSNoReplacementOutsideTemplate) { TemplateReplacements substitutions; substitutions["test"] = "word"; substitutions["5"] = "number"; const TestCase kTestCases[] = { // No substitutions should occur in normal JS code. {"console.log('hello world');", "console.log('hello world');"}, // Has HTML content but nothing to substitute. {"Polymer({\n" " _template: html`\n" " \n" "`,\n" " is: 'foo-element',\n" " onClick_: function() { console.log('hello'); },\n" "});", "Polymer({\n" " _template: html`\n" " \n" "`,\n" " is: 'foo-element',\n" " onClick_: function() { console.log('hello'); },\n" "});"}, // Basic substitution with template on 1 line. {"Polymer({\n" " _template: html`\n" "
$i18n{test}
`,\n" " is: 'foo-element',\n" "});", "Polymer({\n" " _template: html`\n" "
word
`,\n" " is: 'foo-element',\n" "});"}, // No replacement if start/end comments are missing. {"Polymer({\n" " _template: html`
$i18n{test}
`,\n" " is: 'foo-element',\n" "});", "Polymer({\n" " _template: html`
$i18n{test}
`,\n" " is: 'foo-element',\n" "});"}, // No replacement outside start/end comments, even if inside the HTML // _template string. {"Polymer({\n" " _template: $i18n{test} html`$i18n{5}" "
Hello
$i18n{test}`,\n" " is: 'foo-element',\n" "});", "Polymer({\n" " _template: $i18n{test} html`$i18n{5}" "
Hello
$i18n{test}`,\n" " is: 'foo-element',\n" "});"}, // Test case in which only the first $i18n{...} should be substituted, // since the second is not in the HTML template string. {"Polymer({\n" " _template: html`\n" "\n" " \n" "\n" " `,\n" " is: 'foo-element',\n" " onClick_: function() { console.log($i18n{5}); },\n" "});", "Polymer({\n" " _template: html`\n" "\n" " \n" "\n" " `,\n" " is: 'foo-element',\n" " onClick_: function() { console.log($i18n{5}); },\n" "});"}, // Test case with multiple valid substitutions. {"Polymer({\n" " _template: html`\n" "\n" " \n" " $i18n{5}\n" "\n" " `,\n" " is: 'foo-element',\n" " onClick_: function() { console.log('hello'); },\n" "});", "Polymer({\n" " _template: html`\n" "\n" " \n" " number\n" "\n" " `,\n" " is: 'foo-element',\n" " onClick_: function() { console.log('hello'); },\n" "});"}}; std::string formatted; for (const TestCase test_case : kTestCases) { ASSERT_TRUE(ReplaceTemplateExpressionsInJS(test_case.js_in, substitutions, &formatted)); EXPECT_EQ(test_case.expected_out, formatted); formatted.clear(); } } TEST(TemplateExpressionsTest, JSReplacementsEscape) { TemplateReplacements substitutions; substitutions["backtickSample"] = "`, attached: function() { alert(1); },_template: html`"; substitutions["dollarSignSample"] = "5$"; substitutions["punctuationSample"] = "a\"b'ce&f"; substitutions["htmlSample"] = "
hello
"; const TestCase kTestCases[] = { // Substitution with a backtick in the replacement. {"Polymer({\n" " _template: html`\n" "\n" " \n" " $i18n{backtickSample}\n" " \n" " \n" "\n" " `,\n" " is: 'foo-element',\n" " onClick_: function() { console.log('hello'); },\n" "});", "Polymer({\n" " _template: html`\n" "\n" " \n" " \\`, attached: function() { alert(1); },_template: html\\`\n" " \n" " \n" "\n" " `,\n" " is: 'foo-element',\n" " onClick_: function() { console.log('hello'); },\n" "});"}, // Backtick in one replacement, HTML escapes in other replacements. Class // based syntax. {"class FooElement extends PolymerElement {\n" " static get template() {\n" " return html`\n" " $i18n{backtickSample}\n" " \n" " \n" "
$i18n{htmlSample}
`;\n" " }\n" " static get is() {\n" " return 'foo-element';\n" " }\n" " onClick_() { console.log('hello'); }\n" "};", "class FooElement extends PolymerElement {\n" " static get template() {\n" " return html`\n" " \\`, attached: function() { alert(1); },_template: html\\`\n" " \n" " \n" "
<div>hello</div>
" "`;\n" " }\n" " static get is() {\n" " return 'foo-element';\n" " }\n" " onClick_() { console.log('hello'); }\n" "};"}, // Replacement contains a '$' that isn't accompanied by a subsequent '{', // so should be replaced correctly. {"Polymer({\n" " _template: html`\n" "\n" "
Price is: $i18n{dollarSignSample}
\n" "\n" " `,\n" " is: 'foo-element',\n" "});", "Polymer({\n" " _template: html`\n" "\n" "
Price is: 5$
\n" "\n" " `,\n" " is: 'foo-element',\n" "});"}}; std::string formatted; for (const TestCase test_case : kTestCases) { ASSERT_TRUE(ReplaceTemplateExpressionsInJS(test_case.js_in, substitutions, &formatted)); EXPECT_EQ(test_case.expected_out, formatted); formatted.clear(); } } TEST(TemplateExpressionsTest, JSReplacementsError) { TemplateReplacements substitutions; substitutions["test"] = "${foo + bar}"; substitutions["testa"] = "5$"; substitutions["testb"] = "{a + b}"; // All these cases should fail. const TestCase kTestCases[] = { // Nested templates not allowed. {"class FooElement extends PolymerElement {\n" " static get template() { return html`\n" "\n" " _template: html`\n" "\n" " Hello\n" "\n" " `,\n" "
World
\n" "\n" " `;}\n" " static get is() { return 'foo-element'; }\n" "};", ""}, // 2 starts, one end. {"Polymer({\n" " _template: html`\n" "\n" " _template: html`\n" "\n" " Hello\n" "
World
\n" "\n" " `,\n" " is: 'foo-element',\n" "});", ""}, // Replacement contains "${". {"class FooElement extends PolymerElement {\n" " static get template() { return html`\n" "\n" "
$i18n{test}
\n" "`;\n" " }\n" " static get is() { return 'foo-element'; }\n" "};", ""}, // 2 replacements, when combined, create "${". {"Polymer({\n" " _template: html`\n" "\n" "
$i18n{testa}$i18n{testb}
\n" "\n" " `,\n" " is: 'foo-element',\n" "});", ""}, // Replacement, when combined with content preceding it, creates "${". {"class FooElement extends PolymerElement {\n" " static get template() { return html`\n" "\n" "
Price is: $$i18n{testb}
\n" "\n" " `;}\n" " static get is() {return 'foo-element'; }\n" "};", ""}, // HTML _template string is not terminated. {"Polymer({\n" " _template: html`\n" "\n" "
Price is: $i18n{testa}
\n" "\n" " is: 'foo-element',\n" "});", ""}, }; std::string formatted; for (const TestCase test_case : kTestCases) { ASSERT_FALSE(ReplaceTemplateExpressionsInJS(test_case.js_in, substitutions, &formatted)); formatted.clear(); } } TEST(TemplateExpressionsTest, JSMultipleTemplates) { TemplateReplacements substitutions; substitutions["test"] = "word"; substitutions["5"] = "number"; const TestCase kTestCases[] = { // Only the second template has substitutions {"Polymer({\n" " _template: html`\n" "
Hello
\n" "`,\n" " is: 'foo-element',\n" "});" "Polymer({\n" " _template: html`\n" "
$i18n{5}$i18n{test}
\n" "`,\n" " is: 'bar-element',\n" "});", "Polymer({\n" " _template: html`\n" "
Hello
\n" "`,\n" " is: 'foo-element',\n" "});" "Polymer({\n" " _template: html`\n" "
numberword
\n" "`,\n" " is: 'bar-element',\n" "});"}, // 2 templates, both with substitutions. {"Polymer({\n" " _template: html`\n" "
$i18n{test}
\n" "`,\n" " is: 'foo-element',\n" "});" "Polymer({\n" " _template: html`\n" "
$i18n{5}
\n" "`,\n" " is: 'bar-element',\n" "});", "Polymer({\n" " _template: html`\n" "
word
\n" "`,\n" " is: 'foo-element',\n" "});" "Polymer({\n" " _template: html`\n" "
number
\n" "`,\n" " is: 'bar-element',\n" "});"}, // 2 class based templates {"class FooElement extends PolymerElement {\n" " static get template() {\n" " return html`\n" "
$i18n{test}
\n" "`;\n" " }\n" " static get is() {\n" " return 'foo-element';\n" " }\n" "};\n" "class BarElement extends PolymerElement {\n" " static get template() {\n" " return html`\n" "
$i18n{5}
\n" "`;\n" " }\n" " static get is() {\n" " return 'bar-element';\n" " }\n" "};", "class FooElement extends PolymerElement {\n" " static get template() {\n" " return html`\n" "
word
\n" "`;\n" " }\n" " static get is() {\n" " return 'foo-element';\n" " }\n" "};\n" "class BarElement extends PolymerElement {\n" " static get template() {\n" " return html`\n" "
number
\n" "`;\n" " }\n" " static get is() {\n" " return 'bar-element';\n" " }\n" "};"}, // One element has no UI. {"Polymer({\n" " _template: null,\n" " is: 'bar-element',\n" "});\n" "Polymer({\n" " _template: html`\n" "\n" " \n" "\n" " `,\n" " is: 'foo-element',\n" " onClick_: function() { console.log('hello'); },\n" "});", "Polymer({\n" " _template: null,\n" " is: 'bar-element',\n" "});\n" "Polymer({\n" " _template: html`\n" "\n" " \n" "\n" " `,\n" " is: 'foo-element',\n" " onClick_: function() { console.log('hello'); },\n" "});"}, // One element only has a template (e.g. style), with class based syntax. {"class StyleElement extends PolymerElement {\n" " static get template() {return html`\n" "
`;}\n" "};\n" "class FooElement extends PolymerElement {\n" " static get template() {\n" " return html`\n" " \n" " `;\n" " }\n" " static get is() { return 'foo-element'; }\n" " onClick_() { console.log('hello'); }\n" "};", "class StyleElement extends PolymerElement {\n" " static get template() {return html`\n" "
`;}\n" "};\n" "class FooElement extends PolymerElement {\n" " static get template() {\n" " return html`\n" " \n" " `;\n" " }\n" " static get is() { return 'foo-element'; }\n" " onClick_() { console.log('hello'); }\n" "};"}, // 2 minified templates, both with substitutions. {"Polymer({_template:html`
$i18n{test}" "
`,is:'foo-element',}); " "Polymer({_template:html`
$i18n{5}" "
`,is:'bar-element',});", "Polymer({_template:html`
word
" "`,is:'foo-element',}); " "Polymer({_template:html`
number
" "`,is:'bar-element',});"}, {"class FooElement extends PolymerElement {" "static get template(){return html`" "
$i18n{test}
`;} " "static get is(){return 'foo-element';}};\n" "Polymer({_template:html`
$i18n{5}" "
`,is:'bar-element',});", "class FooElement extends PolymerElement {" "static get template(){return html`" "
word
`;} " "static get is(){return 'foo-element';}};\n" "Polymer({_template:html`
number
" "`,is:'bar-element',});"}}; std::string formatted; for (const TestCase test_case : kTestCases) { ASSERT_TRUE(ReplaceTemplateExpressionsInJS(test_case.js_in, substitutions, &formatted)); EXPECT_EQ(test_case.expected_out, formatted); formatted.clear(); } } TEST(TemplateExpressionsTest, ReplaceTemplateExpressionsPolymerQuotingJS) { static TemplateReplacements substitutions; substitutions["quotesAndCommas"] = R"(don't "moo", they said)"; substitutions["backslashes"] = R"(\ \\ \n \r \t \b \f \0)"; const TestCase kTestCases[] = { // Case: quotesAndCommas {R"(
[[Call('$i18nPolymer{quotesAndCommas}')]]
)", R"(
[[Call('don\\'t "moo"\\, they said')]]
)"}, // Case: backslashes {R"(
[[Call('$i18nPolymer{backslashes}')]]
)", R"(
[[Call('\\\\ \\\\\\\\ \\\\n \\\\r \\\\t \\\\b \\\\f \\\\0')]]
)"}, }; std::string formatted; for (const TestCase test_case : kTestCases) { ASSERT_TRUE(ReplaceTemplateExpressionsInJS(test_case.js_in, substitutions, &formatted)); EXPECT_EQ(test_case.expected_out, formatted); formatted.clear(); } } } // namespace ui