Skip to content

Commit 1b9da8e

Browse files
committed
feat: add units tests for vm store
1 parent cf4c89b commit 1b9da8e

File tree

7 files changed

+238
-27
lines changed

7 files changed

+238
-27
lines changed

.eslintrc.cjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@ module.exports = {
1414
{
1515
files: [
1616
"*.test.ts",
17+
"*.test.tsx"
1718
],
19+
rules: {
20+
'sonarjs/no-identical-functions': 'off',
21+
'sonarjs/no-nested-functions': 'off',
22+
'unicorn/consistent-function-scoping': 'off',
23+
'unicorn/no-this-assignment': 'off',
24+
'@typescript-eslint/ban-ts-comment': 'off'
25+
},
1826
parserOptions: {
1927
project: 'tsconfig.test.json',
2028
},

src/hoc/with-view-model.test.tsx

Lines changed: 168 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { act, render, screen } from '@testing-library/react';
1+
import { act, fireEvent, render, screen } from '@testing-library/react';
22
import { observer } from 'mobx-react-lite';
3-
import { ReactNode } from 'react';
4-
import { describe, expect, test } from 'vitest';
3+
import { ReactNode, useState } from 'react';
4+
import { describe, expect, test, vi } from 'vitest';
55

6-
import { ViewModelsProvider } from '..';
6+
import { ViewModelStore, ViewModelsProvider } from '..';
77
import { createCounter } from '../utils';
88
import { TestViewModelStoreImpl } from '../view-model/abstract-view-model.store.test';
99
import { TestViewModelImpl } from '../view-model/view-model.impl.test';
@@ -112,30 +112,179 @@ describe('withViewModel', () => {
112112
expect(await screen.findAllByText('hello my-test')).toHaveLength(2);
113113
});
114114

115-
test('renders with view model store', async () => {
115+
test('View should be only mounted (renders only 1 time)', () => {
116116
class VM extends TestViewModelImpl {}
117-
const View = observer(({ model }: ViewModelProps<VM>) => {
117+
const View = vi.fn(({ model }: ViewModelProps<VM>) => {
118+
return <div>{`hello ${model.id}`}</div>;
119+
});
120+
const Component = withViewModel(VM, { generateId: createIdGenerator() })(
121+
View,
122+
);
123+
124+
render(<Component />);
125+
expect(View).toHaveBeenCalledTimes(1);
126+
});
127+
128+
test('withViewModel wrapper should by only mounted (renders only 1 time)', () => {
129+
class VM extends TestViewModelImpl {}
130+
const View = vi.fn(({ model }: ViewModelProps<VM>) => {
131+
return <div>{`hello ${model.id}`}</div>;
132+
});
133+
134+
const useHookSpy = vi.fn(() => {});
135+
136+
const Component = withViewModel(VM, {
137+
generateId: createIdGenerator(),
138+
reactHooks: useHookSpy, // the save renders count as withViewModel wrapper
139+
})(View);
140+
141+
render(<Component />);
142+
expect(useHookSpy).toHaveBeenCalledTimes(1);
143+
});
144+
145+
test('View should be updated when payload is changed', async () => {
146+
class VM extends TestViewModelImpl<{ counter: number }> {}
147+
const View = vi.fn(({ model }: ViewModelProps<VM>) => {
148+
return <div>{`hello ${model.id}`}</div>;
149+
});
150+
const Component = withViewModel(VM, { generateId: createIdGenerator() })(
151+
View,
152+
);
153+
154+
const SuperContainer = () => {
155+
const [counter, setCounter] = useState(0);
156+
118157
return (
119-
<div>
120-
<div>{`hello my friend. Model id is ${model.id}`}</div>
121-
</div>
158+
<>
159+
<button
160+
data-testid={'increment'}
161+
onClick={() => setCounter(counter + 1)}
162+
>
163+
increment
164+
</button>
165+
<Component payload={{ counter }} />
166+
</>
122167
);
168+
};
169+
170+
await act(() => render(<SuperContainer />));
171+
172+
const incrementButton = screen.getByTestId('increment');
173+
174+
fireEvent.click(incrementButton);
175+
fireEvent.click(incrementButton);
176+
fireEvent.click(incrementButton);
177+
178+
expect(View).toHaveBeenCalledTimes(4);
179+
});
180+
181+
test('View should have actual payload state', async () => {
182+
let vm: TestViewModelImpl<{ counter: number }> | null;
183+
184+
class VM extends TestViewModelImpl<{ counter: number }> {
185+
constructor(...args: any[]) {
186+
super(...args);
187+
vm = this;
188+
}
189+
}
190+
191+
const View = vi.fn(({ model }: ViewModelProps<VM>) => {
192+
return <div>{`hello ${model.id}`}</div>;
123193
});
124-
const Component = withViewModel(VM, { generateId: () => '1' })(View);
125-
const vmStore = new TestViewModelStoreImpl();
194+
const Component = withViewModel(VM, { generateId: createIdGenerator() })(
195+
View,
196+
);
197+
198+
const SuperContainer = () => {
199+
const [counter, setCounter] = useState(0);
126200

127-
const Wrapper = ({ children }: { children?: ReactNode }) => {
128201
return (
129-
<ViewModelsProvider value={vmStore}>{children}</ViewModelsProvider>
202+
<>
203+
<button
204+
data-testid={'increment'}
205+
onClick={() => setCounter(counter + 1)}
206+
>
207+
increment
208+
</button>
209+
<Component payload={{ counter }} />
210+
</>
130211
);
131212
};
132213

133-
await act(async () =>
134-
render(<Component />, {
135-
wrapper: Wrapper,
136-
}),
137-
);
214+
await act(() => render(<SuperContainer />));
215+
216+
const incrementButton = screen.getByTestId('increment');
217+
218+
fireEvent.click(incrementButton);
219+
fireEvent.click(incrementButton);
220+
fireEvent.click(incrementButton);
138221

139-
expect(screen.getByText('hello my friend. Model id is VM_1')).toBeDefined();
222+
// @ts-ignore
223+
expect(vm?.payload).toEqual({ counter: 3 });
224+
});
225+
226+
describe('with ViewModelStore', () => {
227+
test('renders', async () => {
228+
class VM extends TestViewModelImpl {}
229+
const View = observer(({ model }: ViewModelProps<VM>) => {
230+
return (
231+
<div>
232+
<div>{`hello my friend. Model id is ${model.id}`}</div>
233+
</div>
234+
);
235+
});
236+
const Component = withViewModel(VM, { generateId: () => '1' })(View);
237+
const vmStore = new TestViewModelStoreImpl();
238+
239+
const Wrapper = ({ children }: { children?: ReactNode }) => {
240+
return (
241+
<ViewModelsProvider value={vmStore}>{children}</ViewModelsProvider>
242+
);
243+
};
244+
245+
await act(async () =>
246+
render(<Component />, {
247+
wrapper: Wrapper,
248+
}),
249+
);
250+
251+
expect(
252+
screen.getByText('hello my friend. Model id is VM_1'),
253+
).toBeDefined();
254+
});
255+
256+
test('able to get access to view model store', async () => {
257+
let viewModels: ViewModelStore = null as any;
258+
259+
class VM extends TestViewModelImpl {
260+
constructor(params: any) {
261+
super(params);
262+
viewModels = params.viewModels;
263+
}
264+
}
265+
const View = observer(({ model }: ViewModelProps<VM>) => {
266+
return (
267+
<div>
268+
<div>{`hello my friend. Model id is ${model.id}`}</div>
269+
</div>
270+
);
271+
});
272+
const Component = withViewModel(VM, { generateId: () => '1' })(View);
273+
const vmStore = new TestViewModelStoreImpl();
274+
275+
const Wrapper = ({ children }: { children?: ReactNode }) => {
276+
return (
277+
<ViewModelsProvider value={vmStore}>{children}</ViewModelsProvider>
278+
);
279+
};
280+
281+
await act(async () =>
282+
render(<Component />, {
283+
wrapper: Wrapper,
284+
}),
285+
);
286+
287+
expect(viewModels).toBeDefined();
288+
});
140289
});
141290
});

src/hoc/with-view-model.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export function withViewModel(
120120
parentViewModelId,
121121
payload,
122122
VM: Model,
123+
viewModels,
123124
parentViewModel:
124125
(parentViewModelId && instances.get(parentViewModelId)) || null,
125126
fallback: config?.fallback,

src/view-model/abstract-view-model.store.test.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AbstractViewModelStore } from './abstract-view-model.store';
66
import { TestAbstractViewModelImpl } from './abstract-view-model.test';
77
import { AbstractViewModelParams } from './abstract-view-model.types';
88
import { ViewModel } from './view-model';
9+
import { TestViewModelImpl } from './view-model.impl.test';
910
import { ViewModelStore } from './view-model.store';
1011
import {
1112
ViewModelCreateConfig,
@@ -20,14 +21,7 @@ export class TestViewModelStoreImpl extends AbstractViewModelStore {
2021

2122
createViewModel<VM extends ViewModel>(config: ViewModelCreateConfig<VM>): VM {
2223
const VM = config.VM;
23-
24-
const params: AbstractViewModelParams<VM['payload']> = {
25-
id: config.id,
26-
payload: config.payload,
27-
parentViewModelId: config.parentViewModelId,
28-
};
29-
30-
return new VM(params);
24+
return new VM(config);
3125
}
3226

3327
generateViewModelId<VM extends ViewModel>(
@@ -99,4 +93,47 @@ describe('AbstractViewModelStore', () => {
9993

10094
expect(childVM.parentViewModel.id).toBe('parent');
10195
});
96+
97+
it('able to get access to view model by id', async () => {
98+
const vmStore = new TestViewModelStoreImpl();
99+
100+
const vm = new TestViewModelImpl();
101+
102+
await vmStore.attach(vm);
103+
104+
expect(vmStore.get(vm.id)).toBe(vm);
105+
});
106+
107+
it('able to get access to view model by Class', async () => {
108+
const vmStore = new TestViewModelStoreImpl();
109+
110+
class MyVM extends TestViewModelImpl {}
111+
const vm = new MyVM();
112+
113+
await vmStore.attach(vm);
114+
115+
expect(vmStore.get(MyVM)).toBe(vm);
116+
});
117+
118+
it('able to get instance id by id (getId method)', async () => {
119+
const vmStore = new TestViewModelStoreImpl();
120+
121+
class MyVM extends TestViewModelImpl {}
122+
const vm = new MyVM();
123+
124+
await vmStore.attach(vm);
125+
126+
expect(vmStore.getId(vm.id)).toBe(vm.id);
127+
});
128+
129+
it('able to get instance id by Class (getId method)', async () => {
130+
const vmStore = new TestViewModelStoreImpl();
131+
132+
class MyVM extends TestViewModelImpl {}
133+
const vm = new MyVM();
134+
135+
await vmStore.attach(vm);
136+
137+
expect(vmStore.getId(MyVM)).toBe(vm.id);
138+
});
102139
});

src/view-model/abstract-view-model.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import { AnyObject, EmptyObject, Maybe } from '../utils/types';
77

88
import { AbstractViewModelParams } from './abstract-view-model.types';
99
import { ViewModel } from './view-model';
10+
import { ViewModelStore } from './view-model.store';
1011
import { AnyViewModel } from './view-model.types';
1112

13+
declare const process: { env: { NODE_ENV?: string } };
14+
1215
export abstract class AbstractViewModel<
1316
Payload extends AnyObject = EmptyObject,
1417
ParentViewModel extends AnyViewModel | null = null,
@@ -52,6 +55,16 @@ export abstract class AbstractViewModel<
5255
});
5356
}
5457

58+
protected get viewModels(): ViewModelStore {
59+
if (process.env.NODE_ENV !== 'production' && !this.params.viewModels) {
60+
console.warn(
61+
'accessing to viewModels is not possible. [viewModels] param is not setted during to creating instance AbstractViewModel',
62+
);
63+
}
64+
65+
return this.params.viewModels!;
66+
}
67+
5568
/**
5669
* The method is called when the view starts mounting
5770
*/

src/view-model/abstract-view-model.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AnyObject, EmptyObject, Maybe } from '../utils/types';
22

3+
import { ViewModelStore } from './view-model.store';
34
import { AnyViewModel } from './view-model.types';
45

56
export interface AbstractViewModelParams<
@@ -8,6 +9,7 @@ export interface AbstractViewModelParams<
89
> {
910
id: string;
1011
payload: Payload;
12+
viewModels?: Maybe<ViewModelStore>;
1113
parentViewModelId?: Maybe<string>;
1214
parentViewModel?: Maybe<ParentViewModel>;
1315
ctx?: AnyObject;

tsconfig.test.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
},
2626
"include": [
2727
"src/**/*.test.ts",
28+
"src/**/*.test.tsx",
2829
"node_modules"
2930
],
3031
"exclude": []

0 commit comments

Comments
 (0)