diff options
Diffstat (limited to 'dev/lib/handlebars/src/spec/index.helpers.test.ts')
-rw-r--r-- | dev/lib/handlebars/src/spec/index.helpers.test.ts | 958 |
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'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]; + } +} |