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 }}...'
build: ${{ ( github.ref_type == 'tag' && 'false' ) || matrix.projectName }}
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 }}'
- 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 }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
build:
name: Check Asset Sizes
@ -42,6 +45,8 @@ jobs:
uses: ./.github/actions/setup-woocommerce-monorepo
with:
php-version: false
install: '@woocommerce/plugin-woocommerce...'
build: '@woocommerce/plugin-woocommerce'
pull-package-deps: '@woocommerce/plugin-woocommerce'
- uses: preactjs/compressed-size-action@f780fd104362cfce9e118f9198df2ee37d12946c
@ -49,9 +54,9 @@ jobs:
BROWSERSLIST_IGNORE_OLD_DATA: true
with:
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'
build-script: '--filter="@woocommerce/plugin-woocommerce" build'
clean-script: '--if-present buildclean'
clean-script: '--if-present clean:build'
minimum-change-threshold: 100
omit-unchanged: true

View File

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

View File

@ -2,31 +2,79 @@
set -eo pipefail
function title() {
echo -e "\n\033[1m$1\033[0m"
}
# The commented variables are for troubleshooting locally. The commented commands below are also for local troubleshooting.
# 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
echo "::error::GITHUB_EVENT_NAME must be set"
exit 1
fi
title "Installing NVM"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash > /dev/null
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
echo "Installed version: $(nvm -v)"
function title() {
echo -e "\n\033[1m$1\033[0m"
}
title "Installing dependencies"
pnpm install --frozen-lockfile --filter="compare-perf" > /dev/null
if [ "$GITHUB_EVENT_NAME" == "push" ] || [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then
mkdir -p $ARTIFACTS_PATH && export WP_ARTIFACTS_PATH=$ARTIFACTS_PATH
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
title "Comparing performance with trunk"
pnpm --filter="compare-perf" run compare perf $GITHUB_SHA trunk --tests-branch $GITHUB_SHA
elif [[ "$GITHUB_EVENT_NAME" == "push" ]]; then
title "Comparing performance with base branch"
# It should be 3d7d7f02017383937f1a4158d433d0e5d44b3dc9, but we pick 55f855a2e6d769b5ae44305b2772eb30d3e721df
# where compare-perf reporting mode was introduced for processing the provided reports.
BASE_SHA=55f855a2e6d769b5ae44305b2772eb30d3e721df
HEAD_BRANCH=$(git rev-parse --abbrev-ref HEAD)
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
# chance that the reference commit used for performance test stability
# 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 tracked on https://www.codevitals.run/project/woo for all existing
# metrics.
BASE_SHA=3d7d7f02017383937f1a4158d433d0e5d44b3dc9
echo "WP_VERSION: $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_MAJOR"
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
echo '##[endgroup]'
title "Publish results to CodeVitals"
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
if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then
title "##[group]Publish results to CodeVitals"
COMMITTED_AT=$(git show -s $GITHUB_SHA --format="%cI")
pnpm --filter="compare-perf" run log $CODEVITALS_PROJECT_TOKEN trunk $GITHUB_SHA $BASE_SHA $COMMITTED_AT
echo '##[endgroup]'
fi
else
echo "Unsupported event: $GITHUB_EVENT_NAME"
fi

View File

@ -13,7 +13,7 @@ To get up and running within the WooCommerce Monorepo, you will need to make sur
### 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.
- [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.
- [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",
"cherry-pick": "node ./tools/cherry-pick/bin/run",
"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",
"postinstall": "husky",
"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
*/
import { ColorPalette } from './types';
const MAX_COLOR_PALETTES = 4;
export const ColorPalettes = ( {
colorPalettes,
totalPalettes,
@ -10,33 +18,71 @@ export const ColorPalettes = ( {
colorPalettes: ColorPalette[];
totalPalettes: number;
} ) => {
let extra = null;
const canFit = totalPalettes <= MAX_COLOR_PALETTES;
if ( totalPalettes > 4 ) {
extra = <li className="more_palettes">+{ totalPalettes - 4 }</li>;
const descriptionId = useInstanceId(
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 (
<ul className="theme-card__color-palettes">
{ colorPalettes.map( ( colorPalette ) => (
<li
key={ colorPalette.title }
style={ {
background:
'linear-gradient(to right, ' +
colorPalette.primary +
' 0px, ' +
colorPalette.primary +
' 50%, ' +
colorPalette.secondary +
' 50%, ' +
colorPalette.secondary +
' 100%' +
')',
} }
></li>
) ) }
{ extra }
</ul>
<>
<ul
className="theme-card__color-palettes"
aria-label={ __( 'Color palettes', 'woocommerce' ) }
aria-describedby={ descriptionId }
>
{ colorPalettes.map( ( colorPalette ) => (
<li
key={ colorPalette.title }
aria-label={ colorPalette.title }
style={ {
background:
'linear-gradient(to right, ' +
colorPalette.primary +
' 0px, ' +
colorPalette.primary +
' 50%, ' +
colorPalette.secondary +
' 50%, ' +
colorPalette.secondary +
' 100%' +
')',
} }
/>
) ) }
{ renderMore() }
</ul>
{ renderDescription() }
</>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,22 +9,24 @@ import { screen, render } from '@testing-library/react';
import ShippingPlaceholder from '../shipping-placeholder';
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(
<ShippingPlaceholder
showCalculator={ false }
addressProvided={ false }
isCheckout={ true }
isShippingCalculatorOpen={ false }
setIsShippingCalculatorOpen={ jest.fn() }
/>
);
expect(
screen.getByText( 'No shipping options available' )
screen.getByText( 'Enter address to calculate' )
).toBeInTheDocument();
rerender(
<ShippingPlaceholder
showCalculator={ false }
isCheckout={ false }
addressProvided={ false }
isShippingCalculatorOpen={ false }
setIsShippingCalculatorOpen={ jest.fn() }
/>
@ -33,4 +35,19 @@ describe( 'ShippingPlaceholder', () => {
screen.getByText( 'Calculated during checkout' )
).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 (
<FormTokenField
displayTransform={ transformTokenIntoProductName }
label={ __( 'Hand-Picked Products', 'woocommerce' ) }
label={ __( 'Hand-Picked', 'woocommerce' ) }
onChange={ onTokenChange }
onInputChange={ isLargeCatalog ? handleSearch : undefined }
suggestions={ suggestions }
@ -186,7 +186,7 @@ const HandPickedProductsControl = ( {
return (
<ToolsPanelItem
label={ __( 'Hand-Picked Products', 'woocommerce' ) }
label={ __( 'Hand-Picked', 'woocommerce' ) }
hasValue={ () => !! selectedProductIds?.length }
onDeselect={ deselectCallback }
resetAllFilter={ deselectCallback }

View File

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

View File

@ -48,6 +48,20 @@ function TaxonomyControls( {
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 (
<>
{ taxonomies.map( ( taxonomy: Taxonomy ) => {
@ -75,7 +89,7 @@ function TaxonomyControls( {
return (
<ToolsPanelItem
key={ slug }
label={ name }
label={ normalizeName( name ) }
hasValue={ () => termIds.length }
onDeselect={ deselectCallback }
resetAllFilter={ deselectCallback }

View File

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

View File

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

View File

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

View File

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

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 ( {
pageObject,
page,

View File

@ -9,7 +9,6 @@ import { test as base, expect } from '@woocommerce/e2e-utils';
*/
import ProductCollectionPage, {
BLOCK_LABELS,
Collections,
SELECTORS,
} from './product-collection.page';
@ -87,11 +86,7 @@ test.describe( 'Product Collection', () => {
await admin.createNewPost();
} );
test.skip( 'does not render', async ( {
page,
editor,
pageObject,
} ) => {
test( 'does not render', async ( { page, editor, pageObject } ) => {
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost( 'featured' );
await pageObject.addFilter( 'Price Range' );
@ -106,7 +101,7 @@ test.describe( 'Product Collection', () => {
).toBeVisible();
// The "No results found" info is rendered in editor for all collections.
await expect(
featuredBlock.getByText( 'No results found' )
featuredBlock.getByText( 'No products to display' )
).toBeVisible();
await pageObject.publishAndGoToFrontend();
@ -114,7 +109,9 @@ test.describe( 'Product Collection', () => {
const content = page.locator( 'main' );
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
@ -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.
//'taxonomy-product_attribute': {
// templateTitle: 'Product Attribute',
@ -747,102 +744,104 @@ test.describe( 'Product Collection', () => {
// frontendPage: '/product-attribute/color/',
// legacyBlockName: 'woocommerce/legacy-template',
//},
'taxonomy-product_cat': {
{
templateTitle: 'Product Category',
slug: 'taxonomy-product_cat',
frontendPage: '/product-category/music/',
legacyBlockName: 'woocommerce/legacy-template',
expectedProductsCount: 2,
},
'taxonomy-product_tag': {
{
templateTitle: 'Product Tag',
slug: 'taxonomy-product_tag',
frontendPage: '/product-tag/recommended/',
legacyBlockName: 'woocommerce/legacy-template',
expectedProductsCount: 2,
},
'archive-product': {
{
templateTitle: 'Product Catalog',
slug: 'archive-product',
frontendPage: '/shop/',
legacyBlockName: 'woocommerce/legacy-template',
expectedProductsCount: 16,
},
'product-search-results': {
{
templateTitle: 'Product Search Results',
slug: 'product-search-results',
frontendPage: '/?s=shirt&post_type=product',
legacyBlockName: 'woocommerce/legacy-template',
expectedProductsCount: 3,
},
};
];
for ( const {
templateTitle,
slug,
frontendPage,
legacyBlockName,
expectedProductsCount,
} of Object.values( templates ) ) {
test.describe( `${ templateTitle } template`, () => {
test( 'Product Collection block matches with classic template block', async ( {
pageObject,
requestUtils,
admin,
editor,
page,
} ) => {
await pageObject.refreshLocators( 'frontend' );
templates.forEach(
( {
templateTitle,
slug,
frontendPage,
legacyBlockName,
expectedProductsCount,
} ) => {
test.describe( `${ templateTitle } template`, () => {
test( 'Product Collection block matches with classic template block', async ( {
pageObject,
requestUtils,
admin,
editor,
page,
} ) => {
await pageObject.refreshLocators( 'frontend' );
await page.goto( frontendPage );
await page.goto( frontendPage );
const productCollectionProductNames =
await pageObject.getProductNames();
const productCollectionProductNames =
await pageObject.getProductNames();
const template = await requestUtils.createTemplate(
'wp_template',
{
slug,
title: 'classic template test',
content: 'howdy',
}
);
const template = await requestUtils.createTemplate(
'wp_template',
{
slug,
title: 'classic template test',
content: 'howdy',
}
);
await admin.visitSiteEditor( {
postId: template.id,
postType: 'wp_template',
canvas: 'edit',
await admin.visitSiteEditor( {
postId: template.id,
postType: 'wp_template',
canvas: 'edit',
} );
await expect(
editor.canvas.getByText( 'howdy' )
).toBeVisible();
await editor.insertBlock( { name: legacyBlockName } );
await editor.saveSiteEditorEntities( {
isOnlyCurrentEntityDirty: true,
} );
await page.goto( frontendPage );
const classicProducts = page.locator(
'.woocommerce-loop-product__title'
);
await expect( classicProducts ).toHaveCount(
expectedProductsCount
);
const classicProductsNames =
await classicProducts.allTextContents();
expect( classicProductsNames ).toEqual(
productCollectionProductNames
);
} );
await expect(
editor.canvas.getByText( 'howdy' )
).toBeVisible();
await editor.insertBlock( { name: legacyBlockName } );
await editor.saveSiteEditorEntities( {
isOnlyCurrentEntityDirty: true,
} );
await page.goto( frontendPage );
const classicProducts = page.locator(
'.woocommerce-loop-product__title'
);
await expect( classicProducts ).toHaveCount(
expectedProductsCount
);
const classicProductsNames =
await classicProducts.allTextContents();
expect( classicProductsNames ).toEqual(
productCollectionProductNames
);
} );
} );
}
}
);
test.describe( 'Editor: In taxonomies templates', () => {
test( 'Products by specific category template displays products from this category', async ( {
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(
name:
| 'Show Hand-picked Products'
| 'Show Hand-picked'
| 'Keyword'
| 'Show product categories'
| '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
* for 3PDs to register new product collections.
*/
test.describe( 'Product Collection registration', () => {
test.describe( 'Product Collection: Register Product Collection', () => {
const MY_REGISTERED_COLLECTIONS = {
myCustomCollection: {
name: 'My Custom Collection',
@ -56,7 +56,7 @@ test.describe( 'Product Collection registration', () => {
// Activate plugin which registers custom product collections
test.beforeEach( async ( { requestUtils } ) => {
await requestUtils.activatePlugin(
'register-product-collection-tester'
'woocommerce-blocks-test-register-product-collection'
);
} );
@ -323,7 +323,7 @@ test.describe( 'Product Collection registration', () => {
],
},
].forEach( ( collection ) => {
for ( const template of collection.previewLabelTemplate ) {
collection.previewLabelTemplate.forEach( ( template ) => {
test( `Collection "${ collection.name }" should show preview label in "${ template }"`, async ( {
pageObject,
editor,
@ -341,7 +341,7 @@ test.describe( 'Product Collection registration', () => {
await expect( previewButtonLocator ).toBeVisible();
} );
}
} );
test( `Collection "${ collection.name }" should not show preview label in a post`, async ( {
pageObject,
@ -429,7 +429,7 @@ test.describe( 'Product Collection registration', () => {
} );
// Product picker should be shown in Editor
await admin.page.reload();
await page.reload();
const deletedProductPicker = editor.canvas.getByText(
'Previously selected product'
);
@ -465,4 +465,127 @@ test.describe( 'Product Collection registration', () => {
await page.reload();
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 './request-utils';
export * from './shipping';
export * from './storeApi';
export * from './test';

View File

@ -15,33 +15,6 @@ import {
} from './templates';
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 */
getTemplates: typeof getTemplates = getTemplates.bind( this );
/** @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,
RequestUtils,
ShippingUtils,
StoreApiUtils,
} from '@woocommerce/e2e-utils';
/**
@ -108,7 +107,6 @@ const test = base.extend<
editor: Editor;
pageUtils: PageUtils;
frontendUtils: FrontendUtils;
storeApiUtils: StoreApiUtils;
performanceUtils: PerformanceUtils;
snapshotConfig: void;
shippingUtils: ShippingUtils;
@ -135,6 +133,10 @@ const test = base.extend<
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 }` );
},
pageUtils: async ( { page }, use ) => {
@ -146,9 +148,6 @@ const test = base.extend<
performanceUtils: async ( { page }, use ) => {
await use( new PerformanceUtils( page ) );
},
storeApiUtils: async ( { requestUtils }, use ) => {
await use( new StoreApiUtils( requestUtils ) );
},
shippingUtils: async ( { page, admin }, use ) => {
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){
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 {
const CRON_NAME = 'woocommerce_marketplace_cron_fetch_promotions';
const TRANSIENT_NAME = 'woocommerce_marketplace_promotions_v2';
const TRANSIENT_LIFE_SPAN = DAY_IN_SECONDS;
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() {
// 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_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 (
defined( 'DOING_AJAX' ) && DOING_AJAX
@ -53,24 +63,33 @@ class WC_Admin_Marketplace_Promotions {
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::maybe_show_bubble_promotions();
}
/**
* Fetch promotions from the API and store them in a transient.
* Fetching can be suppressed by the `woocommerce_marketplace_suppress_promotions` filter.
* Schedule a daily cron event to fetch promotions.
*
* @version 9.5.0
*
* @return void
*/
private static function maybe_update_promotions() {
// Fetch promotions if they're not in the transient.
if ( false !== get_transient( self::TRANSIENT_NAME ) ) {
return;
private static function schedule_cron_event() {
if ( ! wp_next_scheduled( self::CRON_NAME ) ) {
wp_schedule_event( time(), 'daily', self::CRON_NAME );
}
}
/**
* Fetch promotions from the API and store them in a transient.
*
* @return void
*/
public static function update_promotions() {
// Fetch promotions from the API.
$promotions = self::fetch_marketplace_promotions();
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.
* It's no longer needed as a transient is used to store the data.
* When WooCommerce is disabled, clear the WP Cron event we use to fetch promotions.
*
* @version 9.5.0
*
* @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' ) ) {
as_unschedule_all_actions( 'woocommerce_marketplace_fetch_promotions' );
}

View File

@ -26,7 +26,7 @@ class WC_Brands {
public function __construct() {
$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();
}

View File

@ -11,6 +11,10 @@
"license": "GPL-3.0+",
"scripts": {
"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:copy-assets:legacy": "wireit",
"build:project:copy-assets:admin": "wireit",
@ -74,6 +78,9 @@
"test:unit:env:watch": "pnpm test:php:env:watch",
"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: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:copy-assets": "wireit",
"wp-env": "wp-env"
@ -483,7 +490,6 @@
"start": "test:perf:ci-setup"
},
"events": [
"pull_request",
"push"
]
},
@ -504,6 +510,9 @@
"tests/metrics/**",
".wp-env.json"
],
"testEnv": {
"start": "env:test"
},
"events": [
"push"
],

View File

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

View File

@ -37,6 +37,7 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
'font_size',
'padding',
'text_color',
'extra_classes',
);
/**
@ -272,10 +273,6 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
$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 );
$classes[] = $global_style_classes;

View File

@ -414,12 +414,9 @@ class MiniCart extends AbstractBlock {
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'] );
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';
$product_count_color = array_key_exists( 'productCountColor', $attributes ) ? esc_attr( $attributes['productCountColor']['color'] ) : '';

View File

@ -36,16 +36,11 @@ abstract class AbstractOrderConfirmationBlock extends AbstractBlock {
$order = $this->get_order();
$permission = $this->get_view_order_permissions( $order );
$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 );
if ( ! empty( $classes_and_styles['classes'] ) ) {
$classname .= ' ' . $classes_and_styles['classes'];
}
return $block_content ? sprintf(
'<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'] ),
$block_content,
esc_attr( $this->block_name ),

View File

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

View File

@ -89,7 +89,7 @@ class ProductCategories extends AbstractDynamicBlock {
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_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'];
@ -116,10 +116,6 @@ class ProductCategories extends AbstractDynamicBlock {
$classes[] = "align{$attributes['align']}";
}
if ( ! empty( $attributes['className'] ) ) {
$classes[] = $attributes['className'];
}
if ( $attributes['isDropdown'] ) {
$classes[] = 'is-dropdown';
} else {

View File

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

View File

@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* ProductGallery class.
@ -125,7 +126,7 @@ class ProductGallery extends AbstractBlock {
}
$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() : '';
$product_gallery_first_image = ProductGalleryUtils::get_product_gallery_image_ids( $product, 1 );
$product_gallery_first_image_id = reset( $product_gallery_first_image );

View File

@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* ProductGalleryPager class.
@ -55,7 +56,7 @@ class ProductGalleryPager extends AbstractBlock {
}
$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 ) ) );
$post_id = $block->context['postId'] ?? '';
$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'] : '';
$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 ) {
return sprintf(
'<div class="wc-block-components-product-image wc-block-grid__product-image %1$s" style="%2$s">
%3$s
'<div %1$s>
%2$s
</div>',
esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classes_and_styles['styles'] ),
$wrapper_attributes,
$this->render_anchor(
$product,
$this->render_on_sale_badge( $product, $parsed_attributes ),

View File

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

View File

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

View File

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

View File

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

View File

@ -54,7 +54,6 @@ class ProductResultsCount extends AbstractBlock {
'wc-block-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( 'style', $parsed_style_attributes['styles'] );

View File

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

View File

@ -110,8 +110,9 @@ class ProductSaleBadge extends AbstractBlock {
return null;
}
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$classname = isset( $attributes['className'] ) ? $attributes['className'] : '';
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
$classname = StyleAttributesUtils::get_classes_by_attributes( $attributes, array( 'extra_classes' ) );
$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 );
$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--in-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;
}
/**
* 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.
*
@ -717,6 +736,7 @@ class StyleAttributesUtils {
'text_color' => self::get_text_color_class_and_style( $attributes ),
'text_decoration' => self::get_text_decoration_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 ) ) {

View File

@ -25,11 +25,6 @@ class Brands {
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-coupons.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.
}
/**
* 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;
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.
@ -30,7 +31,7 @@ class ComingSoonAdminBarBadge {
*/
public function site_visibility_badge( $wp_admin_bar ) {
// Early exit if LYS feature is disabled.
if ( ! Features::is_enabled( 'launch-your-store' ) ) {
if ( ! FeaturesUtil::feature_is_enabled( 'site_visibility_badge' ) ) {
return;
}
@ -68,7 +69,7 @@ class ComingSoonAdminBarBadge {
*/
public function output_css() {
// Early exit if LYS feature is disabled.
if ( ! Features::is_enabled( 'launch-your-store' ) ) {
if ( ! FeaturesUtil::feature_is_enabled( 'site_visibility_badge' ) ) {
return;
}

View File

@ -39,7 +39,7 @@ class ComingSoonRequestHandler {
* @internal
*
* @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 ) {
global $wp;
@ -91,8 +91,8 @@ class ComingSoonRequestHandler {
}
if ( $is_fse_theme ) {
// Since we've already rendered a template, return null to ensure no other template is rendered.
return null;
// Since we've already rendered a template, return empty string to ensure no other template is rendered.
return '';
} else {
// In non-FSE themes, other templates will still be rendered.
// We need to exit to prevent further processing.

View File

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

View File

@ -66,7 +66,8 @@ class Packages {
* @since 3.7.0
*/
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.
add_action( 'activate_plugin', array( __CLASS__, 'deactivate_merged_plugins' ) );
@ -149,6 +150,18 @@ class 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.
*

View File

@ -12,7 +12,7 @@
*
* @see https://docs.woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 8.6.0
* @version 9.5.0
*/
if ( ! defined( 'ABSPATH' ) ) {
@ -26,7 +26,7 @@ if ( ! $notices ) {
?>
<?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">
<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>

View File

@ -33,6 +33,18 @@ config = {
'**/merchant/launch-your-store.spec.js',
'**/merchant/lost-password.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/,
},

View File

@ -246,6 +246,14 @@ test.describe(
'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 }` );
await reviewRow.hover();
await reviewRow.getByRole( 'button', { name: 'Reply' } ).click();
@ -256,7 +264,12 @@ test.describe(
const replyText = `Thank you for your feedback! (replied ${ Date.now() })`;
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
.locator( 'a.comments-view-item-link' )

View File

@ -3,7 +3,13 @@ const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default;
test.describe(
'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 } );

View File

@ -65,7 +65,11 @@ async function userDeletionTest( page, username ) {
page.getByRole( 'heading', { name: 'Delete Users' } )
).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();
} );