@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.
- package/dist/create-base-query.d.mts +168 -0
- package/dist/create-base-query.d.mts.map +1 -0
- package/dist/create-base-query.mjs +91 -0
- package/dist/create-base-query.mjs.map +1 -0
- package/dist/devtools/index.d.mts +3 -0
- package/dist/devtools/index.mjs +2 -0
- package/dist/devtools/production/index.d.mts +4 -0
- package/dist/devtools/production/index.mjs +2 -0
- package/dist/devtools/stub.d.mts +5 -0
- package/dist/devtools/stub.d.mts.map +1 -0
- package/dist/devtools/stub.mjs +9 -0
- package/dist/devtools/stub.mjs.map +1 -0
- package/dist/devtools/types.d.mts +106 -0
- package/dist/devtools/types.d.mts.map +1 -0
- package/dist/devtools/types.mjs +1 -0
- package/{devtools/with-devtools.d.ts → dist/devtools/with-devtools.d.mts} +4 -1
- package/dist/devtools/with-devtools.d.mts.map +1 -0
- package/dist/devtools/with-devtools.mjs +110 -0
- package/dist/devtools/with-devtools.mjs.map +1 -0
- package/dist/devtools-panel/index.d.mts +3 -0
- package/dist/devtools-panel/index.mjs +2 -0
- package/{devtools-panel/inject-devtools-panel.d.ts → dist/devtools-panel/inject-devtools-panel.d.mts} +4 -1
- package/dist/devtools-panel/inject-devtools-panel.d.mts.map +1 -0
- package/dist/devtools-panel/inject-devtools-panel.mjs +67 -0
- package/dist/devtools-panel/inject-devtools-panel.mjs.map +1 -0
- package/dist/devtools-panel/production/index.d.mts +4 -0
- package/dist/devtools-panel/production/index.mjs +2 -0
- package/dist/devtools-panel/stub.d.mts +5 -0
- package/dist/devtools-panel/stub.d.mts.map +1 -0
- package/dist/devtools-panel/stub.mjs +7 -0
- package/dist/devtools-panel/stub.mjs.map +1 -0
- package/dist/devtools-panel/types.d.mts +51 -0
- package/dist/devtools-panel/types.d.mts.map +1 -0
- package/dist/devtools-panel/types.mjs +1 -0
- package/dist/index.d.mts +16 -0
- package/{index.mjs → dist/index.mjs} +8 -21
- package/dist/index.mjs.map +1 -0
- package/{infinite-query-options.d.ts → dist/infinite-query-options.d.mts} +28 -25
- package/dist/infinite-query-options.d.mts.map +1 -0
- package/dist/infinite-query-options.mjs +8 -0
- package/dist/infinite-query-options.mjs.map +1 -0
- package/{inject-infinite-query.d.ts → dist/inject-infinite-query.d.mts} +13 -10
- package/dist/inject-infinite-query.d.mts.map +1 -0
- package/dist/inject-infinite-query.mjs +13 -0
- package/dist/inject-infinite-query.mjs.map +1 -0
- package/{inject-is-fetching.d.ts → dist/inject-is-fetching.d.mts} +11 -8
- package/dist/inject-is-fetching.d.mts.map +1 -0
- package/dist/inject-is-fetching.mjs +69 -0
- package/dist/inject-is-fetching.mjs.map +1 -0
- package/{inject-is-mutating.d.ts → dist/inject-is-mutating.d.mts} +11 -8
- package/dist/inject-is-mutating.d.mts.map +1 -0
- package/dist/inject-is-mutating.mjs +52 -0
- package/dist/inject-is-mutating.mjs.map +1 -0
- package/{inject-is-restoring.d.ts → dist/inject-is-restoring.d.mts} +10 -8
- package/dist/inject-is-restoring.d.mts.map +1 -0
- package/dist/inject-is-restoring.mjs +35 -0
- package/dist/inject-is-restoring.mjs.map +1 -0
- package/{inject-mutation-state.d.ts → dist/inject-mutation-state.d.mts} +13 -11
- package/dist/inject-mutation-state.d.mts.map +1 -0
- package/dist/inject-mutation-state.mjs +103 -0
- package/dist/inject-mutation-state.mjs.map +1 -0
- package/{inject-mutation.d.ts → dist/inject-mutation.d.mts} +12 -9
- package/dist/inject-mutation.d.mts.map +1 -0
- package/dist/inject-mutation.mjs +226 -0
- package/dist/inject-mutation.mjs.map +1 -0
- package/dist/inject-queries-experimental/index.d.mts +2 -0
- package/dist/inject-queries-experimental/index.mjs +2 -0
- package/{inject-queries.d.ts → dist/inject-queries.d.mts} +33 -53
- package/dist/inject-queries.d.mts.map +1 -0
- package/dist/inject-queries.mjs +203 -0
- package/dist/inject-queries.mjs.map +1 -0
- package/{inject-query-client.d.ts → dist/inject-query-client.d.mts} +6 -3
- package/dist/inject-query-client.d.mts.map +1 -0
- package/dist/inject-query-client.mjs +23 -0
- package/dist/inject-query-client.mjs.map +1 -0
- package/{inject-query.d.ts → dist/inject-query.d.mts} +13 -10
- package/dist/inject-query.d.mts.map +1 -0
- package/dist/inject-query.mjs +12 -0
- package/dist/inject-query.mjs.map +1 -0
- package/{mutation-options.d.ts → dist/mutation-options.d.mts} +5 -2
- package/dist/mutation-options.d.mts.map +1 -0
- package/dist/mutation-options.mjs +8 -0
- package/dist/mutation-options.mjs.map +1 -0
- package/dist/pending-tasks-compat.d.mts +9 -0
- package/dist/pending-tasks-compat.d.mts.map +1 -0
- package/dist/pending-tasks-compat.mjs +13 -0
- package/dist/pending-tasks-compat.mjs.map +1 -0
- package/{providers.d.ts → dist/providers.d.mts} +7 -5
- package/dist/providers.d.mts.map +1 -0
- package/dist/providers.mjs +124 -0
- package/dist/providers.mjs.map +1 -0
- package/{query-options.d.ts → dist/query-options.d.mts} +34 -31
- package/dist/query-options.d.mts.map +1 -0
- package/dist/query-options.mjs +8 -0
- package/dist/query-options.mjs.map +1 -0
- package/{signal-proxy.d.ts → dist/signal-proxy.d.mts} +5 -4
- package/dist/signal-proxy.d.mts.map +1 -0
- package/dist/signal-proxy.mjs +35 -0
- package/dist/signal-proxy.mjs.map +1 -0
- package/{types.d.ts → dist/types.d.mts} +24 -26
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +6 -0
- package/dist/types.mjs.map +1 -0
- package/package.json +50 -20
- package/src/__tests__/infinite-query-options.test-d.ts +314 -0
- package/src/__tests__/infinite-query-options.test.ts +19 -0
- package/src/__tests__/inject-devtools-panel.test.ts +196 -0
- package/src/__tests__/inject-infinite-query.test-d.ts +66 -0
- package/src/__tests__/inject-infinite-query.test.ts +227 -0
- package/src/__tests__/inject-is-fetching.test.ts +108 -0
- package/src/__tests__/inject-is-mutating.test.ts +113 -0
- package/src/__tests__/inject-is-restoring.test.ts +93 -0
- package/src/__tests__/inject-mutation-state.test-d.ts +22 -0
- package/src/__tests__/inject-mutation-state.test.ts +198 -0
- package/src/__tests__/inject-mutation.test-d.ts +158 -0
- package/src/__tests__/inject-mutation.test.ts +1161 -0
- package/src/__tests__/inject-queries.test-d.ts +196 -0
- package/src/__tests__/inject-queries.test.ts +240 -0
- package/src/__tests__/inject-query.test-d.ts +357 -0
- package/src/__tests__/inject-query.test.ts +700 -0
- package/src/__tests__/mutation-options.test-d.ts +234 -0
- package/src/__tests__/mutation-options.test.ts +344 -0
- package/src/__tests__/pending-tasks.test.ts +644 -0
- package/src/__tests__/provide-query-client.test.ts +38 -0
- package/src/__tests__/provide-tanstack-query.test.ts +38 -0
- package/src/__tests__/query-options.test-d.ts +262 -0
- package/src/__tests__/query-options.test.ts +14 -0
- package/src/__tests__/signal-proxy.test.ts +27 -0
- package/src/__tests__/test-utils.ts +77 -0
- package/src/__tests__/with-devtools.test.ts +674 -0
- package/src/create-base-query.ts +176 -0
- package/src/devtools/index.ts +8 -0
- package/src/devtools/production/index.ts +1 -0
- package/src/devtools/stub.ts +7 -0
- package/src/devtools/types.ts +116 -0
- package/src/devtools/with-devtools.ts +182 -0
- package/src/devtools-panel/index.ts +8 -0
- package/src/devtools-panel/inject-devtools-panel.ts +110 -0
- package/src/devtools-panel/production/index.ts +1 -0
- package/src/devtools-panel/stub.ts +7 -0
- package/src/devtools-panel/types.ts +57 -0
- package/src/index.ts +60 -0
- package/src/infinite-query-options.ts +354 -0
- package/src/inject-infinite-query.ts +296 -0
- package/src/inject-is-fetching.ts +97 -0
- package/src/inject-is-mutating.ts +80 -0
- package/src/inject-is-restoring.ts +53 -0
- package/src/inject-mutation-state.ts +167 -0
- package/src/inject-mutation.ts +328 -0
- package/src/inject-queries-experimental/index.ts +1 -0
- package/src/inject-queries.ts +480 -0
- package/src/inject-query-client.ts +22 -0
- package/src/inject-query.ts +208 -0
- package/src/mutation-options.ts +121 -0
- package/src/pending-tasks-compat.ts +27 -0
- package/src/providers.ts +170 -0
- package/src/query-options.ts +292 -0
- package/src/signal-proxy.ts +46 -0
- package/src/types.ts +429 -0
- package/create-base-query.d.ts +0 -164
- package/create-base-query.mjs +0 -99
- package/create-base-query.mjs.map +0 -1
- package/devtools/index.d.ts +0 -2
- package/devtools/index.mjs +0 -5
- package/devtools/index.mjs.map +0 -1
- package/devtools/production/index.d.ts +0 -1
- package/devtools/stub.d.ts +0 -2
- package/devtools/stub.mjs +0 -8
- package/devtools/stub.mjs.map +0 -1
- package/devtools/types.d.ts +0 -103
- package/devtools/with-devtools.mjs +0 -106
- package/devtools/with-devtools.mjs.map +0 -1
- package/devtools-panel/index.d.ts +0 -2
- package/devtools-panel/index.mjs +0 -5
- package/devtools-panel/index.mjs.map +0 -1
- package/devtools-panel/inject-devtools-panel.mjs +0 -72
- package/devtools-panel/inject-devtools-panel.mjs.map +0 -1
- package/devtools-panel/production/index.d.ts +0 -1
- package/devtools-panel/stub.d.ts +0 -2
- package/devtools-panel/stub.mjs +0 -8
- package/devtools-panel/stub.mjs.map +0 -1
- package/devtools-panel/types.d.ts +0 -48
- package/index.d.ts +0 -25
- package/index.mjs.map +0 -1
- package/infinite-query-options.mjs +0 -7
- package/infinite-query-options.mjs.map +0 -1
- package/inject-infinite-query.mjs +0 -18
- package/inject-infinite-query.mjs.map +0 -1
- package/inject-is-fetching.mjs +0 -31
- package/inject-is-fetching.mjs.map +0 -1
- package/inject-is-mutating.mjs +0 -31
- package/inject-is-mutating.mjs.map +0 -1
- package/inject-is-restoring.mjs +0 -21
- package/inject-is-restoring.mjs.map +0 -1
- package/inject-mutation-state.mjs +0 -51
- package/inject-mutation-state.mjs.map +0 -1
- package/inject-mutation.mjs +0 -94
- package/inject-mutation.mjs.map +0 -1
- package/inject-queries-experimental/index.d.ts +0 -1
- package/inject-queries-experimental/index.mjs +0 -5
- package/inject-queries-experimental/index.mjs.map +0 -1
- package/inject-queries.mjs +0 -82
- package/inject-queries.mjs.map +0 -1
- package/inject-query-client.mjs +0 -9
- package/inject-query-client.mjs.map +0 -1
- package/inject-query.mjs +0 -14
- package/inject-query.mjs.map +0 -1
- package/mutation-options.mjs +0 -7
- package/mutation-options.mjs.map +0 -1
- package/pending-tasks-compat.d.ts +0 -7
- package/pending-tasks-compat.mjs +0 -19
- package/pending-tasks-compat.mjs.map +0 -1
- package/providers.mjs +0 -32
- package/providers.mjs.map +0 -1
- package/query-options.mjs +0 -7
- package/query-options.mjs.map +0 -1
- package/signal-proxy.mjs +0 -29
- package/signal-proxy.mjs.map +0 -1
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApplicationRef,
|
|
3
|
+
Component,
|
|
4
|
+
provideZonelessChangeDetection,
|
|
5
|
+
} from '@angular/core'
|
|
6
|
+
import { TestBed } from '@angular/core/testing'
|
|
7
|
+
import { HttpClient, provideHttpClient } from '@angular/common/http'
|
|
8
|
+
import {
|
|
9
|
+
HttpTestingController,
|
|
10
|
+
provideHttpClientTesting,
|
|
11
|
+
} from '@angular/common/http/testing'
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
13
|
+
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
|
14
|
+
import { lastValueFrom } from 'rxjs'
|
|
15
|
+
import {
|
|
16
|
+
QueryClient,
|
|
17
|
+
injectMutation,
|
|
18
|
+
injectQuery,
|
|
19
|
+
onlineManager,
|
|
20
|
+
provideTanStackQuery,
|
|
21
|
+
} from '..'
|
|
22
|
+
|
|
23
|
+
describe('PendingTasks Integration', () => {
|
|
24
|
+
let queryClient: QueryClient
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.useFakeTimers()
|
|
28
|
+
|
|
29
|
+
queryClient = new QueryClient({
|
|
30
|
+
defaultOptions: {
|
|
31
|
+
queries: {
|
|
32
|
+
retry: false,
|
|
33
|
+
},
|
|
34
|
+
mutations: {
|
|
35
|
+
retry: false,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
TestBed.configureTestingModule({
|
|
41
|
+
providers: [
|
|
42
|
+
provideZonelessChangeDetection(),
|
|
43
|
+
provideTanStackQuery(queryClient),
|
|
44
|
+
],
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
onlineManager.setOnline(true)
|
|
50
|
+
queryClient.clear()
|
|
51
|
+
vi.useRealTimers()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('Synchronous Resolution', () => {
|
|
55
|
+
it('should handle synchronous queryFn with whenStable()', async () => {
|
|
56
|
+
const app = TestBed.inject(ApplicationRef)
|
|
57
|
+
|
|
58
|
+
const key = queryKey()
|
|
59
|
+
const query = TestBed.runInInjectionContext(() =>
|
|
60
|
+
injectQuery(() => ({
|
|
61
|
+
queryKey: key,
|
|
62
|
+
queryFn: () => 'instant-data', // Resolves synchronously
|
|
63
|
+
})),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// Should start as pending even with synchronous data
|
|
67
|
+
expect(query.status()).toBe('pending')
|
|
68
|
+
expect(query.data()).toBeUndefined()
|
|
69
|
+
|
|
70
|
+
const stablePromise = app.whenStable()
|
|
71
|
+
// Flush microtasks to allow TanStack Query's scheduled notifications to process
|
|
72
|
+
await Promise.resolve()
|
|
73
|
+
await vi.advanceTimersByTimeAsync(10)
|
|
74
|
+
await stablePromise
|
|
75
|
+
|
|
76
|
+
// Should work correctly even though queryFn was synchronous
|
|
77
|
+
expect(query.status()).toBe('success')
|
|
78
|
+
expect(query.data()).toBe('instant-data')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should handle synchronous error with whenStable()', async () => {
|
|
82
|
+
const app = TestBed.inject(ApplicationRef)
|
|
83
|
+
|
|
84
|
+
const key = queryKey()
|
|
85
|
+
const query = TestBed.runInInjectionContext(() =>
|
|
86
|
+
injectQuery(() => ({
|
|
87
|
+
queryKey: key,
|
|
88
|
+
queryFn: () => {
|
|
89
|
+
throw new Error('instant-error')
|
|
90
|
+
}, // Throws synchronously
|
|
91
|
+
})),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
const stablePromise = app.whenStable()
|
|
95
|
+
// Flush microtasks to allow TanStack Query's scheduled notifications to process
|
|
96
|
+
await Promise.resolve()
|
|
97
|
+
await vi.advanceTimersByTimeAsync(10)
|
|
98
|
+
await stablePromise
|
|
99
|
+
|
|
100
|
+
expect(query.status()).toBe('error')
|
|
101
|
+
expect(query.error()).toEqual(new Error('instant-error'))
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should handle synchronous mutationFn with whenStable()', async () => {
|
|
105
|
+
const app = TestBed.inject(ApplicationRef)
|
|
106
|
+
let mutationFnCalled = false
|
|
107
|
+
|
|
108
|
+
const mutation = TestBed.runInInjectionContext(() =>
|
|
109
|
+
injectMutation(() => ({
|
|
110
|
+
mutationFn: async (data: string) => {
|
|
111
|
+
mutationFnCalled = true
|
|
112
|
+
await Promise.resolve()
|
|
113
|
+
return `processed: ${data}`
|
|
114
|
+
},
|
|
115
|
+
})),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
mutation.mutate('test')
|
|
119
|
+
|
|
120
|
+
TestBed.tick()
|
|
121
|
+
|
|
122
|
+
const stablePromise = app.whenStable()
|
|
123
|
+
await Promise.resolve()
|
|
124
|
+
await vi.advanceTimersByTimeAsync(10)
|
|
125
|
+
await stablePromise
|
|
126
|
+
|
|
127
|
+
expect(mutationFnCalled).toBe(true)
|
|
128
|
+
expect(mutation.isSuccess()).toBe(true)
|
|
129
|
+
expect(mutation.data()).toBe('processed: test')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should handle synchronous mutation error with whenStable()', async () => {
|
|
133
|
+
const app = TestBed.inject(ApplicationRef)
|
|
134
|
+
|
|
135
|
+
const mutation = TestBed.runInInjectionContext(() =>
|
|
136
|
+
injectMutation(() => ({
|
|
137
|
+
mutationFn: async () => {
|
|
138
|
+
await Promise.resolve()
|
|
139
|
+
throw new Error('sync-mutation-error')
|
|
140
|
+
},
|
|
141
|
+
})),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
mutation.mutate()
|
|
145
|
+
|
|
146
|
+
TestBed.tick()
|
|
147
|
+
|
|
148
|
+
const stablePromise = app.whenStable()
|
|
149
|
+
await Promise.resolve()
|
|
150
|
+
await vi.advanceTimersByTimeAsync(10)
|
|
151
|
+
await stablePromise
|
|
152
|
+
|
|
153
|
+
expect(mutation.isError()).toBe(true)
|
|
154
|
+
expect(mutation.error()).toEqual(new Error('sync-mutation-error'))
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('Race Conditions', () => {
|
|
159
|
+
it('should handle query that completes during initial subscription', async () => {
|
|
160
|
+
const key = queryKey()
|
|
161
|
+
const app = TestBed.inject(ApplicationRef)
|
|
162
|
+
let resolveQuery: (value: string) => void
|
|
163
|
+
|
|
164
|
+
const queryPromise = new Promise<string>((resolve) => {
|
|
165
|
+
resolveQuery = resolve
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const query = TestBed.runInInjectionContext(() =>
|
|
169
|
+
injectQuery(() => ({
|
|
170
|
+
queryKey: key,
|
|
171
|
+
queryFn: () => queryPromise,
|
|
172
|
+
})),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
// Resolve immediately to create potential race condition
|
|
176
|
+
resolveQuery!('race-data')
|
|
177
|
+
|
|
178
|
+
const stablePromise = app.whenStable()
|
|
179
|
+
await Promise.resolve()
|
|
180
|
+
await vi.advanceTimersByTimeAsync(10)
|
|
181
|
+
await stablePromise
|
|
182
|
+
|
|
183
|
+
expect(query.status()).toBe('success')
|
|
184
|
+
expect(query.data()).toBe('race-data')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should handle rapid refetches without task leaks', async () => {
|
|
188
|
+
const app = TestBed.inject(ApplicationRef)
|
|
189
|
+
let callCount = 0
|
|
190
|
+
|
|
191
|
+
const key = queryKey()
|
|
192
|
+
const query = TestBed.runInInjectionContext(() =>
|
|
193
|
+
injectQuery(() => ({
|
|
194
|
+
queryKey: key,
|
|
195
|
+
queryFn: async () => {
|
|
196
|
+
callCount++
|
|
197
|
+
await sleep(10)
|
|
198
|
+
return `data-${callCount}`
|
|
199
|
+
},
|
|
200
|
+
})),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
// Trigger multiple rapid refetches
|
|
204
|
+
query.refetch()
|
|
205
|
+
query.refetch()
|
|
206
|
+
query.refetch()
|
|
207
|
+
|
|
208
|
+
const stablePromise = app.whenStable()
|
|
209
|
+
await vi.advanceTimersByTimeAsync(20)
|
|
210
|
+
await stablePromise
|
|
211
|
+
|
|
212
|
+
expect(query.status()).toBe('success')
|
|
213
|
+
expect(query.data()).toMatch(/^data-\d+$/)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should keep PendingTasks active while query retry is paused offline', async () => {
|
|
217
|
+
const app = TestBed.inject(ApplicationRef)
|
|
218
|
+
let attempt = 0
|
|
219
|
+
|
|
220
|
+
const key = queryKey()
|
|
221
|
+
const query = TestBed.runInInjectionContext(() =>
|
|
222
|
+
injectQuery(() => ({
|
|
223
|
+
queryKey: key,
|
|
224
|
+
retry: 1,
|
|
225
|
+
retryDelay: 50, // Longer delay to ensure we can go offline before retry
|
|
226
|
+
queryFn: async () => {
|
|
227
|
+
attempt++
|
|
228
|
+
if (attempt === 1) {
|
|
229
|
+
throw new Error('offline-fail')
|
|
230
|
+
}
|
|
231
|
+
await sleep(10)
|
|
232
|
+
return 'final-data'
|
|
233
|
+
},
|
|
234
|
+
})),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
// Allow the initial attempt to start and fail
|
|
238
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
239
|
+
await Promise.resolve()
|
|
240
|
+
|
|
241
|
+
// Wait for the first attempt to complete and start retry delay
|
|
242
|
+
await vi.advanceTimersByTimeAsync(10)
|
|
243
|
+
await Promise.resolve()
|
|
244
|
+
|
|
245
|
+
expect(query.status()).toBe('pending')
|
|
246
|
+
expect(query.fetchStatus()).toBe('fetching')
|
|
247
|
+
|
|
248
|
+
// Simulate the app going offline during retry delay
|
|
249
|
+
onlineManager.setOnline(false)
|
|
250
|
+
|
|
251
|
+
// Advance past the retry delay to trigger the pause
|
|
252
|
+
await vi.advanceTimersByTimeAsync(50)
|
|
253
|
+
await Promise.resolve()
|
|
254
|
+
|
|
255
|
+
expect(query.fetchStatus()).toBe('paused')
|
|
256
|
+
|
|
257
|
+
const stablePromise = app.whenStable()
|
|
258
|
+
let stableResolved = false
|
|
259
|
+
void stablePromise.then(() => {
|
|
260
|
+
stableResolved = true
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
await Promise.resolve()
|
|
264
|
+
|
|
265
|
+
// PendingTasks should continue blocking stability while the fetch is paused
|
|
266
|
+
expect(stableResolved).toBe(false)
|
|
267
|
+
expect(query.status()).toBe('pending')
|
|
268
|
+
|
|
269
|
+
// Bring the app back online so the retry can continue
|
|
270
|
+
onlineManager.setOnline(true)
|
|
271
|
+
|
|
272
|
+
// Give time for the retry to resume and complete
|
|
273
|
+
await vi.advanceTimersByTimeAsync(20)
|
|
274
|
+
await Promise.resolve()
|
|
275
|
+
|
|
276
|
+
await stablePromise
|
|
277
|
+
|
|
278
|
+
expect(stableResolved).toBe(true)
|
|
279
|
+
expect(query.status()).toBe('success')
|
|
280
|
+
expect(query.data()).toBe('final-data')
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
describe('Component Destruction', () => {
|
|
285
|
+
@Component({
|
|
286
|
+
template: '',
|
|
287
|
+
})
|
|
288
|
+
class TestComponent {
|
|
289
|
+
query = injectQuery(() => ({
|
|
290
|
+
queryKey: ['component-query'],
|
|
291
|
+
queryFn: () => sleep(100).then(() => 'component-data'),
|
|
292
|
+
}))
|
|
293
|
+
|
|
294
|
+
mutation = injectMutation(() => ({
|
|
295
|
+
mutationFn: (data: string) =>
|
|
296
|
+
sleep(100).then(() => `processed: ${data}`),
|
|
297
|
+
}))
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
it('should cleanup pending tasks when component with active query is destroyed', async () => {
|
|
301
|
+
const app = TestBed.inject(ApplicationRef)
|
|
302
|
+
const fixture = TestBed.createComponent(TestComponent)
|
|
303
|
+
|
|
304
|
+
// Start the query
|
|
305
|
+
expect(fixture.componentInstance.query.status()).toBe('pending')
|
|
306
|
+
|
|
307
|
+
// Destroy component while query is running
|
|
308
|
+
fixture.destroy()
|
|
309
|
+
|
|
310
|
+
// Angular should become stable even though component was destroyed
|
|
311
|
+
const stablePromise = app.whenStable()
|
|
312
|
+
await vi.advanceTimersByTimeAsync(150)
|
|
313
|
+
|
|
314
|
+
await expect(stablePromise).resolves.toEqual(undefined)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('should cleanup pending tasks when component with active mutation is destroyed', async () => {
|
|
318
|
+
const app = TestBed.inject(ApplicationRef)
|
|
319
|
+
const fixture = TestBed.createComponent(TestComponent)
|
|
320
|
+
|
|
321
|
+
fixture.componentInstance.mutation.mutate('test')
|
|
322
|
+
|
|
323
|
+
// Destroy component while mutation is running
|
|
324
|
+
fixture.destroy()
|
|
325
|
+
|
|
326
|
+
// Angular should become stable even though component was destroyed
|
|
327
|
+
const stablePromise = app.whenStable()
|
|
328
|
+
await vi.advanceTimersByTimeAsync(150)
|
|
329
|
+
|
|
330
|
+
await expect(stablePromise).resolves.toEqual(undefined)
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
describe('Concurrent Operations', () => {
|
|
335
|
+
it('should handle multiple queries running simultaneously', async () => {
|
|
336
|
+
const key1 = queryKey()
|
|
337
|
+
const key2 = queryKey()
|
|
338
|
+
const key3 = queryKey()
|
|
339
|
+
const app = TestBed.inject(ApplicationRef)
|
|
340
|
+
|
|
341
|
+
const query1 = TestBed.runInInjectionContext(() =>
|
|
342
|
+
injectQuery(() => ({
|
|
343
|
+
queryKey: key1,
|
|
344
|
+
queryFn: () => sleep(30).then(() => 'data-1'),
|
|
345
|
+
})),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
const query2 = TestBed.runInInjectionContext(() =>
|
|
349
|
+
injectQuery(() => ({
|
|
350
|
+
queryKey: key2,
|
|
351
|
+
queryFn: () => sleep(50).then(() => 'data-2'),
|
|
352
|
+
})),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
const query3 = TestBed.runInInjectionContext(() =>
|
|
356
|
+
injectQuery(() => ({
|
|
357
|
+
queryKey: key3,
|
|
358
|
+
queryFn: () => 'instant-data', // Synchronous
|
|
359
|
+
})),
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
// All queries should start
|
|
363
|
+
expect(query1.status()).toBe('pending')
|
|
364
|
+
expect(query2.status()).toBe('pending')
|
|
365
|
+
expect(query3.status()).toBe('pending')
|
|
366
|
+
|
|
367
|
+
const stablePromise = app.whenStable()
|
|
368
|
+
await vi.advanceTimersByTimeAsync(60)
|
|
369
|
+
await stablePromise
|
|
370
|
+
|
|
371
|
+
// All queries should be complete
|
|
372
|
+
expect(query1.status()).toBe('success')
|
|
373
|
+
expect(query1.data()).toBe('data-1')
|
|
374
|
+
expect(query2.status()).toBe('success')
|
|
375
|
+
expect(query2.data()).toBe('data-2')
|
|
376
|
+
expect(query3.status()).toBe('success')
|
|
377
|
+
expect(query3.data()).toBe('instant-data')
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('should handle multiple mutations running simultaneously', async () => {
|
|
381
|
+
const app = TestBed.inject(ApplicationRef)
|
|
382
|
+
|
|
383
|
+
const mutation1 = TestBed.runInInjectionContext(() =>
|
|
384
|
+
injectMutation(() => ({
|
|
385
|
+
mutationFn: (data: string) =>
|
|
386
|
+
sleep(30).then(() => `processed-1: ${data}`),
|
|
387
|
+
})),
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
const mutation2 = TestBed.runInInjectionContext(() =>
|
|
391
|
+
injectMutation(() => ({
|
|
392
|
+
mutationFn: (data: string) =>
|
|
393
|
+
sleep(50).then(() => `processed-2: ${data}`),
|
|
394
|
+
})),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
const mutation3 = TestBed.runInInjectionContext(() =>
|
|
398
|
+
injectMutation(() => ({
|
|
399
|
+
mutationFn: async (data: string) => {
|
|
400
|
+
await Promise.resolve()
|
|
401
|
+
return `processed-3: ${data}`
|
|
402
|
+
},
|
|
403
|
+
})),
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
// Start all mutations
|
|
407
|
+
mutation1.mutate('test1')
|
|
408
|
+
mutation2.mutate('test2')
|
|
409
|
+
mutation3.mutate('test3')
|
|
410
|
+
|
|
411
|
+
TestBed.tick()
|
|
412
|
+
|
|
413
|
+
const stablePromise = app.whenStable()
|
|
414
|
+
await vi.advanceTimersByTimeAsync(60)
|
|
415
|
+
await stablePromise
|
|
416
|
+
|
|
417
|
+
// All mutations should be complete
|
|
418
|
+
expect(mutation1.isSuccess()).toBe(true)
|
|
419
|
+
expect(mutation1.data()).toBe('processed-1: test1')
|
|
420
|
+
expect(mutation2.isSuccess()).toBe(true)
|
|
421
|
+
expect(mutation2.data()).toBe('processed-2: test2')
|
|
422
|
+
expect(mutation3.isSuccess()).toBe(true)
|
|
423
|
+
expect(mutation3.data()).toBe('processed-3: test3')
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it('should handle mixed queries and mutations', async () => {
|
|
427
|
+
const app = TestBed.inject(ApplicationRef)
|
|
428
|
+
|
|
429
|
+
const key = queryKey()
|
|
430
|
+
const query = TestBed.runInInjectionContext(() =>
|
|
431
|
+
injectQuery(() => ({
|
|
432
|
+
queryKey: key,
|
|
433
|
+
queryFn: () => sleep(40).then(() => 'query-data'),
|
|
434
|
+
})),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
const mutation = TestBed.runInInjectionContext(() =>
|
|
438
|
+
injectMutation(() => ({
|
|
439
|
+
mutationFn: (data: string) =>
|
|
440
|
+
sleep(60).then(() => `mutation: ${data}`),
|
|
441
|
+
})),
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
// Start both operations
|
|
445
|
+
mutation.mutate('test')
|
|
446
|
+
|
|
447
|
+
const stablePromise = app.whenStable()
|
|
448
|
+
await vi.advanceTimersByTimeAsync(70)
|
|
449
|
+
await stablePromise
|
|
450
|
+
|
|
451
|
+
// Both should be complete
|
|
452
|
+
expect(query.status()).toBe('success')
|
|
453
|
+
expect(query.data()).toBe('query-data')
|
|
454
|
+
expect(mutation.isSuccess()).toBe(true)
|
|
455
|
+
expect(mutation.data()).toBe('mutation: test')
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
describe('HttpClient Integration', () => {
|
|
460
|
+
beforeEach(() => {
|
|
461
|
+
TestBed.configureTestingModule({
|
|
462
|
+
providers: [provideHttpClient(), provideHttpClientTesting()],
|
|
463
|
+
})
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('should handle multiple HttpClient requests with lastValueFrom', async () => {
|
|
467
|
+
const app = TestBed.inject(ApplicationRef)
|
|
468
|
+
const httpClient = TestBed.inject(HttpClient)
|
|
469
|
+
const httpTestingController = TestBed.inject(HttpTestingController)
|
|
470
|
+
|
|
471
|
+
const key1 = queryKey()
|
|
472
|
+
const key2 = queryKey()
|
|
473
|
+
|
|
474
|
+
const query1 = TestBed.runInInjectionContext(() =>
|
|
475
|
+
injectQuery(() => ({
|
|
476
|
+
queryKey: key1,
|
|
477
|
+
queryFn: () =>
|
|
478
|
+
lastValueFrom(httpClient.get<{ id: number }>('/api/1')),
|
|
479
|
+
})),
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
const query2 = TestBed.runInInjectionContext(() =>
|
|
483
|
+
injectQuery(() => ({
|
|
484
|
+
queryKey: key2,
|
|
485
|
+
queryFn: () =>
|
|
486
|
+
lastValueFrom(httpClient.get<{ id: number }>('/api/2')),
|
|
487
|
+
})),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
// Schedule HTTP responses
|
|
491
|
+
setTimeout(() => {
|
|
492
|
+
const req1 = httpTestingController.expectOne('/api/1')
|
|
493
|
+
req1.flush({ id: 1 })
|
|
494
|
+
|
|
495
|
+
const req2 = httpTestingController.expectOne('/api/2')
|
|
496
|
+
req2.flush({ id: 2 })
|
|
497
|
+
}, 10)
|
|
498
|
+
|
|
499
|
+
const stablePromise = app.whenStable()
|
|
500
|
+
await vi.advanceTimersByTimeAsync(20)
|
|
501
|
+
await stablePromise
|
|
502
|
+
|
|
503
|
+
expect(query1.status()).toBe('success')
|
|
504
|
+
expect(query1.data()).toEqual({ id: 1 })
|
|
505
|
+
expect(query2.status()).toBe('success')
|
|
506
|
+
expect(query2.data()).toEqual({ id: 2 })
|
|
507
|
+
|
|
508
|
+
httpTestingController.verify()
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it('should handle HttpClient request cancellation', async () => {
|
|
512
|
+
const app = TestBed.inject(ApplicationRef)
|
|
513
|
+
const httpClient = TestBed.inject(HttpClient)
|
|
514
|
+
const httpTestingController = TestBed.inject(HttpTestingController)
|
|
515
|
+
|
|
516
|
+
const key = queryKey()
|
|
517
|
+
const query = TestBed.runInInjectionContext(() =>
|
|
518
|
+
injectQuery(() => ({
|
|
519
|
+
queryKey: key,
|
|
520
|
+
queryFn: () =>
|
|
521
|
+
lastValueFrom(httpClient.get<{ data: string }>('/api/cancel')),
|
|
522
|
+
})),
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
// Cancel the request before it completes
|
|
526
|
+
setTimeout(() => {
|
|
527
|
+
const req = httpTestingController.expectOne('/api/cancel')
|
|
528
|
+
req.error(new ProgressEvent('error'), {
|
|
529
|
+
status: 0,
|
|
530
|
+
statusText: 'Unknown Error',
|
|
531
|
+
})
|
|
532
|
+
}, 10)
|
|
533
|
+
|
|
534
|
+
const stablePromise = app.whenStable()
|
|
535
|
+
await vi.advanceTimersByTimeAsync(20)
|
|
536
|
+
await stablePromise
|
|
537
|
+
|
|
538
|
+
expect(query.status()).toBe('error')
|
|
539
|
+
|
|
540
|
+
httpTestingController.verify()
|
|
541
|
+
})
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
describe('Edge Cases', () => {
|
|
545
|
+
it('should handle query cancellation mid-flight', async () => {
|
|
546
|
+
const key = queryKey()
|
|
547
|
+
const app = TestBed.inject(ApplicationRef)
|
|
548
|
+
|
|
549
|
+
const query = TestBed.runInInjectionContext(() =>
|
|
550
|
+
injectQuery(() => ({
|
|
551
|
+
queryKey: key,
|
|
552
|
+
queryFn: () => sleep(100).then(() => 'data'),
|
|
553
|
+
})),
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
// Cancel the query after a short delay
|
|
557
|
+
setTimeout(() => {
|
|
558
|
+
queryClient.cancelQueries({ queryKey: key })
|
|
559
|
+
}, 20)
|
|
560
|
+
|
|
561
|
+
// Advance to the cancellation point
|
|
562
|
+
await vi.advanceTimersByTimeAsync(20)
|
|
563
|
+
|
|
564
|
+
TestBed.tick()
|
|
565
|
+
|
|
566
|
+
const stablePromise = app.whenStable()
|
|
567
|
+
await vi.advanceTimersByTimeAsync(130)
|
|
568
|
+
await stablePromise
|
|
569
|
+
|
|
570
|
+
// Cancellation should restore the pre-fetch state
|
|
571
|
+
expect(query.status()).toBe('pending')
|
|
572
|
+
expect(query.fetchStatus()).toBe('idle')
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
it('should handle query retry and pending task tracking', async () => {
|
|
576
|
+
const app = TestBed.inject(ApplicationRef)
|
|
577
|
+
let attemptCount = 0
|
|
578
|
+
|
|
579
|
+
const key = queryKey()
|
|
580
|
+
const query = TestBed.runInInjectionContext(() =>
|
|
581
|
+
injectQuery(() => ({
|
|
582
|
+
queryKey: key,
|
|
583
|
+
retry: 2,
|
|
584
|
+
retryDelay: 10,
|
|
585
|
+
queryFn: async () => {
|
|
586
|
+
attemptCount++
|
|
587
|
+
if (attemptCount <= 2) {
|
|
588
|
+
throw new Error(`Attempt ${attemptCount} failed`)
|
|
589
|
+
}
|
|
590
|
+
return 'success-data'
|
|
591
|
+
},
|
|
592
|
+
})),
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
const stablePromise = app.whenStable()
|
|
596
|
+
await vi.advanceTimersByTimeAsync(50)
|
|
597
|
+
await stablePromise
|
|
598
|
+
|
|
599
|
+
expect(query.status()).toBe('success')
|
|
600
|
+
expect(query.data()).toBe('success-data')
|
|
601
|
+
expect(attemptCount).toBe(3) // Initial + 2 retries
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it('should handle mutation with optimistic updates', async () => {
|
|
605
|
+
const app = TestBed.inject(ApplicationRef)
|
|
606
|
+
const testQueryKey = queryKey()
|
|
607
|
+
|
|
608
|
+
queryClient.setQueryData(testQueryKey, 'initial-data')
|
|
609
|
+
|
|
610
|
+
const mutation = TestBed.runInInjectionContext(() =>
|
|
611
|
+
injectMutation(() => ({
|
|
612
|
+
mutationFn: (newData: string) => sleep(50).then(() => newData),
|
|
613
|
+
onMutate: async (newData) => {
|
|
614
|
+
// Optimistic update
|
|
615
|
+
const previousData = queryClient.getQueryData(testQueryKey)
|
|
616
|
+
queryClient.setQueryData(testQueryKey, newData)
|
|
617
|
+
return { previousData }
|
|
618
|
+
},
|
|
619
|
+
onError: (_err, _newData, context) => {
|
|
620
|
+
// Rollback on error
|
|
621
|
+
if (context?.previousData) {
|
|
622
|
+
queryClient.setQueryData(testQueryKey, context.previousData)
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
})),
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
mutation.mutate('optimistic-data')
|
|
629
|
+
|
|
630
|
+
await Promise.resolve()
|
|
631
|
+
|
|
632
|
+
// Data should be optimistically updated immediately
|
|
633
|
+
expect(queryClient.getQueryData(testQueryKey)).toBe('optimistic-data')
|
|
634
|
+
|
|
635
|
+
const stablePromise = app.whenStable()
|
|
636
|
+
await vi.advanceTimersByTimeAsync(60)
|
|
637
|
+
await stablePromise
|
|
638
|
+
|
|
639
|
+
expect(mutation.isSuccess()).toBe(true)
|
|
640
|
+
expect(mutation.data()).toBe('optimistic-data')
|
|
641
|
+
expect(queryClient.getQueryData(testQueryKey)).toBe('optimistic-data')
|
|
642
|
+
})
|
|
643
|
+
})
|
|
644
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { InjectionToken, provideZonelessChangeDetection } from '@angular/core'
|
|
4
|
+
import { QueryClient } from '@tanstack/query-core'
|
|
5
|
+
import { provideQueryClient } from '../providers'
|
|
6
|
+
|
|
7
|
+
describe('provideQueryClient', () => {
|
|
8
|
+
it('should provide a QueryClient instance directly', () => {
|
|
9
|
+
const queryClient = new QueryClient()
|
|
10
|
+
|
|
11
|
+
TestBed.configureTestingModule({
|
|
12
|
+
providers: [
|
|
13
|
+
provideZonelessChangeDetection(),
|
|
14
|
+
provideQueryClient(queryClient),
|
|
15
|
+
],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const providedQueryClient = TestBed.inject(QueryClient)
|
|
19
|
+
expect(providedQueryClient).toBe(queryClient)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should provide a QueryClient instance using an InjectionToken', () => {
|
|
23
|
+
const queryClient = new QueryClient()
|
|
24
|
+
const CUSTOM_QUERY_CLIENT = new InjectionToken<QueryClient>('', {
|
|
25
|
+
factory: () => queryClient,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
TestBed.configureTestingModule({
|
|
29
|
+
providers: [
|
|
30
|
+
provideZonelessChangeDetection(),
|
|
31
|
+
provideQueryClient(CUSTOM_QUERY_CLIENT),
|
|
32
|
+
],
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const providedQueryClient = TestBed.inject(QueryClient)
|
|
36
|
+
expect(providedQueryClient).toBe(queryClient)
|
|
37
|
+
})
|
|
38
|
+
})
|