@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/README.md +297 -2
  3. package/dist/core/auth/handlers.d.ts.map +1 -1
  4. package/dist/core/auth/handlers.js +29 -1
  5. package/dist/core/bot-protection/turnstile-manager.d.ts +0 -1
  6. package/dist/core/bot-protection/turnstile-manager.d.ts.map +1 -1
  7. package/dist/core/bot-protection/turnstile-manager.js +0 -1
  8. package/dist/core/cart/cart-recovery.d.ts +210 -0
  9. package/dist/core/cart/cart-recovery.d.ts.map +1 -0
  10. package/dist/core/cart/cart-recovery.js +271 -0
  11. package/dist/core/index.d.ts +2 -0
  12. package/dist/core/index.d.ts.map +1 -1
  13. package/dist/core/index.js +2 -0
  14. package/dist/react/components/AddToCartButton.d.ts +49 -0
  15. package/dist/react/components/AddToCartButton.d.ts.map +1 -0
  16. package/dist/react/components/AddToCartButton.js +47 -0
  17. package/dist/react/components/CartCount.d.ts +35 -0
  18. package/dist/react/components/CartCount.d.ts.map +1 -0
  19. package/dist/react/components/CartCount.js +23 -0
  20. package/dist/react/components/CartTotals.d.ts +54 -0
  21. package/dist/react/components/CartTotals.d.ts.map +1 -0
  22. package/dist/react/components/CartTotals.js +38 -0
  23. package/dist/react/components/Image.d.ts +42 -0
  24. package/dist/react/components/Image.d.ts.map +1 -0
  25. package/dist/react/components/Image.js +33 -0
  26. package/dist/react/components/Money.d.ts +32 -0
  27. package/dist/react/components/Money.d.ts.map +1 -0
  28. package/dist/react/components/Money.js +27 -0
  29. package/dist/react/components/PriceDisplay.d.ts +34 -0
  30. package/dist/react/components/PriceDisplay.d.ts.map +1 -0
  31. package/dist/react/components/PriceDisplay.js +21 -0
  32. package/dist/react/components/index.d.ts +15 -0
  33. package/dist/react/components/index.d.ts.map +1 -0
  34. package/dist/react/components/index.js +14 -0
  35. package/dist/react/cookies.d.ts +21 -0
  36. package/dist/react/cookies.d.ts.map +1 -1
  37. package/dist/react/cookies.js +29 -1
  38. package/dist/react/hooks/use-auth.d.ts +19 -46
  39. package/dist/react/hooks/use-auth.d.ts.map +1 -1
  40. package/dist/react/hooks/use-auth.js +24 -141
  41. package/dist/react/hooks/use-cart-manager.d.ts +75 -15
  42. package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
  43. package/dist/react/hooks/use-cart-manager.js +106 -194
  44. package/dist/react/hooks/use-login.d.ts +40 -0
  45. package/dist/react/hooks/use-login.d.ts.map +1 -0
  46. package/dist/react/hooks/use-login.js +75 -0
  47. package/dist/react/hooks/use-logout.d.ts +40 -0
  48. package/dist/react/hooks/use-logout.d.ts.map +1 -0
  49. package/dist/react/hooks/use-logout.js +50 -0
  50. package/dist/react/hooks/use-refresh-token.d.ts +40 -0
  51. package/dist/react/hooks/use-refresh-token.d.ts.map +1 -0
  52. package/dist/react/hooks/use-refresh-token.js +66 -0
  53. package/dist/react/index.d.ts +6 -2
  54. package/dist/react/index.d.ts.map +1 -1
  55. package/dist/react/index.js +6 -1
  56. package/dist/react/server/get-storefront-client.d.ts +15 -5
  57. package/dist/react/server/get-storefront-client.d.ts.map +1 -1
  58. package/dist/react/stores/cart.store.d.ts +57 -10
  59. package/dist/react/stores/cart.store.d.ts.map +1 -1
  60. package/dist/react/stores/cart.store.js +112 -21
  61. package/dist/react/stores/store-context.d.ts.map +1 -1
  62. package/dist/react/stores/store-context.js +0 -2
  63. package/package.json +11 -4
  64. package/dist/__tests__/unit/test-helpers.d.ts +0 -46
  65. package/dist/__tests__/unit/test-helpers.d.ts.map +0 -1
  66. 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 implementation via DI (getActions getter).
6
- * Cart ID persisted in cookie (SSR/edge visible) — follows currency store pattern.
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: async (cartId) => api.getCart(cartId),
14
- * createCart: async () => api.createCart().then(c => c.id),
15
- * addLines: async (cartId, lines) => api.addLines(cartId, lines),
16
- * updateLines: async (cartId, lines) => api.updateLines(cartId, lines),
17
- * removeLines: async (cartId, lineIds) => api.removeLines(cartId, lineIds),
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({ getActions: () => actions });
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: shared Promise in closure (not in state)
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
- // Clean up old localStorage entry from pre-cookie era (v1 persist)
41
- if (typeof localStorage !== 'undefined') {
65
+ function emitExpired(event) {
66
+ if (!config.onExpired)
67
+ return;
42
68
  try {
43
- localStorage.removeItem('cart-storage');
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
- catch { /* ignore */ }
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 (auto-init if no cartId)
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
- const cart = await actions.addLines(cartId, lines);
103
- set({ isLoading: false, error: null });
104
- config.onMutationSuccess?.('addToCart', cart);
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 (error if no cartId)
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 (silent return if no cartId)
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,CAczC;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"}
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.0",
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
- }