const should = require('should'); const sinon = require('sinon'); const _ = require('lodash'); const helpers = require('../../../core/frontend/helpers'); const handlebars = require('../../../core/frontend/services/theme-engine/engine').handlebars; describe('{{#foreach}} helper', function () { let options; let context; let _this; let resultData; afterEach(function () { sinon.restore(); }); describe('(function call)', function () { beforeEach(function () { context = []; _this = {}; resultData = []; function fn(input, data) { resultData.push(_.cloneDeep(data)); } options = { fn: sinon.spy(fn), inverse: sinon.spy(), data: {} }; }); function runTest(self, _context, _options) { helpers.foreach.call(self, _context, _options); } it('should not populate data if no private data is supplied (array)', function () { delete options.data; options.hash = { columns: 0 }; // test with context as an array context = 'hello world this is ghost'.split(' '); runTest(_this, context, options); options.fn.called.should.be.true(); options.fn.getCalls().length.should.eql(_.size(context)); _.each(context, function (value, index) { options.fn.getCall(index).args[0].should.eql(value); should(options.fn.getCall(index).args[1].data).be.undefined(); }); }); it('should not populate data if no private data is supplied (object)', function () { delete options.data; options.hash = { columns: 0 }; context = { one: 'hello', two: 'world', three: 'this', four: 'is', five: 'ghost' }; runTest(_this, context, options); options.fn.called.should.be.true(); options.fn.getCalls().length.should.eql(_.size(context)); _.each(_.keys(context), function (value, index) { options.fn.getCall(index).args[0].should.eql(context[value]); should(options.fn.getCall(index).args[1].data).be.undefined(); }); }); it('should populate data when private data is supplied (array)', function () { const expected = [ {first: true, last: false, even: false, odd: true, rowStart: false, rowEnd: false}, {first: false, last: false, even: true, odd: false, rowStart: false, rowEnd: false}, {first: false, last: false, even: false, odd: true, rowStart: false, rowEnd: false}, {first: false, last: false, even: true, odd: false, rowStart: false, rowEnd: false}, {first: false, last: true, even: false, odd: true, rowStart: false, rowEnd: false} ]; options.hash = { columns: 0 }; context = 'hello world this is ghost'.split(' '); runTest(_this, context, options); options.fn.called.should.be.true(); options.fn.getCalls().length.should.eql(_.size(context)); _.each(context, function (value, index) { options.fn.getCall(index).args[0].should.eql(value); should(options.fn.getCall(index).args[1].data).not.be.undefined(); // Expected properties resultData[index].data.should.containEql(expected[index]); // Incrementing properties resultData[index].data.should.have.property('key', index); resultData[index].data.should.have.property('index', index); resultData[index].data.should.have.property('number', index + 1); }); resultData[_.size(context) - 1].data.should.eql(options.fn.lastCall.args[1].data); }); it('should populate data when private data is supplied (object)', function () { const expected = [ {first: true, last: false, even: false, odd: true, rowStart: false, rowEnd: false}, {first: false, last: false, even: true, odd: false, rowStart: false, rowEnd: false}, {first: false, last: false, even: false, odd: true, rowStart: false, rowEnd: false}, {first: false, last: false, even: true, odd: false, rowStart: false, rowEnd: false}, {first: false, last: true, even: false, odd: true, rowStart: false, rowEnd: false} ]; options.hash = { columns: 0 }; context = { one: 'hello', two: 'world', three: 'this', four: 'is', five: 'ghost' }; runTest(_this, context, options); options.fn.called.should.be.true(); options.fn.getCalls().length.should.eql(_.size(context)); _.each(_.keys(context), function (value, index) { options.fn.getCall(index).args[0].should.eql(context[value]); should(options.fn.getCall(index).args[1].data).not.be.undefined(); // Expected properties resultData[index].data.should.containEql(expected[index]); // Incrementing properties resultData[index].data.should.have.property('key', value); resultData[index].data.should.have.property('index', index); resultData[index].data.should.have.property('number', index + 1); }); resultData[_.size(context) - 1].data.should.eql(options.fn.lastCall.args[1].data); }); it('should handle rowStart and rowEnd for multiple columns (array)', function () { const expected = [ {first: true, last: false, even: false, odd: true, rowStart: true, rowEnd: false}, {first: false, last: false, even: true, odd: false, rowStart: false, rowEnd: true}, {first: false, last: false, even: false, odd: true, rowStart: true, rowEnd: false}, {first: false, last: false, even: true, odd: false, rowStart: false, rowEnd: true}, {first: false, last: true, even: false, odd: true, rowStart: true, rowEnd: false} ]; options.hash = { columns: 2 }; // test with context as an array context = 'hello world this is ghost'.split(' '); runTest(_this, context, options); options.fn.called.should.be.true(); options.fn.getCalls().length.should.eql(_.size(context)); _.each(context, function (value, index) { options.fn.getCall(index).args[0].should.eql(value); should(options.fn.getCall(index).args[1].data).not.be.undefined(); // Expected properties resultData[index].data.should.containEql(expected[index]); // Incrementing properties resultData[index].data.should.have.property('key', index); resultData[index].data.should.have.property('index', index); resultData[index].data.should.have.property('number', index + 1); }); resultData[_.size(context) - 1].data.should.eql(options.fn.lastCall.args[1].data); }); it('should handle rowStart and rowEnd for multiple columns (array)', function () { const expected = [ {first: true, last: false, even: false, odd: true, rowStart: true, rowEnd: false}, {first: false, last: false, even: true, odd: false, rowStart: false, rowEnd: true}, {first: false, last: false, even: false, odd: true, rowStart: true, rowEnd: false}, {first: false, last: false, even: true, odd: false, rowStart: false, rowEnd: true}, {first: false, last: true, even: false, odd: true, rowStart: true, rowEnd: false} ]; options.hash = { columns: 2 }; // test with context as an object context = { one: 'hello', two: 'world', three: 'this', four: 'is', five: 'ghost' }; runTest(_this, context, options); options.fn.called.should.be.true(); options.fn.getCalls().length.should.eql(_.size(context)); _.each(_.keys(context), function (value, index) { options.fn.getCall(index).args[0].should.eql(context[value]); should(options.fn.getCall(index).args[1].data).not.be.undefined(); // Expected properties resultData[index].data.should.containEql(expected[index]); // Incrementing properties resultData[index].data.should.have.property('key', value); resultData[index].data.should.have.property('index', index); resultData[index].data.should.have.property('number', index + 1); }); resultData[_.size(context) - 1].data.should.eql(options.fn.lastCall.args[1].data); }); it('should return the correct inverse result if no context is provided', function () { _this = 'the inverse data'; options.hash = { columns: 0 }; runTest(_this, context, options); options.fn.called.should.be.false(); options.inverse.called.should.be.true(); options.inverse.calledOnce.should.be.true(); }); }); describe('(compile)', function () { const objectHash = { posts: { first: {title: 'first'}, second: {title: 'second'}, third: {title: 'third'}, fourth: {title: 'fourth'}, fifth: {title: 'fifth'} } }; const objectHashWithVis = { posts: { first: {title: 'first', visibility: 'members', slug: 'first', html: ''}, second: {title: 'second'}, third: {title: 'third'}, fourth: {title: 'fourth'}, fifth: {title: 'fifth'} } }; const arrayHash = { posts: [ {title: 'first'}, {title: 'second'}, {title: 'third'}, {title: 'fourth'}, {title: 'fifth'} ] }; const arrayHashWithVis = { posts: [ {title: 'first', visibility: 'members', slug: 'first', html: ''}, {title: 'second'}, {title: 'third'}, {title: 'fourth'}, {title: 'fifth'} ] }; const arrayHash2 = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'}; const objectHash2 = { goodbyes: {foo: {text: 'goodbye'}, bar: {text: 'Goodbye'}, baz: {text: 'GOODBYE'}}, world: 'world' }; function shouldCompileToExpected(templateString, hash, expected) { const template = handlebars.compile(templateString); const result = template(hash); result.should.eql(expected); } before(function () { handlebars.registerHelper('foreach', helpers.foreach); }); /** Many of these are copied direct from the handlebars spec */ it('object and @key', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, objectHash, expected); }); it('@index', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('@number', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('nested @index', function () { const templateString = '{{#foreach goodbyes}}{{@index}}. {{text}}! {{#foreach ../goodbyes}}{{@index}} {{/foreach}}After {{@index}} {{/foreach}}{{@index}}cruel {{world}}!'; const expected = '0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!'; shouldCompileToExpected(templateString, arrayHash2, expected); shouldCompileToExpected(templateString, objectHash2, expected); }); it('array block params', function () { const templateString = '{{#foreach goodbyes as |value index|}}{{index}}. {{value.text}}! {{#foreach ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/foreach}} After {{index}} {{/foreach}}{{index}}cruel {{world}}!'; const expected = '0. goodbye! 0 0 0 1 0 2 After 0 1. Goodbye! 1 0 1 1 1 2 After 1 2. GOODBYE! 2 0 2 1 2 2 After 2 cruel world!'; shouldCompileToExpected(templateString, arrayHash2, expected); }); it('object block params', function () { const templateString = '{{#foreach goodbyes as |value index|}}{{index}}. {{value.text}}! {{#foreach ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/foreach}} After {{index}} {{/foreach}}{{index}}cruel {{world}}!'; const expected = 'foo. goodbye! foo foo foo bar foo baz After foo bar. Goodbye! bar foo bar bar bar baz After bar baz. GOODBYE! baz foo baz bar baz baz After baz cruel world!'; shouldCompileToExpected(templateString, objectHash2, expected); }); it('@first', function () { const templateString = '{{#foreach goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/foreach}}cruel {{world}}!'; const expected = 'goodbye! cruel world!'; shouldCompileToExpected(templateString, arrayHash2, expected); shouldCompileToExpected(templateString, objectHash2, expected); }); it('nested @first', function () { const templateString = '{{#foreach goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#foreach ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/foreach}}{{#if @first}} {{text}}!{{/if}}) {{/foreach}}cruel {{world}}!'; const expected = '(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!'; shouldCompileToExpected(templateString, arrayHash2, expected); shouldCompileToExpected(templateString, objectHash2, expected); }); it('@last', function () { const templateString = '{{#foreach goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/foreach}}cruel {{world}}!'; const expected = 'GOODBYE! cruel world!'; shouldCompileToExpected(templateString, arrayHash2, expected); shouldCompileToExpected(templateString, objectHash2, expected); }); it('nested @last', function () { const templateString = '{{#foreach goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#foreach ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/foreach}}{{#if @last}} {{text}}!{{/if}}) {{/foreach}}cruel {{world}}!'; const expected = '(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!'; shouldCompileToExpected(templateString, arrayHash2, expected); shouldCompileToExpected(templateString, objectHash2, expected); }); it('@last in foreach with limit', function () { const templateString = '{{#foreach goodbyes limit="2"}}{{#if @last}}{{text}}! {{/if}}{{/foreach}}cruel {{world}}!'; const expected = 'Goodbye! cruel world!'; shouldCompileToExpected(templateString, arrayHash2, expected); shouldCompileToExpected(templateString, objectHash2, expected); }); it('foreach with limit 1', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('foreach with limit 3', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('foreach with limit 3 and post with members visibility', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHashWithVis, expected); shouldCompileToExpected(templateString, objectHashWithVis, expected); }); it('foreach with from 2', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('foreach with to 4', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('foreach with from 2 and to 3', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('foreach with from 3 and limit 2', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('foreach with from 2, to 5 and limit 3', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('@first in foreach with from 2 and to 4', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('@last in foreach with from 2 and to 4', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('@last in foreach with from 4', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('@first in foreach with from 4', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('@last in foreach with to 4', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('@first in foreach with to 4', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('@last in foreach with from 4 and limit 3', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('@first in foreach with from 4 and limit 3', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('@last in foreach with to 4 and limit 3', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); it('@first in foreach with to 4 and limit 3', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, arrayHash, expected); shouldCompileToExpected(templateString, objectHash, expected); }); describe('Internal Tags', function () { const tagArrayHash = { tags: [ {name: 'first', visibility: 'public'}, {name: 'second', visibility: 'public'}, {name: 'third', visibility: 'internal'}, {name: 'fourth', visibility: 'public'}, {name: 'fifth', visibility: 'public'} ] }; const tagObjectHash = { tags: { first: {name: 'first', visibility: 'public'}, second: {name: 'second', visibility: 'public'}, third: {name: 'third', visibility: 'internal'}, fourth: {name: 'fourth', visibility: 'public'}, fifth: {name: 'fifth', visibility: 'public'} } }; it('will not output internal tags', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, tagObjectHash, expected); shouldCompileToExpected(templateString, tagArrayHash, expected); }); it('should still correctly apply from & limit tags', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, tagObjectHash, expected); shouldCompileToExpected(templateString, tagArrayHash, expected); }); it('should output all tags with visibility="all"', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, tagObjectHash, expected); shouldCompileToExpected(templateString, tagArrayHash, expected); }); it('should output all tags with visibility property set with visibility="public,internal"', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, tagObjectHash, expected); shouldCompileToExpected(templateString, tagArrayHash, expected); }); it('should output all tags with visibility="internal"', function () { const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, tagObjectHash, expected); shouldCompileToExpected(templateString, tagArrayHash, expected); }); it('should output nothing if all tags are internal', function () { const internalTagArrayHash = { tags: [ {name: 'first', visibility: 'internal'}, {name: 'second', visibility: 'internal'} ] }; const internalTagObjectHash = { tags: { first: {name: 'first', visibility: 'internal'}, second: {name: 'second', visibility: 'internal'} } }; const templateString = ''; const expected = ''; shouldCompileToExpected(templateString, internalTagObjectHash, expected); shouldCompileToExpected(templateString, internalTagArrayHash, expected); }); }); }); });