@navios/di-react 0.1.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/LICENSE +7 -0
- package/README.md +176 -0
- package/lib/_tsup-dts-rollup.d.mts +304 -0
- package/lib/_tsup-dts-rollup.d.ts +304 -0
- package/lib/index.d.mts +18 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +405 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +392 -0
- package/lib/index.mjs.map +1 -0
- package/package.json +44 -0
- package/project.json +61 -0
- package/src/hooks/__tests__/use-container.spec.mts +52 -0
- package/src/hooks/__tests__/use-invalidate.spec.mts +216 -0
- package/src/hooks/__tests__/use-optional-service.spec.mts +233 -0
- package/src/hooks/__tests__/use-service.spec.mts +212 -0
- package/src/hooks/__tests__/use-suspense-service.spec.mts +286 -0
- package/src/hooks/index.mts +8 -0
- package/src/hooks/use-container.mts +13 -0
- package/src/hooks/use-invalidate.mts +122 -0
- package/src/hooks/use-optional-service.mts +259 -0
- package/src/hooks/use-scope.mts +26 -0
- package/src/hooks/use-service.mts +201 -0
- package/src/hooks/use-suspense-service.mts +222 -0
- package/src/index.mts +8 -0
- package/src/providers/__tests__/scope-provider.spec.mts +280 -0
- package/src/providers/container-provider.mts +22 -0
- package/src/providers/context.mts +5 -0
- package/src/providers/index.mts +5 -0
- package/src/providers/scope-provider.mts +88 -0
- package/src/types.mts +21 -0
- package/tsconfig.json +13 -0
- package/tsup.config.mts +13 -0
- package/vitest.config.mts +9 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Container, Injectable, InjectionToken, Registry } from '@navios/di'
|
|
2
|
+
|
|
3
|
+
import { act, render, screen, waitFor } from '@testing-library/react'
|
|
4
|
+
import { createElement, useMemo } from 'react'
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import { z } from 'zod/v4'
|
|
7
|
+
|
|
8
|
+
import { ContainerProvider } from '../../providers/container-provider.mjs'
|
|
9
|
+
import { useInvalidate, useInvalidateInstance } from '../use-invalidate.mjs'
|
|
10
|
+
import { useService } from '../use-service.mjs'
|
|
11
|
+
|
|
12
|
+
describe('useInvalidate', () => {
|
|
13
|
+
let container: Container
|
|
14
|
+
let registry: Registry
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
registry = new Registry()
|
|
18
|
+
container = new Container(registry)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await container.dispose()
|
|
23
|
+
vi.clearAllMocks()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const createWrapper = (children: React.ReactNode) =>
|
|
27
|
+
createElement(ContainerProvider, { container, children })
|
|
28
|
+
|
|
29
|
+
describe('with class tokens', () => {
|
|
30
|
+
it('should invalidate a service and trigger re-fetch', async () => {
|
|
31
|
+
let instanceCount = 0
|
|
32
|
+
|
|
33
|
+
@Injectable({ registry })
|
|
34
|
+
class CounterService {
|
|
35
|
+
public readonly id: number
|
|
36
|
+
|
|
37
|
+
constructor() {
|
|
38
|
+
instanceCount++
|
|
39
|
+
this.id = instanceCount
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let invalidateFn: (() => Promise<void>) | null = null
|
|
44
|
+
|
|
45
|
+
function TestComponent() {
|
|
46
|
+
const { data, isSuccess } = useService(CounterService)
|
|
47
|
+
const invalidate = useInvalidate(CounterService)
|
|
48
|
+
invalidateFn = invalidate
|
|
49
|
+
|
|
50
|
+
if (!isSuccess) {
|
|
51
|
+
return createElement('div', { 'data-testid': 'loading' }, 'Loading...')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return createElement('div', { 'data-testid': 'counter' }, String(data!.id))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
render(createWrapper(createElement(TestComponent)))
|
|
58
|
+
|
|
59
|
+
// Wait for initial load
|
|
60
|
+
await waitFor(() => {
|
|
61
|
+
expect(screen.getByTestId('counter')).toBeDefined()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(screen.getByTestId('counter').textContent).toBe('1')
|
|
65
|
+
|
|
66
|
+
// Invalidate the service
|
|
67
|
+
await act(async () => {
|
|
68
|
+
await invalidateFn!()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Wait for re-fetch
|
|
72
|
+
await waitFor(() => {
|
|
73
|
+
expect(screen.getByTestId('counter').textContent).toBe('2')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
expect(instanceCount).toBe(2)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('with injection tokens and args', () => {
|
|
81
|
+
it('should invalidate a service with specific args', async () => {
|
|
82
|
+
let instanceCount = 0
|
|
83
|
+
const instances = new Map<string, number>()
|
|
84
|
+
|
|
85
|
+
const UserSchema = z.object({ userId: z.string() })
|
|
86
|
+
const UserToken = InjectionToken.create<
|
|
87
|
+
{ userId: string; instanceId: number },
|
|
88
|
+
typeof UserSchema
|
|
89
|
+
>('User', UserSchema)
|
|
90
|
+
|
|
91
|
+
@Injectable({ registry, token: UserToken })
|
|
92
|
+
class _UserService {
|
|
93
|
+
public userId: string
|
|
94
|
+
public instanceId: number
|
|
95
|
+
|
|
96
|
+
constructor(args: z.infer<typeof UserSchema>) {
|
|
97
|
+
this.userId = args.userId
|
|
98
|
+
const currentCount = instances.get(args.userId) ?? 0
|
|
99
|
+
instanceCount++
|
|
100
|
+
this.instanceId = instanceCount
|
|
101
|
+
instances.set(args.userId, this.instanceId)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let invalidateUser1: (() => Promise<void>) | null = null
|
|
106
|
+
|
|
107
|
+
function TestComponent() {
|
|
108
|
+
const args1 = useMemo(() => ({ userId: 'user1' }), [])
|
|
109
|
+
const args2 = useMemo(() => ({ userId: 'user2' }), [])
|
|
110
|
+
|
|
111
|
+
const { data: user1, isSuccess: isSuccess1 } = useService(UserToken, args1)
|
|
112
|
+
const { data: user2, isSuccess: isSuccess2 } = useService(UserToken, args2)
|
|
113
|
+
const invalidate1 = useInvalidate(UserToken, args1)
|
|
114
|
+
invalidateUser1 = invalidate1
|
|
115
|
+
|
|
116
|
+
if (!isSuccess1 || !isSuccess2) {
|
|
117
|
+
return createElement('div', { 'data-testid': 'loading' }, 'Loading...')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return createElement('div', null, [
|
|
121
|
+
createElement('span', { key: '1', 'data-testid': 'user1' }, String(user1!.instanceId)),
|
|
122
|
+
createElement('span', { key: '2', 'data-testid': 'user2' }, String(user2!.instanceId)),
|
|
123
|
+
])
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
render(createWrapper(createElement(TestComponent)))
|
|
127
|
+
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(screen.getByTestId('user1')).toBeDefined()
|
|
130
|
+
expect(screen.getByTestId('user2')).toBeDefined()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
expect(screen.getByTestId('user1').textContent).toBe('1')
|
|
134
|
+
expect(screen.getByTestId('user2').textContent).toBe('2')
|
|
135
|
+
|
|
136
|
+
// Invalidate only user1
|
|
137
|
+
await act(async () => {
|
|
138
|
+
await invalidateUser1!()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Wait for user1 to be re-fetched
|
|
142
|
+
await waitFor(() => {
|
|
143
|
+
expect(screen.getByTestId('user1').textContent).toBe('3')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// user2 should still be the same
|
|
147
|
+
expect(screen.getByTestId('user2').textContent).toBe('2')
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('useInvalidateInstance', () => {
|
|
153
|
+
let container: Container
|
|
154
|
+
let registry: Registry
|
|
155
|
+
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
registry = new Registry()
|
|
158
|
+
container = new Container(registry)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
afterEach(async () => {
|
|
162
|
+
await container.dispose()
|
|
163
|
+
vi.clearAllMocks()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const createWrapper = (children: React.ReactNode) =>
|
|
167
|
+
createElement(ContainerProvider, { container, children })
|
|
168
|
+
|
|
169
|
+
it('should invalidate a service instance directly', async () => {
|
|
170
|
+
let instanceCount = 0
|
|
171
|
+
|
|
172
|
+
@Injectable({ registry })
|
|
173
|
+
class CounterService {
|
|
174
|
+
public readonly id: number
|
|
175
|
+
|
|
176
|
+
constructor() {
|
|
177
|
+
instanceCount++
|
|
178
|
+
this.id = instanceCount
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let invalidateInstanceFn: ((instance: unknown) => Promise<void>) | null = null
|
|
183
|
+
let currentInstance: CounterService | undefined
|
|
184
|
+
|
|
185
|
+
function TestComponent() {
|
|
186
|
+
const { data, isSuccess } = useService(CounterService)
|
|
187
|
+
const invalidateInstance = useInvalidateInstance()
|
|
188
|
+
invalidateInstanceFn = invalidateInstance
|
|
189
|
+
currentInstance = data
|
|
190
|
+
|
|
191
|
+
if (!isSuccess) {
|
|
192
|
+
return createElement('div', { 'data-testid': 'loading' }, 'Loading...')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return createElement('div', { 'data-testid': 'counter' }, String(data!.id))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
render(createWrapper(createElement(TestComponent)))
|
|
199
|
+
|
|
200
|
+
await waitFor(() => {
|
|
201
|
+
expect(screen.getByTestId('counter')).toBeDefined()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
expect(screen.getByTestId('counter').textContent).toBe('1')
|
|
205
|
+
|
|
206
|
+
// Invalidate the instance directly
|
|
207
|
+
await act(async () => {
|
|
208
|
+
await invalidateInstanceFn!(currentInstance!)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// Wait for re-fetch
|
|
212
|
+
await waitFor(() => {
|
|
213
|
+
expect(screen.getByTestId('counter').textContent).toBe('2')
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
})
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { Container, Injectable, InjectionToken, Registry } from '@navios/di'
|
|
2
|
+
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
4
|
+
import { createElement, useMemo } from 'react'
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import { z } from 'zod/v4'
|
|
7
|
+
|
|
8
|
+
import { ContainerProvider } from '../../providers/container-provider.mjs'
|
|
9
|
+
import { useOptionalService } from '../use-optional-service.mjs'
|
|
10
|
+
|
|
11
|
+
describe('useOptionalService', () => {
|
|
12
|
+
let container: Container
|
|
13
|
+
let registry: Registry
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
registry = new Registry()
|
|
17
|
+
container = new Container(registry)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await container.dispose()
|
|
22
|
+
vi.clearAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const createWrapper = (children: React.ReactNode) =>
|
|
26
|
+
createElement(ContainerProvider, { container, children })
|
|
27
|
+
|
|
28
|
+
describe('when service is registered', () => {
|
|
29
|
+
it('should load the service successfully', async () => {
|
|
30
|
+
@Injectable({ registry })
|
|
31
|
+
class TestService {
|
|
32
|
+
getValue() {
|
|
33
|
+
return 'test-value'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function TestComponent() {
|
|
38
|
+
const { data, isSuccess, isNotFound, isLoading } = useOptionalService(TestService)
|
|
39
|
+
|
|
40
|
+
if (isLoading) {
|
|
41
|
+
return createElement('div', { 'data-testid': 'loading' }, 'Loading...')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (isNotFound) {
|
|
45
|
+
return createElement('div', { 'data-testid': 'not-found' }, 'Not Found')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (isSuccess) {
|
|
49
|
+
return createElement('div', { 'data-testid': 'result' }, data!.getValue())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return createElement('div', { 'data-testid': 'idle' }, 'Idle')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
render(createWrapper(createElement(TestComponent)))
|
|
56
|
+
|
|
57
|
+
await waitFor(() => {
|
|
58
|
+
expect(screen.getByTestId('result')).toBeDefined()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(screen.getByTestId('result').textContent).toBe('test-value')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should load service with injection token and args', async () => {
|
|
65
|
+
const UserSchema = z.object({ userId: z.string() })
|
|
66
|
+
const UserToken = InjectionToken.create<
|
|
67
|
+
{ userId: string; name: string },
|
|
68
|
+
typeof UserSchema
|
|
69
|
+
>('User', UserSchema)
|
|
70
|
+
|
|
71
|
+
@Injectable({ registry, token: UserToken })
|
|
72
|
+
class _UserService {
|
|
73
|
+
public userId: string
|
|
74
|
+
public name: string
|
|
75
|
+
|
|
76
|
+
constructor(args: z.infer<typeof UserSchema>) {
|
|
77
|
+
this.userId = args.userId
|
|
78
|
+
this.name = `User ${args.userId}`
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function TestComponent() {
|
|
83
|
+
const args = useMemo(() => ({ userId: '123' }), [])
|
|
84
|
+
const { data, isSuccess, isNotFound } = useOptionalService(UserToken, args)
|
|
85
|
+
|
|
86
|
+
if (isNotFound) {
|
|
87
|
+
return createElement('div', { 'data-testid': 'not-found' }, 'Not Found')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (isSuccess) {
|
|
91
|
+
return createElement('div', { 'data-testid': 'result' }, data!.name)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return createElement('div', { 'data-testid': 'loading' }, 'Loading...')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render(createWrapper(createElement(TestComponent)))
|
|
98
|
+
|
|
99
|
+
await waitFor(() => {
|
|
100
|
+
expect(screen.getByTestId('result')).toBeDefined()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(screen.getByTestId('result').textContent).toBe('User 123')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('when service is not registered', () => {
|
|
108
|
+
it('should return isNotFound true for unregistered class token', async () => {
|
|
109
|
+
// Create a class that is NOT registered with Injectable
|
|
110
|
+
class UnregisteredService {
|
|
111
|
+
getValue() {
|
|
112
|
+
return 'value'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function TestComponent() {
|
|
117
|
+
const { isSuccess, isNotFound, isError, isLoading } =
|
|
118
|
+
useOptionalService(UnregisteredService)
|
|
119
|
+
|
|
120
|
+
if (isLoading) {
|
|
121
|
+
return createElement('div', { 'data-testid': 'loading' }, 'Loading...')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (isNotFound) {
|
|
125
|
+
return createElement('div', { 'data-testid': 'not-found' }, 'Service Not Found')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (isError) {
|
|
129
|
+
return createElement('div', { 'data-testid': 'error' }, 'Error')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isSuccess) {
|
|
133
|
+
return createElement('div', { 'data-testid': 'success' }, 'Success')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return createElement('div', { 'data-testid': 'idle' }, 'Idle')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
render(createWrapper(createElement(TestComponent)))
|
|
140
|
+
|
|
141
|
+
await waitFor(() => {
|
|
142
|
+
const notFound = screen.queryByTestId('not-found')
|
|
143
|
+
const error = screen.queryByTestId('error')
|
|
144
|
+
// Either not-found or error is acceptable for unregistered services
|
|
145
|
+
expect(notFound || error).toBeTruthy()
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should return isNotFound true for unregistered injection token', async () => {
|
|
150
|
+
const UnregisteredToken = InjectionToken.create<{ value: string }>('Unregistered')
|
|
151
|
+
|
|
152
|
+
function TestComponent() {
|
|
153
|
+
const { isSuccess, isNotFound, isError, isLoading } =
|
|
154
|
+
useOptionalService(UnregisteredToken)
|
|
155
|
+
|
|
156
|
+
if (isLoading) {
|
|
157
|
+
return createElement('div', { 'data-testid': 'loading' }, 'Loading...')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (isNotFound) {
|
|
161
|
+
return createElement('div', { 'data-testid': 'not-found' }, 'Token Not Found')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (isError) {
|
|
165
|
+
return createElement('div', { 'data-testid': 'error' }, 'Error')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (isSuccess) {
|
|
169
|
+
return createElement('div', { 'data-testid': 'success' }, 'Success')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return createElement('div', { 'data-testid': 'idle' }, 'Idle')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
render(createWrapper(createElement(TestComponent)))
|
|
176
|
+
|
|
177
|
+
await waitFor(() => {
|
|
178
|
+
const notFound = screen.queryByTestId('not-found')
|
|
179
|
+
const error = screen.queryByTestId('error')
|
|
180
|
+
// Either not-found or error is acceptable for unregistered tokens
|
|
181
|
+
expect(notFound || error).toBeTruthy()
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
describe('refetch functionality', () => {
|
|
187
|
+
it('should allow manual refetch', async () => {
|
|
188
|
+
let instanceCount = 0
|
|
189
|
+
|
|
190
|
+
@Injectable({ registry })
|
|
191
|
+
class CounterService {
|
|
192
|
+
public readonly id: number
|
|
193
|
+
|
|
194
|
+
constructor() {
|
|
195
|
+
instanceCount++
|
|
196
|
+
this.id = instanceCount
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let refetchFn: (() => void) | null = null
|
|
201
|
+
|
|
202
|
+
function TestComponent() {
|
|
203
|
+
const { data, isSuccess, refetch } = useOptionalService(CounterService)
|
|
204
|
+
refetchFn = refetch
|
|
205
|
+
|
|
206
|
+
if (!isSuccess) {
|
|
207
|
+
return createElement('div', { 'data-testid': 'loading' }, 'Loading...')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return createElement('div', { 'data-testid': 'counter' }, String(data!.id))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
render(createWrapper(createElement(TestComponent)))
|
|
214
|
+
|
|
215
|
+
await waitFor(() => {
|
|
216
|
+
expect(screen.getByTestId('counter')).toBeDefined()
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
expect(screen.getByTestId('counter').textContent).toBe('1')
|
|
220
|
+
|
|
221
|
+
// Note: refetch alone won't create a new instance since the service is cached
|
|
222
|
+
// It will return the same cached instance
|
|
223
|
+
refetchFn!()
|
|
224
|
+
|
|
225
|
+
await waitFor(() => {
|
|
226
|
+
expect(screen.getByTestId('counter')).toBeDefined()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// Same instance because it's cached
|
|
230
|
+
expect(screen.getByTestId('counter').textContent).toBe('1')
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
})
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { Container, Injectable, InjectionToken, Registry } from '@navios/di'
|
|
2
|
+
|
|
3
|
+
import { act, renderHook, waitFor } from '@testing-library/react'
|
|
4
|
+
import { createElement, useMemo } from 'react'
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
6
|
+
import { z } from 'zod/v4'
|
|
7
|
+
|
|
8
|
+
import { ContainerProvider } from '../../providers/container-provider.mjs'
|
|
9
|
+
import { useService } from '../use-service.mjs'
|
|
10
|
+
|
|
11
|
+
describe('useService', () => {
|
|
12
|
+
let container: Container
|
|
13
|
+
let registry: Registry
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
registry = new Registry()
|
|
17
|
+
container = new Container(registry)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await container.clear()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const createWrapper = () => {
|
|
25
|
+
return ({ children }: { children: React.ReactNode }) =>
|
|
26
|
+
// @ts-expect-error - container is required
|
|
27
|
+
createElement(ContainerProvider, { container }, children)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('with class tokens', () => {
|
|
31
|
+
it('should load a service and return success state', async () => {
|
|
32
|
+
@Injectable({ registry })
|
|
33
|
+
class TestService {
|
|
34
|
+
getValue() {
|
|
35
|
+
return 'test-value'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { result } = renderHook(() => useService(TestService), {
|
|
40
|
+
wrapper: createWrapper(),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Initially loading
|
|
44
|
+
expect(result.current.isLoading).toBe(true)
|
|
45
|
+
expect(result.current.data).toBeUndefined()
|
|
46
|
+
|
|
47
|
+
// Wait for success
|
|
48
|
+
await waitFor(() => {
|
|
49
|
+
expect(result.current.isSuccess).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(result.current.data).toBeInstanceOf(TestService)
|
|
53
|
+
expect(result.current.data?.getValue()).toBe('test-value')
|
|
54
|
+
expect(result.current.isLoading).toBe(false)
|
|
55
|
+
expect(result.current.isError).toBe(false)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should return error state when service fails to load', async () => {
|
|
59
|
+
@Injectable({ registry })
|
|
60
|
+
class FailingService {
|
|
61
|
+
constructor() {
|
|
62
|
+
throw new Error('Service initialization failed')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { result } = renderHook(() => useService(FailingService), {
|
|
67
|
+
wrapper: createWrapper(),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
await waitFor(() => {
|
|
71
|
+
expect(result.current.isError).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
expect(result.current.error?.message).toBe(
|
|
75
|
+
'Service initialization failed',
|
|
76
|
+
)
|
|
77
|
+
expect(result.current.data).toBeUndefined()
|
|
78
|
+
expect(result.current.isLoading).toBe(false)
|
|
79
|
+
expect(result.current.isSuccess).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it.skip('should refetch service when refetch is called', async () => {
|
|
83
|
+
let callCount = 0
|
|
84
|
+
|
|
85
|
+
@Injectable({ registry })
|
|
86
|
+
class CountingService {
|
|
87
|
+
public count: number
|
|
88
|
+
|
|
89
|
+
constructor() {
|
|
90
|
+
callCount++
|
|
91
|
+
this.count = callCount
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { result } = renderHook(() => useService(CountingService), {
|
|
96
|
+
wrapper: createWrapper(),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
await waitFor(() => {
|
|
100
|
+
expect(result.current.isSuccess).toBe(true)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(result.current.data?.count).toBe(1)
|
|
104
|
+
|
|
105
|
+
// Invalidate the service first so refetch creates a new instance
|
|
106
|
+
await container.invalidate(result.current.data)
|
|
107
|
+
|
|
108
|
+
// Trigger refetch
|
|
109
|
+
await act(async () => {
|
|
110
|
+
result.current.refetch()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
await waitFor(() => {
|
|
114
|
+
expect(result.current.data?.count).toBe(2)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('with injection tokens', () => {
|
|
120
|
+
it('should load a service with injection token', async () => {
|
|
121
|
+
const TestToken = InjectionToken.create<{ value: string }>('TestToken')
|
|
122
|
+
|
|
123
|
+
@Injectable({ registry, token: TestToken })
|
|
124
|
+
class TestService {
|
|
125
|
+
value = 'token-value'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { result } = renderHook(() => useService(TestToken), {
|
|
129
|
+
wrapper: createWrapper(),
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
expect(result.current.isSuccess).toBe(true)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
expect(result.current.data?.value).toBe('token-value')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should load a service with injection token and args', async () => {
|
|
140
|
+
const UserSchema = z.object({ userId: z.string() })
|
|
141
|
+
const UserToken = InjectionToken.create<
|
|
142
|
+
{ userId: string; name: string },
|
|
143
|
+
typeof UserSchema
|
|
144
|
+
>('User', UserSchema)
|
|
145
|
+
|
|
146
|
+
@Injectable({ registry, token: UserToken })
|
|
147
|
+
class UserService {
|
|
148
|
+
public userId: string
|
|
149
|
+
public name: string
|
|
150
|
+
|
|
151
|
+
constructor(args: z.infer<typeof UserSchema>) {
|
|
152
|
+
this.userId = args.userId
|
|
153
|
+
this.name = `User-${args.userId}`
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const { result } = renderHook(
|
|
158
|
+
() => {
|
|
159
|
+
const args = useMemo(() => ({ userId: '123' }), [])
|
|
160
|
+
return useService(UserToken, args)
|
|
161
|
+
},
|
|
162
|
+
{ wrapper: createWrapper() },
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
await waitFor(() => {
|
|
166
|
+
expect(result.current.isSuccess).toBe(true)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
expect(result.current.data?.userId).toBe('123')
|
|
170
|
+
expect(result.current.data?.name).toBe('User-123')
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('service invalidation', () => {
|
|
175
|
+
it('should re-fetch when service is invalidated', async () => {
|
|
176
|
+
let instanceCount = 0
|
|
177
|
+
|
|
178
|
+
@Injectable({ registry })
|
|
179
|
+
class InvalidatableService {
|
|
180
|
+
public instanceId: number
|
|
181
|
+
|
|
182
|
+
constructor() {
|
|
183
|
+
instanceCount++
|
|
184
|
+
this.instanceId = instanceCount
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { result } = renderHook(() => useService(InvalidatableService), {
|
|
189
|
+
wrapper: createWrapper(),
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
await waitFor(() => {
|
|
193
|
+
expect(result.current.isSuccess).toBe(true)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
expect(result.current.data?.instanceId).toBe(1)
|
|
197
|
+
|
|
198
|
+
// Invalidate the service
|
|
199
|
+
await act(async () => {
|
|
200
|
+
await container.invalidate(result.current.data)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// Wait for re-fetch with increased timeout for event propagation
|
|
204
|
+
await waitFor(
|
|
205
|
+
() => {
|
|
206
|
+
expect(result.current.data?.instanceId).toBe(2)
|
|
207
|
+
},
|
|
208
|
+
{ timeout: 2000 },
|
|
209
|
+
)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
})
|