'use strict'; var fs = require('fs'); var path = require('path'); var assert = require('assert'); var cp = require('child_process'); var mkdirp = require('mkdirp'); var rimraf = require('rimraf'); // Sets directory to output coverage data to // Incremented every time getRunner() is called. var covCount = 1; var isIstanbul = process.env.running_under_istanbul; /* * I/O utilities for temporary directory. */ function j(paths) { return path.join.apply(path, paths); } function t(paths) { paths = Array.isArray(paths) ? paths : [paths]; var args = [__dirname, 'temp'].concat(paths); return j(args); } function r(paths) { return fs.readFileSync(t(paths), 'utf8'); } function rs(paths) { return fs.createReadStream(t(paths)); } function w(paths, content) { return fs.writeFileSync(t(paths), content); } function a(paths, content) { return fs.appendFileSync(t(paths), content); } function u(paths) { return fs.unlinkSync(t(paths)); } /** * Gets an array containing the routine to run the pug CLI. If this file is * being processed with istanbul then this function will return a routine * asking istanbul to store coverage data to a unique directory * (cov-pt/). */ function getRunner() { var pugExe = j([__dirname, '..', 'index.js']); if (!isIstanbul) return [process.argv[0], [pugExe]]; else { return [ 'istanbul', [ 'cover', '--print', 'none', '--report', 'none', '--root', process.cwd(), '--dir', process.cwd() + '/cov-pt' + (covCount++), pugExe, '--' ] ]; } } /* * Run Pug CLI. * * @param args Array of arguments * @param [stdin] Stream of standard input * @param callback Function to call when the process finishes */ function run(args, stdin, callback) { if (arguments.length === 2) { callback = stdin; stdin = null; } var runner = getRunner(); var proc = cp.execFile(runner[0], runner[1].concat(args), { cwd: t([]) }, callback); if (stdin) stdin.pipe(proc.stdin); } /** * Set timing limits for a test case */ function timing(testCase) { if (isIstanbul) { testCase.timeout(20000); testCase.slow(3000); } else { testCase.timeout(12500); testCase.slow(2000); } } /* * Make temporary directories */ rimraf.sync(t([])); mkdirp.sync(t(['depwatch'])); mkdirp.sync(t(['inputs', 'level-1-1'])); mkdirp.sync(t(['inputs', 'level-1-2'])); mkdirp.sync(t(['outputs', 'level-1-1'])); mkdirp.sync(t(['outputs', 'level-1-2'])); /* * CLI utilities */ describe('miscellanea', function () { timing(this); it('--version', function (done) { run(['-V'], function (err, stdout) { if (err) done(err); assert.equal(stdout.trim(), 'pug version: ' + require('pug/package.json').version + '\npug-cli version: ' + require('../package.json').version); run(['--version'], function (err, stdout) { if (err) done(err); assert.equal(stdout.trim(), 'pug version: ' + require('pug/package.json').version + '\npug-cli version: ' + require('../package.json').version); done() }); }); }); it('--help', function (done) { // only check that it doesn't crash run(['-h'], function (err, stdout) { if (err) done(err); run(['--help'], function (err, stdout) { if (err) done(err); done() }); }); }); }); describe('HTML output', function () { timing(this); it('works', function (done) { w('input.pug', '.foo bar'); w('input.html', '

output not written

'); run(['--no-debug', 'input.pug'], function (err) { if (err) return done(err); var html = r('input.html'); assert(html === '
bar
'); done(); }); }); it('--extension', function (done) { w('input.pug', '.foo bar'); w('input.special-html', '

output not written

'); run(['--no-debug', '-E', 'special-html', 'input.pug'], function (err) { if (err) return done(err); var html = r('input.special-html'); assert(html === '
bar
'); done(); }); }); it('--basedir', function (done) { w('input.pug', 'extends /dependency1.pug'); w('input.html', '

output not written

'); run(['--no-debug', '-b', j([__dirname, 'dependencies']), 'input.pug'], function (err, stdout) { if (err) return done(err); var html = r('input.html'); assert.equal(html, ''); done(); }); }); context('--obj', function () { it('JavaScript syntax works', function (done) { w('input.pug', '.foo= loc'); w('input.html', '

output not written

'); run(['--no-debug', '--obj', "{'loc':'str'}", 'input.pug'], function (err) { if (err) return done(err); var html = r('input.html'); assert(html === '
str
'); done(); }); }); it('JavaScript syntax does not accept UTF newlines', function (done) { w('input.pug', '.foo= loc'); w('input.html', '

output not written

'); run(['--no-debug', '--obj', "{'loc':'st\u2028r'}", 'input.pug'], function (err) { if (!err) return done(new Error('expecting error')); done(); }); }); it('JSON syntax accept UTF newlines', function (done) { w('input.pug', '.foo= loc'); w('input.html', '

output not written

'); run(['--no-debug', '--obj', '{"loc":"st\u2028r"}', 'input.pug'], function (err) { if (err) return done(err); var html = r('input.html'); assert.equal(html, '
st\u2028r
'); done(); }); }); it('JSON file', function (done) { w('obj.json', '{"loc":"str"}'); w('input.pug', '.foo= loc'); w('input.html', '

output not written

'); run(['--no-debug', '--obj', 'obj.json', 'input.pug'], function (err) { if (err) return done(err); var html = r('input.html'); assert(html === '
str
'); done(); }); }); it('JavaScript module', function (done) { w('obj.js', 'module.exports = {loc: "str"};'); w('input.pug', '.foo= loc'); w('input.html', '

output not written

'); run(['--no-debug', '--obj', 'obj.js', 'input.pug'], function (err) { if (err) return done(err); var html = r('input.html'); assert(html === '
str
'); done(); }); }); }); it('stdio', function (done) { w('input.pug', '.foo bar'); run(['--no-debug'], rs('input.pug'), function (err, stdout, stderr) { if (err) return done(err); assert(stdout === '
bar
'); done(); }); }); context('--out', function () { it('works', function (done) { w('input.pug', '.foo bar'); w('input.html', '

output not written

'); run(['--no-debug', '--out', 'outputs', 'input.pug'], function (err) { if (err) return done(err); var html = r(['outputs', 'input.html']); assert(html === '
bar
'); done(); }); }); it('works when input is a directory', function (done) { w(['inputs', 'input.pug'], '.foo bar 1'); w(['inputs', 'level-1-1', 'input.pug'], '.foo bar 1-1'); w(['inputs', 'level-1-2', 'input.pug'], '.foo bar 1-2'); w(['outputs', 'input.html'], 'BIG FAT HEN 1'); w(['outputs', 'level-1-1', 'input.html'], 'BIG FAT HEN 1-1'); w(['outputs', 'level-1-2', 'input.html'], 'BIG FAT HEN 1-2'); run(['--no-debug', '--hierarchy', '--out', 'outputs', 'inputs'], function (err) { if (err) return done(err); var html = r(['outputs', 'input.html']); assert(html === '
bar 1
'); var html = r(['outputs', 'level-1-1', 'input.html']); assert(html === '
bar 1-1
'); var html = r(['outputs', 'level-1-2', 'input.html']); assert(html === '
bar 1-2
'); done(); }); }); }); it('--silent', function (done) { w('input.pug', '.foo bar'); w('input.html', '

output not written

'); run(['--no-debug', '-s', 'input.pug'], function (err, stdout) { if (err) return done(err); var html = r('input.html'); assert.equal(html, '
bar
'); assert.equal(stdout, ''); w('input.html', '

output not written

'); run(['--no-debug', '--silent', 'input.pug'], function (err, stdout) { if (err) return done(err); var html = r('input.html'); assert.equal(html, '
bar
'); assert.equal(stdout, ''); done(); }); }); }); }); describe('client JavaScript output', function () { timing(this); it('works', function (done) { w('input.pug', '.foo bar'); w('input.js', 'throw new Error("output not written");'); run(['--no-debug', '--client', 'input.pug'], function (err) { if (err) return done(err); var template = Function('', r('input.js') + ';return template;')(); assert(template() === '
bar
'); done(); }); }); it('--name', function (done) { w('input.pug', '.foo bar'); w('input.js', 'throw new Error("output not written");'); run(['--no-debug', '--client', '--name', 'myTemplate', 'input.pug'], function (err) { if (err) return done(err); var template = Function('', r('input.js') + ';return myTemplate;')(); assert(template() === '
bar
'); done(); }); }); it('--name --extension', function (done) { w('input.pug', '.foo bar'); w('input.special-js', 'throw new Error("output not written");'); run(['--no-debug', '--client', '-E', 'special-js', 'input.pug'], function (err) { if (err) return done(err); var template = Function('', r('input.special-js') + ';return template;')(); assert(template() === '
bar
'); done(); }); }); it('stdio', function (done) { w('input.pug', '.foo bar'); w('input.js', 'throw new Error("output not written");'); run(['--no-debug', '--client'], rs('input.pug'), function (err, stdout) { if (err) return done(err); var template = Function('', stdout + ';return template;')(); assert(template() === '
bar
'); done(); }); }); it('--name-after-file', function (done) { w('input-file.pug', '.foo bar'); w('input-file.js', 'throw new Error("output not written");'); run(['--no-debug', '--client', '--name-after-file', 'input-file.pug'], function (err, stdout, stderr) { if (err) return done(err); var template = Function('', r('input-file.js') + ';return inputFileTemplate;')(); assert(template() === '
bar
'); return done(); }); }); it('--name-after-file _InPuTwIthWEiRdNaMME.pug', function (done) { w('_InPuTwIthWEiRdNaMME.pug', '.foo bar'); w('_InPuTwIthWEiRdNaMME.js', 'throw new Error("output not written");'); run(['--no-debug', '--client', '--name-after-file', '_InPuTwIthWEiRdNaMME.pug'], function (err, stdout, stderr) { if (err) return done(err); var template = Function('', r('_InPuTwIthWEiRdNaMME.js') + ';return InputwithweirdnammeTemplate;')(); assert(template() === '
bar
'); return done(); }); }); }); describe('--watch', function () { var watchProc; var stdout = ''; function cleanup() { stdout = ''; if (!watchProc) return; watchProc.stderr.removeAllListeners('data'); watchProc.stdout.removeAllListeners('data'); watchProc.removeAllListeners('error'); watchProc.removeAllListeners('close'); } after(function () { cleanup(); watchProc.kill('SIGINT'); watchProc = null; }); beforeEach(cleanup); afterEach(function (done) { // pug --watch can only detect changes that are at least 1 second apart setTimeout(done, 1000); }); it('pass 1: initial compilation', function (done) { timing(this); w('input-file.pug', '.foo bar'); w('input-file.js', 'throw new Error("output not written (pass 1)");'); var cmd = getRunner(); cmd[1].push('--no-debug', '--client', '--name-after-file', '--watch', 'input-file.pug'); watchProc = cp.spawn(cmd[0], cmd[1], { cwd: t([]) }); watchProc.stdout.setEncoding('utf8'); watchProc.stderr.setEncoding('utf8'); watchProc.on('error', done); watchProc.stdout.on('data', function(buf) { stdout += buf; if (/rendered/.test(stdout)) { cleanup(); var template = Function('', r('input-file.js') + ';return inputFileTemplate;')(); assert(template() === '
bar
'); return done(); } }); }); it('pass 2: change the file', function (done) { w('input-file.js', 'throw new Error("output not written (pass 2)");'); watchProc.on('error', done); watchProc.stdout.on('data', function(buf) { stdout += buf; if (/rendered/.test(stdout)) { cleanup(); var template = Function('', r('input-file.js') + ';return inputFileTemplate;')(); assert(template() === '
baz
'); return done(); } }); w('input-file.pug', '.foo baz'); }); it('pass 3: remove the file then add it back', function (done) { w('input-file.js', 'throw new Error("output not written (pass 3)");'); watchProc.on('error', done) watchProc.stdout.on('data', function(buf) { stdout += buf; if (/rendered/.test(stdout)) { cleanup(); var template = Function('', r('input-file.js') + ';return inputFileTemplate;')(); assert(template() === '
bat
'); return done(); } }); u('input-file.pug'); setTimeout(function () { w('input-file.pug', '.foo bat'); }, 250); }); it('pass 4: intentional errors in the pug file', function (done) { var stderr = ''; var errored = false; watchProc.on('error', done); watchProc.on('close', function() { errored = true; return done(new Error('Pug should not terminate in watch mode')); }); watchProc.stdout.on('data', function(buf) { stdout += buf; if (/rendered/.test(stdout)) { stdout = ''; return done(new Error('Pug compiles an erroneous file w/o error')); } }); watchProc.stderr.on('data', function(buf) { stderr += buf; if (!/Invalid indentation/.test(stderr)) return; stderr = ''; var template = Function('', r('input-file.js') + ';return inputFileTemplate;')(); assert(template() === '
bat
'); watchProc.stderr.removeAllListeners('data'); watchProc.stdout.removeAllListeners('data'); watchProc.removeAllListeners('error'); watchProc.removeAllListeners('exit'); // The stderr event will always fire sooner than the close event. // Wait for it. setTimeout(function() { if (!errored) done(); }, 100); }); w('input-file.pug', [ 'div', ' div', '\tarticle' ].join('\n')); }); }); describe('--watch with dependencies', function () { var watchProc; var stdout = ''; before(function () { function copy(file) { w(['depwatch', file], fs.readFileSync(j([__dirname, 'dependencies', file]))); } copy('include2.pug'); copy('dependency2.pug'); copy('dependency3.pug'); }); function cleanup() { stdout = ''; if (!watchProc) return; watchProc.stderr.removeAllListeners('data'); watchProc.stdout.removeAllListeners('data'); watchProc.removeAllListeners('error'); watchProc.removeAllListeners('close'); } after(function () { cleanup(); watchProc.kill('SIGINT'); watchProc = null; }); beforeEach(cleanup); afterEach(function (done) { // pug --watch can only detect changes that are at least 1 second apart setTimeout(done, 1000); }); it('pass 1: initial compilation', function (done) { timing(this); w(['depwatch', 'include2.html'], 'output not written (pass 1)'); w(['depwatch', 'dependency2.html'], 'output not written (pass 1)'); var cmd = getRunner(); cmd[1].push('--watch', 'include2.pug', 'dependency2.pug'); watchProc = cp.spawn(cmd[0], cmd[1], { cwd: t('depwatch') }); watchProc.stdout.setEncoding('utf8'); watchProc.stderr.setEncoding('utf8'); watchProc.on('error', done); watchProc.stdout.on('data', function(buf) { stdout += buf; if ((stdout.match(/rendered/g) || []).length === 2) { cleanup(); var output = r(['depwatch', 'include2.html']); assert.equal(output.trim(), 'dependency3'); output = r(['depwatch', 'dependency2.html']); assert.equal(output.trim(), 'dependency3'); return done(); } }); }); it('pass 2: change a dependency', function (done) { timing(this); w(['depwatch', 'include2.html'], 'output not written (pass 2)'); w(['depwatch', 'dependency2.html'], 'output not written (pass 2)'); watchProc.on('error', done); watchProc.stdout.on('data', function(buf) { stdout += buf; if ((stdout.match(/rendered/g) || []).length === 2) { cleanup(); var output = r(['depwatch', 'include2.html']); assert.equal(output.trim(), 'dependency3

Hey

'); output = r(['depwatch', 'dependency2.html']); assert.equal(output.trim(), 'dependency3

Hey

'); return done(); } }); a(['depwatch', 'dependency2.pug'], '\np Hey\n'); }); it('pass 3: change a deeper dependency', function (done) { timing(this); w(['depwatch', 'include2.html'], 'output not written (pass 3)'); w(['depwatch', 'dependency2.html'], 'output not written (pass 3)'); watchProc.on('error', done) watchProc.stdout.on('data', function(buf) { stdout += buf; if ((stdout.match(/rendered/g) || []).length === 2) { cleanup(); var output = r(['depwatch', 'include2.html']); assert.equal(output.trim(), 'dependency3

Foo

Hey

'); output = r(['depwatch', 'dependency2.html']); assert.equal(output.trim(), 'dependency3

Foo

Hey

'); return done(); } }); a(['depwatch', 'dependency3.pug'], '\np Foo\n'); }); it('pass 4: change main file', function (done) { timing(this); w(['depwatch', 'include2.html'], 'output not written (pass 4)'); w(['depwatch', 'dependency2.html'], 'output not written (pass 4)'); watchProc.on('error', done); watchProc.stdout.on('data', function(buf) { stdout += buf; if ((stdout.match(/rendered/g) || []).length === 1) { cleanup(); var output = r(['depwatch', 'include2.html']); assert.equal(output.trim(), 'dependency3

Foo

Hey

Baz

'); output = r(['depwatch', 'dependency2.html']); assert.equal(output.trim(), 'output not written (pass 4)'); return done(); } }); a(['depwatch', 'include2.pug'], '\np Baz\n'); }); });