@tapcart/mobile-components 0.13.0 → 0.14.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/__tests__/use-infinite-scroll-params.test.d.ts +2 -0
- package/dist/components/hooks/__tests__/use-infinite-scroll-params.test.d.ts.map +1 -0
- package/dist/components/hooks/__tests__/use-infinite-scroll-params.test.js +35 -0
- package/dist/components/hooks/use-block-conditional-rendering.d.ts +2 -0
- package/dist/components/hooks/use-block-conditional-rendering.d.ts.map +1 -1
- package/dist/components/hooks/use-block-conditional-rendering.js +10 -9
- package/dist/components/hooks/use-block-conditional-rendering.test.d.ts +2 -0
- package/dist/components/hooks/use-block-conditional-rendering.test.d.ts.map +1 -0
- package/dist/components/hooks/use-block-conditional-rendering.test.js +112 -0
- package/dist/components/hooks/use-infinite-scroll.d.ts +2 -0
- package/dist/components/hooks/use-infinite-scroll.d.ts.map +1 -1
- package/dist/components/hooks/use-infinite-scroll.js +20 -1
- package/dist/lib/conditional-rendering.util.test.d.ts +2 -0
- package/dist/lib/conditional-rendering.util.test.d.ts.map +1 -0
- package/dist/lib/conditional-rendering.util.test.js +116 -0
- package/dist/styles.css +10 -6
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-infinite-scroll-params.test.d.ts","sourceRoot":"","sources":["../../../../components/hooks/__tests__/use-infinite-scroll-params.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { formatSearchParamsAsNextQueryVariables, stripLayoutOnlyParams, LAYOUT_ONLY_QUERY_PARAMS, } from "../use-infinite-scroll";
|
|
2
|
+
// In API mode the formatted searchParams object is what forks the product cache
|
|
3
|
+
// key: it's hashed into the server Redis key (the by-page route does
|
|
4
|
+
// md5(JSON.stringify(body))) and serialized into the client IDB key. The
|
|
5
|
+
// layout-only `column` param must not survive into that object, or column=2 and
|
|
6
|
+
// column=3 read independent, independently-aged snapshots of the same
|
|
7
|
+
// collection (the Diamond Art Club bug).
|
|
8
|
+
const format = (qs) => stripLayoutOnlyParams(formatSearchParamsAsNextQueryVariables(new URLSearchParams(qs)));
|
|
9
|
+
describe("layout-only param stripping (column cache-key fork fix)", () => {
|
|
10
|
+
it("strips `column` but keeps the data-affecting params", () => {
|
|
11
|
+
const formatted = format("country=US&lang=EN&collectionId=463133769921&collectionHandle=diamond-painting-restocks&column=3");
|
|
12
|
+
expect(formatted).not.toHaveProperty("column");
|
|
13
|
+
expect(formatted).toEqual({
|
|
14
|
+
country: "US",
|
|
15
|
+
lang: "EN",
|
|
16
|
+
collectionId: "463133769921",
|
|
17
|
+
collectionHandle: "diamond-painting-restocks",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
it("yields IDENTICAL cache-key input for column=2 vs column=3", () => {
|
|
21
|
+
const base = "country=US&lang=EN&collectionId=463133769921&collectionHandle=diamond-painting-restocks";
|
|
22
|
+
const col2 = format(`${base}&column=2`);
|
|
23
|
+
const col3 = format(`${base}&column=3`);
|
|
24
|
+
// Same serialization => one shared Redis + IDB entry across layout variants.
|
|
25
|
+
expect(JSON.stringify(col2)).toEqual(JSON.stringify(col3));
|
|
26
|
+
});
|
|
27
|
+
it("never strips real filter/sort params", () => {
|
|
28
|
+
const formatted = format("column=3&sort_by=best-selling&filter.p.tag=in-stock");
|
|
29
|
+
expect(formatted).toEqual({
|
|
30
|
+
sort_by: "best-selling",
|
|
31
|
+
"filter.p.tag": "in-stock",
|
|
32
|
+
});
|
|
33
|
+
expect(LAYOUT_ONLY_QUERY_PARAMS).toContain("column");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -4,6 +4,8 @@ export declare const useBlockConditionalRendering: (_props: {
|
|
|
4
4
|
apiUrl: string;
|
|
5
5
|
deviceVariables: any;
|
|
6
6
|
customerVariables: any;
|
|
7
|
+
sessionVariables?: any;
|
|
8
|
+
storageVariables?: any;
|
|
7
9
|
}, _block: PhoenixBlock, mobileComponentOverrides?: any) => {
|
|
8
10
|
shouldShow: boolean;
|
|
9
11
|
isLoading: any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-block-conditional-rendering.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-block-conditional-rendering.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,YAAY,EAGb,MAAM,kBAAkB,CAAA;AAezB,eAAO,MAAM,4BAA4B,WAC/B;IACN,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,GAAG,CAAA;IACpB,iBAAiB,EAAE,GAAG,CAAA;CACvB,UACO,YAAY,6BACO,GAAG;;;
|
|
1
|
+
{"version":3,"file":"use-block-conditional-rendering.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-block-conditional-rendering.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,YAAY,EAGb,MAAM,kBAAkB,CAAA;AAezB,eAAO,MAAM,4BAA4B,WAC/B;IACN,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,GAAG,CAAA;IACpB,iBAAiB,EAAE,GAAG,CAAA;IACtB,gBAAgB,CAAC,EAAE,GAAG,CAAA;IACtB,gBAAgB,CAAC,EAAE,GAAG,CAAA;CACvB,UACO,YAAY,6BACO,GAAG;;;CAwT/B,CAAA"}
|
|
@@ -16,12 +16,12 @@ import { useCollection as mclUseCollection } from "./use-collection";
|
|
|
16
16
|
import { useSearchParams } from "next/navigation";
|
|
17
17
|
import { gidFromId, countNumberOfTagsInState, getEnvState, shouldShowBlock, evaluateConditions, parseBCP47Locale, } from "../../lib/utils";
|
|
18
18
|
export const useBlockConditionalRendering = (_props, _block, mobileComponentOverrides) => {
|
|
19
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j
|
|
20
|
-
const
|
|
19
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
20
|
+
const _k = ((_a = _block === null || _block === void 0 ? void 0 : _block.visibilityConditions) === null || _a === void 0 ? void 0 : _a.conditions) || {}, { enabled: conditionalsV1IsEnabled = false, exclude: isInverse = false } = _k, restOfProps = __rest(_k, ["enabled", "exclude"]);
|
|
21
21
|
const { _version = 1, conditions, enabled: conditionalsV2IsEnabled = false, } = (_block === null || _block === void 0 ? void 0 : _block.visibilityConditions) || {};
|
|
22
22
|
const isConditionalsEnabled = conditionalsV1IsEnabled || conditionalsV2IsEnabled;
|
|
23
23
|
const blockState = restOfProps;
|
|
24
|
-
const { appId = "", apiUrl = "", deviceVariables, customerVariables } = _props;
|
|
24
|
+
const { appId = "", apiUrl = "", deviceVariables, customerVariables, sessionVariables, storageVariables, } = _props;
|
|
25
25
|
const searchParams = useSearchParams();
|
|
26
26
|
const [deviceLanguage = "en", deviceCountry = "US"] = parseBCP47Locale(deviceVariables === null || deviceVariables === void 0 ? void 0 : deviceVariables.locale);
|
|
27
27
|
const country = (_c = (_b = deviceVariables === null || deviceVariables === void 0 ? void 0 : deviceVariables.countryCode) !== null && _b !== void 0 ? _b : searchParams === null || searchParams === void 0 ? void 0 : searchParams.get("country")) !== null && _c !== void 0 ? _c : deviceCountry;
|
|
@@ -87,9 +87,7 @@ export const useBlockConditionalRendering = (_props, _block, mobileComponentOver
|
|
|
87
87
|
country,
|
|
88
88
|
productMetafieldsQuery,
|
|
89
89
|
]);
|
|
90
|
-
const useProductsSkipArguments = useMemo(() => (mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts)
|
|
91
|
-
? { skip: true }
|
|
92
|
-
: null, [mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts]);
|
|
90
|
+
const useProductsSkipArguments = useMemo(() => ((mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts) ? { skip: true } : null), [mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts]);
|
|
93
91
|
const useProductsInput = useMemo(() => shouldFetchProduct ? useProductsArguments : useProductsSkipArguments, [shouldFetchProduct, useProductsArguments, useProductsSkipArguments]);
|
|
94
92
|
const { products, error: useProductsError, isLoading: isProductsLoading, } = useProducts(useProductsInput);
|
|
95
93
|
const blockCollectionMetafields = useMemo(() => {
|
|
@@ -180,8 +178,11 @@ export const useBlockConditionalRendering = (_props, _block, mobileComponentOver
|
|
|
180
178
|
product,
|
|
181
179
|
collection: specificCollection,
|
|
182
180
|
customer: customerVariables,
|
|
183
|
-
userGroup
|
|
184
|
-
|
|
181
|
+
// userGroup/storage live at the top level of the SDK variables object
|
|
182
|
+
// (variables.session / variables.storage), as siblings of device and
|
|
183
|
+
// customer — not nested under device.
|
|
184
|
+
userGroup: sessionVariables === null || sessionVariables === void 0 ? void 0 : sessionVariables.userGroup,
|
|
185
|
+
storage: storageVariables,
|
|
185
186
|
searchParams,
|
|
186
187
|
});
|
|
187
188
|
const evaluateV1Conditions = () => {
|
|
@@ -216,5 +217,5 @@ export const useBlockConditionalRendering = (_props, _block, mobileComponentOver
|
|
|
216
217
|
console.error("Error evaluating block visibility conditions:", e);
|
|
217
218
|
shouldShow = true; // Fail-safe to show block
|
|
218
219
|
}
|
|
219
|
-
return
|
|
220
|
+
return { shouldShow, isLoading };
|
|
220
221
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-block-conditional-rendering.test.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-block-conditional-rendering.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
// Controlled by tests; the next/navigation mock reads it on each call.
|
|
3
|
+
let mockSearchParams = new URLSearchParams();
|
|
4
|
+
jest.mock("next/navigation", () => ({
|
|
5
|
+
useSearchParams: () => mockSearchParams,
|
|
6
|
+
}));
|
|
7
|
+
// The hook fetches products/collections via SWR-backed hooks; stub them so no
|
|
8
|
+
// network/fetch happens. userGroup/storage/queryParam paths don't need them.
|
|
9
|
+
jest.mock("./use-products", () => ({
|
|
10
|
+
useProducts: () => ({ products: [], error: undefined, isLoading: false }),
|
|
11
|
+
}));
|
|
12
|
+
jest.mock("./use-collection", () => ({
|
|
13
|
+
useCollection: () => ({
|
|
14
|
+
specificCollection: {},
|
|
15
|
+
error: undefined,
|
|
16
|
+
loading: false,
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
import { useBlockConditionalRendering } from "./use-block-conditional-rendering";
|
|
20
|
+
const v2Block = (conditions, enabled = true) => ({
|
|
21
|
+
_id: "block-1",
|
|
22
|
+
visibilityConditions: { _version: 2, enabled, conditions },
|
|
23
|
+
});
|
|
24
|
+
const baseProps = {
|
|
25
|
+
appId: "app-1",
|
|
26
|
+
apiUrl: "https://example.test",
|
|
27
|
+
deviceVariables: { locale: "en-US" },
|
|
28
|
+
customerVariables: {},
|
|
29
|
+
sessionVariables: { userGroup: 1 },
|
|
30
|
+
storageVariables: {},
|
|
31
|
+
};
|
|
32
|
+
const run = (props, block) => renderHook(() => useBlockConditionalRendering(props, block, undefined)).result
|
|
33
|
+
.current;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
mockSearchParams = new URLSearchParams();
|
|
36
|
+
});
|
|
37
|
+
describe("useBlockConditionalRendering - userGroup", () => {
|
|
38
|
+
const block = v2Block({
|
|
39
|
+
logic: "OR",
|
|
40
|
+
conditions: [{ type: "userGroup", operator: "equals", value: "1" }],
|
|
41
|
+
});
|
|
42
|
+
it("shows the block when the top-level session.userGroup matches", () => {
|
|
43
|
+
// Regression guard: userGroup must be read from sessionVariables
|
|
44
|
+
// (variables.session.userGroup), NOT variables.device.session.userGroup.
|
|
45
|
+
const { shouldShow } = run(Object.assign(Object.assign({}, baseProps), { sessionVariables: { userGroup: 1 } }), block);
|
|
46
|
+
expect(shouldShow).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
it("hides the block when the user is in a different group", () => {
|
|
49
|
+
const { shouldShow } = run(Object.assign(Object.assign({}, baseProps), { sessionVariables: { userGroup: 2 } }), block);
|
|
50
|
+
expect(shouldShow).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it("hides the block (fails closed) when no session is present", () => {
|
|
53
|
+
const { shouldShow } = run(Object.assign(Object.assign({}, baseProps), { sessionVariables: undefined }), block);
|
|
54
|
+
expect(shouldShow).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
it("does NOT read userGroup from deviceVariables.session", () => {
|
|
57
|
+
// If the wiring regresses to deviceVariables.session.userGroup, this
|
|
58
|
+
// (group on device, not session) would incorrectly show the block.
|
|
59
|
+
const { shouldShow } = run(Object.assign(Object.assign({}, baseProps), { sessionVariables: undefined, deviceVariables: { locale: "en-US", session: { userGroup: 1 } } }), block);
|
|
60
|
+
expect(shouldShow).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe("useBlockConditionalRendering - queryParam", () => {
|
|
64
|
+
const block = v2Block({
|
|
65
|
+
logic: "OR",
|
|
66
|
+
conditions: [
|
|
67
|
+
{
|
|
68
|
+
type: "queryParam",
|
|
69
|
+
operator: "equals",
|
|
70
|
+
value: { key: "utm", value: "spring" },
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
it("shows when the query param matches", () => {
|
|
75
|
+
mockSearchParams = new URLSearchParams("utm=spring");
|
|
76
|
+
expect(run(baseProps, block).shouldShow).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
it("hides when the query param does not match", () => {
|
|
79
|
+
mockSearchParams = new URLSearchParams("utm=fall");
|
|
80
|
+
expect(run(baseProps, block).shouldShow).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe("useBlockConditionalRendering - storageVariable", () => {
|
|
84
|
+
const block = v2Block({
|
|
85
|
+
logic: "OR",
|
|
86
|
+
conditions: [
|
|
87
|
+
{
|
|
88
|
+
type: "storageVariable",
|
|
89
|
+
operator: "equals",
|
|
90
|
+
value: { key: "promo", value: "active" },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
it("shows when the storage value matches", () => {
|
|
95
|
+
expect(run(Object.assign(Object.assign({}, baseProps), { storageVariables: { promo: "active" } }), block)
|
|
96
|
+
.shouldShow).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
it("hides when the storage value does not match", () => {
|
|
99
|
+
expect(run(Object.assign(Object.assign({}, baseProps), { storageVariables: { promo: "inactive" } }), block)
|
|
100
|
+
.shouldShow).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("useBlockConditionalRendering - disabled", () => {
|
|
104
|
+
it("shows the block when visibility conditions are disabled", () => {
|
|
105
|
+
const block = v2Block({
|
|
106
|
+
logic: "OR",
|
|
107
|
+
conditions: [{ type: "userGroup", operator: "equals", value: "999" }],
|
|
108
|
+
}, false // enabled: false
|
|
109
|
+
);
|
|
110
|
+
expect(run(baseProps, block).shouldShow).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -35,6 +35,8 @@ interface UseInfiniteScrollReturn {
|
|
|
35
35
|
isValidating: boolean;
|
|
36
36
|
}
|
|
37
37
|
export declare const formatSearchParamsAsNextQueryVariables: (searchParams: ReadonlyURLSearchParams) => {};
|
|
38
|
+
export declare const LAYOUT_ONLY_QUERY_PARAMS: string[];
|
|
39
|
+
export declare const stripLayoutOnlyParams: (params: Record<string, unknown>) => Record<string, unknown>;
|
|
38
40
|
declare const constructURL: (apiURL: string) => string;
|
|
39
41
|
declare const useInfiniteScroll: ({ searchClient, initialData, queryVariables: queryVariableProps, direction, productLimit, threshold, interval, customFetcher, customGetKey, shouldSkipFetch: shouldSkipFetchProp, }: UseInfiniteScrollProps) => UseInfiniteScrollReturn;
|
|
40
42
|
export { useInfiniteScroll, constructURL };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-infinite-scroll.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-infinite-scroll.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,uBAAuB,EAAmB,MAAM,iBAAiB,CAAA;AAE1E,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAG5D,UAAU,QAAQ;IAChB,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,GAAG,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAA;CACxC;AAED,UAAU,sBAAsB;IAE9B,YAAY,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAGxC,WAAW,CAAC,EAAE,QAAQ,CAAA;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAGpC,SAAS,CAAC,EAAE,UAAU,GAAG,YAAY,CAAA;IACrC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;IAChD,YAAY,CAAC,EAAE,CACb,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,GAAG,GAAG,IAAI,EAC5B,GAAG,IAAI,EAAE,GAAG,EAAE,KACX,GAAG,CAAA;IACR,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED,UAAU,uBAAuB;IAC/B,IAAI,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAA;IAC5B,KAAK,EAAE,GAAG,CAAA;IACV,oBAAoB,EAAE,OAAO,CAAA;IAC7B,aAAa,EAAE,OAAO,GAAG,SAAS,CAAA;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,OAAO,CAAA;IACtB,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,KAAK,IAAI,CAAA;IAChD,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,oBAAoB,EAAE,GAAG,EAAE,CAAA;IAC3B,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,OAAO,CAAA;CACtB;AAED,eAAO,MAAM,sCAAsC,iBACnC,uBAAuB,OAsBtC,CAAA;AAED,QAAA,MAAM,YAAY,WAAY,MAAM,WAGnC,CAAA;AAED,QAAA,MAAM,iBAAiB,wLAgBpB,sBAAsB,KAAG,
|
|
1
|
+
{"version":3,"file":"use-infinite-scroll.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-infinite-scroll.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,uBAAuB,EAAmB,MAAM,iBAAiB,CAAA;AAE1E,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAG5D,UAAU,QAAQ;IAChB,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,GAAG,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAA;CACxC;AAED,UAAU,sBAAsB;IAE9B,YAAY,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAGxC,WAAW,CAAC,EAAE,QAAQ,CAAA;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAGpC,SAAS,CAAC,EAAE,UAAU,GAAG,YAAY,CAAA;IACrC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;IAChD,YAAY,CAAC,EAAE,CACb,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,GAAG,GAAG,IAAI,EAC5B,GAAG,IAAI,EAAE,GAAG,EAAE,KACX,GAAG,CAAA;IACR,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED,UAAU,uBAAuB;IAC/B,IAAI,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAA;IAC5B,KAAK,EAAE,GAAG,CAAA;IACV,oBAAoB,EAAE,OAAO,CAAA;IAC7B,aAAa,EAAE,OAAO,GAAG,SAAS,CAAA;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,OAAO,CAAA;IACtB,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,KAAK,IAAI,CAAA;IAChD,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,oBAAoB,EAAE,GAAG,EAAE,CAAA;IAC3B,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,OAAO,CAAA;CACtB;AAED,eAAO,MAAM,sCAAsC,iBACnC,uBAAuB,OAsBtC,CAAA;AAWD,eAAO,MAAM,wBAAwB,UAAa,CAAA;AAIlD,eAAO,MAAM,qBAAqB,WACxB,OAAO,MAAM,EAAE,OAAO,CAAC,KAC9B,OAAO,MAAM,EAAE,OAAO,CAMxB,CAAA;AAED,QAAA,MAAM,YAAY,WAAY,MAAM,WAGnC,CAAA;AAED,QAAA,MAAM,iBAAiB,wLAgBpB,sBAAsB,KAAG,uBAuU3B,CAAA;AAED,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,CAAA"}
|
|
@@ -38,6 +38,25 @@ export const formatSearchParamsAsNextQueryVariables = (searchParams) => {
|
|
|
38
38
|
});
|
|
39
39
|
return formattedParams;
|
|
40
40
|
};
|
|
41
|
+
// URL params that only affect how the grid is laid out, never which products a
|
|
42
|
+
// collection/search returns. They MUST NOT enter the product cache key: in API
|
|
43
|
+
// mode this `searchParams` object is hashed into the server Redis key (the
|
|
44
|
+
// by-page route does `md5(JSON.stringify(body))`) and serialized into the
|
|
45
|
+
// client IDB cache key. Leaving `column` in meant column=2 and column=3 forked
|
|
46
|
+
// into two independent cache entries, each holding a snapshot of the same
|
|
47
|
+
// collection taken at a different time — so switching the column view could
|
|
48
|
+
// surface different (and independently-aged) products. Stripping them collapses
|
|
49
|
+
// every layout variant onto one shared cache entry.
|
|
50
|
+
export const LAYOUT_ONLY_QUERY_PARAMS = ["column"];
|
|
51
|
+
// Returns a copy of `params` with the layout-only keys removed (never mutates
|
|
52
|
+
// the input).
|
|
53
|
+
export const stripLayoutOnlyParams = (params) => {
|
|
54
|
+
const result = Object.assign({}, params);
|
|
55
|
+
for (const param of LAYOUT_ONLY_QUERY_PARAMS) {
|
|
56
|
+
delete result[param];
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
};
|
|
41
60
|
const constructURL = (apiURL) => {
|
|
42
61
|
const url = new URL(apiURL);
|
|
43
62
|
return url.toString();
|
|
@@ -90,7 +109,7 @@ customFetcher, customGetKey, shouldSkipFetch: shouldSkipFetchProp, }) => {
|
|
|
90
109
|
const queryVariables = useMemo(() => {
|
|
91
110
|
if (usingSearchClient)
|
|
92
111
|
return null;
|
|
93
|
-
const formattedParams = formatSearchParamsAsNextQueryVariables(searchParams);
|
|
112
|
+
const formattedParams = stripLayoutOnlyParams(formatSearchParamsAsNextQueryVariables(searchParams));
|
|
94
113
|
return Object.assign(Object.assign({}, queryVariableProps), { searchParams: formattedParams });
|
|
95
114
|
}, [queryVariableProps, searchParams, usingSearchClient]);
|
|
96
115
|
const apiGetKey = (pageIndex, previousPageData, queryVariables) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditional-rendering.util.test.d.ts","sourceRoot":"","sources":["../../lib/conditional-rendering.util.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { evaluateConditions, getEnvState } from "./utils";
|
|
2
|
+
// Pure-function coverage for the v2 block visibility-condition logic:
|
|
3
|
+
// getEnvState (env assembly) and evaluateConditions/evaluateSingleCondition
|
|
4
|
+
// (matching + AND/OR/NOT). evaluateSingleCondition is not exported, so it is
|
|
5
|
+
// exercised through single-condition groups.
|
|
6
|
+
const baseLocation = { country: "US", language: "en", deviceId: "device-1" };
|
|
7
|
+
describe("getEnvState", () => {
|
|
8
|
+
it("maps a numeric userGroup to a string-valued tag", () => {
|
|
9
|
+
const env = getEnvState({ location: baseLocation, userGroup: 1 });
|
|
10
|
+
expect(env.userGroup).toEqual([{ value: "1" }]);
|
|
11
|
+
});
|
|
12
|
+
it("maps a string userGroup as-is", () => {
|
|
13
|
+
const env = getEnvState({ location: baseLocation, userGroup: "7" });
|
|
14
|
+
expect(env.userGroup).toEqual([{ value: "7" }]);
|
|
15
|
+
});
|
|
16
|
+
it("returns an empty userGroup tag list when userGroup is missing", () => {
|
|
17
|
+
expect(getEnvState({ location: baseLocation }).userGroup).toEqual([]);
|
|
18
|
+
expect(getEnvState({ location: baseLocation, userGroup: undefined }).userGroup).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
it("exposes storage and searchParams as the underscored context keys", () => {
|
|
21
|
+
const params = new URLSearchParams("utm=spring");
|
|
22
|
+
const env = getEnvState({
|
|
23
|
+
location: baseLocation,
|
|
24
|
+
storage: { promo: "active" },
|
|
25
|
+
searchParams: params,
|
|
26
|
+
});
|
|
27
|
+
expect(env._storage).toEqual({ promo: "active" });
|
|
28
|
+
expect(env._searchParams).toBe(params);
|
|
29
|
+
});
|
|
30
|
+
it("defaults storage/searchParams to empty/null", () => {
|
|
31
|
+
const env = getEnvState({ location: baseLocation });
|
|
32
|
+
expect(env._storage).toEqual({});
|
|
33
|
+
expect(env._searchParams).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("evaluateConditions - userGroup", () => {
|
|
37
|
+
const ctx = { userGroup: [{ value: "1" }] };
|
|
38
|
+
const group = (operator, value) => ({
|
|
39
|
+
logic: "OR",
|
|
40
|
+
conditions: [{ type: "userGroup", operator, value }],
|
|
41
|
+
});
|
|
42
|
+
it("matches the user's group with equals", () => {
|
|
43
|
+
expect(evaluateConditions(group("equals", "1"), ctx)).toBe(true);
|
|
44
|
+
expect(evaluateConditions(group("equals", "2"), ctx)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
it("handles does not equal", () => {
|
|
47
|
+
expect(evaluateConditions(group("does not equal", "2"), ctx)).toBe(true);
|
|
48
|
+
expect(evaluateConditions(group("does not equal", "1"), ctx)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
it("does not match when no group is present (fails closed)", () => {
|
|
51
|
+
// empty userGroup array (e.g. session.userGroup undefined at runtime)
|
|
52
|
+
expect(evaluateConditions(group("equals", "1"), { userGroup: [] })).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe("evaluateConditions - storageVariable", () => {
|
|
56
|
+
const ctx = { _storage: { promo: "active" } };
|
|
57
|
+
const cond = (operator, key, value) => ({
|
|
58
|
+
logic: "OR",
|
|
59
|
+
conditions: [{ type: "storageVariable", operator, value: { key, value } }],
|
|
60
|
+
});
|
|
61
|
+
it("compares the stored value", () => {
|
|
62
|
+
expect(evaluateConditions(cond("equals", "promo", "active"), ctx)).toBe(true);
|
|
63
|
+
expect(evaluateConditions(cond("equals", "promo", "inactive"), ctx)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
it("treats a missing key as not-equal", () => {
|
|
66
|
+
expect(evaluateConditions(cond("equals", "missing", "x"), ctx)).toBe(false);
|
|
67
|
+
expect(evaluateConditions(cond("does not equal", "missing", "x"), ctx)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("evaluateConditions - queryParam", () => {
|
|
71
|
+
const ctx = { _searchParams: new URLSearchParams("utm=spring") };
|
|
72
|
+
const cond = (operator, key, value) => ({
|
|
73
|
+
logic: "OR",
|
|
74
|
+
conditions: [{ type: "queryParam", operator, value: { key, value } }],
|
|
75
|
+
});
|
|
76
|
+
it("compares the query param value", () => {
|
|
77
|
+
expect(evaluateConditions(cond("equals", "utm", "spring"), ctx)).toBe(true);
|
|
78
|
+
expect(evaluateConditions(cond("equals", "utm", "fall"), ctx)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
it("treats a missing param as not-equal", () => {
|
|
81
|
+
expect(evaluateConditions(cond("equals", "missing", "x"), ctx)).toBe(false);
|
|
82
|
+
expect(evaluateConditions(cond("does not equal", "missing", "x"), ctx)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe("evaluateConditions - logic operators", () => {
|
|
86
|
+
const ctx = {
|
|
87
|
+
userGroup: [{ value: "1" }],
|
|
88
|
+
_storage: { promo: "active" },
|
|
89
|
+
};
|
|
90
|
+
const userGroupIs1 = { type: "userGroup", operator: "equals", value: "1" };
|
|
91
|
+
const userGroupIs2 = { type: "userGroup", operator: "equals", value: "2" };
|
|
92
|
+
it("AND requires every condition", () => {
|
|
93
|
+
expect(evaluateConditions({ logic: "AND", conditions: [userGroupIs1, userGroupIs1] }, ctx)).toBe(true);
|
|
94
|
+
expect(evaluateConditions({ logic: "AND", conditions: [userGroupIs1, userGroupIs2] }, ctx)).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
it("OR requires at least one condition", () => {
|
|
97
|
+
expect(evaluateConditions({ logic: "OR", conditions: [userGroupIs2, userGroupIs1] }, ctx)).toBe(true);
|
|
98
|
+
expect(evaluateConditions({ logic: "OR", conditions: [userGroupIs2, userGroupIs2] }, ctx)).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
it("NOT negates an all-true group", () => {
|
|
101
|
+
expect(evaluateConditions({ logic: "NOT", conditions: [userGroupIs1] }, ctx)).toBe(false);
|
|
102
|
+
expect(evaluateConditions({ logic: "NOT", conditions: [userGroupIs2] }, ctx)).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
it("evaluates nested groups", () => {
|
|
105
|
+
expect(evaluateConditions({
|
|
106
|
+
logic: "AND",
|
|
107
|
+
conditions: [
|
|
108
|
+
userGroupIs1,
|
|
109
|
+
{ logic: "OR", conditions: [userGroupIs2, userGroupIs1] },
|
|
110
|
+
],
|
|
111
|
+
}, ctx)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
it("passes when there are no conditions", () => {
|
|
114
|
+
expect(evaluateConditions({ logic: "AND", conditions: [] }, ctx)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
});
|
package/dist/styles.css
CHANGED
|
@@ -1496,9 +1496,6 @@ video {
|
|
|
1496
1496
|
.max-w-\[480px\] {
|
|
1497
1497
|
max-width: 480px;
|
|
1498
1498
|
}
|
|
1499
|
-
.max-w-\[50\%\] {
|
|
1500
|
-
max-width: 50%;
|
|
1501
|
-
}
|
|
1502
1499
|
.max-w-\[5rem\] {
|
|
1503
1500
|
max-width: 5rem;
|
|
1504
1501
|
}
|
|
@@ -1520,6 +1517,9 @@ video {
|
|
|
1520
1517
|
.flex-1 {
|
|
1521
1518
|
flex: 1 1 0%;
|
|
1522
1519
|
}
|
|
1520
|
+
.flex-\[0_0_138px\] {
|
|
1521
|
+
flex: 0 0 138px;
|
|
1522
|
+
}
|
|
1523
1523
|
.flex-\[0_0_226px\] {
|
|
1524
1524
|
flex: 0 0 226px;
|
|
1525
1525
|
}
|
|
@@ -1550,9 +1550,6 @@ video {
|
|
|
1550
1550
|
.basis-\[300px\] {
|
|
1551
1551
|
flex-basis: 300px;
|
|
1552
1552
|
}
|
|
1553
|
-
.basis-\[88\%\] {
|
|
1554
|
-
flex-basis: 88%;
|
|
1555
|
-
}
|
|
1556
1553
|
.basis-full {
|
|
1557
1554
|
flex-basis: 100%;
|
|
1558
1555
|
}
|
|
@@ -1860,6 +1857,10 @@ video {
|
|
|
1860
1857
|
-moz-column-gap: 0.5rem;
|
|
1861
1858
|
column-gap: 0.5rem;
|
|
1862
1859
|
}
|
|
1860
|
+
.gap-x-3 {
|
|
1861
|
+
-moz-column-gap: 0.75rem;
|
|
1862
|
+
column-gap: 0.75rem;
|
|
1863
|
+
}
|
|
1863
1864
|
.gap-x-\[7px\] {
|
|
1864
1865
|
-moz-column-gap: 7px;
|
|
1865
1866
|
column-gap: 7px;
|
|
@@ -1867,6 +1868,9 @@ video {
|
|
|
1867
1868
|
.gap-y-0 {
|
|
1868
1869
|
row-gap: 0px;
|
|
1869
1870
|
}
|
|
1871
|
+
.gap-y-1 {
|
|
1872
|
+
row-gap: 0.25rem;
|
|
1873
|
+
}
|
|
1870
1874
|
.gap-y-2 {
|
|
1871
1875
|
row-gap: 0.5rem;
|
|
1872
1876
|
}
|