@doswiftly/storefront-sdk 11.2.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 +98 -0
- package/README.md +214 -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/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/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 +34 -0
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
- package/dist/react/hooks/use-cart-manager.js +22 -19
- 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 +5 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -0
- 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.map +1 -1
- package/dist/react/stores/cart.store.js +0 -7
- package/dist/react/stores/store-context.d.ts.map +1 -1
- package/dist/react/stores/store-context.js +0 -2
- package/package.json +5 -1
- 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
- package/dist/__tests__/unit/use-cart-manager.test.d.ts +0 -2
- package/dist/__tests__/unit/use-cart-manager.test.d.ts.map +0 -1
- package/dist/__tests__/unit/use-cart-manager.test.js +0 -400
|
@@ -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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use-cart-manager.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/use-cart-manager.test.tsx"],"names":[],"mappings":""}
|
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
// @vitest-environment jsdom
|
|
3
|
-
/**
|
|
4
|
-
* Unit tests for useCartManager — DX-first cart hook with automatic stale-cart
|
|
5
|
-
* recovery.
|
|
6
|
-
*
|
|
7
|
-
* Mocks the StorefrontClientContext directly (no full StorefrontProvider stack)
|
|
8
|
-
* so each test owns its CartClient spy and can simulate recoverable / fatal
|
|
9
|
-
* errors per-mutation. Recovery semantics under the hood are covered by
|
|
10
|
-
* cart-recovery.test.ts — these tests focus on the React wrapper:
|
|
11
|
-
*
|
|
12
|
-
* - per-operation taxonomy reaches the right code path (replay vs bail)
|
|
13
|
-
* - onExpired subscribers fire on bail / recreate-fail
|
|
14
|
-
* - isLoading / error state transitions are correct
|
|
15
|
-
* - cookie-based cart id persistence works through document.cookie
|
|
16
|
-
*
|
|
17
|
-
* Note: hook is rendered via `renderHook` from @testing-library/react. Awaits
|
|
18
|
-
* inside `act(async () => { ... })` flush React effects before assertions.
|
|
19
|
-
*/
|
|
20
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
21
|
-
import { renderHook, act } from '@testing-library/react';
|
|
22
|
-
import { useCartManager } from '../../react/hooks/use-cart-manager';
|
|
23
|
-
import { StorefrontError, ErrorCodes } from '../../core/errors';
|
|
24
|
-
import { CartClient } from '../../core/cart/cart-client';
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Mock StorefrontClientContext — supply our own cartClient per test
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
let activeCartClient;
|
|
29
|
-
vi.mock('../../react/providers/storefront-client-provider', () => ({
|
|
30
|
-
useStorefrontClientContext: () => ({
|
|
31
|
-
client: {},
|
|
32
|
-
cartClient: activeCartClient,
|
|
33
|
-
authClient: {},
|
|
34
|
-
}),
|
|
35
|
-
}));
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
// Fixtures
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
function mockStorefrontClient() {
|
|
40
|
-
return {
|
|
41
|
-
query: vi.fn(async () => ({})),
|
|
42
|
-
mutate: vi.fn(async () => ({})),
|
|
43
|
-
use: vi.fn(),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
const fakeCart = (id = 'cart-fresh') => ({
|
|
47
|
-
id,
|
|
48
|
-
lines: { edges: [], nodes: [], pageInfo: {}, totalCount: 0 },
|
|
49
|
-
totalQuantity: 0,
|
|
50
|
-
cost: { total: { amount: '0', currencyCode: 'PLN' }, subtotal: { amount: '0', currencyCode: 'PLN' } },
|
|
51
|
-
});
|
|
52
|
-
const FAKE_CART = fakeCart();
|
|
53
|
-
function recoverableError(code, message) {
|
|
54
|
-
return new StorefrontError({
|
|
55
|
-
code: ErrorCodes.USER_ERROR,
|
|
56
|
-
message: message ?? 'User errors in response',
|
|
57
|
-
userErrors: [{ message: message ?? 'simulated', code, field: [] }],
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
function nonRecoverableError() {
|
|
61
|
-
return new StorefrontError({
|
|
62
|
-
code: ErrorCodes.USER_ERROR,
|
|
63
|
-
message: 'Out of stock',
|
|
64
|
-
userErrors: [{ message: 'low stock', code: 'NOT_ENOUGH_IN_STOCK', field: [] }],
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
function setCartCookie(value) {
|
|
68
|
-
if (value === null) {
|
|
69
|
-
document.cookie = 'cart-id=;max-age=0;path=/';
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
document.cookie = `cart-id=${value};path=/`;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function getCartCookie() {
|
|
76
|
-
const match = document.cookie.match(/(?:^|;\s*)cart-id=([^;]*)/);
|
|
77
|
-
return match ? decodeURIComponent(match[1]) : null;
|
|
78
|
-
}
|
|
79
|
-
const Wrapper = ({ children }) => _jsx(_Fragment, { children: children });
|
|
80
|
-
beforeEach(() => {
|
|
81
|
-
activeCartClient = new CartClient(mockStorefrontClient());
|
|
82
|
-
setCartCookie(null);
|
|
83
|
-
});
|
|
84
|
-
afterEach(() => {
|
|
85
|
-
vi.restoreAllMocks();
|
|
86
|
-
setCartCookie(null);
|
|
87
|
-
});
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
// addItem — auto-replay
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
describe('useCartManager — addItem (auto-replay)', () => {
|
|
92
|
-
it('creates cart on first add when cookie empty (Phase 0)', async () => {
|
|
93
|
-
const createSpy = vi.spyOn(activeCartClient, 'create').mockResolvedValue({ cart: FAKE_CART, warnings: [] });
|
|
94
|
-
const addSpy = vi.spyOn(activeCartClient, 'addItems').mockResolvedValue({ cart: FAKE_CART, warnings: [] });
|
|
95
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
96
|
-
await act(async () => {
|
|
97
|
-
await result.current.addItem([{ variantId: 'v1', quantity: 1 }]);
|
|
98
|
-
});
|
|
99
|
-
expect(createSpy).toHaveBeenCalledTimes(1);
|
|
100
|
-
expect(addSpy).toHaveBeenCalledWith('cart-fresh', [{ variantId: 'v1', quantity: 1 }]);
|
|
101
|
-
expect(getCartCookie()).toBe('cart-fresh');
|
|
102
|
-
});
|
|
103
|
-
it('uses cookie cart-id on subsequent adds', async () => {
|
|
104
|
-
setCartCookie('existing-cart');
|
|
105
|
-
const createSpy = vi.spyOn(activeCartClient, 'create');
|
|
106
|
-
const addSpy = vi.spyOn(activeCartClient, 'addItems').mockResolvedValue({ cart: FAKE_CART, warnings: [] });
|
|
107
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
108
|
-
await act(async () => {
|
|
109
|
-
await result.current.addItem([{ variantId: 'v1', quantity: 1 }]);
|
|
110
|
-
});
|
|
111
|
-
expect(createSpy).not.toHaveBeenCalled();
|
|
112
|
-
expect(addSpy).toHaveBeenCalledWith('existing-cart', [{ variantId: 'v1', quantity: 1 }]);
|
|
113
|
-
});
|
|
114
|
-
it('auto-recovers on CART_NOT_FOUND via atomic cartCreate({ lines })', async () => {
|
|
115
|
-
setCartCookie('stale-cart');
|
|
116
|
-
const stale = recoverableError('CART_NOT_FOUND');
|
|
117
|
-
vi.spyOn(activeCartClient, 'addItems').mockRejectedValue(stale);
|
|
118
|
-
const createSpy = vi.spyOn(activeCartClient, 'create').mockResolvedValue({
|
|
119
|
-
cart: fakeCart('cart-recovered'),
|
|
120
|
-
warnings: [],
|
|
121
|
-
});
|
|
122
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
123
|
-
await act(async () => {
|
|
124
|
-
const outcome = await result.current.addItem([{ variantId: 'v1', quantity: 2 }]);
|
|
125
|
-
expect(outcome.cart.id).toBe('cart-recovered');
|
|
126
|
-
});
|
|
127
|
-
// Atomic recreate: cartCreate({ lines: [...] }) — NOT empty create + addItems
|
|
128
|
-
expect(createSpy).toHaveBeenCalledWith({ lines: [{ variantId: 'v1', quantity: 2 }] });
|
|
129
|
-
expect(getCartCookie()).toBe('cart-recovered');
|
|
130
|
-
});
|
|
131
|
-
it('auto-recovers on ALREADY_COMPLETED (post-checkout locked cart)', async () => {
|
|
132
|
-
setCartCookie('locked-cart');
|
|
133
|
-
vi.spyOn(activeCartClient, 'addItems').mockRejectedValue(recoverableError('ALREADY_COMPLETED'));
|
|
134
|
-
vi.spyOn(activeCartClient, 'create').mockResolvedValue({
|
|
135
|
-
cart: fakeCart('post-checkout-cart'),
|
|
136
|
-
warnings: [],
|
|
137
|
-
});
|
|
138
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
139
|
-
await act(async () => {
|
|
140
|
-
await result.current.addItem([{ variantId: 'v1' }]);
|
|
141
|
-
});
|
|
142
|
-
expect(getCartCookie()).toBe('post-checkout-cart');
|
|
143
|
-
});
|
|
144
|
-
it('PL locale message does not break detection (regression for legacy useCartManager bug)', async () => {
|
|
145
|
-
setCartCookie('stale');
|
|
146
|
-
vi.spyOn(activeCartClient, 'addItems').mockRejectedValue(recoverableError('CART_NOT_FOUND', 'Nie znaleziono koszyka'));
|
|
147
|
-
const createSpy = vi.spyOn(activeCartClient, 'create').mockResolvedValue({
|
|
148
|
-
cart: fakeCart('pl-recovered'),
|
|
149
|
-
warnings: [],
|
|
150
|
-
});
|
|
151
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
152
|
-
await act(async () => {
|
|
153
|
-
await result.current.addItem([{ variantId: 'v1' }]);
|
|
154
|
-
});
|
|
155
|
-
expect(createSpy).toHaveBeenCalledTimes(1);
|
|
156
|
-
});
|
|
157
|
-
it('propagates non-recoverable errors (stock) without recovery', async () => {
|
|
158
|
-
setCartCookie('cart-1');
|
|
159
|
-
const stock = nonRecoverableError();
|
|
160
|
-
vi.spyOn(activeCartClient, 'addItems').mockRejectedValue(stock);
|
|
161
|
-
const createSpy = vi.spyOn(activeCartClient, 'create');
|
|
162
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
163
|
-
await act(async () => {
|
|
164
|
-
await expect(result.current.addItem([{ variantId: 'oos' }])).rejects.toBe(stock);
|
|
165
|
-
});
|
|
166
|
-
expect(createSpy).not.toHaveBeenCalled();
|
|
167
|
-
expect(getCartCookie()).toBe('cart-1');
|
|
168
|
-
expect(result.current.error).toBe('Out of stock');
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
|
-
// updateItem / removeItem — bail
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
describe('useCartManager — updateItem / removeItem (bail)', () => {
|
|
175
|
-
it('updateItem on CART_NOT_FOUND throws CartRecoveryNotPossibleError + fires onExpired', async () => {
|
|
176
|
-
setCartCookie('stale');
|
|
177
|
-
vi.spyOn(activeCartClient, 'updateItems').mockRejectedValue(recoverableError('CART_NOT_FOUND'));
|
|
178
|
-
const createSpy = vi.spyOn(activeCartClient, 'create');
|
|
179
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
180
|
-
const listener = vi.fn();
|
|
181
|
-
act(() => {
|
|
182
|
-
result.current.onExpired(listener);
|
|
183
|
-
});
|
|
184
|
-
await act(async () => {
|
|
185
|
-
await expect(result.current.updateItem([{ id: 'line-1', quantity: 5 }])).rejects.toMatchObject({ name: 'CartRecoveryNotPossibleError', reason: 'state-dependent' });
|
|
186
|
-
});
|
|
187
|
-
expect(createSpy).not.toHaveBeenCalled(); // no replay — operation is state-dependent
|
|
188
|
-
expect(getCartCookie()).toBeNull(); // cookie cleared
|
|
189
|
-
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ reason: 'state-dependent', operation: 'updateItem' }));
|
|
190
|
-
});
|
|
191
|
-
it('removeItem on ALREADY_COMPLETED bails the same way', async () => {
|
|
192
|
-
setCartCookie('locked');
|
|
193
|
-
vi.spyOn(activeCartClient, 'removeItems').mockRejectedValue(recoverableError('ALREADY_COMPLETED'));
|
|
194
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
195
|
-
const listener = vi.fn();
|
|
196
|
-
act(() => {
|
|
197
|
-
result.current.onExpired(listener);
|
|
198
|
-
});
|
|
199
|
-
await act(async () => {
|
|
200
|
-
await expect(result.current.removeItem(['line-1'])).rejects.toMatchObject({
|
|
201
|
-
name: 'CartRecoveryNotPossibleError',
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ operation: 'removeItem' }));
|
|
205
|
-
});
|
|
206
|
-
it('updateItem propagates non-recoverable error without bail', async () => {
|
|
207
|
-
setCartCookie('cart-1');
|
|
208
|
-
const validation = new Error('Quantity must be positive');
|
|
209
|
-
vi.spyOn(activeCartClient, 'updateItems').mockRejectedValue(validation);
|
|
210
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
211
|
-
const listener = vi.fn();
|
|
212
|
-
act(() => {
|
|
213
|
-
result.current.onExpired(listener);
|
|
214
|
-
});
|
|
215
|
-
await act(async () => {
|
|
216
|
-
await expect(result.current.updateItem([{ id: 'l1', quantity: -1 }])).rejects.toBe(validation);
|
|
217
|
-
});
|
|
218
|
-
expect(getCartCookie()).toBe('cart-1'); // cookie not cleared — cart still good
|
|
219
|
-
expect(listener).not.toHaveBeenCalled();
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
// ---------------------------------------------------------------------------
|
|
223
|
-
// Other auto-replay mutations
|
|
224
|
-
// ---------------------------------------------------------------------------
|
|
225
|
-
describe('useCartManager — checkout-flow mutations (auto-replay)', () => {
|
|
226
|
-
it('updateDiscountCodes auto-replays via cartCreate({ discountCodes })', async () => {
|
|
227
|
-
setCartCookie('stale');
|
|
228
|
-
vi.spyOn(activeCartClient, 'updateDiscountCodes').mockRejectedValue(recoverableError('CART_NOT_FOUND'));
|
|
229
|
-
const createSpy = vi.spyOn(activeCartClient, 'create').mockResolvedValue({
|
|
230
|
-
cart: fakeCart('rec'),
|
|
231
|
-
warnings: [],
|
|
232
|
-
});
|
|
233
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
234
|
-
await act(async () => {
|
|
235
|
-
await result.current.updateDiscountCodes(['SAVE10', 'FREESHIP']);
|
|
236
|
-
});
|
|
237
|
-
expect(createSpy).toHaveBeenCalledWith({ discountCodes: ['SAVE10', 'FREESHIP'] });
|
|
238
|
-
});
|
|
239
|
-
it('updateNote auto-replays via cartCreate({ note })', async () => {
|
|
240
|
-
setCartCookie('stale');
|
|
241
|
-
vi.spyOn(activeCartClient, 'updateNote').mockRejectedValue(recoverableError('CART_NOT_FOUND'));
|
|
242
|
-
const createSpy = vi.spyOn(activeCartClient, 'create').mockResolvedValue({
|
|
243
|
-
cart: fakeCart('rec'),
|
|
244
|
-
warnings: [],
|
|
245
|
-
});
|
|
246
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
247
|
-
await act(async () => {
|
|
248
|
-
await result.current.updateNote('Gift wrap');
|
|
249
|
-
});
|
|
250
|
-
expect(createSpy).toHaveBeenCalledWith({ note: 'Gift wrap' });
|
|
251
|
-
});
|
|
252
|
-
it('updateBuyerIdentity auto-replays via cartCreate({ buyerIdentity })', async () => {
|
|
253
|
-
setCartCookie('stale');
|
|
254
|
-
vi.spyOn(activeCartClient, 'updateBuyerIdentity').mockRejectedValue(recoverableError('CART_NOT_FOUND'));
|
|
255
|
-
const createSpy = vi.spyOn(activeCartClient, 'create').mockResolvedValue({
|
|
256
|
-
cart: fakeCart('rec'),
|
|
257
|
-
warnings: [],
|
|
258
|
-
});
|
|
259
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
260
|
-
await act(async () => {
|
|
261
|
-
await result.current.updateBuyerIdentity({ email: 'user@example.com' });
|
|
262
|
-
});
|
|
263
|
-
expect(createSpy).toHaveBeenCalledWith({ buyerIdentity: { email: 'user@example.com' } });
|
|
264
|
-
});
|
|
265
|
-
it('setShippingAddress auto-replays via cartCreate({ shippingAddress })', async () => {
|
|
266
|
-
setCartCookie('stale');
|
|
267
|
-
vi.spyOn(activeCartClient, 'setShippingAddress').mockRejectedValue(recoverableError('CART_NOT_FOUND'));
|
|
268
|
-
const createSpy = vi.spyOn(activeCartClient, 'create').mockResolvedValue({
|
|
269
|
-
cart: fakeCart('rec'),
|
|
270
|
-
warnings: [],
|
|
271
|
-
});
|
|
272
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
273
|
-
const address = {
|
|
274
|
-
firstName: 'Anna',
|
|
275
|
-
lastName: 'Nowak',
|
|
276
|
-
streetLine1: 'ul. Krakowska 1',
|
|
277
|
-
city: 'Warszawa',
|
|
278
|
-
country: 'PL',
|
|
279
|
-
postalCode: '00-001',
|
|
280
|
-
};
|
|
281
|
-
await act(async () => {
|
|
282
|
-
await result.current.setShippingAddress(address);
|
|
283
|
-
});
|
|
284
|
-
expect(createSpy).toHaveBeenCalledWith({ shippingAddress: address });
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
// ---------------------------------------------------------------------------
|
|
288
|
-
// getCart — read with auto-clear
|
|
289
|
-
// ---------------------------------------------------------------------------
|
|
290
|
-
describe('useCartManager — getCart', () => {
|
|
291
|
-
it('returns cart on success', async () => {
|
|
292
|
-
setCartCookie('existing');
|
|
293
|
-
vi.spyOn(activeCartClient, 'get').mockResolvedValue(FAKE_CART);
|
|
294
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
295
|
-
let cart;
|
|
296
|
-
await act(async () => {
|
|
297
|
-
cart = await result.current.getCart();
|
|
298
|
-
});
|
|
299
|
-
expect(cart).toEqual(FAKE_CART);
|
|
300
|
-
});
|
|
301
|
-
it('returns null + clears cookie when backend returns CART_NOT_FOUND via userErrors', async () => {
|
|
302
|
-
setCartCookie('stale');
|
|
303
|
-
vi.spyOn(activeCartClient, 'get').mockRejectedValue(recoverableError('CART_NOT_FOUND'));
|
|
304
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
305
|
-
const listener = vi.fn();
|
|
306
|
-
act(() => {
|
|
307
|
-
result.current.onExpired(listener);
|
|
308
|
-
});
|
|
309
|
-
let cart;
|
|
310
|
-
await act(async () => {
|
|
311
|
-
cart = await result.current.getCart();
|
|
312
|
-
});
|
|
313
|
-
expect(cart).toBeNull();
|
|
314
|
-
expect(getCartCookie()).toBeNull();
|
|
315
|
-
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ reason: 'state-dependent', operation: 'getCart' }));
|
|
316
|
-
});
|
|
317
|
-
it('returns null without fetching when cookie is empty', async () => {
|
|
318
|
-
const getSpy = vi.spyOn(activeCartClient, 'get');
|
|
319
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
320
|
-
let cart;
|
|
321
|
-
await act(async () => {
|
|
322
|
-
cart = await result.current.getCart();
|
|
323
|
-
});
|
|
324
|
-
expect(cart).toBeNull();
|
|
325
|
-
expect(getSpy).not.toHaveBeenCalled();
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
// ---------------------------------------------------------------------------
|
|
329
|
-
// onExpired lifecycle + reactive state
|
|
330
|
-
// ---------------------------------------------------------------------------
|
|
331
|
-
describe('useCartManager — onExpired + state', () => {
|
|
332
|
-
it('listener unsubscribe stops further notifications', async () => {
|
|
333
|
-
setCartCookie('stale');
|
|
334
|
-
vi.spyOn(activeCartClient, 'updateItems').mockRejectedValue(recoverableError('CART_NOT_FOUND'));
|
|
335
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
336
|
-
const listener = vi.fn();
|
|
337
|
-
let unsubscribe;
|
|
338
|
-
act(() => {
|
|
339
|
-
unsubscribe = result.current.onExpired(listener);
|
|
340
|
-
});
|
|
341
|
-
act(() => {
|
|
342
|
-
unsubscribe();
|
|
343
|
-
});
|
|
344
|
-
await act(async () => {
|
|
345
|
-
await expect(result.current.updateItem([{ id: 'l1', quantity: 1 }])).rejects.toBeDefined();
|
|
346
|
-
});
|
|
347
|
-
expect(listener).not.toHaveBeenCalled();
|
|
348
|
-
});
|
|
349
|
-
it('isLoading toggles around a mutation', async () => {
|
|
350
|
-
setCartCookie('cart-1');
|
|
351
|
-
let resolveAdd = null;
|
|
352
|
-
const addPromise = new Promise((r) => {
|
|
353
|
-
resolveAdd = r;
|
|
354
|
-
});
|
|
355
|
-
vi.spyOn(activeCartClient, 'addItems').mockImplementation(() => addPromise);
|
|
356
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
357
|
-
expect(result.current.isLoading).toBe(false);
|
|
358
|
-
let pending;
|
|
359
|
-
act(() => {
|
|
360
|
-
pending = result.current.addItem([{ variantId: 'v1' }]);
|
|
361
|
-
});
|
|
362
|
-
// After invocation but before resolve — isLoading should be true on next tick
|
|
363
|
-
await act(async () => {
|
|
364
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
365
|
-
});
|
|
366
|
-
expect(result.current.isLoading).toBe(true);
|
|
367
|
-
await act(async () => {
|
|
368
|
-
resolveAdd({ cart: FAKE_CART, warnings: [] });
|
|
369
|
-
await pending;
|
|
370
|
-
});
|
|
371
|
-
expect(result.current.isLoading).toBe(false);
|
|
372
|
-
});
|
|
373
|
-
it('error state is populated on failure and cleared on subsequent success', async () => {
|
|
374
|
-
setCartCookie('cart-1');
|
|
375
|
-
const fail = new Error('boom');
|
|
376
|
-
const addSpy = vi.spyOn(activeCartClient, 'addItems')
|
|
377
|
-
.mockRejectedValueOnce(fail)
|
|
378
|
-
.mockResolvedValueOnce({ cart: FAKE_CART, warnings: [] });
|
|
379
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
380
|
-
await act(async () => {
|
|
381
|
-
await expect(result.current.addItem([{ variantId: 'v1' }])).rejects.toBe(fail);
|
|
382
|
-
});
|
|
383
|
-
expect(result.current.error).toBe('boom');
|
|
384
|
-
await act(async () => {
|
|
385
|
-
await result.current.addItem([{ variantId: 'v1' }]);
|
|
386
|
-
});
|
|
387
|
-
expect(result.current.error).toBeNull();
|
|
388
|
-
expect(addSpy).toHaveBeenCalledTimes(2);
|
|
389
|
-
});
|
|
390
|
-
it('clearCart removes the cookie', async () => {
|
|
391
|
-
setCartCookie('to-clear');
|
|
392
|
-
const { result } = renderHook(() => useCartManager(), { wrapper: Wrapper });
|
|
393
|
-
expect(result.current.getCartId()).toBe('to-clear');
|
|
394
|
-
act(() => {
|
|
395
|
-
result.current.clearCart();
|
|
396
|
-
});
|
|
397
|
-
expect(getCartCookie()).toBeNull();
|
|
398
|
-
expect(result.current.getCartId()).toBeNull();
|
|
399
|
-
});
|
|
400
|
-
});
|