diff --git a/.eslintrc.json b/.eslintrc.json index 61e8895..24b8984 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,4 +1,8 @@ { + "extends": [ + "eslint:recommended" + ], + "env": { "browser": false, "es6": true, @@ -6,6 +10,15 @@ "mocha": true }, + "parserOptions":{ + "ecmaVersion": 9, + "sourceType": "module", + "ecmaFeatures": { + "modules": true, + "experimentalObjectRestSpread": true + } + }, + "globals": { "document": false, "navigator": false, diff --git a/.travis.yml b/.travis.yml index f6cf862..dedfdd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,3 @@ node_js: - node - '9' - '8' - - '7' - - '6' - - '5' - - '4' diff --git a/appveyor.yml b/appveyor.yml index 8d7807c..14fab18 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,17 +4,13 @@ environment: # node.js - nodejs_version: "9.0" - nodejs_version: "8.0" - - nodejs_version: "7.0" - - nodejs_version: "6.0" - - nodejs_version: "5.0" - - nodejs_version: "4.0" # Install scripts. (runs after repo cloning) install: # Get the latest stable version of Node.js or io.js - ps: Install-Product node $env:nodejs_version # install modules - - npm install + - appveyor-retry npm install # Post-install test scripts. test_script: diff --git a/examples/refactor.js b/examples/refactor.js new file mode 100644 index 0000000..883b9b4 --- /dev/null +++ b/examples/refactor.js @@ -0,0 +1,80 @@ +'use strict'; + +const AsyncHelpers = require('../index'); + +async function main() { + const asyncHelpers = new AsyncHelpers(); + + asyncHelpers.wrapHelper('lower', (val) => val && val.toLowerCase()); + + console.log('all raw:', asyncHelpers.rawHelpers); + console.log('lower sync:', asyncHelpers.helper('lower')); + console.log('all wrapped:', asyncHelpers.wrappedHelpers); + console.log('lower wrapped:'); + console.log(asyncHelpers.wrapHelper('lower')); + + console.log('lower non wrapped:'); + console.log(asyncHelpers.helper('lower')); + + const lower = asyncHelpers.wrapHelper('lower'); + + console.log('lower wrapped id:', lower('FOO')); // => asyncId + console.log('lower wrapped:', await asyncHelpers.resolveId(lower('FOO'))); + // => resolves to 'foo' + + asyncHelpers.wrapHelper('lowerAsync', async(str) => str && str.toLowerCase()); + const lowerAsync = asyncHelpers.wrapHelper('lowerAsync'); + + console.log('lowerAsync wrapped id:', lowerAsync('QUX')); // => asyncId + console.log('lowerAsync wrapped:', await asyncHelpers.resolveId(lowerAsync('QUX'))); + // => resolves to 'qux' + + asyncHelpers.wrapHelper('lowerCb', (str, cb) => cb(null, str && str.toLowerCase())); + const lowerCb = asyncHelpers.wrapHelper('lowerCb'); + + console.log('lowerCb wrapped id:', lowerCb('GGG')); // => asyncId + console.log('lowerCb wrapped:', await asyncHelpers.resolveId(lowerCb('GGG'))); + // => resolves to 'ggg' + + // resolveIds (plural) + const str = `foo ${lowerCb('KKK222')} bar ${lowerAsync('QYX')}`; + console.log('resolveIds:', await asyncHelpers.resolveIds(str)); + // => foo kkk222 bar qyx + + asyncHelpers.wrapHelper('sumAsync', (a, b) => Promise.resolve(a + b)); + const sumAsync = asyncHelpers.wrapHelper('sumAsync'); + console.log('sumAsync res:', await asyncHelpers.resolveIds(sumAsync(2, 4))); +} + +main(); + +// asyncHelpers.helper('upper', (str) => str && str.toUpperCase()); +// asyncHelpers.helper('upperAsync', async(str) => str && str.toUpperCase()); +// asyncHelpers.helper('upperAsyncCb', (str, cb) => cb(null, str && str.toUpperCase())); + +// asyncHelpers.wrapHelper('lower', (str) => str && str.toLowerCase()); +// asyncHelpers.wrapHelper('lowerAsync', async(str) => str && str.toLowerCase()); + +// const upperWrapped = asyncHelpers.helper('upper'); +// const upperNotWrap = asyncHelpers.wrapHelper('upper'); + +// const lowerWrapped = asyncHelpers.wrapHelper('lower'); +// const lowerNotWrap = asyncHelpers.helper('lower'); + +// console.log(lowerWrapped); +// console.log(lowerNotWrap); + +// const allWrapped = asyncHelpers.wrapHelper(); + +// console.log(allWrapped); +// console.log(lowerNotWrap('ZZZ')); +// asyncHelpers.resolveId(lowerWrapped('FOO')).then(console.log); + +// const lowerAsyncId = allWrapped.lowerAsync('FOO'); +// console.log(lowerAsyncId); +// asyncHelpers.resolveId(lowerAsyncId).then(console.log); + +// const upperAsync = asyncHelpers.wrapHelper('upperAsync'); +// console.log(upperAsync('low')); + +// asyncHelpers.resolveId(upperAsync('low')).then(console.log); diff --git a/index.js b/index.js index 5c7b910..b6a7ce5 100644 --- a/index.js +++ b/index.js @@ -7,555 +7,171 @@ 'use strict'; -var typeOf = require('kind-of'); -var co = require('co'); - -/** - * Caches - */ - -var cache = {}; -var stash = {}; - -/** - * Create a new instance of AsyncHelpers - * - * ```js - * var asyncHelpers = new AsyncHelpers(); - * ``` - * - * @param {Object} `options` options to pass to instance - * @return {Object} new AsyncHelpers instance - * @api public - */ - -function AsyncHelpers(options) { - if (!(this instanceof AsyncHelpers)) { - return new AsyncHelpers(options); - } - this.options = Object.assign({}, options); - this.prefix = this.options.prefix || '{$ASYNCID$'; - this.globalCounter = AsyncHelpers.globalCounter++; - this.helpers = {}; - this.counter = 0; - this.prefixRegex = toRegex(this.prefix); -} - -/** - * Keep track of instances created for generating globally - * unique ids - * @type {Number} - */ - -AsyncHelpers.globalCounter = 0; -AsyncHelpers.cache = cache; -AsyncHelpers.stash = stash; - -/** - * Add a helper to the cache. - * - * ```js - * asyncHelpers.set('upper', function(str, cb) { - * cb(null, str.toUpperCase()); - * }); - * ``` - * - * @param {String} `name` Name of the helper - * @param {Function} `fn` Helper function - * @return {Object} Returns `this` for chaining - * @api public - */ - -AsyncHelpers.prototype.set = function(name, fn) { - if (isObject(name)) { - var keys = Object.keys(name); - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - this.set(key, name[key]); +const util = require('util'); +const isAsyncWithCb = require('is-async-function'); + +class AsyncHelpers { + constructor(options) { + this.options = Object.assign({}, options); + this.helperIds = {}; + this.rawHelpers = {}; + this.wrappedHelpers = {}; + this.prefix = this.options.prefix || '@@@ASYNCID@'; + this.counter = 0; + } + + /** + * Define a helper `fn` with `name`. + * If `fn` is missing or not a function, returns the helper with `name`. + * If `name` is missing or not a string, returns all defined helpers. + * If `name` is object, calls itself for each key/value pair. + * + * @name .helper + * @param {string|object|undefined} [name] + * @param {function} [fn] + * @returns {AsyncHelpers|function|object} + * @memberof AsyncHelpers + * @api public + */ + helper(name, fn) { + if (isObject(name)) { + Object.keys(name).forEach((key) => { + this.helper(key, name[key]); + }); + return this; + } + if (!name) { + return this.rawHelpers; } + if (typeof name !== 'string') { + throw new TypeError('AsyncHelpers#helper: expect `name` to be non empty string'); + } + if (name && !fn) { + return this.rawHelpers[name]; + } + if (!fn || typeof fn !== 'function') { + throw new TypeError('AsyncHelpers#helper: expect `fn` to be function'); + } + this.rawHelpers[name] = fn; return this; } - if (typeof name !== 'string') { - throw new TypeError('AsyncHelpers#set: expected `name` to be a string'); - } - if (typeof fn !== 'function' && !isObject(fn)) { - throw new TypeError('AsyncHelpers#set: expected `fn` to be a function or object'); - } - - this.helpers[name] = fn; - return this; -}; - -/** - * Get all helpers or a helper with the given name. - * - * ```js - * var helpers = asyncHelpers.get(); - * var wrappedHelpers = asyncHelpers.get({wrap: true}); - * ``` - * - * @param {String} `name` Optionally pass in a name of a helper to get. - * @param {Object} `options` Additional options to use. - * @option {Boolean} `wrap` Wrap the helper(s) with async processing capibilities - * @return {Function|Object} Single helper function when `name` is provided, otherwise object of all helpers - * @api public - */ - -AsyncHelpers.prototype.get = function(helper, options) { - if (typeof helper === 'string') { - return this.wrapHelper.apply(this, arguments); - } - return this.wrapHelpers(this.helpers, helper); -}; - -/** - * Wrap a helper with async handling capibilities. - * - * ```js - * var wrappedHelper = asyncHelpers.wrap('upper'); - * var wrappedHelpers = asyncHelpers.wrap(); - * ``` - * - * @param {String} `helper` Optionally pass the name of the helper to wrap - * @return {Function|Object} Single wrapped helper function when `name` is provided, otherwise object of all wrapped helpers. - * @api public - */ - -AsyncHelpers.prototype.wrapHelper = function(helper, options) { - if (isObject(helper) && typeof options === 'undefined') { - options = helper; - helper = this.helpers; - } - - options = options || {}; - helper = helper || this.helpers; + wrapHelper(name, helper) { + if (isObject(name)) { + Object.keys(name).forEach((key) => { + this.wrapHelper(key, name[key]); + }); + return this.wrappedHelpers; + } + // .wrapHelper() -> wrap all the raw ones + if (!name) { + return this.wrapHelper(this.rawHelpers); + } + helper = this.wrappedHelpers[name] || this.rawHelpers[name] || helper; - var type = typeOf(helper); - switch (type) { - case 'string': - return this.wrapHelper(this.helpers[helper], options); - case 'object': - return this.wrapHelpers(helper, options); - case 'function': - if (isHelperGroup(helper)) { - return this.wrapHelpers(helper, options); - } - if (options.wrap && helper.wrapped !== true) { - return this.wrapper(helper.name || helper.displayName, helper, this); - } + if (helper && helper.wrapped) { return helper; - default: { - throw new TypeError('AsyncHelpers.wrapHelper: unsupported type: ' + type); } - } -}; - -/** - * Wrap an object of helpers to enable async handling - * @param {Object} `helpers` - * @param {Object} `options` - */ - -AsyncHelpers.prototype.wrapHelpers = function(helpers, options) { - if (!isObject(helpers) && isHelperGroup(helpers)) { - throw new TypeError('expected helpers to be an object'); - } - - var res = {}; - var keys = Object.keys(helpers); - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - var helper = helpers[key]; - if (isObject(helper)) { - res[key] = this.wrapHelpers(helper, options); - } else { - if (helper.wrapped !== true) { - if (typeOf(helper) === 'function' && !helper.name && !helper.displayName) { - helper.displayName = key; - } - res[key] = this.wrapHelper(helper, options); - } else { - res[key] = helper; - } + if (!helper) { + throw new TypeError(`AsyncHelpers#wrapHelper: cannot find helper "${name}" name`); } - } - return res; -}; + const self = this; -/** - * Returns a wrapper function for a single helper. - * @param {String} `name` The name of the helper - * @param {Function} `fn` The actual helper function - * @return {String} Returns an async ID to use for resolving the value. ex: `{$ASYNCID$!$8$}` - */ - -AsyncHelpers.prototype.wrapper = function(name, fn) { - var prefix = appendPrefix(this.prefix, this.globalCounter); - var self = this; + // if we only use `wrapHelper` as both defining and wrapping + if (!this.rawHelpers[name]) { + this.rawHelpers[name] = helper; + this.rawHelpers[name].wrapped = undefined; + } + const fn = isAsyncWithCb(helper) ? util.promisify(helper) : helper; - // wrap the helper and generate a unique ID for resolving it - function wrapper() { - var num = self.counter++; - var id = createId(prefix, num); + this.wrappedHelpers[name] = function wrappedHelper(...args) { + self.counter += 1; + const id = `${self.prefix}@${name}@${self.counter}@@@`; + self.helperIds[id] = { id, name, args, fn, ctx: this || {}, count: self.counter }; - var token = { - name: name, - async: !!fn.async, - prefix: prefix, - num: num, - id: id, - fn: fn, - args: [].slice.call(arguments) + return id; }; + this.wrappedHelpers[name].wrapped = true; - define(token, 'context', this); - stash[id] = token; - return id; + return this.wrappedHelpers[name]; } - define(wrapper, 'wrapped', true); - define(wrapper, 'helperName', name); - return wrapper; -}; - -/** - * Reset all the stashed helpers. - * - * ```js - * asyncHelpers.reset(); - * ``` - * @return {Object} Returns `this` to enable chaining - * @api public - */ - -AsyncHelpers.prototype.reset = function() { - stash = {}; - this.counter = 0; - return this; -}; - -/** - * Get all matching ids from the given `str` - * @return {Array} Returns an array of matching ids - */ - -AsyncHelpers.prototype.matches = function(str) { - if (typeof str !== 'string') { - throw new TypeError('AsyncHelpers#matches expects a string'); - } - return str.match(this.prefixRegex); -}; - -/** - * Returns true if the given string has an async helper id - * @return {Boolean} - */ - -AsyncHelpers.prototype.hasAsyncId = function(str) { - if (typeof str !== 'string') { - throw new TypeError('AsyncHelpers#hasAsyncId expects a string'); + hasAsyncId(val) { + const re = new RegExp(`${this.prefix}@.+@\\d+@@@`, 'g'); + return val && typeof val === 'string' && re.test(val); } - return str.indexOf(this.prefix) !== -1; -}; -/** - * Resolve a stashed helper by the generated id. - * This is a generator function and should be used with [co][] - * - * ```js - * var upper = asyncHelpers.get('upper', {wrap: true}); - * var id = upper('doowb'); - * - * co(asyncHelpers.resolveId(id)) - * .then(console.log) - * .catch(console.error); - * - * //=> DOOWB - * ``` - * - * @param {String} `key` ID generated when from executing a wrapped helper. - * @api public - */ + async resolveIds(str) { + const promises = Object.keys(this.helperIds) + .map(async(id) => { + return { id, value: await this.resolveId(id)}; + }); -AsyncHelpers.prototype.resolveId = function * (key) { - if (typeof key !== 'string') { - throw new Error('AsyncHelpers#resolveId: expects `key` to be a string.'); - } + return Promise.all(promises).then((res) => { + const result = res.reduce( + (acc, { id, value }) => { + if (acc === id) return value; + return acc.replace(new RegExp(id, 'g'), value); + }, + str, + ); - var helper = stash[key]; - if (!helper) { - throw new Error('AsyncHelpers#resolveId: cannot resolve helper: "' + key + '"'); + return result; + }); } - var args = yield this.resolveArgs(helper); - var self = this; - var str; + async resolveId(key) { + if (!this.helperIds[key]) { + throw new Error(`AsyncHelpers#resolveId: cannot resolve helper with "${key}" id`); + } + const { id, name, fn, args, ctx, value } = this.helperIds[key]; - return yield function(cb) { - if (typeof helper.fn !== 'function') { - cb(null, helper.fn); - return; + if ('value' in this.helperIds[id]) { + return value; } - var next = function(err, val) { - if (typeof val !== 'undefined') { - helper.fn = val; - cb(err, helper.fn); - return; + const argz = args.map(async(arg) => { + const item = this.helperIds[arg]; + + if (item) { + return this.resolveId(item.id); } - cb(err, ''); - return; - }; - if (helper.fn.async) { - var callback = function(err, result) { - if (err) { - next(formatError(err, helper, args)); - return; + if (isObject(arg) && isObject(arg.hash)) { + for (const [key, val] of Object.entries(arg.hash)) { + arg.hash[key] = this.hasAsyncId(val) ? await this.resolveId(val) : val; } - - if (typeof result === 'string' && self.hasAsyncId(result)) { - self.resolveIds(result, next); - return; + return arg; + } + if (Array.isArray(arg)) { + const res = []; + for (const ele of arg) { + res.push(this.hasAsyncId(ele) ? await this.resolveId(ele) : ele); } - - next(null, result); - return; - }; - - args.push(callback); - } + return res; + } + return arg; + }); try { - str = helper.fn.apply(helper.context, args); - if (typeof str === 'string' && self.hasAsyncId(str)) { - self.resolveIds(str, next); - return; - } + this.helperIds[id].value = await fn.apply(ctx, await Promise.all(argz)); } catch (err) { - next(formatError(err, helper, args)); - return; - } + const msg = `AsyncHelpers#resolveId: helper with name "${name}" and id "${id}" failed`; - if (!helper.fn.async) { - next(null, str); - return; + throw new Error(`${msg}: ${err.message}`); } - // do nothing - }; -}; - -/** - * Generator function for resolving helper arguments - * that contain async ids. This function should be used - * with [co][]. - * - * This is used inside `resolveId`: - * - * ```js - * var args = yield co(asyncHelpers.resolveArgs(helper)); - * ``` - * @param {Object} `helper` helper object with an `argRefs` array. - */ - -AsyncHelpers.prototype.resolveArgs = function * (helper) { - for (var i = 0; i < helper.args.length; i++) { - var arg = helper.args[i]; - if (!arg) continue; - - if (typeof arg === 'string' && this.hasAsyncId(arg)) { - helper.args[i] = yield this.resolveId(arg); - - } else if (isObject(arg) && isObject(arg.hash)) { - arg.hash = yield this.resolveObject(arg.hash); + const val = this.helperIds[id].value; + if (this.hasAsyncId(val)) { + return this.resolveIds(val); } - } - return helper.args; -}; -/** - * Generator function for resolving values on an object - * that contain async ids. This function should be used - * with [co][]. - * - * This is used inside `resolveArgs`: - * - * ```js - * var args = yield co(asyncHelpers.resolveObject(options.hash)); - * ``` - * @param {Object} `obj` object with with values that may be async ids. - * @returns {Object} Object with resolved values. - */ - -AsyncHelpers.prototype.resolveObject = function * (obj) { - var keys = Object.keys(obj); - var self = this; - - return yield keys.reduce(function(acc, key) { - return co(function * () { - var val = acc[key]; - if (typeof val === 'string' && self.hasAsyncId(val)) { - acc[key] = yield self.resolveId(val); - } - return acc; - }); - }, obj); -}; - -/** - * After rendering a string using wrapped async helpers, - * use `resolveIds` to invoke the original async helpers and replace - * the async ids with results from the async helpers. - * - * ```js - * asyncHelpers.resolveIds(renderedString, function(err, content) { - * if (err) return console.error(err); - * console.log(content); - * }); - * ``` - * @param {String} `str` String containing async ids - * @param {Function} `cb` Callback function accepting an `err` and `content` parameters. - * @api public - */ - -AsyncHelpers.prototype.resolveIds = function(str, cb) { - if (typeof cb !== 'function') { - throw new TypeError('AsyncHelpers#resolveIds() expects a callback function.'); - } - if (typeof str !== 'string') { - return cb(new TypeError('AsyncHelpers#resolveIds() expects a string.')); + return val; } - - var matches = this.matches(str); - var self = this; - - co(function * () { - if (!matches) { - return str; - }; - - for (var i = 0; i < matches.length; i++) { - var key = matches[i]; - var val = yield self.resolveId(key); - str = str.split(key).join(val); - } - return str; - }) - .then((res) => cb(null, res)) - .catch(cb); -}; - -/** - * Format an error message to provide better information about the - * helper and the arguments passed to the helper when the error occurred. - * - * @param {Object} `err` Error object - * @param {Object} `helper` helper object to provide more information - * @param {Array} `args` Array of arguments passed to the helper. - * @return {Object} Formatted Error object - */ - -function formatError(err, helper, args) { - err.helper = helper; - define(err, 'args', args); - return err; -} - -/** - * Create a prefix to use when generating an async id. - * - * @param {String} `prefix` prefix string to start with. - * @param {String} `counter` string to append. - * @return {String} new prefix - */ - -function appendPrefix(prefix, counter) { - return prefix + counter + '$'; -} - -/** - * Create an async id from the provided prefix and counter. - * - * @param {String} `prefix` prefix string to start with - * @param {String} `counter` string to append. - * @return {String} async id - */ - -function createId(prefix, counter) { - return appendPrefix(prefix, counter) + '}'; -} - -/** - * Create a regular expression based on the given `prefix`. - * @param {String} `prefix` - * @return {RegExp} - */ - -function toRegex(prefix) { - var key = appendPrefix(prefix, '(\\d+)'); - if (cache.hasOwnProperty(key)) { - return cache[key]; - } - var regex = new RegExp(createRegexString(key), 'g'); - cache[key] = regex; - return regex; -} - -/** - * Create a string to pass into `RegExp` for checking for and finding async ids. - * @param {String} `prefix` prefix to use for the first part of the regex - * @return {String} string to pass into `RegExp` - */ - -function createRegexString(prefix) { - var key = 'createRegexString:' + prefix; - if (cache.hasOwnProperty(key)) { - return cache[key]; - } - var str = (prefix + '(\\d+)$}').replace(/\\?([${}])/g, '\\$1'); - cache[key] = str; - return str; -} - -/** - * Return true if the given value is a helper "group" - */ - -function isHelperGroup(helpers) { - if (!helpers) return false; - if (helpers.isGroup) { - return true; - } - if (typeof helpers === 'function' || isObject(helpers)) { - var keys = Object.keys(helpers).filter(function(name) { - return ['async', 'sync', 'displayName'].indexOf(name) === -1; - }); - return keys.length > 1; - } - return false; } -/** - * Return true if the given value is an object - */ - function isObject(val) { - return typeOf(val) === 'object'; + return val && typeof val === 'object' && !Array.isArray(val); } -function define(obj, key, val) { - Object.defineProperty(obj, key, { - configurable: true, - enumerable: false, - value: val - }); -} - -/** - * Expose `AsyncHelpers` - */ - module.exports = AsyncHelpers; - diff --git a/package.json b/package.json index 4b4d8b5..b8f0607 100644 --- a/package.json +++ b/package.json @@ -22,22 +22,19 @@ "node": ">=4.0.0" }, "scripts": { - "test": "mocha" + "test": "mocha", + "lint": "eslint '**/*.js' --cache --fix --format codeframe" }, "dependencies": { - "co": "^4.6.0", - "kind-of": "^6.0.0" + "escape-string-regexp": "^1.0.5", + "is-async-function": "^1.2.3" }, "devDependencies": { "async": "^2.5.0", - "gulp": "^3.9.1", - "gulp-eslint": "^4.0.0", - "gulp-format-md": "^1.0.0", - "gulp-istanbul": "^1.1.2", - "gulp-mocha": "^3.0.1", + "eslint": "^5.9.0", "handlebars": "^4.0.11", "lodash": "^4.17.4", - "mocha": "^3.5.2" + "mocha": "^5.2.0" }, "keywords": [ "async", @@ -75,4 +72,4 @@ "reflinks": true } } -} \ No newline at end of file +} diff --git a/test/handlebars.js b/test/handlebars.js index 216e269..b28c600 100644 --- a/test/handlebars.js +++ b/test/handlebars.js @@ -1,10 +1,9 @@ 'use strict'; -require('mocha'); var assert = require('assert'); var Handlebars = require('handlebars'); var helpers = require('./support/helpers').handlebars; -var AsyncHelpers = require('../'); +var AsyncHelpers = require('../index'); var asyncHelpers; var hbs; @@ -13,6 +12,7 @@ var tmpl = [ 'input: {{> (partialName name="foo") }}', 'input: {{name}}', 'upper: {{upper name}}', + 'upperAsync: {{upperAsync name}}', 'lower: {{lower name}}', 'spacer: {{spacer name}}', 'spacer-delim: {{spacer name "-"}}', @@ -21,6 +21,11 @@ var tmpl = [ 'block: {{#block}}{{upper name}}{{/block}}', 'ifConditional1: {{#if (equals "foo" foo)}}{{upper name}}{{/if}}', 'ifConditional2: {{#if (equals "baz" bar)}}{{upper name}}{{/if}}', + 'ifConditional3: {{#if foo}}{{upper name}}{{/if}}', + 'ifConditional4: {{#if (false)}}{{upper name}}{{/if}}', + 'ifConditional5: {{#if (equalSync "foo" foo)}}{{upper name}}{{/if}}', + 'ifConditional6: {{#if false}}{{upper name}}{{/if}}', + 'ifConditional7: {{#if null}}{{upper name}}{{/if}}', 'useHash: {{#useHash me=(lookup this "person")}}{{me.first}} {{me.last}}{{/useHash}}', 'sum: {{sum 1 2 3}}', 'lookup(this "person"): {{lookup this "person"}}' @@ -32,22 +37,22 @@ describe('handlebars', function() { hbs.registerPartial('custom', 'a partial'); asyncHelpers = new AsyncHelpers(); - asyncHelpers.set(hbs.helpers); - helpers.getPartial.async = true; - helpers.partialName.async = true; + asyncHelpers.helper(hbs.helpers); // add the helpers to asyncHelpers - asyncHelpers.set('if', hbs.helpers.if); - asyncHelpers.set('getPartial', helpers.getPartial); - asyncHelpers.set('equals', helpers.equals); - asyncHelpers.set('partialName', helpers.partialName); - asyncHelpers.set('upper', helpers.upper); - asyncHelpers.set('lower', helpers.lower); - asyncHelpers.set('spacer', helpers.spacer); - asyncHelpers.set('block', helpers.block); - asyncHelpers.set('useHash', helpers.useHash); - asyncHelpers.set('lookup', helpers.lookup); - asyncHelpers.set('sum', helpers.sum); + asyncHelpers.helper('if', hbs.helpers.if); + asyncHelpers.helper('getPartial', helpers.getPartial); + asyncHelpers.helper('equals', helpers.equals); + asyncHelpers.helper('equalSync', helpers.equalSync); + asyncHelpers.helper('partialName', helpers.partialName); + asyncHelpers.helper('upper', helpers.upper); + asyncHelpers.helper('upperAsync', helpers.upperAsync); + asyncHelpers.helper('lower', helpers.lower); + asyncHelpers.helper('spacer', helpers.spacer); + asyncHelpers.helper('block', helpers.block); + asyncHelpers.helper('useHash', helpers.useHash); + asyncHelpers.helper('lookup', helpers.lookup); + asyncHelpers.helper('sum', helpers.sum); }); it('should work in handlebars', function(done) { @@ -59,7 +64,7 @@ describe('handlebars', function() { // pull the helpers back out and wrap them // with async handling functionality - var wrappedHelpers = asyncHelpers.get({wrap: true}); + var wrappedHelpers = asyncHelpers.wrapHelper(); // register the helpers with Handlebars hbs.registerHelper(wrappedHelpers); @@ -72,29 +77,41 @@ describe('handlebars', function() { var rendered = fn({ name: 'doowb', customName: 'custom', - person: {first: 'Brian', last: 'Woodward', toString: function() { return this.first + ' ' + this.last; }}, + person: { + first: 'Brian', + last: 'Woodward', + toString: function() { return this.first + ' ' + this.last; } + }, bar: 'baz' }); - asyncHelpers.resolveIds(rendered, function(err, content) { - if (err) return done(err); - assert.deepEqual(content, [ - 'input: custom', - 'input: doowb', - 'upper: DOOWB', - 'lower: doowb', - 'spacer: d o o w b', - 'spacer-delim: d-o-o-w-b', - 'lower(upper): doowb', - 'spacer(upper, lower): DxOxOxWxB', - 'block: DOOWB', - 'ifConditional1: ', - 'ifConditional2: DOOWB', - 'useHash: Brian Woodward', - 'sum: 6', - 'lookup(this "person"): Brian Woodward' - ].join('\n')); - done(); - }); + asyncHelpers.resolveIds(rendered) + .then(function(content) { + // console.log('content', content); + assert.deepEqual(content, [ + 'input: custom', + 'input: doowb', + 'upper: DOOWB', + 'upperAsync: DOOWB', + 'lower: doowb', + 'spacer: d o o w b', + 'spacer-delim: d-o-o-w-b', + 'lower(upper): doowb', + 'spacer(upper, lower): DxOxOxWxB', + 'block: DOOWB', + 'ifConditional1: ', + 'ifConditional2: DOOWB', + 'ifConditional3: ', + 'ifConditional4: ', + 'ifConditional5: ', + 'ifConditional6: ', + 'ifConditional7: ', + 'useHash: Brian Woodward', + 'sum: 6', + 'lookup(this "person"): Brian Woodward' + ].join('\n')); + done(); + }) + .catch(done); }); }); diff --git a/test/lodash.js b/test/lodash.js index 6df7bf3..6cbe091 100644 --- a/test/lodash.js +++ b/test/lodash.js @@ -3,7 +3,7 @@ var assert = require('assert'); var _ = require('lodash'); var helpers = require('./support/helpers').lodash; -var AsyncHelpers = require('../'); +var AsyncHelpers = require('../index'); describe('lodash', function() { it('should work in lodash', function(done) { @@ -11,18 +11,18 @@ describe('lodash', function() { var asyncHelpers = new AsyncHelpers(); // add the helpers to asyncHelpers - asyncHelpers.set('upper', helpers.upper); - asyncHelpers.set('lower', helpers.lower); - asyncHelpers.set('spacer', helpers.spacer); + asyncHelpers.helper('upper', helpers.upper); + asyncHelpers.helper('upperAsync', helpers.upperAsync); + asyncHelpers.helper('lower', helpers.lower); + asyncHelpers.helper('spacer', helpers.spacer); - // pull the helpers back out and wrap them - // with async handling functionality - var wrapped = asyncHelpers.get({wrap: true}); + var wrapped = asyncHelpers.wrapHelper(); // using Lodash, render a template with helpers var tmpl = [ 'input: <%= name %>', 'upper: <%= upper(name) %>', + 'upperAsync: <%= upperAsync(name) %>', 'lower: <%= lower(name) %>', 'spacer: <%= spacer(name) %>', 'spacer-delim: <%= spacer(name, "-") %>', @@ -31,23 +31,25 @@ describe('lodash', function() { ].join('\n'); // compile the template passing `helpers` in as `imports` - var fn = _.template(tmpl, { imports: wrapped}); + var fn = _.template(tmpl, { imports: wrapped }); // render the compiled template with the simple context object - var rendered = fn({name: 'doowb'}); - - asyncHelpers.resolveIds(rendered, function(err, content) { - if (err) return done(err); - assert.deepEqual(content, [ - 'input: doowb', - 'upper: DOOWB', - 'lower: doowb', - 'spacer: d o o w b', - 'spacer-delim: d-o-o-w-b', - 'lower(upper): doowb', - 'spacer(upper, lower): DxOxOxWxB' - ].join('\n')); - done(); - }); + var rendered = fn({ name: 'doowb' }); + + asyncHelpers.resolveIds(rendered) + .then((content) => { + assert.deepEqual(content, [ + 'input: doowb', + 'upper: DOOWB', + 'upperAsync: DOOWB', + 'lower: doowb', + 'spacer: d o o w b', + 'spacer-delim: d-o-o-w-b', + 'lower(upper): doowb', + 'spacer(upper, lower): DxOxOxWxB' + ].join('\n')); + done(); + }) + .catch(done); }); }); diff --git a/test/support/helpers.js b/test/support/helpers.js index f9feeaa..1933625 100644 --- a/test/support/helpers.js +++ b/test/support/helpers.js @@ -5,6 +5,9 @@ var helpers = { upper: function(str) { return str.toUpperCase(); }, + upperAsync: async function(str) { + return str.toUpperCase(); + }, getPartial: function(str, options, cb) { var args = [].slice.call(arguments); cb = args.pop(); @@ -14,11 +17,12 @@ var helpers = { cb(null, str.toLowerCase()); }, partialName: function partialName(options, cb) { - cb(null, this.customName || options.hash.name) + cb(null, this.customName || options.hash.name); }, is: function(val, options, cb) { cb(null, val === true); }, + equalSync: (a, b) => a == b, equals: function(a, b, options, cb) { cb(null, a == b); }, @@ -34,6 +38,7 @@ var helpers = { return options.fn(this); }, useHash: function(options) { + // console.log(options); return options.fn(options.hash || {}); }, lookup: function(obj, prop) { @@ -57,6 +62,9 @@ var helpers = { upper: function(str) { return str.toUpperCase(); }, + upperAsync: async function(str) { + return str.toUpperCase(); + }, lower: function(str, cb) { cb(null, str.toLowerCase()); }, @@ -70,12 +78,4 @@ var helpers = { } }; -// async helpers must have an `async` property -helpers.handlebars.is.async = true; -helpers.handlebars.equals.async = true; -helpers.handlebars.lower.async = true; -helpers.handlebars.spacer.async = true; -helpers.lodash.lower.async = true; -helpers.lodash.spacer.async = true; - module.exports = helpers; diff --git a/test/test.js b/test/test.js index f1f50ab..5c521a9 100644 --- a/test/test.js +++ b/test/test.js @@ -1,9 +1,7 @@ 'use strict'; -require('mocha'); var assert = require('assert'); -var co = require('co'); -var AsyncHelpers = require('../'); +var AsyncHelpers = require('../index'); var asyncHelpers = null; describe('async-helpers', function() { @@ -11,77 +9,121 @@ describe('async-helpers', function() { AsyncHelpers.globalCounter = 0; asyncHelpers = new AsyncHelpers(); }); - describe('set', function() { + + it('should throw error if name not a string', () => { + assert.throws(() => asyncHelpers.helper(123), TypeError); + assert.throws(() => asyncHelpers.helper(123), /expect `name` to be non empty string/); + }); + + it('should throw error if fn not a function', () => { + assert.throws(() => asyncHelpers.helper('abc', 123), TypeError); + assert.throws(() => asyncHelpers.helper('abc', 123), /expect `fn` to be function/); + }); + + it('should throw error if object value is not a function', () => { + assert.throws(() => asyncHelpers.helper({ abc: 123 }), TypeError); + assert.throws(() => asyncHelpers.helper({ abc: 123 }), /expect `fn` to be function/); + assert.ok(asyncHelpers.helper({ abc: () => {} })); + }); + it('should set a sync helper', function() { var upper = function(str) { return str.toUpperCase(); }; - asyncHelpers.set('upper', upper); - assert(typeof asyncHelpers.helpers.upper !== 'undefined', 'upper should be defined on `helpers`'); - assert.deepEqual(asyncHelpers.helpers.upper.toString(), upper.toString()); + asyncHelpers.helper('upper', upper); + assert(typeof asyncHelpers.rawHelpers.upper !== 'undefined', 'upper should be defined on `rawHelpers`'); + assert.deepEqual(asyncHelpers.rawHelpers.upper.toString(), upper.toString()); }); it('should set an async helper', function() { - var upper = function(str, cb) { + asyncHelpers.helper('upper', (str, cb) => { cb(null, str.toUpperCase()); - }; - upper.async = true; - asyncHelpers.set('upper', upper); - assert(typeof asyncHelpers.helpers.upper !== 'undefined', 'upper should be defined on `helpers`'); - assert(asyncHelpers.helpers.upper.async); + }); + + assert(typeof asyncHelpers.rawHelpers.upper !== 'undefined', 'upper should be defined on `rawHelpers`'); + assert(asyncHelpers.rawHelpers.upper); }); }); describe('get', function() { it('should get the helper as is', function() { - var upper = function(str) { - return str.toUpperCase(); - }; - asyncHelpers.set('upper', upper); - assert.deepEqual(asyncHelpers.get('upper').toString(), upper.toString()); + const upper = (str) => str && str.toUpperCase(); + asyncHelpers.helper('upper', upper); + + assert.deepEqual(asyncHelpers.helper('upper').toString(), upper.toString()); + assert.deepEqual(asyncHelpers.rawHelpers.upper.toString(), upper.toString()); }); - it('should get a wrapped helper', function() { - var upper = function(str) { - return str.toUpperCase(); - }; - asyncHelpers.set('upper', upper); - assert.notEqual(asyncHelpers.get('upper', { wrap: true }).toString(), upper.toString()); + it('should get all "raw" (non wrapped) helpers', function() { + asyncHelpers.helper('upper', (str) => str && str.toUpperCase()); + asyncHelpers.helper('lower', (str) => str && str.toLowerCase()); + + assert.deepEqual(Object.keys(asyncHelpers.helper()), ['upper', 'lower']); + assert.deepEqual(Object.keys(asyncHelpers.rawHelpers), ['upper', 'lower']); + }); + + it('should get all wrapped helpers', function() { + asyncHelpers.helper('upper', (str) => str && str.toUpperCase()); + asyncHelpers.helper('lower', (str) => str && str.toLowerCase()); + + assert.deepEqual(Object.keys(asyncHelpers.wrapHelper()), ['upper', 'lower']); + assert.deepEqual(Object.keys(asyncHelpers.wrappedHelpers), ['upper', 'lower']); }); }); describe('helpers', function() { - it('should return actual value when not wrapped', function() { + it('should return actual value when not wrapped and not async', function() { var upper = function(str) { return str.toUpperCase(); }; - asyncHelpers.set('upper', upper); - assert.deepEqual(asyncHelpers.get('upper')('doowb'), 'DOOWB'); + asyncHelpers.helper('upper', upper); + assert.strictEqual(typeof asyncHelpers.helper('upper'), 'function'); + assert.strictEqual(asyncHelpers.helper('upper')('doowb'), 'DOOWB'); }); - it('should return an async id when wrapped', function() { - var upper = function(str) { - return str.toUpperCase(); - }; - asyncHelpers.set('upper', upper); - assert.deepEqual(asyncHelpers.get('upper', { wrap: true })('doowb'), '{$ASYNCID$0$0$}'); - }); + it('should return an async id when helper is asynchronous', async() => { + asyncHelpers.helper('upperAsync', async(str) => str && str.toUpperCase()); + asyncHelpers.helper('upperCb', (str, cb) => { + cb(null, str && str.toUpperCase()); + }); + + // we need to use `.wrapHelper`, instead of `.helper` method + // because `helper` gets the raw "as is" helper, as it was defined. + // Only the wrapped helpers are returning async ids. + const upperAsync = asyncHelpers.wrapHelper('upperAsync'); + const upperCb = asyncHelpers.wrapHelper('upperCb'); + + assert.ok(asyncHelpers.prefix === '@@@ASYNCID@', 'default async id prefix should be @@@ASYNCID@'); + + // we dont need `await`, because the returned wrapped + // helper is always a regular function + const resAsync = upperAsync('doowb'); + assert.ok(resAsync.startsWith(asyncHelpers.prefix)); - it('should increment globalCounter for multiple instances of AsyncHelpers', function() { - var asyncHelpers2 = new AsyncHelpers(); - assert.notEqual(asyncHelpers.globalCounter, asyncHelpers2.globalCounter); - assert.equal(asyncHelpers.globalCounter, 0); - assert.equal(asyncHelpers2.globalCounter, 1); + const resCb = upperCb('doowb'); + assert.ok(resCb.startsWith(asyncHelpers.prefix)); + + // non wrapped helper + const upper = asyncHelpers.helper('upperAsync'); + + // we need `await` here, because the helper is regular `async` function + const res = await upper('foobar'); + assert.strictEqual(res, 'FOOBAR'); }); - it('should return an async id with a custom prefix', function() { - var asyncHelpers2 = new AsyncHelpers({prefix: '{$custom$prefix$$'}); - var upper = function(str) { - return str.toUpperCase(); - }; - asyncHelpers2.set('upper', upper); - assert.deepEqual(asyncHelpers2.get('upper', { wrap: true })('doowb'), '{$custom$prefix$$1$0$}'); + it('should return an async id with a custom prefix', async function() { + const customPrefix = '~!@custom@prefix@@'; + var asyncHelpers2 = new AsyncHelpers({ prefix: customPrefix }); + + assert.ok(asyncHelpers2.prefix === customPrefix, 'incorrect prefix'); + + asyncHelpers2.helper('upper', (str, cb) => { + cb(null, str.toUpperCase()); + }); + + const upper = asyncHelpers2.wrapHelper('upper'); + assert.ok((await upper('doowb')).startsWith(customPrefix)); }); it('should support helpers that take arrays as an argument', function() { @@ -92,30 +134,27 @@ describe('async-helpers', function() { }; // use the async mapSeries function for the helper var map = async.mapSeries; - // make sure asyncHelpers knows this is an async function - map.async = true; - asyncHelpers.set('map', map); - var helper = asyncHelpers.get('map', {wrap: true}); + + asyncHelpers.helper('map', map); + var wrappedHelper = asyncHelpers.wrapHelper('map'); // call the helper to get the id - var id = helper(['doowb', 'jonschlinkert'], upper); - assert.equal(id, '{$ASYNCID$0$0$}'); + var id = wrappedHelper(['doowb', 'jonschlinkert'], upper); + assert.ok(id.startsWith(asyncHelpers.prefix)); - // resolveId the id - return co(asyncHelpers.resolveId(id)) + return asyncHelpers.resolveIds(id) .then(function(val) { assert.deepEqual(val, ['DOOWB', 'JONSCHLINKERT']); }); }); - it('should support helpers used as arguments that return objects', function(done) { + it('should support helpers used as arguments that return objects', function() { var profile = function(user, next) { if (typeof user !== 'object') { return next(new Error('Expected user to be an object but got ' + (typeof user))); } next(null, user.name); }; - profile.async = true; var user = function(name, next) { var res = { @@ -124,39 +163,64 @@ describe('async-helpers', function() { }; next(null, res); }; - user.async = true; - asyncHelpers.set('user', user); - asyncHelpers.set('profile', profile); - var userHelper = asyncHelpers.get('user', {wrap: true}); + + asyncHelpers.helper('user', user); + asyncHelpers.helper('profile', profile); + + var userHelper = asyncHelpers.wrapHelper('user'); var userId = userHelper('doowb'); - assert.equal(userId, '{$ASYNCID$0$0$}'); - var profileHelper = asyncHelpers.get('profile', {wrap: true}); + assert.ok(userId.startsWith(asyncHelpers.prefix)); + + var profileHelper = asyncHelpers.wrapHelper('profile'); var profileId = profileHelper(userId); - asyncHelpers.resolveIds(profileId, function(err, val) { - if (err) return done(err); + return asyncHelpers.resolveIds(profileId).then(function(val) { assert.deepEqual(val, 'doowb'); - done(); + }); + }); + + it('should support sync and async value items in argument arrays', () => { + const helpers = new AsyncHelpers(); + + helpers.wrapHelper('upperAsync', async(val) => val && val.toUpperCase()); + + const upperAsync = helpers.wrapHelper('upperAsync'); + const upperAsyncId = upperAsync('qux'); + + helpers.wrapHelper('addOne', (arr) => arr.map((x) => x + 1)); + + const addOneHelper = helpers.wrapHelper('addOne'); + const result = addOneHelper(['aaa', upperAsyncId, 'foo', upperAsync('bar')]); + + return helpers.resolveId(result).then((res) => { + assert.deepStrictEqual(res, ['aaa1', 'QUX1', 'foo1', 'BAR1']); }); }); }); describe('errors', function() { - it('should handle errors in sync helpers', function() { + it('should handle thrown errors in sync helpers', function() { var asyncHelpers3 = new AsyncHelpers(); var upper = function(str) { - throw new Error('UPPER Error'); + throw new Error('yeah UPPER sync Error'); }; - asyncHelpers3.set('upper', upper); - var helper = asyncHelpers3.get('upper', {wrap: true}); - var id = helper('doowb'); - return co(asyncHelpers3.resolveId(id)) + asyncHelpers3.helper('upper', upper); + + // We need to wrap the helper, to test this + // because the non-wrapped one will throw directly, + // even before the `resolveIds` call. + // Because the `.helper()` method does nothing more than just type checking + // and adding/getting to/from a key value cache. + var wrappedHelper = asyncHelpers3.wrapHelper('upper'); + var id = wrappedHelper('doowb'); + + return asyncHelpers3.resolveIds(id) .then(function(val) { return Promise.reject(new Error('expected an error')); }) .catch(function(err) { - assert(err.hasOwnProperty('helper'), 'Expected a `helper` property on `err`'); + assert.ok(/yeah UPPER sync Error/.test(err.message), 'expect to throw from sync helper'); }); }); @@ -165,168 +229,153 @@ describe('async-helpers', function() { var upper = function(str, next) { throw new Error('UPPER Error'); }; - upper.async = true; - asyncHelpers3.set('upper', upper); - var helper = asyncHelpers3.get('upper', {wrap: true}); + + asyncHelpers3.helper('upper', upper); + + var helper = asyncHelpers3.wrapHelper('upper'); var id = helper('doowb'); - return co(asyncHelpers3.resolveId(id)) + + return asyncHelpers3.resolveIds(id) .then(function(val) { return Promise.reject(new Error('expected an error')); }) .catch(function(err) { - assert(err.hasOwnProperty('helper'), 'Expected a `helper` property on `err`'); + assert.ok(/UPPER Error/.test(err.message), 'expect to throw from async helper'); }); }); it('should handle returned errors in async helpers', function() { var asyncHelpers3 = new AsyncHelpers(); var upper = function(str, next) { - next(new Error('UPPER Error')); + next(new Error('async returned UPPER Error')); }; - upper.async = true; - asyncHelpers3.set('upper', upper); - var helper = asyncHelpers3.get('upper', {wrap: true}); + + asyncHelpers3.helper('upper', upper); + + var helper = asyncHelpers3.wrapHelper('upper'); var id = helper('doowb'); - return co(asyncHelpers3.resolveId(id)) + + return asyncHelpers3.resolveIds(id) .then(function(val) { - throw new Error('expected an error'); + return Promise.reject(new Error('expected an error')); }) .catch(function(err) { - assert(err.hasOwnProperty('helper'), 'Expected a `helper` property on `err`'); + assert.ok(/async returned UPPER Error/.test(err.message), 'expect to throw from async helper'); }); }); it('should handle errors with arguments with circular references', function() { var asyncHelpers3 = new AsyncHelpers(); var upper = function(str, next) { - throw new Error('UPPER Error'); + throw new Error('circular UPPER Error'); }; - upper.async = true; - asyncHelpers3.set('upper', upper); - var helper = asyncHelpers3.get('upper', {wrap: true}); + + asyncHelpers3.helper('upper', upper); + + var helper = asyncHelpers3.wrapHelper('upper'); var obj = {username: 'doowb'}; obj.profile = obj; + var id = helper(obj); - return co(asyncHelpers3.resolveId(id)) + + return asyncHelpers3.resolveIds(id) .then(function(val) { - throw new Error('Expected an error'); + return Promise.reject(new Error('expected an error')); }) .catch(function(err) { - assert(err.hasOwnProperty('helper'), 'Expected a `helper` property on `err`'); + assert.ok(/circular UPPER Error/.test(err.message), 'expect to throw from async helper'); }); }); }); - describe('wrapHelper', function() { - it('should return the helper when given the helper name', function() { - var upper = function(str) { - return str.toUpperCase(); - }; - asyncHelpers.set('upper', upper); - var fn = asyncHelpers.wrapHelper('upper'); - assert.equal(fn, upper); - assert.deepEqual(fn('doowb'), 'DOOWB'); + describe('wrapHelper', () => { + it('should throw when `fn` is not a function and `name` not find in rawHelpers ', () => { + assert.throws(() => asyncHelpers.wrapHelper('upper'), TypeError); + assert.throws(() => asyncHelpers.wrapHelper('upper'), /cannot find helper "upper" name/); }); - it('should return the wrapped helper when given the helper name and wrap option is true', function() { - var upper = function(str) { - return str.toUpperCase(); - }; - asyncHelpers.set('upper', upper); - var fn = asyncHelpers.wrapHelper('upper', {wrap: true}); - assert.notEqual(fn, upper); - assert.notEqual(fn.toString(), upper.toString()); - assert.deepEqual(fn('doowb'), '{$ASYNCID$0$0$}'); - }); + it('should return all defined helpers as wrapped', () => { + asyncHelpers.helper('one', () => 1); + asyncHelpers.helper('two', () => 2); + asyncHelpers.wrapHelper('three', () => 3); + asyncHelpers.wrapHelper('someAsync', async() => 4); - it('should return a function when given a function', function() { - var upper = function(str) { - return str.toUpperCase(); - }; - var fn = asyncHelpers.wrapHelper(upper); - assert.equal(fn, upper); - assert.deepEqual(fn('doowb'), 'DOOWB'); - }); + const rawHelpers = asyncHelpers.helper(); + const wrappedHelpers = asyncHelpers.wrapHelper(); - it('should return a wrapped function when given a function and wrap option is true', function() { - var upper = function(str) { - return str.toUpperCase(); - }; - var fn = asyncHelpers.wrapHelper(upper, {wrap: true}); - assert.notEqual(fn, upper); - assert.deepEqual(fn('doowb'), '{$ASYNCID$0$0$}'); + assert.deepEqual(Object.keys(rawHelpers), ['one', 'two', 'three', 'someAsync']); + assert.ok(wrappedHelpers.one); + assert.ok(wrappedHelpers.two); + assert.ok(wrappedHelpers.three); + assert.ok(wrappedHelpers.someAsync); }); - it('should return an object of helpers when given an object of helpers', function() { - var helpers = { - upper: function(str) { return str.toUpperCase(); }, - lower: function(str) { return str.toLowerCase(); } - }; - asyncHelpers.set(helpers); - var obj = asyncHelpers.wrapHelper(); - assert.deepEqual(obj, helpers); - assert.equal(obj.upper('doowb'), 'DOOWB'); - assert.equal(obj.lower('DOOWB'), 'doowb'); + it('returned wrapped helper should have `wrapped` prop on it which is true', () => { + const wrappedHelper = asyncHelpers.wrapHelper('foobar', () => 123); + + assert.strictEqual(wrappedHelper.wrapped, true); + assert.strictEqual(wrappedHelper().startsWith(asyncHelpers.prefix), true); + + assert.ok(asyncHelpers.wrapHelper('foobar').wrapped); + + const helper = asyncHelpers.helper('foobar'); + assert.strictEqual(helper.wrapped, undefined); + assert.strictEqual(helper(), 123); }); - it('should return an object of wrapped helpers when given an object of helpers and wrap option is true', function() { - var helpers = { - upper: function(str) { return str.toUpperCase(); }, - lower: function(str) { return str.toLowerCase(); } - }; - asyncHelpers.set(helpers); - var obj = asyncHelpers.wrapHelper({wrap: true}); - assert.notDeepEqual(obj, helpers); - assert.equal(obj.upper('doowb'), '{$ASYNCID$0$0$}'); - assert.equal(obj.lower('DOOWB'), '{$ASYNCID$0$1$}'); + it('should return `fn` helper if it is already wrapped', () => { + const wrappedHelper = asyncHelpers.wrapHelper('fooqux', () => 123); + const helper = asyncHelpers.wrapHelper('fooqux', wrappedHelper); + + assert.strictEqual(wrappedHelper.toString(), helper.toString()); }); + }); - it.skip('should return an object of helpers from a helper group', function() { - var helpers = function() {}; - helpers.isGroup = true; - helpers.upper = function(str) { return str.toUpperCase(); }; - helpers.lower = function(str) { return str.toLowerCase(); }; - asyncHelpers.set('my-group', helpers); - var res = asyncHelpers.wrapHelper('my-group'); - assert.deepEqual(res, helpers); - assert.equal(res.upper('doowb'), 'DOOWB'); - assert.equal(res.lower('DOOWB'), 'doowb'); + describe('resolveId', () => { + it('should return rejected promise when cannot find async id ', () => { + return asyncHelpers.resolveId(123456) + .then(() => { + throw new Error('should throw an error'); + }) + .catch((err) => { + assert.ok(/cannot resolve helper with/.test(err.message)); + }); }); }); - describe('wrapHelpers', function() { + // describe('wrapHelpers', function() { - }); + // }); - describe('wrapper', function() { + // describe('wrapper', function() { - }); + // }); - describe('reset', function() { + // describe('reset', function() { - }); + // }); - describe('matches', function() { + // describe('matches', function() { - }); + // }); - describe('hasAsyncId', function() { + // describe('hasAsyncId', function() { - }); + // }); - describe('resolveId', function() { + // describe('resolveId', function() { - }); + // }); - describe('resolveArgs', function() { + // describe('resolveArgs', function() { - }); + // }); - describe('resolveObject', function() { + // describe('resolveObject', function() { - }); + // }); - describe('resolveIds', function() { + // describe('resolveIds', function() { - }); + // }); });