From 991e407fa9fc7f98693e205b11786e7549fb240d Mon Sep 17 00:00:00 2001 From: Thomas Roberts <5656702+opr@users.noreply.github.com> Date: Mon, 13 Mar 2023 11:49:28 +0000 Subject: [PATCH] Remove WC Core shipping settings if Cart/Checkout blocks are in use (https://github.com/woocommerce/woocommerce-blocks/pull/8679) * Add CartCheckoutUtils class This class will store reusable methods relating to Cart/Checkout Blocks, i.e. whether they are used on the Cart/Checkout page. * Update ShippingController to use the new CartCheckoutUtils function This will reduce code duplication when checking if the Cart/Checkout blocks are in use on the Cart/Checkout page. * Add filter to remove shipping settings when Cart/Checkout are default * Ensure setting displays correctly if cart is default but not checkout * Add tests to ensure core shipping settings update correctly * Add setCartCheckoutPages function to update set the cart/checkout page * Force shipping to be enabled if the Checkout block is in use. * Add filter to override cost requires address option * Add shippingCostRequiresAddress option * Check if the address is required before showing rates * Show shipping rates in editor * Add shippingCostRequiresAddress attribute to shipping methods block * Update frontend type to show shippingCostRequiresAddress is a prop * Add control to toggle shippingCostRequiresAddress option * Show address notice in the correct scenario * Send shippingCostRequiresAddress to Block in front end context * Add e2e test for editor control * Add e2e tests for shipping options on the front end * Add updateAttributeInSiblingBlock function * Add shippingCostRequiresAddress to shipping method block * Ensure attribute is updated in both blocks when editing * In Shipping Methods Block, show correct component based on block setting * Show correct block in editor * Remove broken test from PR * Clean up updateAttributeInSiblingBlock * Add setCartCheckoutPages function to update set the cart/checkout page * Add tests to ensure core shipping settings update correctly * Add isAddressComplete function Borrowed from woocommerce/woocommerce-blocks#8141 * Check if the address is required before showing rates * Show shipping rates in editor * Show address notice in the correct scenario * Add e2e tests for shipping options on the front end * Ensure errorId is passed to StateInput * Add fullShippingAddressPushed action to wc/store/cart * Add fullShippingAddressPushed case to reducer * Ensure fullShippingAddressPushed is set when initialising cart store * Add fullShippingAddressPushed selector and default state entry * Add shippingAddressHasValidationErrors util function * Do not overwrite addresses when selecting a rate * Set whether full address has been pushed when saving address changes * In Shipping Methods Block, show correct component based on block setting * Don't show from price if rates should be hidden until address entered * Check city validation errors to assert if shipping address is valid * Rename merchant.js to merchant.ts * Move local pickup functions to common merchant util * Update local pickup tests to use common merchant utils * Add test to ensure setting toggles in both blocks * Add navigating to settings and saving in merchant util * Create addPickupLocation merchant util * Add test for local pickup and require full address * Make sure correct conditions are met to show shipping options * Ensure checkbox is checked during local pickup tests * Unset the checkbox when tests are finished running * Update checkout block fixture * Prevent error in unit tests * Import validation store key from constants Required because importing from the index causes the validation data store to register twice * Update checkout terms test to wait for button not to be disabled * Revert "Add isAddressComplete function" This reverts commit 9967dc0d4f10cf638859ae085e6f4cc2901dd299. --- .../components/state-input/StateInputProps.ts | 1 + .../components/state-input/state-input.tsx | 2 + .../attributes.tsx | 4 + .../checkout-shipping-method-block/block.json | 4 + .../checkout-shipping-method-block/block.tsx | 10 +- .../checkout-shipping-method-block/edit.tsx | 35 +++ .../frontend.tsx | 3 + .../attributes.tsx | 4 + .../block.json | 4 + .../checkout-shipping-methods-block/block.tsx | 20 +- .../checkout-shipping-methods-block/edit.tsx | 43 +++- .../frontend.tsx | 6 +- .../assets/js/data/cart/action-types.ts | 1 + .../assets/js/data/cart/actions.ts | 17 +- .../assets/js/data/cart/default-state.ts | 1 + .../assets/js/data/cart/push-changes.ts | 6 + .../assets/js/data/cart/reducers.ts | 9 + .../assets/js/data/cart/resolvers.ts | 5 + .../assets/js/data/cart/selectors.ts | 7 + .../assets/js/data/cart/test/push-changes.ts | 1 + .../assets/js/data/cart/utils.ts | 29 +++ .../assets/js/types/type-defs/cart.ts | 2 + .../assets/js/utils/attributes.ts | 33 +++ .../src/Shipping/ShippingController.php | 105 ++++++++- .../src/Utils/CartCheckoutUtils.php | 28 +++ .../__fixtures__/checkout.fixture.json | 2 +- .../tests/e2e/specs/backend/checkout.test.js | 74 +++++++ .../e2e/specs/merchant/checkout-terms.test.js | 5 + .../e2e/specs/merchant/local-pickup.test.ts | 162 +++++++++----- .../shopper/cart-checkout/checkout.test.js | 204 +++++++++++++++++- .../tests/utils/merchant.js | 17 -- .../tests/utils/merchant.ts | 110 ++++++++++ 32 files changed, 871 insertions(+), 83 deletions(-) create mode 100644 plugins/woocommerce-blocks/src/Utils/CartCheckoutUtils.php delete mode 100644 plugins/woocommerce-blocks/tests/utils/merchant.js create mode 100644 plugins/woocommerce-blocks/tests/utils/merchant.ts diff --git a/plugins/woocommerce-blocks/assets/js/base/components/state-input/StateInputProps.ts b/plugins/woocommerce-blocks/assets/js/base/components/state-input/StateInputProps.ts index bf66f2cfba4..341dbe34c27 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/state-input/StateInputProps.ts +++ b/plugins/woocommerce-blocks/assets/js/base/components/state-input/StateInputProps.ts @@ -8,6 +8,7 @@ export interface StateInputProps { onChange: ( value: string ) => void; required?: boolean; errorMessage?: string; + errorId?: string; } export type StateInputWithStatesProps = StateInputProps & { diff --git a/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx b/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx index 15d8b6f61b9..7bb069d5999 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx @@ -36,6 +36,7 @@ const StateInput = ( { autoComplete = 'off', value = '', required = false, + errorId = '', }: StateInputWithStatesProps ): JSX.Element => { const countryStates = states[ country ]; const options = useMemo( @@ -102,6 +103,7 @@ const StateInput = ( { 'Please select a state.', 'woo-gutenberg-products-block' ) } + errorId={ errorId } required={ required } autoComplete={ autoComplete } /> diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/attributes.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/attributes.tsx index 42dfb4696fa..9c21e4de517 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/attributes.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/attributes.tsx @@ -44,4 +44,8 @@ export default { remove: true, }, }, + shippingCostRequiresAddress: { + type: 'boolean', + default: false, + }, }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.json b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.json index b080adafec9..b3255985c2d 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.json +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.json @@ -19,6 +19,10 @@ "remove": true, "move": true } + }, + "shippingCostRequiresAddress": { + "type": "boolean", + "default": false } }, "parent": [ "woocommerce/checkout-fields-block" ], diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.tsx index e59b5e91725..d6bbbfac0de 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.tsx @@ -17,6 +17,7 @@ import './style.scss'; import { RatePrice, getLocalPickupPrices, getShippingPrices } from './shared'; import type { minMaxPrices } from './shared'; import { defaultLocalPickupText, defaultShippingText } from './constants'; +import { shippingAddressHasValidationErrors } from '../../../../data/cart/utils'; const LocalPickupSelector = ( { checked, @@ -71,15 +72,19 @@ const ShippingSelector = ( { showPrice, showIcon, toggleText, + shippingCostRequiresAddress = false, }: { checked: string; rate: minMaxPrices; showPrice: boolean; showIcon: boolean; + shippingCostRequiresAddress: boolean; toggleText: string; } ) => { + const rateShouldBeHidden = + shippingCostRequiresAddress && shippingAddressHasValidationErrors(); const Price = - rate.min === undefined ? ( + rate.min === undefined || rateShouldBeHidden ? ( { __( 'calculated with an address', @@ -122,11 +127,13 @@ const Block = ( { showIcon, localPickupText, shippingText, + shippingCostRequiresAddress = false, }: { checked: string; onChange: ( value: string ) => void; showPrice: boolean; showIcon: boolean; + shippingCostRequiresAddress: boolean; localPickupText: string; shippingText: string; } ): JSX.Element | null => { @@ -145,6 +152,7 @@ const Block = ( { rate={ getShippingPrices( shippingRates[ 0 ]?.shipping_rates ) } showPrice={ showPrice } showIcon={ showIcon } + shippingCostRequiresAddress={ shippingCostRequiresAddress } toggleText={ shippingText || defaultShippingText } /> ) => void; } ): JSX.Element | null => { + const toggleAttribute = ( key: keyof Attributes ): void => { + const newAttributes = {} as Partial< Attributes >; + newAttributes[ key ] = ! ( attributes[ key ] as boolean ); + setAttributes( newAttributes ); + }; + const { setPrefersCollection } = useDispatch( CHECKOUT_STORE_KEY ); const { prefersCollection } = useSelect( ( select ) => { const checkoutStore = select( CHECKOUT_STORE_KEY ); @@ -210,6 +221,30 @@ export const Edit = ( { ) } > + + { + updateAttributeInSiblingBlock( + clientId, + 'shippingCostRequiresAddress', + selected, + 'woocommerce/checkout-shipping-methods-block' + ); + + toggleAttribute( 'shippingCostRequiresAddress' ); + } } + /> + diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/frontend.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/frontend.tsx index 74ae2cc83f8..86f5154a87b 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/frontend.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/frontend.tsx @@ -25,10 +25,12 @@ const FrontendBlock = ( { showIcon, shippingText, localPickupText, + shippingCostRequiresAddress, }: { title: string; description: string; showStepNumber: boolean; + shippingCostRequiresAddress: boolean; children: JSX.Element; className?: string; showPrice: boolean; @@ -90,6 +92,7 @@ const FrontendBlock = ( { showIcon={ showIcon } localPickupText={ localPickupText } shippingText={ shippingText } + shippingCostRequiresAddress={ shippingCostRequiresAddress } /> { children } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/attributes.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/attributes.tsx index b36f320660d..17cb4f6bf2f 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/attributes.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/attributes.tsx @@ -24,4 +24,8 @@ export default { remove: true, }, }, + shippingCostRequiresAddress: { + type: 'boolean', + default: false, + }, }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.json b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.json index df53c0117c8..b4c5dc5192f 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.json +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.json @@ -19,6 +19,10 @@ "remove": true, "move": true } + }, + "shippingCostRequiresAddress": { + "type": "boolean", + "default": false } }, "parent": [ "woocommerce/checkout-fields-block" ], diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx index fb241284d8a..89e67fb766e 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx @@ -21,11 +21,14 @@ import type { CartShippingPackageShippingRate, } from '@woocommerce/types'; import type { ReactElement } from 'react'; +import { useSelect } from '@wordpress/data'; +import { CART_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies */ import './style.scss'; +import { shippingAddressHasValidationErrors } from '../../../../data/cart/utils'; /** * Renders a shipping rate control option. @@ -52,7 +55,10 @@ const renderShippingRatesControlOption = ( }; }; -const Block = ( { noShippingPlaceholder = null } ): ReactElement | null => { +const Block = ( { + noShippingPlaceholder = null, + shippingCostRequiresAddress = false, +} ): ReactElement | null => { const { isEditor } = useEditorContext(); const { @@ -63,6 +69,10 @@ const Block = ( { noShippingPlaceholder = null } ): ReactElement | null => { isCollectable, } = useShippingData(); + const shippingAddressPushed = useSelect( ( select ) => { + return select( CART_STORE_KEY ).getFullShippingAddressPushed(); + } ); + const filteredShippingRates = isCollectable ? shippingRates.map( ( shippingRatesPackage ) => { return { @@ -81,13 +91,15 @@ const Block = ( { noShippingPlaceholder = null } ): ReactElement | null => { return null; } + const shippingAddressIsComplete = ! shippingAddressHasValidationErrors(); + const shippingRatesPackageCount = getShippingRatesPackageCount( shippingRates ); if ( - ! isEditor && - ! hasCalculatedShipping && - ! shippingRatesPackageCount + ( ! hasCalculatedShipping && ! shippingRatesPackageCount ) || + ( shippingCostRequiresAddress && + ( ! shippingAddressPushed || ! shippingAddressIsComplete ) ) ) { return (

diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/edit.tsx index ac1d3bbd9bf..0edb15d412a 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/edit.tsx @@ -4,12 +4,14 @@ import classnames from 'classnames'; import { __ } from '@wordpress/i18n'; import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; -import { PanelBody, ExternalLink } from '@wordpress/components'; +import { PanelBody, ExternalLink, ToggleControl } from '@wordpress/components'; import { ADMIN_URL, getSetting } from '@woocommerce/settings'; import ExternalLinkCard from '@woocommerce/editor-components/external-link-card'; import { innerBlockAreas } from '@woocommerce/blocks-checkout'; import { useCheckoutAddress } from '@woocommerce/base-context/hooks'; import Noninteractive from '@woocommerce/base-components/noninteractive'; +import { Attributes } from '@woocommerce/blocks/checkout/types'; +import { updateAttributeInSiblingBlock } from '@woocommerce/utils'; /** * Internal dependencies @@ -32,12 +34,15 @@ type shippingAdminLink = { export const Edit = ( { attributes, setAttributes, + clientId, }: { + clientId: string; attributes: { title: string; description: string; showStepNumber: boolean; className: string; + shippingCostRequiresAddress: boolean; }; setAttributes: ( attributes: Record< string, unknown > ) => void; } ): JSX.Element | null => { @@ -54,6 +59,12 @@ export const Edit = ( { return null; } + const toggleAttribute = ( key: keyof Attributes ): void => { + const newAttributes = {} as Partial< Attributes >; + newAttributes[ key ] = ! ( attributes[ key ] as boolean ); + setAttributes( newAttributes ); + }; + return ( + + { + updateAttributeInSiblingBlock( + clientId, + 'shippingCostRequiresAddress', + selected, + 'woocommerce/checkout-shipping-method-block' + ); + toggleAttribute( 'shippingCostRequiresAddress' ); + } } + /> + { globalShippingMethods.length > 0 && ( - } /> + } + shippingCostRequiresAddress={ + attributes.shippingCostRequiresAddress + } + /> diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/frontend.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/frontend.tsx index dd67d39fba5..9fced3fa2e7 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/frontend.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/frontend.tsx @@ -20,6 +20,7 @@ const FrontendBlock = ( { showStepNumber, children, className, + shippingCostRequiresAddress = false, }: { title: string; description: string; @@ -31,6 +32,7 @@ const FrontendBlock = ( { showStepNumber: boolean; children: JSX.Element; className?: string; + shippingCostRequiresAddress: boolean; } ) => { const checkoutIsProcessing = useSelect( ( select ) => select( CHECKOUT_STORE_KEY ).isProcessing() @@ -53,7 +55,9 @@ const FrontendBlock = ( { description={ description } showStepNumber={ showStepNumber } > - + { children } ); diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/action-types.ts b/plugins/woocommerce-blocks/assets/js/data/cart/action-types.ts index ce5ffa64032..6ee20f94551 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/action-types.ts +++ b/plugins/woocommerce-blocks/assets/js/data/cart/action-types.ts @@ -1,5 +1,6 @@ export const ACTION_TYPES = { SET_CART_DATA: 'SET_CART_DATA', + SET_FULL_SHIPPING_ADDRESS_PUSHED: 'SET_FULL_SHIPPING_ADDRESS_PUSHED', SET_ERROR_DATA: 'SET_ERROR_DATA', APPLYING_COUPON: 'APPLYING_COUPON', REMOVING_COUPON: 'REMOVING_COUPON', diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/actions.ts b/plugins/woocommerce-blocks/assets/js/data/cart/actions.ts index c7cc97261a1..393680c7cbf 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/actions.ts +++ b/plugins/woocommerce-blocks/assets/js/data/cart/actions.ts @@ -417,7 +417,14 @@ export const selectShippingRate = }, cache: 'no-store', } ); - dispatch.receiveCart( response ); + // Remove shipping and billing address from the response, so we don't overwrite what the shopper is + // entering in the form if rates suddenly appear mid-edit. + const { + shipping_address: shippingAddress, + billing_address: billingAddress, + ...rest + } = response; + dispatch.receiveCart( rest ); return response as CartResponse; } catch ( error ) { dispatch.receiveError( error ); @@ -474,6 +481,13 @@ export const updateCustomerData = } }; +export const setFullShippingAddressPushed = ( + fullShippingAddressPushed: boolean +) => ( { + type: types.SET_FULL_SHIPPING_ADDRESS_PUSHED, + fullShippingAddressPushed, +} ); + type Actions = | typeof addItemToCart | typeof applyCoupon @@ -494,6 +508,7 @@ type Actions = | typeof setShippingAddress | typeof shippingRatesBeingSelected | typeof updateCustomerData + | typeof setFullShippingAddressPushed | typeof updatingCustomerData; export type CartAction = ReturnOrGeneratorYieldUnion< Actions | Thunks >; diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/default-state.ts b/plugins/woocommerce-blocks/assets/js/data/cart/default-state.ts index 8ce98387a77..112f5eeb94f 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/default-state.ts +++ b/plugins/woocommerce-blocks/assets/js/data/cart/default-state.ts @@ -100,6 +100,7 @@ export const defaultCartState: CartState = { applyingCoupon: '', removingCoupon: '', isCartDataStale: false, + fullShippingAddressPushed: false, }, errors: EMPTY_CART_ERRORS, }; diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/push-changes.ts b/plugins/woocommerce-blocks/assets/js/data/cart/push-changes.ts index 8410a3a0a70..2fd720de047 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/push-changes.ts +++ b/plugins/woocommerce-blocks/assets/js/data/cart/push-changes.ts @@ -17,6 +17,7 @@ import isShallowEqual from '@wordpress/is-shallow-equal'; import { STORE_KEY } from './constants'; import { VALIDATION_STORE_KEY } from '../validation'; import { processErrorResponse } from '../utils'; +import { shippingAddressHasValidationErrors } from './utils'; type CustomerData = { billingAddress: CartBillingAddress; @@ -192,6 +193,11 @@ const updateCustomerData = debounce( (): void => { ) as BaseAddressKey[] ), ]; } + } ) + .finally( () => { + if ( ! shippingAddressHasValidationErrors() ) { + dispatch( STORE_KEY ).setFullShippingAddressPushed( true ); + } } ); } }, 1000 ); diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/reducers.ts b/plugins/woocommerce-blocks/assets/js/data/cart/reducers.ts index 67a9dcfe471..3ab809ec8c6 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/reducers.ts +++ b/plugins/woocommerce-blocks/assets/js/data/cart/reducers.ts @@ -48,6 +48,15 @@ const reducer: Reducer< CartState > = ( action: Partial< CartAction > ) => { switch ( action.type ) { + case types.SET_FULL_SHIPPING_ADDRESS_PUSHED: + state = { + ...state, + metaData: { + ...state.metaData, + fullShippingAddressPushed: action.fullShippingAddressPushed, + }, + }; + break; case types.SET_ERROR_DATA: if ( action.error ) { state = { diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/resolvers.ts b/plugins/woocommerce-blocks/assets/js/data/cart/resolvers.ts index 71b4c386d18..080b021d0db 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/resolvers.ts +++ b/plugins/woocommerce-blocks/assets/js/data/cart/resolvers.ts @@ -9,6 +9,7 @@ import { CartResponse } from '@woocommerce/types'; */ import { CART_API_ERROR } from './constants'; import type { CartDispatchFromMap, CartResolveSelectFromMap } from './index'; +import { shippingAddressHasValidationErrors } from './utils'; /** * Resolver for retrieving all cart data. @@ -27,6 +28,10 @@ export const getCartData = receiveError( CART_API_ERROR ); return; } + + if ( ! shippingAddressHasValidationErrors() ) { + dispatch.setFullShippingAddressPushed( true ); + } receiveCart( cartData ); }; diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/selectors.ts b/plugins/woocommerce-blocks/assets/js/data/cart/selectors.ts index f4644a4b2d2..59041eb804e 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/selectors.ts +++ b/plugins/woocommerce-blocks/assets/js/data/cart/selectors.ts @@ -222,3 +222,10 @@ export const getItemsPendingQuantityUpdate = ( state: CartState ): string[] => { export const getItemsPendingDelete = ( state: CartState ): string[] => { return state.cartItemsPendingDelete; }; + +/** + * Whether the address has changes that have not been synced with the server. + */ +export const getFullShippingAddressPushed = ( state: CartState ): boolean => { + return state.metaData.fullShippingAddressPushed; +}; diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/test/push-changes.ts b/plugins/woocommerce-blocks/assets/js/data/cart/test/push-changes.ts index fd4ee9b827f..8bf726c2aa5 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/test/push-changes.ts +++ b/plugins/woocommerce-blocks/assets/js/data/cart/test/push-changes.ts @@ -64,6 +64,7 @@ jest.mock( '../utils', () => ( { // need to update payment methods, they are not relevant to the tests in this file. jest.mock( '../update-payment-methods', () => ( { debouncedUpdatePaymentMethods: jest.fn(), + updatePaymentMethods: jest.fn(), } ) ); describe( 'pushChanges', () => { diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/utils.ts b/plugins/woocommerce-blocks/assets/js/data/cart/utils.ts index 1130198a750..19afead5670 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/utils.ts +++ b/plugins/woocommerce-blocks/assets/js/data/cart/utils.ts @@ -3,9 +3,38 @@ */ import { camelCase, mapKeys } from 'lodash'; import { Cart, CartResponse } from '@woocommerce/types'; +import { select } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { STORE_KEY as VALIDATION_STORE_KEY } from '../validation/constants'; export const mapCartResponseToCart = ( responseCart: CartResponse ): Cart => { return mapKeys( responseCart, ( _, key ) => camelCase( key ) ) as unknown as Cart; }; + +export const shippingAddressHasValidationErrors = () => { + const validationStore = select( VALIDATION_STORE_KEY ); + // Check if the shipping address form has validation errors - if not then we know the full required + // address has been pushed to the server. + const stateValidationErrors = + validationStore.getValidationError( 'shipping_state' ); + const address1ValidationErrors = + validationStore.getValidationError( 'shipping_address_1' ); + const countryValidationErrors = + validationStore.getValidationError( 'shipping_country' ); + const postcodeValidationErrors = + validationStore.getValidationError( 'shipping_postcode' ); + const cityValidationErrors = + validationStore.getValidationError( 'shipping_city' ); + return [ + cityValidationErrors, + stateValidationErrors, + address1ValidationErrors, + countryValidationErrors, + postcodeValidationErrors, + ].some( ( entry ) => typeof entry !== 'undefined' ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/types/type-defs/cart.ts b/plugins/woocommerce-blocks/assets/js/types/type-defs/cart.ts index d0ecf0095b8..b1fd1cf7b4f 100644 --- a/plugins/woocommerce-blocks/assets/js/types/type-defs/cart.ts +++ b/plugins/woocommerce-blocks/assets/js/types/type-defs/cart.ts @@ -210,6 +210,8 @@ export interface CartMeta { isCartDataStale: boolean; applyingCoupon: string; removingCoupon: string; + /* Whether the full address has been previously pushed to the server */ + fullShippingAddressPushed: boolean; } export interface ExtensionCartUpdateArgs { data: Record< string, unknown >; diff --git a/plugins/woocommerce-blocks/assets/js/utils/attributes.ts b/plugins/woocommerce-blocks/assets/js/utils/attributes.ts index 3470c7a36b2..9a52106f26f 100644 --- a/plugins/woocommerce-blocks/assets/js/utils/attributes.ts +++ b/plugins/woocommerce-blocks/assets/js/utils/attributes.ts @@ -10,6 +10,7 @@ import { AttributeWithTerms, isAttributeTerm, } from '@woocommerce/types'; +import { dispatch, select } from '@wordpress/data'; const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] ); @@ -108,3 +109,35 @@ export const getTaxonomyFromAttributeId = ( attributeId: number ) => { const attribute = getAttributeFromID( attributeId ); return attribute ? attribute.taxonomy : null; }; + +/** + * Updates an attribute in a sibling block. Useful if two settings control the same attribute, but you don't want to + * have this attribute exist on a parent block. + */ +export const updateAttributeInSiblingBlock = ( + clientId: string, + attribute: string, + newValue: unknown, + siblingBlockName: string +) => { + const store = select( 'core/block-editor' ); + const actions = dispatch( 'core/block-editor' ); + const parentBlocks = store.getBlockParents( clientId ); + + let shippingMethodsBlockClientId = ''; + + // Loop through parent block's children until we find woocommerce/checkout-shipping-methods-block. + // Also set this attribute in the woocommerce/checkout-shipping-methods-block. + parentBlocks.forEach( ( parent ) => { + const childBlock = store + .getBlock( parent ) + .innerBlocks.find( ( child ) => child.name === siblingBlockName ); + if ( ! childBlock ) { + return; + } + shippingMethodsBlockClientId = childBlock.clientId; + } ); + actions.updateBlockAttributes( shippingMethodsBlockClientId, { + [ attribute ]: newValue, + } ); +}; diff --git a/plugins/woocommerce-blocks/src/Shipping/ShippingController.php b/plugins/woocommerce-blocks/src/Shipping/ShippingController.php index ce88e97fa98..d0bdae7e910 100644 --- a/plugins/woocommerce-blocks/src/Shipping/ShippingController.php +++ b/plugins/woocommerce-blocks/src/Shipping/ShippingController.php @@ -3,6 +3,8 @@ namespace Automattic\WooCommerce\Blocks\Shipping; use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; +use Automattic\WooCommerce\Blocks\Tests\BlockTypes\Cart; +use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils; use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils; use Automattic\WooCommerce\Utilities\ArrayUtil; @@ -51,6 +53,7 @@ class ShippingController { ); } $this->asset_data_registry->add( 'collectableMethodIds', array( 'Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils', 'get_local_pickup_method_ids' ), true ); + $this->asset_data_registry->add( 'shippingCostRequiresAddress', get_option( 'woocommerce_shipping_cost_requires_address', false ) === 'yes' ); add_action( 'rest_api_init', [ $this, 'register_settings' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'hydrate_client_settings' ] ); @@ -60,6 +63,105 @@ class ShippingController { add_filter( 'woocommerce_shipping_packages', array( $this, 'filter_shipping_packages' ) ); add_filter( 'pre_update_option_woocommerce_pickup_location_settings', array( $this, 'flush_cache' ) ); add_filter( 'pre_update_option_pickup_location_pickup_locations', array( $this, 'flush_cache' ) ); + add_filter( 'woocommerce_shipping_settings', array( $this, 'remove_shipping_settings' ) ); + add_filter( 'wc_shipping_enabled', array( $this, 'force_shipping_enabled' ), 100, 1 ); + + // This is required to short circuit `show_shipping` from class-wc-cart.php - without it, that function + // returns based on the option's value in the DB and we can't override it any other way. + add_filter( 'option_woocommerce_shipping_cost_requires_address', array( $this, 'override_cost_requires_address_option' ) ); + } + + /** + * Overrides the option to force shipping calculations NOT to wait until an address is entered, but only if the + * Checkout page contains the Checkout Block. + * + * @param boolean $value Whether shipping cost calculation requires address to be entered. + * @return boolean Whether shipping cost calculation should require an address to be entered before calculating. + */ + public function override_cost_requires_address_option( $value ) { + if ( CartCheckoutUtils::is_checkout_block_default() ) { + return 'no'; + } + return $value; + } + + /** + * Force shipping to be enabled if the Checkout block is in use on the Checkout page. + * + * @param boolean $enabled Whether shipping is currently enabled. + * @return boolean Whether shipping should continue to be enabled/disabled. + */ + public function force_shipping_enabled( $enabled ) { + if ( CartCheckoutUtils::is_checkout_block_default() ) { + return true; + } + return $enabled; + } + + /** + * If the Checkout block Remove shipping settings from WC Core's admin panels that are now block settings. + * + * @param array $settings The default WC shipping settings. + * @return array|mixed The filtered settings with relevant items removed. + */ + public function remove_shipping_settings( $settings ) { + + // Do not add the "Hide shipping costs until an address is entered" setting if the Checkout block is not used on the WC checkout page. + if ( CartCheckoutUtils::is_checkout_block_default() ) { + $settings = array_filter( + $settings, + function( $setting ) { + return ! in_array( + $setting['id'], + array( + 'woocommerce_shipping_cost_requires_address', + ), + true + ); + } + ); + } + + // Do not add the shipping calculator setting if the Cart block is not used on the WC cart page. + if ( CartCheckoutUtils::is_cart_block_default() ) { + + // If the Cart is default, but not the checkout, we should ensure the 'Calculations' title is added to the + // `woocommerce_shipping_cost_requires_address` options group, since it is attached to the + // `woocommerce_enable_shipping_calc` option that we're going to remove later. + if ( ! CartCheckoutUtils::is_checkout_block_default() ) { + $calculations_title = ''; + + // Get Calculations title so we can add it to 'Hide shipping costs until an address is entered' option. + foreach ( $settings as $setting ) { + if ( 'woocommerce_enable_shipping_calc' === $setting['id'] ) { + $calculations_title = $setting['title']; + break; + } + } + + // Add Calculations title to 'Hide shipping costs until an address is entered' option. + foreach ( $settings as $index => $setting ) { + if ( 'woocommerce_shipping_cost_requires_address' === $setting['id'] ) { + $settings[ $index ]['title'] = $calculations_title; + $settings[ $index ]['checkboxgroup'] = 'start'; + break; + } + } + } + $settings = array_filter( + $settings, + function( $setting ) { + return ! in_array( + $setting['id'], + array( + 'woocommerce_enable_shipping_calc', + ), + true + ); + } + ); + } + return $settings; } /** @@ -223,8 +325,7 @@ class ShippingController { * Registers the Local Pickup shipping method used by the Checkout Block. */ public function register_local_pickup() { - $checkout_page_id = wc_get_page_id( 'checkout' ); - if ( $checkout_page_id && has_block( 'woocommerce/checkout', $checkout_page_id ) ) { + if ( CartCheckoutUtils::is_checkout_block_default() ) { wc()->shipping->register_shipping_method( new PickupLocation() ); } } diff --git a/plugins/woocommerce-blocks/src/Utils/CartCheckoutUtils.php b/plugins/woocommerce-blocks/src/Utils/CartCheckoutUtils.php new file mode 100644 index 00000000000..8252ca0fcb3 --- /dev/null +++ b/plugins/woocommerce-blocks/src/Utils/CartCheckoutUtils.php @@ -0,0 +1,28 @@ +\n

\n
\n
\n\n\n\n
\n\n\n\n
\n\n\n\n
\n\n\n\n
\n\n\n\n
\n\n\n\n
\n\n\n\n
\n\n\n\n
\n
\n\n\n\n
\n
\n
\n
\n"} +{"title":"Checkout Block","pageContent":"\n
\n
\n
\n\n\n\n
\n\n\n\n
\n\n\n\n
\n\n\n\n
\n\n\n\n
\n\n\n\n
\n\n\n\n
\n\n\n\n
\n
\n\n\n\n
\n
\n
\n
\n"} diff --git a/plugins/woocommerce-blocks/tests/e2e/specs/backend/checkout.test.js b/plugins/woocommerce-blocks/tests/e2e/specs/backend/checkout.test.js index bc905565682..cf661d8e429 100644 --- a/plugins/woocommerce-blocks/tests/e2e/specs/backend/checkout.test.js +++ b/plugins/woocommerce-blocks/tests/e2e/specs/backend/checkout.test.js @@ -22,6 +22,7 @@ import { openWidgetEditor, closeModalIfExists, } from '../../utils.js'; +import { merchant as merchantUtils } from '../../../utils/merchant'; const block = { name: 'Checkout', @@ -64,6 +65,79 @@ describe( `${ block.name } Block`, () => { await selectBlockByName( block.slug ); } ); + it( 'can toggle "hide shipping costs until an address is entered"', async () => { + await selectBlockByName( + 'woocommerce/checkout-shipping-methods-block' + ); + const toggleLabel = await findLabelWithText( + 'Hide shipping costs until an address is entered' + ); + await toggleLabel.click(); + const shippingOptionsRequireAddressText = await page.$x( + '//p[contains(text(), "Shipping options will be displayed here after entering your full shipping address.")]' + ); + await expect( shippingOptionsRequireAddressText ).toHaveLength( + 1 + ); + + await toggleLabel.click(); + await expect( page ).toMatchElement( + '.wc-block-components-shipping-rates-control' + ); + } ); + + it( 'toggles the same setting in shipping method and shipping methods blocks', async () => { + await merchantUtils.goToLocalPickupSettingsPage(); + await merchantUtils.enableLocalPickup(); + await merchantUtils.saveLocalPickupSettingsPageWithRefresh(); + + await visitBlockPage( `${ block.name } Block` ); + await expect( page ).toClick( + '.wc-block-checkout__shipping-method button', + { text: 'Shipping' } + ); + await openDocumentSettingsSidebar(); + const toggleLabel = await findLabelWithText( + 'Hide shipping costs until an address is entered' + ); + await toggleLabel.click(); + const [ label ] = await page.$x( + '//label[contains(., "Hide shipping costs until an address is entered")]' + ); + const shippingMethodForValue = await page.evaluate( + ( passedLabel ) => passedLabel.getAttribute( 'for' ), + label + ); + const shippingMethodSettingIsChecked = await page.evaluate( + ( passedShippingMethodForValue ) => + document.getElementById( passedShippingMethodForValue ) + .checked, + shippingMethodForValue + ); + await expect( shippingMethodSettingIsChecked ).toBe( true ); + await selectBlockByName( + 'woocommerce/checkout-shipping-methods-block' + ); + const [ shippingMethodsLabel ] = await page.$x( + '//label[contains(., "Hide shipping costs until an address is entered")]' + ); + const shippingMethodsLabelForValue = await page.evaluate( + ( passedShippingMethodsLabel ) => + passedShippingMethodsLabel.getAttribute( 'for' ), + shippingMethodsLabel + ); + const shippingMethodLabelIsChecked = await page.evaluate( + ( passedShippingMethodsLabelForValue ) => + document.getElementById( + passedShippingMethodsLabelForValue + ).checked, + shippingMethodsLabelForValue + ); + expect( shippingMethodSettingIsChecked ).toBe( + shippingMethodLabelIsChecked + ); + } ); + it( 'can enable dark mode inputs', async () => { const toggleLabel = await findLabelWithText( 'Dark mode inputs' diff --git a/plugins/woocommerce-blocks/tests/e2e/specs/merchant/checkout-terms.test.js b/plugins/woocommerce-blocks/tests/e2e/specs/merchant/checkout-terms.test.js index c27733f85cc..70824729214 100644 --- a/plugins/woocommerce-blocks/tests/e2e/specs/merchant/checkout-terms.test.js +++ b/plugins/woocommerce-blocks/tests/e2e/specs/merchant/checkout-terms.test.js @@ -83,6 +83,11 @@ describe( 'Merchant → Checkout → Can adjust T&S and Privacy Policy options', await shopper.block.goToCheckout(); await shopper.block.fillBillingDetails( BILLING_DETAILS ); + // Wait for the "Place Order" button to avoid flakey tests. + await page.waitForSelector( + '.wc-block-components-checkout-place-order-button:not([disabled])' + ); + // Placing an order now, must lead to an error. await page.click( '.wc-block-components-checkout-place-order-button' ); diff --git a/plugins/woocommerce-blocks/tests/e2e/specs/merchant/local-pickup.test.ts b/plugins/woocommerce-blocks/tests/e2e/specs/merchant/local-pickup.test.ts index 6c34d155f08..cc845c53789 100644 --- a/plugins/woocommerce-blocks/tests/e2e/specs/merchant/local-pickup.test.ts +++ b/plugins/woocommerce-blocks/tests/e2e/specs/merchant/local-pickup.test.ts @@ -3,52 +3,17 @@ */ import { switchUserToAdmin, visitAdminPage } from '@wordpress/e2e-test-utils'; import { findLabelWithText } from '@woocommerce/blocks-test-utils'; +import WooCommerceRestApi from '@woocommerce/woocommerce-rest-api'; +import { default as axios } from 'axios'; -const goToSettingsPage = async () => { - await visitAdminPage( - 'admin.php', - 'page=wc-settings&tab=shipping§ion=pickup_location' - ); - await page.waitForSelector( - '#wc-shipping-method-pickup-location-settings-container' - ); -}; - -const saveSettingsPageWithRefresh = async () => { - await expect( page ).toClick( 'button', { - text: 'Save changes', - } ); - await expect( page ).toMatchElement( '.components-snackbar__content', { - text: 'Local Pickup settings have been saved.', - } ); - await goToSettingsPage(); -}; +/** + * Internal dependencies + */ +import { merchant } from '../../../utils'; const setDefaults = async () => { - const enabledLabel = await findLabelWithText( 'Enable local pickup' ); - const enabledChecked = await page.$eval( - '#inspector-checkbox-control-1', - ( el ) => ( el as HTMLInputElement ).checked - ); - if ( enabledChecked ) { - await enabledLabel.click(); - } - - await expect( page ).toFill( - 'input[name="local_pickup_title"]', - 'Local Pickup' - ); - - const costLabel = await findLabelWithText( - 'Add a price for customers who choose local pickup' - ); - const costChecked = await page.$eval( - '#inspector-checkbox-control-1', - ( el ) => ( el as HTMLInputElement ).checked - ); - if ( costChecked ) { - await costLabel.click(); - } + await merchant.enableLocalPickup(); + await merchant.removeCostForLocalPickup(); }; const clearLocations = async () => { @@ -66,32 +31,123 @@ const clearLocations = async () => { } }; +/** + * Sets the WC Cart and Checkout page IDs to the IDs of the pages with the given slugs. + */ +const setCartCheckoutPages = async ( { + cartSlug, + checkoutSlug, +}: { + cartSlug: string; + checkoutSlug: string; +} ) => { + const WPAPI = `${ process.env.WORDPRESS_BASE_URL }/wp-json/wp/v2/pages`; + const response = await axios.get( `${ WPAPI }?per_page=100` ); + const pages = response.data; + const cartBlock = pages.find( ( page ) => page.slug === cartSlug ); + const checkoutBlock = pages.find( ( page ) => page.slug === checkoutSlug ); + const WooCommerce = new WooCommerceRestApi( { + url: `${ process.env.WORDPRESS_BASE_URL }/`, + consumerKey: 'consumer_key', // Your consumer key + consumerSecret: 'consumer_secret', // Your consumer secret + version: 'wc/v3', + axiosConfig: { + auth: { + username: process.env.WORDPRESS_LOGIN, + password: process.env.WORDPRESS_PASSWORD, + }, + }, + } ); + const fixture = [ + { + id: 'woocommerce_cart_page_id', + value: cartBlock.id.toString() || '', + }, + { + id: 'woocommerce_checkout_page_id', + value: checkoutBlock.id.toString() || '', + }, + ]; + + await WooCommerce.post( 'settings/advanced/batch', { + update: fixture, + } ); +}; describe( `Local Pickup Settings`, () => { beforeAll( async () => { await switchUserToAdmin(); - await goToSettingsPage(); + await merchant.goToLocalPickupSettingsPage(); await setDefaults(); await clearLocations(); - await saveSettingsPageWithRefresh(); + await merchant.saveLocalPickupSettingsPageWithRefresh(); } ); afterAll( async () => { await switchUserToAdmin(); - await goToSettingsPage(); + await merchant.goToLocalPickupSettingsPage(); await setDefaults(); await clearLocations(); - await saveSettingsPageWithRefresh(); + await merchant.saveLocalPickupSettingsPageWithRefresh(); } ); beforeEach( async () => { await switchUserToAdmin(); - await goToSettingsPage(); + await merchant.goToLocalPickupSettingsPage(); } ); it( 'renders without crashing', async () => { await expect( page ).toMatchElement( '#local-pickup-settings' ); } ); + describe( 'Core Settings', () => { + afterAll( async () => { + await setCartCheckoutPages( { + cartSlug: 'cart-block', + checkoutSlug: 'checkout-block', + } ); + } ); + it( 'hides the correct shipping options if Checkout block is the default', async () => { + await visitAdminPage( + 'admin.php', + 'page=wc-settings&tab=shipping§ion=options' + ); + const hideShippingLabel = await findLabelWithText( + 'Hide shipping costs until an address is entered' + ); + expect( hideShippingLabel ).toBeUndefined(); + + const shippingCalculatorLabel = await findLabelWithText( + 'Enable the shipping calculator on the cart page' + ); + expect( shippingCalculatorLabel ).toBeUndefined(); + } ); + + it( 'does not hide the relevant setting if Cart or Checkout block is not the default', async () => { + await setCartCheckoutPages( { + cartSlug: 'cart', + checkoutSlug: 'checkout', + } ); + + await visitAdminPage( + 'admin.php', + 'page=wc-settings&tab=advanced' + ); + await visitAdminPage( + 'admin.php', + 'page=wc-settings&tab=shipping§ion=options' + ); + const hideShippingLabel = await page.$x( + '//label[contains(., "Hide shipping costs until an address is entered")]' + ); + await expect( hideShippingLabel ).toHaveLength( 1 ); + + const shippingCalculatorLabel = await page.$x( + '//label[contains(., "Enable the shipping calculator on the cart page")]' + ); + await expect( shippingCalculatorLabel ).toHaveLength( 1 ); + } ); + } ); + describe( 'Global Settings', () => { it( 'allows toggling of enabled on', async () => { const initialChecked = await page.$eval( @@ -102,7 +158,7 @@ describe( `Local Pickup Settings`, () => { 'Enable local pickup' ); await toggleLabel.click(); - await saveSettingsPageWithRefresh(); + await merchant.saveLocalPickupSettingsPageWithRefresh(); expect( await page.$eval( @@ -118,7 +174,7 @@ describe( `Local Pickup Settings`, () => { 'Local Pickup Test' ); - await saveSettingsPageWithRefresh(); + await merchant.saveLocalPickupSettingsPageWithRefresh(); expect( await page.$eval( @@ -158,7 +214,7 @@ describe( `Local Pickup Settings`, () => { 'none' ); - await saveSettingsPageWithRefresh(); + await merchant.saveLocalPickupSettingsPageWithRefresh(); const refreshChecked = await page.$eval( '#inspector-checkbox-control-1', @@ -218,7 +274,7 @@ describe( `Local Pickup Settings`, () => { text: 'Done', } ); - await saveSettingsPageWithRefresh(); + await merchant.saveLocalPickupSettingsPageWithRefresh(); await expect( page ).toMatchElement( '.pickup-locations tbody tr td', @@ -247,7 +303,7 @@ describe( `Local Pickup Settings`, () => { text: 'Delete location', } ); - await saveSettingsPageWithRefresh(); + await merchant.saveLocalPickupSettingsPageWithRefresh(); await expect( page ).not.toMatchElement( '.pickup-locations tbody tr td', { diff --git a/plugins/woocommerce-blocks/tests/e2e/specs/shopper/cart-checkout/checkout.test.js b/plugins/woocommerce-blocks/tests/e2e/specs/shopper/cart-checkout/checkout.test.js index 40aeed75443..8fbb77c9c40 100644 --- a/plugins/woocommerce-blocks/tests/e2e/specs/shopper/cart-checkout/checkout.test.js +++ b/plugins/woocommerce-blocks/tests/e2e/specs/shopper/cart-checkout/checkout.test.js @@ -29,7 +29,7 @@ import { SIMPLE_VIRTUAL_PRODUCT_NAME, BASE_URL, } from '../../../../utils'; - +import { merchant as merchantUtils } from '../../../../utils/merchant'; import { createCoupon } from '../../../utils'; let coupon; @@ -297,6 +297,44 @@ describe( 'Shopper → Checkout', () => { const NORMAL_SHIPPING_NAME = 'Normal Shipping'; const NORMAL_SHIPPING_PRICE = '$20.00'; + afterAll( async () => { + await merchant.login(); + await visitBlockPage( 'Checkout Block' ); + await openDocumentSettingsSidebar(); + await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await selectBlockByName( + 'woocommerce/checkout-shipping-methods-block' + ); + const [ label ] = await page.$x( + '//label[contains(., "Hide shipping costs until an address is entered")]' + ); + const shippingMethodForValue = await page.evaluate( + ( passedLabel ) => passedLabel.getAttribute( 'for' ), + label + ); + let shippingMethodSettingIsChecked = await page.evaluate( + ( passedShippingMethodForValue ) => + document.getElementById( passedShippingMethodForValue ) + .checked, + shippingMethodForValue + ); + if ( ! shippingMethodSettingIsChecked ) { + await setCheckbox( + await getToggleIdByLabel( + 'Hide shipping costs until an address is entered' + ) + ); + } + shippingMethodSettingIsChecked = await page.evaluate( + ( passedShippingMethodForValue ) => + document.getElementById( passedShippingMethodForValue ) + .checked, + shippingMethodForValue + ); + + await merchantUtils.disableLocalPickup(); + } ); + it( 'User can choose free shipping', async () => { await shopper.block.goToShop(); await shopper.addToCartFromShopPage( SIMPLE_PHYSICAL_PRODUCT_NAME ); @@ -326,11 +364,175 @@ describe( 'Shopper → Checkout', () => { await expect( page ).toMatch( 'Order received' ); await expect( page ).toMatch( NORMAL_SHIPPING_NAME ); } ); + + it( 'User sees the correct shipping options based on block settings', async () => { + await preventCompatibilityNotice(); + await merchant.login(); + await visitBlockPage( 'Checkout Block' ); + await openDocumentSettingsSidebar(); + await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await selectBlockByName( + 'woocommerce/checkout-shipping-methods-block' + ); + + const [ label ] = await page.$x( + '//label[contains(., "Hide shipping costs until an address is entered")]' + ); + const shippingMethodForValue = await page.evaluate( + ( passedLabel ) => passedLabel.getAttribute( 'for' ), + label + ); + let shippingMethodSettingIsChecked = await page.evaluate( + ( passedShippingMethodForValue ) => + document.getElementById( passedShippingMethodForValue ) + .checked, + shippingMethodForValue + ); + if ( ! shippingMethodSettingIsChecked ) { + await setCheckbox( + await getToggleIdByLabel( + 'Hide shipping costs until an address is entered' + ) + ); + } + shippingMethodSettingIsChecked = await page.evaluate( + ( passedShippingMethodForValue ) => + document.getElementById( passedShippingMethodForValue ) + .checked, + shippingMethodForValue + ); + await expect( shippingMethodSettingIsChecked ).toBe( true ); + await saveOrPublish(); + await shopper.block.emptyCart(); + // Log out to have a fresh empty cart. + await shopper.logout(); + await shopper.block.goToShop(); + await shopper.addToCartFromShopPage( SIMPLE_PHYSICAL_PRODUCT_NAME ); + await shopper.block.goToCheckout(); + // Expect no shipping options to be shown, but with a friendly message. + const shippingOptionsRequireAddressText = await page.$x( + '//p[contains(text(), "Shipping options will be displayed here after entering your full shipping address.")]' + ); + expect( shippingOptionsRequireAddressText ).toHaveLength( 1 ); + + // Enter the address and expect shipping options to be shown. + await shopper.block.fillInCheckoutWithTestData(); + await expect( page ).toMatchElement( + '.wc-block-components-shipping-rates-control' + ); + + // This sequence will reset the checkout form. + await shopper.login(); + await shopper.logout(); + + await preventCompatibilityNotice(); + await merchant.login(); + await visitBlockPage( 'Checkout Block' ); + await openDocumentSettingsSidebar(); + await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await selectBlockByName( + 'woocommerce/checkout-shipping-methods-block' + ); + + await unsetCheckbox( + await getToggleIdByLabel( + 'Hide shipping costs until an address is entered' + ) + ); + await saveOrPublish(); + await shopper.block.emptyCart(); + + await shopper.block.goToShop(); + await shopper.addToCartFromShopPage( SIMPLE_PHYSICAL_PRODUCT_NAME ); + await shopper.block.goToCheckout(); + + // Expect the shipping options to be displayed without entering an address. + await expect( page ).toMatchElement( + '.wc-block-components-shipping-rates-control' + ); + } ); + + it( 'User does not see shipping rates until full address is entered', async () => { + await preventCompatibilityNotice(); + await merchant.login(); + + await merchantUtils.enableLocalPickup(); + await merchantUtils.addLocalPickupLocation(); + await visitBlockPage( 'Checkout Block' ); + await openDocumentSettingsSidebar(); + await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await selectBlockByName( + 'woocommerce/checkout-shipping-methods-block' + ); + + await setCheckbox( + await getToggleIdByLabel( + 'Hide shipping costs until an address is entered' + ) + ); + await saveOrPublish(); + await shopper.block.emptyCart(); + // Log out to have a fresh empty cart. + await shopper.logout(); + await shopper.block.goToShop(); + await shopper.addToCartFromShopPage( SIMPLE_PHYSICAL_PRODUCT_NAME ); + await shopper.block.goToCheckout(); + + // Expect no shipping options to be shown, but with a friendly message. + const shippingOptionsRequireAddressText = await page.$x( + '//p[contains(text(), "Shipping options will be displayed here after entering your full shipping address.")]' + ); + + expect( shippingOptionsRequireAddressText ).toHaveLength( 1 ); + + await expect( page ).toClick( + '.wc-block-checkout__shipping-method button', + { text: 'Shipping' } + ); + + // Enter the address but not city and expect shipping options not to be shown. + await shopper.block.fillInCheckoutWithTestData( { city: '' } ); + + await expect( page ).not.toMatchElement( + '.wc-block-components-shipping-rates-control' + ); + + // This sequence will reset the checkout form. + await shopper.login(); + await shopper.logout(); + + await preventCompatibilityNotice(); + await merchant.login(); + await visitBlockPage( 'Checkout Block' ); + await openDocumentSettingsSidebar(); + await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await selectBlockByName( + 'woocommerce/checkout-shipping-methods-block' + ); + + await unsetCheckbox( + await getToggleIdByLabel( + 'Hide shipping costs until an address is entered' + ) + ); + await saveOrPublish(); + await shopper.block.emptyCart(); + + await shopper.block.goToShop(); + await shopper.addToCartFromShopPage( SIMPLE_PHYSICAL_PRODUCT_NAME ); + await shopper.block.goToCheckout(); + + // Expect the shipping options to be displayed without entering an address. + await expect( page ).toMatchElement( + '.wc-block-components-shipping-rates-control' + ); + } ); } ); describe( 'Coupons', () => { beforeAll( async () => { coupon = await createCoupon( { usageLimit: 1 } ); + await shopper.logout(); await shopper.login(); } ); diff --git a/plugins/woocommerce-blocks/tests/utils/merchant.js b/plugins/woocommerce-blocks/tests/utils/merchant.js deleted file mode 100644 index 21b86cfaeda..00000000000 --- a/plugins/woocommerce-blocks/tests/utils/merchant.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * External dependencies - */ -import { merchant as wcMerchant } from '@woocommerce/e2e-utils'; -import { visitAdminPage } from '@wordpress/e2e-test-utils'; - -export const merchant = { - ...wcMerchant, - changeLanguage: async ( language ) => { - await visitAdminPage( 'options-general.php' ); - await page.select( 'select#WPLANG', language ); - await page.click( 'input[type="submit"]' ); - await page.waitForSelector( '#setting-error-settings_updated', { - visible: true, - } ); - }, -}; diff --git a/plugins/woocommerce-blocks/tests/utils/merchant.ts b/plugins/woocommerce-blocks/tests/utils/merchant.ts new file mode 100644 index 00000000000..344170a69a4 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/utils/merchant.ts @@ -0,0 +1,110 @@ +/** + * External dependencies + */ +import { merchant as wcMerchant } from '@woocommerce/e2e-utils'; +import { visitAdminPage } from '@wordpress/e2e-test-utils'; +import { findLabelWithText } from '@woocommerce/blocks-test-utils'; + +export const merchant = { + ...wcMerchant, + changeLanguage: async ( language ) => { + await visitAdminPage( 'options-general.php' ); + await page.select( 'select#WPLANG', language ); + await page.click( 'input[type="submit"]' ); + await page.waitForSelector( '#setting-error-settings_updated', { + visible: true, + } ); + }, + goToLocalPickupSettingsPage: async () => { + await visitAdminPage( + 'admin.php', + 'page=wc-settings&tab=shipping§ion=pickup_location' + ); + await page.waitForSelector( + '#wc-shipping-method-pickup-location-settings-container' + ); + }, + saveLocalPickupSettingsPageWithRefresh: async () => { + await expect( page ).toClick( 'button', { + text: 'Save changes', + } ); + await expect( page ).toMatchElement( '.components-snackbar__content', { + text: 'Local Pickup settings have been saved.', + } ); + await merchant.goToLocalPickupSettingsPage(); + }, + enableLocalPickup: async () => { + await merchant.goToLocalPickupSettingsPage(); + const enabledLabel = await findLabelWithText( 'Enable local pickup' ); + const enabledChecked = await page.$eval( + '#inspector-checkbox-control-1', + ( el ) => ( el as HTMLInputElement ).checked + ); + if ( ! enabledChecked ) { + await enabledLabel.click(); + } + + await expect( page ).toFill( + 'input[name="local_pickup_title"]', + 'Local Pickup' + ); + await merchant.saveLocalPickupSettingsPageWithRefresh(); + }, + disableLocalPickup: async () => { + await merchant.goToLocalPickupSettingsPage(); + const enabledLabel = await findLabelWithText( 'Enable local pickup' ); + const enabledChecked = await page.$eval( + '#inspector-checkbox-control-1', + ( el ) => ( el as HTMLInputElement ).checked + ); + if ( enabledChecked ) { + await enabledLabel.click(); + } + await merchant.saveLocalPickupSettingsPageWithRefresh(); + }, + removeCostForLocalPickup: async () => { + const costLabel = await findLabelWithText( + 'Add a price for customers who choose local pickup' + ); + const costChecked = await page.$eval( + '#inspector-checkbox-control-1', + ( el ) => ( el as HTMLInputElement ).checked + ); + if ( costChecked ) { + await costLabel.click(); + } + }, + addLocalPickupLocation: async () => { + await merchant.goToLocalPickupSettingsPage(); + await expect( page ).toClick( 'button', { + text: 'Add pickup location', + } ); + await expect( page ).toFill( + 'input[name="location_name"]', + 'Test Location' + ); + await expect( page ).toFill( + 'input[name="location_address"]', + 'Test Address 1' + ); + await expect( page ).toFill( + 'input[name="location_city"]', + 'Test City' + ); + await expect( page ).toFill( + 'input[name="location_postcode"]', + '90210' + ); + await expect( page ).toFill( + 'input[name="pickup_details"]', + 'Collect from store' + ); + await expect( page ).toSelect( + 'select[name="location_country"]', + 'US' + ); + await expect( page ).toSelect( 'select[name="location_state"]', 'CA' ); + await expect( page ).toClick( 'button', { text: 'Done' } ); + await merchant.saveLocalPickupSettingsPageWithRefresh(); + }, +};