summaryrefslogtreecommitdiff
path: root/dev/lib/handlebars/src/spec/index.helpers.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'dev/lib/handlebars/src/spec/index.helpers.test.ts')
-rw-r--r--dev/lib/handlebars/src/spec/index.helpers.test.ts958
1 files changed, 958 insertions, 0 deletions
diff --git a/dev/lib/handlebars/src/spec/index.helpers.test.ts b/dev/lib/handlebars/src/spec/index.helpers.test.ts
new file mode 100644
index 00000000..4cfa39bb
--- /dev/null
+++ b/dev/lib/handlebars/src/spec/index.helpers.test.ts
@@ -0,0 +1,958 @@
+/*
+ * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js),
+ * and may include modifications made by Elasticsearch B.V.
+ * Elasticsearch B.V. licenses this file to you under the MIT License.
+ * See `packages/kbn-handlebars/LICENSE` for more information.
+ */
+
+import Handlebars, { type HelperOptions } from '../..';
+import { expectTemplate } from '../__jest__/test_bench';
+
+beforeEach(() => {
+ global.kbnHandlebarsEnv = Handlebars.create();
+});
+
+afterEach(() => {
+ global.kbnHandlebarsEnv = null;
+});
+
+describe('helpers', () => {
+ it('helper with complex lookup$', () => {
+ expectTemplate('{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}')
+ .withInput({
+ prefix: '/root',
+ goodbyes: [{ text: 'Goodbye', url: 'goodbye' }],
+ })
+ .withHelper('link', function (this: any, prefix) {
+ return '<a href="' + prefix + '/' + this.url + '">' + this.text + '</a>';
+ })
+ .toCompileTo('<a href="/root/goodbye">Goodbye</a>');
+ });
+
+ it('helper for raw block gets raw content', () => {
+ expectTemplate('{{{{raw}}}} {{test}} {{{{/raw}}}}')
+ .withInput({ test: 'hello' })
+ .withHelper('raw', function (options: HelperOptions) {
+ return options.fn();
+ })
+ .toCompileTo(' {{test}} ');
+ });
+
+ it('helper for raw block gets parameters', () => {
+ expectTemplate('{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}')
+ .withInput({ test: 'hello' })
+ .withHelper('raw', function (a, b, c, options: HelperOptions) {
+ const ret = options.fn() + a + b + c;
+ return ret;
+ })
+ .toCompileTo(' {{test}} 123');
+ });
+
+ describe('raw block parsing (with identity helper-function)', () => {
+ function runWithIdentityHelper(template: string, expected: string) {
+ expectTemplate(template)
+ .withHelper('identity', function (options: HelperOptions) {
+ return options.fn();
+ })
+ .toCompileTo(expected);
+ }
+
+ it('helper for nested raw block gets raw content', () => {
+ runWithIdentityHelper(
+ '{{{{identity}}}} {{{{b}}}} {{{{/b}}}} {{{{/identity}}}}',
+ ' {{{{b}}}} {{{{/b}}}} '
+ );
+ });
+
+ it('helper for nested raw block works with empty content', () => {
+ runWithIdentityHelper('{{{{identity}}}}{{{{/identity}}}}', '');
+ });
+
+ it.skip('helper for nested raw block works if nested raw blocks are broken', () => {
+ // This test was introduced in 4.4.4, but it was not the actual problem that lead to the patch release
+ // The test is deactivated, because in 3.x this template cases an exception and it also does not work in 4.4.3
+ // If anyone can make this template work without breaking everything else, then go for it,
+ // but for now, this is just a known bug, that will be documented.
+ runWithIdentityHelper(
+ '{{{{identity}}}} {{{{a}}}} {{{{ {{{{/ }}}} }}}} {{{{/identity}}}}',
+ ' {{{{a}}}} {{{{ {{{{/ }}}} }}}} '
+ );
+ });
+
+ it('helper for nested raw block closes after first matching close', () => {
+ runWithIdentityHelper(
+ '{{{{identity}}}}abc{{{{/identity}}}} {{{{identity}}}}abc{{{{/identity}}}}',
+ 'abc abc'
+ );
+ });
+
+ it('helper for nested raw block throw exception when with missing closing braces', () => {
+ const string = '{{{{a}}}} {{{{/a';
+ expectTemplate(string).toThrow();
+ });
+ });
+
+ it('helper block with identical context', () => {
+ expectTemplate('{{#goodbyes}}{{name}}{{/goodbyes}}')
+ .withInput({ name: 'Alan' })
+ .withHelper('goodbyes', function (this: any, options: HelperOptions) {
+ let out = '';
+ const byes = ['Goodbye', 'goodbye', 'GOODBYE'];
+ for (let i = 0, j = byes.length; i < j; i++) {
+ out += byes[i] + ' ' + options.fn(this) + '! ';
+ }
+ return out;
+ })
+ .toCompileTo('Goodbye Alan! goodbye Alan! GOODBYE Alan! ');
+ });
+
+ it('helper block with complex lookup expression', () => {
+ expectTemplate('{{#goodbyes}}{{../name}}{{/goodbyes}}')
+ .withInput({ name: 'Alan' })
+ .withHelper('goodbyes', function (options: HelperOptions) {
+ let out = '';
+ const byes = ['Goodbye', 'goodbye', 'GOODBYE'];
+ for (let i = 0, j = byes.length; i < j; i++) {
+ out += byes[i] + ' ' + options.fn({}) + '! ';
+ }
+ return out;
+ })
+ .toCompileTo('Goodbye Alan! goodbye Alan! GOODBYE Alan! ');
+ });
+
+ it('helper with complex lookup and nested template', () => {
+ expectTemplate('{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}')
+ .withInput({
+ prefix: '/root',
+ goodbyes: [{ text: 'Goodbye', url: 'goodbye' }],
+ })
+ .withHelper('link', function (this: any, prefix, options: HelperOptions) {
+ return '<a href="' + prefix + '/' + this.url + '">' + options.fn(this) + '</a>';
+ })
+ .toCompileTo('<a href="/root/goodbye">Goodbye</a>');
+ });
+
+ it('helper with complex lookup and nested template in VM+Compiler', () => {
+ expectTemplate('{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}')
+ .withInput({
+ prefix: '/root',
+ goodbyes: [{ text: 'Goodbye', url: 'goodbye' }],
+ })
+ .withHelper('link', function (this: any, prefix, options: HelperOptions) {
+ return '<a href="' + prefix + '/' + this.url + '">' + options.fn(this) + '</a>';
+ })
+ .toCompileTo('<a href="/root/goodbye">Goodbye</a>');
+ });
+
+ it('helper returning undefined value', () => {
+ expectTemplate(' {{nothere}}')
+ .withHelpers({
+ nothere() {},
+ })
+ .toCompileTo(' ');
+
+ expectTemplate(' {{#nothere}}{{/nothere}}')
+ .withHelpers({
+ nothere() {},
+ })
+ .toCompileTo(' ');
+ });
+
+ it('block helper', () => {
+ expectTemplate('{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!')
+ .withInput({ world: 'world' })
+ .withHelper('goodbyes', function (options: HelperOptions) {
+ return options.fn({ text: 'GOODBYE' });
+ })
+ .toCompileTo('GOODBYE! cruel world!');
+ });
+
+ it('block helper staying in the same context', () => {
+ expectTemplate('{{#form}}<p>{{name}}</p>{{/form}}')
+ .withInput({ name: 'Yehuda' })
+ .withHelper('form', function (this: any, options: HelperOptions) {
+ return '<form>' + options.fn(this) + '</form>';
+ })
+ .toCompileTo('<form><p>Yehuda</p></form>');
+ });
+
+ it('block helper should have context in this', () => {
+ function link(this: any, options: HelperOptions) {
+ return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>';
+ }
+
+ expectTemplate('<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>')
+ .withInput({
+ people: [
+ { name: 'Alan', id: 1 },
+ { name: 'Yehuda', id: 2 },
+ ],
+ })
+ .withHelper('link', link)
+ .toCompileTo(
+ '<ul><li><a href="/people/1">Alan</a></li><li><a href="/people/2">Yehuda</a></li></ul>'
+ );
+ });
+
+ it('block helper for undefined value', () => {
+ expectTemplate("{{#empty}}shouldn't render{{/empty}}").toCompileTo('');
+ });
+
+ it('block helper passing a new context', () => {
+ expectTemplate('{{#form yehuda}}<p>{{name}}</p>{{/form}}')
+ .withInput({ yehuda: { name: 'Yehuda' } })
+ .withHelper('form', function (context, options: HelperOptions) {
+ return '<form>' + options.fn(context) + '</form>';
+ })
+ .toCompileTo('<form><p>Yehuda</p></form>');
+ });
+
+ it('block helper passing a complex path context', () => {
+ expectTemplate('{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}')
+ .withInput({ yehuda: { name: 'Yehuda', cat: { name: 'Harold' } } })
+ .withHelper('form', function (context, options: HelperOptions) {
+ return '<form>' + options.fn(context) + '</form>';
+ })
+ .toCompileTo('<form><p>Harold</p></form>');
+ });
+
+ it('nested block helpers', () => {
+ expectTemplate('{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}')
+ .withInput({
+ yehuda: { name: 'Yehuda' },
+ })
+ .withHelper('link', function (this: any, options: HelperOptions) {
+ return '<a href="' + this.name + '">' + options.fn(this) + '</a>';
+ })
+ .withHelper('form', function (context, options: HelperOptions) {
+ return '<form>' + options.fn(context) + '</form>';
+ })
+ .toCompileTo('<form><p>Yehuda</p><a href="Yehuda">Hello</a></form>');
+ });
+
+ it('block helper inverted sections', () => {
+ const string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}";
+ function list(this: any, context: any, options: HelperOptions) {
+ if (context.length > 0) {
+ let out = '<ul>';
+ for (let i = 0, j = context.length; i < j; i++) {
+ out += '<li>';
+ out += options.fn(context[i]);
+ out += '</li>';
+ }
+ out += '</ul>';
+ return out;
+ } else {
+ return '<p>' + options.inverse(this) + '</p>';
+ }
+ }
+
+ // the meaning here may be kind of hard to catch, but list.not is always called,
+ // so we should see the output of both
+ expectTemplate(string)
+ .withInput({ people: [{ name: 'Alan' }, { name: 'Yehuda' }] })
+ .withHelpers({ list })
+ .toCompileTo('<ul><li>Alan</li><li>Yehuda</li></ul>');
+
+ expectTemplate(string)
+ .withInput({ people: [] })
+ .withHelpers({ list })
+ .toCompileTo("<p><em>Nobody's here</em></p>");
+
+ expectTemplate('{{#list people}}Hello{{^}}{{message}}{{/list}}')
+ .withInput({
+ people: [],
+ message: "Nobody's here",
+ })
+ .withHelpers({ list })
+ .toCompileTo('<p>Nobody&#x27;s here</p>');
+ });
+
+ it('pathed lambas with parameters', () => {
+ const hash = {
+ helper: () => 'winning',
+ };
+ // @ts-expect-error
+ hash.hash = hash;
+
+ const helpers = {
+ './helper': () => 'fail',
+ };
+
+ expectTemplate('{{./helper 1}}').withInput(hash).withHelpers(helpers).toCompileTo('winning');
+ expectTemplate('{{hash/helper 1}}').withInput(hash).withHelpers(helpers).toCompileTo('winning');
+ });
+
+ describe('helpers hash', () => {
+ it('providing a helpers hash', () => {
+ expectTemplate('Goodbye {{cruel}} {{world}}!')
+ .withInput({ cruel: 'cruel' })
+ .withHelpers({
+ world() {
+ return 'world';
+ },
+ })
+ .toCompileTo('Goodbye cruel world!');
+
+ expectTemplate('Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!')
+ .withInput({ iter: [{ cruel: 'cruel' }] })
+ .withHelpers({
+ world() {
+ return 'world';
+ },
+ })
+ .toCompileTo('Goodbye cruel world!');
+ });
+
+ it('in cases of conflict, helpers win', () => {
+ expectTemplate('{{{lookup}}}')
+ .withInput({ lookup: 'Explicit' })
+ .withHelpers({
+ lookup() {
+ return 'helpers';
+ },
+ })
+ .toCompileTo('helpers');
+
+ expectTemplate('{{lookup}}')
+ .withInput({ lookup: 'Explicit' })
+ .withHelpers({
+ lookup() {
+ return 'helpers';
+ },
+ })
+ .toCompileTo('helpers');
+ });
+
+ it('the helpers hash is available is nested contexts', () => {
+ expectTemplate('{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}')
+ .withInput({ outer: { inner: { unused: [] } } })
+ .withHelpers({
+ helper() {
+ return 'helper';
+ },
+ })
+ .toCompileTo('helper');
+ });
+
+ it('the helper hash should augment the global hash', () => {
+ kbnHandlebarsEnv!.registerHelper('test_helper', function () {
+ return 'found it!';
+ });
+
+ expectTemplate('{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}')
+ .withInput({ cruel: 'cruel' })
+ .withHelpers({
+ world() {
+ return 'world!';
+ },
+ })
+ .toCompileTo('found it! Goodbye cruel world!!');
+ });
+ });
+
+ describe('registration', () => {
+ it('unregisters', () => {
+ deleteAllKeys(kbnHandlebarsEnv!.helpers);
+
+ kbnHandlebarsEnv!.registerHelper('foo', function () {
+ return 'fail';
+ });
+ expect(kbnHandlebarsEnv!.helpers.foo).toBeDefined();
+ kbnHandlebarsEnv!.unregisterHelper('foo');
+ expect(kbnHandlebarsEnv!.helpers.foo).toBeUndefined();
+ });
+
+ it('allows multiple globals', () => {
+ const ifHelper = kbnHandlebarsEnv!.helpers.if;
+ deleteAllKeys(kbnHandlebarsEnv!.helpers);
+
+ kbnHandlebarsEnv!.registerHelper({
+ if: ifHelper,
+ world() {
+ return 'world!';
+ },
+ testHelper() {
+ return 'found it!';
+ },
+ });
+
+ expectTemplate('{{testHelper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}')
+ .withInput({ cruel: 'cruel' })
+ .toCompileTo('found it! Goodbye cruel world!!');
+ });
+
+ it('fails with multiple and args', () => {
+ expect(() => {
+ kbnHandlebarsEnv!.registerHelper(
+ // @ts-expect-error TypeScript is complaining about the invalid input just as the thrown error
+ {
+ world() {
+ return 'world!';
+ },
+ testHelper() {
+ return 'found it!';
+ },
+ },
+ {}
+ );
+ }).toThrow('Arg not supported with multiple helpers');
+ });
+ });
+
+ it('decimal number literals work', () => {
+ expectTemplate('Message: {{hello -1.2 1.2}}')
+ .withHelper('hello', function (times, times2) {
+ if (typeof times !== 'number') {
+ times = 'NaN';
+ }
+ if (typeof times2 !== 'number') {
+ times2 = 'NaN';
+ }
+ return 'Hello ' + times + ' ' + times2 + ' times';
+ })
+ .toCompileTo('Message: Hello -1.2 1.2 times');
+ });
+
+ it('negative number literals work', () => {
+ expectTemplate('Message: {{hello -12}}')
+ .withHelper('hello', function (times) {
+ if (typeof times !== 'number') {
+ times = 'NaN';
+ }
+ return 'Hello ' + times + ' times';
+ })
+ .toCompileTo('Message: Hello -12 times');
+ });
+
+ describe('String literal parameters', () => {
+ it('simple literals work', () => {
+ expectTemplate('Message: {{hello "world" 12 true false}}')
+ .withHelper('hello', function (param, times, bool1, bool2) {
+ if (typeof times !== 'number') {
+ times = 'NaN';
+ }
+ if (typeof bool1 !== 'boolean') {
+ bool1 = 'NaB';
+ }
+ if (typeof bool2 !== 'boolean') {
+ bool2 = 'NaB';
+ }
+ return 'Hello ' + param + ' ' + times + ' times: ' + bool1 + ' ' + bool2;
+ })
+ .toCompileTo('Message: Hello world 12 times: true false');
+ });
+
+ it('using a quote in the middle of a parameter raises an error', () => {
+ expectTemplate('Message: {{hello wo"rld"}}').toThrow(Error);
+ });
+
+ it('escaping a String is possible', () => {
+ expectTemplate('Message: {{{hello "\\"world\\""}}}')
+ .withHelper('hello', function (param) {
+ return 'Hello ' + param;
+ })
+ .toCompileTo('Message: Hello "world"');
+ });
+
+ it("it works with ' marks", () => {
+ expectTemplate('Message: {{{hello "Alan\'s world"}}}')
+ .withHelper('hello', function (param) {
+ return 'Hello ' + param;
+ })
+ .toCompileTo("Message: Hello Alan's world");
+ });
+ });
+
+ describe('multiple parameters', () => {
+ it('simple multi-params work', () => {
+ expectTemplate('Message: {{goodbye cruel world}}')
+ .withInput({ cruel: 'cruel', world: 'world' })
+ .withHelper('goodbye', function (cruel, world) {
+ return 'Goodbye ' + cruel + ' ' + world;
+ })
+ .toCompileTo('Message: Goodbye cruel world');
+ });
+
+ it('block multi-params work', () => {
+ expectTemplate('Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}')
+ .withInput({ cruel: 'cruel', world: 'world' })
+ .withHelper('goodbye', function (cruel, world, options: HelperOptions) {
+ return options.fn({ greeting: 'Goodbye', adj: cruel, noun: world });
+ })
+ .toCompileTo('Message: Goodbye cruel world');
+ });
+ });
+
+ describe('hash', () => {
+ it('helpers can take an optional hash', () => {
+ expectTemplate('{{goodbye cruel="CRUEL" world="WORLD" times=12}}')
+ .withHelper('goodbye', function (options: HelperOptions) {
+ return (
+ 'GOODBYE ' +
+ options.hash.cruel +
+ ' ' +
+ options.hash.world +
+ ' ' +
+ options.hash.times +
+ ' TIMES'
+ );
+ })
+ .toCompileTo('GOODBYE CRUEL WORLD 12 TIMES');
+ });
+
+ it('helpers can take an optional hash with booleans', () => {
+ function goodbye(options: HelperOptions) {
+ if (options.hash.print === true) {
+ return 'GOODBYE ' + options.hash.cruel + ' ' + options.hash.world;
+ } else if (options.hash.print === false) {
+ return 'NOT PRINTING';
+ } else {
+ return 'THIS SHOULD NOT HAPPEN';
+ }
+ }
+
+ expectTemplate('{{goodbye cruel="CRUEL" world="WORLD" print=true}}')
+ .withHelper('goodbye', goodbye)
+ .toCompileTo('GOODBYE CRUEL WORLD');
+
+ expectTemplate('{{goodbye cruel="CRUEL" world="WORLD" print=false}}')
+ .withHelper('goodbye', goodbye)
+ .toCompileTo('NOT PRINTING');
+ });
+
+ it('block helpers can take an optional hash', () => {
+ expectTemplate('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}')
+ .withHelper('goodbye', function (this: any, options: HelperOptions) {
+ return (
+ 'GOODBYE ' +
+ options.hash.cruel +
+ ' ' +
+ options.fn(this) +
+ ' ' +
+ options.hash.times +
+ ' TIMES'
+ );
+ })
+ .toCompileTo('GOODBYE CRUEL world 12 TIMES');
+ });
+
+ it('block helpers can take an optional hash with single quoted stings', () => {
+ expectTemplate('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}')
+ .withHelper('goodbye', function (this: any, options: HelperOptions) {
+ return (
+ 'GOODBYE ' +
+ options.hash.cruel +
+ ' ' +
+ options.fn(this) +
+ ' ' +
+ options.hash.times +
+ ' TIMES'
+ );
+ })
+ .toCompileTo('GOODBYE CRUEL world 12 TIMES');
+ });
+
+ it('block helpers can take an optional hash with booleans', () => {
+ function goodbye(this: any, options: HelperOptions) {
+ if (options.hash.print === true) {
+ return 'GOODBYE ' + options.hash.cruel + ' ' + options.fn(this);
+ } else if (options.hash.print === false) {
+ return 'NOT PRINTING';
+ } else {
+ return 'THIS SHOULD NOT HAPPEN';
+ }
+ }
+
+ expectTemplate('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}')
+ .withHelper('goodbye', goodbye)
+ .toCompileTo('GOODBYE CRUEL world');
+
+ expectTemplate('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}')
+ .withHelper('goodbye', goodbye)
+ .toCompileTo('NOT PRINTING');
+ });
+ });
+
+ describe('helperMissing', () => {
+ it('if a context is not found, helperMissing is used', () => {
+ expectTemplate('{{hello}} {{link_to world}}').toThrow(/Missing helper: "link_to"/);
+ });
+
+ it('if a context is not found, custom helperMissing is used', () => {
+ expectTemplate('{{hello}} {{link_to world}}')
+ .withInput({ hello: 'Hello', world: 'world' })
+ .withHelper('helperMissing', function (mesg, options: HelperOptions) {
+ if (options.name === 'link_to') {
+ return new Handlebars.SafeString('<a>' + mesg + '</a>');
+ }
+ })
+ .toCompileTo('Hello <a>world</a>');
+ });
+
+ it('if a value is not found, custom helperMissing is used', () => {
+ expectTemplate('{{hello}} {{link_to}}')
+ .withInput({ hello: 'Hello', world: 'world' })
+ .withHelper('helperMissing', function (options: HelperOptions) {
+ if (options.name === 'link_to') {
+ return new Handlebars.SafeString('<a>winning</a>');
+ }
+ })
+ .toCompileTo('Hello <a>winning</a>');
+ });
+ });
+
+ describe('knownHelpers', () => {
+ it('Known helper should render helper', () => {
+ expectTemplate('{{hello}}')
+ .withCompileOptions({
+ knownHelpers: { hello: true },
+ })
+ .withHelper('hello', function () {
+ return 'foo';
+ })
+ .toCompileTo('foo');
+ });
+
+ it('Unknown helper in knownHelpers only mode should be passed as undefined', () => {
+ expectTemplate('{{typeof hello}}')
+ .withCompileOptions({
+ knownHelpers: { typeof: true },
+ knownHelpersOnly: true,
+ })
+ .withHelper('typeof', function (arg) {
+ return typeof arg;
+ })
+ .withHelper('hello', function () {
+ return 'foo';
+ })
+ .toCompileTo('undefined');
+ });
+
+ it('Builtin helpers available in knownHelpers only mode', () => {
+ expectTemplate('{{#unless foo}}bar{{/unless}}')
+ .withCompileOptions({
+ knownHelpersOnly: true,
+ })
+ .toCompileTo('bar');
+ });
+
+ it('Field lookup works in knownHelpers only mode', () => {
+ expectTemplate('{{foo}}')
+ .withCompileOptions({
+ knownHelpersOnly: true,
+ })
+ .withInput({ foo: 'bar' })
+ .toCompileTo('bar');
+ });
+
+ it('Conditional blocks work in knownHelpers only mode', () => {
+ expectTemplate('{{#foo}}bar{{/foo}}')
+ .withCompileOptions({
+ knownHelpersOnly: true,
+ })
+ .withInput({ foo: 'baz' })
+ .toCompileTo('bar');
+ });
+
+ it('Invert blocks work in knownHelpers only mode', () => {
+ expectTemplate('{{^foo}}bar{{/foo}}')
+ .withCompileOptions({
+ knownHelpersOnly: true,
+ })
+ .withInput({ foo: false })
+ .toCompileTo('bar');
+ });
+
+ it('Functions are bound to the context in knownHelpers only mode', () => {
+ expectTemplate('{{foo}}')
+ .withCompileOptions({
+ knownHelpersOnly: true,
+ })
+ .withInput({
+ foo() {
+ return this.bar;
+ },
+ bar: 'bar',
+ })
+ .toCompileTo('bar');
+ });
+
+ it('Unknown helper call in knownHelpers only mode should throw', () => {
+ expectTemplate('{{typeof hello}}')
+ .withCompileOptions({ knownHelpersOnly: true })
+ .toThrow(Error);
+ });
+ });
+
+ describe('blockHelperMissing', () => {
+ it('lambdas are resolved by blockHelperMissing, not handlebars proper', () => {
+ expectTemplate('{{#truthy}}yep{{/truthy}}')
+ .withInput({
+ truthy() {
+ return true;
+ },
+ })
+ .toCompileTo('yep');
+ });
+
+ it('lambdas resolved by blockHelperMissing are bound to the context', () => {
+ expectTemplate('{{#truthy}}yep{{/truthy}}')
+ .withInput({
+ truthy() {
+ return this.truthiness();
+ },
+ truthiness() {
+ return false;
+ },
+ })
+ .toCompileTo('');
+ });
+ });
+
+ describe('name field', () => {
+ const helpers = {
+ blockHelperMissing(...args: any[]) {
+ return 'missing: ' + args[args.length - 1].name;
+ },
+ helperMissing(...args: any[]) {
+ return 'helper missing: ' + args[args.length - 1].name;
+ },
+ helper(...args: any[]) {
+ return 'ran: ' + args[args.length - 1].name;
+ },
+ };
+
+ it('should include in ambiguous mustache calls', () => {
+ expectTemplate('{{helper}}').withHelpers(helpers).toCompileTo('ran: helper');
+ });
+
+ it('should include in helper mustache calls', () => {
+ expectTemplate('{{helper 1}}').withHelpers(helpers).toCompileTo('ran: helper');
+ });
+
+ it('should include in ambiguous block calls', () => {
+ expectTemplate('{{#helper}}{{/helper}}').withHelpers(helpers).toCompileTo('ran: helper');
+ });
+
+ it('should include in simple block calls', () => {
+ expectTemplate('{{#./helper}}{{/./helper}}')
+ .withHelpers(helpers)
+ .toCompileTo('missing: ./helper');
+ });
+
+ it('should include in helper block calls', () => {
+ expectTemplate('{{#helper 1}}{{/helper}}').withHelpers(helpers).toCompileTo('ran: helper');
+ });
+
+ it('should include in known helper calls', () => {
+ expectTemplate('{{helper}}')
+ .withCompileOptions({
+ knownHelpers: { helper: true },
+ knownHelpersOnly: true,
+ })
+ .withHelpers(helpers)
+ .toCompileTo('ran: helper');
+ });
+
+ it('should include full id', () => {
+ expectTemplate('{{#foo.helper}}{{/foo.helper}}')
+ .withInput({ foo: {} })
+ .withHelpers(helpers)
+ .toCompileTo('missing: foo.helper');
+ });
+
+ it('should include full id if a hash is passed', () => {
+ expectTemplate('{{#foo.helper bar=baz}}{{/foo.helper}}')
+ .withInput({ foo: {} })
+ .withHelpers(helpers)
+ .toCompileTo('helper missing: foo.helper');
+ });
+ });
+
+ describe('name conflicts', () => {
+ it('helpers take precedence over same-named context properties', () => {
+ expectTemplate('{{goodbye}} {{cruel world}}')
+ .withHelper('goodbye', function (this: any) {
+ return this.goodbye.toUpperCase();
+ })
+ .withHelper('cruel', function (world) {
+ return 'cruel ' + world.toUpperCase();
+ })
+ .withInput({
+ goodbye: 'goodbye',
+ world: 'world',
+ })
+ .toCompileTo('GOODBYE cruel WORLD');
+ });
+
+ it('helpers take precedence over same-named context properties$', () => {
+ expectTemplate('{{#goodbye}} {{cruel world}}{{/goodbye}}')
+ .withHelper('goodbye', function (this: any, options: HelperOptions) {
+ return this.goodbye.toUpperCase() + options.fn(this);
+ })
+ .withHelper('cruel', function (world) {
+ return 'cruel ' + world.toUpperCase();
+ })
+ .withInput({
+ goodbye: 'goodbye',
+ world: 'world',
+ })
+ .toCompileTo('GOODBYE cruel WORLD');
+ });
+
+ it('Scoped names take precedence over helpers', () => {
+ expectTemplate('{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}')
+ .withHelper('goodbye', function (this: any) {
+ return this.goodbye.toUpperCase();
+ })
+ .withHelper('cruel', function (world) {
+ return 'cruel ' + world.toUpperCase();
+ })
+ .withInput({
+ goodbye: 'goodbye',
+ world: 'world',
+ })
+ .toCompileTo('goodbye cruel WORLD cruel GOODBYE');
+ });
+
+ it('Scoped names take precedence over block helpers', () => {
+ expectTemplate('{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}')
+ .withHelper('goodbye', function (this: any, options: HelperOptions) {
+ return this.goodbye.toUpperCase() + options.fn(this);
+ })
+ .withHelper('cruel', function (world) {
+ return 'cruel ' + world.toUpperCase();
+ })
+ .withInput({
+ goodbye: 'goodbye',
+ world: 'world',
+ })
+ .toCompileTo('GOODBYE cruel WORLD goodbye');
+ });
+ });
+
+ describe('block params', () => {
+ it('should take presedence over context values', () => {
+ expectTemplate('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}')
+ .withInput({ value: 'foo' })
+ .withHelper('goodbyes', function (options: HelperOptions) {
+ expect(options.fn.blockParams).toEqual(1);
+ return options.fn({ value: 'bar' }, { blockParams: [1, 2] });
+ })
+ .toCompileTo('1foo');
+ });
+
+ it('should take presedence over helper values', () => {
+ expectTemplate('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}')
+ .withHelper('value', function () {
+ return 'foo';
+ })
+ .withHelper('goodbyes', function (options: HelperOptions) {
+ expect(options.fn.blockParams).toEqual(1);
+ return options.fn({}, { blockParams: [1, 2] });
+ })
+ .toCompileTo('1foo');
+ });
+
+ it('should not take presedence over pathed values', () => {
+ expectTemplate('{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}')
+ .withInput({ value: 'bar' })
+ .withHelper('value', function () {
+ return 'foo';
+ })
+ .withHelper('goodbyes', function (this: any, options: HelperOptions) {
+ expect(options.fn.blockParams).toEqual(1);
+ return options.fn(this, { blockParams: [1, 2] });
+ })
+ .toCompileTo('barfoo');
+ });
+
+ it('should take presednece over parent block params', () => {
+ let value: number;
+ expectTemplate(
+ '{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}',
+ {
+ beforeEach() {
+ value = 1;
+ },
+ }
+ )
+ .withInput({ value: 'foo' })
+ .withHelper('goodbyes', function (options: HelperOptions) {
+ return options.fn(
+ { value: 'bar' },
+ {
+ blockParams: options.fn.blockParams === 1 ? [value++, value++] : undefined,
+ }
+ );
+ })
+ .toCompileTo('13foo');
+ });
+
+ it('should allow block params on chained helpers', () => {
+ expectTemplate('{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}')
+ .withInput({ value: 'foo' })
+ .withHelper('goodbyes', function (options: HelperOptions) {
+ expect(options.fn.blockParams).toEqual(1);
+ return options.fn({ value: 'bar' }, { blockParams: [1, 2] });
+ })
+ .toCompileTo('1foo');
+ });
+ });
+
+ describe('built-in helpers malformed arguments ', () => {
+ it('if helper - too few arguments', () => {
+ expectTemplate('{{#if}}{{/if}}').toThrow(/#if requires exactly one argument/);
+ });
+
+ it('if helper - too many arguments, string', () => {
+ expectTemplate('{{#if test "string"}}{{/if}}').toThrow(/#if requires exactly one argument/);
+ });
+
+ it('if helper - too many arguments, undefined', () => {
+ expectTemplate('{{#if test undefined}}{{/if}}').toThrow(/#if requires exactly one argument/);
+ });
+
+ it('if helper - too many arguments, null', () => {
+ expectTemplate('{{#if test null}}{{/if}}').toThrow(/#if requires exactly one argument/);
+ });
+
+ it('unless helper - too few arguments', () => {
+ expectTemplate('{{#unless}}{{/unless}}').toThrow(/#unless requires exactly one argument/);
+ });
+
+ it('unless helper - too many arguments', () => {
+ expectTemplate('{{#unless test null}}{{/unless}}').toThrow(
+ /#unless requires exactly one argument/
+ );
+ });
+
+ it('with helper - too few arguments', () => {
+ expectTemplate('{{#with}}{{/with}}').toThrow(/#with requires exactly one argument/);
+ });
+
+ it('with helper - too many arguments', () => {
+ expectTemplate('{{#with test "string"}}{{/with}}').toThrow(
+ /#with requires exactly one argument/
+ );
+ });
+ });
+
+ describe('the lookupProperty-option', () => {
+ it('should be passed to custom helpers', () => {
+ expectTemplate('{{testHelper}}')
+ .withHelper('testHelper', function testHelper(this: any, options: HelperOptions) {
+ return options.lookupProperty(this, 'testProperty');
+ })
+ .withInput({ testProperty: 'abc' })
+ .toCompileTo('abc');
+ });
+ });
+});
+
+function deleteAllKeys(obj: { [key: string]: any }) {
+ for (const key of Object.keys(obj)) {
+ delete obj[key];
+ }
+}