@servicetitan/react-ioc 23.2.3 → 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 +6 -4
- package/dist/provide.d.ts.map +1 -1
- package/dist/provide.js +12 -1
- package/dist/provide.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/provide.test.tsx +71 -317
- package/src/__tests__/provider.test.tsx +352 -0
- package/src/provide.tsx +25 -11
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
|
package/dist/provide.d.ts.map
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
package/dist/provide.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provide.js","sourceRoot":"","sources":["../src/provide.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,QAAQ,EAA+D,MAAM,YAAY,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.
|
|
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": "
|
|
39
|
+
"gitHead": "4a3420cf55cf34ff10b5ba6164d940b39f0925e6"
|
|
39
40
|
}
|
|
@@ -1,343 +1,97 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
38
|
+
test('renders component with props', () => {
|
|
39
|
+
subject();
|
|
68
40
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
76
|
-
|
|
44
|
+
test('wraps component within <Provider>', () => {
|
|
45
|
+
subject();
|
|
77
46
|
|
|
78
|
-
|
|
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
|
-
|
|
50
|
+
describe('with fallback options', () => {
|
|
51
|
+
const LoadingFallback: FC = () => null;
|
|
52
|
+
const ErrorFallback: FC = () => null;
|
|
85
53
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
await waitFor(() => {
|
|
89
|
-
expect(stat.queueRender).toEqual(queueRender);
|
|
90
|
-
});
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
options = { loadingFallback: <LoadingFallback />, errorFallback: <ErrorFallback /> };
|
|
91
56
|
});
|
|
92
57
|
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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('
|
|
149
|
-
|
|
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
|
-
|
|
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(
|
|
169
|
-
|
|
170
|
-
|
|
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('
|
|
236
|
-
|
|
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
|
-
|
|
264
|
-
expect(stat.testInitialize).toHaveBeenCalledWith('StoreWithInitialize-2');
|
|
265
|
-
});
|
|
266
|
-
});
|
|
82
|
+
beforeEach(() => (options = <LoadingFallback />));
|
|
267
83
|
|
|
268
|
-
|
|
84
|
+
test('passes {loadingFallback} option', () => {
|
|
85
|
+
subject();
|
|
269
86
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
+
}
|