Skip to content

Commit 05eeb4a

Browse files
author
jie-dai_sfemu
committed
Sync from monorepo
Template version: 0.4.0-alpha.2 Uses NPM packages @salesforce/storefront-next-* v0.4.0-alpha.2 Synced by: jie-dai_sfemu Monorepo SHA: 7ddb8bd04fee34318f7fb32a0e5e133cf9b4e59e Latest change: 7ddb8bd04 - @W-22264655 add back edit button (#1580)
1 parent 5772eda commit 05eeb4a

6 files changed

Lines changed: 235 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## v0.4.0-dev (Apr 30, 2026)
2+
3+
- **Cart line item:** Restore the edit button for non-standard, non-bonus products alongside the newly added wishlist toggle and remove action.
4+
15
## v0.4.0-dev (Apr 28, 2026)
26

37
- **Cart line item:** Removed the edit action from the line card, added an inline wishlist add/remove toggle, removed the product description block, and removed the delivery pill so the row focuses on image, title, attributes, price, and quantity.

lighthouserc.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ module.exports = {
120120
'categories:best-practices': ['error', { minScore: 0.96, aggregationMethod: 'median' }],
121121
'resource-summary:script:size': [
122122
'error',
123-
{ maxNumericValue: 455000, aggregationMethod: 'median' },
123+
{ maxNumericValue: 460000, aggregationMethod: 'median' },
124124
],
125125
'resource-summary:document:size': [
126126
'error',

src/components/cart/cart-content.test.tsx

Lines changed: 193 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616
import { describe, test, expect, vi, beforeEach } from 'vitest';
17-
import { render, screen, waitFor } from '@testing-library/react';
17+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
1818
import { getTranslation } from '@salesforce/storefront-next-runtime/i18n';
1919

2020
const { t } = getTranslation();
@@ -168,7 +168,7 @@ describe('CartContent', () => {
168168
});
169169

170170
describe('Line item secondary actions', () => {
171-
test('renders remove and wishlist controls for each cart item with itemId', async () => {
171+
test('renders remove, edit, and wishlist controls for each cart item with itemId', async () => {
172172
renderCartContent({
173173
basket: mockBasket,
174174
productsByItemId: mockProductMap,
@@ -177,6 +177,8 @@ describe('CartContent', () => {
177177

178178
expect(screen.getByTestId('remove-item-item-1')).toBeInTheDocument();
179179
expect(screen.getByTestId('remove-item-item-2')).toBeInTheDocument();
180+
expect(screen.getByTestId('edit-item-item-1')).toBeInTheDocument();
181+
expect(screen.getByTestId('edit-item-item-2')).toBeInTheDocument();
180182
expect(await screen.findByTestId('cart-add-wishlist-item-1')).toBeInTheDocument();
181183
expect(await screen.findByTestId('cart-add-wishlist-item-2')).toBeInTheDocument();
182184
});
@@ -197,8 +199,10 @@ describe('CartContent', () => {
197199
});
198200

199201
expect(screen.queryByTestId('remove-item-item-1')).not.toBeInTheDocument();
202+
expect(screen.queryByTestId('edit-item-item-1')).not.toBeInTheDocument();
200203
expect(screen.queryByTestId('cart-add-wishlist-item-1')).not.toBeInTheDocument();
201204
expect(screen.getByTestId('remove-item-item-2')).toBeInTheDocument();
205+
expect(screen.getByTestId('edit-item-item-2')).toBeInTheDocument();
202206
expect(await screen.findByTestId('cart-add-wishlist-item-2')).toBeInTheDocument();
203207
});
204208

@@ -269,4 +273,191 @@ describe('CartContent', () => {
269273
expect(screen.queryByTestId('cart-add-wishlist-item-1')).not.toBeInTheDocument();
270274
});
271275
});
276+
277+
describe('CartItemEditButton Integration', () => {
278+
test('renders edit buttons for each cart item', () => {
279+
renderCartContent({
280+
basket: mockBasket,
281+
productsByItemId: mockProductMap,
282+
bonusProductsById: mockBonusProductsById,
283+
});
284+
285+
expect(screen.getByTestId('edit-item-item-1')).toBeInTheDocument();
286+
expect(screen.getByTestId('edit-item-item-2')).toBeInTheDocument();
287+
288+
const editButtons = screen.getAllByText(t('actionCard:edit'));
289+
expect(editButtons).toHaveLength(2);
290+
});
291+
292+
test('applies correct className to edit buttons', () => {
293+
renderCartContent({
294+
basket: mockBasket,
295+
productsByItemId: mockProductMap,
296+
bonusProductsById: mockBonusProductsById,
297+
});
298+
299+
const editButton1 = screen.getByTestId('edit-item-item-1');
300+
const editButton2 = screen.getByTestId('edit-item-item-2');
301+
302+
expect(editButton1).toHaveClass('pl-0');
303+
expect(editButton2).toHaveClass('pl-0');
304+
});
305+
306+
test('opens product modal when edit button is clicked', () => {
307+
renderCartContent({
308+
basket: mockBasket,
309+
productsByItemId: mockProductMap,
310+
bonusProductsById: mockBonusProductsById,
311+
});
312+
313+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
314+
315+
const editButton = screen.getByTestId('edit-item-item-1');
316+
fireEvent.click(editButton);
317+
318+
expect(screen.getByRole('dialog')).toBeInTheDocument();
319+
expect(screen.getByText(t('editItem:title'))).toBeInTheDocument();
320+
});
321+
322+
test('can close modal using close button', () => {
323+
renderCartContent({
324+
basket: mockBasket,
325+
productsByItemId: mockProductMap,
326+
bonusProductsById: mockBonusProductsById,
327+
});
328+
329+
const editButton = screen.getByTestId('edit-item-item-1');
330+
fireEvent.click(editButton);
331+
332+
expect(screen.getByRole('dialog')).toBeInTheDocument();
333+
334+
const closeButton = screen.getByRole('button', { name: /close/i });
335+
fireEvent.click(closeButton);
336+
337+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
338+
});
339+
340+
test('does not render edit buttons when product has no itemId', () => {
341+
const basketWithoutItemIds = {
342+
...mockBasket,
343+
productItems: [
344+
{ quantity: 2, productId: 'product-1' },
345+
{ itemId: 'item-2', quantity: 1, productId: 'product-2' },
346+
],
347+
};
348+
349+
renderCartContent({
350+
basket: basketWithoutItemIds,
351+
productsByItemId: mockProductMap,
352+
bonusProductsById: mockBonusProductsById,
353+
});
354+
355+
expect(screen.queryByTestId('edit-item-item-1')).not.toBeInTheDocument();
356+
expect(screen.getByTestId('edit-item-item-2')).toBeInTheDocument();
357+
});
358+
});
359+
360+
describe('Edit button visibility (CartItemEditButton presence)', () => {
361+
function renderWith(productsByItemId: Record<string, any>) {
362+
const basket = {
363+
basketId: 'b1',
364+
productItems: [{ itemId: 'item-1', quantity: 1, productId: 'p1' }],
365+
};
366+
return renderCartContent({ basket, productsByItemId, bonusProductsById: mockBonusProductsById });
367+
}
368+
369+
const editBtn = () => screen.queryByTestId('edit-item-item-1');
370+
371+
test('standard product does not show CartItemEditButton', () => {
372+
renderWith({ 'item-1': { id: 'p1', type: { item: true } } } as any);
373+
expect(editBtn()).not.toBeInTheDocument();
374+
});
375+
376+
test('product with variants shows CartItemEditButton', () => {
377+
renderWith({ 'item-1': { id: 'p1', variants: [{} as any] } } as any);
378+
expect(editBtn()).toBeInTheDocument();
379+
});
380+
381+
test('missing product details mapping shows CartItemEditButton', () => {
382+
const basket = { basketId: 'b1', productItems: [{ itemId: 'item-1', quantity: 1, productId: 'p1' }] };
383+
renderCartContent({
384+
basket,
385+
productsByItemId: {} as any,
386+
bonusProductsById: mockBonusProductsById,
387+
});
388+
expect(editBtn()).toBeInTheDocument();
389+
});
390+
391+
test('bundle product shows CartItemEditButton', () => {
392+
renderWith({ 'item-1': { id: 'p1', type: { bundle: true } } } as any);
393+
expect(editBtn()).toBeInTheDocument();
394+
});
395+
396+
test('parent product shows CartItemEditButton', () => {
397+
renderWith({ 'item-1': { id: 'p1', type: { master: true } } } as any);
398+
expect(editBtn()).toBeInTheDocument();
399+
});
400+
401+
test('choice-based bonus product does not show CartItemEditButton', () => {
402+
const basket = {
403+
basketId: 'b1',
404+
productItems: [
405+
{
406+
itemId: 'item-1',
407+
quantity: 1,
408+
productId: 'p1',
409+
bonusProductLineItem: true,
410+
bonusDiscountLineItemId: 'bonus-discount-choice-1',
411+
},
412+
],
413+
bonusDiscountLineItems: [
414+
{
415+
id: 'bonus-discount-choice-1',
416+
promotionId: 'promo-choice-1',
417+
maxBonusItems: 3,
418+
bonusProducts: [{ productId: 'p1', productName: 'Choice Bonus Product 1' }],
419+
},
420+
],
421+
};
422+
423+
renderCartContent({
424+
basket: basket as any,
425+
productsByItemId: { 'item-1': { id: 'p1', variants: [{} as any] } } as any,
426+
bonusProductsById: { p1: { id: 'p1', name: 'Choice Bonus Product 1' } } as any,
427+
});
428+
429+
expect(editBtn()).not.toBeInTheDocument();
430+
expect(screen.getByTestId('remove-item-item-1')).toBeInTheDocument();
431+
});
432+
433+
test('auto bonus product does not show CartItemEditButton', () => {
434+
const basket = {
435+
basketId: 'b1',
436+
productItems: [
437+
{
438+
itemId: 'item-1',
439+
quantity: 1,
440+
productId: 'p1',
441+
bonusProductLineItem: true,
442+
bonusDiscountLineItemId: 'bonus-discount-auto-1',
443+
},
444+
],
445+
bonusDiscountLineItems: [
446+
{
447+
id: 'bonus-discount-auto-1',
448+
promotionId: 'promo-auto-1',
449+
maxBonusItems: 1,
450+
},
451+
],
452+
};
453+
454+
renderCartContent({
455+
basket: basket as any,
456+
productsByItemId: { 'item-1': { id: 'p1', variants: [{} as any] } } as any,
457+
bonusProductsById: mockBonusProductsById,
458+
});
459+
460+
expect(editBtn()).not.toBeInTheDocument();
461+
});
462+
});
272463
});

src/components/cart/cart-content.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type { ShopperBasketsV2, ShopperProducts, ShopperPromotions } from '@sale
2121
// Components
2222
import ProductItemsList from '@/components/product-items-list';
2323
import { RemoveItemButtonWithConfirmation } from '@/components/buttons/remove-item-button-with-confirmation';
24+
import { CartItemEditButton } from '@/components/cart/cart-item-edit-button';
2425
import CartEmpty from './cart-empty';
2526
import CartTitle from './cart-title';
2627
import OrderSummary from '@/components/order-summary';
@@ -39,7 +40,7 @@ import CartDeliveryOption from '@/extensions/bopis/components/delivery-options/c
3940
import { UITarget } from '@/targets/ui-target';
4041

4142
// utils
42-
import { isBonusProduct, isRuleBasedPromotion, type EnrichedProductItem } from '@/lib/product-utils';
43+
import { isStandardProduct, isBonusProduct, isRuleBasedPromotion, type EnrichedProductItem } from '@/lib/product-utils';
4344

4445
const LazyBonusProductSelection = lazy(() => import('@/components/cart/bonus-product-selection'));
4546
const LazyBonusProductModal = lazy(() =>
@@ -160,11 +161,14 @@ export default function CartContent({
160161
}
161162

162163
const isBonusProd = isBonusProduct(product);
164+
const isStandardProd = isStandardProduct(product);
165+
const shouldShowEditButton = !isStandardProd && !isBonusProd;
163166
const shouldShowWishlist = !isBonusProd;
164167

165168
return (
166169
<div className="flex gap-2">
167170
<RemoveItemButtonWithConfirmation itemId={product.itemId} className="pl-0" />
171+
{shouldShowEditButton && <CartItemEditButton product={product} className="pl-0" />}
168172
{shouldShowWishlist && (
169173
<Suspense fallback={null}>
170174
<LazyCartItemAddToWishlistButton

src/components/cart/cart-item-edit-button.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { ShopperBasketsV2, ShopperProducts } from '@salesforce/storefront-n
2424
import { CartItemModal } from '@/components/cart-item-modal';
2525
import { Button } from '@/components/ui/button';
2626
import { useTranslation } from 'react-i18next';
27+
import { cn } from '@/lib/utils';
2728

2829
// Constants
2930

@@ -45,7 +46,7 @@ interface CartItemEditButtonProps {
4546
* @param props - Component props
4647
* @returns JSX element with edit button and product modal
4748
*/
48-
export function CartItemEditButton({ product, className = '' }: CartItemEditButtonProps): ReactElement {
49+
export function CartItemEditButton({ product, className }: CartItemEditButtonProps): ReactElement {
4950
// Modal state management
5051
const { t } = useTranslation('actionCard');
5152
const [isOpen, setIsOpen] = useState(false);
@@ -54,7 +55,8 @@ export function CartItemEditButton({ product, className = '' }: CartItemEditButt
5455
<>
5556
<Button
5657
variant="link"
57-
className={`text-xs cursor-pointer hover:no-underline h-auto p-0 ${className ?? ''}`}
58+
size="sm"
59+
className={cn('text-xs cursor-pointer hover:no-underline', className)}
5860
aria-label={`${t('edit')} ${product.productName ?? ''}`}
5961
data-testid={`edit-item-${product.itemId}`}
6062
onClick={() => setIsOpen(true)}>

0 commit comments

Comments
 (0)