aboutsummaryrefslogtreecommitdiff
path: root/dev/lib/handlebars/index.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'dev/lib/handlebars/index.test.ts')
-rw-r--r--dev/lib/handlebars/index.test.ts567
1 files changed, 567 insertions, 0 deletions
diff --git a/dev/lib/handlebars/index.test.ts b/dev/lib/handlebars/index.test.ts
new file mode 100644
index 00000000..ed607db1
--- /dev/null
+++ b/dev/lib/handlebars/index.test.ts
@@ -0,0 +1,567 @@
+/*
+ * 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('&lt;foo&gt;');
+
+ expectTemplate('{{value}}')
+ .withCompileOptions({ noEscape: false })
+ .withInput({ value: '<foo>' })
+ .toCompileTo('&lt;foo&gt;');
+
+ 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);
+ });
+ });
+ });
+});