@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
@@ -0,0 +1,142 @@
1
+ import { render } from "@testing-library/react";
2
+ import type { ReactNode } from "react";
3
+ import { vi } from "vitest";
4
+ import { SearchBarViewLg, SearchBarViewSm } from "../index";
5
+
6
+ vi.mock("next-intl", () => ({
7
+ useTranslations: () => (key: string) => key,
8
+ }));
9
+
10
+ vi.mock(
11
+ "@evenicanpm/storefront-core/src/components-v2/search-bar/compound/search-bar-root",
12
+ () => ({
13
+ default: Object.assign(
14
+ ({
15
+ children,
16
+ handleCloseDrawer,
17
+ }: {
18
+ children: ReactNode;
19
+ handleCloseDrawer?: () => void;
20
+ }) => (
21
+ <button
22
+ type="button"
23
+ data-testid="search-bar"
24
+ onClick={handleCloseDrawer}
25
+ >
26
+ {children}
27
+ </button>
28
+ ),
29
+ {
30
+ TextField: () => <input data-testid="search-input" />,
31
+ Results: Object.assign(
32
+ ({ children }: { children: ReactNode }) => (
33
+ <div data-testid="results">{children}</div>
34
+ ),
35
+ {
36
+ ItemCardLg: () => <div data-testid="item-card-lg">Item</div>,
37
+ ItemCardSm: () => <div data-testid="item-card-sm">Item</div>,
38
+ },
39
+ ),
40
+ },
41
+ ),
42
+ }),
43
+ );
44
+
45
+ describe("SearchBar components", () => {
46
+ describe("SearchBarViewLg", () => {
47
+ it("renders large search bar view", () => {
48
+ const { container } = render(<SearchBarViewLg />);
49
+ expect(
50
+ container.querySelector("[data-testid='search-bar']"),
51
+ ).toBeTruthy();
52
+ });
53
+
54
+ it("accepts handleCloseDrawer prop", () => {
55
+ const handleClose = vi.fn();
56
+ const { container } = render(
57
+ <SearchBarViewLg handleCloseDrawer={handleClose} />,
58
+ );
59
+ expect(
60
+ container.querySelector("[data-testid='search-bar']"),
61
+ ).toBeTruthy();
62
+ });
63
+
64
+ it("renders search bar structure", () => {
65
+ const { container } = render(<SearchBarViewLg />);
66
+ const searchBar = container.querySelector("[data-testid='search-bar']");
67
+ expect(searchBar).toBeTruthy();
68
+ expect(searchBar?.children.length).toBeGreaterThan(0);
69
+ });
70
+
71
+ it("renders with results container", () => {
72
+ const { container } = render(<SearchBarViewLg />);
73
+ const searchBar = container.querySelector("[data-testid='search-bar']");
74
+ expect(searchBar).toBeTruthy();
75
+ });
76
+ });
77
+
78
+ describe("SearchBarViewSm", () => {
79
+ it("renders small search bar view", () => {
80
+ const { container } = render(<SearchBarViewSm />);
81
+ expect(
82
+ container.querySelector("[data-testid='search-bar']"),
83
+ ).toBeTruthy();
84
+ });
85
+
86
+ it("accepts handleCloseDrawer prop", () => {
87
+ const handleClose = vi.fn();
88
+ const { container } = render(
89
+ <SearchBarViewSm handleCloseDrawer={handleClose} />,
90
+ );
91
+ expect(
92
+ container.querySelector("[data-testid='search-bar']"),
93
+ ).toBeTruthy();
94
+ });
95
+
96
+ it("renders search bar structure", () => {
97
+ const { container } = render(<SearchBarViewSm />);
98
+ const searchBar = container.querySelector("[data-testid='search-bar']");
99
+ expect(searchBar).toBeTruthy();
100
+ });
101
+
102
+ it("renders with results container", () => {
103
+ const { container } = render(<SearchBarViewSm />);
104
+ const searchBar = container.querySelector("[data-testid='search-bar']");
105
+ expect(searchBar?.children.length).toBeGreaterThan(0);
106
+ });
107
+
108
+ it("renders different from large view", () => {
109
+ const { container: lgContainer } = render(<SearchBarViewLg />);
110
+ const { container: smContainer } = render(<SearchBarViewSm />);
111
+ expect(
112
+ lgContainer.querySelector("[data-testid='search-bar']"),
113
+ ).toBeTruthy();
114
+ expect(
115
+ smContainer.querySelector("[data-testid='search-bar']"),
116
+ ).toBeTruthy();
117
+ });
118
+ });
119
+
120
+ describe("SearchBar integration", () => {
121
+ it("renders search bar without close handler", () => {
122
+ const { container } = render(
123
+ <SearchBarViewLg handleCloseDrawer={undefined} />,
124
+ );
125
+ expect(
126
+ container.querySelector("[data-testid='search-bar']"),
127
+ ).toBeTruthy();
128
+ });
129
+
130
+ it("both views render successfully", () => {
131
+ const { container: lgContainer } = render(<SearchBarViewLg />);
132
+ expect(
133
+ lgContainer.querySelector("[data-testid='search-bar']"),
134
+ ).toBeTruthy();
135
+
136
+ const { container: smContainer } = render(<SearchBarViewSm />);
137
+ expect(
138
+ smContainer.querySelector("[data-testid='search-bar']"),
139
+ ).toBeTruthy();
140
+ });
141
+ });
142
+ });
@@ -29,7 +29,7 @@ export default function SearchResultsContainer({
29
29
  category: state.searchCategory,
30
30
  });
31
31
 
32
- if (!results || results.length === 0) {
32
+ if (!results || !Array.isArray(results) || results.length === 0) {
33
33
  return <SearchResultCard>{t("Search.noResults")}</SearchResultCard>;
34
34
  }
35
35
 
@@ -0,0 +1,100 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { vi } from "vitest";
3
+ import { Section } from "../compound/section";
4
+
5
+ vi.mock("next-intl", () => ({
6
+ useTranslations: () => (key: string) => key,
7
+ }));
8
+
9
+ vi.mock(
10
+ "@evenicanpm/storefront-core/src/components-v2/section/compound/section-header",
11
+ () => ({
12
+ SectionHeader: () => <div data-testid="section-header">Header</div>,
13
+ }),
14
+ );
15
+
16
+ vi.mock("@mui/material/Container", () => ({
17
+ default: ({ className, children }: any) => (
18
+ <div data-testid="mui-container" className={className}>
19
+ {children}
20
+ </div>
21
+ ),
22
+ }));
23
+
24
+ describe("Section", () => {
25
+ it("renders section with children", () => {
26
+ render(
27
+ <Section>
28
+ <div>Section Content</div>
29
+ </Section>,
30
+ );
31
+ expect(screen.getByText("Section Content")).toBeInTheDocument();
32
+ });
33
+
34
+ it("renders with Container component", () => {
35
+ const { container } = render(
36
+ <Section>
37
+ <div>Content</div>
38
+ </Section>,
39
+ );
40
+ expect(
41
+ container.querySelector("[data-testid='mui-container']"),
42
+ ).toBeTruthy();
43
+ });
44
+
45
+ it("passes className to Container", () => {
46
+ const { container } = render(
47
+ <Section>
48
+ <div>Content</div>
49
+ </Section>,
50
+ );
51
+ const containerElement = container.querySelector(
52
+ "[data-testid='mui-container']",
53
+ );
54
+ expect(containerElement?.className).toContain("pb-1");
55
+ });
56
+
57
+ it("accepts Box props", () => {
58
+ const { container } = render(
59
+ <Section sx={{ backgroundColor: "red" }}>
60
+ <div>Content</div>
61
+ </Section>,
62
+ );
63
+ expect(container.firstChild).toBeTruthy();
64
+ });
65
+
66
+ it("renders with spacing", () => {
67
+ render(
68
+ <Section>
69
+ <div>Spaced Content</div>
70
+ </Section>,
71
+ );
72
+ expect(screen.getByText("Spaced Content")).toBeInTheDocument();
73
+ });
74
+
75
+ it("exposes Header subcomponent", () => {
76
+ expect(Section.Header).toBeDefined();
77
+ });
78
+
79
+ it("renders multiple children inside container", () => {
80
+ render(
81
+ <Section>
82
+ <div>Child 1</div>
83
+ <div>Child 2</div>
84
+ <div>Child 3</div>
85
+ </Section>,
86
+ );
87
+ expect(screen.getByText("Child 1")).toBeInTheDocument();
88
+ expect(screen.getByText("Child 2")).toBeInTheDocument();
89
+ expect(screen.getByText("Child 3")).toBeInTheDocument();
90
+ });
91
+
92
+ it("accepts custom sx styling", () => {
93
+ const { container } = render(
94
+ <Section sx={{ padding: 2, margin: 1 }}>
95
+ <div>Styled Section</div>
96
+ </Section>,
97
+ );
98
+ expect(container.firstChild).toBeTruthy();
99
+ });
100
+ });
@@ -0,0 +1,156 @@
1
+ import { render, screen, fireEvent } from "@testing-library/react";
2
+ import type { ReactNode } from "react";
3
+ import { vi } from "vitest";
4
+ import SideNav from "../side-nav";
5
+
6
+ vi.mock("@evenicanpm/storefront-core/src/components-v2/scrollbar", () => ({
7
+ default: ({
8
+ children,
9
+ autoHide,
10
+ }: {
11
+ children: ReactNode;
12
+ autoHide?: boolean;
13
+ }) => (
14
+ <div data-testid="scrollbar" data-autohide={autoHide}>
15
+ {children}
16
+ </div>
17
+ ),
18
+ }));
19
+
20
+ vi.mock("@mui/material/Drawer", () => ({
21
+ default: ({
22
+ open,
23
+ onClose,
24
+ children,
25
+ anchor,
26
+ }: {
27
+ open: boolean;
28
+ onClose?: () => void;
29
+ children: ReactNode;
30
+ anchor: string;
31
+ }) => (
32
+ <button
33
+ type="button"
34
+ data-testid="drawer"
35
+ data-open={open}
36
+ data-anchor={anchor}
37
+ onClick={() => onClose?.()}
38
+ >
39
+ {children}
40
+ </button>
41
+ ),
42
+ }));
43
+
44
+ describe("SideNav", () => {
45
+ const mockHandler = (toggle: () => void) => (
46
+ <button type="button" data-testid="toggle-button" onClick={toggle}>
47
+ Toggle
48
+ </button>
49
+ );
50
+
51
+ it("renders side nav with handler and children", () => {
52
+ render(
53
+ <SideNav handler={mockHandler}>
54
+ <div>Side Nav Content</div>
55
+ </SideNav>,
56
+ );
57
+ expect(screen.getByText("Side Nav Content")).toBeInTheDocument();
58
+ expect(screen.getByTestId("toggle-button")).toBeInTheDocument();
59
+ });
60
+
61
+ it("renders drawer component", () => {
62
+ const { container } = render(
63
+ <SideNav handler={mockHandler}>
64
+ <div>Content</div>
65
+ </SideNav>,
66
+ );
67
+ expect(container.querySelector("[data-testid='drawer']")).toBeTruthy();
68
+ });
69
+
70
+ it("renders with default position left", () => {
71
+ const { container } = render(
72
+ <SideNav handler={mockHandler}>
73
+ <div>Content</div>
74
+ </SideNav>,
75
+ );
76
+ const drawer = container.querySelector("[data-testid='drawer']");
77
+ expect(drawer?.getAttribute("data-anchor")).toBe("left");
78
+ });
79
+
80
+ it("renders with custom position right", () => {
81
+ const { container } = render(
82
+ <SideNav position="right" handler={mockHandler}>
83
+ <div>Content</div>
84
+ </SideNav>,
85
+ );
86
+ const drawer = container.querySelector("[data-testid='drawer']");
87
+ expect(drawer?.getAttribute("data-anchor")).toBe("right");
88
+ });
89
+
90
+ it("manages drawer state", () => {
91
+ const { container } = render(
92
+ <SideNav handler={mockHandler}>
93
+ <div>Content</div>
94
+ </SideNav>,
95
+ );
96
+ const drawer = container.querySelector("[data-testid='drawer']");
97
+ expect(drawer?.getAttribute("data-open")).toBe("false");
98
+ });
99
+
100
+ it("accepts custom width", () => {
101
+ const { container } = render(
102
+ <SideNav width={300} handler={mockHandler}>
103
+ <div>Content</div>
104
+ </SideNav>,
105
+ );
106
+ expect(container.querySelector("[data-testid='drawer']")).toBeTruthy();
107
+ });
108
+
109
+ it("uses scrollbar for children", () => {
110
+ const { container } = render(
111
+ <SideNav handler={mockHandler}>
112
+ <div>Scrollable Content</div>
113
+ </SideNav>,
114
+ );
115
+ expect(container.querySelector("[data-testid='scrollbar']")).toBeTruthy();
116
+ });
117
+
118
+ it("handles drawer close", () => {
119
+ const { container } = render(
120
+ <SideNav handler={mockHandler}>
121
+ <div>Content</div>
122
+ </SideNav>,
123
+ );
124
+ const drawer = container.querySelector("[data-testid='drawer']");
125
+ if (drawer) fireEvent.click(drawer);
126
+ expect(screen.getByTestId("toggle-button")).toBeInTheDocument();
127
+ });
128
+
129
+ it("accepts external toggle function", () => {
130
+ const externalToggle = vi.fn();
131
+ const { container } = render(
132
+ <SideNav toggle={externalToggle} handler={mockHandler}>
133
+ <div>Content</div>
134
+ </SideNav>,
135
+ );
136
+ expect(container.querySelector("[data-testid='drawer']")).toBeTruthy();
137
+ });
138
+
139
+ it("syncs external and internal state", () => {
140
+ const { rerender, container } = render(
141
+ <SideNav open={true} handler={mockHandler}>
142
+ <div>Content</div>
143
+ </SideNav>,
144
+ );
145
+ let drawer = container.querySelector("[data-testid='drawer']");
146
+ expect(drawer?.getAttribute("data-open")).toBe("true");
147
+
148
+ rerender(
149
+ <SideNav open={false} handler={mockHandler}>
150
+ <div>Content</div>
151
+ </SideNav>,
152
+ );
153
+ drawer = container.querySelector("[data-testid='drawer']");
154
+ expect(drawer?.getAttribute("data-open")).toBe("false");
155
+ });
156
+ });
@@ -23,23 +23,22 @@ const AddToWishlistButton = (props: Props) => {
23
23
  };
24
24
 
25
25
  // conditionally render button styles based on variant (pdp or minicart)
26
- let style: any, btnFullwidth: boolean;
26
+ let style: any;
27
+ let btnFullwidth: boolean;
27
28
  let btnColor: ButtonProps["color"];
28
29
  let btnVariant: ButtonProps["variant"];
29
- switch (props.variant) {
30
- case "minicart":
31
- style = {
32
- px: 4,
33
- };
34
- btnFullwidth = true;
35
- btnVariant = "outlined";
36
- btnColor = "primary";
37
- break;
38
- default:
39
- btnFullwidth = false;
40
- btnVariant = "contained";
41
- btnColor = "secondary";
42
- break;
30
+
31
+ if (props.variant === "minicart") {
32
+ style = {
33
+ px: 4,
34
+ };
35
+ btnFullwidth = true;
36
+ btnVariant = "outlined";
37
+ btnColor = "primary";
38
+ } else {
39
+ btnFullwidth = false;
40
+ btnVariant = "contained";
41
+ btnColor = "secondary";
43
42
  }
44
43
 
45
44
  return (
@@ -9,16 +9,14 @@ type ExtendedNavLinkProps = NavLinkProps & {
9
9
  isCurrentPath?: boolean;
10
10
  };
11
11
 
12
- export const MainContainer: ComponentType<CardProps> = styled(Card)(
13
- ({ theme }) => ({
14
- paddingBottom: "1.5rem",
15
- [theme.breakpoints.down("md")]: {
16
- boxShadow: "none",
17
- overflowY: "auto",
18
- height: "calc(100vh - 64px)",
19
- },
20
- }),
21
- ) as typeof Card;
12
+ export const MainContainer = styled(Card)(({ theme }) => ({
13
+ paddingBottom: "1.5rem",
14
+ [theme.breakpoints.down("md")]: {
15
+ boxShadow: "none",
16
+ overflowY: "auto",
17
+ height: "calc(100vh - 64px)",
18
+ },
19
+ }));
22
20
 
23
21
  export const StyledNavLink: ComponentType<ExtendedNavLinkProps> = styled(
24
22
  NavLink,
@@ -83,9 +83,10 @@ const AddressListItem = ({ address, children, sx }: RootProps) => {
83
83
  };
84
84
 
85
85
  // ----------------- Subcomponents -----------------
86
+ type FieldValue = string | number | boolean;
86
87
  interface FieldProps {
87
88
  field: keyof Address; // "Name" | "FullAddress" | "Phone" etc.
88
- render?: (value: string | number | boolean, address: Address) => ReactNode;
89
+ render?: (value: FieldValue, address: Address) => ReactNode;
89
90
  }
90
91
 
91
92
  AddressListItem.Field = ({ field, render }: FieldProps) => {
@@ -166,10 +166,15 @@ CheckoutForm.Step = <T extends object>({
166
166
  const t = useTranslations("Checkout");
167
167
  if (!ctx) return null;
168
168
  const { activeStep, setActiveStep, cart } = ctx;
169
+
170
+ if (stepNumber === undefined) {
171
+ throw new Error("CheckoutForm.Step must be used inside CheckoutForm.Steps");
172
+ }
173
+
169
174
  return (
170
175
  <CheckoutStep
171
176
  title={t(title)}
172
- stepNumber={stepNumber!}
177
+ stepNumber={stepNumber}
173
178
  activeStep={activeStep}
174
179
  setActiveStep={setActiveStep}
175
180
  cart={cart}
@@ -179,12 +179,12 @@ const NewAddressForm = ({
179
179
  const ctx: NewAddressFormCtx = {
180
180
  form,
181
181
  countries,
182
- states: states
183
- ? states?.map((s) => ({
182
+ states: Array.isArray(states)
183
+ ? states.map((s) => ({
184
184
  StateName: s.StateName ?? "",
185
185
  StateId: s.StateId ?? "",
186
186
  }))
187
- : undefined,
187
+ : [],
188
188
  selectedCountry,
189
189
  setSelectedCountry,
190
190
  errors,
@@ -312,11 +312,12 @@ NewAddressForm.StateSelect = () => {
312
312
  render={({ field }) => (
313
313
  <Select labelId="state-label" {...field}>
314
314
  <MenuItem value="0">Select a state</MenuItem>
315
- {states?.map(({ StateName, StateId }) => (
316
- <MenuItem key={StateId} value={StateId}>
317
- {StateName}
318
- </MenuItem>
319
- ))}
315
+ {Array.isArray(states) &&
316
+ states?.map(({ StateName, StateId }) => (
317
+ <MenuItem key={StateId} value={StateId}>
318
+ {StateName}
319
+ </MenuItem>
320
+ ))}
320
321
  </Select>
321
322
  )}
322
323
  />
@@ -155,7 +155,7 @@ const FindInStoreModal = ({
155
155
  if (!stores) {
156
156
  return [];
157
157
  }
158
- const _stores = [...Object.values(stores)];
158
+ const _stores = Object.values(stores);
159
159
  if (hideOutOfStock) {
160
160
  return _stores.filter(
161
161
  (store) => !isOutOfStock(store.location.OrgUnitNumber || ""),
@@ -13,6 +13,7 @@ function ThumbnailWithSkeleton({ src }: Readonly<{ src: string }>) {
13
13
  {!loaded && <Skeleton variant="rounded" width={50} height={50} />}
14
14
  <img
15
15
  src={src}
16
+ alt="Product thumbnail"
16
17
  onLoad={() => setLoaded(true)}
17
18
  style={{
18
19
  display: loaded ? "block" : "none",