UNPKG

mics

Version:

Multiple Inheritance Class System: Intuitive mixins for ES6 classes

438 lines (355 loc) 13.9 kB
import ulog from 'ulog' import { expect } from 'chai' import { spy } from 'sinon' import t from 'tcomb' import { mix, is, like } from './' const log = ulog('mics:spec') describe('mix([superclass] [, ...mixins] [, factory])', function() { it('is a function', function(){ expect(mix).to.be.a('function') }) it('creates a mixin from a class factory', function(){ const M = mix(superclass => class M extends superclass {}) expect(M).to.be.a('function') }) it('creates a mixin from other mixins and a class factory', function(){ const X = mix(superclass => class X extends superclass {}) const Y = mix(superclass => class X extends superclass {}) const M = mix(X, Y, superclass => class M extends superclass {}) expect(M).to.be.a('function') }) it('creates a mixin from other mixins', function(){ const X = mix(superclass => class X extends superclass {}) const Y = mix(superclass => class X extends superclass {}) const M = mix(X, Y) expect(isMixin(M)).to.eq(true) }) it('creates a mix from a superclass', function(){ let C = mix(class Base {}) expect(C).to.be.a('function') expect(isMixin(C)).to.eq(true) // special case: class with one-arg constructor looks like a factory class Base {constructor(){}} C = mix(Base) expect(C).to.be.a('function') expect(isMixin(C)).to.eq(true) expect(new C() instanceof Base).to.eq(true) }) it('creates a mix from a mixed superclass', function(){ const C = mix(class Base {}) const D = mix(C) expect(isMixin(C)).to.eq(true) expect(isMixin(D)).to.eq(true) expect(new D() instanceof D).to.eq(true) }) it('created mixins can be invoked with new to instantiate instances', function(){ const M = mix(superclass => class M extends superclass {}) const m = new M() expect(m).to.be.an('object') }) it('created mixins can be invoked without new to instantiate instances', function(){ const M = mix(superclass => class M extends superclass {}) const m = M() expect(m).to.be.an('object') }) it('arguments passed when invoking the mixin with new are passed on to the constructor', function(){ let xarg,yarg,zarg const M = mix(superclass => class M extends superclass { constructor(x, y, z) { super() xarg = x yarg = y zarg = z } }) new M('x','y','z') expect(xarg).to.eq('x') expect(yarg).to.eq('y') expect(zarg).to.eq('z') }) it('arguments passed when invoking the mixin without new are passed on to the constructor', function(){ let xarg,yarg,zarg const M = mix(superclass => class M extends superclass { constructor(x, y, z) { super() xarg = x yarg = y zarg = z } }) new M('x','y','z') expect(xarg).to.eq('x') expect(yarg).to.eq('y') expect(zarg).to.eq('z') }) it('var args in constructor has correct length when invoking with new', function(){ let argsarg const M = mix(superclass => class M extends superclass { constructor(...args) { super() argsarg = args } }) new M('x','y','z') expect(argsarg.length).to.eq(3) new M() expect(argsarg.length).to.eq(0) }) it('var args in constructor has correct length when invoking without new', function(){ let argsarg const M = mix(superclass => class M extends superclass { constructor(...args) { super() argsarg = args } }) M('x','y','z') expect(argsarg.length).to.eq(3) M() expect(argsarg.length).to.eq(0) }) it('result of invoking constructor with new is instanceof mixin', function(){ const M = mix(superclass => class M extends superclass {}) const m = new M('x','y','z') expect(m instanceof M).to.eq(true) }) it('result of invoking constructor without new is instanceof mixin', function(){ const M = mix(superclass => class M extends superclass {}) const m = M('x','y','z') expect(m instanceof M).to.eq(true) }) it('picks up a static class method `constructor` and uses it in place of the default constructor', function(){ const check = spy() const M = mix(superclass => class M extends superclass { static constructor(...args) { // todo: missing superclass constructor invocation? // stijn: this is not a normal constructor but a 'static' constructor... // It is the ES5 function that is returned by `mix`, can be invoked without // `new` and in turn calls the 'real' ES6 constructor. // Unless you make a static constructor like in this test, `mix` will // generate an ES5 constructor function for you on the fly. This mechanism // allows you to customize that. The call to `new this(...args)` will // call the ES6 constructor which will call super as usual. log.log('Custom constructor') check() return new this(...args) } }) M() expect(check.called).to.eq(true) }) it('has no side effects on it\'s arguments', function(){ class Test{} expect(isMixin(Test)).to.eq(false) const M = mix(Test) expect(isMixin(M)).to.eq(true) expect(isMixin(Test)).to.eq(false) const N = mix(Test, superclass => class N extends superclass {}) expect(isMixin(N)).to.eq(true) expect(isMixin(Test)).to.eq(false) }) }) describe('is(x , type)', function(){ it('is a function', function(){ expect(is).to.be.a('function') }) it('accepts one or two arguments', function(){ expect(is.length).to.eq(2) }) it('tests whether object `x` implements `type`', function(){ const X = mix(superclass => class X extends superclass {}) const x = new X() expect(is(x, X)).to.eq(true) expect(is(x, Date)).to.eq(false) }) it('tests whether class `x` implements `type`', function(){ const Y = mix(superclass => class Y extends superclass {}) const X = class X extends mix(Y) {} expect(is(X, Y)).to.eq(true) }) it('tests whether mixin `x` implements `type`', function(){ const Y = mix(superclass => class Y extends superclass {}) const X = mix(Y, superclass => class X extends superclass {}) expect(is(X, Y)).to.eq(true) }) }) describe('like(type)', function(){ it('is a function', function(){ expect(like).to.be.a('function') }) it('tests whether `x` can be treated as `type` (has the same interface)', function(){ const Looker = mix(superclass => class Looker extends superclass { look(){} }) expect(like('Hi', Looker)).to.eq(false) expect(like(8, Looker)).to.eq(false) expect(like({}, Looker)).to.eq(false) expect(like(new Looker(), Looker)).to.eq(true) expect(like({ look(){} }, Looker)).to.eq(true) expect(like({ walk(){} }, Looker)).to.eq(false) class Base {look(){}} expect(like(Base, Looker)).to.eq(true) expect(like(new Base(), Looker)).to.eq(true) class Derived extends Base {} expect(like(Derived, Looker)).to.eq(true) expect(like(new Derived(), Looker)).to.eq(true) }) it('allows mixins to be used as interfaces', (done) => { const expected = 'Hello, World!' const Thenable = mix(superclass => class Thenable extends superclass { then() {} }) class MyPromise { then(resolve) { resolve(expected) } } const promise = new MyPromise() expect(like(promise, Thenable)).to.eq(true) Promise.resolve(promise).then((result) => { expect(result).to.eq(expected) done() }) }) }) describe('mix example', function(){ it ('shows how to create a mixin using an es6 class', function(){ const constr = spy() const look = spy() const Looker = mix(superclass => class Looker extends superclass { constructor() { super() log.log('A looker is born!') constr() } look() { log.log('Looking good!') look() } }) expect(Looker).to.be.a('function') const looker = new Looker() expect(looker).to.be.an('object') expect(constr.called).to.eq(true) expect(looker).to.have.a.property('look') expect(looker.look).to.be.a('function') looker.look() expect(look.called).to.eq(true) }) it('shows how to composes multiple mixins', function(){ const look = spy() const walk = spy() const talk = spy() const Looker = mix(superclass => class Looker extends superclass { look(){ log.log('Looking good!') look() } }) const Walker = mix(superclass => class Walker extends superclass { walk(){ log.log('Step, step, step...') walk() } }) const Talker = mix(superclass => class Talker extends superclass { talk(){ log.log('Blah, blah, blah...') talk() } }) const duckTalk = spy() const Duck = mix(Looker, Walker, Talker, superclass => class Duck extends superclass { talk(){ log.log('Quack, quack, quack!') duckTalk() super.talk() } }) const duck = new Duck() expect(duck).to.be.an('object') expect(duck instanceof Duck).to.eq(true) expect(duck instanceof Looker).to.eq(true) expect(duck instanceof Walker).to.eq(false) expect(duck).to.have.a.property('look') expect(duck.look).to.be.a('function') duck.look() expect(look.called).to.eq(true) expect(duck).to.have.a.property('walk') expect(duck.walk).to.be.a('function') duck.walk() expect(walk.called).to.eq(true) expect(duck).to.have.a.property('talk') expect(duck.talk).to.be.a('function') duck.talk() expect(talk.called).to.eq(true) expect(duckTalk.called).to.eq(true) }) }) describe('type checked mixins with tcomb', function(){ it ('shows how to create an immutable, type-checked mixin with tcomb', function(){ // This is experimental... I think it shows we need to be able to hook into the // mixin process itself. To enable this example I already added a hook for the // ES5 constructor function: the `static constructor` will be picked up by `mix` // and used as the result instead of the default generated constructor. const a = spy() const b = spy() const Person = mix(superclass => class Person extends superclass { static get type() { if (!this._tcomb) this._tcomb = t.struct({ name: t.String, // required string surname: t.maybe(t.String), // optional string age: t.Integer, // required integer tags: t.list(t.String) // a list of strings }, 'Person') return this._tcomb } static testA() { a() log.log('A') } static testB() { this.testA() b() log.log('B') } static constructor(...args) { // todo: missing superclass constructor call? return this.type(new this(...args)) } constructor(...args) { super(...args) Object.assign(this, ...args) } }) expect(function(){ Person({ surname: 'Canti' }); }).to.throw(TypeError) // required fields missing expect(function(){ Person({ name: 'Stijn', age: 40, tags: ['developer'] }); }).to.not.throw() // ok expect(function(){ const person = Person({ name: 'Stijn', age: 40, tags: ['developer'] }); person.age = 41 }).to.throw(TypeError) // immutable expect(function(){ Person.testB() expect (a.called).to.eq(true) expect (b.called).to.eq(true) }).to.not.throw() // ok }) }) // Helper copied straight from `mics`. Was used in some tests so this is easiest function isMixin(x) { return (typeof x == 'function') && !!x.mixin }