@doswiftly/storefront-sdk 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +430 -0
- package/dist/__tests__/unit/test-helpers.d.ts +46 -0
- package/dist/__tests__/unit/test-helpers.d.ts.map +1 -0
- package/dist/__tests__/unit/test-helpers.js +72 -0
- package/dist/core/auth/auth-client.d.ts +46 -0
- package/dist/core/auth/auth-client.d.ts.map +1 -0
- package/dist/core/auth/auth-client.js +82 -0
- package/dist/core/auth/cookie-config.d.ts +18 -0
- package/dist/core/auth/cookie-config.d.ts.map +1 -0
- package/dist/core/auth/cookie-config.js +18 -0
- package/dist/core/auth/handlers.d.ts +32 -0
- package/dist/core/auth/handlers.d.ts.map +1 -0
- package/dist/core/auth/handlers.js +127 -0
- package/dist/core/auth/routes.d.ts +21 -0
- package/dist/core/auth/routes.d.ts.map +1 -0
- package/dist/core/auth/routes.js +14 -0
- package/dist/core/auth/token-client.d.ts +26 -0
- package/dist/core/auth/token-client.d.ts.map +1 -0
- package/dist/core/auth/token-client.js +42 -0
- package/dist/core/auth/types.d.ts +53 -0
- package/dist/core/auth/types.d.ts.map +1 -0
- package/dist/core/auth/types.js +4 -0
- package/dist/core/cache.d.ts +54 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +82 -0
- package/dist/core/cart/cart-client.d.ts +57 -0
- package/dist/core/cart/cart-client.d.ts.map +1 -0
- package/dist/core/cart/cart-client.js +89 -0
- package/dist/core/cart/types.d.ts +110 -0
- package/dist/core/cart/types.d.ts.map +1 -0
- package/dist/core/cart/types.js +6 -0
- package/dist/core/client/compose.d.ts +9 -0
- package/dist/core/client/compose.d.ts.map +1 -0
- package/dist/core/client/compose.js +9 -0
- package/dist/core/client/create-client.d.ts +15 -0
- package/dist/core/client/create-client.d.ts.map +1 -0
- package/dist/core/client/create-client.js +85 -0
- package/dist/core/client/dedupe.d.ts +7 -0
- package/dist/core/client/dedupe.d.ts.map +1 -0
- package/dist/core/client/dedupe.js +16 -0
- package/dist/core/client/execute.d.ts +20 -0
- package/dist/core/client/execute.d.ts.map +1 -0
- package/dist/core/client/execute.js +48 -0
- package/dist/core/client/hash.d.ts +7 -0
- package/dist/core/client/hash.d.ts.map +1 -0
- package/dist/core/client/hash.js +21 -0
- package/dist/core/client/operation-name.d.ts +7 -0
- package/dist/core/client/operation-name.d.ts.map +1 -0
- package/dist/core/client/operation-name.js +10 -0
- package/dist/core/client/types.d.ts +126 -0
- package/dist/core/client/types.d.ts.map +1 -0
- package/dist/core/client/types.js +26 -0
- package/dist/core/errors.d.ts +43 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +43 -0
- package/dist/core/format.d.ts +92 -0
- package/dist/core/format.d.ts.map +1 -0
- package/dist/core/format.js +216 -0
- package/dist/core/helpers/assert-no-user-errors.d.ts +10 -0
- package/dist/core/helpers/assert-no-user-errors.d.ts.map +1 -0
- package/dist/core/helpers/assert-no-user-errors.js +16 -0
- package/dist/core/helpers/normalize-connection.d.ts +36 -0
- package/dist/core/helpers/normalize-connection.d.ts.map +1 -0
- package/dist/core/helpers/normalize-connection.js +21 -0
- package/dist/core/helpers/sanitize-html.d.ts +8 -0
- package/dist/core/helpers/sanitize-html.d.ts.map +1 -0
- package/dist/core/helpers/sanitize-html.js +35 -0
- package/dist/core/index.d.ts +59 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +68 -0
- package/dist/core/middleware/auth.d.ts +16 -0
- package/dist/core/middleware/auth.d.ts.map +1 -0
- package/dist/core/middleware/auth.js +22 -0
- package/dist/core/middleware/currency.d.ts +15 -0
- package/dist/core/middleware/currency.d.ts.map +1 -0
- package/dist/core/middleware/currency.js +21 -0
- package/dist/core/middleware/errors.d.ts +24 -0
- package/dist/core/middleware/errors.d.ts.map +1 -0
- package/dist/core/middleware/errors.js +77 -0
- package/dist/core/middleware/retry.d.ts +22 -0
- package/dist/core/middleware/retry.d.ts.map +1 -0
- package/dist/core/middleware/retry.js +58 -0
- package/dist/core/middleware/timeout.d.ts +19 -0
- package/dist/core/middleware/timeout.d.ts.map +1 -0
- package/dist/core/middleware/timeout.js +51 -0
- package/dist/core/operations/auth.d.ts +11 -0
- package/dist/core/operations/auth.d.ts.map +1 -0
- package/dist/core/operations/auth.js +112 -0
- package/dist/core/operations/cart.d.ts +15 -0
- package/dist/core/operations/cart.d.ts.map +1 -0
- package/dist/core/operations/cart.js +169 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/react/cookies.d.ts +28 -0
- package/dist/react/cookies.d.ts.map +1 -0
- package/dist/react/cookies.js +49 -0
- package/dist/react/helpers/create-store-context.d.ts +37 -0
- package/dist/react/helpers/create-store-context.d.ts.map +1 -0
- package/dist/react/helpers/create-store-context.js +47 -0
- package/dist/react/hooks/use-auth.d.ts +65 -0
- package/dist/react/hooks/use-auth.d.ts.map +1 -0
- package/dist/react/hooks/use-auth.js +168 -0
- package/dist/react/hooks/use-cart-manager.d.ts +30 -0
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -0
- package/dist/react/hooks/use-cart-manager.js +223 -0
- package/dist/react/hooks/use-currency.d.ts +11 -0
- package/dist/react/hooks/use-currency.d.ts.map +1 -0
- package/dist/react/hooks/use-currency.js +19 -0
- package/dist/react/hooks/use-debounced-value.d.ts +15 -0
- package/dist/react/hooks/use-debounced-value.d.ts.map +1 -0
- package/dist/react/hooks/use-debounced-value.js +25 -0
- package/dist/react/hooks/use-hydrated.d.ts +9 -0
- package/dist/react/hooks/use-hydrated.d.ts.map +1 -0
- package/dist/react/hooks/use-hydrated.js +14 -0
- package/dist/react/hooks/use-storefront-client.d.ts +6 -0
- package/dist/react/hooks/use-storefront-client.d.ts.map +1 -0
- package/dist/react/hooks/use-storefront-client.js +8 -0
- package/dist/react/index.d.ts +30 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +34 -0
- package/dist/react/providers/currency-provider.d.ts +14 -0
- package/dist/react/providers/currency-provider.d.ts.map +1 -0
- package/dist/react/providers/currency-provider.js +20 -0
- package/dist/react/providers/storefront-client-provider.d.ts +33 -0
- package/dist/react/providers/storefront-client-provider.d.ts.map +1 -0
- package/dist/react/providers/storefront-client-provider.js +57 -0
- package/dist/react/providers/storefront-provider.d.ts +42 -0
- package/dist/react/providers/storefront-provider.d.ts.map +1 -0
- package/dist/react/providers/storefront-provider.js +40 -0
- package/dist/react/server/get-storefront-client.d.ts +42 -0
- package/dist/react/server/get-storefront-client.d.ts.map +1 -0
- package/dist/react/server/get-storefront-client.js +44 -0
- package/dist/react/server/index.d.ts +2 -0
- package/dist/react/server/index.d.ts.map +1 -0
- package/dist/react/server/index.js +1 -0
- package/dist/react/stores/auth.store.d.ts +48 -0
- package/dist/react/stores/auth.store.d.ts.map +1 -0
- package/dist/react/stores/auth.store.js +67 -0
- package/dist/react/stores/currency.store.d.ts +29 -0
- package/dist/react/stores/currency.store.d.ts.map +1 -0
- package/dist/react/stores/currency.store.js +76 -0
- package/dist/react/stores/index.d.ts +8 -0
- package/dist/react/stores/index.d.ts.map +1 -0
- package/dist/react/stores/index.js +10 -0
- package/dist/react/stores/store-context.d.ts +27 -0
- package/dist/react/stores/store-context.d.ts.map +1 -0
- package/dist/react/stores/store-context.js +62 -0
- package/package.json +71 -0
- package/src/__tests__/contract/storefront-api.contract.test.ts +450 -0
- package/src/__tests__/unit/auth-client.test.ts +210 -0
- package/src/__tests__/unit/cart-client.test.ts +233 -0
- package/src/__tests__/unit/create-client.test.ts +356 -0
- package/src/__tests__/unit/helpers.test.ts +377 -0
- package/src/__tests__/unit/middleware.test.ts +374 -0
- package/src/__tests__/unit/test-helpers.ts +103 -0
- package/src/core/auth/auth-client.ts +123 -0
- package/src/core/auth/cookie-config.ts +23 -0
- package/src/core/auth/handlers.ts +168 -0
- package/src/core/auth/routes.ts +26 -0
- package/src/core/auth/token-client.ts +51 -0
- package/src/core/auth/types.ts +54 -0
- package/src/core/cache.ts +102 -0
- package/src/core/cart/cart-client.ts +150 -0
- package/src/core/cart/types.ts +104 -0
- package/src/core/client/compose.ts +15 -0
- package/src/core/client/create-client.ts +129 -0
- package/src/core/client/dedupe.ts +19 -0
- package/src/core/client/execute.ts +70 -0
- package/src/core/client/hash.ts +21 -0
- package/src/core/client/operation-name.ts +12 -0
- package/src/core/client/types.ts +171 -0
- package/src/core/errors.ts +67 -0
- package/src/core/format.ts +254 -0
- package/src/core/helpers/assert-no-user-errors.ts +21 -0
- package/src/core/helpers/normalize-connection.ts +48 -0
- package/src/core/helpers/sanitize-html.ts +42 -0
- package/src/core/index.ts +148 -0
- package/src/core/middleware/auth.ts +27 -0
- package/src/core/middleware/currency.ts +26 -0
- package/src/core/middleware/errors.ts +86 -0
- package/src/core/middleware/retry.ts +75 -0
- package/src/core/middleware/timeout.ts +61 -0
- package/src/core/operations/auth.ts +123 -0
- package/src/core/operations/cart.ts +185 -0
- package/src/index.ts +25 -0
- package/src/react/cookies.ts +54 -0
- package/src/react/helpers/create-store-context.ts +56 -0
- package/src/react/hooks/use-auth.ts +218 -0
- package/src/react/hooks/use-cart-manager.ts +236 -0
- package/src/react/hooks/use-currency.ts +23 -0
- package/src/react/hooks/use-debounced-value.ts +30 -0
- package/src/react/hooks/use-hydrated.ts +20 -0
- package/src/react/hooks/use-storefront-client.ts +12 -0
- package/src/react/index.ts +45 -0
- package/src/react/providers/currency-provider.tsx +30 -0
- package/src/react/providers/storefront-client-provider.tsx +90 -0
- package/src/react/providers/storefront-provider.tsx +71 -0
- package/src/react/server/get-storefront-client.ts +60 -0
- package/src/react/server/index.ts +1 -0
- package/src/react/stores/auth.store.ts +112 -0
- package/src/react/stores/currency.store.ts +113 -0
- package/src/react/stores/index.ts +17 -0
- package/src/react/stores/store-context.tsx +82 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for CartClient — plain async cart API.
|
|
3
|
+
*
|
|
4
|
+
* Tests all cart operations with mocked StorefrontClient.
|
|
5
|
+
* Verifies assertNoUserErrors integration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
9
|
+
import { CartClient } from '../../core/cart/cart-client';
|
|
10
|
+
import { StorefrontError, ErrorCodes } from '../../core/errors';
|
|
11
|
+
import type { StorefrontClient } from '../../core/client/types';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Mock StorefrontClient
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function createMockClient(responses: Record<string, unknown>): StorefrontClient {
|
|
18
|
+
return {
|
|
19
|
+
query: vi.fn(async (_doc: unknown, _vars?: unknown) => responses),
|
|
20
|
+
mutate: vi.fn(async (_doc: unknown, _vars?: unknown) => responses),
|
|
21
|
+
use: vi.fn(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MOCK_CART = {
|
|
26
|
+
id: 'cart-1',
|
|
27
|
+
lines: [
|
|
28
|
+
{ id: 'line-1', quantity: 2, merchandise: { id: 'var-1', title: 'Test Product' }, cost: { amountPerQuantity: { amount: '10.00', currencyCode: 'PLN' }, totalAmount: { amount: '20.00', currencyCode: 'PLN' } } },
|
|
29
|
+
],
|
|
30
|
+
totalQuantity: 2,
|
|
31
|
+
cost: { totalAmount: { amount: '20.00', currencyCode: 'PLN' }, subtotalAmount: { amount: '20.00', currencyCode: 'PLN' } },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Tests
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
describe('CartClient', () => {
|
|
39
|
+
describe('get()', () => {
|
|
40
|
+
it('should query cart by ID', async () => {
|
|
41
|
+
const client = createMockClient({ cart: MOCK_CART });
|
|
42
|
+
const cartClient = new CartClient(client);
|
|
43
|
+
|
|
44
|
+
const cart = await cartClient.get('cart-1');
|
|
45
|
+
|
|
46
|
+
expect(cart).toEqual(MOCK_CART);
|
|
47
|
+
expect(client.query).toHaveBeenCalledWith(
|
|
48
|
+
expect.any(String),
|
|
49
|
+
{ id: 'cart-1' },
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return null for non-existent cart', async () => {
|
|
54
|
+
const client = createMockClient({ cart: null });
|
|
55
|
+
const cartClient = new CartClient(client);
|
|
56
|
+
|
|
57
|
+
const cart = await cartClient.get('expired-cart');
|
|
58
|
+
expect(cart).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('create()', () => {
|
|
63
|
+
it('should create empty cart', async () => {
|
|
64
|
+
const client = createMockClient({
|
|
65
|
+
cartCreate: { cart: MOCK_CART, userErrors: [] },
|
|
66
|
+
});
|
|
67
|
+
const cartClient = new CartClient(client);
|
|
68
|
+
|
|
69
|
+
const cart = await cartClient.create();
|
|
70
|
+
|
|
71
|
+
expect(cart).toEqual(MOCK_CART);
|
|
72
|
+
expect(client.mutate).toHaveBeenCalledWith(
|
|
73
|
+
expect.any(String),
|
|
74
|
+
{ input: {} },
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should create cart with initial lines', async () => {
|
|
79
|
+
const client = createMockClient({
|
|
80
|
+
cartCreate: { cart: MOCK_CART, userErrors: [] },
|
|
81
|
+
});
|
|
82
|
+
const cartClient = new CartClient(client);
|
|
83
|
+
|
|
84
|
+
const cart = await cartClient.create({
|
|
85
|
+
lines: [{ merchandiseId: 'var-1', quantity: 2 }],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(cart).toEqual(MOCK_CART);
|
|
89
|
+
expect(client.mutate).toHaveBeenCalledWith(
|
|
90
|
+
expect.any(String),
|
|
91
|
+
{ input: { lines: [{ merchandiseId: 'var-1', quantity: 2 }] } },
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should throw StorefrontError on user errors', async () => {
|
|
96
|
+
const client = createMockClient({
|
|
97
|
+
cartCreate: {
|
|
98
|
+
cart: null,
|
|
99
|
+
userErrors: [{ message: 'Invalid input', field: ['lines', '0', 'merchandiseId'] }],
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
const cartClient = new CartClient(client);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await cartClient.create({ lines: [{ merchandiseId: 'bad-id', quantity: 1 }] });
|
|
106
|
+
expect.fail('Should have thrown');
|
|
107
|
+
} catch (err) {
|
|
108
|
+
expect(err).toBeInstanceOf(StorefrontError);
|
|
109
|
+
const sfErr = err as StorefrontError;
|
|
110
|
+
expect(sfErr.code).toBe(ErrorCodes.USER_ERROR);
|
|
111
|
+
expect(sfErr.hasUserErrors).toBe(true);
|
|
112
|
+
expect(sfErr.userErrors[0].message).toBe('Invalid input');
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('addItems()', () => {
|
|
118
|
+
it('should add lines to cart', async () => {
|
|
119
|
+
const client = createMockClient({
|
|
120
|
+
cartLinesAdd: { cart: MOCK_CART, userErrors: [] },
|
|
121
|
+
});
|
|
122
|
+
const cartClient = new CartClient(client);
|
|
123
|
+
|
|
124
|
+
const cart = await cartClient.addItems('cart-1', [
|
|
125
|
+
{ merchandiseId: 'var-1', quantity: 1 },
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
expect(cart).toEqual(MOCK_CART);
|
|
129
|
+
expect(client.mutate).toHaveBeenCalledWith(
|
|
130
|
+
expect.any(String),
|
|
131
|
+
{ cartId: 'cart-1', lines: [{ merchandiseId: 'var-1', quantity: 1 }] },
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should throw on user errors (e.g., out of stock)', async () => {
|
|
136
|
+
const client = createMockClient({
|
|
137
|
+
cartLinesAdd: {
|
|
138
|
+
cart: null,
|
|
139
|
+
userErrors: [{ message: 'Out of stock', code: 'INSUFFICIENT_INVENTORY' }],
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const cartClient = new CartClient(client);
|
|
143
|
+
|
|
144
|
+
await expect(
|
|
145
|
+
cartClient.addItems('cart-1', [{ merchandiseId: 'var-1', quantity: 100 }]),
|
|
146
|
+
).rejects.toThrow(StorefrontError);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('updateItems()', () => {
|
|
151
|
+
it('should update line quantities', async () => {
|
|
152
|
+
const updatedCart = { ...MOCK_CART, totalQuantity: 5 };
|
|
153
|
+
const client = createMockClient({
|
|
154
|
+
cartLinesUpdate: { cart: updatedCart, userErrors: [] },
|
|
155
|
+
});
|
|
156
|
+
const cartClient = new CartClient(client);
|
|
157
|
+
|
|
158
|
+
const cart = await cartClient.updateItems('cart-1', [
|
|
159
|
+
{ id: 'line-1', quantity: 5 },
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
expect(cart.totalQuantity).toBe(5);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('removeItems()', () => {
|
|
167
|
+
it('should remove lines from cart', async () => {
|
|
168
|
+
const emptyCart = { ...MOCK_CART, lines: [], totalQuantity: 0 };
|
|
169
|
+
const client = createMockClient({
|
|
170
|
+
cartLinesRemove: { cart: emptyCart, userErrors: [] },
|
|
171
|
+
});
|
|
172
|
+
const cartClient = new CartClient(client);
|
|
173
|
+
|
|
174
|
+
const cart = await cartClient.removeItems('cart-1', ['line-1']);
|
|
175
|
+
|
|
176
|
+
expect(cart.lines).toHaveLength(0);
|
|
177
|
+
expect(cart.totalQuantity).toBe(0);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('updateDiscountCodes()', () => {
|
|
182
|
+
it('should apply discount codes', async () => {
|
|
183
|
+
const client = createMockClient({
|
|
184
|
+
cartDiscountCodesUpdate: { cart: MOCK_CART, userErrors: [] },
|
|
185
|
+
});
|
|
186
|
+
const cartClient = new CartClient(client);
|
|
187
|
+
|
|
188
|
+
const cart = await cartClient.updateDiscountCodes('cart-1', ['SAVE10']);
|
|
189
|
+
|
|
190
|
+
expect(cart).toEqual(MOCK_CART);
|
|
191
|
+
expect(client.mutate).toHaveBeenCalledWith(
|
|
192
|
+
expect.any(String),
|
|
193
|
+
{ cartId: 'cart-1', discountCodes: ['SAVE10'] },
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('updateNote()', () => {
|
|
199
|
+
it('should update cart note', async () => {
|
|
200
|
+
const client = createMockClient({
|
|
201
|
+
cartNoteUpdate: { cart: MOCK_CART, userErrors: [] },
|
|
202
|
+
});
|
|
203
|
+
const cartClient = new CartClient(client);
|
|
204
|
+
|
|
205
|
+
const cart = await cartClient.updateNote('cart-1', 'Gift wrap please');
|
|
206
|
+
|
|
207
|
+
expect(cart).toEqual(MOCK_CART);
|
|
208
|
+
expect(client.mutate).toHaveBeenCalledWith(
|
|
209
|
+
expect.any(String),
|
|
210
|
+
{ cartId: 'cart-1', note: 'Gift wrap please' },
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('updateBuyerIdentity()', () => {
|
|
216
|
+
it('should update buyer identity', async () => {
|
|
217
|
+
const client = createMockClient({
|
|
218
|
+
cartBuyerIdentityUpdate: { cart: MOCK_CART, userErrors: [] },
|
|
219
|
+
});
|
|
220
|
+
const cartClient = new CartClient(client);
|
|
221
|
+
|
|
222
|
+
const cart = await cartClient.updateBuyerIdentity('cart-1', {
|
|
223
|
+
email: 'test@example.com',
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(cart).toEqual(MOCK_CART);
|
|
227
|
+
expect(client.mutate).toHaveBeenCalledWith(
|
|
228
|
+
expect.any(String),
|
|
229
|
+
{ cartId: 'cart-1', buyerIdentity: { email: 'test@example.com' } },
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for createStorefrontClient — transport factory.
|
|
3
|
+
*
|
|
4
|
+
* Tests: query/mutate, middleware pipeline, request deduplication,
|
|
5
|
+
* lazy pipeline compilation, use() API, TypedDocumentString support.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
9
|
+
import { createStorefrontClient } from '../../core/client/create-client';
|
|
10
|
+
import { TypedDocumentString } from '../../core/client/types';
|
|
11
|
+
import { createSpyFetch, createMockFetch } from './test-helpers';
|
|
12
|
+
|
|
13
|
+
const PRODUCT_QUERY = 'query Product($handle: String!) { product(handle: $handle) { id title } }';
|
|
14
|
+
const CART_CREATE = 'mutation CartCreate($input: CartInput!) { cartCreate(input: $input) { cart { id } userErrors { message } } }';
|
|
15
|
+
|
|
16
|
+
describe('createStorefrontClient', () => {
|
|
17
|
+
it('should create a client with query and mutate methods', () => {
|
|
18
|
+
const client = createStorefrontClient({
|
|
19
|
+
apiUrl: 'https://api.test.com',
|
|
20
|
+
shopSlug: 'test-shop',
|
|
21
|
+
fetch: createMockFetch({}),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(client.query).toBeDefined();
|
|
25
|
+
expect(client.mutate).toBeDefined();
|
|
26
|
+
expect(client.use).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('query()', () => {
|
|
30
|
+
it('should send POST request with correct body', async () => {
|
|
31
|
+
const { fetch, calls } = createSpyFetch({ product: { id: '1', title: 'Test' } });
|
|
32
|
+
|
|
33
|
+
const client = createStorefrontClient({
|
|
34
|
+
apiUrl: 'https://api.test.com',
|
|
35
|
+
shopSlug: 'my-shop',
|
|
36
|
+
fetch,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await client.query(PRODUCT_QUERY, { handle: 'test-product' });
|
|
40
|
+
|
|
41
|
+
expect(calls).toHaveLength(1);
|
|
42
|
+
expect(calls[0].url).toBe('https://api.test.com/storefront/graphql');
|
|
43
|
+
|
|
44
|
+
const body = JSON.parse(calls[0].init.body as string);
|
|
45
|
+
expect(body.query).toBe(PRODUCT_QUERY);
|
|
46
|
+
expect(body.variables).toEqual({ handle: 'test-product' });
|
|
47
|
+
expect(body.operationName).toBe('Product');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should include X-Shop-Slug header', async () => {
|
|
51
|
+
const { fetch, calls } = createSpyFetch({ product: null });
|
|
52
|
+
|
|
53
|
+
const client = createStorefrontClient({
|
|
54
|
+
apiUrl: 'https://api.test.com',
|
|
55
|
+
shopSlug: 'my-shop',
|
|
56
|
+
fetch,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await client.query(PRODUCT_QUERY, { handle: 'test' });
|
|
60
|
+
|
|
61
|
+
const headers = calls[0].init.headers as Record<string, string>;
|
|
62
|
+
expect(headers['X-Shop-Slug']).toBe('my-shop');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should include X-Operation-Name header for named queries', async () => {
|
|
66
|
+
const { fetch, calls } = createSpyFetch({ product: null });
|
|
67
|
+
|
|
68
|
+
const client = createStorefrontClient({
|
|
69
|
+
apiUrl: 'https://api.test.com',
|
|
70
|
+
shopSlug: 'test',
|
|
71
|
+
fetch,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await client.query(PRODUCT_QUERY, { handle: 'test' });
|
|
75
|
+
|
|
76
|
+
const headers = calls[0].init.headers as Record<string, string>;
|
|
77
|
+
expect(headers['X-Operation-Name']).toBe('Product');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should NOT include X-Operation-Name for anonymous queries', async () => {
|
|
81
|
+
const { fetch, calls } = createSpyFetch({ something: true });
|
|
82
|
+
|
|
83
|
+
const client = createStorefrontClient({
|
|
84
|
+
apiUrl: 'https://api.test.com',
|
|
85
|
+
shopSlug: 'test',
|
|
86
|
+
fetch,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await client.query('{ shop { id } }');
|
|
90
|
+
|
|
91
|
+
const headers = calls[0].init.headers as Record<string, string>;
|
|
92
|
+
expect(headers['X-Operation-Name']).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should return data from response', async () => {
|
|
96
|
+
const mockData = { product: { id: '1', title: 'Cool Shirt' } };
|
|
97
|
+
const client = createStorefrontClient({
|
|
98
|
+
apiUrl: 'https://api.test.com',
|
|
99
|
+
shopSlug: 'test',
|
|
100
|
+
fetch: createMockFetch(mockData),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result = await client.query(PRODUCT_QUERY, { handle: 'cool-shirt' });
|
|
104
|
+
expect(result).toEqual(mockData);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should strip trailing slash from apiUrl', async () => {
|
|
108
|
+
const { fetch, calls } = createSpyFetch({});
|
|
109
|
+
|
|
110
|
+
const client = createStorefrontClient({
|
|
111
|
+
apiUrl: 'https://api.test.com/',
|
|
112
|
+
shopSlug: 'test',
|
|
113
|
+
fetch,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await client.query('{ shop { id } }');
|
|
117
|
+
expect(calls[0].url).toBe('https://api.test.com/storefront/graphql');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should merge defaultHeaders into request', async () => {
|
|
121
|
+
const { fetch, calls } = createSpyFetch({});
|
|
122
|
+
|
|
123
|
+
const client = createStorefrontClient({
|
|
124
|
+
apiUrl: 'https://api.test.com',
|
|
125
|
+
shopSlug: 'test',
|
|
126
|
+
defaultHeaders: { 'X-Custom': 'value' },
|
|
127
|
+
fetch,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await client.query('{ shop { id } }');
|
|
131
|
+
|
|
132
|
+
const headers = calls[0].init.headers as Record<string, string>;
|
|
133
|
+
expect(headers['X-Custom']).toBe('value');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should support TypedDocumentString', async () => {
|
|
137
|
+
const typedDoc = new TypedDocumentString<{ product: { id: string } }, { handle: string }>(PRODUCT_QUERY);
|
|
138
|
+
|
|
139
|
+
const { fetch, calls } = createSpyFetch({ product: { id: '1' } });
|
|
140
|
+
|
|
141
|
+
const client = createStorefrontClient({
|
|
142
|
+
apiUrl: 'https://api.test.com',
|
|
143
|
+
shopSlug: 'test',
|
|
144
|
+
fetch,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const result = await client.query(typedDoc, { handle: 'test' });
|
|
148
|
+
|
|
149
|
+
const body = JSON.parse(calls[0].init.body as string);
|
|
150
|
+
expect(body.query).toBe(PRODUCT_QUERY);
|
|
151
|
+
expect(result).toEqual({ product: { id: '1' } });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should not include empty variables in request body', async () => {
|
|
155
|
+
const { fetch, calls } = createSpyFetch({});
|
|
156
|
+
|
|
157
|
+
const client = createStorefrontClient({
|
|
158
|
+
apiUrl: 'https://api.test.com',
|
|
159
|
+
shopSlug: 'test',
|
|
160
|
+
fetch,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await client.query('{ shop { id } }');
|
|
164
|
+
|
|
165
|
+
const body = JSON.parse(calls[0].init.body as string);
|
|
166
|
+
expect(body.variables).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('mutate()', () => {
|
|
171
|
+
it('should send mutation correctly', async () => {
|
|
172
|
+
const mockData = { cartCreate: { cart: { id: 'cart-1' }, userErrors: [] } };
|
|
173
|
+
const { fetch, calls } = createSpyFetch(mockData);
|
|
174
|
+
|
|
175
|
+
const client = createStorefrontClient({
|
|
176
|
+
apiUrl: 'https://api.test.com',
|
|
177
|
+
shopSlug: 'test',
|
|
178
|
+
fetch,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const result = await client.mutate(CART_CREATE, { input: {} });
|
|
182
|
+
|
|
183
|
+
expect(result).toEqual(mockData);
|
|
184
|
+
const body = JSON.parse(calls[0].init.body as string);
|
|
185
|
+
expect(body.operationName).toBe('CartCreate');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('request deduplication', () => {
|
|
190
|
+
it('should deduplicate identical concurrent queries', async () => {
|
|
191
|
+
const { fetch, calls } = createSpyFetch({ product: { id: '1' } });
|
|
192
|
+
|
|
193
|
+
const client = createStorefrontClient({
|
|
194
|
+
apiUrl: 'https://api.test.com',
|
|
195
|
+
shopSlug: 'test',
|
|
196
|
+
fetch,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Fire 3 identical queries concurrently
|
|
200
|
+
const [r1, r2, r3] = await Promise.all([
|
|
201
|
+
client.query(PRODUCT_QUERY, { handle: 'test' }),
|
|
202
|
+
client.query(PRODUCT_QUERY, { handle: 'test' }),
|
|
203
|
+
client.query(PRODUCT_QUERY, { handle: 'test' }),
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
// Only 1 actual fetch
|
|
207
|
+
expect(calls).toHaveLength(1);
|
|
208
|
+
// All return same data
|
|
209
|
+
expect(r1).toEqual(r2);
|
|
210
|
+
expect(r2).toEqual(r3);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should NOT deduplicate queries with different variables', async () => {
|
|
214
|
+
const { fetch, calls } = createSpyFetch({ product: { id: '1' } });
|
|
215
|
+
|
|
216
|
+
const client = createStorefrontClient({
|
|
217
|
+
apiUrl: 'https://api.test.com',
|
|
218
|
+
shopSlug: 'test',
|
|
219
|
+
fetch,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await Promise.all([
|
|
223
|
+
client.query(PRODUCT_QUERY, { handle: 'product-a' }),
|
|
224
|
+
client.query(PRODUCT_QUERY, { handle: 'product-b' }),
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
expect(calls).toHaveLength(2);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should NOT deduplicate mutations', async () => {
|
|
231
|
+
const { fetch, calls } = createSpyFetch({ cartCreate: { cart: { id: '1' }, userErrors: [] } });
|
|
232
|
+
|
|
233
|
+
const client = createStorefrontClient({
|
|
234
|
+
apiUrl: 'https://api.test.com',
|
|
235
|
+
shopSlug: 'test',
|
|
236
|
+
fetch,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await Promise.all([
|
|
240
|
+
client.mutate(CART_CREATE, { input: {} }),
|
|
241
|
+
client.mutate(CART_CREATE, { input: {} }),
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
expect(calls).toHaveLength(2);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('use() — imperative middleware', () => {
|
|
249
|
+
it('should add middleware that runs on subsequent requests', async () => {
|
|
250
|
+
const { fetch, calls } = createSpyFetch({ data: true });
|
|
251
|
+
|
|
252
|
+
const client = createStorefrontClient({
|
|
253
|
+
apiUrl: 'https://api.test.com',
|
|
254
|
+
shopSlug: 'test',
|
|
255
|
+
fetch,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
client.use((req, next) => {
|
|
259
|
+
req.headers['X-Added'] = 'by-use';
|
|
260
|
+
return next(req);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await client.query('{ shop { id } }');
|
|
264
|
+
|
|
265
|
+
const headers = calls[0].init.headers as Record<string, string>;
|
|
266
|
+
expect(headers['X-Added']).toBe('by-use');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should invalidate compiled pipeline on use()', async () => {
|
|
270
|
+
let callOrder: string[] = [];
|
|
271
|
+
|
|
272
|
+
const client = createStorefrontClient({
|
|
273
|
+
apiUrl: 'https://api.test.com',
|
|
274
|
+
shopSlug: 'test',
|
|
275
|
+
fetch: createMockFetch({}),
|
|
276
|
+
middleware: [
|
|
277
|
+
async (req, next) => { callOrder.push('A'); return next(req); },
|
|
278
|
+
],
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
await client.query('{ shop { id } }');
|
|
282
|
+
expect(callOrder).toEqual(['A']);
|
|
283
|
+
|
|
284
|
+
callOrder = [];
|
|
285
|
+
client.use(async (req, next) => { callOrder.push('B'); return next(req); });
|
|
286
|
+
|
|
287
|
+
await client.query('{ shop { id } }');
|
|
288
|
+
expect(callOrder).toEqual(['A', 'B']);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('middleware pipeline', () => {
|
|
293
|
+
it('should execute middleware in registration order', async () => {
|
|
294
|
+
const order: string[] = [];
|
|
295
|
+
|
|
296
|
+
const client = createStorefrontClient({
|
|
297
|
+
apiUrl: 'https://api.test.com',
|
|
298
|
+
shopSlug: 'test',
|
|
299
|
+
fetch: createMockFetch({}),
|
|
300
|
+
middleware: [
|
|
301
|
+
async (req, next) => { order.push('first'); return next(req); },
|
|
302
|
+
async (req, next) => { order.push('second'); return next(req); },
|
|
303
|
+
async (req, next) => { order.push('third'); return next(req); },
|
|
304
|
+
],
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await client.query('{ shop { id } }');
|
|
308
|
+
expect(order).toEqual(['first', 'second', 'third']);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should allow middleware to modify request headers', async () => {
|
|
312
|
+
const { fetch, calls } = createSpyFetch({});
|
|
313
|
+
|
|
314
|
+
const client = createStorefrontClient({
|
|
315
|
+
apiUrl: 'https://api.test.com',
|
|
316
|
+
shopSlug: 'test',
|
|
317
|
+
fetch,
|
|
318
|
+
middleware: [
|
|
319
|
+
async (req, next) => {
|
|
320
|
+
req.headers['Authorization'] = 'Bearer test-token';
|
|
321
|
+
return next(req);
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await client.query('{ shop { id } }');
|
|
327
|
+
|
|
328
|
+
const headers = calls[0].init.headers as Record<string, string>;
|
|
329
|
+
expect(headers['Authorization']).toBe('Bearer test-token');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should allow middleware to short-circuit', async () => {
|
|
333
|
+
const { fetch, calls } = createSpyFetch({});
|
|
334
|
+
|
|
335
|
+
const client = createStorefrontClient({
|
|
336
|
+
apiUrl: 'https://api.test.com',
|
|
337
|
+
shopSlug: 'test',
|
|
338
|
+
fetch,
|
|
339
|
+
middleware: [
|
|
340
|
+
async (_req, _next) => {
|
|
341
|
+
// Short-circuit — don't call next()
|
|
342
|
+
return {
|
|
343
|
+
data: { cached: true },
|
|
344
|
+
status: 200,
|
|
345
|
+
headers: new Headers(),
|
|
346
|
+
};
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const result = await client.query('{ shop { id } }');
|
|
352
|
+
expect(result).toEqual({ cached: true });
|
|
353
|
+
expect(calls).toHaveLength(0); // No fetch call
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|