@ethlete/query 6.0.0-next.5 → 6.0.0-next.51
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/CHANGELOG.md +772 -0
- package/fesm2022/ethlete-query-testing.mjs +441 -38
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +15961 -5330
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/deprecate-legacy-queries/migration.js +77 -0
- package/generators/deprecate-legacy-queries/schema.json +26 -0
- package/generators/generators.json +15 -0
- package/generators/migrate-query-client-features/migration.js +142 -0
- package/generators/migrate-query-client-features/report.js +136 -0
- package/generators/migrate-query-client-features/schema.json +26 -0
- package/generators/migrate-query-opt-in-features/migration.js +194 -0
- package/generators/migrate-query-opt-in-features/report.js +143 -0
- package/generators/migrate-query-opt-in-features/schema.json +31 -0
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +133 -0
- package/generators/migrate-to-query-v3/cleanup-migration.js +111 -99
- package/generators/migrate-to-query-v3/http-client-check.js +93 -0
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +129 -40
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +237 -152
- package/generators/migrate-to-query-v3/migration-scope.js +38 -0
- package/generators/migrate-to-query-v3/migration.js +17 -9
- package/generators/migrate-to-query-v3/module-graph.js +153 -0
- package/generators/migrate-to-query-v3/query-client-migration.js +152 -53
- package/generators/migrate-to-query-v3/rename-symbols.js +206 -0
- package/generators/migrate-to-query-v3/schema.json +12 -0
- package/generators/migrate-to-query-v3/shared.js +51 -2
- package/generators/prep-for-query-v3/migration.js +71 -14
- package/migrations.json +44 -0
- package/package.json +29 -13
- package/types/ethlete-query-testing.d.ts +179 -12
- package/types/ethlete-query.d.ts +8935 -3203
|
@@ -27,12 +27,12 @@ const setupAuthTest = (config) => {
|
|
|
27
27
|
}), extractRefreshTokens = (response) => ({
|
|
28
28
|
accessToken: response.accessToken,
|
|
29
29
|
refreshToken: response.refreshToken,
|
|
30
|
-
}), features, bearerDecryptFn,
|
|
30
|
+
}), buildRefreshArgs, features, bearerDecryptFn, refreshStrategy, minRefreshInterval, refreshIfExpired, expiresInPropertyName, onRefreshFailure, } = config;
|
|
31
31
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
32
|
const login = querySetup.createPost(loginPath, {});
|
|
33
33
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
34
|
const refresh = querySetup.createPost(refreshPath, {});
|
|
35
|
-
const
|
|
35
|
+
const { inject: injectAuth } = createBearerAuthProvider({
|
|
36
36
|
name: 'test-auth',
|
|
37
37
|
queryClientRef: querySetup.queryClientRef,
|
|
38
38
|
queries: [
|
|
@@ -43,16 +43,17 @@ const setupAuthTest = (config) => {
|
|
|
43
43
|
withRefreshQuery('refresh', {
|
|
44
44
|
queryCreator: refresh,
|
|
45
45
|
extractTokens: extractRefreshTokens,
|
|
46
|
+
buildArgs: buildRefreshArgs,
|
|
46
47
|
autoRetryOn401,
|
|
47
48
|
refreshStrategy,
|
|
48
49
|
minRefreshInterval,
|
|
49
50
|
refreshIfExpired,
|
|
50
51
|
expiresInPropertyName,
|
|
52
|
+
onRefreshFailure,
|
|
51
53
|
}),
|
|
52
54
|
],
|
|
53
55
|
features: (features ?? []),
|
|
54
56
|
bearerDecryptFn,
|
|
55
|
-
multiTabSync,
|
|
56
57
|
});
|
|
57
58
|
const auth = TestBed.runInInjectionContext(() => {
|
|
58
59
|
const provider = injectAuth();
|
|
@@ -96,37 +97,356 @@ const setupAuthTest = (config) => {
|
|
|
96
97
|
};
|
|
97
98
|
};
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
100
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
101
|
+
/**
|
|
102
|
+
* Replaces `globalThis.BroadcastChannel` with an in-memory bus.
|
|
103
|
+
*
|
|
104
|
+
* Messages are structured-cloned at post time (so a non-cloneable payload throws exactly like the
|
|
105
|
+
* real API does), delivered on a microtask (never synchronously), and never delivered back to the
|
|
106
|
+
* channel that posted them - all three matching browser behavior. `await Promise.resolve()` in a
|
|
107
|
+
* spec is enough to flush delivery.
|
|
108
|
+
*/
|
|
109
|
+
const installFakeBroadcastChannel = () => {
|
|
110
|
+
const original = globalThis.BroadcastChannel;
|
|
111
|
+
const instances = new Set();
|
|
112
|
+
const posted = [];
|
|
113
|
+
class FakeBroadcastChannel {
|
|
114
|
+
constructor(name) {
|
|
115
|
+
this.name = name;
|
|
116
|
+
this.closed = false;
|
|
117
|
+
this.onmessage = null;
|
|
118
|
+
instances.add(this);
|
|
109
119
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
postMessage(data) {
|
|
121
|
+
if (this.closed)
|
|
122
|
+
throw new DOMException('Channel is closed', 'InvalidStateError');
|
|
123
|
+
const clone = structuredClone(data);
|
|
124
|
+
posted.push({ channel: this.name, data: clone });
|
|
125
|
+
for (const instance of instances) {
|
|
126
|
+
if (instance === this || instance.closed || instance.name !== this.name)
|
|
127
|
+
continue;
|
|
128
|
+
queueMicrotask(() => {
|
|
129
|
+
if (instance.closed)
|
|
130
|
+
return;
|
|
131
|
+
instance.onmessage?.({ data: structuredClone(clone) });
|
|
132
|
+
});
|
|
133
|
+
}
|
|
119
134
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
close() {
|
|
136
|
+
this.closed = true;
|
|
137
|
+
instances.delete(this);
|
|
123
138
|
}
|
|
124
|
-
|
|
125
|
-
|
|
139
|
+
addEventListener() {
|
|
140
|
+
throw new Error('The fake BroadcastChannel only supports `onmessage`.');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
globalThis.BroadcastChannel = FakeBroadcastChannel;
|
|
144
|
+
return {
|
|
145
|
+
posted,
|
|
146
|
+
restore: () => {
|
|
147
|
+
for (const instance of [...instances]) {
|
|
148
|
+
instance.closed = true;
|
|
149
|
+
}
|
|
150
|
+
instances.clear();
|
|
151
|
+
globalThis.BroadcastChannel = original;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Installs an in-memory `navigator.locks` implementing exclusive locks: one holder per name, the
|
|
157
|
+
* rest queued FIFO, granted on a microtask (never synchronously), released when the callback's
|
|
158
|
+
* promise settles. `signal` is honored for queued requests and - as in the real API - ignored once a
|
|
159
|
+
* request has been granted.
|
|
160
|
+
*
|
|
161
|
+
* `ifAvailable` is a try-lock: a request that cannot be granted straight away runs its callback with
|
|
162
|
+
* `null` instead of queueing, exactly as the real API does. `steal` releases the current holder -
|
|
163
|
+
* rejecting its request with an `AbortError` - and is granted ahead of everything queued.
|
|
164
|
+
*
|
|
165
|
+
* `mode: 'shared'` is not implemented; nothing in the library uses it.
|
|
166
|
+
*/
|
|
167
|
+
const installFakeWebLocks = () => {
|
|
168
|
+
const originalDescriptor = Object.getOwnPropertyDescriptor(navigator, 'locks');
|
|
169
|
+
const holders = new Map();
|
|
170
|
+
const queues = new Map();
|
|
171
|
+
const abortError = () => new DOMException('The lock request was aborted', 'AbortError');
|
|
172
|
+
const pump = (name) => {
|
|
173
|
+
if (holders.has(name))
|
|
174
|
+
return;
|
|
175
|
+
const waiter = queues.get(name)?.shift();
|
|
176
|
+
if (!waiter)
|
|
126
177
|
return;
|
|
178
|
+
waiter.granted = true;
|
|
179
|
+
holders.set(name, waiter);
|
|
180
|
+
const settle = (settleWaiter) => {
|
|
181
|
+
// Only if this waiter is still the holder: a stolen lock already has a new one, and the callback
|
|
182
|
+
// of the tab it was taken from settles afterwards.
|
|
183
|
+
if (holders.get(name) === waiter)
|
|
184
|
+
holders.delete(name);
|
|
185
|
+
waiter.releaseSignal();
|
|
186
|
+
settleWaiter();
|
|
187
|
+
pump(name);
|
|
188
|
+
};
|
|
189
|
+
// `pump` itself only ever runs from a microtask, so calling the callback right here keeps the
|
|
190
|
+
// grant exactly one microtask away from the `request()` call - which is what makes a single
|
|
191
|
+
// `flushMultiTabSync()` per hop enough in specs.
|
|
192
|
+
try {
|
|
193
|
+
Promise.resolve(waiter.callback({ name, mode: 'exclusive' })).then((value) => settle(() => waiter.resolve(value)), (error) => settle(() => waiter.reject(error)));
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
settle(() => waiter.reject(error));
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const request = (name, optionsOrCallback, maybeCallback) => {
|
|
200
|
+
const options = (typeof optionsOrCallback === 'function' ? {} : (optionsOrCallback ?? {}));
|
|
201
|
+
const callback = (typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback);
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
if (options.signal?.aborted) {
|
|
204
|
+
reject(abortError());
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const waiter = { callback, resolve, reject, granted: false, releaseSignal: () => undefined };
|
|
208
|
+
const signal = options.signal;
|
|
209
|
+
if (signal) {
|
|
210
|
+
const onAbort = () => {
|
|
211
|
+
waiter.releaseSignal();
|
|
212
|
+
if (waiter.granted)
|
|
213
|
+
return;
|
|
214
|
+
const queue = queues.get(name);
|
|
215
|
+
if (queue)
|
|
216
|
+
queues.set(name, queue.filter((entry) => entry !== waiter));
|
|
217
|
+
reject(abortError());
|
|
218
|
+
};
|
|
219
|
+
signal.addEventListener('abort', onAbort);
|
|
220
|
+
waiter.releaseSignal = () => {
|
|
221
|
+
waiter.releaseSignal = () => undefined;
|
|
222
|
+
signal.removeEventListener('abort', onAbort);
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (options.steal) {
|
|
226
|
+
queueMicrotask(() => {
|
|
227
|
+
const holder = holders.get(name);
|
|
228
|
+
if (holder) {
|
|
229
|
+
holders.delete(name);
|
|
230
|
+
holder.reject(abortError());
|
|
231
|
+
}
|
|
232
|
+
queues.set(name, [waiter, ...(queues.get(name) ?? [])]);
|
|
233
|
+
pump(name);
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// Availability is decided a microtask out, not here: that is when the real API decides it too, so
|
|
238
|
+
// two try-locks requested in the same tick still resolve to one winner and one `null`.
|
|
239
|
+
if (options.ifAvailable) {
|
|
240
|
+
queueMicrotask(() => {
|
|
241
|
+
if (holders.has(name) || (queues.get(name)?.length ?? 0) > 0) {
|
|
242
|
+
waiter.releaseSignal();
|
|
243
|
+
Promise.resolve(callback(null)).then(resolve, reject);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
queues.set(name, [waiter]);
|
|
247
|
+
pump(name);
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
queues.set(name, [...(queues.get(name) ?? []), waiter]);
|
|
252
|
+
queueMicrotask(() => pump(name));
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
// Both lists are reported, because a queued request is how a tab that is *not* the holder still
|
|
256
|
+
// shows up - which is what the auth leader election counts its tabs by.
|
|
257
|
+
const query = () => Promise.resolve({
|
|
258
|
+
held: [...holders.keys()].map((name) => ({ name, mode: 'exclusive' })),
|
|
259
|
+
pending: [...queues.entries()].flatMap(([name, waiters]) => waiters.map(() => ({ name, mode: 'exclusive' }))),
|
|
260
|
+
});
|
|
261
|
+
Object.defineProperty(navigator, 'locks', {
|
|
262
|
+
value: {
|
|
263
|
+
request,
|
|
264
|
+
query,
|
|
265
|
+
},
|
|
266
|
+
configurable: true,
|
|
267
|
+
writable: true,
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
heldNames: () => [...holders.keys()],
|
|
271
|
+
pendingNames: () => [...queues.entries()].filter(([, queue]) => queue.length > 0).map(([name]) => name),
|
|
272
|
+
restore: () => {
|
|
273
|
+
for (const waiter of [...holders.values(), ...[...queues.values()].flat()])
|
|
274
|
+
waiter.releaseSignal();
|
|
275
|
+
holders.clear();
|
|
276
|
+
queues.clear();
|
|
277
|
+
if (originalDescriptor) {
|
|
278
|
+
Object.defineProperty(navigator, 'locks', originalDescriptor);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
delete navigator.locks;
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* Flushes the microtask queue, which is where both fakes schedule their work - message delivery and
|
|
288
|
+
* lock grants. Awaiting it once is enough for a single hop; a grant that has to wait for another
|
|
289
|
+
* tab's release needs one await per hop.
|
|
290
|
+
*/
|
|
291
|
+
const flushMultiTabSync = () => Promise.resolve();
|
|
292
|
+
|
|
293
|
+
const quotaError = () => new DOMException('The quota has been exceeded.', 'QuotaExceededError');
|
|
294
|
+
const createFakeQueryPersistenceStore = () => {
|
|
295
|
+
const store = new Map();
|
|
296
|
+
const calls = { loadIndex: 0, read: 0, write: 0, remove: 0, clear: 0 };
|
|
297
|
+
const deferredReads = [];
|
|
298
|
+
const deferredLoadIndexes = [];
|
|
299
|
+
let failingWrites = 0;
|
|
300
|
+
let writeError = null;
|
|
301
|
+
let failingRemoves = 0;
|
|
302
|
+
let removeError = null;
|
|
303
|
+
let loadIndexError = null;
|
|
304
|
+
let isDeferringReads = false;
|
|
305
|
+
let isDeferringLoadIndex = false;
|
|
306
|
+
// Cloning on the way in and on the way out is not pedantry: a real store hands back a copy, so a
|
|
307
|
+
// spec (or the engine) mutating a hydrated body must not be able to change what is "on disk".
|
|
308
|
+
const clone = (value) => structuredClone(value);
|
|
309
|
+
const loadIndex = async () => {
|
|
310
|
+
calls.loadIndex++;
|
|
311
|
+
if (loadIndexError !== null) {
|
|
312
|
+
const error = loadIndexError;
|
|
313
|
+
loadIndexError = null;
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
// Taken before the gate, not after: a real store answers with what it held when the read started,
|
|
317
|
+
// so anything that empties it while a deferred load is held must not change what that load returns.
|
|
318
|
+
const snapshot = Array.from(store.values()).map(({ body: _body, ...meta }) => clone(meta));
|
|
319
|
+
if (isDeferringLoadIndex) {
|
|
320
|
+
await new Promise((resolve) => deferredLoadIndexes.push(resolve));
|
|
321
|
+
}
|
|
322
|
+
return snapshot;
|
|
323
|
+
};
|
|
324
|
+
const read = async (key) => {
|
|
325
|
+
calls.read++;
|
|
326
|
+
if (isDeferringReads) {
|
|
327
|
+
await new Promise((resolve) => deferredReads.push(resolve));
|
|
127
328
|
}
|
|
128
|
-
|
|
329
|
+
const entry = store.get(key);
|
|
330
|
+
return entry ? { body: clone(entry.body), version: entry.version } : null;
|
|
129
331
|
};
|
|
332
|
+
const write = async (entries) => {
|
|
333
|
+
calls.write++;
|
|
334
|
+
if (failingWrites > 0) {
|
|
335
|
+
failingWrites--;
|
|
336
|
+
throw writeError ?? quotaError();
|
|
337
|
+
}
|
|
338
|
+
for (const entry of entries) {
|
|
339
|
+
store.set(entry.key, clone(entry));
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
const remove = async (keys) => {
|
|
343
|
+
calls.remove++;
|
|
344
|
+
if (failingRemoves > 0) {
|
|
345
|
+
failingRemoves--;
|
|
346
|
+
throw removeError ?? new Error('The removal failed.');
|
|
347
|
+
}
|
|
348
|
+
for (const key of keys) {
|
|
349
|
+
store.delete(key);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const clear = async () => {
|
|
353
|
+
calls.clear++;
|
|
354
|
+
store.clear();
|
|
355
|
+
};
|
|
356
|
+
return {
|
|
357
|
+
adapter: { loadIndex, read, write, remove, clear, isSupported: true },
|
|
358
|
+
seed: (entries) => {
|
|
359
|
+
for (const entry of entries) {
|
|
360
|
+
store.set(entry.key, clone(entry));
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
entries: () => Array.from(store.values()).map(clone),
|
|
364
|
+
entry: (key) => {
|
|
365
|
+
const entry = store.get(key);
|
|
366
|
+
return entry ? clone(entry) : undefined;
|
|
367
|
+
},
|
|
368
|
+
calls: () => ({ ...calls }),
|
|
369
|
+
failNextWrites: (count, error) => {
|
|
370
|
+
failingWrites = count;
|
|
371
|
+
writeError = error ?? null;
|
|
372
|
+
},
|
|
373
|
+
failNextRemoves: (count, error) => {
|
|
374
|
+
failingRemoves = count;
|
|
375
|
+
removeError = error ?? null;
|
|
376
|
+
},
|
|
377
|
+
failNextLoadIndex: (error) => {
|
|
378
|
+
loadIndexError = error ?? quotaError();
|
|
379
|
+
},
|
|
380
|
+
deferReads: () => {
|
|
381
|
+
isDeferringReads = true;
|
|
382
|
+
},
|
|
383
|
+
flushReads: async () => {
|
|
384
|
+
isDeferringReads = false;
|
|
385
|
+
const pending = deferredReads.splice(0, deferredReads.length);
|
|
386
|
+
for (const resolve of pending) {
|
|
387
|
+
resolve();
|
|
388
|
+
}
|
|
389
|
+
await Promise.resolve();
|
|
390
|
+
},
|
|
391
|
+
deferLoadIndex: () => {
|
|
392
|
+
isDeferringLoadIndex = true;
|
|
393
|
+
},
|
|
394
|
+
flushLoadIndex: async () => {
|
|
395
|
+
isDeferringLoadIndex = false;
|
|
396
|
+
const pending = deferredLoadIndexes.splice(0, deferredLoadIndexes.length);
|
|
397
|
+
for (const resolve of pending) {
|
|
398
|
+
resolve();
|
|
399
|
+
}
|
|
400
|
+
await Promise.resolve();
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
let originalWarn = null;
|
|
406
|
+
let originalError = null;
|
|
407
|
+
const filteredWarn = (...args) => {
|
|
408
|
+
const message = args[0];
|
|
409
|
+
if (typeof message === 'string' && message.includes('auto-refresh')) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
originalWarn?.(...args);
|
|
413
|
+
};
|
|
414
|
+
const filteredError = (...args) => {
|
|
415
|
+
const message = args[0];
|
|
416
|
+
if (message && typeof message === 'object' && 'name' in message && message.name === 'HttpErrorResponse') {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (typeof message === 'string' && message.includes('Failed to decrypt bearer token')) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (typeof message === 'string' && message.includes('Failed to extract tokens from')) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
originalError?.(...args);
|
|
426
|
+
};
|
|
427
|
+
// Capture only what is not already the wrapper, so repeated calls reinstall the one filter instead
|
|
428
|
+
// of nesting a new closure over the previous one and stranding the pristine handlers.
|
|
429
|
+
const installConsoleFilters = () => {
|
|
430
|
+
if (console.warn !== filteredWarn)
|
|
431
|
+
originalWarn = console.warn;
|
|
432
|
+
if (console.error !== filteredError)
|
|
433
|
+
originalError = console.error;
|
|
434
|
+
console.warn = filteredWarn;
|
|
435
|
+
console.error = filteredError;
|
|
436
|
+
};
|
|
437
|
+
const restoreConsole = () => {
|
|
438
|
+
if (originalWarn && console.warn === filteredWarn)
|
|
439
|
+
console.warn = originalWarn;
|
|
440
|
+
if (originalError && console.error === filteredError)
|
|
441
|
+
console.error = originalError;
|
|
442
|
+
originalWarn = null;
|
|
443
|
+
originalError = null;
|
|
444
|
+
};
|
|
445
|
+
const setupQueryTest = (config) => {
|
|
446
|
+
const baseUrl = config?.baseUrl ?? 'https://api.test.com';
|
|
447
|
+
const name = config?.name ?? 'test';
|
|
448
|
+
const mockErrorHandler = config?.mockErrorHandler !== false;
|
|
449
|
+
installConsoleFilters();
|
|
130
450
|
const providers = [
|
|
131
451
|
provideHttpClient(),
|
|
132
452
|
provideHttpClientTesting(),
|
|
@@ -140,8 +460,8 @@ const setupQueryTest = (config) => {
|
|
|
140
460
|
}
|
|
141
461
|
TestBed.configureTestingModule({ providers });
|
|
142
462
|
const queryClientRef = createQueryClient({ baseUrl, name });
|
|
143
|
-
|
|
144
|
-
const
|
|
463
|
+
return TestBed.runInInjectionContext(() => {
|
|
464
|
+
const { inject } = queryClientRef;
|
|
145
465
|
const queryClient = inject();
|
|
146
466
|
if (!queryClient) {
|
|
147
467
|
throw new Error('Failed to create query client in test setup');
|
|
@@ -158,21 +478,104 @@ const setupQueryTest = (config) => {
|
|
|
158
478
|
createPut: createPutQuery(queryClientRef),
|
|
159
479
|
createPatch: createPatchQuery(queryClientRef),
|
|
160
480
|
createDelete: createDeleteQuery(queryClientRef),
|
|
481
|
+
restoreConsole,
|
|
161
482
|
};
|
|
162
483
|
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* A socket io test double, for specs around `createWebSocketClient` - which takes its `io` factory as
|
|
488
|
+
* an option precisely so a spec (or an app that never opens a socket) needs no `socket.io-client`.
|
|
489
|
+
*
|
|
490
|
+
* Nothing here connects on its own: the client's own `connect()` only flips
|
|
491
|
+
* {@link WebSocketTestDouble.state}, and it is {@link WebSocketTestDouble.serverConnect} that fires
|
|
492
|
+
* the `connect` listener. That split is what makes "joined a room while disconnected, and it was
|
|
493
|
+
* re-joined after a reconnect" testable.
|
|
494
|
+
*/
|
|
495
|
+
const createWebSocketTestDouble = () => {
|
|
496
|
+
const sent = [];
|
|
497
|
+
const delivered = [];
|
|
498
|
+
const buffered = [];
|
|
499
|
+
const listeners = new Map();
|
|
500
|
+
const anyListeners = [];
|
|
501
|
+
const outgoingListeners = [];
|
|
502
|
+
let connection = null;
|
|
503
|
+
let withCredentials = null;
|
|
504
|
+
let auth;
|
|
505
|
+
const handshakes = [];
|
|
506
|
+
let connectRequested = false;
|
|
507
|
+
let disconnected = false;
|
|
508
|
+
let connected = false;
|
|
509
|
+
let pingExpired = false;
|
|
510
|
+
let recovered = false;
|
|
511
|
+
const deliver = (message) => {
|
|
512
|
+
for (const listener of outgoingListeners)
|
|
513
|
+
listener(message.event, message.data);
|
|
514
|
+
delivered.push(message);
|
|
515
|
+
};
|
|
516
|
+
const socket = {
|
|
517
|
+
connect: () => void (connectRequested = true),
|
|
518
|
+
disconnect: () => void (disconnected = true),
|
|
519
|
+
emit: (event, data) => {
|
|
520
|
+
sent.push({ event, data });
|
|
521
|
+
if (connected && !pingExpired)
|
|
522
|
+
deliver({ event, data });
|
|
523
|
+
else
|
|
524
|
+
buffered.push({ event, data });
|
|
525
|
+
},
|
|
526
|
+
on: (event, listener) => void listeners.set(event, listener),
|
|
527
|
+
onAny: (listener) => void anyListeners.push(listener),
|
|
528
|
+
onAnyOutgoing: (listener) => void outgoingListeners.push(listener),
|
|
529
|
+
get recovered() {
|
|
530
|
+
return recovered;
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
return {
|
|
534
|
+
io: (url, options) => {
|
|
535
|
+
connection = { url, transports: options.transports };
|
|
536
|
+
withCredentials = options.withCredentials;
|
|
537
|
+
auth = options.auth;
|
|
538
|
+
return socket;
|
|
539
|
+
},
|
|
540
|
+
connection: () => connection,
|
|
541
|
+
withCredentials: () => withCredentials,
|
|
542
|
+
handshakes: () => [...handshakes],
|
|
543
|
+
sent: () => [...sent],
|
|
544
|
+
delivered: () => [...delivered],
|
|
545
|
+
state: () => ({ connectRequested, disconnected }),
|
|
546
|
+
serverConnect: (options) => {
|
|
547
|
+
if (auth)
|
|
548
|
+
auth((data) => handshakes.push(data));
|
|
549
|
+
else
|
|
550
|
+
handshakes.push(null);
|
|
551
|
+
recovered = options?.recovered ?? false;
|
|
552
|
+
connected = true;
|
|
553
|
+
pingExpired = false;
|
|
554
|
+
for (const message of buffered.splice(0))
|
|
555
|
+
deliver(message);
|
|
556
|
+
listeners.get('connect')?.();
|
|
557
|
+
},
|
|
558
|
+
serverDisconnect: () => {
|
|
559
|
+
connected = false;
|
|
560
|
+
pingExpired = false;
|
|
561
|
+
listeners.get('disconnect')?.();
|
|
562
|
+
},
|
|
563
|
+
serverPingExpire: () => void (pingExpired = true),
|
|
564
|
+
serverSend: (message) => {
|
|
565
|
+
const frame = JSON.stringify(message);
|
|
566
|
+
for (const listener of anyListeners)
|
|
567
|
+
listener('message', frame);
|
|
568
|
+
},
|
|
569
|
+
serverSendRaw: (frame) => {
|
|
570
|
+
for (const listener of anyListeners)
|
|
571
|
+
listener('message', frame);
|
|
572
|
+
},
|
|
167
573
|
};
|
|
168
|
-
// Store the restore function for cleanup
|
|
169
|
-
setup._restoreConsole = restoreConsole;
|
|
170
|
-
return setup;
|
|
171
574
|
};
|
|
172
575
|
|
|
173
576
|
/**
|
|
174
577
|
* Generated bundle index. Do not edit.
|
|
175
578
|
*/
|
|
176
579
|
|
|
177
|
-
export { expectAndFlush, expectFlushAndWait, setupAuthTest, setupQueryTest };
|
|
580
|
+
export { createFakeQueryPersistenceStore, createWebSocketTestDouble, expectAndFlush, expectFlushAndWait, flushMultiTabSync, installFakeBroadcastChannel, installFakeWebLocks, setupAuthTest, setupQueryTest };
|
|
178
581
|
//# sourceMappingURL=ethlete-query-testing.mjs.map
|