@windstream/react-shared-components 0.2.87 → 0.2.88

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windstream/react-shared-components",
3
- "version": "0.2.87",
3
+ "version": "0.2.88",
4
4
  "type": "module",
5
5
  "description": "Shared React components for Kinetic applications",
6
6
  "main": "dist/index.js",
@@ -19,11 +19,18 @@ jest.mock("@shared/components/list", () => ({
19
19
  ListItem: ({
20
20
  children,
21
21
  className,
22
+ ...rest
22
23
  }: {
23
24
  children: React.ReactNode;
24
25
  className?: string;
25
26
  }) => (
26
- <li data-testid="list-item" className={className}>
27
+ <li
28
+ data-testid="list-item"
29
+ className={className}
30
+ {...Object.fromEntries(
31
+ Object.entries(rest).filter(([k]) => k.startsWith("data-"))
32
+ )}
33
+ >
27
34
  {children}
28
35
  </li>
29
36
  ),
@@ -109,6 +116,29 @@ describe("Checklist", () => {
109
116
  });
110
117
  });
111
118
 
119
+ describe("fieldProps", () => {
120
+ it("spreads each structured item's fieldProps onto its own row", () => {
121
+ render(
122
+ <Checklist
123
+ items={[
124
+ { title: "One", fieldProps: { "data-contentful-entry-id": "a" } },
125
+ { title: "Two", fieldProps: { "data-contentful-entry-id": "b" } },
126
+ ]}
127
+ />
128
+ );
129
+ const rows = screen.getAllByTestId("list-item");
130
+ expect(rows[0]).toHaveAttribute("data-contentful-entry-id", "a");
131
+ expect(rows[1]).toHaveAttribute("data-contentful-entry-id", "b");
132
+ });
133
+
134
+ it("renders no tagging attributes for plain or untagged items", () => {
135
+ render(<Checklist items={["Plain", { title: "Structured" }]} />);
136
+ screen.getAllByTestId("list-item").forEach(row => {
137
+ expect(row).not.toHaveAttribute("data-contentful-entry-id");
138
+ });
139
+ });
140
+ });
141
+
112
142
  describe("icons", () => {
113
143
  it("renders check icons by default", () => {
114
144
  render(<Checklist items={["Item 1"]} />);
@@ -74,6 +74,7 @@ export const Checklist: React.FC<ChecklistProps> = props => {
74
74
  `${showIcons ? "flex" : ""} ${iconPosition === "top" ? "items-start" : "items-center"} gap-2 text-text-secondary`,
75
75
  listItemClassName
76
76
  )}
77
+ {...(structured ? item.fieldProps : undefined)}
77
78
  >
78
79
  {showIcons && hasCustomIcon ? (
79
80
  <div className="flex shrink-0 leading-none">
@@ -2,6 +2,8 @@ export type ChecklistItem = {
2
2
  title: React.ReactNode;
3
3
  iconUrl?: string;
4
4
  iconAlt?: string;
5
+ /** Extra DOM attribute props, spread onto this item's list row. */
6
+ fieldProps?: Record<string, unknown>;
5
7
  };
6
8
 
7
9
  export type ChecklistProps = {
@@ -179,6 +179,29 @@ describe("ProductCard", () => {
179
179
  });
180
180
  });
181
181
 
182
+ describe("Missing price", () => {
183
+ it.each([
184
+ ["empty", ""],
185
+ ["undefined", undefined],
186
+ ])("hides the price block when price is %s", (_label, price) => {
187
+ const { container } = render(
188
+ <ProductCard
189
+ {...defaultProps}
190
+ price={price}
191
+ priceSuffixBottom="for 12 months"
192
+ />
193
+ );
194
+ expect(screen.queryByText("$")).not.toBeInTheDocument();
195
+ expect(screen.queryByText(/\/mo/)).not.toBeInTheDocument();
196
+ expect(screen.queryByText("for 12 months")).not.toBeInTheDocument();
197
+ expect(container.textContent).not.toContain("0.00");
198
+ expect(screen.getByRole("heading", { level: 3 })).toHaveTextContent(
199
+ "Kinetic 500"
200
+ );
201
+ expect(screen.getByTestId("select-plan-btn")).toBeInTheDocument();
202
+ });
203
+ });
204
+
182
205
  describe("Best Value", () => {
183
206
  it("renders best value banner when bestValue is true", () => {
184
207
  render(<ProductCard {...defaultProps} bestValue={true} />);
@@ -300,11 +323,13 @@ describe("ProductCard", () => {
300
323
  });
301
324
 
302
325
  it("renders the real gift slot (not the empty placeholder) when the card has its own gift", () => {
303
- const giftBadge = [
304
- { title: "Free router", icon: "/router.png" },
305
- ] as any;
326
+ const giftBadge = [{ title: "Free router", icon: "/router.png" }] as any;
306
327
  const { container } = render(
307
- <ProductCard {...defaultProps} giftBadge={giftBadge} hasAnyGift={true} />
328
+ <ProductCard
329
+ {...defaultProps}
330
+ giftBadge={giftBadge}
331
+ hasAnyGift={true}
332
+ />
308
333
  );
309
334
  expect(
310
335
  container.querySelector("[data-pc-gift-empty]")
@@ -137,51 +137,53 @@ export const ProductCard: React.FC<ProductCardProps> = ({
137
137
  ) : null}
138
138
  </Text>
139
139
  </div>
140
- <div className="flex flex-col items-start md:items-end">
141
- <div className="flex items-start" {...fieldProps?.price}>
142
- <Text
143
- as="span"
144
- className={cx(
145
- priceUnitClass,
146
- "mr-1 mt-[2px]",
147
- isDark ? "text-text-inverse" : "text-text"
148
- )}
149
- >
150
- $
151
- </Text>
152
- <Text
153
- as="span"
154
- className={cx(
155
- priceIntegerClass,
156
- "leading-none",
157
- isDark ? "text-text-inverse" : "text-text"
158
- )}
159
- >
160
- {price.split(".")[0]}
161
- </Text>
162
- <Text
163
- as="span"
164
- className={cx(
165
- priceUnitClass,
166
- isDark ? "text-text-inverse" : "text-text"
167
- )}
168
- >
169
- {`${price.split(".")[1]}${priceUnit}`}
170
- </Text>
140
+ {price ? (
141
+ <div className="flex flex-col items-start md:items-end">
142
+ <div className="flex items-start" {...fieldProps?.price}>
143
+ <Text
144
+ as="span"
145
+ className={cx(
146
+ priceUnitClass,
147
+ "mr-1 mt-[2px]",
148
+ isDark ? "text-text-inverse" : "text-text"
149
+ )}
150
+ >
151
+ $
152
+ </Text>
153
+ <Text
154
+ as="span"
155
+ className={cx(
156
+ priceIntegerClass,
157
+ "leading-none",
158
+ isDark ? "text-text-inverse" : "text-text"
159
+ )}
160
+ >
161
+ {price.split(".")[0]}
162
+ </Text>
163
+ <Text
164
+ as="span"
165
+ className={cx(
166
+ priceUnitClass,
167
+ isDark ? "text-text-inverse" : "text-text"
168
+ )}
169
+ >
170
+ {`${price.split(".")[1]}${priceUnit}`}
171
+ </Text>
172
+ </div>
173
+ {priceSuffixBottom ? (
174
+ <Text
175
+ as="span"
176
+ {...fieldProps?.priceSuffixBottom}
177
+ className={cx(
178
+ "micro text-nowrap",
179
+ isDark ? "text-text-inverse" : "text-text"
180
+ )}
181
+ >
182
+ {priceSuffixBottom}
183
+ </Text>
184
+ ) : null}
171
185
  </div>
172
- {priceSuffixBottom ? (
173
- <Text
174
- as="span"
175
- {...fieldProps?.priceSuffixBottom}
176
- className={cx(
177
- "micro text-nowrap",
178
- isDark ? "text-text-inverse" : "text-text"
179
- )}
180
- >
181
- {priceSuffixBottom}
182
- </Text>
183
- ) : null}
184
- </div>
186
+ ) : null}
185
187
  </header>
186
188
 
187
189
  {/* Description */}
@@ -10,7 +10,7 @@ export interface ProductCardProps {
10
10
  speed: string;
11
11
  techType?: string;
12
12
  ismaxSpeed?: boolean;
13
- price: string;
13
+ price?: string;
14
14
  /** The price unit rendered after the cents, e.g. "/mo" or "/mo*". Defaults
15
15
  * to "/mo". Lets Contentful control a trailing disclaimer star. */
16
16
  priceUnit?: string;
@@ -34,11 +34,13 @@ jest.mock("@shared/contentful/blocks/cards/product-card", () => ({
34
34
  enableBusinessSettings,
35
35
  giftBadge,
36
36
  hasAnyGift,
37
+ price,
37
38
  }: any) => (
38
39
  <article
39
40
  data-testid="product-card"
40
41
  data-plan={planName}
41
42
  data-speed={speed}
43
+ data-price={price}
42
44
  data-expanded={isExpanded}
43
45
  data-business={enableBusinessSettings ? "true" : "false"}
44
46
  >
@@ -373,6 +375,28 @@ describe("ProductCardCarousel", () => {
373
375
  expect(card).toHaveAttribute("data-plan", "500 Mbps");
374
376
  });
375
377
 
378
+ it("passes the formatted price when a price is authored", () => {
379
+ const fields = createProductFields([sampleProduct]);
380
+ render(<ProductCardCarousel fields={fields} />);
381
+ expect(screen.getByTestId("product-card")).toHaveAttribute(
382
+ "data-price",
383
+ "49.99"
384
+ );
385
+ });
386
+
387
+ it.each([
388
+ ["undefined", undefined],
389
+ ["empty", ""],
390
+ ])("passes an empty price (not 0.00) when price is %s", (_label, price) => {
391
+ const item = { ...sampleProduct, price };
392
+ const fields = createProductFields([item]);
393
+ render(<ProductCardCarousel fields={fields} />);
394
+ expect(screen.getByTestId("product-card")).toHaveAttribute(
395
+ "data-price",
396
+ ""
397
+ );
398
+ });
399
+
376
400
  it("renders Next arrow and hides Previous on first slide in carousel mode", async () => {
377
401
  const items = Array.from({ length: 4 }, (_, i) => ({
378
402
  ...sampleProduct,
@@ -380,7 +380,7 @@ function ProductCardPanel({
380
380
  const planName = planContent?.[0] || "";
381
381
  const planSubtext = planContent?.[1] || "";
382
382
  const priceCents = item?.priceSuffix?.split("/")?.[0] || "00";
383
- const formattedPrice = `${item?.price || "0"}.${priceCents}`;
383
+ const formattedPrice = item?.price ? `${item.price}.${priceCents}` : "";
384
384
  // Preserve the full unit after the cents (e.g. "/mo*") so a Contentful
385
385
  // disclaimer star renders; fall back to "/mo" when no unit is present.
386
386
  const suffixParts = item?.priceSuffix?.split("/") || [];