/* * 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'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]; } }