1 /* jshint mocha: true */
7 var ejs = require('..')
9 , read = fs.readFileSync
10 , assert = require('assert')
11 , path = require('path');
14 fs.mkdirSync(__dirname + '/tmp');
16 if (ex.code !== 'EEXIST') {
21 // From https://gist.github.com/pguillory/729616
22 function hook_stdio(stream, callback) {
23 var old_write = stream.write;
25 stream.write = (function() {
26 return function(string, encoding, fd) {
27 callback(string, encoding, fd);
32 stream.write = old_write;
37 * Load fixture `name`.
40 function fixture(name) {
41 return read('test/fixtures/' + name, 'utf8').replace(/\r/g, '').trim();
49 users.push({name: 'geddy'});
50 users.push({name: 'neil'});
51 users.push({name: 'alex'});
53 suite('ejs.compile(str, options)', function () {
54 test('compile to a function', function () {
55 var fn = ejs.compile('<p>yay</p>');
56 assert.equal(fn(), '<p>yay</p>');
59 test('empty input works', function () {
60 var fn = ejs.compile('');
61 assert.equal(fn(), '');
64 test('throw if there are syntax errors', function () {
66 ejs.compile(fixture('fail.ejs'));
69 assert.ok(err.message.indexOf('compiling ejs') > -1);
72 ejs.compile(fixture('fail.ejs'), {filename: 'fail.ejs'});
75 assert.ok(err.message.indexOf('fail.ejs') > -1);
79 throw new Error('no error reported when there should be');
82 test('allow customizing delimiter local var', function () {
84 fn = ejs.compile('<p><?= name ?></p>', {delimiter: '?'});
85 assert.equal(fn({name: 'geddy'}), '<p>geddy</p>');
87 fn = ejs.compile('<p><:= name :></p>', {delimiter: ':'});
88 assert.equal(fn({name: 'geddy'}), '<p>geddy</p>');
90 fn = ejs.compile('<p><$= name $></p>', {delimiter: '$'});
91 assert.equal(fn({name: 'geddy'}), '<p>geddy</p>');
94 test('default to using ejs.delimiter', function () {
97 fn = ejs.compile('<p><&= name &></p>');
98 assert.equal(fn({name: 'geddy'}), '<p>geddy</p>');
100 fn = ejs.compile('<p><|= name |></p>', {delimiter: '|'});
101 assert.equal(fn({name: 'geddy'}), '<p>geddy</p>');
102 delete ejs.delimiter;
105 test('have a working client option', function () {
109 fn = ejs.compile('<p><%= foo %></p>', {client: true});
111 if (!process.env.running_under_istanbul) {
112 eval('var preFn = ' + str);
113 assert.equal(preFn({foo: 'bar'}), '<p>bar</p>');
117 test('support client mode without locals', function () {
121 fn = ejs.compile('<p><%= "foo" %></p>', {client: true});
123 if (!process.env.running_under_istanbul) {
124 eval('var preFn = ' + str);
125 assert.equal(preFn(), '<p>foo</p>');
130 suite('ejs.render(str, data)', function () {
131 test('render the template', function () {
132 assert.equal(ejs.render('<p>yay</p>'), '<p>yay</p>');
135 test('empty input works', function () {
136 assert.equal(ejs.render(''), '');
139 test('undefined renders nothing escaped', function () {
140 assert.equal(ejs.render('<%= undefined %>'), '');
143 test('undefined renders nothing raw', function () {
144 assert.equal(ejs.render('<%- undefined %>'), '');
147 test('null renders nothing escaped', function () {
148 assert.equal(ejs.render('<%= null %>'), '');
151 test('null renders nothing raw', function () {
152 assert.equal(ejs.render('<%- null %>'), '');
155 test('zero-value data item renders something escaped', function () {
156 assert.equal(ejs.render('<%= 0 %>'), '0');
159 test('zero-value data object renders something raw', function () {
160 assert.equal(ejs.render('<%- 0 %>'), '0');
163 test('accept locals', function () {
164 assert.equal(ejs.render('<p><%= name %></p>', {name: 'geddy'}),
168 test('accept locals without using with() {}', function () {
169 assert.equal(ejs.render('<p><%= locals.name %></p>', {name: 'geddy'},
172 assert.throws(function() {
173 ejs.render('<p><%= name %></p>', {name: 'geddy'},
175 }, /name is not defined/);
178 test('accept custom name for locals', function () {
179 ejs.localsName = 'it';
180 assert.equal(ejs.render('<p><%= it.name %></p>', {name: 'geddy'},
183 assert.throws(function() {
184 ejs.render('<p><%= name %></p>', {name: 'geddy'},
186 }, /name is not defined/);
187 ejs.localsName = 'locals';
190 test('support caching (pass 1)', function () {
191 var file = __dirname + '/tmp/render.ejs'
192 , options = {cache: true, filename: file}
193 , out = ejs.render('<p>Old</p>', {}, options)
194 , expected = '<p>Old</p>';
195 assert.equal(out, expected);
198 test('support caching (pass 2)', function () {
199 var file = __dirname + '/tmp/render.ejs'
200 , options = {cache: true, filename: file}
201 , out = ejs.render('<p>New</p>', {}, options)
202 , expected = '<p>Old</p>';
203 assert.equal(out, expected);
207 suite('ejs.renderFile(path, [data], [options], fn)', function () {
208 test('render a file', function(done) {
209 ejs.renderFile('test/fixtures/para.ejs', function(err, html) {
213 assert.equal(html, '<p>hey</p>');
218 test('callback is async', function(done) {
220 ejs.renderFile('test/fixtures/para.ejs', function(err, html) {
224 throw new Error('not async');
229 test('accept locals', function(done) {
230 var data = {name: 'fonebone'}
231 , options = {delimiter: '$'};
232 ejs.renderFile('test/fixtures/user.ejs', data, options, function(err, html) {
236 assert.equal(html, '<h1>fonebone</h1>');
241 test('accept locals without using with() {}', function(done) {
242 var data = {name: 'fonebone'}
243 , options = {delimiter: '$', _with: false}
245 ejs.renderFile('test/fixtures/user-no-with.ejs', data, options,
246 function(err, html) {
248 if (doneCount === 2) {
254 assert.equal(html, '<h1>fonebone</h1>');
256 if (doneCount === 2) {
260 ejs.renderFile('test/fixtures/user.ejs', data, options, function(err) {
262 if (doneCount === 2) {
266 return done(new Error('error not thrown'));
269 if (doneCount === 2) {
275 test('not catch err thrown by callback', function(done) {
276 var data = {name: 'fonebone'}
277 , options = {delimiter: '$'}
280 var d = require('domain').create();
281 d.on('error', function (err) {
282 assert.equal(counter, 1);
283 assert.equal(err.message, 'Exception in callback');
287 // process.nextTick() needed to work around mochajs/mocha#513
289 // tl;dr: mocha doesn't support synchronous exception throwing in
290 // domains. Have to make it async. Ticket closed because: "domains are
292 process.nextTick(function () {
293 ejs.renderFile('test/fixtures/user.ejs', data, options,
297 assert.notEqual(err.message, 'Exception in callback');
300 throw new Error('Exception in callback');
306 test('support caching (pass 1)', function (done) {
307 var expected = '<p>Old</p>'
308 , file = __dirname + '/tmp/renderFile.ejs'
309 , options = {cache: true};
310 fs.writeFileSync(file, '<p>Old</p>');
312 ejs.renderFile(file, {}, options, function (err, out) {
316 assert.equal(out, expected);
321 test('support caching (pass 2)', function (done) {
322 var expected = '<p>Old</p>'
323 , file = __dirname + '/tmp/renderFile.ejs'
324 , options = {cache: true};
325 fs.writeFileSync(file, '<p>New</p>');
327 ejs.renderFile(file, {}, options, function (err, out) {
331 assert.equal(out, expected);
337 suite('ejs.clearCache()', function () {
338 test('work properly', function () {
339 var expected = '<p>Old</p>'
340 , file = __dirname + '/tmp/clearCache.ejs'
341 , options = {cache: true, filename: file}
342 , out = ejs.render('<p>Old</p>', {}, options);
343 assert.equal(out, expected);
347 expected = '<p>New</p>';
348 out = ejs.render('<p>New</p>', {}, options);
349 assert.equal(out, expected);
353 suite('<%', function () {
354 test('without semicolons', function () {
355 assert.equal(ejs.render(fixture('no.semicolons.ejs')),
356 fixture('no.semicolons.html'));
360 suite('<%=', function () {
361 test('escape &<script>', function () {
362 assert.equal(ejs.render('<%= name %>', {name: ' <script>'}),
363 '&nbsp;<script>');
366 test('should escape \'', function () {
367 assert.equal(ejs.render('<%= name %>', {name: 'The Jones\'s'}),
371 test('should escape &foo_bar;', function () {
372 assert.equal(ejs.render('<%= name %>', {name: '&foo_bar;'}),
377 suite('<%-', function () {
378 test('not escape', function () {
379 assert.equal(ejs.render('<%- name %>', {name: '<script>'}),
383 test('terminate gracefully if no close tag is found', function () {
385 ejs.compile('<h1>oops</h1><%- name ->');
386 throw new Error('Expected parse failure');
389 assert.ok(err.message.indexOf('Could not find matching close tag for') > -1);
394 suite('%>', function () {
395 test('produce newlines', function () {
396 assert.equal(ejs.render(fixture('newlines.ejs'), {users: users}),
397 fixture('newlines.html'));
399 test('works with `-%>` interspersed', function () {
400 assert.equal(ejs.render(fixture('newlines.mixed.ejs'), {users: users}),
401 fixture('newlines.mixed.html'));
403 test('consecutive tags work', function () {
404 assert.equal(ejs.render(fixture('consecutive-tags.ejs')),
405 fixture('consecutive-tags.html'));
409 suite('-%>', function () {
410 test('not produce newlines', function () {
411 assert.equal(ejs.render(fixture('no.newlines.ejs'), {users: users}),
412 fixture('no.newlines.html'));
414 test('stack traces work', function () {
416 ejs.render(fixture('no.newlines.error.ejs'));
419 if (e.message.indexOf('>> 4| <%= qdata %>') > -1) {
424 throw new Error('Expected ReferenceError');
428 suite('<%%', function () {
429 test('produce literals', function () {
430 assert.equal(ejs.render('<%%- "foo" %>'),
433 test('work without an end tag', function () {
434 assert.equal(ejs.render('<%%'), '<%');
435 assert.equal(ejs.render(fixture('literal.ejs'), {}, {delimiter: ' '}),
436 fixture('literal.html'));
440 suite('single quotes', function () {
441 test('not mess up the constructed function', function () {
442 assert.equal(ejs.render(fixture('single-quote.ejs')),
443 fixture('single-quote.html'));
447 suite('double quotes', function () {
448 test('not mess up the constructed function', function () {
449 assert.equal(ejs.render(fixture('double-quote.ejs')),
450 fixture('double-quote.html'));
454 suite('backslashes', function () {
455 test('escape', function () {
456 assert.equal(ejs.render(fixture('backslash.ejs')),
457 fixture('backslash.html'));
461 suite('messed up whitespace', function () {
462 test('work', function () {
463 assert.equal(ejs.render(fixture('messed.ejs'), {users: users}),
464 fixture('messed.html'));
468 suite('exceptions', function () {
469 test('produce useful stack traces', function () {
471 ejs.render(fixture('error.ejs'), {}, {filename: 'error.ejs'});
474 assert.equal(err.path, 'error.ejs');
475 assert.equal(err.stack.split('\n').slice(0, 8).join('\n'), fixture('error.out'));
478 throw new Error('no error reported when there should be');
481 test('not include fancy stack info if compileDebug is false', function () {
483 ejs.render(fixture('error.ejs'), {}, {
484 filename: 'error.ejs',
489 assert.ok(!err.path);
490 assert.notEqual(err.stack.split('\n').slice(0, 8).join('\n'), fixture('error.out'));
493 throw new Error('no error reported when there should be');
497 test('log JS source when debug is set', function (done) {
499 , needToExit = false;
500 unhook = hook_stdio(process.stdout, function (str) {
505 if (out.indexOf('__output')) {
512 ejs.render(fixture('hello-world.ejs'), {}, {debug: true});
514 teardown(function() {
523 suite('include()', function () {
524 test('include ejs', function () {
525 var file = 'test/fixtures/include-simple.ejs';
526 assert.equal(ejs.render(fixture('include-simple.ejs'), {}, {filename: file}),
527 fixture('include-simple.html'));
530 test('include ejs fails without `filename`', function () {
532 ejs.render(fixture('include-simple.ejs'));
535 assert.ok(err.message.indexOf('requires the \'filename\' option') > -1);
538 throw new Error('expected inclusion error');
541 test('strips BOM', function () {
543 ejs.render('<%- include("fixtures/includes/bom.ejs") %>',
544 {}, {filename: path.join(__dirname, 'f.ejs')}),
545 '<p>This is a file with BOM.</p>');
548 test('include ejs with locals', function () {
549 var file = 'test/fixtures/include.ejs';
550 assert.equal(ejs.render(fixture('include.ejs'), {pets: users}, {filename: file, delimiter: '@'}),
551 fixture('include.html'));
554 test('include ejs with absolute path and locals', function () {
555 var file = 'test/fixtures/include-abspath.ejs';
556 assert.equal(ejs.render(fixture('include-abspath.ejs'),
557 {dir: path.join(__dirname, 'fixtures'), pets: users, path: path},
558 {filename: file, delimiter: '@'}),
559 fixture('include.html'));
562 test('work when nested', function () {
563 var file = 'test/fixtures/menu.ejs';
564 assert.equal(ejs.render(fixture('menu.ejs'), {pets: users}, {filename: file}),
565 fixture('menu.html'));
568 test('work with a variable path', function () {
569 var file = 'test/fixtures/menu_var.ejs',
570 includePath = 'includes/menu-item';
571 assert.equal(ejs.render(fixture('menu.ejs'), {pets: users, varPath: includePath}, {filename: file}),
572 fixture('menu.html'));
575 test('include arbitrary files as-is', function () {
576 var file = 'test/fixtures/include.css.ejs';
577 assert.equal(ejs.render(fixture('include.css.ejs'), {pets: users}, {filename: file}),
578 fixture('include.css.html'));
581 test('pass compileDebug to include', function () {
582 var file = 'test/fixtures/include.ejs'
584 fn = ejs.compile(fixture('include.ejs'), {
587 , compileDebug: false
590 // Render without a required variable reference
594 assert.equal(e.message, 'pets is not defined');
598 throw new Error('no error reported when there should be');
601 test('is dynamic', function () {
602 fs.writeFileSync(__dirname + '/tmp/include.ejs', '<p>Old</p>');
603 var file = 'test/fixtures/include_cache.ejs'
604 , options = {filename: file}
605 , out = ejs.compile(fixture('include_cache.ejs'), options);
606 assert.equal(out(), '<p>Old</p>');
608 fs.writeFileSync(__dirname + '/tmp/include.ejs', '<p>New</p>');
609 assert.equal(out(), '<p>New</p>');
612 test('support caching (pass 1)', function () {
613 fs.writeFileSync(__dirname + '/tmp/include.ejs', '<p>Old</p>');
614 var file = 'test/fixtures/include_cache.ejs'
615 , options = {cache: true, filename: file}
616 , out = ejs.render(fixture('include_cache.ejs'), {}, options)
617 , expected = fixture('include_cache.html');
618 assert.equal(out, expected);
621 test('support caching (pass 2)', function () {
622 fs.writeFileSync(__dirname + '/tmp/include.ejs', '<p>New</p>');
623 var file = 'test/fixtures/include_cache.ejs'
624 , options = {cache: true, filename: file}
625 , out = ejs.render(fixture('include_cache.ejs'), {}, options)
626 , expected = fixture('include_cache.html');
627 assert.equal(out, expected);
631 suite('preprocessor include', function () {
632 test('work', function () {
633 var file = 'test/fixtures/include_preprocessor.ejs';
634 assert.equal(ejs.render(fixture('include_preprocessor.ejs'), {pets: users}, {filename: file, delimiter: '@'}),
635 fixture('include_preprocessor.html'));
638 test('fails without `filename`', function () {
640 ejs.render(fixture('include_preprocessor.ejs'), {pets: users}, {delimiter: '@'});
643 assert.ok(err.message.indexOf('requires the \'filename\' option') > -1);
646 throw new Error('expected inclusion error');
649 test('strips BOM', function () {
651 ejs.render('<% include fixtures/includes/bom.ejs %>',
652 {}, {filename: path.join(__dirname, 'f.ejs')}),
653 '<p>This is a file with BOM.</p>');
656 test('work when nested', function () {
657 var file = 'test/fixtures/menu_preprocessor.ejs';
658 assert.equal(ejs.render(fixture('menu_preprocessor.ejs'), {pets: users}, {filename: file}),
659 fixture('menu_preprocessor.html'));
662 test('include arbitrary files as-is', function () {
663 var file = 'test/fixtures/include_preprocessor.css.ejs';
664 assert.equal(ejs.render(fixture('include_preprocessor.css.ejs'), {pets: users}, {filename: file}),
665 fixture('include_preprocessor.css.html'));
668 test('pass compileDebug to include', function () {
669 var file = 'test/fixtures/include_preprocessor.ejs'
671 fn = ejs.compile(fixture('include_preprocessor.ejs'), {
674 , compileDebug: false
677 // Render without a required variable reference
681 assert.equal(e.message, 'pets is not defined');
685 throw new Error('no error reported when there should be');
688 test('is static', function () {
689 fs.writeFileSync(__dirname + '/tmp/include_preprocessor.ejs', '<p>Old</p>');
690 var file = 'test/fixtures/include_preprocessor_cache.ejs'
691 , options = {filename: file}
692 , out = ejs.compile(fixture('include_preprocessor_cache.ejs'), options);
693 assert.equal(out(), '<p>Old</p>');
695 fs.writeFileSync(__dirname + '/tmp/include_preprocessor.ejs', '<p>New</p>');
696 assert.equal(out(), '<p>Old</p>');
699 test('support caching (pass 1)', function () {
700 fs.writeFileSync(__dirname + '/tmp/include_preprocessor.ejs', '<p>Old</p>');
701 var file = 'test/fixtures/include_preprocessor_cache.ejs'
702 , options = {cache: true, filename: file}
703 , out = ejs.render(fixture('include_preprocessor_cache.ejs'), {}, options)
704 , expected = fixture('include_preprocessor_cache.html');
705 assert.equal(out, expected);
708 test('support caching (pass 2)', function () {
709 fs.writeFileSync(__dirname + '/tmp/include_preprocessor.ejs', '<p>New</p>');
710 var file = 'test/fixtures/include_preprocessor_cache.ejs'
711 , options = {cache: true, filename: file}
712 , out = ejs.render(fixture('include_preprocessor_cache.ejs'), {}, options)
713 , expected = fixture('include_preprocessor_cache.html');
714 assert.equal(out, expected);
718 suite('comments', function () {
719 test('fully render with comments removed', function () {
720 assert.equal(ejs.render(fixture('comments.ejs')),
721 fixture('comments.html'));
725 suite('require', function () {
727 // Only works with inline/preprocessor includes
728 test('allow ejs templates to be required as node modules', function () {
729 var file = 'test/fixtures/include_preprocessor.ejs'
730 , template = require(__dirname + '/fixtures/menu_preprocessor.ejs');
731 if (!process.env.running_under_istanbul) {
732 assert.equal(template({filename: file, pets: users}),
733 fixture('menu_preprocessor.html'));
738 suite('examples', function () {
740 fs.readdirSync('examples').forEach(function (f) {
741 if (!/\.js$/.test(f)) {
744 suite(f, function () {
745 test('doesn\'t throw any errors', function () {
746 var stderr = hook_stdio(process.stderr, noop)
747 , stdout = hook_stdio(process.stdout, noop);
749 require('../examples/' + f);