diff options
Diffstat (limited to 'dev/lib/handlebars/src/spec/index.builtins.test.ts')
-rw-r--r-- | dev/lib/handlebars/src/spec/index.builtins.test.ts | 676 |
1 files changed, 676 insertions, 0 deletions
diff --git a/dev/lib/handlebars/src/spec/index.builtins.test.ts b/dev/lib/handlebars/src/spec/index.builtins.test.ts new file mode 100644 index 00000000..c47ec29f --- /dev/null +++ b/dev/lib/handlebars/src/spec/index.builtins.test.ts @@ -0,0 +1,676 @@ +/* + * 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. + */ + +/* eslint-disable max-classes-per-file */ + +import Handlebars from '../..'; +import { expectTemplate } from '../__jest__/test_bench'; + +describe('builtin helpers', () => { + describe('#if', () => { + it('if', () => { + const string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!'; + + expectTemplate(string) + .withInput({ + goodbye: true, + world: 'world', + }) + .toCompileTo('GOODBYE cruel world!'); + + expectTemplate(string) + .withInput({ + goodbye: 'dummy', + world: 'world', + }) + .toCompileTo('GOODBYE cruel world!'); + + expectTemplate(string) + .withInput({ + goodbye: false, + world: 'world', + }) + .toCompileTo('cruel world!'); + + expectTemplate(string).withInput({ world: 'world' }).toCompileTo('cruel world!'); + + expectTemplate(string) + .withInput({ + goodbye: ['foo'], + world: 'world', + }) + .toCompileTo('GOODBYE cruel world!'); + + expectTemplate(string) + .withInput({ + goodbye: [], + world: 'world', + }) + .toCompileTo('cruel world!'); + + expectTemplate(string) + .withInput({ + goodbye: 0, + world: 'world', + }) + .toCompileTo('cruel world!'); + + expectTemplate('{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!') + .withInput({ + goodbye: 0, + world: 'world', + }) + .toCompileTo('GOODBYE cruel world!'); + }); + + it('if with function argument', () => { + const string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!'; + + expectTemplate(string) + .withInput({ + goodbye() { + return true; + }, + world: 'world', + }) + .toCompileTo('GOODBYE cruel world!'); + + expectTemplate(string) + .withInput({ + goodbye() { + return this.world; + }, + world: 'world', + }) + .toCompileTo('GOODBYE cruel world!'); + + expectTemplate(string) + .withInput({ + goodbye() { + return false; + }, + world: 'world', + }) + .toCompileTo('cruel world!'); + + expectTemplate(string) + .withInput({ + goodbye() { + return this.foo; + }, + world: 'world', + }) + .toCompileTo('cruel world!'); + }); + + it('should not change the depth list', () => { + expectTemplate('{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}') + .withInput({ + foo: { goodbye: true }, + world: 'world', + }) + .toCompileTo('GOODBYE cruel world!'); + }); + }); + + describe('#with', () => { + it('with', () => { + expectTemplate('{{#with person}}{{first}} {{last}}{{/with}}') + .withInput({ + person: { + first: 'Alan', + last: 'Johnson', + }, + }) + .toCompileTo('Alan Johnson'); + }); + + it('with with function argument', () => { + expectTemplate('{{#with person}}{{first}} {{last}}{{/with}}') + .withInput({ + person() { + return { + first: 'Alan', + last: 'Johnson', + }; + }, + }) + .toCompileTo('Alan Johnson'); + }); + + it('with with else', () => { + expectTemplate( + '{{#with person}}Person is present{{else}}Person is not present{{/with}}' + ).toCompileTo('Person is not present'); + }); + + it('with provides block parameter', () => { + expectTemplate('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}') + .withInput({ + person: { + first: 'Alan', + last: 'Johnson', + }, + }) + .toCompileTo('Alan Johnson'); + }); + + it('works when data is disabled', () => { + expectTemplate('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}') + .withInput({ person: { first: 'Alan', last: 'Johnson' } }) + .withCompileOptions({ data: false }) + .toCompileTo('Alan Johnson'); + }); + }); + + describe('#each', () => { + it('each', () => { + const string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; + + expectTemplate(string) + .withInput({ + goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], + world: 'world', + }) + .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!'); + + expectTemplate(string) + .withInput({ + goodbyes: [], + world: 'world', + }) + .toCompileTo('cruel world!'); + }); + + it('each without data', () => { + expectTemplate('{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!') + .withInput({ + goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], + world: 'world', + }) + .withRuntimeOptions({ data: false }) + .withCompileOptions({ data: false }) + .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!'); + + expectTemplate('{{#each .}}{{.}}{{/each}}') + .withInput({ goodbyes: 'cruel', world: 'world' }) + .withRuntimeOptions({ data: false }) + .withCompileOptions({ data: false }) + .toCompileTo('cruelworld'); + }); + + it('each without context', () => { + expectTemplate('{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!') + .withInput(undefined) + .toCompileTo('cruel !'); + }); + + it('each with an object and @key', () => { + const string = '{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!'; + + function Clazz(this: any) { + this['<b>#1</b>'] = { text: 'goodbye' }; + this[2] = { text: 'GOODBYE' }; + } + Clazz.prototype.foo = 'fail'; + const hash = { goodbyes: new (Clazz as any)(), world: 'world' }; + + // Object property iteration order is undefined according to ECMA spec, + // so we need to check both possible orders + // @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop + try { + expectTemplate(string) + .withInput(hash) + .toCompileTo('<b>#1</b>. goodbye! 2. GOODBYE! cruel world!'); + } catch (e) { + expectTemplate(string) + .withInput(hash) + .toCompileTo('2. GOODBYE! <b>#1</b>. goodbye! cruel world!'); + } + + expectTemplate(string) + .withInput({ + goodbyes: {}, + world: 'world', + }) + .toCompileTo('cruel world!'); + }); + + it('each with @index', () => { + expectTemplate('{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!') + .withInput({ + goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], + world: 'world', + }) + .toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!'); + }); + + it('each with nested @index', () => { + expectTemplate( + '{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!' + ) + .withInput({ + goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], + world: 'world', + }) + .toCompileTo( + '0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!' + ); + }); + + it('each with block params', () => { + expectTemplate( + '{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!' + ) + .withInput({ + goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }], + world: 'world', + }) + .toCompileTo('0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!'); + }); + + // TODO: This test has been added to the `4.x` branch of the handlebars.js repo along with a code-fix, + // but a new version of the handlebars package containing this fix has not yet been published to npm. + // + // Before enabling this code, a new version of handlebars needs to be released and the corresponding + // updates needs to be applied to this implementation. + // + // See: https://github.com/handlebars-lang/handlebars.js/commit/30dbf0478109ded8f12bb29832135d480c17e367 + it.skip('each with block params and strict compilation', () => { + expectTemplate('{{#each goodbyes as |value index|}}{{index}}. {{value.text}}!{{/each}}') + .withCompileOptions({ strict: true }) + .withInput({ goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }] }) + .toCompileTo('0. goodbye!1. Goodbye!'); + }); + + it('each object with @index', () => { + expectTemplate('{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!') + .withInput({ + goodbyes: { + a: { text: 'goodbye' }, + b: { text: 'Goodbye' }, + c: { text: 'GOODBYE' }, + }, + world: 'world', + }) + .toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!'); + }); + + it('each with @first', () => { + expectTemplate('{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!') + .withInput({ + goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], + world: 'world', + }) + .toCompileTo('goodbye! cruel world!'); + }); + + it('each with nested @first', () => { + expectTemplate( + '{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!' + ) + .withInput({ + goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], + world: 'world', + }) + .toCompileTo('(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!'); + }); + + it('each object with @first', () => { + expectTemplate('{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!') + .withInput({ + goodbyes: { foo: { text: 'goodbye' }, bar: { text: 'Goodbye' } }, + world: 'world', + }) + .toCompileTo('goodbye! cruel world!'); + }); + + it('each with @last', () => { + expectTemplate('{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!') + .withInput({ + goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], + world: 'world', + }) + .toCompileTo('GOODBYE! cruel world!'); + }); + + it('each object with @last', () => { + expectTemplate('{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!') + .withInput({ + goodbyes: { foo: { text: 'goodbye' }, bar: { text: 'Goodbye' } }, + world: 'world', + }) + .toCompileTo('Goodbye! cruel world!'); + }); + + it('each with nested @last', () => { + expectTemplate( + '{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!' + ) + .withInput({ + goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], + world: 'world', + }) + .toCompileTo('(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!'); + }); + + it('each with function argument', () => { + const string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; + + expectTemplate(string) + .withInput({ + goodbyes() { + return [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }]; + }, + world: 'world', + }) + .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!'); + + expectTemplate(string) + .withInput({ + goodbyes: [], + world: 'world', + }) + .toCompileTo('cruel world!'); + }); + + it('each object when last key is an empty string', () => { + expectTemplate('{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!') + .withInput({ + goodbyes: { + a: { text: 'goodbye' }, + b: { text: 'Goodbye' }, + '': { text: 'GOODBYE' }, + }, + world: 'world', + }) + .toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!'); + }); + + it('data passed to helpers', () => { + expectTemplate('{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}') + .withInput({ letters: ['a', 'b', 'c'] }) + .withHelper('detectDataInsideEach', function (options) { + return options.data && options.data.exclaim; + }) + .withRuntimeOptions({ + data: { + exclaim: '!', + }, + }) + .toCompileTo('a!b!c!'); + }); + + it('each on implicit context', () => { + expectTemplate('{{#each}}{{text}}! {{/each}}cruel world!').toThrow(Handlebars.Exception); + }); + + it('each on iterable', () => { + class Iterator { + private arr: any[]; + private index: number = 0; + + constructor(arr: any[]) { + this.arr = arr; + } + + next() { + const value = this.arr[this.index]; + const done = this.index === this.arr.length; + if (!done) { + this.index++; + } + return { value, done }; + } + } + + class Iterable { + private arr: any[]; + + constructor(arr: any[]) { + this.arr = arr; + } + + [Symbol.iterator]() { + return new Iterator(this.arr); + } + } + + const string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; + + expectTemplate(string) + .withInput({ + goodbyes: new Iterable([{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }]), + world: 'world', + }) + .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!'); + + expectTemplate(string) + .withInput({ + goodbyes: new Iterable([]), + world: 'world', + }) + .toCompileTo('cruel world!'); + }); + }); + + describe('#log', function () { + /* eslint-disable no-console */ + let $log: typeof console.log; + let $info: typeof console.info; + let $error: typeof console.error; + + beforeEach(function () { + $log = console.log; + $info = console.info; + $error = console.error; + + global.kbnHandlebarsEnv = Handlebars.create(); + }); + + afterEach(function () { + console.log = $log; + console.info = $info; + console.error = $error; + + global.kbnHandlebarsEnv = null; + }); + + it('should call logger at default level', function () { + let levelArg; + let logArg; + kbnHandlebarsEnv!.log = function (level, arg) { + levelArg = level; + logArg = arg; + }; + + expectTemplate('{{log blah}}').withInput({ blah: 'whee' }).toCompileTo(''); + expect(1).toEqual(levelArg); + expect('whee').toEqual(logArg); + }); + + it('should call logger at data level', function () { + let levelArg; + let logArg; + kbnHandlebarsEnv!.log = function (level, arg) { + levelArg = level; + logArg = arg; + }; + + expectTemplate('{{log blah}}') + .withInput({ blah: 'whee' }) + .withRuntimeOptions({ data: { level: '03' } }) + .withCompileOptions({ data: true }) + .toCompileTo(''); + expect('03').toEqual(levelArg); + expect('whee').toEqual(logArg); + }); + + it('should output to info', function () { + let calls = 0; + const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; + + console.info = function (info) { + expect('whee').toEqual(info); + calls++; + if (calls === callsExpected) { + console.info = $info; + console.log = $log; + } + }; + console.log = function (log) { + expect('whee').toEqual(log); + calls++; + if (calls === callsExpected) { + console.info = $info; + console.log = $log; + } + }; + + expectTemplate('{{log blah}}').withInput({ blah: 'whee' }).toCompileTo(''); + expect(calls).toEqual(callsExpected); + }); + + it('should log at data level', function () { + let calls = 0; + const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; + + console.error = function (log) { + expect('whee').toEqual(log); + calls++; + if (calls === callsExpected) console.error = $error; + }; + + expectTemplate('{{log blah}}') + .withInput({ blah: 'whee' }) + .withRuntimeOptions({ data: { level: '03' } }) + .withCompileOptions({ data: true }) + .toCompileTo(''); + expect(calls).toEqual(callsExpected); + }); + + it('should handle missing logger', function () { + let calls = 0; + const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; + + // @ts-expect-error + console.error = undefined; + console.log = function (log) { + expect('whee').toEqual(log); + calls++; + if (calls === callsExpected) console.log = $log; + }; + + expectTemplate('{{log blah}}') + .withInput({ blah: 'whee' }) + .withRuntimeOptions({ data: { level: '03' } }) + .withCompileOptions({ data: true }) + .toCompileTo(''); + expect(calls).toEqual(callsExpected); + }); + + it('should handle string log levels', function () { + let calls = 0; + const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; + + console.error = function (log) { + expect('whee').toEqual(log); + calls++; + }; + + expectTemplate('{{log blah}}') + .withInput({ blah: 'whee' }) + .withRuntimeOptions({ data: { level: 'error' } }) + .withCompileOptions({ data: true }) + .toCompileTo(''); + expect(calls).toEqual(callsExpected); + + calls = 0; + + expectTemplate('{{log blah}}') + .withInput({ blah: 'whee' }) + .withRuntimeOptions({ data: { level: 'ERROR' } }) + .withCompileOptions({ data: true }) + .toCompileTo(''); + expect(calls).toEqual(callsExpected); + }); + + it('should handle hash log levels [1]', function () { + let calls = 0; + const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; + + console.error = function (log) { + expect('whee').toEqual(log); + calls++; + }; + + expectTemplate('{{log blah level="error"}}').withInput({ blah: 'whee' }).toCompileTo(''); + expect(calls).toEqual(callsExpected); + }); + + it('should handle hash log levels [2]', function () { + let called = false; + + console.info = + console.log = + console.error = + console.debug = + function () { + called = true; + console.info = console.log = console.error = console.debug = $log; + }; + + expectTemplate('{{log blah level="debug"}}').withInput({ blah: 'whee' }).toCompileTo(''); + expect(false).toEqual(called); + }); + + it('should pass multiple log arguments', function () { + let calls = 0; + const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; + + console.info = console.log = function (log1, log2, log3) { + expect('whee').toEqual(log1); + expect('foo').toEqual(log2); + expect(1).toEqual(log3); + calls++; + if (calls === callsExpected) console.log = $log; + }; + + expectTemplate('{{log blah "foo" 1}}').withInput({ blah: 'whee' }).toCompileTo(''); + expect(calls).toEqual(callsExpected); + }); + + it('should pass zero log arguments', function () { + let calls = 0; + const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; + + console.info = console.log = function () { + expect(arguments.length).toEqual(0); + calls++; + if (calls === callsExpected) console.log = $log; + }; + + expectTemplate('{{log}}').withInput({ blah: 'whee' }).toCompileTo(''); + expect(calls).toEqual(callsExpected); + }); + /* eslint-enable no-console */ + }); + + describe('#lookup', () => { + it('should lookup arbitrary content', () => { + expectTemplate('{{#each goodbyes}}{{lookup ../data .}}{{/each}}') + .withInput({ goodbyes: [0, 1], data: ['foo', 'bar'] }) + .toCompileTo('foobar'); + }); + + it('should not fail on undefined value', () => { + expectTemplate('{{#each goodbyes}}{{lookup ../bar .}}{{/each}}') + .withInput({ goodbyes: [0, 1], data: ['foo', 'bar'] }) + .toCompileTo(''); + }); + }); +}); |