@lucaspaganini/value-objects
Version:
TypeScript first validation and class creation library
164 lines (138 loc) • 5.34 kB
text/typescript
import { expectTypeOf } from 'expect-type'
import { VOError, VOObject, VOObjectOptions } from '../..'
import { isNull } from '../utils'
import { constructorFn } from './utils'
describe('VOObject', () => {
class AAA {
constructor(raw: 123) {
if (isNull(raw)) throw new VOError('AAA test error')
}
valueOf(): 'aaa' {
return 'aaa'
}
}
class BBB {
constructor(raw: 456) {
if (isNull(raw)) throw new VOError('BBB test error')
}
valueOf(): 'bbb' {
return 'bbb'
}
}
class CCC {
constructor(raw: 789) {
if (isNull(raw)) throw new VOError('CCC test error')
}
valueOf(): 'ccc' {
return 'ccc'
}
}
it('Should return a class that can be extended', () => {
class Test extends VOObject({ aaa: AAA, bbb: BBB, ccc: CCC }) {
test() {
return 'test'
}
}
const instance = new Test({ aaa: 123, bbb: 456, ccc: 789 })
expect(instance.test()).toBe('test')
expect(instance.valueOf()).toEqual({ aaa: 'aaa', bbb: 'bbb', ccc: 'ccc' })
})
it('Should return an record of instantiated inner classes', () => {
class Test extends VOObject({ aaa: AAA, bbb: BBB, ccc: CCC }) {}
const instance = new Test({ aaa: 123, bbb: 456, ccc: 789 })
expect(instance.aaa instanceof AAA).toBe(true)
expect(instance.bbb instanceof BBB).toBe(true)
expect(instance.ccc instanceof CCC).toBe(true)
})
it("Should call each inner class' valueOf() when it's valueOf() is called", () => {
class Test extends VOObject({ aaa: AAA, bbb: BBB, ccc: CCC }) {}
const instance = new Test({ aaa: 123, bbb: 456, ccc: 789 })
const aaa = new AAA(123)
const bbb = new BBB(456)
const ccc = new CCC(789)
expect(instance.valueOf()).toEqual({ aaa: aaa.valueOf(), bbb: bbb.valueOf(), ccc: ccc.valueOf() })
})
it("Should add a .prop property to errors thrown by the instantiation of it's inner classes", () => {
class Test extends VOObject({ aaa: AAA, bbb: BBB, ccc: CCC }, { maxErrors: 10 }) {}
const tests = [
{ aaa: null, props: ['aaa'] },
{ bbb: null, props: ['bbb'] },
{ ccc: null, props: ['ccc'] },
{ aaa: null, bbb: null, props: ['aaa', 'bbb'] },
{ bbb: null, ccc: null, props: ['bbb', 'ccc'] },
{ aaa: null, ccc: null, props: ['aaa', 'ccc'] },
{ aaa: null, bbb: null, ccc: null, props: ['aaa', 'bbb', 'ccc'] },
]
for (const test of tests) {
const validRaws = { aaa: 123, bbb: 456, ccc: 789 } as const
const fn = () => new Test({ ...validRaws, ...(<any>test) })
expect(fn).toThrowMatching(
(errArray): boolean =>
Array.isArray(errArray) &&
errArray.length === test.props.length &&
errArray.every(VOError.is) &&
errArray.every(err => test.props.includes(<string>err.path.toArray().pop())),
)
}
})
it('Should throw on base class creation if one of the options is invalid', () => {
const tests: Array<VOObjectOptions & { error: string | null }> = [
{ maxErrors: -1, error: 'Too small' },
{ maxErrors: 0, error: null },
{ maxErrors: 1, error: null },
{ maxErrors: 500, error: null },
{ maxErrors: 1.5, error: 'Not an integer' },
{ maxErrors: '123' as any, error: 'Wrong raw value type' },
]
for (const test of tests) {
const fn = () => VOObject({ aaa: AAA }, test)
if (isNull(test.error)) expect(fn).not.toThrow()
else expect(fn).toThrowError(test.error)
}
})
it('Should be able to set a max errors and throw when the limit is reached or when there are no more values to instantiate', () => {
const tests = [10, 99, 287, 573, 904, 2187, 6700]
class Base {
constructor(raw: number) {
throw new VOError('Test error')
}
valueOf(): 'base test' {
return 'base test'
}
}
const schema = Array.from({ length: Math.max(...tests) * 2 }).reduce<{ [key: string]: typeof Base }>(
(acc, _, i) => {
acc[i.toString()] = Base
return acc
},
{},
)
const rawValues = Object.keys(schema).reduce<{ [key: string]: number }>((acc, key, i) => {
acc[key] = i
return acc
}, {})
for (const maxErrors of tests) {
class Test extends VOObject(schema, { maxErrors }) {}
const fn = () => new Test(rawValues)
expect(fn).toThrowMatching((errArray): boolean => {
const props = new Set(Object.keys(schema))
return (
Array.isArray(errArray) &&
errArray.length === maxErrors &&
errArray.every(VOError.is) &&
errArray.every(err => props.has(<string>err.path.toArray().pop()))
)
})
}
})
it('Should have the correct types', () => {
class Test extends VOObject({ aaa: AAA, bbb: BBB, ccc: CCC }) {}
const instance = new Test({ aaa: 123, bbb: 456, ccc: 789 })
expectTypeOf(constructorFn(Test)).toEqualTypeOf<(r: { aaa: 123; bbb: 456; ccc: 789 }) => Test>()
expectTypeOf(instance.aaa).toEqualTypeOf<AAA>()
expectTypeOf(instance.bbb).toEqualTypeOf<BBB>()
expectTypeOf(instance.ccc).toEqualTypeOf<CCC>()
expectTypeOf(instance.valueOf()).toEqualTypeOf<{ aaa: 'aaa'; bbb: 'bbb'; ccc: 'ccc' }>()
expectTypeOf(instance.toRaw()).toEqualTypeOf<{ aaa: 'aaa'; bbb: 'bbb'; ccc: 'ccc' }>()
})
})