@tapcart/mobile-components 0.17.2 → 0.18.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/dist/components/hooks/use-infinite-scroll-sort-key.test.d.ts +2 -0
- package/dist/components/hooks/use-infinite-scroll-sort-key.test.d.ts.map +1 -0
- package/dist/components/hooks/use-infinite-scroll-sort-key.test.js +68 -0
- package/dist/components/hooks/use-recommendations.d.ts +8 -1
- package/dist/components/hooks/use-recommendations.d.ts.map +1 -1
- package/dist/components/hooks/use-recommendations.js +5 -4
- package/dist/components/hooks/use-recommendations.test.js +21 -0
- package/dist/components/hooks/use-sort-filter.js +1 -1
- package/dist/styles.css +12 -0
- package/package.json +3 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-infinite-scroll-sort-key.test.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-infinite-scroll-sort-key.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,68 @@
|
|
|
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 { createElement } from "react";
|
|
11
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
12
|
+
import { SWRConfig } from "swr";
|
|
13
|
+
import useSWRInfinite from "swr/infinite";
|
|
14
|
+
// Mirrors the client contract: getKey exposes only the shopper-chosen sort,
|
|
15
|
+
// and the provider default arrives while the page-1 response is handled.
|
|
16
|
+
const makeClient = (hydrateInto) => ({
|
|
17
|
+
userSort: "",
|
|
18
|
+
displaySort: "",
|
|
19
|
+
getKey(pageIndex, previousPageData) {
|
|
20
|
+
if (previousPageData && !previousPageData.hasMore)
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
page: pageIndex + 1,
|
|
24
|
+
sort: this.userSort,
|
|
25
|
+
collectionId: "12345",
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
onResponse() {
|
|
29
|
+
if (!this.userSort)
|
|
30
|
+
this[hydrateInto] = "creation_date";
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
// SWR's cache is global, so each test needs its own provider or the second
|
|
34
|
+
// render would read the first one's page-1 entry and never fetch.
|
|
35
|
+
const wrapper = ({ children }) => createElement(SWRConfig, { value: { provider: () => new Map() } }, children);
|
|
36
|
+
const renderPages = (client, fetcher) => renderHook(() => useSWRInfinite((i, prev) => client.getKey(i, prev), fetcher, {
|
|
37
|
+
revalidateFirstPage: false,
|
|
38
|
+
revalidateOnFocus: false,
|
|
39
|
+
revalidateOnMount: true,
|
|
40
|
+
dedupingInterval: 0,
|
|
41
|
+
}), { wrapper });
|
|
42
|
+
const makeFetcher = (client) => jest.fn((key) => __awaiter(void 0, void 0, void 0, function* () {
|
|
43
|
+
client.onResponse();
|
|
44
|
+
return { products: [{ id: `p${key.page}` }], hasMore: false };
|
|
45
|
+
}));
|
|
46
|
+
describe("useSWRInfinite key stability across page-1 sort hydration", () => {
|
|
47
|
+
it("refetches page 1 when the response hydrates a key-visible field", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
48
|
+
const client = makeClient("userSort");
|
|
49
|
+
const fetcher = makeFetcher(client);
|
|
50
|
+
const { result } = renderPages(client, fetcher);
|
|
51
|
+
yield waitFor(() => expect(result.current.data).toBeDefined());
|
|
52
|
+
yield waitFor(() => expect(fetcher).toHaveBeenCalledTimes(2));
|
|
53
|
+
// Proves the harness detects the regression: the key moved, so SWR issued
|
|
54
|
+
// a second page-1 request, this time carrying the hydrated sort.
|
|
55
|
+
expect(fetcher.mock.calls[0][0].sort).toBe("");
|
|
56
|
+
expect(fetcher.mock.calls[1][0].sort).toBe("creation_date");
|
|
57
|
+
}));
|
|
58
|
+
it("fetches page 1 once when the response hydrates a display-only field", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
59
|
+
const client = makeClient("displaySort");
|
|
60
|
+
const fetcher = makeFetcher(client);
|
|
61
|
+
const { result } = renderPages(client, fetcher);
|
|
62
|
+
yield waitFor(() => expect(result.current.data).toBeDefined());
|
|
63
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
64
|
+
expect(fetcher.mock.calls[0][0].sort).toBe("");
|
|
65
|
+
// The default is still available to label the sort drawer.
|
|
66
|
+
expect(client.displaySort).toBe("creation_date");
|
|
67
|
+
}));
|
|
68
|
+
});
|
|
@@ -5,6 +5,13 @@ interface UseRecommendationProps {
|
|
|
5
5
|
customSearchConfig?: Record<string, any>;
|
|
6
6
|
queryVariables: Record<string, any>;
|
|
7
7
|
apiURL: string;
|
|
8
|
+
/**
|
|
9
|
+
* Opt out of fetching entirely. Rules of Hooks force callers to invoke this
|
|
10
|
+
* hook unconditionally, and an empty query otherwise counts as "fetch
|
|
11
|
+
* trending", so a caller that only sometimes wants recommendations has no
|
|
12
|
+
* other way to stay quiet. Defaults to true to preserve existing behavior.
|
|
13
|
+
*/
|
|
14
|
+
enabled?: boolean;
|
|
8
15
|
}
|
|
9
16
|
interface UseRecommendationReturn {
|
|
10
17
|
products: Product[];
|
|
@@ -14,6 +21,6 @@ interface UseRecommendationReturn {
|
|
|
14
21
|
isLoading: boolean;
|
|
15
22
|
error: any;
|
|
16
23
|
}
|
|
17
|
-
declare const useRecommendations: ({ searchClient, query, customSearchConfig, queryVariables, apiURL, }: UseRecommendationProps) => UseRecommendationReturn;
|
|
24
|
+
declare const useRecommendations: ({ searchClient, query, customSearchConfig, queryVariables, apiURL, enabled, }: UseRecommendationProps) => UseRecommendationReturn;
|
|
18
25
|
export { useRecommendations };
|
|
19
26
|
//# sourceMappingURL=use-recommendations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-recommendations.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-recommendations.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAOxE,UAAU,sBAAsB;IAE9B,YAAY,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAGxC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,MAAM,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"use-recommendations.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-recommendations.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAOxE,UAAU,sBAAsB;IAE9B,YAAY,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAGxC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,MAAM,EAAE,MAAM,CAAA;IAEd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAUD,UAAU,uBAAuB;IAC/B,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,MAAM,EAAE,GAAG,EAAE,CAAA;IACb,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,GAAG,CAAA;CACX;AAmBD,QAAA,MAAM,kBAAkB,kFAWrB,sBAAsB,KAAG,uBA8H3B,CAAA;AAED,OAAO,EAAE,kBAAkB,EAAE,CAAA"}
|
|
@@ -27,7 +27,7 @@ const useRecommendations = ({
|
|
|
27
27
|
// Search client props
|
|
28
28
|
searchClient, query = "", customSearchConfig,
|
|
29
29
|
// API props (backwards compatibility)
|
|
30
|
-
queryVariables, apiURL, }) => {
|
|
30
|
+
queryVariables, apiURL, enabled = true, }) => {
|
|
31
31
|
// When searchClient is provided, use search client approach
|
|
32
32
|
const usingSearchClient = Boolean(searchClient);
|
|
33
33
|
const searchParams = useSearchParams();
|
|
@@ -39,9 +39,10 @@ queryVariables, apiURL, }) => {
|
|
|
39
39
|
// - query is empty → show trending searches
|
|
40
40
|
// - query meets minimum length (≥3 chars)
|
|
41
41
|
// - searchClient has a customSearchConfig (e.g., filter-only queries on PDP)
|
|
42
|
-
const shouldFetch =
|
|
43
|
-
recommendation.length
|
|
44
|
-
|
|
42
|
+
const shouldFetch = enabled &&
|
|
43
|
+
(!recommendation.trim().length ||
|
|
44
|
+
recommendation.length >= MIN_QUERY_LENGTH ||
|
|
45
|
+
(usingSearchClient && Boolean(customSearchConfig)));
|
|
45
46
|
const [cachedRecommendation, setCachedRecommendations] = React.useState(recommendationsLocalStorage.getCacheItem({
|
|
46
47
|
id: `${recommendation}-${queryVariables === null || queryVariables === void 0 ? void 0 : queryVariables.language}`,
|
|
47
48
|
}));
|
|
@@ -82,4 +82,25 @@ describe("useRecommendations", () => {
|
|
|
82
82
|
// Empty query now triggers a trending fetch regardless of searchClient
|
|
83
83
|
expect(result.current.isLoading).toBe(true);
|
|
84
84
|
});
|
|
85
|
+
describe("enabled", () => {
|
|
86
|
+
it("should not fetch when enabled is false", () => {
|
|
87
|
+
const { result } = renderHook(() => useRecommendations(Object.assign({ searchClient: mockSearchClient, query: "", enabled: false }, defaultProps)));
|
|
88
|
+
expect(result.current.products).toEqual([]);
|
|
89
|
+
expect(result.current.isLoading).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
it("should not fetch the legacy API path when enabled is false", () => {
|
|
92
|
+
// Collection-mode carousels land here: no searchClient and an empty
|
|
93
|
+
// query, which would otherwise POST to /{appId}/api/v1/recommendations.
|
|
94
|
+
const { result } = renderHook(() => useRecommendations(Object.assign({ query: "", enabled: false }, defaultProps)));
|
|
95
|
+
expect(result.current.isLoading).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
it("should fetch when enabled is omitted, preserving existing behavior", () => {
|
|
98
|
+
const { result } = renderHook(() => useRecommendations(Object.assign({ searchClient: mockSearchClient, query: "" }, defaultProps)));
|
|
99
|
+
expect(result.current.isLoading).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
it("should fetch when enabled is explicitly true", () => {
|
|
102
|
+
const { result } = renderHook(() => useRecommendations(Object.assign({ searchClient: mockSearchClient, query: "abc", enabled: true }, defaultProps)));
|
|
103
|
+
expect(result.current.isLoading).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
85
106
|
});
|
|
@@ -88,7 +88,7 @@ const useSortFilter = ({ initialData, queryVariables, dynamicKey, searchClient,
|
|
|
88
88
|
relatedCategories: [],
|
|
89
89
|
},
|
|
90
90
|
sortOptions: [],
|
|
91
|
-
dynamicFiltersEnabled:
|
|
91
|
+
dynamicFiltersEnabled: searchClient.isDynamicFiltersEnabled(),
|
|
92
92
|
integrations: [],
|
|
93
93
|
},
|
|
94
94
|
data: [],
|
package/dist/styles.css
CHANGED
|
@@ -1124,6 +1124,9 @@ video {
|
|
|
1124
1124
|
.hidden {
|
|
1125
1125
|
display: none;
|
|
1126
1126
|
}
|
|
1127
|
+
.aspect-\[2\/3\] {
|
|
1128
|
+
aspect-ratio: 2/3;
|
|
1129
|
+
}
|
|
1127
1130
|
.aspect-\[327\/280\] {
|
|
1128
1131
|
aspect-ratio: 327/280;
|
|
1129
1132
|
}
|
|
@@ -1845,6 +1848,9 @@ video {
|
|
|
1845
1848
|
.items-baseline {
|
|
1846
1849
|
align-items: baseline;
|
|
1847
1850
|
}
|
|
1851
|
+
.items-stretch {
|
|
1852
|
+
align-items: stretch;
|
|
1853
|
+
}
|
|
1848
1854
|
.justify-start {
|
|
1849
1855
|
justify-content: flex-start;
|
|
1850
1856
|
}
|
|
@@ -2238,6 +2244,9 @@ video {
|
|
|
2238
2244
|
--tw-bg-opacity: 1;
|
|
2239
2245
|
background-color: rgb(253 255 218 / var(--tw-bg-opacity, 1));
|
|
2240
2246
|
}
|
|
2247
|
+
.bg-\[var\(--element-divider-color\2c var\(--coreColors-dividingLines\)\)\] {
|
|
2248
|
+
background-color: var(--element-divider-color,var(--coreColors-dividingLines));
|
|
2249
|
+
}
|
|
2241
2250
|
.bg-\[var\(--stateColors-skeleton\)\] {
|
|
2242
2251
|
background-color: var(--stateColors-skeleton);
|
|
2243
2252
|
}
|
|
@@ -3107,6 +3116,9 @@ video {
|
|
|
3107
3116
|
--tw-grayscale: grayscale(100%);
|
|
3108
3117
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
|
3109
3118
|
}
|
|
3119
|
+
.\!filter {
|
|
3120
|
+
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow) !important;
|
|
3121
|
+
}
|
|
3110
3122
|
.filter {
|
|
3111
3123
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
|
3112
3124
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tapcart/mobile-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"style": "dist/styles.css",
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
"clsx": "^1.2.1",
|
|
81
81
|
"dayjs": "^1.11.13",
|
|
82
82
|
"dompurify": "^3.2.2",
|
|
83
|
-
"embla-carousel-autoplay": "8.
|
|
84
|
-
"embla-carousel-fade": "8.
|
|
83
|
+
"embla-carousel-autoplay": "8.6.0",
|
|
84
|
+
"embla-carousel-fade": "8.6.0",
|
|
85
85
|
"embla-carousel-react": "^8.3.0",
|
|
86
86
|
"input-otp": "^1.4.2",
|
|
87
87
|
"lucide-react": "^0.488.0",
|