immutable-class-tester
Version:
A helper for testing immutable classes
144 lines (143 loc) • 5.86 kB
JavaScript
import deepEqual from 'deep-equal';
import hasOwnProp from 'has-own-prop';
const PROPERTY_KEYS = [
'name',
'defaultValue',
'possibleValues',
'validate',
'immutableClass',
'immutableClassArray',
'immutableClassLookup',
'equal',
'toJS',
'type',
'contextTransform',
'preserveUndefined',
'emptyArrayIsOk',
];
export function testImmutableClass(ClassFn, objects, options = {}) {
if (typeof ClassFn !== 'function')
throw new TypeError(`ClassFn must be a constructor function`);
if (!Array.isArray(objects) || !objects.length) {
throw new TypeError(`objects must be a non-empty array of js to test`);
}
const newThrows = options.newThrows;
const context = options.context;
const className = ClassFn.name;
if (className.length < 1)
throw new Error(`Class must have a name of at least 1 letter`);
const instanceName = className[0].toLowerCase() + className.substring(1);
if (typeof ClassFn.fromJS !== 'function')
throw new Error(`${className}.fromJS should exist`);
const instance = ClassFn.fromJS(objects[0], context);
const objectProto = Object.prototype;
if (instance.valueOf === objectProto.valueOf) {
throw new Error(`Instance should implement valueOf`);
}
if (instance.toString === objectProto.toString) {
throw new Error(`Instance should implement toString`);
}
if (typeof instance.toJS !== 'function') {
throw new Error(`Instance should have a toJS function`);
}
if (typeof instance.toJSON !== 'function') {
throw new Error(`Instance should have a toJSON function`);
}
if (typeof instance.equals !== 'function') {
throw new Error(`Instance should have an equals function`);
}
if (ClassFn.PROPERTIES) {
if (!Array.isArray(ClassFn.PROPERTIES)) {
throw new Error('PROPERTIES should be an array');
}
ClassFn.PROPERTIES.forEach((property, i) => {
if (typeof property.name !== 'string') {
throw new Error(`Property ${i} is missing a name`);
}
Object.keys(property).forEach(key => {
if (!PROPERTY_KEYS.includes(key)) {
throw new Error(`PROPERTIES should include ${key}`);
}
});
});
}
for (let i = 0; i < objects.length; i++) {
const where = `[in object ${i}]`;
const objectJSON = JSON.stringify(objects[i]);
const objectCopy1 = JSON.parse(objectJSON);
const objectCopy2 = JSON.parse(objectJSON);
const inst = ClassFn.fromJS(objectCopy1, context);
if (!deepEqual(objectCopy1, objectCopy2)) {
throw new Error(`${className}.fromJS function modified its input :-(`);
}
if (!(inst instanceof ClassFn)) {
throw new Error(`${className}.fromJS did not return a ${className} instance ${where}`);
}
if (typeof inst.toString() !== 'string') {
throw new Error(`${instanceName}.toString() must return a string ${where}`);
}
if (inst.equals(undefined) !== false) {
throw new Error(`${instanceName}.equals(undefined) should be false ${where}`);
}
if (inst.equals(null) !== false) {
throw new Error(`${instanceName}.equals(null) should be false ${where}`);
}
if (inst.equals([]) !== false) {
throw new Error(`${instanceName}.equals([]) should be false ${where}`);
}
if (!deepEqual(inst.toJS(), objects[i])) {
throw new Error(`${className}.fromJS(obj).toJS() was not a fixed point (did not deep equal obj) ${where}`);
}
const instValueOf = inst.valueOf();
if (inst.equals(instValueOf)) {
throw new Error(`inst.equals(inst.valueOf()) ${where}`);
}
const instLazyCopy = {};
for (const key in inst) {
if (!hasOwnProp(inst, key))
continue;
instLazyCopy[key] = inst[key];
}
if (inst.equals(instLazyCopy)) {
throw new Error(`inst.equals(*an object with the same values*) ${where}`);
}
if (newThrows) {
let badInst;
let thrownError;
try {
badInst = new ClassFn(instValueOf);
}
catch (e) {
thrownError = e;
}
if (!thrownError || badInst) {
throw new Error(`new ${className} did not throw as indicated ${where}`);
}
}
else {
const instValueCopy = new ClassFn(instValueOf);
if (!inst.equals(instValueCopy)) {
throw new Error(`new ${className}().toJS() is not equal to the original ${where}`);
}
if (!deepEqual(instValueCopy.toJS(), inst.toJS())) {
throw new Error(`new ${className}(${instanceName}.valueOf()).toJS() returned something bad ${where}`);
}
}
const instJSONCopy = ClassFn.fromJS(JSON.parse(JSON.stringify(inst)), context);
if (!inst.equals(instJSONCopy)) {
throw new Error(`JS Copy does not equal original ${where}`);
}
if (!deepEqual(instJSONCopy.toJS(), inst.toJS())) {
throw new Error(`${className}.fromJS(JSON.parse(JSON.stringify(${instanceName}))).toJS() returned something bad ${where}`);
}
}
for (let j = 0; j < objects.length; j++) {
const objectJ = ClassFn.fromJS(objects[j], context);
for (let k = j; k < objects.length; k++) {
const objectK = ClassFn.fromJS(objects[k], context);
if (objectJ.equals(objectK) !== Boolean(j === k)) {
throw new Error(`Equality of objects ${j} and ${k} was wrong`);
}
}
}
}