@tapcart/mobile-components 0.12.10 → 0.12.12
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/dist/components/hooks/use-infinite-wishlist.d.ts +27 -12
- package/dist/components/hooks/use-infinite-wishlist.d.ts.map +1 -1
- package/dist/components/hooks/use-infinite-wishlist.js +158 -37
- package/dist/components/hooks/use-infinite-wishlist.test.d.ts +2 -0
- package/dist/components/hooks/use-infinite-wishlist.test.d.ts.map +1 -0
- package/dist/components/hooks/use-infinite-wishlist.test.js +1244 -0
- package/dist/components/hooks/use-reviews.d.ts.map +1 -1
- package/dist/components/hooks/use-reviews.js +8 -7
- package/dist/components/hooks/use-scroll-direction.d.ts.map +1 -1
- package/dist/components/hooks/use-scroll-direction.js +15 -2
- package/dist/components/ui/carousel.d.ts.map +1 -1
- package/dist/components/ui/carousel.js +48 -25
- package/dist/components/ui/text.js +1 -1
- package/dist/components/ui/virtual-grid.d.ts.map +1 -1
- package/dist/components/ui/virtual-grid.js +44 -2
- package/dist/lib/utils.d.ts +3 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +4 -3
- package/dist/styles.css +31 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1244 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { renderHook, act, waitFor } from "@testing-library/react";
|
|
11
|
+
import { useInfiniteWishlist } from "./use-infinite-wishlist";
|
|
12
|
+
// ─── Module mocks ────────────────────────────────────────────────────────────
|
|
13
|
+
jest.mock("./use-collection", () => ({
|
|
14
|
+
useCollection: jest.fn(),
|
|
15
|
+
}));
|
|
16
|
+
jest.mock("react-intersection-observer", () => ({
|
|
17
|
+
useInView: jest.fn(),
|
|
18
|
+
}));
|
|
19
|
+
jest.mock("../../lib/utils", () => ({
|
|
20
|
+
getProductGidsFromIds: jest.fn((ids) => ids.map((id) => id.startsWith("gid://shopify/Product/")
|
|
21
|
+
? id
|
|
22
|
+
: `gid://shopify/Product/${id}`)),
|
|
23
|
+
getVariantGidsFromIds: jest.fn((ids) => ids.map((id) => id.startsWith("gid://shopify/ProductVariant/")
|
|
24
|
+
? id
|
|
25
|
+
: `gid://shopify/ProductVariant/${id}`)),
|
|
26
|
+
}));
|
|
27
|
+
// ─── Imports after mocks ─────────────────────────────────────────────────────
|
|
28
|
+
import { useCollection } from "./use-collection";
|
|
29
|
+
import { useInView } from "react-intersection-observer";
|
|
30
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
31
|
+
const mockUseCollection = useCollection;
|
|
32
|
+
const mockUseInView = useInView;
|
|
33
|
+
const MOCK_LOAD_MORE_REF = jest.fn();
|
|
34
|
+
// Stable array references used as hook props – must live outside renderHook
|
|
35
|
+
// callbacks so React's effect dep comparison sees the same reference each render.
|
|
36
|
+
const THREE_ITEMS = [{ productId: "1" }, { productId: "2" }, { productId: "3" }];
|
|
37
|
+
const TWO_ITEMS = [{ productId: "1" }, { productId: "2" }];
|
|
38
|
+
const ONE_ITEM = [{ productId: "1" }];
|
|
39
|
+
const EMPTY_ITEMS = [];
|
|
40
|
+
// Same product, two variants — item keys are variant GIDs so two separate cards
|
|
41
|
+
const TWO_ITEMS_WITH_VARIANTS = [
|
|
42
|
+
{ productId: "2", variantId: "v3" },
|
|
43
|
+
{ productId: "1", variantId: "v2" },
|
|
44
|
+
];
|
|
45
|
+
const TWO_ITEMS_VARIANT_IDS_ONE = [
|
|
46
|
+
{ productId: "1", variantId: "v1" },
|
|
47
|
+
{ productId: "1", variantId: "v999" },
|
|
48
|
+
];
|
|
49
|
+
// GID-format productIds for removal and sort-ordering tests (product.id is GID).
|
|
50
|
+
const GID_1 = "gid://shopify/Product/1";
|
|
51
|
+
const GID_2 = "gid://shopify/Product/2";
|
|
52
|
+
const GID_3 = "gid://shopify/Product/3";
|
|
53
|
+
const GID_4 = "gid://shopify/Product/4";
|
|
54
|
+
const TWO_ITEMS_GIDS = [{ productId: GID_1 }, { productId: GID_2 }];
|
|
55
|
+
const THREE_ITEMS_GIDS = [
|
|
56
|
+
{ productId: GID_1 },
|
|
57
|
+
{ productId: GID_2 },
|
|
58
|
+
{ productId: GID_3 },
|
|
59
|
+
];
|
|
60
|
+
const FOUR_ITEMS_GIDS = [
|
|
61
|
+
{ productId: GID_1 },
|
|
62
|
+
{ productId: GID_2 },
|
|
63
|
+
{ productId: GID_3 },
|
|
64
|
+
{ productId: GID_4 },
|
|
65
|
+
];
|
|
66
|
+
const FOUR_ITEMS_GIDS_NO_2 = [
|
|
67
|
+
{ productId: GID_1 },
|
|
68
|
+
{ productId: GID_3 },
|
|
69
|
+
{ productId: GID_4 },
|
|
70
|
+
];
|
|
71
|
+
function makeProduct(id, variantIds = [`${id}-v1`]) {
|
|
72
|
+
return {
|
|
73
|
+
id: `gid://shopify/Product/${id}`,
|
|
74
|
+
handle: `product-${id}`,
|
|
75
|
+
title: `Product ${id}`,
|
|
76
|
+
description: `Description ${id}`,
|
|
77
|
+
availableForSale: true,
|
|
78
|
+
vendor: "TestVendor",
|
|
79
|
+
variants: variantIds.map((vid) => ({
|
|
80
|
+
id: `gid://shopify/ProductVariant/${vid}`,
|
|
81
|
+
title: `Variant ${vid}`,
|
|
82
|
+
price: { amount: "10.00" },
|
|
83
|
+
compareAtPrice: { amount: "15.00" },
|
|
84
|
+
availableForSale: true,
|
|
85
|
+
sku: `SKU-${vid}`,
|
|
86
|
+
selectedOptions: [],
|
|
87
|
+
})),
|
|
88
|
+
featuredImage: {
|
|
89
|
+
url: `https://example.com/${id}.jpg`,
|
|
90
|
+
altText: `Image ${id}`,
|
|
91
|
+
width: 100,
|
|
92
|
+
height: 100,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function setupInView(inView) {
|
|
97
|
+
mockUseInView.mockReturnValue({ ref: MOCK_LOAD_MORE_REF, inView });
|
|
98
|
+
}
|
|
99
|
+
function setupCollection(collectionId) {
|
|
100
|
+
mockUseCollection.mockReturnValue({
|
|
101
|
+
collections: collectionId ? [{ id: collectionId }] : [],
|
|
102
|
+
specificCollection: null,
|
|
103
|
+
maxProductCollection: null,
|
|
104
|
+
loading: false,
|
|
105
|
+
error: null,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// queryVariables must also be a stable module-level reference so it doesn't
|
|
109
|
+
// recreate fetchProducts/loadMore on every render.
|
|
110
|
+
const DEFAULT_QUERY_VARS = { language: "EN", country: "US", appId: "app-123" };
|
|
111
|
+
const DEFAULT_PROPS = {
|
|
112
|
+
initialItems: THREE_ITEMS,
|
|
113
|
+
chunkSize: 2,
|
|
114
|
+
apiUrl: "https://api.example.com",
|
|
115
|
+
shouldMock: false,
|
|
116
|
+
queryVariables: DEFAULT_QUERY_VARS,
|
|
117
|
+
};
|
|
118
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
119
|
+
describe("useInfiniteWishlist", () => {
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
jest.clearAllMocks();
|
|
122
|
+
setupInView(false);
|
|
123
|
+
setupCollection(undefined);
|
|
124
|
+
global.fetch = jest.fn();
|
|
125
|
+
});
|
|
126
|
+
// ── Initial state ──────────────────────────────────────────────────────────
|
|
127
|
+
describe("initial state", () => {
|
|
128
|
+
it("returns empty products, not loading, no error, and hasMore when ids exist", () => {
|
|
129
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
130
|
+
expect(result.current.products).toEqual([]);
|
|
131
|
+
expect(result.current.isLoading).toBe(false);
|
|
132
|
+
expect(result.current.isLoadingMore).toBe(false);
|
|
133
|
+
expect(result.current.error).toBeNull();
|
|
134
|
+
expect(result.current.hasMore).toBe(true);
|
|
135
|
+
expect(result.current.loadMoreRef).toBe(MOCK_LOAD_MORE_REF);
|
|
136
|
+
expect(typeof result.current.loadMore).toBe("function");
|
|
137
|
+
expect(typeof result.current.reset).toBe("function");
|
|
138
|
+
});
|
|
139
|
+
it("hasMore is false when initialItems is empty", () => {
|
|
140
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: EMPTY_ITEMS })));
|
|
141
|
+
expect(result.current.hasMore).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
// ── Non-mock mode: loadMore ────────────────────────────────────────────────
|
|
145
|
+
describe("non-mock mode", () => {
|
|
146
|
+
it("fetches first chunk of products on loadMore", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
147
|
+
// Multiple variants per product so getProductOrVariantKey returns p.id (product GID)
|
|
148
|
+
const products = [
|
|
149
|
+
makeProduct("1", ["v1", "v2"]),
|
|
150
|
+
makeProduct("2", ["v1", "v2"]),
|
|
151
|
+
];
|
|
152
|
+
global.fetch.mockResolvedValueOnce({
|
|
153
|
+
ok: true,
|
|
154
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return products; }),
|
|
155
|
+
});
|
|
156
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
157
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
158
|
+
yield result.current.loadMore();
|
|
159
|
+
}));
|
|
160
|
+
expect(result.current.products).toEqual(products);
|
|
161
|
+
expect(result.current.isLoading).toBe(false);
|
|
162
|
+
}));
|
|
163
|
+
it("uses chunkSize to limit how many ids are fetched per call", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
164
|
+
var _a;
|
|
165
|
+
const products = [
|
|
166
|
+
makeProduct("1", ["v1", "v2"]),
|
|
167
|
+
makeProduct("2", ["v1", "v2"]),
|
|
168
|
+
];
|
|
169
|
+
global.fetch.mockResolvedValueOnce({
|
|
170
|
+
ok: true,
|
|
171
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return products; }),
|
|
172
|
+
});
|
|
173
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: THREE_ITEMS, chunkSize: 2 })));
|
|
174
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
175
|
+
yield result.current.loadMore();
|
|
176
|
+
}));
|
|
177
|
+
const [calledUrl] = global.fetch.mock.calls[0];
|
|
178
|
+
const url = new URL(calledUrl);
|
|
179
|
+
const ids = (_a = url.searchParams.get("ids")) === null || _a === void 0 ? void 0 : _a.split(",");
|
|
180
|
+
expect(ids).toHaveLength(2);
|
|
181
|
+
}));
|
|
182
|
+
it("appends products on subsequent loadMore calls", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
183
|
+
const chunk1 = [
|
|
184
|
+
makeProduct("1", ["v1", "v2"]),
|
|
185
|
+
makeProduct("2", ["v1", "v2"]),
|
|
186
|
+
];
|
|
187
|
+
const chunk2 = [makeProduct("3", ["v1", "v2"])];
|
|
188
|
+
global.fetch
|
|
189
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return chunk1; }) })
|
|
190
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return chunk2; }) });
|
|
191
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
192
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
193
|
+
yield result.current.loadMore();
|
|
194
|
+
}));
|
|
195
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
196
|
+
yield result.current.loadMore();
|
|
197
|
+
}));
|
|
198
|
+
expect(result.current.products).toHaveLength(3);
|
|
199
|
+
expect(result.current.products[0]).toEqual(chunk1[0]);
|
|
200
|
+
expect(result.current.products[2]).toEqual(chunk2[0]);
|
|
201
|
+
}));
|
|
202
|
+
it("updates hasMore correctly as products are loaded", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
203
|
+
const chunk1 = [
|
|
204
|
+
makeProduct("1", ["v1", "v2"]),
|
|
205
|
+
makeProduct("2", ["v1", "v2"]),
|
|
206
|
+
];
|
|
207
|
+
const chunk2 = [makeProduct("3", ["v1", "v2"])];
|
|
208
|
+
global.fetch
|
|
209
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return chunk1; }) })
|
|
210
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return chunk2; }) });
|
|
211
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
212
|
+
expect(result.current.hasMore).toBe(true);
|
|
213
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
214
|
+
yield result.current.loadMore();
|
|
215
|
+
}));
|
|
216
|
+
// One id ("3") still remaining
|
|
217
|
+
expect(result.current.hasMore).toBe(true);
|
|
218
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
219
|
+
yield result.current.loadMore();
|
|
220
|
+
}));
|
|
221
|
+
expect(result.current.hasMore).toBe(false);
|
|
222
|
+
}));
|
|
223
|
+
it("does not fetch when there are no more ids", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
224
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: EMPTY_ITEMS })));
|
|
225
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
226
|
+
yield result.current.loadMore();
|
|
227
|
+
}));
|
|
228
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
229
|
+
}));
|
|
230
|
+
it("builds the request URL with ids, language, and country params", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
231
|
+
;
|
|
232
|
+
global.fetch.mockResolvedValueOnce({
|
|
233
|
+
ok: true,
|
|
234
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return [makeProduct("1")]; }),
|
|
235
|
+
});
|
|
236
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: ONE_ITEM, chunkSize: 1 })));
|
|
237
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
238
|
+
yield result.current.loadMore();
|
|
239
|
+
}));
|
|
240
|
+
const [calledUrl] = global.fetch.mock.calls[0];
|
|
241
|
+
expect(calledUrl).toContain("/products/by-ids");
|
|
242
|
+
expect(calledUrl).toContain("language=EN");
|
|
243
|
+
expect(calledUrl).toContain("country=US");
|
|
244
|
+
expect(calledUrl).toContain("gid%3A%2F%2Fshopify%2FProduct%2F1");
|
|
245
|
+
}));
|
|
246
|
+
it("omits country and language params when not provided", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
247
|
+
;
|
|
248
|
+
global.fetch.mockResolvedValueOnce({
|
|
249
|
+
ok: true,
|
|
250
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return [makeProduct("1")]; }),
|
|
251
|
+
});
|
|
252
|
+
const emptyQueryVars = {};
|
|
253
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: ONE_ITEM, chunkSize: 1, queryVariables: emptyQueryVars })));
|
|
254
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
255
|
+
yield result.current.loadMore();
|
|
256
|
+
}));
|
|
257
|
+
const [calledUrl] = global.fetch.mock.calls[0];
|
|
258
|
+
expect(calledUrl).not.toContain("language=");
|
|
259
|
+
expect(calledUrl).not.toContain("country=");
|
|
260
|
+
}));
|
|
261
|
+
it("sets error state and throws when fetch returns non-ok response", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
262
|
+
var _b;
|
|
263
|
+
;
|
|
264
|
+
global.fetch.mockResolvedValueOnce({
|
|
265
|
+
ok: false,
|
|
266
|
+
status: 500,
|
|
267
|
+
});
|
|
268
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
269
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
270
|
+
try {
|
|
271
|
+
yield result.current.loadMore();
|
|
272
|
+
}
|
|
273
|
+
catch (_c) {
|
|
274
|
+
// expected
|
|
275
|
+
}
|
|
276
|
+
}));
|
|
277
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
278
|
+
expect((_b = result.current.error) === null || _b === void 0 ? void 0 : _b.message).toContain("HTTP error: 500");
|
|
279
|
+
}));
|
|
280
|
+
it("sets error state when fetch throws a network error", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
281
|
+
var _d;
|
|
282
|
+
;
|
|
283
|
+
global.fetch.mockRejectedValueOnce(new Error("Network failure"));
|
|
284
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
285
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
286
|
+
try {
|
|
287
|
+
yield result.current.loadMore();
|
|
288
|
+
}
|
|
289
|
+
catch (_e) {
|
|
290
|
+
// expected
|
|
291
|
+
}
|
|
292
|
+
}));
|
|
293
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
294
|
+
expect((_d = result.current.error) === null || _d === void 0 ? void 0 : _d.message).toBe("Network failure");
|
|
295
|
+
}));
|
|
296
|
+
it("sets isLoading true while fetching and false after with no prior products", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
297
|
+
let resolveFetch;
|
|
298
|
+
const fetchPromise = new Promise((resolve) => {
|
|
299
|
+
resolveFetch = resolve;
|
|
300
|
+
});
|
|
301
|
+
global.fetch.mockReturnValueOnce(fetchPromise);
|
|
302
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
303
|
+
act(() => {
|
|
304
|
+
result.current.loadMore();
|
|
305
|
+
});
|
|
306
|
+
// No products yet → isLoading (not isLoadingMore)
|
|
307
|
+
expect(result.current.isLoading).toBe(true);
|
|
308
|
+
expect(result.current.isLoadingMore).toBe(false);
|
|
309
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
310
|
+
resolveFetch({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return []; }) });
|
|
311
|
+
yield fetchPromise;
|
|
312
|
+
}));
|
|
313
|
+
expect(result.current.isLoading).toBe(false);
|
|
314
|
+
}));
|
|
315
|
+
it("shows isLoadingMore (not isLoading) when products already exist", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
316
|
+
const chunk1 = [
|
|
317
|
+
makeProduct("1", ["v1", "v2"]),
|
|
318
|
+
makeProduct("2", ["v1", "v2"]),
|
|
319
|
+
];
|
|
320
|
+
global.fetch.mockResolvedValueOnce({
|
|
321
|
+
ok: true,
|
|
322
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return chunk1; }),
|
|
323
|
+
});
|
|
324
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
325
|
+
// Load first chunk so products array is populated
|
|
326
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
327
|
+
yield result.current.loadMore();
|
|
328
|
+
}));
|
|
329
|
+
let resolveSecond;
|
|
330
|
+
const secondPromise = new Promise((resolve) => {
|
|
331
|
+
resolveSecond = resolve;
|
|
332
|
+
});
|
|
333
|
+
global.fetch.mockReturnValueOnce(secondPromise);
|
|
334
|
+
act(() => {
|
|
335
|
+
result.current.loadMore();
|
|
336
|
+
});
|
|
337
|
+
expect(result.current.isLoading).toBe(false);
|
|
338
|
+
expect(result.current.isLoadingMore).toBe(true);
|
|
339
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
340
|
+
resolveSecond({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return []; }) });
|
|
341
|
+
yield secondPromise;
|
|
342
|
+
}));
|
|
343
|
+
}));
|
|
344
|
+
it("prevents concurrent loadMore calls via loadingRef guard", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
345
|
+
let resolveFirst;
|
|
346
|
+
const firstPromise = new Promise((resolve) => {
|
|
347
|
+
resolveFirst = resolve;
|
|
348
|
+
});
|
|
349
|
+
global.fetch.mockReturnValueOnce(firstPromise);
|
|
350
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
351
|
+
// Both calls happen synchronously; the second should be blocked by
|
|
352
|
+
// loadingRef.current being true after the first starts.
|
|
353
|
+
act(() => {
|
|
354
|
+
result.current.loadMore();
|
|
355
|
+
result.current.loadMore();
|
|
356
|
+
});
|
|
357
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
358
|
+
resolveFirst({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return [makeProduct("1")]; }) });
|
|
359
|
+
yield firstPromise;
|
|
360
|
+
}));
|
|
361
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
362
|
+
}));
|
|
363
|
+
});
|
|
364
|
+
// ── initialItems with variants (one card per item key) ──────────────────────
|
|
365
|
+
describe("initialItems with variants", () => {
|
|
366
|
+
it("returns products filtered and ordered by initialItems (variant keys)", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
367
|
+
const product1 = makeProduct("1", ["v1", "v2"]);
|
|
368
|
+
const product2 = makeProduct("2", ["v3"]);
|
|
369
|
+
global.fetch.mockResolvedValueOnce({
|
|
370
|
+
ok: true,
|
|
371
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return [product1, product2]; }),
|
|
372
|
+
});
|
|
373
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: TWO_ITEMS_WITH_VARIANTS, chunkSize: 2 })));
|
|
374
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
375
|
+
yield result.current.loadMore();
|
|
376
|
+
}));
|
|
377
|
+
expect(result.current.products).toHaveLength(2);
|
|
378
|
+
// Order follows initialItems: v3 (product2) first, then v2 (product1)
|
|
379
|
+
expect(result.current.products[0].id).toBe(product2.id);
|
|
380
|
+
expect(result.current.products[0].variants).toHaveLength(1);
|
|
381
|
+
expect(result.current.products[0].variants[0].id).toContain("v3");
|
|
382
|
+
expect(result.current.products[1].id).toBe(product1.id);
|
|
383
|
+
expect(result.current.products[1].variants[0].id).toContain("v2");
|
|
384
|
+
}));
|
|
385
|
+
it("preserves global variant order across paginated loads (chunkSize=1)", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
386
|
+
const product1 = makeProduct("1", ["v1", "v2"]);
|
|
387
|
+
const product2 = makeProduct("2", ["v3"]);
|
|
388
|
+
global.fetch
|
|
389
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return [product2]; }) })
|
|
390
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return [product1]; }) });
|
|
391
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: TWO_ITEMS_WITH_VARIANTS, chunkSize: 1 })));
|
|
392
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
393
|
+
yield result.current.loadMore(); // loads product2 (has v3)
|
|
394
|
+
}));
|
|
395
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
396
|
+
yield result.current.loadMore(); // loads product1 (has v2)
|
|
397
|
+
}));
|
|
398
|
+
expect(result.current.products).toHaveLength(2);
|
|
399
|
+
// Final order must follow variantIds: v3 (product2) first, v2 (product1) second.
|
|
400
|
+
expect(result.current.products[0].id).toBe(product2.id);
|
|
401
|
+
expect(result.current.products[0].variants[0].id).toContain("v3");
|
|
402
|
+
expect(result.current.products[1].id).toBe(product1.id);
|
|
403
|
+
expect(result.current.products[1].variants[0].id).toContain("v2");
|
|
404
|
+
}));
|
|
405
|
+
it("excludes variants not found in product list", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
406
|
+
const product1 = makeProduct("1", ["v1"]);
|
|
407
|
+
global.fetch.mockResolvedValueOnce({
|
|
408
|
+
ok: true,
|
|
409
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return [product1]; }),
|
|
410
|
+
});
|
|
411
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: TWO_ITEMS_VARIANT_IDS_ONE, chunkSize: 1 })));
|
|
412
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
413
|
+
yield result.current.loadMore();
|
|
414
|
+
}));
|
|
415
|
+
expect(result.current.products).toHaveLength(1);
|
|
416
|
+
expect(result.current.products[0].variants[0].id).toContain("v1");
|
|
417
|
+
}));
|
|
418
|
+
it("returns all products when items have no variantId", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
419
|
+
const products = [
|
|
420
|
+
makeProduct("1", ["v1", "v2"]),
|
|
421
|
+
makeProduct("2", ["v1", "v2"]),
|
|
422
|
+
];
|
|
423
|
+
global.fetch.mockResolvedValueOnce({
|
|
424
|
+
ok: true,
|
|
425
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return products; }),
|
|
426
|
+
});
|
|
427
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: TWO_ITEMS, chunkSize: 2 })));
|
|
428
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
429
|
+
yield result.current.loadMore();
|
|
430
|
+
}));
|
|
431
|
+
expect(result.current.products).toEqual(products);
|
|
432
|
+
}));
|
|
433
|
+
it("same productId with two variantIds: one API call, two cards", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
434
|
+
const product1 = makeProduct("1", ["v1", "v2"]);
|
|
435
|
+
global.fetch.mockResolvedValueOnce({
|
|
436
|
+
ok: true,
|
|
437
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return [product1]; }),
|
|
438
|
+
});
|
|
439
|
+
const twoVariantsSameProduct = [
|
|
440
|
+
{ productId: "1", variantId: "v1" },
|
|
441
|
+
{ productId: "1", variantId: "v2" },
|
|
442
|
+
];
|
|
443
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: twoVariantsSameProduct, chunkSize: 2 })));
|
|
444
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
445
|
+
yield result.current.loadMore();
|
|
446
|
+
}));
|
|
447
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
448
|
+
expect(result.current.products).toHaveLength(2);
|
|
449
|
+
expect(result.current.products[0].variants[0].id).toContain("v1");
|
|
450
|
+
expect(result.current.products[1].variants[0].id).toContain("v2");
|
|
451
|
+
}));
|
|
452
|
+
it("isPureRemoval removes one of two variant cards and leaves the other", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
453
|
+
const product1 = makeProduct("1", ["v1", "v2"]);
|
|
454
|
+
global.fetch.mockResolvedValueOnce({
|
|
455
|
+
ok: true,
|
|
456
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return [product1]; }),
|
|
457
|
+
});
|
|
458
|
+
const twoVariants = [
|
|
459
|
+
{ productId: "1", variantId: "v1" },
|
|
460
|
+
{ productId: "1", variantId: "v2" },
|
|
461
|
+
];
|
|
462
|
+
const oneVariant = [{ productId: "1", variantId: "v1" }];
|
|
463
|
+
const { result, rerender } = renderHook(({ items }) => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: items, chunkSize: 2 })), { initialProps: { items: twoVariants } });
|
|
464
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
465
|
+
yield result.current.loadMore();
|
|
466
|
+
}));
|
|
467
|
+
expect(result.current.products).toHaveLength(2);
|
|
468
|
+
rerender({ items: oneVariant });
|
|
469
|
+
expect(result.current.products).toHaveLength(1);
|
|
470
|
+
expect(result.current.products[0].variants[0].id).toContain("v1");
|
|
471
|
+
}));
|
|
472
|
+
it("isPureAddition adds second variant of an already-loaded product", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
473
|
+
const product1 = makeProduct("1", ["v1", "v2"]);
|
|
474
|
+
global.fetch
|
|
475
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return [product1]; }) })
|
|
476
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return [product1]; }) });
|
|
477
|
+
const oneVariant = [{ productId: "1", variantId: "v1" }];
|
|
478
|
+
const twoVariants = [
|
|
479
|
+
{ productId: "1", variantId: "v1" },
|
|
480
|
+
{ productId: "1", variantId: "v2" },
|
|
481
|
+
];
|
|
482
|
+
const { result, rerender } = renderHook(({ items }) => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: items, chunkSize: 2 })), { initialProps: { items: oneVariant } });
|
|
483
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
484
|
+
yield result.current.loadMore();
|
|
485
|
+
}));
|
|
486
|
+
expect(result.current.products).toHaveLength(1);
|
|
487
|
+
rerender({ items: twoVariants });
|
|
488
|
+
expect(result.current.products).toHaveLength(1);
|
|
489
|
+
expect(result.current.hasMore).toBe(true);
|
|
490
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
491
|
+
yield result.current.loadMore();
|
|
492
|
+
}));
|
|
493
|
+
expect(result.current.products).toHaveLength(2);
|
|
494
|
+
}));
|
|
495
|
+
it("stale-response bail-out when allItemKeys ref changes during fetch", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
496
|
+
let resolveFirst;
|
|
497
|
+
const firstPromise = new Promise((resolve) => {
|
|
498
|
+
resolveFirst = resolve;
|
|
499
|
+
});
|
|
500
|
+
global.fetch.mockReturnValueOnce(firstPromise);
|
|
501
|
+
const { result, rerender } = renderHook(({ items }) => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: items, chunkSize: 2 })), { initialProps: { items: TWO_ITEMS } });
|
|
502
|
+
act(() => {
|
|
503
|
+
result.current.loadMore();
|
|
504
|
+
});
|
|
505
|
+
rerender({ items: ONE_ITEM });
|
|
506
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
507
|
+
resolveFirst({
|
|
508
|
+
ok: true,
|
|
509
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
510
|
+
return [
|
|
511
|
+
makeProduct("1", ["v1", "v2"]),
|
|
512
|
+
makeProduct("2", ["v1", "v2"]),
|
|
513
|
+
];
|
|
514
|
+
}),
|
|
515
|
+
});
|
|
516
|
+
yield firstPromise;
|
|
517
|
+
}));
|
|
518
|
+
expect(result.current.products).toHaveLength(0);
|
|
519
|
+
}));
|
|
520
|
+
});
|
|
521
|
+
// ── Product sort ordering ─────────────────────────────────────────────────
|
|
522
|
+
describe("product sort ordering (product-only items)", () => {
|
|
523
|
+
it("sorts products to match the requested ID order when the API returns them out of order", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
524
|
+
const product1 = makeProduct("1", ["v1", "v2"]);
|
|
525
|
+
const product2 = makeProduct("2", ["v1", "v2"]);
|
|
526
|
+
const product3 = makeProduct("3", ["v1", "v2"]);
|
|
527
|
+
global.fetch.mockResolvedValueOnce({
|
|
528
|
+
ok: true,
|
|
529
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return [product3, product1, product2]; }),
|
|
530
|
+
});
|
|
531
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: THREE_ITEMS_GIDS, chunkSize: 3 })));
|
|
532
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
533
|
+
yield result.current.loadMore();
|
|
534
|
+
}));
|
|
535
|
+
expect(result.current.products[0].id).toBe(GID_1);
|
|
536
|
+
expect(result.current.products[1].id).toBe(GID_2);
|
|
537
|
+
expect(result.current.products[2].id).toBe(GID_3);
|
|
538
|
+
}));
|
|
539
|
+
it("preserves wishlist order across paginated chunks", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
540
|
+
;
|
|
541
|
+
global.fetch
|
|
542
|
+
.mockResolvedValueOnce({
|
|
543
|
+
ok: true,
|
|
544
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
545
|
+
return [
|
|
546
|
+
makeProduct("2", ["v1", "v2"]),
|
|
547
|
+
makeProduct("1", ["v1", "v2"]),
|
|
548
|
+
];
|
|
549
|
+
}),
|
|
550
|
+
})
|
|
551
|
+
.mockResolvedValueOnce({
|
|
552
|
+
ok: true,
|
|
553
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return [makeProduct("3", ["v1", "v2"])]; }),
|
|
554
|
+
});
|
|
555
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: THREE_ITEMS_GIDS, chunkSize: 2 })));
|
|
556
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
557
|
+
yield result.current.loadMore();
|
|
558
|
+
}));
|
|
559
|
+
expect(result.current.products[0].id).toBe(GID_1);
|
|
560
|
+
expect(result.current.products[1].id).toBe(GID_2);
|
|
561
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
562
|
+
yield result.current.loadMore();
|
|
563
|
+
}));
|
|
564
|
+
expect(result.current.products).toHaveLength(3);
|
|
565
|
+
expect(result.current.products[2].id).toBe(GID_3);
|
|
566
|
+
}));
|
|
567
|
+
});
|
|
568
|
+
// ── Mock mode ──────────────────────────────────────────────────────────────
|
|
569
|
+
describe("mock mode (shouldMock: true)", () => {
|
|
570
|
+
it("does not fetch until collectionToFetchID is available", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
571
|
+
setupCollection(undefined);
|
|
572
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { shouldMock: true, initialItems: EMPTY_ITEMS })));
|
|
573
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
574
|
+
yield new Promise((resolve) => setTimeout(resolve, 0));
|
|
575
|
+
}));
|
|
576
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
577
|
+
expect(result.current.products).toEqual([]);
|
|
578
|
+
}));
|
|
579
|
+
it("auto-fetches mocked products when collectionToFetchID becomes available", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
580
|
+
const mockedProducts = [makeProduct("m1"), makeProduct("m2")];
|
|
581
|
+
setupCollection("coll-abc");
|
|
582
|
+
global.fetch.mockResolvedValueOnce({
|
|
583
|
+
ok: true,
|
|
584
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({ products: mockedProducts }); }),
|
|
585
|
+
});
|
|
586
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { shouldMock: true, initialItems: EMPTY_ITEMS })));
|
|
587
|
+
yield waitFor(() => {
|
|
588
|
+
expect(result.current.products).toHaveLength(2);
|
|
589
|
+
});
|
|
590
|
+
expect(result.current.products).toEqual(mockedProducts);
|
|
591
|
+
}));
|
|
592
|
+
it("posts to /products/by-page with correct body", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
593
|
+
setupCollection("coll-xyz");
|
|
594
|
+
global.fetch.mockResolvedValueOnce({
|
|
595
|
+
ok: true,
|
|
596
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
597
|
+
return ({
|
|
598
|
+
products: [makeProduct("m1"), makeProduct("m2")],
|
|
599
|
+
});
|
|
600
|
+
}),
|
|
601
|
+
});
|
|
602
|
+
renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { shouldMock: true, initialItems: EMPTY_ITEMS })));
|
|
603
|
+
yield waitFor(() => {
|
|
604
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
605
|
+
});
|
|
606
|
+
const [calledUrl, fetchOptions] = global.fetch.mock
|
|
607
|
+
.calls[0];
|
|
608
|
+
expect(calledUrl).toContain("/products/by-page");
|
|
609
|
+
expect(fetchOptions.method).toBe("POST");
|
|
610
|
+
const body = JSON.parse(fetchOptions.body);
|
|
611
|
+
expect(body).toMatchObject({
|
|
612
|
+
language: "EN",
|
|
613
|
+
country: "US",
|
|
614
|
+
appId: "app-123",
|
|
615
|
+
collectionId: "coll-xyz",
|
|
616
|
+
});
|
|
617
|
+
}));
|
|
618
|
+
it("slices mocked products to at most 2", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
619
|
+
setupCollection("coll-abc");
|
|
620
|
+
global.fetch.mockResolvedValueOnce({
|
|
621
|
+
ok: true,
|
|
622
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
623
|
+
return ({
|
|
624
|
+
products: [makeProduct("1"), makeProduct("2"), makeProduct("3")],
|
|
625
|
+
});
|
|
626
|
+
}),
|
|
627
|
+
});
|
|
628
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { shouldMock: true, initialItems: EMPTY_ITEMS })));
|
|
629
|
+
yield waitFor(() => {
|
|
630
|
+
expect(result.current.products).toHaveLength(2);
|
|
631
|
+
});
|
|
632
|
+
}));
|
|
633
|
+
it("replaces products instead of appending on subsequent loadMore calls", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
634
|
+
setupCollection("coll-abc");
|
|
635
|
+
const batch = [makeProduct("m1"), makeProduct("m2")];
|
|
636
|
+
global.fetch
|
|
637
|
+
.mockResolvedValueOnce({
|
|
638
|
+
ok: true,
|
|
639
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({ products: batch }); }),
|
|
640
|
+
})
|
|
641
|
+
.mockResolvedValueOnce({
|
|
642
|
+
ok: true,
|
|
643
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({ products: batch }); }),
|
|
644
|
+
});
|
|
645
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { shouldMock: true, initialItems: EMPTY_ITEMS })));
|
|
646
|
+
yield waitFor(() => {
|
|
647
|
+
expect(result.current.products).toHaveLength(2);
|
|
648
|
+
});
|
|
649
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
650
|
+
yield result.current.loadMore();
|
|
651
|
+
}));
|
|
652
|
+
// Still 2 products – replaced, not appended
|
|
653
|
+
expect(result.current.products).toHaveLength(2);
|
|
654
|
+
}));
|
|
655
|
+
it("hasMore is true when products.length < 2 in mock mode", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
656
|
+
setupCollection("coll-abc");
|
|
657
|
+
global.fetch.mockResolvedValueOnce({
|
|
658
|
+
ok: true,
|
|
659
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({ products: [makeProduct("m1")] }); }),
|
|
660
|
+
});
|
|
661
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { shouldMock: true, initialItems: EMPTY_ITEMS })));
|
|
662
|
+
yield waitFor(() => {
|
|
663
|
+
expect(result.current.products).toHaveLength(1);
|
|
664
|
+
});
|
|
665
|
+
expect(result.current.hasMore).toBe(true);
|
|
666
|
+
}));
|
|
667
|
+
it("hasMore is false when products.length >= 2 in mock mode", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
668
|
+
setupCollection("coll-abc");
|
|
669
|
+
global.fetch.mockResolvedValueOnce({
|
|
670
|
+
ok: true,
|
|
671
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
672
|
+
return ({
|
|
673
|
+
products: [makeProduct("m1"), makeProduct("m2")],
|
|
674
|
+
});
|
|
675
|
+
}),
|
|
676
|
+
});
|
|
677
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { shouldMock: true, initialItems: EMPTY_ITEMS })));
|
|
678
|
+
yield waitFor(() => {
|
|
679
|
+
expect(result.current.products).toHaveLength(2);
|
|
680
|
+
});
|
|
681
|
+
expect(result.current.hasMore).toBe(false);
|
|
682
|
+
}));
|
|
683
|
+
it("returns empty products when server response has no products key", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
684
|
+
// Covers the `data.products?.slice(0, 2) || []` fallback in
|
|
685
|
+
// fetchMockedProducts when the server omits the products array.
|
|
686
|
+
setupCollection("coll-abc");
|
|
687
|
+
global.fetch.mockResolvedValueOnce({
|
|
688
|
+
ok: true,
|
|
689
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); }),
|
|
690
|
+
});
|
|
691
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { shouldMock: true, initialItems: EMPTY_ITEMS })));
|
|
692
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
693
|
+
yield new Promise((resolve) => setTimeout(resolve, 0));
|
|
694
|
+
}));
|
|
695
|
+
expect(result.current.products).toEqual([]);
|
|
696
|
+
expect(result.current.error).toBeNull();
|
|
697
|
+
}));
|
|
698
|
+
it("returns empty array from fetchMockedProducts when collectionToFetchID is absent", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
699
|
+
setupCollection(undefined);
|
|
700
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { shouldMock: true, initialItems: EMPTY_ITEMS })));
|
|
701
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
702
|
+
yield result.current.loadMore();
|
|
703
|
+
}));
|
|
704
|
+
expect(result.current.products).toEqual([]);
|
|
705
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
706
|
+
}));
|
|
707
|
+
});
|
|
708
|
+
// ── initialItems change effect ─────────────────────────────────────────────
|
|
709
|
+
describe("initialItems change", () => {
|
|
710
|
+
it("preserves products and surfaces hasMore when a new item is appended", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
711
|
+
;
|
|
712
|
+
global.fetch.mockResolvedValueOnce({
|
|
713
|
+
ok: true,
|
|
714
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
715
|
+
return [
|
|
716
|
+
makeProduct("1", ["v1", "v2"]),
|
|
717
|
+
makeProduct("2", ["v1", "v2"]),
|
|
718
|
+
];
|
|
719
|
+
}),
|
|
720
|
+
});
|
|
721
|
+
const { result, rerender } = renderHook(({ items }) => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: items })), { initialProps: { items: TWO_ITEMS } });
|
|
722
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
723
|
+
yield result.current.loadMore();
|
|
724
|
+
}));
|
|
725
|
+
expect(result.current.products).toHaveLength(2);
|
|
726
|
+
// Pure addition: existing products must be preserved, new item surfaced via hasMore
|
|
727
|
+
rerender({ items: THREE_ITEMS });
|
|
728
|
+
expect(result.current.products).toHaveLength(2);
|
|
729
|
+
expect(result.current.hasMore).toBe(true);
|
|
730
|
+
}));
|
|
731
|
+
it("does not reset on the initial mount", () => {
|
|
732
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
733
|
+
expect(result.current.products).toEqual([]);
|
|
734
|
+
});
|
|
735
|
+
it("does not reset when a new array reference has identical content", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
736
|
+
;
|
|
737
|
+
global.fetch.mockResolvedValueOnce({
|
|
738
|
+
ok: true,
|
|
739
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
740
|
+
return [
|
|
741
|
+
makeProduct("1", ["v1", "v2"]),
|
|
742
|
+
makeProduct("2", ["v1", "v2"]),
|
|
743
|
+
];
|
|
744
|
+
}),
|
|
745
|
+
});
|
|
746
|
+
const { result, rerender } = renderHook(({ items }) => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: items })), { initialProps: { items: TWO_ITEMS } });
|
|
747
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
748
|
+
yield result.current.loadMore();
|
|
749
|
+
}));
|
|
750
|
+
expect(result.current.products).toHaveLength(2);
|
|
751
|
+
// New reference, same values → must be a no-op
|
|
752
|
+
rerender({ items: [{ productId: "1" }, { productId: "2" }] });
|
|
753
|
+
expect(result.current.products).toHaveLength(2);
|
|
754
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
755
|
+
}));
|
|
756
|
+
it("removes a product in-place without resetting when a single item is removed", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
757
|
+
;
|
|
758
|
+
global.fetch.mockResolvedValueOnce({
|
|
759
|
+
ok: true,
|
|
760
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
761
|
+
return [
|
|
762
|
+
makeProduct("1", ["v1", "v2"]),
|
|
763
|
+
makeProduct("2", ["v1", "v2"]),
|
|
764
|
+
makeProduct("3", ["v1", "v2"]),
|
|
765
|
+
];
|
|
766
|
+
}),
|
|
767
|
+
});
|
|
768
|
+
const { result, rerender } = renderHook(({ items }) => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: items, chunkSize: 3 })), { initialProps: { items: THREE_ITEMS_GIDS } });
|
|
769
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
770
|
+
yield result.current.loadMore();
|
|
771
|
+
}));
|
|
772
|
+
expect(result.current.products).toHaveLength(3);
|
|
773
|
+
rerender({ items: TWO_ITEMS_GIDS });
|
|
774
|
+
expect(result.current.products).toHaveLength(2);
|
|
775
|
+
expect(result.current.products.map((p) => p.id)).not.toContain(GID_3);
|
|
776
|
+
// Products were filtered in-place – no re-fetch
|
|
777
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
778
|
+
}));
|
|
779
|
+
it("removes multiple products in-place when several items are removed at once", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
780
|
+
;
|
|
781
|
+
global.fetch.mockResolvedValueOnce({
|
|
782
|
+
ok: true,
|
|
783
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
784
|
+
return [
|
|
785
|
+
makeProduct("1", ["v1", "v2"]),
|
|
786
|
+
makeProduct("2", ["v1", "v2"]),
|
|
787
|
+
makeProduct("3", ["v1", "v2"]),
|
|
788
|
+
];
|
|
789
|
+
}),
|
|
790
|
+
});
|
|
791
|
+
const { result, rerender } = renderHook(({ items }) => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: items, chunkSize: 3 })), { initialProps: { items: THREE_ITEMS_GIDS } });
|
|
792
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
793
|
+
yield result.current.loadMore();
|
|
794
|
+
}));
|
|
795
|
+
expect(result.current.products).toHaveLength(3);
|
|
796
|
+
rerender({ items: [{ productId: GID_2 }] });
|
|
797
|
+
expect(result.current.products).toHaveLength(1);
|
|
798
|
+
expect(result.current.products[0].id).toBe(GID_2);
|
|
799
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
800
|
+
}));
|
|
801
|
+
it("recalculates loadedIndex correctly after removal so subsequent loadMore fetches the right next chunk", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
802
|
+
// Setup: 4 items, chunkSize 2.
|
|
803
|
+
// Step 1 – load first chunk [GID_1, GID_2], loadedIndex becomes 2.
|
|
804
|
+
// Step 2 – remove GID_2, new list is [GID_1, GID_3, GID_4].
|
|
805
|
+
// newLoadedIndex should be 1 (only GID_1 was in prev[0..1]).
|
|
806
|
+
// Step 3 – loadMore must fetch [GID_3, GID_4], not re-fetch GID_1.
|
|
807
|
+
;
|
|
808
|
+
global.fetch
|
|
809
|
+
.mockResolvedValueOnce({
|
|
810
|
+
ok: true,
|
|
811
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
812
|
+
return [
|
|
813
|
+
makeProduct("1", ["v1", "v2"]),
|
|
814
|
+
makeProduct("2", ["v1", "v2"]),
|
|
815
|
+
];
|
|
816
|
+
}),
|
|
817
|
+
})
|
|
818
|
+
.mockResolvedValueOnce({
|
|
819
|
+
ok: true,
|
|
820
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
821
|
+
return [
|
|
822
|
+
makeProduct("3", ["v1", "v2"]),
|
|
823
|
+
makeProduct("4", ["v1", "v2"]),
|
|
824
|
+
];
|
|
825
|
+
}),
|
|
826
|
+
});
|
|
827
|
+
const { result, rerender } = renderHook(({ items }) => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: items, chunkSize: 2 })), { initialProps: { items: FOUR_ITEMS_GIDS } });
|
|
828
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
829
|
+
yield result.current.loadMore();
|
|
830
|
+
}));
|
|
831
|
+
expect(result.current.products).toHaveLength(2);
|
|
832
|
+
rerender({ items: FOUR_ITEMS_GIDS_NO_2 });
|
|
833
|
+
expect(result.current.products).toHaveLength(1);
|
|
834
|
+
expect(result.current.products[0].id).toBe(GID_1);
|
|
835
|
+
expect(result.current.hasMore).toBe(true);
|
|
836
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
837
|
+
yield result.current.loadMore();
|
|
838
|
+
}));
|
|
839
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
840
|
+
expect(result.current.products).toHaveLength(3);
|
|
841
|
+
expect(result.current.products.map((p) => p.id)).toContain(GID_3);
|
|
842
|
+
expect(result.current.products.map((p) => p.id)).toContain(GID_4);
|
|
843
|
+
}));
|
|
844
|
+
it("keeps existing products visible and exposes the new item via hasMore when an item is added", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
845
|
+
;
|
|
846
|
+
global.fetch
|
|
847
|
+
.mockResolvedValueOnce({
|
|
848
|
+
ok: true,
|
|
849
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
850
|
+
return [
|
|
851
|
+
makeProduct("1", ["v1", "v2"]),
|
|
852
|
+
makeProduct("2", ["v1", "v2"]),
|
|
853
|
+
makeProduct("3", ["v1", "v2"]),
|
|
854
|
+
];
|
|
855
|
+
}),
|
|
856
|
+
})
|
|
857
|
+
.mockResolvedValueOnce({
|
|
858
|
+
ok: true,
|
|
859
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return [makeProduct("4", ["v1", "v2"])]; }),
|
|
860
|
+
});
|
|
861
|
+
const { result, rerender } = renderHook(({ items }) => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: items, chunkSize: 3 })), { initialProps: { items: THREE_ITEMS_GIDS } });
|
|
862
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
863
|
+
yield result.current.loadMore();
|
|
864
|
+
}));
|
|
865
|
+
expect(result.current.products).toHaveLength(3);
|
|
866
|
+
rerender({ items: FOUR_ITEMS_GIDS });
|
|
867
|
+
// Existing products preserved — no skeleton flash
|
|
868
|
+
expect(result.current.products).toHaveLength(3);
|
|
869
|
+
expect(result.current.hasMore).toBe(true);
|
|
870
|
+
// Fetch the new item
|
|
871
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
872
|
+
yield result.current.loadMore();
|
|
873
|
+
}));
|
|
874
|
+
expect(result.current.products).toHaveLength(4);
|
|
875
|
+
expect(result.current.products[3].id).toBe(GID_4);
|
|
876
|
+
}));
|
|
877
|
+
it("does not create duplicate products when loadMore re-requests an already-loaded ID", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
878
|
+
;
|
|
879
|
+
global.fetch
|
|
880
|
+
.mockResolvedValueOnce({
|
|
881
|
+
ok: true,
|
|
882
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
883
|
+
return [
|
|
884
|
+
makeProduct("1", ["v1", "v2"]),
|
|
885
|
+
makeProduct("2", ["v1", "v2"]),
|
|
886
|
+
];
|
|
887
|
+
}),
|
|
888
|
+
})
|
|
889
|
+
.mockResolvedValueOnce({
|
|
890
|
+
ok: true,
|
|
891
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
892
|
+
return [
|
|
893
|
+
makeProduct("new", ["v1", "v2"]),
|
|
894
|
+
makeProduct("1", ["v1", "v2"]),
|
|
895
|
+
];
|
|
896
|
+
}),
|
|
897
|
+
});
|
|
898
|
+
const GID_NEW = "gid://shopify/Product/new";
|
|
899
|
+
const initialItems = [{ productId: GID_1 }, { productId: GID_2 }];
|
|
900
|
+
const itemsWithPrepend = [
|
|
901
|
+
{ productId: GID_NEW },
|
|
902
|
+
{ productId: GID_1 },
|
|
903
|
+
{ productId: GID_2 },
|
|
904
|
+
];
|
|
905
|
+
const { result, rerender } = renderHook(({ items }) => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: items, chunkSize: 2 })), { initialProps: { items: initialItems } });
|
|
906
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
907
|
+
yield result.current.loadMore();
|
|
908
|
+
}));
|
|
909
|
+
expect(result.current.products).toHaveLength(2);
|
|
910
|
+
rerender({ items: itemsWithPrepend });
|
|
911
|
+
expect(result.current.products).toHaveLength(2);
|
|
912
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
913
|
+
yield result.current.loadMore();
|
|
914
|
+
}));
|
|
915
|
+
const ids = result.current.products.map((p) => p.id);
|
|
916
|
+
const uniqueIds = new Set(ids);
|
|
917
|
+
expect(uniqueIds.size).toBe(ids.length);
|
|
918
|
+
expect(ids).toEqual([GID_NEW, GID_1, GID_2]);
|
|
919
|
+
}));
|
|
920
|
+
it("performs a full reset when the list is replaced with different items", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
921
|
+
;
|
|
922
|
+
global.fetch.mockResolvedValueOnce({
|
|
923
|
+
ok: true,
|
|
924
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
925
|
+
return [
|
|
926
|
+
makeProduct("1", ["v1", "v2"]),
|
|
927
|
+
makeProduct("2", ["v1", "v2"]),
|
|
928
|
+
];
|
|
929
|
+
}),
|
|
930
|
+
});
|
|
931
|
+
const DIFFERENT_ITEMS = [
|
|
932
|
+
{ productId: "10" },
|
|
933
|
+
{ productId: "11" },
|
|
934
|
+
{ productId: "12" },
|
|
935
|
+
];
|
|
936
|
+
const { result, rerender } = renderHook(({ items }) => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: items })), { initialProps: { items: TWO_ITEMS } });
|
|
937
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
938
|
+
yield result.current.loadMore();
|
|
939
|
+
}));
|
|
940
|
+
expect(result.current.products).toHaveLength(2);
|
|
941
|
+
rerender({ items: DIFFERENT_ITEMS });
|
|
942
|
+
expect(result.current.products).toEqual([]);
|
|
943
|
+
expect(result.current.hasMore).toBe(true);
|
|
944
|
+
}));
|
|
945
|
+
});
|
|
946
|
+
// ── inView effect ─────────────────────────────────────────────────────────
|
|
947
|
+
describe("inView intersection observer", () => {
|
|
948
|
+
it("triggers loadMore when element comes into view and hasMore is true", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
949
|
+
setupInView(true);
|
|
950
|
+
global.fetch.mockResolvedValueOnce({
|
|
951
|
+
ok: true,
|
|
952
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
953
|
+
return [
|
|
954
|
+
makeProduct("1", ["v1", "v2"]),
|
|
955
|
+
makeProduct("2", ["v1", "v2"]),
|
|
956
|
+
];
|
|
957
|
+
}),
|
|
958
|
+
});
|
|
959
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: TWO_ITEMS, chunkSize: 2 })));
|
|
960
|
+
yield waitFor(() => {
|
|
961
|
+
expect(result.current.products).toHaveLength(2);
|
|
962
|
+
});
|
|
963
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
964
|
+
}));
|
|
965
|
+
it("does not trigger loadMore when inView is false", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
966
|
+
setupInView(false);
|
|
967
|
+
renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
968
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
969
|
+
yield new Promise((resolve) => setTimeout(resolve, 0));
|
|
970
|
+
}));
|
|
971
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
972
|
+
}));
|
|
973
|
+
it("does not trigger loadMore when inView but no more items", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
974
|
+
setupInView(true);
|
|
975
|
+
renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: EMPTY_ITEMS })));
|
|
976
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
977
|
+
yield new Promise((resolve) => setTimeout(resolve, 0));
|
|
978
|
+
}));
|
|
979
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
980
|
+
}));
|
|
981
|
+
});
|
|
982
|
+
// ── reset ─────────────────────────────────────────────────────────────────
|
|
983
|
+
describe("reset", () => {
|
|
984
|
+
it("clears products and resets loadedIndex", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
985
|
+
;
|
|
986
|
+
global.fetch.mockResolvedValueOnce({
|
|
987
|
+
ok: true,
|
|
988
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
989
|
+
return [
|
|
990
|
+
makeProduct("1", ["v1", "v2"]),
|
|
991
|
+
makeProduct("2", ["v1", "v2"]),
|
|
992
|
+
];
|
|
993
|
+
}),
|
|
994
|
+
});
|
|
995
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
996
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
997
|
+
yield result.current.loadMore();
|
|
998
|
+
}));
|
|
999
|
+
expect(result.current.products).toHaveLength(2);
|
|
1000
|
+
act(() => {
|
|
1001
|
+
result.current.reset();
|
|
1002
|
+
});
|
|
1003
|
+
expect(result.current.products).toEqual([]);
|
|
1004
|
+
expect(result.current.hasMore).toBe(true);
|
|
1005
|
+
}));
|
|
1006
|
+
it("allows re-fetching from the start after reset", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1007
|
+
const products = [
|
|
1008
|
+
makeProduct("1", ["v1", "v2"]),
|
|
1009
|
+
makeProduct("2", ["v1", "v2"]),
|
|
1010
|
+
];
|
|
1011
|
+
global.fetch
|
|
1012
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return products; }) })
|
|
1013
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return products; }) });
|
|
1014
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: TWO_ITEMS, chunkSize: 2 })));
|
|
1015
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1016
|
+
yield result.current.loadMore();
|
|
1017
|
+
}));
|
|
1018
|
+
act(() => {
|
|
1019
|
+
result.current.reset();
|
|
1020
|
+
});
|
|
1021
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1022
|
+
yield result.current.loadMore();
|
|
1023
|
+
}));
|
|
1024
|
+
expect(result.current.products).toHaveLength(2);
|
|
1025
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
1026
|
+
}));
|
|
1027
|
+
});
|
|
1028
|
+
// ── useCollection integration ─────────────────────────────────────────────
|
|
1029
|
+
describe("useCollection integration", () => {
|
|
1030
|
+
it("passes correct options to useCollection", () => {
|
|
1031
|
+
renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { shouldMock: true })));
|
|
1032
|
+
expect(mockUseCollection).toHaveBeenCalledWith(expect.objectContaining({
|
|
1033
|
+
apiUrl: DEFAULT_PROPS.apiUrl,
|
|
1034
|
+
appId: "app-123",
|
|
1035
|
+
language: "EN",
|
|
1036
|
+
getCollections: true,
|
|
1037
|
+
limit: 1,
|
|
1038
|
+
}));
|
|
1039
|
+
});
|
|
1040
|
+
it("passes getCollections: false when shouldMock is false", () => {
|
|
1041
|
+
renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { shouldMock: false })));
|
|
1042
|
+
expect(mockUseCollection).toHaveBeenCalledWith(expect.objectContaining({ getCollections: false }));
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
describe("API contract and backward compatibility", () => {
|
|
1046
|
+
it("returns all fields so forked blocks do not break", () => {
|
|
1047
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
1048
|
+
expect(result.current).toMatchObject({
|
|
1049
|
+
products: expect.any(Array),
|
|
1050
|
+
isLoading: expect.any(Boolean),
|
|
1051
|
+
isLoadingMore: expect.any(Boolean),
|
|
1052
|
+
error: null,
|
|
1053
|
+
hasMore: expect.any(Boolean),
|
|
1054
|
+
loadMore: expect.any(Function),
|
|
1055
|
+
loadMoreRef: expect.any(Function),
|
|
1056
|
+
reset: expect.any(Function),
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
it("products: initial empty array, then fetched product data in order after loadMore", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1060
|
+
const p1 = makeProduct("1", ["v1", "v2"]);
|
|
1061
|
+
const p2 = makeProduct("2", ["v1", "v2"]);
|
|
1062
|
+
global.fetch.mockResolvedValueOnce({
|
|
1063
|
+
ok: true,
|
|
1064
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return [p1, p2]; }),
|
|
1065
|
+
});
|
|
1066
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
1067
|
+
expect(result.current.products).toEqual([]);
|
|
1068
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1069
|
+
yield result.current.loadMore();
|
|
1070
|
+
}));
|
|
1071
|
+
expect(result.current.products).toHaveLength(2);
|
|
1072
|
+
expect(result.current.products[0].id).toBe(p1.id);
|
|
1073
|
+
expect(result.current.products[1].id).toBe(p2.id);
|
|
1074
|
+
}));
|
|
1075
|
+
it("isLoading: true only while first fetch in progress and no products yet", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1076
|
+
let resolveFetch;
|
|
1077
|
+
const fetchPromise = new Promise((resolve) => {
|
|
1078
|
+
resolveFetch = resolve;
|
|
1079
|
+
});
|
|
1080
|
+
global.fetch.mockReturnValueOnce(fetchPromise);
|
|
1081
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
1082
|
+
expect(result.current.products).toHaveLength(0);
|
|
1083
|
+
act(() => {
|
|
1084
|
+
result.current.loadMore();
|
|
1085
|
+
});
|
|
1086
|
+
expect(result.current.isLoading).toBe(true);
|
|
1087
|
+
expect(result.current.isLoadingMore).toBe(false);
|
|
1088
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1089
|
+
resolveFetch({
|
|
1090
|
+
ok: true,
|
|
1091
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1092
|
+
return [
|
|
1093
|
+
makeProduct("1", ["v1", "v2"]),
|
|
1094
|
+
makeProduct("2", ["v1", "v2"]),
|
|
1095
|
+
];
|
|
1096
|
+
}),
|
|
1097
|
+
});
|
|
1098
|
+
yield fetchPromise;
|
|
1099
|
+
}));
|
|
1100
|
+
expect(result.current.isLoading).toBe(false);
|
|
1101
|
+
expect(result.current.products).toHaveLength(2);
|
|
1102
|
+
}));
|
|
1103
|
+
it("isLoadingMore: true while loading when products already exist", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1104
|
+
const chunk1 = [
|
|
1105
|
+
makeProduct("1", ["v1", "v2"]),
|
|
1106
|
+
makeProduct("2", ["v1", "v2"]),
|
|
1107
|
+
];
|
|
1108
|
+
let resolveSecond;
|
|
1109
|
+
const secondPromise = new Promise((resolve) => {
|
|
1110
|
+
resolveSecond = resolve;
|
|
1111
|
+
});
|
|
1112
|
+
global.fetch
|
|
1113
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return chunk1; }) })
|
|
1114
|
+
.mockReturnValueOnce(secondPromise);
|
|
1115
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
1116
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1117
|
+
yield result.current.loadMore();
|
|
1118
|
+
}));
|
|
1119
|
+
expect(result.current.products).toHaveLength(2);
|
|
1120
|
+
act(() => {
|
|
1121
|
+
result.current.loadMore();
|
|
1122
|
+
});
|
|
1123
|
+
expect(result.current.isLoading).toBe(false);
|
|
1124
|
+
expect(result.current.isLoadingMore).toBe(true);
|
|
1125
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1126
|
+
resolveSecond({
|
|
1127
|
+
ok: true,
|
|
1128
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return [makeProduct("3", ["v1", "v2"])]; }),
|
|
1129
|
+
});
|
|
1130
|
+
yield secondPromise;
|
|
1131
|
+
}));
|
|
1132
|
+
expect(result.current.isLoadingMore).toBe(false);
|
|
1133
|
+
expect(result.current.products).toHaveLength(3);
|
|
1134
|
+
}));
|
|
1135
|
+
it("error: null on success, set when fetch fails", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1136
|
+
;
|
|
1137
|
+
global.fetch.mockResolvedValueOnce({
|
|
1138
|
+
ok: false,
|
|
1139
|
+
status: 500,
|
|
1140
|
+
});
|
|
1141
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
1142
|
+
expect(result.current.error).toBeNull();
|
|
1143
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1144
|
+
try {
|
|
1145
|
+
yield result.current.loadMore();
|
|
1146
|
+
}
|
|
1147
|
+
catch (_a) {
|
|
1148
|
+
// expected
|
|
1149
|
+
}
|
|
1150
|
+
}));
|
|
1151
|
+
expect(result.current.error).not.toBeNull();
|
|
1152
|
+
}));
|
|
1153
|
+
it("hasMore: true when items remain to load, false when empty or all loaded", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1154
|
+
const twoProducts = [
|
|
1155
|
+
makeProduct("1", ["v1", "v2"]),
|
|
1156
|
+
makeProduct("2", ["v1", "v2"]),
|
|
1157
|
+
];
|
|
1158
|
+
global.fetch.mockResolvedValueOnce({
|
|
1159
|
+
ok: true,
|
|
1160
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return twoProducts; }),
|
|
1161
|
+
});
|
|
1162
|
+
const empty = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: EMPTY_ITEMS })));
|
|
1163
|
+
expect(empty.result.current.hasMore).toBe(false);
|
|
1164
|
+
const withItems = renderHook(() => useInfiniteWishlist(Object.assign(Object.assign({}, DEFAULT_PROPS), { initialItems: TWO_ITEMS })));
|
|
1165
|
+
expect(withItems.result.current.hasMore).toBe(true);
|
|
1166
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1167
|
+
yield withItems.result.current.loadMore();
|
|
1168
|
+
}));
|
|
1169
|
+
expect(withItems.result.current.hasMore).toBe(false);
|
|
1170
|
+
expect(withItems.result.current.products).toHaveLength(2);
|
|
1171
|
+
}));
|
|
1172
|
+
it("loadMore: fetches next chunk and merges into products in order", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1173
|
+
const chunk1 = [
|
|
1174
|
+
makeProduct("1", ["v1", "v2"]),
|
|
1175
|
+
makeProduct("2", ["v1", "v2"]),
|
|
1176
|
+
];
|
|
1177
|
+
const chunk2 = [makeProduct("3", ["v1", "v2"])];
|
|
1178
|
+
global.fetch
|
|
1179
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return chunk1; }) })
|
|
1180
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return chunk2; }) });
|
|
1181
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
1182
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1183
|
+
yield result.current.loadMore();
|
|
1184
|
+
}));
|
|
1185
|
+
expect(result.current.products).toHaveLength(2);
|
|
1186
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1187
|
+
yield result.current.loadMore();
|
|
1188
|
+
}));
|
|
1189
|
+
expect(result.current.products).toHaveLength(3);
|
|
1190
|
+
expect(result.current.products[2].id).toContain("3");
|
|
1191
|
+
}));
|
|
1192
|
+
it("loadMoreRef: stable ref callback for intersection observer", () => {
|
|
1193
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
1194
|
+
expect(typeof result.current.loadMoreRef).toBe("function");
|
|
1195
|
+
expect(() => result.current.loadMoreRef(null)).not.toThrow();
|
|
1196
|
+
expect(() => result.current.loadMoreRef(document.createElement("div"))).not.toThrow();
|
|
1197
|
+
});
|
|
1198
|
+
it("reset: clears products and loaded state, next loadMore fetches from start", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1199
|
+
const twoProducts = [
|
|
1200
|
+
makeProduct("1", ["v1", "v2"]),
|
|
1201
|
+
makeProduct("2", ["v1", "v2"]),
|
|
1202
|
+
];
|
|
1203
|
+
global.fetch
|
|
1204
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return twoProducts; }) })
|
|
1205
|
+
.mockResolvedValueOnce({ ok: true, json: () => __awaiter(void 0, void 0, void 0, function* () { return twoProducts; }) });
|
|
1206
|
+
const { result } = renderHook(() => useInfiniteWishlist(Object.assign({}, DEFAULT_PROPS)));
|
|
1207
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1208
|
+
yield result.current.loadMore();
|
|
1209
|
+
}));
|
|
1210
|
+
expect(result.current.products).toHaveLength(2);
|
|
1211
|
+
act(() => {
|
|
1212
|
+
result.current.reset();
|
|
1213
|
+
});
|
|
1214
|
+
expect(result.current.products).toEqual([]);
|
|
1215
|
+
expect(result.current.hasMore).toBe(true);
|
|
1216
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1217
|
+
yield result.current.loadMore();
|
|
1218
|
+
}));
|
|
1219
|
+
expect(result.current.products).toHaveLength(2);
|
|
1220
|
+
}));
|
|
1221
|
+
it("accepts legacy initialProductIds + variantIds and behaves like initialItems", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1222
|
+
const product1 = makeProduct("1", ["v1", "v2"]);
|
|
1223
|
+
const product2 = makeProduct("2", ["v3"]);
|
|
1224
|
+
global.fetch.mockResolvedValueOnce({
|
|
1225
|
+
ok: true,
|
|
1226
|
+
json: () => __awaiter(void 0, void 0, void 0, function* () { return [product1, product2]; }),
|
|
1227
|
+
});
|
|
1228
|
+
const { result } = renderHook(() => useInfiniteWishlist({
|
|
1229
|
+
chunkSize: 2,
|
|
1230
|
+
apiUrl: DEFAULT_PROPS.apiUrl,
|
|
1231
|
+
shouldMock: false,
|
|
1232
|
+
queryVariables: DEFAULT_PROPS.queryVariables,
|
|
1233
|
+
initialProductIds: ["1", "2"],
|
|
1234
|
+
variantIds: ["v2", "v3"],
|
|
1235
|
+
}));
|
|
1236
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1237
|
+
yield result.current.loadMore();
|
|
1238
|
+
}));
|
|
1239
|
+
expect(result.current.products).toHaveLength(2);
|
|
1240
|
+
expect(result.current.products[0].variants[0].id).toContain("v2");
|
|
1241
|
+
expect(result.current.products[1].variants[0].id).toContain("v3");
|
|
1242
|
+
}));
|
|
1243
|
+
});
|
|
1244
|
+
});
|