remx
Version:
Opinionated mobx
352 lines (309 loc) • 10.9 kB
JavaScript
import * as remx from './remx';
import * as mobx from 'mobx';
import { grabConsole } from '../utils/testUtils';
const strictError = expect.stringContaining(
'[MobX] Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed. Tried to modify:'
);
describe('remx!', () => {
let state, setters, getters, getNameCalled, getFullNameCalled;
beforeEach(() => {
getNameCalled = 0;
getFullNameCalled = 0;
state = remx.state({
name: 'Gandalf',
lastName: 'The grey',
age: {
is: 0
},
job: {
experience: 'infinite'
},
dynamicallyCreatedKeys: {},
multiPropObject: {
prop1: '1',
prop2: '2',
prop3: '3'
},
race: 'unknown'
});
setters = remx.setters({
bla: 'blabla',
setMultiPropObject() {
state.multiPropObject.prop1 = 'changed!';
state.multiPropObject.prop2 = 'changed!';
state.multiPropObject.prop3 = 'changed!';
},
setName(name) {
state.name = name;
},
setAge(n) {
state.age.is = n;
},
createKeyDynamically(n) {
state.dynamicallyCreatedKeys.inner = n;
},
usingMerge(n) {
remx.merge(state, { dynamicallyCreatedKeys: { foo: n } });
},
usingMergeWithValue(v) {
remx.merge(state, { dynamicallyCreatedKeys: v });
},
setJobDescriptionUsingMerge(v) {
remx.merge(state, { job: { description: v } });
},
setRace(v) {
remx.merge(state, { race: v });
}
});
getters = remx.getters({
getMultiPropObject() {
return state.multiPropObject;
},
getName() {
getNameCalled++;
return state.name;
},
getFullName(separator) {
getFullNameCalled++;
return getters.getName() + separator + state.lastName;
},
getDynamicallyCreatedKey() {
return state.dynamicallyCreatedKeys.inner || 'not yet set';
},
getAge() {
return state.age.is;
}
});
});
it('preserves objects shapes', () => {
expect(Object.keys(state)).toEqual([
'name',
'lastName',
'age',
'job',
'dynamicallyCreatedKeys',
'multiPropObject',
'race'
]);
expect(Object.keys(getters)).toEqual([
'getMultiPropObject',
'getName',
'getFullName',
'getDynamicallyCreatedKey',
'getAge'
]);
expect(Object.keys(setters)).toEqual([
'setMultiPropObject',
'setName',
'setAge',
'createKeyDynamically',
'usingMerge',
'usingMergeWithValue',
'setJobDescriptionUsingMerge',
'setRace'
]);
});
it('wraps observable state without impacting testability', () => {
expect(state.name).toEqual('Gandalf');
expect(state.lastName).toEqual('The grey');
});
it('wraps observable state without impacting testability', () => {
expect(state.name).toEqual('Gandalf');
expect(state.lastName).toEqual('The grey');
});
it('enforces strict mode, no one is allowed to touch state outside of mobx actions', () => {
const stopObservation = mobx.autorun(() => getters.getName());
expect(grabConsole(() => {
state.name = 'hi';
})).toEqual([['warn', strictError]]);
stopObservation();
});
it('enforces strict mode recursively', () => {
const stopObservation = mobx.autorun(() => getters.getAge());
expect(grabConsole(() => {
state.age.is = 3;
})).toEqual([['warn', strictError]]);
stopObservation();
});
it('setters are wrapped in mobx action', () => {
expect(state.name).toEqual('Gandalf');
setters.setName('other');
expect(state.name).toEqual('other');
});
it('setters can mutate nested objects', () => {
expect(state.age.is).toEqual(0);
setters.setAge(29);
expect(state.age.is).toEqual(29);
});
it('setters handle functions only', () => {
expect(setters.bla).toEqual(undefined);
});
it('getters are accessors', () => {
expect(getters.getName).toBeInstanceOf(Function);
expect(getters.getName()).toEqual('Gandalf');
});
it('getters argumentless functions are treated normally', () => {
expect(getNameCalled).toBe(0);
expect(getters.getName()).toEqual('Gandalf');
expect(getNameCalled).toBe(1);
for (let i = 0; i < 4; i++) {
expect(getters.getName()).toEqual('Gandalf');
}
expect(getNameCalled).toBe(5);
setters.setName('bob');
expect(getters.getName()).toEqual('bob');
expect(getNameCalled).toBe(6);
});
it('getters argumentless functions are cached when observed', () => {
expect(getNameCalled).toBe(0);
const stop = mobx.autorun(() => getters.getName());
expect(getNameCalled).toBe(1);
expect(getters.getName()).toEqual('Gandalf');
expect(getNameCalled).toBe(1);
for (let i = 0; i < 100; i++) {
expect(getters.getName()).toEqual('Gandalf');
}
expect(getNameCalled).toBe(1);
setters.setName('bob');
for (let i = 0; i < 100; i++) {
expect(getters.getName()).toEqual('bob');
}
expect(getNameCalled).toBe(2);
stop();
});
it('getters wrap argumentless functions in computed values', () => {
expect(getters.getName).toBeInstanceOf(Function);
expect(getters.getName()).toEqual('Gandalf');
expect(mobx.isComputed(getters.__computed.getName)).toBe(true);
});
it('getters with arguments are treated normally and not cached', () => {
expect(getFullNameCalled).toBe(0);
expect(getters.getFullName(' ')).toEqual('Gandalf The grey');
expect(getFullNameCalled).toBe(1);
expect(getters.getFullName('---', '=')).toEqual('Gandalf---The grey');
expect(getFullNameCalled).toBe(2);
});
it('if caching is desired, wrap the underlying call with argumentless getter', () => { // eslint-disable-line max-statements
expect(getFullNameCalled).toBe(0);
expect(getNameCalled).toBe(0);
const stop = mobx.autorun(() => getters.getFullName());
expect(getFullNameCalled).toBe(1);
expect(getNameCalled).toBe(1);
expect(getters.getFullName(' ')).toEqual('Gandalf The grey');
expect(getFullNameCalled).toBe(2);
expect(getNameCalled).toBe(1);
expect(getters.getFullName('---', '=')).toEqual('Gandalf---The grey');
expect(getFullNameCalled).toBe(3);
expect(getNameCalled).toBe(1);
setters.setName('bob');
expect(getFullNameCalled).toBe(4); // autorun calls this
expect(getNameCalled).toBe(2);
expect(getters.getFullName(' ')).toEqual('bob The grey');
expect(getFullNameCalled).toBe(5);
expect(getNameCalled).toBe(2);
expect(getters.getFullName(' ')).toEqual('bob The grey');
expect(getFullNameCalled).toBe(6);
expect(getNameCalled).toBe(2);
stop();
});
it('should keep objects and arrays untouched', () => {
const observable = remx.state({ arr: [], obj: {} });
expect(observable.arr).toEqual([]);
expect(observable.obj).toEqual({});
});
it('should keep arrays iterable', () => {
const observable = remx.state({ arr: [] });
expect(observable.arr[Symbol.iterator]).toEqual([][Symbol.iterator]);
});
it('trasitive changes in observable objects that are created dynamically are respected', () => {
expect(getters.getDynamicallyCreatedKey()).toEqual('not yet set');
setters.createKeyDynamically('Gandalf');
expect(getters.getDynamicallyCreatedKey()).toEqual('Gandalf');
setters.createKeyDynamically('Gandalf2');
expect(getters.getDynamicallyCreatedKey()).toEqual('Gandalf2');
});
it('merge function', () => {
expect(remx.merge).toBeInstanceOf(Function);
expect(state.dynamicallyCreatedKeys).toEqual({});
setters.usingMerge(`bla`);
expect(state.dynamicallyCreatedKeys).toEqual({ foo: 'bla' });
setters.usingMerge(undefined);
expect(state.dynamicallyCreatedKeys).toEqual({ foo: undefined });
});
it('state merge with boolean addition', () => {
expect(state.dynamicallyCreatedKeys).toEqual({});
setters.usingMergeWithValue(false);
expect(state.dynamicallyCreatedKeys).toEqual(false);
});
it('state merge with falsey', () => {
expect(state.dynamicallyCreatedKeys).toEqual({});
setters.usingMergeWithValue(undefined);
expect(state.dynamicallyCreatedKeys).toEqual(undefined);
});
it('state merge will not remove not-overriden values', () => {
expect(state.job.experience).toEqual('infinite');
expect(state.job.description).toEqual(undefined);
setters.setJobDescriptionUsingMerge('Wizard');
expect(state.job.experience).toEqual('infinite');
expect(state.job.description).toEqual('Wizard');
});
it('state merge works with non-objects', () => {
expect(state.race).toEqual('unknown');
setters.setRace('not human');
expect(state.race).toEqual('not human');
});
it('should allow setting a complex object', () => {
const complexName = {
value: 'bla',
someFunc() {
return true;
}
};
setters.setName(complexName);
expect(getters.getName().value).toBe('bla');
expect(getters.getName().someFunc()).toBe(true);
});
it('should apply changes to the state only when setter is finished', () => {
let callCount = 0;
mobx.autorun(() => {
const prop1 = getters.getMultiPropObject().prop1;
const prop2 = getters.getMultiPropObject().prop2;
const prop3 = getters.getMultiPropObject().prop3;
callCount++;
return prop1 + prop2 + prop3; // fake usage to satisfy linter
});
setters.setMultiPropObject();
expect(callCount).toBe(2);
});
describe('logger', () => {
it('should expose a register logger function', () => {
expect(remx.registerLoggerForDebug).toBeTruthy();
});
it('should warn you when registerLogger', () => {
global.console.warn = jest.fn();
remx.registerLoggerForDebug(jest.fn());
expect(console.warn).toBeCalledWith('Remx logger has been activated. make sure to disable it in production.');
});
it('should call to logger on every setter', () => {
const spy = jest.fn();
remx.registerLoggerForDebug(spy);
setters.setName('bla');
expect(spy.mock.calls[0][0]).toEqual({ action: 'setter', name: 'setName', args: ['bla'] });
});
it('should call to logger on every getter', () => {
const spy = jest.fn();
remx.registerLoggerForDebug(spy);
getters.getName('bla');
expect(spy.mock.calls[0][0]).toEqual({ action: 'getter', name: 'getName', args: ['bla'] });
});
});
it('should export fake toJS for migration purposes', () => {
const consoleBackup = console.warn;
console.warn = jest.fn();
expect(remx.toJS).toBeDefined();
const array = [1, 2, 3];
expect(remx.toJS(array)).toEqual(array);
expect(console.warn.mock.calls[0][0]).toContain('deprecate');
console.warn = consoleBackup;
});
});