@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.
Files changed (52) hide show
  1. package/__mocks__/next-image.tsx +3 -1
  2. package/package.json +3 -3
  3. package/src/api-manager/datasources/d365/d365-product.datasource.ts +1 -1
  4. package/src/api-manager/datasources/d365/utils/product-inventory-utils.ts +1 -3
  5. package/src/api-manager/datasources/e4/cart/e4-cart.remaps.ts +7 -5
  6. package/src/api-manager/datasources/e4/product/e4-product.datasource.ts +1 -4
  7. package/src/api-manager/datasources/e4/product/e4-product.translator.ts +13 -19
  8. package/src/api-manager/datasources/e4/utils/unwrap-e4-variants.ts +10 -13
  9. package/src/auth/auth-options.ts +34 -1
  10. package/src/auth/providers/entra-external-id-provider.ts +50 -0
  11. package/src/auth/providers/index.ts +8 -1
  12. package/src/auth/refresh-token.ts +1 -1
  13. package/src/cms/blocks/interfaces.ts +2 -0
  14. package/src/components-v2/BazaarMenu.stories.tsx +1 -1
  15. package/src/components-v2/BazaarMenu.tsx +4 -3
  16. package/src/components-v2/T/__tests__/T.test.tsx +91 -0
  17. package/src/components-v2/accordion/__tests__/accordion-header.test.tsx +28 -0
  18. package/src/components-v2/carousel/__tests__/carousel.test.tsx +208 -0
  19. package/src/components-v2/carousel-cards/carousel-card-1/__tests__/carousel-card-1.test.tsx +81 -0
  20. package/src/components-v2/categories/__tests__/category-menu.test.tsx +115 -0
  21. package/src/components-v2/categories/category-list/category-list.tsx +5 -1
  22. package/src/components-v2/categories/category-menu.tsx +11 -5
  23. package/src/components-v2/flex-box/__tests__/flex-box.test.tsx +88 -0
  24. package/src/components-v2/header/__tests__/header.test.tsx +122 -0
  25. package/src/components-v2/icons/__tests__/icons.test.tsx +53 -0
  26. package/src/components-v2/mobile-navigation/__tests__/mobile-navigation-bar.test.tsx +94 -0
  27. package/src/components-v2/nav-link/__tests__/nav-link.test.tsx +75 -0
  28. package/src/components-v2/navbar/__tests__/navbar.test.tsx +96 -0
  29. package/src/components-v2/product-cards/__tests__/product-cards.test.tsx +91 -0
  30. package/src/components-v2/product-dialog/__tests__/product-dialog.test.tsx +129 -0
  31. package/src/components-v2/product-dimensions/__tests__/product-dimensions.test.tsx +130 -0
  32. package/src/components-v2/product-dimensions/compound/dimensions-group-list-chip.tsx +36 -37
  33. package/src/components-v2/product-quantity-buttons/__tests__/product-quantity-buttons.test.tsx +77 -0
  34. package/src/components-v2/product-quantity-variants/__tests__/product-quantity-variants.test.tsx +109 -0
  35. package/src/components-v2/products-view/__tests__/products-view.test.tsx +147 -0
  36. package/src/components-v2/progress/__tests__/progress.test.tsx +62 -0
  37. package/src/components-v2/scrollbar/Scrollbar.stories.tsx +2 -2
  38. package/src/components-v2/scrollbar/__tests__/scrollbar.test.tsx +93 -0
  39. package/src/components-v2/search-bar/__tests__/search-bar.test.tsx +142 -0
  40. package/src/components-v2/search-bar/compound/results.tsx +1 -1
  41. package/src/components-v2/section/__tests__/section.test.tsx +100 -0
  42. package/src/components-v2/side-nav/__tests__/side-nav.test.tsx +156 -0
  43. package/src/components-v2/wishlist-dialogs/add-to-wishlist/add-to-wishlist-button.tsx +14 -15
  44. package/src/components-v2/wishlist-dialogs/styles.ts +8 -10
  45. package/src/pages-v2/account/addresses/address-item.tsx +2 -1
  46. package/src/pages-v2/checkout/checkout-alt-form/checkout-form.tsx +6 -1
  47. package/src/pages-v2/checkout/checkout-alt-form/steps/address/new-address-form.tsx +9 -8
  48. package/src/pages-v2/product-details/bopis/find-in-store-modal.tsx +1 -1
  49. package/src/pages-v2/product-details/product-intro/compound/thumbnail-with-skeleton.tsx +1 -0
  50. package/src/pages-v2/product-list/product-list-context.tsx +123 -44
  51. package/src/pages-v2/product-list/range-filter.tsx +36 -25
  52. package/src/pages-v2/product-list/selected-facets.tsx +15 -8
@@ -1,7 +1,9 @@
1
1
  // __mocks__/next/image.js
2
2
  const Image = (props) => {
3
+ const { alt = "", ...rest } = props;
4
+
3
5
  // eslint-disable-next-line @next/next/no-img-element
4
- return <img {...props} />;
6
+ return <img alt={alt} {...rest} />;
5
7
  };
6
8
 
7
9
  export default Image;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evenicanpm/storefront-core",
3
- "version": "1.6.0",
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.6.0"
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": "9b18fe6294cc0e57332ec8703825a609ac7b73b2"
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
- ...(queryResultSettings ?? {}),
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 translated = merge(product, productMap) as DynProduct & {
34
- ProductTypeValue?: number;
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 as ProductSearchResult[];
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.AttributeName ||
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.AttributeValue ||
223
- typeof rawValue.Count !== "number"
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.AttributeValue,
241
- RightValueBoundString: rawValue.AttributeValue,
237
+ LeftValueBoundString: rawValue.value,
238
+ RightValueBoundString: rawValue.value,
242
239
  UnitText: "",
243
240
  RowNumber: 0,
244
- Count: rawValue.Count,
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.AttributeId?.toLowerCase() ?? "",
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
- productData,
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
- v.Attributes?.map((attr: Maybe<Attributes>) => {
34
- const dimension = attr ? merge(attr, dimensionMap) : null;
27
+ const attrs: UnwrappedAttribute[] = variants.flatMap((v) =>
28
+ (v.Attributes ?? [])
29
+ .map((attr) => {
30
+ if (!attr) return null;
35
31
 
36
- if (!dimension) return null;
32
+ const dimension = merge(attr, dimensionMap);
37
33
 
38
34
  return {
39
- ...dimension,
35
+ ...(dimension as object),
40
36
  ProductIds: [v.ProductId],
41
- } as UnwrappedAttribute;
42
- }).filter(Boolean) as UnwrappedAttribute[],
37
+ };
38
+ })
39
+ .filter((item): item is UnwrappedAttribute => item !== null),
43
40
  );
44
41
 
45
42
  const grouped: UnwrappedAttribute[][] = attrs.reduce((acc, item) => {
@@ -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
- _token.accessToken = account.access_token;
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 [authentikProvider, azureProvider, keycloakProvider];
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,
@@ -131,6 +131,8 @@ export interface ImageBlock {
131
131
  id: number;
132
132
  __component: "blocks.image-block";
133
133
  media: MediaData;
134
+ size?: string;
135
+ justify?: string;
134
136
  }
135
137
 
136
138
  export interface RichTextBlock {
@@ -16,7 +16,7 @@ export const Basic: Story = {
16
16
  args: {
17
17
  direction: "left",
18
18
  handler: (onClick) => (
19
- <Button variant="contained" onClick={onClick as any}>
19
+ <Button variant="contained" onClick={onClick}>
20
20
  Open Menu
21
21
  </Button>
22
22
  ),
@@ -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?: (e: (event: React.MouseEvent) => void) => ReactNode;
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 = (event: React.MouseEvent<Element>) =>
29
- setAnchorEl(event.currentTarget as HTMLElement);
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
+ });