@tanstack/angular-query-experimental 5.103.2 → 5.104.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.
Files changed (218) hide show
  1. package/dist/create-base-query.d.mts +168 -0
  2. package/dist/create-base-query.d.mts.map +1 -0
  3. package/dist/create-base-query.mjs +91 -0
  4. package/dist/create-base-query.mjs.map +1 -0
  5. package/dist/devtools/index.d.mts +3 -0
  6. package/dist/devtools/index.mjs +2 -0
  7. package/dist/devtools/production/index.d.mts +4 -0
  8. package/dist/devtools/production/index.mjs +2 -0
  9. package/dist/devtools/stub.d.mts +5 -0
  10. package/dist/devtools/stub.d.mts.map +1 -0
  11. package/dist/devtools/stub.mjs +9 -0
  12. package/dist/devtools/stub.mjs.map +1 -0
  13. package/dist/devtools/types.d.mts +106 -0
  14. package/dist/devtools/types.d.mts.map +1 -0
  15. package/dist/devtools/types.mjs +1 -0
  16. package/{devtools/with-devtools.d.ts → dist/devtools/with-devtools.d.mts} +4 -1
  17. package/dist/devtools/with-devtools.d.mts.map +1 -0
  18. package/dist/devtools/with-devtools.mjs +110 -0
  19. package/dist/devtools/with-devtools.mjs.map +1 -0
  20. package/dist/devtools-panel/index.d.mts +3 -0
  21. package/dist/devtools-panel/index.mjs +2 -0
  22. package/{devtools-panel/inject-devtools-panel.d.ts → dist/devtools-panel/inject-devtools-panel.d.mts} +4 -1
  23. package/dist/devtools-panel/inject-devtools-panel.d.mts.map +1 -0
  24. package/dist/devtools-panel/inject-devtools-panel.mjs +67 -0
  25. package/dist/devtools-panel/inject-devtools-panel.mjs.map +1 -0
  26. package/dist/devtools-panel/production/index.d.mts +4 -0
  27. package/dist/devtools-panel/production/index.mjs +2 -0
  28. package/dist/devtools-panel/stub.d.mts +5 -0
  29. package/dist/devtools-panel/stub.d.mts.map +1 -0
  30. package/dist/devtools-panel/stub.mjs +7 -0
  31. package/dist/devtools-panel/stub.mjs.map +1 -0
  32. package/dist/devtools-panel/types.d.mts +51 -0
  33. package/dist/devtools-panel/types.d.mts.map +1 -0
  34. package/dist/devtools-panel/types.mjs +1 -0
  35. package/dist/index.d.mts +16 -0
  36. package/{index.mjs → dist/index.mjs} +8 -21
  37. package/dist/index.mjs.map +1 -0
  38. package/{infinite-query-options.d.ts → dist/infinite-query-options.d.mts} +28 -25
  39. package/dist/infinite-query-options.d.mts.map +1 -0
  40. package/dist/infinite-query-options.mjs +8 -0
  41. package/dist/infinite-query-options.mjs.map +1 -0
  42. package/{inject-infinite-query.d.ts → dist/inject-infinite-query.d.mts} +13 -10
  43. package/dist/inject-infinite-query.d.mts.map +1 -0
  44. package/dist/inject-infinite-query.mjs +13 -0
  45. package/dist/inject-infinite-query.mjs.map +1 -0
  46. package/{inject-is-fetching.d.ts → dist/inject-is-fetching.d.mts} +11 -8
  47. package/dist/inject-is-fetching.d.mts.map +1 -0
  48. package/dist/inject-is-fetching.mjs +69 -0
  49. package/dist/inject-is-fetching.mjs.map +1 -0
  50. package/{inject-is-mutating.d.ts → dist/inject-is-mutating.d.mts} +11 -8
  51. package/dist/inject-is-mutating.d.mts.map +1 -0
  52. package/dist/inject-is-mutating.mjs +52 -0
  53. package/dist/inject-is-mutating.mjs.map +1 -0
  54. package/{inject-is-restoring.d.ts → dist/inject-is-restoring.d.mts} +10 -8
  55. package/dist/inject-is-restoring.d.mts.map +1 -0
  56. package/dist/inject-is-restoring.mjs +35 -0
  57. package/dist/inject-is-restoring.mjs.map +1 -0
  58. package/{inject-mutation-state.d.ts → dist/inject-mutation-state.d.mts} +13 -11
  59. package/dist/inject-mutation-state.d.mts.map +1 -0
  60. package/dist/inject-mutation-state.mjs +103 -0
  61. package/dist/inject-mutation-state.mjs.map +1 -0
  62. package/{inject-mutation.d.ts → dist/inject-mutation.d.mts} +12 -9
  63. package/dist/inject-mutation.d.mts.map +1 -0
  64. package/dist/inject-mutation.mjs +226 -0
  65. package/dist/inject-mutation.mjs.map +1 -0
  66. package/dist/inject-queries-experimental/index.d.mts +2 -0
  67. package/dist/inject-queries-experimental/index.mjs +2 -0
  68. package/{inject-queries.d.ts → dist/inject-queries.d.mts} +33 -53
  69. package/dist/inject-queries.d.mts.map +1 -0
  70. package/dist/inject-queries.mjs +203 -0
  71. package/dist/inject-queries.mjs.map +1 -0
  72. package/{inject-query-client.d.ts → dist/inject-query-client.d.mts} +6 -3
  73. package/dist/inject-query-client.d.mts.map +1 -0
  74. package/dist/inject-query-client.mjs +23 -0
  75. package/dist/inject-query-client.mjs.map +1 -0
  76. package/{inject-query.d.ts → dist/inject-query.d.mts} +13 -10
  77. package/dist/inject-query.d.mts.map +1 -0
  78. package/dist/inject-query.mjs +12 -0
  79. package/dist/inject-query.mjs.map +1 -0
  80. package/{mutation-options.d.ts → dist/mutation-options.d.mts} +5 -2
  81. package/dist/mutation-options.d.mts.map +1 -0
  82. package/dist/mutation-options.mjs +8 -0
  83. package/dist/mutation-options.mjs.map +1 -0
  84. package/dist/pending-tasks-compat.d.mts +9 -0
  85. package/dist/pending-tasks-compat.d.mts.map +1 -0
  86. package/dist/pending-tasks-compat.mjs +13 -0
  87. package/dist/pending-tasks-compat.mjs.map +1 -0
  88. package/{providers.d.ts → dist/providers.d.mts} +7 -5
  89. package/dist/providers.d.mts.map +1 -0
  90. package/dist/providers.mjs +124 -0
  91. package/dist/providers.mjs.map +1 -0
  92. package/{query-options.d.ts → dist/query-options.d.mts} +34 -31
  93. package/dist/query-options.d.mts.map +1 -0
  94. package/dist/query-options.mjs +8 -0
  95. package/dist/query-options.mjs.map +1 -0
  96. package/{signal-proxy.d.ts → dist/signal-proxy.d.mts} +5 -4
  97. package/dist/signal-proxy.d.mts.map +1 -0
  98. package/dist/signal-proxy.mjs +35 -0
  99. package/dist/signal-proxy.mjs.map +1 -0
  100. package/{types.d.ts → dist/types.d.mts} +24 -26
  101. package/dist/types.d.mts.map +1 -0
  102. package/dist/types.mjs +6 -0
  103. package/dist/types.mjs.map +1 -0
  104. package/package.json +50 -20
  105. package/src/__tests__/infinite-query-options.test-d.ts +314 -0
  106. package/src/__tests__/infinite-query-options.test.ts +19 -0
  107. package/src/__tests__/inject-devtools-panel.test.ts +196 -0
  108. package/src/__tests__/inject-infinite-query.test-d.ts +66 -0
  109. package/src/__tests__/inject-infinite-query.test.ts +227 -0
  110. package/src/__tests__/inject-is-fetching.test.ts +108 -0
  111. package/src/__tests__/inject-is-mutating.test.ts +113 -0
  112. package/src/__tests__/inject-is-restoring.test.ts +93 -0
  113. package/src/__tests__/inject-mutation-state.test-d.ts +22 -0
  114. package/src/__tests__/inject-mutation-state.test.ts +198 -0
  115. package/src/__tests__/inject-mutation.test-d.ts +158 -0
  116. package/src/__tests__/inject-mutation.test.ts +1161 -0
  117. package/src/__tests__/inject-queries.test-d.ts +196 -0
  118. package/src/__tests__/inject-queries.test.ts +240 -0
  119. package/src/__tests__/inject-query.test-d.ts +357 -0
  120. package/src/__tests__/inject-query.test.ts +700 -0
  121. package/src/__tests__/mutation-options.test-d.ts +234 -0
  122. package/src/__tests__/mutation-options.test.ts +344 -0
  123. package/src/__tests__/pending-tasks.test.ts +644 -0
  124. package/src/__tests__/provide-query-client.test.ts +38 -0
  125. package/src/__tests__/provide-tanstack-query.test.ts +38 -0
  126. package/src/__tests__/query-options.test-d.ts +262 -0
  127. package/src/__tests__/query-options.test.ts +14 -0
  128. package/src/__tests__/signal-proxy.test.ts +27 -0
  129. package/src/__tests__/test-utils.ts +77 -0
  130. package/src/__tests__/with-devtools.test.ts +674 -0
  131. package/src/create-base-query.ts +176 -0
  132. package/src/devtools/index.ts +8 -0
  133. package/src/devtools/production/index.ts +1 -0
  134. package/src/devtools/stub.ts +7 -0
  135. package/src/devtools/types.ts +116 -0
  136. package/src/devtools/with-devtools.ts +182 -0
  137. package/src/devtools-panel/index.ts +8 -0
  138. package/src/devtools-panel/inject-devtools-panel.ts +110 -0
  139. package/src/devtools-panel/production/index.ts +1 -0
  140. package/src/devtools-panel/stub.ts +7 -0
  141. package/src/devtools-panel/types.ts +57 -0
  142. package/src/index.ts +60 -0
  143. package/src/infinite-query-options.ts +354 -0
  144. package/src/inject-infinite-query.ts +296 -0
  145. package/src/inject-is-fetching.ts +97 -0
  146. package/src/inject-is-mutating.ts +80 -0
  147. package/src/inject-is-restoring.ts +53 -0
  148. package/src/inject-mutation-state.ts +167 -0
  149. package/src/inject-mutation.ts +328 -0
  150. package/src/inject-queries-experimental/index.ts +1 -0
  151. package/src/inject-queries.ts +480 -0
  152. package/src/inject-query-client.ts +22 -0
  153. package/src/inject-query.ts +208 -0
  154. package/src/mutation-options.ts +121 -0
  155. package/src/pending-tasks-compat.ts +27 -0
  156. package/src/providers.ts +170 -0
  157. package/src/query-options.ts +292 -0
  158. package/src/signal-proxy.ts +46 -0
  159. package/src/types.ts +429 -0
  160. package/create-base-query.d.ts +0 -164
  161. package/create-base-query.mjs +0 -99
  162. package/create-base-query.mjs.map +0 -1
  163. package/devtools/index.d.ts +0 -2
  164. package/devtools/index.mjs +0 -5
  165. package/devtools/index.mjs.map +0 -1
  166. package/devtools/production/index.d.ts +0 -1
  167. package/devtools/stub.d.ts +0 -2
  168. package/devtools/stub.mjs +0 -8
  169. package/devtools/stub.mjs.map +0 -1
  170. package/devtools/types.d.ts +0 -103
  171. package/devtools/with-devtools.mjs +0 -106
  172. package/devtools/with-devtools.mjs.map +0 -1
  173. package/devtools-panel/index.d.ts +0 -2
  174. package/devtools-panel/index.mjs +0 -5
  175. package/devtools-panel/index.mjs.map +0 -1
  176. package/devtools-panel/inject-devtools-panel.mjs +0 -72
  177. package/devtools-panel/inject-devtools-panel.mjs.map +0 -1
  178. package/devtools-panel/production/index.d.ts +0 -1
  179. package/devtools-panel/stub.d.ts +0 -2
  180. package/devtools-panel/stub.mjs +0 -8
  181. package/devtools-panel/stub.mjs.map +0 -1
  182. package/devtools-panel/types.d.ts +0 -48
  183. package/index.d.ts +0 -25
  184. package/index.mjs.map +0 -1
  185. package/infinite-query-options.mjs +0 -7
  186. package/infinite-query-options.mjs.map +0 -1
  187. package/inject-infinite-query.mjs +0 -18
  188. package/inject-infinite-query.mjs.map +0 -1
  189. package/inject-is-fetching.mjs +0 -31
  190. package/inject-is-fetching.mjs.map +0 -1
  191. package/inject-is-mutating.mjs +0 -31
  192. package/inject-is-mutating.mjs.map +0 -1
  193. package/inject-is-restoring.mjs +0 -21
  194. package/inject-is-restoring.mjs.map +0 -1
  195. package/inject-mutation-state.mjs +0 -51
  196. package/inject-mutation-state.mjs.map +0 -1
  197. package/inject-mutation.mjs +0 -94
  198. package/inject-mutation.mjs.map +0 -1
  199. package/inject-queries-experimental/index.d.ts +0 -1
  200. package/inject-queries-experimental/index.mjs +0 -5
  201. package/inject-queries-experimental/index.mjs.map +0 -1
  202. package/inject-queries.mjs +0 -82
  203. package/inject-queries.mjs.map +0 -1
  204. package/inject-query-client.mjs +0 -9
  205. package/inject-query-client.mjs.map +0 -1
  206. package/inject-query.mjs +0 -14
  207. package/inject-query.mjs.map +0 -1
  208. package/mutation-options.mjs +0 -7
  209. package/mutation-options.mjs.map +0 -1
  210. package/pending-tasks-compat.d.ts +0 -7
  211. package/pending-tasks-compat.mjs +0 -19
  212. package/pending-tasks-compat.mjs.map +0 -1
  213. package/providers.mjs +0 -32
  214. package/providers.mjs.map +0 -1
  215. package/query-options.mjs +0 -7
  216. package/query-options.mjs.map +0 -1
  217. package/signal-proxy.mjs +0 -29
  218. package/signal-proxy.mjs.map +0 -1
@@ -0,0 +1,700 @@
1
+ import {
2
+ ApplicationRef,
3
+ Component,
4
+ Injector,
5
+ computed,
6
+ input,
7
+ provideZonelessChangeDetection,
8
+ signal,
9
+ } from '@angular/core'
10
+ import { TestBed } from '@angular/core/testing'
11
+ import { HttpClient, provideHttpClient } from '@angular/common/http'
12
+ import {
13
+ HttpTestingController,
14
+ provideHttpClientTesting,
15
+ } from '@angular/common/http/testing'
16
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
17
+ import { render } from '@testing-library/angular'
18
+ import { queryKey, sleep } from '@tanstack/query-test-utils'
19
+ import { lastValueFrom } from 'rxjs'
20
+ import {
21
+ QueryCache,
22
+ QueryClient,
23
+ injectQuery,
24
+ provideIsRestoring,
25
+ provideTanStackQuery,
26
+ } from '..'
27
+ import { setSignalInputs } from './test-utils'
28
+
29
+ describe('injectQuery', () => {
30
+ let queryCache: QueryCache
31
+ let queryClient: QueryClient
32
+
33
+ beforeEach(() => {
34
+ vi.useFakeTimers()
35
+ queryCache = new QueryCache()
36
+ queryClient = new QueryClient({ queryCache })
37
+ TestBed.configureTestingModule({
38
+ providers: [
39
+ provideZonelessChangeDetection(),
40
+ provideTanStackQuery(queryClient),
41
+ ],
42
+ })
43
+ })
44
+
45
+ afterEach(() => {
46
+ vi.useRealTimers()
47
+ })
48
+
49
+ it('should return pending status initially', () => {
50
+ const key = queryKey()
51
+ const query = TestBed.runInInjectionContext(() => {
52
+ return injectQuery(() => ({
53
+ queryKey: key,
54
+ queryFn: () => sleep(10).then(() => 'Some data'),
55
+ }))
56
+ })
57
+
58
+ expect(query.status()).toBe('pending')
59
+ expect(query.isPending()).toBe(true)
60
+ expect(query.isFetching()).toBe(true)
61
+ expect(query.isStale()).toBe(true)
62
+ expect(query.isFetched()).toBe(false)
63
+ })
64
+
65
+ it('should resolve to success and update signal: injectQuery()', async () => {
66
+ const key = queryKey()
67
+
68
+ @Component({
69
+ template: `
70
+ <div>status: {{ query.status() }}</div>
71
+ <div>data: {{ query.data() ?? 'none' }}</div>
72
+ <div>isPending: {{ query.isPending() }}</div>
73
+ <div>isFetching: {{ query.isFetching() }}</div>
74
+ <div>isFetched: {{ query.isFetched() }}</div>
75
+ <div>isSuccess: {{ query.isSuccess() }}</div>
76
+ `,
77
+ })
78
+ class Page {
79
+ readonly query = injectQuery(() => ({
80
+ queryKey: key,
81
+ queryFn: () => sleep(10).then(() => 'result2'),
82
+ }))
83
+ }
84
+
85
+ const rendered = await render(Page)
86
+
87
+ await vi.advanceTimersByTimeAsync(11)
88
+ rendered.fixture.detectChanges()
89
+
90
+ expect(rendered.getByText('status: success')).toBeInTheDocument()
91
+ expect(rendered.getByText('data: result2')).toBeInTheDocument()
92
+ expect(rendered.getByText('isPending: false')).toBeInTheDocument()
93
+ expect(rendered.getByText('isFetching: false')).toBeInTheDocument()
94
+ expect(rendered.getByText('isFetched: true')).toBeInTheDocument()
95
+ expect(rendered.getByText('isSuccess: true')).toBeInTheDocument()
96
+ })
97
+
98
+ it('should reject and update signal', async () => {
99
+ const key = queryKey()
100
+
101
+ @Component({
102
+ template: `
103
+ <div>status: {{ query.status() }}</div>
104
+ <div>data: {{ query.data() ?? 'none' }}</div>
105
+ <div>error: {{ query.error()?.message ?? 'none' }}</div>
106
+ <div>isPending: {{ query.isPending() }}</div>
107
+ <div>isFetching: {{ query.isFetching() }}</div>
108
+ <div>isError: {{ query.isError() }}</div>
109
+ <div>failureCount: {{ query.failureCount() }}</div>
110
+ <div>failureReason: {{ query.failureReason()?.message ?? 'none' }}</div>
111
+ `,
112
+ })
113
+ class Page {
114
+ readonly query = injectQuery(() => ({
115
+ retry: false,
116
+ queryKey: key,
117
+ queryFn: () =>
118
+ sleep(10).then(() => Promise.reject(new Error('Some error'))),
119
+ }))
120
+ }
121
+
122
+ const rendered = await render(Page)
123
+
124
+ await vi.advanceTimersByTimeAsync(11)
125
+ rendered.fixture.detectChanges()
126
+
127
+ expect(rendered.getByText('status: error')).toBeInTheDocument()
128
+ expect(rendered.getByText('data: none')).toBeInTheDocument()
129
+ expect(rendered.getByText('error: Some error')).toBeInTheDocument()
130
+ expect(rendered.getByText('isPending: false')).toBeInTheDocument()
131
+ expect(rendered.getByText('isFetching: false')).toBeInTheDocument()
132
+ expect(rendered.getByText('isError: true')).toBeInTheDocument()
133
+ expect(rendered.getByText('failureCount: 1')).toBeInTheDocument()
134
+ expect(rendered.getByText('failureReason: Some error')).toBeInTheDocument()
135
+ })
136
+
137
+ it('should be able to select a part of the data with select', async () => {
138
+ const key = queryKey()
139
+
140
+ @Component({
141
+ template: `<div>data: {{ query.data() ?? 'none' }}</div>`,
142
+ })
143
+ class Page {
144
+ readonly query = injectQuery<{ name: string }, Error, string>(() => ({
145
+ queryKey: key,
146
+ queryFn: () => sleep(10).then(() => ({ name: 'test' })),
147
+ select: (data) => data.name,
148
+ }))
149
+ }
150
+
151
+ const rendered = await render(Page)
152
+
153
+ expect(rendered.getByText('data: none')).toBeInTheDocument()
154
+
155
+ await vi.advanceTimersByTimeAsync(11)
156
+ rendered.fixture.detectChanges()
157
+
158
+ expect(rendered.getByText('data: test')).toBeInTheDocument()
159
+ })
160
+
161
+ it('should show placeholderData until queryFn resolves and then expose real data', async () => {
162
+ const key = queryKey()
163
+
164
+ @Component({
165
+ template: `
166
+ <div>data: {{ query.data() }}</div>
167
+ <div>isPlaceholderData: {{ query.isPlaceholderData() }}</div>
168
+ <div>isSuccess: {{ query.isSuccess() }}</div>
169
+ `,
170
+ })
171
+ class Page {
172
+ readonly query = injectQuery(() => ({
173
+ queryKey: key,
174
+ queryFn: () => sleep(10).then(() => 'real-data'),
175
+ placeholderData: 'placeholder',
176
+ }))
177
+ }
178
+
179
+ const rendered = await render(Page)
180
+
181
+ expect(rendered.getByText('data: placeholder')).toBeInTheDocument()
182
+ expect(rendered.getByText('isPlaceholderData: true')).toBeInTheDocument()
183
+ expect(rendered.getByText('isSuccess: true')).toBeInTheDocument()
184
+
185
+ await vi.advanceTimersByTimeAsync(11)
186
+ rendered.fixture.detectChanges()
187
+
188
+ expect(rendered.getByText('data: real-data')).toBeInTheDocument()
189
+ expect(rendered.getByText('isPlaceholderData: false')).toBeInTheDocument()
190
+ expect(rendered.getByText('isSuccess: true')).toBeInTheDocument()
191
+ })
192
+
193
+ it('should update query on options contained signal change', async () => {
194
+ const key1 = queryKey()
195
+ const key2 = queryKey()
196
+ const key = signal(key1)
197
+ const queryFn = vi.fn(() => sleep(10).then(() => 'Some data'))
198
+
199
+ const query = TestBed.runInInjectionContext(() => {
200
+ return injectQuery(() => ({
201
+ queryKey: key(),
202
+ queryFn,
203
+ }))
204
+ })
205
+
206
+ await vi.advanceTimersByTimeAsync(0)
207
+ expect(queryFn).toHaveBeenCalledTimes(1)
208
+
209
+ await vi.advanceTimersByTimeAsync(10)
210
+ expect(query.status()).toBe('success')
211
+
212
+ key.set(key2)
213
+ TestBed.tick()
214
+
215
+ expect(queryFn).toHaveBeenCalledTimes(2)
216
+ // should call queryFn with context containing the new queryKey
217
+ expect(queryFn).toHaveBeenNthCalledWith(2, {
218
+ client: queryClient,
219
+ meta: undefined,
220
+ queryKey: key2,
221
+ signal: expect.anything(),
222
+ })
223
+ })
224
+
225
+ it('should only run query once enabled signal is set to true', async () => {
226
+ const key = queryKey()
227
+ const queryFn = vi.fn(() => sleep(10).then(() => 'Some data'))
228
+ const enabled = signal(false)
229
+
230
+ const query = TestBed.runInInjectionContext(() => {
231
+ return injectQuery(() => ({
232
+ queryKey: key,
233
+ queryFn,
234
+ enabled: enabled(),
235
+ }))
236
+ })
237
+
238
+ expect(queryFn).not.toHaveBeenCalled()
239
+ expect(query.status()).toBe('pending')
240
+
241
+ enabled.set(true)
242
+
243
+ await vi.advanceTimersByTimeAsync(10)
244
+ expect(queryFn).toHaveBeenCalledTimes(1)
245
+ expect(query.status()).toBe('success')
246
+ })
247
+
248
+ it('should not fetch while the enabled signal is false, and fetch again once it is truthy', async () => {
249
+ const key = queryKey()
250
+ const queryFn = vi.fn(() => sleep(10).then(() => 'Some data'))
251
+ const filter = signal('a')
252
+
253
+ const query = TestBed.runInInjectionContext(() => {
254
+ return injectQuery(() => ({
255
+ queryKey: [...key, filter()],
256
+ queryFn,
257
+ enabled: !!filter(),
258
+ }))
259
+ })
260
+
261
+ await vi.advanceTimersByTimeAsync(10)
262
+ expect(queryFn).toHaveBeenCalledTimes(1)
263
+ expect(query.status()).toBe('success')
264
+
265
+ filter.set('')
266
+
267
+ await vi.advanceTimersByTimeAsync(10)
268
+ expect(queryFn).toHaveBeenCalledTimes(1)
269
+ expect(query.isFetching()).toBe(false)
270
+
271
+ filter.set('b')
272
+
273
+ await vi.advanceTimersByTimeAsync(10)
274
+ expect(queryFn).toHaveBeenCalledTimes(2)
275
+ })
276
+
277
+ it('should properly execute dependent queries', async () => {
278
+ const key1 = queryKey()
279
+ const key2 = queryKey()
280
+
281
+ const query1 = TestBed.runInInjectionContext(() => {
282
+ return injectQuery(() => ({
283
+ queryKey: key1,
284
+ queryFn: () => sleep(10).then(() => 'Some data'),
285
+ }))
286
+ })
287
+
288
+ const dependentQueryFn = vi
289
+ .fn()
290
+ .mockImplementation(() => sleep(1000).then(() => 'Some data'))
291
+
292
+ const query2 = TestBed.runInInjectionContext(() => {
293
+ return injectQuery(
294
+ computed(() => ({
295
+ queryKey: key2,
296
+ queryFn: dependentQueryFn,
297
+ enabled: !!query1.data(),
298
+ })),
299
+ )
300
+ })
301
+
302
+ expect(query1.data()).toStrictEqual(undefined)
303
+ expect(query2.fetchStatus()).toStrictEqual('idle')
304
+ expect(dependentQueryFn).not.toHaveBeenCalled()
305
+
306
+ await vi.advanceTimersByTimeAsync(11)
307
+
308
+ expect(query1.data()).toStrictEqual('Some data')
309
+ expect(query2.fetchStatus()).toStrictEqual('fetching')
310
+
311
+ await vi.advanceTimersByTimeAsync(1002)
312
+
313
+ expect(query2.fetchStatus()).toStrictEqual('idle')
314
+ expect(query2.status()).toStrictEqual('success')
315
+ expect(dependentQueryFn).toHaveBeenCalledTimes(1)
316
+ expect(dependentQueryFn).toHaveBeenCalledWith(
317
+ expect.objectContaining({ queryKey: key2 }),
318
+ )
319
+ })
320
+
321
+ it('should use the current value for the queryKey when refetch is called', async () => {
322
+ const key = queryKey()
323
+ const queryFn = vi.fn(() => sleep(10).then(() => 'Some data'))
324
+ const keySignal = signal('key11')
325
+
326
+ const query = TestBed.runInInjectionContext(() => {
327
+ return injectQuery(() => ({
328
+ queryKey: [...key, keySignal()],
329
+ queryFn,
330
+ enabled: false,
331
+ }))
332
+ })
333
+
334
+ expect(queryFn).not.toHaveBeenCalled()
335
+
336
+ void query.refetch()
337
+ await vi.advanceTimersByTimeAsync(10)
338
+
339
+ expect(queryFn).toHaveBeenCalledTimes(1)
340
+ expect(queryFn).toHaveBeenNthCalledWith(
341
+ 1,
342
+ expect.objectContaining({
343
+ queryKey: [...key, 'key11'],
344
+ }),
345
+ )
346
+
347
+ keySignal.set('key12')
348
+
349
+ void query.refetch()
350
+ await vi.advanceTimersByTimeAsync(10)
351
+
352
+ expect(queryFn).toHaveBeenCalledTimes(2)
353
+ expect(queryFn).toHaveBeenNthCalledWith(
354
+ 2,
355
+ expect.objectContaining({
356
+ queryKey: [...key, 'key12'],
357
+ }),
358
+ )
359
+ expect(query.data()).toBe('Some data')
360
+ })
361
+
362
+ it('should keep initialData visible alongside the error when a refetch fails', async () => {
363
+ const key = queryKey()
364
+
365
+ @Component({
366
+ template: `
367
+ <div>data: {{ query.data() }}</div>
368
+ <div>isError: {{ query.isError() }}</div>
369
+ `,
370
+ })
371
+ class Page {
372
+ readonly query = injectQuery(() => ({
373
+ queryKey: key,
374
+ queryFn: () =>
375
+ sleep(10).then(() => Promise.reject(new Error('Some error'))),
376
+ initialData: 'initial',
377
+ retry: false,
378
+ }))
379
+ }
380
+
381
+ const rendered = await render(Page)
382
+
383
+ expect(rendered.getByText('data: initial')).toBeInTheDocument()
384
+ expect(rendered.getByText('isError: false')).toBeInTheDocument()
385
+
386
+ await vi.advanceTimersByTimeAsync(11)
387
+ rendered.fixture.detectChanges()
388
+ expect(rendered.getByText('data: initial')).toBeInTheDocument()
389
+ expect(rendered.getByText('isError: true')).toBeInTheDocument()
390
+ })
391
+
392
+ describe('throwOnError', () => {
393
+ it('should evaluate throwOnError when query is expected to throw', async () => {
394
+ const key = queryKey()
395
+ const throwOnError = vi.fn()
396
+ TestBed.runInInjectionContext(() => {
397
+ return injectQuery(() => ({
398
+ queryKey: key,
399
+ queryFn: () =>
400
+ sleep(10).then(() => Promise.reject(new Error('Some error'))),
401
+ retry: false,
402
+ throwOnError,
403
+ }))
404
+ })
405
+
406
+ await vi.advanceTimersByTimeAsync(11)
407
+ expect(throwOnError).toHaveBeenCalledTimes(1)
408
+ expect(throwOnError).toHaveBeenCalledWith(
409
+ Error('Some error'),
410
+ expect.objectContaining({
411
+ state: expect.objectContaining({ status: 'error' }),
412
+ }),
413
+ )
414
+ })
415
+
416
+ it('should throw when throwOnError is true', async () => {
417
+ const key = queryKey()
418
+ TestBed.runInInjectionContext(() => {
419
+ return injectQuery(() => ({
420
+ queryKey: key,
421
+ queryFn: () =>
422
+ sleep(0).then(() => Promise.reject(new Error('Some error'))),
423
+ throwOnError: true,
424
+ }))
425
+ })
426
+
427
+ await expect(vi.runAllTimersAsync()).rejects.toThrow('Some error')
428
+ })
429
+
430
+ it('should throw when throwOnError function returns true', async () => {
431
+ const key = queryKey()
432
+ TestBed.runInInjectionContext(() => {
433
+ return injectQuery(() => ({
434
+ queryKey: key,
435
+ queryFn: () =>
436
+ sleep(0).then(() => Promise.reject(new Error('Some error'))),
437
+ throwOnError: () => true,
438
+ }))
439
+ })
440
+
441
+ await expect(vi.runAllTimersAsync()).rejects.toThrow('Some error')
442
+ })
443
+ })
444
+
445
+ it('should render with required signal inputs', async () => {
446
+ @Component({
447
+ selector: 'app-fake',
448
+ template: `{{ query.data() }}`,
449
+ })
450
+ class FakeComponent {
451
+ name = input.required<string>()
452
+
453
+ query = injectQuery(() => ({
454
+ queryKey: ['fake', this.name()],
455
+ queryFn: () => this.name(),
456
+ }))
457
+ }
458
+
459
+ const fixture = TestBed.createComponent(FakeComponent)
460
+ setSignalInputs(fixture.componentInstance, {
461
+ name: 'signal-input-required-test',
462
+ })
463
+
464
+ fixture.detectChanges()
465
+ await vi.advanceTimersByTimeAsync(0)
466
+
467
+ expect(fixture.componentInstance.query.data()).toEqual(
468
+ 'signal-input-required-test',
469
+ )
470
+ })
471
+
472
+ describe('isRestoring', () => {
473
+ it('should not fetch for the duration of the restoring period when isRestoring is true', async () => {
474
+ const key = queryKey()
475
+ const queryFn = vi
476
+ .fn()
477
+ .mockImplementation(() => sleep(10).then(() => 'data'))
478
+
479
+ TestBed.configureTestingModule({
480
+ providers: [provideIsRestoring(signal(true).asReadonly())],
481
+ })
482
+
483
+ const query = TestBed.runInInjectionContext(() =>
484
+ injectQuery(() => ({
485
+ queryKey: key,
486
+ queryFn,
487
+ })),
488
+ )
489
+
490
+ await vi.advanceTimersByTimeAsync(0)
491
+ expect(query.status()).toBe('pending')
492
+ expect(query.fetchStatus()).toBe('idle')
493
+ expect(query.data()).toBeUndefined()
494
+ expect(queryFn).toHaveBeenCalledTimes(0)
495
+
496
+ await vi.advanceTimersByTimeAsync(10)
497
+ expect(query.status()).toBe('pending')
498
+ expect(query.fetchStatus()).toBe('idle')
499
+ expect(query.data()).toBeUndefined()
500
+ expect(queryFn).toHaveBeenCalledTimes(0)
501
+ })
502
+ })
503
+
504
+ describe('injection context', () => {
505
+ it('should throw NG0203 with descriptive error outside injection context', () => {
506
+ const key = queryKey()
507
+ expect(() => {
508
+ injectQuery(() => ({
509
+ queryKey: key,
510
+ queryFn: () => sleep(0).then(() => 'Some data'),
511
+ }))
512
+ }).toThrow(/NG0203(.*?)injectQuery/)
513
+ })
514
+
515
+ it('should be usable outside injection context when passing an injector', () => {
516
+ const key = queryKey()
517
+ const query = injectQuery(
518
+ () => ({
519
+ queryKey: key,
520
+ queryFn: () => sleep(0).then(() => 'Some data'),
521
+ }),
522
+ {
523
+ injector: TestBed.inject(Injector),
524
+ },
525
+ )
526
+
527
+ expect(query.status()).toBe('pending')
528
+ })
529
+
530
+ it('should complete queries before whenStable() resolves', async () => {
531
+ const key = queryKey()
532
+ const app = TestBed.inject(ApplicationRef)
533
+
534
+ const query = TestBed.runInInjectionContext(() =>
535
+ injectQuery(() => ({
536
+ queryKey: key,
537
+ queryFn: () => sleep(50).then(() => 'test data'),
538
+ })),
539
+ )
540
+
541
+ expect(query.status()).toBe('pending')
542
+ expect(query.data()).toBeUndefined()
543
+
544
+ const stablePromise = app.whenStable()
545
+ await vi.advanceTimersByTimeAsync(60)
546
+ await stablePromise
547
+
548
+ expect(query.status()).toBe('success')
549
+ expect(query.data()).toBe('test data')
550
+ })
551
+
552
+ it('should complete HttpClient-based queries before whenStable() resolves', async () => {
553
+ TestBed.configureTestingModule({
554
+ providers: [provideHttpClient(), provideHttpClientTesting()],
555
+ })
556
+
557
+ const app = TestBed.inject(ApplicationRef)
558
+ const httpClient = TestBed.inject(HttpClient)
559
+ const httpTestingController = TestBed.inject(HttpTestingController)
560
+
561
+ // Create a query using HttpClient
562
+ const key = queryKey()
563
+ const query = TestBed.runInInjectionContext(() =>
564
+ injectQuery(() => ({
565
+ queryKey: key,
566
+ queryFn: () =>
567
+ lastValueFrom(httpClient.get<{ message: string }>('/api/test')),
568
+ })),
569
+ )
570
+
571
+ // Schedule the HTTP response
572
+ setTimeout(() => {
573
+ const req = httpTestingController.expectOne('/api/test')
574
+ req.flush({ message: 'http test data' })
575
+ }, 10)
576
+
577
+ // Initial state
578
+ expect(query.status()).toBe('pending')
579
+
580
+ // Advance timers and wait for Angular to be "stable"
581
+ const stablePromise = app.whenStable()
582
+ await vi.advanceTimersByTimeAsync(20)
583
+ await stablePromise
584
+
585
+ // Query should be complete after whenStable() thanks to PendingTasks integration
586
+ expect(query.status()).toBe('success')
587
+ expect(query.data()).toEqual({ message: 'http test data' })
588
+
589
+ httpTestingController.verify()
590
+ })
591
+
592
+ it('should handle synchronous queryFn with staleTime', async () => {
593
+ const app = TestBed.inject(ApplicationRef)
594
+ let callCount = 0
595
+
596
+ const key = queryKey()
597
+ const query = TestBed.runInInjectionContext(() =>
598
+ injectQuery(() => ({
599
+ queryKey: key,
600
+ staleTime: 1000,
601
+ queryFn: () => {
602
+ callCount++
603
+ return `sync-data-${callCount}`
604
+ },
605
+ })),
606
+ )
607
+
608
+ // Synchronize pending effects
609
+ TestBed.tick()
610
+
611
+ const stablePromise = app.whenStable()
612
+ await stablePromise
613
+
614
+ expect(query.status()).toBe('success')
615
+ expect(query.data()).toBe('sync-data-1')
616
+ expect(callCount).toBe(1)
617
+
618
+ await query.refetch()
619
+ await Promise.resolve()
620
+ await vi.runAllTimersAsync()
621
+ await app.whenStable()
622
+
623
+ expect(query.status()).toBe('success')
624
+ expect(query.data()).toBe('sync-data-2')
625
+ expect(callCount).toBe(2)
626
+ })
627
+
628
+ it('should handle enabled/disabled transitions with synchronous queryFn', async () => {
629
+ const app = TestBed.inject(ApplicationRef)
630
+ const enabledSignal = signal(false)
631
+ let callCount = 0
632
+
633
+ const key = queryKey()
634
+ const query = TestBed.runInInjectionContext(() =>
635
+ injectQuery(() => ({
636
+ queryKey: key,
637
+ enabled: enabledSignal(),
638
+ queryFn: () => {
639
+ callCount++
640
+ return `sync-data-${callCount}`
641
+ },
642
+ })),
643
+ )
644
+
645
+ // Initially disabled
646
+ TestBed.tick()
647
+ await app.whenStable()
648
+ expect(query.status()).toBe('pending')
649
+ expect(query.data()).toBeUndefined()
650
+ expect(callCount).toBe(0)
651
+
652
+ // Enable the query
653
+ enabledSignal.set(true)
654
+ TestBed.tick()
655
+
656
+ await app.whenStable()
657
+ expect(query.status()).toBe('success')
658
+ expect(query.data()).toBe('sync-data-1')
659
+ expect(callCount).toBe(1)
660
+ })
661
+
662
+ it('should handle query invalidation with synchronous data', async () => {
663
+ const app = TestBed.inject(ApplicationRef)
664
+ const testKey = queryKey()
665
+ let callCount = 0
666
+
667
+ const query = TestBed.runInInjectionContext(() =>
668
+ injectQuery(() => ({
669
+ queryKey: testKey,
670
+ queryFn: () => {
671
+ callCount++
672
+ return `sync-data-${callCount}`
673
+ },
674
+ })),
675
+ )
676
+
677
+ // Synchronize pending effects
678
+ TestBed.tick()
679
+
680
+ await app.whenStable()
681
+ expect(query.status()).toBe('success')
682
+ expect(query.data()).toBe('sync-data-1')
683
+ expect(callCount).toBe(1)
684
+
685
+ // Invalidate the query
686
+ queryClient.invalidateQueries({ queryKey: testKey })
687
+ TestBed.tick()
688
+
689
+ // Wait for the invalidation to trigger a refetch
690
+ await Promise.resolve()
691
+ await vi.advanceTimersByTimeAsync(10)
692
+ TestBed.tick()
693
+
694
+ await app.whenStable()
695
+ expect(query.status()).toBe('success')
696
+ expect(query.data()).toBe('sync-data-2')
697
+ expect(callCount).toBe(2)
698
+ })
699
+ })
700
+ })