mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
567 lines
17 KiB
TypeScript
567 lines
17 KiB
TypeScript
/*
|
|
* Elasticsearch B.V licenses this file to you under the MIT License.
|
|
* See `packages/kbn-handlebars/LICENSE` for more information.
|
|
*/
|
|
|
|
/**
|
|
* ABOUT THIS FILE:
|
|
*
|
|
* This file is for tests not copied from the upstream handlebars project, but
|
|
* tests that we feel are needed in order to fully cover our use-cases.
|
|
*/
|
|
|
|
import Handlebars from '.';
|
|
import type { HelperOptions, TemplateDelegate } from './src/types';
|
|
import { expectTemplate, forEachCompileFunctionName } from './src/__jest__/test_bench';
|
|
|
|
it('Handlebars.create', () => {
|
|
expect(Handlebars.create()).toMatchSnapshot();
|
|
});
|
|
|
|
describe('Handlebars.compileAST', () => {
|
|
describe('compiler options', () => {
|
|
it('noEscape', () => {
|
|
expectTemplate('{{value}}').withInput({ value: '<foo>' }).toCompileTo('<foo>');
|
|
|
|
expectTemplate('{{value}}')
|
|
.withCompileOptions({ noEscape: false })
|
|
.withInput({ value: '<foo>' })
|
|
.toCompileTo('<foo>');
|
|
|
|
expectTemplate('{{value}}')
|
|
.withCompileOptions({ noEscape: true })
|
|
.withInput({ value: '<foo>' })
|
|
.toCompileTo('<foo>');
|
|
});
|
|
});
|
|
|
|
it('invalid template', () => {
|
|
expectTemplate('{{value').withInput({ value: 42 }).toThrow(`Parse error on line 1:
|
|
{{value
|
|
--^
|
|
Expecting 'ID', 'STRING', 'NUMBER', 'BOOLEAN', 'UNDEFINED', 'NULL', 'DATA', got 'INVALID'`);
|
|
});
|
|
|
|
if (!process.env.EVAL) {
|
|
it('reassign', () => {
|
|
const fn = Handlebars.compileAST;
|
|
expect(fn('{{value}}')({ value: 42 })).toEqual('42');
|
|
});
|
|
}
|
|
});
|
|
|
|
// Extra "helpers" tests
|
|
describe('helpers', () => {
|
|
it('Only provide options.fn/inverse to block helpers', () => {
|
|
function toHaveProperties(...args: any[]) {
|
|
toHaveProperties.calls++;
|
|
const options = args[args.length - 1];
|
|
expect(options).toHaveProperty('fn');
|
|
expect(options).toHaveProperty('inverse');
|
|
return 42;
|
|
}
|
|
toHaveProperties.calls = 0;
|
|
|
|
function toNotHaveProperties(...args: any[]) {
|
|
toNotHaveProperties.calls++;
|
|
const options = args[args.length - 1];
|
|
expect(options).not.toHaveProperty('fn');
|
|
expect(options).not.toHaveProperty('inverse');
|
|
return 42;
|
|
}
|
|
toNotHaveProperties.calls = 0;
|
|
|
|
const nonBlockTemplates = ['{{foo}}', '{{foo 1 2}}'];
|
|
const blockTemplates = ['{{#foo}}42{{/foo}}', '{{#foo 1 2}}42{{/foo}}'];
|
|
|
|
for (const template of nonBlockTemplates) {
|
|
expectTemplate(template)
|
|
.withInput({
|
|
foo: toNotHaveProperties,
|
|
})
|
|
.toCompileTo('42');
|
|
|
|
expectTemplate(template).withHelper('foo', toNotHaveProperties).toCompileTo('42');
|
|
}
|
|
|
|
for (const template of blockTemplates) {
|
|
expectTemplate(template)
|
|
.withInput({
|
|
foo: toHaveProperties,
|
|
})
|
|
.toCompileTo('42');
|
|
|
|
expectTemplate(template).withHelper('foo', toHaveProperties).toCompileTo('42');
|
|
}
|
|
|
|
const factor = process.env.AST || process.env.EVAL ? 1 : 2;
|
|
expect(toNotHaveProperties.calls).toEqual(nonBlockTemplates.length * 2 * factor);
|
|
expect(toHaveProperties.calls).toEqual(blockTemplates.length * 2 * factor);
|
|
});
|
|
|
|
it('should pass expected "this" to helper functions (without input)', () => {
|
|
expectTemplate('{{hello "world" 12 true false}}')
|
|
.withHelper('hello', function (this: any, ...args: any[]) {
|
|
expect(this).toMatchInlineSnapshot(`Object {}`);
|
|
})
|
|
.toCompileTo('');
|
|
});
|
|
|
|
it('should pass expected "this" to helper functions (with input)', () => {
|
|
expectTemplate('{{hello "world" 12 true false}}')
|
|
.withHelper('hello', function (this: any, ...args: any[]) {
|
|
expect(this).toMatchInlineSnapshot(`
|
|
Object {
|
|
"people": Array [
|
|
Object {
|
|
"id": 1,
|
|
"name": "Alan",
|
|
},
|
|
Object {
|
|
"id": 2,
|
|
"name": "Yehuda",
|
|
},
|
|
],
|
|
}
|
|
`);
|
|
})
|
|
.withInput({
|
|
people: [
|
|
{ name: 'Alan', id: 1 },
|
|
{ name: 'Yehuda', id: 2 },
|
|
],
|
|
})
|
|
.toCompileTo('');
|
|
});
|
|
|
|
it('should pass expected "this" and arguments to helper functions (non-block helper)', () => {
|
|
expectTemplate('{{hello "world" 12 true false}}')
|
|
.withHelper('hello', function (this: any, ...args: any[]) {
|
|
expect(args).toMatchInlineSnapshot(`
|
|
Array [
|
|
"world",
|
|
12,
|
|
true,
|
|
false,
|
|
Object {
|
|
"data": Object {
|
|
"root": Object {
|
|
"people": Array [
|
|
Object {
|
|
"id": 1,
|
|
"name": "Alan",
|
|
},
|
|
Object {
|
|
"id": 2,
|
|
"name": "Yehuda",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"hash": Object {},
|
|
"loc": Object {
|
|
"end": Object {
|
|
"column": 31,
|
|
"line": 1,
|
|
},
|
|
"start": Object {
|
|
"column": 0,
|
|
"line": 1,
|
|
},
|
|
},
|
|
"lookupProperty": [Function],
|
|
"name": "hello",
|
|
},
|
|
]
|
|
`);
|
|
})
|
|
.withInput({
|
|
people: [
|
|
{ name: 'Alan', id: 1 },
|
|
{ name: 'Yehuda', id: 2 },
|
|
],
|
|
})
|
|
.toCompileTo('');
|
|
});
|
|
|
|
it('should pass expected "this" and arguments to helper functions (block helper)', () => {
|
|
expectTemplate('{{#hello "world" 12 true false}}{{/hello}}')
|
|
.withHelper('hello', function (this: any, ...args: any[]) {
|
|
expect(args).toMatchInlineSnapshot(`
|
|
Array [
|
|
"world",
|
|
12,
|
|
true,
|
|
false,
|
|
Object {
|
|
"data": Object {
|
|
"root": Object {
|
|
"people": Array [
|
|
Object {
|
|
"id": 1,
|
|
"name": "Alan",
|
|
},
|
|
Object {
|
|
"id": 2,
|
|
"name": "Yehuda",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
"fn": [Function],
|
|
"hash": Object {},
|
|
"inverse": [Function],
|
|
"loc": Object {
|
|
"end": Object {
|
|
"column": 42,
|
|
"line": 1,
|
|
},
|
|
"start": Object {
|
|
"column": 0,
|
|
"line": 1,
|
|
},
|
|
},
|
|
"lookupProperty": [Function],
|
|
"name": "hello",
|
|
},
|
|
]
|
|
`);
|
|
})
|
|
.withInput({
|
|
people: [
|
|
{ name: 'Alan', id: 1 },
|
|
{ name: 'Yehuda', id: 2 },
|
|
],
|
|
})
|
|
.toCompileTo('');
|
|
});
|
|
});
|
|
|
|
// Extra "blocks" tests
|
|
describe('blocks', () => {
|
|
describe('decorators', () => {
|
|
it('should only call decorator once', () => {
|
|
let calls = 0;
|
|
const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2;
|
|
expectTemplate('{{#helper}}{{*decorator}}{{/helper}}')
|
|
.withHelper('helper', () => {})
|
|
.withDecorator('decorator', () => {
|
|
calls++;
|
|
})
|
|
.toCompileTo('');
|
|
expect(calls).toEqual(callsExpected);
|
|
});
|
|
|
|
forEachCompileFunctionName((compileName) => {
|
|
it(`should call decorator again if render function is called again for #${compileName}`, () => {
|
|
global.kbnHandlebarsEnv = Handlebars.create();
|
|
|
|
kbnHandlebarsEnv!.registerDecorator('decorator', () => {
|
|
calls++;
|
|
});
|
|
|
|
const compile = kbnHandlebarsEnv![compileName].bind(kbnHandlebarsEnv);
|
|
const render = compile('{{*decorator}}');
|
|
|
|
let calls = 0;
|
|
expect(render()).toEqual('');
|
|
expect(calls).toEqual(1);
|
|
|
|
calls = 0;
|
|
expect(render()).toEqual('');
|
|
expect(calls).toEqual(1);
|
|
|
|
global.kbnHandlebarsEnv = null;
|
|
});
|
|
});
|
|
|
|
it('should pass expected options to nested decorator', () => {
|
|
expectTemplate('{{#helper}}{{*decorator foo}}{{/helper}}')
|
|
.withHelper('helper', () => {})
|
|
.withDecorator('decorator', function (fn, props, container, options) {
|
|
expect(options).toMatchInlineSnapshot(`
|
|
Object {
|
|
"args": Array [
|
|
"bar",
|
|
],
|
|
"data": Object {
|
|
"root": Object {
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
"hash": Object {},
|
|
"loc": Object {
|
|
"end": Object {
|
|
"column": 29,
|
|
"line": 1,
|
|
},
|
|
"start": Object {
|
|
"column": 11,
|
|
"line": 1,
|
|
},
|
|
},
|
|
"name": "decorator",
|
|
}
|
|
`);
|
|
})
|
|
.withInput({ foo: 'bar' })
|
|
.toCompileTo('');
|
|
});
|
|
|
|
it('should pass expected options to root decorator with no args', () => {
|
|
expectTemplate('{{*decorator}}')
|
|
.withDecorator('decorator', function (fn, props, container, options) {
|
|
expect(options).toMatchInlineSnapshot(`
|
|
Object {
|
|
"args": Array [],
|
|
"data": Object {
|
|
"root": Object {
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
"hash": Object {},
|
|
"loc": Object {
|
|
"end": Object {
|
|
"column": 14,
|
|
"line": 1,
|
|
},
|
|
"start": Object {
|
|
"column": 0,
|
|
"line": 1,
|
|
},
|
|
},
|
|
"name": "decorator",
|
|
}
|
|
`);
|
|
})
|
|
.withInput({ foo: 'bar' })
|
|
.toCompileTo('');
|
|
});
|
|
|
|
it('should pass expected options to root decorator with one arg', () => {
|
|
expectTemplate('{{*decorator foo}}')
|
|
.withDecorator('decorator', function (fn, props, container, options) {
|
|
expect(options).toMatchInlineSnapshot(`
|
|
Object {
|
|
"args": Array [
|
|
undefined,
|
|
],
|
|
"data": Object {
|
|
"root": Object {
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
"hash": Object {},
|
|
"loc": Object {
|
|
"end": Object {
|
|
"column": 18,
|
|
"line": 1,
|
|
},
|
|
"start": Object {
|
|
"column": 0,
|
|
"line": 1,
|
|
},
|
|
},
|
|
"name": "decorator",
|
|
}
|
|
`);
|
|
})
|
|
.withInput({ foo: 'bar' })
|
|
.toCompileTo('');
|
|
});
|
|
|
|
describe('return values', () => {
|
|
for (const [desc, template, result] of [
|
|
['non-block', '{{*decorator}}cont{{*decorator}}ent', 'content'],
|
|
['block', '{{#*decorator}}con{{/decorator}}tent', 'tent'],
|
|
]) {
|
|
describe(desc, () => {
|
|
const falsy = [undefined, null, false, 0, ''];
|
|
const truthy = [true, 42, 'foo', {}];
|
|
|
|
// Falsy return values from decorators are simply ignored and the
|
|
// execution falls back to default behavior which is to render the
|
|
// other parts of the template.
|
|
for (const value of falsy) {
|
|
it(`falsy value (type ${typeof value}): ${JSON.stringify(value)}`, () => {
|
|
expectTemplate(template)
|
|
.withDecorator('decorator', () => value)
|
|
.toCompileTo(result);
|
|
});
|
|
}
|
|
|
|
// Truthy return values from decorators are expected to be functions
|
|
// and the program will attempt to call them. We expect an error to
|
|
// be thrown in this case.
|
|
for (const value of truthy) {
|
|
it(`non-falsy value (type ${typeof value}): ${JSON.stringify(value)}`, () => {
|
|
expectTemplate(template)
|
|
.withDecorator('decorator', () => value)
|
|
.toThrow('is not a function');
|
|
});
|
|
}
|
|
|
|
// If the decorator return value is a custom function, its return
|
|
// value will be the final content of the template.
|
|
for (const value of [...falsy, ...truthy]) {
|
|
it(`function returning ${typeof value}: ${JSON.stringify(value)}`, () => {
|
|
expectTemplate(template)
|
|
.withDecorator('decorator', () => () => value)
|
|
.toCompileTo(value as string);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('custom return function should be called with expected arguments and its return value should be rendered in the template', () => {
|
|
it('root decorator', () => {
|
|
expectTemplate('{{*decorator}}world')
|
|
.withInput({ me: 'my' })
|
|
.withDecorator(
|
|
'decorator',
|
|
(fn): TemplateDelegate =>
|
|
(context, options) => {
|
|
expect(context).toMatchInlineSnapshot(`
|
|
Object {
|
|
"me": "my",
|
|
}
|
|
`);
|
|
expect(options).toMatchInlineSnapshot(`
|
|
Object {
|
|
"decorators": Object {
|
|
"decorator": [Function],
|
|
},
|
|
"helpers": Object {},
|
|
"partials": Object {},
|
|
}
|
|
`);
|
|
return `hello ${context.me} ${fn()}!`;
|
|
}
|
|
)
|
|
.toCompileTo('hello my world!');
|
|
});
|
|
|
|
it('decorator nested inside of array-helper', () => {
|
|
expectTemplate('{{#arr}}{{*decorator}}world{{/arr}}')
|
|
.withInput({ arr: ['my'] })
|
|
.withDecorator(
|
|
'decorator',
|
|
(fn): TemplateDelegate =>
|
|
(context, options) => {
|
|
expect(context).toMatchInlineSnapshot(`"my"`);
|
|
expect(options).toMatchInlineSnapshot(`
|
|
Object {
|
|
"blockParams": Array [
|
|
"my",
|
|
0,
|
|
],
|
|
"data": Object {
|
|
"_parent": Object {
|
|
"root": Object {
|
|
"arr": Array [
|
|
"my",
|
|
],
|
|
},
|
|
},
|
|
"first": true,
|
|
"index": 0,
|
|
"key": 0,
|
|
"last": true,
|
|
"root": Object {
|
|
"arr": Array [
|
|
"my",
|
|
],
|
|
},
|
|
},
|
|
}
|
|
`);
|
|
return `hello ${context} ${fn()}!`;
|
|
}
|
|
)
|
|
.toCompileTo('hello my world!');
|
|
});
|
|
|
|
it('decorator nested inside of custom helper', () => {
|
|
expectTemplate('{{#helper}}{{*decorator}}world{{/helper}}')
|
|
.withHelper('helper', function (options: HelperOptions) {
|
|
return options.fn('my', { foo: 'bar' } as any);
|
|
})
|
|
.withDecorator(
|
|
'decorator',
|
|
(fn): TemplateDelegate =>
|
|
(context, options) => {
|
|
expect(context).toMatchInlineSnapshot(`"my"`);
|
|
expect(options).toMatchInlineSnapshot(`
|
|
Object {
|
|
"foo": "bar",
|
|
}
|
|
`);
|
|
return `hello ${context} ${fn()}!`;
|
|
}
|
|
)
|
|
.toCompileTo('hello my world!');
|
|
});
|
|
});
|
|
|
|
it('should call multiple decorators in the same program body in the expected order and get the expected output', () => {
|
|
let decoratorCall = 0;
|
|
let progCall = 0;
|
|
expectTemplate('{{*decorator}}con{{*decorator}}tent', {
|
|
beforeRender() {
|
|
// ensure the counters are reset between EVAL/AST render calls
|
|
decoratorCall = 0;
|
|
progCall = 0;
|
|
},
|
|
})
|
|
.withInput({
|
|
decoratorCall: 0,
|
|
progCall: 0,
|
|
})
|
|
.withDecorator('decorator', (fn) => {
|
|
const decoratorCallOrder = ++decoratorCall;
|
|
const ret: TemplateDelegate = () => {
|
|
const progCallOrder = ++progCall;
|
|
return `(decorator: ${decoratorCallOrder}, prog: ${progCallOrder}, fn: "${fn()}")`;
|
|
};
|
|
return ret;
|
|
})
|
|
.toCompileTo('(decorator: 2, prog: 1, fn: "(decorator: 1, prog: 2, fn: "content")")');
|
|
});
|
|
|
|
describe('registration', () => {
|
|
beforeEach(() => {
|
|
global.kbnHandlebarsEnv = Handlebars.create();
|
|
});
|
|
|
|
afterEach(() => {
|
|
global.kbnHandlebarsEnv = null;
|
|
});
|
|
|
|
it('should be able to call decorators registered using the `registerDecorator` function', () => {
|
|
let calls = 0;
|
|
const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2;
|
|
|
|
kbnHandlebarsEnv!.registerDecorator('decorator', () => {
|
|
calls++;
|
|
});
|
|
|
|
expectTemplate('{{*decorator}}').toCompileTo('');
|
|
expect(calls).toEqual(callsExpected);
|
|
});
|
|
|
|
it('should not be able to call decorators unregistered using the `unregisterDecorator` function', () => {
|
|
let calls = 0;
|
|
|
|
kbnHandlebarsEnv!.registerDecorator('decorator', () => {
|
|
calls++;
|
|
});
|
|
|
|
kbnHandlebarsEnv!.unregisterDecorator('decorator');
|
|
|
|
expectTemplate('{{*decorator}}').toThrow('lookupProperty(...) is not a function');
|
|
expect(calls).toEqual(0);
|
|
});
|
|
});
|
|
});
|
|
});
|