@servicetitan/react-ioc 23.2.2 → 23.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/provide.d.ts CHANGED
@@ -4,12 +4,14 @@ export interface WrappedComponent<P> {
4
4
  wrappedComponent: ComponentType<P>;
5
5
  wrappedInstance: ReactInstance | undefined;
6
6
  }
7
+ interface FallbackOptions {
8
+ loadingFallback?: JSX.Element;
9
+ errorFallback?: JSX.Element;
10
+ }
7
11
  export declare type ProvideDecorator = (stores: {
8
12
  singletons?: ProviderConfigEntrySingletons<any>[];
9
13
  instances?: ProviderConfigEntryInstances<any>[];
10
- }, options?: {
11
- loadingFallback?: JSX.Element;
12
- errorFallback?: JSX.Element;
13
- }) => <T extends ComponentType>(target: T) => T & WrappedComponent<T>;
14
+ }, options?: JSX.Element | FallbackOptions) => <T extends ComponentType<any>>(target: T) => T & WrappedComponent<T>;
14
15
  export declare const provide: ProvideDecorator;
16
+ export {};
15
17
  //# sourceMappingURL=provide.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"provide.d.ts","sourceRoot":"","sources":["../src/provide.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAErD,OAAO,EAAY,6BAA6B,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAEnG,MAAM,WAAW,gBAAgB,CAAC,CAAC;IAC/B,gBAAgB,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IACnC,eAAe,EAAE,aAAa,GAAG,SAAS,CAAC;CAC9C;AAED,oBAAY,gBAAgB,GAAG,CAC3B,MAAM,EAAE;IACJ,UAAU,CAAC,EAAE,6BAA6B,CAAC,GAAG,CAAC,EAAE,CAAC;IAClD,SAAS,CAAC,EAAE,4BAA4B,CAAC,GAAG,CAAC,EAAE,CAAC;CACnD,EACD,OAAO,CAAC,EAAE;IACN,eAAe,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IAC9B,aAAa,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;CAC/B,KACA,CAAC,CAAC,SAAS,aAAa,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC;AAErE,eAAO,MAAM,OAAO,kBASM,CAAC"}
1
+ {"version":3,"file":"provide.d.ts","sourceRoot":"","sources":["../src/provide.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAErD,OAAO,EAAY,6BAA6B,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAEnG,MAAM,WAAW,gBAAgB,CAAC,CAAC;IAC/B,gBAAgB,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IACnC,eAAe,EAAE,aAAa,GAAG,SAAS,CAAC;CAC9C;AAED,UAAU,eAAe;IACrB,eAAe,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IAC9B,aAAa,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;CAC/B;AAED,oBAAY,gBAAgB,GAAG,CAC3B,MAAM,EAAE;IACJ,UAAU,CAAC,EAAE,6BAA6B,CAAC,GAAG,CAAC,EAAE,CAAC;IAClD,SAAS,CAAC,EAAE,4BAA4B,CAAC,GAAG,CAAC,EAAE,CAAC;CACnD,EACD,OAAO,CAAC,EAAE,GAAG,CAAC,OAAO,GAAG,eAAe,KACtC,CAAC,CAAC,SAAS,aAAa,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC;AAE1E,eAAO,MAAM,OAAO,kBAKM,CAAC"}
package/dist/provide.js CHANGED
@@ -1,4 +1,15 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Provider } from './provider';
3
- export const provide = ((stores, options) => (Component) => props => (_jsx(Provider, Object.assign({}, stores, { loadingFallback: options === null || options === void 0 ? void 0 : options.loadingFallback, errorFallback: options === null || options === void 0 ? void 0 : options.errorFallback }, { children: _jsx(Component, Object.assign({}, props)) }))));
3
+ export const provide = ((stores, elementOrOptions) => (Component) => props => (_jsx(Provider, Object.assign({}, stores, getOptions(elementOrOptions), { children: _jsx(Component, Object.assign({}, props)) }))));
4
+ function getOptions(arg) {
5
+ return arg && isOptionsObject(arg) ? arg : { loadingFallback: arg };
6
+ }
7
+ function isOptionsObject(arg) {
8
+ return (Object.hasOwn(arg, 'loadingFallback') ||
9
+ Object.hasOwn(arg, 'errorFallback') ||
10
+ isEmptyObject(arg));
11
+ }
12
+ function isEmptyObject(arg) {
13
+ return typeof arg === 'object' && Object.keys(arg).length === 0;
14
+ }
4
15
  //# sourceMappingURL=provide.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"provide.js","sourceRoot":"","sources":["../src/provide.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,QAAQ,EAA+D,MAAM,YAAY,CAAC;AAkBnG,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,SAAwB,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAC/E,CACI,KAAC,QAAQ,oBACD,MAAM,IACV,eAAe,EAAE,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,eAAe,EACzC,aAAa,EAAE,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,aAAa,gBAErC,KAAC,SAAS,oBAAK,KAAK,EAAI,IACjB,CACd,CAAqB,CAAC"}
1
+ {"version":3,"file":"provide.js","sourceRoot":"","sources":["../src/provide.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,QAAQ,EAA+D,MAAM,YAAY,CAAC;AAoBnG,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC,MAAM,EAAE,gBAAgB,EAAE,EAAE,CAAC,CAAC,SAAwB,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CACxF,CACI,KAAC,QAAQ,oBAAK,MAAM,EAAM,UAAU,CAAC,gBAAgB,CAAC,cAClD,KAAC,SAAS,oBAAK,KAAK,EAAI,IACjB,CACd,CAAqB,CAAC;AAE3B,SAAS,UAAU,CAAC,GAAmC;IACnD,OAAO,GAAG,IAAI,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,GAAG,EAAE,CAAC;AACxE,CAAC;AAED,SAAS,eAAe,CAAC,GAAkC;IACvD,OAAO,CACH,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,iBAAiB,CAAC;QACrC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,eAAe,CAAC;QACnC,aAAa,CAAC,GAAG,CAAC,CACrB,CAAC;AACN,CAAC;AAED,SAAS,aAAa,CAAC,GAAkC;IACrD,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;AACpE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/react-ioc",
3
- "version": "23.2.2",
3
+ "version": "23.3.0",
4
4
  "description": "",
5
5
  "homepage": "https://docs.st.dev/docs/frontend/react-ioc",
6
6
  "repository": {
@@ -20,6 +20,7 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "@servicetitan/startup-jest": "^1.1.0",
23
+ "@testing-library/jest-dom": "^6.4.2",
23
24
  "@testing-library/react": "^14.2.1",
24
25
  "@types/react": "~18.2.55",
25
26
  "@types/react-test-renderer": "~18.0.7",
@@ -35,5 +36,5 @@
35
36
  "cli": {
36
37
  "webpack": false
37
38
  },
38
- "gitHead": "f63b207ce977be71bdd36cd8571a0d5eb82a2348"
39
+ "gitHead": "4a3420cf55cf34ff10b5ba6164d940b39f0925e6"
39
40
  }
@@ -1,343 +1,97 @@
1
- import { PropsWithChildren, ReactElement, ReactNode, useEffect } from 'react';
2
- import { provide, inject, injectable, useDependencies, Store, symbolToken, Provider } from '..';
3
- import TestRenderer from 'react-test-renderer';
4
- import { waitFor } from '@testing-library/react';
5
-
6
- @injectable()
7
- class StatStore {
8
- readonly id = 'stat';
9
- counters: Record<string, number> = {};
10
- queueCreate: string[] = [];
11
- queueRender: string[] = [];
12
-
13
- testInitialize: (id: string) => void = jest.fn();
14
- testRender = (id: string) => {
15
- this.queueRender.push(id);
1
+ import '@testing-library/jest-dom';
2
+ import { FC, Fragment, PropsWithChildren } from 'react';
3
+
4
+ import { provide } from '../provide';
5
+ import { render, screen } from '@testing-library/react';
6
+
7
+ jest.mock('../provider', () => ({
8
+ Provider: ({ children, ...props }: PropsWithChildren<any>) => {
9
+ return (
10
+ <Fragment>
11
+ {`<Provider ${JSON.stringify(props)} />`}
12
+ {children}
13
+ </Fragment>
14
+ );
15
+ },
16
+ }));
17
+
18
+ describe('[react-ioc] provide', () => {
19
+ let stores: Parameters<typeof provide>[0];
20
+ let options: Parameters<typeof provide>[1];
21
+ let props: Record<string, any> | undefined;
22
+
23
+ const subject = () => {
24
+ const Component = provide(
25
+ stores,
26
+ options
27
+ )(renderProps => <div>{`<Component ${JSON.stringify(renderProps)} />`}</div>);
28
+
29
+ return render(<Component {...props} />);
16
30
  };
17
31
 
18
- register(name: string) {
19
- if (!this.counters[name]) {
20
- this.counters[name] = 0;
21
- }
22
-
23
- this.counters[name]++;
24
-
25
- const id = `${name}-${this.counters[name]}`;
26
- this.queueCreate.push(id);
27
-
28
- return id;
29
- }
30
- }
31
-
32
- @injectable()
33
- class StoreSimple {
34
- readonly id;
35
-
36
- constructor(@inject(StatStore) stat: StatStore) {
37
- this.id = stat.register('StoreSimple');
38
- }
39
- }
40
-
41
- @injectable()
42
- class StoreWithInitialize extends Store {
43
- readonly id;
44
-
45
- constructor(@inject(StatStore) private stat: StatStore) {
46
- super();
47
- this.id = stat.register('StoreWithInitialize');
48
- }
49
-
50
- initialize() {
51
- this.stat.testInitialize(this.id);
52
- }
53
- }
54
-
55
- type TestStore = typeof StoreSimple | typeof StoreWithInitialize | ReturnType<typeof symbolToken>;
56
-
57
- const getTestComponent = (inst: TestStore) => {
58
- return ({ children }: PropsWithChildren<{}>) => {
59
- const [{ id }, { testRender }] = useDependencies<[any, StatStore]>(inst, StatStore);
60
- useEffect(() => {
61
- testRender(id);
62
- }, [testRender, id]);
63
- return <div>{children}</div>;
64
- };
65
- };
32
+ beforeEach(() => {
33
+ stores = { singletons: [], instances: [] };
34
+ options = undefined;
35
+ props = { foo: 'bar' };
36
+ });
66
37
 
67
- type ProviderStoreProps = Parameters<typeof provide>[0];
38
+ test('renders component with props', () => {
39
+ subject();
68
40
 
69
- function TestProvider(props: PropsWithChildren<ProviderStoreProps & { store: TestStore }>) {
70
- const { singletons, instances, children, store = StoreSimple } = props;
71
- const Component = provide({ singletons, instances })(getTestComponent(store));
72
- return <Component>{children}</Component>;
73
- }
41
+ expect(screen.getByText(`<Component ${JSON.stringify(props)} />`)).toBeInTheDocument();
42
+ });
74
43
 
75
- describe(`[react-ioc] ${provide.name}`, () => {
76
- let stat!: StatStore;
44
+ test('wraps component within <Provider>', () => {
45
+ subject();
77
46
 
78
- const Stat = provide({ singletons: [StatStore] })(({ children }: PropsWithChildren<{}>) => {
79
- const [statStore] = useDependencies(StatStore);
80
- stat = statStore;
81
- return <div>{children}</div>;
47
+ expect(screen.getByText(`<Provider ${JSON.stringify(stores)} />`)).toBeInTheDocument();
82
48
  });
83
49
 
84
- const subject = (children: ReactNode) => TestRenderer.create(<Stat>{children}</Stat>);
50
+ describe('with fallback options', () => {
51
+ const LoadingFallback: FC = () => null;
52
+ const ErrorFallback: FC = () => null;
85
53
 
86
- const expectQueueRenderToBe = (queueRender: string[]) =>
87
- TestRenderer.act(async () => {
88
- await waitFor(() => {
89
- expect(stat.queueRender).toEqual(queueRender);
90
- });
54
+ beforeEach(() => {
55
+ options = { loadingFallback: <LoadingFallback />, errorFallback: <ErrorFallback /> };
91
56
  });
92
57
 
93
- describe('check nested stores', () => {
94
- describe('singletons', () => {
95
- test('should reuse singletons for each container.get', async () => {
96
- subject(
97
- <TestProvider store={StoreSimple} singletons={[StoreSimple]}>
98
- <TestProvider store={StoreSimple} />
99
- </TestProvider>
100
- );
101
-
102
- expect(stat.queueCreate).toEqual(['StoreSimple-1']);
58
+ test('passes fallback options to <Provider>', () => {
59
+ subject();
103
60
 
104
- await expectQueueRenderToBe(['StoreSimple-1', 'StoreSimple-1']);
105
- });
106
-
107
- test('should create separate singletons for nested tokens', async () => {
108
- subject(
109
- <TestProvider store={StoreSimple} singletons={[StoreSimple]}>
110
- <TestProvider store={StoreSimple} singletons={[StoreSimple]} />
111
- </TestProvider>
112
- );
113
-
114
- expect(stat.queueCreate).toEqual(['StoreSimple-1', 'StoreSimple-2']);
115
-
116
- await expectQueueRenderToBe(['StoreSimple-1', 'StoreSimple-2']);
117
- });
118
-
119
- test('should reuse singleton when using provide inheritance and instance exists', async () => {
120
- subject(
121
- <TestProvider store={StoreSimple} singletons={[StoreSimple]}>
122
- <TestProvider
123
- store={StoreSimple}
124
- singletons={[{ provide: StoreSimple, inherit: true }]}
125
- />
126
- </TestProvider>
127
- );
128
-
129
- expect(stat.queueCreate).toEqual(['StoreSimple-1']);
130
-
131
- await expectQueueRenderToBe(['StoreSimple-1', 'StoreSimple-1']);
132
- });
133
-
134
- test('should create singletons when using provide inheritance and instance not exists', () => {
135
- subject(
136
- <TestProvider
137
- store={StoreSimple}
138
- singletons={[{ provide: StoreSimple, inherit: true }]}
139
- >
140
- <TestProvider store={StoreSimple} singletons={[StoreSimple]} />
141
- </TestProvider>
142
- );
143
-
144
- expect(stat.queueCreate).toEqual(['StoreSimple-1', 'StoreSimple-2']);
145
- });
61
+ expect(
62
+ screen.getByText(`<Provider ${JSON.stringify({ ...stores, ...options })} />`)
63
+ ).toBeInTheDocument();
146
64
  });
147
65
 
148
- describe('instances', () => {
149
- test('should create separate instances for each container.get', async () => {
150
- subject(
151
- <TestProvider store={StoreSimple} instances={[StoreSimple]}>
152
- <TestProvider store={StoreSimple} />
153
- </TestProvider>
154
- );
155
-
156
- expect(stat.queueCreate).toEqual(['StoreSimple-1', 'StoreSimple-2']);
66
+ describe('with empty fallback options', () => {
67
+ beforeEach(() => (options = {}));
157
68
 
158
- await expectQueueRenderToBe(['StoreSimple-1', 'StoreSimple-2']);
159
- });
160
-
161
- test('should create separate instances for nested tokens', async () => {
162
- subject(
163
- <TestProvider store={StoreSimple} instances={[StoreSimple]}>
164
- <TestProvider store={StoreSimple} instances={[StoreSimple]} />
165
- </TestProvider>
166
- );
69
+ test('does not pass options to <Provider>', () => {
70
+ subject();
167
71
 
168
- expect(stat.queueCreate).toEqual(['StoreSimple-1', 'StoreSimple-2']);
169
-
170
- await expectQueueRenderToBe(['StoreSimple-1', 'StoreSimple-2']);
171
- });
172
- });
173
-
174
- describe('useValue', () => {
175
- const valueToken = symbolToken('valueToken');
176
-
177
- test('create new values', async () => {
178
- subject(
179
- <TestProvider
180
- store={valueToken}
181
- singletons={[{ provide: valueToken, useValue: { id: 'value1' } }]}
182
- >
183
- <TestProvider
184
- store={valueToken}
185
- singletons={[{ provide: valueToken, useValue: { id: 'value2' } }]}
186
- />
187
- </TestProvider>
188
- );
189
-
190
- await expectQueueRenderToBe(['value1', 'value2']);
191
- });
192
-
193
- test('reuse nested values', async () => {
194
- subject(
195
- <TestProvider
196
- store={valueToken}
197
- singletons={[{ provide: valueToken, useValue: { id: 'value1' } }]}
198
- >
199
- <TestProvider
200
- store={valueToken}
201
- singletons={[
202
- { provide: valueToken, useValue: { id: 'value2' }, inherit: true },
203
- ]}
204
- />
205
- </TestProvider>
206
- );
207
-
208
- await expectQueueRenderToBe(['value1', 'value1']);
209
- });
210
-
211
- test('no reuse nested values for instances', async () => {
212
- subject(
213
- <TestProvider
214
- store={valueToken}
215
- singletons={[{ provide: valueToken, useValue: { id: 'value1' } }]}
216
- >
217
- <TestProvider
218
- store={valueToken}
219
- instances={[
220
- {
221
- provide: valueToken,
222
- useValue: { id: 'value2' },
223
- inherit: true,
224
- } as any,
225
- ]}
226
- />
227
- </TestProvider>
228
- );
229
-
230
- await expectQueueRenderToBe(['value1', 'value2']);
72
+ expect(
73
+ screen.getByText(`<Provider ${JSON.stringify({ ...stores })} />`)
74
+ ).toBeInTheDocument();
231
75
  });
232
76
  });
233
77
  });
234
78
 
235
- describe('check store initialization', () => {
236
- test('should wait for store initialization', async () => {
237
- subject(
238
- <TestProvider store={StoreWithInitialize} singletons={[StoreWithInitialize]} />
239
- );
240
-
241
- expect(stat.queueRender).toEqual([]);
242
-
243
- await TestRenderer.act(async () => {
244
- await waitFor(() => {
245
- expect(stat.testInitialize).toHaveBeenCalledWith('StoreWithInitialize-1');
246
- });
247
- });
248
-
249
- await expectQueueRenderToBe(['StoreWithInitialize-1']);
250
- });
251
- test('should wait for for nested initialization', async () => {
252
- subject(
253
- <TestProvider store={StoreWithInitialize} singletons={[StoreWithInitialize]}>
254
- <TestProvider store={StoreWithInitialize} singletons={[StoreWithInitialize]} />
255
- </TestProvider>
256
- );
257
-
258
- await TestRenderer.act(async () => {
259
- await waitFor(() => {
260
- expect(stat.testInitialize).toHaveBeenCalledWith('StoreWithInitialize-1');
261
- });
79
+ describe('with legacy fallback element', () => {
80
+ const LoadingFallback: FC = () => null;
262
81
 
263
- await waitFor(() => {
264
- expect(stat.testInitialize).toHaveBeenCalledWith('StoreWithInitialize-2');
265
- });
266
- });
82
+ beforeEach(() => (options = <LoadingFallback />));
267
83
 
268
- expect(stat.queueCreate).toEqual(['StoreWithInitialize-1', 'StoreWithInitialize-2']);
84
+ test('passes {loadingFallback} option', () => {
85
+ subject();
269
86
 
270
- await expectQueueRenderToBe(['StoreWithInitialize-1', 'StoreWithInitialize-2']);
271
- });
272
- test('should initialize only once when store is reused', async () => {
273
- subject(
274
- <TestProvider store={StoreWithInitialize} singletons={[StoreWithInitialize]}>
275
- <TestProvider
276
- store={StoreWithInitialize}
277
- singletons={[{ provide: StoreWithInitialize, inherit: true }]}
278
- />
279
- </TestProvider>
280
- );
281
-
282
- await TestRenderer.act(async () => {
283
- await waitFor(() => {
284
- expect(stat.testInitialize).toHaveBeenCalledWith('StoreWithInitialize-1');
285
- });
286
- });
287
-
288
- expect(stat.queueCreate).toEqual(['StoreWithInitialize-1']);
289
- await expectQueueRenderToBe(['StoreWithInitialize-1', 'StoreWithInitialize-1']);
290
- expect(stat.testInitialize).toHaveBeenCalledTimes(1);
291
- });
292
-
293
- describe('error initialization', () => {
294
- @injectable()
295
- class StoreWithErrorInitialize extends Store {
296
- initialize() {
297
- throw new Error('error');
298
- }
299
- }
300
- @injectable()
301
- class StoreWithAsyncErrorInitialize extends Store {
302
- async initialize() {
303
- await new Promise<void>(resolve => resolve());
304
- throw new Error('error');
305
- }
306
- }
307
-
308
- const subject = async (
309
- children: ReactElement
310
- ): Promise<ReturnType<typeof TestRenderer.create>> => {
311
- let result: ReturnType<typeof TestRenderer.create> | undefined = undefined;
312
-
313
- await TestRenderer.act(async () => {
314
- // we need this function to be async because of TestRenderer warnings
315
- await new Promise<void>(resolve => resolve());
316
- result = TestRenderer.create(children);
317
- });
318
-
319
- if (!result) {
320
- throw new Error();
321
- }
322
-
323
- return result;
324
- };
325
-
326
- test('async error initialization should show error', async () => {
327
- const { root } = await subject(
328
- <Provider singletons={[StoreWithAsyncErrorInitialize]}>hello</Provider>
329
- );
330
-
331
- expect(root.findByType('h2')).toBeTruthy();
332
- });
333
-
334
- test('sync error initialization should show error', async () => {
335
- const { root } = await subject(
336
- <Provider singletons={[StoreWithErrorInitialize]}>hello</Provider>
337
- );
338
-
339
- expect(root.findByType('h2')).toBeTruthy();
340
- });
87
+ expect(
88
+ screen.getByText(
89
+ `<Provider ${JSON.stringify({
90
+ ...stores,
91
+ loadingFallback: <LoadingFallback />,
92
+ })} />`
93
+ )
94
+ ).toBeInTheDocument();
341
95
  });
342
96
  });
343
97
  });
@@ -0,0 +1,352 @@
1
+ import { PropsWithChildren, ReactElement, ReactNode, useEffect } from 'react';
2
+ import {
3
+ inject,
4
+ injectable,
5
+ useDependencies,
6
+ Store,
7
+ symbolToken,
8
+ Provider,
9
+ ProviderProps,
10
+ } from '..';
11
+ import TestRenderer from 'react-test-renderer';
12
+ import { waitFor } from '@testing-library/react';
13
+
14
+ @injectable()
15
+ class StatStore {
16
+ readonly id = 'stat';
17
+ counters: Record<string, number> = {};
18
+ queueCreate: string[] = [];
19
+ queueRender: string[] = [];
20
+
21
+ testInitialize: (id: string) => void = jest.fn();
22
+ testRender = (id: string) => {
23
+ this.queueRender.push(id);
24
+ };
25
+
26
+ register(name: string) {
27
+ if (!this.counters[name]) {
28
+ this.counters[name] = 0;
29
+ }
30
+
31
+ this.counters[name]++;
32
+
33
+ const id = `${name}-${this.counters[name]}`;
34
+ this.queueCreate.push(id);
35
+
36
+ return id;
37
+ }
38
+ }
39
+
40
+ @injectable()
41
+ class StoreSimple {
42
+ readonly id;
43
+
44
+ constructor(@inject(StatStore) stat: StatStore) {
45
+ this.id = stat.register('StoreSimple');
46
+ }
47
+ }
48
+
49
+ @injectable()
50
+ class StoreWithInitialize extends Store {
51
+ readonly id;
52
+
53
+ constructor(@inject(StatStore) private stat: StatStore) {
54
+ super();
55
+ this.id = stat.register('StoreWithInitialize');
56
+ }
57
+
58
+ initialize() {
59
+ this.stat.testInitialize(this.id);
60
+ }
61
+ }
62
+
63
+ type TestStore = typeof StoreSimple | typeof StoreWithInitialize | ReturnType<typeof symbolToken>;
64
+
65
+ const getTestComponent = (inst: TestStore) => {
66
+ return ({ children }: PropsWithChildren<{}>) => {
67
+ const [{ id }, { testRender }] = useDependencies<[any, StatStore]>(inst, StatStore);
68
+ useEffect(() => {
69
+ testRender(id);
70
+ }, [testRender, id]);
71
+ return <div>{children}</div>;
72
+ };
73
+ };
74
+
75
+ function TestProvider(props: PropsWithChildren<ProviderProps & { store: TestStore }>) {
76
+ const { singletons, instances, children, store = StoreSimple } = props;
77
+ const Component = getTestComponent(store);
78
+ return (
79
+ <Provider singletons={singletons} instances={instances}>
80
+ <Component>{children}</Component>
81
+ </Provider>
82
+ );
83
+ }
84
+
85
+ describe('[react-ioc] Provider', () => {
86
+ let stat: StatStore;
87
+
88
+ beforeEach(() => (stat = new StatStore()));
89
+
90
+ const subject = (children: ReactNode) =>
91
+ TestRenderer.create(
92
+ <Provider singletons={[{ provide: StatStore, useValue: stat }]}>{children}</Provider>
93
+ );
94
+
95
+ const expectQueueRenderToBe = (queueRender: string[]) =>
96
+ TestRenderer.act(async () => {
97
+ await waitFor(() => {
98
+ expect(stat.queueRender).toEqual(queueRender);
99
+ });
100
+ });
101
+
102
+ describe('with singletons', () => {
103
+ test('reuses singletons for each container.get', async () => {
104
+ subject(
105
+ <TestProvider store={StoreSimple} singletons={[StoreSimple]}>
106
+ <TestProvider store={StoreSimple} />
107
+ </TestProvider>
108
+ );
109
+
110
+ expect(stat.queueCreate).toEqual(['StoreSimple-1']);
111
+
112
+ await expectQueueRenderToBe(['StoreSimple-1', 'StoreSimple-1']);
113
+ });
114
+
115
+ test('creates separate singletons for nested tokens', async () => {
116
+ subject(
117
+ <TestProvider store={StoreSimple} singletons={[StoreSimple]}>
118
+ <TestProvider store={StoreSimple} singletons={[StoreSimple]} />
119
+ </TestProvider>
120
+ );
121
+
122
+ expect(stat.queueCreate).toEqual(['StoreSimple-1', 'StoreSimple-2']);
123
+
124
+ await expectQueueRenderToBe(['StoreSimple-1', 'StoreSimple-2']);
125
+ });
126
+
127
+ test('reuses inheritable singleton', async () => {
128
+ subject(
129
+ <TestProvider store={StoreSimple} singletons={[StoreSimple]}>
130
+ <TestProvider
131
+ store={StoreSimple}
132
+ singletons={[{ provide: StoreSimple, inherit: true }]}
133
+ />
134
+ </TestProvider>
135
+ );
136
+
137
+ expect(stat.queueCreate).toEqual(['StoreSimple-1']);
138
+
139
+ await expectQueueRenderToBe(['StoreSimple-1', 'StoreSimple-1']);
140
+ });
141
+
142
+ test('creates singletons when inheritable singleton does not exist', () => {
143
+ subject(
144
+ <TestProvider
145
+ store={StoreSimple}
146
+ singletons={[{ provide: StoreSimple, inherit: true }]}
147
+ >
148
+ <TestProvider store={StoreSimple} singletons={[StoreSimple]} />
149
+ </TestProvider>
150
+ );
151
+
152
+ expect(stat.queueCreate).toEqual(['StoreSimple-1', 'StoreSimple-2']);
153
+ });
154
+ });
155
+
156
+ describe('with instances', () => {
157
+ test('creates separate instances for each container.get', async () => {
158
+ subject(
159
+ <TestProvider store={StoreSimple} instances={[StoreSimple]}>
160
+ <TestProvider store={StoreSimple} />
161
+ </TestProvider>
162
+ );
163
+
164
+ expect(stat.queueCreate).toEqual(['StoreSimple-1', 'StoreSimple-2']);
165
+
166
+ await expectQueueRenderToBe(['StoreSimple-1', 'StoreSimple-2']);
167
+ });
168
+
169
+ test('creates separate instances for nested tokens', async () => {
170
+ subject(
171
+ <TestProvider store={StoreSimple} instances={[StoreSimple]}>
172
+ <TestProvider store={StoreSimple} instances={[StoreSimple]} />
173
+ </TestProvider>
174
+ );
175
+
176
+ expect(stat.queueCreate).toEqual(['StoreSimple-1', 'StoreSimple-2']);
177
+
178
+ await expectQueueRenderToBe(['StoreSimple-1', 'StoreSimple-2']);
179
+ });
180
+ });
181
+
182
+ describe('useValue', () => {
183
+ const valueToken = symbolToken('valueToken');
184
+
185
+ test('creates new values', async () => {
186
+ subject(
187
+ <TestProvider
188
+ store={valueToken}
189
+ singletons={[{ provide: valueToken, useValue: { id: 'value1' } }]}
190
+ >
191
+ <TestProvider
192
+ store={valueToken}
193
+ singletons={[{ provide: valueToken, useValue: { id: 'value2' } }]}
194
+ />
195
+ </TestProvider>
196
+ );
197
+
198
+ await expectQueueRenderToBe(['value1', 'value2']);
199
+ });
200
+
201
+ test('reuses nested values', async () => {
202
+ subject(
203
+ <TestProvider
204
+ store={valueToken}
205
+ singletons={[{ provide: valueToken, useValue: { id: 'value1' } }]}
206
+ >
207
+ <TestProvider
208
+ store={valueToken}
209
+ singletons={[
210
+ { provide: valueToken, useValue: { id: 'value2' }, inherit: true },
211
+ ]}
212
+ />
213
+ </TestProvider>
214
+ );
215
+
216
+ await expectQueueRenderToBe(['value1', 'value1']);
217
+ });
218
+
219
+ test('does not reuse nested values for instances', async () => {
220
+ subject(
221
+ <TestProvider
222
+ store={valueToken}
223
+ singletons={[{ provide: valueToken, useValue: { id: 'value1' } }]}
224
+ >
225
+ <TestProvider
226
+ store={valueToken}
227
+ instances={[
228
+ {
229
+ provide: valueToken,
230
+ useValue: { id: 'value2' },
231
+ inherit: true,
232
+ } as any,
233
+ ]}
234
+ />
235
+ </TestProvider>
236
+ );
237
+
238
+ await expectQueueRenderToBe(['value1', 'value2']);
239
+ });
240
+ });
241
+
242
+ describe('check store initialization', () => {
243
+ test('waits for store initialization', async () => {
244
+ subject(
245
+ <TestProvider store={StoreWithInitialize} singletons={[StoreWithInitialize]} />
246
+ );
247
+
248
+ expect(stat.queueRender).toEqual([]);
249
+
250
+ await TestRenderer.act(async () => {
251
+ await waitFor(() => {
252
+ expect(stat.testInitialize).toHaveBeenCalledWith('StoreWithInitialize-1');
253
+ });
254
+ });
255
+
256
+ await expectQueueRenderToBe(['StoreWithInitialize-1']);
257
+ });
258
+
259
+ test('waits for nested initialization', async () => {
260
+ subject(
261
+ <TestProvider store={StoreWithInitialize} singletons={[StoreWithInitialize]}>
262
+ <TestProvider store={StoreWithInitialize} singletons={[StoreWithInitialize]} />
263
+ </TestProvider>
264
+ );
265
+
266
+ await TestRenderer.act(async () => {
267
+ await waitFor(() => {
268
+ expect(stat.testInitialize).toHaveBeenCalledWith('StoreWithInitialize-1');
269
+ });
270
+
271
+ await waitFor(() => {
272
+ expect(stat.testInitialize).toHaveBeenCalledWith('StoreWithInitialize-2');
273
+ });
274
+ });
275
+
276
+ expect(stat.queueCreate).toEqual(['StoreWithInitialize-1', 'StoreWithInitialize-2']);
277
+
278
+ await expectQueueRenderToBe(['StoreWithInitialize-1', 'StoreWithInitialize-2']);
279
+ });
280
+
281
+ test('initializes only once when store is reused', async () => {
282
+ subject(
283
+ <TestProvider store={StoreWithInitialize} singletons={[StoreWithInitialize]}>
284
+ <TestProvider
285
+ store={StoreWithInitialize}
286
+ singletons={[{ provide: StoreWithInitialize, inherit: true }]}
287
+ />
288
+ </TestProvider>
289
+ );
290
+
291
+ await TestRenderer.act(async () => {
292
+ await waitFor(() => {
293
+ expect(stat.testInitialize).toHaveBeenCalledWith('StoreWithInitialize-1');
294
+ });
295
+ });
296
+
297
+ expect(stat.queueCreate).toEqual(['StoreWithInitialize-1']);
298
+ await expectQueueRenderToBe(['StoreWithInitialize-1', 'StoreWithInitialize-1']);
299
+ expect(stat.testInitialize).toHaveBeenCalledTimes(1);
300
+ });
301
+
302
+ describe('error initialization', () => {
303
+ @injectable()
304
+ class StoreWithErrorInitialize extends Store {
305
+ initialize() {
306
+ throw new Error('error');
307
+ }
308
+ }
309
+ @injectable()
310
+ class StoreWithAsyncErrorInitialize extends Store {
311
+ async initialize() {
312
+ await new Promise<void>(resolve => resolve());
313
+ throw new Error('error');
314
+ }
315
+ }
316
+
317
+ const subject = async (
318
+ children: ReactElement
319
+ ): Promise<ReturnType<typeof TestRenderer.create>> => {
320
+ let result: ReturnType<typeof TestRenderer.create> | undefined = undefined;
321
+
322
+ await TestRenderer.act(async () => {
323
+ // we need this function to be async because of TestRenderer warnings
324
+ await new Promise<void>(resolve => resolve());
325
+ result = TestRenderer.create(children);
326
+ });
327
+
328
+ if (!result) {
329
+ throw new Error();
330
+ }
331
+
332
+ return result;
333
+ };
334
+
335
+ test('async error initialization shows error', async () => {
336
+ const { root } = await subject(
337
+ <Provider singletons={[StoreWithAsyncErrorInitialize]}>hello</Provider>
338
+ );
339
+
340
+ expect(root.findByType('h2')).toBeTruthy();
341
+ });
342
+
343
+ test('sync error initialization shows error', async () => {
344
+ const { root } = await subject(
345
+ <Provider singletons={[StoreWithErrorInitialize]}>hello</Provider>
346
+ );
347
+
348
+ expect(root.findByType('h2')).toBeTruthy();
349
+ });
350
+ });
351
+ });
352
+ });
package/src/provide.tsx CHANGED
@@ -7,24 +7,38 @@ export interface WrappedComponent<P> {
7
7
  wrappedInstance: ReactInstance | undefined;
8
8
  }
9
9
 
10
+ interface FallbackOptions {
11
+ loadingFallback?: JSX.Element;
12
+ errorFallback?: JSX.Element;
13
+ }
14
+
10
15
  export type ProvideDecorator = (
11
16
  stores: {
12
17
  singletons?: ProviderConfigEntrySingletons<any>[];
13
18
  instances?: ProviderConfigEntryInstances<any>[];
14
19
  },
15
- options?: {
16
- loadingFallback?: JSX.Element;
17
- errorFallback?: JSX.Element;
18
- }
19
- ) => <T extends ComponentType>(target: T) => T & WrappedComponent<T>;
20
+ options?: JSX.Element | FallbackOptions
21
+ ) => <T extends ComponentType<any>>(target: T) => T & WrappedComponent<T>;
20
22
 
21
- export const provide = ((stores, options) => (Component: ComponentType) => props =>
23
+ export const provide = ((stores, elementOrOptions) => (Component: ComponentType) => props =>
22
24
  (
23
- <Provider
24
- {...stores}
25
- loadingFallback={options?.loadingFallback}
26
- errorFallback={options?.errorFallback}
27
- >
25
+ <Provider {...stores} {...getOptions(elementOrOptions)}>
28
26
  <Component {...props} />
29
27
  </Provider>
30
28
  )) as ProvideDecorator;
29
+
30
+ function getOptions(arg?: FallbackOptions | JSX.Element): FallbackOptions | undefined {
31
+ return arg && isOptionsObject(arg) ? arg : { loadingFallback: arg };
32
+ }
33
+
34
+ function isOptionsObject(arg: FallbackOptions | JSX.Element): arg is FallbackOptions {
35
+ return (
36
+ Object.hasOwn(arg, 'loadingFallback') ||
37
+ Object.hasOwn(arg, 'errorFallback') ||
38
+ isEmptyObject(arg)
39
+ );
40
+ }
41
+
42
+ function isEmptyObject(arg: FallbackOptions | JSX.Element) {
43
+ return typeof arg === 'object' && Object.keys(arg).length === 0;
44
+ }