Merge branch 'trunk' of https://github.com/woocommerce/woocommerce into add/49335-related-products-collection

This commit is contained in:
Manish Menaria 2024-09-03 13:07:57 +05:30
commit 58d5c95dfe
59 changed files with 3001 additions and 1674 deletions

View File

@ -172,3 +172,27 @@ addFilter(
(endpoint) => 'https://my-custom-endpoint.com/js-error-log'
);
```
### `woocommerce_remote_logging_request_uri_whitelist`
Modifies the list of whitelisted query parameters that won't be masked in the logged request URI
**Parameters:**
- `whitelist` (string[]): The default whitelist.
**Return value:** (string[]) The modified whitelist.
**Usage example:**
```js
import { addFilter } from '@wordpress/hooks';
addFilter(
'woocommerce_remote_logging_request_uri_whitelist',
'my-plugin',
( whitelist ) => {
return [ ...whitelist, 'exampleParam' ]
}
);
```

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Add query params sanitisation

View File

@ -33,6 +33,44 @@ export const REMOTE_LOGGING_LOG_ENDPOINT_FILTER =
export const REMOTE_LOGGING_JS_ERROR_ENDPOINT_FILTER =
'woocommerce_remote_logging_js_error_endpoint';
export const REMOTE_LOGGING_REQUEST_URI_PARAMS_WHITELIST_FILTER =
'woocommerce_remote_logging_request_uri_whitelist';
export const REMPOTE_LOGGING_REQUEST_URI_PARAMS_DEFAULT_WHITELIST = [
'path',
'page',
'step',
'task',
'tab',
'section',
'status',
'post_type',
'taxonomy',
'action',
];
export const sanitiseRequestUriParams = ( search: string ) => {
const params = new URLSearchParams( search );
/**
* This filter modifies the list of whitelisted query parameters that won't be masked
* in the logged request URI
*
* @filter woocommerce_remote_logging_request_uri_whitelist
* @param {string[]} whitelist The default whitelist
*/
const whitelist = applyFilters(
REMOTE_LOGGING_REQUEST_URI_PARAMS_WHITELIST_FILTER,
REMPOTE_LOGGING_REQUEST_URI_PARAMS_DEFAULT_WHITELIST
) as typeof REMPOTE_LOGGING_REQUEST_URI_PARAMS_DEFAULT_WHITELIST;
for ( const [ key ] of params ) {
if ( ! whitelist.includes( key ) ) {
params.set( key, 'xxxxxx' );
}
}
return params.toString();
};
const REMOTE_LOGGING_LAST_ERROR_SENT_KEY =
'wc_remote_logging_last_error_sent_time';
@ -106,7 +144,8 @@ export class RemoteLogger {
properties: {
...extraData?.properties,
request_uri:
window.location.pathname + window.location.search,
window.location.pathname +
sanitiseRequestUriParams( window.location.search ),
},
} ),
trace: this.getFormattedStackFrame(
@ -213,7 +252,8 @@ export class RemoteLogger {
tags: [ 'js-unhandled-error' ],
properties: {
request_uri:
window.location.pathname + window.location.search,
window.location.pathname +
sanitiseRequestUriParams( window.location.search ),
},
} ),
trace: this.getFormattedStackFrame( trace ),

View File

@ -13,6 +13,8 @@ import {
REMOTE_LOGGING_ERROR_DATA_FILTER,
REMOTE_LOGGING_LOG_ENDPOINT_FILTER,
REMOTE_LOGGING_JS_ERROR_ENDPOINT_FILTER,
sanitiseRequestUriParams,
REMOTE_LOGGING_REQUEST_URI_PARAMS_WHITELIST_FILTER,
} from '../remote-logger';
import { fetchMock } from './__mocks__/fetch';
@ -380,3 +382,24 @@ describe( 'captureException', () => {
expect( fetchMock ).not.toHaveBeenCalled();
} );
} );
describe( 'sanitiseRequestUriParams', () => {
afterEach(() => {
removeFilter(REMOTE_LOGGING_REQUEST_URI_PARAMS_WHITELIST_FILTER, 'test' );
})
it( 'should replace non-whitelisted params with xxxxxx', () => {
expect(sanitiseRequestUriParams('?path=home&user=admin&token=abc123')).toEqual('path=home&user=xxxxxx&token=xxxxxx')
})
it( 'should not replace whitelisted params with xxxxxx', () => {
expect(sanitiseRequestUriParams('?path=home')).toEqual('path=home')
})
it( 'should not do anything if empty string is passed in', () => {
expect(sanitiseRequestUriParams('')).toEqual('')
})
it( 'should apply filters correctly', () => {
addFilter( REMOTE_LOGGING_REQUEST_URI_PARAMS_WHITELIST_FILTER, 'test', (defaultWhitelist) => {
return [ ... defaultWhitelist, 'foo' ];
})
expect(sanitiseRequestUriParams('?path=home&foo=bar&user=admin&token=abc123')).toEqual('path=home&foo=bar&user=xxxxxx&token=xxxxxx')
})
})

View File

@ -275,6 +275,13 @@ export const getPages = () => {
__( 'Profiler', 'woocommerce' ),
],
capability: 'manage_woocommerce',
layout: {
header: false,
footer: false,
showNotices: true,
showStoreAlerts: false,
showPluginArea: false,
},
} );
}
}

View File

@ -1,17 +1,17 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { Button, ExternalLink } from '@wordpress/components';
import { compose } from '@wordpress/compose';
import interpolateComponents from '@automattic/interpolate-components';
import PropTypes from 'prop-types';
import { get, isArray } from 'lodash';
import { PLUGINS_STORE_NAME } from '@woocommerce/data';
import { withDispatch, withSelect } from '@wordpress/data';
import { recordEvent } from '@woocommerce/tracks';
import { getSetting } from '@woocommerce/settings';
import { getSetting, getAdminLink } from '@woocommerce/settings';
import { Link } from '@woocommerce/components';
/**
* Internal dependencies
@ -19,29 +19,28 @@ import { getSetting } from '@woocommerce/settings';
import '../style.scss';
import DismissModal from '../dismiss-modal';
import SetupNotice, { setupErrorTypes } from '../setup-notice';
import { getWcsAssets, acceptWcsTos } from '../wcs-api';
import {
getWcsAssets,
acceptWcsTos,
getWcsLabelPurchaseConfigs,
} from '../wcs-api';
const wcAssetUrl = getSetting( 'wcAssetUrl', '' );
const wcsPluginSlug = 'woocommerce-services';
const wcShippingPluginSlug = 'woocommerce-shipping';
const wcstPluginSlug = 'woocommerce-services';
export class ShippingBanner extends Component {
constructor( props ) {
super( props );
const orderId = new URL( window.location.href ).searchParams.get(
'post'
);
this.state = {
showShippingBanner: true,
isDismissModalOpen: false,
setupErrorReason: setupErrorTypes.SETUP,
orderId: parseInt( orderId, 10 ),
wcsAssetsLoaded: false,
wcsAssetsLoading: false,
wcsSetupError: false,
isShippingLabelButtonBusy: false,
installText: this.getInstallText(),
isWcsModalOpen: false,
};
}
@ -76,8 +75,8 @@ export class ShippingBanner extends Component {
const { activePlugins } = this.props;
this.setState( { isShippingLabelButtonBusy: true } );
this.trackElementClicked( 'shipping_banner_create_label' );
if ( ! activePlugins.includes( wcsPluginSlug ) ) {
this.installAndActivatePlugins( wcsPluginSlug );
if ( ! activePlugins.includes( wcShippingPluginSlug ) ) {
this.installAndActivatePlugins( wcShippingPluginSlug );
} else {
this.acceptTosAndGetWCSAssets();
}
@ -85,7 +84,14 @@ export class ShippingBanner extends Component {
async installAndActivatePlugins( pluginSlug ) {
// Avoid double activating.
const { installPlugins, activatePlugins, isRequesting } = this.props;
const {
installPlugins,
activatePlugins,
isRequesting,
activePlugins,
isWcstCompatible,
isIncompatibleWCShippingInstalled,
} = this.props;
if ( isRequesting ) {
return false;
}
@ -107,7 +113,25 @@ export class ShippingBanner extends Component {
return;
}
this.acceptTosAndGetWCSAssets();
/**
* If a incompatible version of the WooCommerce Shipping plugin is installed, the necessary endpoints
* are not available, so we need to reload the page to ensure to make the plugin usable.
*/
if ( isIncompatibleWCShippingInstalled ) {
window.location.reload( true );
return;
}
if (
! activePlugins.includes( wcShippingPluginSlug ) &&
isWcstCompatible
) {
this.acceptTosAndGetWCSAssets();
} else {
this.setState( {
showShippingBanner: false,
} );
}
}
woocommerceServiceLinkClicked = () => {
@ -120,7 +144,7 @@ export class ShippingBanner extends Component {
banner_name: 'wcadmin_install_wcs_prompt',
jetpack_installed: activePlugins.includes( 'jetpack' ),
jetpack_connected: isJetpackConnected,
wcs_installed: activePlugins.includes( wcsPluginSlug ),
wcs_installed: activePlugins.includes( wcShippingPluginSlug ),
...customProps,
} );
};
@ -135,16 +159,21 @@ export class ShippingBanner extends Component {
} );
};
acceptTosAndGetWCSAssets() {
acceptTosAndGetWCSAssets = () => {
return acceptWcsTos()
.then( () => getWcsLabelPurchaseConfigs( this.props.orderId ) )
.then( ( configs ) => {
window.WCShipping_Config = configs.config;
return configs;
} )
.then( () => getWcsAssets() )
.then( ( wcsAssets ) => this.loadWcsAssets( wcsAssets ) )
.catch( () => this.setState( { wcsSetupError: true } ) );
}
.catch( () => {
this.setState( { wcsSetupError: true } );
} );
};
generateMetaBoxHtml( nodeId, title, args ) {
const argsJsonString = JSON.stringify( args ).replace( /"/g, '"' ); // JS has no native html_entities so we just replace.
const togglePanelText = __( 'Toggle panel:', 'woocommerce' );
return `
@ -159,8 +188,7 @@ export class ShippingBanner extends Component {
</div>
</div>
<div class="inside">
<div class="wcc-root woocommerce wc-connect-create-shipping-label" data-args="${ argsJsonString }">
</div>
<div class="wcc-root woocommerce woocommerce-shipping-shipping-label" id="woocommerce-shipping-shipping-label-${ args.context }"></div>
</div>
</div>
`;
@ -174,27 +202,24 @@ export class ShippingBanner extends Component {
this.setState( { wcsAssetsLoading: true } );
const jsPath = assets.wc_connect_admin_script;
const stylePath = assets.wc_connect_admin_style;
const labelPurchaseMetaboxId = 'woocommerce-order-label';
const shipmentTrackingMetaboxId = 'woocommerce-order-shipment-tracking';
const jsPath = assets.wcshipping_create_label_script;
const stylePath = assets.wcshipping_create_label_style;
if ( undefined === window.wcsPluginData ) {
const assetPath = jsPath.substring(
0,
jsPath.lastIndexOf( '/' ) + 1
);
window.wcsPluginData = { assetPath };
}
const shipmentTrackingJsPath =
assets.wcshipping_shipment_tracking_script;
const shipmentTrackingStylePath =
assets.wcshipping_shipment_tracking_style;
const { orderId } = this.state;
const { itemsCount } = this.props;
const { activePlugins } = this.props;
document.getElementById( labelPurchaseMetaboxId )?.remove();
const shippingLabelContainerHtml = this.generateMetaBoxHtml(
'woocommerce-order-label',
labelPurchaseMetaboxId,
__( 'Shipping Label', 'woocommerce' ),
{
order: { id: orderId },
context: 'shipping_label',
items: itemsCount,
}
);
// Insert shipping label metabox just above main order details box.
@ -202,13 +227,12 @@ export class ShippingBanner extends Component {
.getElementById( 'woocommerce-order-data' )
.insertAdjacentHTML( 'beforebegin', shippingLabelContainerHtml );
document.getElementById( shipmentTrackingMetaboxId )?.remove();
const shipmentTrackingHtml = this.generateMetaBoxHtml(
'woocommerce-order-shipment-tracking',
shipmentTrackingMetaboxId,
__( 'Shipment Tracking', 'woocommerce' ),
{
order: { id: orderId },
context: 'shipment_tracking',
items: itemsCount,
}
);
// Insert tracking metabox in the side after the order actions.
@ -224,6 +248,13 @@ export class ShippingBanner extends Component {
window.jQuery( '#woocommerce-order-label' ).hide();
}
document
.querySelectorAll( 'script[src*="/woocommerce-services/"]' )
.forEach( ( node ) => node.remove?.() );
document
.querySelectorAll( 'link[href*="/woocommerce-services/"]' )
.forEach( ( node ) => node.remove?.() );
Promise.all( [
new Promise( ( resolve, reject ) => {
const script = document.createElement( 'script' );
@ -233,9 +264,16 @@ export class ShippingBanner extends Component {
script.onerror = reject;
document.body.appendChild( script );
} ),
new Promise( ( resolve, reject ) => {
const script = document.createElement( 'script' );
script.src = shipmentTrackingJsPath;
script.async = true;
script.onload = resolve;
script.onerror = reject;
document.body.appendChild( script );
} ),
new Promise( ( resolve, reject ) => {
if ( stylePath !== '' ) {
const head = document.getElementsByTagName( 'head' )[ 0 ];
const link = document.createElement( 'link' );
link.rel = 'stylesheet';
link.type = 'text/css';
@ -243,7 +281,23 @@ export class ShippingBanner extends Component {
link.media = 'all';
link.onload = resolve;
link.onerror = reject;
head.appendChild( link );
link.id = 'wcshipping-injected-styles';
document.head.appendChild( link );
} else {
resolve();
}
} ),
new Promise( ( resolve, reject ) => {
if ( shipmentTrackingStylePath !== '' ) {
const link = document.createElement( 'link' );
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = shipmentTrackingStylePath;
link.media = 'all';
link.onload = resolve;
link.onerror = reject;
link.id = 'wcshipping-injected-styles';
document.head.appendChild( link );
} else {
resolve();
}
@ -254,133 +308,61 @@ export class ShippingBanner extends Component {
wcsAssetsLoading: false,
isShippingLabelButtonBusy: false,
} );
this.openWcsModal();
// Reshow the shipping label metabox.
if ( window.jQuery ) {
window.jQuery( '#woocommerce-order-label' ).show();
}
document.getElementById(
'woocommerce-admin-print-label'
).style.display = 'none';
/**
* We'll only get to this point if either WCS&T is not active or is active but compatible with WooCommerce Shipping
* so once we check if the WCS&T is not active, we can open the label purchase modal immediately.
*/
if ( ! activePlugins.includes( wcstPluginSlug ) ) {
this.openWcsModal();
}
} );
}
getInstallText = () => {
const { activePlugins } = this.props;
if ( activePlugins.includes( wcsPluginSlug ) ) {
// If WCS is active, then the only remaining step is to agree to the ToS.
return __(
'You\'ve already installed WooCommerce Shipping. By clicking "Create shipping label", you agree to its {{tosLink}}Terms of Service{{/tosLink}}.',
'woocommerce'
);
}
return __(
'By clicking "Create shipping label", {{wcsLink}}WooCommerce Shipping{{/wcsLink}} will be installed and you agree to its {{tosLink}}Terms of Service{{/tosLink}}.',
'woocommerce'
);
};
openWcsModal() {
if ( window.wcsGetAppStoreAsync ) {
window
.wcsGetAppStoreAsync( 'wc-connect-create-shipping-label' )
.then( ( wcsStore ) => {
const state = wcsStore.getState();
const { orderId } = this.state;
const siteId = state.ui.selectedSiteId;
// Since the button is dynamically added, we need to wait for it to become selectable and then click it.
const wcsStoreUnsubscribe = wcsStore.subscribe( () => {
const latestState = wcsStore.getState();
const buttonSelector =
'#woocommerce-shipping-shipping-label-shipping_label button';
if ( window.MutationObserver ) {
const observer = new window.MutationObserver(
( mutationsList, observing ) => {
const button = document.querySelector( buttonSelector );
if ( button ) {
button.click();
observing.disconnect();
}
}
);
const shippingLabelState = get(
latestState,
[
'extensions',
'woocommerce',
'woocommerceServices',
siteId,
'shippingLabel',
orderId,
],
null
);
const labelSettingsState = get(
latestState,
[
'extensions',
'woocommerce',
'woocommerceServices',
siteId,
'labelSettings',
],
null
);
const packageState = get(
latestState,
[
'extensions',
'woocommerce',
'woocommerceServices',
siteId,
'packages',
],
null
);
const locationsState = get( latestState, [
'extensions',
'woocommerce',
'sites',
siteId,
'data',
'locations',
] );
if (
shippingLabelState &&
labelSettingsState &&
labelSettingsState.meta &&
packageState &&
locationsState
) {
if (
shippingLabelState.loaded &&
labelSettingsState.meta.isLoaded &&
packageState.isLoaded &&
isArray( locationsState ) &&
! this.state.isWcsModalOpen
) {
if ( window.jQuery ) {
this.setState( { isWcsModalOpen: true } );
window
.jQuery(
'.shipping-label__new-label-button'
)
.click();
}
wcsStore.dispatch( {
type: 'NOTICE_CREATE',
notice: {
duration: 10000,
status: 'is-success',
text: __(
'Plugin installed and activated',
'woocommerce'
),
},
} );
} else if (
shippingLabelState.showPurchaseDialog
) {
wcsStoreUnsubscribe();
if ( window.jQuery ) {
window
.jQuery( '#woocommerce-order-label' )
.show();
}
}
}
} );
document.getElementById(
'woocommerce-admin-print-label'
).style.display = 'none';
} );
observer.observe(
document.getElementById(
'woocommerce-shipping-shipping-label-shipping_label'
) ??
document.getElementById( 'wpbody-content' ) ??
document.body,
{
childList: true,
subtree: true,
}
);
} else {
const interval = setInterval( () => {
const targetElement = document.querySelector( buttonSelector );
if ( targetElement ) {
targetElement.click();
clearInterval( interval );
}
}, 300 );
}
}
@ -390,10 +372,40 @@ export class ShippingBanner extends Component {
showShippingBanner,
isShippingLabelButtonBusy,
} = this.state;
const { isWcstCompatible } = this.props;
if ( ! showShippingBanner && ! isWcstCompatible ) {
document
.getElementById( 'woocommerce-admin-print-label' )
.classList.add( 'error' );
return (
<p>
<strong>
{ interpolateComponents( {
mixedString: __(
'Please {{pluginPageLink}}update{{/pluginPageLink}} the WooCommerce Shipping & Tax plugin to the latest version to ensure compatibility with WooCommerce Shipping.',
'woocommerce'
),
components: {
pluginPageLink: (
<Link
href={ getAdminLink( 'plugins.php' ) }
target="_blank"
type="wp-admin"
/>
),
},
} ) }
</strong>
</p>
);
}
if ( ! showShippingBanner ) {
return null;
}
const { actionButtonLabel, headline } = this.props;
return (
<div>
<div className="wc-admin-shipping-banner-container">
@ -403,15 +415,17 @@ export class ShippingBanner extends Component {
alt={ __( 'Shipping ', 'woocommerce' ) }
/>
<div className="wc-admin-shipping-banner-blob">
<h3>
{ __(
'Print discounted shipping labels with a click.',
'woocommerce'
) }
</h3>
<h3>{ headline }</h3>
<p>
{ interpolateComponents( {
mixedString: this.state.installText,
mixedString: sprintf(
// translators: %s is the action button label.
__(
'By clicking "%s", {{wcsLink}}WooCommerce Shipping{{/wcsLink}} will be installed and you agree to its {{tosLink}}Terms of Service{{/tosLink}}.',
'woocommerce'
),
actionButtonLabel
),
components: {
tosLink: (
<ExternalLink
@ -445,7 +459,7 @@ export class ShippingBanner extends Component {
isBusy={ isShippingLabelButtonBusy }
onClick={ this.createShippingLabelClicked }
>
{ __( 'Create shipping label', 'woocommerce' ) }
{ actionButtonLabel }
</Button>
<button
@ -471,12 +485,13 @@ export class ShippingBanner extends Component {
}
ShippingBanner.propTypes = {
itemsCount: PropTypes.number.isRequired,
isJetpackConnected: PropTypes.bool.isRequired,
activePlugins: PropTypes.array.isRequired,
activatePlugins: PropTypes.func.isRequired,
installPlugins: PropTypes.func.isRequired,
isRequesting: PropTypes.bool.isRequired,
orderId: PropTypes.number.isRequired,
isWcstCompatible: PropTypes.bool.isRequired,
};
export default compose(
@ -487,11 +502,32 @@ export default compose(
const isRequesting =
isPluginsRequesting( 'activatePlugins' ) ||
isPluginsRequesting( 'installPlugins' );
const activePlugins = getActivePlugins();
const actionButtonLabel = activePlugins.includes( wcstPluginSlug )
? __( 'Install WooCommerce Shipping', 'woocommerce' )
: __( 'Create shipping label', 'woocommerce' );
const headline = activePlugins.includes( wcstPluginSlug )
? __(
'Print discounted shipping labels with a click, now with the dedicated plugin!',
'woocommerce'
)
: __(
'Print discounted shipping labels with a click.',
'woocommerce'
);
return {
isRequesting,
isJetpackConnected: isJetpackConnected(),
activePlugins: getActivePlugins(),
activePlugins,
actionButtonLabel,
headline,
orderId: parseInt( window.wcShippingCoreData.order_id, 10 ),
isWcstCompatible: [ 1, '1' ].includes(
window.wcShippingCoreData.is_wcst_compatible
),
isIncompatibleWCShippingInstalled: [ 1, '1' ].includes(
window.wcShippingCoreData.is_incompatible_wcshipping_installed
),
};
} ),
withDispatch( ( dispatch ) => {

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { createElement, Fragment } from '@wordpress/element';
import { Fragment } from '@wordpress/element';
import { recordEvent } from '@woocommerce/tracks';
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
@ -9,7 +9,11 @@ import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import { acceptWcsTos, getWcsAssets } from '../../wcs-api.js';
import {
acceptWcsTos,
getWcsAssets,
getWcsLabelPurchaseConfigs,
} from '../../wcs-api.js';
import { ShippingBanner } from '../index.js';
jest.mock( '../../wcs-api.js' );
@ -18,25 +22,28 @@ acceptWcsTos.mockReturnValue( Promise.resolve() );
jest.mock( '@woocommerce/tracks' );
const wcsPluginSlug = 'woocommerce-services';
const wcsPluginSlug = 'woocommerce-shipping';
const wcstPluginSlug = 'woocommerce-services';
describe( 'Tracking impression in shippingBanner', () => {
const expectedTrackingData = {
banner_name: 'wcadmin_install_wcs_prompt',
jetpack_connected: true,
jetpack_installed: true,
wcs_installed: true,
wcs_installed: false,
};
it( 'should record an event when user sees banner loaded', () => {
render(
<ShippingBanner
isJetpackConnected={ true }
activePlugins={ [ wcsPluginSlug, 'jetpack' ] }
activePlugins={ [ wcstPluginSlug, 'jetpack' ] }
itemsCount={ 1 }
activatePlugins={ jest.fn() }
installPlugins={ jest.fn() }
isRequesting={ false }
isWcstCompatible={ true }
orderId={ 1 }
/>
);
expect( recordEvent ).toHaveBeenCalledTimes( 1 );
@ -59,25 +66,31 @@ describe( 'Tracking clicks in shippingBanner', () => {
};
it( 'should record an event when user clicks "Create shipping label"', async () => {
const actionButtonLabel = 'Create shipping label';
const { getByRole } = render(
<ShippingBanner
isJetpackConnected={ true }
activePlugins={ [ wcsPluginSlug, 'jetpack' ] }
installPlugins={ jest.fn() }
activatePlugins={ jest.fn() }
activePlugins={ [ 'jetpack' ] }
installPlugins={ jest
.fn()
.mockResolvedValue( { success: true } ) }
activatePlugins={ jest
.fn()
.mockResolvedValue( { success: true } ) }
isRequesting={ false }
itemsCount={ 1 }
orderId={ 1 }
isWcstCompatible={ true }
actionButtonLabel={ actionButtonLabel }
/>
);
userEvent.click(
getByRole( 'button', { name: 'Create shipping label' } )
);
userEvent.click( getByRole( 'button', { name: actionButtonLabel } ) );
await waitFor( () =>
expect( recordEvent ).toHaveBeenCalledWith(
'banner_element_clicked',
getExpectedTrackingData( 'shipping_banner_create_label' )
getExpectedTrackingData( 'shipping_banner_create_label', false )
)
);
} );
@ -92,6 +105,8 @@ describe( 'Tracking clicks in shippingBanner', () => {
activatePlugins={ jest.fn() }
isRequesting={ false }
itemsCount={ 1 }
orderId={ 1 }
isWcstCompatible={ true }
/>
);
@ -114,11 +129,13 @@ describe( 'Tracking clicks in shippingBanner', () => {
const { getByRole } = render(
<ShippingBanner
isJetpackConnected={ true }
activePlugins={ [ wcsPluginSlug, 'jetpack' ] }
activePlugins={ [ wcstPluginSlug, 'jetpack' ] }
installPlugins={ jest.fn() }
activatePlugins={ jest.fn() }
isRequesting={ false }
itemsCount={ 1 }
orderId={ 1 }
isWcstCompatible={ true }
/>
);
@ -129,7 +146,7 @@ describe( 'Tracking clicks in shippingBanner', () => {
await waitFor( () =>
expect( recordEvent ).toHaveBeenCalledWith(
'banner_element_clicked',
getExpectedTrackingData( 'shipping_banner_dimiss' )
getExpectedTrackingData( 'shipping_banner_dimiss', false )
)
);
} );
@ -148,6 +165,8 @@ describe( 'Create shipping label button', () => {
};
it( 'should install WooCommerce Shipping when button is clicked', async () => {
const actionButtonLabel = 'Create shipping label';
const { getByRole } = render(
<ShippingBanner
isJetpackConnected={ true }
@ -156,22 +175,26 @@ describe( 'Create shipping label button', () => {
installPlugins={ installPlugins }
isRequesting={ false }
itemsCount={ 1 }
orderId={ 1 }
isWcstCompatible={ true }
actionButtonLabel={ actionButtonLabel }
/>
);
userEvent.click(
getByRole( 'button', {
name: 'Create shipping label',
name: actionButtonLabel,
} )
);
await waitFor( () =>
expect( installPlugins ).toHaveBeenCalledWith( [
'woocommerce-services',
'woocommerce-shipping',
] )
);
} );
it( 'should activate WooCommerce Shipping when installation finishes', async () => {
const actionButtonLabel = 'Create shipping label';
const { getByRole } = render(
<ShippingBanner
isJetpackConnected={ true }
@ -180,38 +203,46 @@ describe( 'Create shipping label button', () => {
installPlugins={ installPlugins }
isRequesting={ false }
itemsCount={ 1 }
orderId={ 1 }
isWcstCompatible={ true }
actionButtonLabel={ actionButtonLabel }
/>
);
userEvent.click(
getByRole( 'button', {
name: 'Create shipping label',
name: actionButtonLabel,
} )
);
await waitFor( () =>
expect( activatePlugins ).toHaveBeenCalledWith( [
'woocommerce-services',
'woocommerce-shipping',
] )
);
} );
it( 'should perform a request to accept the TOS and get WCS assets to load', async () => {
getWcsLabelPurchaseConfigs.mockReturnValueOnce( Promise.resolve( {} ) );
getWcsAssets.mockReturnValueOnce( Promise.resolve( {} ) );
const actionButtonLabel = 'Create shipping label';
const { getByRole } = render(
<ShippingBanner
isJetpackConnected={ true }
activatePlugins={ activatePlugins }
activePlugins={ [ wcsPluginSlug ] }
activePlugins={ [ wcstPluginSlug ] }
installPlugins={ installPlugins }
isRequesting={ false }
itemsCount={ 1 }
orderId={ 1 }
isWcstCompatible={ true }
actionButtonLabel={ actionButtonLabel }
/>
);
userEvent.click(
getByRole( 'button', {
name: 'Create shipping label',
name: actionButtonLabel,
} )
);
@ -220,9 +251,16 @@ describe( 'Create shipping label button', () => {
} );
it( 'should load WCS assets when a path is provided', async () => {
const actionButtonLabel = 'Create shipping label';
getWcsLabelPurchaseConfigs.mockReturnValueOnce( Promise.resolve( {} ) );
const mockAssets = {
wc_connect_admin_script: '/path/to/wcs.js',
wc_connect_admin_style: '/path/to/wcs.css',
wcshipping_create_label_script: '/path/to/wcs.js',
wcshipping_create_label_style: '/path/to/wcs.css',
wcshipping_shipment_tracking_script:
'/wcshipping_shipment_tracking_script',
wcshipping_shipment_tracking_style:
'/wcshipping_shipment_tracking_style',
};
getWcsAssets.mockReturnValueOnce(
Promise.resolve( {
@ -237,17 +275,20 @@ describe( 'Create shipping label button', () => {
<ShippingBanner
isJetpackConnected={ true }
activatePlugins={ activatePlugins }
activePlugins={ [ wcsPluginSlug, 'jetpack' ] }
activePlugins={ [ wcstPluginSlug, 'jetpack' ] }
installPlugins={ installPlugins }
isRequesting={ false }
itemsCount={ 1 }
orderId={ 1 }
isWcstCompatible={ true }
actionButtonLabel={ actionButtonLabel }
/>
</Fragment>
);
userEvent.click(
getByRole( 'button', {
name: 'Create shipping label',
name: actionButtonLabel,
} )
);
@ -263,40 +304,44 @@ describe( 'Create shipping label button', () => {
).toBeInTheDocument();
// Check that the script and style elements have been created.
expect( document.getElementsByTagName( 'script' )[ 0 ].src ).toBe(
'http://localhost' + mockAssets.wc_connect_admin_script
const allScriptSrcs = Array.from(
document.querySelectorAll( 'script' )
).map( ( script ) => script.src );
const allLinkHrefs = Array.from(
document.querySelectorAll( 'link' )
).map( ( link ) => link.href );
expect( allScriptSrcs ).toContain(
'http://localhost' + mockAssets.wcshipping_create_label_script
);
expect( document.getElementsByTagName( 'link' )[ 0 ].href ).toBe(
'http://localhost' + mockAssets.wc_connect_admin_style
expect( allScriptSrcs ).toContain(
'http://localhost' + mockAssets.wcshipping_shipment_tracking_script
);
expect( allLinkHrefs ).toContain(
'http://localhost' + mockAssets.wcshipping_create_label_style
);
expect( allLinkHrefs ).toContain(
'http://localhost' + mockAssets.wcshipping_shipment_tracking_style
);
} );
it( 'should open WCS modal', async () => {
window.wcsGetAppStoreAsync = jest.fn();
const getState = jest.fn();
const dispatch = jest.fn();
const subscribe = jest.fn();
window.wcsGetAppStoreAsync.mockReturnValueOnce(
Promise.resolve( {
getState,
dispatch,
subscribe,
} )
);
getState.mockReturnValueOnce( {
ui: {
selectedSiteId: 'SITE_ID',
},
} );
const actionButtonLabel = 'Create shipping label';
getWcsLabelPurchaseConfigs.mockReturnValueOnce( Promise.resolve( {} ) );
getWcsAssets.mockReturnValueOnce(
Promise.resolve( {
assets: {
// Easy to identify string in our hijacked setter function.
wc_connect_admin_script: 'wc_connect_admin_script',
wcshipping_create_label_script:
'wcshipping_create_label_script',
wcshipping_shipment_tracking_script:
'wcshipping_create_label_script',
// Empty string to avoid creating a script tag we also have to hijack.
wc_connect_admin_style: '',
wcshipping_create_label_style: '',
wcshipping_shipment_tracking_style: '',
},
} )
);
@ -306,8 +351,15 @@ describe( 'Create shipping label button', () => {
// const scriptSrcProperty = window.HTMLScriptElement.prototype.src;
Object.defineProperty( window.HTMLScriptElement.prototype, 'src', {
set( src ) {
if ( src === 'wc_connect_admin_script' ) {
setTimeout( () => this.onload() );
if (
[
'wcshipping_create_label_script',
'wcshipping_shipment_tracking_script',
].includes( src )
) {
setTimeout( () => {
this.onload();
}, 1 );
}
},
} );
@ -324,33 +376,38 @@ describe( 'Create shipping label button', () => {
installPlugins={ installPlugins }
isRequesting={ false }
itemsCount={ 1 }
orderId={ 1 }
isWcstCompatible={ true }
actionButtonLabel={ actionButtonLabel }
/>
</Fragment>
);
const openWcsModalSpy = jest.spyOn(
ShippingBanner.prototype,
'openWcsModal'
);
// Initiate the loading of WCS assets on first click.
userEvent.click(
getByRole( 'button', {
name: 'Create shipping label',
name: actionButtonLabel,
} )
);
await waitFor( () =>
expect( window.wcsGetAppStoreAsync ).toHaveBeenCalledWith(
'wc-connect-create-shipping-label'
)
);
await waitFor( () => expect( getState ).toHaveBeenCalledTimes( 1 ) );
await waitFor( () => expect( subscribe ).toHaveBeenCalledTimes( 1 ) );
await waitFor( () => {
expect(
document.getElementById( 'woocommerce-admin-print-label' )
).not.toBeVisible();
} );
expect(
document.getElementById( 'woocommerce-admin-print-label' )
).not.toBeVisible();
expect( openWcsModalSpy ).toHaveBeenCalledTimes( 1 );
} );
} );
describe( 'In the process of installing, activating, loading assets for WooCommerce Service', () => {
it( 'should show a busy loading state on "Create shipping label" and should disable "Close Print Label Banner"', async () => {
const actionButtonLabel = 'Create shipping label';
const { getByRole } = render(
<ShippingBanner
isJetpackConnected={ true }
@ -359,24 +416,25 @@ describe( 'In the process of installing, activating, loading assets for WooComme
installPlugins={ jest.fn() }
isRequesting={ true }
itemsCount={ 1 }
orderId={ 1 }
isWcstCompatible={ true }
actionButtonLabel={ actionButtonLabel }
/>
);
expect(
getByRole( 'button', { name: 'Create shipping label' } )
getByRole( 'button', { name: actionButtonLabel } )
).not.toHaveClass( 'is-busy' );
expect(
getByRole( 'button', { name: 'Close Print Label Banner.' } )
).toBeEnabled();
userEvent.click(
getByRole( 'button', { name: 'Create shipping label' } )
);
userEvent.click( getByRole( 'button', { name: actionButtonLabel } ) );
await waitFor( () =>
expect(
getByRole( 'button', { name: 'Create shipping label' } )
getByRole( 'button', { name: actionButtonLabel } )
).toHaveClass( 'is-busy' )
);
@ -396,6 +454,9 @@ describe( 'Setup error message', () => {
installPlugins={ jest.fn() }
itemsCount={ 1 }
isRequesting={ false }
orderId={ 1 }
isWcstCompatible={ true }
actionButtonLabel="Create shipping label"
/>
);
@ -407,6 +468,7 @@ describe( 'Setup error message', () => {
} );
it( 'should show if there is installation error', async () => {
const actionButtonLabel = 'Create shipping label';
const { getByRole, getByText } = render(
<ShippingBanner
isJetpackConnected={ true }
@ -417,12 +479,13 @@ describe( 'Setup error message', () => {
} ) }
itemsCount={ 1 }
isRequesting={ false }
orderId={ 1 }
isWcstCompatible={ true }
actionButtonLabel={ actionButtonLabel }
/>
);
userEvent.click(
getByRole( 'button', { name: 'Create shipping label' } )
);
userEvent.click( getByRole( 'button', { name: actionButtonLabel } ) );
await waitFor( () =>
expect(
@ -434,6 +497,8 @@ describe( 'Setup error message', () => {
} );
it( 'should show if there is activation error', async () => {
const actionButtonLabel = 'Create shipping label';
const { getByRole, getByText } = render(
<ShippingBanner
isJetpackConnected={ true }
@ -446,12 +511,13 @@ describe( 'Setup error message', () => {
} ) }
itemsCount={ 1 }
isRequesting={ false }
orderId={ 1 }
isWcstCompatible={ true }
actionButtonLabel={ actionButtonLabel }
/>
);
userEvent.click(
getByRole( 'button', { name: 'Create shipping label' } )
);
userEvent.click( getByRole( 'button', { name: actionButtonLabel } ) );
await waitFor( () =>
expect(
@ -471,16 +537,16 @@ describe( 'The message in the banner', () => {
activatePlugins={ jest.fn() }
activePlugins={ activePlugins }
installPlugins={ jest.fn() }
wcsPluginSlug={ 'woocommerce-services' }
isRequesting={ true }
itemsCount={ 1 }
orderId={ 1 }
isWcstCompatible={ true }
actionButtonLabel="Create shipping label"
/>
);
const notActivatedMessage =
'By clicking "Create shipping label", WooCommerce Shipping(opens in a new tab) will be installed and you agree to its Terms of Service(opens in a new tab).';
const activatedMessage =
'You\'ve already installed WooCommerce Shipping. By clicking "Create shipping label", you agree to its Terms of Service(opens in a new tab).';
it( 'should show install text "By clicking "Create shipping label"..." when first loaded.', () => {
const { container } = createShippingBannerWrapper( {
@ -502,11 +568,13 @@ describe( 'The message in the banner', () => {
<ShippingBanner
isJetpackConnected={ true }
activatePlugins={ jest.fn() }
activePlugins={ [ 'woocommerce-services' ] }
activePlugins={ [ wcstPluginSlug ] }
installPlugins={ jest.fn() }
wcsPluginSlug={ 'woocommerce-services' }
isRequesting={ true }
itemsCount={ 1 }
orderId={ 1 }
isWcstCompatible={ true }
actionButtonLabel="Create shipping label"
/>
);
@ -515,15 +583,71 @@ describe( 'The message in the banner', () => {
.textContent
).toBe( notActivatedMessage );
} );
} );
it( 'should show install text "By clicking "You\'ve already installed WooCommerce Shipping."..." when WooCommerce Service is already installed.', () => {
const { container } = createShippingBannerWrapper( {
activePlugins: [ 'woocommerce-services' ],
describe( 'If incompatible WCS&T is active', () => {
const installPlugins = jest.fn().mockReturnValue( {
success: true,
} );
const activatePlugins = jest.fn().mockReturnValue( {
success: true,
} );
beforeEach( () => {
acceptWcsTos.mockClear();
} );
it( 'should install and activate but show an error notice when an incompatible version of WCS&T is installed', async () => {
const actionButtonLabel = 'Install WooCommerce Shipping';
const { getByRole, getByText } = render(
<Fragment>
<div id="woocommerce-order-data" />
<div id="woocommerce-order-actions" />
<div id="woocommerce-admin-print-label" />
<ShippingBanner
isJetpackConnected={ true }
activatePlugins={ activatePlugins }
activePlugins={ [ wcstPluginSlug, 'jetpack' ] }
installPlugins={ installPlugins }
itemsCount={ 1 }
isRequesting={ false }
orderId={ 1 }
isWcstCompatible={ false }
actionButtonLabel={ actionButtonLabel }
/>
</Fragment>
);
userEvent.click( getByRole( 'button', { name: actionButtonLabel } ) );
await waitFor( () => {
expect( installPlugins ).toHaveBeenCalledWith( [ wcsPluginSlug ] );
} );
await waitFor( () => {
expect( activatePlugins ).toHaveBeenCalledWith( [ wcsPluginSlug ] );
} );
expect(
container.querySelector( '.wc-admin-shipping-banner-blob p' )
.textContent
).toBe( activatedMessage );
await waitFor( () => {
expect( acceptWcsTos ).not.toHaveBeenCalled();
} );
const notice = getByText( ( _, element ) => {
const hasText = ( node ) =>
node.textContent ===
'Please update the WooCommerce Shipping & Tax plugin to the latest version to ensure compatibility with WooCommerce Shipping.';
const nodeHasText = hasText( element );
const childrenDontHaveText = Array.from( element.children ).every(
( child ) => ! hasText( child )
);
return nodeHasText && childrenDontHaveText;
} );
await waitFor( () => expect( notice ).toBeInTheDocument() );
// Assert that the "update" link is present
const updateLink = getByText( /update/i );
expect( updateLink ).toBeInTheDocument();
expect( updateLink.tagName ).toBe( 'A' ); // Ensures it's a link
} );
} );

View File

@ -66,6 +66,16 @@
.components-external-link__icon {
display: none;
}
&.error {
min-height: auto;
.inside {
padding-bottom: 6px;
p {
font-size: 13px;
}
}
}
}
.components-modal__frame.wc-admin-shipping-banner__dismiss-modal {

View File

@ -4,7 +4,7 @@
import apiFetch from '@wordpress/api-fetch';
export function acceptWcsTos() {
const path = '/wc/v1/connect/tos';
const path = '/wcshipping/v1/tos';
return apiFetch( {
path,
method: 'POST',
@ -13,7 +13,15 @@ export function acceptWcsTos() {
}
export function getWcsAssets() {
const path = '/wc/v1/connect/assets';
const path = '/wcshipping/v1/assets';
return apiFetch( {
path,
method: 'GET',
} );
}
export function getWcsLabelPurchaseConfigs( orderId ) {
const path = `wcshipping/v1/config/label-purchase/${ orderId }`;
return apiFetch( {
path,
method: 'GET',

View File

@ -0,0 +1,81 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import { Icon, info } from '@wordpress/icons';
import ProductControl from '@woocommerce/editor-components/product-control';
import type { SelectedOption } from '@woocommerce/block-hocs';
import { createInterpolateElement } from '@wordpress/element';
import {
Placeholder,
// @ts-expect-error Using experimental features
__experimentalHStack as HStack,
// @ts-expect-error Using experimental features
__experimentalText as Text,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import type { ProductCollectionEditComponentProps } from '../types';
import { getCollectionByName } from '../collections';
const ProductPicker = ( props: ProductCollectionEditComponentProps ) => {
const blockProps = useBlockProps();
const attributes = props.attributes;
const collection = getCollectionByName( attributes.collection );
if ( ! collection ) {
return;
}
return (
<div { ...blockProps }>
<Placeholder className="wc-blocks-product-collection__editor-product-picker">
<HStack alignment="center">
<Icon
icon={ info }
className="wc-blocks-product-collection__info-icon"
/>
<Text>
{ createInterpolateElement(
sprintf(
/* translators: %s: collection title */
__(
'<strong>%s</strong> requires a product to be selected in order to display associated items.',
'woocommerce'
),
collection.title
),
{
strong: <strong />,
}
) }
</Text>
</HStack>
<ProductControl
selected={
attributes.query?.productReference as SelectedOption
}
onChange={ ( value = [] ) => {
const isValidId = ( value[ 0 ]?.id ?? null ) !== null;
if ( isValidId ) {
props.setAttributes( {
query: {
...attributes.query,
productReference: value[ 0 ].id,
},
} );
}
} }
messages={ {
search: __( 'Select a product', 'woocommerce' ),
} }
/>
</Placeholder>
</div>
);
};
export default ProductPicker;

View File

@ -168,3 +168,49 @@ $max-button-width: calc(100% / #{$max-button-columns});
color: var(--wp-components-color-accent-inverted, #fff);
}
}
// Editor Product Picker
.wc-blocks-product-collection__editor-product-picker {
.wc-blocks-product-collection__info-icon {
fill: var(--wp--preset--color--luminous-vivid-orange, #e26f56);
}
}
// Linked Product Control
.wc-block-product-collection-linked-product-control {
width: 100%;
text-align: left;
&__button {
width: 100%;
height: 100%;
padding: 10px;
border: 1px solid $gray-300;
}
&__image-container {
flex-shrink: 0;
width: 45px;
height: 45px;
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
}
&__content {
text-align: left;
}
}
.wc-block-product-collection-linked-product__popover-content .components-popover__content {
width: 100%;
.woocommerce-search-list__search {
border: 0;
padding: 0;
}
}

View File

@ -4,18 +4,25 @@
import { store as blockEditorStore } from '@wordpress/block-editor';
import { useState } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { useGetLocation } from '@woocommerce/blocks/product-template/utils';
/**
* Internal dependencies
*/
import type { ProductCollectionEditComponentProps } from '../types';
import {
ProductCollectionEditComponentProps,
ProductCollectionUIStatesInEditor,
} from '../types';
import ProductCollectionPlaceholder from './product-collection-placeholder';
import ProductCollectionContent from './product-collection-content';
import CollectionSelectionModal from './collection-selection-modal';
import './editor.scss';
import { getProductCollectionUIStateInEditor } from '../utils';
import ProductPicker from './ProductPicker';
const Edit = ( props: ProductCollectionEditComponentProps ) => {
const { clientId, attributes } = props;
const location = useGetLocation( props.context, props.clientId );
const [ isSelectionModalOpen, setIsSelectionModalOpen ] = useState( false );
const hasInnerBlocks = useSelect(
@ -24,9 +31,37 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
[ clientId ]
);
const Component = hasInnerBlocks
? ProductCollectionContent
: ProductCollectionPlaceholder;
const productCollectionUIStateInEditor =
getProductCollectionUIStateInEditor( {
hasInnerBlocks,
location,
attributes: props.attributes,
usesReference: props.usesReference,
} );
/**
* Component to render based on the UI state.
*/
let Component,
isUsingReferencePreviewMode = false;
switch ( productCollectionUIStateInEditor ) {
case ProductCollectionUIStatesInEditor.COLLECTION_PICKER:
Component = ProductCollectionPlaceholder;
break;
case ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER:
Component = ProductPicker;
break;
case ProductCollectionUIStatesInEditor.VALID:
Component = ProductCollectionContent;
break;
case ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW:
Component = ProductCollectionContent;
isUsingReferencePreviewMode = true;
break;
default:
// By default showing collection chooser.
Component = ProductCollectionPlaceholder;
}
return (
<>
@ -35,6 +70,9 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
openCollectionSelectionModal={ () =>
setIsSelectionModalOpen( true )
}
isUsingReferencePreviewMode={ isUsingReferencePreviewMode }
location={ location }
usesReference={ props.usesReference }
/>
{ isSelectionModalOpen && (
<CollectionSelectionModal

View File

@ -50,6 +50,7 @@ import LayoutOptionsControl from './layout-options-control';
import FeaturedProductsControl from './featured-products-control';
import CreatedControl from './created-control';
import PriceRangeControl from './price-range-control';
import LinkedProductControl from './linked-product-control';
const prepareShouldShowFilter =
( hideControls: FilterName[] ) => ( filter: FilterName ) => {
@ -121,6 +122,13 @@ const ProductCollectionInspectorControls = (
return (
<InspectorControls>
<LinkedProductControl
query={ props.attributes.query }
setAttributes={ props.setAttributes }
usesReference={ props.usesReference }
location={ props.location }
/>
<ToolsPanel
label={ __( 'Settings', 'woocommerce' ) }
resetAll={ () => {

View File

@ -0,0 +1,166 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import ProductControl from '@woocommerce/editor-components/product-control';
import { SelectedOption } from '@woocommerce/block-hocs';
import { useState, useMemo } from '@wordpress/element';
import type { WooCommerceBlockLocation } from '@woocommerce/blocks/product-template/utils';
import type { ProductResponseItem } from '@woocommerce/types';
import { decodeEntities } from '@wordpress/html-entities';
import {
PanelBody,
PanelRow,
Button,
Flex,
FlexItem,
Dropdown,
// @ts-expect-error Using experimental features
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalText as Text,
Spinner,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import { useGetProduct } from '../../utils';
import type {
ProductCollectionQuery,
ProductCollectionSetAttributes,
} from '../../types';
const ProductButton: React.FC< {
isOpen: boolean;
onToggle: () => void;
product: ProductResponseItem | null;
isLoading: boolean;
} > = ( { isOpen, onToggle, product, isLoading } ) => {
if ( isLoading && ! product ) {
return <Spinner />;
}
return (
<Button
className="wc-block-product-collection-linked-product-control__button"
onClick={ onToggle }
aria-expanded={ isOpen }
disabled={ isLoading }
>
<Flex direction="row" expanded justify="flex-start">
<FlexItem className="wc-block-product-collection-linked-product-control__image-container">
<img
src={ product?.images?.[ 0 ]?.src }
alt={ product?.name }
/>
</FlexItem>
<Flex
direction="column"
align="flex-start"
gap={ 1 }
className="wc-block-product-collection-linked-product-control__content"
>
<FlexItem>
<Text color="inherit" lineHeight={ 1 }>
{ product?.name
? decodeEntities( product.name )
: '' }
</Text>
</FlexItem>
<FlexItem>
<Text color="inherit" lineHeight={ 1 }>
{ product?.sku }
</Text>
</FlexItem>
</Flex>
</Flex>
</Button>
);
};
const LinkedProductPopoverContent: React.FC< {
query: ProductCollectionQuery;
setAttributes: ProductCollectionSetAttributes;
setIsDropdownOpen: React.Dispatch< React.SetStateAction< boolean > >;
} > = ( { query, setAttributes, setIsDropdownOpen } ) => (
<ProductControl
selected={ query?.productReference as SelectedOption }
onChange={ ( value: SelectedOption[] = [] ) => {
const productId = value[ 0 ]?.id ?? null;
if ( productId !== null ) {
setAttributes( {
query: {
...query,
productReference: productId,
},
} );
setIsDropdownOpen( false );
}
} }
messages={ {
search: __( 'Select a product', 'woocommerce' ),
} }
/>
);
const LinkedProductControl = ( {
query,
setAttributes,
location,
usesReference,
}: {
query: ProductCollectionQuery;
setAttributes: ProductCollectionSetAttributes;
location: WooCommerceBlockLocation;
usesReference: string[] | undefined;
} ) => {
const [ isDropdownOpen, setIsDropdownOpen ] = useState< boolean >( false );
const { product, isLoading } = useGetProduct( query.productReference );
const showLinkedProductControl = useMemo( () => {
const isInRequiredLocation = usesReference?.includes( location.type );
const isProductContextRequired = usesReference?.includes( 'product' );
const isProductContextSelected =
( query?.productReference ?? null ) !== null;
return (
isProductContextRequired &&
! isInRequiredLocation &&
isProductContextSelected
);
}, [ location.type, query?.productReference, usesReference ] );
if ( ! showLinkedProductControl ) return null;
return (
<PanelBody title={ __( 'Linked Product', 'woocommerce' ) }>
<PanelRow>
<Dropdown
className="wc-block-product-collection-linked-product-control"
contentClassName="wc-block-product-collection-linked-product__popover-content"
popoverProps={ { placement: 'left-start' } }
renderToggle={ ( { isOpen, onToggle } ) => (
<ProductButton
isOpen={ isOpen }
onToggle={ onToggle }
product={ product }
isLoading={ isLoading }
/>
) }
renderContent={ () => (
<LinkedProductPopoverContent
query={ query }
setAttributes={ setAttributes }
setIsDropdownOpen={ setIsDropdownOpen }
/>
) }
open={ isDropdownOpen }
onToggle={ () => setIsDropdownOpen( ! isDropdownOpen ) }
/>
</PanelRow>
</PanelBody>
);
};
export default LinkedProductControl;

View File

@ -10,7 +10,6 @@ import { useInstanceId } from '@wordpress/compose';
import { useEffect, useRef, useMemo } from '@wordpress/element';
import { Button } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useGetLocation } from '@woocommerce/blocks/product-template/utils';
import fastDeepEqual from 'fast-deep-equal/es6';
/**
@ -68,19 +67,23 @@ const useQueryId = (
const ProductCollectionContent = ( {
preview: { setPreviewState, initialPreviewState } = {},
usesReference,
...props
}: ProductCollectionEditComponentProps ) => {
const isInitialAttributesSet = useRef( false );
const { clientId, attributes, setAttributes } = props;
const location = useGetLocation( props.context, props.clientId );
const {
clientId,
attributes,
setAttributes,
location,
isUsingReferencePreviewMode,
} = props;
useSetPreviewState( {
setPreviewState,
setAttributes,
location,
attributes,
usesReference,
isUsingReferencePreviewMode,
} );
const blockProps = useBlockProps();

View File

@ -9,6 +9,16 @@ import { type AttributeMetadata } from '@woocommerce/types';
*/
import { WooCommerceBlockLocation } from '../product-template/utils';
export enum ProductCollectionUIStatesInEditor {
COLLECTION_PICKER = 'collection_chooser',
PRODUCT_REFERENCE_PICKER = 'product_context_picker',
VALID_WITH_PREVIEW = 'uses_reference_preview_mode',
VALID = 'valid',
// Future states
// INVALID = 'invalid',
// DELETED_PRODUCT_REFERENCE = 'deleted_product_reference',
}
export interface ProductCollectionAttributes {
query: ProductCollectionQuery;
queryId: number;
@ -99,6 +109,7 @@ export interface ProductCollectionQuery {
woocommerceRelatedTo: string[];
priceRange: undefined | PriceRange;
filterable: boolean;
productReference?: number;
}
export type ProductCollectionEditComponentProps =
@ -112,6 +123,8 @@ export type ProductCollectionEditComponentProps =
context: {
templateSlug: string;
};
isUsingReferencePreviewMode: boolean;
location: WooCommerceBlockLocation;
};
export type TProductCollectionOrder = 'asc' | 'desc';

View File

@ -6,8 +6,10 @@ import { addFilter } from '@wordpress/hooks';
import { select } from '@wordpress/data';
import { isWpVersion } from '@woocommerce/settings';
import type { BlockEditProps, Block } from '@wordpress/blocks';
import { useLayoutEffect } from '@wordpress/element';
import { useEffect, useLayoutEffect, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import type { ProductResponseItem } from '@woocommerce/types';
import { getProduct } from '@woocommerce/editor-components/utils';
import {
createBlock,
// @ts-expect-error Type definitions for this function are missing in Guteberg
@ -18,13 +20,14 @@ import {
* Internal dependencies
*/
import {
type ProductCollectionAttributes,
type TProductCollectionOrder,
type TProductCollectionOrderBy,
type ProductCollectionQuery,
type ProductCollectionDisplayLayout,
type PreviewState,
type SetPreviewState,
ProductCollectionAttributes,
TProductCollectionOrder,
TProductCollectionOrderBy,
ProductCollectionQuery,
ProductCollectionDisplayLayout,
PreviewState,
SetPreviewState,
ProductCollectionUIStatesInEditor,
} from './types';
import {
coreQueryPaginationBlockName,
@ -166,41 +169,14 @@ export const addProductCollectionToQueryPaginationParentOrAncestor = () => {
};
/**
* Get the preview message for the Product Collection block based on the usesReference.
* There are two scenarios:
* 1. When usesReference is product, the preview message will be:
* "Actual products will vary depending on the product being viewed."
* 2. For all other usesReference, the preview message will be:
* "Actual products will vary depending on the page being viewed."
*
* This message will be shown when the usesReference isn't available on the Editor side, but is available on the Frontend.
* Get the message to show in the preview label when the block is in preview mode based
* on the `usesReference` value.
*/
export const getUsesReferencePreviewMessage = (
location: WooCommerceBlockLocation,
usesReference?: string[]
isUsingReferencePreviewMode: boolean
) => {
if ( ! ( Array.isArray( usesReference ) && usesReference.length > 0 ) ) {
return '';
}
if ( usesReference.includes( location.type ) ) {
/**
* Block shouldn't be in preview mode when:
* 1. Current location is archive and termId is available.
* 2. Current location is product and productId is available.
*
* Because in these cases, we have required context on the editor side.
*/
const isArchiveLocationWithTermId =
location.type === LocationType.Archive &&
( location.sourceData?.termId ?? null ) !== null;
const isProductLocationWithProductId =
location.type === LocationType.Product &&
( location.sourceData?.productId ?? null ) !== null;
if ( isArchiveLocationWithTermId || isProductLocationWithProductId ) {
return '';
}
if ( isUsingReferencePreviewMode ) {
if ( location.type === LocationType.Product ) {
return __(
'Actual products will vary depending on the product being viewed.',
@ -217,12 +193,77 @@ export const getUsesReferencePreviewMessage = (
return '';
};
export const getProductCollectionUIStateInEditor = ( {
location,
usesReference,
attributes,
hasInnerBlocks,
}: {
location: WooCommerceBlockLocation;
usesReference?: string[] | undefined;
attributes: ProductCollectionAttributes;
hasInnerBlocks: boolean;
} ): ProductCollectionUIStatesInEditor => {
const isInRequiredLocation = usesReference?.includes( location.type );
const isCollectionSelected = !! attributes.collection;
/**
* Case 1: Product context picker
*/
const isProductContextRequired = usesReference?.includes( 'product' );
const isProductContextSelected =
( attributes.query?.productReference ?? null ) !== null;
if (
isCollectionSelected &&
isProductContextRequired &&
! isInRequiredLocation &&
! isProductContextSelected
) {
return ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER;
}
/**
* Case 2: Preview mode - based on `usesReference` value
*/
if ( isInRequiredLocation ) {
/**
* Block shouldn't be in preview mode when:
* 1. Current location is archive and termId is available.
* 2. Current location is product and productId is available.
*
* Because in these cases, we have required context on the editor side.
*/
const isArchiveLocationWithTermId =
location.type === LocationType.Archive &&
( location.sourceData?.termId ?? null ) !== null;
const isProductLocationWithProductId =
location.type === LocationType.Product &&
( location.sourceData?.productId ?? null ) !== null;
if (
! isArchiveLocationWithTermId &&
! isProductLocationWithProductId
) {
return ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW;
}
}
/**
* Case 3: Collection chooser
*/
if ( ! hasInnerBlocks && ! isCollectionSelected ) {
return ProductCollectionUIStatesInEditor.COLLECTION_PICKER;
}
return ProductCollectionUIStatesInEditor.VALID;
};
export const useSetPreviewState = ( {
setPreviewState,
location,
attributes,
setAttributes,
usesReference,
isUsingReferencePreviewMode,
}: {
setPreviewState?: SetPreviewState | undefined;
location: WooCommerceBlockLocation;
@ -231,6 +272,7 @@ export const useSetPreviewState = ( {
attributes: Partial< ProductCollectionAttributes >
) => void;
usesReference?: string[] | undefined;
isUsingReferencePreviewMode: boolean;
} ) => {
const setState = ( newPreviewState: PreviewState ) => {
setAttributes( {
@ -240,8 +282,6 @@ export const useSetPreviewState = ( {
},
} );
};
const isCollectionUsesReference =
usesReference && usesReference?.length > 0;
/**
* When usesReference is available on Frontend but not on Editor side,
@ -249,10 +289,10 @@ export const useSetPreviewState = ( {
*/
const usesReferencePreviewMessage = getUsesReferencePreviewMessage(
location,
usesReference
isUsingReferencePreviewMode
);
useLayoutEffect( () => {
if ( isCollectionUsesReference ) {
if ( isUsingReferencePreviewMode ) {
setAttributes( {
__privatePreviewState: {
isPreview: usesReferencePreviewMessage.length > 0,
@ -263,12 +303,12 @@ export const useSetPreviewState = ( {
}, [
setAttributes,
usesReferencePreviewMessage,
isCollectionUsesReference,
isUsingReferencePreviewMode,
] );
// Running setPreviewState function provided by Collection, if it exists.
useLayoutEffect( () => {
if ( ! setPreviewState && ! isCollectionUsesReference ) {
if ( ! setPreviewState && ! isUsingReferencePreviewMode ) {
return;
}
@ -294,11 +334,14 @@ export const useSetPreviewState = ( {
* - Products by tag
* - Products by attribute
*/
const termId =
location.type === LocationType.Archive
? location.sourceData?.termId
: null;
useLayoutEffect( () => {
if ( ! setPreviewState && ! isCollectionUsesReference ) {
if ( ! setPreviewState && ! isUsingReferencePreviewMode ) {
const isGenericArchiveTemplate =
location.type === LocationType.Archive &&
location.sourceData?.termId === null;
location.type === LocationType.Archive && termId === null;
setAttributes( {
__privatePreviewState: {
@ -315,11 +358,11 @@ export const useSetPreviewState = ( {
}, [
attributes?.query?.inherit,
usesReferencePreviewMessage,
location.sourceData?.termId,
termId,
location.type,
setAttributes,
setPreviewState,
isCollectionUsesReference,
isUsingReferencePreviewMode,
] );
};
@ -356,3 +399,35 @@ export const getDefaultProductCollection = () =>
},
createBlocksFromInnerBlocksTemplate( INNER_BLOCKS_TEMPLATE )
);
export const useGetProduct = ( productId: number | undefined ) => {
const [ product, setProduct ] = useState< ProductResponseItem | null >(
null
);
const [ isLoading, setIsLoading ] = useState< boolean >( false );
useEffect( () => {
const fetchProduct = async () => {
if ( productId ) {
setIsLoading( true );
try {
const fetchedProduct = ( await getProduct(
productId
) ) as ProductResponseItem;
setProduct( fetchedProduct );
} catch ( error ) {
setProduct( null );
} finally {
setIsLoading( false );
}
} else {
setProduct( null );
setIsLoading( false );
}
};
fetchProduct();
}, [ productId ] );
return { product, isLoading };
};

View File

@ -266,7 +266,7 @@ const ProductTemplateEdit = (
products: getEntityRecords( 'postType', postType, {
...query,
...restQueryArgs,
location,
productCollectionLocation: location,
productCollectionQueryContext,
previewState: __privateProductCollectionPreviewState,
/**

View File

@ -63,17 +63,65 @@ const prepareIsInGenericTemplate =
( entitySlug: string ): boolean =>
templateSlug === entitySlug;
export type WooCommerceBlockLocation = ReturnType<
typeof createLocationObject
>;
interface WooCommerceBaseLocation {
type: LocationType;
sourceData?: object | undefined;
}
const createLocationObject = (
type: LocationType,
sourceData: Record< string, unknown > = {}
) => ( {
type,
sourceData,
} );
interface ProductLocation extends WooCommerceBaseLocation {
type: LocationType.Product;
sourceData?:
| {
productId: number;
}
| undefined;
}
interface ArchiveLocation extends WooCommerceBaseLocation {
type: LocationType.Archive;
sourceData?:
| {
taxonomy: string;
termId: number;
}
| undefined;
}
interface CartLocation extends WooCommerceBaseLocation {
type: LocationType.Cart;
sourceData?:
| {
productIds: number[];
}
| undefined;
}
interface OrderLocation extends WooCommerceBaseLocation {
type: LocationType.Order;
sourceData?:
| {
orderId: number;
}
| undefined;
}
interface SiteLocation extends WooCommerceBaseLocation {
type: LocationType.Site;
sourceData?: object | undefined;
}
export type WooCommerceBlockLocation =
| ProductLocation
| ArchiveLocation
| CartLocation
| OrderLocation
| SiteLocation;
const createLocationObject = ( type: LocationType, sourceData: object = {} ) =>
( {
type,
sourceData,
} as WooCommerceBlockLocation );
type ContextProperties = {
templateSlug: string;
@ -83,7 +131,7 @@ type ContextProperties = {
export const useGetLocation = < T, >(
context: Context< T & ContextProperties >,
clientId: string
) => {
): WooCommerceBlockLocation => {
const templateSlug = context.templateSlug || '';
const postId = context.postId || null;

View File

@ -62,6 +62,16 @@ interface ProductControlProps {
* Whether to show variations in the list of items available.
*/
showVariations?: boolean;
/**
* Different messages to display in the component.
* If any of the messages are not provided, the default message will be used.
*/
messages?: {
list?: string;
noItems?: string;
search?: string;
updated?: string;
};
}
const messages = {
@ -188,7 +198,7 @@ const ProductControl = (
} else if ( showVariations ) {
return renderItemWithVariations;
}
return () => null;
return undefined;
};
if ( error ) {
@ -216,7 +226,10 @@ const ProductControl = (
onChange={ onChange }
renderItem={ getRenderItemFunc() }
onSearch={ onSearch }
messages={ messages }
messages={ {
...messages,
...props.messages,
} }
isHierarchical
/>
);

View File

@ -9,6 +9,7 @@ import { test as base, expect } from '@woocommerce/e2e-utils';
*/
import ProductCollectionPage, {
BLOCK_LABELS,
Collections,
SELECTORS,
} from './product-collection.page';
@ -402,7 +403,7 @@ test.describe( 'Product Collection', () => {
} );
} );
test.describe( 'Location is recognised', () => {
test.describe( 'Location is recognized', () => {
const filterRequest = ( request: Request ) => {
const url = request.url();
return (
@ -418,7 +419,9 @@ test.describe( 'Product Collection', () => {
return (
url.includes( 'wp/v2/product' ) &&
searchParams.get( 'isProductCollectionBlock' ) === 'true' &&
!! searchParams.get( `location[sourceData][productId]` )
!! searchParams.get(
`productCollectionLocation[sourceData][productId]`
)
);
};
@ -430,26 +433,30 @@ test.describe( 'Product Collection', () => {
if ( locationType === 'product' ) {
return {
type: searchParams.get( 'location[type]' ),
type: searchParams.get( 'productCollectionLocation[type]' ),
productId: searchParams.get(
`location[sourceData][productId]`
`productCollectionLocation[sourceData][productId]`
),
};
}
if ( locationType === 'archive' ) {
return {
type: searchParams.get( 'location[type]' ),
type: searchParams.get( 'productCollectionLocation[type]' ),
taxonomy: searchParams.get(
`location[sourceData][taxonomy]`
`productCollectionLocation[sourceData][taxonomy]`
),
termId: searchParams.get(
`productCollectionLocation[sourceData][termId]`
),
termId: searchParams.get( `location[sourceData][termId]` ),
};
}
return {
type: searchParams.get( 'location[type]' ),
sourceData: searchParams.get( `location[sourceData]` ),
type: searchParams.get( 'productCollectionLocation[type]' ),
sourceData: searchParams.get(
`productCollectionLocation[sourceData]`
),
};
};
@ -482,10 +489,10 @@ test.describe( 'Product Collection', () => {
pageObject.BLOCK_NAME
);
const locationReuqestPromise =
const locationRequestPromise =
page.waitForRequest( filterProductRequest );
await pageObject.chooseCollectionInTemplate( 'featured' );
const locationRequest = await locationReuqestPromise;
const locationRequest = await locationRequestPromise;
const { type, productId } = getLocationDetailsFromRequest(
locationRequest,
@ -961,3 +968,309 @@ test.describe( 'Product Collection', () => {
} );
} );
} );
test.describe( 'Testing "usesReference" argument in "registerProductCollection"', () => {
const MY_REGISTERED_COLLECTIONS = {
myCustomCollectionWithProductContext: {
name: 'My Custom Collection - Product Context',
label: 'Block: My Custom Collection - Product Context',
previewLabelTemplate: [ 'woocommerce/woocommerce//single-product' ],
shouldShowProductPicker: true,
},
myCustomCollectionWithCartContext: {
name: 'My Custom Collection - Cart Context',
label: 'Block: My Custom Collection - Cart Context',
previewLabelTemplate: [ 'woocommerce/woocommerce//page-cart' ],
shouldShowProductPicker: false,
},
myCustomCollectionWithOrderContext: {
name: 'My Custom Collection - Order Context',
label: 'Block: My Custom Collection - Order Context',
previewLabelTemplate: [
'woocommerce/woocommerce//order-confirmation',
],
shouldShowProductPicker: false,
},
myCustomCollectionWithArchiveContext: {
name: 'My Custom Collection - Archive Context',
label: 'Block: My Custom Collection - Archive Context',
previewLabelTemplate: [
'woocommerce/woocommerce//taxonomy-product_cat',
],
shouldShowProductPicker: false,
},
myCustomCollectionMultipleContexts: {
name: 'My Custom Collection - Multiple Contexts',
label: 'Block: My Custom Collection - Multiple Contexts',
previewLabelTemplate: [
'woocommerce/woocommerce//single-product',
'woocommerce/woocommerce//order-confirmation',
],
shouldShowProductPicker: true,
},
};
// Activate plugin which registers custom product collections
test.beforeEach( async ( { requestUtils } ) => {
await requestUtils.activatePlugin(
'register-product-collection-tester'
);
} );
Object.entries( MY_REGISTERED_COLLECTIONS ).forEach(
( [ key, collection ] ) => {
for ( const template of collection.previewLabelTemplate ) {
test( `Collection "${ collection.name }" should show preview label in "${ template }"`, async ( {
pageObject,
editor,
} ) => {
await pageObject.goToEditorTemplate( template );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate(
key as Collections
);
const block = editor.canvas.getByLabel( collection.label );
const previewButtonLocator = block.getByTestId(
SELECTORS.previewButtonTestID
);
await expect( previewButtonLocator ).toBeVisible();
} );
}
test( `Collection "${ collection.name }" should not show preview label in a post`, async ( {
pageObject,
editor,
admin,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost( key as Collections );
// Check visibility of product picker
const editorProductPicker = editor.canvas.locator(
SELECTORS.productPicker
);
const expectedVisibility = collection.shouldShowProductPicker
? 'toBeVisible'
: 'toBeHidden';
await expect( editorProductPicker )[ expectedVisibility ]();
if ( collection.shouldShowProductPicker ) {
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
}
// At this point, the product picker should be hidden
await expect( editorProductPicker ).toBeHidden();
// Check visibility of preview label
const block = editor.canvas.getByLabel( collection.label );
const previewButtonLocator = block.getByTestId(
SELECTORS.previewButtonTestID
);
await expect( previewButtonLocator ).toBeHidden();
} );
test( `Collection "${ collection.name }" should not show preview label in Product Catalog template`, async ( {
pageObject,
editor,
} ) => {
await pageObject.goToProductCatalogAndInsertCollection(
key as Collections
);
const block = editor.canvas.getByLabel( collection.label );
const previewButtonLocator = block.getByTestId(
SELECTORS.previewButtonTestID
);
await expect( previewButtonLocator ).toBeHidden();
} );
}
);
} );
test.describe( 'Product picker', () => {
const MY_REGISTERED_COLLECTIONS_THAT_NEEDS_PRODUCT = {
myCustomCollectionWithProductContext: {
name: 'My Custom Collection - Product Context',
label: 'Block: My Custom Collection - Product Context',
collection:
'woocommerce/product-collection/my-custom-collection-product-context',
},
myCustomCollectionMultipleContexts: {
name: 'My Custom Collection - Multiple Contexts',
label: 'Block: My Custom Collection - Multiple Contexts',
collection:
'woocommerce/product-collection/my-custom-collection-multiple-contexts',
},
};
// Activate plugin which registers custom product collections
test.beforeEach( async ( { requestUtils } ) => {
await requestUtils.activatePlugin(
'register-product-collection-tester'
);
} );
Object.entries( MY_REGISTERED_COLLECTIONS_THAT_NEEDS_PRODUCT ).forEach(
( [ key, collection ] ) => {
test( `For collection "${ collection.name }" - manually selected product reference should be available on Frontend in a post`, async ( {
pageObject,
admin,
page,
editor,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost( key as Collections );
// Verify that product picker is shown in Editor
const editorProductPicker = editor.canvas.locator(
SELECTORS.productPicker
);
await expect( editorProductPicker ).toBeVisible();
// Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
await expect( editorProductPicker ).toBeHidden();
// On Frontend, verify that product reference is a number
await pageObject.publishAndGoToFrontend();
const collectionWithProductContext = page.locator(
`[data-collection="${ collection.collection }"]`
);
const queryAttribute = JSON.parse(
( await collectionWithProductContext.getAttribute(
'data-query'
) ) || '{}'
);
expect( typeof queryAttribute?.productReference ).toBe(
'number'
);
} );
test( `For collection "${ collection.name }" - changing product using inspector control`, async ( {
pageObject,
admin,
page,
editor,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost( key as Collections );
// Verify that product picker is shown in Editor
const editorProductPicker = editor.canvas.locator(
SELECTORS.productPicker
);
await expect( editorProductPicker ).toBeVisible();
// Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
await expect( editorProductPicker ).toBeHidden();
// Verify that Album is selected
await expect(
admin.page.locator( SELECTORS.linkedProductControl.button )
).toContainText( 'Album' );
// Change product using inspector control to Beanie
await admin.page
.locator( SELECTORS.linkedProductControl.button )
.click();
await admin.page
.locator( SELECTORS.linkedProductControl.popoverContent )
.getByLabel( 'Beanie', { exact: true } )
.click();
await expect(
admin.page.locator( SELECTORS.linkedProductControl.button )
).toContainText( 'Beanie' );
// On Frontend, verify that product reference is a number
await pageObject.publishAndGoToFrontend();
const collectionWithProductContext = page.locator(
`[data-collection="${ collection.collection }"]`
);
const queryAttribute = JSON.parse(
( await collectionWithProductContext.getAttribute(
'data-query'
) ) || '{}'
);
expect( typeof queryAttribute?.productReference ).toBe(
'number'
);
} );
test( `For collection "${ collection.name }" - product picker shouldn't be shown in Single Product template`, async ( {
pageObject,
admin,
editor,
} ) => {
await admin.visitSiteEditor( {
postId: `woocommerce/woocommerce//single-product`,
postType: 'wp_template',
canvas: 'edit',
} );
await editor.canvas.locator( 'body' ).click();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate(
key as Collections
);
const editorProductPicker = editor.canvas.locator(
SELECTORS.productPicker
);
await expect( editorProductPicker ).toBeHidden();
} );
}
);
test( 'Product picker should work as expected while changing collection using "Choose collection" button from Toolbar', async ( {
pageObject,
admin,
editor,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost(
'myCustomCollectionWithProductContext'
);
// Verify that product picker is shown in Editor
const editorProductPicker = editor.canvas.locator(
SELECTORS.productPicker
);
await expect( editorProductPicker ).toBeVisible();
// Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
await expect( editorProductPicker ).toBeHidden();
// Change collection using Toolbar
await pageObject.changeCollectionUsingToolbar(
'myCustomCollectionMultipleContexts'
);
await expect( editorProductPicker ).toBeVisible();
// Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
await expect( editorProductPicker ).toBeHidden();
// Product picker should be hidden for collections that don't need product
await pageObject.changeCollectionUsingToolbar( 'featured' );
await expect( editorProductPicker ).toBeHidden();
} );
} );

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { Locator, Page } from '@playwright/test';
import { FrameLocator, Locator, Page } from '@playwright/test';
import { Editor, Admin } from '@woocommerce/e2e-utils';
import { BlockRepresentation } from '@wordpress/e2e-test-utils-playwright/build-types/editor/insert-block';
@ -62,6 +62,12 @@ export const SELECTORS = {
previewButtonTestID: 'product-collection-preview-button',
collectionPlaceholder:
'[data-type="woocommerce/product-collection"] .components-placeholder',
productPicker: '.wc-blocks-product-collection__editor-product-picker',
linkedProductControl: {
button: '.wc-block-product-collection-linked-product-control__button',
popoverContent:
'.wc-block-product-collection-linked-product__popover-content',
},
};
export type Collections =
@ -200,10 +206,31 @@ class ProductCollectionPage {
}
}
async chooseProductInEditorProductPickerIfAvailable(
pageReference: Page | FrameLocator
) {
const editorProductPicker = pageReference.locator(
SELECTORS.productPicker
);
if ( await editorProductPicker.isVisible() ) {
await editorProductPicker
.locator( 'label' )
.filter( {
hasText: 'Album',
} )
.click();
}
}
async createNewPostAndInsertBlock( collection?: Collections ) {
await this.admin.createNewPost();
await this.insertProductCollection();
await this.chooseCollectionInPost( collection );
// If product picker is available, choose a product.
await this.chooseProductInEditorProductPickerIfAvailable(
this.admin.page
);
await this.refreshLocators( 'editor' );
await this.editor.openDocumentSettingsSidebar();
}
@ -345,6 +372,10 @@ class ProductCollectionPage {
await this.editor.canvas.locator( 'body' ).click();
await this.insertProductCollection();
await this.chooseCollectionInTemplate( collection );
// If product picker is available, choose a product.
await this.chooseProductInEditorProductPickerIfAvailable(
this.editor.canvas
);
await this.refreshLocators( 'editor' );
}
@ -571,6 +602,30 @@ class ProductCollectionPage {
.click();
}
async changeCollectionUsingToolbar( collection: Collections ) {
// Click "Choose collection" button in the toolbar.
await this.admin.page
.getByRole( 'toolbar', { name: 'Block Tools' } )
.getByRole( 'button', { name: 'Choose collection' } )
.click();
// Select the collection from the modal.
const collectionChooserModal = this.admin.page.locator(
'.wc-blocks-product-collection__modal'
);
await collectionChooserModal
.getByRole( 'button', {
name: collectionToButtonNameMap[ collection ],
} )
.click();
await collectionChooserModal
.getByRole( 'button', {
name: 'Continue',
} )
.click();
}
async setDisplaySettings( {
itemsPerPage,
offset,

View File

@ -0,0 +1,4 @@
Significance: major
Type: add
Product Collection - Show product picker in Editor when collection requires a product but not available <details> A collection can define if it requires a product context. This can be done using `usesReference` argument i.e. ```tsx __experimentalRegisterProductCollection({ ..., usesReference: ['product'], ) ``` When product context doesn't exist in current template/page/post etc. then we show product picker in Editor. This way, merchant can manually provide a product context to the collection.

View File

@ -0,0 +1,4 @@
Significance: major
Type: add
Product Collection - Implement Inspector control to change selected product

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Comment: Change to underlying Settings feature usage

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix Settings Save button when WP List Tables are present

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Added more paths to remote logger query param whitelist

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix My Account block icon being too small when inserted via block hooks

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Add check to ensure themes API is safe

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix badge number fails to match the number of unfinished tasks

View File

@ -0,0 +1,4 @@
Significance: minor
Type: enhancement
Add query params masking to remote logger

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update WooCommerce Shipping Promo Banner to install the latest version of WooCommerce Shipping instead of WCS&T.

View File

@ -98,9 +98,10 @@
event
) {
// Toggling WP List Table checkboxes should not trigger navigation warnings.
// Theses checkboxes only select/unselect rows, they don't change the form.
if (
$check_column.length &&
$check_column.has( event.target )
$check_column.has( event.target ).length
) {
return;
}

View File

@ -27,7 +27,6 @@ class Features {
*/
protected static $optional_features = array(
'navigation' => array( 'default' => 'no' ),
'settings' => array( 'default' => 'no' ),
'analytics' => array( 'default' => 'yes' ),
'remote-inbox-notifications' => array( 'default' => 'yes' ),
);
@ -353,7 +352,6 @@ class Features {
'Automattic\WooCommerce\Internal\Admin\Marketing' => 'Automattic\WooCommerce\Admin\Features\Marketing',
'Automattic\WooCommerce\Internal\Admin\MobileAppBanner' => 'Automattic\WooCommerce\Admin\Features\MobileAppBanner',
'Automattic\WooCommerce\Internal\Admin\RemoteInboxNotifications' => 'Automattic\WooCommerce\Admin\Features\RemoteInboxNotifications',
'Automattic\WooCommerce\Internal\Admin\SettingsNavigationFeature' => 'Automattic\WooCommerce\Admin\Features\Settings',
'Automattic\WooCommerce\Internal\Admin\ShippingLabelBanner' => 'Automattic\WooCommerce\Admin\Features\ShippingLabelBanner',
'Automattic\WooCommerce\Internal\Admin\ShippingLabelBannerDisplayRules' => 'Automattic\WooCommerce\Admin\Features\ShippingLabelBannerDisplayRules',
'Automattic\WooCommerce\Internal\Admin\WcPayWelcomePage' => 'Automattic\WooCommerce\Admin\Features\WcPayWelcomePage',

View File

@ -1,21 +1,16 @@
<?php
<?php //phpcs:ignore Generic.PHP.RequireStrictTypes.MissingDeclaration
/**
* WooCommerce Settings.
*/
namespace Automattic\WooCommerce\Internal\Admin;
namespace Automattic\WooCommerce\Admin\Features;
use Automattic\WooCommerce\Admin\PageController;
/**
* Contains backend logic for the Settings feature.
*/
class SettingsNavigationFeature {
/**
* Option name used to toggle this feature.
*/
const TOGGLE_OPTION_NAME = 'woocommerce_settings_enabled';
class Settings {
/**
* Class instance.
*
@ -41,12 +36,6 @@ class SettingsNavigationFeature {
return;
}
add_filter( 'woocommerce_settings_features', array( $this, 'add_feature_toggle' ) );
if ( 'yes' !== get_option( 'woocommerce_settings_enabled', 'no' ) ) {
return;
}
add_filter( 'woocommerce_admin_shared_settings', array( __CLASS__, 'add_component_settings' ) );
// Run this after the original WooCommerce settings have been added.
add_action( 'admin_menu', array( $this, 'register_pages' ), 60 );
@ -75,26 +64,6 @@ class SettingsNavigationFeature {
return $settings;
}
/**
* Add the feature toggle to the features settings.
*
* @param array $features Feature sections.
* @return array
*/
public static function add_feature_toggle( $features ) {
$features[] = array(
'title' => __( 'Settings', 'woocommerce' ),
'desc' => __(
'Adds the new WooCommerce settings UI.',
'woocommerce'
),
'id' => 'woocommerce_settings_enabled',
'type' => 'checkbox',
);
return $features;
}
/**
* Registers settings pages.
*/

View File

@ -68,6 +68,7 @@ class CustomerAccount extends AbstractBlock {
public function modify_hooked_block_attributes( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ) {
$parsed_hooked_block['attrs']['displayStyle'] = 'icon_only';
$parsed_hooked_block['attrs']['iconStyle'] = 'line';
$parsed_hooked_block['attrs']['iconClass'] = 'wc-block-customer-account__account-icon';
/*
* The Mini Cart block (which is hooked into the header) has a margin of 0.5em on the left side.

View File

@ -78,10 +78,10 @@ class OnboardingThemes {
usort(
$themes,
function ( $product_1, $product_2 ) {
if ( ! property_exists( $product_1, 'id' ) || ! property_exists( $product_1, 'slug' ) ) {
if ( ! is_object( $product_1 ) || ! property_exists( $product_1, 'id' ) || ! property_exists( $product_1, 'slug' ) ) {
return 1;
}
if ( ! property_exists( $product_2, 'id' ) || ! property_exists( $product_2, 'slug' ) ) {
if ( ! is_object( $product_2 ) || ! property_exists( $product_2, 'id' ) || ! property_exists( $product_2, 'slug' ) ) {
return 1;
}
if ( in_array( 'Storefront', array( $product_1->slug, $product_2->slug ), true ) ) {
@ -110,16 +110,22 @@ class OnboardingThemes {
$themes = array();
if ( ! is_wp_error( $theme_data ) ) {
$theme_data = json_decode( $theme_data['body'] );
$woo_themes = property_exists( $theme_data, 'products' ) ? $theme_data->products : array();
$sorted_themes = self::sort_woocommerce_themes( $woo_themes );
$theme_data = json_decode( $theme_data['body'] );
foreach ( $sorted_themes as $theme ) {
$slug = sanitize_title_with_dashes( $theme->slug );
$themes[ $slug ] = (array) $theme;
$themes[ $slug ]['is_installed'] = false;
$themes[ $slug ]['has_woocommerce_support'] = true;
$themes[ $slug ]['slug'] = $slug;
if ( $theme_data ) {
$woo_themes = property_exists( $theme_data, 'products' ) ? $theme_data->products : array();
$sorted_themes = self::sort_woocommerce_themes( $woo_themes );
foreach ( $sorted_themes as $theme ) {
if ( ! isset( $theme->slug ) ) {
continue;
}
$slug = sanitize_title_with_dashes( $theme->slug );
$themes[ $slug ] = (array) $theme;
$themes[ $slug ]['is_installed'] = false;
$themes[ $slug ]['has_woocommerce_support'] = true;
$themes[ $slug ]['slug'] = $slug;
}
}
}

View File

@ -6,6 +6,8 @@
namespace Automattic\WooCommerce\Internal\Admin;
use Automattic\Jetpack\Connection\Manager as Jetpack_Connection_Manager;
use Automattic\WooCommerce\Utilities\OrderUtil;
use function WP_CLI\Utils\get_plugin_name;
/**
* Shows print shipping label banner on edit order page.
@ -19,6 +21,9 @@ class ShippingLabelBanner {
*/
private $shipping_label_banner_display_rules;
private const MIN_COMPATIBLE_WCST_VERSION = '2.7.0';
private const MIN_COMPATIBLE_WCSHIPPING_VERSION = '1.1.0';
/**
* Constructor
*/
@ -36,40 +41,26 @@ class ShippingLabelBanner {
*/
private function should_show_meta_box() {
if ( ! $this->shipping_label_banner_display_rules ) {
$jetpack_version = null;
$jetpack_connected = null;
$wcs_version = null;
$wcs_tos_accepted = null;
if ( defined( 'JETPACK__VERSION' ) ) {
$jetpack_version = JETPACK__VERSION;
}
$dotcom_connected = null;
$wcs_version = null;
if ( class_exists( Jetpack_Connection_Manager::class ) ) {
$jetpack_connected = ( new Jetpack_Connection_Manager() )->has_connected_owner();
$dotcom_connected = ( new Jetpack_Connection_Manager() )->has_connected_owner();
}
if ( class_exists( '\WC_Connect_Loader' ) ) {
$wcs_version = \WC_Connect_Loader::get_wcs_version();
}
if ( class_exists( '\WC_Connect_Options' ) ) {
$wcs_tos_accepted = \WC_Connect_Options::get_option( 'tos_accepted' );
if ( class_exists( '\Automattic\WCShipping\Utils' ) ) {
$wcs_version = \Automattic\WCShipping\Utils::get_wcshipping_version();
}
$incompatible_plugins = class_exists( '\WC_Shipping_Fedex_Init' ) ||
class_exists( '\WC_Shipping_UPS_Init' ) ||
class_exists( '\WC_Integration_ShippingEasy' ) ||
class_exists( '\WC_ShipStation_Integration' ) ||
class_exists( '\Automattic\WCShipping\Loader' ) ||
class_exists( '\Automattic\WCTax\Loader' );
class_exists( '\WC_ShipStation_Integration' );
$this->shipping_label_banner_display_rules =
new ShippingLabelBannerDisplayRules(
$jetpack_version,
$jetpack_connected,
$dotcom_connected,
$wcs_version,
$wcs_tos_accepted,
$incompatible_plugins
);
}
@ -79,15 +70,12 @@ class ShippingLabelBanner {
/**
* Add metabox to order page.
*
* @param string $post_type current post type.
* @param \WP_Post $post Current post object.
*/
public function add_meta_boxes( $post_type, $post ) {
if ( 'shop_order' !== $post_type ) {
public function add_meta_boxes() {
if ( ! OrderUtil::is_order_edit_screen() ) {
return;
}
$order = wc_get_order( $post );
if ( $this->should_show_meta_box() ) {
add_meta_box(
'woocommerce-admin-print-label',
@ -98,8 +86,6 @@ class ShippingLabelBanner {
'high',
array(
'context' => 'shipping_label',
'order' => $post->ID,
'items' => $this->count_shippable_items( $order ),
)
);
add_action( 'admin_enqueue_scripts', array( $this, 'add_print_shipping_label_script' ) );
@ -132,14 +118,30 @@ class ShippingLabelBanner {
public function add_print_shipping_label_script( $hook ) {
WCAdminAssets::register_style( 'print-shipping-label-banner', 'style', array( 'wp-components' ) );
WCAdminAssets::register_script( 'wp-admin-scripts', 'print-shipping-label-banner', true );
$wcst_version = null;
$wcshipping_installed_version = null;
$order = wc_get_order();
if ( class_exists( '\WC_Connect_Loader' ) ) {
$wcst_version = \WC_Connect_Loader::get_wcs_version();
}
$wc_shipping_plugin_file = WP_PLUGIN_DIR . '/woocommerce-shipping/woocommerce-shipping.php';
if ( file_exists( $wc_shipping_plugin_file ) ) {
$plugin_data = get_plugin_data( $wc_shipping_plugin_file );
$wcshipping_installed_version = $plugin_data['Version'];
}
$payload = array(
'nonce' => wp_create_nonce( 'wp_rest' ),
'baseURL' => get_rest_url(),
'wcs_server_connection' => true,
// If WCS&T is not installed, it's considered compatible.
'is_wcst_compatible' => $wcst_version ? (int) version_compare( $wcst_version, self::MIN_COMPATIBLE_WCST_VERSION, '>=' ) : 1,
'order_id' => $order ? $order->get_id() : null,
// The banner is shown if the plugin is installed but not active, so we need to check if the installed version is compatible.
'is_incompatible_wcshipping_installed' => $wcshipping_installed_version ?
(int) version_compare( $wcshipping_installed_version, self::MIN_COMPATIBLE_WCSHIPPING_VERSION, '<' )
: 0,
);
wp_localize_script( 'print-shipping-label-banner', 'wcConnectData', $payload );
wp_localize_script( 'wc-admin-print-shipping-label-banner', 'wcShippingCoreData', $payload );
}
/**

View File

@ -6,24 +6,23 @@
namespace Automattic\WooCommerce\Internal\Admin;
/**
* Determines whether or not the Shipping Label Banner should be displayed
* Determines whether the Shipping Label Banner should be displayed
*/
class ShippingLabelBannerDisplayRules {
/**
* Holds the installed Jetpack version.
*
* @var string
*/
private $jetpack_version;
/**
* Whether or not the installed Jetpack is connected.
* Whether the site is connected to wordpress.com.
*
* @var bool
*/
private $jetpack_connected;
private $dotcom_connected;
/**
* Whether installed plugins are incompatible with the banner.
*
* @var bool
*/
private $no_incompatible_plugins_installed;
/**
* Holds the installed WooCommerce Shipping & Tax version.
@ -32,34 +31,6 @@ class ShippingLabelBannerDisplayRules {
*/
private $wcs_version;
/**
* Whether or not there're plugins installed incompatible with the banner.
*
* @var bool
*/
private $no_incompatible_plugins_installed;
/**
* Whether or not the WooCommerce Shipping & Tax ToS has been accepted.
*
* @var bool
*/
private $wcs_tos_accepted;
/**
* Minimum supported Jetpack version.
*
* @var string
*/
private $min_jetpack_version = '4.4';
/**
* Minimum supported WooCommerce Shipping & Tax version.
*
* @var string
*/
private $min_wcs_version = '1.22.5';
/**
* Supported countries by USPS, see: https://webpmt.usps.gov/pmt010.cfm
*
@ -78,17 +49,13 @@ class ShippingLabelBannerDisplayRules {
/**
* Constructor.
*
* @param string $jetpack_version Installed Jetpack version to check.
* @param bool $jetpack_connected Is Jetpack connected?.
* @param string $wcs_version Installed WooCommerce Shipping & Tax version to check.
* @param bool $wcs_tos_accepted WooCommerce Shipping & Tax Terms of Service accepted?.
* @param bool $incompatible_plugins_installed Are there any incompatible plugins installed?.
* @param bool $dotcom_connected Is site connected to wordpress.com?.
* @param string|null $wcs_version Installed WooCommerce Shipping version to check, null if not installed.
* @param bool $incompatible_plugins_installed Are there any incompatible plugins installed?.
*/
public function __construct( $jetpack_version, $jetpack_connected, $wcs_version, $wcs_tos_accepted, $incompatible_plugins_installed ) {
$this->jetpack_version = $jetpack_version;
$this->jetpack_connected = $jetpack_connected;
public function __construct( $dotcom_connected, $wcs_version, $incompatible_plugins_installed ) {
$this->dotcom_connected = $dotcom_connected;
$this->wcs_version = $wcs_version;
$this->wcs_tos_accepted = $wcs_tos_accepted;
$this->no_incompatible_plugins_installed = ! $incompatible_plugins_installed;
}
@ -97,15 +64,11 @@ class ShippingLabelBannerDisplayRules {
*/
public function should_display_banner() {
return $this->banner_not_dismissed() &&
$this->jetpack_installed_and_active() &&
$this->jetpack_up_to_date() &&
$this->jetpack_connected &&
$this->dotcom_connected &&
$this->no_incompatible_plugins_installed &&
$this->order_has_shippable_products() &&
$this->store_in_us_and_usd() &&
( $this->wcs_not_installed() || (
$this->wcs_up_to_date() && ! $this->wcs_tos_accepted
) );
$this->wcs_not_installed();
}
/**
@ -129,36 +92,13 @@ class ShippingLabelBannerDisplayRules {
return ! $dismissed_for_good && ! $dismissed_24h;
}
/**
* Checks if jetpack is installed and active.
*
* @return bool
*/
private function jetpack_installed_and_active() {
return ! ! $this->jetpack_version;
}
/**
* Checks if Jetpack version is supported.
*
* @return bool
*/
private function jetpack_up_to_date() {
return version_compare( $this->jetpack_version, $this->min_jetpack_version, '>=' );
}
/**
* Checks if there's a shippable product in the current order.
*
* @return bool
*/
private function order_has_shippable_products() {
$post = get_post();
if ( ! $post ) {
return false;
}
$order = wc_get_order( get_post()->ID );
$order = wc_get_order();
if ( ! $order ) {
return false;
@ -197,11 +137,4 @@ class ShippingLabelBannerDisplayRules {
private function wcs_not_installed() {
return ! $this->wcs_version;
}
/**
* Checks if WooCommerce Shipping & Tax is up to date.
*/
private function wcs_up_to_date() {
return $this->wcs_version && version_compare( $this->wcs_version, $this->min_wcs_version, '>=' );
}
}

View File

@ -70,7 +70,7 @@ class RemoteLogger extends \WC_Log_Handler {
'wc_version' => WC()->version,
'php_version' => phpversion(),
'wp_version' => get_bloginfo( 'version' ),
'request_uri' => filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL ),
'request_uri' => $this->sanitize_request_uri( filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL ) ),
),
);
@ -431,4 +431,63 @@ class RemoteLogger extends \WC_Log_Handler {
protected function is_dev_or_local_environment() {
return in_array( wp_get_environment_type(), array( 'development', 'local' ), true );
}
/**
* Sanitize the request URI to only allow certain query parameters.
*
* @param string $request_uri The request URI to sanitize.
* @return string The sanitized request URI.
*/
private function sanitize_request_uri( $request_uri ) {
$default_whitelist = array(
'path',
'page',
'step',
'task',
'tab',
'section',
'status',
'post_type',
'taxonomy',
'action',
);
/**
* Filter to allow other plugins to whitelist request_uri query parameter values for unmasked remote logging.
*
* @since 9.4.0
*
* @param string $default_whitelist The default whitelist of query parameters.
*/
$whitelist = apply_filters( 'woocommerce_remote_logger_request_uri_whitelist', $default_whitelist );
$parsed_url = wp_parse_url( $request_uri );
if ( ! isset( $parsed_url['query'] ) ) {
return $request_uri;
}
parse_str( $parsed_url['query'], $query_params );
foreach ( $query_params as $key => &$value ) {
if ( ! in_array( $key, $whitelist, true ) ) {
$value = 'xxxxxx';
}
}
$parsed_url['query'] = http_build_query( $query_params );
return $this->build_url( $parsed_url );
}
/**
* Build a URL from its parsed components.
*
* @param array $parsed_url The parsed URL components.
* @return string The built URL.
*/
private function build_url( $parsed_url ) {
$path = $parsed_url['path'] ?? '';
$query = isset( $parsed_url['query'] ) ? "?{$parsed_url['query']}" : '';
$fragment = isset( $parsed_url['fragment'] ) ? "#{$parsed_url['fragment']}" : '';
return "$path$query$fragment";
}
}

View File

@ -96,7 +96,7 @@ const test = baseTest.extend( {
},
} );
test.describe( 'Restricted coupon management', { tag: '@services' }, () => {
test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
for ( const couponType of Object.keys( couponData ) ) {
test( `can create new ${ couponType } coupon`, async ( {
page,

View File

@ -2,15 +2,19 @@ const { test } = require( '../../../../fixtures/block-editor-fixtures' );
const { expect } = require( '@playwright/test' );
const { clickOnTab } = require( '../../../../utils/simple-products' );
const { api } = require( '../../../../utils' );
const { helpers } = require( '../../../../utils' );
const NEW_EDITOR_ADD_PRODUCT_URL =
'wp-admin/admin.php?page=wc-admin&path=%2Fadd-product';
const isTrackingSupposedToBeEnabled = !! process.env.ENABLE_TRACKING;
const uniqueId = helpers.random();
let categoryId = 0;
const categoryName = `cat_${ uniqueId }`;
const productName = `Product ${ uniqueId }`;
const productData = {
name: `Linked product Name ${ new Date().getTime().toString() }`,
name: `Linked ${ productName }`,
summary: 'This is a product summary',
};
@ -20,27 +24,45 @@ let productId = 0;
test.describe( 'General tab', { tag: '@gutenberg' }, () => {
test.describe( 'Linked product', () => {
test.beforeAll( async () => {
test.beforeAll( async ( { api } ) => {
await api
.post( 'products/categories', {
name: categoryName,
} )
.then( ( response ) => {
categoryId = response.data.id;
} );
for ( let i = 1; i <= 5; i++ ) {
const product = {
name: `Product name ${ i } ${ new Date()
.getTime()
.toString() }`,
productPrice: `${ i }00`,
name: `Product ${ uniqueId } ${ i }`,
regular_price: `${ i }0000`,
sale_price: `${ i }000`,
type: 'simple',
categories: [ { id: categoryId } ],
};
linkedProductsData.push( product );
const id = await api.create.product( product );
productIds.push( id );
await api.post( 'products', product ).then( ( response ) => {
productIds.push( response.data.id );
linkedProductsData.push( product );
} );
}
} );
test.afterAll( async () => {
test.afterAll( async ( { api } ) => {
for ( const aProductId of productIds ) {
await api.deletePost.product( aProductId );
await api.delete( `products/${ aProductId }`, {
force: true,
} );
}
await api.deletePost.product( productId );
await api.delete( `products/${ productId }`, {
force: true,
} );
await api.delete( `products/categories/${ categoryId }`, {
force: true,
} );
} );
test.skip(
isTrackingSupposedToBeEnabled,
'The block product editor is not being tested'
@ -61,7 +83,30 @@ test.describe( 'General tab', { tag: '@gutenberg' }, () => {
.last()
.fill( productData.summary );
// Include in category
await clickOnTab( 'Organization', page );
const waitForCategoriesResponse = page.waitForResponse(
( response ) =>
response.url().includes( '/wp-json/wp/v2/product_cat' ) &&
response.status() === 200
);
await page.getByLabel( 'Categories' ).click();
await waitForCategoriesResponse;
await page.getByLabel( categoryName ).check();
await page.getByLabel( `Remove Uncategorized` ).click();
await expect(
page.getByLabel( `Remove ${ categoryName }` )
).toBeVisible();
const waitForProductsSearchResponse = page.waitForResponse(
( response ) =>
response
.url()
.includes( '/wp-json/wc/v3/products?search' ) &&
response.status() === 200
);
await clickOnTab( 'Linked products', page );
await waitForProductsSearchResponse;
await expect(
page.getByRole( 'heading', {
@ -75,7 +120,7 @@ test.describe( 'General tab', { tag: '@gutenberg' }, () => {
)
.first()
.getByRole( 'combobox' )
.fill( linkedProductsData[ 0 ].name );
.fill( productName );
await page.getByText( linkedProductsData[ 0 ].name ).click();
@ -92,7 +137,7 @@ test.describe( 'General tab', { tag: '@gutenberg' }, () => {
await chooseProductsResponsePromise;
await expect(
page.getByRole( 'row', { name: 'Product name' } )
page.getByRole( 'row', { name: productName } )
).toHaveCount( 4 );
const upsellsRows = page.locator(

View File

@ -130,157 +130,173 @@ test.describe(
} );
} );
test( 'allows customer to calculate Free Shipping in cart block if in Netherlands', async ( {
page,
context,
cartBlockPage,
} ) => {
await context.clearCookies();
test(
'allows customer to calculate Free Shipping in cart block if in Netherlands',
{ tag: [ '@could-be-unit-test' ] },
async ( { page, context, cartBlockPage } ) => {
await context.clearCookies();
await addAProductToCart( page, product1Id );
await page.goto( cartBlockPage.slug );
await addAProductToCart( page, product1Id );
await page.goto( cartBlockPage.slug );
// Set shipping country to Netherlands
await page.getByLabel( 'Add an address for shipping' ).click();
await page
.getByRole( 'combobox' )
.first()
.selectOption( 'Netherlands' );
await page.getByLabel( 'Postal code' ).fill( '1011AA' );
await page.getByLabel( 'City' ).fill( 'Amsterdam' );
await page.getByRole( 'button', { name: 'Update' } ).click();
// Set shipping country to Netherlands
await page.getByLabel( 'Add an address for shipping' ).click();
await page
.getByRole( 'combobox' )
.first()
.selectOption( 'Netherlands' );
await page.getByLabel( 'Postal code' ).fill( '1011AA' );
await page.getByLabel( 'City' ).fill( 'Amsterdam' );
await page.getByRole( 'button', { name: 'Update' } ).click();
// Verify shipping costs
await expect(
page.getByRole( 'group' ).getByText( 'Free shipping' )
).toBeVisible();
await expect(
page.getByRole( 'strong' ).getByText( 'Free', { exact: true } )
).toBeVisible();
await expect( page.getByText( '$' ).nth( 2 ) ).toContainText(
firstProductPrice
);
} );
// Verify shipping costs
await expect(
page.getByRole( 'group' ).getByText( 'Free shipping' )
).toBeVisible();
await expect(
page
.getByRole( 'strong' )
.getByText( 'Free', { exact: true } )
).toBeVisible();
await expect( page.getByText( '$' ).nth( 2 ) ).toContainText(
firstProductPrice
);
}
);
test( 'allows customer to calculate Flat rate and Local pickup in cart block if in Portugal', async ( {
page,
context,
cartBlockPage,
} ) => {
await context.clearCookies();
test(
'allows customer to calculate Flat rate and Local pickup in cart block if in Portugal',
{ tag: [ '@could-be-unit-test' ] },
async ( { page, context, cartBlockPage } ) => {
await context.clearCookies();
await addAProductToCart( page, product1Id );
await page.goto( cartBlockPage.slug );
await addAProductToCart( page, product1Id );
await page.goto( cartBlockPage.slug );
// Set shipping country to Portugal
await page.getByLabel( 'Add an address for shipping' ).click();
await page
.getByRole( 'combobox' )
.first()
.selectOption( 'Portugal' );
await page.getByLabel( 'Postal code' ).fill( '1000-001' );
await page.getByLabel( 'City' ).fill( 'Lisbon' );
await page.getByRole( 'button', { name: 'Update' } ).click();
// Set shipping country to Portugal
await page.getByLabel( 'Add an address for shipping' ).click();
await page
.getByRole( 'combobox' )
.first()
.selectOption( 'Portugal' );
await page.getByLabel( 'Postal code' ).fill( '1000-001' );
await page.getByLabel( 'City' ).fill( 'Lisbon' );
await page.getByRole( 'button', { name: 'Update' } ).click();
// Verify shipping costs
await expect(
page.getByRole( 'group' ).getByText( 'Flat rate' )
).toBeVisible();
await expect( page.getByText( 'Shipping$5.00Flat' ) ).toBeVisible();
await expect(
page.getByText( `$${ firstProductWithFlatRate }` )
).toBeVisible();
// Verify shipping costs
await expect(
page.getByRole( 'group' ).getByText( 'Flat rate' )
).toBeVisible();
await expect(
page.getByText( 'Shipping$5.00Flat' )
).toBeVisible();
await expect(
page.getByText( `$${ firstProductWithFlatRate }` )
).toBeVisible();
// Set shipping to local pickup instead of flat rate
await page.getByRole( 'group' ).getByText( 'Local pickup' ).click();
// Set shipping to local pickup instead of flat rate
await page
.getByRole( 'group' )
.getByText( 'Local pickup' )
.click();
// Verify updated shipping costs
await expect( page.getByText( 'ShippingFreeLocal' ) ).toBeVisible();
await expect( page.getByText( '$' ).nth( 2 ) ).toContainText(
firstProductPrice
);
} );
// Verify updated shipping costs
await expect(
page.getByText( 'ShippingFreeLocal' )
).toBeVisible();
await expect( page.getByText( '$' ).nth( 2 ) ).toContainText(
firstProductPrice
);
}
);
test( 'should show correct total cart block price after updating quantity', async ( {
page,
context,
cartBlockPage,
} ) => {
await context.clearCookies();
test(
'should show correct total cart block price after updating quantity',
{ tag: [ '@could-be-unit-test' ] },
async ( { page, context, cartBlockPage } ) => {
await context.clearCookies();
await addAProductToCart( page, product1Id );
await page.goto( cartBlockPage.slug );
await addAProductToCart( page, product1Id );
await page.goto( cartBlockPage.slug );
// Set shipping country to Portugal
await page.getByLabel( 'Add an address for shipping' ).click();
await page
.getByRole( 'combobox' )
.first()
.selectOption( 'Portugal' );
await page.getByLabel( 'Postal code' ).fill( '1000-001' );
await page.getByLabel( 'City' ).fill( 'Lisbon' );
await page.getByRole( 'button', { name: 'Update' } ).click();
// Set shipping country to Portugal
await page.getByLabel( 'Add an address for shipping' ).click();
await page
.getByRole( 'combobox' )
.first()
.selectOption( 'Portugal' );
await page.getByLabel( 'Postal code' ).fill( '1000-001' );
await page.getByLabel( 'City' ).fill( 'Lisbon' );
await page.getByRole( 'button', { name: 'Update' } ).click();
// Increase product quantity and verify the updated price
await page.getByLabel( 'Increase quantity of First' ).click();
await expect(
page.getByText(
`$${
parseInt( firstProductPrice, 10 ) +
parseInt( firstProductPrice, 10 ) +
5
}`.toString()
)
).toBeVisible();
} );
// Increase product quantity and verify the updated price
await page.getByLabel( 'Increase quantity of First' ).click();
await expect(
page.getByText(
`$${
parseInt( firstProductPrice, 10 ) +
parseInt( firstProductPrice, 10 ) +
5
}`.toString()
)
).toBeVisible();
}
);
test( 'should show correct total cart block price with 2 different products and flat rate/local pickup', async ( {
page,
context,
cartBlockPage,
} ) => {
await context.clearCookies();
test(
'should show correct total cart block price with 2 different products and flat rate/local pickup',
{ tag: [ '@could-be-unit-test' ] },
async ( { page, context, cartBlockPage } ) => {
await context.clearCookies();
await addAProductToCart( page, product1Id );
await addAProductToCart( page, product2Id );
await page.goto( cartBlockPage.slug );
await addAProductToCart( page, product1Id );
await addAProductToCart( page, product2Id );
await page.goto( cartBlockPage.slug );
// Set shipping country to Portugal
await page.getByLabel( 'Add an address for shipping' ).click();
await page
.getByRole( 'combobox' )
.first()
.selectOption( 'Portugal' );
await page.getByLabel( 'Postal code' ).fill( '1000-001' );
await page.getByLabel( 'City' ).fill( 'Lisbon' );
await page.getByRole( 'button', { name: 'Update' } ).click();
// Set shipping country to Portugal
await page.getByLabel( 'Add an address for shipping' ).click();
await page
.getByRole( 'combobox' )
.first()
.selectOption( 'Portugal' );
await page.getByLabel( 'Postal code' ).fill( '1000-001' );
await page.getByLabel( 'City' ).fill( 'Lisbon' );
await page.getByRole( 'button', { name: 'Update' } ).click();
// Verify shipping costs
await expect(
page.getByRole( 'group' ).getByText( 'Flat rate' )
).toBeVisible();
await expect( page.getByText( 'Shipping$5.00Flat' ) ).toBeVisible();
await expect(
page.getByText(
`$${
parseInt( firstProductPrice, 10 ) +
parseInt( secondProductPrice, 10 ) +
5
}`.toString()
)
).toBeVisible();
// Verify shipping costs
await expect(
page.getByRole( 'group' ).getByText( 'Flat rate' )
).toBeVisible();
await expect(
page.getByText( 'Shipping$5.00Flat' )
).toBeVisible();
await expect(
page.getByText(
`$${
parseInt( firstProductPrice, 10 ) +
parseInt( secondProductPrice, 10 ) +
5
}`.toString()
)
).toBeVisible();
// Set shipping to local pickup instead of flat rate
await page.getByRole( 'group' ).getByText( 'Local pickup' ).click();
// Set shipping to local pickup instead of flat rate
await page
.getByRole( 'group' )
.getByText( 'Local pickup' )
.click();
// Verify updated shipping costs
await expect( page.getByText( 'ShippingFreeLocal' ) ).toBeVisible();
await expect(
page
.locator( 'div' )
.filter( { hasText: /^\$30\.00$/ } )
.locator( 'span' )
).toBeVisible();
} );
// Verify updated shipping costs
await expect(
page.getByText( 'ShippingFreeLocal' )
).toBeVisible();
await expect(
page
.locator( 'div' )
.filter( { hasText: /^\$30\.00$/ } )
.locator( 'span' )
).toBeVisible();
}
);
}
);

View File

@ -127,17 +127,111 @@ test.describe(
} );
} );
test( 'allows cart block to apply coupon of any type', async ( {
page,
} ) => {
const totals = [ '$50.00', '$27.50', '$45.00' ];
test(
'allows cart block to apply coupon of any type',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
const totals = [ '$50.00', '$27.50', '$45.00' ];
// apply all coupon types
for ( let i = 0; i < coupons.length; i++ ) {
// apply all coupon types
for ( let i = 0; i < coupons.length; i++ ) {
await page
.getByRole( 'button', { name: 'Add a coupon' } )
.click();
await page
.getByLabel( 'Enter code' )
.fill( coupons[ i ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
.locator(
'.wc-block-components-notice-banner__content'
)
.getByText(
`Coupon code "${ coupons[ i ].code }" has been applied to your cart.`
)
).toBeVisible();
await expect(
page.locator(
'.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value'
)
).toHaveText( totals[ i ] );
await page
.getByLabel( `Remove coupon "${ coupons[ i ].code }"` )
.click();
await expect(
page
.locator(
'.wc-block-components-notice-banner__content'
)
.getByText(
`Coupon code "${ coupons[ i ].code }" has been removed from your cart.`
)
).toBeVisible();
}
}
);
test(
'allows cart block to apply multiple coupons',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
const totals = [ '$50.00', '$22.50', '$12.50' ];
const totalsReverse = [ '$17.50', '$45.00', '$55.00' ];
const discounts = [ '-$5.00', '-$32.50', '-$42.50' ];
// add all coupons and verify prices
for ( let i = 0; i < coupons.length; i++ ) {
await page
.getByRole( 'button', { name: 'Add a coupon' } )
.click();
await page
.getByLabel( 'Enter code' )
.fill( coupons[ i ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
.locator(
'.wc-block-components-notice-banner__content'
)
.getByText(
`Coupon code "${ coupons[ i ].code }" has been applied to your cart.`
)
).toBeVisible();
await expect(
page.locator(
'.wc-block-components-totals-discount > .wc-block-components-totals-item__value'
)
).toHaveText( discounts[ i ] );
await expect(
page.locator(
'.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value'
)
).toHaveText( totals[ i ] );
}
for ( let i = 0; i < coupons.length; i++ ) {
await page
.getByLabel( `Remove coupon "${ coupons[ i ].code }"` )
.click();
await expect(
page.locator(
'.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value'
)
).toHaveText( totalsReverse[ i ] );
}
}
);
test(
'prevents cart block applying same coupon twice',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
// try to add two same coupons and verify the error message
await page
.getByRole( 'button', { name: 'Add a coupon' } )
.click();
await page.getByLabel( 'Enter code' ).fill( coupons[ i ].code );
await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
@ -145,114 +239,40 @@ test.describe(
'.wc-block-components-notice-banner__content'
)
.getByText(
`Coupon code "${ coupons[ i ].code }" has been applied to your cart.`
`Coupon code "${ coupons[ 0 ].code }" has been applied to your cart.`
)
).toBeVisible();
await expect(
page.locator(
'.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value'
)
).toHaveText( totals[ i ] );
await page
.getByLabel( `Remove coupon "${ coupons[ i ].code }"` )
.click();
await expect(
page
.locator(
'.wc-block-components-notice-banner__content'
)
.getByText(
`Coupon code "${ coupons[ i ].code }" has been removed from your cart.`
)
).toBeVisible();
}
} );
test( 'allows cart block to apply multiple coupons', async ( {
page,
} ) => {
const totals = [ '$50.00', '$22.50', '$12.50' ];
const totalsReverse = [ '$17.50', '$45.00', '$55.00' ];
const discounts = [ '-$5.00', '-$32.50', '-$42.50' ];
// add all coupons and verify prices
for ( let i = 0; i < coupons.length; i++ ) {
await page
.getByRole( 'button', { name: 'Add a coupon' } )
.click();
await page.getByLabel( 'Enter code' ).fill( coupons[ i ].code );
await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
.locator(
'.wc-block-components-notice-banner__content'
)
.getByRole( 'alert' )
.getByText(
`Coupon code "${ coupons[ i ].code }" has been applied to your cart.`
`Coupon code "${ coupons[ 0 ].code }" has already been applied.`
)
).toBeVisible();
await expect(
page.locator(
'.wc-block-components-totals-discount > .wc-block-components-totals-item__value'
)
).toHaveText( discounts[ i ] );
await expect(
page.locator(
'.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value'
)
).toHaveText( totals[ i ] );
}
);
for ( let i = 0; i < coupons.length; i++ ) {
test(
'prevents cart block applying coupon with usage limit',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
// add coupon with usage limit
await page
.getByLabel( `Remove coupon "${ coupons[ i ].code }"` )
.getByRole( 'button', { name: 'Add a coupon' } )
.click();
await page.getByLabel( 'Enter code' ).fill( couponLimitedCode );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page.locator(
'.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value'
)
).toHaveText( totalsReverse[ i ] );
page
.getByRole( 'alert' )
.getByText( 'Coupon usage limit has been reached.' )
).toBeVisible();
}
} );
test( 'prevents cart block applying same coupon twice', async ( {
page,
} ) => {
// try to add two same coupons and verify the error message
await page.getByRole( 'button', { name: 'Add a coupon' } ).click();
await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
.locator( '.wc-block-components-notice-banner__content' )
.getByText(
`Coupon code "${ coupons[ 0 ].code }" has been applied to your cart.`
)
).toBeVisible();
await page.getByRole( 'button', { name: 'Add a coupon' } ).click();
await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
.getByRole( 'alert' )
.getByText(
`Coupon code "${ coupons[ 0 ].code }" has already been applied.`
)
).toBeVisible();
} );
test( 'prevents cart block applying coupon with usage limit', async ( {
page,
} ) => {
// add coupon with usage limit
await page.getByRole( 'button', { name: 'Add a coupon' } ).click();
await page.getByLabel( 'Enter code' ).fill( couponLimitedCode );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
.getByRole( 'alert' )
.getByText( 'Coupon usage limit has been reached.' )
).toBeVisible();
} );
);
}
);

View File

@ -73,116 +73,132 @@ test.describe( 'Cart Block page', { tag: [ '@payments', '@services' ] }, () => {
} );
} );
test( 'can see empty cart, add and remove simple & cross sell product, increase to max quantity', async ( {
page,
testPage,
} ) => {
await goToPageEditor( { page } );
await fillPageTitle( page, testPage.title );
await insertBlockByShortcut( page, 'Cart' );
await publishPage( page, testPage.title );
test(
'can see empty cart, add and remove simple & cross sell product, increase to max quantity',
{ tag: [ '@could-be-unit-test' ] },
async ( { page, testPage } ) => {
await goToPageEditor( { page } );
await fillPageTitle( page, testPage.title );
await insertBlockByShortcut( page, 'Cart' );
await publishPage( page, testPage.title );
// go to the page to test empty cart block
await page.goto( testPage.slug );
await expect(
page.getByRole( 'heading', { name: testPage.title } )
).toBeVisible();
await expect(
await page.getByText( 'Your cart is currently empty!' ).count()
).toBeGreaterThan( 0 );
await expect(
page.getByRole( 'link', { name: 'Browse store' } )
).toBeVisible();
await page.getByRole( 'link', { name: 'Browse store' } ).click();
await expect(
page.getByRole( 'heading', { name: 'Shop' } )
).toBeVisible();
// go to the page to test empty cart block
await page.goto( testPage.slug );
await expect(
page.getByRole( 'heading', { name: testPage.title } )
).toBeVisible();
await expect(
await page.getByText( 'Your cart is currently empty!' ).count()
).toBeGreaterThan( 0 );
await expect(
page.getByRole( 'link', { name: 'Browse store' } )
).toBeVisible();
await page.getByRole( 'link', { name: 'Browse store' } ).click();
await expect(
page.getByRole( 'heading', { name: 'Shop' } )
).toBeVisible();
await addAProductToCart( page, product1Id );
await page.goto( testPage.slug );
await expect(
page.getByRole( 'heading', { name: testPage.title } )
).toBeVisible();
await expect(
page.getByRole( 'link', { name: simpleProductName, exact: true } )
).toBeVisible();
await expect( page.getByText( simpleProductDesc ) ).toBeVisible();
await expect(
page.getByText( `Save $${ singleProductSalePrice }` )
).toBeVisible();
await addAProductToCart( page, product1Id );
await page.goto( testPage.slug );
await expect(
page.getByRole( 'heading', { name: testPage.title } )
).toBeVisible();
await expect(
page.getByRole( 'link', {
name: simpleProductName,
exact: true,
} )
).toBeVisible();
await expect( page.getByText( simpleProductDesc ) ).toBeVisible();
await expect(
page.getByText( `Save $${ singleProductSalePrice }` )
).toBeVisible();
// increase product quantity to its maximum
await expect( page.getByText( '2 left in stock' ) ).toBeVisible();
await page
.getByRole( 'button' )
.filter( { hasText: '', exact: true } )
.click();
await expect(
page.locator(
'.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value'
)
).toContainText( `$${ doubleProductsPrice.toString() }` );
await expect(
page.getByRole( 'button' ).filter( { hasText: '', exact: true } )
).toBeDisabled();
// increase product quantity to its maximum
await expect( page.getByText( '2 left in stock' ) ).toBeVisible();
await page
.getByRole( 'button' )
.filter( { hasText: '', exact: true } )
.click();
await expect(
page.locator(
'.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value'
)
).toContainText( `$${ doubleProductsPrice.toString() }` );
await expect(
page
.getByRole( 'button' )
.filter( { hasText: '', exact: true } )
).toBeDisabled();
// add cross-sell products to cart
await expect(
page.getByRole( 'heading', { name: 'You may be interested in…' } )
).toBeVisible();
await page
.getByLabel( `Add to cart: “${ simpleProductName } Cross-Sell 1”` )
.click();
await expect(
page
.locator( '.wc-block-cart-items' )
.getByText( `${ simpleProductName } Cross-Sell 1` )
).toBeVisible();
await page
.getByLabel( `Add to cart: “${ simpleProductName } Cross-Sell 2”` )
.click();
await expect(
page
.locator( '.wc-block-cart-items' )
.getByText( `${ simpleProductName } Cross-Sell 2` )
).toBeVisible();
// add cross-sell products to cart
await expect(
page.getByRole( 'heading', {
name: 'You may be interested in…',
} )
).toBeVisible();
await page
.getByLabel(
`Add to cart: “${ simpleProductName } Cross-Sell 1”`
)
.click();
await expect(
page
.locator( '.wc-block-cart-items' )
.getByText( `${ simpleProductName } Cross-Sell 1` )
).toBeVisible();
await page
.getByLabel(
`Add to cart: “${ simpleProductName } Cross-Sell 2”`
)
.click();
await expect(
page
.locator( '.wc-block-cart-items' )
.getByText( `${ simpleProductName } Cross-Sell 2` )
).toBeVisible();
await page.goto( testPage.slug );
await expect(
page.getByRole( 'heading', { name: testPage.title } )
).toBeVisible();
await expect(
page.getByRole( 'heading', { name: 'You may be interested in…' } )
).toBeHidden();
await expect(
page.locator(
'.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value'
)
).toContainText(
`$${ singleProductWithCrossSellProducts.toString() }`
);
await page.goto( testPage.slug );
await expect(
page.getByRole( 'heading', { name: testPage.title } )
).toBeVisible();
await expect(
page.getByRole( 'heading', {
name: 'You may be interested in…',
} )
).toBeHidden();
await expect(
page.locator(
'.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value'
)
).toContainText(
`$${ singleProductWithCrossSellProducts.toString() }`
);
// remove cross-sell products from cart
await page.locator( ':nth-match(:text("Remove item"), 3)' ).click();
await page.locator( ':nth-match(:text("Remove item"), 2)' ).click();
await expect(
page.getByRole( 'heading', { name: 'You may be interested in…' } )
).toBeVisible();
// remove cross-sell products from cart
await page.locator( ':nth-match(:text("Remove item"), 3)' ).click();
await page.locator( ':nth-match(:text("Remove item"), 2)' ).click();
await expect(
page.getByRole( 'heading', {
name: 'You may be interested in…',
} )
).toBeVisible();
// check if the link to proceed to the checkout exists
await expect(
page.getByRole( 'link', {
name: 'Proceed to Checkout',
} )
).toBeVisible();
// check if the link to proceed to the checkout exists
await expect(
page.getByRole( 'link', {
name: 'Proceed to Checkout',
} )
).toBeVisible();
// remove product from cart
await page.locator( ':text("Remove item")' ).click();
await expect(
page.getByText( 'Your cart is currently empty!' )
).toBeVisible();
await expect(
page.getByRole( 'link', { name: 'Browse store' } )
).toBeVisible();
} );
// remove product from cart
await page.locator( ':text("Remove item")' ).click();
await expect(
page.getByText( 'Your cart is currently empty!' )
).toBeVisible();
await expect(
page.getByRole( 'link', { name: 'Browse store' } )
).toBeVisible();
}
);
} );

View File

@ -125,108 +125,120 @@ test.describe(
} );
} );
test( 'allows customer to calculate Free Shipping if in Germany', async ( {
page,
} ) => {
await page.goto( '/cart/' );
// Set shipping country to Germany
await page.locator( 'a.shipping-calculator-button' ).click();
await page
.locator( '#calc_shipping_country' )
.selectOption( shippingCountryDE );
await page.locator( 'button[name="calc_shipping"]' ).click();
test(
'allows customer to calculate Free Shipping if in Germany',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
await page.goto( '/cart/' );
// Set shipping country to Germany
await page.locator( 'a.shipping-calculator-button' ).click();
await page
.locator( '#calc_shipping_country' )
.selectOption( shippingCountryDE );
await page.locator( 'button[name="calc_shipping"]' ).click();
// Verify shipping costs
await expect(
page.locator( '.shipping ul#shipping_method > li' )
).toContainText( 'Free shipping' );
await expect(
page.locator( '.order-total .amount' )
).toContainText( firstProductPrice );
} );
// Verify shipping costs
await expect(
page.locator( '.shipping ul#shipping_method > li' )
).toContainText( 'Free shipping' );
await expect(
page.locator( '.order-total .amount' )
).toContainText( firstProductPrice );
}
);
test( 'allows customer to calculate Flat rate and Local pickup if in France', async ( {
page,
} ) => {
await page.goto( '/cart/' );
// Set shipping country to France
await page.locator( 'a.shipping-calculator-button' ).click();
await page
.locator( '#calc_shipping_country' )
.selectOption( shippingCountryFR );
await page.locator( 'button[name="calc_shipping"]' ).click();
test(
'allows customer to calculate Flat rate and Local pickup if in France',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
await page.goto( '/cart/' );
// Set shipping country to France
await page.locator( 'a.shipping-calculator-button' ).click();
await page
.locator( '#calc_shipping_country' )
.selectOption( shippingCountryFR );
await page.locator( 'button[name="calc_shipping"]' ).click();
// Verify shipping costs
await expect( page.locator( '.shipping .amount' ) ).toContainText(
'$5.00'
);
await expect(
page.locator( '.order-total .amount' )
).toContainText( `$${ firstProductWithFlatRate }` );
// Verify shipping costs
await expect(
page.locator( '.shipping .amount' )
).toContainText( '$5.00' );
await expect(
page.locator( '.order-total .amount' )
).toContainText( `$${ firstProductWithFlatRate }` );
// Set shipping to local pickup instead of flat rate
await page.locator( 'text=Local pickup' ).click();
// Set shipping to local pickup instead of flat rate
await page.locator( 'text=Local pickup' ).click();
// Verify updated shipping costs
await expect(
page.locator( '.order-total .amount' ).first()
).toContainText( `$${ firstProductPrice }` );
} );
// Verify updated shipping costs
await expect(
page.locator( '.order-total .amount' ).first()
).toContainText( `$${ firstProductPrice }` );
}
);
test( 'should show correct total cart price after updating quantity', async ( {
page,
} ) => {
await page.goto( '/cart/' );
await page.locator( 'input.qty' ).fill( '4' );
await page.locator( 'text=Update cart' ).click();
test(
'should show correct total cart price after updating quantity',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
await page.goto( '/cart/' );
await page.locator( 'input.qty' ).fill( '4' );
await page.locator( 'text=Update cart' ).click();
// Set shipping country to France
await page.locator( 'a.shipping-calculator-button' ).click();
await page
.locator( '#calc_shipping_country' )
.selectOption( shippingCountryFR );
await page.locator( 'button[name="calc_shipping"]' ).click();
// Set shipping country to France
await page.locator( 'a.shipping-calculator-button' ).click();
await page
.locator( '#calc_shipping_country' )
.selectOption( shippingCountryFR );
await page.locator( 'button[name="calc_shipping"]' ).click();
await expect(
page.locator( '.order-total .amount' )
).toContainText( `$${ fourProductsWithFlatRate }` );
} );
await expect(
page.locator( '.order-total .amount' )
).toContainText( `$${ fourProductsWithFlatRate }` );
}
);
test( 'should show correct total cart price with 2 products and flat rate', async ( {
page,
} ) => {
await addAProductToCart( page, secondProductId );
test(
'should show correct total cart price with 2 products and flat rate',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
await addAProductToCart( page, secondProductId );
await page.goto( '/cart/' );
await page.locator( 'a.shipping-calculator-button' ).click();
await page
.locator( '#calc_shipping_country' )
.selectOption( shippingCountryFR );
await page.locator( 'button[name="calc_shipping"]' ).click();
await page.goto( '/cart/' );
await page.locator( 'a.shipping-calculator-button' ).click();
await page
.locator( '#calc_shipping_country' )
.selectOption( shippingCountryFR );
await page.locator( 'button[name="calc_shipping"]' ).click();
await expect( page.locator( '.shipping .amount' ) ).toContainText(
'$5.00'
);
await expect(
page.locator( '.order-total .amount' )
).toContainText( `$${ twoProductsWithFlatRate }` );
} );
await expect(
page.locator( '.shipping .amount' )
).toContainText( '$5.00' );
await expect(
page.locator( '.order-total .amount' )
).toContainText( `$${ twoProductsWithFlatRate }` );
}
);
test( 'should show correct total cart price with 2 products without flat rate', async ( {
page,
} ) => {
await addAProductToCart( page, secondProductId );
test(
'should show correct total cart price with 2 products without flat rate',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
await addAProductToCart( page, secondProductId );
// Set shipping country to Spain
await page.goto( '/cart/' );
await page.locator( 'a.shipping-calculator-button' ).click();
await page.locator( '#calc_shipping_country' ).selectOption( 'ES' );
await page.locator( 'button[name="calc_shipping"]' ).click();
// Set shipping country to Spain
await page.goto( '/cart/' );
await page.locator( 'a.shipping-calculator-button' ).click();
await page
.locator( '#calc_shipping_country' )
.selectOption( 'ES' );
await page.locator( 'button[name="calc_shipping"]' ).click();
// Verify shipping costs
await expect(
page.locator( '.order-total .amount' )
).toContainText( `$${ twoProductsTotal }` );
} );
// Verify shipping costs
await expect(
page.locator( '.order-total .amount' )
).toContainText( `$${ twoProductsTotal }` );
}
);
}
);

View File

@ -38,7 +38,7 @@ let productId,
test.describe(
'Shopper Cart & Checkout Block Tax Display',
{ tag: [ '@payments', '@services', '@hpos' ] },
{ tag: [ '@payments', '@services', '@hpos', '@could-be-unit-test' ] },
() => {
test.use( { storageState: process.env.ADMINSTATE } );
test.beforeAll( async ( { baseURL } ) => {
@ -240,7 +240,7 @@ test.describe(
test.describe(
'Shopper Cart & Checkout Block Tax Rounding',
{ tag: [ '@payments', '@services' ] },
{ tag: [ '@payments', '@services', '@could-be-unit-test' ] },
() => {
test.beforeAll( async ( { baseURL } ) => {
const api = new wcApi( {
@ -484,7 +484,7 @@ test.describe(
test.describe(
'Shopper Cart & Checkout Block Tax Levels',
{ tag: [ '@payments', '@services' ] },
{ tag: [ '@payments', '@services', '@could-be-unit-test' ] },
() => {
test.beforeAll( async ( { baseURL } ) => {
const api = new wcApi( {
@ -809,7 +809,7 @@ test.describe(
test.describe(
'Shipping Cart & Checkout Block Tax',
{ tag: [ '@payments', '@services' ] },
{ tag: [ '@payments', '@services', '@could-be-unit-test' ] },
() => {
test.beforeAll( async ( { baseURL } ) => {
const api = new wcApi( {

View File

@ -24,7 +24,7 @@ let productId,
test.describe.serial(
'Tax rates in the cart and checkout',
{ tag: [ '@payments', '@services', '@hpos' ] },
{ tag: [ '@payments', '@services', '@hpos', '@could-be-unit-test' ] },
() => {
test.beforeAll( async ( { baseURL } ) => {
const api = new wcApi( {

View File

@ -306,60 +306,65 @@ test.describe(
} );
} );
test( 'restores total when coupons are removed', async ( {
page,
context,
} ) => {
await test.step( 'Load cart page and try restoring total when removed coupons', async () => {
await addAProductToCart( page, firstProductId );
test(
'restores total when coupons are removed',
{ tag: [ '@could-be-unit-test' ] },
async ( { page, context } ) => {
await test.step( 'Load cart page and try restoring total when removed coupons', async () => {
await addAProductToCart( page, firstProductId );
await page.goto( '/cart/' );
await page.locator( '#coupon_code' ).fill( coupons[ 0 ].code );
await page
.getByRole( 'button', { name: 'Apply coupon' } )
.click();
await page.goto( '/cart/' );
await page
.locator( '#coupon_code' )
.fill( coupons[ 0 ].code );
await page
.getByRole( 'button', { name: 'Apply coupon' } )
.click();
// confirm numbers
await expect(
page.locator( '.cart-discount .amount' )
).toContainText( discounts[ 0 ] );
await expect(
page.locator( '.order-total .amount' )
).toContainText( totals[ 0 ] );
// confirm numbers
await expect(
page.locator( '.cart-discount .amount' )
).toContainText( discounts[ 0 ] );
await expect(
page.locator( '.order-total .amount' )
).toContainText( totals[ 0 ] );
await page.locator( 'a.woocommerce-remove-coupon' ).click();
await page.locator( 'a.woocommerce-remove-coupon' ).click();
await expect(
page.locator( '.order-total .amount' )
).toContainText( '$20.00' );
} );
await expect(
page.locator( '.order-total .amount' )
).toContainText( '$20.00' );
} );
await context.clearCookies();
await context.clearCookies();
await test.step( 'Load checkout page and try restoring total when removed coupons', async () => {
await addAProductToCart( page, firstProductId );
await test.step( 'Load checkout page and try restoring total when removed coupons', async () => {
await addAProductToCart( page, firstProductId );
await page.goto( '/checkout/' );
await page
.locator( 'text=Click here to enter your code' )
.click();
await page.locator( '#coupon_code' ).fill( coupons[ 0 ].code );
await page.locator( 'text=Apply coupon' ).click();
await page.goto( '/checkout/' );
await page
.locator( 'text=Click here to enter your code' )
.click();
await page
.locator( '#coupon_code' )
.fill( coupons[ 0 ].code );
await page.locator( 'text=Apply coupon' ).click();
// confirm numbers
await expect(
page.locator( '.cart-discount .amount' )
).toContainText( discounts[ 0 ] );
await expect(
page.locator( '.order-total .amount' )
).toContainText( totals[ 0 ] );
// confirm numbers
await expect(
page.locator( '.cart-discount .amount' )
).toContainText( discounts[ 0 ] );
await expect(
page.locator( '.order-total .amount' )
).toContainText( totals[ 0 ] );
await page.locator( 'a.woocommerce-remove-coupon' ).click();
await page.locator( 'a.woocommerce-remove-coupon' ).click();
await expect(
page.locator( '.order-total .amount' )
).toContainText( '$20.00' );
} );
} );
await expect(
page.locator( '.order-total .amount' )
).toContainText( '$20.00' );
} );
}
);
}
);

View File

@ -17,7 +17,7 @@ const awaitCartPageResponse = ( page ) =>
test.describe(
'Cart & Checkout Restricted Coupons',
{ tag: [ '@payments', '@services', '@hpos' ] },
{ tag: [ '@payments', '@services', '@hpos', '@could-be-unit-test' ] },
() => {
let firstProductId,
secondProductId,

View File

@ -3,7 +3,7 @@ const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default;
test.describe(
'Cart > Redirect to cart from shop',
{ tag: [ '@payments', '@services' ] },
{ tag: [ '@payments', '@services', '@not-e2e' ] },
() => {
let productId;
const productName = 'A redirect product test';

View File

@ -81,100 +81,116 @@ test.describe( 'Cart page', { tag: [ '@payments', '@services' ] }, () => {
await responsePromise;
}
test( 'should display no item in the cart', async ( { page } ) => {
await page.goto( '/cart/' );
await expect(
page.getByText( 'Your cart is currently empty.' )
).toBeVisible();
} );
test( 'should add the product to the cart from the shop page', async ( {
page,
} ) => {
await goToShopPageAndAddProductToCart( page, productName );
await page.goto( '/cart/' );
await expect( page.locator( 'td.product-name' ) ).toContainText(
productName
);
} );
test( 'should increase item quantity when "Add to cart" of the same product is clicked', async ( {
page,
} ) => {
let qty = 2;
while ( qty-- ) {
await goToShopPageAndAddProductToCart( page, productName );
test(
'should display no item in the cart',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
await page.goto( '/cart/' );
await expect(
page.getByText( 'Your cart is currently empty.' )
).toBeVisible();
}
);
await page.goto( '/cart/' );
await expect( page.locator( 'input.qty' ) ).toHaveValue( '2' );
} );
test(
'should add the product to the cart from the shop page',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
await goToShopPageAndAddProductToCart( page, productName );
test( 'should update quantity when updated via quantity input', async ( {
page,
} ) => {
await goToShopPageAndAddProductToCart( page, productName );
await page.goto( '/cart/' );
await expect( page.locator( 'td.product-name' ) ).toContainText(
productName
);
}
);
await page.goto( '/cart/' );
await page.locator( 'input.qty' ).fill( '2' );
await page.locator( 'text=Update cart' ).click();
test(
'should increase item quantity when "Add to cart" of the same product is clicked',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
let qty = 2;
while ( qty-- ) {
await goToShopPageAndAddProductToCart( page, productName );
}
await expect( page.locator( '.order-total .amount' ) ).toContainText(
`$${ twoProductPrice }`
);
} );
await page.goto( '/cart/' );
await expect( page.locator( 'input.qty' ) ).toHaveValue( '2' );
}
);
test( 'should remove the item from the cart when remove is clicked', async ( {
page,
} ) => {
await goToShopPageAndAddProductToCart( page, productName );
await page.goto( '/cart/' );
test(
'should update quantity when updated via quantity input',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
await goToShopPageAndAddProductToCart( page, productName );
// make sure that the product is in the cart
await expect( page.locator( '.order-total .amount' ) ).toContainText(
`$${ productPrice }`
);
await page.goto( '/cart/' );
await page.locator( 'input.qty' ).fill( '2' );
await page.locator( 'text=Update cart' ).click();
await page.locator( 'a.remove' ).click();
await expect(
page.locator( '.order-total .amount' )
).toContainText( `$${ twoProductPrice }` );
}
);
await expect(
page.getByText( `${ productName }” removed` )
).toBeVisible();
await expect(
page.getByText( 'Your cart is currently empty' )
).toBeVisible();
} );
test(
'should remove the item from the cart when remove is clicked',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
await goToShopPageAndAddProductToCart( page, productName );
await page.goto( '/cart/' );
test( 'should update subtotal in cart totals when adding product to the cart', async ( {
page,
} ) => {
await goToShopPageAndAddProductToCart( page, productName );
// make sure that the product is in the cart
await expect(
page.locator( '.order-total .amount' )
).toContainText( `$${ productPrice }` );
await page.goto( '/cart/' );
await expect( page.locator( '.cart-subtotal .amount' ) ).toContainText(
`$${ productPrice }`
);
await page.locator( 'a.remove' ).click();
await page.locator( 'input.qty' ).fill( '2' );
await page.locator( 'text=Update cart' ).click();
await expect(
page.getByText( `${ productName }” removed` )
).toBeVisible();
await expect(
page.getByText( 'Your cart is currently empty' )
).toBeVisible();
}
);
await expect( page.locator( '.order-total .amount' ) ).toContainText(
`$${ twoProductPrice }`
);
} );
test(
'should update subtotal in cart totals when adding product to the cart',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
await goToShopPageAndAddProductToCart( page, productName );
test( 'should go to the checkout page when "Proceed to Checkout" is clicked', async ( {
page,
} ) => {
await goToShopPageAndAddProductToCart( page, productName );
await page.goto( '/cart/' );
await expect(
page.locator( '.cart-subtotal .amount' )
).toContainText( `$${ productPrice }` );
await page.goto( '/cart/' );
await page.locator( 'input.qty' ).fill( '2' );
await page.locator( 'text=Update cart' ).click();
await page.locator( '.checkout-button' ).click();
await expect(
page.locator( '.order-total .amount' )
).toContainText( `$${ twoProductPrice }` );
}
);
await expect( page.locator( '#order_review' ) ).toBeVisible();
} );
test(
'should go to the checkout page when "Proceed to Checkout" is clicked',
{ tag: [ '@could-be-unit-test' ] },
async ( { page } ) => {
await goToShopPageAndAddProductToCart( page, productName );
await page.goto( '/cart/' );
await page.locator( '.checkout-button' ).click();
await expect( page.locator( '#order_review' ) ).toBeVisible();
}
);
test( 'can manage cross-sell products and maximum item quantity', async ( {
page,

View File

@ -72,51 +72,43 @@ async function runComingSoonTests( themeContext = '' ) {
} );
}
test.describe(
'Launch Your Store front end - logged out',
{ tag: [ '@payments', '@services' ] },
() => {
test.afterAll( async ( { baseURL } ) => {
try {
await setOption(
request,
baseURL,
'woocommerce_coming_soon',
'no'
);
} catch ( error ) {
console.log( error );
}
} );
test.describe( 'Block Theme (Twenty Twenty Four)', () => {
test.beforeAll( async () => {
await activateTheme( 'twentytwentyfour' );
} );
test.afterAll( async () => {
// Reset theme to the default.
await activateTheme( DEFAULT_THEME );
} );
runComingSoonTests( test.step, test.use );
} );
test.describe( 'Classic Theme (Storefront)', () => {
test.beforeAll( async () => {
await activateTheme( 'storefront' );
} );
test.afterAll( async () => {
// Reset theme to the default.
await activateTheme( DEFAULT_THEME );
} );
runComingSoonTests(
test.step,
test.use,
'Classic Theme (Storefront)'
test.describe( 'Launch Your Store front end - logged out', () => {
test.afterAll( async ( { baseURL } ) => {
try {
await setOption(
request,
baseURL,
'woocommerce_coming_soon',
'no'
);
} catch ( error ) {
console.log( error );
}
} );
test.describe( 'Block Theme (Twenty Twenty Four)', () => {
test.beforeAll( async () => {
await activateTheme( 'twentytwentyfour' );
} );
}
);
test.afterAll( async () => {
// Reset theme to the default.
await activateTheme( DEFAULT_THEME );
} );
runComingSoonTests( test.step, test.use );
} );
test.describe( 'Classic Theme (Storefront)', () => {
test.beforeAll( async () => {
await activateTheme( 'storefront' );
} );
test.afterAll( async () => {
// Reset theme to the default.
await activateTheme( DEFAULT_THEME );
} );
runComingSoonTests( test.step, test.use, 'Classic Theme (Storefront)' );
} );
} );

View File

@ -3,7 +3,7 @@ const { test, expect } = require( '@playwright/test' );
// test case for bug https://github.com/woocommerce/woocommerce/pull/46429
test.describe(
'Check the title of the shop page after the page has been deleted',
{ tag: [ '@payments', '@services' ] },
{ tag: [ '@payments', '@services', '@could-be-unit-test' ] },
() => {
test.use( { storageState: process.env.ADMINSTATE } );
test.beforeEach( async ( { page } ) => {

View File

@ -6,7 +6,7 @@ const test = baseTest.extend( {
test(
'logged-in customer can comment on a post',
{ tag: [ '@gutenberg', '@payments', '@services' ] },
{ tag: [ '@non-critical' ] },
async ( { page } ) => {
await page.goto( 'hello-world/' );
await expect(

View File

@ -12,13 +12,6 @@ use Automattic\WooCommerce\Internal\Admin\ShippingLabelBannerDisplayRules;
*/
class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Case {
/**
* Jetpack version to test the display manager.
*
* @var string
*/
private $valid_jetpack_version = '4.4';
/**
* Stores the default WordPress options stored in the database.
*
@ -66,7 +59,6 @@ class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Ca
/**
* Test if the banner is displayed when all conditions are satisfied:
* - Banner NOT dismissed
* - Jetpack >= 4.4 installed and active
* - Jetpack Connected
* - No incompatible extensions installed:
* - Shipstation not installed
@ -76,13 +68,12 @@ class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Ca
* - Order contains physical products which need to be shipped (we should check that the order status is not set to complete)
* - Store is located in US
* - Store currency is set to USD
* - WCS plugin not installed OR WCS is installed *AND* ToS have NOT been accepted *AND* WCS version is 1.22.5 or greater
* (The 1.22.5 or greater requirement is so we can launch the shipping modal from the banner)
* - WCS plugin not installed OR WCS is installed
*/
public function test_display_banner_if_all_conditions_are_met() {
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
function ( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( true, null, false );
$that->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), true );
}
@ -93,7 +84,7 @@ class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Ca
* Test if the banner is hidden when Jetpack is not active.
*/
public function test_if_banner_hidden_when_jetpack_disconnected() {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( null, null, null, null, null );
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( null, null, null );
$this->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), false );
}
@ -103,7 +94,7 @@ class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Ca
*/
public function test_if_banner_hidden_when_dismiss_option_enabled() {
update_option( 'woocommerce_shipping_dismissed_timestamp', -1 );
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( true, '1.22.5', false );
$this->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), false );
}
@ -115,7 +106,7 @@ class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Ca
$two_hours_from_ago = ( time() - 2 * 60 * 60 ) * 1000;
update_option( 'woocommerce_shipping_dismissed_timestamp', $two_hours_from_ago );
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( true, '1.22.5', false );
$this->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), false );
}
@ -128,8 +119,8 @@ class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Ca
update_option( 'woocommerce_shipping_dismissed_timestamp', $twenty_four_hours_one_sec_ago );
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
function ( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( true, null, false, false );
$that->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), true );
}
@ -140,7 +131,7 @@ class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Ca
* Test if the banner is hidden when no shippable product available.
*/
public function test_if_banner_hidden_when_no_shippable_product() {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( true, '1.22.5', false );
$this->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), false );
}
@ -151,8 +142,8 @@ class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Ca
public function test_if_banner_hidden_when_store_is_not_in_us() {
update_option( 'woocommerce_default_country', 'ES' );
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
function ( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( true, '1.22.5', false, false );
$that->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), false );
}
@ -165,8 +156,8 @@ class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Ca
public function test_if_banner_hidden_when_currency_is_not_usd() {
update_option( 'woocommerce_currency', 'EUR' );
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, false );
function ( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( true, '1.22.5', false );
$that->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), false );
}
@ -178,34 +169,8 @@ class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Ca
*/
public function test_if_banner_hidden_when_incompatible_plugin_installed() {
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', false, true );
$that->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), false );
}
);
}
/**
* Test if the banner is hidden when Jetpack version is not at least 4.4.
*/
public function test_if_banner_hidden_when_jetpack_version_is_old() {
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.3', true, '1.22.5', false, false );
$that->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), false );
}
);
}
/**
* Test if the banner is hidden when the WooCommerce Shipping & Tax Terms of Service has been already accepted.
*/
public function test_if_banner_hidden_when_wcs_tos_accepted() {
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.5', true, false );
function ( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( true, '1.22.5', false, true );
$that->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), false );
}
@ -217,8 +182,8 @@ class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Ca
*/
public function test_if_banner_hidden_when_wcs_not_installed() {
$this->with_order(
function( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( '4.4', true, '1.22.4', false, false );
function ( $that ) {
$shipping_label_banner_display_rules = new ShippingLabelBannerDisplayRules( true, '1.22.4', false );
$that->assertEquals( $shipping_label_banner_display_rules->should_display_banner(), false );
}
@ -230,20 +195,10 @@ class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Ca
*/
private function create_order() {
$product = WC_Helper_Product::create_simple_product();
$product->set_props( array( 'virtual' => true ) );
$order = new WC_Order();
$order_item = new WC_Order_Item_Product();
$order_item->set_props( array( 'product' => $product ) );
$order->add_item( $order_item );
$order->save();
global $post;
// phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
$post = new \stdClass();
$post->ID = $order->get_id();
$order = WC_Helper_Order::create_order( 1, $product );
global $theorder;
$theorder = $order;
return $order;
}
@ -274,5 +229,4 @@ class WC_Admin_Tests_Shipping_Label_Banner_Display_Rules extends WC_Unit_Test_Ca
$this->destroy_order( $order );
}
}