diff --git a/.husky/post-checkout b/.husky/post-checkout
index cddb5753bc3..1485ab1707b 100755
--- a/.husky/post-checkout
+++ b/.husky/post-checkout
@@ -1,35 +1,30 @@
#!/usr/bin/env bash
. "$(dirname "$0")/_/husky.sh"
-# '1' is branch
+# The hook documentation: https://git-scm.com/docs/githooks.html#_post_checkout
CHECKOUT_TYPE=$3
-redColoured='\033[0;31m'
+HEAD_NEW=$2
+HEAD_PREVIOUS=$1
+
whiteColoured='\033[0m'
+orangeColoured='\033[1;33m'
+# '1' is a branch checkout
if [ "$CHECKOUT_TYPE" = '1' ]; then
- canUpdateDependencies='no'
-
# Prompt about pnpm versions mismatch when switching between branches.
- currentPnpmVersion=$( ( command -v pnpm > /dev/null && pnpm -v ) || echo 'n/a' )
+ currentPnpmVersion=$( ( command -v pnpm > /dev/null && pnpm -v 2>/dev/null ) || echo 'n/a' )
targetPnpmVersion=$( grep packageManager package.json | sed -nr 's/.+packageManager.+pnpm@([[:digit:].]+).+/\1/p' )
if [ "$currentPnpmVersion" != "$targetPnpmVersion" ]; then
- printf "${redColoured}pnpm versions mismatch: in use '$currentPnpmVersion', needed '$targetPnpmVersion'. Here some hints how to solve this:\n"
- printf "${redColoured}* actualize environment: 'nvm use && pnpm -v' (the most common case)\n"
- printf "${redColoured}* install: 'npm install -g pnpm@$targetPnpmVersion'\n"
- else
- canUpdateDependencies='yes'
+ printf "${orangeColoured}pnpm versions mismatch: in use '$currentPnpmVersion', needed '$targetPnpmVersion'. If you are working on something in this branch, here are some hints on how to solve this:\n"
+ printf "${orangeColoured}* actualize environment: 'nvm use && pnpm -v' (the most common case)\n"
+ printf "${orangeColoured}* install: 'npm install -g pnpm@$targetPnpmVersion'\n"
fi
# Auto-refresh dependencies when switching between branches.
- changedManifests=$( ( git diff --name-only HEAD ORIG_HEAD | grep -E '(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|composer.json|composer.lock)$' ) || echo '' )
+ changedManifests=$( ( git diff --name-only $HEAD_NEW $HEAD_PREVIOUS | grep -E '(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|composer.json|composer.lock)$' ) || echo '' )
if [ -n "$changedManifests" ]; then
- printf "${whiteColoured}It was a change in the following file(s) - refreshing dependencies:\n"
+ printf "${whiteColoured}The following file(s) in the new branch differs from the original one, dependencies might need to be refreshed:\n"
printf "${whiteColoured} %s\n" $changedManifests
-
- if [ "$canUpdateDependencies" = 'yes' ]; then
- pnpm install --frozen-lockfile
- else
- printf "${redColoured}Skipping dependencies refresh. Please actualize pnpm version and execute 'pnpm install --frozen-lockfile' manually.\n"
- fi
+ printf "${orangeColoured}If you are working on something in this branch, ensure to refresh dependencies with 'pnpm install --frozen-lockfile'\n"
fi
fi
diff --git a/.husky/post-merge b/.husky/post-merge
index 7ff64bebced..bc022e8fede 100755
--- a/.husky/post-merge
+++ b/.husky/post-merge
@@ -1,6 +1,8 @@
#!/usr/bin/env bash
. "$(dirname "$0")/_/husky.sh"
+# The hook documentation: https://git-scm.com/docs/githooks.html#_post_merge
+
changedManifests=$( ( git diff --name-only HEAD ORIG_HEAD | grep -E '(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|composer.json|composer.lock)$' ) || echo '' )
if [ -n "$changedManifests" ]; then
printf "It was a change in the following file(s) - refreshing dependencies:\n"
diff --git a/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md b/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md
index 3b7a53c9fe8..d5cbef24096 100644
--- a/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md
+++ b/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md
@@ -254,4 +254,4 @@ Displaying the variation in the front store works a bit differently for variable
## How to find hooks?
-Everyone will have their own preferred way, but for me, the quickest way is to look in the WooCommere plugin code. The code for each data section can be found in `/woocommerce/includes/admin/meta-boxes/views`. To view how the inventory section is handled check the `html-product-data-inventory.php` file, and for variations take a look at `html-variation-admin.php`.
+Everyone will have their own preferred way, but for me, the quickest way is to look in the WooCommerce plugin code. The code for each data section can be found in `/woocommerce/includes/admin/meta-boxes/views`. To view how the inventory section is handled check the `html-product-data-inventory.php` file, and for variations take a look at `html-variation-admin.php`.
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/editor.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/editor.tsx
index c2f8c454009..93920c3f71f 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/editor.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/editor.tsx
@@ -23,6 +23,7 @@ import { GlobalStylesRenderer } from '@wordpress/edit-site/build-module/componen
/**
* Internal dependencies
*/
+import { trackEvent } from '../tracking';
import { editorIsLoaded } from '../utils';
import { BlockEditorContainer } from './block-editor-container';
@@ -63,6 +64,7 @@ export const Editor = ( { isLoading }: { isLoading: boolean } ) => {
useEffect( () => {
if ( ! isLoading ) {
editorIsLoaded();
+ trackEvent( 'customize_your_store_assembler_hub_editor_loaded' );
}
}, [ isLoading ] );
diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss
index d713efc1a80..952780f05e2 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.scss
@@ -1,13 +1,17 @@
@import "../../stylesheets/_variables.scss";
.woocommerce-marketplace__category-selector {
+ position: relative;
display: flex;
align-items: stretch;
- margin: $grid-unit-20 0 0 0;
+ margin: 0;
+ overflow-x: auto;
}
.woocommerce-marketplace__category-item {
cursor: pointer;
+ white-space: nowrap;
+ margin-bottom: 0;
.components-dropdown {
height: 100%;
@@ -50,7 +54,6 @@
.woocommerce-marketplace__category-selector--full-width {
display: none;
- margin-top: $grid-unit-15;
}
@media screen and (max-width: $break-medium) {
@@ -122,3 +125,22 @@
background-color: $gray-900;
}
}
+
+.woocommerce-marketplace__category-navigation-button {
+ border: none;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ height: 100%;
+ width: 50px;
+}
+
+.woocommerce-marketplace__category-navigation-button--prev {
+ background: linear-gradient(to right, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
+ left: 0;
+}
+
+.woocommerce-marketplace__category-navigation-button--next {
+ background: linear-gradient(to left, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
+ right: 0;
+}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx
index 07b298cde66..9625aef5862 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/category-selector/category-selector.tsx
@@ -1,20 +1,21 @@
/**
* External dependencies
*/
-import { useState, useEffect } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
+import { useState, useEffect, useRef } from '@wordpress/element';
import { useQuery } from '@woocommerce/navigation';
-import clsx from 'clsx';
+import { Icon } from '@wordpress/components';
+import { useDebounce } from '@wordpress/compose';
+import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import CategoryLink from './category-link';
-import CategoryDropdown from './category-dropdown';
import { Category, CategoryAPIItem } from './types';
import { fetchCategories } from '../../utils/functions';
-import './category-selector.scss';
import { ProductType } from '../product-list/types';
+import CategoryDropdown from './category-dropdown';
+import './category-selector.scss';
const ALL_CATEGORIES_SLUGS = {
[ ProductType.extension ]: '_all',
@@ -29,32 +30,21 @@ interface CategorySelectorProps {
export default function CategorySelector(
props: CategorySelectorProps
): JSX.Element {
- const [ visibleItems, setVisibleItems ] = useState< Category[] >( [] );
- const [ dropdownItems, setDropdownItems ] = useState< Category[] >( [] );
const [ selected, setSelected ] = useState< Category >();
const [ isLoading, setIsLoading ] = useState( false );
+ const [ categoriesToShow, setCategoriesToShow ] = useState< Category[] >(
+ []
+ );
+ const [ isOverflowing, setIsOverflowing ] = useState( false );
+ const [ scrollPosition, setScrollPosition ] = useState<
+ 'start' | 'middle' | 'end'
+ >( 'start' );
+
+ const categorySelectorRef = useRef< HTMLUListElement >( null );
+ const selectedCategoryRef = useRef< HTMLLIElement >( null );
const query = useQuery();
- useEffect( () => {
- // If no category is selected, show All as selected
- let categoryToSearch = ALL_CATEGORIES_SLUGS[ props.type ];
-
- if ( query.category ) {
- categoryToSearch = query.category;
- }
-
- const allCategories = visibleItems.concat( dropdownItems );
-
- const selectedCategory = allCategories.find(
- ( category ) => category.slug === categoryToSearch
- );
-
- if ( selectedCategory ) {
- setSelected( selectedCategory );
- }
- }, [ query.category, props.type, visibleItems, dropdownItems ] );
-
useEffect( () => {
setIsLoading( true );
@@ -72,21 +62,125 @@ export default function CategorySelector(
return category.slug !== '_featured';
} );
- // Split array into two from 7th item
- const visibleCategoryItems = categories.slice( 0, 7 );
- const dropdownCategoryItems = categories.slice( 7 );
-
- setVisibleItems( visibleCategoryItems );
- setDropdownItems( dropdownCategoryItems );
+ setCategoriesToShow( categories );
} )
.catch( () => {
- setVisibleItems( [] );
- setDropdownItems( [] );
+ setCategoriesToShow( [] );
} )
.finally( () => {
setIsLoading( false );
} );
- }, [ props.type ] );
+ }, [ props.type, setCategoriesToShow ] );
+
+ useEffect( () => {
+ // If no category is selected, show All as selected
+ let categoryToSearch = ALL_CATEGORIES_SLUGS[ props.type ];
+
+ if ( query.category ) {
+ categoryToSearch = query.category;
+ }
+
+ const selectedCategory = categoriesToShow.find(
+ ( category ) => category.slug === categoryToSearch
+ );
+
+ if ( selectedCategory ) {
+ setSelected( selectedCategory );
+ }
+ }, [ query.category, props.type, categoriesToShow ] );
+
+ useEffect( () => {
+ if ( selectedCategoryRef.current ) {
+ selectedCategoryRef.current.scrollIntoView( {
+ block: 'nearest',
+ inline: 'center',
+ } );
+ }
+ }, [ selected ] );
+
+ function checkOverflow() {
+ if (
+ categorySelectorRef.current &&
+ categorySelectorRef.current.parentElement?.scrollWidth
+ ) {
+ const isContentOverflowing =
+ categorySelectorRef.current.scrollWidth >
+ categorySelectorRef.current.parentElement.scrollWidth;
+
+ setIsOverflowing( isContentOverflowing );
+ }
+ }
+
+ function checkScrollPosition() {
+ const ulElement = categorySelectorRef.current;
+
+ if ( ! ulElement ) {
+ return;
+ }
+
+ const { scrollLeft, scrollWidth, clientWidth } = ulElement;
+
+ if ( scrollLeft < 10 ) {
+ setScrollPosition( 'start' );
+
+ return;
+ }
+
+ if ( scrollLeft + clientWidth < scrollWidth ) {
+ setScrollPosition( 'middle' );
+
+ return;
+ }
+
+ if ( scrollLeft + clientWidth === scrollWidth ) {
+ setScrollPosition( 'end' );
+ }
+ }
+
+ const debouncedCheckOverflow = useDebounce( checkOverflow, 300 );
+ const debouncedScrollPosition = useDebounce( checkScrollPosition, 100 );
+
+ function scrollCategories( scrollAmount: number ) {
+ if ( categorySelectorRef.current ) {
+ categorySelectorRef.current.scrollTo( {
+ left: categorySelectorRef.current.scrollLeft + scrollAmount,
+ behavior: 'smooth',
+ } );
+ }
+ }
+
+ function scrollToNextCategories() {
+ scrollCategories( 200 );
+ }
+
+ function scrollToPrevCategories() {
+ scrollCategories( -200 );
+ }
+
+ useEffect( () => {
+ window.addEventListener( 'resize', debouncedCheckOverflow );
+
+ const ulElement = categorySelectorRef.current;
+
+ if ( ulElement ) {
+ ulElement.addEventListener( 'scroll', debouncedScrollPosition );
+ }
+
+ return () => {
+ window.removeEventListener( 'resize', debouncedCheckOverflow );
+
+ if ( ulElement ) {
+ ulElement.removeEventListener(
+ 'scroll',
+ debouncedScrollPosition
+ );
+ }
+ };
+ }, [ debouncedCheckOverflow, debouncedScrollPosition ] );
+
+ useEffect( () => {
+ checkOverflow();
+ }, [ categoriesToShow ] );
function mobileCategoryDropdownLabel() {
const allCategoriesText = __( 'All Categories', 'woocommerce' );
@@ -102,16 +196,6 @@ export default function CategorySelector(
return selected.label;
}
- function isSelectedInDropdown() {
- if ( ! selected ) {
- return false;
- }
-
- return dropdownItems.find(
- ( category ) => category.slug === selected.slug
- );
- }
-
if ( isLoading ) {
return (
<>
@@ -131,50 +215,62 @@ export default function CategorySelector(
return (
<>
-
- { visibleItems.map( ( category ) => (
+
+ { categoriesToShow.map( ( category ) => (
-
) ) }
- -
- { dropdownItems.length > 0 && (
-
- ) }
-
-
+ { isOverflowing && (
+ <>
+
+
+ >
+ ) }
>
);
}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/constants.ts b/plugins/woocommerce-admin/client/marketplace/components/constants.ts
index de76eded4e2..d9b7febe49a 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/constants.ts
+++ b/plugins/woocommerce-admin/client/marketplace/components/constants.ts
@@ -10,7 +10,7 @@ export const MARKETPLACE_SEARCH_API_PATH =
'/wp-json/wccom-extensions/1.0/search';
export const MARKETPLACE_CATEGORY_API_PATH =
'/wp-json/wccom-extensions/1.0/categories';
-export const MARKETPLACE_ITEMS_PER_PAGE = 60;
+export const MARKETPLACE_ITEMS_PER_PAGE = 60; // This should match the number of results returned by the API
export const MARKETPLACE_SEARCH_RESULTS_PER_PAGE = 8;
export const MARKETPLACE_CART_PATH = MARKETPLACE_HOST + '/cart/';
export const MARKETPLACE_RENEW_SUBSCRIPTON_PATH =
diff --git a/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx b/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx
index 1a4679dc4c1..06f1a17825b 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/content/content.tsx
@@ -1,22 +1,29 @@
/**
* External dependencies
*/
-import { useContext, useEffect, useState } from '@wordpress/element';
+import {
+ useContext,
+ useEffect,
+ useState,
+ useCallback,
+} from '@wordpress/element';
import { useQuery } from '@woocommerce/navigation';
+import { speak } from '@wordpress/a11y';
+import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import './content.scss';
-import { Product, ProductType, SearchResultType } from '../product-list/types';
+import { Product, ProductType } from '../product-list/types';
import { getAdminSetting } from '~/utils/admin-settings';
import Discover from '../discover/discover';
import Products from '../products/products';
-import SearchResults from '../search-results/search-results';
import MySubscriptions from '../my-subscriptions/my-subscriptions';
import { MarketplaceContext } from '../../contexts/marketplace-context';
-import { fetchSearchResults } from '../../utils/functions';
+import { fetchSearchResults, getProductType } from '../../utils/functions';
import { SubscriptionsContextProvider } from '../../contexts/subscriptions-context';
+import { SearchResultsCountType } from '../../contexts/types';
import {
recordMarketplaceView,
recordLegacyTabView,
@@ -26,149 +33,350 @@ import Promotions from '../promotions/promotions';
import ConnectNotice from '~/marketplace/components/connect-notice/connect-notice';
import PluginInstallNotice from '../woo-update-manager-plugin/plugin-install-notice';
import SubscriptionsExpiredExpiringNotice from '~/marketplace/components/my-subscriptions/subscriptions-expired-expiring-notice';
+import LoadMoreButton from '../load-more-button/load-more-button';
export default function Content(): JSX.Element {
const marketplaceContextValue = useContext( MarketplaceContext );
- const [ products, setProducts ] = useState< Product[] >( [] );
- const { setIsLoading, selectedTab, setHasBusinessServices } =
- marketplaceContextValue;
+ const [ allProducts, setAllProducts ] = useState< Product[] >( [] );
+ const [ filteredProducts, setFilteredProducts ] = useState< Product[] >(
+ []
+ );
+ const [ currentPage, setCurrentPage ] = useState( 1 );
+ const [ totalPagesCategory, setTotalPagesCategory ] = useState( 1 );
+ const [ totalPagesExtensions, setTotalPagesExtensions ] = useState( 1 );
+ const [ totalPagesThemes, setTotalPagesThemes ] = useState( 1 );
+ const [ totalPagesBusinessServices, setTotalPagesBusinessServices ] =
+ useState( 1 );
+ const [ firstNewProductId, setFirstNewProductId ] = useState< number >( 0 );
+ const [ isLoadingMore, setIsLoadingMore ] = useState( false );
+
+ const {
+ isLoading,
+ setIsLoading,
+ selectedTab,
+ setHasBusinessServices,
+ setSearchResultsCount,
+ } = marketplaceContextValue;
const query = useQuery();
- // On initial load of the in-app marketplace, fetch extensions, themes and business services
- // and check if there are any business services available on WCCOM
- useEffect( () => {
- const categories = [ '', 'themes', 'business-services' ];
- const abortControllers = categories.map( () => new AbortController() );
+ const searchCompleteAnnouncement = ( count: number ): void => {
+ speak(
+ sprintf(
+ // translators: %d is the number of products found.
+ __( '%d products found', 'woocommerce' ),
+ count
+ )
+ );
+ };
- categories.forEach( ( category: string, index ) => {
- const params = new URLSearchParams();
- if ( category !== '' ) {
- params.append( 'category', category );
- }
+ const tagProductsWithType = (
+ products: Product[],
+ type: ProductType
+ ): Product[] => {
+ return products.map( ( product ) => ( {
+ ...product,
+ type,
+ } ) );
+ };
- const wccomSettings = getAdminSetting( 'wccomHelper', false );
- if ( wccomSettings.storeCountry ) {
- params.append( 'country', wccomSettings.storeCountry );
- }
-
- fetchSearchResults( params, abortControllers[ index ].signal ).then(
- ( productList ) => {
- if ( category === 'business-services' ) {
- setHasBusinessServices( productList.length > 0 );
- }
- }
- );
- return () => {
- abortControllers.forEach( ( controller ) => {
- controller.abort();
- } );
- };
- } );
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [] );
-
- // Get the content for this screen
- useEffect( () => {
+ const loadMoreProducts = useCallback( () => {
+ setIsLoadingMore( true );
+ const params = new URLSearchParams();
const abortController = new AbortController();
- if (
- query.tab === undefined ||
- ( query.tab &&
- [ '', 'discover', 'my-subscriptions' ].includes( query.tab ) )
- ) {
- return;
+ if ( query.category && query.category !== '_all' ) {
+ params.append( 'category', query.category );
}
- setIsLoading( true );
- setProducts( [] );
-
- const params = new URLSearchParams();
+ if ( query.tab === 'themes' || query.tab === 'business-services' ) {
+ params.append( 'category', query.tab );
+ }
if ( query.term ) {
params.append( 'term', query.term );
}
- if ( query.category ) {
- params.append(
- 'category',
- query.category === '_all' ? '' : query.category
- );
- } else if ( query?.tab === 'themes' ) {
- params.append( 'category', 'themes' );
- } else if ( query?.tab === 'business-services' ) {
- params.append( 'category', 'business-services' );
- } else if ( query?.tab === 'search' ) {
- params.append( 'category', 'extensions-themes-business-services' );
- }
-
const wccomSettings = getAdminSetting( 'wccomHelper', false );
if ( wccomSettings.storeCountry ) {
params.append( 'country', wccomSettings.storeCountry );
}
+ params.append( 'page', ( currentPage + 1 ).toString() );
+
fetchSearchResults( params, abortController.signal )
.then( ( productList ) => {
- setProducts( productList );
+ setAllProducts( ( prevProducts ) => {
+ const flattenedPrevProducts = Array.isArray(
+ prevProducts[ 0 ]
+ )
+ ? prevProducts.flat()
+ : prevProducts;
+
+ const newProducts = productList.products.filter(
+ ( newProduct ) =>
+ ! flattenedPrevProducts.some(
+ ( prevProduct ) =>
+ prevProduct.id === newProduct.id
+ )
+ );
+
+ if ( newProducts.length > 0 ) {
+ setFirstNewProductId( newProducts[ 0 ].id ?? 0 );
+ }
+
+ const combinedProducts = [
+ ...flattenedPrevProducts,
+ ...newProducts,
+ ];
+
+ return combinedProducts;
+ } );
+
+ speak( __( 'More products loaded', 'woocommerce' ) );
+ setCurrentPage( ( prevPage ) => prevPage + 1 );
+ setIsLoadingMore( false );
} )
.catch( () => {
- setProducts( [] );
+ speak( __( 'Error loading more products', 'woocommerce' ) );
} )
.finally( () => {
- // we are recording both the new and legacy events here for now
- // they're separate methods to make it easier to remove the legacy one later
- const marketplaceViewProps = {
- view: query?.tab,
- search_term: query?.term,
- product_type: query?.section,
- category: query?.category,
- };
-
- recordMarketplaceView( marketplaceViewProps );
- recordLegacyTabView( marketplaceViewProps );
- setIsLoading( false );
+ setIsLoadingMore( false );
} );
+
return () => {
abortController.abort();
};
}, [
+ currentPage,
+ query.category,
+ query.term,
+ query.tab,
+ setIsLoadingMore,
+ ] );
+
+ useEffect( () => {
+ // if it's a paginated request, don't use this effect
+ if ( currentPage > 1 ) {
+ return;
+ }
+
+ const categories: Array< {
+ category: keyof SearchResultsCountType;
+ type: ProductType;
+ } > = [
+ { category: 'extensions', type: ProductType.extension },
+ { category: 'themes', type: ProductType.theme },
+ {
+ category: 'business-services',
+ type: ProductType.businessService,
+ },
+ ];
+ const abortControllers = categories.map( () => new AbortController() );
+
+ setIsLoading( true );
+ setAllProducts( [] );
+
+ // If query.category is present and not '_all', only fetch that category
+ if ( query.category && query.category !== '_all' ) {
+ const params = new URLSearchParams();
+
+ params.append( 'category', query.category );
+
+ if ( query.term ) {
+ params.append( 'term', query.term );
+ }
+
+ const wccomSettings = getAdminSetting( 'wccomHelper', false );
+ if ( wccomSettings.storeCountry ) {
+ params.append( 'country', wccomSettings.storeCountry );
+ }
+
+ fetchSearchResults( params, abortControllers[ 0 ].signal )
+ .then( ( productList ) => {
+ setAllProducts( productList.products );
+ setTotalPagesCategory( productList.totalPages );
+ setSearchResultsCount( {
+ [ query.tab ]: productList.totalProducts,
+ } );
+
+ searchCompleteAnnouncement( productList.totalProducts );
+ } )
+ .catch( () => {
+ setAllProducts( [] );
+ } )
+ .finally( () => {
+ setIsLoading( false );
+ } );
+ } else {
+ // Fetch all tabs when query.term or query.category changes
+ Promise.all(
+ categories.map( ( { category, type }, index ) => {
+ const params = new URLSearchParams();
+ if ( category !== 'extensions' ) {
+ params.append( 'category', category );
+ }
+ if ( query.term ) {
+ params.append( 'term', query.term );
+ }
+
+ const wccomSettings = getAdminSetting(
+ 'wccomHelper',
+ false
+ );
+ if ( wccomSettings.storeCountry ) {
+ params.append( 'country', wccomSettings.storeCountry );
+ }
+
+ return fetchSearchResults(
+ params,
+ abortControllers[ index ].signal
+ ).then( ( productList ) => {
+ const typedProducts = tagProductsWithType(
+ productList.products,
+ type
+ );
+ if ( category === 'business-services' ) {
+ setHasBusinessServices( typedProducts.length > 0 );
+ }
+ return {
+ products: typedProducts,
+ totalPages: productList.totalPages,
+ totalProducts: productList.totalProducts,
+ type,
+ };
+ } );
+ } )
+ )
+ .then( ( results ) => {
+ const combinedProducts = results.flatMap(
+ ( result ) => result.products
+ );
+
+ setAllProducts( combinedProducts );
+
+ setSearchResultsCount( {
+ extensions: results.find(
+ ( i ) => i.type === 'extension'
+ )?.totalProducts,
+ themes: results.find( ( i ) => i.type === 'theme' )
+ ?.totalProducts,
+ 'business-services': results.find(
+ ( i ) => i.type === 'business-service'
+ )?.totalProducts,
+ } );
+
+ results.forEach( ( result ) => {
+ switch ( result.type ) {
+ case ProductType.extension:
+ setTotalPagesExtensions( result.totalPages );
+ break;
+ case ProductType.theme:
+ setTotalPagesThemes( result.totalPages );
+ break;
+ case ProductType.businessService:
+ setTotalPagesBusinessServices(
+ result.totalPages
+ );
+ break;
+ }
+ } );
+
+ searchCompleteAnnouncement(
+ results.reduce( ( acc, curr ) => {
+ return acc + curr.totalProducts;
+ }, 0 )
+ );
+ } )
+ .catch( () => {
+ setAllProducts( [] );
+ } )
+ .finally( () => {
+ setIsLoading( false );
+ } );
+ }
+
+ return () => {
+ abortControllers.forEach( ( controller ) => {
+ controller.abort();
+ } );
+ };
+ }, [
+ query.tab,
query.term,
query.category,
- query?.tab,
+ setHasBusinessServices,
setIsLoading,
- query?.section,
+ setSearchResultsCount,
+ currentPage,
] );
+ // Filter the products based on the selected tab
+ useEffect( () => {
+ let filtered: Product[] | null;
+ switch ( selectedTab ) {
+ case 'extensions':
+ filtered = allProducts.filter(
+ ( p ) => p.type === ProductType.extension
+ );
+ break;
+ case 'themes':
+ filtered = allProducts.filter(
+ ( p ) => p.type === ProductType.theme
+ );
+ break;
+ case 'business-services':
+ filtered = allProducts.filter(
+ ( p ) => p.type === ProductType.businessService
+ );
+ break;
+ default:
+ filtered = [];
+ }
+ setFilteredProducts( filtered );
+ }, [ selectedTab, allProducts ] );
+
+ // Record tab view events when the query changes
+ useEffect( () => {
+ const marketplaceViewProps = {
+ view: query?.tab,
+ search_term: query?.term,
+ product_type: query?.section,
+ category: query?.category,
+ };
+ recordMarketplaceView( marketplaceViewProps );
+ recordLegacyTabView( marketplaceViewProps );
+ }, [ query?.tab, query?.term, query?.section, query?.category ] );
+
+ // Reset current page when tab, term, or category changes
+ useEffect( () => {
+ setCurrentPage( 1 );
+ setFirstNewProductId( 0 );
+ }, [ selectedTab, query?.category, query?.term ] );
+
+ // Maintain product focus for accessibility
+ useEffect( () => {
+ if ( firstNewProductId ) {
+ setTimeout( () => {
+ const firstNewProduct = document.getElementById(
+ `product-${ firstNewProductId }`
+ );
+ if ( firstNewProduct ) {
+ firstNewProduct.focus();
+ }
+ }, 0 );
+ }
+ }, [ firstNewProductId ] );
+
const renderContent = (): JSX.Element => {
switch ( selectedTab ) {
case 'extensions':
- return (
-
- );
case 'themes':
- return (
-
- );
case 'business-services':
return (
- );
- case 'search':
- return (
-
);
case 'discover':
@@ -184,10 +392,29 @@ export default function Content(): JSX.Element {
}
};
+ const shouldShowLoadMoreButton = () => {
+ if ( ! query.category || query.category === '_all' ) {
+ // Check against total pages for the selected tab
+ switch ( selectedTab ) {
+ case 'extensions':
+ return currentPage < totalPagesExtensions;
+ case 'themes':
+ return currentPage < totalPagesThemes;
+ case 'business-services':
+ return currentPage < totalPagesBusinessServices;
+ default:
+ return false;
+ }
+ } else {
+ // Check against totalPagesCategory for specific category
+ return currentPage < totalPagesCategory;
+ }
+ };
+
return (
-
+
{ selectedTab !== 'business-services' &&
selectedTab !== 'my-subscriptions' &&
}
{ selectedTab !== 'business-services' &&
}
@@ -197,11 +424,15 @@ export default function Content(): JSX.Element {
{ selectedTab !== 'business-services' && (
) }
- { selectedTab !== 'business-services' && (
-
- ) }
{ renderContent() }
+ { ! isLoading && shouldShowLoadMoreButton() && (
+
+ ) }
);
}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/header/header.scss b/plugins/woocommerce-admin/client/marketplace/components/header/header.scss
index 11616f62a7f..f4e14f0ff23 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/header/header.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/header/header.scss
@@ -5,6 +5,7 @@
background: #fff;
border-bottom: 1px solid $gutenberg-gray-300;
display: grid;
+ gap: $medium-gap;
grid-template: "mktpl-title mktpl-search mktpl-meta" 60px
"mktpl-tabs mktpl-tabs mktpl-tabs" auto / 1fr 320px 36px;
padding: 0 $content-spacing-large;
@@ -73,17 +74,3 @@
padding: 0 $content-spacing-small;
}
}
-
-.woocommerce-marketplace__search {
- margin-right: $medium-gap;
- margin-top: 10px;
-
- input[type="search"] {
- all: unset;
- flex-grow: 1;
- }
-
- @media (width <= $breakpoint-medium) {
- margin: $content-spacing-small;
- }
-}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/load-more-button/load-more-button.tsx b/plugins/woocommerce-admin/client/marketplace/components/load-more-button/load-more-button.tsx
new file mode 100644
index 00000000000..07fb2ea6c48
--- /dev/null
+++ b/plugins/woocommerce-admin/client/marketplace/components/load-more-button/load-more-button.tsx
@@ -0,0 +1,37 @@
+/**
+ * External dependencies
+ */
+import { Button } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { speak } from '@wordpress/a11y';
+import { queueRecordEvent } from '@woocommerce/tracks';
+
+interface LoadMoreProps {
+ onLoadMore: () => void;
+ isBusy: boolean;
+ disabled: boolean;
+}
+
+export default function LoadMoreButton( props: LoadMoreProps ) {
+ const { onLoadMore, isBusy, disabled } = props;
+ function handleClick() {
+ queueRecordEvent( 'marketplace_load_more_button_clicked', {} );
+ onLoadMore();
+ }
+
+ if ( isBusy ) {
+ speak( __( 'Loading more products', 'woocommerce' ) );
+ }
+
+ return (
+
+ );
+}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx
index 22039a28ce1..e96484e6def 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.tsx
@@ -193,6 +193,8 @@ function ProductCard( props: ProductCardProps ): JSX.Element {
return (
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx
index da81e0d8059..9ecdd54c98a 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx
@@ -3,7 +3,6 @@
*/
import { __ } from '@wordpress/i18n';
import { useEffect, useState } from '@wordpress/element';
-import { useQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
@@ -22,8 +21,6 @@ export default function NoResults( props: {
} ): JSX.Element {
const [ productGroups, setProductGroups ] = useState< ProductGroup[] >();
const [ isLoading, setIsLoading ] = useState( false );
- const query = useQuery();
- const showCategorySelector = query.tab === 'search' && query.section;
const productGroupsForSearchType = {
[ SearchResultType.all ]: [
'most-popular',
@@ -123,10 +120,6 @@ export default function NoResults( props: {
}
function categorySelector() {
- if ( ! showCategorySelector ) {
- return <>>;
- }
-
if ( props.type === SearchResultType.all ) {
return <>>;
}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts b/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts
index e768a3430d1..a9720ad070c 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts
+++ b/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts
@@ -1,5 +1,7 @@
export type SearchAPIJSONType = {
products: Array< SearchAPIProductType >;
+ total_pages: number;
+ total_products: number;
};
export type SearchAPIProductType = {
diff --git a/plugins/woocommerce-admin/client/marketplace/components/products/products.scss b/plugins/woocommerce-admin/client/marketplace/components/products/products.scss
index 2d92bfd5259..4fb5f89218c 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/products/products.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/products/products.scss
@@ -9,10 +9,21 @@
}
}
+
.woocommerce-marketplace__sub-header {
display: flex;
-
- .woocommerce-marketplace__customize-your-store-button {
- margin: 16px 0 6px auto;
- }
+ align-items: center;
+ justify-content: space-between;
+ gap: 32px;
}
+
+.woocommerce-marketplace__sub-header__categories {
+ flex: 1;
+ overflow-x: auto;
+ position: relative;
+}
+
+.woocommerce-marketplace__customize-your-store-button {
+ flex-shrink: 0;
+}
+
diff --git a/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx b/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx
index 8ecd211e4d2..a078e991198 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/products/products.tsx
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { __, _n, sprintf } from '@wordpress/i18n';
+import { __ } from '@wordpress/i18n';
import {
createInterpolateElement,
useContext,
@@ -24,7 +24,6 @@ import ProductListContent from '../product-list-content/product-list-content';
import ProductLoader from '../product-loader/product-loader';
import NoResults from '../product-list-content/no-results';
import { Product, ProductType, SearchResultType } from '../product-list/types';
-import { MARKETPLACE_ITEMS_PER_PAGE } from '../constants';
import { ADMIN_URL } from '~/utils/admin-settings';
import { ThemeSwitchWarningModal } from '~/customize-store/intro/warning-modals';
@@ -54,12 +53,10 @@ const LABELS = {
export default function Products( props: ProductsProps ) {
const marketplaceContextValue = useContext( MarketplaceContext );
- const { isLoading, selectedTab } = marketplaceContextValue;
+ const { isLoading } = marketplaceContextValue;
const label = LABELS[ props.type ].label;
- const singularLabel = LABELS[ props.type ].singularLabel;
const query = useQuery();
const category = query?.category;
- const perPage = props.perPage ?? MARKETPLACE_ITEMS_PER_PAGE;
interface Theme {
stylesheet?: string;
}
@@ -94,42 +91,30 @@ export default function Products( props: ProductsProps ) {
}
// Store the total number of products before we slice it later.
- const productTotalCount = props.products?.length ?? 0;
- const products = props.products?.slice( 0, perPage ) ?? [];
-
- let title = sprintf(
- // translators: %s: plural item type (e.g. extensions, themes)
- __( '0 %s found', 'woocommerce' ),
- label
- );
-
- if ( productTotalCount > 0 ) {
- title = sprintf(
- // translators: %1$s: number of items, %2$s: singular item label, %3$s: plural item label
- _n( '%1$s %2$s', '%1$s %3$s', productTotalCount, 'woocommerce' ),
- productTotalCount,
- singularLabel,
- label
- );
- }
+ const products = props.products ?? [];
const labelForClassName =
label === 'business services' ? 'business-services' : label;
const baseContainerClass = 'woocommerce-marketplace__search-';
- const baseProductListTitleClass = 'product-list-title--';
const containerClassName = clsx( baseContainerClass + labelForClassName );
- const productListTitleClassName = clsx(
- 'woocommerce-marketplace__product-list-title',
- baseContainerClass + baseProductListTitleClass + labelForClassName,
- { 'is-loading': isLoading }
- );
const viewAllButonClassName = clsx(
'woocommerce-marketplace__view-all-button',
baseContainerClass + 'button-' + labelForClassName
);
+ if ( isLoading ) {
+ return (
+ <>
+ { props.categorySelector && (
+
+ ) }
+
+ >
+ );
+ }
+
if ( products.length === 0 ) {
let type = SearchResultType.all;
@@ -154,28 +139,14 @@ export default function Products( props: ProductsProps ) {
: ''
);
- if ( isLoading ) {
- return (
- <>
- { props.categorySelector && (
-
- ) }
-
- >
- );
- }
-
return (
- { selectedTab === 'search' && (
-
- { isLoading ? ' ' : title }
-
- ) }
-
- { props.categorySelector && (
-
- ) }
+
+
{ isModalOpen && (
product.type === ProductType.extension
- );
- const themeList = props.products.filter(
- ( product ) => product.type === ProductType.theme
- );
- const businessServiceList = props.products.filter(
- ( product ) => product.type === ProductType.businessService
- );
-
- const hasExtensions = extensionList.length > 0;
- const hasThemes = themeList.length > 0;
- const hasBusinessServices = businessServiceList.length > 0;
- const hasOnlyExtensions =
- hasExtensions && ! hasThemes && ! hasBusinessServices;
- const hasOnlyThemes = hasThemes && ! hasExtensions && ! hasBusinessServices;
- const hasOnlyBusinessServices =
- hasBusinessServices && ! hasExtensions && ! hasThemes;
-
- const marketplaceContextValue = useContext( MarketplaceContext );
- const { isLoading, hasBusinessServices: canShowBusinessServices } =
- marketplaceContextValue;
-
- const query = useQuery();
- const showCategorySelector = query.section ? true : false;
- const searchTerm = query.term ? query.term : '';
-
- type Overrides = {
- categorySelector?: boolean;
- showAllButton?: boolean;
- perPage?: number;
- };
-
- function productsComponent(
- products: Product[],
- type: ProductType,
- overrides: Overrides = {}
- ) {
- return (
-
- );
- }
-
- function extensionsComponent( overrides: Overrides = {} ) {
- return productsComponent(
- extensionList,
- ProductType.extension,
- overrides
- );
- }
-
- function themesComponent( overrides: Overrides = {} ) {
- return productsComponent( themeList, ProductType.theme, overrides );
- }
-
- function businessServicesComponent( overrides: Overrides = {} ) {
- return productsComponent(
- businessServiceList,
- ProductType.businessService,
- overrides
- );
- }
-
- const content = () => {
- if ( query?.section === SearchResultType.extension ) {
- return extensionsComponent( { showAllButton: false } );
- }
-
- if ( query?.section === SearchResultType.theme ) {
- return themesComponent( { showAllButton: false } );
- }
-
- if ( query?.section === SearchResultType.businessService ) {
- return businessServicesComponent( { showAllButton: false } );
- }
-
- // Components can handle their isLoading state. So we can put all three on the page.
- if ( isLoading ) {
- return (
- <>
- { extensionsComponent() }
- { themesComponent() }
- { businessServicesComponent() }
- >
- );
- }
-
- // If we did finish loading items, and there are no results, show the no results component.
- if (
- ! isLoading &&
- ! hasExtensions &&
- ! hasThemes &&
- ! hasBusinessServices
- ) {
- return (
-
- );
- }
-
- // If we're done loading, we can put these components on the page.
- return (
- <>
- { hasExtensions
- ? extensionsComponent( {
- categorySelector: hasOnlyExtensions || undefined,
- showAllButton: hasOnlyExtensions
- ? false
- : undefined,
- perPage: hasOnlyExtensions
- ? MARKETPLACE_ITEMS_PER_PAGE
- : MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
- } )
- : null }
- { hasThemes
- ? themesComponent( {
- categorySelector: hasOnlyThemes || undefined,
- showAllButton: hasOnlyThemes ? false : undefined,
- perPage: hasOnlyThemes
- ? MARKETPLACE_ITEMS_PER_PAGE
- : MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
- } )
- : null }
- { hasBusinessServices
- ? businessServicesComponent( {
- categorySelector:
- hasOnlyBusinessServices || undefined,
- showAllButton: hasOnlyBusinessServices
- ? false
- : undefined,
- perPage: hasOnlyBusinessServices
- ? MARKETPLACE_ITEMS_PER_PAGE
- : MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
- } )
- : null }
- >
- );
- };
-
- return (
-
- { content() }
-
- );
-}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/search/search.scss b/plugins/woocommerce-admin/client/marketplace/components/search/search.scss
index e6725b7ca2d..0fb878f214f 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/search/search.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/search/search.scss
@@ -2,29 +2,15 @@
.woocommerce-marketplace__search {
grid-area: mktpl-search;
- background: $gutenberg-gray-100;
- border: 1.5px solid transparent;
- border-radius: 2px;
- display: flex;
- height: 40px;
- padding: 4px 8px 4px 12px;
-
- input[type="search"] {
- all: unset;
- flex-grow: 1;
- }
-
- &:focus-within {
- background: #fff;
- border-color: var(--wp-admin-theme-color, #3858e9);
- }
+ margin-top: 15px;
+ width: 320px;
@media (width <= $breakpoint-medium) {
margin: $grid-unit-20 $grid-unit-20 $grid-unit-10 $grid-unit-20;
+ width: calc(100% - $grid-unit-20 * 2);
+
+ .components-input-control__input {
+ font-size: 13px !important;
+ }
}
}
-
-.woocommerce-marketplace__search-button {
- all: unset;
- cursor: pointer;
-}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx b/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx
index e03582ca4e8..f9572c7c7fa 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/search/search.tsx
@@ -2,26 +2,20 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
-import { Icon, search } from '@wordpress/icons';
-import { useContext, useEffect, useState } from '@wordpress/element';
+import { useEffect, useState } from '@wordpress/element';
import { navigateTo, getNewPath, useQuery } from '@woocommerce/navigation';
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+// eslint-disable-next-line @woocommerce/dependency-group
+import { SearchControl } from '@wordpress/components';
+// The @ts-ignore is needed because the SearchControl types are not exported from the @wordpress/components package,
+// even though the component itself is. This is likely due to an older version of the package being used.
/**
* Internal dependencies
*/
import './search.scss';
import { MARKETPLACE_PATH } from '../constants';
-import { MarketplaceContext } from '../../contexts/marketplace-context';
-
-const searchPlaceholder = __(
- 'Search for extensions, themes, and business services',
- 'woocommerce'
-);
-
-const searchPlaceholderNoBusinessServices = __(
- 'Search for extensions and themes',
- 'woocommerce'
-);
/**
* Search component.
@@ -30,14 +24,10 @@ const searchPlaceholderNoBusinessServices = __(
*/
function Search(): JSX.Element {
const [ searchTerm, setSearchTerm ] = useState( '' );
- const { hasBusinessServices } = useContext( MarketplaceContext );
+ const searchPlaceholder = __( 'Search Marketplace', 'woocommerce' );
const query = useQuery();
- const placeholder = hasBusinessServices
- ? searchPlaceholder
- : searchPlaceholderNoBusinessServices;
-
useEffect( () => {
if ( query.term ) {
setSearchTerm( query.term );
@@ -46,21 +36,16 @@ function Search(): JSX.Element {
}
}, [ query.term ] );
- useEffect( () => {
- if ( query.tab !== 'search' ) {
- setSearchTerm( '' );
- }
- }, [ query.tab ] );
-
const runSearch = () => {
- const term = searchTerm.trim();
+ const newQuery: { term?: string; tab?: string } = query;
- const newQuery: { term?: string; tab?: string } = {};
- if ( term !== '' ) {
- newQuery.term = term;
- newQuery.tab = 'search';
+ // If we're on 'Discover' or 'My subscriptions' when a search is initiated, move to the extensions tab
+ if ( ! newQuery.tab || newQuery.tab === 'my-subscriptions' ) {
+ newQuery.tab = 'extensions';
}
+ newQuery.term = searchTerm.trim();
+
// When the search term changes, we reset the query string on purpose.
navigateTo( {
url: getNewPath( newQuery, MARKETPLACE_PATH, {} ),
@@ -69,12 +54,6 @@ function Search(): JSX.Element {
return [];
};
- const handleInputChange = (
- event: React.ChangeEvent< HTMLInputElement >
- ) => {
- setSearchTerm( event.target.value );
- };
-
const handleKeyUp = ( event: { key: string } ) => {
if ( event.key === 'Enter' ) {
runSearch();
@@ -86,32 +65,14 @@ function Search(): JSX.Element {
};
return (
-
-
-
-
-
+
);
}
diff --git a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss
index 008fe4e1b84..3a8e7c152e6 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss
+++ b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.scss
@@ -45,6 +45,18 @@
text-align: center;
z-index: 26;
}
+
+ &__update-count-extensions,
+ &__update-count-themes,
+ &__update-count-business-services {
+ background-color: $gutenberg-gray-300;
+ color: $gutenberg-gray-700;
+
+ &.is-active {
+ background-color: #000;
+ color: #fff;
+ }
+ }
}
@media (width <= $breakpoint-medium) {
diff --git a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx
index 1f852a2b982..052a06df435 100644
--- a/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/components/tabs/tabs.tsx
@@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
-import { useContext, useEffect, useState } from '@wordpress/element';
+import { useContext, useEffect, useState, useMemo } from '@wordpress/element';
import { Button } from '@wordpress/components';
import clsx from 'clsx';
import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
@@ -35,63 +35,26 @@ interface Tabs {
const wccomSettings = getAdminSetting( 'wccomHelper', {} );
const wooUpdateCount = wccomSettings?.wooUpdateCount ?? 0;
-const tabs: Tabs = {
- search: {
- name: 'search',
- title: __( 'Search results', 'woocommerce' ),
- showUpdateCount: false,
- updateCount: 0,
- },
- discover: {
- name: 'discover',
- title: __( 'Discover', 'woocommerce' ),
- showUpdateCount: false,
- updateCount: 0,
- },
- extensions: {
- name: 'extensions',
- title: __( 'Extensions', 'woocommerce' ),
- showUpdateCount: false,
- updateCount: 0,
- },
- themes: {
- name: 'themes',
- title: __( 'Themes', 'woocommerce' ),
- showUpdateCount: false,
- updateCount: 0,
- },
- 'business-services': {
- name: 'business-services',
- title: __( 'Business services', 'woocommerce' ),
- showUpdateCount: false,
- updateCount: 0,
- },
- 'my-subscriptions': {
- name: 'my-subscriptions',
- title: __( 'My subscriptions', 'woocommerce' ),
- showUpdateCount: true,
- updateCount: wooUpdateCount,
- },
-};
-
-const setUrlTabParam = ( tabKey: string ) => {
+const setUrlTabParam = ( tabKey: string, query: Record< string, string > ) => {
+ const term = query.term ? { term: query.term.trim() } : {};
navigateTo( {
url: getNewPath(
{ tab: tabKey === DEFAULT_TAB_KEY ? undefined : tabKey },
MARKETPLACE_PATH,
- {}
+ term
),
} );
};
-const getVisibleTabs = ( selectedTab: string, hasBusinessServices = false ) => {
+const getVisibleTabs = (
+ selectedTab: string,
+ hasBusinessServices = false,
+ tabs: Tabs
+) => {
if ( selectedTab === '' ) {
return tabs;
}
const currentVisibleTabs = { ...tabs };
- if ( selectedTab !== 'search' ) {
- delete currentVisibleTabs.search;
- }
if ( ! hasBusinessServices ) {
delete currentVisibleTabs[ 'business-services' ];
}
@@ -101,7 +64,9 @@ const getVisibleTabs = ( selectedTab: string, hasBusinessServices = false ) => {
const renderTabs = (
marketplaceContextValue: MarketplaceContextType,
- visibleTabs: Tabs
+ visibleTabs: Tabs,
+ tabs: Tabs,
+ query: Record< string, string >
) => {
const { selectedTab, setSelectedTab } = marketplaceContextValue;
@@ -110,7 +75,7 @@ const renderTabs = (
return;
}
setSelectedTab( tabKey );
- setUrlTabParam( tabKey );
+ setUrlTabParam( tabKey, query );
};
const tabContent = [];
@@ -143,7 +108,15 @@ const renderTabs = (
{ tabs[ tabKey ]?.title }
{ tabs[ tabKey ]?.showUpdateCount &&
tabs[ tabKey ]?.updateCount > 0 && (
-
+
{ tabs[ tabKey ]?.updateCount }
) }
@@ -157,23 +130,70 @@ const renderTabs = (
const Tabs = ( props: TabsProps ): JSX.Element => {
const { additionalClassNames } = props;
const marketplaceContextValue = useContext( MarketplaceContext );
- const { selectedTab, setSelectedTab, hasBusinessServices } =
+ const { selectedTab, isLoading, setSelectedTab, hasBusinessServices } =
marketplaceContextValue;
- const [ visibleTabs, setVisibleTabs ] = useState( getVisibleTabs( '' ) );
+ const { searchResultsCount } = marketplaceContextValue;
const query: Record< string, string > = useQuery();
+ const tabs: Tabs = useMemo(
+ () => ( {
+ discover: {
+ name: 'discover',
+ title: __( 'Discover', 'woocommerce' ),
+ showUpdateCount: false,
+ updateCount: 0,
+ },
+ extensions: {
+ name: 'extensions',
+ title: __( 'Extensions', 'woocommerce' ),
+ showUpdateCount: !! query.term && ! isLoading,
+ updateCount: searchResultsCount.extensions,
+ },
+ themes: {
+ name: 'themes',
+ title: __( 'Themes', 'woocommerce' ),
+ showUpdateCount: !! query.term && ! isLoading,
+ updateCount: searchResultsCount.themes,
+ },
+ 'business-services': {
+ name: 'business-services',
+ title: __( 'Business services', 'woocommerce' ),
+ showUpdateCount: !! query.term && ! isLoading,
+ updateCount: searchResultsCount[ 'business-services' ],
+ },
+ 'my-subscriptions': {
+ name: 'my-subscriptions',
+ title: __( 'My subscriptions', 'woocommerce' ),
+ showUpdateCount: true,
+ updateCount: wooUpdateCount,
+ },
+ } ),
+ [ query, isLoading, searchResultsCount ]
+ );
+
+ const [ visibleTabs, setVisibleTabs ] = useState(
+ getVisibleTabs( '', false, tabs )
+ );
+
useEffect( () => {
if ( query?.tab && tabs[ query.tab ] ) {
setSelectedTab( query.tab );
} else if ( Object.keys( query ).length > 0 ) {
setSelectedTab( DEFAULT_TAB_KEY );
}
- }, [ query, setSelectedTab ] );
+ }, [ query, setSelectedTab, tabs ] );
useEffect( () => {
- setVisibleTabs( getVisibleTabs( selectedTab, hasBusinessServices ) );
- }, [ selectedTab, hasBusinessServices ] );
+ setVisibleTabs(
+ getVisibleTabs( selectedTab, hasBusinessServices, tabs )
+ );
+
+ if ( selectedTab === 'business-services' && ! hasBusinessServices ) {
+ setUrlTabParam( 'extensions', query );
+ }
+ }, [ selectedTab, hasBusinessServices, query, tabs ] );
+
return (
);
};
diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx b/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx
index 53dd1bb5789..0631bf5725e 100644
--- a/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/contexts/marketplace-context.tsx
@@ -1,12 +1,17 @@
/**
* External dependencies
*/
-import { useState, useEffect, createContext } from '@wordpress/element';
+import {
+ useState,
+ useEffect,
+ useCallback,
+ createContext,
+} from '@wordpress/element';
/**
* Internal dependencies
*/
-import { MarketplaceContextType } from './types';
+import { SearchResultsCountType, MarketplaceContextType } from './types';
import { getAdminSetting } from '../../utils/admin-settings';
export const MarketplaceContext = createContext< MarketplaceContextType >( {
@@ -18,6 +23,12 @@ export const MarketplaceContext = createContext< MarketplaceContextType >( {
addInstalledProduct: () => {},
hasBusinessServices: false,
setHasBusinessServices: () => {},
+ searchResultsCount: {
+ extensions: 0,
+ themes: 0,
+ 'business-services': 0,
+ },
+ setSearchResultsCount: () => {},
} );
export function MarketplaceContextProvider( props: {
@@ -29,6 +40,22 @@ export function MarketplaceContextProvider( props: {
[]
);
const [ hasBusinessServices, setHasBusinessServices ] = useState( false );
+ const [ searchResultsCount, setSearchResultsCountState ] =
+ useState< SearchResultsCountType >( {
+ extensions: 0,
+ themes: 0,
+ 'business-services': 0,
+ } );
+
+ const setSearchResultsCount = useCallback(
+ ( updatedCounts: Partial< SearchResultsCountType > ) => {
+ setSearchResultsCountState( ( prev ) => ( {
+ ...prev,
+ ...updatedCounts,
+ } ) );
+ },
+ []
+ );
/**
* Knowing installed products will help us to determine which products
@@ -59,6 +86,8 @@ export function MarketplaceContextProvider( props: {
addInstalledProduct,
hasBusinessServices,
setHasBusinessServices,
+ searchResultsCount,
+ setSearchResultsCount,
};
return (
diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/types.ts b/plugins/woocommerce-admin/client/marketplace/contexts/types.ts
index 969ce88d2e1..3240729871e 100644
--- a/plugins/woocommerce-admin/client/marketplace/contexts/types.ts
+++ b/plugins/woocommerce-admin/client/marketplace/contexts/types.ts
@@ -8,6 +8,12 @@ import { Options } from '@wordpress/notices';
*/
import { Subscription } from '../components/my-subscriptions/types';
+export interface SearchResultsCountType {
+ extensions: number;
+ themes: number;
+ 'business-services': number;
+}
+
export type MarketplaceContextType = {
isLoading: boolean;
setIsLoading: ( isLoading: boolean ) => void;
@@ -17,6 +23,10 @@ export type MarketplaceContextType = {
addInstalledProduct: ( slug: string ) => void;
hasBusinessServices: boolean;
setHasBusinessServices: ( hasBusinessServices: boolean ) => void;
+ searchResultsCount: SearchResultsCountType;
+ setSearchResultsCount: (
+ updatedCounts: Partial< SearchResultsCountType >
+ ) => void;
};
export type SubscriptionsContextType = {
diff --git a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx
index c0415e1b501..e376e7a9961 100644
--- a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx
+++ b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx
@@ -107,7 +107,11 @@ async function fetchJsonWithCache(
async function fetchSearchResults(
params: URLSearchParams,
abortSignal?: AbortSignal
-): Promise< Product[] > {
+): Promise< {
+ products: Product[];
+ totalPages: number;
+ totalProducts: number;
+} > {
const url =
MARKETPLACE_HOST +
MARKETPLACE_SEARCH_API_PATH +
@@ -153,9 +157,12 @@ async function fetchSearchResults(
};
}
);
- resolve( products );
+ const totalPages = ( json as SearchAPIJSONType ).total_pages;
+ const totalProducts = ( json as SearchAPIJSONType )
+ .total_products;
+ resolve( { products, totalPages, totalProducts } );
} )
- .catch( () => reject );
+ .catch( reject );
} );
}
@@ -176,6 +183,17 @@ async function fetchDiscoverPageData(): Promise< ProductGroup[] > {
}
}
+function getProductType( tab: string ): ProductType {
+ switch ( tab ) {
+ case 'themes':
+ return ProductType.theme;
+ case 'business-services':
+ return ProductType.businessService;
+ default:
+ return ProductType.extension;
+ }
+}
+
function fetchCategories( type: ProductType ): Promise< CategoryAPIItem[] > {
const url = new URL( MARKETPLACE_HOST + MARKETPLACE_CATEGORY_API_PATH );
@@ -482,6 +500,7 @@ export {
fetchCategories,
fetchDiscoverPageData,
fetchSearchResults,
+ getProductType,
fetchSubscriptions,
refreshSubscriptions,
getInstallUrl,
diff --git a/plugins/woocommerce-admin/client/marketplace/utils/tracking.ts b/plugins/woocommerce-admin/client/marketplace/utils/tracking.ts
index 6729c824c55..8b08027770e 100644
--- a/plugins/woocommerce-admin/client/marketplace/utils/tracking.ts
+++ b/plugins/woocommerce-admin/client/marketplace/utils/tracking.ts
@@ -42,11 +42,6 @@ function recordMarketplaceView( props: MarketplaceViewProps ) {
eventProps.category = '_all';
}
- // User clicks the `View All` button on search results
- if ( view && view === 'search' && product_type && ! category ) {
- eventProps.category = '_all';
- }
-
recordEvent( 'marketplace_view', eventProps );
}
@@ -80,11 +75,6 @@ function recordLegacyTabView( props: MarketplaceViewProps ) {
case 'themes':
oldEventProps.section = 'themes';
break;
- case 'search':
- oldEventName = 'extensions_view_search';
- oldEventProps.section = view;
- oldEventProps.search_term = search_term || '';
- break;
case 'my-subscriptions':
oldEventName = 'subscriptions_view';
oldEventProps.section = 'helper';
diff --git a/plugins/woocommerce/changelog/51309-update-21591-in-app-category-selector b/plugins/woocommerce/changelog/51309-update-21591-in-app-category-selector
new file mode 100644
index 00000000000..504e3d4d2d2
--- /dev/null
+++ b/plugins/woocommerce/changelog/51309-update-21591-in-app-category-selector
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Update In-App Marketplace category selector
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/51313-update-wccom-21595-in-app-search-component b/plugins/woocommerce/changelog/51313-update-wccom-21595-in-app-search-component
new file mode 100644
index 00000000000..42a06d9bf86
--- /dev/null
+++ b/plugins/woocommerce/changelog/51313-update-wccom-21595-in-app-search-component
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Replace marketplace search component with SearchControl from @wordpress/components
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/51342-fix-21596-search-loading-state b/plugins/woocommerce/changelog/51342-fix-21596-search-loading-state
new file mode 100644
index 00000000000..a55cd0e6601
--- /dev/null
+++ b/plugins/woocommerce/changelog/51342-fix-21596-search-loading-state
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix the loading state for the In-App Marketplace search
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/51422-e2e-external-expand-wpcom-suite-part3 b/plugins/woocommerce/changelog/51422-e2e-external-expand-wpcom-suite-part3
new file mode 100644
index 00000000000..4eeb743cc9d
--- /dev/null
+++ b/plugins/woocommerce/changelog/51422-e2e-external-expand-wpcom-suite-part3
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Expand the e2e suite we're running on WPCOM part #3.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/51434-add-wccom-21568-in-app-load-more-button b/plugins/woocommerce/changelog/51434-add-wccom-21568-in-app-load-more-button
new file mode 100644
index 00000000000..50f6c905990
--- /dev/null
+++ b/plugins/woocommerce/changelog/51434-add-wccom-21568-in-app-load-more-button
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Added a Load More button to product lists on the Extensions page, to request additional search results from WooCommerce.com.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/51449-dev-harden-added-to-cart b/plugins/woocommerce/changelog/51449-dev-harden-added-to-cart
deleted file mode 100644
index 99351de4130..00000000000
--- a/plugins/woocommerce/changelog/51449-dev-harden-added-to-cart
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fix bug where manually triggering `added_to_cart` event without a button element caused an Exception.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/add-50832-loading-time b/plugins/woocommerce/changelog/add-50832-loading-time
new file mode 100644
index 00000000000..3b1f7bef150
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-50832-loading-time
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Track customize_your_store_assembler_hub_editor_loaded event to measure CYS loading time
diff --git a/plugins/woocommerce/changelog/fix-51472-fix-deprecation-notice b/plugins/woocommerce/changelog/fix-51472-fix-deprecation-notice
new file mode 100644
index 00000000000..79ce818f52b
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-51472-fix-deprecation-notice
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Wrap parse_str under a check to resolve deprecation notice
diff --git a/plugins/woocommerce/changelog/fix-unit-test-trac-61739 b/plugins/woocommerce/changelog/fix-unit-test-trac-61739
new file mode 100644
index 00000000000..45cd4544e59
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-unit-test-trac-61739
@@ -0,0 +1,5 @@
+Significance: patch
+Type: dev
+Comment: Update unit test to account for WordPress nightly change. See core trac ticket 61739
+
+
diff --git a/plugins/woocommerce/changelog/refactor-wccom-21576-search-tab b/plugins/woocommerce/changelog/refactor-wccom-21576-search-tab
new file mode 100644
index 00000000000..ba8046f3451
--- /dev/null
+++ b/plugins/woocommerce/changelog/refactor-wccom-21576-search-tab
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Change the way search results are displayed in the in-app marketplace
diff --git a/plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count b/plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count
new file mode 100644
index 00000000000..b0edda219c3
--- /dev/null
+++ b/plugins/woocommerce/changelog/tweak-21597-in-app-search-results-count
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add search result counts to the in-app marketplace header tabs (Extensions area)
diff --git a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php
index cadf53a7bc5..0ce9d749a2b 100644
--- a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php
+++ b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php
@@ -125,6 +125,7 @@ class WC_Product_CSV_Importer_Controller {
// Check that file is within an allowed location.
if ( $is_valid_file ) {
+ $normalized_path = wp_normalize_path( $path );
$in_valid_location = false;
$valid_locations = array();
$valid_locations[] = ABSPATH;
@@ -135,7 +136,8 @@ class WC_Product_CSV_Importer_Controller {
}
foreach ( $valid_locations as $valid_location ) {
- if ( 0 === stripos( $path, trailingslashit( realpath( $valid_location ) ) ) ) {
+ $normalized_location = wp_normalize_path( realpath( $valid_location ) );
+ if ( 0 === stripos( $normalized_path, trailingslashit( $normalized_location ) ) ) {
$in_valid_location = true;
break;
}
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
index b5a611d6f8a..b6da3a8e879 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
@@ -656,21 +656,21 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
// Fire actions to let 3rd parties know the stock is about to be changed.
if ( $product->is_type( 'variation' ) ) {
/**
- * Action to signal that the value of 'stock_quantity' for a variation is about to change.
- *
- * @param WC_Product $product The variation whose stock is about to change.
- *
- * @since 4.9
- */
+ * Action to signal that the value of 'stock_quantity' for a variation is about to change.
+ *
+ * @since 4.9
+ *
+ * @param int $product The variation whose stock is about to change.
+ */
do_action( 'woocommerce_variation_before_set_stock', $product );
} else {
/**
- * Action to signal that the value of 'stock_quantity' for a product is about to change.
- *
- * @param WC_Product $product The product whose stock is about to change.
- *
- * @since 4.9
- */
+ * Action to signal that the value of 'stock_quantity' for a product is about to change.
+ *
+ * @since 4.9
+ *
+ * @param int $product The product whose stock is about to change.
+ */
do_action( 'woocommerce_product_before_set_stock', $product );
}
break;
diff --git a/plugins/woocommerce/includes/wc-stock-functions.php b/plugins/woocommerce/includes/wc-stock-functions.php
index 1489a0e6630..e81b31e8c50 100644
--- a/plugins/woocommerce/includes/wc-stock-functions.php
+++ b/plugins/woocommerce/includes/wc-stock-functions.php
@@ -242,10 +242,31 @@ function wc_trigger_stock_change_notifications( $order, $changes ) {
return;
}
- $order_notes = array();
+ $order_notes = array();
+ $no_stock_amount = absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) );
foreach ( $changes as $change ) {
- $order_notes[] = $change['product']->get_formatted_name() . ' ' . $change['from'] . '→' . $change['to'];
+ $order_notes[] = $change['product']->get_formatted_name() . ' ' . $change['from'] . '→' . $change['to'];
+ $low_stock_amount = absint( wc_get_low_stock_amount( wc_get_product( $change['product']->get_id() ) ) );
+ if ( $change['to'] <= $no_stock_amount ) {
+ /**
+ * Action to signal that the value of 'stock_quantity' for a variation is about to change.
+ *
+ * @since 4.9
+ *
+ * @param int $product The variation whose stock is about to change.
+ */
+ do_action( 'woocommerce_no_stock', wc_get_product( $change['product']->get_id() ) );
+ } elseif ( $change['to'] <= $low_stock_amount ) {
+ /**
+ * Action to signal that the value of 'stock_quantity' for a product is about to change.
+ *
+ * @since 4.9
+ *
+ * @param int $product The product whose stock is about to change.
+ */
+ do_action( 'woocommerce_low_stock', wc_get_product( $change['product']->get_id() ) );
+ }
if ( $change['to'] < 0 ) {
/**
@@ -312,8 +333,6 @@ function wc_trigger_stock_change_actions( $product ) {
do_action( 'woocommerce_low_stock', $product );
}
}
-add_action( 'woocommerce_variation_set_stock', 'wc_trigger_stock_change_actions' );
-add_action( 'woocommerce_product_set_stock', 'wc_trigger_stock_change_actions' );
/**
* Increase stock levels for items within an order.
@@ -485,11 +504,8 @@ function wc_get_low_stock_amount( WC_Product $product ) {
$low_stock_amount = $product->get_low_stock_amount();
if ( '' === $low_stock_amount && $product->is_type( 'variation' ) ) {
- $parent_product = wc_get_product( $product->get_parent_id() );
-
- if ( $parent_product instanceof WC_Product ) {
- $low_stock_amount = $parent_product->get_low_stock_amount();
- }
+ $product = wc_get_product( $product->get_parent_id() );
+ $low_stock_amount = $product->get_low_stock_amount();
}
if ( '' === $low_stock_amount ) {
diff --git a/plugins/woocommerce/src/Admin/WCAdminHelper.php b/plugins/woocommerce/src/Admin/WCAdminHelper.php
index 9e234762eb8..768102e35fb 100644
--- a/plugins/woocommerce/src/Admin/WCAdminHelper.php
+++ b/plugins/woocommerce/src/Admin/WCAdminHelper.php
@@ -154,11 +154,13 @@ class WCAdminHelper {
'post_type' => 'product',
);
- parse_str( wp_parse_url( $url, PHP_URL_QUERY ), $url_params );
-
- foreach ( $params as $key => $param ) {
- if ( isset( $url_params[ $key ] ) && $url_params[ $key ] === $param ) {
- return true;
+ $query_string = wp_parse_url( $url, PHP_URL_QUERY );
+ if ( $query_string ) {
+ parse_str( $query_string, $url_params );
+ foreach ( $params as $key => $param ) {
+ if ( isset( $url_params[ $key ] ) && $url_params[ $key ] === $param ) {
+ return true;
+ }
}
}
diff --git a/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js
index 27ec548c34a..a3e2135c0ad 100644
--- a/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js
+++ b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js
@@ -23,6 +23,16 @@ config = {
'**/merchant/create-order.spec.js',
'**/merchant/create-page.spec.js',
'**/merchant/create-post.spec.js',
+ '**/merchant/create-restricted-coupons.spec.js',
+ '**/merchant/create-shipping-classes.spec.js',
+ '**/merchant/create-shipping-zones.spec.js',
+ '**/merchant/create-woocommerce-blocks.spec.js',
+ '**/merchant/create-woocommerce-patterns.spec.js',
+ '**/merchant/customer-list.spec.js',
+ '**/merchant/customer-payment-page.spec.js',
+ '**/merchant/launch-your-store.spec.js',
+ '**/merchant/lost-password.spec.js',
+ '**/merchant/order-bulk-edit.spec.js',
],
grepInvert: /@skip-on-default-wpcom/,
},
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-restricted-coupons.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-restricted-coupons.spec.js
index 3fd0b4c1717..2d6f69e7eb0 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-restricted-coupons.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-restricted-coupons.spec.js
@@ -103,397 +103,414 @@ const test = baseTest.extend( {
},
} );
-test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
- for ( const couponType of Object.keys( couponData ) ) {
- test( `can create new ${ couponType } coupon`, async ( {
- page,
- coupon,
- product,
- } ) => {
- // create basics for the coupon
- await test.step( 'add new coupon', async () => {
- await page.goto(
- 'wp-admin/post-new.php?post_type=shop_coupon'
- );
- await page
- .getByLabel( 'Coupon code' )
- .fill( couponData[ couponType ].code );
- await page
- .getByPlaceholder( 'Description (optional)' )
- .fill( couponData[ couponType ].description );
- await page
- .getByPlaceholder( '0' )
- .fill( couponData[ couponType ].amount );
- await expect( page.getByText( 'Move to Trash' ) ).toBeVisible();
- } );
-
- // set up the restrictions for each coupon type
- // set minimum spend
- if ( couponType === 'minimumSpend' ) {
- await test.step( 'set minimum spend coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'No minimum' )
- .fill( couponData[ couponType ].minSpend );
- } );
- }
- // set maximum spend
- if ( couponType === 'maximumSpend' ) {
- await test.step( 'set maximum spend coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'No maximum' )
- .fill( couponData[ couponType ].maxSpend );
- } );
- }
- // set individual use
- if ( couponType === 'individualUse' ) {
- await test.step( 'set individual use coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page.getByLabel( 'Individual use only' ).check();
- } );
- }
- // set exclude sale items
- if ( couponType === 'excludeSaleItems' ) {
- await test.step( 'set exclude sale items coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page.getByLabel( 'Exclude sale items' ).check();
- } );
- }
- // set product categories
- if ( couponType === 'productCategories' ) {
- await test.step( 'set product categories coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'Any category' )
- .pressSequentially( 'Uncategorized' );
- await page
- .getByRole( 'option', { name: 'Uncategorized' } )
- .click();
- } );
- }
- // set exclude product categories
- if ( couponType === 'excludeProductCategories' ) {
- await test.step( 'set exclude product categories coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'No categories' )
- .pressSequentially( 'Uncategorized' );
- await page
- .getByRole( 'option', { name: 'Uncategorized' } )
- .click();
- } );
- }
-
- // Skip Brands tests while behind a feature flag.
- const skipBrandsTests = true;
-
- // set exclude product brands
- if ( couponType === 'excludeProductBrands' && ! skipBrandsTests ) {
- await test.step( 'set exclude product brands coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'No brands' )
- .pressSequentially( 'WooCommerce Apparels' );
- await page
- .getByRole( 'option', { name: 'WooCommerce Apparels' } )
- .click();
- } );
- }
- // set products
- if ( couponType === 'products' ) {
- await test.step( 'set products coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'Search for a product…' )
- .first()
- .pressSequentially( product.name );
- await page
- .getByRole( 'option', { name: product.name } )
- .click();
- } );
- }
- // set exclude products
- if ( couponType === 'excludeProducts' ) {
- await test.step( 'set exclude products coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'Search for a product…' )
- .last()
- .pressSequentially( product.name );
- await page
- .getByRole( 'option', { name: product.name } )
- .click();
- } );
- }
- // set allowed emails
- if ( couponType === 'allowedEmails' ) {
- await test.step( 'set allowed emails coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'No restrictions' )
- .fill( couponData[ couponType ].allowedEmails[ 0 ] );
- } );
- }
- // set usage limit
- if ( couponType === 'usageLimitPerCoupon' ) {
- await test.step( 'set usage limit coupon', async () => {
- await page
- .getByRole( 'link', { name: 'Usage limits' } )
- .click();
- await page
- .getByLabel( 'Usage limit per coupon' )
- .fill( couponData[ couponType ].usageLimit );
- } );
- }
- // set usage limit per user
- if ( couponType === 'usageLimitPerUser' ) {
- await test.step( 'set usage limit per user coupon', async () => {
- await page
- .getByRole( 'link', { name: 'Usage limits' } )
- .click();
- await page
- .getByLabel( 'Usage limit per user' )
- .fill( couponData[ couponType ].usageLimitPerUser );
- } );
- }
-
- // publish the coupon and retrieve the id
- await test.step( 'publish the coupon', async () => {
- await page
- .getByRole( 'button', { name: 'Publish', exact: true } )
- .click();
- await expect(
- page.getByText( 'Coupon updated.' )
- ).toBeVisible();
- coupon.id = page.url().match( /(?<=post=)\d+/ )[ 0 ];
- expect( coupon.id ).toBeDefined();
- } );
-
- // verify the creation of the coupon and basic details
- await test.step( 'verify coupon creation', async () => {
- await page.goto( 'wp-admin/edit.php?post_type=shop_coupon' );
- await expect(
- page.getByRole( 'cell', {
- name: couponData[ couponType ].code,
- } )
- ).toBeVisible();
- await expect(
- page.getByRole( 'cell', {
- name: couponData[ couponType ].description,
- } )
- ).toBeVisible();
- await expect(
- page.getByRole( 'cell', {
- name: couponData[ couponType ].amount,
- exact: true,
- } )
- ).toBeVisible();
-
- await page
- .getByRole( 'link', {
- name: couponData[ couponType ].code,
- } )
- .first()
- .click();
- } );
-
- // verify the restrictions for each coupon type
- // verify minimum spend
- if ( couponType === 'minimumSpend' ) {
- await test.step( 'verify minimum spend coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByPlaceholder( 'No minimum' )
- ).toHaveValue( couponData[ couponType ].minSpend );
- } );
- }
-
- // verify maximum spend
- if ( couponType === 'maximumSpend' ) {
- await test.step( 'verify maximum spend coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByPlaceholder( 'No maximum' )
- ).toHaveValue( couponData[ couponType ].maxSpend );
- } );
- }
-
- // verify individual use
- if ( couponType === 'individualUse' ) {
- await test.step( 'verify individual use coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByLabel( 'Individual use only' )
- ).toBeChecked();
- } );
- }
-
- // verify exclude sale items
- if ( couponType === 'excludeSaleItems' ) {
- await test.step( 'verify exclude sale items coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByLabel( 'Exclude sale items' )
- ).toBeChecked();
- } );
- }
-
- // verify product categories
- if ( couponType === 'productCategories' ) {
- await test.step( 'verify product categories coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByRole( 'listitem', {
- name: 'Uncategorized',
- } )
- ).toBeVisible();
- } );
- }
-
- // verify exclude product categories
- if ( couponType === 'excludeProductCategories' ) {
- await test.step( 'verify exclude product categories coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByRole( 'listitem', {
- name: 'Uncategorized',
- } )
- ).toBeVisible();
- } );
- }
-
- // verify products
- if ( couponType === 'products' ) {
- await test.step( 'verify products coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByRole( 'listitem', { name: product.name } )
- ).toBeVisible();
- } );
- }
-
- // verify exclude products
- if ( couponType === 'excludeProducts' ) {
- await test.step( 'verify exclude products coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByRole( 'listitem', { name: product.name } )
- ).toBeVisible();
- } );
- }
-
- // verify allowed emails
- if ( couponType === 'allowedEmails' ) {
- await test.step( 'verify allowed emails coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByPlaceholder( 'No restrictions' )
- ).toHaveValue(
- couponData[ couponType ].allowedEmails[ 0 ]
+test.describe(
+ 'Restricted coupon management',
+ { tag: [ '@services', '@skip-on-default-wpcom' ] },
+ () => {
+ for ( const couponType of Object.keys( couponData ) ) {
+ test( `can create new ${ couponType } coupon`, async ( {
+ page,
+ coupon,
+ product,
+ } ) => {
+ // create basics for the coupon
+ await test.step( 'add new coupon', async () => {
+ await page.goto(
+ 'wp-admin/post-new.php?post_type=shop_coupon'
);
- } );
- }
-
- // verify usage limit
- if ( couponType === 'usageLimitPerCoupon' ) {
- await test.step( 'verify usage limit coupon', async () => {
await page
- .getByRole( 'link', { name: 'Usage limits' } )
+ .getByLabel( 'Coupon code' )
+ .fill( couponData[ couponType ].code );
+ await page
+ .getByPlaceholder( 'Description (optional)' )
+ .fill( couponData[ couponType ].description );
+ await page
+ .getByPlaceholder( '0' )
+ .fill( couponData[ couponType ].amount );
+ await expect(
+ page.getByText( 'Move to Trash' )
+ ).toBeVisible();
+ } );
+
+ // set up the restrictions for each coupon type
+ // set minimum spend
+ if ( couponType === 'minimumSpend' ) {
+ await test.step( 'set minimum spend coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await page
+ .getByPlaceholder( 'No minimum' )
+ .fill( couponData[ couponType ].minSpend );
+ } );
+ }
+ // set maximum spend
+ if ( couponType === 'maximumSpend' ) {
+ await test.step( 'set maximum spend coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await page
+ .getByPlaceholder( 'No maximum' )
+ .fill( couponData[ couponType ].maxSpend );
+ } );
+ }
+ // set individual use
+ if ( couponType === 'individualUse' ) {
+ await test.step( 'set individual use coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await page.getByLabel( 'Individual use only' ).check();
+ } );
+ }
+ // set exclude sale items
+ if ( couponType === 'excludeSaleItems' ) {
+ await test.step( 'set exclude sale items coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await page.getByLabel( 'Exclude sale items' ).check();
+ } );
+ }
+ // set product categories
+ if ( couponType === 'productCategories' ) {
+ await test.step( 'set product categories coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await page
+ .getByPlaceholder( 'Any category' )
+ .pressSequentially( 'Uncategorized' );
+ await page
+ .getByRole( 'option', { name: 'Uncategorized' } )
+ .click();
+ } );
+ }
+ // set exclude product categories
+ if ( couponType === 'excludeProductCategories' ) {
+ await test.step( 'set exclude product categories coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await page
+ .getByPlaceholder( 'No categories' )
+ .pressSequentially( 'Uncategorized' );
+ await page
+ .getByRole( 'option', { name: 'Uncategorized' } )
+ .click();
+ } );
+ }
+
+ // Skip Brands tests while behind a feature flag.
+ const skipBrandsTests = true;
+
+ // set exclude product brands
+ if (
+ couponType === 'excludeProductBrands' &&
+ ! skipBrandsTests
+ ) {
+ await test.step( 'set exclude product brands coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await page
+ .getByPlaceholder( 'No brands' )
+ .pressSequentially( 'WooCommerce Apparels' );
+ await page
+ .getByRole( 'option', {
+ name: 'WooCommerce Apparels',
+ } )
+ .click();
+ } );
+ }
+ // set products
+ if ( couponType === 'products' ) {
+ await test.step( 'set products coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await page
+ .getByPlaceholder( 'Search for a product…' )
+ .first()
+ .pressSequentially( product.name );
+ await page
+ .getByRole( 'option', { name: product.name } )
+ .click();
+ } );
+ }
+ // set exclude products
+ if ( couponType === 'excludeProducts' ) {
+ await test.step( 'set exclude products coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await page
+ .getByPlaceholder( 'Search for a product…' )
+ .last()
+ .pressSequentially( product.name );
+ await page
+ .getByRole( 'option', { name: product.name } )
+ .click();
+ } );
+ }
+ // set allowed emails
+ if ( couponType === 'allowedEmails' ) {
+ await test.step( 'set allowed emails coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await page
+ .getByPlaceholder( 'No restrictions' )
+ .fill(
+ couponData[ couponType ].allowedEmails[ 0 ]
+ );
+ } );
+ }
+ // set usage limit
+ if ( couponType === 'usageLimitPerCoupon' ) {
+ await test.step( 'set usage limit coupon', async () => {
+ await page
+ .getByRole( 'link', { name: 'Usage limits' } )
+ .click();
+ await page
+ .getByLabel( 'Usage limit per coupon' )
+ .fill( couponData[ couponType ].usageLimit );
+ } );
+ }
+ // set usage limit per user
+ if ( couponType === 'usageLimitPerUser' ) {
+ await test.step( 'set usage limit per user coupon', async () => {
+ await page
+ .getByRole( 'link', { name: 'Usage limits' } )
+ .click();
+ await page
+ .getByLabel( 'Usage limit per user' )
+ .fill( couponData[ couponType ].usageLimitPerUser );
+ } );
+ }
+
+ // publish the coupon and retrieve the id
+ await test.step( 'publish the coupon', async () => {
+ await page
+ .getByRole( 'button', { name: 'Publish', exact: true } )
.click();
await expect(
- page.getByLabel( 'Usage limit per coupon' )
- ).toHaveValue( couponData[ couponType ].usageLimit );
+ page.getByText( 'Coupon updated.' )
+ ).toBeVisible();
+ coupon.id = page.url().match( /(?<=post=)\d+/ )[ 0 ];
+ expect( coupon.id ).toBeDefined();
} );
- }
- // verify usage limit per user
- if ( couponType === 'usageLimitPerUser' ) {
- await test.step( 'verify usage limit per user coupon', async () => {
- await page
- .getByRole( 'link', { name: 'Usage limits' } )
- .click();
+ // verify the creation of the coupon and basic details
+ await test.step( 'verify coupon creation', async () => {
+ await page.goto(
+ 'wp-admin/edit.php?post_type=shop_coupon'
+ );
await expect(
- page.getByLabel( 'Usage limit per user' )
- ).toHaveValue( couponData[ couponType ].usageLimitPerUser );
+ page.getByRole( 'cell', {
+ name: couponData[ couponType ].code,
+ } )
+ ).toBeVisible();
+ await expect(
+ page.getByRole( 'cell', {
+ name: couponData[ couponType ].description,
+ } )
+ ).toBeVisible();
+ await expect(
+ page.getByRole( 'cell', {
+ name: couponData[ couponType ].amount,
+ exact: true,
+ } )
+ ).toBeVisible();
+
+ await page
+ .getByRole( 'link', {
+ name: couponData[ couponType ].code,
+ } )
+ .first()
+ .click();
} );
- }
- } );
+
+ // verify the restrictions for each coupon type
+ // verify minimum spend
+ if ( couponType === 'minimumSpend' ) {
+ await test.step( 'verify minimum spend coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await expect(
+ page.getByPlaceholder( 'No minimum' )
+ ).toHaveValue( couponData[ couponType ].minSpend );
+ } );
+ }
+
+ // verify maximum spend
+ if ( couponType === 'maximumSpend' ) {
+ await test.step( 'verify maximum spend coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await expect(
+ page.getByPlaceholder( 'No maximum' )
+ ).toHaveValue( couponData[ couponType ].maxSpend );
+ } );
+ }
+
+ // verify individual use
+ if ( couponType === 'individualUse' ) {
+ await test.step( 'verify individual use coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await expect(
+ page.getByLabel( 'Individual use only' )
+ ).toBeChecked();
+ } );
+ }
+
+ // verify exclude sale items
+ if ( couponType === 'excludeSaleItems' ) {
+ await test.step( 'verify exclude sale items coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await expect(
+ page.getByLabel( 'Exclude sale items' )
+ ).toBeChecked();
+ } );
+ }
+
+ // verify product categories
+ if ( couponType === 'productCategories' ) {
+ await test.step( 'verify product categories coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await expect(
+ page.getByRole( 'listitem', {
+ name: 'Uncategorized',
+ } )
+ ).toBeVisible();
+ } );
+ }
+
+ // verify exclude product categories
+ if ( couponType === 'excludeProductCategories' ) {
+ await test.step( 'verify exclude product categories coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await expect(
+ page.getByRole( 'listitem', {
+ name: 'Uncategorized',
+ } )
+ ).toBeVisible();
+ } );
+ }
+
+ // verify products
+ if ( couponType === 'products' ) {
+ await test.step( 'verify products coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await expect(
+ page.getByRole( 'listitem', { name: product.name } )
+ ).toBeVisible();
+ } );
+ }
+
+ // verify exclude products
+ if ( couponType === 'excludeProducts' ) {
+ await test.step( 'verify exclude products coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await expect(
+ page.getByRole( 'listitem', { name: product.name } )
+ ).toBeVisible();
+ } );
+ }
+
+ // verify allowed emails
+ if ( couponType === 'allowedEmails' ) {
+ await test.step( 'verify allowed emails coupon', async () => {
+ await page
+ .getByRole( 'link', {
+ name: 'Usage restriction',
+ } )
+ .click();
+ await expect(
+ page.getByPlaceholder( 'No restrictions' )
+ ).toHaveValue(
+ couponData[ couponType ].allowedEmails[ 0 ]
+ );
+ } );
+ }
+
+ // verify usage limit
+ if ( couponType === 'usageLimitPerCoupon' ) {
+ await test.step( 'verify usage limit coupon', async () => {
+ await page
+ .getByRole( 'link', { name: 'Usage limits' } )
+ .click();
+ await expect(
+ page.getByLabel( 'Usage limit per coupon' )
+ ).toHaveValue( couponData[ couponType ].usageLimit );
+ } );
+ }
+
+ // verify usage limit per user
+ if ( couponType === 'usageLimitPerUser' ) {
+ await test.step( 'verify usage limit per user coupon', async () => {
+ await page
+ .getByRole( 'link', { name: 'Usage limits' } )
+ .click();
+ await expect(
+ page.getByLabel( 'Usage limit per user' )
+ ).toHaveValue(
+ couponData[ couponType ].usageLimitPerUser
+ );
+ } );
+ }
+ } );
+ }
}
-} );
+);
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-shipping-zones.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-shipping-zones.spec.js
index 93bc2a1d605..37dbf85ec6d 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-shipping-zones.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-shipping-zones.spec.js
@@ -60,8 +60,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
// this shipping zone already exists, don't create it
} else {
await page.goto(
- 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new',
- { waitUntil: 'networkidle' }
+ 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new'
);
await page
.getByPlaceholder( 'Zone name' )
@@ -92,10 +91,8 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
.getByRole( 'button', { name: 'Continue' } )
.last()
.click();
- await page.waitForLoadState( 'networkidle' );
await page.locator( '#btn-ok' ).click();
- await page.waitForLoadState( 'networkidle' );
await expect(
page
@@ -132,8 +129,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
// this shipping zone already exists, don't create it
} else {
await page.goto(
- 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new',
- { waitUntil: 'networkidle' }
+ 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new'
);
await page
.getByPlaceholder( 'Zone name' )
@@ -159,10 +155,8 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
.getByRole( 'button', { name: 'Continue' } )
.last()
.click();
- await page.waitForLoadState( 'networkidle' );
await page.locator( '#btn-ok' ).click();
- await page.waitForLoadState( 'networkidle' );
await expect(
page
@@ -196,8 +190,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
// this shipping zone already exists, don't create it
} else {
await page.goto(
- 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new',
- { waitUntil: 'networkidle' }
+ 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new'
);
await page
.getByPlaceholder( 'Zone name' )
@@ -209,7 +202,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
input.click();
input.fill( 'Canada' );
- await page.getByText( 'Canada' ).last().click();
+ await page.getByLabel( 'Canada', { exact: true } ).click();
// Close dropdown
await page.getByPlaceholder( 'Zone name' ).click();
@@ -222,10 +215,8 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
.getByRole( 'button', { name: 'Continue' } )
.last()
.click();
- await page.waitForLoadState( 'networkidle' );
await page.locator( '#btn-ok' ).click();
- await page.waitForLoadState( 'networkidle' );
await expect(
page
@@ -240,7 +231,6 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
.click();
await page.getByLabel( 'Cost', { exact: true } ).fill( '10' );
await page.getByRole( 'button', { name: 'Save' } ).last().click();
- await page.waitForLoadState( 'networkidle' );
await page.goto(
'wp-admin/admin.php?page=wc-settings&tab=shipping'
@@ -342,8 +332,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
// this shipping zone already exists, don't create it
} else {
await page.goto(
- 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new',
- { waitUntil: 'networkidle' }
+ 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new'
);
await page.locator( '#zone_name' ).fill( shippingZoneNameFlatRate );
@@ -353,7 +342,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
input.click();
input.type( 'Canada' );
- await page.getByText( 'Canada' ).last().click();
+ await page.getByLabel( 'Canada', { exact: true } ).click();
// Close dropdown
await page.keyboard.press( 'Escape' );
@@ -366,10 +355,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
.last()
.click();
- await page.waitForLoadState( 'networkidle' );
-
await page.locator( '#btn-ok' ).click();
- await page.waitForLoadState( 'networkidle' );
await expect(
page
@@ -384,13 +370,17 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
.click();
await page.locator( '#woocommerce_flat_rate_cost' ).fill( '10' );
await page.locator( '#btn-ok' ).click();
- await page.waitForLoadState( 'networkidle' );
- await page.locator( 'text=Delete' ).waitFor();
+ await expect(
+ page.getByRole( 'cell', { name: 'Edit | Delete', exact: true } )
+ ).toBeVisible();
page.on( 'dialog', ( dialog ) => dialog.accept() );
- await page.locator( 'text=Delete' ).click();
+ await page
+ .getByRole( 'cell', { name: 'Edit | Delete', exact: true } )
+ .locator( 'text=Delete' )
+ .click();
await expect(
page.locator( '.wc-shipping-zone-method-blank-state' )
@@ -482,7 +472,6 @@ test.describe( 'Verifies shipping options from customer perspective', () => {
await context.clearCookies();
await page.goto( `/shop/?add-to-cart=${ productId }` );
- await page.waitForLoadState( 'networkidle' );
} );
test.afterAll( async ( { baseURL } ) => {
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-blocks.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-blocks.spec.js
index 7ade737367f..05b21525efa 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-blocks.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-blocks.spec.js
@@ -52,7 +52,14 @@ const test = baseTest.extend( {
test.describe(
'Add WooCommerce Blocks Into Page',
- { tag: [ '@gutenberg', '@services', '@skip-on-default-pressable' ] },
+ {
+ tag: [
+ '@gutenberg',
+ '@services',
+ '@skip-on-default-pressable',
+ '@skip-on-default-wpcom',
+ ],
+ },
() => {
test.beforeAll( async ( { api } ) => {
// add product attribute
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-patterns.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-patterns.spec.js
index ee6d56b0d9c..671788097b4 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-patterns.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-patterns.spec.js
@@ -28,7 +28,14 @@ const test = baseTest.extend( {
test.describe(
'Add WooCommerce Patterns Into Page',
- { tag: [ '@gutenberg', '@services', '@skip-on-default-pressable' ] },
+ {
+ tag: [
+ '@gutenberg',
+ '@services',
+ '@skip-on-default-pressable',
+ '@skip-on-default-wpcom',
+ ],
+ },
() => {
test( 'can insert WooCommerce patterns into page', async ( {
page,
@@ -86,7 +93,9 @@ test.describe(
// check some elements from added patterns
for ( let i = 1; i < wooPatterns.length; i++ ) {
await expect(
- page.getByText( `${ wooPatterns[ i ].button }` )
+ page.getByRole( 'link', {
+ name: `${ wooPatterns[ i ].button }`,
+ } )
).toBeVisible();
}
} );
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-list.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-list.spec.js
index 1b8cb58846a..3a4501f9871 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-list.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-list.spec.js
@@ -85,7 +85,7 @@ test.describe( 'Merchant > Customer List', { tag: '@services' }, () => {
test(
'Merchant can view a list of all customers, filter and download',
- { tag: '@skip-on-default-pressable' },
+ { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] },
async ( { page, customers } ) => {
await test.step( 'Go to the customers reports page', async () => {
const responsePromise = page.waitForResponse(
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-payment-page.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-payment-page.spec.js
index 652d819edd2..adc673add2b 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-payment-page.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-payment-page.spec.js
@@ -110,6 +110,17 @@ test.describe(
await test.step( 'Select payment method and pay for the order', async () => {
// explicitly select the payment method
await page.getByText( 'Direct bank transfer' ).click();
+
+ // Handle notice if present
+ await page.addLocatorHandler(
+ page.getByRole( 'link', { name: 'Dismiss' } ),
+ async () => {
+ await page
+ .getByRole( 'link', { name: 'Dismiss' } )
+ .click();
+ }
+ );
+
// pay for the order
await page
.getByRole( 'button', { name: 'Pay for order' } )
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/launch-your-store.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/launch-your-store.spec.js
index cfef43102a9..4d40109a591 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/launch-your-store.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/launch-your-store.spec.js
@@ -4,7 +4,7 @@ const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default;
test.describe(
'Launch Your Store - logged in',
- { tag: [ '@gutenberg', '@services' ] },
+ { tag: [ '@gutenberg', '@services', '@skip-on-default-wpcom' ] },
() => {
test.use( { storageState: process.env.ADMINSTATE } );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/product-reviews.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/product-reviews.php
index df53aeece57..a68ad775f87 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/product-reviews.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/product-reviews.php
@@ -1,4 +1,7 @@
assertEquals( 200, $response->get_status() );
$this->assertEquals( 10, count( $product_reviews ) );
- $this->assertContains(
- array(
- 'id' => $review_id,
- 'date_created' => $product_reviews[0]['date_created'],
- 'date_created_gmt' => $product_reviews[0]['date_created_gmt'],
- 'product_id' => $product->get_id(),
- 'product_name' => $product->get_name(),
- 'product_permalink' => $product->get_permalink(),
- 'status' => 'approved',
- 'reviewer' => 'admin',
- 'reviewer_email' => 'woo@woo.local',
- 'review' => "Review content here
\n",
- 'rating' => 0,
- 'verified' => false,
- 'reviewer_avatar_urls' => $product_reviews[0]['reviewer_avatar_urls'],
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v3/products/reviews/' . $review_id ),
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => $review_id,
+ 'date_created' => $product_reviews[0]['date_created'],
+ 'date_created_gmt' => $product_reviews[0]['date_created_gmt'],
+ 'product_id' => $product->get_id(),
+ 'product_name' => $product->get_name(),
+ 'product_permalink' => $product->get_permalink(),
+ 'status' => 'approved',
+ 'reviewer' => 'admin',
+ 'reviewer_email' => 'woo@woo.local',
+ 'review' => "Review content here
\n",
+ 'rating' => 0,
+ 'verified' => false,
+ 'reviewer_avatar_urls' => $product_reviews[0]['reviewer_avatar_urls'],
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/products/reviews/' . $review_id ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v3/products/reviews' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/products/reviews' ),
+ ),
),
- ),
- 'up' => array(
- array(
- 'href' => rest_url( '/wc/v3/products/' . $product->get_id() ),
+ 'up' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/products/' . $product->get_id() ),
+ ),
),
),
),
- ),
- $product_reviews
+ $product_reviews[0]
+ )
);
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php
index 32a96fb95dc..f185a811097 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php
@@ -6,6 +6,7 @@
* @since 3.0.0
*/
+use Automattic\WooCommerce\Utilities\ArrayUtil;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
/**
@@ -482,29 +483,39 @@ class Settings_V2 extends WC_REST_Unit_Test_Case {
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/products' ) );
$data = $response->get_data();
$this->assertTrue( is_array( $data ) );
- $this->assertContains(
- array(
- 'id' => 'woocommerce_downloads_require_login',
- 'label' => 'Access restriction',
- 'description' => 'Downloads require login',
- 'type' => 'checkbox',
- 'default' => 'no',
- 'tip' => 'This setting does not apply to guest purchases.',
- 'value' => 'no',
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v2/settings/products/woocommerce_downloads_require_login' ),
+ $data_download_required_login = null;
+ foreach ( $data as $setting ) {
+ if ( 'woocommerce_downloads_require_login' === $setting['id'] ) {
+ $data_download_required_login = $setting;
+ break;
+ }
+ }
+ $this->assertNotEmpty( $data_download_required_login );
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => 'woocommerce_downloads_require_login',
+ 'label' => 'Access restriction',
+ 'description' => 'Downloads require login',
+ 'type' => 'checkbox',
+ 'default' => 'no',
+ 'tip' => 'This setting does not apply to guest purchases.',
+ 'value' => 'no',
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/settings/products/woocommerce_downloads_require_login' ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v2/settings/products' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/settings/products' ),
+ ),
),
),
),
- ),
- $data
+ $data_download_required_login
+ )
);
// test get single.
@@ -540,29 +551,41 @@ class Settings_V2 extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
- $this->assertContains(
- array(
- 'id' => 'recipient',
- 'label' => 'Recipient(s)',
- 'description' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org
.',
- 'type' => 'text',
- 'default' => '',
- 'tip' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org
.',
- 'value' => '',
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v2/settings/email_new_order/recipient' ),
+ $recipient_setting = null;
+ foreach ( $settings as $setting ) {
+ if ( 'recipient' === $setting['id'] ) {
+ $recipient_setting = $setting;
+ break;
+ }
+ }
+
+ $this->assertNotEmpty( $recipient_setting );
+
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => 'recipient',
+ 'label' => 'Recipient(s)',
+ 'description' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org
.',
+ 'type' => 'text',
+ 'default' => '',
+ 'tip' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org
.',
+ 'value' => '',
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/settings/email_new_order/recipient' ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v2/settings/email_new_order' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/settings/email_new_order' ),
+ ),
),
),
),
- ),
- $settings
+ $recipient_setting
+ )
);
// test get single.
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-methods.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-methods.php
index 3f13ecb2ee1..d27efd5b67b 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-methods.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-methods.php
@@ -1,4 +1,7 @@
get_data();
$this->assertEquals( 200, $response->get_status() );
- $this->assertContains(
- array(
- 'id' => 'free_shipping',
- 'title' => 'Free shipping',
- 'description' => 'Free shipping is a special method which can be triggered with coupons and minimum spends.',
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping_methods/free_shipping' ),
+
+ $free_shipping_method = null;
+ foreach ( $methods as $method ) {
+ if ( 'free_shipping' === $method['id'] ) {
+ $free_shipping_method = $method;
+ break;
+ }
+ }
+ $this->assertNotEmpty( $free_shipping_method );
+
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => 'free_shipping',
+ 'title' => 'Free shipping',
+ 'description' => 'Free shipping is a special method which can be triggered with coupons and minimum spends.',
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping_methods/free_shipping' ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping_methods' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping_methods' ),
+ ),
),
),
),
- ),
- $methods
+ $free_shipping_method
+ )
);
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-zones.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-zones.php
index bc020791d02..1e15bb91e02 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-zones.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-zones.php
@@ -1,5 +1,7 @@
assertEquals( 200, $response->get_status() );
$this->assertEquals( count( $data ), 1 );
- $this->assertContains(
- array(
- 'id' => $data[0]['id'],
- 'name' => 'Locations not covered by your other zones',
- 'order' => 0,
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[0]['id'] ),
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => $data[0]['id'],
+ 'name' => 'Locations not covered by your other zones',
+ 'order' => 0,
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[0]['id'] ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones' ),
+ ),
),
- ),
- 'describedby' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[0]['id'] . '/locations' ),
+ 'describedby' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[0]['id'] . '/locations' ),
+ ),
),
),
),
- ),
- $data
+ $data[0]
+ )
);
// Create a zone and make sure it's in the response
@@ -108,30 +112,32 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( count( $data ), 2 );
- $this->assertContains(
- array(
- 'id' => $data[1]['id'],
- 'name' => 'Zone 1',
- 'order' => 0,
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[1]['id'] ),
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => $data[1]['id'],
+ 'name' => 'Zone 1',
+ 'order' => 0,
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[1]['id'] ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones' ),
+ ),
),
- ),
- 'describedby' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[1]['id'] . '/locations' ),
+ 'describedby' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[1]['id'] . '/locations' ),
+ ),
),
),
),
- ),
- $data
+ $data[1]
+ )
);
}
@@ -195,30 +201,32 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
$data = $response->get_data();
$this->assertEquals( 201, $response->get_status() );
- $this->assertEquals(
- array(
- 'id' => $data['id'],
- 'name' => 'Test Zone',
- 'order' => 1,
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones/' . $data['id'] ),
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => $data['id'],
+ 'name' => 'Test Zone',
+ 'order' => 1,
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones/' . $data['id'] ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones' ),
+ ),
),
- ),
- 'describedby' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones/' . $data['id'] . '/locations' ),
+ 'describedby' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones/' . $data['id'] . '/locations' ),
+ ),
),
),
),
- ),
- $data
+ $data
+ )
);
}
@@ -260,30 +268,32 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
- $this->assertEquals(
- array(
- 'id' => $zone->get_id(),
- 'name' => 'Zone Test',
- 'order' => 2,
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ),
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => $zone->get_id(),
+ 'name' => 'Zone Test',
+ 'order' => 2,
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones' ),
+ ),
),
- ),
- 'describedby' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ),
+ 'describedby' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ),
+ ),
),
),
),
- ),
- $data
+ $data
+ )
);
}
@@ -359,30 +369,32 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
- $this->assertEquals(
- array(
- 'id' => $zone->get_id(),
- 'name' => 'Test Zone',
- 'order' => 0,
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ),
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => $zone->get_id(),
+ 'name' => 'Test Zone',
+ 'order' => 0,
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones' ),
+ ),
),
- ),
- 'describedby' => array(
- array(
- 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ),
+ 'describedby' => array(
+ array(
+ 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ),
+ ),
),
),
),
- ),
- $data
+ $data
+ )
);
}
@@ -624,13 +636,13 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( count( $data ), 1 );
- $this->assertContains( $expected, $data );
+ $this->assertEmpty( ArrayUtil::deep_assoc_array_diff( $expected, $data[0] ) );
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ) );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
- $this->assertEquals( $expected, $data );
+ $this->assertEmpty( ArrayUtil::deep_assoc_array_diff( $expected, $data ) );
}
/**
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/product-reviews.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/product-reviews.php
index 11aa94c16b7..b655ffb538a 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/product-reviews.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/product-reviews.php
@@ -1,4 +1,7 @@
assertEquals( 200, $response->get_status() );
$this->assertEquals( 10, count( $product_reviews ) );
- $this->assertContains(
- array(
- 'id' => $review_id,
- 'date_created' => $product_reviews[0]['date_created'],
- 'date_created_gmt' => $product_reviews[0]['date_created_gmt'],
- 'product_id' => $product->get_id(),
- 'product_name' => $product->get_name(),
- 'product_permalink' => $product->get_permalink(),
- 'status' => 'approved',
- 'reviewer' => 'admin',
- 'reviewer_email' => 'woo@woo.local',
- 'review' => "Review content here
\n",
- 'rating' => 0,
- 'verified' => false,
- 'reviewer_avatar_urls' => $product_reviews[0]['reviewer_avatar_urls'],
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v3/products/reviews/' . $review_id ),
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => $review_id,
+ 'date_created' => $product_reviews[0]['date_created'],
+ 'date_created_gmt' => $product_reviews[0]['date_created_gmt'],
+ 'product_id' => $product->get_id(),
+ 'product_name' => $product->get_name(),
+ 'product_permalink' => $product->get_permalink(),
+ 'status' => 'approved',
+ 'reviewer' => 'admin',
+ 'reviewer_email' => 'woo@woo.local',
+ 'review' => "Review content here
\n",
+ 'rating' => 0,
+ 'verified' => false,
+ 'reviewer_avatar_urls' => $product_reviews[0]['reviewer_avatar_urls'],
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/products/reviews/' . $review_id ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v3/products/reviews' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/products/reviews' ),
+ ),
),
- ),
- 'up' => array(
- array(
- 'href' => rest_url( '/wc/v3/products/' . $product->get_id() ),
+ 'up' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/products/' . $product->get_id() ),
+ ),
),
),
),
- ),
- $product_reviews
+ $product_reviews[0]
+ )
);
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php
index d8890569eff..d10062ec2ee 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php
@@ -6,6 +6,7 @@
* @since 3.5.0
*/
+use Automattic\WooCommerce\Utilities\ArrayUtil;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
/**
@@ -481,29 +482,42 @@ class Settings extends WC_REST_Unit_Test_Case {
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/products' ) );
$data = $response->get_data();
$this->assertTrue( is_array( $data ) );
- $this->assertContains(
- array(
- 'id' => 'woocommerce_downloads_require_login',
- 'label' => 'Access restriction',
- 'description' => 'Downloads require login',
- 'type' => 'checkbox',
- 'default' => 'no',
- 'tip' => 'This setting does not apply to guest purchases.',
- 'value' => 'no',
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v3/settings/products/woocommerce_downloads_require_login' ),
+
+ $setting_downloads_required = null;
+ foreach ( $data as $setting ) {
+ if ( 'woocommerce_downloads_require_login' === $setting['id'] ) {
+ $setting_downloads_required = $setting;
+ break;
+ }
+ }
+
+ $this->assertNotEmpty( $setting_downloads_required );
+
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => 'woocommerce_downloads_require_login',
+ 'label' => 'Access restriction',
+ 'description' => 'Downloads require login',
+ 'type' => 'checkbox',
+ 'default' => 'no',
+ 'tip' => 'This setting does not apply to guest purchases.',
+ 'value' => 'no',
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/settings/products/woocommerce_downloads_require_login' ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v3/settings/products' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/settings/products' ),
+ ),
),
),
),
- ),
- $data
+ $setting_downloads_required
+ )
);
// test get single.
@@ -539,29 +553,41 @@ class Settings extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
- $this->assertContains(
- array(
- 'id' => 'recipient',
- 'label' => 'Recipient(s)',
- 'description' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org
.',
- 'type' => 'text',
- 'default' => '',
- 'tip' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org
.',
- 'value' => '',
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v3/settings/email_new_order/recipient' ),
+ $recipient_setting = null;
+ foreach ( $settings as $setting ) {
+ if ( 'recipient' === $setting['id'] ) {
+ $recipient_setting = $setting;
+ break;
+ }
+ }
+
+ $this->assertNotEmpty( $recipient_setting );
+
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => 'recipient',
+ 'label' => 'Recipient(s)',
+ 'description' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org
.',
+ 'type' => 'text',
+ 'default' => '',
+ 'tip' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org
.',
+ 'value' => '',
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/settings/email_new_order/recipient' ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v3/settings/email_new_order' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/settings/email_new_order' ),
+ ),
),
),
),
- ),
- $settings
+ $recipient_setting
+ )
);
// test get single.
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-methods.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-methods.php
index 31dc36c1b14..05d38ad0517 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-methods.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-methods.php
@@ -1,4 +1,7 @@
get_data();
$this->assertEquals( 200, $response->get_status() );
- $this->assertContains(
- array(
- 'id' => 'free_shipping',
- 'title' => 'Free shipping',
- 'description' => 'Free shipping is a special method which can be triggered with coupons and minimum spends.',
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping_methods/free_shipping' ),
+
+ $free_shipping = null;
+ foreach ( $methods as $method ) {
+ if ( 'free_shipping' === $method['id'] ) {
+ $free_shipping = $method;
+ break;
+ }
+ }
+ $this->assertNotEmpty( $free_shipping );
+
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => 'free_shipping',
+ 'title' => 'Free shipping',
+ 'description' => 'Free shipping is a special method which can be triggered with coupons and minimum spends.',
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping_methods/free_shipping' ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping_methods' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping_methods' ),
+ ),
),
),
),
- ),
- $methods
+ $free_shipping
+ )
);
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-zones.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-zones.php
index 1dd58034653..3c49902d989 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-zones.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-zones.php
@@ -1,5 +1,7 @@
assertEquals( 200, $response->get_status() );
$this->assertEquals( count( $data ), 1 );
- $this->assertContains(
- array(
- 'id' => $data[0]['id'],
- 'name' => 'Locations not covered by your other zones',
- 'order' => 0,
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[0]['id'] ),
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => $data[0]['id'],
+ 'name' => 'Locations not covered by your other zones',
+ 'order' => 0,
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[0]['id'] ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones' ),
+ ),
),
- ),
- 'describedby' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[0]['id'] . '/locations' ),
+ 'describedby' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[0]['id'] . '/locations' ),
+ ),
),
),
),
- ),
- $data
+ $data[0]
+ )
);
// Create a zone and make sure it's in the response
@@ -111,30 +115,32 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( count( $data ), 2 );
- $this->assertContains(
- array(
- 'id' => $data[1]['id'],
- 'name' => 'Zone 1',
- 'order' => 0,
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[1]['id'] ),
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => $data[1]['id'],
+ 'name' => 'Zone 1',
+ 'order' => 0,
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[1]['id'] ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones' ),
+ ),
),
- ),
- 'describedby' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[1]['id'] . '/locations' ),
+ 'describedby' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[1]['id'] . '/locations' ),
+ ),
),
),
),
- ),
- $data
+ $data[1]
+ )
);
}
@@ -202,30 +208,32 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
$data = $response->get_data();
$this->assertEquals( 201, $response->get_status() );
- $this->assertEquals(
- array(
- 'id' => $data['id'],
- 'name' => 'Test Zone',
- 'order' => 1,
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones/' . $data['id'] ),
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => $data['id'],
+ 'name' => 'Test Zone',
+ 'order' => 1,
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones/' . $data['id'] ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones' ),
+ ),
),
- ),
- 'describedby' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones/' . $data['id'] . '/locations' ),
+ 'describedby' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones/' . $data['id'] . '/locations' ),
+ ),
),
),
),
- ),
- $data
+ $data
+ )
);
}
@@ -269,30 +277,32 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
- $this->assertEquals(
- array(
- 'id' => $zone->get_id(),
- 'name' => 'Zone Test',
- 'order' => 2,
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ),
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => $zone->get_id(),
+ 'name' => 'Zone Test',
+ 'order' => 2,
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones' ),
+ ),
),
- ),
- 'describedby' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ),
+ 'describedby' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ),
+ ),
),
),
),
- ),
- $data
+ $data
+ )
);
}
@@ -373,30 +383,32 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
- $this->assertEquals(
- array(
- 'id' => $zone->get_id(),
- 'name' => 'Test Zone',
- 'order' => 0,
- '_links' => array(
- 'self' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ),
+ $this->assertEmpty(
+ ArrayUtil::deep_assoc_array_diff(
+ array(
+ 'id' => $zone->get_id(),
+ 'name' => 'Test Zone',
+ 'order' => 0,
+ '_links' => array(
+ 'self' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ),
+ ),
),
- ),
- 'collection' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones' ),
+ 'collection' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones' ),
+ ),
),
- ),
- 'describedby' => array(
- array(
- 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ),
+ 'describedby' => array(
+ array(
+ 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ),
+ ),
),
),
),
- ),
- $data
+ $data
+ )
);
}
@@ -644,13 +656,12 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( count( $data ), 1 );
- $this->assertContains( $expected, $data );
-
+ $this->assertEmpty( ArrayUtil::deep_assoc_array_diff( $expected, $data[0] ) );
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ) );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
- $this->assertEquals( $expected, $data );
+ $this->assertEmpty( ArrayUtil::deep_assoc_array_diff( $expected, $data ) );
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php b/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php
index 90f312be98b..3e8bbb7bed8 100644
--- a/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php
+++ b/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php
@@ -356,104 +356,4 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
$this->assertIsIntAndEquals( $site_wide_low_stock_amount, wc_get_low_stock_amount( $var1 ) );
}
-
- /**
- * @testdox Test that the `woocommerce_low_stock` action fires when a product stock hits the low stock threshold.
- */
- public function test_wc_update_product_stock_low_stock_action() {
- $product = WC_Helper_Product::create_simple_product();
- $product->set_manage_stock( true );
- $product->save();
-
- $low_stock_amount = wc_get_low_stock_amount( $product );
- $initial_stock = $low_stock_amount + 2;
-
- wc_update_product_stock( $product->get_id(), $initial_stock );
-
- $action_fired = false;
- $callback = function () use ( &$action_fired ) {
- $action_fired = true;
- };
- add_action( 'woocommerce_low_stock', $callback );
-
- // Test with `wc_update_product_stock`.
- wc_update_product_stock( $product->get_id(), 1, 'decrease' );
- $this->assertFalse( $action_fired );
- wc_update_product_stock( $product->get_id(), 1, 'decrease' );
- $this->assertTrue( $action_fired );
-
- $action_fired = false;
-
- // Test with the data store.
- $product->set_stock_quantity( $initial_stock );
- $product->save();
- $this->assertFalse( $action_fired );
- $product->set_stock_quantity( $low_stock_amount );
- $product->save();
- $this->assertTrue( $action_fired );
-
- remove_action( 'woocommerce_low_stock', $callback );
- }
-
- /**
- * @testdox Test that the `woocommerce_no_stock` action fires when a product stock hits the no stock threshold.
- */
- public function test_wc_update_product_stock_no_stock_action() {
- $product = WC_Helper_Product::create_simple_product();
- $product->set_manage_stock( true );
- $product->save();
-
- $no_stock_amount = get_option( 'woocommerce_notify_no_stock_amount', 0 );
- $initial_stock = $no_stock_amount + 2;
-
- wc_update_product_stock( $product->get_id(), $initial_stock );
-
- $action_fired = false;
- $callback = function () use ( &$action_fired ) {
- $action_fired = true;
- };
- add_action( 'woocommerce_no_stock', $callback );
-
- // Test with `wc_update_product_stock`.
- wc_update_product_stock( $product->get_id(), 1, 'decrease' );
- $this->assertFalse( $action_fired );
- wc_update_product_stock( $product->get_id(), 1, 'decrease' );
- $this->assertTrue( $action_fired );
-
- $action_fired = false;
-
- // Test with the data store.
- $product->set_stock_quantity( $initial_stock );
- $product->save();
- $this->assertFalse( $action_fired );
- $product->set_stock_quantity( $no_stock_amount );
- $product->save();
- $this->assertTrue( $action_fired );
-
- remove_action( 'woocommerce_no_stock', $callback );
- }
-
- /**
- * @testdox The wc_trigger_stock_change_actions function should only trigger actions if the product is set
- * to manage stock.
- */
- public function test_wc_trigger_stock_change_actions_bails_early_for_unmanaged_stock() {
- $action_fired = false;
- $callback = function () use ( &$action_fired ) {
- $action_fired = true;
- };
- add_action( 'woocommerce_no_stock', $callback );
-
- $product = WC_Helper_Product::create_simple_product();
-
- $this->assertFalse( $action_fired );
-
- $product->set_manage_stock( true );
- $product->set_stock_quantity( 0 );
- $product->save();
-
- $this->assertTrue( $action_fired );
-
- remove_action( 'woocommerce_no_stock', $callback );
- }
}