@doswiftly/storefront-sdk 11.1.0 → 11.3.1
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 +131 -0
- package/README.md +297 -2
- package/dist/core/auth/handlers.d.ts.map +1 -1
- package/dist/core/auth/handlers.js +29 -1
- package/dist/core/bot-protection/turnstile-manager.d.ts +0 -1
- package/dist/core/bot-protection/turnstile-manager.d.ts.map +1 -1
- package/dist/core/bot-protection/turnstile-manager.js +0 -1
- package/dist/core/cart/cart-recovery.d.ts +210 -0
- package/dist/core/cart/cart-recovery.d.ts.map +1 -0
- package/dist/core/cart/cart-recovery.js +271 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/react/components/AddToCartButton.d.ts +49 -0
- package/dist/react/components/AddToCartButton.d.ts.map +1 -0
- package/dist/react/components/AddToCartButton.js +47 -0
- package/dist/react/components/CartCount.d.ts +35 -0
- package/dist/react/components/CartCount.d.ts.map +1 -0
- package/dist/react/components/CartCount.js +23 -0
- package/dist/react/components/CartTotals.d.ts +54 -0
- package/dist/react/components/CartTotals.d.ts.map +1 -0
- package/dist/react/components/CartTotals.js +38 -0
- package/dist/react/components/Image.d.ts +42 -0
- package/dist/react/components/Image.d.ts.map +1 -0
- package/dist/react/components/Image.js +33 -0
- package/dist/react/components/Money.d.ts +32 -0
- package/dist/react/components/Money.d.ts.map +1 -0
- package/dist/react/components/Money.js +27 -0
- package/dist/react/components/PriceDisplay.d.ts +34 -0
- package/dist/react/components/PriceDisplay.d.ts.map +1 -0
- package/dist/react/components/PriceDisplay.js +21 -0
- package/dist/react/components/index.d.ts +15 -0
- package/dist/react/components/index.d.ts.map +1 -0
- package/dist/react/components/index.js +14 -0
- package/dist/react/cookies.d.ts +21 -0
- package/dist/react/cookies.d.ts.map +1 -1
- package/dist/react/cookies.js +29 -1
- package/dist/react/hooks/use-auth.d.ts +19 -46
- package/dist/react/hooks/use-auth.d.ts.map +1 -1
- package/dist/react/hooks/use-auth.js +24 -141
- package/dist/react/hooks/use-cart-manager.d.ts +75 -15
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
- package/dist/react/hooks/use-cart-manager.js +106 -194
- package/dist/react/hooks/use-login.d.ts +40 -0
- package/dist/react/hooks/use-login.d.ts.map +1 -0
- package/dist/react/hooks/use-login.js +75 -0
- package/dist/react/hooks/use-logout.d.ts +40 -0
- package/dist/react/hooks/use-logout.d.ts.map +1 -0
- package/dist/react/hooks/use-logout.js +50 -0
- package/dist/react/hooks/use-refresh-token.d.ts +40 -0
- package/dist/react/hooks/use-refresh-token.d.ts.map +1 -0
- package/dist/react/hooks/use-refresh-token.js +66 -0
- package/dist/react/index.d.ts +6 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +6 -1
- package/dist/react/server/get-storefront-client.d.ts +15 -5
- package/dist/react/server/get-storefront-client.d.ts.map +1 -1
- package/dist/react/stores/cart.store.d.ts +57 -10
- package/dist/react/stores/cart.store.d.ts.map +1 -1
- package/dist/react/stores/cart.store.js +112 -21
- package/dist/react/stores/store-context.d.ts.map +1 -1
- package/dist/react/stores/store-context.js +0 -2
- package/package.json +11 -4
- package/dist/__tests__/unit/test-helpers.d.ts +0 -46
- package/dist/__tests__/unit/test-helpers.d.ts.map +0 -1
- package/dist/__tests__/unit/test-helpers.js +0 -72
|
@@ -1,27 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cart Store — DI-based cart state management with cookie persistence
|
|
2
|
+
* Cart Store — DI-based cart state management with cookie persistence and
|
|
3
|
+
* automatic stale-cart recovery.
|
|
3
4
|
*
|
|
4
|
-
* SDK orchestrates cart lifecycle (init, mutations, error handling).
|
|
5
|
-
* Template provides CartActions
|
|
6
|
-
*
|
|
5
|
+
* SDK orchestrates cart lifecycle (init, mutations, recovery, error handling).
|
|
6
|
+
* Template provides `CartActions` via DI (getActions getter). Cart id persisted
|
|
7
|
+
* in cookie (SSR/edge visible) — follows currency store pattern.
|
|
8
|
+
*
|
|
9
|
+
* Per-operation recovery strategy (DX-first — caller never thinks about it):
|
|
10
|
+
*
|
|
11
|
+
* - **`addToCart`** auto-replays on stale-cart errors. If the template
|
|
12
|
+
* implements the optional `actions.createCartWithLines`, recovery is atomic
|
|
13
|
+
* (single `cartCreate({ lines })` round trip). Otherwise falls back to
|
|
14
|
+
* `createCart()` + `addLines()` (2 round trips, same end result).
|
|
15
|
+
*
|
|
16
|
+
* - **`updateQuantity`** and **`removeFromCart`** bail on stale-cart errors:
|
|
17
|
+
* local cart id is cleared, `onExpired` listeners fire, error surfaces via
|
|
18
|
+
* `onMutationError`. Replaying on a fresh empty cart would silently lose
|
|
19
|
+
* user intent (the lineId no longer exists).
|
|
20
|
+
*
|
|
21
|
+
* Stale-cart detection inspects `err.userErrors[].code` (CART_NOT_FOUND /
|
|
22
|
+
* ALREADY_COMPLETED) — locale-proof, see {@link isCartRecoverableError}.
|
|
7
23
|
*
|
|
8
24
|
* @example
|
|
9
25
|
* ```typescript
|
|
10
26
|
* import { createCartStore, type CartActions } from '@doswiftly/storefront-sdk/react';
|
|
11
27
|
*
|
|
12
28
|
* const actions: CartActions = {
|
|
13
|
-
* fetchCart:
|
|
14
|
-
* createCart:
|
|
15
|
-
* addLines:
|
|
16
|
-
* updateLines:
|
|
17
|
-
* removeLines:
|
|
29
|
+
* fetchCart: (id) => api.getCart(id),
|
|
30
|
+
* createCart: () => api.createCart().then(c => c.id),
|
|
31
|
+
* addLines: (id, lines) => api.addLines(id, lines),
|
|
32
|
+
* updateLines: (id, lines) => api.updateLines(id, lines),
|
|
33
|
+
* removeLines: (id, ids) => api.removeLines(id, ids),
|
|
34
|
+
* // optional — enables atomic add-to-cart recovery
|
|
35
|
+
* createCartWithLines: (lines) => api.cartCreate({ lines }),
|
|
18
36
|
* };
|
|
19
37
|
*
|
|
20
|
-
* const store = createCartStore({
|
|
38
|
+
* const store = createCartStore({
|
|
39
|
+
* getActions: () => actions,
|
|
40
|
+
* onExpired: (e) => toast.error('Koszyk wygasł, dodaj produkty ponownie'),
|
|
41
|
+
* });
|
|
21
42
|
* ```
|
|
22
43
|
*/
|
|
23
44
|
import { createStore } from 'zustand/vanilla';
|
|
24
45
|
import { CART_COOKIE_NAME, CART_COOKIE_MAX_AGE } from '../../core/cart/cookie-config';
|
|
46
|
+
import { isCartRecoverableError, } from '../../core/cart/cart-recovery';
|
|
25
47
|
import { getCookie, setCookie, deleteCookie } from '../cookies';
|
|
26
48
|
// ---------------------------------------------------------------------------
|
|
27
49
|
// Selectors
|
|
@@ -33,16 +55,31 @@ export const selectCartIsLoading = (state) => state.isLoading;
|
|
|
33
55
|
// Factory
|
|
34
56
|
// ---------------------------------------------------------------------------
|
|
35
57
|
export function createCartStore(config) {
|
|
36
|
-
// Deduplication
|
|
58
|
+
// Deduplication for initCart only — first-create collision is the common
|
|
59
|
+
// case (multiple components mounting and seeing an empty cookie). Recovery
|
|
60
|
+
// recreates are NOT deduplicated: each carries its own lines payload and
|
|
61
|
+
// merging would silently drop the second caller's intent.
|
|
37
62
|
let initPromise = null;
|
|
38
63
|
// Read initial cartId from cookie (client-side only, returns null on server)
|
|
39
64
|
const initialCartId = getCookie(CART_COOKIE_NAME);
|
|
40
|
-
|
|
41
|
-
|
|
65
|
+
function emitExpired(event) {
|
|
66
|
+
if (!config.onExpired)
|
|
67
|
+
return;
|
|
42
68
|
try {
|
|
43
|
-
|
|
69
|
+
config.onExpired(event);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Listener must not break recovery flow.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function recreateWithLines(lines) {
|
|
76
|
+
const actions = config.getActions();
|
|
77
|
+
if (actions.createCartWithLines) {
|
|
78
|
+
return actions.createCartWithLines(lines);
|
|
44
79
|
}
|
|
45
|
-
|
|
80
|
+
// Fallback: two-step (create empty cart, then add lines)
|
|
81
|
+
const newCartId = await actions.createCart();
|
|
82
|
+
return actions.addLines(newCartId, lines);
|
|
46
83
|
}
|
|
47
84
|
async function performInit(set, get) {
|
|
48
85
|
const actions = config.getActions();
|
|
@@ -86,11 +123,12 @@ export function createCartStore(config) {
|
|
|
86
123
|
});
|
|
87
124
|
return initPromise;
|
|
88
125
|
},
|
|
89
|
-
// Orchestrated: addToCart
|
|
126
|
+
// Orchestrated: addToCart — auto-replay on stale cart.
|
|
90
127
|
addToCart: async (lines) => {
|
|
91
128
|
const actions = config.getActions();
|
|
92
129
|
set({ isLoading: true, error: null });
|
|
93
130
|
try {
|
|
131
|
+
// Phase 0 — ensure cart exists.
|
|
94
132
|
let cartId = get().cartId;
|
|
95
133
|
if (!cartId) {
|
|
96
134
|
await get().initCart();
|
|
@@ -99,16 +137,45 @@ export function createCartStore(config) {
|
|
|
99
137
|
if (!cartId) {
|
|
100
138
|
throw new Error('Failed to initialize cart');
|
|
101
139
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
140
|
+
// Phase 1 — happy path.
|
|
141
|
+
try {
|
|
142
|
+
const cart = await actions.addLines(cartId, lines);
|
|
143
|
+
set({ isLoading: false, error: null });
|
|
144
|
+
config.onMutationSuccess?.('addToCart', cart);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
if (!isCartRecoverableError(err))
|
|
149
|
+
throw err;
|
|
150
|
+
// Phase 2 — auto-replay against a fresh cart.
|
|
151
|
+
const oldCartId = cartId;
|
|
152
|
+
try {
|
|
153
|
+
const cart = await recreateWithLines(lines);
|
|
154
|
+
set({ cartId: cart.id, isLoading: false, error: null });
|
|
155
|
+
config.onMutationSuccess?.('addToCart', cart);
|
|
156
|
+
}
|
|
157
|
+
catch (recoverErr) {
|
|
158
|
+
const reason = isCartRecoverableError(recoverErr)
|
|
159
|
+
? 'retry-also-failed'
|
|
160
|
+
: 'recreate-failed';
|
|
161
|
+
// Clear local cart id so next interaction starts clean.
|
|
162
|
+
set({ cartId: null, isLoading: false, error: recoverErr });
|
|
163
|
+
config.onMutationError?.('addToCart', recoverErr);
|
|
164
|
+
emitExpired({
|
|
165
|
+
reason,
|
|
166
|
+
oldCartId,
|
|
167
|
+
operation: 'addToCart',
|
|
168
|
+
cause: recoverErr,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
105
172
|
}
|
|
106
173
|
catch (error) {
|
|
107
174
|
set({ error, isLoading: false });
|
|
108
175
|
config.onMutationError?.('addToCart', error);
|
|
109
176
|
}
|
|
110
177
|
},
|
|
111
|
-
// Orchestrated: updateQuantity (
|
|
178
|
+
// Orchestrated: updateQuantity — bail on stale cart (lineId is dead).
|
|
112
179
|
updateQuantity: async (lines) => {
|
|
113
180
|
const actions = config.getActions();
|
|
114
181
|
const cartId = get().cartId;
|
|
@@ -125,11 +192,24 @@ export function createCartStore(config) {
|
|
|
125
192
|
config.onMutationSuccess?.('updateQuantity', cart);
|
|
126
193
|
}
|
|
127
194
|
catch (error) {
|
|
195
|
+
if (isCartRecoverableError(error)) {
|
|
196
|
+
// Clear local cart — replaying on a fresh cart would lose the user's
|
|
197
|
+
// intent (the lineId no longer exists).
|
|
198
|
+
set({ cartId: null, isLoading: false, error });
|
|
199
|
+
config.onMutationError?.('updateQuantity', error);
|
|
200
|
+
emitExpired({
|
|
201
|
+
reason: 'state-dependent',
|
|
202
|
+
oldCartId: cartId,
|
|
203
|
+
operation: 'updateQuantity',
|
|
204
|
+
cause: error,
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
128
208
|
set({ error, isLoading: false });
|
|
129
209
|
config.onMutationError?.('updateQuantity', error);
|
|
130
210
|
}
|
|
131
211
|
},
|
|
132
|
-
// Orchestrated: removeFromCart
|
|
212
|
+
// Orchestrated: removeFromCart — bail on stale cart (lineId is dead).
|
|
133
213
|
removeFromCart: async (lineIds) => {
|
|
134
214
|
const cartId = get().cartId;
|
|
135
215
|
if (!cartId)
|
|
@@ -142,6 +222,17 @@ export function createCartStore(config) {
|
|
|
142
222
|
config.onMutationSuccess?.('removeFromCart', cart);
|
|
143
223
|
}
|
|
144
224
|
catch (error) {
|
|
225
|
+
if (isCartRecoverableError(error)) {
|
|
226
|
+
set({ cartId: null, isLoading: false, error });
|
|
227
|
+
config.onMutationError?.('removeFromCart', error);
|
|
228
|
+
emitExpired({
|
|
229
|
+
reason: 'state-dependent',
|
|
230
|
+
oldCartId: cartId,
|
|
231
|
+
operation: 'removeFromCart',
|
|
232
|
+
cause: error,
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
145
236
|
set({ error, isLoading: false });
|
|
146
237
|
config.onMutationError?.('removeFromCart', error);
|
|
147
238
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store-context.d.ts","sourceRoot":"","sources":["../../../src/react/stores/store-context.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAMtD,eAAO,MAAM,gBAAgB,qDAAkD,CAAC;AAChF,eAAO,MAAM,oBAAoB,yDAAsD,CAAC;AACxF,eAAO,MAAM,oBAAoB,yDAAsD,CAAC;AAMxF,wBAAgB,YAAY,IAAI,SAAS,CAAC;AAC1C,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,SAAS,KAAK,CAAC,GAAG,CAAC,CAAC;AAQlE,wBAAgB,eAAe,IAAI,QAAQ,CAAC,SAAS,CAAC,CAIrD;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,OAAO,
|
|
1
|
+
{"version":3,"file":"store-context.d.ts","sourceRoot":"","sources":["../../../src/react/stores/store-context.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAMtD,eAAO,MAAM,gBAAgB,qDAAkD,CAAC;AAChF,eAAO,MAAM,oBAAoB,yDAAsD,CAAC;AACxF,eAAO,MAAM,oBAAoB,yDAAsD,CAAC;AAMxF,wBAAgB,YAAY,IAAI,SAAS,CAAC;AAC1C,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,SAAS,KAAK,CAAC,GAAG,CAAC,CAAC;AAQlE,wBAAgB,eAAe,IAAI,QAAQ,CAAC,SAAS,CAAC,CAIrD;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAoBzC;AAMD,wBAAgB,gBAAgB,IAAI,aAAa,CAAC;AAClD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,CAAC,GAAG,CAAC,CAAC;AAQ1E,wBAAgB,mBAAmB,IAAI,QAAQ,CAAC,aAAa,CAAC,CAI7D;AAMD,wBAAgB,gBAAgB,IAAI,aAAa,CAAC;AAClD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,CAAC,GAAG,CAAC,CAAC;AAQ1E,wBAAgB,mBAAmB,IAAI,QAAQ,CAAC,aAAa,CAAC,CAI7D"}
|
|
@@ -39,9 +39,7 @@ export function useAuthHydrated() {
|
|
|
39
39
|
const [hydrated, setHydrated] = useState(false);
|
|
40
40
|
useEffect(() => {
|
|
41
41
|
const persistApi = store.persist;
|
|
42
|
-
// Subscribe to future hydration completions
|
|
43
42
|
const unsubFinish = persistApi.onFinishHydration(() => setHydrated(true));
|
|
44
|
-
// Check if hydration already completed before effect ran
|
|
45
43
|
if (persistApi.hasHydrated())
|
|
46
44
|
setHydrated(true);
|
|
47
45
|
return unsubFinish;
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doswiftly/storefront-sdk",
|
|
3
|
-
"version": "11.1
|
|
3
|
+
"version": "11.3.1",
|
|
4
4
|
"description": "Storefront runtime SDK for DoSwiftly Commerce — layered transport, middleware pipeline, React providers, Zustand stores, cache strategies. 0 runtime dependencies in core.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"files": [
|
|
7
8
|
"dist",
|
|
8
9
|
"README.md",
|
|
@@ -39,16 +40,20 @@
|
|
|
39
40
|
],
|
|
40
41
|
"author": "DoSwiftly Team",
|
|
41
42
|
"license": "MIT",
|
|
42
|
-
"dependencies": {},
|
|
43
43
|
"devDependencies": {
|
|
44
|
+
"@testing-library/react": "^16.1.0",
|
|
44
45
|
"@types/node": "^22.10.2",
|
|
45
46
|
"@types/react": "^18.3.0 || ^19.0.0",
|
|
47
|
+
"@types/react-dom": "^19.0.0",
|
|
48
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
46
49
|
"fast-check": "^3.23.2",
|
|
50
|
+
"jsdom": "^25.0.1",
|
|
47
51
|
"next": "^16.2.3",
|
|
52
|
+
"react": "^19.0.0",
|
|
53
|
+
"react-dom": "^19.0.0",
|
|
48
54
|
"typescript": "^5.7.2",
|
|
49
55
|
"vitest": "^4.1.0",
|
|
50
|
-
"zustand": "^5.0.2"
|
|
51
|
-
"react": "^19.0.0"
|
|
56
|
+
"zustand": "^5.0.2"
|
|
52
57
|
},
|
|
53
58
|
"peerDependencies": {
|
|
54
59
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -71,6 +76,8 @@
|
|
|
71
76
|
"test:watch": "vitest",
|
|
72
77
|
"test:unit": "vitest run src/__tests__/unit/",
|
|
73
78
|
"test:contract": "vitest run src/__tests__/contract/",
|
|
79
|
+
"test:coverage": "vitest run --coverage",
|
|
80
|
+
"doctor": "node scripts/doctor.cjs",
|
|
74
81
|
"validate:cart": "node scripts/validate-cart-operations.cjs --strict"
|
|
75
82
|
}
|
|
76
83
|
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared test helpers — mock fetch factory, mock client builder.
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Create a mock fetch that returns the specified GraphQL response.
|
|
6
|
-
*/
|
|
7
|
-
export declare function createMockFetch(responseData: unknown, options?: {
|
|
8
|
-
errors?: Array<{
|
|
9
|
-
message: string;
|
|
10
|
-
}>;
|
|
11
|
-
status?: number;
|
|
12
|
-
delay?: number;
|
|
13
|
-
}): typeof globalThis.fetch;
|
|
14
|
-
/**
|
|
15
|
-
* Create a mock fetch that tracks all calls and returns specified response.
|
|
16
|
-
*/
|
|
17
|
-
export declare function createSpyFetch(responseData: unknown, options?: {
|
|
18
|
-
errors?: Array<{
|
|
19
|
-
message: string;
|
|
20
|
-
}>;
|
|
21
|
-
status?: number;
|
|
22
|
-
}): {
|
|
23
|
-
fetch: typeof globalThis.fetch;
|
|
24
|
-
calls: {
|
|
25
|
-
url: string;
|
|
26
|
-
init: RequestInit;
|
|
27
|
-
}[];
|
|
28
|
-
};
|
|
29
|
-
/**
|
|
30
|
-
* Create a mock fetch that fails with a network error.
|
|
31
|
-
*/
|
|
32
|
-
export declare function createNetworkErrorFetch(errorMessage?: string): typeof globalThis.fetch;
|
|
33
|
-
/**
|
|
34
|
-
* Create a mock fetch that returns different responses on subsequent calls.
|
|
35
|
-
*/
|
|
36
|
-
export declare function createSequenceFetch(responses: Array<{
|
|
37
|
-
data?: unknown;
|
|
38
|
-
errors?: Array<{
|
|
39
|
-
message: string;
|
|
40
|
-
}>;
|
|
41
|
-
status?: number;
|
|
42
|
-
}>): {
|
|
43
|
-
fetch: typeof globalThis.fetch;
|
|
44
|
-
callCount: () => number;
|
|
45
|
-
};
|
|
46
|
-
//# sourceMappingURL=test-helpers.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/test-helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;GAEG;AACH,wBAAgB,eAAe,CAC7B,YAAY,EAAE,OAAO,EACrB,OAAO,CAAC,EAAE;IACR,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,OAAO,UAAU,CAAC,KAAK,CAqBzB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,YAAY,EAAE,OAAO,EACrB,OAAO,CAAC,EAAE;IACR,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;WAqB0B,OAAO,UAAU,CAAC,KAAK;;aAnBxB,MAAM;cAAQ,WAAW;;EAoBpD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,SAAiB,GAAG,OAAO,UAAU,CAAC,KAAK,CAI9F;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GACzF;IAAE,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAAC,SAAS,EAAE,MAAM,MAAM,CAAA;CAAE,CAiB7D"}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared test helpers — mock fetch factory, mock client builder.
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Create a mock fetch that returns the specified GraphQL response.
|
|
6
|
-
*/
|
|
7
|
-
export function createMockFetch(responseData, options) {
|
|
8
|
-
const { errors, status = 200, delay: delayMs } = options ?? {};
|
|
9
|
-
return async (_url, _init) => {
|
|
10
|
-
if (delayMs) {
|
|
11
|
-
await new Promise(r => setTimeout(r, delayMs));
|
|
12
|
-
}
|
|
13
|
-
// Check for abort signal
|
|
14
|
-
if (_init?.signal?.aborted) {
|
|
15
|
-
throw new DOMException('The operation was aborted', 'AbortError');
|
|
16
|
-
}
|
|
17
|
-
const body = { data: responseData };
|
|
18
|
-
if (errors)
|
|
19
|
-
body.errors = errors;
|
|
20
|
-
return new Response(JSON.stringify(body), {
|
|
21
|
-
status,
|
|
22
|
-
headers: { 'Content-Type': 'application/json' },
|
|
23
|
-
});
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Create a mock fetch that tracks all calls and returns specified response.
|
|
28
|
-
*/
|
|
29
|
-
export function createSpyFetch(responseData, options) {
|
|
30
|
-
const calls = [];
|
|
31
|
-
const { errors, status = 200 } = options ?? {};
|
|
32
|
-
const fetchFn = async (url, init) => {
|
|
33
|
-
calls.push({ url: url.toString(), init: init });
|
|
34
|
-
if (init?.signal?.aborted) {
|
|
35
|
-
throw new DOMException('The operation was aborted', 'AbortError');
|
|
36
|
-
}
|
|
37
|
-
const body = { data: responseData };
|
|
38
|
-
if (errors)
|
|
39
|
-
body.errors = errors;
|
|
40
|
-
return new Response(JSON.stringify(body), {
|
|
41
|
-
status,
|
|
42
|
-
headers: { 'Content-Type': 'application/json' },
|
|
43
|
-
});
|
|
44
|
-
};
|
|
45
|
-
return { fetch: fetchFn, calls };
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Create a mock fetch that fails with a network error.
|
|
49
|
-
*/
|
|
50
|
-
export function createNetworkErrorFetch(errorMessage = 'fetch failed') {
|
|
51
|
-
return async () => {
|
|
52
|
-
throw new TypeError(errorMessage);
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Create a mock fetch that returns different responses on subsequent calls.
|
|
57
|
-
*/
|
|
58
|
-
export function createSequenceFetch(responses) {
|
|
59
|
-
let index = 0;
|
|
60
|
-
const fetchFn = async (_url, _init) => {
|
|
61
|
-
const response = responses[Math.min(index, responses.length - 1)];
|
|
62
|
-
index++;
|
|
63
|
-
const body = { data: response.data ?? null };
|
|
64
|
-
if (response.errors)
|
|
65
|
-
body.errors = response.errors;
|
|
66
|
-
return new Response(JSON.stringify(body), {
|
|
67
|
-
status: response.status ?? 200,
|
|
68
|
-
headers: { 'Content-Type': 'application/json' },
|
|
69
|
-
});
|
|
70
|
-
};
|
|
71
|
-
return { fetch: fetchFn, callCount: () => index };
|
|
72
|
-
}
|