Merge branch 'trunk' into try/add-settings-refresh-next

This commit is contained in:
paul sealock 2024-10-01 16:22:59 -06:00
commit 2a6750ca31
85 changed files with 1717 additions and 1364 deletions

View File

@ -132,7 +132,7 @@ jobs:
install: '${{ matrix.projectName }}...' install: '${{ matrix.projectName }}...'
build: ${{ ( github.ref_type == 'tag' && 'false' ) || matrix.projectName }} build: ${{ ( github.ref_type == 'tag' && 'false' ) || matrix.projectName }}
build-type: ${{ ( matrix.testType == 'unit:php' && 'backend' ) || 'full' }} build-type: ${{ ( matrix.testType == 'unit:php' && 'backend' ) || 'full' }}
pull-playwright-cache: ${{ matrix.testEnv.shouldCreate && matrix.testType == 'e2e' }} pull-playwright-cache: ${{ matrix.testEnv.shouldCreate && ( matrix.testType == 'e2e' || matrix.testType == 'performance' ) }}
pull-package-deps: '${{ matrix.projectName }}' pull-package-deps: '${{ matrix.projectName }}'
- name: 'Update wp-env config' - name: 'Update wp-env config'

View File

@ -29,6 +29,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
cancel-in-progress: true cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs: jobs:
build: build:
name: Check Asset Sizes name: Check Asset Sizes
@ -42,6 +45,8 @@ jobs:
uses: ./.github/actions/setup-woocommerce-monorepo uses: ./.github/actions/setup-woocommerce-monorepo
with: with:
php-version: false php-version: false
install: '@woocommerce/plugin-woocommerce...'
build: '@woocommerce/plugin-woocommerce'
pull-package-deps: '@woocommerce/plugin-woocommerce' pull-package-deps: '@woocommerce/plugin-woocommerce'
- uses: preactjs/compressed-size-action@f780fd104362cfce9e118f9198df2ee37d12946c - uses: preactjs/compressed-size-action@f780fd104362cfce9e118f9198df2ee37d12946c
@ -49,9 +54,9 @@ jobs:
BROWSERSLIST_IGNORE_OLD_DATA: true BROWSERSLIST_IGNORE_OLD_DATA: true
with: with:
repo-token: '${{ secrets.GITHUB_TOKEN }}' repo-token: '${{ secrets.GITHUB_TOKEN }}'
pattern: './{packages/js/!(*e2e*|*internal*|*test*|*plugin*|*create*),plugins/woocommerce-blocks}/{build,build-style}/**/*.{js,css}' pattern: './{packages/js/!(*e2e*|*internal*|*test*|*plugin*|*create*),plugins/woocommerce-blocks,plugins/woocommerce-admin,plugins/woocommerce/client/legacy}/{build,build-style}/**/*.{js,css}'
install-script: 'pnpm install --filter="@woocommerce/plugin-woocommerce..." --frozen-lockfile --config.dedupe-peer-dependents=false --ignore-scripts' install-script: 'pnpm install --filter="@woocommerce/plugin-woocommerce..." --frozen-lockfile --config.dedupe-peer-dependents=false --ignore-scripts'
build-script: '--filter="@woocommerce/plugin-woocommerce" build' build-script: '--filter="@woocommerce/plugin-woocommerce" build'
clean-script: '--if-present buildclean' clean-script: '--if-present clean:build'
minimum-change-threshold: 100 minimum-change-threshold: 100
omit-unchanged: true omit-unchanged: true

View File

@ -16,6 +16,9 @@ concurrency:
group: build-${{ github.event_name == 'push' && github.run_id || 'pr' }}-${{ github.ref }} group: build-${{ github.event_name == 'push' && github.run_id || 'pr' }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
FORCE_COLOR: 1
permissions: {} permissions: {}
jobs: jobs:

View File

@ -2,31 +2,79 @@
set -eo pipefail set -eo pipefail
function title() { # The commented variables are for troubleshooting locally. The commented commands below are also for local troubleshooting.
echo -e "\n\033[1m$1\033[0m" # GITHUB_EVENT_NAME='pull_request'
} # GITHUB_SHA=$(git rev-parse HEAD)
# ARTIFACTS_PATH="$(realpath $(dirname -- ${BASH_SOURCE[0]})/../../../tools/compare-perf)/artifacts"
if [[ -z "$GITHUB_EVENT_NAME" ]]; then if [[ -z "$GITHUB_EVENT_NAME" ]]; then
echo "::error::GITHUB_EVENT_NAME must be set" echo "::error::GITHUB_EVENT_NAME must be set"
exit 1 exit 1
fi fi
title "Installing NVM" function title() {
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash > /dev/null echo -e "\n\033[1m$1\033[0m"
export NVM_DIR="$HOME/.nvm" }
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
echo "Installed version: $(nvm -v)"
title "Installing dependencies" if [ "$GITHUB_EVENT_NAME" == "push" ] || [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then
pnpm install --frozen-lockfile --filter="compare-perf" > /dev/null mkdir -p $ARTIFACTS_PATH && export WP_ARTIFACTS_PATH=$ARTIFACTS_PATH
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then # It should be 3d7d7f02017383937f1a4158d433d0e5d44b3dc9, but we pick 55f855a2e6d769b5ae44305b2772eb30d3e721df
title "Comparing performance with trunk" # where compare-perf reporting mode was introduced for processing the provided reports.
pnpm --filter="compare-perf" run compare perf $GITHUB_SHA trunk --tests-branch $GITHUB_SHA BASE_SHA=55f855a2e6d769b5ae44305b2772eb30d3e721df
HEAD_BRANCH=$(git rev-parse --abbrev-ref HEAD)
elif [[ "$GITHUB_EVENT_NAME" == "push" ]]; then
title "Comparing performance with base branch"
WP_VERSION=$(awk -F ': ' '/^Tested up to/{print $2}' readme.txt) WP_VERSION=$(awk -F ': ' '/^Tested up to/{print $2}' readme.txt)
title "Comparing performance between: $BASE_SHA@trunk (base) and $GITHUB_SHA@$HEAD_BRANCH (head) on WordPress v$WP_VERSION"
title "##[group]Setting up necessary tooling"
pnpm --filter="@woocommerce/plugin-woocommerce" test:e2e:install > /dev/null &
pnpm install --filter='compare-perf...' --frozen-lockfile --config.dedupe-peer-dependents=false --ignore-scripts
echo '##[endgroup]'
if test -n "$(find $ARTIFACTS_PATH -maxdepth 1 -name "*_${GITHUB_SHA}_*" -print -quit)"; then
title "Skipping benchmarking head as benchmarking results already available under $ARTIFACTS_PATH"
else
# title "##[group]Building head"
# git -c core.hooksPath=/dev/null checkout --quiet $HEAD_BRANCH > /dev/null && echo 'On' $(git rev-parse HEAD)
# pnpm run --if-present clean:build
# pnpm install --filter='@woocommerce/plugin-woocommerce...' --frozen-lockfile --config.dedupe-peer-dependents=false
# pnpm --filter='@woocommerce/plugin-woocommerce' build
# echo '##[endgroup]'
title "##[group]Benchmarking head"
RESULTS_ID="editor_${GITHUB_SHA}_round-1" pnpm --filter="@woocommerce/plugin-woocommerce" test:metrics editor
RESULTS_ID="product-editor_${GITHUB_SHA}_round-1" pnpm --filter="@woocommerce/plugin-woocommerce" test:metrics product-editor
echo '##[endgroup]'
fi
if test -n "$(find $ARTIFACTS_PATH -maxdepth 1 -name "*_${BASE_SHA}_*" -print -quit)"; then
title "Skipping benchmarking baseline as benchmarking results already available under $ARTIFACTS_PATH"
else
title "##[group]Checkout baseline"
git fetch --no-tags --quiet --unshallow origin trunk
echo '##[endgroup]'
title "##[group]Building baseline"
( git -c core.hooksPath=/dev/null checkout --quiet $BASE_SHA > /dev/null || git reset --hard $BASE_SHA ) && echo 'On' $(git rev-parse HEAD)
pnpm run --if-present clean:build &
pnpm install --filter='@woocommerce/plugin-woocommerce...' --frozen-lockfile --config.dedupe-peer-dependents=false
pnpm --filter='@woocommerce/plugin-woocommerce' build
echo '##[endgroup]'
title "##[group]Benchmarking baseline"
RESULTS_ID="editor_${BASE_SHA}_round-1" pnpm --filter="@woocommerce/plugin-woocommerce" test:metrics editor
RESULTS_ID="product-editor_${BASE_SHA}_round-1" pnpm --filter="@woocommerce/plugin-woocommerce" test:metrics product-editor
echo '##[endgroup]'
# title "##[group]Restoring codebase state back to head"
# git -c core.hooksPath=/dev/null checkout --quiet $HEAD_BRANCH > /dev/null && echo 'On' $(git rev-parse HEAD)
# pnpm install --frozen-lockfile > /dev/null &
# pnpm run --if-present clean:build
# echo '##[endgroup]'
fi
title "##[group]Processing reports under $ARTIFACTS_PATH"
ls -l $ARTIFACTS_PATH
# Updating the WP version used for performance jobs means theres a high # Updating the WP version used for performance jobs means theres a high
# chance that the reference commit used for performance test stability # chance that the reference commit used for performance test stability
# becomes incompatible with the WP version. So, every time the "Tested up # becomes incompatible with the WP version. So, every time the "Tested up
@ -36,15 +84,16 @@ elif [[ "$GITHUB_EVENT_NAME" == "push" ]]; then
# - Be compatible with the new WP version used in the “Tested up to” flag. # - Be compatible with the new WP version used in the “Tested up to” flag.
# - Be tracked on https://www.codevitals.run/project/woo for all existing # - Be tracked on https://www.codevitals.run/project/woo for all existing
# metrics. # metrics.
BASE_SHA=3d7d7f02017383937f1a4158d433d0e5d44b3dc9
echo "WP_VERSION: $WP_VERSION"
IFS=. read -ra WP_VERSION_ARRAY <<< "$WP_VERSION" IFS=. read -ra WP_VERSION_ARRAY <<< "$WP_VERSION"
WP_MAJOR="${WP_VERSION_ARRAY[0]}.${WP_VERSION_ARRAY[1]}" pnpm --filter="compare-perf" run compare perf $GITHUB_SHA $BASE_SHA --tests-branch $GITHUB_SHA --wp-version "${WP_VERSION_ARRAY[0]}.${WP_VERSION_ARRAY[1]}" --ci --skip-benchmarking
pnpm --filter="compare-perf" run compare perf $GITHUB_SHA $BASE_SHA --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" echo '##[endgroup]'
title "Publish results to CodeVitals" if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then
COMMITTED_AT=$(git show -s $GITHUB_SHA --format="%cI") title "##[group]Publish results to CodeVitals"
pnpm --filter="compare-perf" run log $CODEVITALS_PROJECT_TOKEN trunk $GITHUB_SHA $BASE_SHA $COMMITTED_AT COMMITTED_AT=$(git show -s $GITHUB_SHA --format="%cI")
pnpm --filter="compare-perf" run log $CODEVITALS_PROJECT_TOKEN trunk $GITHUB_SHA $BASE_SHA $COMMITTED_AT
echo '##[endgroup]'
fi
else else
echo "Unsupported event: $GITHUB_EVENT_NAME" echo "Unsupported event: $GITHUB_EVENT_NAME"
fi fi

View File

@ -13,7 +13,7 @@ To get up and running within the WooCommerce Monorepo, you will need to make sur
### Prerequisites ### Prerequisites
- [NVM](https://github.com/nvm-sh/nvm#installing-and-updating): While you can always install Node through other means, we recommend using NVM to ensure you're aligned with the version used by our development teams. Our repository contains [an `.nvmrc` file](.nvmrc) which helps ensure you are using the correct version of Node. - [NVM](https://github.com/nvm-sh/nvm#installing-and-updating): While you can always install Node through other means, we recommend using NVM to ensure you're aligned with the version used by our development teams. Our repository contains [an `.nvmrc` file](.nvmrc) which helps ensure you are using the correct version of Node.
- [PNPM](https://pnpm.io/installation): Our repository utilizes PNPM to manage project dependencies and run various scripts involved in building and testing projects. - [PNPM](https://pnpm.io/installation): Our repository utilizes PNPM version 9.1.3 to manage project dependencies and run various scripts involved in building and testing projects.
- [PHP 7.4+](https://www.php.net/manual/en/install.php): WooCommerce Core currently features a minimum PHP version of 7.4. It is also needed to run Composer and various project build scripts. See [troubleshooting](DEVELOPMENT.md#troubleshooting) for troubleshooting problems installing PHP. - [PHP 7.4+](https://www.php.net/manual/en/install.php): WooCommerce Core currently features a minimum PHP version of 7.4. It is also needed to run Composer and various project build scripts. See [troubleshooting](DEVELOPMENT.md#troubleshooting) for troubleshooting problems installing PHP.
- [Composer](https://getcomposer.org/doc/00-intro.md): We use Composer to manage all of the dependencies for PHP packages and plugins. - [Composer](https://getcomposer.org/doc/00-intro.md): We use Composer to manage all of the dependencies for PHP packages and plugins.

View File

@ -24,7 +24,7 @@
"lint": "pnpm -r lint", "lint": "pnpm -r lint",
"cherry-pick": "node ./tools/cherry-pick/bin/run", "cherry-pick": "node ./tools/cherry-pick/bin/run",
"clean": "rimraf -g '**/node_modules' '**/.wireit' && pnpm store prune", "clean": "rimraf -g '**/node_modules' '**/.wireit' && pnpm store prune",
"buildclean": "git clean --force -d -X ./packages ./plugins ./tools", "clean:build": "rimraf -g 'packages/js/*/build' 'packages/js/*/build-*' 'packages/js/*/dist' 'plugins/*/build' 'plugins/woocommerce/client/legacy/build' && git clean --force -d -X --quiet ./plugins/woocommerce/assets",
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"postinstall": "husky", "postinstall": "husky",
"sync-dependencies": "pnpm exec syncpack -- fix-mismatches", "sync-dependencies": "pnpm exec syncpack -- fix-mismatches",

View File

@ -1,8 +1,16 @@
/**
* External dependencies
*/
import { useInstanceId } from '@wordpress/compose';
import { __, sprintf } from '@wordpress/i18n';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { ColorPalette } from './types'; import { ColorPalette } from './types';
const MAX_COLOR_PALETTES = 4;
export const ColorPalettes = ( { export const ColorPalettes = ( {
colorPalettes, colorPalettes,
totalPalettes, totalPalettes,
@ -10,33 +18,71 @@ export const ColorPalettes = ( {
colorPalettes: ColorPalette[]; colorPalettes: ColorPalette[];
totalPalettes: number; totalPalettes: number;
} ) => { } ) => {
let extra = null; const canFit = totalPalettes <= MAX_COLOR_PALETTES;
if ( totalPalettes > 4 ) { const descriptionId = useInstanceId(
extra = <li className="more_palettes">+{ totalPalettes - 4 }</li>; ColorPalettes,
'color-palettes-description'
) as string;
function renderMore() {
if ( canFit ) return null;
return (
<li aria-hidden="true" className="more_palettes">
+{ totalPalettes - 4 }
</li>
);
}
function renderDescription() {
if ( canFit ) return null;
return (
<p
id={ descriptionId }
className="theme-card__color-palettes-description"
>
{ sprintf(
/* translators: $d is the total amount of color palettes */
__(
'There are a total of %d color palettes',
'woocommerce'
),
totalPalettes
) }
</p>
);
} }
return ( return (
<ul className="theme-card__color-palettes"> <>
{ colorPalettes.map( ( colorPalette ) => ( <ul
<li className="theme-card__color-palettes"
key={ colorPalette.title } aria-label={ __( 'Color palettes', 'woocommerce' ) }
style={ { aria-describedby={ descriptionId }
background: >
'linear-gradient(to right, ' + { colorPalettes.map( ( colorPalette ) => (
colorPalette.primary + <li
' 0px, ' + key={ colorPalette.title }
colorPalette.primary + aria-label={ colorPalette.title }
' 50%, ' + style={ {
colorPalette.secondary + background:
' 50%, ' + 'linear-gradient(to right, ' +
colorPalette.secondary + colorPalette.primary +
' 100%' + ' 0px, ' +
')', colorPalette.primary +
} } ' 50%, ' +
></li> colorPalette.secondary +
) ) } ' 50%, ' +
{ extra } colorPalette.secondary +
</ul> ' 100%' +
')',
} }
/>
) ) }
{ renderMore() }
</ul>
{ renderDescription() }
</>
); );
}; };

View File

@ -314,6 +314,10 @@
text-align: center; text-align: center;
} }
} }
&-description {
@include visually-hidden();
}
} }
.theme-card__free { .theme-card__free {

View File

@ -355,9 +355,16 @@ export function subscriptionStatus(
); );
} }
return subscription.autorenew let status;
? __( 'Active', 'woocommerce' ) if ( subscription.lifetime ) {
: __( 'Cancelled', 'woocommerce' ); status = __( 'Lifetime', 'woocommerce' );
} else if ( subscription.autorenew ) {
status = __( 'Active', 'woocommerce' );
} else {
status = __( 'Cancelled', 'woocommerce' );
}
return status;
} }
return { return {
display: getStatus(), display: getStatus(),
@ -377,7 +384,7 @@ export function actions( subscription: Subscription ): TableRow {
let actionButton = null; let actionButton = null;
if ( subscription.product_key === '' ) { if ( subscription.product_key === '' ) {
actionButton = <SubscribeButton subscription={ subscription } />; actionButton = <SubscribeButton subscription={ subscription } />;
} else if ( subscription.expired ) { } else if ( subscription.expired && ! subscription.lifetime ) {
actionButton = <RenewButton subscription={ subscription } />; actionButton = <RenewButton subscription={ subscription } />;
} else if ( } else if (
subscription.local.installed === false && subscription.local.installed === false &&
@ -391,7 +398,7 @@ export function actions( subscription: Subscription ): TableRow {
actionButton = ( actionButton = (
<ConnectButton subscription={ subscription } variant="link" /> <ConnectButton subscription={ subscription } variant="link" />
); );
} else if ( ! subscription.autorenew ) { } else if ( ! subscription.autorenew && ! subscription.lifetime ) {
actionButton = <AutoRenewButton subscription={ subscription } />; actionButton = <AutoRenewButton subscription={ subscription } />;
} }

View File

@ -11,4 +11,8 @@
.wc-block-components-totals-footer-item-tax { .wc-block-components-totals-footer-item-tax {
margin-bottom: 0; margin-bottom: 0;
} }
.wc-block-components-totals-item__value {
font-weight: bold;
}
} }

View File

@ -114,6 +114,7 @@ export const TotalsShipping = ( {
<ShippingPlaceholder <ShippingPlaceholder
showCalculator={ showCalculator } showCalculator={ showCalculator }
isCheckout={ isCheckout } isCheckout={ isCheckout }
addressProvided={ addressComplete }
isShippingCalculatorOpen={ isShippingCalculatorOpen={
isShippingCalculatorOpen isShippingCalculatorOpen
} }

View File

@ -12,22 +12,27 @@ export interface ShippingPlaceholderProps {
showCalculator: boolean; showCalculator: boolean;
isShippingCalculatorOpen: boolean; isShippingCalculatorOpen: boolean;
isCheckout?: boolean; isCheckout?: boolean;
addressProvided: boolean;
setIsShippingCalculatorOpen: CalculatorButtonProps[ 'setIsShippingCalculatorOpen' ]; setIsShippingCalculatorOpen: CalculatorButtonProps[ 'setIsShippingCalculatorOpen' ];
} }
export const ShippingPlaceholder = ( { export const ShippingPlaceholder = ( {
showCalculator, showCalculator,
addressProvided,
isShippingCalculatorOpen, isShippingCalculatorOpen,
setIsShippingCalculatorOpen, setIsShippingCalculatorOpen,
isCheckout = false, isCheckout = false,
}: ShippingPlaceholderProps ): JSX.Element => { }: ShippingPlaceholderProps ): JSX.Element => {
if ( ! showCalculator ) { if ( ! showCalculator ) {
const label = addressProvided
? __( 'No available delivery option', 'woocommerce' )
: __( 'Enter address to calculate', 'woocommerce' );
return ( return (
<em> <span className="wc-block-components-shipping-placeholder__value">
{ isCheckout { isCheckout
? __( 'No shipping options available', 'woocommerce' ) ? label
: __( 'Calculated during checkout', 'woocommerce' ) } : __( 'Calculated during checkout', 'woocommerce' ) }
</em> </span>
); );
} }

View File

@ -11,7 +11,6 @@
text-transform: uppercase; text-transform: uppercase;
} }
.wc-block-components-shipping-address { .wc-block-components-shipping-address {
margin-top: $gap; margin-top: $gap;
display: block; display: block;
@ -50,6 +49,10 @@
opacity: 0.8; opacity: 0.8;
} }
} }
.wc-block-components-shipping-placeholder__value {
@include font-size(small);
}
} }
// Extra classes for specificity. // Extra classes for specificity.

View File

@ -9,22 +9,24 @@ import { screen, render } from '@testing-library/react';
import ShippingPlaceholder from '../shipping-placeholder'; import ShippingPlaceholder from '../shipping-placeholder';
describe( 'ShippingPlaceholder', () => { describe( 'ShippingPlaceholder', () => {
it( 'should show correct text if showCalculator is false', () => { it( 'should show correct text if showCalculator is false and addressProvided is false', () => {
const { rerender } = render( const { rerender } = render(
<ShippingPlaceholder <ShippingPlaceholder
showCalculator={ false } showCalculator={ false }
addressProvided={ false }
isCheckout={ true } isCheckout={ true }
isShippingCalculatorOpen={ false } isShippingCalculatorOpen={ false }
setIsShippingCalculatorOpen={ jest.fn() } setIsShippingCalculatorOpen={ jest.fn() }
/> />
); );
expect( expect(
screen.getByText( 'No shipping options available' ) screen.getByText( 'Enter address to calculate' )
).toBeInTheDocument(); ).toBeInTheDocument();
rerender( rerender(
<ShippingPlaceholder <ShippingPlaceholder
showCalculator={ false } showCalculator={ false }
isCheckout={ false } isCheckout={ false }
addressProvided={ false }
isShippingCalculatorOpen={ false } isShippingCalculatorOpen={ false }
setIsShippingCalculatorOpen={ jest.fn() } setIsShippingCalculatorOpen={ jest.fn() }
/> />
@ -33,4 +35,19 @@ describe( 'ShippingPlaceholder', () => {
screen.getByText( 'Calculated during checkout' ) screen.getByText( 'Calculated during checkout' )
).toBeInTheDocument(); ).toBeInTheDocument();
} ); } );
it( 'should show correct text if showCalculator is false and addressProvided is true', () => {
render(
<ShippingPlaceholder
showCalculator={ false }
addressProvided={ true }
isCheckout={ true }
isShippingCalculatorOpen={ false }
setIsShippingCalculatorOpen={ jest.fn() }
/>
);
expect(
screen.getByText( 'No available delivery option' )
).toBeInTheDocument();
} );
} ); } );

View File

@ -151,7 +151,7 @@ export const HandPickedProductsControlField = ( {
return ( return (
<FormTokenField <FormTokenField
displayTransform={ transformTokenIntoProductName } displayTransform={ transformTokenIntoProductName }
label={ __( 'Hand-Picked Products', 'woocommerce' ) } label={ __( 'Hand-Picked', 'woocommerce' ) }
onChange={ onTokenChange } onChange={ onTokenChange }
onInputChange={ isLargeCatalog ? handleSearch : undefined } onInputChange={ isLargeCatalog ? handleSearch : undefined }
suggestions={ suggestions } suggestions={ suggestions }
@ -186,7 +186,7 @@ const HandPickedProductsControl = ( {
return ( return (
<ToolsPanelItem <ToolsPanelItem
label={ __( 'Hand-Picked Products', 'woocommerce' ) } label={ __( 'Hand-Picked', 'woocommerce' ) }
hasValue={ () => !! selectedProductIds?.length } hasValue={ () => !! selectedProductIds?.length }
onDeselect={ deselectCallback } onDeselect={ deselectCallback }
resetAllFilter={ deselectCallback } resetAllFilter={ deselectCallback }

View File

@ -49,7 +49,7 @@ const StockStatusControl = ( props: QueryControlProps ) => {
return ( return (
<ToolsPanelItem <ToolsPanelItem
label={ __( 'Stock status', 'woocommerce' ) } label={ __( 'Stock Status', 'woocommerce' ) }
hasValue={ () => hasValue={ () =>
! fastDeepEqual( ! fastDeepEqual(
query.woocommerceStockStatus, query.woocommerceStockStatus,
@ -61,7 +61,7 @@ const StockStatusControl = ( props: QueryControlProps ) => {
isShownByDefault isShownByDefault
> >
<FormTokenField <FormTokenField
label={ __( 'Stock status', 'woocommerce' ) } label={ __( 'Stock Status', 'woocommerce' ) }
onChange={ ( statusLabels ) => { onChange={ ( statusLabels ) => {
const woocommerceStockStatus = statusLabels const woocommerceStockStatus = statusLabels
.map( getStockStatusIdByLabel ) .map( getStockStatusIdByLabel )

View File

@ -48,6 +48,20 @@ function TaxonomyControls( {
return null; return null;
} }
/**
* Normalize the name so first letter of every word is capitalized.
*/
const normalizeName = ( name: string | undefined | null ) => {
if ( ! name ) {
return '';
}
return name
.split( ' ' )
.map( ( word ) => word.charAt( 0 ).toUpperCase() + word.slice( 1 ) )
.join( ' ' );
};
return ( return (
<> <>
{ taxonomies.map( ( taxonomy: Taxonomy ) => { { taxonomies.map( ( taxonomy: Taxonomy ) => {
@ -75,7 +89,7 @@ function TaxonomyControls( {
return ( return (
<ToolsPanelItem <ToolsPanelItem
key={ slug } key={ slug }
label={ name } label={ normalizeName( name ) }
hasValue={ () => termIds.length } hasValue={ () => termIds.length }
onDeselect={ deselectCallback } onDeselect={ deselectCallback }
resetAllFilter={ deselectCallback } resetAllFilter={ deselectCallback }

View File

@ -9,10 +9,6 @@
flex-grow: 1; flex-grow: 1;
} }
.wc-block-components-totals-item__value {
font-weight: bold;
}
.wc-block-components-totals-item__description { .wc-block-components-totals-item__description {
@include font-size(small); @include font-size(small);
width: 100%; width: 100%;

View File

@ -1,13 +1,11 @@
<?php <?php
/** /**
* Plugin Name: Register Product Collection Tester * Plugin Name: WooCommerce Blocks Test Register Product Collection
* Description: A plugin to test the registerProductCollection function from WooCommerce Blocks. * Description: Used to tests the registerProductCollection function.
* Plugin URI: https://github.com/woocommerce/woocommerce * Plugin URI: https://github.com/woocommerce/woocommerce
* Author: WooCommerce * Author: WooCommerce
* *
* * @package woocommerce-blocks-test-register-product-collection
* @package register-product-collection-tester
*/ */
// Exit if accessed directly. // Exit if accessed directly.
@ -20,9 +18,9 @@ function register_product_collections_script()
{ {
wp_enqueue_script( wp_enqueue_script(
'rpc_register_product_collections', 'rpc_register_product_collections',
plugins_url('register-product-collection-tester/index.js', __FILE__), plugins_url('register-product-collection.js', __FILE__),
array('wp-element', 'wp-blocks', 'wp-i18n', 'wp-components', 'wp-editor', 'wc-blocks', 'wc-blocks-registry'), array('wp-element', 'wp-blocks', 'wp-i18n', 'wp-components', 'wp-editor', 'wc-blocks', 'wc-blocks-registry'),
filemtime(plugin_dir_path(__FILE__) . 'register-product-collection-tester/index.js'), filemtime(plugin_dir_path(__FILE__) . 'register-product-collection.js'),
true true
); );
} }

View File

@ -19,203 +19,195 @@ const test = base.extend< { pageObject: ProductCollectionPage } >( {
}, },
} ); } );
test.describe( 'Product Collection', () => { test.describe( 'Product Collection: Collections', () => {
test.describe( 'Collections', () => { test( 'New Arrivals Collection can be added and displays proper products', async ( {
test( 'New Arrivals Collection can be added and displays proper products', async ( { pageObject,
pageObject, } ) => {
} ) => { await pageObject.createNewPostAndInsertBlock( 'newArrivals' );
// New Arrivals are by default filtered to display products from last 7 days.
// Products in our test env have creation date set to much older, hence
// no products are expected to be displayed by default.
await expect( pageObject.products ).toHaveCount( 0 );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 0 );
} );
// When creating reviews programmatically the ratings are not propagated
// properly so products order by rating is undeterministic in test env.
// eslint-disable-next-line playwright/no-skipped-test
test.skip( 'Top Rated Collection can be added and displays proper products', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock( 'topRated' );
const topRatedProducts = [
'V Neck T Shirt',
'Hoodie',
'Hoodie with Logo',
'T-Shirt',
'Beanie',
];
await expect( pageObject.products ).toHaveCount( 5 );
await expect( pageObject.productTitles ).toHaveText( topRatedProducts );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 5 );
} );
// There's no orders in test env so the order of Best Sellers
// is undeterministic in test env. Requires further work.
// eslint-disable-next-line playwright/no-skipped-test
test.skip( 'Best Sellers Collection can be added and displays proper products', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock( 'bestSellers' );
const bestSellersProducts = [
'Album',
'Hoodie',
'Single',
'Hoodie with Logo',
'T-Shirt with Logo',
];
await expect( pageObject.products ).toHaveCount( 5 );
await expect( pageObject.productTitles ).toHaveText(
bestSellersProducts
);
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 5 );
} );
test( 'On Sale Collection can be added and displays proper products', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock( 'onSale' );
const onSaleProducts = [
'Beanie',
'Beanie with Logo',
'Belt',
'Cap',
'Hoodie',
];
await expect( pageObject.products ).toHaveCount( 5 );
await expect( pageObject.productTitles ).toHaveText( onSaleProducts );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 5 );
} );
test( 'Featured Collection can be added and displays proper products', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock( 'featured' );
const featuredProducts = [
'Cap',
'Hoodie with Zipper',
'Sunglasses',
'V-Neck T-Shirt',
];
await expect( pageObject.products ).toHaveCount( 4 );
await expect( pageObject.productTitles ).toHaveText( featuredProducts );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 4 );
} );
test( 'Product Catalog Collection can be added in post and syncs query with template', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock( 'productCatalog' );
const usePageContextToggle = pageObject
.locateSidebarSettings()
.locator( `${ SELECTORS.usePageContextControl } input` );
await expect( usePageContextToggle ).toBeVisible();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 9 );
} );
test( 'Product Catalog Collection can be added in product archive and syncs query with template', async ( {
pageObject,
editor,
admin,
} ) => {
await admin.visitSiteEditor( {
postId: 'woocommerce/woocommerce//archive-product',
postType: 'wp_template',
canvas: 'edit',
} );
await editor.setContent( '' );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate();
await editor.openDocumentSettingsSidebar();
const sidebarSettings = pageObject.locateSidebarSettings();
const input = sidebarSettings.locator(
`${ SELECTORS.usePageContextControl } input`
);
await expect( input ).toBeChecked();
} );
test.describe( 'Have hidden implementation in UI', () => {
test( 'New Arrivals', async ( { pageObject } ) => {
await pageObject.createNewPostAndInsertBlock( 'newArrivals' ); await pageObject.createNewPostAndInsertBlock( 'newArrivals' );
const input = await pageObject.getOrderByElement();
// New Arrivals are by default filtered to display products from last 7 days. await expect( input ).toBeHidden();
// Products in our test env have creation date set to much older, hence
// no products are expected to be displayed by default.
await expect( pageObject.products ).toHaveCount( 0 );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 0 );
} ); } );
// When creating reviews programmatically the ratings are not propagated test( 'Top Rated', async ( { pageObject } ) => {
// properly so products order by rating is undeterministic in test env.
// eslint-disable-next-line playwright/no-skipped-test
test.skip( 'Top Rated Collection can be added and displays proper products', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock( 'topRated' ); await pageObject.createNewPostAndInsertBlock( 'topRated' );
const input = await pageObject.getOrderByElement();
const topRatedProducts = [ await expect( input ).toBeHidden();
'V Neck T Shirt',
'Hoodie',
'Hoodie with Logo',
'T-Shirt',
'Beanie',
];
await expect( pageObject.products ).toHaveCount( 5 );
await expect( pageObject.productTitles ).toHaveText(
topRatedProducts
);
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 5 );
} ); } );
// There's no orders in test env so the order of Best Sellers test( 'Best Sellers', async ( { pageObject } ) => {
// is undeterministic in test env. Requires further work.
// eslint-disable-next-line playwright/no-skipped-test
test.skip( 'Best Sellers Collection can be added and displays proper products', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock( 'bestSellers' ); await pageObject.createNewPostAndInsertBlock( 'bestSellers' );
const input = await pageObject.getOrderByElement();
const bestSellersProducts = [ await expect( input ).toBeHidden();
'Album',
'Hoodie',
'Single',
'Hoodie with Logo',
'T-Shirt with Logo',
];
await expect( pageObject.products ).toHaveCount( 5 );
await expect( pageObject.productTitles ).toHaveText(
bestSellersProducts
);
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 5 );
} ); } );
test( 'On Sale Collection can be added and displays proper products', async ( { test( 'On Sale', async ( { pageObject } ) => {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock( 'onSale' ); await pageObject.createNewPostAndInsertBlock( 'onSale' );
const onSaleProducts = [
'Beanie',
'Beanie with Logo',
'Belt',
'Cap',
'Hoodie',
];
await expect( pageObject.products ).toHaveCount( 5 );
await expect( pageObject.productTitles ).toHaveText(
onSaleProducts
);
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 5 );
} );
test( 'Featured Collection can be added and displays proper products', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock( 'featured' );
const featuredProducts = [
'Cap',
'Hoodie with Zipper',
'Sunglasses',
'V-Neck T-Shirt',
];
await expect( pageObject.products ).toHaveCount( 4 );
await expect( pageObject.productTitles ).toHaveText(
featuredProducts
);
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 4 );
} );
test( 'Product Catalog Collection can be added in post and syncs query with template', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock( 'productCatalog' );
const usePageContextToggle = pageObject
.locateSidebarSettings()
.locator( `${ SELECTORS.usePageContextControl } input` );
await expect( usePageContextToggle ).toBeVisible();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 9 );
} );
test( 'Product Catalog Collection can be added in product archive and syncs query with template', async ( {
pageObject,
editor,
admin,
} ) => {
await admin.visitSiteEditor( {
postId: 'woocommerce/woocommerce//archive-product',
postType: 'wp_template',
canvas: 'edit',
} );
await editor.setContent( '' );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate();
await editor.openDocumentSettingsSidebar();
const sidebarSettings = pageObject.locateSidebarSettings(); const sidebarSettings = pageObject.locateSidebarSettings();
const input = sidebarSettings.locator( const input = sidebarSettings.getByLabel(
`${ SELECTORS.usePageContextControl } input` SELECTORS.onSaleControlLabel
); );
await expect( input ).toBeChecked(); await expect( input ).toBeHidden();
} ); } );
test.describe( 'Have hidden implementation in UI', () => { test( 'Featured', async ( { pageObject } ) => {
test( 'New Arrivals', async ( { pageObject } ) => { await pageObject.createNewPostAndInsertBlock( 'featured' );
await pageObject.createNewPostAndInsertBlock( 'newArrivals' ); const sidebarSettings = pageObject.locateSidebarSettings();
const input = await pageObject.getOrderByElement(); const input = sidebarSettings.getByLabel(
SELECTORS.featuredControlLabel
);
await expect( input ).toBeHidden(); await expect( input ).toBeHidden();
} );
test( 'Top Rated', async ( { pageObject } ) => {
await pageObject.createNewPostAndInsertBlock( 'topRated' );
const input = await pageObject.getOrderByElement();
await expect( input ).toBeHidden();
} );
test( 'Best Sellers', async ( { pageObject } ) => {
await pageObject.createNewPostAndInsertBlock( 'bestSellers' );
const input = await pageObject.getOrderByElement();
await expect( input ).toBeHidden();
} );
test( 'On Sale', async ( { pageObject } ) => {
await pageObject.createNewPostAndInsertBlock( 'onSale' );
const sidebarSettings = pageObject.locateSidebarSettings();
const input = sidebarSettings.getByLabel(
SELECTORS.onSaleControlLabel
);
await expect( input ).toBeHidden();
} );
test( 'Featured', async ( { pageObject } ) => {
await pageObject.createNewPostAndInsertBlock( 'featured' );
const sidebarSettings = pageObject.locateSidebarSettings();
const input = sidebarSettings.getByLabel(
SELECTORS.featuredControlLabel
);
await expect( input ).toBeHidden();
} );
} ); } );
} ); } );
} ); } );

View File

@ -86,35 +86,33 @@ const test = base.extend< { pageObject: ProductCollectionPage } >( {
}, },
} ); } );
test.describe( 'Compatibility Layer with Product Collection block', () => { test.describe( 'Product Collection: Compatibility Layer', () => {
test.describe( 'Product Archive with Product Collection block', () => { test.beforeEach( async ( { pageObject, requestUtils } ) => {
test.beforeEach( async ( { pageObject, requestUtils } ) => { await requestUtils.activatePlugin(
await requestUtils.activatePlugin( 'woocommerce-blocks-test-product-collection-compatibility-layer'
'woocommerce-blocks-test-product-collection-compatibility-layer' );
); await pageObject.goToProductCatalogFrontend();
await pageObject.goToProductCatalogFrontend();
} );
for ( const scenario of singleOccurrenceScenarios ) {
test( `${ scenario.title } is attached to the page`, async ( {
pageObject,
} ) => {
const hooks = pageObject.locateByTestId( scenario.dataTestId );
await expect( hooks ).toHaveCount( scenario.amount );
await expect( hooks ).toHaveText( scenario.content );
} );
}
for ( const scenario of multipleOccurrenceScenarios ) {
test( `${ scenario.title } is attached to the page`, async ( {
pageObject,
} ) => {
const hooks = pageObject.locateByTestId( scenario.dataTestId );
await expect( hooks ).toHaveCount( scenario.amount );
await expect( hooks.first() ).toHaveText( scenario.content );
} );
}
} ); } );
for ( const scenario of singleOccurrenceScenarios ) {
test( `${ scenario.title } is attached to the page`, async ( {
pageObject,
} ) => {
const hooks = pageObject.locateByTestId( scenario.dataTestId );
await expect( hooks ).toHaveCount( scenario.amount );
await expect( hooks ).toHaveText( scenario.content );
} );
}
for ( const scenario of multipleOccurrenceScenarios ) {
test( `${ scenario.title } is attached to the page`, async ( {
pageObject,
} ) => {
const hooks = pageObject.locateByTestId( scenario.dataTestId );
await expect( hooks ).toHaveCount( scenario.amount );
await expect( hooks.first() ).toHaveText( scenario.content );
} );
}
} ); } );

View File

@ -19,7 +19,7 @@ const test = base.extend< { pageObject: ProductCollectionPage } >( {
}, },
} ); } );
test.describe( 'Product Collection - extensibility JS events', () => { test.describe( 'Product Collection: Extensibility Events', () => {
test( 'emits wc-blocks_product_list_rendered event on init and on page change', async ( { test( 'emits wc-blocks_product_list_rendered event on init and on page change', async ( {
pageObject, pageObject,
page, page,

View File

@ -9,7 +9,6 @@ import { test as base, expect } from '@woocommerce/e2e-utils';
*/ */
import ProductCollectionPage, { import ProductCollectionPage, {
BLOCK_LABELS, BLOCK_LABELS,
Collections,
SELECTORS, SELECTORS,
} from './product-collection.page'; } from './product-collection.page';
@ -87,11 +86,7 @@ test.describe( 'Product Collection', () => {
await admin.createNewPost(); await admin.createNewPost();
} ); } );
test.skip( 'does not render', async ( { test( 'does not render', async ( { page, editor, pageObject } ) => {
page,
editor,
pageObject,
} ) => {
await pageObject.insertProductCollection(); await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost( 'featured' ); await pageObject.chooseCollectionInPost( 'featured' );
await pageObject.addFilter( 'Price Range' ); await pageObject.addFilter( 'Price Range' );
@ -106,7 +101,7 @@ test.describe( 'Product Collection', () => {
).toBeVisible(); ).toBeVisible();
// The "No results found" info is rendered in editor for all collections. // The "No results found" info is rendered in editor for all collections.
await expect( await expect(
featuredBlock.getByText( 'No results found' ) featuredBlock.getByText( 'No products to display' )
).toBeVisible(); ).toBeVisible();
await pageObject.publishAndGoToFrontend(); await pageObject.publishAndGoToFrontend();
@ -114,7 +109,9 @@ test.describe( 'Product Collection', () => {
const content = page.locator( 'main' ); const content = page.locator( 'main' );
await expect( content ).not.toContainText( 'Featured products' ); await expect( content ).not.toContainText( 'Featured products' );
await expect( content ).not.toContainText( 'No results found' ); await expect( content ).not.toContainText(
'No products to display'
);
} ); } );
// This test ensures the runtime render state is correctly reset for // This test ensures the runtime render state is correctly reset for
@ -739,7 +736,7 @@ test.describe( 'Product Collection', () => {
} ); } );
} ); } );
const templates = { const templates = [
// This test is disabled because archives are disabled for attributes by default. This can be uncommented when this is toggled on. // This test is disabled because archives are disabled for attributes by default. This can be uncommented when this is toggled on.
//'taxonomy-product_attribute': { //'taxonomy-product_attribute': {
// templateTitle: 'Product Attribute', // templateTitle: 'Product Attribute',
@ -747,102 +744,104 @@ test.describe( 'Product Collection', () => {
// frontendPage: '/product-attribute/color/', // frontendPage: '/product-attribute/color/',
// legacyBlockName: 'woocommerce/legacy-template', // legacyBlockName: 'woocommerce/legacy-template',
//}, //},
'taxonomy-product_cat': { {
templateTitle: 'Product Category', templateTitle: 'Product Category',
slug: 'taxonomy-product_cat', slug: 'taxonomy-product_cat',
frontendPage: '/product-category/music/', frontendPage: '/product-category/music/',
legacyBlockName: 'woocommerce/legacy-template', legacyBlockName: 'woocommerce/legacy-template',
expectedProductsCount: 2, expectedProductsCount: 2,
}, },
'taxonomy-product_tag': { {
templateTitle: 'Product Tag', templateTitle: 'Product Tag',
slug: 'taxonomy-product_tag', slug: 'taxonomy-product_tag',
frontendPage: '/product-tag/recommended/', frontendPage: '/product-tag/recommended/',
legacyBlockName: 'woocommerce/legacy-template', legacyBlockName: 'woocommerce/legacy-template',
expectedProductsCount: 2, expectedProductsCount: 2,
}, },
'archive-product': { {
templateTitle: 'Product Catalog', templateTitle: 'Product Catalog',
slug: 'archive-product', slug: 'archive-product',
frontendPage: '/shop/', frontendPage: '/shop/',
legacyBlockName: 'woocommerce/legacy-template', legacyBlockName: 'woocommerce/legacy-template',
expectedProductsCount: 16, expectedProductsCount: 16,
}, },
'product-search-results': { {
templateTitle: 'Product Search Results', templateTitle: 'Product Search Results',
slug: 'product-search-results', slug: 'product-search-results',
frontendPage: '/?s=shirt&post_type=product', frontendPage: '/?s=shirt&post_type=product',
legacyBlockName: 'woocommerce/legacy-template', legacyBlockName: 'woocommerce/legacy-template',
expectedProductsCount: 3, expectedProductsCount: 3,
}, },
}; ];
for ( const { templates.forEach(
templateTitle, ( {
slug, templateTitle,
frontendPage, slug,
legacyBlockName, frontendPage,
expectedProductsCount, legacyBlockName,
} of Object.values( templates ) ) { expectedProductsCount,
test.describe( `${ templateTitle } template`, () => { } ) => {
test( 'Product Collection block matches with classic template block', async ( { test.describe( `${ templateTitle } template`, () => {
pageObject, test( 'Product Collection block matches with classic template block', async ( {
requestUtils, pageObject,
admin, requestUtils,
editor, admin,
page, editor,
} ) => { page,
await pageObject.refreshLocators( 'frontend' ); } ) => {
await pageObject.refreshLocators( 'frontend' );
await page.goto( frontendPage ); await page.goto( frontendPage );
const productCollectionProductNames = const productCollectionProductNames =
await pageObject.getProductNames(); await pageObject.getProductNames();
const template = await requestUtils.createTemplate( const template = await requestUtils.createTemplate(
'wp_template', 'wp_template',
{ {
slug, slug,
title: 'classic template test', title: 'classic template test',
content: 'howdy', content: 'howdy',
} }
); );
await admin.visitSiteEditor( { await admin.visitSiteEditor( {
postId: template.id, postId: template.id,
postType: 'wp_template', postType: 'wp_template',
canvas: 'edit', canvas: 'edit',
} );
await expect(
editor.canvas.getByText( 'howdy' )
).toBeVisible();
await editor.insertBlock( { name: legacyBlockName } );
await editor.saveSiteEditorEntities( {
isOnlyCurrentEntityDirty: true,
} );
await page.goto( frontendPage );
const classicProducts = page.locator(
'.woocommerce-loop-product__title'
);
await expect( classicProducts ).toHaveCount(
expectedProductsCount
);
const classicProductsNames =
await classicProducts.allTextContents();
expect( classicProductsNames ).toEqual(
productCollectionProductNames
);
} ); } );
await expect(
editor.canvas.getByText( 'howdy' )
).toBeVisible();
await editor.insertBlock( { name: legacyBlockName } );
await editor.saveSiteEditorEntities( {
isOnlyCurrentEntityDirty: true,
} );
await page.goto( frontendPage );
const classicProducts = page.locator(
'.woocommerce-loop-product__title'
);
await expect( classicProducts ).toHaveCount(
expectedProductsCount
);
const classicProductsNames =
await classicProducts.allTextContents();
expect( classicProductsNames ).toEqual(
productCollectionProductNames
);
} ); } );
} ); }
} );
test.describe( 'Editor: In taxonomies templates', () => { test.describe( 'Editor: In taxonomies templates', () => {
test( 'Products by specific category template displays products from this category', async ( { test( 'Products by specific category template displays products from this category', async ( {
admin, admin,
@ -906,309 +905,3 @@ test.describe( 'Product Collection', () => {
} ); } );
} ); } );
} ); } );
test.describe( 'Testing "usesReference" argument in "registerProductCollection"', () => {
const MY_REGISTERED_COLLECTIONS = {
myCustomCollectionWithProductContext: {
name: 'My Custom Collection - Product Context',
label: 'Block: My Custom Collection - Product Context',
previewLabelTemplate: [ 'woocommerce/woocommerce//single-product' ],
shouldShowProductPicker: true,
},
myCustomCollectionWithCartContext: {
name: 'My Custom Collection - Cart Context',
label: 'Block: My Custom Collection - Cart Context',
previewLabelTemplate: [ 'woocommerce/woocommerce//page-cart' ],
shouldShowProductPicker: false,
},
myCustomCollectionWithOrderContext: {
name: 'My Custom Collection - Order Context',
label: 'Block: My Custom Collection - Order Context',
previewLabelTemplate: [
'woocommerce/woocommerce//order-confirmation',
],
shouldShowProductPicker: false,
},
myCustomCollectionWithArchiveContext: {
name: 'My Custom Collection - Archive Context',
label: 'Block: My Custom Collection - Archive Context',
previewLabelTemplate: [
'woocommerce/woocommerce//taxonomy-product_cat',
],
shouldShowProductPicker: false,
},
myCustomCollectionMultipleContexts: {
name: 'My Custom Collection - Multiple Contexts',
label: 'Block: My Custom Collection - Multiple Contexts',
previewLabelTemplate: [
'woocommerce/woocommerce//single-product',
'woocommerce/woocommerce//order-confirmation',
],
shouldShowProductPicker: true,
},
};
// Activate plugin which registers custom product collections
test.beforeEach( async ( { requestUtils } ) => {
await requestUtils.activatePlugin(
'register-product-collection-tester'
);
} );
Object.entries( MY_REGISTERED_COLLECTIONS ).forEach(
( [ key, collection ] ) => {
for ( const template of collection.previewLabelTemplate ) {
test( `Collection "${ collection.name }" should show preview label in "${ template }"`, async ( {
pageObject,
editor,
} ) => {
await pageObject.goToEditorTemplate( template );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate(
key as Collections
);
const block = editor.canvas.getByLabel( collection.label );
const previewButtonLocator = block.getByTestId(
SELECTORS.previewButtonTestID
);
await expect( previewButtonLocator ).toBeVisible();
} );
}
test( `Collection "${ collection.name }" should not show preview label in a post`, async ( {
pageObject,
editor,
admin,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost( key as Collections );
// Check visibility of product picker
const editorProductPicker = editor.canvas.locator(
SELECTORS.productPicker
);
const expectedVisibility = collection.shouldShowProductPicker
? 'toBeVisible'
: 'toBeHidden';
await expect( editorProductPicker )[ expectedVisibility ]();
if ( collection.shouldShowProductPicker ) {
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
}
// At this point, the product picker should be hidden
await expect( editorProductPicker ).toBeHidden();
// Check visibility of preview label
const block = editor.canvas.getByLabel( collection.label );
const previewButtonLocator = block.getByTestId(
SELECTORS.previewButtonTestID
);
await expect( previewButtonLocator ).toBeHidden();
} );
test( `Collection "${ collection.name }" should not show preview label in Product Catalog template`, async ( {
pageObject,
editor,
} ) => {
await pageObject.goToProductCatalogAndInsertCollection(
key as Collections
);
const block = editor.canvas.getByLabel( collection.label );
const previewButtonLocator = block.getByTestId(
SELECTORS.previewButtonTestID
);
await expect( previewButtonLocator ).toBeHidden();
} );
}
);
} );
test.describe( 'Product picker', () => {
const MY_REGISTERED_COLLECTIONS_THAT_NEEDS_PRODUCT = {
myCustomCollectionWithProductContext: {
name: 'My Custom Collection - Product Context',
label: 'Block: My Custom Collection - Product Context',
collection:
'woocommerce/product-collection/my-custom-collection-product-context',
},
myCustomCollectionMultipleContexts: {
name: 'My Custom Collection - Multiple Contexts',
label: 'Block: My Custom Collection - Multiple Contexts',
collection:
'woocommerce/product-collection/my-custom-collection-multiple-contexts',
},
};
// Activate plugin which registers custom product collections
test.beforeEach( async ( { requestUtils } ) => {
await requestUtils.activatePlugin(
'register-product-collection-tester'
);
} );
Object.entries( MY_REGISTERED_COLLECTIONS_THAT_NEEDS_PRODUCT ).forEach(
( [ key, collection ] ) => {
test( `For collection "${ collection.name }" - manually selected product reference should be available on Frontend in a post`, async ( {
pageObject,
admin,
page,
editor,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost( key as Collections );
// Verify that product picker is shown in Editor
const editorProductPicker = editor.canvas.locator(
SELECTORS.productPicker
);
await expect( editorProductPicker ).toBeVisible();
// Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
await expect( editorProductPicker ).toBeHidden();
// On Frontend, verify that product reference is a number
await pageObject.publishAndGoToFrontend();
const collectionWithProductContext = page.locator(
`[data-collection="${ collection.collection }"]`
);
const queryAttribute = JSON.parse(
( await collectionWithProductContext.getAttribute(
'data-query'
) ) || '{}'
);
expect( typeof queryAttribute?.productReference ).toBe(
'number'
);
} );
test( `For collection "${ collection.name }" - changing product using inspector control`, async ( {
pageObject,
admin,
page,
editor,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost( key as Collections );
// Verify that product picker is shown in Editor
const editorProductPicker = editor.canvas.locator(
SELECTORS.productPicker
);
await expect( editorProductPicker ).toBeVisible();
// Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
await expect( editorProductPicker ).toBeHidden();
// Verify that Album is selected
await expect(
admin.page.locator( SELECTORS.linkedProductControl.button )
).toContainText( 'Album' );
// Change product using inspector control to Beanie
await admin.page
.locator( SELECTORS.linkedProductControl.button )
.click();
await admin.page
.locator( SELECTORS.linkedProductControl.popoverContent )
.getByLabel( 'Beanie', { exact: true } )
.click();
await expect(
admin.page.locator( SELECTORS.linkedProductControl.button )
).toContainText( 'Beanie' );
// On Frontend, verify that product reference is a number
await pageObject.publishAndGoToFrontend();
const collectionWithProductContext = page.locator(
`[data-collection="${ collection.collection }"]`
);
const queryAttribute = JSON.parse(
( await collectionWithProductContext.getAttribute(
'data-query'
) ) || '{}'
);
expect( typeof queryAttribute?.productReference ).toBe(
'number'
);
} );
test( `For collection "${ collection.name }" - "From current product" is chosen by default`, async ( {
pageObject,
admin,
editor,
} ) => {
await admin.visitSiteEditor( {
postId: `woocommerce/woocommerce//single-product`,
postType: 'wp_template',
canvas: 'edit',
} );
await editor.canvas.locator( 'body' ).click();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate(
key as Collections
);
const productToShowControl = admin.page.getByText(
'From the current product'
);
await expect( productToShowControl ).toBeChecked();
} );
}
);
test( 'Product picker should work as expected while changing collection using "Choose collection" button from Toolbar', async ( {
pageObject,
admin,
editor,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost(
'myCustomCollectionWithProductContext'
);
// Verify that product picker is shown in Editor
const editorProductPicker = editor.canvas.locator(
SELECTORS.productPicker
);
await expect( editorProductPicker ).toBeVisible();
// Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
await expect( editorProductPicker ).toBeHidden();
// Change collection using Toolbar
await pageObject.changeCollectionUsingToolbar(
'myCustomCollectionMultipleContexts'
);
await expect( editorProductPicker ).toBeVisible();
// Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
await expect( editorProductPicker ).toBeHidden();
// Product picker should be hidden for collections that don't need product
await pageObject.changeCollectionUsingToolbar( 'featured' );
await expect( editorProductPicker ).toBeHidden();
} );
} );

View File

@ -396,7 +396,7 @@ class ProductCollectionPage {
async addFilter( async addFilter(
name: name:
| 'Show Hand-picked Products' | 'Show Hand-picked'
| 'Keyword' | 'Keyword'
| 'Show product categories' | 'Show product categories'
| 'Show product tags' | 'Show product tags'

View File

@ -0,0 +1,204 @@
/**
* External dependencies
*/
import { test as base, expect } from '@woocommerce/e2e-utils';
/**
* Internal dependencies
*/
import ProductCollectionPage, {
Collections,
SELECTORS,
} from './product-collection.page';
const test = base.extend< { pageObject: ProductCollectionPage } >( {
pageObject: async ( { page, admin, editor }, use ) => {
const pageObject = new ProductCollectionPage( {
page,
admin,
editor,
} );
await use( pageObject );
},
} );
test.describe( 'Product Collection: Product Picker', () => {
const CUSTOM_COLLECTIONS = [
{
id: 'myCustomCollectionWithProductContext',
name: 'My Custom Collection - Product Context',
label: 'Block: My Custom Collection - Product Context',
collection:
'woocommerce/product-collection/my-custom-collection-product-context',
},
{
id: 'myCustomCollectionMultipleContexts',
name: 'My Custom Collection - Multiple Contexts',
label: 'Block: My Custom Collection - Multiple Contexts',
collection:
'woocommerce/product-collection/my-custom-collection-multiple-contexts',
},
];
// Activate plugin which registers custom product collections
test.beforeEach( async ( { requestUtils } ) => {
await requestUtils.activatePlugin(
'woocommerce-blocks-test-register-product-collection'
);
} );
CUSTOM_COLLECTIONS.forEach( ( collection ) => {
test( `For collection "${ collection.name }" - manually selected product reference should be available on Frontend in a post`, async ( {
pageObject,
admin,
page,
editor,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost(
collection.id as Collections
);
// Verify that product picker is shown in Editor
const editorProductPicker = editor.canvas.locator(
SELECTORS.productPicker
);
await expect( editorProductPicker ).toBeVisible();
// Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
await expect( editorProductPicker ).toBeHidden();
// On Frontend, verify that product reference is a number
await pageObject.publishAndGoToFrontend();
const collectionWithProductContext = page.locator(
`[data-collection="${ collection.collection }"]`
);
const queryAttribute = JSON.parse(
( await collectionWithProductContext.getAttribute(
'data-query'
) ) || '{}'
);
expect( typeof queryAttribute?.productReference ).toBe( 'number' );
} );
test( `For collection "${ collection.name }" - changing product using inspector control`, async ( {
pageObject,
admin,
page,
editor,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost(
collection.id as Collections
);
// Verify that product picker is shown in Editor
const editorProductPicker = editor.canvas.locator(
SELECTORS.productPicker
);
await expect( editorProductPicker ).toBeVisible();
// Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
await expect( editorProductPicker ).toBeHidden();
// Verify that Album is selected
await expect(
admin.page.locator( SELECTORS.linkedProductControl.button )
).toContainText( 'Album' );
// Change product using inspector control to Beanie
await admin.page
.locator( SELECTORS.linkedProductControl.button )
.click();
await admin.page
.locator( SELECTORS.linkedProductControl.popoverContent )
.getByLabel( 'Beanie', { exact: true } )
.click();
await expect(
admin.page.locator( SELECTORS.linkedProductControl.button )
).toContainText( 'Beanie' );
// On Frontend, verify that product reference is a number
await pageObject.publishAndGoToFrontend();
const collectionWithProductContext = page.locator(
`[data-collection="${ collection.collection }"]`
);
const queryAttribute = JSON.parse(
( await collectionWithProductContext.getAttribute(
'data-query'
) ) || '{}'
);
expect( typeof queryAttribute?.productReference ).toBe( 'number' );
} );
test( `For collection "${ collection.name }" - "From current product" is chosen by default`, async ( {
pageObject,
admin,
editor,
} ) => {
await admin.visitSiteEditor( {
postId: `woocommerce/woocommerce//single-product`,
postType: 'wp_template',
canvas: 'edit',
} );
await editor.canvas.locator( 'body' ).click();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate(
collection.id as Collections
);
const productToShowControl = admin.page.getByText(
'From the current product'
);
await expect( productToShowControl ).toBeChecked();
} );
} );
test( 'Product picker should work as expected while changing collection using "Choose collection" button from Toolbar', async ( {
pageObject,
admin,
editor,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost(
'myCustomCollectionWithProductContext'
);
// Verify that product picker is shown in Editor
const editorProductPicker = editor.canvas.locator(
SELECTORS.productPicker
);
await expect( editorProductPicker ).toBeVisible();
// Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
await expect( editorProductPicker ).toBeHidden();
// Change collection using Toolbar
await pageObject.changeCollectionUsingToolbar(
'myCustomCollectionMultipleContexts'
);
await expect( editorProductPicker ).toBeVisible();
// Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
await expect( editorProductPicker ).toBeHidden();
// Product picker should be hidden for collections that don't need product
await pageObject.changeCollectionUsingToolbar( 'featured' );
await expect( editorProductPicker ).toBeHidden();
} );
} );

View File

@ -33,7 +33,7 @@ const test = base.extend< { pageObject: ProductCollectionPage } >( {
* These E2E tests are for `registerProductCollection` which we are exposing * These E2E tests are for `registerProductCollection` which we are exposing
* for 3PDs to register new product collections. * for 3PDs to register new product collections.
*/ */
test.describe( 'Product Collection registration', () => { test.describe( 'Product Collection: Register Product Collection', () => {
const MY_REGISTERED_COLLECTIONS = { const MY_REGISTERED_COLLECTIONS = {
myCustomCollection: { myCustomCollection: {
name: 'My Custom Collection', name: 'My Custom Collection',
@ -56,7 +56,7 @@ test.describe( 'Product Collection registration', () => {
// Activate plugin which registers custom product collections // Activate plugin which registers custom product collections
test.beforeEach( async ( { requestUtils } ) => { test.beforeEach( async ( { requestUtils } ) => {
await requestUtils.activatePlugin( await requestUtils.activatePlugin(
'register-product-collection-tester' 'woocommerce-blocks-test-register-product-collection'
); );
} ); } );
@ -323,7 +323,7 @@ test.describe( 'Product Collection registration', () => {
], ],
}, },
].forEach( ( collection ) => { ].forEach( ( collection ) => {
for ( const template of collection.previewLabelTemplate ) { collection.previewLabelTemplate.forEach( ( template ) => {
test( `Collection "${ collection.name }" should show preview label in "${ template }"`, async ( { test( `Collection "${ collection.name }" should show preview label in "${ template }"`, async ( {
pageObject, pageObject,
editor, editor,
@ -341,7 +341,7 @@ test.describe( 'Product Collection registration', () => {
await expect( previewButtonLocator ).toBeVisible(); await expect( previewButtonLocator ).toBeVisible();
} ); } );
} } );
test( `Collection "${ collection.name }" should not show preview label in a post`, async ( { test( `Collection "${ collection.name }" should not show preview label in a post`, async ( {
pageObject, pageObject,
@ -429,7 +429,7 @@ test.describe( 'Product Collection registration', () => {
} ); } );
// Product picker should be shown in Editor // Product picker should be shown in Editor
await admin.page.reload(); await page.reload();
const deletedProductPicker = editor.canvas.getByText( const deletedProductPicker = editor.canvas.getByText(
'Previously selected product' 'Previously selected product'
); );
@ -465,4 +465,127 @@ test.describe( 'Product Collection registration', () => {
await page.reload(); await page.reload();
await expect( deletedProductPicker ).toBeVisible(); await expect( deletedProductPicker ).toBeVisible();
} ); } );
test.describe( 'with "usesReference" argument', () => {
[
{
id: 'myCustomCollectionWithProductContext',
name: 'My Custom Collection - Product Context',
label: 'Block: My Custom Collection - Product Context',
previewLabelTemplate: [
'woocommerce/woocommerce//single-product',
],
shouldShowProductPicker: true,
},
{
id: 'myCustomCollectionWithCartContext',
name: 'My Custom Collection - Cart Context',
label: 'Block: My Custom Collection - Cart Context',
previewLabelTemplate: [ 'woocommerce/woocommerce//page-cart' ],
shouldShowProductPicker: false,
},
{
id: 'myCustomCollectionWithOrderContext',
name: 'My Custom Collection - Order Context',
label: 'Block: My Custom Collection - Order Context',
previewLabelTemplate: [
'woocommerce/woocommerce//order-confirmation',
],
shouldShowProductPicker: false,
},
{
id: 'myCustomCollectionWithArchiveContext',
name: 'My Custom Collection - Archive Context',
label: 'Block: My Custom Collection - Archive Context',
previewLabelTemplate: [
'woocommerce/woocommerce//taxonomy-product_cat',
],
shouldShowProductPicker: false,
},
{
id: 'myCustomCollectionMultipleContexts',
name: 'My Custom Collection - Multiple Contexts',
label: 'Block: My Custom Collection - Multiple Contexts',
previewLabelTemplate: [
'woocommerce/woocommerce//single-product',
'woocommerce/woocommerce//order-confirmation',
],
shouldShowProductPicker: true,
},
].forEach( ( collection ) => {
collection.previewLabelTemplate.forEach( ( template ) => {
test( `Collection "${ collection.name }" should show preview label in "${ template }"`, async ( {
pageObject,
editor,
} ) => {
await pageObject.goToEditorTemplate( template );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate(
collection.id as Collections
);
const block = editor.canvas.getByLabel( collection.label );
const previewButtonLocator = block.getByTestId(
SELECTORS.previewButtonTestID
);
await expect( previewButtonLocator ).toBeVisible();
} );
} );
test( `Collection "${ collection.name }" should not show preview label in a post`, async ( {
pageObject,
editor,
admin,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost(
collection.id as Collections
);
// Check visibility of product picker
const editorProductPicker = editor.canvas.locator(
SELECTORS.productPicker
);
const expectedVisibility = collection.shouldShowProductPicker
? 'toBeVisible'
: 'toBeHidden';
await expect( editorProductPicker )[ expectedVisibility ]();
if ( collection.shouldShowProductPicker ) {
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
}
// At this point, the product picker should be hidden
await expect( editorProductPicker ).toBeHidden();
// Check visibility of preview label
const block = editor.canvas.getByLabel( collection.label );
const previewButtonLocator = block.getByTestId(
SELECTORS.previewButtonTestID
);
await expect( previewButtonLocator ).toBeHidden();
} );
test( `Collection "${ collection.name }" should not show preview label in Product Catalog template`, async ( {
pageObject,
editor,
} ) => {
await pageObject.goToProductCatalogAndInsertCollection(
collection.id as Collections
);
const block = editor.canvas.getByLabel( collection.label );
const previewButtonLocator = block.getByTestId(
SELECTORS.previewButtonTestID
);
await expect( previewButtonLocator ).toBeHidden();
} );
} );
} );
} ); } );

View File

@ -10,7 +10,6 @@ export * from './mini-cart';
export * from './performance'; export * from './performance';
export * from './request-utils'; export * from './request-utils';
export * from './shipping'; export * from './shipping';
export * from './storeApi';
export * from './test'; export * from './test';

View File

@ -15,33 +15,6 @@ import {
} from './templates'; } from './templates';
export class RequestUtils extends CoreRequestUtils { export class RequestUtils extends CoreRequestUtils {
// The `setup` override is necessary only until
// https://github.com/WordPress/gutenberg/pull/59362 is merged.
static async setup( ...args: Parameters< typeof CoreRequestUtils.setup > ) {
const { request, user, storageState, storageStatePath, baseURL } =
await CoreRequestUtils.setup( ...args );
// We need those checks to satisfy TypeScript.
if ( ! storageState ) {
throw new Error( 'Storage state is required' );
}
if ( ! storageStatePath ) {
throw new Error( 'Storage state path is required' );
}
if ( ! baseURL ) {
throw new Error( 'Base URL is required' );
}
return new this( request, {
user,
storageState,
storageStatePath,
baseURL,
} );
}
/** @borrows getTemplates as this.getTemplates */ /** @borrows getTemplates as this.getTemplates */
getTemplates: typeof getTemplates = getTemplates.bind( this ); getTemplates: typeof getTemplates = getTemplates.bind( this );
/** @borrows revertTemplate as this.revertTemplate */ /** @borrows revertTemplate as this.revertTemplate */

View File

@ -1 +0,0 @@
export * from './store-api-utils.page';

View File

@ -1,30 +0,0 @@
/**
* External dependencies
*/
import { RequestUtils } from '@wordpress/e2e-test-utils-playwright';
export class StoreApiUtils {
private requestUtils: RequestUtils;
constructor( requestUtils: RequestUtils ) {
this.requestUtils = requestUtils;
}
// @todo: It is necessary work to a middleware to avoid this kind of code.
async cleanCart() {
const response = await this.requestUtils.request.get(
'/wp-json/wc/store/cart'
);
const { nonce } = response.headers();
await this.requestUtils.request.delete(
`/wp-json/wc/store/v1/cart/items`,
{
headers: {
nonce,
},
}
);
}
}

View File

@ -16,7 +16,6 @@ import {
PerformanceUtils, PerformanceUtils,
RequestUtils, RequestUtils,
ShippingUtils, ShippingUtils,
StoreApiUtils,
} from '@woocommerce/e2e-utils'; } from '@woocommerce/e2e-utils';
/** /**
@ -108,7 +107,6 @@ const test = base.extend<
editor: Editor; editor: Editor;
pageUtils: PageUtils; pageUtils: PageUtils;
frontendUtils: FrontendUtils; frontendUtils: FrontendUtils;
storeApiUtils: StoreApiUtils;
performanceUtils: PerformanceUtils; performanceUtils: PerformanceUtils;
snapshotConfig: void; snapshotConfig: void;
shippingUtils: ShippingUtils; shippingUtils: ShippingUtils;
@ -135,6 +133,10 @@ const test = base.extend<
window.localStorage.clear(); window.localStorage.clear();
} ); } );
// Dispose the current APIRequestContext to free up resources.
await page.request.dispose();
// Reset the database to the initial state via snapshot import.
await wpCLI( `db import ${ DB_EXPORT_FILE }` ); await wpCLI( `db import ${ DB_EXPORT_FILE }` );
}, },
pageUtils: async ( { page }, use ) => { pageUtils: async ( { page }, use ) => {
@ -146,9 +148,6 @@ const test = base.extend<
performanceUtils: async ( { page }, use ) => { performanceUtils: async ( { page }, use ) => {
await use( new PerformanceUtils( page ) ); await use( new PerformanceUtils( page ) );
}, },
storeApiUtils: async ( { requestUtils }, use ) => {
await use( new StoreApiUtils( requestUtils ) );
},
shippingUtils: async ( { page, admin }, use ) => { shippingUtils: async ( { page, admin }, use ) => {
await use( new ShippingUtils( page, admin ) ); await use( new ShippingUtils( page, admin ) );
}, },

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Comment: These are changes to build commands.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Add missing `wp-block-woocommerce-{name}` class to Add to Cart with Options, Product Image, Product Rating, Product Rating Stars, Product Rating Counter and Product Image blocks.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Resolved fatal error when applying Brands-restricted coupon

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Changes to copy and styling of delivery summary in checkout block.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Expand the e2e suite we're running on WPCOM part #5.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Return an empty string from `template_include` filter instead of null to avoid PHP fatal error with conflicting plugins using strict types.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Ensure Product Collection filter names are consistently capitalized.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: add
Add a11y to the color swatches

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add core feature for site visibility badge

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Render Advanced CSS Classes in the Product Image block

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix an issue where database is randomly disconnected while running Blocks E2E tests.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Blocks E2E: fix plugin namespace

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Remove alert role from informational notices

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix subscription status and action items for free (lifetime) subscriptions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
CI: update performance metrics job benchmarking.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
CI: omit caching baseline results in perfromance metrics job.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
CI: update perfromance metrics job and improve execution time.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Changed how we fetch WooCommerce promotions. We're doing it async so as not to affect the loading of wp-admin.

View File

@ -8795,3 +8795,8 @@ table.bar_chart {
html:has(#status-table-templates){ html:has(#status-table-templates){
scroll-padding-top: 80px; scroll-padding-top: 80px;
} }
// Fix for Safari bug: https://bugs.webkit.org/show_bug.cgi?id=280063.
.woocommerce-admin-page #postbox-container-2 {
clear: left;
}

View File

@ -15,6 +15,7 @@ if ( ! defined( 'ABSPATH' ) ) {
*/ */
class WC_Admin_Marketplace_Promotions { class WC_Admin_Marketplace_Promotions {
const CRON_NAME = 'woocommerce_marketplace_cron_fetch_promotions';
const TRANSIENT_NAME = 'woocommerce_marketplace_promotions_v2'; const TRANSIENT_NAME = 'woocommerce_marketplace_promotions_v2';
const TRANSIENT_LIFE_SPAN = DAY_IN_SECONDS; const TRANSIENT_LIFE_SPAN = DAY_IN_SECONDS;
const PROMOTIONS_API_URL = 'https://woocommerce.com/wp-json/wccom-extensions/3.0/promotions'; const PROMOTIONS_API_URL = 'https://woocommerce.com/wp-json/wccom-extensions/3.0/promotions';
@ -39,7 +40,16 @@ class WC_Admin_Marketplace_Promotions {
public static function init() { public static function init() {
// A legacy hook that can be triggered by action scheduler. // A legacy hook that can be triggered by action scheduler.
add_action( 'woocommerce_marketplace_fetch_promotions', array( __CLASS__, 'clear_deprecated_action' ) ); add_action( 'woocommerce_marketplace_fetch_promotions', array( __CLASS__, 'clear_deprecated_action' ) );
add_action( 'woocommerce_marketplace_fetch_promotions_clear', array( __CLASS__, 'clear_scheduled_event' ) ); add_action(
'woocommerce_marketplace_fetch_promotions_clear',
array(
__CLASS__,
'clear_deprecated_scheduled_event',
)
);
// Fetch promotions from the API and store them in a transient.
add_action( self::CRON_NAME, array( __CLASS__, 'update_promotions' ) );
if ( if (
defined( 'DOING_AJAX' ) && DOING_AJAX defined( 'DOING_AJAX' ) && DOING_AJAX
@ -53,24 +63,33 @@ class WC_Admin_Marketplace_Promotions {
return; return;
} }
self::maybe_update_promotions(); self::schedule_cron_event();
register_deactivation_hook( WC_PLUGIN_FILE, array( __CLASS__, 'clear_cron_event' ) );
self::$locale = ( self::$locale ?? get_user_locale() ) ?? 'en_US'; self::$locale = ( self::$locale ?? get_user_locale() ) ?? 'en_US';
self::maybe_show_bubble_promotions(); self::maybe_show_bubble_promotions();
} }
/** /**
* Fetch promotions from the API and store them in a transient. * Schedule a daily cron event to fetch promotions.
* Fetching can be suppressed by the `woocommerce_marketplace_suppress_promotions` filter. *
* @version 9.5.0
* *
* @return void * @return void
*/ */
private static function maybe_update_promotions() { private static function schedule_cron_event() {
// Fetch promotions if they're not in the transient. if ( ! wp_next_scheduled( self::CRON_NAME ) ) {
if ( false !== get_transient( self::TRANSIENT_NAME ) ) { wp_schedule_event( time(), 'daily', self::CRON_NAME );
return;
} }
}
/**
* Fetch promotions from the API and store them in a transient.
*
* @return void
*/
public static function update_promotions() {
// Fetch promotions from the API. // Fetch promotions from the API.
$promotions = self::fetch_marketplace_promotions(); $promotions = self::fetch_marketplace_promotions();
set_transient( self::TRANSIENT_NAME, $promotions, self::TRANSIENT_LIFE_SPAN ); set_transient( self::TRANSIENT_NAME, $promotions, self::TRANSIENT_LIFE_SPAN );
@ -326,12 +345,24 @@ class WC_Admin_Marketplace_Promotions {
} }
/** /**
* Clear the scheduled action that was used to fetch promotions in WooCommerce 8.8. * When WooCommerce is disabled, clear the WP Cron event we use to fetch promotions.
* It's no longer needed as a transient is used to store the data. *
* @version 9.5.0
* *
* @return void * @return void
*/ */
public static function clear_scheduled_event() { public static function clear_cron_event() {
$timestamp = wp_next_scheduled( self::CRON_NAME );
wp_unschedule_event( $timestamp, self::CRON_NAME );
}
/**
* Clear deprecated scheduled action that was used to fetch promotions in WooCommerce 8.8.
* Replaced with a transient in WooCommerce 9.0.
*
* @return void
*/
public static function clear_deprecated_scheduled_event() {
if ( function_exists( 'as_unschedule_all_actions' ) ) { if ( function_exists( 'as_unschedule_all_actions' ) ) {
as_unschedule_all_actions( 'woocommerce_marketplace_fetch_promotions' ); as_unschedule_all_actions( 'woocommerce_marketplace_fetch_promotions' );
} }

View File

@ -26,7 +26,7 @@ class WC_Brands {
public function __construct() { public function __construct() {
$this->template_url = apply_filters( 'woocommerce_template_url', 'woocommerce/' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment $this->template_url = apply_filters( 'woocommerce_template_url', 'woocommerce/' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
add_action( 'plugins_loaded', array( $this, 'register_hooks' ), 2 ); add_action( 'plugins_loaded', array( $this, 'register_hooks' ), 11 );
$this->register_shortcodes(); $this->register_shortcodes();
} }

View File

@ -11,6 +11,10 @@
"license": "GPL-3.0+", "license": "GPL-3.0+",
"scripts": { "scripts": {
"build": "pnpm --if-present --workspace-concurrency=Infinity --stream --filter=\"$npm_package_name...\" '/^build:project:.*$/'", "build": "pnpm --if-present --workspace-concurrency=Infinity --stream --filter=\"$npm_package_name...\" '/^build:project:.*$/'",
"build:admin": "pnpm --if-present --workspace-concurrency=Infinity --stream --filter=\"@woocommerce/admin-library...\" --filter=\"$npm_package_name\" '/^build:project:.*$/'",
"build:blocks": "pnpm --if-present --workspace-concurrency=Infinity --stream --filter=\"@woocommerce/block-library...\" --filter=\"$npm_package_name\" '/^build:project:.*$/'",
"build:classic-assets": "pnpm --if-present --workspace-concurrency=Infinity --stream --filter=\"@woocommerce/classic-assets...\" --filter=\"$npm_package_name\" '/^build:project:.*$/'",
"build:zip": "./bin/build-zip.sh",
"build:project": "pnpm --if-present '/^build:project:.*$/'", "build:project": "pnpm --if-present '/^build:project:.*$/'",
"build:project:copy-assets:legacy": "wireit", "build:project:copy-assets:legacy": "wireit",
"build:project:copy-assets:admin": "wireit", "build:project:copy-assets:admin": "wireit",
@ -74,6 +78,9 @@
"test:unit:env:watch": "pnpm test:php:env:watch", "test:unit:env:watch": "pnpm test:php:env:watch",
"update-wp-env": "php ./tests/e2e-pw/bin/update-wp-env.php", "update-wp-env": "php ./tests/e2e-pw/bin/update-wp-env.php",
"watch:build": "pnpm --if-present --workspace-concurrency=Infinity --filter=\"$npm_package_name...\" --parallel '/^watch:build:project:.*$/'", "watch:build": "pnpm --if-present --workspace-concurrency=Infinity --filter=\"$npm_package_name...\" --parallel '/^watch:build:project:.*$/'",
"watch:build:admin": "pnpm --if-present --workspace-concurrency=Infinity --filter=\"@woocommerce/admin-library...\" --filter=\"$npm_package_name\" --parallel '/^watch:build:project:.*$/'",
"watch:build:blocks": "pnpm --if-present --workspace-concurrency=Infinity --filter=\"@woocommerce/block-library...\" --filter=\"$npm_package_name\" --parallel '/^watch:build:project:.*$/'",
"watch:build:classic-assets": "pnpm --if-present --workspace-concurrency=Infinity --filter=\"@woocommerce/classic-assets...\" --filter=\"$npm_package_name\" --parallel '/^watch:build:project:.*$/'",
"watch:build:project": "pnpm --if-present run '/^watch:build:project:.*$/'", "watch:build:project": "pnpm --if-present run '/^watch:build:project:.*$/'",
"watch:build:project:copy-assets": "wireit", "watch:build:project:copy-assets": "wireit",
"wp-env": "wp-env" "wp-env": "wp-env"
@ -483,7 +490,6 @@
"start": "test:perf:ci-setup" "start": "test:perf:ci-setup"
}, },
"events": [ "events": [
"pull_request",
"push" "push"
] ]
}, },
@ -504,6 +510,9 @@
"tests/metrics/**", "tests/metrics/**",
".wp-env.json" ".wp-env.json"
], ],
"testEnv": {
"start": "env:test"
},
"events": [ "events": [
"push" "push"
], ],

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes; namespace Automattic\WooCommerce\Blocks\BlockTypes;
@ -79,9 +80,9 @@ class AddToCartForm extends AbstractBlock {
/** /**
* Trigger the single product add to cart action for each product type. * Trigger the single product add to cart action for each product type.
* *
* @since 9.7.0 * @since 9.7.0
*/ */
do_action( 'woocommerce_' . $product->get_type() . '_add_to_cart' ); do_action( 'woocommerce_' . $product->get_type() . '_add_to_cart' );
$product = ob_get_clean(); $product = ob_get_clean();
@ -99,16 +100,30 @@ class AddToCartForm extends AbstractBlock {
$product = $this->add_is_descendent_of_single_product_block_hidden_input_to_product_form( $product, $is_descendent_of_single_product_block ); $product = $this->add_is_descendent_of_single_product_block_hidden_input_to_product_form( $product, $is_descendent_of_single_product_block );
} }
$classname = $attributes['className'] ?? '';
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$product_classname = $is_descendent_of_single_product_block ? 'product' : ''; $product_classname = $is_descendent_of_single_product_block ? 'product' : '';
$classes = implode(
' ',
array_filter(
array(
'wp-block-add-to-cart-form wc-block-add-to-cart-form',
esc_attr( $classes_and_styles['classes'] ),
esc_attr( $product_classname ),
)
)
);
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => $classes,
'style' => esc_attr( $classes_and_styles['styles'] ),
)
);
$form = sprintf( $form = sprintf(
'<div class="wp-block-add-to-cart-form wc-block-add-to-cart-form %1$s %2$s %3$s" style="%4$s">%5$s</div>', '<div %1$s>%2$s</div>',
esc_attr( $classes_and_styles['classes'] ), $wrapper_attributes,
esc_attr( $classname ),
esc_attr( $product_classname ),
esc_attr( $classes_and_styles['styles'] ),
$product $product
); );

View File

@ -37,6 +37,7 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
'font_size', 'font_size',
'padding', 'padding',
'text_color', 'text_color',
'extra_classes',
); );
/** /**
@ -272,10 +273,6 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
$classes[] = "has-{$attributes['contentAlign']}-content"; $classes[] = "has-{$attributes['contentAlign']}-content";
} }
if ( isset( $attributes['className'] ) ) {
$classes[] = $attributes['className'];
}
$global_style_classes = StyleAttributesUtils::get_classes_by_attributes( $attributes, $this->global_style_wrapper ); $global_style_classes = StyleAttributesUtils::get_classes_by_attributes( $attributes, $this->global_style_wrapper );
$classes[] = $global_style_classes; $classes[] = $global_style_classes;

View File

@ -414,12 +414,9 @@ class MiniCart extends AbstractBlock {
return ''; return '';
} }
$classes_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array( 'text_color', 'background_color', 'font_size', 'font_weight', 'font_family' ) ); $classes_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array( 'text_color', 'background_color', 'font_size', 'font_weight', 'font_family', 'extra_classes' ) );
$wrapper_classes = sprintf( 'wc-block-mini-cart wp-block-woocommerce-mini-cart %s', $classes_styles['classes'] ); $wrapper_classes = sprintf( 'wc-block-mini-cart wp-block-woocommerce-mini-cart %s', $classes_styles['classes'] );
if ( ! empty( $attributes['className'] ) ) { $wrapper_styles = $classes_styles['styles'];
$wrapper_classes .= ' ' . $attributes['className'];
}
$wrapper_styles = $classes_styles['styles'];
$icon_color = array_key_exists( 'iconColor', $attributes ) ? esc_attr( $attributes['iconColor']['color'] ) : 'currentColor'; $icon_color = array_key_exists( 'iconColor', $attributes ) ? esc_attr( $attributes['iconColor']['color'] ) : 'currentColor';
$product_count_color = array_key_exists( 'productCountColor', $attributes ) ? esc_attr( $attributes['productCountColor']['color'] ) : ''; $product_count_color = array_key_exists( 'productCountColor', $attributes ) ? esc_attr( $attributes['productCountColor']['color'] ) : '';

View File

@ -36,16 +36,11 @@ abstract class AbstractOrderConfirmationBlock extends AbstractBlock {
$order = $this->get_order(); $order = $this->get_order();
$permission = $this->get_view_order_permissions( $order ); $permission = $this->get_view_order_permissions( $order );
$block_content = $order ? $this->render_content( $order, $permission, $attributes, $content ) : $this->render_content_fallback(); $block_content = $order ? $this->render_content( $order, $permission, $attributes, $content ) : $this->render_content_fallback();
$classname = $attributes['className'] ?? '';
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
if ( ! empty( $classes_and_styles['classes'] ) ) {
$classname .= ' ' . $classes_and_styles['classes'];
}
return $block_content ? sprintf( return $block_content ? sprintf(
'<div class="wp-block-%5$s-%4$s wc-block-%4$s %1$s" style="%2$s">%3$s</div>', '<div class="wp-block-%5$s-%4$s wc-block-%4$s %1$s" style="%2$s">%3$s</div>',
esc_attr( trim( $classname ) ), esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classes_and_styles['styles'] ), esc_attr( $classes_and_styles['styles'] ),
$block_content, $block_content,
esc_attr( $this->block_name ), esc_attr( $this->block_name ),

View File

@ -2,6 +2,8 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation; namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/** /**
* Status class. * Status class.
*/ */
@ -26,7 +28,7 @@ class Status extends AbstractOrderConfirmationBlock {
*/ */
protected function render( $attributes, $content, $block ) { protected function render( $attributes, $content, $block ) {
$order = $this->get_order(); $order = $this->get_order();
$classname = $attributes['className'] ?? ''; $classname = StyleAttributesUtils::get_classes_by_attributes( $attributes, array( 'extra_classes' ) );
if ( isset( $attributes['align'] ) ) { if ( isset( $attributes['align'] ) ) {
$classname .= " align{$attributes['align']}"; $classname .= " align{$attributes['align']}";

View File

@ -100,8 +100,8 @@ class ProductButton extends AbstractBlock {
$ajax_add_to_cart_enabled = get_option( 'woocommerce_enable_ajax_add_to_cart' ) === 'yes'; $ajax_add_to_cart_enabled = get_option( 'woocommerce_enable_ajax_add_to_cart' ) === 'yes';
$is_ajax_button = $ajax_add_to_cart_enabled && ! $cart_redirect_after_add && $product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && $product->is_in_stock(); $is_ajax_button = $ajax_add_to_cart_enabled && ! $cart_redirect_after_add && $product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && $product->is_in_stock();
$html_element = $is_ajax_button ? 'button' : 'a'; $html_element = $is_ajax_button ? 'button' : 'a';
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); $styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
$classname = $attributes['className'] ?? ''; $classname = StyleAttributesUtils::get_classes_by_attributes( $attributes, array( 'extra_classes' ) );
$custom_width_classes = isset( $attributes['width'] ) ? 'has-custom-width wp-block-button__width-' . $attributes['width'] : ''; $custom_width_classes = isset( $attributes['width'] ) ? 'has-custom-width wp-block-button__width-' . $attributes['width'] : '';
$custom_align_classes = isset( $attributes['textAlign'] ) ? 'align-' . $attributes['textAlign'] : ''; $custom_align_classes = isset( $attributes['textAlign'] ) ? 'align-' . $attributes['textAlign'] : '';
$html_classes = implode( $html_classes = implode(

View File

@ -89,7 +89,7 @@ class ProductCategories extends AbstractDynamicBlock {
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes(
$attributes, $attributes,
array( 'line_height', 'text_color', 'font_size' ) array( 'line_height', 'text_color', 'font_size', 'extra_classes' )
); );
$classes = $this->get_container_classes( $attributes ) . ' ' . $classes_and_styles['classes']; $classes = $this->get_container_classes( $attributes ) . ' ' . $classes_and_styles['classes'];
@ -116,10 +116,6 @@ class ProductCategories extends AbstractDynamicBlock {
$classes[] = "align{$attributes['align']}"; $classes[] = "align{$attributes['align']}";
} }
if ( ! empty( $attributes['className'] ) ) {
$classes[] = $attributes['className'];
}
if ( $attributes['isDropdown'] ) { if ( $attributes['isDropdown'] ) {
$classes[] = 'is-dropdown'; $classes[] = 'is-dropdown';
} else { } else {

View File

@ -76,18 +76,15 @@ class ProductDetails extends AbstractBlock {
$tabs = $tabs_html->get_updated_html(); $tabs = $tabs_html->get_updated_html();
} }
$classname = $attributes['className'] ?? '';
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
return sprintf( return sprintf(
'<div class="wp-block-woocommerce-product-details %1$s %2$s"> '<div class="wp-block-woocommerce-product-details %1$s">
<div style="%3$s"> <div style="%2$s">
%4$s %3$s
</div> </div>
</div>', </div>',
esc_attr( $classes_and_styles['classes'] ), esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classname ),
esc_attr( $classes_and_styles['styles'] ), esc_attr( $classes_and_styles['styles'] ),
$tabs $tabs
); );

View File

@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils; use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils; use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/** /**
* ProductGallery class. * ProductGallery class.
@ -125,7 +126,7 @@ class ProductGallery extends AbstractBlock {
} }
$number_of_thumbnails = $block->attributes['thumbnailsNumberOfThumbnails'] ?? 0; $number_of_thumbnails = $block->attributes['thumbnailsNumberOfThumbnails'] ?? 0;
$classname = $attributes['className'] ?? ''; $classname = StyleAttributesUtils::get_classes_by_attributes( $attributes, array( 'extra_classes' ) );
$dialog = isset( $attributes['mode'] ) && 'full' !== $attributes['mode'] ? $this->render_dialog() : ''; $dialog = isset( $attributes['mode'] ) && 'full' !== $attributes['mode'] ? $this->render_dialog() : '';
$product_gallery_first_image = ProductGalleryUtils::get_product_gallery_image_ids( $product, 1 ); $product_gallery_first_image = ProductGalleryUtils::get_product_gallery_image_ids( $product, 1 );
$product_gallery_first_image_id = reset( $product_gallery_first_image ); $product_gallery_first_image_id = reset( $product_gallery_first_image );

View File

@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes; namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils; use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/** /**
* ProductGalleryPager class. * ProductGalleryPager class.
@ -55,7 +56,7 @@ class ProductGalleryPager extends AbstractBlock {
} }
$number_of_thumbnails = $block->context['thumbnailsNumberOfThumbnails'] ?? 0; $number_of_thumbnails = $block->context['thumbnailsNumberOfThumbnails'] ?? 0;
$classname = $attributes['className'] ?? ''; $classname = StyleAttributesUtils::get_classes_by_attributes( $attributes, array( 'extra_classes' ) );
$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => trim( $classname ) ) ); $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => trim( $classname ) ) );
$post_id = $block->context['postId'] ?? ''; $post_id = $block->context['postId'] ?? '';
$product = wc_get_product( $post_id ); $product = wc_get_product( $post_id );

View File

@ -208,13 +208,29 @@ class ProductImage extends AbstractBlock {
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : ''; $post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$product = wc_get_product( $post_id ); $product = wc_get_product( $post_id );
$classes = implode(
' ',
array_filter(
array(
'wc-block-components-product-image wc-block-grid__product-image',
esc_attr( $classes_and_styles['classes'] ),
)
)
);
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => $classes,
'style' => esc_attr( $classes_and_styles['styles'] ),
)
);
if ( $product ) { if ( $product ) {
return sprintf( return sprintf(
'<div class="wc-block-components-product-image wc-block-grid__product-image %1$s" style="%2$s"> '<div %1$s>
%3$s %2$s
</div>', </div>',
esc_attr( $classes_and_styles['classes'] ), $wrapper_attributes,
esc_attr( $classes_and_styles['styles'] ),
$this->render_anchor( $this->render_anchor(
$product, $product,
$this->render_on_sale_badge( $product, $parsed_attributes ), $this->render_on_sale_badge( $product, $parsed_attributes ),

View File

@ -1,6 +1,8 @@
<?php <?php
namespace Automattic\WooCommerce\Blocks\BlockTypes; namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/** /**
* ProductImageGallery class. * ProductImageGallery class.
*/ */
@ -67,7 +69,7 @@ class ProductImageGallery extends AbstractBlock {
$product_image_gallery_html = ob_get_clean(); $product_image_gallery_html = ob_get_clean();
$product = $previous_product; $product = $previous_product;
$classname = $attributes['className'] ?? ''; $classname = StyleAttributesUtils::get_classes_by_attributes( $attributes, array( 'extra_classes' ) );
return sprintf( return sprintf(
'<div class="wp-block-woocommerce-product-image-gallery %1$s">%2$s %3$s</div>', '<div class="wp-block-woocommerce-product-image-gallery %1$s">%2$s %3$s</div>',
esc_attr( $classname ), esc_attr( $classname ),

View File

@ -202,13 +202,29 @@ class ProductRating extends AbstractBlock {
10 10
); );
$classes = implode(
' ',
array_filter(
array(
'wc-block-components-product-rating wc-block-grid__product-rating',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $styles_and_classes['classes'] ),
)
)
);
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => $classes,
'style' => esc_attr( $styles_and_classes['styles'] ?? '' ),
)
);
return sprintf( return sprintf(
'<div class="wc-block-components-product-rating wc-block-grid__product-rating %1$s %2$s" style="%3$s"> '<div %1$s>
%4$s %2$s
</div>', </div>',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ), $wrapper_attributes,
esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ),
$rating_html $rating_html
); );
} }

View File

@ -132,7 +132,7 @@ class ProductRatingCounter extends AbstractBlock {
* @param int $count Total number of ratings. * @param int $count Total number of ratings.
* @return string * @return string
*/ */
$filter_rating_html = function( $html, $rating, $count ) use ( $post_id, $product_rating, $product_reviews_count, $is_descendent_of_single_product_block, $is_descendent_of_single_product_template ) { $filter_rating_html = function ( $html, $rating, $count ) use ( $post_id, $product_rating, $product_reviews_count, $is_descendent_of_single_product_block, $is_descendent_of_single_product_template ) {
$product_permalink = get_permalink( $post_id ); $product_permalink = get_permalink( $post_id );
$reviews_count = $count; $reviews_count = $count;
$average_rating = $rating; $average_rating = $rating;
@ -193,17 +193,32 @@ class ProductRatingCounter extends AbstractBlock {
10 10
); );
$classes = implode(
' ',
array_filter(
array(
'wc-block-components-product-rating-counter wc-block-grid__product-rating-counter',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $styles_and_classes['classes'] ),
)
)
);
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => $classes,
'style' => esc_attr( $styles_and_classes['styles'] ?? '' ),
)
);
return sprintf( return sprintf(
'<div class="wc-block-components-product-rating-counter wc-block-grid__product-rating-counter %1$s %2$s" style="%3$s"> '<div %1$s>
%4$s %2$s
</div>', </div>',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ), $wrapper_attributes,
esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ),
$rating_html $rating_html
); );
} }
return ''; return '';
} }
} }

View File

@ -149,13 +149,29 @@ class ProductRatingStars extends AbstractBlock {
10 10
); );
$classes = implode(
' ',
array_filter(
array(
'wc-block-components-product-rating wc-block-grid__product-rating',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $styles_and_classes['classes'] ),
)
)
);
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => $classes,
'style' => esc_attr( $styles_and_classes['styles'] ?? '' ),
)
);
return sprintf( return sprintf(
'<div class="wc-block-components-product-rating wc-block-grid__product-rating %1$s %2$s" style="%3$s"> '<div %1$s>
%4$s %2$s
</div>', </div>',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ), $wrapper_attributes,
esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ),
$rating_html $rating_html
); );
} }

View File

@ -54,7 +54,6 @@ class ProductResultsCount extends AbstractBlock {
'wc-block-product-results-count', 'wc-block-product-results-count',
'wp-block-woocommerce-product-results-count', 'wp-block-woocommerce-product-results-count',
), ),
isset( $attributes['className'] ) ? array( $attributes['className'] ) : array(),
); );
$p->set_attribute( 'class', implode( ' ', $classes ) ); $p->set_attribute( 'class', implode( ' ', $classes ) );
$p->set_attribute( 'style', $parsed_style_attributes['styles'] ); $p->set_attribute( 'style', $parsed_style_attributes['styles'] );

View File

@ -2,6 +2,8 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes; namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/** /**
* ProductReviews class. * ProductReviews class.
*/ */
@ -40,13 +42,11 @@ class ProductReviews extends AbstractBlock {
$reviews = ob_get_clean(); $reviews = ob_get_clean();
$classname = $attributes['className'] ?? '';
return sprintf( return sprintf(
'<div class="wp-block-woocommerce-product-reviews %1$s"> '<div class="wp-block-woocommerce-product-reviews %1$s">
%2$s %2$s
</div>', </div>',
esc_attr( $classname ), StyleAttributesUtils::get_classes_by_attributes( $attributes, array( 'extra_classes' ) ),
$reviews $reviews
); );
} }

View File

@ -110,8 +110,9 @@ class ProductSaleBadge extends AbstractBlock {
return null; return null;
} }
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
$classname = isset( $attributes['className'] ) ? $attributes['className'] : '';
$classname = StyleAttributesUtils::get_classes_by_attributes( $attributes, array( 'extra_classes' ) );
$align = isset( $attributes['align'] ) ? $attributes['align'] : ''; $align = isset( $attributes['align'] ) ? $attributes['align'] : '';

View File

@ -101,7 +101,6 @@ class ProductStockIndicator extends AbstractBlock {
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$classnames = isset( $classes_and_styles['classes'] ) ? ' ' . $classes_and_styles['classes'] . ' ' : ''; $classnames = isset( $classes_and_styles['classes'] ) ? ' ' . $classes_and_styles['classes'] . ' ' : '';
$classnames .= isset( $attributes['className'] ) ? ' ' . $attributes['className'] . ' ' : '';
$classnames .= ! $is_in_stock ? ' wc-block-components-product-stock-indicator--out-of-stock ' : ''; $classnames .= ! $is_in_stock ? ' wc-block-components-product-stock-indicator--out-of-stock ' : '';
$classnames .= $is_in_stock ? ' wc-block-components-product-stock-indicator--in-stock ' : ''; $classnames .= $is_in_stock ? ' wc-block-components-product-stock-indicator--in-stock ' : '';
$classnames .= $is_low_stock ? ' wc-block-components-product-stock-indicator--low-stock ' : ''; $classnames .= $is_low_stock ? ' wc-block-components-product-stock-indicator--low-stock ' : '';

View File

@ -687,6 +687,25 @@ class StyleAttributesUtils {
return self::EMPTY_STYLE; return self::EMPTY_STYLE;
} }
/**
* Get extra CSS classes from attributes.
*
* @param array $attributes Block attributes.
* @return array
*/
public static function get_classes_from_attributes( $attributes ) {
$extra_css_classes = $attributes['className'] ?? '';
if ( '' !== $extra_css_classes ) {
return array(
'class' => $extra_css_classes,
'style' => null,
);
}
return self::EMPTY_STYLE;
}
/** /**
* Get classes and styles from attributes. * Get classes and styles from attributes.
* *
@ -717,6 +736,7 @@ class StyleAttributesUtils {
'text_color' => self::get_text_color_class_and_style( $attributes ), 'text_color' => self::get_text_color_class_and_style( $attributes ),
'text_decoration' => self::get_text_decoration_class_and_style( $attributes ), 'text_decoration' => self::get_text_decoration_class_and_style( $attributes ),
'text_transform' => self::get_text_transform_class_and_style( $attributes ), 'text_transform' => self::get_text_transform_class_and_style( $attributes ),
'extra_classes' => self::get_classes_from_attributes( $attributes ),
); );
if ( ! empty( $properties ) ) { if ( ! empty( $properties ) ) {

View File

@ -25,11 +25,6 @@ class Brands {
return; return;
} }
// If the WooCommerce Brands plugin is activated via the WP CLI using the '--skip-plugins' flag, deactivate it here.
if ( function_exists( 'wc_brands_init' ) ) {
remove_action( 'plugins_loaded', 'wc_brands_init', 1 );
}
include_once WC_ABSPATH . 'includes/class-wc-brands.php'; include_once WC_ABSPATH . 'includes/class-wc-brands.php';
include_once WC_ABSPATH . 'includes/class-wc-brands-coupons.php'; include_once WC_ABSPATH . 'includes/class-wc-brands-coupons.php';
include_once WC_ABSPATH . 'includes/class-wc-brands-brand-settings-manager.php'; include_once WC_ABSPATH . 'includes/class-wc-brands-brand-settings-manager.php';
@ -58,4 +53,19 @@ class Brands {
} }
return ( $assignment <= 6 ); // Considering 5% of the 0-120 range. return ( $assignment <= 6 ); // Considering 5% of the 0-120 range.
} }
/**
* If WooCommerce Brands gets activated forcibly, without WooCommerce active (e.g. via '--skip-plugins'),
* remove WooCommerce Brands initialization functions early on in the 'plugins_loaded' timeline.
*/
public static function prepare() {
if ( ! self::is_enabled() ) {
return;
}
if ( function_exists( 'wc_brands_init' ) ) {
remove_action( 'plugins_loaded', 'wc_brands_init', 1 );
}
}
} }

View File

@ -4,7 +4,8 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\ComingSoon; namespace Automattic\WooCommerce\Internal\ComingSoon;
use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Utilities\FeaturesUtil;
/** /**
* Adds hooks to add a badge to the WordPress admin bar showing site visibility. * Adds hooks to add a badge to the WordPress admin bar showing site visibility.
@ -30,7 +31,7 @@ class ComingSoonAdminBarBadge {
*/ */
public function site_visibility_badge( $wp_admin_bar ) { public function site_visibility_badge( $wp_admin_bar ) {
// Early exit if LYS feature is disabled. // Early exit if LYS feature is disabled.
if ( ! Features::is_enabled( 'launch-your-store' ) ) { if ( ! FeaturesUtil::feature_is_enabled( 'site_visibility_badge' ) ) {
return; return;
} }
@ -68,7 +69,7 @@ class ComingSoonAdminBarBadge {
*/ */
public function output_css() { public function output_css() {
// Early exit if LYS feature is disabled. // Early exit if LYS feature is disabled.
if ( ! Features::is_enabled( 'launch-your-store' ) ) { if ( ! FeaturesUtil::feature_is_enabled( 'site_visibility_badge' ) ) {
return; return;
} }

View File

@ -39,7 +39,7 @@ class ComingSoonRequestHandler {
* @internal * @internal
* *
* @param string $template The path to the previously determined template. * @param string $template The path to the previously determined template.
* @return string|null The path to the 'coming soon' template or null to prevent further template loading in FSE themes. * @return string The path to the 'coming soon' template or any empty string to prevent further template loading in FSE themes.
*/ */
public function handle_template_include( $template ) { public function handle_template_include( $template ) {
global $wp; global $wp;
@ -91,8 +91,8 @@ class ComingSoonRequestHandler {
} }
if ( $is_fse_theme ) { if ( $is_fse_theme ) {
// Since we've already rendered a template, return null to ensure no other template is rendered. // Since we've already rendered a template, return empty string to ensure no other template is rendered.
return null; return '';
} else { } else {
// In non-FSE themes, other templates will still be rendered. // In non-FSE themes, other templates will still be rendered.
// We need to exit to prevent further processing. // We need to exit to prevent further processing.

View File

@ -164,7 +164,7 @@ class FeaturesController {
private function get_feature_definitions() { private function get_feature_definitions() {
if ( empty( $this->features ) ) { if ( empty( $this->features ) ) {
$legacy_features = array( $legacy_features = array(
'analytics' => array( 'analytics' => array(
'name' => __( 'Analytics', 'woocommerce' ), 'name' => __( 'Analytics', 'woocommerce' ),
'description' => __( 'Enable WooCommerce Analytics', 'woocommerce' ), 'description' => __( 'Enable WooCommerce Analytics', 'woocommerce' ),
'option_key' => Analytics::TOGGLE_OPTION_NAME, 'option_key' => Analytics::TOGGLE_OPTION_NAME,
@ -173,7 +173,7 @@ class FeaturesController {
'disable_ui' => false, 'disable_ui' => false,
'is_legacy' => true, 'is_legacy' => true,
), ),
'product_block_editor' => array( 'product_block_editor' => array(
'name' => __( 'New product editor', 'woocommerce' ), 'name' => __( 'New product editor', 'woocommerce' ),
'description' => __( 'Try the new product editor (Beta)', 'woocommerce' ), 'description' => __( 'Try the new product editor (Beta)', 'woocommerce' ),
'is_experimental' => true, 'is_experimental' => true,
@ -194,13 +194,13 @@ class FeaturesController {
return $string; return $string;
}, },
), ),
'cart_checkout_blocks' => array( 'cart_checkout_blocks' => array(
'name' => __( 'Cart & Checkout Blocks', 'woocommerce' ), 'name' => __( 'Cart & Checkout Blocks', 'woocommerce' ),
'description' => __( 'Optimize for faster checkout', 'woocommerce' ), 'description' => __( 'Optimize for faster checkout', 'woocommerce' ),
'is_experimental' => false, 'is_experimental' => false,
'disable_ui' => true, 'disable_ui' => true,
), ),
'marketplace' => array( 'marketplace' => array(
'name' => __( 'Marketplace', 'woocommerce' ), 'name' => __( 'Marketplace', 'woocommerce' ),
'description' => __( 'description' => __(
'New, faster way to find extensions and themes for your WooCommerce store', 'New, faster way to find extensions and themes for your WooCommerce store',
@ -213,7 +213,7 @@ class FeaturesController {
), ),
// Marked as a legacy feature to avoid compatibility checks, which aren't really relevant to this feature. // Marked as a legacy feature to avoid compatibility checks, which aren't really relevant to this feature.
// https://github.com/woocommerce/woocommerce/pull/39701#discussion_r1376976959. // https://github.com/woocommerce/woocommerce/pull/39701#discussion_r1376976959.
'order_attribution' => array( 'order_attribution' => array(
'name' => __( 'Order Attribution', 'woocommerce' ), 'name' => __( 'Order Attribution', 'woocommerce' ),
'description' => __( 'description' => __(
'Enable this feature to track and credit channels and campaigns that contribute to orders on your site', 'Enable this feature to track and credit channels and campaigns that contribute to orders on your site',
@ -224,7 +224,19 @@ class FeaturesController {
'is_legacy' => true, 'is_legacy' => true,
'is_experimental' => false, 'is_experimental' => false,
), ),
'hpos_fts_indexes' => array( 'site_visibility_badge' => array(
'name' => __( 'Site visibility badge', 'woocommerce' ),
'description' => __(
'Enable the site visibility badge in the WordPress admin bar',
'woocommerce'
),
'enabled_by_default' => true,
'disable_ui' => false,
'is_legacy' => true,
'is_experimental' => false,
'disabled' => false,
),
'hpos_fts_indexes' => array(
'name' => __( 'HPOS Full text search indexes', 'woocommerce' ), 'name' => __( 'HPOS Full text search indexes', 'woocommerce' ),
'description' => __( 'description' => __(
'Create and use full text search indexes for orders. This feature only works with high-performance order storage.', 'Create and use full text search indexes for orders. This feature only works with high-performance order storage.',
@ -235,7 +247,7 @@ class FeaturesController {
'is_legacy' => true, 'is_legacy' => true,
'option_key' => CustomOrdersTableController::HPOS_FTS_INDEX_OPTION, 'option_key' => CustomOrdersTableController::HPOS_FTS_INDEX_OPTION,
), ),
'remote_logging' => array( 'remote_logging' => array(
'name' => __( 'Remote Logging', 'woocommerce' ), 'name' => __( 'Remote Logging', 'woocommerce' ),
'description' => __( 'description' => __(
'Enable this feature to log errors and related data to Automattic servers for debugging purposes and to improve WooCommerce', 'Enable this feature to log errors and related data to Automattic servers for debugging purposes and to improve WooCommerce',

View File

@ -66,7 +66,8 @@ class Packages {
* @since 3.7.0 * @since 3.7.0
*/ */
public static function init() { public static function init() {
add_action( 'plugins_loaded', array( __CLASS__, 'on_init' ), 0 ); add_action( 'plugins_loaded', array( __CLASS__, 'prepare_packages' ), -100 );
add_action( 'plugins_loaded', array( __CLASS__, 'on_init' ), 10 );
// Prevent plugins already merged into WooCommerce core from getting activated as standalone plugins. // Prevent plugins already merged into WooCommerce core from getting activated as standalone plugins.
add_action( 'activate_plugin', array( __CLASS__, 'deactivate_merged_plugins' ) ); add_action( 'activate_plugin', array( __CLASS__, 'deactivate_merged_plugins' ) );
@ -149,6 +150,18 @@ class Packages {
return array_key_exists( $package, self::get_enabled_packages() ); return array_key_exists( $package, self::get_enabled_packages() );
} }
/**
* Prepare merged packages for initialization.
* Especially useful when running actions early in the 'plugins_loaded' timeline.
*/
public static function prepare_packages() {
foreach ( self::get_enabled_packages() as $package_name => $package_class ) {
if ( method_exists( $package_class, 'prepare' ) ) {
call_user_func( array( $package_class, 'prepare' ) );
}
}
}
/** /**
* Deactivates merged feature plugins. * Deactivates merged feature plugins.
* *

View File

@ -12,7 +12,7 @@
* *
* @see https://docs.woocommerce.com/document/template-structure/ * @see https://docs.woocommerce.com/document/template-structure/
* @package WooCommerce\Templates * @package WooCommerce\Templates
* @version 8.6.0 * @version 9.5.0
*/ */
if ( ! defined( 'ABSPATH' ) ) { if ( ! defined( 'ABSPATH' ) ) {
@ -26,7 +26,7 @@ if ( ! $notices ) {
?> ?>
<?php foreach ( $notices as $notice ) : ?> <?php foreach ( $notices as $notice ) : ?>
<div class="wc-block-components-notice-banner is-info"<?php echo wc_get_notice_data_attr( $notice ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> role="alert"> <div class="wc-block-components-notice-banner is-info"<?php echo wc_get_notice_data_attr( $notice ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> role="status">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false">
<path d="M12 3.2c-4.8 0-8.8 3.9-8.8 8.8 0 4.8 3.9 8.8 8.8 8.8 4.8 0 8.8-3.9 8.8-8.8 0-4.8-4-8.8-8.8-8.8zm0 16c-4 0-7.2-3.3-7.2-7.2C4.8 8 8 4.8 12 4.8s7.2 3.3 7.2 7.2c0 4-3.2 7.2-7.2 7.2zM11 17h2v-6h-2v6zm0-8h2V7h-2v2z"></path> <path d="M12 3.2c-4.8 0-8.8 3.9-8.8 8.8 0 4.8 3.9 8.8 8.8 8.8 4.8 0 8.8-3.9 8.8-8.8 0-4.8-4-8.8-8.8-8.8zm0 16c-4 0-7.2-3.3-7.2-7.2C4.8 8 8 4.8 12 4.8s7.2 3.3 7.2 7.2c0 4-3.2 7.2-7.2 7.2zM11 17h2v-6h-2v6zm0-8h2V7h-2v2z"></path>
</svg> </svg>

View File

@ -33,6 +33,18 @@ config = {
'**/merchant/launch-your-store.spec.js', '**/merchant/launch-your-store.spec.js',
'**/merchant/lost-password.spec.js', '**/merchant/lost-password.spec.js',
'**/merchant/order-bulk-edit.spec.js', '**/merchant/order-bulk-edit.spec.js',
'**/merchant/product-images.spec.js',
'**/merchant/product-import-csv.spec.js',
'**/merchant/product-linked-products.spec.js',
'**/merchant/product-reviews.spec.js',
'**/merchant/product-search.spec.js',
'**/merchant/product-settings.spec.js',
'**/merchant/settings-general.spec.js',
'**/merchant/settings-shipping.spec.js',
'**/merchant/settings-tax.spec.js',
'**/merchant/settings-woo-com.spec.js',
'**/merchant/users-create.spec.js',
'**/merchant/users-manage.spec.js',
], ],
grepInvert: /@skip-on-default-wpcom/, grepInvert: /@skip-on-default-wpcom/,
}, },

View File

@ -246,6 +246,14 @@ test.describe(
'wp-admin/edit.php?post_type=product&page=product-reviews' 'wp-admin/edit.php?post_type=product&page=product-reviews'
); );
// Handle notice if present
await page.addLocatorHandler(
page.getByRole( 'link', { name: 'Dismiss' } ),
async () => {
await page.getByRole( 'link', { name: 'Dismiss' } ).click();
}
);
const reviewRow = page.locator( `#comment-${ review.id }` ); const reviewRow = page.locator( `#comment-${ review.id }` );
await reviewRow.hover(); await reviewRow.hover();
await reviewRow.getByRole( 'button', { name: 'Reply' } ).click(); await reviewRow.getByRole( 'button', { name: 'Reply' } ).click();
@ -256,7 +264,12 @@ test.describe(
const replyText = `Thank you for your feedback! (replied ${ Date.now() })`; const replyText = `Thank you for your feedback! (replied ${ Date.now() })`;
await replyTextArea.fill( replyText ); await replyTextArea.fill( replyText );
await page.locator( 'button.save.button.button-primary' ).click(); await page
.getByRole( 'cell', { name: 'Reply to Comment' } )
.getByRole( 'button', { name: 'Reply', exact: true } )
.click();
await expect( replyTextArea ).toBeHidden();
const productLink = await reviewRow const productLink = await reviewRow
.locator( 'a.comments-view-item-link' ) .locator( 'a.comments-view-item-link' )

View File

@ -3,7 +3,13 @@ const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default;
test.describe( test.describe(
'WooCommerce woo.com Settings', 'WooCommerce woo.com Settings',
{ tag: [ '@services', '@skip-on-default-pressable' ] }, {
tag: [
'@services',
'@skip-on-default-pressable',
'@skip-on-default-wpcom',
],
},
() => { () => {
test.use( { storageState: process.env.ADMINSTATE } ); test.use( { storageState: process.env.ADMINSTATE } );

View File

@ -65,7 +65,11 @@ async function userDeletionTest( page, username ) {
page.getByRole( 'heading', { name: 'Delete Users' } ) page.getByRole( 'heading', { name: 'Delete Users' } )
).toBeVisible(); ).toBeVisible();
await expect( page.getByText( `${ username }` ) ).toBeVisible(); await expect(
page
.getByText( 'Delete Users You have' )
.getByText( `${ username }` )
).toBeVisible();
await page.getByRole( 'button', { name: 'Confirm Deletion' } ).click(); await page.getByRole( 'button', { name: 'Confirm Deletion' } ).click();
} ); } );