@evenicanpm/storefront-core 1.6.0 → 1.8.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/__mocks__/next-image.tsx +3 -1
- package/package.json +3 -3
- package/src/api-manager/datasources/d365/d365-product.datasource.ts +1 -1
- package/src/api-manager/datasources/d365/utils/product-inventory-utils.ts +1 -3
- package/src/api-manager/datasources/e4/cart/e4-cart.remaps.ts +7 -5
- package/src/api-manager/datasources/e4/product/e4-product.datasource.ts +1 -4
- package/src/api-manager/datasources/e4/product/e4-product.translator.ts +13 -19
- package/src/api-manager/datasources/e4/utils/unwrap-e4-variants.ts +10 -13
- package/src/auth/auth-options.ts +34 -1
- package/src/auth/providers/entra-external-id-provider.ts +50 -0
- package/src/auth/providers/index.ts +8 -1
- package/src/auth/refresh-token.ts +1 -1
- package/src/cms/blocks/interfaces.ts +2 -0
- package/src/components-v2/BazaarMenu.stories.tsx +1 -1
- package/src/components-v2/BazaarMenu.tsx +4 -3
- package/src/components-v2/T/__tests__/T.test.tsx +91 -0
- package/src/components-v2/accordion/__tests__/accordion-header.test.tsx +28 -0
- package/src/components-v2/carousel/__tests__/carousel.test.tsx +208 -0
- package/src/components-v2/carousel-cards/carousel-card-1/__tests__/carousel-card-1.test.tsx +81 -0
- package/src/components-v2/categories/__tests__/category-menu.test.tsx +115 -0
- package/src/components-v2/categories/category-list/category-list.tsx +5 -1
- package/src/components-v2/categories/category-menu.tsx +11 -5
- package/src/components-v2/flex-box/__tests__/flex-box.test.tsx +88 -0
- package/src/components-v2/header/__tests__/header.test.tsx +122 -0
- package/src/components-v2/icons/__tests__/icons.test.tsx +53 -0
- package/src/components-v2/mobile-navigation/__tests__/mobile-navigation-bar.test.tsx +94 -0
- package/src/components-v2/nav-link/__tests__/nav-link.test.tsx +75 -0
- package/src/components-v2/navbar/__tests__/navbar.test.tsx +96 -0
- package/src/components-v2/product-cards/__tests__/product-cards.test.tsx +91 -0
- package/src/components-v2/product-dialog/__tests__/product-dialog.test.tsx +129 -0
- package/src/components-v2/product-dimensions/__tests__/product-dimensions.test.tsx +130 -0
- package/src/components-v2/product-dimensions/compound/dimensions-group-list-chip.tsx +36 -37
- package/src/components-v2/product-quantity-buttons/__tests__/product-quantity-buttons.test.tsx +77 -0
- package/src/components-v2/product-quantity-variants/__tests__/product-quantity-variants.test.tsx +109 -0
- package/src/components-v2/products-view/__tests__/products-view.test.tsx +147 -0
- package/src/components-v2/progress/__tests__/progress.test.tsx +62 -0
- package/src/components-v2/scrollbar/Scrollbar.stories.tsx +2 -2
- package/src/components-v2/scrollbar/__tests__/scrollbar.test.tsx +93 -0
- package/src/components-v2/search-bar/__tests__/search-bar.test.tsx +142 -0
- package/src/components-v2/search-bar/compound/results.tsx +1 -1
- package/src/components-v2/section/__tests__/section.test.tsx +100 -0
- package/src/components-v2/side-nav/__tests__/side-nav.test.tsx +156 -0
- package/src/components-v2/wishlist-dialogs/add-to-wishlist/add-to-wishlist-button.tsx +14 -15
- package/src/components-v2/wishlist-dialogs/styles.ts +8 -10
- package/src/pages-v2/account/addresses/address-item.tsx +2 -1
- package/src/pages-v2/checkout/checkout-alt-form/checkout-form.tsx +6 -1
- package/src/pages-v2/checkout/checkout-alt-form/steps/address/new-address-form.tsx +9 -8
- package/src/pages-v2/product-details/bopis/find-in-store-modal.tsx +1 -1
- package/src/pages-v2/product-details/product-intro/compound/thumbnail-with-skeleton.tsx +1 -0
- package/src/pages-v2/product-list/product-list-context.tsx +123 -44
- package/src/pages-v2/product-list/range-filter.tsx +36 -25
- package/src/pages-v2/product-list/selected-facets.tsx +15 -8
package/__mocks__/next-image.tsx
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evenicanpm/storefront-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Core module for D365/e4 Headless Storefront",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"author": "Evenica",
|
|
17
17
|
"license": "ISC",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@evenicanpm/ui": "^1.
|
|
19
|
+
"@evenicanpm/ui": "^1.8.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@storybook/addon-docs": "^9.1.6",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"esbuild": "0.19.11"
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "485f77c5cce7c36849fb9d416d1e503d0f9299a3"
|
|
53
53
|
}
|
|
@@ -150,7 +150,7 @@ class D365ProductDatasource extends D365DatasourceBase implements ProductApi {
|
|
|
150
150
|
|
|
151
151
|
const refinerSearchSettings = { ...searchCriteria };
|
|
152
152
|
const queryResultSettingsWithCount: IQueryResultSettings = {
|
|
153
|
-
...
|
|
153
|
+
...queryResultSettings,
|
|
154
154
|
count: true,
|
|
155
155
|
};
|
|
156
156
|
|
|
@@ -17,9 +17,7 @@ const createInventoryAvailabilitySearchCriteria = (
|
|
|
17
17
|
defaultDeliveryMode: DeliveryMode,
|
|
18
18
|
) => {
|
|
19
19
|
const salesUnitType = 2; // Sales unit type is always 2 for product inventory
|
|
20
|
-
const deliveryMode = defaultDeliveryMode
|
|
21
|
-
? defaultDeliveryMode
|
|
22
|
-
: DeliveryMode.shipping;
|
|
20
|
+
const deliveryMode = defaultDeliveryMode || DeliveryMode.shipping;
|
|
23
21
|
|
|
24
22
|
const inventoryAvailabilitySearchCriteria: InventoryAvailabilitySearchCriteria =
|
|
25
23
|
{
|
|
@@ -30,12 +30,14 @@ export const cartLineMap = {
|
|
|
30
30
|
if (!product) return;
|
|
31
31
|
|
|
32
32
|
// For some reason transform is run multiple times. This prevents merge from running multiple times and overwriting translation.
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const raw = merge(product, productMap) as unknown as DynProduct;
|
|
34
|
+
|
|
35
|
+
if (!raw?.RecordId) return product;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
...raw,
|
|
39
|
+
ProductTypeValue: 4, // default for now. Type = Standalone
|
|
35
40
|
};
|
|
36
|
-
if (!translated.RecordId) return product;
|
|
37
|
-
translated.ProductTypeValue = 4; // default for now. Type = Standalone
|
|
38
|
-
return translated;
|
|
39
41
|
},
|
|
40
42
|
},
|
|
41
43
|
"[].id": "[].LineId",
|
|
@@ -13,9 +13,6 @@ import type {
|
|
|
13
13
|
SimpleProduct,
|
|
14
14
|
} from "@msdyn365-commerce/retail-proxy";
|
|
15
15
|
|
|
16
|
-
// See TODO below
|
|
17
|
-
//import { generateCategoryFilterString } from "./e4.translator";
|
|
18
|
-
|
|
19
16
|
const product: ProductApi = {
|
|
20
17
|
async getProductById(
|
|
21
18
|
input: productSchema.GetProductByIdInput,
|
|
@@ -92,7 +89,7 @@ const product: ProductApi = {
|
|
|
92
89
|
return null;
|
|
93
90
|
})
|
|
94
91
|
.filter(Boolean) as ProductSearchResult[];
|
|
95
|
-
return products
|
|
92
|
+
return products;
|
|
96
93
|
},
|
|
97
94
|
|
|
98
95
|
async getProductsList(
|
|
@@ -207,7 +207,7 @@ export const refinersTransformValidate = (
|
|
|
207
207
|
if (
|
|
208
208
|
!rawRefiner ||
|
|
209
209
|
typeof rawRefiner !== "object" ||
|
|
210
|
-
!rawRefiner.
|
|
210
|
+
!rawRefiner.name ||
|
|
211
211
|
!Array.isArray(rawRefiner.value)
|
|
212
212
|
) {
|
|
213
213
|
console.warn("Skipping invalid raw refiner object:", rawRefiner);
|
|
@@ -219,8 +219,8 @@ export const refinersTransformValidate = (
|
|
|
219
219
|
if (
|
|
220
220
|
!rawValue ||
|
|
221
221
|
typeof rawValue !== "object" ||
|
|
222
|
-
!rawValue.
|
|
223
|
-
typeof rawValue.
|
|
222
|
+
!rawValue.value ||
|
|
223
|
+
typeof rawValue.count !== "number"
|
|
224
224
|
) {
|
|
225
225
|
console.warn(
|
|
226
226
|
"Skipping invalid raw refiner value object:",
|
|
@@ -229,33 +229,29 @@ export const refinersTransformValidate = (
|
|
|
229
229
|
return null;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
RECORD_ID_MAPPER.add(
|
|
233
|
-
Number(rawValue?.id),
|
|
234
|
-
rawValue?.AttributeId ?? "",
|
|
235
|
-
);
|
|
232
|
+
RECORD_ID_MAPPER.add(Number(rawValue?.id), rawValue?.id ?? "");
|
|
236
233
|
|
|
237
234
|
return {
|
|
238
235
|
RefinerRecordId: Number(rawValue?.id),
|
|
239
236
|
DataTypeValue: DATA_TYPE_VALUE_STRING,
|
|
240
|
-
LeftValueBoundString: rawValue.
|
|
241
|
-
RightValueBoundString: rawValue.
|
|
237
|
+
LeftValueBoundString: rawValue.value,
|
|
238
|
+
RightValueBoundString: rawValue.value,
|
|
242
239
|
UnitText: "",
|
|
243
240
|
RowNumber: 0,
|
|
244
|
-
Count: rawValue.
|
|
241
|
+
Count: rawValue.count,
|
|
245
242
|
LeftValueBoundLocalizedString: "",
|
|
246
243
|
RightValueBoundLocalizedString: "",
|
|
247
244
|
ExtensionProperties: [],
|
|
248
245
|
RefinerSourceValue: SOURCE_VALUE_ATTRIBUTE,
|
|
249
246
|
SwatchImageUrl: "",
|
|
250
|
-
SwatchColorHexCode:
|
|
251
|
-
rawValue?.AttributeId === "color" ? rawValue.AttributeValue : "",
|
|
247
|
+
SwatchColorHexCode: rawValue?.id === "color" ? rawValue.value : "",
|
|
252
248
|
"@odata.type": ODATA_TYPE_REFINER_VALUE,
|
|
253
249
|
};
|
|
254
250
|
})
|
|
255
251
|
.filter((value): value is TransformedRefinerValue => value !== null);
|
|
256
252
|
|
|
257
253
|
const isDimension = DIMENSION_ATTRIBUTE_IDS.has(
|
|
258
|
-
rawRefiner.
|
|
254
|
+
rawRefiner.id?.toLowerCase() ?? "",
|
|
259
255
|
);
|
|
260
256
|
|
|
261
257
|
return {
|
|
@@ -292,16 +288,14 @@ export const refinersTransformValidate = (
|
|
|
292
288
|
export const productTransformValidate = (
|
|
293
289
|
productData: Product,
|
|
294
290
|
): ProductDetails => {
|
|
295
|
-
const translatedData = merge(
|
|
296
|
-
|
|
297
|
-
productDetailsMap,
|
|
298
|
-
) as ProductDetails;
|
|
291
|
+
const translatedData = merge(productData, productDetailsMap);
|
|
292
|
+
|
|
299
293
|
try {
|
|
300
|
-
productSchema.parse(translatedData);
|
|
294
|
+
return productSchema.parse(translatedData) as ProductDetails;
|
|
301
295
|
} catch (e) {
|
|
302
296
|
console.warn("Product data does not match retail-proxy schema", e);
|
|
297
|
+
return translatedData as ProductDetails; // fallback only if needed
|
|
303
298
|
}
|
|
304
|
-
return translatedData;
|
|
305
299
|
};
|
|
306
300
|
|
|
307
301
|
export const productPricesTransformValidate = (
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Attributes,
|
|
3
|
-
Maybe,
|
|
4
|
-
Variants,
|
|
5
|
-
} from "@evenicanpm/storefront-core/src/api-manager/datasources/e4/graphqlRequestSdk";
|
|
1
|
+
import type { Variants } from "@evenicanpm/storefront-core/src/api-manager/datasources/e4/graphqlRequestSdk";
|
|
6
2
|
import { dimensionMap } from "@evenicanpm/storefront-core/src/api-manager/datasources/e4/product/e4-product.remaps";
|
|
7
3
|
import { merge } from "object-mapper";
|
|
8
4
|
|
|
@@ -28,18 +24,19 @@ export default function unwrapE4Variants(
|
|
|
28
24
|
variants: Variants[],
|
|
29
25
|
): UnwrappedAttribute[][] {
|
|
30
26
|
if (!variants?.length) return [];
|
|
31
|
-
const attrs: UnwrappedAttribute[] = variants.flatMap(
|
|
32
|
-
(v)
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
const attrs: UnwrappedAttribute[] = variants.flatMap((v) =>
|
|
28
|
+
(v.Attributes ?? [])
|
|
29
|
+
.map((attr) => {
|
|
30
|
+
if (!attr) return null;
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
const dimension = merge(attr, dimensionMap);
|
|
37
33
|
|
|
38
34
|
return {
|
|
39
|
-
...dimension,
|
|
35
|
+
...(dimension as object),
|
|
40
36
|
ProductIds: [v.ProductId],
|
|
41
|
-
}
|
|
42
|
-
})
|
|
37
|
+
};
|
|
38
|
+
})
|
|
39
|
+
.filter((item): item is UnwrappedAttribute => item !== null),
|
|
43
40
|
);
|
|
44
41
|
|
|
45
42
|
const grouped: UnwrappedAttribute[][] = attrs.reduce((acc, item) => {
|
package/src/auth/auth-options.ts
CHANGED
|
@@ -23,6 +23,35 @@ const createAuthOptions = (): NextAuthOptions => {
|
|
|
23
23
|
return {
|
|
24
24
|
providers,
|
|
25
25
|
secret,
|
|
26
|
+
cookies: {
|
|
27
|
+
csrfToken: {
|
|
28
|
+
name: "next-auth.csrf-token",
|
|
29
|
+
options: {
|
|
30
|
+
httpOnly: true,
|
|
31
|
+
sameSite: "none",
|
|
32
|
+
path: "/",
|
|
33
|
+
secure: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
pkceCodeVerifier: {
|
|
37
|
+
name: "next-auth.pkce.code_verifier",
|
|
38
|
+
options: {
|
|
39
|
+
httpOnly: true,
|
|
40
|
+
sameSite: "none",
|
|
41
|
+
path: "/",
|
|
42
|
+
secure: true,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
nonce: {
|
|
46
|
+
name: "next-auth.nonce",
|
|
47
|
+
options: {
|
|
48
|
+
httpOnly: true,
|
|
49
|
+
sameSite: "none",
|
|
50
|
+
path: "/",
|
|
51
|
+
secure: true,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
26
55
|
// Order in which callbacks are called = signin > redirect > jwt > session. Thats why session callback has token info.
|
|
27
56
|
callbacks: {
|
|
28
57
|
// By default, NextAuth uses "session" with strategy: "jwt"
|
|
@@ -31,7 +60,11 @@ const createAuthOptions = (): NextAuthOptions => {
|
|
|
31
60
|
// account and profile is available only in first callback after signin
|
|
32
61
|
if (account && profile) {
|
|
33
62
|
_token.provider = account.provider;
|
|
34
|
-
|
|
63
|
+
// use id_token for entra external id and access_token for aadb2c
|
|
64
|
+
_token.accessToken =
|
|
65
|
+
_token.provider === "entra-external-id"
|
|
66
|
+
? account.id_token
|
|
67
|
+
: account.access_token;
|
|
35
68
|
_token.refreshToken = account.refresh_token;
|
|
36
69
|
}
|
|
37
70
|
return _token;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Provider } from "next-auth/providers";
|
|
2
|
+
|
|
3
|
+
const createEntraExternalIdProvider = (): Provider => {
|
|
4
|
+
const clientId = process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CLIENT_ID || "";
|
|
5
|
+
const customScopes =
|
|
6
|
+
process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CUSTOM_SCOPES || "";
|
|
7
|
+
const tenantName =
|
|
8
|
+
process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_TENANT_NAME || "";
|
|
9
|
+
const clientSecret =
|
|
10
|
+
process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CLIENT_SECRET || "";
|
|
11
|
+
|
|
12
|
+
// Build auth url and wellKnown from the loginAuthority
|
|
13
|
+
const loginAuthority = `https://${tenantName}.ciamlogin.com/${tenantName}.onmicrosoft.com`;
|
|
14
|
+
const authUrl = `${loginAuthority}/oauth2/v2.0/authorize`;
|
|
15
|
+
const wellKnownUrl = `${loginAuthority}/v2.0/.well-known/openid-configuration`;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
id: "entra-external-id",
|
|
19
|
+
clientId: clientId,
|
|
20
|
+
client: {
|
|
21
|
+
client_id: clientId,
|
|
22
|
+
response_types: ["code id_token"],
|
|
23
|
+
client_secret: clientSecret,
|
|
24
|
+
// token_endpoint_auth_method: 'none',
|
|
25
|
+
},
|
|
26
|
+
type: "oauth",
|
|
27
|
+
idToken: true,
|
|
28
|
+
name: "Microsoft Entra External ID",
|
|
29
|
+
authorization: {
|
|
30
|
+
url: authUrl,
|
|
31
|
+
params: {
|
|
32
|
+
scope: customScopes,
|
|
33
|
+
prompt: "login",
|
|
34
|
+
response_type: "code id_token",
|
|
35
|
+
response_mode: "form_post",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
wellKnown: wellKnownUrl,
|
|
39
|
+
profile(profile) {
|
|
40
|
+
return {
|
|
41
|
+
id: profile.oid,
|
|
42
|
+
name: profile.name,
|
|
43
|
+
email: profile.email,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
checks: ["pkce", "nonce"],
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default createEntraExternalIdProvider;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import azureADB2CProvider from "@/auth/providers/aadb2c-provider";
|
|
2
2
|
import authentikProvider from "@/auth/providers/authentik-provider";
|
|
3
3
|
import keycloakProvider from "@/auth/providers/keycloak-provider";
|
|
4
|
+
import createEntraExternalIdProvider from "@/auth/providers/entra-external-id-provider";
|
|
4
5
|
|
|
5
6
|
export const getProviders = () => {
|
|
6
7
|
const azureProvider = azureADB2CProvider();
|
|
8
|
+
const entraExternalIdProvider = createEntraExternalIdProvider();
|
|
7
9
|
// once we stop hard coding others use loader function here
|
|
8
|
-
return [
|
|
10
|
+
return [
|
|
11
|
+
authentikProvider,
|
|
12
|
+
azureProvider,
|
|
13
|
+
keycloakProvider,
|
|
14
|
+
entraExternalIdProvider,
|
|
15
|
+
];
|
|
9
16
|
};
|
|
@@ -53,7 +53,7 @@ const checkAccessTokenExpiry = async (
|
|
|
53
53
|
const matchedProvider = providers.find(
|
|
54
54
|
(p) => p.id.toLowerCase() === token.provider.toLowerCase(),
|
|
55
55
|
);
|
|
56
|
-
if (!matchedProvider) {
|
|
56
|
+
if (!matchedProvider || !("wellKnown" in matchedProvider)) {
|
|
57
57
|
token.error = "NoProviderMatch";
|
|
58
58
|
return {
|
|
59
59
|
token,
|
|
@@ -8,7 +8,7 @@ import { Fragment, type ReactNode, useState } from "react";
|
|
|
8
8
|
interface Props {
|
|
9
9
|
open?: boolean;
|
|
10
10
|
sx?: SxProps<Theme>;
|
|
11
|
-
handler?: (
|
|
11
|
+
handler?: (onClick: React.MouseEventHandler<HTMLElement>) => ReactNode;
|
|
12
12
|
options: (e: () => void) => ReactNode;
|
|
13
13
|
direction?: "left" | "right" | "center";
|
|
14
14
|
}
|
|
@@ -25,8 +25,9 @@ export default function BazaarMenu({
|
|
|
25
25
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
|
26
26
|
|
|
27
27
|
const handleClose = () => setAnchorEl(null);
|
|
28
|
-
const handleClick
|
|
29
|
-
setAnchorEl(event.currentTarget
|
|
28
|
+
const handleClick: React.MouseEventHandler<HTMLElement> = (event) => {
|
|
29
|
+
setAnchorEl(event.currentTarget);
|
|
30
|
+
};
|
|
30
31
|
|
|
31
32
|
return (
|
|
32
33
|
<Fragment>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { vi } from "vitest";
|
|
3
|
+
import T from "../index";
|
|
4
|
+
|
|
5
|
+
vi.mock("next-intl", () => ({
|
|
6
|
+
useTranslations: vi.fn(() => (key: string) => {
|
|
7
|
+
const translations: Record<string, string> = {
|
|
8
|
+
"HomePage.welcome": "Welcome to our store",
|
|
9
|
+
"HomePage.products": "Products",
|
|
10
|
+
"Navigation.home": "Home",
|
|
11
|
+
"Navigation.about": "About Us",
|
|
12
|
+
"Common.loading": "Loading...",
|
|
13
|
+
"Common.error": "An error occurred",
|
|
14
|
+
};
|
|
15
|
+
return translations[key] || key;
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe("T (Translation Component)", () => {
|
|
20
|
+
it("renders translation for given path", () => {
|
|
21
|
+
render(<T path="HomePage.welcome" />);
|
|
22
|
+
expect(screen.getByText("Welcome to our store")).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("renders multiple translations", () => {
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<>
|
|
28
|
+
<T path="HomePage.welcome" />
|
|
29
|
+
<T path="Navigation.home" />
|
|
30
|
+
</>,
|
|
31
|
+
);
|
|
32
|
+
const text = container.textContent || "";
|
|
33
|
+
expect(text).toContain("Welcome to our store");
|
|
34
|
+
expect(text).toContain("Home");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders fallback path when translation not found", () => {
|
|
38
|
+
render(<T path="Unknown.path" />);
|
|
39
|
+
expect(screen.getByText("Unknown.path")).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("handles nested translation paths", () => {
|
|
43
|
+
render(<T path="Navigation.home" />);
|
|
44
|
+
expect(screen.getByText("Home")).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders common translations", () => {
|
|
48
|
+
render(<T path="Common.loading" />);
|
|
49
|
+
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("renders multiple different paths together", () => {
|
|
53
|
+
const { container } = render(
|
|
54
|
+
<>
|
|
55
|
+
<T path="HomePage.welcome" />
|
|
56
|
+
<T path="Navigation.home" />
|
|
57
|
+
</>,
|
|
58
|
+
);
|
|
59
|
+
const text = container.textContent || "";
|
|
60
|
+
expect(text).toContain("Welcome to our store");
|
|
61
|
+
expect(text).toContain("Home");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("renders translation with special characters", () => {
|
|
65
|
+
render(<T path="Common.error" />);
|
|
66
|
+
expect(screen.getByText("An error occurred")).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("accepts dynamic paths", () => {
|
|
70
|
+
const paths = ["HomePage.welcome", "Navigation.home", "Common.loading"];
|
|
71
|
+
const { container } = render(
|
|
72
|
+
<>
|
|
73
|
+
{paths.map((path) => (
|
|
74
|
+
<T key={path} path={path} />
|
|
75
|
+
))}
|
|
76
|
+
</>,
|
|
77
|
+
);
|
|
78
|
+
const text = container.textContent || "";
|
|
79
|
+
expect(text).toContain("Welcome to our store");
|
|
80
|
+
expect(text).toContain("Home");
|
|
81
|
+
expect(text).toContain("Loading...");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("re-renders when path changes", () => {
|
|
85
|
+
const { rerender } = render(<T path="HomePage.welcome" />);
|
|
86
|
+
expect(screen.getByText("Welcome to our store")).toBeInTheDocument();
|
|
87
|
+
|
|
88
|
+
rerender(<T path="Navigation.home" />);
|
|
89
|
+
expect(screen.getByText("Home")).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import AccordionHeader from "../accordion-header";
|
|
3
|
+
|
|
4
|
+
describe("accordion-header", () => {
|
|
5
|
+
it("renders the provided header text", () => {
|
|
6
|
+
render(<AccordionHeader>Accordion Title</AccordionHeader>);
|
|
7
|
+
|
|
8
|
+
expect(screen.getByText("Accordion Title")).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("shows the caret icon by default", () => {
|
|
12
|
+
const { container } = render(
|
|
13
|
+
<AccordionHeader>Accordion With Icon</AccordionHeader>,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
expect(container.querySelector(".caret")).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("hides the caret icon when showIcon is false", () => {
|
|
20
|
+
const { container } = render(
|
|
21
|
+
<AccordionHeader showIcon={false}>
|
|
22
|
+
Accordion Without Icon
|
|
23
|
+
</AccordionHeader>,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(container.querySelector(".caret")).not.toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
});
|