mutrait
Version:
A simple, powerful trait applier for ES6+ classes
646 lines (519 loc) • 16.3 kB
JavaScript
/* global suite suiteSetup test */
const { assert } = require('chai')
const {
apply,
isTraitificationOf,
wrap,
unwrap,
expresses,
BareTrait,
Trait,
Dedupe,
HasInstance,
superclass,
trait,
traits
} = require('..')
suite('mutrait', () => {
suite('apply() and isTraitificationOf()', () => {
test('apply() applies a trait', () => {
const T = (s) => class extends s {
test () {
return true
}
}
class Test extends apply(Object, T) {}
const i = new Test()
assert.isTrue(i.test())
})
test('isApplication() returns true for a trait applied by apply()', () => {
const T = (s) => class extends s {}
assert.isTrue(isTraitificationOf(apply(Object, T).prototype, T))
})
test('isApplication() works expressing wrapped traits', () => {
const T = (s) => class extends s {}
const Wrapped = wrap(T, (superclass) => apply(superclass, T))
assert.isTrue(isTraitificationOf(Wrapped(Object).prototype, Wrapped))
})
test('isApplication() returns false when it should', () => {
const T = (s) => class extends s {}
const U = (s) => class extends s {}
assert.isFalse(isTraitificationOf(apply(Object, T).prototype, U))
})
})
suite('expresses()', () => {
test('expresses() returns true for a trait applied by apply()', () => {
const T = (s) => class extends s {}
assert.isTrue(expresses(apply(Object, T).prototype, T))
})
})
suite('wrap() and unwrap()', () => {
test('wrap() sets the prototype', () => {
const f = (x) => x * x
f.test = true
const wrapper = (x) => f(x)
wrap(f, wrapper)
assert.isTrue(wrapper.test)
assert.equal(f, Object.getPrototypeOf(wrapper))
})
test('unwrap() returns the wrapped function', () => {
const f = (x) => x * x
const wrapper = (x) => f(x)
wrap(f, wrapper)
assert.equal(f, unwrap(wrapper))
})
})
suite('BareTrait', () => {
test('mixin application is on prototype chain', () => {
const T = BareTrait((s) => class extends s {})
class C extends T(class {}) {}
const i = new C()
assert.isTrue(expresses(i, T))
})
test('methods on trait are present', () => {
const T = BareTrait((s) => class extends s {
foo () { return 'foo' }
})
class C extends T(class {}) {}
const i = new C()
assert.deepEqual(i.foo(), 'foo')
})
test('fields on trait are present', () => {
const T = BareTrait((s) => class extends s {
constructor () {
super(...arguments)
this.field = 12
}
foo () { return this.field }
})
class C extends T(class {}) {}
const i = new C()
assert.deepEqual(i.field, 12)
assert.deepEqual(i.foo(), 12)
})
test('properties on trait are present', () => {
const T = BareTrait((s) => class extends s {
constructor () {
super(...arguments)
this.field = 12
}
get foo () { return this.field }
})
class C extends T(class {}) {}
const i = new C()
assert.deepEqual(i.field, 12)
assert.deepEqual(i.foo, 12)
})
test('fields on superclass are present', () => {
const T = BareTrait((s) => class extends s {
constructor () {
super(...arguments)
this.superclassField = 12
}
})
class Super {
foo () { return this.superclassField }
}
class C extends T(Super) {}
const i = new C()
assert.deepEqual(i.superclassField, 12)
assert.deepEqual(i.foo(), 12)
})
test('methods on subclass are present', () => {
const T = BareTrait((s) => class extends s {})
class C extends T(class {}) {
foo () { return 'foo' }
}
const i = new C()
assert.deepEqual(i.foo(), 'foo')
})
test('fields on subclass are present', () => {
const T = BareTrait((s) => class extends s {})
class C extends T(class {}) {
constructor () {
super(...arguments)
this.field = 12
}
foo () { return 12 }
}
const i = new C()
assert.deepEqual(i.field, 12)
assert.deepEqual(i.foo(), 12)
})
test('methods on trait override superclass', () => {
const T = BareTrait((s) => class extends s {
foo () { return 'bar' }
})
class Super {
foo () { return 'foo' }
}
class C extends T(Super) {}
const i = new C()
assert.deepEqual(i.foo(), 'bar')
})
test('fields on trait override superclass', () => {
const T = BareTrait((s) => class extends s {
constructor () {
super(...arguments)
this.field = 12
}
foo () { return this.field }
})
class Super {
constructor () {
this.field = 13
}
foo () { return this.field }
}
class C extends T(Super) {}
const i = new C()
assert.deepEqual(i.field, 12)
assert.deepEqual(i.foo(), 12)
})
test('methods on trait can call super', () => {
const T = BareTrait((s) => class extends s {
foo () { return super.foo() }
})
class Super {
foo () { return 'superfoo' }
}
class C extends T(Super) {}
const i = new C()
assert.deepEqual(i.foo(), 'superfoo')
})
test('methods on subclass override superclass', () => {
const T = BareTrait((s) => class extends s {})
class Super {
foo () { return 'superfoo' }
}
class C extends T(Super) {
foo () { return 'subfoo' }
}
const i = new C()
assert.deepEqual(i.foo(), 'subfoo')
})
test('fields on subclass override superclass', () => {
const T = BareTrait((s) => class extends s {})
class Super {
constructor () {
this.field = 12
}
foo () { return 12 }
}
class C extends T(Super) {
constructor () {
super(...arguments)
this.field = 13
}
foo () { return this.field }
}
const i = new C()
assert.deepEqual(i.field, 13)
assert.deepEqual(i.foo(), 13)
})
test('methods on subclass override mixin', () => {
const T = BareTrait((s) => class extends s {
foo () { return 'mixinfoo' }
})
class Super {}
class C extends T(Super) {
foo () { return 'subfoo' }
}
const i = new C()
assert.deepEqual(i.foo(), 'subfoo')
})
test('fields on subclass override trait', () => {
const T = BareTrait((s) => class extends s {
constructor () {
super(...arguments)
this.field = 12
}
foo () { return this.field }
})
class Super {}
class C extends T(Super) {
constructor () {
super(...arguments)
this.field = 13
}
foo () { return this.field }
}
const i = new C()
assert.deepEqual(i.field, 13)
assert.deepEqual(i.foo(), 13)
})
test('methods on subclass can call super to superclass', () => {
const M = BareTrait((s) => class extends s {})
class S {
foo () { return 'superfoo' }
}
class C extends M(S) {
foo () { return super.foo() }
}
const i = new C()
assert.deepEqual(i.foo(), 'superfoo')
})
})
suite('Dedupe', () => {
test('applies the trait the first time', () => {
const T = Dedupe(BareTrait((s) => class extends s {}))
class C extends T(class {}) {}
const i = new C()
assert.isTrue(expresses(i, T))
})
test('doesn\'t apply the trait the second time', () => {
let applicationCount = 0
const T = Dedupe(BareTrait((s) => {
applicationCount++
return class extends s {}
}))
class C extends T(T(Object)) {}
const i = new C()
assert.isTrue(expresses(i, T))
assert.equal(1, applicationCount)
})
})
suite('HasInstance', () => {
let hasNativeHasInstance = false
suiteSetup(() => {
// Enable the @@hasInstance patch in mixwith.HasInstance
if (!Symbol.hasInstance) {
Symbol.hasInstance = Symbol('hasInstance')
}
class Check {
static [Symbol.hasInstance] (o) { return true }
}
hasNativeHasInstance = 1 instanceof Check
})
test('subclasses implement traits', () => {
const T = HasInstance((s) => class extends s {})
class C extends T(class {}) {}
const i = new C()
if (hasNativeHasInstance) {
assert.instanceOf(i, C)
} else {
assert.isTrue(C[Symbol.hasInstance](i))
}
})
})
const nthPrototypeOf = (it, n) => {
if (n < 1) throw new Error('n must be >= 1')
const proto = Object.getPrototypeOf(it)
return n === 1 ? proto : nthPrototypeOf(proto, n - 1)
}
suite('superclass().expressing()', () => {
test('applies trait in order expressing superclass', () => {
const T1 = BareTrait((s) => class extends s {})
const T2 = BareTrait((t) => class extends t {})
class Super {}
class C extends superclass(Super).expressing(T1, T2) {}
const i = new C()
assert.isTrue(expresses(i, T1))
assert.isTrue(expresses(i, T2))
assert.isTrue(isTraitificationOf(nthPrototypeOf(i, 2), T2))
assert.isTrue(isTraitificationOf(nthPrototypeOf(i, 3), T1))
assert.equal(nthPrototypeOf(i, 4), Super.prototype)
})
test('applies traits in order expressing no superclass', () => {
const T1 = BareTrait((s) => class extends s {})
const T2 = BareTrait((s) => class extends s {})
class C extends traits(T1, T2) {}
const i = new C()
assert.isTrue(expresses(i, T1))
assert.isTrue(expresses(i, T2))
assert.isTrue(isTraitificationOf(nthPrototypeOf(i, 2), T2))
assert.isTrue(isTraitificationOf(nthPrototypeOf(i, 3), T1))
assert.isNotNull(nthPrototypeOf(i, 4))
assert.equal(nthPrototypeOf(i, 5), Object.prototype)
assert.isTrue(nthPrototypeOf(i, 6) === null)
})
test('superclass() can omit the superclass', () => {
const T = BareTrait((s) => class extends s {
static staticMixinMethod () {
return 42
}
foo () {
return 'foo'
}
snafu () {
return 'M.snafu'
}
})
class C extends traits(T) {
static staticClassMethod () {
return 7
}
bar () {
return 'bar'
}
snafu () {
return 'C.snafu'
}
}
let i = new C()
assert.isTrue(expresses(i, T), 'expresses')
assert.isTrue(isTraitificationOf(nthPrototypeOf(i, 2), T), 'isTraitificationOf')
assert.equal('foo', i.foo())
assert.equal('bar', i.bar())
assert.equal('C.snafu', i.snafu())
assert.equal(42, C.staticMixinMethod())
assert.equal(7, C.staticClassMethod())
})
test('class instanceof trait', () => {
const T = Trait(c => class extends c {})
const U = Trait(d => class extends d {})
class C extends traits(T, U) {}
const c = new C()
assert.isTrue(c instanceof C)
assert.isTrue(expresses(c, T))
assert.isTrue(expresses(c, U))
assert.isTrue(c instanceof T)
assert.isTrue(c instanceof U)
})
})
suite('supertraits', () => {
test('single supertrait', () => {
const Supertrait = Trait(s => class extends s {
foo () { return 'foo' }
})
const Subtrait = Trait(s => class extends superclass(s).expressing(Supertrait) {
bar () { return 'bar' }
})
class C extends trait(Subtrait) {
snafu () { return 'snafu' }
}
const c = new C()
assert.equal(c.foo(), 'foo')
assert.equal(c.bar(), 'bar')
assert.equal(c.snafu(), 'snafu')
})
test('multiple supertraits', () => {
const Supertrait1 = Trait(s => class extends s {
foo1 () { return 'foo1' }
})
const Supertrait2 = Trait(s => class extends s {
foo2 () { return 'foo2' }
})
const Subtrait = Trait(s => class extends superclass(s).expressing(Supertrait1, Supertrait2) {
bar () { return 'bar' }
})
class C extends trait(Subtrait) {
snafu () { return 'snafu' }
}
const c = new C()
assert.equal(c.foo1(), 'foo1')
assert.equal(c.foo2(), 'foo2')
assert.equal(c.bar(), 'bar')
assert.equal(c.snafu(), 'snafu')
})
test('single supertrait with correct overrides', () => {
const Supertrait = Trait(s => class extends s {
foo () { return 'foo from Supertrait' }
bar () { return 'bar from Supertrait' }
snafu () { return 'snafu from Supertrait' }
})
const Subtrait = Trait(s => {
return class extends superclass(s).expressing(Supertrait) {
bar () { return 'bar from Subtrait' }
snafu () { return 'snafu from Subtrait' }
}
})
class C extends trait(Subtrait) {
snafu () { return 'snafu from C' }
}
const c = new C()
assert.equal(c.foo(), 'foo from Supertrait')
assert.equal(c.bar(), 'bar from Subtrait')
assert.equal(c.snafu(), 'snafu from C')
})
test('multiple supertraits with correct overrides', () => {
const Supertrait1 = Trait(s => class extends s {
bleep () { return 'bleep from Supertrait1' }
foo () { return 'foo from Supertrait1' }
bar () { return 'bar from Supertrait1' }
snafu () { return 'snafu from Supertrait1' }
})
const Supertrait2 = Trait(s => class extends s {
foo () { return 'foo from Supertrait2' }
bar () { return 'bar from Supertrait2' }
snafu () { return 'snafu from Supertrait2' }
})
const Subtrait = Trait(s =>
class extends superclass(s).expressing(Supertrait1, Supertrait2) {
bar () { return 'bar from Subtrait' }
snafu () { return 'snafu from Subtrait' }
})
class C extends trait(Subtrait) {
snafu () { return 'snafu from C' }
}
const c = new C()
assert.equal(c.bleep(), 'bleep from Supertrait1')
assert.equal(c.foo(), 'foo from Supertrait2')
assert.equal(c.bar(), 'bar from Subtrait')
assert.equal(c.snafu(), 'snafu from C')
assert.isTrue(c instanceof C)
assert.isTrue(c instanceof Subtrait)
assert.isTrue(c instanceof Supertrait2)
assert.isTrue(c instanceof Supertrait1)
})
})
suite('real-world-ish traits', () => {
test('validation works', () => {
const Nameable = Trait(superclass => class extends superclass {
constructor () {
super(...arguments)
this._firstName = ''
this._lastName = ''
}
get fullName () {
return `${this._firstName} ${this._lastName}`
}
set firstName (it) {
this._firstName = this.checkFirstName(it)
}
get firstName () {
return this._firstName
}
checkFirstName (it) {
return it
}
set lastName (it) {
this._lastName = this.checkLastName(it)
}
get lastName () {
return this._lastName
}
checkLastName (it) {
return it
}
})
class Person extends trait(Nameable) {
checkFirstName (it) {
if (!it) throw new Error('nothing given')
return it
}
checkLastName (it) {
if (!it) throw new Error('nothing given')
return it
}
}
const first = 'Cheeky'
const last = 'Monkey'
const me = new Person()
me.firstName = first
me.lastName = last
assert.equal(first, me._firstName)
assert.equal(last, me._lastName)
assert.equal(first, me.firstName)
assert.equal(last, me.lastName)
assert.equal(`${first} ${last}`, me.fullName)
assert.throws(() => {
me.firstName = null
})
assert.throws(() => {
me.lastName = null
})
})
})
})