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
title "##[group]Publish results to CodeVitals"
COMMITTED_AT=$(git show -s $GITHUB_SHA --format="%cI") 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 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,17 +18,52 @@ 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"> <>
<ul
className="theme-card__color-palettes"
aria-label={ __( 'Color palettes', 'woocommerce' ) }
aria-describedby={ descriptionId }
>
{ colorPalettes.map( ( colorPalette ) => ( { colorPalettes.map( ( colorPalette ) => (
<li <li
key={ colorPalette.title } key={ colorPalette.title }
aria-label={ colorPalette.title }
style={ { style={ {
background: background:
'linear-gradient(to right, ' + 'linear-gradient(to right, ' +
@ -34,9 +77,12 @@ export const ColorPalettes = ( {
' 100%' + ' 100%' +
')', ')',
} } } }
></li> />
) ) } ) ) }
{ extra } { renderMore() }
</ul> </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,8 +19,7 @@ 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,
} ) => { } ) => {
@ -53,9 +52,7 @@ test.describe( 'Product Collection', () => {
]; ];
await expect( pageObject.products ).toHaveCount( 5 ); await expect( pageObject.products ).toHaveCount( 5 );
await expect( pageObject.productTitles ).toHaveText( await expect( pageObject.productTitles ).toHaveText( topRatedProducts );
topRatedProducts
);
await pageObject.publishAndGoToFrontend(); await pageObject.publishAndGoToFrontend();
@ -102,9 +99,7 @@ test.describe( 'Product Collection', () => {
]; ];
await expect( pageObject.products ).toHaveCount( 5 ); await expect( pageObject.products ).toHaveCount( 5 );
await expect( pageObject.productTitles ).toHaveText( await expect( pageObject.productTitles ).toHaveText( onSaleProducts );
onSaleProducts
);
await pageObject.publishAndGoToFrontend(); await pageObject.publishAndGoToFrontend();
@ -124,9 +119,7 @@ test.describe( 'Product Collection', () => {
]; ];
await expect( pageObject.products ).toHaveCount( 4 ); await expect( pageObject.products ).toHaveCount( 4 );
await expect( pageObject.productTitles ).toHaveText( await expect( pageObject.productTitles ).toHaveText( featuredProducts );
featuredProducts
);
await pageObject.publishAndGoToFrontend(); await pageObject.publishAndGoToFrontend();
@ -217,5 +210,4 @@ test.describe( 'Product Collection', () => {
await expect( input ).toBeHidden(); await expect( input ).toBeHidden();
} ); } );
} ); } );
} );
} ); } );

View File

@ -86,8 +86,7 @@ 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'
@ -116,5 +115,4 @@ test.describe( 'Compatibility Layer with Product Collection block', () => {
await expect( hooks.first() ).toHaveText( scenario.content ); 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

@ -19,28 +19,21 @@ const test = base.extend< { pageObject: ProductCollectionPage } >( {
}, },
} ); } );
test.describe( 'Product Collection', () => { test.describe( 'Product Collection: Inspector Controls', () => {
test.describe( 'Inspector Controls', () => {
test( 'Reflects the correct number of columns according to sidebar settings', async ( { test( 'Reflects the correct number of columns according to sidebar settings', async ( {
pageObject, pageObject,
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock(); await pageObject.createNewPostAndInsertBlock();
await pageObject.setNumberOfColumns( 2 ); await pageObject.setNumberOfColumns( 2 );
await expect( pageObject.productTemplate ).toHaveClass( await expect( pageObject.productTemplate ).toHaveClass( /columns-2/ );
/columns-2/
);
await pageObject.setNumberOfColumns( 4 ); await pageObject.setNumberOfColumns( 4 );
await expect( pageObject.productTemplate ).toHaveClass( await expect( pageObject.productTemplate ).toHaveClass( /columns-4/ );
/columns-4/
);
await pageObject.publishAndGoToFrontend(); await pageObject.publishAndGoToFrontend();
await expect( pageObject.productTemplate ).toHaveClass( await expect( pageObject.productTemplate ).toHaveClass( /columns-4/ );
/columns-4/
);
} ); } );
test( 'Order By - sort products by title in descending order correctly', async ( { test( 'Order By - sort products by title in descending order correctly', async ( {
@ -104,9 +97,9 @@ test.describe( 'Product Collection', () => {
} ) => { } ) => {
await pageObject.createNewPostAndInsertBlock(); await pageObject.createNewPostAndInsertBlock();
await pageObject.addFilter( 'Show Hand-picked Products' ); await pageObject.addFilter( 'Show Hand-picked' );
const filterName = 'Hand-picked Products'; const filterName = 'Hand-picked';
await pageObject.setFilterComboboxValue( filterName, [ 'Album' ] ); await pageObject.setFilterComboboxValue( filterName, [ 'Album' ] );
await expect( pageObject.products ).toHaveCount( 1 ); await expect( pageObject.products ).toHaveCount( 1 );
@ -144,9 +137,7 @@ test.describe( 'Product Collection', () => {
const filterName = 'Product categories'; const filterName = 'Product categories';
await pageObject.addFilter( 'Show product categories' ); await pageObject.addFilter( 'Show product categories' );
await pageObject.setFilterComboboxValue( filterName, [ await pageObject.setFilterComboboxValue( filterName, [ 'Clothing' ] );
'Clothing',
] );
await expect( pageObject.productTitles ).toHaveText( [ await expect( pageObject.productTitles ).toHaveText( [
'Logo Collection', 'Logo Collection',
] ); ] );
@ -560,9 +551,7 @@ test.describe( 'Product Collection', () => {
await page.getByLabel( 'Toggle block inserter' ).click(); await page.getByLabel( 'Toggle block inserter' ).click();
await page.getByRole( 'tab', { name: 'Patterns' } ).click(); await page.getByRole( 'tab', { name: 'Patterns' } ).click();
await page await page.getByPlaceholder( 'Search' ).fill( 'product filters' );
.getByPlaceholder( 'Search' )
.fill( 'product filters' );
await page.getByLabel( 'Product Filters' ).click(); await page.getByLabel( 'Product Filters' ).click();
const postId = await editor.publishPost(); const postId = await editor.publishPost();
@ -611,9 +600,7 @@ test.describe( 'Product Collection', () => {
await page.getByLabel( 'Toggle block inserter' ).click(); await page.getByLabel( 'Toggle block inserter' ).click();
await page.getByRole( 'tab', { name: 'Patterns' } ).click(); await page.getByRole( 'tab', { name: 'Patterns' } ).click();
await page await page.getByPlaceholder( 'Search' ).fill( 'product filters' );
.getByPlaceholder( 'Search' )
.fill( 'product filters' );
await page.getByLabel( 'Product Filters' ).click(); await page.getByLabel( 'Product Filters' ).click();
await expect( pageObject.products ).toHaveCount( 2 ); await expect( pageObject.products ).toHaveCount( 2 );
@ -635,5 +622,4 @@ test.describe( 'Product Collection', () => {
await expect( pageObject.products ).toHaveCount( 1 ); await expect( pageObject.products ).toHaveCount( 1 );
} ); } );
} ); } );
} );
} ); } );

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,43 +744,44 @@ 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, templateTitle,
slug, slug,
frontendPage, frontendPage,
legacyBlockName, legacyBlockName,
expectedProductsCount, expectedProductsCount,
} of Object.values( templates ) ) { } ) => {
test.describe( `${ templateTitle } template`, () => { test.describe( `${ templateTitle } template`, () => {
test( 'Product Collection block matches with classic template block', async ( { test( 'Product Collection block matches with classic template block', async ( {
pageObject, pageObject,
@ -843,6 +841,7 @@ test.describe( 'Product Collection', () => {
} ); } );
} ); } );
} }
);
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;
@ -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' : '';
$form = sprintf( $classes = implode(
'<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>', ' ',
array_filter(
array(
'wp-block-add-to-cart-form wc-block-add-to-cart-form',
esc_attr( $classes_and_styles['classes'] ), esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classname ),
esc_attr( $product_classname ), esc_attr( $product_classname ),
esc_attr( $classes_and_styles['styles'] ), )
)
);
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => $classes,
'style' => esc_attr( $classes_and_styles['styles'] ),
)
);
$form = sprintf(
'<div %1$s>%2$s</div>',
$wrapper_attributes,
$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,11 +414,8 @@ 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_classes .= ' ' . $attributes['className'];
}
$wrapper_styles = $classes_styles['styles']; $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';

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
); );
return sprintf( $classes = implode(
'<div class="wc-block-components-product-rating wc-block-grid__product-rating %1$s %2$s" style="%3$s"> ' ',
%4$s array_filter(
</div>', array(
'wc-block-components-product-rating wc-block-grid__product-rating',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ), esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $styles_and_classes['classes'] ), esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ), )
)
);
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => $classes,
'style' => esc_attr( $styles_and_classes['styles'] ?? '' ),
)
);
return sprintf(
'<div %1$s>
%2$s
</div>',
$wrapper_attributes,
$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
); );
return sprintf( $classes = implode(
'<div class="wc-block-components-product-rating-counter wc-block-grid__product-rating-counter %1$s %2$s" style="%3$s"> ' ',
%4$s array_filter(
</div>', array(
'wc-block-components-product-rating-counter wc-block-grid__product-rating-counter',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ), esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $styles_and_classes['classes'] ), esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ), )
)
);
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => $classes,
'style' => esc_attr( $styles_and_classes['styles'] ?? '' ),
)
);
return sprintf(
'<div %1$s>
%2$s
</div>',
$wrapper_attributes,
$rating_html $rating_html
); );
} }
return ''; return '';
} }
} }

View File

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

@ -224,6 +224,18 @@ class FeaturesController {
'is_legacy' => true, 'is_legacy' => true,
'is_experimental' => false, 'is_experimental' => false,
), ),
'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( 'hpos_fts_indexes' => array(
'name' => __( 'HPOS Full text search indexes', 'woocommerce' ), 'name' => __( 'HPOS Full text search indexes', 'woocommerce' ),
'description' => __( 'description' => __(

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();
} ); } );