Interactivity API and Product Button (https://github.com/woocommerce/woocommerce-blocks/pull/10006)
* Update Interactivity API JS files * Disable TS checks in the Interactivity API for now * Add new SSR files * Replace wp_ prefixes with wc_ ones * Replace wp- prefix with wc- * Replace guternberg_ prefix with woocommerce_ * Remove file comments from Gutenberg * Rename files with `wp` prefix * Fix code to load Interactivity API php files * Remove TODO comments * Replace @wordpress with @woocommerce * Update Webpack configuration * Fix directive prefix * Remove interactivity folder from tsconfig exclude * Add client-side navigation meta tag code * Remove unneeded blocks.php file * Fix store tag id * Register Interactivity API runtime script * Fix Interactivity API runtime registering * Remove all files related to directive processing in PHP * Move json_encode to Store's render method * WIP * WIP * WIP * WIP * Preserve previous context * Ignore Minicart block on client-side navigation * Refresh page on store updatRefresh page on store updatee * Refactor logic * Add console error when a path is missing * fix PHP lint error * WIP store * use store approach * update jest configuration * restore Mini Cart changes * move cart store subscription to interactivity package * move interactivity flag * format HTML * move addToCartText to the context * Load product-query stylesheet when rendering the Products block * update sideEffects array * fix catch * rename moreThanOneItem to isThereMoreThanOneItem * improve how scripts are enqueued * update default value for the filter woocommerce_blocks_enable_interactivity_api * Update assets/js/atomic/blocks/product-elements/button/block.json Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com> * Update assets/js/interactivity/cart/cart-store.ts Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com> * fix block.json * remove updateStore function * restore interactivity api changes * import cart store * show notice when there is an error * add logic to dequeue script on classic themes and block themes * imrpove logic about notice * Interactivity API: add `afterLoad` callbacks to `store()` function (https://github.com/woocommerce/woocommerce-blocks/pull/10338) * show notice when there is an error * Add initial implementation for store callbacks * Run `afterLoad` callbacks after `init` * Move cart state subscription to Product button * Remove cart-store from Interactivity API internals * Change callbacks with options and save only afterLoad callbacks * ProductButton: Add animation (https://github.com/woocommerce/woocommerce-blocks/pull/10351) * implement animation * improve logic * refactor logic * refactor code * address feedback about code style * add support for woocommerce_add_to_cart_quantity * Fix animation flickering * Introduce wp-effect, reduce the amount of numberOfItem variables to 2 and consolidate animation status * add support for added class * Remove unnecessary selector * Don't fetch cart if it was already fetched * remove added class --------- Co-authored-by: Luis Herranz <luisherranz@gmail.com> --------- Co-authored-by: Luigi <gigitux@gmail.com> Co-authored-by: Luis Herranz <luisherranz@gmail.com> * update deepsignal * remove added class * update deepsignal * Interactivity API and Product Button: Add E2E tests (https://github.com/woocommerce/woocommerce-blocks/pull/10036) * Add FrontendUtils class * fix conflicts * use locator * restore click usage * Product Button: Add E2E test * fix util * fix E2E tests * remove comment * Add E2E test to ensure that woocommerce_product_add_to_cart_text works * update sideEffects array * add zip and unzip as package * fix wp-env configuration * fix E2E test * add report * try now * try now * try now * fix E2E test * E2E: Add documentation for testing actions and filters. Fixes woocommerce/woocommerce-blocks#10135 (https://github.com/woocommerce/woocommerce-blocks/pull/10206) * update description * fix label * rename files * make requestUtils private * remove page.goto * use toHaveCount * use productsToDisplay variable * fix E2E tests * rename class utils --------- Co-authored-by: Daniel Dudzic <daniel.dudzic@automattic.com> --------- Co-authored-by: David Arenas <david.arenas@automattic.com> Co-authored-by: Luis Herranz <luisherranz@gmail.com> Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com> Co-authored-by: Daniel Dudzic <daniel.dudzic@automattic.com>
This commit is contained in:
parent
4771c70fc9
commit
b90e0ffdc9
|
@ -162,6 +162,7 @@ module.exports = {
|
||||||
'@wordpress/url',
|
'@wordpress/url',
|
||||||
'@woocommerce/blocks-test-utils',
|
'@woocommerce/blocks-test-utils',
|
||||||
'@woocommerce/e2e-utils',
|
'@woocommerce/e2e-utils',
|
||||||
|
'@woocommerce/e2e-mocks',
|
||||||
'babel-jest',
|
'babel-jest',
|
||||||
'dotenv',
|
'dotenv',
|
||||||
'jest-environment-puppeteer',
|
'jest-environment-puppeteer',
|
||||||
|
|
|
@ -14,7 +14,8 @@
|
||||||
"wp-content/plugins/gutenberg-test-plugins": "./node_modules/@wordpress/e2e-tests/plugins",
|
"wp-content/plugins/gutenberg-test-plugins": "./node_modules/@wordpress/e2e-tests/plugins",
|
||||||
"wp-content/plugins/woocommerce-blocks": ".",
|
"wp-content/plugins/woocommerce-blocks": ".",
|
||||||
"wp-content/plugins/woocommerce-gutenberg-products-block": ".",
|
"wp-content/plugins/woocommerce-gutenberg-products-block": ".",
|
||||||
"wp-cli.yml": "./wp-cli.yml"
|
"wp-cli.yml": "./wp-cli.yml",
|
||||||
|
"custom-plugins" : "./tests/e2e/mocks/custom-plugins"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
"background": false,
|
"background": false,
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"interactivity": true,
|
||||||
"html": false,
|
"html": false,
|
||||||
"typography": {
|
"typography": {
|
||||||
"fontSize": true,
|
"fontSize": true,
|
||||||
|
@ -57,6 +58,9 @@
|
||||||
"label": "Outline"
|
"label": "Outline"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"viewScript": [
|
||||||
|
"wc-product-button-interactivity-frontend"
|
||||||
|
],
|
||||||
"apiVersion": 2,
|
"apiVersion": 2,
|
||||||
"$schema": "https://schemas.wp.org/trunk/block.json"
|
"$schema": "https://schemas.wp.org/trunk/block.json"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,279 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||||
|
import { store as interactivityStore } from '@woocommerce/interactivity';
|
||||||
|
import { dispatch, select, subscribe } from '@wordpress/data';
|
||||||
|
import { Cart } from '@woocommerce/type-defs/cart';
|
||||||
|
import { createRoot } from '@wordpress/element';
|
||||||
|
import NoticeBanner from '@woocommerce/base-components/notice-banner';
|
||||||
|
|
||||||
|
type Context = {
|
||||||
|
woocommerce: {
|
||||||
|
isLoading: boolean;
|
||||||
|
addToCartText: string;
|
||||||
|
productId: number;
|
||||||
|
displayViewCart: boolean;
|
||||||
|
quantityToAdd: number;
|
||||||
|
temporaryNumberOfItems: number;
|
||||||
|
animationStatus: AnimationStatus;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
enum AnimationStatus {
|
||||||
|
IDLE = 'IDLE',
|
||||||
|
SLIDE_OUT = 'SLIDE-OUT',
|
||||||
|
SLIDE_IN = 'SLIDE-IN',
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
woocommerce: {
|
||||||
|
cart: Cart | undefined;
|
||||||
|
inTheCartText: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type Store = {
|
||||||
|
state: State;
|
||||||
|
context: Context;
|
||||||
|
selectors: any;
|
||||||
|
ref: HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const storeNoticeClass = '.wc-block-store-notices';
|
||||||
|
|
||||||
|
const createNoticeContainer = () => {
|
||||||
|
const noticeContainer = document.createElement( 'div' );
|
||||||
|
noticeContainer.classList.add( storeNoticeClass.replace( '.', '' ) );
|
||||||
|
return noticeContainer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const injectNotice = ( domNode: Element, errorMessage: string ) => {
|
||||||
|
const root = createRoot( domNode );
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<NoticeBanner status="error" onRemove={ () => root.unmount() }>
|
||||||
|
{ errorMessage }
|
||||||
|
</NoticeBanner>
|
||||||
|
);
|
||||||
|
|
||||||
|
domNode?.scrollIntoView( {
|
||||||
|
behavior: 'smooth',
|
||||||
|
inline: 'nearest',
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProductById = ( cartState: Cart | undefined, productId: number ) => {
|
||||||
|
return cartState?.items.find( ( item ) => item.id === productId );
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTextButton = ( {
|
||||||
|
addToCartText,
|
||||||
|
inTheCartText,
|
||||||
|
numberOfItems,
|
||||||
|
}: {
|
||||||
|
addToCartText: string;
|
||||||
|
inTheCartText: string;
|
||||||
|
numberOfItems: number;
|
||||||
|
} ) => {
|
||||||
|
if ( numberOfItems === 0 ) {
|
||||||
|
return addToCartText;
|
||||||
|
}
|
||||||
|
return inTheCartText.replace( '###', numberOfItems.toString() );
|
||||||
|
};
|
||||||
|
|
||||||
|
const productButtonSelectors = {
|
||||||
|
woocommerce: {
|
||||||
|
addToCartText: ( store: Store ) => {
|
||||||
|
const { context, state, selectors } = store;
|
||||||
|
|
||||||
|
// We use the temporary number of items when there's no animation, or the
|
||||||
|
// second part of the animation hasn't started.
|
||||||
|
if (
|
||||||
|
context.woocommerce.animationStatus === AnimationStatus.IDLE ||
|
||||||
|
context.woocommerce.animationStatus ===
|
||||||
|
AnimationStatus.SLIDE_OUT
|
||||||
|
) {
|
||||||
|
return getTextButton( {
|
||||||
|
addToCartText: context.woocommerce.addToCartText,
|
||||||
|
inTheCartText: state.woocommerce.inTheCartText,
|
||||||
|
numberOfItems: context.woocommerce.temporaryNumberOfItems,
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
return getTextButton( {
|
||||||
|
addToCartText: context.woocommerce.addToCartText,
|
||||||
|
inTheCartText: state.woocommerce.inTheCartText,
|
||||||
|
numberOfItems:
|
||||||
|
selectors.woocommerce.numberOfItemsInTheCart( store ),
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
displayViewCart: ( store: Store ) => {
|
||||||
|
const { context, selectors } = store;
|
||||||
|
if ( ! context.woocommerce.displayViewCart ) return false;
|
||||||
|
if ( ! selectors.woocommerce.hasCartLoaded( store ) ) {
|
||||||
|
return context.woocommerce.temporaryNumberOfItems > 0;
|
||||||
|
}
|
||||||
|
return selectors.woocommerce.numberOfItemsInTheCart( store ) > 0;
|
||||||
|
},
|
||||||
|
hasCartLoaded: ( { state }: { state: State } ) => {
|
||||||
|
return state.woocommerce.cart !== undefined;
|
||||||
|
},
|
||||||
|
numberOfItemsInTheCart: ( { state, context }: Store ) => {
|
||||||
|
const product = getProductById(
|
||||||
|
state.woocommerce.cart,
|
||||||
|
context.woocommerce.productId
|
||||||
|
);
|
||||||
|
return product?.quantity || 0;
|
||||||
|
},
|
||||||
|
slideOutAnimation: ( { context }: Store ) =>
|
||||||
|
context.woocommerce.animationStatus === AnimationStatus.SLIDE_OUT,
|
||||||
|
slideInAnimation: ( { context }: Store ) =>
|
||||||
|
context.woocommerce.animationStatus === AnimationStatus.SLIDE_IN,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interactivityStore(
|
||||||
|
// @ts-expect-error: Store function isn't typed.
|
||||||
|
{
|
||||||
|
selectors: productButtonSelectors,
|
||||||
|
actions: {
|
||||||
|
woocommerce: {
|
||||||
|
addToCart: async ( store: Store ) => {
|
||||||
|
const { context, selectors, ref } = store;
|
||||||
|
|
||||||
|
if ( ! ref.classList.contains( 'ajax_add_to_cart' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.woocommerce.isLoading = true;
|
||||||
|
|
||||||
|
// Allow 3rd parties to validate and quit early.
|
||||||
|
// https://github.com/woocommerce/woocommerce/blob/154dd236499d8a440edf3cde712511b56baa8e45/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js/#L74-L77
|
||||||
|
const event = new CustomEvent(
|
||||||
|
'should_send_ajax_request.adding_to_cart',
|
||||||
|
{ detail: [ ref ], cancelable: true }
|
||||||
|
);
|
||||||
|
const shouldSendRequest =
|
||||||
|
document.body.dispatchEvent( event );
|
||||||
|
|
||||||
|
if ( shouldSendRequest === false ) {
|
||||||
|
const ajaxNotSentEvent = new CustomEvent(
|
||||||
|
'ajax_request_not_sent.adding_to_cart',
|
||||||
|
{ detail: [ false, false, ref ] }
|
||||||
|
);
|
||||||
|
document.body.dispatchEvent( ajaxNotSentEvent );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dispatch( storeKey ).addItemToCart(
|
||||||
|
context.woocommerce.productId,
|
||||||
|
context.woocommerce.quantityToAdd
|
||||||
|
);
|
||||||
|
|
||||||
|
// After the cart has been updated, sync the temporary number of
|
||||||
|
// items again.
|
||||||
|
context.woocommerce.temporaryNumberOfItems =
|
||||||
|
selectors.woocommerce.numberOfItemsInTheCart(
|
||||||
|
store
|
||||||
|
);
|
||||||
|
} catch ( error ) {
|
||||||
|
const storeNoticeBlock =
|
||||||
|
document.querySelector( storeNoticeClass );
|
||||||
|
|
||||||
|
if ( ! storeNoticeBlock ) {
|
||||||
|
document
|
||||||
|
.querySelector( '.entry-content' )
|
||||||
|
?.prepend( createNoticeContainer() );
|
||||||
|
}
|
||||||
|
|
||||||
|
const domNode =
|
||||||
|
storeNoticeBlock ??
|
||||||
|
document.querySelector( storeNoticeClass );
|
||||||
|
|
||||||
|
if ( domNode ) {
|
||||||
|
injectNotice( domNode, error.message );
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't care about errors blocking execution, but will
|
||||||
|
// console.error for troubleshooting.
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error( error );
|
||||||
|
} finally {
|
||||||
|
context.woocommerce.displayViewCart = true;
|
||||||
|
context.woocommerce.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleAnimationEnd: (
|
||||||
|
store: Store & { event: AnimationEvent }
|
||||||
|
) => {
|
||||||
|
const { event, context, selectors } = store;
|
||||||
|
if ( event.animationName === 'slideOut' ) {
|
||||||
|
// When the first part of the animation (slide-out) ends, we move
|
||||||
|
// to the second part (slide-in).
|
||||||
|
context.woocommerce.animationStatus =
|
||||||
|
AnimationStatus.SLIDE_IN;
|
||||||
|
} else if ( event.animationName === 'slideIn' ) {
|
||||||
|
// When the second part of the animation ends, we update the
|
||||||
|
// temporary number of items to sync it with the cart and reset the
|
||||||
|
// animation status so it can be triggered again.
|
||||||
|
context.woocommerce.temporaryNumberOfItems =
|
||||||
|
selectors.woocommerce.numberOfItemsInTheCart(
|
||||||
|
store
|
||||||
|
);
|
||||||
|
context.woocommerce.animationStatus =
|
||||||
|
AnimationStatus.IDLE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
woocommerce: {
|
||||||
|
startAnimation: ( store: Store ) => {
|
||||||
|
const { context, selectors } = store;
|
||||||
|
// We start the animation if the cart has loaded, the temporary number
|
||||||
|
// of items is out of sync with the number of items in the cart, the
|
||||||
|
// button is not loading (because that means the user started the
|
||||||
|
// interaction) and the animation hasn't started yet.
|
||||||
|
if (
|
||||||
|
selectors.woocommerce.hasCartLoaded( store ) &&
|
||||||
|
context.woocommerce.temporaryNumberOfItems !==
|
||||||
|
selectors.woocommerce.numberOfItemsInTheCart(
|
||||||
|
store
|
||||||
|
) &&
|
||||||
|
! context.woocommerce.isLoading &&
|
||||||
|
context.woocommerce.animationStatus ===
|
||||||
|
AnimationStatus.IDLE
|
||||||
|
) {
|
||||||
|
context.woocommerce.animationStatus =
|
||||||
|
AnimationStatus.SLIDE_OUT;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
afterLoad: ( store: Store ) => {
|
||||||
|
const { state, selectors } = store;
|
||||||
|
// Subscribe to changes in Cart data.
|
||||||
|
subscribe( () => {
|
||||||
|
const cartData = select( storeKey ).getCartData();
|
||||||
|
const isResolutionFinished =
|
||||||
|
select( storeKey ).hasFinishedResolution( 'getCartData' );
|
||||||
|
if ( isResolutionFinished ) {
|
||||||
|
state.woocommerce.cart = cartData;
|
||||||
|
}
|
||||||
|
}, storeKey );
|
||||||
|
|
||||||
|
// This selector triggers a fetch of the Cart data. It is done in a
|
||||||
|
// `requestIdleCallback` to avoid potential performance issues.
|
||||||
|
requestIdleCallback( () => {
|
||||||
|
if ( ! selectors.woocommerce.hasCartLoaded( store ) ) {
|
||||||
|
select( storeKey ).getCartData();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
|
@ -1,15 +1,78 @@
|
||||||
.wp-block-button.wc-block-components-product-button {
|
.wp-block-button.wc-block-components-product-button {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: $gap-small;
|
||||||
|
|
||||||
|
.wp-block-button__link {
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: normal;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
// Set button font size and padding so it inherits from parent.
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
font-size: 1em;
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading::after {
|
||||||
|
font-family: WooCommerce; /* stylelint-disable-line */
|
||||||
|
content: "\e031";
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
from {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateY(90%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.wc-block-components-product-button__button {
|
.wc-block-components-product-button__button {
|
||||||
border-style: none;
|
border-style: none;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: auto;
|
|
||||||
margin-left: auto;
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
span {
|
||||||
|
|
||||||
|
&.wc-block-slide-out {
|
||||||
|
animation: slideOut 0.1s linear 1 normal forwards;
|
||||||
|
}
|
||||||
|
&.wc-block-slide-in {
|
||||||
|
animation: slideIn 0.1s linear 1 normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wc-block-components-product-button__button--placeholder {
|
.wc-block-components-product-button__button--placeholder {
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import registerDirectives from './directives';
|
import registerDirectives from './directives';
|
||||||
import { init } from './router';
|
import { init } from './router';
|
||||||
export { store } from './store';
|
import { rawStore, afterLoads } from './store';
|
||||||
|
|
||||||
export { navigate } from './router';
|
export { navigate } from './router';
|
||||||
|
export { store } from './store';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the Interactivity API.
|
* Initialize the Interactivity API.
|
||||||
|
@ -9,6 +11,7 @@ export { navigate } from './router';
|
||||||
document.addEventListener( 'DOMContentLoaded', async () => {
|
document.addEventListener( 'DOMContentLoaded', async () => {
|
||||||
registerDirectives();
|
registerDirectives();
|
||||||
await init();
|
await init();
|
||||||
|
afterLoads.forEach( ( afterLoad ) => afterLoad( rawStore ) );
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log( 'Interactivity API started' );
|
console.log( 'Interactivity API started' );
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -32,12 +32,15 @@ const getSerializedState = () => {
|
||||||
return {};
|
return {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const afterLoads = new Set();
|
||||||
|
|
||||||
const rawState = getSerializedState();
|
const rawState = getSerializedState();
|
||||||
export const rawStore = { state: deepSignal( rawState ) };
|
export const rawStore = { state: deepSignal( rawState ) };
|
||||||
|
|
||||||
if ( typeof window !== 'undefined' ) window.store = rawStore;
|
if ( typeof window !== 'undefined' ) window.store = rawStore;
|
||||||
|
|
||||||
export const store = ( { state, ...block } ) => {
|
export const store = ( { state, ...block }, { afterLoad } = {} ) => {
|
||||||
deepMerge( rawStore, block );
|
deepMerge( rawStore, block );
|
||||||
deepMerge( rawState, state );
|
deepMerge( rawState, state );
|
||||||
|
if ( afterLoad ) afterLoads.add( afterLoad );
|
||||||
};
|
};
|
||||||
|
|
|
@ -161,6 +161,8 @@ const entries = {
|
||||||
...getBlockEntries( 'frontend.{t,j}s{,x}' ),
|
...getBlockEntries( 'frontend.{t,j}s{,x}' ),
|
||||||
'mini-cart-component':
|
'mini-cart-component':
|
||||||
'./assets/js/blocks/mini-cart/component-frontend.tsx',
|
'./assets/js/blocks/mini-cart/component-frontend.tsx',
|
||||||
|
'product-button-interactivity':
|
||||||
|
'./assets/js/atomic/blocks/product-elements/button/frontend.tsx',
|
||||||
},
|
},
|
||||||
payments: {
|
payments: {
|
||||||
'wc-payment-method-cheque':
|
'wc-payment-method-cheque':
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
"compare-versions": "4.1.3",
|
"compare-versions": "4.1.3",
|
||||||
"config": "3.3.7",
|
"config": "3.3.7",
|
||||||
"dataloader": "2.1.0",
|
"dataloader": "2.1.0",
|
||||||
"deepsignal": "^1.1.0",
|
"deepsignal": "1.3.6",
|
||||||
"dinero.js": "1.9.1",
|
"dinero.js": "1.9.1",
|
||||||
"dompurify": "^2.4.0",
|
"dompurify": "^2.4.0",
|
||||||
"downshift": "6.1.7",
|
"downshift": "6.1.7",
|
||||||
|
@ -25874,15 +25874,28 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/deepsignal": {
|
"node_modules/deepsignal": {
|
||||||
"version": "1.1.1",
|
"version": "1.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/deepsignal/-/deepsignal-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepsignal/-/deepsignal-1.3.6.tgz",
|
||||||
"integrity": "sha512-ZcRi5KogA7hQiJcnUDGrDno7OCwfyBdcP96yf4BHF3JsUc8SRt3w24RZX6L0Ejijnol8Jt5cV5X8h0dotixHHA==",
|
"integrity": "sha512-yjd+vtiznL6YaMptOsKnEKkPr60OEApa+LRe+Qe6Ile/RfCOrELKk/YM3qVpXFZiyOI3Ng67GDEyjAlqVc697g==",
|
||||||
"dependencies": {
|
|
||||||
"@preact/signals": "^1.0.0",
|
|
||||||
"@preact/signals-core": "^1.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"preact": "10.x"
|
"@preact/signals": "^1.1.4",
|
||||||
|
"@preact/signals-core": "^1.3.1",
|
||||||
|
"@preact/signals-react": "^1.3.3",
|
||||||
|
"preact": "^10.16.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@preact/signals": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@preact/signals-core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@preact/signals-react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"preact": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/default-browser-id": {
|
"node_modules/default-browser-id": {
|
||||||
|
@ -43935,9 +43948,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/preact": {
|
"node_modules/preact": {
|
||||||
"version": "10.11.3",
|
"version": "10.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.16.0.tgz",
|
||||||
"integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==",
|
"integrity": "sha512-XTSj3dJ4roKIC93pald6rWuB2qQJO9gO2iLLyTe87MrjQN+HklueLsmskbywEWqCHlclgz3/M4YLL2iBr9UmMA==",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/preact"
|
"url": "https://opencollective.com/preact"
|
||||||
|
@ -73591,13 +73604,10 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"deepsignal": {
|
"deepsignal": {
|
||||||
"version": "1.1.1",
|
"version": "1.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/deepsignal/-/deepsignal-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepsignal/-/deepsignal-1.3.6.tgz",
|
||||||
"integrity": "sha512-ZcRi5KogA7hQiJcnUDGrDno7OCwfyBdcP96yf4BHF3JsUc8SRt3w24RZX6L0Ejijnol8Jt5cV5X8h0dotixHHA==",
|
"integrity": "sha512-yjd+vtiznL6YaMptOsKnEKkPr60OEApa+LRe+Qe6Ile/RfCOrELKk/YM3qVpXFZiyOI3Ng67GDEyjAlqVc697g==",
|
||||||
"requires": {
|
"requires": {}
|
||||||
"@preact/signals": "^1.0.0",
|
|
||||||
"@preact/signals-core": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"default-browser-id": {
|
"default-browser-id": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
|
@ -87596,9 +87606,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"preact": {
|
"preact": {
|
||||||
"version": "10.11.3",
|
"version": "10.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.16.0.tgz",
|
||||||
"integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg=="
|
"integrity": "sha512-XTSj3dJ4roKIC93pald6rWuB2qQJO9gO2iLLyTe87MrjQN+HklueLsmskbywEWqCHlclgz3/M4YLL2iBr9UmMA=="
|
||||||
},
|
},
|
||||||
"prelude-ls": {
|
"prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
|
|
|
@ -267,7 +267,7 @@
|
||||||
"compare-versions": "4.1.3",
|
"compare-versions": "4.1.3",
|
||||||
"config": "3.3.7",
|
"config": "3.3.7",
|
||||||
"dataloader": "2.1.0",
|
"dataloader": "2.1.0",
|
||||||
"deepsignal": "^1.1.0",
|
"deepsignal": "1.3.6",
|
||||||
"dinero.js": "1.9.1",
|
"dinero.js": "1.9.1",
|
||||||
"dompurify": "^2.4.0",
|
"dompurify": "^2.4.0",
|
||||||
"downshift": "6.1.7",
|
"downshift": "6.1.7",
|
||||||
|
|
|
@ -15,11 +15,20 @@ class ProductButton extends AbstractBlock {
|
||||||
*/
|
*/
|
||||||
protected $block_name = 'product-button';
|
protected $block_name = 'product-button';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It is necessary to register and enqueue assets during the render phase because we want to load assets only if the block has the content.
|
* Get the frontend script handle for this block type.
|
||||||
|
*
|
||||||
|
* @param string $key Data to get, or default to everything.
|
||||||
*/
|
*/
|
||||||
protected function register_block_type_assets() {
|
protected function get_block_type_script( $key = null ) {
|
||||||
return null;
|
$script = [
|
||||||
|
'handle' => 'wc-' . $this->block_name . '-interactivity-frontend',
|
||||||
|
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-interactivity-frontend' ),
|
||||||
|
'dependencies' => [ 'wc-interactivity' ],
|
||||||
|
];
|
||||||
|
|
||||||
|
return $key ? $script[ $key ] : $script;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,6 +38,32 @@ class ProductButton extends AbstractBlock {
|
||||||
return [ 'query', 'queryId', 'postId' ];
|
return [ 'query', 'queryId', 'postId' ];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue frontend assets for this block, just in time for rendering.
|
||||||
|
*
|
||||||
|
* @param array $attributes Any attributes that currently are available from the block.
|
||||||
|
*/
|
||||||
|
protected function enqueue_assets( array $attributes ) {
|
||||||
|
parent::enqueue_assets( $attributes );
|
||||||
|
if ( wc_current_theme_is_fse_theme() ) {
|
||||||
|
add_action(
|
||||||
|
'wp_enqueue_scripts',
|
||||||
|
array( $this, 'dequeue_add_to_cart_scripts' )
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->dequeue_add_to_cart_scripts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dequeue the add-to-cart script.
|
||||||
|
* The block uses Interactivity API, it isn't necessary enqueue the add-to-cart script.
|
||||||
|
*/
|
||||||
|
public function dequeue_add_to_cart_scripts() {
|
||||||
|
wp_dequeue_script( 'wc-add-to-cart' );
|
||||||
|
wp_dequeue_script( 'wc-add-to-cart-variation' );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Include and render the block.
|
* Include and render the block.
|
||||||
*
|
*
|
||||||
|
@ -42,6 +77,13 @@ class ProductButton extends AbstractBlock {
|
||||||
$product = wc_get_product( $post_id );
|
$product = wc_get_product( $post_id );
|
||||||
|
|
||||||
if ( $product ) {
|
if ( $product ) {
|
||||||
|
$number_of_items_in_cart = $this->get_cart_item_quantities_by_product_id( $product->get_id() );
|
||||||
|
$more_than_one_item = $number_of_items_in_cart > 0;
|
||||||
|
$initial_product_text = $more_than_one_item ? sprintf(
|
||||||
|
/* translators: %s: product number. */
|
||||||
|
__( '%s in cart', 'woo-gutenberg-products-block' ),
|
||||||
|
$number_of_items_in_cart
|
||||||
|
) : $product->add_to_cart_text();
|
||||||
$cart_redirect_after_add = get_option( 'woocommerce_cart_redirect_after_add' ) === 'yes';
|
$cart_redirect_after_add = get_option( 'woocommerce_cart_redirect_after_add' ) === 'yes';
|
||||||
$ajax_add_to_cart_enabled = get_option( 'woocommerce_enable_ajax_add_to_cart' ) === 'yes';
|
$ajax_add_to_cart_enabled = get_option( 'woocommerce_enable_ajax_add_to_cart' ) === 'yes';
|
||||||
$is_ajax_button = $ajax_add_to_cart_enabled && ! $cart_redirect_after_add && $product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && $product->is_in_stock();
|
$is_ajax_button = $ajax_add_to_cart_enabled && ! $cart_redirect_after_add && $product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && $product->is_in_stock();
|
||||||
|
@ -64,6 +106,40 @@ class ProductButton extends AbstractBlock {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
wc_store(
|
||||||
|
array(
|
||||||
|
'state' => array(
|
||||||
|
'woocommerce' => array(
|
||||||
|
'inTheCartText' => sprintf(
|
||||||
|
/* translators: %s: product number. */
|
||||||
|
__( '%s in cart', 'woo-gutenberg-products-block' ),
|
||||||
|
'###'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$default_quantity = 1;
|
||||||
|
|
||||||
|
$context = array(
|
||||||
|
'woocommerce' => array(
|
||||||
|
/**
|
||||||
|
* Filters the change the quantity to add to cart.
|
||||||
|
*
|
||||||
|
* @since $VID:$
|
||||||
|
* @param number $default_quantity The default quantity.
|
||||||
|
* @param number $product_id The product id.
|
||||||
|
*/
|
||||||
|
'quantityToAdd' => apply_filters( 'woocommerce_add_to_cart_quantity', $default_quantity, $product->get_id() ),
|
||||||
|
'productId' => $product->get_id(),
|
||||||
|
'addToCartText' => null !== $product->add_to_cart_text() ? $product->add_to_cart_text() : __( 'Add to cart', 'woo-gutenberg-products-block' ),
|
||||||
|
'temporaryNumberOfItems' => $number_of_items_in_cart,
|
||||||
|
'animationStatus' => 'IDLE',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow filtering of the add to cart button arguments.
|
* Allow filtering of the add to cart button arguments.
|
||||||
*
|
*
|
||||||
|
@ -87,6 +163,25 @@ class ProductButton extends AbstractBlock {
|
||||||
$args['attributes']['aria-label'] = wp_strip_all_tags( $args['attributes']['aria-label'] );
|
$args['attributes']['aria-label'] = wp_strip_all_tags( $args['attributes']['aria-label'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( isset( WC()->cart ) && ! WC()->cart->is_empty() ) {
|
||||||
|
$this->prevent_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
$div_directives = 'data-wc-context=\'' . wp_json_encode( $context, JSON_NUMERIC_CHECK ) . '\'';
|
||||||
|
|
||||||
|
$button_directives = '
|
||||||
|
data-wc-on--click="actions.woocommerce.addToCart"
|
||||||
|
data-wc-class--loading="context.woocommerce.isLoading"
|
||||||
|
';
|
||||||
|
|
||||||
|
$span_button_directives = '
|
||||||
|
data-wc-text="selectors.woocommerce.addToCartText"
|
||||||
|
data-wc-class--wc-block-slide-in="selectors.woocommerce.slideInAnimation"
|
||||||
|
data-wc-class--wc-block-slide-out="selectors.woocommerce.slideOutAnimation"
|
||||||
|
data-wc-effect="effects.woocommerce.startAnimation"
|
||||||
|
data-wc-on--animationend="actions.woocommerce.handleAnimationEnd"
|
||||||
|
';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters the add to cart button class.
|
* Filters the add to cart button class.
|
||||||
*
|
*
|
||||||
|
@ -96,22 +191,83 @@ class ProductButton extends AbstractBlock {
|
||||||
*/
|
*/
|
||||||
return apply_filters(
|
return apply_filters(
|
||||||
'woocommerce_loop_add_to_cart_link',
|
'woocommerce_loop_add_to_cart_link',
|
||||||
sprintf(
|
strtr(
|
||||||
'<div class="wp-block-button wc-block-components-product-button %1$s %2$s">
|
'<div class="wp-block-button wc-block-components-product-button {classes} {custom_classes}"
|
||||||
<%3$s href="%4$s" class="%5$s" style="%6$s" %7$s>%8$s</%3$s>
|
{div_directives}
|
||||||
|
>
|
||||||
|
<{html_element}
|
||||||
|
href="{add_to_cart_url}"
|
||||||
|
class="{button_classes}"
|
||||||
|
style="{button_styles}"
|
||||||
|
{attributes}
|
||||||
|
{button_directives}
|
||||||
|
>
|
||||||
|
<span {span_button_directives}> {add_to_cart_text} </span>
|
||||||
|
</{html_element}>
|
||||||
|
{view_cart_html}
|
||||||
</div>',
|
</div>',
|
||||||
esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
|
array(
|
||||||
esc_attr( $classname . ' ' . $custom_width_classes ),
|
'{classes}' => esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
|
||||||
$html_element,
|
'{custom_classes}' => esc_attr( $classname . ' ' . $custom_width_classes ),
|
||||||
esc_url( $product->add_to_cart_url() ),
|
'{html_element}' => $html_element,
|
||||||
isset( $args['class'] ) ? esc_attr( $args['class'] ) : '',
|
'{add_to_cart_url}' => esc_url( $product->add_to_cart_url() ),
|
||||||
esc_attr( $styles_and_classes['styles'] ),
|
'{button_classes}' => isset( $args['class'] ) ? esc_attr( $args['class'] ) : '',
|
||||||
isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '',
|
'{button_styles}' => esc_attr( $styles_and_classes['styles'] ),
|
||||||
esc_html( $product->add_to_cart_text() )
|
'{attributes}' => isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '',
|
||||||
|
'{add_to_cart_text}' => esc_html( $initial_product_text ),
|
||||||
|
'{div_directives}' => $is_ajax_button ? $div_directives : '',
|
||||||
|
'{button_directives}' => $is_ajax_button ? $button_directives : '',
|
||||||
|
'{span_button_directives}' => $is_ajax_button ? $span_button_directives : '',
|
||||||
|
'{view_cart_html}' => $is_ajax_button ? $this->get_view_cart_html() : '',
|
||||||
|
)
|
||||||
),
|
),
|
||||||
$product,
|
$product,
|
||||||
$args
|
$args
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of items in the cart for a given product id.
|
||||||
|
*
|
||||||
|
* @param number $product_id The product id.
|
||||||
|
* @return number The number of items in the cart.
|
||||||
|
*/
|
||||||
|
private function get_cart_item_quantities_by_product_id( $product_id ) {
|
||||||
|
if ( ! isset( WC()->cart ) ) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cart = WC()->cart->get_cart_item_quantities();
|
||||||
|
return isset( $cart[ $product_id ] ) ? $cart[ $product_id ] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent caching on certain pages
|
||||||
|
*/
|
||||||
|
private function prevent_cache() {
|
||||||
|
\WC_Cache_Helper::set_nocache_constants();
|
||||||
|
nocache_headers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the view cart link html.
|
||||||
|
*
|
||||||
|
* @return string The view cart html.
|
||||||
|
*/
|
||||||
|
private function get_view_cart_html() {
|
||||||
|
return sprintf(
|
||||||
|
'<span hidden data-wc-bind--hidden="!selectors.woocommerce.displayViewCart">
|
||||||
|
<a
|
||||||
|
href="%1$s"
|
||||||
|
class="added_to_cart wc_forward"
|
||||||
|
title="%2$s"
|
||||||
|
>
|
||||||
|
%2$s
|
||||||
|
</a>
|
||||||
|
</span>',
|
||||||
|
wc_get_cart_url(),
|
||||||
|
__( 'View cart', 'woo-gutenberg-products-block' )
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './utils';
|
|
@ -0,0 +1,70 @@
|
||||||
|
# Testing WordPress actions and filters
|
||||||
|
|
||||||
|
This documentation covers testing WordPress actions and filters when writing Playwright tests.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Description](#description)
|
||||||
|
- [Usage](#usage)
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
You can test an action or filter (change) by creating a custom WordPress plugin, installing it and when you are done, removing it.
|
||||||
|
|
||||||
|
The 3 functions responsible are located inside of `tests/e2e-pw/mocks/custom-plugins/utils.ts`:
|
||||||
|
|
||||||
|
- `createPluginFromPHPFile()`
|
||||||
|
- `installPluginFromPHPFile()`
|
||||||
|
- `uninstallPluginFromPHPFile()`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Example: Testing a custom Add to Cart text
|
||||||
|
|
||||||
|
1. Create the custom plugin file.
|
||||||
|
|
||||||
|
`update-product-button-text.php`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Custom Add to Cart Text
|
||||||
|
* Description: Modifies the "Add to Cart" button text for WooCommerce products.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function woocommerce_add_to_cart_button_text_archives() {
|
||||||
|
return 'Buy Now';
|
||||||
|
}
|
||||||
|
|
||||||
|
add_filter( 'woocommerce_product_add_to_cart_text', 'woocommerce_add_to_cart_button_text_archives' );
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install the plugin when running the test.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
test( 'the filter `woocommerce_product_add_to_cart_text` is applied', async ( { frontendUtils } ) => {
|
||||||
|
await installPluginFromPHPFile(
|
||||||
|
`${ __dirname }/update-product-button-text.php`
|
||||||
|
);
|
||||||
|
await frontendUtils.goToShop();
|
||||||
|
const blocks = await frontendUtils.getBlockByName( blockData.name );
|
||||||
|
const buttonWithNewText = await blocks.getByText( 'Buy Now' ).count();
|
||||||
|
|
||||||
|
const productsDisplayed = 16;
|
||||||
|
expect( buttonWithNewText ).toEqual( productsDisplayed );
|
||||||
|
} );
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Remove the plugin when done testing.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
test.afterAll( async () => {
|
||||||
|
await uninstallPluginFromPHPFile(
|
||||||
|
`${ __dirname }/update-product-button-text.php`
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, the test checks whether the filter `woocommerce_product_add_to_cart_text` is applied correctly. It installs the "Custom Add to Cart Text" plugin, navigates to the shop page using `frontendUtils`, and verifies if the "Buy Now" button text appears as expected. Finally, it cleans the cart and uninstalls the plugin.
|
||||||
|
|
||||||
|
You can adapt this example to test other filters and actions by modifying the code accordingly.
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { cli } from '@woocommerce/e2e-utils';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const createPluginFromPHPFile = async ( phpFilePath: string ) => {
|
||||||
|
const absolutePath = path.resolve( phpFilePath );
|
||||||
|
const directory = path.dirname( absolutePath );
|
||||||
|
const fileName = path.basename( phpFilePath );
|
||||||
|
const fileNameZip = fileName.replace( '.php', '' );
|
||||||
|
await cli(
|
||||||
|
`cd ${ directory } && zip ${ fileNameZip }.zip ${ fileName } && mv ${ fileNameZip }.zip ${ __dirname }`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const installPluginFromPHPFile = async ( phpFilePath: string ) => {
|
||||||
|
await createPluginFromPHPFile( phpFilePath );
|
||||||
|
const fileName = path.basename( phpFilePath ).replace( '.php', '' );
|
||||||
|
await cli(
|
||||||
|
`npm run wp-env run tests-cli "wp plugin install /var/www/html/custom-plugins/${ fileName }.zip --activate"`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uninstallPluginFromPHPFile = async ( phpFilePath: string ) => {
|
||||||
|
const fileName = path.basename( phpFilePath ).replace( '.php', '' );
|
||||||
|
await cli(
|
||||||
|
`npm run wp-env run tests-cli "wp plugin delete ${ fileName }"`
|
||||||
|
);
|
||||||
|
await cli( `rm ${ __dirname }/${ fileName }.zip` );
|
||||||
|
};
|
|
@ -15,6 +15,7 @@ import {
|
||||||
STORAGE_STATE_PATH,
|
STORAGE_STATE_PATH,
|
||||||
EditorUtils,
|
EditorUtils,
|
||||||
FrontendUtils,
|
FrontendUtils,
|
||||||
|
StoreApiUtils,
|
||||||
} from '@woocommerce/e2e-utils';
|
} from '@woocommerce/e2e-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,6 +108,7 @@ const test = base.extend<
|
||||||
templateApiUtils: TemplateApiUtils;
|
templateApiUtils: TemplateApiUtils;
|
||||||
editorUtils: EditorUtils;
|
editorUtils: EditorUtils;
|
||||||
frontendUtils: FrontendUtils;
|
frontendUtils: FrontendUtils;
|
||||||
|
storeApiUtils: StoreApiUtils;
|
||||||
snapshotConfig: void;
|
snapshotConfig: void;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -142,6 +144,9 @@ const test = base.extend<
|
||||||
frontendUtils: async ( { page }, use ) => {
|
frontendUtils: async ( { page }, use ) => {
|
||||||
await use( new FrontendUtils( page ) );
|
await use( new FrontendUtils( page ) );
|
||||||
},
|
},
|
||||||
|
storeApiUtils: async ( { requestUtils }, use ) => {
|
||||||
|
await use( new StoreApiUtils( requestUtils ) );
|
||||||
|
},
|
||||||
requestUtils: [
|
requestUtils: [
|
||||||
async ( {}, use, workerInfo ) => {
|
async ( {}, use, workerInfo ) => {
|
||||||
const requestUtils = await RequestUtils.setup( {
|
const requestUtils = await RequestUtils.setup( {
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { expect, test } from '@woocommerce/e2e-playwright-utils';
|
||||||
|
import {
|
||||||
|
installPluginFromPHPFile,
|
||||||
|
uninstallPluginFromPHPFile,
|
||||||
|
} from '@woocommerce/e2e-mocks/custom-plugins';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { blockData, handleAddToCartAjaxSetting } from './utils';
|
||||||
|
|
||||||
|
test.describe( `${ blockData.name } Block`, () => {
|
||||||
|
test.beforeEach( async ( { frontendUtils, storeApiUtils } ) => {
|
||||||
|
await storeApiUtils.cleanCart();
|
||||||
|
await frontendUtils.goToShop();
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'should be visible', async ( { frontendUtils } ) => {
|
||||||
|
const blocks = await frontendUtils.getBlockByName( blockData.slug );
|
||||||
|
await expect( blocks ).toHaveCount(
|
||||||
|
blockData.selectors.frontend.productsToDisplay
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
test( 'should add product to the cart', async ( {
|
||||||
|
frontendUtils,
|
||||||
|
page,
|
||||||
|
} ) => {
|
||||||
|
const blocks = await frontendUtils.getBlockByName( blockData.slug );
|
||||||
|
const block = blocks.first();
|
||||||
|
|
||||||
|
const productId = await block
|
||||||
|
.locator( '[data-product_id]' )
|
||||||
|
.getAttribute( 'data-product_id' );
|
||||||
|
|
||||||
|
const productName = await page
|
||||||
|
.locator( `li.post-${ productId } h3` )
|
||||||
|
.textContent();
|
||||||
|
|
||||||
|
// We want to fail the test if the product name is not found.
|
||||||
|
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||||
|
if ( ! productName ) {
|
||||||
|
return test.fail( ! productName, 'Product name was not found' );
|
||||||
|
}
|
||||||
|
|
||||||
|
await block.locator( 'loading' ).waitFor( {
|
||||||
|
state: 'detached',
|
||||||
|
} );
|
||||||
|
await block.click();
|
||||||
|
await expect( block.getByRole( 'button' ) ).toHaveText( '1 in cart' );
|
||||||
|
await expect( block.getByRole( 'link' ) ).toBeVisible();
|
||||||
|
|
||||||
|
await frontendUtils.goToCheckout();
|
||||||
|
const productElement = page.getByText( productName, {
|
||||||
|
exact: true,
|
||||||
|
} );
|
||||||
|
await expect( productElement ).toBeVisible();
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'should add product to the cart - with ajax disabled', async ( {
|
||||||
|
frontendUtils,
|
||||||
|
page,
|
||||||
|
admin,
|
||||||
|
} ) => {
|
||||||
|
await handleAddToCartAjaxSetting( admin, page, {
|
||||||
|
isChecked: true,
|
||||||
|
} );
|
||||||
|
await frontendUtils.goToShop();
|
||||||
|
|
||||||
|
const blocks = await frontendUtils.getBlockByName( blockData.slug );
|
||||||
|
const block = blocks.first();
|
||||||
|
const button = block.getByRole( 'link' );
|
||||||
|
|
||||||
|
const productId = await button.getAttribute( 'data-product_id' );
|
||||||
|
|
||||||
|
const productName = await page
|
||||||
|
.locator( `li.post-${ productId } h3` )
|
||||||
|
.textContent();
|
||||||
|
|
||||||
|
// We want to fail the test if the product name is not found.
|
||||||
|
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||||
|
if ( ! productName ) {
|
||||||
|
return test.fail( ! productName, 'Product name was not found' );
|
||||||
|
}
|
||||||
|
|
||||||
|
await block.click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.locator( `a[href*="cart=${ productId }"]` )
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await frontendUtils.goToCheckout();
|
||||||
|
|
||||||
|
const productElement = page.getByText( productName, {
|
||||||
|
exact: true,
|
||||||
|
} );
|
||||||
|
|
||||||
|
await expect( productElement ).toBeVisible();
|
||||||
|
|
||||||
|
await handleAddToCartAjaxSetting( admin, page, {
|
||||||
|
isChecked: false,
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'the filter `woocommerce_product_add_to_cart_text` should be applied', async ( {
|
||||||
|
frontendUtils,
|
||||||
|
} ) => {
|
||||||
|
await installPluginFromPHPFile(
|
||||||
|
`${ __dirname }/update-product-button-text.php`
|
||||||
|
);
|
||||||
|
await frontendUtils.goToShop();
|
||||||
|
const blocks = await frontendUtils.getBlockByName( blockData.slug );
|
||||||
|
const buttonWithNewText = blocks.getByText( 'Buy Now' );
|
||||||
|
await expect( buttonWithNewText ).toHaveCount(
|
||||||
|
blockData.selectors.frontend.productsToDisplay
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
test.afterAll( async ( { storeApiUtils } ) => {
|
||||||
|
await storeApiUtils.cleanCart();
|
||||||
|
await uninstallPluginFromPHPFile(
|
||||||
|
`${ __dirname }/update-product-button-text.php`
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Custom Add to Cart Text
|
||||||
|
* Description: Modifies the "Add to Cart" button text for WooCommerce products.
|
||||||
|
* @package WordPress
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the "Add to Cart" button text
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @return string The new text.
|
||||||
|
*/
|
||||||
|
function woocommerce_add_to_cart_button_text_archives() {
|
||||||
|
return 'Buy Now';
|
||||||
|
}
|
||||||
|
|
||||||
|
add_filter( 'woocommerce_product_add_to_cart_text', 'woocommerce_add_to_cart_button_text_archives' );
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { Page } from '@playwright/test';
|
||||||
|
import { Admin } from '@wordpress/e2e-test-utils-playwright';
|
||||||
|
|
||||||
|
export const blockData = {
|
||||||
|
name: 'Product Button',
|
||||||
|
slug: 'woocommerce/product-button',
|
||||||
|
mainClass: '.wc-block-product-button',
|
||||||
|
selectors: {
|
||||||
|
frontend: {
|
||||||
|
productsToDisplay: 16,
|
||||||
|
},
|
||||||
|
editor: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleAddToCartAjaxSetting = async (
|
||||||
|
admin: Admin,
|
||||||
|
page: Page,
|
||||||
|
{
|
||||||
|
isChecked,
|
||||||
|
}: {
|
||||||
|
isChecked: boolean;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
await admin.page.goto( 'wp-admin/admin.php?page=wc-settings&tab=products' );
|
||||||
|
await page
|
||||||
|
.getByRole( 'checkbox', {
|
||||||
|
name: 'Enable AJAX add to cart buttons on archives',
|
||||||
|
checked: isChecked,
|
||||||
|
} )
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole( 'button', {
|
||||||
|
name: 'save',
|
||||||
|
} )
|
||||||
|
.click();
|
||||||
|
};
|
|
@ -1 +1 @@
|
||||||
export * from './TemplateApiUtils';
|
export * from './template-api-utils.page';
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export * from './EditorUtils';
|
export * from './editor-utils.page';
|
||||||
|
|
|
@ -44,6 +44,12 @@ export class FrontendUtils {
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async goToCheckout() {
|
||||||
|
await this.page.goto( '/checkout', {
|
||||||
|
waitUntil: 'commit',
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
async isBlockEarlierThan< T >(
|
async isBlockEarlierThan< T >(
|
||||||
containerBlock: T,
|
containerBlock: T,
|
||||||
firstBlock: string,
|
firstBlock: string,
|
|
@ -1 +1 @@
|
||||||
export * from './FrontendUtils';
|
export * from './frontend-utils.page';
|
||||||
|
|
|
@ -4,3 +4,4 @@ export * from './use-block-theme';
|
||||||
export * from './cli';
|
export * from './cli';
|
||||||
export * from './api';
|
export * from './api';
|
||||||
export * from './editor';
|
export * from './editor';
|
||||||
|
export * from './storeApi';
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './store-api-utils.page';
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { RequestUtils } from '@wordpress/e2e-test-utils-playwright';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class StoreApiUtils {
|
||||||
|
private requestUtils: RequestUtils;
|
||||||
|
|
||||||
|
constructor( requestUtils: RequestUtils ) {
|
||||||
|
this.requestUtils = requestUtils;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo: It is necessary work to a middleware to avoid this kind of code.
|
||||||
|
async cleanCart() {
|
||||||
|
const response = await this.requestUtils.request.get(
|
||||||
|
'/wp-json/wc/store/cart'
|
||||||
|
);
|
||||||
|
|
||||||
|
const { nonce } = response.headers();
|
||||||
|
|
||||||
|
await this.requestUtils.request.delete(
|
||||||
|
`/wp-json/wc/store/v1/cart/items`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
nonce,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,7 +29,8 @@
|
||||||
"@woocommerce/shared-hocs": "assets/js/shared/hocs",
|
"@woocommerce/shared-hocs": "assets/js/shared/hocs",
|
||||||
"@woocommerce/blocks-test-utils": "tests/utils",
|
"@woocommerce/blocks-test-utils": "tests/utils",
|
||||||
"@woocommerce/types": "assets/js/types",
|
"@woocommerce/types": "assets/js/types",
|
||||||
"@woocommerce/utils": "assets/js/utils"
|
"@woocommerce/utils": "assets/js/utils",
|
||||||
|
"@woocommerce/interactivity": "assets/js/interactivity"
|
||||||
},
|
},
|
||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
"@wordpress/jest-preset-default/scripts/setup-globals.js",
|
"@wordpress/jest-preset-default/scripts/setup-globals.js",
|
||||||
|
|
|
@ -65,6 +65,7 @@
|
||||||
"@woocommerce/e2e-utils": [ "tests/e2e/utils" ],
|
"@woocommerce/e2e-utils": [ "tests/e2e/utils" ],
|
||||||
"@woocommerce/e2e-types": [ "tests/e2e/types" ],
|
"@woocommerce/e2e-types": [ "tests/e2e/types" ],
|
||||||
"@woocommerce/e2e-playwright-utils": [ "tests/e2e/playwright-utils" ],
|
"@woocommerce/e2e-playwright-utils": [ "tests/e2e/playwright-utils" ],
|
||||||
|
"@woocommerce/e2e-mocks/*": [ "tests/e2e/mocks/*" ],
|
||||||
"@woocommerce/templates/*": [ "assets/js/templates/*" ]
|
"@woocommerce/templates/*": [ "assets/js/templates/*" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue