diff --git a/.travis.yml b/.travis.yml index f31b6d3..3dfcaec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "4" + - "6" before_script: - npm install -g istanbul script: istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9e38d40 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible Node.js debug attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceRoot}/test.js", + "cwd": "${workspaceRoot}" + }, + { + "type": "node", + "request": "attach", + "name": "Attach to Process", + "port": 5858 + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index e233558..47016b5 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ and Promise - Say goodbye to indentation hell. - [Setup](#setup) - [Getting Started](#getting-started) - [Setlist Chain](#setlist-chain) -- [Setlist Timeout](#setlist-timeout) +- [Wrapping Callback Function](#wrapping-callback-functions-with-promise) +- [Transform Generator Function Class with Promise](#wrapping-class-or-object-with-promise) +- [Create Callback Handler with Generator Function](#create-callback-handler-using-generator-function) - [Testing](#testing) - [Bugs](#bugs) - [License](#license) @@ -29,21 +31,14 @@ npm install --save setlist Then use them in your project by requiring `setlist`. ```javascript -const List = require('setlist'); +const run = require('setlist'); ``` -Now, you are good to go! - ## Getting Started This topics requires you to understand the basics of **Promise** -> **RULE OF THUMB** -> There is always a `yield` keyword before calling -> **asynchronous function** and **generator function**. -> **Do not** use `yield` when working with synchronous function. - This is an example of function using Promises to handle some asynchronous operation. @@ -100,21 +95,26 @@ function* yeayFunction(name) { } ``` -Then, run the `yeayFunction()` with the `List()` function. +> **RULE OF THUMB** +> There is always a `yield` keyword before calling +> **asynchronous function** and **generator function**. +> **Do not** use `yield` when working with synchronous function. + +Then, run the `yeayFunction()` with the `run()` function. ```javascript -List(yeayFunction('Joe')); +run(yeayFunction('Joe')); ``` Done. No more indentation hell, and of course, no callback hells. ## Setlist Chain -You can chain multiple generator function execution with `List(...).next()`. +You can chain multiple generator function execution with `run(...).next()`. ```javascript // Chain multiple execution with .next() -List(yeayFunction('Joe')) +run(yeayFunction('Joe')) .next(anotherFunction()) .next(lastFunction()); ``` @@ -130,87 +130,118 @@ function* taskList() { yield processFunction(status); yield lastFunction(); } -// Just execute the parent generator function -List(taskList()); +// Execute the parent generator function +run(taskList()); ``` -## Working with Callback Functions +## Wrapping Callback Functions with Promise + +Setlist does not work with callback functions. So, in order to use your +callback functions from the past, you can wrap them with `run.promisify()`. -In the past, asynchronous function usually equipped with callback as their last -argument. So, the execution result of async function will be passed directly -to the callback function as argument. Thus, any errors occured when the async -function should be handled manually. +For example, the `setlist` promisify will wrap the file system `fs.readFile()` +function so it can be chained in our generator function. ```javascript -function callbackHandler(err, value, handle) { - // Check for execution error - if (err) { - ... - } else { - callNextFunction(...); - } +// Import fs library +const fs = require('fs'); + +// Create generator function +function* readConfig(filename) { + // Get file content + let content = yield run.promisify(fs.readFile)(filename); + + // Do something with it + return processFileContent(content); } -startProcess(callbackHandler); +// Or do with promise style +run.promisify(fs.readFile)(filename) + .then(function(content) { + return processFileContent(content); + }); ``` -Promisify the callback function also not helping the situation. This is mainly -caused by the promisifier only pass the `resolve()` function as the callback. -As a result, error callbacks will treated as `resolved`, not `rejected`. -Moreover, the second and third result argument will lost because resolve -function only accepts single argument. +## Wrapping Class or Object with Promise -With Setlist, you can directly `throw` error from the generator function and -Setlist will indicate the parent promise as rejected. And yes, the return -of calling `List()` is Promise object so you can chain them with `.catch()` -to catch errors. +If you are planning to write down classes or objects with generator function, +you can transform them into Promise on runtime by calling `run.proxify()` +and pass in your class or object after the class definition. -The `List.async()` also automatically packs the arguments from callback with -array if the argument count is 2 or more. +For class object,it will also automatically transform the prototype object. +Note that you should convert extended class with the `proxify` if you define +new generator function in the extended class. ```javascript -function* taskList() { - // Run callback function - let status = yield List.async(startProcess)(); - - // Now the status var holds array containing 3 items - // and arranged as [err, value, handle] - if (status[0]) { // Err argument - // Directly throw error, the execution of taskList() will not continue - throw status[0]; +class baseClass { + * method() { + ... } - // Continue to the next step - yield callNextFunction(...); + static * staticMethod() { + ... + } } -List(taskList()) - .catch(function(err) { - // Handle error here +// Convert base class +run.proxify(baseClass); + +class extendedClass extends baseClass { + * extMethod() { ... - }); -``` + } +} -Or, you can also pass the callback function directly to the yield with `bind()` -if the function require arguments beside the callback. +// Convert extended class +run.proxify(extendedClass); -```javascript -// This is also okay -let status = yield startProcess.bind(null, ...); +// No need to convert because there is no generator functions +class anotherClass extends baseClass { + syncMethod() { + ... + } +} + +// But this calls will return promise because we already +// transform the base class +anotherClass.staticMethod(); ``` -## Setlist Timeout +## Create Callback Handler using Generator Function + +Some function that require callback handler, such as REPL eval function, +are more reliably written with generator function, at least in my opinion. -It is possible to attach timeout to the Setlist, limiting the execution time -and return promise rejetion on timeout. To activate timeout, pass the time of -timeout (in ms) as the second argument of `List(..., [timeout])` +To wrap the generator function so it can be used with the callback handler +you can use `run.callbackify()` function. ```javascript -// Setlist with time limit of 2s -List(mySetlist(), 2000); +// Import REPL library +const repl = require('repl'); + +// Create repl eval callback handler +function* evalHandle(cmd) { + // Get result from evaluated code + let result; + try { + let result = eval(cmd); + } catch(error) { + // Get recoverable status (See REPL documentation from Node.js) + if (isRecoverable(error)) { + return REPL.Recoverable(error); + } else { + // You can just throw error here and the callbackify will properly + // pass the error to the callback + throw error; + } + } + + // Return result to the callback if the eval suceeds + return result; +} -// Setlist without time limit -List(myOtherSetlist()); +// Start repl session +repl.start({ eval: run.callbackify(evalHandle) }); ``` ## Testing @@ -231,4 +262,4 @@ at ## License -SetlistJS is MIT licensed. +Copyright (c) 2016 Fadhli Dzil Ikram. SetlistJS is MIT licensed. diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..3730a4b --- /dev/null +++ b/index.d.ts @@ -0,0 +1,7 @@ +/** + * SetlistJS + * TypeScript Module Definition + * Copyright (c) 2016 Fadhli Dzil Ikram + */ + +export = require('./lib/setlist'); diff --git a/index.js b/index.js index ccd048e..fd07361 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ /** * SetlistJS * Sequential-ish your async code with generator function - * @author Fadhli Dzil Ikram + * Copyright (c) 2016 Fadhli Dzil Ikram */ module.exports = require('./lib/setlist'); diff --git a/lib/setlist.d.ts b/lib/setlist.d.ts new file mode 100644 index 0000000..1b6191d --- /dev/null +++ b/lib/setlist.d.ts @@ -0,0 +1,19 @@ +/** + * SetlistJS TypeScript Definition + * Enable correct type-hinting on VSCode + * Copyright (c) 2016 Fadhli Dzil Ikram + */ + +interface run { + (fn: PromiseLike | Function | IterableIterator): Promise; + callbackify(fn: Function): Function; + promisify(fn: Function): Function; + proxify(obj: Object): void; + isPromise(fn: any): fn is PromiseLike; + isGenerator(fn: any): fn is IterableIterator; + isFunctionGenerator(fn: any): fn is Function; +} + +declare const run: run; + +export = run; diff --git a/lib/setlist.js b/lib/setlist.js index 592fa12..de5e2e8 100644 --- a/lib/setlist.js +++ b/lib/setlist.js @@ -1,160 +1,174 @@ /** - * SetlistJS - Library Code + * SetlistJS Generator Runner * Sequential-ish your async code with generator function - * @author Fadhli Dzil Ikram + * Copyright (c) 2016 Fadhli Dzil Ikram */ -'use strict'; +const lo = require('lodash'); -function Setlist(fn, timeout) { - // Set timeout default settings - timeout = timeout || 0; - // Check for function inputs - if (Setlist.identify(fn) !== 'GeneratorFunction') { - throw new Error('InvalidParameter'); - } +// Export modules +module.exports = run; +run.callbackify = callbackify; +run.promisify = promisify; +run.proxify = proxify; +run.isPromise = isPromise; +run.isGenerator = isGenerator; +run.isGeneratorFunction = isGeneratorFunction; - // Create new promise - let promise = new Promise(startWorker); - // Overload promise with helper functions - promise.next = Setlist.next.bind(promise); +// Main generator function +function run(fn) { + // Return directly if it is promise + if (isPromise(fn)) return fn; + // Initialize generator function on GenFn input + else if (isGeneratorFunction(fn)) fn = fn(); + // Throw unknown input + else if (!isGenerator(fn)) { + throw new Error('Unknown generator run input'); + } - // Start worker function - function startWorker(resolve, reject) { - // Set asynchronous timeout to prevent process lock up - if (timeout > 0) { - setTimeout(function() { - reject(new Error('TimeoutError')); - }, timeout); - } - // Start the task worker - Setlist.worker(resolve, reject, fn); + // Create new promise generator runner + return new Promise(function(resolve, reject) { + // Run generator function + exec(); + + // Generator run function + function exec(val) { + try { + next(fn.next(val)); + } catch(err) { + reject(err); + } } - // Return promise to the caller - return promise; -} + // Generator error function + function error(val) { + try { + next(fn.throw(val)); + } catch(err) { + reject(err); + } + } + + // Generator next function + function next(r) { + let cont; + let fail; + if (r.done) { + cont = resolve; + fail = reject; + } else { + cont = exec; + fail = error; + } -Setlist.identify = function identify(obj) { - if (typeof obj === 'object') { - if ('then' in obj && typeof obj.then === 'function') { - // This is promise - return 'Promise'; - } else if ('constructor' in obj && 'constructor' in obj.constructor && - obj.constructor.constructor.name === 'GeneratorFunction') { - // This is generator function - return 'GeneratorFunction'; + if (isPromise(r.value)) { + r.value.then(cont).catch(fail); + } else if (isGenerator(r.value)) { + run(r.value).then(cont).catch(fail); + } else { + if (r.done) { + resolve(r.value); } else { - return false; + process.nextTick(() => exec(r.value)); } - } else if (typeof obj === 'function') { - // This is ordinary function - return 'Function'; - } else { - return false; + } } + }); } -Setlist.next = function next(fn) { - // Chain the parent function with new task - return this.then(function() { - // Return new promisified task - return Setlist(fn); - }); -} - -Setlist.async = function async(fn) { - // Checks if the passed parameter is not a function - if (typeof fn !== 'function') { - throw new Error('InvalidParameter'); - } - // Create local execution function - function exec() { - // Let's move the arguments - let args = Array.from(arguments); - // Return new promise and execute the async function - return new Promise(function(resolve) { - // Add new resolve (callback) function as the last parameter - args.push(done); - // We are good to go! - fn.apply(null, args); - // Create function that can pack args to resolve function - function done() { - // Capture arguments - let a = arguments; - // Call resolve based on arument length - if (a.length === 0) { - // No arguments, call straightforward - resolve(); - } else if (a.length === 1) { - // Single argument, pass as ordinary value - resolve(a[0]); - } else { - // Multiple arguments, pack them into array - resolve(Array.from(a)); - } - } - }); - } +// Wrap generator function with async-with-callback style function +function callbackify(fn) { + // Check if the input was not generator function + if (!isGeneratorFunction(fn)) { + throw new Error('Callbackify input is not a Generator Function'); + } - return exec; + return function callbackifier() { + // Get arguments from callback handler (without callback function) + let args = Array.prototype.slice.call(arguments, 0, -1); + // Get callback function itself + let callback = arguments[arguments.length - 1]; + // Run the generator function with o-generator + run(fn.apply(this, args)) + // Then handler (result return) + .then((result) => callback(null, result)) + // Catch handler (error return) + .catch((error) => callback(error, null)); + } } -Setlist.convert = function convert(src) { - // Try to iterate ownPropertyNames and find generator function - // Then convert them into promise return function - let properties = Object.getOwnPropertyNames(src); - let extended = Object.create(src); - // Traverse - for (let property of properties) { - if ('constructor' in src[property] && - src[property].constructor.name === 'GeneratorFunction') { - // Create own wrapping method - extended[property] = function proxy() { - return Setlist(src[property].apply(src, arguments)); - } +// Wrap async-with-callback with function that returns promise +function promisify(fn) { + // Check if the input is not function + if (!lo.isFunction(fn)) { + throw new Error('Promisify input is not a Function'); + } + // Return with new proxy function + return function promisifier() { + let self = this; + // Get arguments from proxy + let args = Array.from(arguments); + // Return promise to user + return new Promise((resolve, reject) => { + // Push callback handler as the last argument + args.push(callback); + // Run the callback function + fn.apply(self, args); + // Define callback handler + function callback(err, r) { + // Get return value as Array + let rArray = Array.prototype.slice.call(arguments, 1); + // Check error status + if (err) { + reject(err); + } else { + if (rArray.length > 1) { + // Resolve callback resolve as Array + resolve(rArray); + } else { + resolve(r); + } } - } - // Return new object - return extended; + } + }); + } } -Setlist.worker = function worker(done, halt, fn, value) { - try { - // Try to run generator function - var r = fn.next(value); - } catch (err) { - // Whoops, this is not good - halt(err); +// Wrap class and prototype method that contains generator function with +// promise proxy so it can be used without explicit usage of generator runner +function proxify(obj) { + if (!lo.isObject(obj)) { + throw new Error('Proxify input is not object'); + } + // Proxify prototype if available + if ('prototype' in obj) proxify(obj.prototype); + // Get all own property member of the object + let properties = Object.getOwnPropertyNames(obj); + for (let property of properties) { + // Skip constructor function + if (property === 'constructor') continue; + // Skip if the property is not generator function + if (!isGeneratorFunction(obj[property])) continue; + // Store current function + let srcFunction = obj[property]; + // Inject object/class with proxified generator function + obj[property] = function proxifier() { + return run(srcFunction.apply(this, arguments)); } + } +} - // Checks if the generator function has done executing function - if (r.done) { - // Fullfill the parent promise with return value - done(r.value); - } else { - // There is more work to do, let's identify the return value - let id = Setlist.identify(r.value); - let thenFunction = Setlist.worker.bind(null, done, halt, fn); - - if (id === 'GeneratorFunction') { - // So, this is the parent generator function. Let's stop the parent - // execution and switch context to the child function - Setlist.worker(thenFunction, halt, r.value); - //Setlist(r.value).catch(halt).then(thenFunction); - } else if (id === 'Promise') { - // Our friend know what to do as it yield promise to us - // Let's promise the next running worker - r.value.catch(halt).then(thenFunction); - } else if (id === 'Function') { - // Well, well, our friend needs help! Let's give them Promise - Setlist.async(r.value)().catch(halt).then(thenFunction); - } else { - // Yuck! you shouldn't do this, mate - thenFunction(r.value); - } +function isPromise(fn) { + return (lo.isObject(fn) && lo.isFunction(fn.then) && + lo.isFunction(fn.catch)); +} - } +function isGenerator(fn) { + return (lo.isObject(fn) && lo.isFunction(fn.next) && + lo.isFunction(fn.throw)); } -module.exports = Setlist; +function isGeneratorFunction(fn) { + return (lo.isFunction(fn) && lo.isObject(fn.constructor) && + fn.constructor.name === 'GeneratorFunction'); +} diff --git a/package.json b/package.json index b0c584e..2944640 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "setlist", - "version": "0.1.4", + "version": "0.2.0", "description": "Sequential-ish your asynchronous code with ES6 Generator Function and Promise", "main": "index.js", "scripts": { @@ -31,10 +31,17 @@ }, "homepage": "https://github.com/adzil/withmandala#readme", "devDependencies": { + "@types/lodash": "^4.14.39", + "@types/mocha": "^2.2.33", + "@types/node": "^6.0.48", + "@types/should": "^8.1.30", "coveralls": "^2.11.14", "istanbul": "^0.4.5", "mocha": "^3.1.2", "mocha-lcov-reporter": "^1.2.0", "should": "^11.1.1" + }, + "dependencies": { + "lodash": "^4.17.2" } } diff --git a/test/setlist.js b/test/setlist.js index eb6fdf4..3ace7ef 100644 --- a/test/setlist.js +++ b/test/setlist.js @@ -4,156 +4,247 @@ * @author Fadhli Dzil Ikram */ -'use strict'; - const $ = require('../lib/setlist'); -const s = require('./snippets'); const should = require('should'); -describe('Method probes', function() { - it('$ should be a function and take 2 arguments', function() { - $.should.be.a.Function().with.lengthOf(2); - }); - it('$.identify() should be a function and take 1 argument', function() { - $.identify.should.be.a.Function().with.lengthOf(1); - }); - it('$.next() should be a function and take 1 argument', function() { - $.next.should.be.a.Function().with.lengthOf(1); - }); - it('$.async() should be a function and take 1 argument', function() { - $.async.should.be.a.Function().with.lengthOf(1); - }); - it('$.convert() should be a function and take 1 argument', function() { - $.convert.should.be.a.Function().with.lengthOf(1); - }); - it('$.worker() should be a function and take 4 arguments', function() { - $.worker.should.be.a.Function().with.lengthOf(4); - }); +let promise = Promise.resolve(); +let genFn = function* () {}; +let fn = function () {}; +let obj = {}; + +describe('Internal type checking test', function() { + it('isPromise() should correctly identify promise-like', function() { + $.isPromise(promise).should.equal(true); + $.isPromise(genFn).should.equal(false); + $.isPromise(genFn()).should.equal(false); + $.isPromise(fn).should.equal(false); + $.isPromise(obj).should.equal(false); + $.isPromise(undefined).should.equal(false); + }); + it('isGenerator() should correctly identify generator', function() { + $.isGenerator(promise).should.equal(false); + $.isGenerator(genFn).should.equal(false); + $.isGenerator(genFn()).should.equal(true); + $.isGenerator(fn).should.equal(false); + $.isGenerator(obj).should.equal(false); + $.isPromise(undefined).should.equal(false); + }); + it('isGeneratorFunction() should correctly identify gen fn', function() { + $.isGeneratorFunction(promise).should.equal(false); + $.isGeneratorFunction(genFn).should.equal(true); + $.isGeneratorFunction(genFn()).should.equal(false); + $.isGeneratorFunction(fn).should.equal(false); + $.isGeneratorFunction(obj).should.equal(false); + $.isGeneratorFunction(undefined).should.equal(false); + }); }); -describe('Object identifier $.identify() test', function() { - it('Should recognize promise object', function() { - $.identify(s.prom).should.equal('Promise'); - }); - it('Should recognize generator function', function() { - $.identify(s.gf()).should.equal('GeneratorFunction'); - }); - it('Should recognize plain function', function() { - $.identify(s.fn).should.equal('Function'); - }); - it('Should not recognize plain object', function() { - $.identify(s.obj).should.equal(false); - }); + +describe('Generator runner sanity checking test', function() { + it('Should return identical promise on promise input', function() { + $(promise).should.equal(promise); + }); + it('Should return promise on generator function input', function() { + $(genFn).should.be.a.Promise(); + }); + it('Should return promise on generator input', function() { + $(genFn()).should.be.a.Promise(); + }); + it('Should throw on invalid input (ordinary function)', function() { + (function() { + $(fn) + }).should.throw(); + }); + it('Should throw on invalid input (ordinary object)', function() { + (function() { + $(obj) + }).should.throw(); + }); }); -describe('Async wrapper $.async() test', function() { - it('Should throw when passed non-function object', function() { - (function() { - $.async(s.obj); - }).should.throwError('InvalidParameter'); - }); - it('Should return as function', function() { - $.async(s.async).should.be.a.Function(); - }); - it('Should return promise when return Fn invoked', function() { - $.identify($.async(s.async)()).should.equal('Promise'); - }); - it('Should resolve the promise with no parameter', function() { - return $.async(s.callback)(undefined) - .should.fulfilledWith(undefined); - }); - it('Should resolve the promise with single parameter', function() { - return $.async(s.callback)(true) - .should.fulfilledWith(true); - }); - it('Should resolve the promise with multiple parameter', function() { - return $.async(s.callbackMulti)(true, false, null, 1) - .should.fulfilledWith([true, false, null, 1]); - }); + +describe('Generator runner functional test', function() { + it('Non-async return', function() { + let gf = function* (v) { return v }; + return $(gf(true)).should.fulfilledWith(true); + }); + it('Non-async yield return', function() { + let gf = function* (v) { return yield v }; + return $(gf(true)).should.fulfilledWith(true); + }); + it('Promise return', function() { + let gf = function* (v) { return Promise.resolve(v) }; + return $(gf(true)).should.fulfilledWith(true); + }); + it('Promise yield return', function() { + let gf = function* (v) { return yield Promise.resolve(v) }; + return $(gf(true)).should.fulfilledWith(true); + }); + it('Generator function return', function() { + let gf = function* (v) { return function* (v) { return v }(v) }; + return $(gf(true)).should.fulfilledWith(true); + }); + it('Generator function yield return', function() { + let gf = function* (v) { return yield function* (v) { return v }(v) }; + return $(gf(true)).should.fulfilledWith(true); + }); }); -describe('Task worker $.worker() test', function() { - it('Should return value from GenFn', function() { - return s.probe($.worker, s.gf(true)).should - .fulfilledWith(true); - }); - it('Should return value from child GenFn', function() { - return s.probe($.worker, s.gfParent(true)).should - .fulfilledWith(true); - }); - it('Should return value from promise', function() { - return s.probe($.worker, s.gfPromise(true)).should - .fulfilledWith(true); - }); - it('Should return value from callback Fn', function() { - return s.probe($.worker, s.gfCallback(true)).should - .fulfilledWith(true); - }); - it('Should return value from yielded value', function() { - return s.probe($.worker, s.gfYield(true)).should - .fulfilledWith(true); - }); - it('Should return value from mixed yields', function() { - return s.probe($.worker, s.gfMix(true)).should - .fulfilledWith(true); - }); - it('Should return value from parent mixed yields', function() { - return s.probe($.worker, s.gfMixParent(true)).should - .fulfilledWith(true); - }); - it('Should handle synchronous throw', function() { - return s.probe($.worker, s.gfErrSync()).should - .rejectedWith('SnippetError'); - }); - it('Should handle child synchronous throw', function() { - return s.probe($.worker, s.gfErrParent()).should - .rejectedWith('SnippetError'); - }); - it('Should handle promise throw', function() { - return s.probe($.worker, s.gfErrPromise()).should - .rejectedWith('SnippetError'); - }); + +describe('Generator runner error catching test', function() { + it('Catch promise error globally', function() { + let gf = function* () { yield Promise.reject(new Error('TestError')) }; + return $(gf).should.be.rejectedWith('TestError'); + }); + it('Catch promise error locally', function() { + let gf = function* () { + try { + yield Promise.reject(new Error('TestError')); + } catch(err) { + if (err.message === 'TestError') { + return true; + } + throw err; + } + }; + return $(gf).should.fulfilledWith(true); + }); + it('Catch generator function error globally', function() { + let gf = function* () { + yield function* () { + throw new Error('TestError'); + }(); + }; + return $(gf).should.be.rejectedWith('TestError'); + }); + it('Catch generator function error locally', function() { + let gf = function* () { + try { + yield function* () { + throw new Error('TestError'); + }(); + } catch(err) { + if (err.message === 'TestError') { + return true; + } + throw err; + } + }; + return $(gf).should.fulfilledWith(true); + }); }); -describe('Setlist $ test', function() { - it('Should throw error on non-GenFn parameter input', function() { - (function() { - $(s.fn()); - }).should.throwError('InvalidParameter'); - }); - it('Should give timeout error when timeout elapsed', function() { - return $(s.gfLong(), 1).should.rejectedWith('TimeoutError'); - }); - it('Should retun promise', function() { - $.identify($(s.gf())).should.equal('Promise'); - }); - it('Should chainable with .next() function', function() { - $(s.gf()).next.should.be.a.Function(); - }); - it('Chainable .next() should also return promise', function() { - $.identify($(s.gf()).next(s.gf())).should.equal('Promise'); - }); - it('Should correctly return value after .next()', function() { - return $(s.gf()).next(s.gf(true)).should.fulfilledWith(true); - }); + +describe('Utility sanity checking test', function() { + it('Callbackify should return function', function() { + $.callbackify(genFn).should.be.a.Function(); + }); + it('Callbackify should throw on non-gen fn input', function() { + (function() { + $.callbackify(fn); + }).should.throw(); + }); + it('Promisify should return function', function() { + $.promisify(fn).should.be.a.Function(); + }); + it('Promisified function should return promise on call', function() { + $.promisify(fn)().should.be.a.Promise(); + }); + it('Promisify should throw on non-function input', function() { + (function() { + $.promisify(obj); + }).should.throw(); + }); + it('Proxify should throw on non-object input', function() { + (function() { + $.proxify('obj'); + }).should.throw(); + }); }); -describe('Setlist $.convert() test', function() { - let target = undefined; - before(function() { - // Start convertion process - target = $.convert(s.objSrc); - }); +describe('Callbackify functional test', function() { + let gfCallback = function* (v) { return v } + let gfCallbackError = function* () { throw new Error('TestError') } + let callbackParent = function (fn, v) { + return new Promise(function(resolve, reject) { + $.callbackify(fn)(v, callbackHandle); - it('Target should only have 1 own property', function() { - Object.getOwnPropertyNames(target).should.have.lengthOf(1); - }); - it('Target should have same version of fn()', function() { - target.fn.should.equal(s.objSrc.fn); - }); - it('Target gf() should be a function', function() { - target.gf.should.be.a.Function(); - }); - it('Target gf() should return a promise', function() { - $.identify(target.gf()).should.equal('Promise'); - }); - it('Target gf() should eventually return value', function() { - return target.gf(true).should.eventually.equal(true); - }); + function callbackHandle(err, value) { + if (err) { + reject(err); + } else { + resolve(value); + } + } + }); + } + + it('Should resolve as value when return from gen fn', function() { + return callbackParent(gfCallback, true).should.fulfilledWith(true); + }); + it('Should catch error from gen fn and map proper callback', function() { + return callbackParent(gfCallbackError, true).should + .rejectedWith('TestError'); + }); }); + +describe('Promisify functional test', function() { + let callbackFn = function(err, v, cb) { + process.nextTick(() => cb(err, v)); + } + let callbackMulti = function(err, v, w, x, cb) { + process.nextTick(() => cb(err, v, w, x)); + } + + it('Should properly pass value to the promise .then()', function() { + return $.promisify(callbackFn)(null, true).should.fulfilledWith(true); + }); + it('Should properly pass error to .catch()', function() { + return $.promisify(callbackFn)(new Error('TestError'), null).should + .rejectedWith('TestError'); + }); + it('Should properly pass array to .then() on multi arguments', function() { + return $.promisify(callbackMulti)(null, true, 1, false).should + .fulfilledWith([true, 1, false]); + }); +}); + +describe('Proxify functional test', function() { + let baseClass = class { + * base() { + return yield Promise.resolve(this.var); + } + + setVar(v) { + this.var = v; + } + + static * base() { + return yield Promise.resolve(this.var); + } + + static setVar(v) { + this.var = v; + } + } + let extendedClass = class extends baseClass { + * ext() { + return yield this.base(); + } + + static * ext() { + return yield this.base(); + } + } + + // Proxify class + $.proxify(baseClass); + $.proxify(extendedClass); + + extendedClass.setVar(true); + let extendedObject = new extendedClass(); + extendedObject.setVar(true); + + it('Should properly resolve this keyword on prototype', function() { + return extendedObject.ext().should.fulfilledWith(true); + }); + it('Should properly resolve this keyword on constructor', function() { + return extendedClass.ext().should.fulfilledWith(true); + }); +}) diff --git a/test/snippets/index.js b/test/snippets/index.js deleted file mode 100644 index 04cb4e3..0000000 --- a/test/snippets/index.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * SetlistJS - Test Snippets - * Define some useful snippets for testing setlist functionality - * @author Fadhli Dzil Ikram - */ - -'use strict'; - -let s = module.exports; - -// Define ordinary function -s.fn = function(v) { - return v; -} - -// Define generator function -s.gf = function*(v) { - return v; -} - -// Define plain object -s.obj = { - test: true -} - -// Define empty promise -s.prom = new Promise((r)=>r()); - -// Define general async generator -s.async = function(cb, x) { - if (x !== undefined) { - setTimeout(x, cb); - } else { - process.nextTick(cb); - } -} - -s.throw = function(cb, x) { - let e = new Error('SnippetError'); - - if (typeof x === 'function') { - x(e); - } else { - throw e; - } -} - -// Define callback function -s.callback = function(v, cb) { - s.async(cb.bind(null, v)); -} - -s.callbackMulti = function(a, b, c, d, cb) { - s.async(cb.bind(null, a, b, c, d)); -} - -// Define promise function -s.promise = function(v) { - return new Promise((r)=>s.async(r.bind(null, v))); -} - -s.promiseErr = function() { - return new Promise((r, x)=>s.async(s.throw.bind(null, r, x))); -} - -// Create worker probe -s.probe = function(worker, fn) { - return new Promise((r, x) => { - try { - worker(r, x, fn); - } catch (err) { - x(err); - } - }); -} - -// Create sample generator functions -s.gfParent = function*(v) { - return yield s.gf(v); -} - -s.gfPromise = function*(v) { - return yield s.promise(v); -} - -s.gfCallback = function*(v) { - return yield s.callback.bind(s, v); -} - -s.gfYield = function*(v) { - return yield v; -} - -s.gfMix = function*(v) { - let val = yield s.gf(v); - val = yield s.promise(val); - val = yield s.callback.bind(s, val); - return yield val; -} - -s.gfMixParent = function*(v) { - let val = yield s.gfMix(v); - return yield s.gfMix(val); -} - -s.gfErrSync = function*() { - return s.throw(); -} - -s.gfErrParent = function*() { - yield s.gfErrSync(); -} - -s.gfErrPromise = function*() { - yield s.promiseErr(); -} - -s.gfLong = function*() { - return yield s.async.bind(s, 1000); -} - -s.objSrc = { - gf: function*(v) { return yield s.promise(v) }, - fn: function(v) { return v } -}