From ef79eab44bfd000792c610b968b5ceefd41e76a0 Mon Sep 17 00:00:00 2001 From: Darius Jahandarie Date: Sat, 4 Nov 2023 18:45:57 +0900 Subject: Modernize codebase - Use ES modules - Remove vendored libs and build them from npm using esbuild - Switch from JSZip to zip.js --- .../handlebars/src/spec/index.regressions.test.ts | 379 +++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 dev/lib/handlebars/src/spec/index.regressions.test.ts (limited to 'dev/lib/handlebars/src/spec/index.regressions.test.ts') diff --git a/dev/lib/handlebars/src/spec/index.regressions.test.ts b/dev/lib/handlebars/src/spec/index.regressions.test.ts new file mode 100644 index 00000000..fc2065fe --- /dev/null +++ b/dev/lib/handlebars/src/spec/index.regressions.test.ts @@ -0,0 +1,379 @@ +/* + * 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, forEachCompileFunctionName } from '../__jest__/test_bench'; + +describe('Regressions', () => { + it('GH-94: Cannot read property of undefined', () => { + expectTemplate('{{#books}}{{title}}{{author.name}}{{/books}}') + .withInput({ + books: [ + { + title: 'The origin of species', + author: { + name: 'Charles Darwin', + }, + }, + { + title: 'Lazarillo de Tormes', + }, + ], + }) + .toCompileTo('The origin of speciesCharles DarwinLazarillo de Tormes'); + }); + + it("GH-150: Inverted sections print when they shouldn't", () => { + const string = '{{^set}}not set{{/set}} :: {{#set}}set{{/set}}'; + expectTemplate(string).toCompileTo('not set :: '); + expectTemplate(string).withInput({ set: undefined }).toCompileTo('not set :: '); + expectTemplate(string).withInput({ set: false }).toCompileTo('not set :: '); + expectTemplate(string).withInput({ set: true }).toCompileTo(' :: set'); + }); + + it('GH-158: Using array index twice, breaks the template', () => { + expectTemplate('{{arr.[0]}}, {{arr.[1]}}') + .withInput({ arr: [1, 2] }) + .toCompileTo('1, 2'); + }); + + it("bug reported by @fat where lambdas weren't being properly resolved", () => { + const string = + 'This is a slightly more complicated {{thing}}..\n' + + '{{! Just ignore this business. }}\n' + + 'Check this out:\n' + + '{{#hasThings}}\n' + + '.\n' + + '{{/hasThings}}\n' + + '{{^hasThings}}\n' + + '\n' + + 'Nothing to check out...\n' + + '{{/hasThings}}'; + + const data = { + thing() { + return 'blah'; + }, + things: [ + { className: 'one', word: '@fat' }, + { className: 'two', word: '@dhg' }, + { className: 'three', word: '@sayrer' }, + ], + hasThings() { + return true; + }, + }; + + const output = + 'This is a slightly more complicated blah..\n' + + 'Check this out:\n' + + '.\n'; + + expectTemplate(string).withInput(data).toCompileTo(output); + }); + + it('GH-408: Multiple loops fail', () => { + expectTemplate('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}') + .withInput([ + { name: 'John Doe', location: { city: 'Chicago' } }, + { name: 'Jane Doe', location: { city: 'New York' } }, + ]) + .toCompileTo('John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe'); + }); + + it('GS-428: Nested if else rendering', () => { + const succeedingTemplate = + '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; + const failingTemplate = + '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; + + const helpers = { + blk(block: HelperOptions) { + return block.fn(''); + }, + inverse(block: HelperOptions) { + return block.inverse(''); + }, + }; + + expectTemplate(succeedingTemplate).withHelpers(helpers).toCompileTo(' Expected '); + expectTemplate(failingTemplate).withHelpers(helpers).toCompileTo(' Expected '); + }); + + it('GH-458: Scoped this identifier', () => { + expectTemplate('{{./foo}}').withInput({ foo: 'bar' }).toCompileTo('bar'); + }); + + it('GH-375: Unicode line terminators', () => { + expectTemplate('\u2028').toCompileTo('\u2028'); + }); + + it('GH-534: Object prototype aliases', () => { + /* eslint-disable no-extend-native */ + // @ts-expect-error + Object.prototype[0xd834] = true; + + expectTemplate('{{foo}}').withInput({ foo: 'bar' }).toCompileTo('bar'); + + // @ts-expect-error + delete Object.prototype[0xd834]; + /* eslint-enable no-extend-native */ + }); + + it('GH-437: Matching escaping', () => { + expectTemplate('{{{a}}').toThrow(/Parse error on/); + expectTemplate('{{a}}}').toThrow(/Parse error on/); + }); + + it('GH-676: Using array in escaping mustache fails', () => { + const data = { arr: [1, 2] }; + expectTemplate('{{arr}}').withInput(data).toCompileTo(data.arr.toString()); + }); + + it('Mustache man page', () => { + expectTemplate( + 'Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}' + ) + .withInput({ + name: 'Chris', + value: 10000, + taxed_value: 10000 - 10000 * 0.4, + in_ca: true, + }) + .toCompileTo('Hello Chris. You have just won $10000! Well, $6000, after taxes.'); + }); + + it('GH-731: zero context rendering', () => { + expectTemplate('{{#foo}} This is {{bar}} ~ {{/foo}}') + .withInput({ + foo: 0, + bar: 'OK', + }) + .toCompileTo(' This is ~ '); + }); + + it('GH-820: zero pathed rendering', () => { + expectTemplate('{{foo.bar}}').withInput({ foo: 0 }).toCompileTo(''); + }); + + it('GH-837: undefined values for helpers', () => { + expectTemplate('{{str bar.baz}}') + .withHelpers({ + str(value) { + return value + ''; + }, + }) + .toCompileTo('undefined'); + }); + + it('GH-926: Depths and de-dupe', () => { + expectTemplate( + '{{#if dater}}{{#each data}}{{../name}}{{/each}}{{else}}{{#each notData}}{{../name}}{{/each}}{{/if}}' + ) + .withInput({ + name: 'foo', + data: [1], + notData: [1], + }) + .toCompileTo('foo'); + }); + + it('GH-1021: Each empty string key', () => { + expectTemplate('{{#each data}}Key: {{@key}}\n{{/each}}') + .withInput({ + data: { + '': 'foo', + name: 'Chris', + value: 10000, + }, + }) + .toCompileTo('Key: \nKey: name\nKey: value\n'); + }); + + it('GH-1054: Should handle simple safe string responses', () => { + expectTemplate('{{#wrap}}{{>partial}}{{/wrap}}') + .withHelpers({ + wrap(options: HelperOptions) { + return new Handlebars.SafeString(options.fn()); + }, + }) + .withPartials({ + partial: '{{#wrap}}{{/wrap}}', + }) + .toCompileTo(''); + }); + + it('GH-1065: Sparse arrays', () => { + const array = []; + array[1] = 'foo'; + array[3] = 'bar'; + expectTemplate('{{#each array}}{{@index}}{{.}}{{/each}}') + .withInput({ array }) + .toCompileTo('1foo3bar'); + }); + + it('GH-1093: Undefined helper context', () => { + expectTemplate('{{#each obj}}{{{helper}}}{{.}}{{/each}}') + .withInput({ obj: { foo: undefined, bar: 'bat' } }) + .withHelpers({ + helper(this: any) { + // It's valid to execute a block against an undefined context, but + // helpers can not do so, so we expect to have an empty object here; + for (const name in this) { + if (Object.prototype.hasOwnProperty.call(this, name)) { + return 'found'; + } + } + // And to make IE happy, check for the known string as length is not enumerated. + return this === 'bat' ? 'found' : 'not'; + }, + }) + .toCompileTo('notfoundbat'); + }); + + it('should support multiple levels of inline partials', () => { + expectTemplate('{{#> layout}}{{#*inline "subcontent"}}subcontent{{/inline}}{{/layout}}') + .withPartials({ + doctype: 'doctype{{> content}}', + layout: '{{#> doctype}}{{#*inline "content"}}layout{{> subcontent}}{{/inline}}{{/doctype}}', + }) + .toCompileTo('doctypelayoutsubcontent'); + }); + + it('GH-1089: should support failover content in multiple levels of inline partials', () => { + expectTemplate('{{#> layout}}{{/layout}}') + .withPartials({ + doctype: 'doctype{{> content}}', + layout: + '{{#> doctype}}{{#*inline "content"}}layout{{#> subcontent}}subcontent{{/subcontent}}{{/inline}}{{/doctype}}', + }) + .toCompileTo('doctypelayoutsubcontent'); + }); + + it('GH-1099: should support greater than 3 nested levels of inline partials', () => { + expectTemplate('{{#> layout}}Outer{{/layout}}') + .withPartials({ + layout: '{{#> inner}}Inner{{/inner}}{{> @partial-block }}', + inner: '', + }) + .toCompileTo('Outer'); + }); + + it('GH-1135 : Context handling within each iteration', () => { + expectTemplate( + '{{#each array}}\n' + + ' 1. IF: {{#if true}}{{../name}}-{{../../name}}-{{../../../name}}{{/if}}\n' + + ' 2. MYIF: {{#myif true}}{{../name}}={{../../name}}={{../../../name}}{{/myif}}\n' + + '{{/each}}' + ) + .withInput({ array: [1], name: 'John' }) + .withHelpers({ + myif(conditional, options: HelperOptions) { + if (conditional) { + return options.fn(this); + } else { + return options.inverse(this); + } + }, + }) + .toCompileTo(' 1. IF: John--\n' + ' 2. MYIF: John==\n'); + }); + + it('GH-1186: Support block params for existing programs', () => { + expectTemplate( + '{{#*inline "test"}}{{> @partial-block }}{{/inline}}' + + '{{#>test }}{{#each listOne as |item|}}{{ item }}{{/each}}{{/test}}' + + '{{#>test }}{{#each listTwo as |item|}}{{ item }}{{/each}}{{/test}}' + ) + .withInput({ + listOne: ['a'], + listTwo: ['b'], + }) + .toCompileTo('ab'); + }); + + it('GH-1319: "unless" breaks when "each" value equals "null"', () => { + expectTemplate('{{#each list}}{{#unless ./prop}}parent={{../value}} {{/unless}}{{/each}}') + .withInput({ + value: 'parent', + list: [null, 'a'], + }) + .toCompileTo('parent=parent parent=parent '); + }); + + it('GH-1341: 4.0.7 release breaks {{#if @partial-block}} usage', () => { + expectTemplate('template {{>partial}} template') + .withPartials({ + partialWithBlock: '{{#if @partial-block}} block {{> @partial-block}} block {{/if}}', + partial: '{{#> partialWithBlock}} partial {{/partialWithBlock}}', + }) + .toCompileTo('template block partial block template'); + }); + + it('should allow hash with protected array names', () => { + expectTemplate('{{helpa length="foo"}}') + .withInput({ array: [1], name: 'John' }) + .withHelpers({ + helpa(options: HelperOptions) { + return options.hash.length; + }, + }) + .toCompileTo('foo'); + }); + + describe('GH-1598: Performance degradation for partials since v4.3.0', () => { + let newHandlebarsInstance: typeof Handlebars; + let spy: jest.SpyInstance; + beforeEach(() => { + newHandlebarsInstance = Handlebars.create(); + }); + afterEach(() => { + spy.mockRestore(); + }); + + forEachCompileFunctionName((compileName) => { + it(`should only compile global partials once when calling #${compileName}`, () => { + const compile = newHandlebarsInstance[compileName].bind(newHandlebarsInstance); + let calls; + switch (compileName) { + case 'compile': + spy = jest.spyOn(newHandlebarsInstance, 'template'); + calls = 3; + break; + case 'compileAST': + spy = jest.spyOn(newHandlebarsInstance, 'compileAST'); + calls = 2; + break; + } + newHandlebarsInstance.registerPartial({ + dude: 'I am a partial', + }); + const string = 'Dudes: {{> dude}} {{> dude}}'; + compile(string)(); // This should compile template + partial once + compile(string)(); // This should only compile template + expect(spy).toHaveBeenCalledTimes(calls); + spy.mockRestore(); + }); + }); + }); + + describe("GH-1639: TypeError: Cannot read property 'apply' of undefined\" when handlebars version > 4.6.0 (undocumented, deprecated usage)", () => { + it('should treat undefined helpers like non-existing helpers', () => { + expectTemplate('{{foo}}') + .withHelper('foo', undefined as any) + .withInput({ foo: 'bar' }) + .toCompileTo('bar'); + }); + }); +}); -- cgit v1.2.3