diff --git a/lib/application.js b/lib/application.js index 47be2a20c1a..8daae3ad9fa 100644 --- a/lib/application.js +++ b/lib/application.js @@ -574,6 +574,31 @@ app.render = function render(name, options, callback) { tryRender(view, renderOptions, done); }; +/** + * Render the given view `name` with `options`, returning a Promise. + * + * Example: + * + * const html = await app.renderAsync('email', { name: 'Tobi' }); + * + * @param {String} name + * @param {Object} options + * @return {Promise} + * @public + */ + +app.renderAsync = function renderAsync(name, options) { + var self = this; + var opts = options || {}; + + return new Promise(function(resolve, reject) { + self.render(name, opts, function(err, str) { + if (err) reject(err); + else resolve(str); + }); + }); +}; + /** * Listen for connections. * diff --git a/test/app.render.js b/test/app.render.js index bd65ce1035b..606663f8a56 100644 --- a/test/app.render.js +++ b/test/app.render.js @@ -381,6 +381,115 @@ describe('app', function(){ }) }) }) + + describe('.renderAsync(name, options)', function(){ + it('should return a promise', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + var result = app.renderAsync('user.tmpl'); + assert.ok(result instanceof Promise); + result.then(function(str) { + assert.strictEqual(str, '

tobi

') + done(); + }).catch(done); + }) + + it('should resolve with rendered string', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.renderAsync('email.tmpl') + .then(function(str) { + assert.strictEqual(str, '

This is an email

') + done(); + }) + .catch(done); + }) + + it('should work with async/await', async function(){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + var str = await app.renderAsync('user.tmpl'); + assert.strictEqual(str, '

tobi

') + }) + + it('should accept options', async function(){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + var str = await app.renderAsync('user.tmpl', { user: { name: 'jane' } }); + assert.strictEqual(str, '

jane

') + }) + + it('should reject when view does not exist', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.renderAsync('nonexistent.tmpl') + .then(function() { + done(new Error('should have rejected')); + }) + .catch(function(err) { + assert.ok(err); + assert.ok(err.message.includes('Failed to lookup view')); + done(); + }); + }) + + it('should reject on render error', function(done){ + var app = express(); + + function View(name, options){ + this.name = name; + this.path = 'fake'; + } + + View.prototype.render = function(options, fn){ + throw new Error('render error!'); + }; + + app.set('view', View); + + app.renderAsync('something') + .then(function() { + done(new Error('should have rejected')); + }) + .catch(function(err) { + assert.ok(err); + assert.strictEqual(err.message, 'render error!'); + done(); + }); + }) + + it('should expose app.locals', async function(){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + var str = await app.renderAsync('user.tmpl', {}); + assert.strictEqual(str, '

tobi

') + }) + + it('should give precedence to renderAsync() locals', async function(){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + var str = await app.renderAsync('user.tmpl', { user: { name: 'jane' } }); + assert.strictEqual(str, '

jane

') + }) + }) }) function createApp() {