diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4262ae81b1f..29380a12e20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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' diff --git a/.github/workflows/pr-assess-bundle-size.yml b/.github/workflows/pr-assess-bundle-size.yml index 8e6b2272f8a..d6d9e2298f8 100644 --- a/.github/workflows/pr-assess-bundle-size.yml +++ b/.github/workflows/pr-assess-bundle-size.yml @@ -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 diff --git a/.github/workflows/pr-build-live-branch.yml b/.github/workflows/pr-build-live-branch.yml index 42c39a49c93..1e92ffc9793 100644 --- a/.github/workflows/pr-build-live-branch.yml +++ b/.github/workflows/pr-build-live-branch.yml @@ -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: diff --git a/.github/workflows/scripts/run-metrics.sh b/.github/workflows/scripts/run-metrics.sh index dfb6565b3e8..521c042a980 100755 --- a/.github/workflows/scripts/run-metrics.sh +++ b/.github/workflows/scripts/run-metrics.sh @@ -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 there’s 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 diff --git a/README.md b/README.md index 4ff249daa69..06872a31ad4 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/package.json b/package.json index fb79de58c39..eb8c83b0391 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/plugins/woocommerce-admin/client/customize-store/intro/color-palettes.tsx b/plugins/woocommerce-admin/client/customize-store/intro/color-palettes.tsx index 94e45e9dd47..cce1a21b986 100644 --- a/plugins/woocommerce-admin/client/customize-store/intro/color-palettes.tsx +++ b/plugins/woocommerce-admin/client/customize-store/intro/color-palettes.tsx @@ -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 =
  • +{ totalPalettes - 4 }
  • ; + const descriptionId = useInstanceId( + ColorPalettes, + 'color-palettes-description' + ) as string; + + function renderMore() { + if ( canFit ) return null; + return ( + + ); + } + + function renderDescription() { + if ( canFit ) return null; + return ( +

    + { sprintf( + /* translators: $d is the total amount of color palettes */ + __( + 'There are a total of %d color palettes', + 'woocommerce' + ), + totalPalettes + ) } +

    + ); } return ( - + <> + + + { renderDescription() } + ); }; diff --git a/plugins/woocommerce-admin/client/customize-store/intro/intro.scss b/plugins/woocommerce-admin/client/customize-store/intro/intro.scss index cd47827cdea..144ca7e8b6d 100644 --- a/plugins/woocommerce-admin/client/customize-store/intro/intro.scss +++ b/plugins/woocommerce-admin/client/customize-store/intro/intro.scss @@ -314,6 +314,10 @@ text-align: center; } } + + &-description { + @include visually-hidden(); + } } .theme-card__free { diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/functions.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/functions.tsx index 0a565cd88f5..b591b860066 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/functions.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/functions.tsx @@ -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 = ; - } else if ( subscription.expired ) { + } else if ( subscription.expired && ! subscription.lifetime ) { actionButton = ; } else if ( subscription.local.installed === false && @@ -391,7 +398,7 @@ export function actions( subscription: Subscription ): TableRow { actionButton = ( ); - } else if ( ! subscription.autorenew ) { + } else if ( ! subscription.autorenew && ! subscription.lifetime ) { actionButton = ; } diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/footer-item/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/footer-item/style.scss index a614ebc9f18..08a9a35b4ef 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/footer-item/style.scss +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/footer-item/style.scss @@ -11,4 +11,8 @@ .wc-block-components-totals-footer-item-tax { margin-bottom: 0; } + + .wc-block-components-totals-item__value { + font-weight: bold; + } } diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/index.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/index.tsx index f3ab7641790..3f6ce7cb192 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/index.tsx @@ -114,6 +114,7 @@ export const TotalsShipping = ( { { if ( ! showCalculator ) { + const label = addressProvided + ? __( 'No available delivery option', 'woocommerce' ) + : __( 'Enter address to calculate', 'woocommerce' ); return ( - + { isCheckout - ? __( 'No shipping options available', 'woocommerce' ) + ? label : __( 'Calculated during checkout', 'woocommerce' ) } - + ); } diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/style.scss index aef027ffdd0..beba6a5a819 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/style.scss +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/style.scss @@ -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. diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-placeholder.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-placeholder.tsx index 469bb35d61a..92700f6a76a 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-placeholder.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-placeholder.tsx @@ -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( ); expect( - screen.getByText( 'No shipping options available' ) + screen.getByText( 'Enter address to calculate' ) ).toBeInTheDocument(); rerender( @@ -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( + + ); + expect( + screen.getByText( 'No available delivery option' ) + ).toBeInTheDocument(); + } ); } ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/hand-picked-products-control.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/hand-picked-products-control.tsx index 52b1174a5ae..436782fd6a1 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/hand-picked-products-control.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/hand-picked-products-control.tsx @@ -151,7 +151,7 @@ export const HandPickedProductsControlField = ( { return ( !! selectedProductIds?.length } onDeselect={ deselectCallback } resetAllFilter={ deselectCallback } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/stock-status-control.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/stock-status-control.tsx index 86647a9bc9d..dfda601e68d 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/stock-status-control.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/stock-status-control.tsx @@ -49,7 +49,7 @@ const StockStatusControl = ( props: QueryControlProps ) => { return ( ! fastDeepEqual( query.woocommerceStockStatus, @@ -61,7 +61,7 @@ const StockStatusControl = ( props: QueryControlProps ) => { isShownByDefault > { const woocommerceStockStatus = statusLabels .map( getStockStatusIdByLabel ) diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/taxonomy-controls/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/taxonomy-controls/index.tsx index 8cf8733b659..cb03225e019 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/taxonomy-controls/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/taxonomy-controls/index.tsx @@ -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 ( termIds.length } onDeselect={ deselectCallback } resetAllFilter={ deselectCallback } diff --git a/plugins/woocommerce-blocks/packages/components/totals/item/style.scss b/plugins/woocommerce-blocks/packages/components/totals/item/style.scss index f350a2984da..5a46563bb8a 100644 --- a/plugins/woocommerce-blocks/packages/components/totals/item/style.scss +++ b/plugins/woocommerce-blocks/packages/components/totals/item/style.scss @@ -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%; diff --git a/plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection-tester/index.js b/plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection.js similarity index 100% rename from plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection-tester/index.js rename to plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection.js diff --git a/plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection-tester.php b/plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection.php similarity index 66% rename from plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection-tester.php rename to plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection.php index 2171cdcc297..e4aedacbe95 100644 --- a/plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection-tester.php +++ b/plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection.php @@ -1,13 +1,11 @@ ( { }, } ); -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(); } ); } ); } ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/compatibility-layer.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/compatibility-layer.block_theme.spec.ts index fc06004254c..49d17315790 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/compatibility-layer.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/compatibility-layer.block_theme.spec.ts @@ -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 ); + } ); + } } ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/extensibility-events.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/extensibility-events.block_theme.spec.ts index 15aafd1f88a..8661492c385 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/extensibility-events.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/extensibility-events.block_theme.spec.ts @@ -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, diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/inspector-controls.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/inspector-controls.block_theme.spec.ts index 92b1723c6a3..50a5a6397eb 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/inspector-controls.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/inspector-controls.block_theme.spec.ts @@ -19,381 +19,396 @@ const test = base.extend< { pageObject: ProductCollectionPage } >( { }, } ); -test.describe( 'Product Collection', () => { - test.describe( 'Inspector Controls', () => { - test( 'Reflects the correct number of columns according to sidebar settings', async ( { - pageObject, - } ) => { - await pageObject.createNewPostAndInsertBlock(); +test.describe( 'Product Collection: Inspector Controls', () => { + test( 'Reflects the correct number of columns according to sidebar settings', async ( { + pageObject, + } ) => { + await pageObject.createNewPostAndInsertBlock(); - await pageObject.setNumberOfColumns( 2 ); - await expect( pageObject.productTemplate ).toHaveClass( - /columns-2/ - ); + await pageObject.setNumberOfColumns( 2 ); + await expect( pageObject.productTemplate ).toHaveClass( /columns-2/ ); - await pageObject.setNumberOfColumns( 4 ); - await expect( pageObject.productTemplate ).toHaveClass( - /columns-4/ - ); + await pageObject.setNumberOfColumns( 4 ); + await expect( pageObject.productTemplate ).toHaveClass( /columns-4/ ); - await pageObject.publishAndGoToFrontend(); + await pageObject.publishAndGoToFrontend(); - await expect( pageObject.productTemplate ).toHaveClass( - /columns-4/ - ); + await expect( pageObject.productTemplate ).toHaveClass( /columns-4/ ); + } ); + + test( 'Order By - sort products by title in descending order correctly', async ( { + pageObject, + } ) => { + await pageObject.createNewPostAndInsertBlock(); + + const sortedTitles = [ + 'WordPress Pennant', + 'V-Neck T-Shirt', + 'T-Shirt with Logo', + 'T-Shirt', + /Sunglasses/, // In the frontend it's "Protected: Sunglasses" + 'Single', + 'Polo', + 'Long Sleeve Tee', + 'Logo Collection', + ]; + + await pageObject.setOrderBy( 'title/desc' ); + await expect( pageObject.productTitles ).toHaveText( sortedTitles ); + + await pageObject.publishAndGoToFrontend(); + await expect( pageObject.productTitles ).toHaveText( sortedTitles ); + } ); + + // Products can be filtered based on 'on sale' status. + test( 'Products can be filtered based on "on sale" status', async ( { + pageObject, + } ) => { + await pageObject.createNewPostAndInsertBlock(); + + let allProducts = pageObject.products; + let saleProducts = pageObject.products.filter( { + hasText: 'Product on sale', } ); - test( 'Order By - sort products by title in descending order correctly', async ( { - pageObject, - } ) => { - await pageObject.createNewPostAndInsertBlock(); + await expect( allProducts ).toHaveCount( 9 ); + await expect( saleProducts ).toHaveCount( 6 ); - const sortedTitles = [ - 'WordPress Pennant', - 'V-Neck T-Shirt', - 'T-Shirt with Logo', - 'T-Shirt', - /Sunglasses/, // In the frontend it's "Protected: Sunglasses" - 'Single', - 'Polo', - 'Long Sleeve Tee', - 'Logo Collection', - ]; - - await pageObject.setOrderBy( 'title/desc' ); - await expect( pageObject.productTitles ).toHaveText( sortedTitles ); - - await pageObject.publishAndGoToFrontend(); - await expect( pageObject.productTitles ).toHaveText( sortedTitles ); + await pageObject.setShowOnlyProductsOnSale( { + onSale: true, } ); - // Products can be filtered based on 'on sale' status. - test( 'Products can be filtered based on "on sale" status', async ( { - pageObject, - } ) => { - await pageObject.createNewPostAndInsertBlock(); + await expect( allProducts ).toHaveCount( 6 ); + await expect( saleProducts ).toHaveCount( 6 ); - let allProducts = pageObject.products; - let saleProducts = pageObject.products.filter( { - hasText: 'Product on sale', - } ); - - await expect( allProducts ).toHaveCount( 9 ); - await expect( saleProducts ).toHaveCount( 6 ); - - await pageObject.setShowOnlyProductsOnSale( { - onSale: true, - } ); - - await expect( allProducts ).toHaveCount( 6 ); - await expect( saleProducts ).toHaveCount( 6 ); - - await pageObject.publishAndGoToFrontend(); - await pageObject.refreshLocators( 'frontend' ); - allProducts = pageObject.products; - saleProducts = pageObject.products.filter( { - hasText: 'Product on sale', - } ); - - await expect( allProducts ).toHaveCount( 6 ); - await expect( saleProducts ).toHaveCount( 6 ); + await pageObject.publishAndGoToFrontend(); + await pageObject.refreshLocators( 'frontend' ); + allProducts = pageObject.products; + saleProducts = pageObject.products.filter( { + hasText: 'Product on sale', } ); - test( 'Products can be filtered based on selection in handpicked products option', async ( { - pageObject, - } ) => { - await pageObject.createNewPostAndInsertBlock(); + await expect( allProducts ).toHaveCount( 6 ); + await expect( saleProducts ).toHaveCount( 6 ); + } ); - await pageObject.addFilter( 'Show Hand-picked Products' ); + test( 'Products can be filtered based on selection in handpicked products option', async ( { + pageObject, + } ) => { + await pageObject.createNewPostAndInsertBlock(); - const filterName = 'Hand-picked Products'; - await pageObject.setFilterComboboxValue( filterName, [ 'Album' ] ); - await expect( pageObject.products ).toHaveCount( 1 ); + await pageObject.addFilter( 'Show Hand-picked' ); - const productNames = [ 'Album', 'Cap' ]; - await pageObject.setFilterComboboxValue( filterName, productNames ); - await expect( pageObject.products ).toHaveCount( 2 ); - await expect( pageObject.productTitles ).toHaveText( productNames ); + const filterName = 'Hand-picked'; + await pageObject.setFilterComboboxValue( filterName, [ 'Album' ] ); + await expect( pageObject.products ).toHaveCount( 1 ); - await pageObject.publishAndGoToFrontend(); - await expect( pageObject.products ).toHaveCount( 2 ); - await expect( pageObject.productTitles ).toHaveText( productNames ); + const productNames = [ 'Album', 'Cap' ]; + await pageObject.setFilterComboboxValue( filterName, productNames ); + await expect( pageObject.products ).toHaveCount( 2 ); + await expect( pageObject.productTitles ).toHaveText( productNames ); + + await pageObject.publishAndGoToFrontend(); + await expect( pageObject.products ).toHaveCount( 2 ); + await expect( pageObject.productTitles ).toHaveText( productNames ); + } ); + + test( 'Products can be filtered based on keyword.', async ( { + pageObject, + } ) => { + await pageObject.createNewPostAndInsertBlock(); + + await pageObject.addFilter( 'Keyword' ); + + await pageObject.setKeyword( 'Album' ); + await expect( pageObject.productTitles ).toHaveText( [ 'Album' ] ); + + await pageObject.setKeyword( 'Cap' ); + await expect( pageObject.productTitles ).toHaveText( [ 'Cap' ] ); + + await pageObject.publishAndGoToFrontend(); + await expect( pageObject.productTitles ).toHaveText( [ 'Cap' ] ); + } ); + + test( 'Products can be filtered based on category.', async ( { + pageObject, + } ) => { + await pageObject.createNewPostAndInsertBlock(); + + const filterName = 'Product categories'; + await pageObject.addFilter( 'Show product categories' ); + await pageObject.setFilterComboboxValue( filterName, [ 'Clothing' ] ); + await expect( pageObject.productTitles ).toHaveText( [ + 'Logo Collection', + ] ); + + await pageObject.setFilterComboboxValue( filterName, [ + 'Accessories', + ] ); + const accessoriesProductNames = [ + 'Beanie', + 'Beanie with Logo', + 'Belt', + 'Cap', + 'Sunglasses', + ]; + await expect( pageObject.productTitles ).toHaveText( + accessoriesProductNames + ); + + await pageObject.publishAndGoToFrontend(); + + const frontendAccessoriesProductNames = [ + 'Beanie', + 'Beanie with Logo', + 'Belt', + 'Cap', + 'Protected: Sunglasses', + ]; + await expect( pageObject.productTitles ).toHaveText( + frontendAccessoriesProductNames + ); + } ); + + test( 'Products can be filtered based on tags.', async ( { + pageObject, + } ) => { + await pageObject.createNewPostAndInsertBlock(); + + const filterName = 'Product tags'; + await pageObject.addFilter( 'Show product tags' ); + await pageObject.setFilterComboboxValue( filterName, [ + 'Recommended', + ] ); + await expect( pageObject.productTitles ).toHaveText( [ + 'Beanie', + 'Hoodie', + ] ); + + await pageObject.publishAndGoToFrontend(); + await expect( pageObject.productTitles ).toHaveText( [ + 'Beanie', + 'Hoodie', + ] ); + } ); + + test( 'Products can be filtered based on product attributes like color, size etc.', async ( { + pageObject, + } ) => { + await pageObject.createNewPostAndInsertBlock(); + + await pageObject.addFilter( 'Show Product Attributes' ); + await pageObject.setProductAttribute( 'Color', 'Green' ); + + await expect( pageObject.products ).toHaveCount( 3 ); + + await pageObject.setProductAttribute( 'Size', 'Large' ); + + await expect( pageObject.products ).toHaveCount( 1 ); + + await pageObject.publishAndGoToFrontend(); + + await expect( pageObject.products ).toHaveCount( 1 ); + } ); + + test( 'Products can be filtered based on stock status (in stock, out of stock, or backorder).', async ( { + pageObject, + } ) => { + await pageObject.createNewPostAndInsertBlock(); + + await pageObject.setFilterComboboxValue( 'Stock status', [ + 'Out of stock', + ] ); + + await expect( pageObject.productTitles ).toHaveText( [ + 'T-Shirt with Logo', + ] ); + + await pageObject.publishAndGoToFrontend(); + + await expect( pageObject.productTitles ).toHaveText( [ + 'T-Shirt with Logo', + ] ); + } ); + + test( 'Products can be filtered based on featured status.', async ( { + pageObject, + } ) => { + await pageObject.createNewPostAndInsertBlock(); + + await expect( pageObject.products ).toHaveCount( 9 ); + + await pageObject.addFilter( 'Featured' ); + await pageObject.setShowOnlyFeaturedProducts( { + featured: true, } ); - test( 'Products can be filtered based on keyword.', async ( { - pageObject, - } ) => { - await pageObject.createNewPostAndInsertBlock(); + // In test data we have only 4 featured products. + await expect( pageObject.products ).toHaveCount( 4 ); - await pageObject.addFilter( 'Keyword' ); + await pageObject.publishAndGoToFrontend(); - await pageObject.setKeyword( 'Album' ); - await expect( pageObject.productTitles ).toHaveText( [ 'Album' ] ); + await expect( pageObject.products ).toHaveCount( 4 ); + } ); - await pageObject.setKeyword( 'Cap' ); - await expect( pageObject.productTitles ).toHaveText( [ 'Cap' ] ); + test( 'Products can be filtered based on created date.', async ( { + pageObject, + } ) => { + await pageObject.createNewPostAndInsertBlock(); - await pageObject.publishAndGoToFrontend(); - await expect( pageObject.productTitles ).toHaveText( [ 'Cap' ] ); + await expect( pageObject.products ).toHaveCount( 9 ); + + await pageObject.addFilter( 'Created' ); + await pageObject.setCreatedFilter( { + operator: 'within', + range: 'last3months', } ); - test( 'Products can be filtered based on category.', async ( { - pageObject, - } ) => { - await pageObject.createNewPostAndInsertBlock(); + // Products are created with the fixed publish date back in 2019 + // so there's no products published in last 3 months. + await expect( pageObject.products ).toHaveCount( 0 ); - const filterName = 'Product categories'; - await pageObject.addFilter( 'Show product categories' ); - await pageObject.setFilterComboboxValue( filterName, [ - 'Clothing', - ] ); - await expect( pageObject.productTitles ).toHaveText( [ - 'Logo Collection', - ] ); - - await pageObject.setFilterComboboxValue( filterName, [ - 'Accessories', - ] ); - const accessoriesProductNames = [ - 'Beanie', - 'Beanie with Logo', - 'Belt', - 'Cap', - 'Sunglasses', - ]; - await expect( pageObject.productTitles ).toHaveText( - accessoriesProductNames - ); - - await pageObject.publishAndGoToFrontend(); - - const frontendAccessoriesProductNames = [ - 'Beanie', - 'Beanie with Logo', - 'Belt', - 'Cap', - 'Protected: Sunglasses', - ]; - await expect( pageObject.productTitles ).toHaveText( - frontendAccessoriesProductNames - ); + await pageObject.setCreatedFilter( { + operator: 'before', + range: 'last3months', } ); - test( 'Products can be filtered based on tags.', async ( { - pageObject, - } ) => { - await pageObject.createNewPostAndInsertBlock(); + await expect( pageObject.products ).toHaveCount( 9 ); - const filterName = 'Product tags'; - await pageObject.addFilter( 'Show product tags' ); - await pageObject.setFilterComboboxValue( filterName, [ - 'Recommended', - ] ); - await expect( pageObject.productTitles ).toHaveText( [ - 'Beanie', - 'Hoodie', - ] ); + await pageObject.publishAndGoToFrontend(); - await pageObject.publishAndGoToFrontend(); - await expect( pageObject.productTitles ).toHaveText( [ - 'Beanie', - 'Hoodie', - ] ); + await expect( pageObject.products ).toHaveCount( 9 ); + } ); + + test( 'Products can be filtered based on price range.', async ( { + pageObject, + } ) => { + await pageObject.createNewPostAndInsertBlock(); + + await expect( pageObject.products ).toHaveCount( 9 ); + + await pageObject.addFilter( 'Price Range' ); + await pageObject.setPriceRange( { + min: '18.33', } ); - test( 'Products can be filtered based on product attributes like color, size etc.', async ( { - pageObject, - } ) => { - await pageObject.createNewPostAndInsertBlock(); + await expect( pageObject.products ).toHaveCount( 7 ); - await pageObject.addFilter( 'Show Product Attributes' ); - await pageObject.setProductAttribute( 'Color', 'Green' ); - - await expect( pageObject.products ).toHaveCount( 3 ); - - await pageObject.setProductAttribute( 'Size', 'Large' ); - - await expect( pageObject.products ).toHaveCount( 1 ); - - await pageObject.publishAndGoToFrontend(); - - await expect( pageObject.products ).toHaveCount( 1 ); + await pageObject.setPriceRange( { + min: '15.28', + max: '17.21', } ); - test( 'Products can be filtered based on stock status (in stock, out of stock, or backorder).', async ( { - pageObject, - } ) => { - await pageObject.createNewPostAndInsertBlock(); + await expect( pageObject.products ).toHaveCount( 1 ); - await pageObject.setFilterComboboxValue( 'Stock status', [ - 'Out of stock', - ] ); - - await expect( pageObject.productTitles ).toHaveText( [ - 'T-Shirt with Logo', - ] ); - - await pageObject.publishAndGoToFrontend(); - - await expect( pageObject.productTitles ).toHaveText( [ - 'T-Shirt with Logo', - ] ); + await pageObject.setPriceRange( { + max: '17.29', } ); - test( 'Products can be filtered based on featured status.', async ( { - pageObject, - } ) => { - await pageObject.createNewPostAndInsertBlock(); + await expect( pageObject.products ).toHaveCount( 4 ); - await expect( pageObject.products ).toHaveCount( 9 ); + await pageObject.publishAndGoToFrontend(); - await pageObject.addFilter( 'Featured' ); - await pageObject.setShowOnlyFeaturedProducts( { - featured: true, - } ); + await expect( pageObject.products ).toHaveCount( 4 ); + } ); - // In test data we have only 4 featured products. - await expect( pageObject.products ).toHaveCount( 4 ); + // See https://github.com/woocommerce/woocommerce/pull/49917 + test( 'Price range is inclusive in both editor and frontend.', async ( { + page, + pageObject, + editor, + } ) => { + await pageObject.createNewPostAndInsertBlock(); - await pageObject.publishAndGoToFrontend(); + await expect( pageObject.products ).toHaveCount( 9 ); - await expect( pageObject.products ).toHaveCount( 4 ); + await pageObject.addFilter( 'Price Range' ); + await pageObject.setPriceRange( { + min: '45', + max: '55', } ); - test( 'Products can be filtered based on created date.', async ( { - pageObject, - } ) => { - await pageObject.createNewPostAndInsertBlock(); + // Wait for the products to be filtered. + await expect( pageObject.products ).not.toHaveCount( 9 ); - await expect( pageObject.products ).toHaveCount( 9 ); + await expect( + pageObject.products.filter( { hasText: '$45.00' } ) + ).not.toHaveCount( 0 ); + await expect( + pageObject.products.filter( { hasText: '$55.00' } ) + ).not.toHaveCount( 0 ); - await pageObject.addFilter( 'Created' ); - await pageObject.setCreatedFilter( { - operator: 'within', - range: 'last3months', - } ); - - // Products are created with the fixed publish date back in 2019 - // so there's no products published in last 3 months. - await expect( pageObject.products ).toHaveCount( 0 ); - - await pageObject.setCreatedFilter( { - operator: 'before', - range: 'last3months', - } ); - - await expect( pageObject.products ).toHaveCount( 9 ); - - await pageObject.publishAndGoToFrontend(); - - await expect( pageObject.products ).toHaveCount( 9 ); + // Reset the price range. + await pageObject.setPriceRange( { + min: '0', + max: '0', } ); - test( 'Products can be filtered based on price range.', async ( { - pageObject, - } ) => { - await pageObject.createNewPostAndInsertBlock(); + await expect( pageObject.products ).toHaveCount( 9 ); - await expect( pageObject.products ).toHaveCount( 9 ); - - await pageObject.addFilter( 'Price Range' ); - await pageObject.setPriceRange( { - min: '18.33', - } ); - - await expect( pageObject.products ).toHaveCount( 7 ); - - await pageObject.setPriceRange( { - min: '15.28', - max: '17.21', - } ); - - await expect( pageObject.products ).toHaveCount( 1 ); - - await pageObject.setPriceRange( { - max: '17.29', - } ); - - await expect( pageObject.products ).toHaveCount( 4 ); - - await pageObject.publishAndGoToFrontend(); - - await expect( pageObject.products ).toHaveCount( 4 ); + await editor.insertBlock( { + name: 'woocommerce/filter-wrapper', + attributes: { filterType: 'price-filter' }, } ); - // See https://github.com/woocommerce/woocommerce/pull/49917 - test( 'Price range is inclusive in both editor and frontend.', async ( { - page, - pageObject, - editor, - } ) => { + await pageObject.publishAndGoToFrontend(); + + await expect( pageObject.products ).toHaveCount( 9 ); + + await page + .getByRole( 'textbox', { + name: 'Filter products by minimum', + } ) + .dblclick(); + await page.keyboard.type( '45' ); + + await page + .getByRole( 'textbox', { + name: 'Filter products by maximum', + } ) + .dblclick(); + await page.keyboard.type( '55' ); + + await page.keyboard.press( 'Tab' ); + + // Wait for the products to be filtered. + await expect( pageObject.products ).not.toHaveCount( 9 ); + + await expect( + pageObject.products.filter( { hasText: '$45.00' } ) + ).not.toHaveCount( 0 ); + await expect( + pageObject.products.filter( { hasText: '$55.00' } ) + ).not.toHaveCount( 0 ); + } ); + + test.describe( '"Use page context" control', () => { + test( 'should be visible on posts', async ( { pageObject } ) => { await pageObject.createNewPostAndInsertBlock(); - await expect( pageObject.products ).toHaveCount( 9 ); - - await pageObject.addFilter( 'Price Range' ); - await pageObject.setPriceRange( { - min: '45', - max: '55', - } ); - - // Wait for the products to be filtered. - await expect( pageObject.products ).not.toHaveCount( 9 ); - await expect( - pageObject.products.filter( { hasText: '$45.00' } ) - ).not.toHaveCount( 0 ); - await expect( - pageObject.products.filter( { hasText: '$55.00' } ) - ).not.toHaveCount( 0 ); - - // Reset the price range. - await pageObject.setPriceRange( { - min: '0', - max: '0', - } ); - - await expect( pageObject.products ).toHaveCount( 9 ); - - await editor.insertBlock( { - name: 'woocommerce/filter-wrapper', - attributes: { filterType: 'price-filter' }, - } ); - - await pageObject.publishAndGoToFrontend(); - - await expect( pageObject.products ).toHaveCount( 9 ); - - await page - .getByRole( 'textbox', { - name: 'Filter products by minimum', - } ) - .dblclick(); - await page.keyboard.type( '45' ); - - await page - .getByRole( 'textbox', { - name: 'Filter products by maximum', - } ) - .dblclick(); - await page.keyboard.type( '55' ); - - await page.keyboard.press( 'Tab' ); - - // Wait for the products to be filtered. - await expect( pageObject.products ).not.toHaveCount( 9 ); - - await expect( - pageObject.products.filter( { hasText: '$45.00' } ) - ).not.toHaveCount( 0 ); - await expect( - pageObject.products.filter( { hasText: '$55.00' } ) - ).not.toHaveCount( 0 ); + pageObject + .locateSidebarSettings() + .locator( SELECTORS.usePageContextControl ) + ).toBeVisible(); } ); - test.describe( '"Use page context" control', () => { - test( 'should be visible on posts', async ( { pageObject } ) => { - await pageObject.createNewPostAndInsertBlock(); + [ + 'woocommerce/woocommerce//archive-product', + 'woocommerce/woocommerce//taxonomy-product_cat', + 'woocommerce/woocommerce//taxonomy-product_tag', + 'woocommerce/woocommerce//taxonomy-product_attribute', + 'woocommerce/woocommerce//product-search-results', + ].forEach( ( slug ) => { + test( `should be visible in archive template: ${ slug }`, async ( { + pageObject, + editor, + } ) => { + await pageObject.goToEditorTemplate( slug ); + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInTemplate(); + await pageObject.focusProductCollection(); + await editor.openDocumentSettingsSidebar(); await expect( pageObject @@ -401,239 +416,210 @@ test.describe( 'Product Collection', () => { .locator( SELECTORS.usePageContextControl ) ).toBeVisible(); } ); + } ); - [ - 'woocommerce/woocommerce//archive-product', - 'woocommerce/woocommerce//taxonomy-product_cat', - 'woocommerce/woocommerce//taxonomy-product_tag', - 'woocommerce/woocommerce//taxonomy-product_attribute', - 'woocommerce/woocommerce//product-search-results', - ].forEach( ( slug ) => { - test( `should be visible in archive template: ${ slug }`, async ( { - pageObject, - editor, - } ) => { - await pageObject.goToEditorTemplate( slug ); - await pageObject.insertProductCollection(); - await pageObject.chooseCollectionInTemplate(); - await pageObject.focusProductCollection(); - await editor.openDocumentSettingsSidebar(); - - await expect( - pageObject - .locateSidebarSettings() - .locator( SELECTORS.usePageContextControl ) - ).toBeVisible(); - } ); - } ); - - [ - 'woocommerce/woocommerce//single-product', - 'twentytwentyfour//home', - 'twentytwentyfour//index', - ].forEach( ( slug ) => { - test( `should be visible in non-archive template: ${ slug }`, async ( { - pageObject, - editor, - } ) => { - await pageObject.goToEditorTemplate( slug ); - await pageObject.insertProductCollection(); - await pageObject.chooseCollectionInTemplate(); - await pageObject.focusProductCollection(); - await editor.openDocumentSettingsSidebar(); - - await expect( - pageObject - .locateSidebarSettings() - .locator( SELECTORS.usePageContextControl ) - ).toBeVisible(); - } ); - } ); - - test( 'should work as expected in Product Catalog template', async ( { + [ + 'woocommerce/woocommerce//single-product', + 'twentytwentyfour//home', + 'twentytwentyfour//index', + ].forEach( ( slug ) => { + test( `should be visible in non-archive template: ${ slug }`, async ( { pageObject, editor, } ) => { - await pageObject.goToEditorTemplate(); + await pageObject.goToEditorTemplate( slug ); + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInTemplate(); await pageObject.focusProductCollection(); await editor.openDocumentSettingsSidebar(); - const sidebarSettings = pageObject.locateSidebarSettings(); - - // Inherit query from template should be visible & enabled by default await expect( - sidebarSettings.locator( SELECTORS.usePageContextControl ) + pageObject + .locateSidebarSettings() + .locator( SELECTORS.usePageContextControl ) ).toBeVisible(); - await expect( - sidebarSettings.locator( - `${ SELECTORS.usePageContextControl } input` - ) - ).toBeChecked(); - - // "On sale control" should be hidden when inherit query from template is enabled - await expect( - sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel ) - ).toBeHidden(); - - // "On sale control" should be visible when inherit query from template is disabled - await pageObject.setInheritQueryFromTemplate( false ); - await expect( - sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel ) - ).toBeVisible(); - - // "On sale control" should retain its state when inherit query from template is enabled again - await pageObject.setShowOnlyProductsOnSale( { - onSale: true, - isLocatorsRefreshNeeded: false, - } ); - await expect( - sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel ) - ).toBeChecked(); - await pageObject.setInheritQueryFromTemplate( true ); - await expect( - sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel ) - ).toBeHidden(); - await pageObject.setInheritQueryFromTemplate( false ); - await expect( - sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel ) - ).toBeVisible(); - await expect( - sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel ) - ).toBeChecked(); } ); + } ); - test( 'is enabled by default unless already enabled elsewhere', async ( { - pageObject, - editor, - } ) => { - const productCollection = editor.canvas.getByLabel( - 'Block: Product Collection', - { exact: true } - ); - const usePageContextToggle = pageObject - .locateSidebarSettings() - .locator( SELECTORS.usePageContextControl ) - .locator( 'input' ); + test( 'should work as expected in Product Catalog template', async ( { + pageObject, + editor, + } ) => { + await pageObject.goToEditorTemplate(); + await pageObject.focusProductCollection(); + await editor.openDocumentSettingsSidebar(); - // First Product Catalog - // Option should be visible & ENABLED by default - await pageObject.goToEditorTemplate(); - await editor.selectBlocks( productCollection.first() ); - await editor.openDocumentSettingsSidebar(); + const sidebarSettings = pageObject.locateSidebarSettings(); - await expect( usePageContextToggle ).toBeChecked(); + // Inherit query from template should be visible & enabled by default + await expect( + sidebarSettings.locator( SELECTORS.usePageContextControl ) + ).toBeVisible(); + await expect( + sidebarSettings.locator( + `${ SELECTORS.usePageContextControl } input` + ) + ).toBeChecked(); - // Second Product Catalog - // Option should be visible & DISABLED by default - await pageObject.insertProductCollection(); - await pageObject.chooseCollectionInTemplate( 'productCatalog' ); - await editor.selectBlocks( productCollection.last() ); + // "On sale control" should be hidden when inherit query from template is enabled + await expect( + sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel ) + ).toBeHidden(); - await expect( usePageContextToggle ).not.toBeChecked(); + // "On sale control" should be visible when inherit query from template is disabled + await pageObject.setInheritQueryFromTemplate( false ); + await expect( + sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel ) + ).toBeVisible(); - // Disable the option in the first Product Catalog - await editor.selectBlocks( productCollection.first() ); - await usePageContextToggle.click(); - - // Third Product Catalog - // Option should be visible & ENABLED by default - await pageObject.insertProductCollection(); - await pageObject.chooseCollectionInTemplate( 'productCatalog' ); - await editor.selectBlocks( productCollection.last() ); - - await expect( usePageContextToggle ).toBeChecked(); + // "On sale control" should retain its state when inherit query from template is enabled again + await pageObject.setShowOnlyProductsOnSale( { + onSale: true, + isLocatorsRefreshNeeded: false, } ); + await expect( + sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel ) + ).toBeChecked(); + await pageObject.setInheritQueryFromTemplate( true ); + await expect( + sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel ) + ).toBeHidden(); + await pageObject.setInheritQueryFromTemplate( false ); + await expect( + sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel ) + ).toBeVisible(); + await expect( + sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel ) + ).toBeChecked(); + } ); - test( 'allows filtering in non-archive context', async ( { - pageObject, - editor, - page, - } ) => { - await pageObject.createNewPostAndInsertBlock(); + test( 'is enabled by default unless already enabled elsewhere', async ( { + pageObject, + editor, + } ) => { + const productCollection = editor.canvas.getByLabel( + 'Block: Product Collection', + { exact: true } + ); + const usePageContextToggle = pageObject + .locateSidebarSettings() + .locator( SELECTORS.usePageContextControl ) + .locator( 'input' ); - await expect( pageObject.products ).toHaveCount( 9 ); + // First Product Catalog + // Option should be visible & ENABLED by default + await pageObject.goToEditorTemplate(); + await editor.selectBlocks( productCollection.first() ); + await editor.openDocumentSettingsSidebar(); - await pageObject.insertProductCollection(); - await pageObject.chooseCollectionInPost( 'productCatalog' ); + await expect( usePageContextToggle ).toBeChecked(); - await expect( pageObject.products ).toHaveCount( 18 ); + // Second Product Catalog + // Option should be visible & DISABLED by default + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInTemplate( 'productCatalog' ); + await editor.selectBlocks( productCollection.last() ); - await page.getByLabel( 'Toggle block inserter' ).click(); - await page.getByRole( 'tab', { name: 'Patterns' } ).click(); - await page - .getByPlaceholder( 'Search' ) - .fill( 'product filters' ); - await page.getByLabel( 'Product Filters' ).click(); + await expect( usePageContextToggle ).not.toBeChecked(); - const postId = await editor.publishPost(); - await page.goto( `/?p=${ postId }` ); + // Disable the option in the first Product Catalog + await editor.selectBlocks( productCollection.first() ); + await usePageContextToggle.click(); - const productCollection = page.locator( - '.wp-block-woocommerce-product-collection' - ); + // Third Product Catalog + // Option should be visible & ENABLED by default + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInTemplate( 'productCatalog' ); + await editor.selectBlocks( productCollection.last() ); - await expect( - productCollection.first().locator( SELECTORS.product ) - ).toHaveCount( 9 ); - await expect( - productCollection.last().locator( SELECTORS.product ) - ).toHaveCount( 9 ); + await expect( usePageContextToggle ).toBeChecked(); + } ); - await page - .getByRole( 'textbox', { - name: 'Filter products by maximum', - } ) - .dblclick(); - await page.keyboard.type( '10' ); - await page.keyboard.press( 'Tab' ); + test( 'allows filtering in non-archive context', async ( { + pageObject, + editor, + page, + } ) => { + await pageObject.createNewPostAndInsertBlock(); - await expect( - productCollection.first().locator( SELECTORS.product ) - ).toHaveCount( 1 ); - await expect( - productCollection.last().locator( SELECTORS.product ) - ).toHaveCount( 9 ); - } ); + await expect( pageObject.products ).toHaveCount( 9 ); - test( 'correctly combines editor and front-end filters', async ( { - pageObject, - editor, - page, - } ) => { - await pageObject.createNewPostAndInsertBlock(); + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInPost( 'productCatalog' ); - await expect( pageObject.products ).toHaveCount( 9 ); + await expect( pageObject.products ).toHaveCount( 18 ); - await pageObject.addFilter( 'Show product categories' ); - await pageObject.setFilterComboboxValue( 'Product categories', [ - 'Music', - ] ); + await page.getByLabel( 'Toggle block inserter' ).click(); + await page.getByRole( 'tab', { name: 'Patterns' } ).click(); + await page.getByPlaceholder( 'Search' ).fill( 'product filters' ); + await page.getByLabel( 'Product Filters' ).click(); - await page.getByLabel( 'Toggle block inserter' ).click(); - await page.getByRole( 'tab', { name: 'Patterns' } ).click(); - await page - .getByPlaceholder( 'Search' ) - .fill( 'product filters' ); - await page.getByLabel( 'Product Filters' ).click(); + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); - await expect( pageObject.products ).toHaveCount( 2 ); + const productCollection = page.locator( + '.wp-block-woocommerce-product-collection' + ); - const postId = await editor.publishPost(); - await page.goto( `/?p=${ postId }` ); - await pageObject.refreshLocators( 'frontend' ); + await expect( + productCollection.first().locator( SELECTORS.product ) + ).toHaveCount( 9 ); + await expect( + productCollection.last().locator( SELECTORS.product ) + ).toHaveCount( 9 ); - await expect( pageObject.products ).toHaveCount( 2 ); + await page + .getByRole( 'textbox', { + name: 'Filter products by maximum', + } ) + .dblclick(); + await page.keyboard.type( '10' ); + await page.keyboard.press( 'Tab' ); - await page - .getByRole( 'textbox', { - name: 'Filter products by maximum', - } ) - .dblclick(); - await page.keyboard.type( '5' ); - await page.keyboard.press( 'Tab' ); + await expect( + productCollection.first().locator( SELECTORS.product ) + ).toHaveCount( 1 ); + await expect( + productCollection.last().locator( SELECTORS.product ) + ).toHaveCount( 9 ); + } ); - await expect( pageObject.products ).toHaveCount( 1 ); - } ); + test( 'correctly combines editor and front-end filters', async ( { + pageObject, + editor, + page, + } ) => { + await pageObject.createNewPostAndInsertBlock(); + + await expect( pageObject.products ).toHaveCount( 9 ); + + await pageObject.addFilter( 'Show product categories' ); + await pageObject.setFilterComboboxValue( 'Product categories', [ + 'Music', + ] ); + + await page.getByLabel( 'Toggle block inserter' ).click(); + await page.getByRole( 'tab', { name: 'Patterns' } ).click(); + await page.getByPlaceholder( 'Search' ).fill( 'product filters' ); + await page.getByLabel( 'Product Filters' ).click(); + + await expect( pageObject.products ).toHaveCount( 2 ); + + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); + await pageObject.refreshLocators( 'frontend' ); + + await expect( pageObject.products ).toHaveCount( 2 ); + + await page + .getByRole( 'textbox', { + name: 'Filter products by maximum', + } ) + .dblclick(); + await page.keyboard.type( '5' ); + await page.keyboard.press( 'Tab' ); + + await expect( pageObject.products ).toHaveCount( 1 ); } ); } ); } ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts index fe46116ab0a..c1370946dfc 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts @@ -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(); - } ); -} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts index 3b94f037eeb..598411e881c 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts @@ -396,7 +396,7 @@ class ProductCollectionPage { async addFilter( name: - | 'Show Hand-picked Products' + | 'Show Hand-picked' | 'Keyword' | 'Show product categories' | 'Show product tags' diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-picker.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-picker.block_theme.spec.ts new file mode 100644 index 00000000000..15d32a85122 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-picker.block_theme.spec.ts @@ -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(); + } ); +} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/register-product-collection-tester.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/register-product-collection.block_theme.spec.ts similarity index 76% rename from plugins/woocommerce-blocks/tests/e2e/tests/product-collection/register-product-collection-tester.block_theme.spec.ts rename to plugins/woocommerce-blocks/tests/e2e/tests/product-collection/register-product-collection.block_theme.spec.ts index 0337a38df1a..9f80f9db671 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/register-product-collection-tester.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/register-product-collection.block_theme.spec.ts @@ -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(); + } ); + } ); + } ); } ); diff --git a/plugins/woocommerce-blocks/tests/e2e/utils/index.ts b/plugins/woocommerce-blocks/tests/e2e/utils/index.ts index f315f401f53..786424cb31a 100644 --- a/plugins/woocommerce-blocks/tests/e2e/utils/index.ts +++ b/plugins/woocommerce-blocks/tests/e2e/utils/index.ts @@ -10,7 +10,6 @@ export * from './mini-cart'; export * from './performance'; export * from './request-utils'; export * from './shipping'; -export * from './storeApi'; export * from './test'; diff --git a/plugins/woocommerce-blocks/tests/e2e/utils/request-utils/index.ts b/plugins/woocommerce-blocks/tests/e2e/utils/request-utils/index.ts index 2c437a6cbe4..e6129f1ab1f 100644 --- a/plugins/woocommerce-blocks/tests/e2e/utils/request-utils/index.ts +++ b/plugins/woocommerce-blocks/tests/e2e/utils/request-utils/index.ts @@ -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 */ diff --git a/plugins/woocommerce-blocks/tests/e2e/utils/storeApi/index.ts b/plugins/woocommerce-blocks/tests/e2e/utils/storeApi/index.ts deleted file mode 100644 index dccbd4a36e5..00000000000 --- a/plugins/woocommerce-blocks/tests/e2e/utils/storeApi/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './store-api-utils.page'; diff --git a/plugins/woocommerce-blocks/tests/e2e/utils/storeApi/store-api-utils.page.ts b/plugins/woocommerce-blocks/tests/e2e/utils/storeApi/store-api-utils.page.ts deleted file mode 100644 index 443feaae503..00000000000 --- a/plugins/woocommerce-blocks/tests/e2e/utils/storeApi/store-api-utils.page.ts +++ /dev/null @@ -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, - }, - } - ); - } -} diff --git a/plugins/woocommerce-blocks/tests/e2e/utils/test.ts b/plugins/woocommerce-blocks/tests/e2e/utils/test.ts index 74493652a49..893e99967d4 100644 --- a/plugins/woocommerce-blocks/tests/e2e/utils/test.ts +++ b/plugins/woocommerce-blocks/tests/e2e/utils/test.ts @@ -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 ) ); }, diff --git a/plugins/woocommerce/changelog/43433-add-plugin-component-build-aliases b/plugins/woocommerce/changelog/43433-add-plugin-component-build-aliases new file mode 100644 index 00000000000..52cf7ac1f9b --- /dev/null +++ b/plugins/woocommerce/changelog/43433-add-plugin-component-build-aliases @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: These are changes to build commands. + diff --git a/plugins/woocommerce/changelog/51558-feat-49739-add-missing-wp-block-classname-to-add-to-cart-form-product-rating-and-product-image-blocks b/plugins/woocommerce/changelog/51558-feat-49739-add-missing-wp-block-classname-to-add-to-cart-form-product-rating-and-product-image-blocks new file mode 100644 index 00000000000..a1ebb706b1c --- /dev/null +++ b/plugins/woocommerce/changelog/51558-feat-49739-add-missing-wp-block-classname-to-add-to-cart-form-product-rating-and-product-image-blocks @@ -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. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51577-issue-1570 b/plugins/woocommerce/changelog/51577-issue-1570 deleted file mode 100644 index 9453f00a502..00000000000 --- a/plugins/woocommerce/changelog/51577-issue-1570 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Resolved fatal error when applying Brands-restricted coupon \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51608-dev-checkout-shipping-options-50557 b/plugins/woocommerce/changelog/51608-dev-checkout-shipping-options-50557 new file mode 100644 index 00000000000..c54645541c5 --- /dev/null +++ b/plugins/woocommerce/changelog/51608-dev-checkout-shipping-options-50557 @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Changes to copy and styling of delivery summary in checkout block. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51745-e2e-external-expand-wpcom-suite-part5 b/plugins/woocommerce/changelog/51745-e2e-external-expand-wpcom-suite-part5 new file mode 100644 index 00000000000..b31adc30d0b --- /dev/null +++ b/plugins/woocommerce/changelog/51745-e2e-external-expand-wpcom-suite-part5 @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Expand the e2e suite we're running on WPCOM part #5. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51751-patch-3 b/plugins/woocommerce/changelog/51751-patch-3 new file mode 100644 index 00000000000..a3480792da1 --- /dev/null +++ b/plugins/woocommerce/changelog/51751-patch-3 @@ -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. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51779-product-collection-standardize-names b/plugins/woocommerce/changelog/51779-product-collection-standardize-names new file mode 100644 index 00000000000..d8e324eb442 --- /dev/null +++ b/plugins/woocommerce/changelog/51779-product-collection-standardize-names @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Ensure Product Collection filter names are consistently capitalized. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/add-51574 b/plugins/woocommerce/changelog/add-51574 new file mode 100644 index 00000000000..cb98b7ee856 --- /dev/null +++ b/plugins/woocommerce/changelog/add-51574 @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add a11y to the color swatches diff --git a/plugins/woocommerce/changelog/add-badge-feature b/plugins/woocommerce/changelog/add-badge-feature new file mode 100644 index 00000000000..29ece22fdc4 --- /dev/null +++ b/plugins/woocommerce/changelog/add-badge-feature @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add core feature for site visibility badge diff --git a/plugins/woocommerce/changelog/fix-add-extra-css-classes-to-product-image b/plugins/woocommerce/changelog/fix-add-extra-css-classes-to-product-image new file mode 100644 index 00000000000..54b208d6aa3 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-add-extra-css-classes-to-product-image @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Render Advanced CSS Classes in the Product Image block diff --git a/plugins/woocommerce/changelog/fix-e2e-db-connection-issue b/plugins/woocommerce/changelog/fix-e2e-db-connection-issue new file mode 100644 index 00000000000..2c3baddd9d6 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-e2e-db-connection-issue @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix an issue where database is randomly disconnected while running Blocks E2E tests. diff --git a/plugins/woocommerce/changelog/fix-e2e-test-plugin-namespace b/plugins/woocommerce/changelog/fix-e2e-test-plugin-namespace new file mode 100644 index 00000000000..0d4f8105e12 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-e2e-test-plugin-namespace @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Blocks E2E: fix plugin namespace diff --git a/plugins/woocommerce/changelog/fix-info-notice-role b/plugins/woocommerce/changelog/fix-info-notice-role new file mode 100644 index 00000000000..7ccfdb5a43d --- /dev/null +++ b/plugins/woocommerce/changelog/fix-info-notice-role @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Remove alert role from informational notices diff --git a/plugins/woocommerce/changelog/fix-wccom-21789-free-subs-display b/plugins/woocommerce/changelog/fix-wccom-21789-free-subs-display new file mode 100644 index 00000000000..8f2c2642e93 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-wccom-21789-free-subs-display @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix subscription status and action items for free (lifetime) subscriptions diff --git a/plugins/woocommerce/changelog/update-49550-feedback-perfromance-job b/plugins/woocommerce/changelog/update-49550-feedback-perfromance-job new file mode 100644 index 00000000000..6a2e359d672 --- /dev/null +++ b/plugins/woocommerce/changelog/update-49550-feedback-perfromance-job @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +CI: update performance metrics job benchmarking. diff --git a/plugins/woocommerce/changelog/update-49550-perfromance-job-no-caching b/plugins/woocommerce/changelog/update-49550-perfromance-job-no-caching new file mode 100644 index 00000000000..ec3f1d153f0 --- /dev/null +++ b/plugins/woocommerce/changelog/update-49550-perfromance-job-no-caching @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +CI: omit caching baseline results in perfromance metrics job. diff --git a/plugins/woocommerce/changelog/update-49550-rework-perfromance-job b/plugins/woocommerce/changelog/update-49550-rework-perfromance-job new file mode 100644 index 00000000000..62345e0e63d --- /dev/null +++ b/plugins/woocommerce/changelog/update-49550-rework-perfromance-job @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +CI: update perfromance metrics job and improve execution time. diff --git a/plugins/woocommerce/changelog/update-wccom-21774-update-promotions-async b/plugins/woocommerce/changelog/update-wccom-21774-update-promotions-async new file mode 100644 index 00000000000..8109aa78d61 --- /dev/null +++ b/plugins/woocommerce/changelog/update-wccom-21774-update-promotions-async @@ -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. diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss index a3d3689347e..f215d91343d 100644 --- a/plugins/woocommerce/client/legacy/css/admin.scss +++ b/plugins/woocommerce/client/legacy/css/admin.scss @@ -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; +} \ No newline at end of file diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-marketplace-promotions.php b/plugins/woocommerce/includes/admin/class-wc-admin-marketplace-promotions.php index 02aedbd9556..6026fcbb020 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-marketplace-promotions.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-marketplace-promotions.php @@ -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' ); } diff --git a/plugins/woocommerce/includes/class-wc-brands.php b/plugins/woocommerce/includes/class-wc-brands.php index 71e1fa71299..43732102f01 100644 --- a/plugins/woocommerce/includes/class-wc-brands.php +++ b/plugins/woocommerce/includes/class-wc-brands.php @@ -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(); } diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index 9b54cf41dca..9683d1f7905 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -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" ], diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php index e2c02850e3f..079538cc417 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php @@ -1,4 +1,5 @@ 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( - '
    %5$s
    ', - esc_attr( $classes_and_styles['classes'] ), - esc_attr( $classname ), - esc_attr( $product_classname ), - esc_attr( $classes_and_styles['styles'] ), + '
    %2$s
    ', + $wrapper_attributes, $product ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php index 344dc858d5d..235bd9a535b 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php @@ -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; diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php index cb5952d2d84..4f9820b193e 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php @@ -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'] ) : ''; diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlock.php index 753e521f516..2afc436432a 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlock.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlock.php @@ -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( '
    %3$s
    ', - esc_attr( trim( $classname ) ), + esc_attr( $classes_and_styles['classes'] ), esc_attr( $classes_and_styles['styles'] ), $block_content, esc_attr( $this->block_name ), diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php index 047be224e28..01eddceae64 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php @@ -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']}"; diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php index c7d7554c6d7..468e832110c 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php @@ -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( diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCategories.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCategories.php index 9ad74fd8016..a9ac5c1db33 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCategories.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCategories.php @@ -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 { diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductDetails.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductDetails.php index 907d198dde9..a17a21a4fad 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductDetails.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductDetails.php @@ -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( - '
    -
    - %4$s + '
    +
    + %3$s
    ', esc_attr( $classes_and_styles['classes'] ), - esc_attr( $classname ), esc_attr( $classes_and_styles['styles'] ), $tabs ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php index 9d52abacc8a..0ecdea898b0 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php @@ -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 ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryPager.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryPager.php index 58e268addc0..ac478cc71c0 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryPager.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryPager.php @@ -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 ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php index 9aadea3a948..e3875d8ffee 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php @@ -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( - '
    - %3$s + '
    + %2$s
    ', - 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 ), diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductImageGallery.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductImageGallery.php index 2c08524fdc0..98dc66b40ed 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductImageGallery.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductImageGallery.php @@ -1,6 +1,8 @@ %2$s %3$s
    ', esc_attr( $classname ), diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductRating.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductRating.php index 1d729677171..9741b4fce7e 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductRating.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductRating.php @@ -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( - '
    - %4$s + '
    + %2$s
    ', - esc_attr( $text_align_styles_and_classes['class'] ?? '' ), - esc_attr( $styles_and_classes['classes'] ), - esc_attr( $styles_and_classes['styles'] ?? '' ), + $wrapper_attributes, $rating_html ); } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductRatingCounter.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductRatingCounter.php index 05dfff98d0f..33318453871 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductRatingCounter.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductRatingCounter.php @@ -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( - '
    - %4$s + '
    + %2$s
    ', - 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 ''; } } - diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductRatingStars.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductRatingStars.php index 1f4b1f487be..bb84c2f7466 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductRatingStars.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductRatingStars.php @@ -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( - '
    - %4$s + '
    + %2$s
    ', - esc_attr( $text_align_styles_and_classes['class'] ?? '' ), - esc_attr( $styles_and_classes['classes'] ), - esc_attr( $styles_and_classes['styles'] ?? '' ), + $wrapper_attributes, $rating_html ); } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductResultsCount.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductResultsCount.php index 61bbe35fdd1..aa5e8b20e8a 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductResultsCount.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductResultsCount.php @@ -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'] ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductReviews.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductReviews.php index 8675cdc2709..719c9dc758a 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductReviews.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductReviews.php @@ -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( '
    %2$s
    ', - esc_attr( $classname ), + StyleAttributesUtils::get_classes_by_attributes( $attributes, array( 'extra_classes' ) ), $reviews ); } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductSaleBadge.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductSaleBadge.php index 5bd428d21e2..b7e958bd43a 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductSaleBadge.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductSaleBadge.php @@ -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'] : ''; diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductStockIndicator.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductStockIndicator.php index 867d6d9d50a..0bc742e2336 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductStockIndicator.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductStockIndicator.php @@ -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 ' : ''; diff --git a/plugins/woocommerce/src/Blocks/Utils/StyleAttributesUtils.php b/plugins/woocommerce/src/Blocks/Utils/StyleAttributesUtils.php index 5205dcc3691..ce5894ce1c9 100644 --- a/plugins/woocommerce/src/Blocks/Utils/StyleAttributesUtils.php +++ b/plugins/woocommerce/src/Blocks/Utils/StyleAttributesUtils.php @@ -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 ) ) { diff --git a/plugins/woocommerce/src/Internal/Brands.php b/plugins/woocommerce/src/Internal/Brands.php index 7ac3cdbab05..4abd1c6fc0b 100644 --- a/plugins/woocommerce/src/Internal/Brands.php +++ b/plugins/woocommerce/src/Internal/Brands.php @@ -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 ); + } + } } diff --git a/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonAdminBarBadge.php b/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonAdminBarBadge.php index 082944cd3b6..140b49a3a6d 100644 --- a/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonAdminBarBadge.php +++ b/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonAdminBarBadge.php @@ -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; } diff --git a/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php b/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php index 4fe29f00e02..762da791728 100644 --- a/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php +++ b/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php @@ -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. diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php index e777fc63fb8..518c1e7027f 100644 --- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php +++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php @@ -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', diff --git a/plugins/woocommerce/src/Packages.php b/plugins/woocommerce/src/Packages.php index eb251bbc908..568ec63c509 100644 --- a/plugins/woocommerce/src/Packages.php +++ b/plugins/woocommerce/src/Packages.php @@ -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. * diff --git a/plugins/woocommerce/templates/block-notices/notice.php b/plugins/woocommerce/templates/block-notices/notice.php index 61235ce81e6..b0b9cece5b8 100644 --- a/plugins/woocommerce/templates/block-notices/notice.php +++ b/plugins/woocommerce/templates/block-notices/notice.php @@ -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 ) { ?> -
    role="alert"> +
    role="status"> diff --git a/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js index a3e2135c0ad..a3f47119ff0 100644 --- a/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js +++ b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js @@ -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/, }, diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-reviews.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-reviews.spec.js index 8b024315391..1f810a6cdf8 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-reviews.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-reviews.spec.js @@ -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' ) diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/settings-woo-com.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/settings-woo-com.spec.js index 2aca4a6eb4a..2ee66f56419 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/settings-woo-com.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/settings-woo-com.spec.js @@ -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 } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/users-manage.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/users-manage.spec.js index 160962718db..00a61eaecf9 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/users-manage.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/users-manage.spec.js @@ -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(); } );