| // 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'c<d>e&f"; |
| substitutions["htmlSample"] = "<div>hello</div>"; |
| 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"] = "<a href=\"example.com\">hello</a>"; |
| EXPECT_EQ("<a href=\"example.com\">hello</a>", |
| 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("<div>[[Call('don\\'t do it')]]", |
| ReplaceTemplateExpressions( |
| "<div>[[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("<div>[[Call('"moo" said the cow')]]", |
| ReplaceTemplateExpressions( |
| "<div>[[Call('$i18nPolymer{doubleSample}')]]", substitutions)); |
| } |
| |
| TEST(TemplateExpressionsTest, ReplaceTemplateExpressionsPolymerMixed) { |
| static TemplateReplacements substitutions; |
| substitutions["punctuationSample"] = "a\"b'c<d>e&f,g"; |
| substitutions["htmlSample"] = "<div>hello</div>"; |
| EXPECT_EQ("a"b\\'c<d>e&f\\\\,g", |
| ReplaceTemplateExpressions("$i18nPolymer{punctuationSample}", |
| substitutions)); |
| EXPECT_EQ("<div>hello</div>", ReplaceTemplateExpressions( |
| "$i18nPolymer{htmlSample}", substitutions)); |
| EXPECT_EQ("multiple: <div>hello</div>, a"b\\'c<d>e&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`<!--_html_template_start_-->\n" |
| " <button on-click=\"onClick_\">Button Name</button>\n" |
| "<!--_html_template_end_-->`,\n" |
| " is: 'foo-element',\n" |
| " onClick_: function() { console.log('hello'); },\n" |
| "});", |
| "Polymer({\n" |
| " _template: html`<!--_html_template_start_-->\n" |
| " <button on-click=\"onClick_\">Button Name</button>\n" |
| "<!--_html_template_end_-->`,\n" |
| " is: 'foo-element',\n" |
| " onClick_: function() { console.log('hello'); },\n" |
| "});"}, |
| // Basic substitution with template on 1 line. |
| {"Polymer({\n" |
| " _template: html`<!--_html_template_start_-->\n" |
| "<div>$i18n{test}</div><!--_html_template_end_-->`,\n" |
| " is: 'foo-element',\n" |
| "});", |
| "Polymer({\n" |
| " _template: html`<!--_html_template_start_-->\n" |
| "<div>word</div><!--_html_template_end_-->`,\n" |
| " is: 'foo-element',\n" |
| "});"}, |
| // No replacement if start/end comments are missing. |
| {"Polymer({\n" |
| " _template: html`<div>$i18n{test}</div>`,\n" |
| " is: 'foo-element',\n" |
| "});", |
| "Polymer({\n" |
| " _template: html`<div>$i18n{test}</div>`,\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}<!--_html_template_start_-->" |
| "<div>Hello</div><!--_html_template_end_-->$i18n{test}`,\n" |
| " is: 'foo-element',\n" |
| "});", |
| "Polymer({\n" |
| " _template: $i18n{test} html`$i18n{5}<!--_html_template_start_-->" |
| "<div>Hello</div><!--_html_template_end_-->$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" |
| "<!--_html_template_start_-->\n" |
| " <button on-click=\"onClick_\">$i18n{test}</button>\n" |
| "<!--_html_template_end_-->\n" |
| " `,\n" |
| " is: 'foo-element',\n" |
| " onClick_: function() { console.log($i18n{5}); },\n" |
| "});", |
| "Polymer({\n" |
| " _template: html`\n" |
| "<!--_html_template_start_-->\n" |
| " <button on-click=\"onClick_\">word</button>\n" |
| "<!--_html_template_end_-->\n" |
| " `,\n" |
| " is: 'foo-element',\n" |
| " onClick_: function() { console.log($i18n{5}); },\n" |
| "});"}, |
| // Test case with multiple valid substitutions. |
| {"Polymer({\n" |
| " _template: html`\n" |
| "<!--_html_template_start_-->\n" |
| " <button on-click=\"onClick_\">$i18n{test}</button>\n" |
| " <span>$i18n{5}</span>\n" |
| "<!--_html_template_end_-->\n" |
| " `,\n" |
| " is: 'foo-element',\n" |
| " onClick_: function() { console.log('hello'); },\n" |
| "});", |
| "Polymer({\n" |
| " _template: html`\n" |
| "<!--_html_template_start_-->\n" |
| " <button on-click=\"onClick_\">word</button>\n" |
| " <span>number</span>\n" |
| "<!--_html_template_end_-->\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'c<d>e&f"; |
| substitutions["htmlSample"] = "<div>hello</div>"; |
| |
| const TestCase kTestCases[] = { |
| // Substitution with a backtick in the replacement. |
| {"Polymer({\n" |
| " _template: html`\n" |
| "<!--_html_template_start_-->\n" |
| " <span>\n" |
| " $i18n{backtickSample}\n" |
| " </span>\n" |
| " <button on-click=\"onClick_\">Button</button>\n" |
| "<!--_html_template_end_-->\n" |
| " `,\n" |
| " is: 'foo-element',\n" |
| " onClick_: function() { console.log('hello'); },\n" |
| "});", |
| "Polymer({\n" |
| " _template: html`\n" |
| "<!--_html_template_start_-->\n" |
| " <span>\n" |
| " \\`, attached: function() { alert(1); },_template: html\\`\n" |
| " </span>\n" |
| " <button on-click=\"onClick_\">Button</button>\n" |
| "<!--_html_template_end_-->\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`<!--_html_template_start_--><span>\n" |
| " $i18n{backtickSample}\n" |
| " </span>\n" |
| " <button on-click=\"onClick_\">\n" |
| " $i18n{punctuationSample}.\n" |
| " </button>\n" |
| " <div>$i18n{htmlSample}</div><!--_html_template_end_-->`;\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`<!--_html_template_start_--><span>\n" |
| " \\`, attached: function() { alert(1); },_template: html\\`\n" |
| " </span>\n" |
| " <button on-click=\"onClick_\">\n" |
| " a"b'c<d>e&f.\n" |
| " </button>\n" |
| " <div><div>hello</div></div>" |
| "<!--_html_template_end_-->`;\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" |
| "<!--_html_template_start_-->\n" |
| " <div>Price is: $i18n{dollarSignSample}</div>\n" |
| "<!--_html_template_end_-->\n" |
| " `,\n" |
| " is: 'foo-element',\n" |
| "});", |
| "Polymer({\n" |
| " _template: html`\n" |
| "<!--_html_template_start_-->\n" |
| " <div>Price is: 5$</div>\n" |
| "<!--_html_template_end_-->\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" |
| "<!--_html_template_start_-->\n" |
| " _template: html`\n" |
| "<!--_html_template_start_-->\n" |
| " <span>Hello</span>\n" |
| "<!--_html_template_end_-->\n" |
| " `,\n" |
| " <div>World</div>\n" |
| "<!--_html_template_end_-->\n" |
| " `;}\n" |
| " static get is() { return 'foo-element'; }\n" |
| "};", |
| ""}, |
| // 2 starts, one end. |
| {"Polymer({\n" |
| " _template: html`\n" |
| "<!--_html_template_start_-->\n" |
| " _template: html`\n" |
| "<!--_html_template_start_-->\n" |
| " <span>Hello</span>\n" |
| " <div>World</div>\n" |
| "<!--_html_template_end_-->\n" |
| " `,\n" |
| " is: 'foo-element',\n" |
| "});", |
| ""}, |
| // Replacement contains "${". |
| {"class FooElement extends PolymerElement {\n" |
| " static get template() { return html`\n" |
| "<!--_html_template_start_-->\n" |
| " <div>$i18n{test}</div>\n" |
| "<!--_html_template_end_-->`;\n" |
| " }\n" |
| " static get is() { return 'foo-element'; }\n" |
| "};", |
| ""}, |
| // 2 replacements, when combined, create "${". |
| {"Polymer({\n" |
| " _template: html`\n" |
| "<!--_html_template_start_-->\n" |
| " <div>$i18n{testa}$i18n{testb}</div>\n" |
| "<!--_html_template_end_-->\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" |
| "<!--_html_template_start_-->\n" |
| " <div>Price is: $$i18n{testb}</div>\n" |
| "<!--_html_template_end_-->\n" |
| " `;}\n" |
| " static get is() {return 'foo-element'; }\n" |
| "};", |
| ""}, |
| // HTML _template string is not terminated. |
| {"Polymer({\n" |
| " _template: html`\n" |
| "<!--_html_template_start_-->\n" |
| " <div>Price is: $i18n{testa}</div>\n" |
| "<!--_html_template_not_end-->\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`<!--_html_template_start_-->\n" |
| " <div>Hello</div>\n" |
| "<!--_html_template_end_-->`,\n" |
| " is: 'foo-element',\n" |
| "});" |
| "Polymer({\n" |
| " _template: html`<!--_html_template_start_-->\n" |
| " <div>$i18n{5}$i18n{test}</div>\n" |
| "<!--_html_template_end_-->`,\n" |
| " is: 'bar-element',\n" |
| "});", |
| "Polymer({\n" |
| " _template: html`<!--_html_template_start_-->\n" |
| " <div>Hello</div>\n" |
| "<!--_html_template_end_-->`,\n" |
| " is: 'foo-element',\n" |
| "});" |
| "Polymer({\n" |
| " _template: html`<!--_html_template_start_-->\n" |
| " <div>numberword</div>\n" |
| "<!--_html_template_end_-->`,\n" |
| " is: 'bar-element',\n" |
| "});"}, |
| // 2 templates, both with substitutions. |
| {"Polymer({\n" |
| " _template: html`<!--_html_template_start_-->\n" |
| " <div>$i18n{test}</div>\n" |
| "<!--_html_template_end_-->`,\n" |
| " is: 'foo-element',\n" |
| "});" |
| "Polymer({\n" |
| " _template: html`<!--_html_template_start_-->\n" |
| " <div>$i18n{5}</div>\n" |
| "<!--_html_template_end_-->`,\n" |
| " is: 'bar-element',\n" |
| "});", |
| "Polymer({\n" |
| " _template: html`<!--_html_template_start_-->\n" |
| " <div>word</div>\n" |
| "<!--_html_template_end_-->`,\n" |
| " is: 'foo-element',\n" |
| "});" |
| "Polymer({\n" |
| " _template: html`<!--_html_template_start_-->\n" |
| " <div>number</div>\n" |
| "<!--_html_template_end_-->`,\n" |
| " is: 'bar-element',\n" |
| "});"}, |
| // 2 class based templates |
| {"class FooElement extends PolymerElement {\n" |
| " static get template() {\n" |
| " return html`<!--_html_template_start_-->\n" |
| " <div>$i18n{test}</div>\n" |
| "<!--_html_template_end_-->`;\n" |
| " }\n" |
| " static get is() {\n" |
| " return 'foo-element';\n" |
| " }\n" |
| "};\n" |
| "class BarElement extends PolymerElement {\n" |
| " static get template() {\n" |
| " return html`<!--_html_template_start_-->\n" |
| " <div>$i18n{5}</div>\n" |
| "<!--_html_template_end_-->`;\n" |
| " }\n" |
| " static get is() {\n" |
| " return 'bar-element';\n" |
| " }\n" |
| "};", |
| "class FooElement extends PolymerElement {\n" |
| " static get template() {\n" |
| " return html`<!--_html_template_start_-->\n" |
| " <div>word</div>\n" |
| "<!--_html_template_end_-->`;\n" |
| " }\n" |
| " static get is() {\n" |
| " return 'foo-element';\n" |
| " }\n" |
| "};\n" |
| "class BarElement extends PolymerElement {\n" |
| " static get template() {\n" |
| " return html`<!--_html_template_start_-->\n" |
| " <div>number</div>\n" |
| "<!--_html_template_end_-->`;\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" |
| "<!--_html_template_start_-->\n" |
| " <button on-click=\"onClick_\">$i18n{test}</button>\n" |
| "<!--_html_template_end_-->\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" |
| "<!--_html_template_start_-->\n" |
| " <button on-click=\"onClick_\">word</button>\n" |
| "<!--_html_template_end_-->\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`<!--_html_template_start_-->\n" |
| "<div></div><!--_html_template_end_-->`;}\n" |
| "};\n" |
| "class FooElement extends PolymerElement {\n" |
| " static get template() {\n" |
| " return html`<!--_html_template_start_-->\n" |
| " <button on-click=\"onClick_\">$i18n{test}</button>\n" |
| " <!--_html_template_end_-->`;\n" |
| " }\n" |
| " static get is() { return 'foo-element'; }\n" |
| " onClick_() { console.log('hello'); }\n" |
| "};", |
| "class StyleElement extends PolymerElement {\n" |
| " static get template() {return html`<!--_html_template_start_-->\n" |
| "<div></div><!--_html_template_end_-->`;}\n" |
| "};\n" |
| "class FooElement extends PolymerElement {\n" |
| " static get template() {\n" |
| " return html`<!--_html_template_start_-->\n" |
| " <button on-click=\"onClick_\">word</button>\n" |
| " <!--_html_template_end_-->`;\n" |
| " }\n" |
| " static get is() { return 'foo-element'; }\n" |
| " onClick_() { console.log('hello'); }\n" |
| "};"}, |
| // 2 minified templates, both with substitutions. |
| {"Polymer({_template:html`<!--_html_template_start_--><div>$i18n{test}" |
| "</div><!--_html_template_end_-->`,is:'foo-element',}); " |
| "Polymer({_template:html`<!--_html_template_start_--><div>$i18n{5}" |
| "</div><!--_html_template_end_-->`,is:'bar-element',});", |
| "Polymer({_template:html`<!--_html_template_start_--><div>word</div>" |
| "<!--_html_template_end_-->`,is:'foo-element',}); " |
| "Polymer({_template:html`<!--_html_template_start_--><div>number</div>" |
| "<!--_html_template_end_-->`,is:'bar-element',});"}, |
| {"class FooElement extends PolymerElement {" |
| "static get template(){return html`<!--_html_template_start_-->" |
| "<div>$i18n{test}</div><!--_html_template_end_-->`;} " |
| "static get is(){return 'foo-element';}};\n" |
| "Polymer({_template:html`<!--_html_template_start_--><div>$i18n{5}" |
| "</div><!--_html_template_end_-->`,is:'bar-element',});", |
| "class FooElement extends PolymerElement {" |
| "static get template(){return html`<!--_html_template_start_-->" |
| "<div>word</div><!--_html_template_end_-->`;} " |
| "static get is(){return 'foo-element';}};\n" |
| "Polymer({_template:html`<!--_html_template_start_--><div>number</div>" |
| "<!--_html_template_end_-->`,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(); |
| } |
| } |
| |
| } // namespace ui |