UNPKG

extensible

Version:

Create highly extensible software components.

446 lines (358 loc) 13 kB
var assert = require('assert'); var sinon = require('sinon'); var extensible = require('../index'); var has = require('has'); for (var k in assert) global[k] = assert[k]; describe('extensible', function() { describe('object', function() { var obj; describe('with extensions', function() { var top, mid, bot, methodName = 'm'; beforeEach(function() { top = { state: function(arg, next) { equal(this, obj); next(arg, passedState = {data: 5}); } }; top[methodName] = function(arg1, arg2, arg3, cb, next, layer) { equal(this, obj); equal(top, layer.impl); next(arg1 * 64, arg2, arg3, function(err, rv) { cb(err, rv / 64); }); }; mid = { state: function(arg, next) { equal(this, obj); next(arg); } }; mid[methodName] = function(arg1, arg2, arg3, cb, next, layer) { equal(this, obj); equal(mid, layer.impl); next(arg1 * 64, arg2, arg3, function(err, rv) { cb(err, rv / 64); }); }; bot = function(opts) { equal(this, obj); // add a method this.$defineMethod(opts.methodName, 'arg1, arg2, arg3, cb'); this.$defineMethod('state', 'arg'); var rv = { state: function(arg, next, layer, state) { equal(this, obj); equal(rv, layer.impl); equal(passedState, state); } }; rv[methodName] = function(arg1, arg2, arg3, cb, next, layer) { equal(this, obj); equal(rv, layer.impl); cb([1, 2], arg1); }; return rv; }; obj = extensible(); obj.$use(bot, {methodName: methodName}); obj.$use(mid); obj.$use(top); sinon.spy(obj.$layers.top.impl, methodName); sinon.spy(obj.$layers.top.next.impl, methodName); sinon.spy(obj.$layers.top.next.next.impl, methodName); }); it('passes arguments from top to bottom layer', function() { obj[methodName](1, 3, 4, function() {}); assert(obj.$layers.top.impl[methodName].calledWith(1, 3, 4)); assert(obj.$layers.top.next.impl[methodName].calledWith(64, 3, 4)); assert(obj.$layers.top.next.next.impl[methodName].calledWith(4096, 3, 4)); }); it('passes result from bottom to top layer', function(done) { obj[methodName](1, null, null, function(err, rv) { deepEqual([1, 2], err); deepEqual(1, rv); done(); }); }); it('should pass state across layers', function() { obj.state(5); // assertion is done in layer definition }); describe('$eachLayer', function() { it('iterates through each layer', function() { var items = []; obj.$eachLayer(function(layer) { items.push(layer.impl); }); deepEqual([obj.$layers.top.next.next.impl, mid, top], items); }); }); describe('$eachMethodDescriptor', function() { it('iterates through each method metadata', function() { var items = []; obj.$eachMethodDescriptor(function(method) { items.push(method); }); meql([{ name: 'm', args: ['arg1', 'arg2', 'arg3', 'cb'] }, { name: 'state', args: ['arg'] }], items); }); }); describe('$getMethodDescriptor', function() { it('gets method by name', function() { meql({name: 'm', args: ['arg1', 'arg2', 'arg3', 'cb']}, obj.$getMethodDescriptor('m')); meql({name: 'state', args: ['arg']}, obj.$getMethodDescriptor('state')); }); }); describe('$instance', function() { it('links through the prototype chain', function() { assert(obj.isPrototypeOf(obj.$instance())); }); it('calls $constructor when defined', function() { obj.$defineMethod('$constructor', 'arg1, arg2'); obj.$use({ $constructor: function(arg1, arg2) { this.arg1 = arg1; this.arg2 = arg2; } }); var obj2 = obj.$instance('a1', 'a2'); equal(obj2.arg1, 'a1'); equal(obj2.arg2, 'a2'); }); }); describe('$fork', function() { var forked; beforeEach(function() { forked = obj.$fork(); }); it('links through the prototype chain', function() { assert(obj.isPrototypeOf(forked)); }); it('links through $parent', function() { equal(forked.$parent, obj); }); describe('$instanceOf', function() { var child; beforeEach(function() { child = forked.$fork(); }); it('is true if an object forked directly', function() { assert(child.$instanceOf(forked)); }); it('is true if an object forked indirectly', function() { assert(child.$instanceOf(obj)); }); }); it('should copy method descriptors', function() { notEqual(obj.$descriptors, forked.$descriptors); meql({ m: { name: 'm', args: ['arg1', 'arg2', 'arg3', 'cb'] }, state: { name: 'state', args: ['arg'] } }, obj.$descriptors); meql({ m: { name: 'm', args: ['arg1', 'arg2', 'arg3', 'cb'] }, state: { name: 'state', args: ['arg'] } }, forked.$descriptors); }); it('should copy layers', function() { notEqual(obj.$layers.top, forked.$layers.top); notEqual(obj.$layers.top.next, forked.$layers.top.next); notEqual(obj.$layers.top.next.next, forked.$layers.top.next.next); equal(obj.$layers.top.impl, forked.$layers.top.impl); equal(obj.$layers.top.next.impl, forked.$layers.top.next.impl); equal(obj.$layers.top.next.next.impl, forked.$layers.top.next.next.impl); }); it('should copy layers', function() { notEqual(obj.$layers.top, forked.$layers.top); notEqual(obj.$layers.top.next, forked.$layers.top.next); notEqual(obj.$layers.top.next.next, forked.$layers.top.next.next); equal(obj.$layers.top.impl, forked.$layers.top.impl); equal(obj.$layers.top.next.impl, forked.$layers.top.next.impl); equal(obj.$layers.top.next.next.impl, forked.$layers.top.next.next.impl); }); describe('forked object', function() { it("wont affect the original object methods", function() { forked.$defineMethod('y'); meql({ m: { name: 'm', args: ['arg1', 'arg2', 'arg3', 'cb'] }, state: { name: 'state', args: ['arg'] } }, obj.$descriptors); meql({ m: {name: 'm', args: ['arg1', 'arg2', 'arg3', 'cb'] }, state: { name: 'state', args: ['arg'] }, y: { name: 'y', args: [] } }, forked.$descriptors); equal(true, 'm' in obj); equal(false, 'y' in obj); equal(true, 'y' in forked); }); it("wont affect the original object layers", function() { forked.$use(top); equal(top, forked.$layers.top.impl); equal(top, forked.$layers.top.next.impl); equal(mid, forked.$layers.top.next.next.impl); equal(top, obj.$layers.top.impl); equal(mid, obj.$layers.top.next.impl); }); }); }); describe('with upgraded method', function() { describe('and implementation', function() { beforeEach(function() { // remove 1 arg obj.$defineMethod(methodName, 'arg1, arg2, cb'); // add a new layer with the new signature var newTop = {}; newTop[methodName] = function(arg1, arg2, cb, next, layer) { equal(this, obj); equal(newTop, layer.impl); // the next layer should be unaffected next(arg1, arg2, 1000, cb); }; obj.$use(newTop); sinon.spy(newTop, methodName); }); it('should expose new API', function() { obj[methodName](1, 3, function() {}); assert(obj.$layers.top.impl[methodName].calledWith(1, 3)); assert(obj.$layers.top.next.impl[methodName].calledWith(1, 3, 1000)); assert(obj.$layers.top.next.next.impl[methodName] .calledWith(64, 3, 1000)); assert(obj.$layers.top.next.next.next.impl[methodName].calledWith( 4096, 3, 1000)); }); }); describe('and missing implementation', function() { it('should throw when DEBUG is set', function() { obj.DEBUG = true; obj.$defineMethod(methodName, 'arg1, arg2, cb'); throws(function() { obj[methodName](1, 3, function() {}); }, /Layer class implementation missing/); }); }); }); }); describe('with missing layer method', function() { it('should throw when DEBUG is true', function() { obj = extensible(); obj.DEBUG = true; obj.$defineMethod('missing', 'arg1, arg2, cb'); obj.$use({ another: function(next) { return 1; } }); throws(function() { obj.missing(1, 3, function() {}); }, /Method 'missing' has no more layers/); }); }); }); describe('callable', function() { var func; beforeEach(function() { func = extensible().$fork(true); }); describe('with a $call method defined', function() { beforeEach(function() { func.$defineMethod('$call', 'name'); func.$use({ $call: function(name, next, layer, state, self) { return 'Hello ' + name + ' from ' + self.origin; } }); func.origin = 'greeter'; }); it('can be called like a function', function() { equal('Hello world from greeter', func('world')); }); it('can be extended normally', function() { func.$defineMethod('$call', 'name, origin'); func.$use({ $call: function(name, another, next) { return next(name) + ', extended by ' + another; } }); equal('Hello world from greeter, extended by foo', func('world', 'foo')); }); it('can be called like a method', function() { func.$defineMethod('$call'); func.$use({ $call: function() { return 'Hello from ' + this.name; } }); var obj = { greet: func, name: 'object' }; equal('Hello from object', obj.greet()); }); it('can be inherited', function() { var f2 = func.$instance(); // this will also alter 'func' since they share layers f2.$use({ $call: function(name, next) { return next('constant'); } }); // modify the origin for f2 f2.origin = 'another'; equal(f2('world'), 'Hello constant from another'); equal(func('world'), 'Hello constant from greeter'); }); it('can be forked', function() { var f2 = func.$fork(); f2.$use({ $call: function(name, next) { return next('constant'); } }); f2.origin = 'another'; equal(f2('world'), 'Hello constant from another'); equal(func('world'), 'Hello world from greeter'); }); }); it('is linked with parent', function() { var f2 = func.$instance(); equal(f2.$parent, func); }); describe('instanceOf', function() { var f1, f2; beforeEach(function() { f1 = func.$fork(); f2 = f1.$fork(); }); it('is true if an object forked directly', function() { assert(f1.$instanceOf(func)); }); it('is true if an object forked indirectly', function() { assert(f2.$instanceOf(func)); }); }); describe('without defining a $call method', function() { it('throws when called like a function', function() { throws(function() { func(); }); }); }); }); }); function meql(expected, actual) { if (!actual.args) { for (var k in actual) { if (!has(actual, k)) continue; delete actual[k].objectMethod; delete actual[k].layerMethod; } } else { delete actual.objectMethod; delete actual.layerMethod; } return deepEqual(expected, actual); }