reactant-module
Version:
A module model for Reactant
724 lines (667 loc) • 16.7 kB
text/typescript
import { apply as applyPatches } from 'mutative';
import { Middleware } from 'redux';
import {
injectable,
createContainer,
state,
createStore,
action,
getStagedState,
enablePatchesKey,
applyMiddleware,
} from '../..';
describe('@action', () => {
test('base', () => {
({
name: 'counter',
})
class Counter {
count = 0;
increase() {
this.count += 1;
}
noChange() {
const { count } = this;
this.count = count;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
unexpectedChange(): any {
this.count = 0;
return this.returnValue;
}
returnValue: any = 0;
}
const ServiceIdentifiers = new Map();
const modules = [Counter];
const container = createContainer({
ServiceIdentifiers,
modules,
options: {
defaultScope: 'Singleton',
},
});
const counter = container.get(Counter);
const store = createStore({
modules,
container,
ServiceIdentifiers,
loadedModules: new Set(),
load: (...args: any[]) => {
//
},
dynamicModules: new Map(),
pluginHooks: {
middleware: [],
beforeCombineRootReducers: [],
afterCombineRootReducers: [],
enhancer: [],
preloadedStateHandler: [],
afterCreateStore: [],
provider: [],
},
});
counter.increase();
expect(counter.count).toBe(1);
expect((counter as any)[enablePatchesKey]).toBe(false);
expect(Object.values(store.getState())).toEqual([{ count: 1 }]);
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {
//
});
counter.noChange();
expect(warn).not.toBeCalledWith(
`There are no state updates to method 'counter.noChange'`
);
warn.mockReset();
for (const value of [
0,
1,
null,
true,
false,
new Promise(() => {
//
}),
function a() {
//
},
{},
[],
Symbol(''),
]) {
expect(() => {
counter.returnValue = value;
counter.unexpectedChange();
}).toThrowError(
/The return value of the method 'unexpectedChange' is not allowed./
);
}
});
test('enable `autoFreeze` in devOptions', () => {
({
name: 'counter',
})
class Counter {
count = 0;
sum = { count: 0 };
increase() {
this.sum.count += 1;
}
increase1() {
this.sum.count += 1;
}
increase2() {
this.count += 1;
}
}
const ServiceIdentifiers = new Map();
const modules = [Counter];
const container = createContainer({
ServiceIdentifiers,
modules,
options: {
defaultScope: 'Singleton',
},
});
const counter = container.get(Counter);
const store = createStore({
modules,
container,
ServiceIdentifiers,
loadedModules: new Set(),
load: (...args: any[]) => {
//
},
dynamicModules: new Map(),
pluginHooks: {
middleware: [],
beforeCombineRootReducers: [],
afterCombineRootReducers: [],
enhancer: [],
preloadedStateHandler: [],
afterCreateStore: [],
provider: [],
},
devOptions: { autoFreeze: true },
});
expect(() => {
store.getState().counter.sum.count = 1;
}).toThrowError(/Cannot assign to read only property/);
counter.increase();
for (const fn of [
() => {
store.getState().counter.sum.count = 1;
},
() => counter.increase1(),
() => counter.increase2(),
]) {
expect(fn).toThrowError(/Cannot assign to read only property/);
}
});
test('inherited module with stagedState about more effects', () => {
({
name: 'foo0',
})
class Foo0 {
count0 = 1;
count1 = 1;
increase() {
this.count0 += 1;
}
decrease() {
this.count0 -= 1;
}
decrease1() {
this.count0 -= 1;
}
}
({
name: 'foo',
})
class Foo extends Foo0 {
count = 1;
add(count: number) {
this.count += count;
}
increase() {
// inheritance
super.increase();
// change state
this.count0 += 1;
// call other action function
this.increase1();
// call unwrapped `@action` function
this.add(this.count);
}
increase1() {
this.count1 += 1;
}
decrease() {
super.decrease();
this.count0 -= 1;
}
decrease1() {
super.decrease1();
}
}
()
class FooBar {
constructor(public foo: Foo, public foo0: Foo0) {}
}
const ServiceIdentifiers = new Map();
const modules = [FooBar];
const container = createContainer({
ServiceIdentifiers,
modules,
options: {
defaultScope: 'Singleton',
},
});
const fooBar = container.get(FooBar);
const store = createStore({
modules,
container,
ServiceIdentifiers,
loadedModules: new Set(),
load: (...args: any[]) => {
//
},
dynamicModules: new Map(),
pluginHooks: {
middleware: [],
beforeCombineRootReducers: [],
afterCombineRootReducers: [],
enhancer: [],
preloadedStateHandler: [],
afterCreateStore: [],
provider: [],
},
});
const subscribe = jest.fn();
store.subscribe(subscribe);
fooBar.foo.increase();
expect(fooBar.foo.count0).toBe(3);
expect(fooBar.foo.count1).toBe(2);
expect(fooBar.foo.count).toBe(2);
fooBar.foo0.increase();
expect(fooBar.foo.count0).toBe(3);
expect(fooBar.foo.count1).toBe(2);
expect(fooBar.foo.count).toBe(2);
// merge the multi-actions changed states as one redux dispatch.
expect(subscribe.mock.calls.length).toBe(2);
// inheritance
fooBar.foo.decrease();
expect(fooBar.foo.count0).toBe(1);
fooBar.foo.decrease1();
expect(fooBar.foo.count0).toBe(0);
});
test('across module changing state', () => {
()
class Foo {
textList: string[] = [];
addText(text: string) {
this.textList.push(text);
}
}
()
class Counter {
constructor(public foo: Foo) {}
count = 0;
increase() {
this.foo.addText(`test${this.count}`);
this.count += 1;
this.foo.addText(`test${this.count}`);
this.count += 1;
this.foo.addText(`test${this.count}`);
}
}
const ServiceIdentifiers = new Map();
const modules = [Counter];
const container = createContainer({
ServiceIdentifiers,
modules,
options: {
defaultScope: 'Singleton',
},
});
const counter = container.get(Counter);
const store = createStore({
modules,
container,
ServiceIdentifiers,
loadedModules: new Set(),
load: (...args: any[]) => {
//
},
dynamicModules: new Map(),
pluginHooks: {
middleware: [],
beforeCombineRootReducers: [],
afterCombineRootReducers: [],
enhancer: [],
preloadedStateHandler: [],
afterCreateStore: [],
provider: [],
},
});
const subscribeFn = jest.fn();
store.subscribe(subscribeFn);
counter.increase();
expect(subscribeFn.mock.calls.length).toBe(1);
expect(counter.count).toEqual(2);
expect(counter.foo.textList).toEqual(['test0', 'test1', 'test2']);
});
test('across module changing state with error', () => {
({
name: 'foo',
})
class Foo {
list: number[] = [];
addItem(num: number) {
if (num === 1) {
// eslint-disable-next-line no-throw-literal
throw 'something error';
} else {
this.list.push(num);
}
}
}
({
name: 'counter',
})
class Counter {
constructor(public foo: Foo) {}
count = 0;
increase() {
this.foo.addItem(this.count);
this.count += 1;
}
increase1() {
this.foo.addItem(this.count + 1);
this.count += 1;
}
}
const ServiceIdentifiers = new Map();
const modules = [Counter];
const container = createContainer({
ServiceIdentifiers,
modules,
options: {
defaultScope: 'Singleton',
},
});
const counter = container.get(Counter);
const store = createStore({
modules,
container,
ServiceIdentifiers,
loadedModules: new Set(),
load: (...args: any[]) => {
//
},
dynamicModules: new Map(),
pluginHooks: {
middleware: [],
beforeCombineRootReducers: [],
afterCombineRootReducers: [],
enhancer: [],
preloadedStateHandler: [],
afterCreateStore: [],
provider: [],
},
});
const subscribeFn = jest.fn();
store.subscribe(subscribeFn);
counter.increase();
expect(subscribeFn.mock.calls.length).toBe(1);
expect(() => {
counter.increase();
}).toThrowError('something error');
expect(getStagedState()).toBeUndefined();
expect(subscribeFn.mock.calls.length).toBe(1);
counter.increase1();
expect(subscribeFn.mock.calls.length).toBe(2);
expect(counter.count).toBe(2);
expect(counter.foo.list).toEqual([0, 2]);
expect(store.getState()).toEqual({
counter: { count: 2 },
foo: { list: [0, 2] },
});
});
test('base with `enablePatches`', () => {
interface Todo {
text: string;
}
({
name: 'todo',
})
class TodoList {
list: Todo[] = [
{
text: 'foo',
},
];
add(text: string) {
this.list.slice(-1)[0].text = text;
this.list.push({ text });
}
}
const actionFn = jest.fn();
const middleware: Middleware = (store) => (next) => (_action) => {
actionFn(_action);
return next(_action);
};
const ServiceIdentifiers = new Map();
const modules = [TodoList, applyMiddleware(middleware)];
const container = createContainer({
ServiceIdentifiers,
modules,
options: {
defaultScope: 'Singleton',
},
});
const todoList = container.get(TodoList);
const store = createStore({
modules,
container,
ServiceIdentifiers,
loadedModules: new Set(),
load: (...args: any[]) => {
//
},
dynamicModules: new Map(),
pluginHooks: {
middleware: [],
beforeCombineRootReducers: [],
afterCombineRootReducers: [],
enhancer: [],
preloadedStateHandler: [],
afterCreateStore: [],
provider: [],
},
devOptions: {
enablePatches: true,
},
});
const originalTodoState = store.getState();
expect(Object.values(store.getState())).toEqual([
{ list: [{ text: 'foo' }] },
]);
expect(actionFn.mock.calls.length).toBe(0);
todoList.add('test');
expect(Object.values(store.getState())).toEqual([
{ list: [{ text: 'test' }, { text: 'test' }] },
]);
expect(actionFn.mock.calls.length).toBe(1);
expect(actionFn.mock.calls[0][0]._patches).toEqual([
{
op: 'replace',
path: ['todo', 'list', 0, 'text'],
value: 'test',
},
{
op: 'add',
path: ['todo', 'list', 1],
value: {
text: 'test',
},
},
]);
expect(actionFn.mock.calls[0][0]._inversePatches).toEqual([
{
op: 'replace',
path: ['todo', 'list', 0, 'text'],
value: 'foo',
},
{
op: 'replace',
path: ['todo', 'list', 'length'],
value: 1,
},
]);
expect(
applyPatches(originalTodoState, actionFn.mock.calls[0][0]._patches)
).toEqual(store.getState());
expect(
applyPatches(originalTodoState, actionFn.mock.calls[0][0]._patches) ===
store.getState()
).toBe(false);
expect(
applyPatches(store.getState(), actionFn.mock.calls[0][0]._inversePatches)
).toEqual(originalTodoState);
});
test('base with `enableInspector`', () => {
interface Todo {
text: string;
}
({
name: 'todo',
})
class TodoList {
list: Todo[] = [
{
text: 'foo',
},
];
add(text: string) {
this.list.slice(-1)[0].text = text;
this.list.push({ text });
}
noChange() {
this.list.slice(-1)[0].text = this.list.slice(-1)[0].text;
}
}
const actionFn = jest.fn();
const middleware: Middleware = (store) => (next) => (_action) => {
actionFn(_action);
return next(_action);
};
const ServiceIdentifiers = new Map();
const modules = [TodoList, applyMiddleware(middleware)];
const container = createContainer({
ServiceIdentifiers,
modules,
options: {
defaultScope: 'Singleton',
},
});
const todoList = container.get(TodoList);
const store = createStore({
modules,
container,
ServiceIdentifiers,
loadedModules: new Set(),
load: (...args: any[]) => {
//
},
dynamicModules: new Map(),
pluginHooks: {
middleware: [],
beforeCombineRootReducers: [],
afterCombineRootReducers: [],
enhancer: [],
preloadedStateHandler: [],
afterCreateStore: [],
provider: [],
},
devOptions: {
enableInspector: true,
},
});
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {
//
});
todoList.noChange();
expect(warn).toBeCalledWith(
`There are no state updates to method 'todo.noChange'`
);
warn.mockReset();
});
test('base with `checkAction`', () => {
interface Todo {
text: string;
}
({
name: 'todo',
})
class TodoList {
list: Todo[] = [
{
text: 'foo',
},
];
add(text: string) {
this.list.slice(-1)[0].text = text;
this.list.push({ text });
}
noChange() {
this.list.slice(-1)[0].text = this.list.slice(-1)[0].text;
}
}
const actionFn = jest.fn();
const middleware: Middleware = (store) => (next) => (_action) => {
actionFn(_action);
return next(_action);
};
const ServiceIdentifiers = new Map();
const modules = [TodoList, applyMiddleware(middleware)];
const container = createContainer({
ServiceIdentifiers,
modules,
options: {
defaultScope: 'Singleton',
},
});
const todoList = container.get(TodoList);
const fn = jest.fn();
const store = createStore({
modules,
container,
ServiceIdentifiers,
loadedModules: new Set(),
load: (...args: any[]) => {
//
},
dynamicModules: new Map(),
pluginHooks: {
middleware: [],
beforeCombineRootReducers: [],
afterCombineRootReducers: [],
enhancer: [],
preloadedStateHandler: [],
afterCreateStore: [],
provider: [],
},
devOptions: {
checkAction: fn,
},
});
todoList.noChange();
expect(fn).toBeCalledTimes(1);
const options = fn.mock.calls[0][0];
expect(options.target).toEqual(todoList);
expect(options.ref.store).toEqual(store);
expect(options.ref.container).toEqual(container);
expect(options.ref.identifier).toEqual('todo');
expect(options.ref.state).toEqual({ list: [{ text: 'foo' }] });
expect(options.ref.initState).toEqual({ list: [{ text: 'foo' }] });
expect(options.ref.strict).toEqual(false);
expect(options.ref.enablePatches).toEqual(false);
expect(options.ref.enableInspector).toEqual(false);
expect(options.ref.checkAction).toEqual(fn);
expect(options.method).toEqual('noChange');
expect(options.args).toEqual([]);
});
});