Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CHECKOUT-8775: Toggle from multi shipping to single shipping and vice versa #2095

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions packages/core/src/app/shipping/AllocatedItemsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,9 @@ import React from "react";

import { IconClose } from "../ui/icon";

import { renderItemContent } from "./ConsignmentLineItemDetail";
import { MultiShippingTableData, MultiShippingTableItemWithType } from "./MultishippingV2Type";

export const getItemContent = (lineItem: MultiShippingTableItemWithType) => {
return <span>
<strong>{`${lineItem.quantity} x `}</strong>
{lineItem.name}
{lineItem.options?.length
? <span className="line-item-options">{` - ${lineItem.options.map(option => option.value).join('/ ')}`}</span>
: ''}
</span>;
};

interface AllocatedItemsListProps {
assignedItems: MultiShippingTableData;
onUnassignItem(itemToDelete: MultiShippingTableItemWithType): void;
Expand All @@ -26,7 +17,7 @@ const AllocatedItemsList = ({ assignedItems, onUnassignItem }: AllocatedItemsLis
<ul className="allocated-line-items-list">
{assignedItems.lineItems.map(item => (
<li key={item.id}>
{getItemContent(item)}
{renderItemContent(item)}
<span data-test={`remove-${item.id.toString()}-button`} onClick={() => onUnassignItem(item)}>
<IconClose />
</span>
Expand Down
10 changes: 2 additions & 8 deletions packages/core/src/app/shipping/ConsignmentLineItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { useCheckout } from "@bigcommerce/checkout/payment-integration-api";

import { IconChevronDown, IconChevronUp } from "../ui/icon";

import { getItemContent } from "./AllocatedItemsList";
import AllocateItemsModal from "./AllocateItemsModal";
import ConsignmentLineItemDetail from "./ConsignmentLineItemDetail";
import { AssignItemFailedError, UnassignItemError } from "./errors";
import { useDeallocateItem } from "./hooks/useDeallocateItem";
import { useMultiShippingConsignmentItems } from "./hooks/useMultishippingConsignmentItems";
Expand Down Expand Up @@ -117,13 +117,7 @@ const ConsignmentLineItem: FunctionComponent<ConsignmentLineItemProps> = ({ cons
</a>
</div>
{showItems
? <ul className="consignment-line-item-list">
{consignment.lineItems.map(lineItem => (
<li key={lineItem.id}>
{getItemContent(lineItem)}
</li>
))}
</ul>
? <ConsignmentLineItemDetail lineItems={consignment.lineItems} />
: null
}
</div>
Expand Down
25 changes: 16 additions & 9 deletions packages/core/src/app/shipping/ConsignmentLineItemDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,30 @@ export interface ConsignmentLineItemDetailProps {
lineItems: MultiShippingTableItemWithType[] | PhysicalItem[]
}

const renderProductOptionDetails = (item: MultiShippingTableItemWithType | PhysicalItem) => {
if (!item.options || !item.options.length) {
return null;
}

return (<span className="line-item-options">{` - ${item.options.map(option => option.value).join(' / ')}`}</span>);
}

export const renderItemContent = (item: MultiShippingTableItemWithType | PhysicalItem) => {
return <span>
<strong>{item.quantity} x </strong>{item.name}
{renderProductOptionDetails(item)}
</span>;
};

const ConsignmentLineItemDetail: FunctionComponent<ConsignmentLineItemDetailProps> = ({
lineItems,
}) => {
const renderProductOptionDetails = (item: MultiShippingTableItemWithType | PhysicalItem) => {
if (!item.options || !item.options.length) {
return null;
}

return (<span className="line-item-options">{` - ${item.options.map(option => option.value).join(' / ')}`}</span>);
}

return (
<ul className="consignment-line-item-list">
{lineItems.map((item) => (
<li key={item.id}>
<strong>{item.quantity} x </strong>{item.name}
{renderProductOptionDetails(item)}
{renderItemContent(item)}
</li>
))}
</ul>
Expand Down
20 changes: 11 additions & 9 deletions packages/core/src/app/shipping/ConsignmentListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,17 @@ const ConsignmentListItem: FunctionComponent<ConsignmentListItemProps> = ({
return (
<div className='consignment-container'>
<div className='consignment-header'>
<h3><TranslatedString data={{ consignmentNumber }} id="shipping.multishipping_consignment_index_heading" /></h3>
<a
className="delete-consignment"
data-test="delete-consignment-button"
href="#"
onClick={preventDefault(handleClose)}
>
<IconClose size={IconSize.Small}/>
</a>
<h3>
<TranslatedString data={{ consignmentNumber }} id="shipping.multishipping_consignment_index_heading" />
</h3>
<a
className="delete-consignment"
data-test="delete-consignment-button"
href="#"
onClick={preventDefault(handleClose)}
>
bc-peng marked this conversation as resolved.
Show resolved Hide resolved
<IconClose size={IconSize.Small} />
</a>
</div>
<ConsignmentAddressSelector
consignment={consignment}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ describe('MultiShippingFormV2 Component', () => {

// eslint-disable-next-line testing-library/no-node-access
const destination2 = screen.getByText('Destination #2').parentElement?.parentElement;
const addressSelectButton = within(destination2).getAllByTestId('address-select-button')[1];
const addressSelectButton = within(destination2).getByTestId('address-select-button');

await userEvent.click(addressSelectButton);

Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/app/shipping/NewConsignment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@ const NewConsignment = ({

return (
<div className='consignment-container'>
<h3 className='consignment-header'>
<TranslatedString data={{ consignmentNumber }} id="shipping.multishipping_consignment_index_heading" />
</h3>
<div className='consignment-header'>
<h3>
<TranslatedString data={{ consignmentNumber }} id="shipping.multishipping_consignment_index_heading" />
</h3>
</div>
<ConsignmentAddressSelector
countriesWithAutocomplete={countriesWithAutocomplete}
defaultCountryCode={defaultCountryCode}
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/app/shipping/Shipping.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,10 @@ describe('Shipping Component', () => {
checkoutSettings: {
...getStoreConfig().checkoutSettings,
hasMultiShippingEnabled: true,
features: {
...getStoreConfig().checkoutSettings.features,
"PROJECT-4159.improve_multi_address_shipping_ui": false,
}
},
})
});
Expand Down
163 changes: 163 additions & 0 deletions packages/core/src/app/shipping/Shipping.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import '@testing-library/jest-dom';
import {
CheckoutSelectors,
CheckoutService,
createCheckoutService,
} from '@bigcommerce/checkout-sdk';
import userEvent from '@testing-library/user-event';
import React, { FunctionComponent } from 'react';

import { ExtensionProvider } from '@bigcommerce/checkout/checkout-extension';
import {
createLocaleContext,
LocaleContext,
LocaleContextType,
} from '@bigcommerce/checkout/locale';
import { CheckoutProvider, PaymentMethodId } from '@bigcommerce/checkout/payment-integration-api';
import { render, screen, within } from '@bigcommerce/checkout/test-utils';

import { getAddressFormFields } from '../address/formField.mock';
import { getCart } from '../cart/carts.mock';
import { getPhysicalItem } from '../cart/lineItem.mock';
import { getCheckout } from '../checkout/checkouts.mock';
import CheckoutStepType from '../checkout/CheckoutStepType';
import { getStoreConfig } from '../config/config.mock';
import { getCustomer } from '../customer/customers.mock';
import { getConsignment } from '../shipping/consignment.mock';

import Shipping, { ShippingProps, WithCheckoutShippingProps } from './Shipping';
import { getShippingAddress } from './shipping-addresses.mock';

describe('Shipping component', () => {
let localeContext: LocaleContextType;
let checkoutService: CheckoutService;
let checkoutState: CheckoutSelectors;
let defaultProps: ShippingProps;
let ComponentTest: FunctionComponent<ShippingProps> & Partial<WithCheckoutShippingProps>;

beforeEach(() => {
localeContext = createLocaleContext(getStoreConfig());
checkoutService = createCheckoutService();

checkoutState = checkoutService.getState();

defaultProps = {
isBillingSameAsShipping: true,
isMultiShippingMode: false,
onToggleMultiShipping: jest.fn(),
cartHasChanged: false,
onSignIn: jest.fn(),
step: {
isActive: true,
isComplete: true,
isEditable: true,
isRequired: true,
type: CheckoutStepType.Shipping
},
providerWithCustomCheckout: PaymentMethodId.StripeUPE,
isShippingMethodLoading: true,
navigateNextStep: jest.fn(),
onUnhandledError: jest.fn(),
};

jest.spyOn(checkoutService, 'loadShippingAddressFields').mockResolvedValue(
{} as CheckoutSelectors,
);

jest.spyOn(checkoutService, 'loadBillingAddressFields').mockResolvedValue(
{} as CheckoutSelectors,
);

jest.spyOn(checkoutService, 'loadShippingOptions').mockResolvedValue(
{} as CheckoutSelectors,
);

jest.spyOn(checkoutService, 'deleteConsignment').mockResolvedValue({} as CheckoutSelectors);

jest.spyOn(checkoutState.data, 'getCart').mockReturnValue({
...getCart(),
lineItems: {
physicalItems: [
{
...getPhysicalItem(),
quantity: 3,
},
],
},
} as Cart);

jest.spyOn(checkoutState.data, 'getShippingAddress').mockReturnValue(getShippingAddress());

jest.spyOn(checkoutState.data, 'getBillingAddress').mockReturnValue(undefined);

jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue(getStoreConfig());

jest.spyOn(checkoutState.data, 'getShippingAddressFields').mockReturnValue(
getAddressFormFields(),
);

jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue({
...getCustomer(),
addresses: [],
});

jest.spyOn(checkoutState.data, 'getConsignments').mockReturnValue([getConsignment()]);

jest.spyOn(checkoutState.data, 'getCheckout').mockReturnValue(getCheckout());

jest.spyOn(checkoutService, 'updateBillingAddress').mockResolvedValue(
{} as CheckoutSelectors,
);
jest.spyOn(checkoutService, 'updateCheckout').mockResolvedValue({} as CheckoutSelectors);
jest.spyOn(checkoutService, 'updateShippingAddress').mockResolvedValue(
{} as CheckoutSelectors,
);

ComponentTest = (props) => (
<CheckoutProvider checkoutService={checkoutService}>
<LocaleContext.Provider value={localeContext}>
<ExtensionProvider checkoutService={checkoutService}>
<Shipping {...props} />
</ExtensionProvider>
</LocaleContext.Provider>
</CheckoutProvider>
);
});


describe('when new multishipping ui is enabled', () => {
beforeEach(async () => {
jest.spyOn(checkoutState.data, 'getConfig').mockReturnValue({
...getStoreConfig(),
checkoutSettings: {
...getStoreConfig().checkoutSettings,
hasMultiShippingEnabled: true,
features: {
...getStoreConfig().checkoutSettings.features,
"PROJECT-4159.improve_multi_address_shipping_ui": true,
},
},
});
});

it('opens confirmation dialog on clicking the link and calls onToggleMultiShipping when confirm is clicked', async () => {
render(<ComponentTest {...defaultProps} isMultiShippingMode={true} />);

const shippingModeToggle = await screen.findByTestId("shipping-mode-toggle");

expect(shippingModeToggle.innerHTML).toBe('Ship to a single address');

await userEvent.click(shippingModeToggle);

const confirmationModal = await screen.findByRole('dialog');

expect(confirmationModal).toBeInTheDocument();
expect(within(confirmationModal).getByText(localeContext.language.translate('shipping.ship_to_single_action'))).toBeInTheDocument();
expect(within(confirmationModal).getByText(localeContext.language.translate('shipping.ship_to_single_message'))).toBeInTheDocument();

await userEvent.click(within(confirmationModal).getByText('Confirm'));

expect(defaultProps.onToggleMultiShipping).toHaveBeenCalled();
});
});
});
18 changes: 12 additions & 6 deletions packages/core/src/app/shipping/Shipping.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ class Shipping extends Component<ShippingProps & WithCheckoutShippingProps, Ship
isInitialValueLoaded={shouldRenderWhileLoading ? !isInitializing : true}
isLoading={ isInitializing }
isMultiShippingMode={isMultiShippingMode}
isNewMultiShippingUIEnabled={isNewMultiShippingUIEnabled}
isShippingMethodLoading={ this.props.isLoading }
onMultiShippingChange={ this.handleMultiShippingModeSwitch }
onSubmit={this.handleSingleShippingSubmit}
Expand All @@ -171,6 +172,7 @@ class Shipping extends Component<ShippingProps & WithCheckoutShippingProps, Ship
<ShippingHeader
isGuest={isGuest}
isMultiShippingMode={isMultiShippingMode}
isNewMultiShippingUIEnabled={isNewMultiShippingUIEnabled}
onMultiShippingChange={this.handleMultiShippingModeSwitch}
shouldShowMultiShipping={shouldShowMultiShipping}
/>
Expand Down Expand Up @@ -201,22 +203,26 @@ class Shipping extends Component<ShippingProps & WithCheckoutShippingProps, Ship
const {
consignments,
isMultiShippingMode,
isNewMultiShippingUIEnabled,
onToggleMultiShipping = noop,
onUnhandledError = noop,
updateShippingAddress,
deleteConsignments,
} = this.props;

if (isMultiShippingMode && consignments.length > 1) {
try {
this.setState({ isInitializing: true });

try {
if (isMultiShippingMode && consignments.length > 1) {
// Collapse all consignments into one
await updateShippingAddress(consignments[0].shippingAddress);
} catch (error) {
onUnhandledError(error);
} finally {
this.setState({ isInitializing: false });
} else if (!isMultiShippingMode && isNewMultiShippingUIEnabled) {
await deleteConsignments();
}
} catch (error) {
onUnhandledError(error);
} finally {
this.setState({ isInitializing: false });
}

onToggleMultiShipping();
Expand Down
Loading