diff --git a/.github/workflows/build-release-zip-file.yml b/.github/workflows/build-release-zip-file.yml index 94ae898d2a6..c6189b41c80 100644 --- a/.github/workflows/build-release-zip-file.yml +++ b/.github/workflows/build-release-zip-file.yml @@ -31,7 +31,7 @@ jobs: run: unzip plugins/woocommerce/woocommerce.zip -d zipfile - name: Upload the zip file as an artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/nightly-builds.yml b/.github/workflows/nightly-builds.yml index 6a3e22fa123..71ce84bf288 100644 --- a/.github/workflows/nightly-builds.yml +++ b/.github/workflows/nightly-builds.yml @@ -4,23 +4,25 @@ on: - cron: '0 0 * * *' # Run at 12 AM UTC. workflow_dispatch: -permissions: {} +env: + SOURCE_REF: trunk + TARGET_REF: nightly + RELEASE_ID: 25945111 + +permissions: { } jobs: build: if: github.repository_owner == 'woocommerce' name: Nightly builds - strategy: - fail-fast: false - matrix: - build: [trunk] + runs-on: ubuntu-20.04 permissions: contents: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: - ref: ${{ matrix.build }} + ref: ${{ env.SOURCE_REF }} - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo @@ -31,26 +33,31 @@ jobs: working-directory: plugins/woocommerce run: bash bin/build-zip.sh - - name: Deploy nightly build - uses: WebFreak001/deploy-nightly@v1.1.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload nightly build + uses: WebFreak001/deploy-nightly@46ecbabd7fad70d3e7d2c97fe8cd54e7a52e215b #v3.2.0 with: - upload_url: https://uploads.github.com/repos/${{ github.repository }}/releases/25945111/assets{?name,label} - release_id: 25945111 + token: ${{ secrets.GITHUB_TOKEN }} + upload_url: https://uploads.github.com/repos/${{ github.repository }}/releases/${{ env.RELEASE_ID }}/assets{?name,label} + release_id: ${{ env.RELEASE_ID }} asset_path: plugins/woocommerce/woocommerce.zip - asset_name: woocommerce-${{ matrix.build }}-nightly.zip + asset_name: woocommerce-${{ env.SOURCE_REF }}-nightly.zip asset_content_type: application/zip max_releases: 1 - update: - name: Update nightly tag commit ref - runs-on: ubuntu-20.04 - permissions: - contents: write - steps: - - name: Update nightly tag - uses: richardsimko/github-tag-action@v1.0.5 + + - name: Update nightly tag commit ref + uses: actions/github-script@v7 with: - tag_name: nightly - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const sourceRef = process.env.SOURCE_REF; + const targetRef = process.env.TARGET_REF; + const branchData = await github.rest.repos.getBranch({ + ...context.repo, + branch: sourceRef, + }); + + await github.rest.git.updateRef({ + ...context.repo, + ref: `tags/${ targetRef }`, + sha: branchData.data.commit.sha, + }); diff --git a/.github/workflows/pr-assess-performance.yml b/.github/workflows/pr-assess-performance.yml new file mode 100644 index 00000000000..b22c1719cd2 --- /dev/null +++ b/.github/workflows/pr-assess-performance.yml @@ -0,0 +1,102 @@ +name: Performance metrics + +on: + pull_request: + paths: + - 'plugins/woocommerce/composer.*' + - 'plugins/woocommerce/client/admin/config/**' + - 'plugins/woocommerce/includes/**' + - 'plugins/woocommerce/lib/**' + - 'plugins/woocommerce/patterns/**' + - 'plugins/woocommerce/src/**' + - 'plugins/woocommerce/templates/**' + - 'plugins/woocommerce/tests/metrics/**' + - 'plugins/woocommerce/.wp-env.json' + - '.github/workflows/pr-assess-performance.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +env: + WP_ARTIFACTS_PATH: ${{ github.workspace }}/tools/compare-perf/artifacts/ + +jobs: + benchmark: + name: Evaluate performance metrics + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + name: Checkout (${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}) + with: + fetch-depth: 0 + + - uses: ./.github/actions/setup-woocommerce-monorepo + name: Install Monorepo + with: + install: '@woocommerce/plugin-woocommerce...' + build: '@woocommerce/plugin-woocommerce' + build-type: 'full' + pull-playwright-cache: true + pull-package-deps: '@woocommerce/plugin-woocommerce' + + #TODO: Inject WordPress version as per plugin requirements (relying to defaults currently). + - name: Start Test Environment + run: | + pnpm --filter="@woocommerce/plugin-woocommerce" test:e2e:install & + pnpm --filter="@woocommerce/plugin-woocommerce" env:test + + # TODO: cache results if pushed to trunk + - name: Measure performance (@${{ github.sha }}) + run: | + 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 + + # In alignment with .github/workflows/scripts/run-metrics.sh, we should checkout 3d7d7f02017383937f1a4158d433d0e5d44b3dc9 + # as baseline. But to avoid switching branches in 'Analyze results' step, we pick 55f855a2e6d769b5ae44305b2772eb30d3e721df + # which introduced reporting mode for the perf utility. + - name: Checkout (55f855a2e6d769b5ae44305b2772eb30d3e721df@trunk, further references as 'baseline') + run: | + git reset --hard && git checkout 55f855a2e6d769b5ae44305b2772eb30d3e721df + echo "WC_TRUNK_SHA=55f855a2e6d769b5ae44305b2772eb30d3e721df" >> $GITHUB_ENV + + # Artifacts download/upload would be more reliable, but we couldn't make it working... + - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 + name: Cache measurements (baseline) + with: + path: tools/compare-perf/artifacts/*_${{ env.WC_TRUNK_SHA }}_* + key: ${{ runner.os }}-woocommerce-performance-measures-${{ env.WC_TRUNK_SHA }} + + - name: Verify cached measurements (baseline) + run: | + if test -n "$(find tools/compare-perf/artifacts/ -maxdepth 1 -name '*_${{ env.WC_TRUNK_SHA }}_*' -print -quit)" + then + echo "WC_MEASURE_BASELINE=no" >> $GITHUB_ENV + else + ls -l tools/compare-perf/artifacts/ + echo "Triggering baseline benchmarking" + echo "WC_MEASURE_BASELINE=yes" >> $GITHUB_ENV + fi + + - name: Build (baseline) + if: ${{ env.WC_MEASURE_BASELINE == 'yes' }} + run: | + git clean -n -d -X ./packages ./plugins | grep -v vendor | grep -v node_modules | sed -e 's/Would remove //g' | tr '\n' '\0' | xargs -0 rm -r + pnpm install --filter='@woocommerce/plugin-woocommerce...' --frozen-lockfile --config.dedupe-peer-dependents=false + pnpm --filter='@woocommerce/plugin-woocommerce' build + + #TODO: is baseline Wordpress version changes, restart environment targeting it. + + - name: Measure performance (@${{ env.WC_TRUNK_SHA }}) + if: ${{ env.WC_MEASURE_BASELINE == 'yes' }} + run: | + RESULTS_ID="editor_${{ env.WC_TRUNK_SHA }}_round-1" pnpm --filter="@woocommerce/plugin-woocommerce" test:metrics editor + RESULTS_ID="product-editor_${{ env.WC_TRUNK_SHA }}_round-1" pnpm --filter="@woocommerce/plugin-woocommerce" test:metrics product-editor + + - name: Analyze results + run: | + pnpm install --filter='compare-perf...' --frozen-lockfile --config.dedupe-peer-dependents=false + pnpm --filter="compare-perf" run compare compare-performance ${{ github.sha }} ${{ env.WC_TRUNK_SHA }} --tests-branch ${{ github.sha }} --skip-benchmarking + + # TODO: Publish to CodeVitals (see .github/workflows/scripts/run-metrics.sh) if pushed to trunk diff --git a/.github/workflows/release-code-freeze.yml b/.github/workflows/release-code-freeze.yml index 633000a1801..68a6304ee93 100644 --- a/.github/workflows/release-code-freeze.yml +++ b/.github/workflows/release-code-freeze.yml @@ -186,7 +186,7 @@ jobs: run: bash bin/build-zip.sh - name: Upload the zip file as an artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -216,7 +216,7 @@ jobs: run: bash bin/build-zip.sh - name: Upload the zip file as an artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -231,7 +231,7 @@ jobs: if: ${{ needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' }} steps: - id: download - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -279,7 +279,7 @@ jobs: working-directory: tools/monorepo-utils - id: download - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -300,7 +300,7 @@ jobs: if: ${{ needs.code-freeze-prep.outputs.isTodayAcceleratedFreeze == 'yes' }} steps: - id: download - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -348,7 +348,7 @@ jobs: working-directory: tools/monorepo-utils - id: download - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -369,7 +369,7 @@ jobs: if: ${{ needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' }} steps: - id: download - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -380,7 +380,7 @@ jobs: run: unzip ${{ steps.download.outputs.download-path }}/woocommerce.zip -d zipfile - name: Upload the zip file as an artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -395,7 +395,7 @@ jobs: if: ${{ needs.code-freeze-prep.outputs.isTodayAcceleratedFreeze == 'yes' }} steps: - id: download - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -406,7 +406,7 @@ jobs: run: unzip ${{ steps.download.outputs.download-path }}/woocommerce.zip -d zipfile - name: Upload the zip file as an artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/storybook-pages.yml b/.github/workflows/storybook-pages.yml index 0e84b6df80f..1aea3190d65 100644 --- a/.github/workflows/storybook-pages.yml +++ b/.github/workflows/storybook-pages.yml @@ -1,6 +1,8 @@ name: Storybook GitHub Pages on: + schedule: + - cron: '30 2 * * *' workflow_dispatch: permissions: diff --git a/.husky/post-checkout b/.husky/post-checkout new file mode 100755 index 00000000000..1485ab1707b --- /dev/null +++ b/.husky/post-checkout @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +. "$(dirname "$0")/_/husky.sh" + +# The hook documentation: https://git-scm.com/docs/githooks.html#_post_checkout +CHECKOUT_TYPE=$3 +HEAD_NEW=$2 +HEAD_PREVIOUS=$1 + +whiteColoured='\033[0m' +orangeColoured='\033[1;33m' + +# '1' is a branch checkout +if [ "$CHECKOUT_TYPE" = '1' ]; then + # Prompt about pnpm versions mismatch when switching between branches. + currentPnpmVersion=$( ( command -v pnpm > /dev/null && pnpm -v 2>/dev/null ) || echo 'n/a' ) + targetPnpmVersion=$( grep packageManager package.json | sed -nr 's/.+packageManager.+pnpm@([[:digit:].]+).+/\1/p' ) + if [ "$currentPnpmVersion" != "$targetPnpmVersion" ]; then + printf "${orangeColoured}pnpm versions mismatch: in use '$currentPnpmVersion', needed '$targetPnpmVersion'. If you are working on something in this branch, here are some hints on how to solve this:\n" + printf "${orangeColoured}* actualize environment: 'nvm use && pnpm -v' (the most common case)\n" + printf "${orangeColoured}* install: 'npm install -g pnpm@$targetPnpmVersion'\n" + fi + + # Auto-refresh dependencies when switching between branches. + changedManifests=$( ( git diff --name-only $HEAD_NEW $HEAD_PREVIOUS | grep -E '(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|composer.json|composer.lock)$' ) || echo '' ) + if [ -n "$changedManifests" ]; then + printf "${whiteColoured}The following file(s) in the new branch differs from the original one, dependencies might need to be refreshed:\n" + printf "${whiteColoured} %s\n" $changedManifests + printf "${orangeColoured}If you are working on something in this branch, ensure to refresh dependencies with 'pnpm install --frozen-lockfile'\n" + fi +fi diff --git a/.husky/post-merge b/.husky/post-merge index 7ff64bebced..bc022e8fede 100755 --- a/.husky/post-merge +++ b/.husky/post-merge @@ -1,6 +1,8 @@ #!/usr/bin/env bash . "$(dirname "$0")/_/husky.sh" +# The hook documentation: https://git-scm.com/docs/githooks.html#_post_merge + changedManifests=$( ( git diff --name-only HEAD ORIG_HEAD | grep -E '(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|composer.json|composer.lock)$' ) || echo '' ) if [ -n "$changedManifests" ]; then printf "It was a change in the following file(s) - refreshing dependencies:\n" diff --git a/.markdownlint.json b/.markdownlint.json index 5e29a079a84..4a2dd1c3ec4 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -3,8 +3,8 @@ "MD003": { "style": "atx" }, "MD007": { "indent": 4 }, "MD013": { "line_length": 9999 }, - "MD024": { "allow_different_nesting": true }, - "MD033": { "allowed_elements": ["video"] }, + "MD024": { "siblings_only": true }, + "MD033": { "allowed_elements": [ "video" ] }, "no-hard-tabs": false, "whitespace": false } diff --git a/.npmrc b/.npmrc index 9d43c15d3cc..a140ea6b576 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,5 @@ ; adding this as npm 7 automatically installs peer dependencies but pnpm does not auto-install-peers=true strict-peer-dependencies=false +; See https://github.com/pnpm/pnpm/pull/8363 (we adding the setting now, to not miss when migrating to pnpm 9.7+) +manage-package-manager-versions=true diff --git a/CODEOWNERS b/CODEOWNERS index 614fa2f46be..532335b71de 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1,14 @@ -/.github/ @woocommerce/atlas +# Monorepo CI and package managers manifests. +/.github/ @woocommerce/flux +**/composer.json @woocommerce/flux +**/package.json @woocommerce/flux + +# Monorepo tooling folders. +/bin/ @woocommerce/flux +/tools/ @woocommerce/flux +/packages/js/eslint-plugin/ @woocommerce/flux +/packages/js/dependency-extraction-webpack-plugin/ @woocommerce/flux + +# Files in root of repository +/.* @woocommerce/flux +/*.* @woocommerce/flux diff --git a/README.md b/README.md index a0dc78caa81..4ff249daa69 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ To get up and running within the WooCommerce Monorepo, you will need to make sur Once you've installed all of the prerequisites, you can run the following commands to get everything working. ```bash -# Ensure that you're using the correct version of Node -nvm use +# Ensure that correct version of Node is installed and being used +nvm install # Install the PHP and Composer dependencies for all of the plugins, packages, and tools pnpm install # Build all of the plugins, packages, and tools in the monorepo diff --git a/changelog.txt b/changelog.txt index e34b03260d4..2ba165e2bdc 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,245 @@ == Changelog == += 9.3.1 2024-09-12 = + +* Tweak - Disable remote logging feature by default [#51312](https://github.com/woocommerce/woocommerce/pull/51312) + += 9.3.0 2024-09-10 = + +**WooCommerce** + +* Enhancement - Add query params masking to remote logger [#51108](https://github.com/woocommerce/woocommerce/pull/51108) +* Update - Added more paths to remote logger query param whitelist [#51108](https://github.com/woocommerce/woocommerce/pull/51108) +* Fix - Revert update to React 18 in Checkout block. [#51289](https://github.com/woocommerce/woocommerce/pull/51289) +* Fix - Add check to ensure themes API is safe [#51081](https://github.com/woocommerce/woocommerce/pull/51081) +* Fix - CYS - Remove usage of `prepare_item_for_response` function in `Images` endpoint. [#50923](https://github.com/woocommerce/woocommerce/pull/50923) +* Fix - Add ability for a screen reader to announce the current tab on a single product page. [#50373](https://github.com/woocommerce/woocommerce/pull/50373) +* Fix - Add a label to the product pagination for the woocommerce pagination [#49924](https://github.com/woocommerce/woocommerce/pull/49924) +* Fix - Add aria-current to the current link in My Account side nav [#49800](https://github.com/woocommerce/woocommerce/pull/49800) +* Fix - Add aria-label on View order button to aid in accessibility for screen readers [#49424](https://github.com/woocommerce/woocommerce/pull/49424) +* Fix - Add CSS outline for site visibility badge keyboard accessibility [#50794](https://github.com/woocommerce/woocommerce/pull/50794) +* Fix - Add scope attribute and aria-label to the product attributes table [#49768](https://github.com/woocommerce/woocommerce/pull/49768) +* Fix - Add to Cart with Options - Fix translation when used inside the Single Product block. [#50628](https://github.com/woocommerce/woocommerce/pull/50628) +* Fix - Allow verified parameter to be set by REST API request [#50525](https://github.com/woocommerce/woocommerce/pull/50525) +* Fix - Avoid PHP warnings if `add-to-cart.php` template does not pass `aria-describedby_text` [#48969](https://github.com/woocommerce/woocommerce/pull/48969) +* Fix - Cart block: Strip HTML tags and decode HTML entities in quantity change notifications. [#50541](https://github.com/woocommerce/woocommerce/pull/50541) +* Fix - Changed from using React.render to React.createRoot for marketing coupons as it has been deprecated since React 18 [#48832](https://github.com/woocommerce/woocommerce/pull/48832) +* Fix - Changed from using React.render to React.createRoot for payment methods promotion, shipping settings region zone as it has been deprecated since React 18 [#48835](https://github.com/woocommerce/woocommerce/pull/48835) +* Fix - Changed from using React.render to React.createRoot for print shipping banner as it has been deprecated since React 18 [#48831](https://github.com/woocommerce/woocommerce/pull/48831) +* Fix - Changed from using React.render to React.createRoot for product-usage-notice-modal as it has been deprecated since React 18 [#50765](https://github.com/woocommerce/woocommerce/pull/50765) +* Fix - Changed from using React.render to React.createRoot for wc addon tour as it has been deprecated since React 18 [#48833](https://github.com/woocommerce/woocommerce/pull/48833) +* Fix - Changed from using React.render to React.createRoot for WCAdmin uses as it has been deprecated since React 18 [#48785](https://github.com/woocommerce/woocommerce/pull/48785) +* Fix - Changed instances of prime marks inappropriately used when apostrophes are supposed to be used for some parts of WC Admin JS/TS/TSX files [#50776](https://github.com/woocommerce/woocommerce/pull/50776) +* Fix - Clear product unique ID (`global_unique_id`) when duplicating products. [#50629](https://github.com/woocommerce/woocommerce/pull/50629) +* Fix - Compatibility Layer: fix 'woocommerce_before_single_product_summary' hook position. [#50392](https://github.com/woocommerce/woocommerce/pull/50392) +* Fix - CYS - Improve the error when a request fails due to permissions [#50211](https://github.com/woocommerce/woocommerce/pull/50211) +* Fix - CYS - Update the "show_on_front" setting to "posts" to avoid overriding the "page" template. [#50083](https://github.com/woocommerce/woocommerce/pull/50083) +* Fix - CYS: disable zoom out on fonts/color pairs iframe [#50498](https://github.com/woocommerce/woocommerce/pull/50498) +* Fix - CYS: Fix auto scroll when a new block is added. [#50431](https://github.com/woocommerce/woocommerce/pull/50431) +* Fix - CYS: Improve opt in flow [#50529](https://github.com/woocommerce/woocommerce/pull/50529) +* Fix - Display address card for virtual products if shopper's address is known [#50127](https://github.com/woocommerce/woocommerce/pull/50127) +* Fix - Enable skipped E2E tests for attributes #50143 [#50143](https://github.com/woocommerce/woocommerce/pull/50143) +* Fix - Ensure coupon errors are visible on block checkout when invalid coupons are removed. [#50412](https://github.com/woocommerce/woocommerce/pull/50412) +* Fix - Ensure low and no stock email notification routine is triggered whenever product stock changes [#49583](https://github.com/woocommerce/woocommerce/pull/49583) +* Fix - Ensure session object is initialized before attempting to get chosen shipping methods [#50774](https://github.com/woocommerce/woocommerce/pull/50774) +* Fix - Ensure that the orders REST endpoint behaves the same as the UI when updating an order to remove a line item. [#50606](https://github.com/woocommerce/woocommerce/pull/50606) +* Fix - Featured Product: Fix variable product Selection dropdown #50633 [#50633](https://github.com/woocommerce/woocommerce/pull/50633) +* Fix - Fix "Product Meta" translations - Register the block server side. [#50625](https://github.com/woocommerce/woocommerce/pull/50625) +* Fix - Fix: ensure the global product object is always ready for compatibility layer by disabling default render routine of Product Templates inner blocks. [#49971](https://github.com/woocommerce/woocommerce/pull/49971) +* Fix - Fix activating the installed subscription when the user has multiple active licenses for the same product. [#49803](https://github.com/woocommerce/woocommerce/pull/49803) +* Fix - Fix address heading level on My Account page. [#49764](https://github.com/woocommerce/woocommerce/pull/49764) +* Fix - Fix an admin bar CSS positioning bug in WordPress.com on mobile [#50709](https://github.com/woocommerce/woocommerce/pull/50709) +* Fix - Fix cart shortcode updates when not used on the main cart page. [#50524](https://github.com/woocommerce/woocommerce/pull/50524) +* Fix - Fix core profiler checkbox vertical alignment and border color [#50151](https://github.com/woocommerce/woocommerce/pull/50151) +* Fix - Fix core profiler set up my store button and TOS are too close to each other [#50579](https://github.com/woocommerce/woocommerce/pull/50579) +* Fix - Fix e2e Google for WooCommerce strict mode violation error [#50189](https://github.com/woocommerce/woocommerce/pull/50189) +* Fix - Fixed Core Profiler's sticky footer button problem [#50727](https://github.com/woocommerce/woocommerce/pull/50727) +* Fix - Fixed placeholders in the classic cart shipping calculator to update with country selection. [#49684](https://github.com/woocommerce/woocommerce/pull/49684) +* Fix - Fixes a bug where some express payment buttons weren't being rendered correctly [#49304](https://github.com/woocommerce/woocommerce/pull/49304) +* Fix - Fix extensionCartUpdates to surface generic error messages, and include documentation for the error handling. [#49762](https://github.com/woocommerce/woocommerce/pull/49762) +* Fix - Fix focus order on checkout block page. [#49649](https://github.com/woocommerce/woocommerce/pull/49649) +* Fix - Fix navigation badge decreases when installing extension in "Grow your business task" [#50584](https://github.com/woocommerce/woocommerce/pull/50584) +* Fix - Fix page titles of the cart and checkout page when using blocks and FSE themes. [#49986](https://github.com/woocommerce/woocommerce/pull/49986) +* Fix - Fix rescheduling of actions that are blocked by other delayed actions [#50082](https://github.com/woocommerce/woocommerce/pull/50082) +* Fix - Fix the "Add payment methods" link in LYS congrat screen redirects to a blank page [#50609](https://github.com/woocommerce/woocommerce/pull/50609) +* Fix - Fix translation - Avoid registering blocks in the wrong context. [#50615](https://github.com/woocommerce/woocommerce/pull/50615) +* Fix - Fix `Product meta` console error. [#50680](https://github.com/woocommerce/woocommerce/pull/50680) +* Fix - Fix `store-title` endpoint - Pass default value to `get_option`. [#50673](https://github.com/woocommerce/woocommerce/pull/50673) +* Fix - Hide save changes button in main payments screen [#50064](https://github.com/woocommerce/woocommerce/pull/50064) +* Fix - In Remote Specs, treat empty arrays as valid cached values so individual engines can return default values. [#50521](https://github.com/woocommerce/woocommerce/pull/50521) +* Fix - Keep focus on shipping option input once selected [#49360](https://github.com/woocommerce/woocommerce/pull/49360) +* Fix - Make the matching variations alert a live region [#50132](https://github.com/woocommerce/woocommerce/pull/50132) +* Fix - Only count published products in productCount [#50503](https://github.com/woocommerce/woocommerce/pull/50503) +* Fix - Prevent fatal error if NULL is provided in array_search under Jetpack Stats [#50696](https://github.com/woocommerce/woocommerce/pull/50696) +* Fix - Prevent Store API orders being placed with empty state [#50028](https://github.com/woocommerce/woocommerce/pull/50028) +* Fix - Prevent sync-on-read from affecting results of HPOS diff CLI tool. [#49726](https://github.com/woocommerce/woocommerce/pull/49726) +* Fix - Product Collection: Fix max price query to include prices less or equal to the given max value. [#49917](https://github.com/woocommerce/woocommerce/pull/49917) +* Fix - Product Collection: fix the preview if used in Products by specific Category or Tag [#49889](https://github.com/woocommerce/woocommerce/pull/49889) +* Fix - Product Price block: prevent price amounts from breaking into multiple lines [#50660](https://github.com/woocommerce/woocommerce/pull/50660) +* Fix - Properly detect active plugins in multisite WP installations. [#50417](https://github.com/woocommerce/woocommerce/pull/50417) +* Fix - Reduce error noise in the user profile screen, by removing the requirement for custom fields to have a class attribute. [#48079](https://github.com/woocommerce/woocommerce/pull/48079) +* Fix - Remove Active Shipping Zones check for displaying shipping calculator on the Cart Page. [#49214](https://github.com/woocommerce/woocommerce/pull/49214) +* Fix - Single product block - Fix translation for title and description in edit mode. [#50599](https://github.com/woocommerce/woocommerce/pull/50599) +* Fix - Store API: Do not resume pending orders--create a new order instead [#50531](https://github.com/woocommerce/woocommerce/pull/50531) +* Fix - Transform labels in shipping zone region selector to decode html entities [#50694](https://github.com/woocommerce/woocommerce/pull/50694) +* Fix - Treat post_type=product as a shop page. [#50567](https://github.com/woocommerce/woocommerce/pull/50567) +* Fix - Update product order status colors to ensure accessible color contrasts [#49934](https://github.com/woocommerce/woocommerce/pull/49934) +* Add - Add an additional field for the email settings that sets the footer text color [#49648](https://github.com/woocommerce/woocommerce/pull/49648) +* Add - Add blueprint behind a feature flag for testing purposes. [#49763](https://github.com/woocommerce/woocommerce/pull/49763) +* Add - Add field for the email footer text color [#49648](https://github.com/woocommerce/woocommerce/pull/49648) +* Add - Add function to clear system status theme info cache [#50803](https://github.com/woocommerce/woocommerce/pull/50803) +* Add - Add methods required by extensions to control product feature usage based on subscription status. [#50218](https://github.com/woocommerce/woocommerce/pull/50218) +* Add - Add parameter to avoid attempting to create the logs directory if it doesn't exist [#49766](https://github.com/woocommerce/woocommerce/pull/49766) +* Add - Add Pattern button to no blocks view on the CYS assembler [#49981](https://github.com/woocommerce/woocommerce/pull/49981) +* Add - Add reactified main payments screen [#49972](https://github.com/woocommerce/woocommerce/pull/49972) +* Add - Add reactify-classic-payments-settings feature flag [#49966](https://github.com/woocommerce/woocommerce/pull/49966) +* Add - Add tracks for WordPress Importer/Export pages. [#50769](https://github.com/woocommerce/woocommerce/pull/50769) +* Add - Add `FilteredGetDataTrait`, `OrderAwareControllerTrait`, and `StatsDataStoreTrait` for extension developers to reuse while creating custom Analytics [#49425](https://github.com/woocommerce/woocommerce/pull/49425) +* Add - Implement server-side remote error logging [#49599](https://github.com/woocommerce/woocommerce/pull/49599) +* Add - Inform screen reader users when mini cart updates [#48295](https://github.com/woocommerce/woocommerce/pull/48295) +* Add - Integrate JS remote logging package in WooCommerce Admin [#50134](https://github.com/woocommerce/woocommerce/pull/50134) +* Add - Product Collection: emit the JS event when PC block is rendered [#50166](https://github.com/woocommerce/woocommerce/pull/50166) +* Add - Product Collection: Enable Context-Aware Previews by Adding `usesReference` to `registerProductCollection` [#49796](https://github.com/woocommerce/woocommerce/pull/49796) +* Add - Track frequency of unhandled JS errors with MC Stats [#50155](https://github.com/woocommerce/woocommerce/pull/50155) +* Add - Use MC Stats for PHP fatal error counting [#49658](https://github.com/woocommerce/woocommerce/pull/49658) +* Add - [E2E tests]: Add product description using the block editor #50232 [#50232](https://github.com/woocommerce/woocommerce/pull/50232) +* Update - Update WooCommerce Shipping Promo Banner to install the latest version of WooCommerce Shipping instead of WCS&T. [#50970](https://github.com/woocommerce/woocommerce/pull/50970) +* Update - Add abbreviations for fields GTIN, UPC, EAN, OR ISBN [#50042](https://github.com/woocommerce/woocommerce/pull/50042) +* Update - Add additional fields to new product editor e2e tests. [#50241](https://github.com/woocommerce/woocommerce/pull/50241) +* Update - Add confirmation prompt for site visibility settings when changing from live to coming soon mode [#50759](https://github.com/woocommerce/woocommerce/pull/50759) +* Update - Add pattern validation for global_unique_id [#50501](https://github.com/woocommerce/woocommerce/pull/50501) +* Update - Add remote logger as a log handler to wc logger [#50430](https://github.com/woocommerce/woocommerce/pull/50430) +* Update - Add request_uri prop to remote logging data [#50671](https://github.com/woocommerce/woocommerce/pull/50671) +* Update - Add woocommerce_coming_soon option for all sites [#50581](https://github.com/woocommerce/woocommerce/pull/50581) +* Update - Comment: Fix typos in documentation. [#50282](https://github.com/woocommerce/woocommerce/pull/50282) +* Update - CYS - Add tests for the Full Composability feature. [#49748](https://github.com/woocommerce/woocommerce/pull/49748) +* Update - CYS - Run appropriate tests depending on the WordPress version. [#50016](https://github.com/woocommerce/woocommerce/pull/50016) +* Update - CYS - Update icon and text colors in the assembler. [#50478](https://github.com/woocommerce/woocommerce/pull/50478) +* Update - CYS: Improve opt-in flow fonts. [#50086](https://github.com/woocommerce/woocommerce/pull/50086) +* Update - CYS: Improve opt-in flow patterns. [#50080](https://github.com/woocommerce/woocommerce/pull/50080) +* Update - CYS: Improve tracking survey [#50196](https://github.com/woocommerce/woocommerce/pull/50196) +* Update - CYS: Improve tracking survey [#50354](https://github.com/woocommerce/woocommerce/pull/50354) +* Update - CYS: Update the tracking URL to the external Fiverr link in sidebar of the **Add your logo** screen. [#50753](https://github.com/woocommerce/woocommerce/pull/50753) +* Update - Enable remote logging feature flag [#50351](https://github.com/woocommerce/woocommerce/pull/50351) +* Update - feat: add `aria-required` attributes to WC form fields [#48371](https://github.com/woocommerce/woocommerce/pull/48371) +* Update - Fixed log-out link behavior so that redirects work, and so that security nonces are automatically added to link in navigation menus. [#49605](https://github.com/woocommerce/woocommerce/pull/49605) +* Update - Migrate LYS user meta [#50664](https://github.com/woocommerce/woocommerce/pull/50664) +* Update - Move marketing task to things to do next task list [#50487](https://github.com/woocommerce/woocommerce/pull/50487) +* Update - Move site visibility badge to admin bar. [#50775](https://github.com/woocommerce/woocommerce/pull/50775) +* Update - Remove "Need help?" modal from onboarding [#47812](https://github.com/woocommerce/woocommerce/pull/47812) +* Update - Remove all links from the CYS sidebars [#50414](https://github.com/woocommerce/woocommerce/pull/50414) +* Update - Remove remote API call from marketing task [#50479](https://github.com/woocommerce/woocommerce/pull/50479) +* Update - Remove WooCommerce Navigation client side feature and deprecate PHP classes. [#50190](https://github.com/woocommerce/woocommerce/pull/50190) +* Update - Renamed columns inside In-App Marketplace > My subscriptions and added action to turn auto-renewal on for a subscription [#49985](https://github.com/woocommerce/woocommerce/pull/49985) +* Update - Rename woocommerce_is_store_page to woocommerce_is_extension_store_page [#50771](https://github.com/woocommerce/woocommerce/pull/50771) +* Update - Reverting the new `buttonAttributes` API. This will be included in a later release [#50763](https://github.com/woocommerce/woocommerce/pull/50763) +* Update - Revert the Zoom Out feature for the CYS experience [#50535](https://github.com/woocommerce/woocommerce/pull/50535) +* Update - Show expiring and expired notices to active and unconnected subscriptions [#50383](https://github.com/woocommerce/woocommerce/pull/50383) +* Update - Store API: Remove the need for nonces when using cart tokens. Remove deprecated X-WC-Store-API-Nonce header. [#50025](https://github.com/woocommerce/woocommerce/pull/50025) +* Update - Strip HTML tags from aria-label in wc_help_tip function [#50103](https://github.com/woocommerce/woocommerce/pull/50103) +* Update - Text adjustments on shipping zones settings page [#50136](https://github.com/woocommerce/woocommerce/pull/50136) +* Update - Update AdditionalPayments task to use default payment gateways [#50674](https://github.com/woocommerce/woocommerce/pull/50674) +* Update - Update add product task button section UI [#50580](https://github.com/woocommerce/woocommerce/pull/50580) +* Update - Update all blocks to use API Version 3. [#48720](https://github.com/woocommerce/woocommerce/pull/48720) +* Update - Update Blueprint settings layout. [#50724](https://github.com/woocommerce/woocommerce/pull/50724) +* Update - Update core profiler continue button container on extension screen [#50582](https://github.com/woocommerce/woocommerce/pull/50582) +* Update - Update Store Alert actions to have unique keys. [#50424](https://github.com/woocommerce/woocommerce/pull/50424) +* Update - Update WooCommercePayments task is_supported to use default suggestions [#50585](https://github.com/woocommerce/woocommerce/pull/50585) +* Update - Enhance CSV path and upload handling in product import [#51344](https://github.com/woocommerce/woocommerce/pull/51344) +* Dev - Execute test env setup on host instead of wp-env container [#51021](https://github.com/woocommerce/woocommerce/pull/51021) +* Dev - Added code docs with examples to the Analytics classes [#49425](https://github.com/woocommerce/woocommerce/pull/49425) +* Dev - Add lost password e2e tests [#50611](https://github.com/woocommerce/woocommerce/pull/50611) +* Dev - Add unit tests for the product_add_publish track. [#49916](https://github.com/woocommerce/woocommerce/pull/49916) +* Dev - CI: introduce PHPUnit tests sharding. [#50084](https://github.com/woocommerce/woocommerce/pull/50084) +* Dev - CI: minor speed boost of wp-env startup. [#50445](https://github.com/woocommerce/woocommerce/pull/50445) +* Dev - CI: speedup assets size verification job execution time. [#50178](https://github.com/woocommerce/woocommerce/pull/50178) +* Dev - CI: Use a single shard when re-running failed tests in CI [#50492](https://github.com/woocommerce/woocommerce/pull/50492) +* Dev - CI config: update changes list to include more paths [#50399](https://github.com/woocommerce/woocommerce/pull/50399) +* Dev - Clean up unused images [#50516](https://github.com/woocommerce/woocommerce/pull/50516) +* Dev - CYS - Document possible Intro pages [#50171](https://github.com/woocommerce/woocommerce/pull/50171) +* Dev - CYS - Move the "ai/patterns" endpoint to woocommerce admin API. [#50372](https://github.com/woocommerce/woocommerce/pull/50372) +* Dev - CYS - Move the "ai/store-info" endpoint to woocommerce admin API [#50363](https://github.com/woocommerce/woocommerce/pull/50363) +* Dev - CYS - Move the ai/business-description endpoint to woocommerce admin API [#50359](https://github.com/woocommerce/woocommerce/pull/50359) +* Dev - CYS - Move the ai/store-title endpoint to woocommerce admin API [#50352](https://github.com/woocommerce/woocommerce/pull/50352) +* Dev - CYS - Move the `ai/images` endpoint to woocommerce admin API [#50365](https://github.com/woocommerce/woocommerce/pull/50365) +* Dev - CYS - Move the `ai/product` endpoint to woocommerce admin API. [#50393](https://github.com/woocommerce/woocommerce/pull/50393) +* Dev - CYS: add E2E tests for fonts installation. [#50210](https://github.com/woocommerce/woocommerce/pull/50210) +* Dev - E2E tests: add a flaky test reporter for Core e2e tests [#50259](https://github.com/woocommerce/woocommerce/pull/50259) +* Dev - E2E tests: add an option to skip the env setup script running before test execution [#50620](https://github.com/woocommerce/woocommerce/pull/50620) +* Dev - E2E tests: add buildkite-test-collector for blocks e2e tests [#50642](https://github.com/woocommerce/woocommerce/pull/50642) +* Dev - E2E tests: add environment reporter [#49988](https://github.com/woocommerce/woocommerce/pull/49988) +* Dev - E2E tests: add hpos-disabled env and tagged tests with hpos tag [#50448](https://github.com/woocommerce/woocommerce/pull/50448) +* Dev - E2E tests: fixed broken logo picker tests [#50473](https://github.com/woocommerce/woocommerce/pull/50473) +* Dev - E2E tests: fix flakiness in page-loads customer page test [#50559](https://github.com/woocommerce/woocommerce/pull/50559) +* Dev - E2E tests: fix flakiness in product attributes test [#50485](https://github.com/woocommerce/woocommerce/pull/50485) +* Dev - E2E tests: removed Github reporter [#50256](https://github.com/woocommerce/woocommerce/pull/50256) +* Dev - E2E tests: Removed unnecessary pause in the test [#50043](https://github.com/woocommerce/woocommerce/pull/50043) +* Dev - E2E tests for verifying approve, spam and reply to product reviews. [#50060](https://github.com/woocommerce/woocommerce/pull/50060) +* Dev - Fix E2E tests SKU field id #49729 [#49729](https://github.com/woocommerce/woocommerce/pull/49729) +* Dev - Fixes a flaky product variations e2e test [#50807](https://github.com/woocommerce/woocommerce/pull/50807) +* Dev - Fix Metrics CI job [#50214](https://github.com/woocommerce/woocommerce/pull/50214) +* Dev - Fix optional param in PHPdoc for `WC_Admin_Marketplace_Promotions` to generate code-reference w/o warnings [#50732](https://github.com/woocommerce/woocommerce/pull/50732) +* Dev - Fix the Metrics job by adding a missing NVM install step [#50482](https://github.com/woocommerce/woocommerce/pull/50482) +* Dev - Make the Metrics tests use utilities provided by the updated @wordpress/e2e-test-utils-playwright package. [#50626](https://github.com/woocommerce/woocommerce/pull/50626) +* Dev - Mark ReportTable tableData prop as not required [#50816](https://github.com/woocommerce/woocommerce/pull/50816) +* Dev - Monorepo: enable new linting rules for PHP (PSR-4 naming, Strict types declaration). [#49438](https://github.com/woocommerce/woocommerce/pull/49438) +* Dev - Monorepo: tweak Webpack loaders paths filtering for better build perfromance. [#49714](https://github.com/woocommerce/woocommerce/pull/49714) +* Dev - move block theme docs to docs site folder [#50638](https://github.com/woocommerce/woocommerce/pull/50638) +* Dev - move part of checkout docs to main docs folder [#49984](https://github.com/woocommerce/woocommerce/pull/49984) +* Dev - Move `ReportError` to `@woocommerce/components` as `AnalyticsError` [#50108](https://github.com/woocommerce/woocommerce/pull/50108) +* Dev - moving product collection docs to main docs folder [#50368](https://github.com/woocommerce/woocommerce/pull/50368) +* Dev - Reduce duplicated code in Analytics controllers, unify their behavior and API. [#49425](https://github.com/woocommerce/woocommerce/pull/49425) +* Dev - Reduce the amount of duplicated code in Analytics `DataStore`s. [#49425](https://github.com/woocommerce/woocommerce/pull/49425) +* Dev - Removed defaultProps from React functional components since they will be deprecated for React 19 [#50266](https://github.com/woocommerce/woocommerce/pull/50266) +* Dev - Removed directive to disable woocommerce_coming_soon in e2e tests so that we get better test coverage [#50344](https://github.com/woocommerce/woocommerce/pull/50344) +* Dev - Render a React placeholder for offline and WooCommerce Payments settings sections [#50008](https://github.com/woocommerce/woocommerce/pull/50008) +* Dev - Replace `Automattic\WooCommerce\Admin\API\Reports\*\Query` classes with a single `GenericQuery` class. [#49425](https://github.com/woocommerce/woocommerce/pull/49425) +* Dev - Switch `render()` to `createRoot().render()` to use React 18 features. [#48843](https://github.com/woocommerce/woocommerce/pull/48843) +* Dev - Tests: moved api core tests as a suite in e2e-pw [#50024](https://github.com/woocommerce/woocommerce/pull/50024) +* Dev - Tweak the lost password e2e logic [#50666](https://github.com/woocommerce/woocommerce/pull/50666) +* Dev - Update @wordpress/e2e-test-utils-playwright core dependency to wp-6.6 [#50274](https://github.com/woocommerce/woocommerce/pull/50274) +* Dev - Updated e2e tests docs to clarify the use of environments [#50530](https://github.com/woocommerce/woocommerce/pull/50530) +* Dev - Updated the workflow prompting for testing instructions to only run once (preventing double comments) [#50034](https://github.com/woocommerce/woocommerce/pull/50034) +* Dev - Update E2E tests for linked list and variation creation with new component changes. [#50128](https://github.com/woocommerce/woocommerce/pull/50128) +* Dev - Update lys e2e tests to test with both classic and block themes [#50657](https://github.com/woocommerce/woocommerce/pull/50657) +* Dev - Update Playwright to 1.46.1 from 1.45.1 [#50772](https://github.com/woocommerce/woocommerce/pull/50772) +* Dev - Update WP version to 6.6 in Blocks wp-env config. [#49704](https://github.com/woocommerce/woocommerce/pull/49704) +* Dev - Use stricter text selector on test [#50848](https://github.com/woocommerce/woocommerce/pull/50848) +* Dev - [Filter Products by Price]: Update view when changing the min/max value #50651 [#50651](https://github.com/woocommerce/woocommerce/pull/50651) +* Tweak - Disable remote logging feature by default [#51312](https://github.com/woocommerce/woocommerce/pull/51312) +* Tweak - Add GTIN in structured data [#50087](https://github.com/woocommerce/woocommerce/pull/50087) +* Tweak - Add link to title, remove link from a description, minor copy changes to site visibility settings page [#50781](https://github.com/woocommerce/woocommerce/pull/50781) +* Tweak - Add the `woocommerce_should_clear_cart_after_payment` filter to influence whether the cart should be cleared after payment. [#44515](https://github.com/woocommerce/woocommerce/pull/44515) +* Tweak - allows the quantity selector on block cart page to render as readonly when editable is false [#49450](https://github.com/woocommerce/woocommerce/pull/49450) +* Tweak - Bump Jetpack COnnection, Jetpack Constants and a8c MC Stats [#50471](https://github.com/woocommerce/woocommerce/pull/50471) +* Tweak - Extract the checkbox list option logic into its own component [#50566](https://github.com/woocommerce/woocommerce/pull/50566) +* Tweak - Make `geolocation_ajax_get_location_hash` case-insensitive, to reduce the number of cache misses. [#45439](https://github.com/woocommerce/woocommerce/pull/45439) +* Tweak - Optimize large image files [#50517](https://github.com/woocommerce/woocommerce/pull/50517) +* Tweak - Product Collection: fix the PHP deprecated warning [#50661](https://github.com/woocommerce/woocommerce/pull/50661) +* Tweak - Reduce core profiler sticky footer height [#50788](https://github.com/woocommerce/woocommerce/pull/50788) +* Tweak - Remove colon from product data meta box checkboxes [#50619](https://github.com/woocommerce/woocommerce/pull/50619) +* Tweak - Remove the code related to the automatic Products (Beta) -> Product Collection upgrade. [#50440](https://github.com/woocommerce/woocommerce/pull/50440) +* Tweak - Set timeout to 2 seconds for helper product-usage-notice-rules endpoint request [#50821](https://github.com/woocommerce/woocommerce/pull/50821) +* Tweak - Update size of site visibility badge. [#50792](https://github.com/woocommerce/woocommerce/pull/50792) +* Tweak - Vertically center product meta elements [#50826](https://github.com/woocommerce/woocommerce/pull/50826) +* Performance - Cache order dates in options for performance. [#50066](https://github.com/woocommerce/woocommerce/pull/50066) +* Performance - Compress pattern placeholder image assets [#50405](https://github.com/woocommerce/woocommerce/pull/50405) +* Performance - Improve performance of maybe_assign_default_product_cat by only dropping cache and term recounting if changes were made in the database [#50006](https://github.com/woocommerce/woocommerce/pull/50006) +* Performance - Improve setup_tasks_remaining performance [#50655](https://github.com/woocommerce/woocommerce/pull/50655) +* Enhancement - Add a filter to override the SKU database lock. [#49755](https://github.com/woocommerce/woocommerce/pull/49755) +* Enhancement - Add email type to Checkout block email field. [#48611](https://github.com/woocommerce/woocommerce/pull/48611) +* Enhancement - Add filter `woocommerce_is_store_page` to modify whether Coming Soon mode considers a URL a store page or not. [#50174](https://github.com/woocommerce/woocommerce/pull/50174) +* Enhancement - Add username in email reset-password link [#49737](https://github.com/woocommerce/woocommerce/pull/49737) +* Enhancement - CYS: improve CTA [#50278](https://github.com/woocommerce/woocommerce/pull/50278) +* Enhancement - Ensure `wccomHelper` data is only loaded on the Extensions page where it's needed. [#49758](https://github.com/woocommerce/woocommerce/pull/49758) +* Enhancement - Fixed minor issues in the developer documentation recently added by public resources team [#50845](https://github.com/woocommerce/woocommerce/pull/50845) +* Enhancement - Hide zoomed product images for screen readers. [#50003](https://github.com/woocommerce/woocommerce/pull/50003) +* Enhancement - Improve hover style on product tabs when using the Minimal style in the Product Details block [#50605](https://github.com/woocommerce/woocommerce/pull/50605) +* Enhancement - Make screen readers announce notice messages once page loads. [#50061](https://github.com/woocommerce/woocommerce/pull/50061) +* Enhancement - Refactor: Migrate the All Products block to API version 3 [#50203](https://github.com/woocommerce/woocommerce/pull/50203) +* Enhancement - Remove opacity from the hover style of the mini cart button [#50240](https://github.com/woocommerce/woocommerce/pull/50240) +* Enhancement - Use standard link color in legal disclaimers on core profiler [#50830](https://github.com/woocommerce/woocommerce/pull/50830) + = 9.2.3 2024-08-26 = **WooCommerce** diff --git a/docs/cart-and-checkout-blocks/additional-checkout-fields.md b/docs/cart-and-checkout-blocks/additional-checkout-fields.md index 29981df9bdc..89af92968c2 100644 --- a/docs/cart-and-checkout-blocks/additional-checkout-fields.md +++ b/docs/cart-and-checkout-blocks/additional-checkout-fields.md @@ -339,11 +339,11 @@ This results in the following address form (the billing form will be the same): The rendered markup looks like this: ```html - + data-custom="custom data" value="" > ``` ### Rendering a checkbox field diff --git a/docs/cart-and-checkout-blocks/available-filters/README.md b/docs/cart-and-checkout-blocks/available-filters/README.md index 6185107f41b..80eeab35f71 100644 --- a/docs/cart-and-checkout-blocks/available-filters/README.md +++ b/docs/cart-and-checkout-blocks/available-filters/README.md @@ -81,7 +81,7 @@ const modifyCartItemClass = ( defaultValue, extensions, args ) => { const modifyCartItemPrice = ( defaultValue, extensions, args ) => { if ( isOrderSummaryContext( args ) ) { - return ' for all items'; + return '<price/> for all items'; } return defaultValue; }; @@ -95,7 +95,7 @@ const modifyItemName = ( defaultValue, extensions, args ) => { const modifySubtotalPriceFormat = ( defaultValue, extensions, args ) => { if ( isOrderSummaryContext( args ) ) { - return ' per item'; + return '<price/> per item'; } return defaultValue; }; diff --git a/docs/cart-and-checkout-blocks/available-filters/order-summary-items.md b/docs/cart-and-checkout-blocks/available-filters/order-summary-items.md index ce4134df03c..c210fa39fcf 100644 --- a/docs/cart-and-checkout-blocks/available-filters/order-summary-items.md +++ b/docs/cart-and-checkout-blocks/available-filters/order-summary-items.md @@ -106,17 +106,17 @@ The `cartItemPrice` filter allows to format the order summary item price. ### Parameters -- _defaultValue_ `string` (default: ``) - The default order summary item price. +- _defaultValue_ `string` (default: `<price/>`) - The default order summary item price. - _extensions_ `object` (default: `{}`) - The extensions object. - _args_ `object` - The arguments object with the following keys: - _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](#cart-object). - _cartItem_ `object` - The order summary item object from `wc/store/cart`, see [order summary item object](#cart-item-object). - _context_ `string` (allowed values: `cart` or `summary`) - The context of the item. -- _validation_ `boolean` - Checks if the return value contains the substring ``. +- _validation_ `boolean` - Checks if the return value contains the substring `<price/>`. ### Returns -- `string` - The modified format of the order summary item price, which must contain the substring ``, or the original price format. +- `string` - The modified format of the order summary item price, which must contain the substring `<price/>`, or the original price format. ### Code examples @@ -132,7 +132,7 @@ const modifyCartItemPrice = ( defaultValue, extensions, args, validation ) => { return defaultValue; } - return ' for all items'; + return '<price/> for all items'; }; registerCheckoutFilters( 'example-extension', { @@ -153,14 +153,14 @@ const modifyCartItemPrice = ( defaultValue, extensions, args, validation ) => { } if ( args?.cartItem?.name === 'Beanie with Logo' ) { - return ' to keep you ☀️'; + return '<price/> to keep you ☀️'; } if ( args?.cartItem?.name === 'Sunglasses' ) { - return ' to keep you ❄️'; + return '<price/> to keep you ❄️'; } - return ' for all items'; + return '<price/> for all items'; }; registerCheckoutFilters( 'example-extension', { @@ -261,17 +261,17 @@ The `subtotalPriceFormat` filter allows to format the order summary item subtota ### Parameters -- _defaultValue_ `string` (default: ``) - The default order summary item subtotal price. +- _defaultValue_ `string` (default: `<price/>`) - The default order summary item subtotal price. - _extensions_ `object` (default: `{}`) - The extensions object. - _args_ `object` - The arguments object with the following keys: - _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](#cart-object). - _cartItem_ `object` - The order summary item object from `wc/store/cart`, see [order summary item object](#cart-item-object). - _context_ `string` (allowed values: `cart` or `summary`) - The context of the item. -- _validation_ `boolean` - Checks if the return value contains the substring ``. +- _validation_ `boolean` - Checks if the return value contains the substring `<price/>`. ### Returns -- `string` - The modified format of the order summary item subtotal price, which must contain the substring ``, or the original price format. +- `string` - The modified format of the order summary item subtotal price, which must contain the substring `<price/>`, or the original price format. ### Code examples @@ -292,7 +292,7 @@ const modifySubtotalPriceFormat = ( return defaultValue; } - return ' per item'; + return '<price/> per item'; }; registerCheckoutFilters( 'example-extension', { @@ -318,14 +318,14 @@ const modifySubtotalPriceFormat = ( } if ( args?.cartItem?.name === 'Beanie with Logo' ) { - return ' per warm beanie'; + return '<price/> per warm beanie'; } if ( args?.cartItem?.name === 'Sunglasses' ) { - return ' per cool sunglasses'; + return '<price/> per cool sunglasses'; } - return ' per item'; + return '<price/> per item'; }; registerCheckoutFilters( 'example-extension', { diff --git a/docs/cart-and-checkout-blocks/available-filters/totals-footer-item.md b/docs/cart-and-checkout-blocks/available-filters/totals-footer-item.md index 360c7627b80..c3a72631330 100644 --- a/docs/cart-and-checkout-blocks/available-filters/totals-footer-item.md +++ b/docs/cart-and-checkout-blocks/available-filters/totals-footer-item.md @@ -71,11 +71,11 @@ The `totalValue` filter allows to format the total price in the footer of the Ca - _extensions_ `object` (default: `{}`) - The extensions object. - _args_ `object` - The arguments object with the following keys: - _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](#cart-object). -- _validation_ `boolean` - Checks if the return value contains the substring ``. +- _validation_ `boolean` - Checks if the return value contains the substring `<price/>`. ### Returns -- `string` - The modified format of the total price, which must contain the substring ``, or the original price format. +- `string` - The modified format of the total price, which must contain the substring `<price/>`, or the original price format. ### Code example @@ -83,7 +83,7 @@ The `totalValue` filter allows to format the total price in the footer of the Ca const { registerCheckoutFilters } = window.wc.blocksCheckout; const modifyTotalsPrice = ( defaultValue, extensions, args, validation ) => { - return 'Pay now'; + return 'Pay <price/> now'; }; registerCheckoutFilters( 'my-extension', { diff --git a/docs/cart-and-checkout-blocks/available-slot-fills.md b/docs/cart-and-checkout-blocks/available-slot-fills.md index 80effcba522..efc47abcfa2 100644 --- a/docs/cart-and-checkout-blocks/available-slot-fills.md +++ b/docs/cart-and-checkout-blocks/available-slot-fills.md @@ -22,11 +22,11 @@ const { ExperimentalOrderMeta } = window.wc.blocksCheckout; const render = () => { return ( - -
+ <ExperimentalOrderMeta> + <div class="wc-block-components-totals-wrapper"> { __( 'Yearly recurring total ...', 'YOUR-TEXTDOMAIN' ) } -
-
+ </div> + </ExperimentalOrderMeta> ); }; @@ -61,9 +61,9 @@ const { ExperimentalOrderShippingPackages } = window.wc.blocksCheckout; const render = () => { return ( - -
{ __( 'Express Shipping', 'YOUR-TEXTDOMAIN' ) }
-
+ <ExperimentalOrderShippingPackages> + <div>{ __( 'Express Shipping', 'YOUR-TEXTDOMAIN' ) }</div> + </ExperimentalOrderShippingPackages> ); }; @@ -104,14 +104,14 @@ const { ExperimentalOrderLocalPickupPackages } = window.wc.blocksCheckout; const render = () => { return ( - -
+ <ExperimentalOrderLocalPickupPackages> + <div> { __( 'By using our convenient local pickup option, you can come to our store and pick up your order. We will send you and email when your order is ready for pickup.', 'YOUR-TEXTDOMAIN' ) } -
-
+ </div> + </ExperimentalOrderLocalPickupPackages> ); }; @@ -143,11 +143,11 @@ const { ExperimentalDiscountsMeta } = window.wc.blocksCheckout; const render = () => { return ( - -
+ <ExperimentalDiscountsMeta> + <div class="wc-block-components-totals-wrapper"> { __( 'You have 98683 coins to spend ...', 'YOUR-TEXTDOMAIN' ) } -
-
+ </div> + </ExperimentalDiscountsMeta> ); }; diff --git a/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md b/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md index 602bcb7deaa..d3457214654 100644 --- a/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md +++ b/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md @@ -43,8 +43,8 @@ The options you feed the configuration instance should be an object in this shap ```js const options = { name: 'my_payment_method', - content:
A React node
, - edit:
A React node
, + content: <div>A React node</div>, + edit: <div>A React node</div>, canMakePayment: () => true, paymentMethodId: 'new_payment_method', supports: { diff --git a/docs/cart-and-checkout-blocks/slot-fills.md b/docs/cart-and-checkout-blocks/slot-fills.md index d694d03bb1e..38003d4921f 100644 --- a/docs/cart-and-checkout-blocks/slot-fills.md +++ b/docs/cart-and-checkout-blocks/slot-fills.md @@ -32,14 +32,14 @@ const { registerPlugin } = wp.plugins; const { ExperimentalOrderMeta } = wc.blocksCheckout; const MyCustomComponent = ( { cart, extensions } ) => { - return
Hello WooCommerce
; + return <div className="my-component">Hello WooCommerce</div>; }; const render = () => { return ( - - - + <ExperimentalOrderMeta> + <MyCustomComponent /> + </ExperimentalOrderMeta> ); }; diff --git a/docs/code-snippets/customising-checkout-fields.md b/docs/code-snippets/customising-checkout-fields.md index 4b8aa96aee8..ab90d600be1 100644 --- a/docs/code-snippets/customising-checkout-fields.md +++ b/docs/code-snippets/customising-checkout-fields.md @@ -295,7 +295,7 @@ function custom_override_checkout_fields( $fields ) { add_action( 'woocommerce_admin_order_data_after_shipping_address', 'my_custom_checkout_field_display_admin_order_meta', 10, 1 ); function my_custom_checkout_field_display_admin_order_meta($order){ - echo '

'. esc_html__( 'Phone From Checkout Form' ) . ': ' . esc_html( $order->get_meta( '_shipping_phone', true ) ) . '

'; + echo '<p><strong>'. esc_html__( 'Phone From Checkout Form' ) . ':</strong> ' . esc_html( $order->get_meta( '_shipping_phone', true ) ) . '</p>'; } ``` @@ -317,7 +317,7 @@ add_action( 'woocommerce_after_order_notes', 'my_custom_checkout_field' ); function my_custom_checkout_field( $checkout ) { - echo '

' . esc_html__( 'My Field' ) . '

'; + echo '<div id="my_custom_checkout_field"><h2>' . esc_html__( 'My Field' ) . '</h2>'; woocommerce_form_field( 'my_field_name', @@ -330,7 +330,7 @@ function my_custom_checkout_field( $checkout ) { $checkout->get_value( 'my_field_name' ) ); - echo '
'; + echo '</div>'; } ``` @@ -387,7 +387,7 @@ If you wish to display the custom field value on the admin order edition page, y add_action( 'woocommerce_admin_order_data_after_billing_address', 'my_custom_checkout_field_display_admin_order_meta', 10, 1 ); function my_custom_checkout_field_display_admin_order_meta( $order ){ - echo '

' . esc_html__( 'My Field' ) . ': ' . esc_html( $order->get_meta( 'My Field', true ) ) . '

'; + echo '<p><strong>' . esc_html__( 'My Field' ) . ':</strong> ' . esc_html( $order->get_meta( 'My Field', true ) ) . '</p>'; } ``` diff --git a/docs/code-snippets/displaying_custom_fields_in_your_theme_or_site.md b/docs/code-snippets/displaying_custom_fields_in_your_theme_or_site.md index 0549ead8a57..6d3caeedb2a 100644 --- a/docs/code-snippets/displaying_custom_fields_in_your_theme_or_site.md +++ b/docs/code-snippets/displaying_custom_fields_in_your_theme_or_site.md @@ -33,7 +33,7 @@ function woocommerce_custom_field_example() { $custom_field_value = get_post_meta( $product->get_id(), 'woo_custom_field', true ); if ( ! empty( $custom_field_value ) ) { - echo '
' . esc_html( $custom_field_value ) . '
'; + echo '<div class="custom-field">' . esc_html( $custom_field_value ) . '</div>'; } } diff --git a/docs/code-snippets/link-to-logged-data.md b/docs/code-snippets/link-to-logged-data.md index a4d31dd362b..d2fdf8da947 100644 --- a/docs/code-snippets/link-to-logged-data.md +++ b/docs/code-snippets/link-to-logged-data.md @@ -19,7 +19,7 @@ if ( defined( 'WC_LOG_DIR' ) ) { $log_url = add_query_arg( 'log_file', $log_key, $log_url ); // Add a link to the logs to the label - $label .= ' | ' . sprintf( \_\_( '%1$sView Log%2$s', 'your-textdomain-here' ), '', '' ); + $label .= ' | ' . sprintf( \_\_( '%1$sView Log%2$s', 'your-textdomain-here' ), '<a href\="' . esc_url( $log_url ) . '">', '</a\>' ); } // Add the logging option to the form fields diff --git a/docs/docs-manifest.json b/docs/docs-manifest.json index f2a9ecc4fc2..d18a4367869 100644 --- a/docs/docs-manifest.json +++ b/docs/docs-manifest.json @@ -100,7 +100,7 @@ "menu_title": "Slot and Fill", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/cart-and-checkout-blocks/slot-fills.md", - "hash": "f83a5fbef86e5ef6b0ec1d63fdbcbf4742f54de1125e535fa0f32f5f80ec794a", + "hash": "a232ca3d53f10857170113f6dc5b37ac7ae629e852629bac015a8d3c2cd1bbc4", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/cart-and-checkout-blocks/slot-fills.md", "id": "e388101586765dd9aca752d66d667d74951a1504" }, @@ -136,7 +136,7 @@ "menu_title": "Available Slots", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/cart-and-checkout-blocks/available-slot-fills.md", - "hash": "770da9156eea1fdc24db0736ce4ccd44ebde4f3b0373cd875b1ae88d4d9c8a49", + "hash": "444d9892cb6552c8394ecdf81816952987b59bc79fa53f3083c3d14a89d1e961", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/cart-and-checkout-blocks/available-slot-fills.md", "id": "c7ac16eee5540b06b6db928f5d03282ff177e84e" }, @@ -145,14 +145,14 @@ "menu_title": "Additional Checkout Fields", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/cart-and-checkout-blocks/additional-checkout-fields.md", - "hash": "1b034ede098b933b6b00a9a27ba33e418b1c88c4883e2b9b191092e32866f7b9", + "hash": "641615864f627be4bb42574df378cea91f4a7fda9edab099558bad06b92ce62d", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/cart-and-checkout-blocks/additional-checkout-fields.md", "id": "cb5dd8d59043a4e53929121b45da7b33b1661ab8" } ], "categories": [ { - "content": "\nThis document lists the filters that are currently available to extensions and offers usage information for each one of them. Information on registering filters can be found on the [Checkout - Filter Registry](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/packages/checkout/filter-registry/README.md) page.\n\n## Cart Line Items filters\n\nThe following [Cart Line Items filters](./cart-line-items.md) are available:\n\n- `cartItemClass`\n- `cartItemPrice`\n- `itemName`\n- `saleBadgePriceFormat`\n- `showRemoveItemLink`\n- `subtotalPriceFormat`\n\nThe following screenshot shows which parts the individual filters affect:\n\n![Cart Line Items](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-26-at-13.12.33.png)\n\n## Order Summary Items filters\n\nThe following [Order Summary Items filters](./order-summary-items.md) are available:\n\n- `cartItemClass`\n- `cartItemPrice`\n- `itemName`\n- `subtotalPriceFormat`\n\nThe following screenshot shows which parts the individual filters affect:\n\n![Order Summary Items](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-26-at-16.29.45.png)\n\n## Totals Footer Item filter\n\nThe following [Totals Footer Item filter](./totals-footer-item.md) is available:\n\n- `totalLabel`\n- `totalValue`\n\n## Checkout and place order button filters\n\nThe following [Checkout and place order button filters](./checkout-and-place-order-button.md) are available:\n\n- `proceedToCheckoutButtonLabel`\n- `proceedToCheckoutButtonLink`\n- `placeOrderButtonLabel`\n\n## Coupon filters\n\nThe following [Coupon filters](./coupons.md) are available:\n\n- `coupons`\n- `showApplyCouponNotice`\n- `showRemoveCouponNotice`\n\n## Additional Cart and Checkout inner block types filter\n\nThe following [Additional Cart and Checkout inner block types filter](./additional-cart-checkout-inner-block-types.md) is available:\n\n- `additionalCartCheckoutInnerBlockTypes`\n\n## Combined filters\n\nFilters can also be combined. The following example shows how to combine some of the available filters.\n\n```tsx\nconst { registerCheckoutFilters } = window.wc.blocksCheckout;\n\nconst isOrderSummaryContext = ( args ) => args?.context === 'summary';\n\nconst modifyCartItemClass = ( defaultValue, extensions, args ) => {\n\tif ( isOrderSummaryContext( args ) ) {\n\t\treturn 'my-custom-class';\n\t}\n\treturn defaultValue;\n};\n\nconst modifyCartItemPrice = ( defaultValue, extensions, args ) => {\n\tif ( isOrderSummaryContext( args ) ) {\n\t\treturn ' for all items';\n\t}\n\treturn defaultValue;\n};\n\nconst modifyItemName = ( defaultValue, extensions, args ) => {\n\tif ( isOrderSummaryContext( args ) ) {\n\t\treturn `${ defaultValue }`;\n\t}\n\treturn defaultValue;\n};\n\nconst modifySubtotalPriceFormat = ( defaultValue, extensions, args ) => {\n\tif ( isOrderSummaryContext( args ) ) {\n\t\treturn ' per item';\n\t}\n\treturn defaultValue;\n};\n\nregisterCheckoutFilters( 'example-extension', {\n\tcartItemClass: modifyCartItemClass,\n\tcartItemPrice: modifyCartItemPrice,\n\titemName: modifyItemName,\n\tsubtotalPriceFormat: modifySubtotalPriceFormat,\n} );\n```\n\n## Troubleshooting\n\nIf you are logged in to the store as an administrator, you should be shown an error like this if your filter is not\nworking correctly. The error will also be shown in your console.\n\n![Troubleshooting](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-30-at-10.52.53.png)\n\n\n", + "content": "\nThis document lists the filters that are currently available to extensions and offers usage information for each one of them. Information on registering filters can be found on the [Checkout - Filter Registry](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/packages/checkout/filter-registry/README.md) page.\n\n## Cart Line Items filters\n\nThe following [Cart Line Items filters](./cart-line-items.md) are available:\n\n- `cartItemClass`\n- `cartItemPrice`\n- `itemName`\n- `saleBadgePriceFormat`\n- `showRemoveItemLink`\n- `subtotalPriceFormat`\n\nThe following screenshot shows which parts the individual filters affect:\n\n![Cart Line Items](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-26-at-13.12.33.png)\n\n## Order Summary Items filters\n\nThe following [Order Summary Items filters](./order-summary-items.md) are available:\n\n- `cartItemClass`\n- `cartItemPrice`\n- `itemName`\n- `subtotalPriceFormat`\n\nThe following screenshot shows which parts the individual filters affect:\n\n![Order Summary Items](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-26-at-16.29.45.png)\n\n## Totals Footer Item filter\n\nThe following [Totals Footer Item filter](./totals-footer-item.md) is available:\n\n- `totalLabel`\n- `totalValue`\n\n## Checkout and place order button filters\n\nThe following [Checkout and place order button filters](./checkout-and-place-order-button.md) are available:\n\n- `proceedToCheckoutButtonLabel`\n- `proceedToCheckoutButtonLink`\n- `placeOrderButtonLabel`\n\n## Coupon filters\n\nThe following [Coupon filters](./coupons.md) are available:\n\n- `coupons`\n- `showApplyCouponNotice`\n- `showRemoveCouponNotice`\n\n## Additional Cart and Checkout inner block types filter\n\nThe following [Additional Cart and Checkout inner block types filter](./additional-cart-checkout-inner-block-types.md) is available:\n\n- `additionalCartCheckoutInnerBlockTypes`\n\n## Combined filters\n\nFilters can also be combined. The following example shows how to combine some of the available filters.\n\n```tsx\nconst { registerCheckoutFilters } = window.wc.blocksCheckout;\n\nconst isOrderSummaryContext = ( args ) => args?.context === 'summary';\n\nconst modifyCartItemClass = ( defaultValue, extensions, args ) => {\n\tif ( isOrderSummaryContext( args ) ) {\n\t\treturn 'my-custom-class';\n\t}\n\treturn defaultValue;\n};\n\nconst modifyCartItemPrice = ( defaultValue, extensions, args ) => {\n\tif ( isOrderSummaryContext( args ) ) {\n\t\treturn '<price/> for all items';\n\t}\n\treturn defaultValue;\n};\n\nconst modifyItemName = ( defaultValue, extensions, args ) => {\n\tif ( isOrderSummaryContext( args ) ) {\n\t\treturn `${ defaultValue }`;\n\t}\n\treturn defaultValue;\n};\n\nconst modifySubtotalPriceFormat = ( defaultValue, extensions, args ) => {\n\tif ( isOrderSummaryContext( args ) ) {\n\t\treturn '<price/> per item';\n\t}\n\treturn defaultValue;\n};\n\nregisterCheckoutFilters( 'example-extension', {\n\tcartItemClass: modifyCartItemClass,\n\tcartItemPrice: modifyCartItemPrice,\n\titemName: modifyItemName,\n\tsubtotalPriceFormat: modifySubtotalPriceFormat,\n} );\n```\n\n## Troubleshooting\n\nIf you are logged in to the store as an administrator, you should be shown an error like this if your filter is not\nworking correctly. The error will also be shown in your console.\n\n![Troubleshooting](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-30-at-10.52.53.png)\n\n\n", "category_slug": "cart-and-checkout-available-filters", "category_title": "Available Filters", "posts": [ @@ -161,7 +161,7 @@ "menu_title": "Totals Footer Item", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/cart-and-checkout-blocks/available-filters/totals-footer-item.md", - "hash": "3a9869d7d7beadb8117c100c3b58675e416e16386ee753f78e1a9087e768053f", + "hash": "6cf668422809b036dca7c1996ae907497a38631dd5bfb7e67d6bf3620425e411", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/cart-and-checkout-blocks/available-filters/totals-footer-item.md", "id": "90a9b8df374082f1713866a58b810303adb4d3da" }, @@ -170,7 +170,7 @@ "menu_title": "Order Summary Items", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/cart-and-checkout-blocks/available-filters/order-summary-items.md", - "hash": "36f1bfa8d192b106d28d71334b42413d4c289a0a8d1f5b76b2f905d6fa453883", + "hash": "1796f53f3d67dd6b47fe8d7f67cbd69bddcaa6416bb5a0cc1a0fc99f42ea9d10", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/cart-and-checkout-blocks/available-filters/order-summary-items.md", "id": "78eb3b135f82a3624a49979e3e93334295abd060" }, @@ -223,7 +223,7 @@ "menu_title": "Payment Method Integration", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md", - "hash": "f60acaaea4a6ac4adf637bc7069c966e01db089f9dfaa937def91165a71a4255", + "hash": "138ffbf27e79ec8b35d2c46e87e3663c203d91fc9ba3f76c43f3cbe76258e5bf", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md", "id": "c9a763b6976ecf03aeb961577c17c31f1ac7c420", "links": { @@ -363,7 +363,7 @@ "menu_title": "Add link to logged data", "tags": "code-snippets", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/link-to-logged-data.md", - "hash": "fd1c3a58da8b7eed11da841d901b4d3cc117c6753c3b3834f3de41ea266490b9", + "hash": "4e51c120a6ea7b14c0e43f11e8eb1b785e4447fbe2b997f5789f10b57c485137", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/link-to-logged-data.md", "id": "34da337f79be5ce857024f541a99d302174ca37d" }, @@ -389,7 +389,7 @@ "menu_title": "Displaying custom fields in theme", "tags": "code-snippet", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/displaying_custom_fields_in_your_theme_or_site.md", - "hash": "8048c2e9e5d25268d17d4f4ca7929e265eddbd4653318dd8f544856ddecd39dd", + "hash": "013acf9daaef92daf49e49315b2c0eba730b96adb8078eaab1146db4afc5270b", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/displaying_custom_fields_in_your_theme_or_site.md", "id": "3e3fd004afda355cf9dbb05f0967523d6d0da1ce" }, @@ -405,7 +405,7 @@ "post_title": "Customizing checkout fields using actions and filters", "tags": "code-snippet", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/customising-checkout-fields.md", - "hash": "8bbfe162402e484ae89427e1aedaed4faa57555b64b5a77ca800f701524314cb", + "hash": "ce63f640d5b91d85c3bbb80128d8a19e9c00d1c0e252abd4f958e29dcc1e60ce", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/customising-checkout-fields.md", "id": "83097d3b7414557fc80dcf9f8f1a708bbdcdd884" }, @@ -676,7 +676,7 @@ { "post_title": "Settings API", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/settings-api.md", - "hash": "ca80728c56d60bb7416bb2865678b9e04807d0e208a4df56b8efaf32e9ac465d", + "hash": "9015453d8be72871bb26a450b86e542aa698c67b93284a04cd2b18008113bf43", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/settings-api.md", "id": "ed56b97b9de350074a302373ebaaa5dcce727e8b" }, @@ -691,7 +691,7 @@ "post_title": "Integrating with coming soon mode", "tags": "how-to, coming-soon", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/integrating-coming-soon-mode.md", - "hash": "8c2087952ae79bb4c3e3977c57d9e933fcfaa418a5bc643b3827059daa5879a7", + "hash": "791cd6d3928b3aafc72a24d0283a404a90a0f021c7c36edaa445eb44978114a3", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/integrating-coming-soon-mode.md", "id": "787743efb6ef0ad509b17735eaf58b2a9a08afbc" }, @@ -700,7 +700,7 @@ "menu_title": "Creating custom settings", "tags": "how-to", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/implementing-settings.md", - "hash": "604d455f9e413c23a208c174ba25611c333e02eef0bafb0d38253f8dd8e3a04c", + "hash": "5cab83a84bb7eb11090bac244754fdae1f8aef1030850d12c29c09054c50bc61", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/implementing-settings.md", "id": "58bcbd3a0cd3b3e5fe738c3bb625cf9b7747c99a" }, @@ -727,7 +727,7 @@ "menu_title": "Implement merchant onboarding", "tags": "how-to", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/handling-merchant-onboarding.md", - "hash": "c73e3c5015e6cda3be9ebd2d5fbda590ac9fa599e5fb02163c971c01060970ad", + "hash": "85fc7d70f47fdb195ad2c690d3b95565169221f9e4d7afa98e88f3824ad0e267", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/handling-merchant-onboarding.md", "id": "89fe15dc232379f546852822230c334d3d940b93" }, @@ -814,7 +814,7 @@ "menu_title": "Add custom product types to Add Products onboarding list", "tags": "how-to", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/adding-custom-products-to-add-products-onboarding-list.md", - "hash": "60e50ef5d7e2ac6d0745c31031140df1dbb3c1b8724230cab1eaedebe3814688", + "hash": "92a8e17f2cd8dc32a78f03970ded1beec2fd60cadbf14c8cefcabbf7abae59c5", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/adding-custom-products-to-add-products-onboarding-list.md", "id": "747321d7fd2eb5c9c3351ea38374dfc80d3ec968" }, @@ -861,7 +861,7 @@ "menu_title": "Troubleshooting Endpoints", "tags": "how-to", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/troubleshooting-endpoints.md", - "hash": "1a015d82f4d82cc2d9f13f188f03c4e6e03b98ea9d22c5a7710547e7d3c8c78f", + "hash": "448bcd827ff44e9eb10d039bfd933cd63a37df05bd694bf80f9d9f978a3afdf5", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/troubleshooting-endpoints.md", "id": "dff57bd736ae83850bfc7e4ac994bd22141d96ee", "links": { @@ -874,7 +874,7 @@ "menu_title": "Development environment setup", "tags": "tutorial, setup", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/development-environment.md", - "hash": "9e471d3f44a882fe61dcad9e5207d51b280a7220aae1bf6e4ae1fbdd68b7e3d4", + "hash": "bf5d77349ea64d1b8e19fe6b7472be35ed92406c5aafe677ce92363fb13f94d4", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/development-environment.md", "id": "9080572a3904349c44c565ca7e1bef1212c58757" }, @@ -903,7 +903,7 @@ "menu_title": "Customizing Endpoint URLs", "tags": "how-to", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/customizing-endpoint-urls.md", - "hash": "7feda75b07a1c11d533afabc7781abb80438ce2fa2c3fb37c173e1275098e720", + "hash": "364ed14d70c49498ba5017104b9c83743322d5095c215262d4311866a76181e5", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/customizing-endpoint-urls.md", "id": "c19e1b1da6543f8a95ee04ba120f4f171f8e6e40", "links": { @@ -956,7 +956,7 @@ "post_title": "HPOS CLI Tools", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/high-performance-order-storage/cli-tools.md", - "hash": "8cd823759ce20551d582c39f57ae79f9e0227a8cb0131146e6b7dac5e7312708", + "hash": "63e5edd55720c963de6700854515ea51946ff734b716ab61793955308b72af91", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/high-performance-order-storage/cli-tools.md", "id": "cdd9d9ad5777d978ba953e3478fbb61cab8fdf59" } @@ -1050,7 +1050,7 @@ "menu_title": "Registering custom collections", "tags": "how-to", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-collection-block/register-product-collection.md", - "hash": "27c321bed35524d74019e015f5eed6cdca7e6c2efe0bc89ffdd2b9b5d43c47e8", + "hash": "6d32bc27924226b032e03624dbeedde3c899c2e8eb777a1fece93bed99544f03", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-collection-block/register-product-collection.md", "id": "3bf26fc7c56ae6e6a56e1171f750f5204fcfcece" }, @@ -1059,7 +1059,7 @@ "menu_title": "DOM Events", "tags": "how-to", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-collection-block/dom-events.md", - "hash": "59a4b49eb146774d33229bc60ab7d8f74381493f6e7089ca8f0e2d0eb433a7a4", + "hash": "85cffe1cc273621f16f7362b5efe28ede9689cee0a6e87d0d426014bacc24b05", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-collection-block/dom-events.md", "id": "c8d247b91472740075871e6b57a9583d893ac650" } @@ -1117,7 +1117,7 @@ { "post_title": "Extending the product form with custom fields", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-editor-development/how-to-guides/custom-field-tutorial.md", - "hash": "f0d0273c0d65739d605448492bfbe684f0ed33f9e6e274df06f26e83cb6ba341", + "hash": "dfa00ed71af6eda1f539684657d5c880850ececea4c07bd11e89a605fab77ec7", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-editor-development/how-to-guides/custom-field-tutorial.md", "id": "fed80efbb225df9054fadd6e1fc45c2cd03e7f99" } @@ -1229,7 +1229,7 @@ "menu_title": "Core critical flows", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/core-critical-flows.md", - "hash": "472f5a240abe907fec83a8a9f88af6699f2d994aa7ae87faa1716a087baa66db", + "hash": "34109195216ebcb5b23e741391b9f355ba861777a5533d4ef1e341472cb5209e", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/core-critical-flows.md", "id": "e561b46694dba223c38b87613ce4907e4e14333a" }, @@ -1329,7 +1329,7 @@ "menu_title": "Extend analytics reports", "tags": "how-to", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/reporting/extending-woocommerce-admin-reports.md", - "hash": "b694b0e857d3ca60acdef2ffaae329a93f0a0243eacc4b192562c7f507f169b3", + "hash": "56712b3583d0b0a4d96eb19153e5abcb8a386fcd083fa56481acf1be530afa25", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/reporting/extending-woocommerce-admin-reports.md", "id": "3ef20148084c97d7f62b565b92df844392ac27f7" }, @@ -1498,7 +1498,7 @@ "post_title": "Classic theme development handbook", "menu_title": "Classic theme development", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/theme-development/classic-theme-developer-handbook.md", - "hash": "1194437fbc2ec82d60c8b73a9742ec650bd90fe734758c3a2b27ed852d4d14f7", + "hash": "95ce7250479a5133bba6c68939d86e4e79708c65044d70727c73f6a88f716da7", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/theme-development/classic-theme-developer-handbook.md", "id": "c2fde53e1dc3efbded3cfe1fb4df27136a3799a4" } @@ -1775,7 +1775,7 @@ "menu_title": "Commands", "tags": "reference", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/wc-cli/wc-cli-commands.md", - "hash": "a926ff45642539e0edc6b4e3dfeba4b31c2d01082700af132a2e8d56cfa25ec5", + "hash": "17bbb18fd0ad0523a5b864f74acbec64c853ae7b42ecd7e6d9dbce1fbe2669aa", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/wc-cli/wc-cli-commands.md", "id": "73d6bc6468d23a9e93d16d574399105b143e43af" }, @@ -1804,5 +1804,5 @@ "categories": [] } ], - "hash": "ffae56d5a4993b151a54ec2117be1acf6a02c9fcab5b5662a2a583ea0e743f1d" + "hash": "47dc2a7e213e1e9d83e93a85dafdf8c7b539cc5474b4166eb6398b734d150ff3" } \ No newline at end of file diff --git a/docs/extension-development/adding-custom-products-to-add-products-onboarding-list.md b/docs/extension-development/adding-custom-products-to-add-products-onboarding-list.md index 4d50b8e4d31..f85464aa86f 100644 --- a/docs/extension-development/adding-custom-products-to-add-products-onboarding-list.md +++ b/docs/extension-development/adding-custom-products-to-add-products-onboarding-list.md @@ -45,8 +45,8 @@ addFilter( key: 'custom-product', title: __('Custom product', 'custom-product'), content: __('Create an awesome custom product.', 'custom-product'), - before: , - after: , + before: <FolderMultipleIcon />, + after: <Icon icon={chevronRight} />, onClick: () => { } }, diff --git a/docs/extension-development/handling-merchant-onboarding.md b/docs/extension-development/handling-merchant-onboarding.md index 3dec70d6f89..5b3adaed57d 100644 --- a/docs/extension-development/handling-merchant-onboarding.md +++ b/docs/extension-development/handling-merchant-onboarding.md @@ -448,7 +448,7 @@ class ExampleNote { $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); // Set the type of layout the note uses. Supported layout types are: - // 'banner', 'plain', 'thumbnail' + // 'plain', 'thumbnail' $note->set_layout( 'plain' ); // Set the image for the note. This property renders as the src diff --git a/docs/extension-development/implementing-settings.md b/docs/extension-development/implementing-settings.md index 4b86c41b714..360fb1e09a6 100644 --- a/docs/extension-development/implementing-settings.md +++ b/docs/extension-development/implementing-settings.md @@ -227,19 +227,19 @@ public function generate_button_html( $key, $data ) { ob_start(); ?> - - - + <tr valign="top"> + <th scope="row" class="titledesc"> + <label for=""></label> get_tooltip_html( $data ); ?> - - -
- - + </th> + <td class="forminp"> + <fieldset< + <legend class="screen-reader-text"><span>" type="button" name="" id="" style="" get_custom_attribute_html( $data ); ?>></button> get_description_html( $data ); ?> -
- - + </fieldset> + </td> + </tr> ) { + if ( get_the_ID() === <page-id> ) { return true; } return $is_excluded; diff --git a/docs/extension-development/settings-api.md b/docs/extension-development/settings-api.md index 6078ceca42a..60ed624a3fe 100644 --- a/docs/extension-development/settings-api.md +++ b/docs/extension-development/settings-api.md @@ -70,10 +70,10 @@ Create a method called `admin_options` containing the following: ```php function admin_options() { ?> -

- + <h2></h2> + <table class="form-table"> generate_settings_html(); ?> -
+ </table> - - - - - - - - - - - - - - - - - +<<>?xml version="1.0" encoding="UTF-8"?> +<configuration> + <system.webServer> + <handlers accessPolicy="Read, Execute, Script" /> + <rewrite> + <rules> + <rule name="wordpress" patternSyntax="Wildcard"> + <match url="*" /> + <conditions> + <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> + <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> + </conditions> + <action type="Rewrite" url="index.php" /> + </rule> + </rules> + </rewrite> + </system.webServer> +</configuration> ``` ### Pages direct to wrong place @@ -74,6 +74,6 @@ Landing on the wrong page when clicking an endpoint URL is typically caused by i ### How to Remove "Downloads" from My Account -Sometimes the "Downloads" endpoint on the "My account" page does not need to be displayed. This can be removed by going to **WooCommerce → Settings → Advanced → Account endpoints** and clearing the Downloads endpoint field. +Sometimes the "Downloads" endpoint on the "My account" page does not need to be displayed. This can be removed by going to **WooCommerce > Settings > Advanced > Account endpoints** and clearing the Downloads endpoint field. ![Account endpoints](https://developer.woocommerce.com/wp-content/uploads/2023/12/Screenshot-2023-04-09-at-11.45.58-PM.png) diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index 0701be60fc1..989587d4153 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -80,22 +80,16 @@ git clone https://github.com/woocommerce/woocommerce.git cd woocommerce ``` -### Activate the required Node version +### Install and Activate the required Node version ```sh -nvm use -Found '/path/to/woocommerce/.nvmrc' with version -Now using node v12.21.0 (npm v6.14.11) +nvm install +Found '/path/to/woocommerce/.nvmrc' with version +v20.17.0 is already installed. +Now using node v20.17.0 (npm v10.8.2) ``` -Note: if you don't have the required version of Node installed, NVM will alert you so you can install it: - -```sh -Found '/path/to/woocommerce/.nvmrc' with version -N/A: version "v12 -> N/A" is not yet installed. - -You need to run "nvm install v12" to install it before using it. -``` +Note: if you don't have the required version of Node installed, NVM will install it. ### Install dependencies diff --git a/docs/getting-started/troubleshooting-endpoints.md b/docs/getting-started/troubleshooting-endpoints.md index 467d9dba1cb..11de5172810 100644 --- a/docs/getting-started/troubleshooting-endpoints.md +++ b/docs/getting-started/troubleshooting-endpoints.md @@ -18,24 +18,24 @@ For more information, learn how to [Customize Endpoints](./customizing-endpoint- On Windows servers, the **web.config** file may not be set correctly to allow for the endpoints to work correctly. In this case, clicking on endpoint links (e.g. /edit-account/ or /customer-logout/) may appear to do nothing except refresh the page. In order to resolve this, try simplifying the **web.config** file on your Windows server. Here's a sample file configuration: ```xml - - - - - - - - - - - - - - - - - - +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <system.webServer> + <handlers accessPolicy="Read, Execute, Script" /> + <rewrite> + <rules> + <rule name="wordpress" patternSyntax="Wildcard"> + <match url="*" /> + <conditions> + <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> + <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> + </conditions> + <action type="Rewrite" url="index.php" /> + </rule> + </rules> + </rewrite> + </system.webServer> +</configuration> ``` ## Pages direct to wrong place @@ -44,6 +44,6 @@ Landing on the wrong page when clicking an endpoint URL is typically caused by i ## How to Remove "Downloads" from My Account -Sometimes the "Downloads" endpoint on the "My account" page does not need to be displayed. This can be removed by going to **WooCommerce → Settings → Advanced → Account endpoints** and clearing the Downloads endpoint field. +Sometimes the "Downloads" endpoint on the "My account" page does not need to be displayed. This can be removed by going to **WooCommerce > Settings > Advanced > Account endpoints** and clearing the Downloads endpoint field. ![Account endpoints](https://developer.woocommerce.com/wp-content/uploads/2023/12/Screenshot-2023-04-09-at-11.45.58-PM.png) diff --git a/docs/high-performance-order-storage/cli-tools.md b/docs/high-performance-order-storage/cli-tools.md index e61035d60cd..c4b1877b9e5 100644 --- a/docs/high-performance-order-storage/cli-tools.md +++ b/docs/high-performance-order-storage/cli-tools.md @@ -229,7 +229,7 @@ The backfill command can be used to selectively migrate order data (or whole ord The exact syntax for this command is as follows: ```plaintext -wp wc hpos backfill --from= --to= [--meta_keys=] [--props=] +wp wc hpos backfill <order_id> --from=<datastore> --to=<datastore> [--meta_keys=<meta_keys>] [--props=<props>] ``` You have to specify which datastore to use as source (either `posts` or `hpos`) and which one to use as destination. The `--meta_keys` and `--props` arguments receive a comma separated list of meta keys and order properties, which can be used to move only certain data from one datastore to the other, instead of the whole order. diff --git a/docs/product-collection-block/dom-events.md b/docs/product-collection-block/dom-events.md index 940e49a929d..608a88d71f1 100644 --- a/docs/product-collection-block/dom-events.md +++ b/docs/product-collection-block/dom-events.md @@ -10,13 +10,13 @@ tags: how-to This event is triggered when Product Collection block was rendered or re-rendered (e.g. due to page change). -### `wc-blocks_product_list_rendered` - `detail` parameters +### `detail` parameters | Parameter | Type | Default value | Description | | ------------------ | ------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `collection` | string | `undefined` | Collection type. It's `undefined` for "create your own" collections as the type is not specified. For other Core collections it can be one of type: `woocommerce/product-collection/best-sellers`, `woocommerce/product-collection/featured`, `woocommerce/product-collection/new-arrivals`, `woocommerce/product-collection/on-sale`, `woocommerce/product-collection/top-rated`. For custom collections it will hold their name. | -### `wc-blocks_product_list_rendered` - Example usage +### Example usage ```javascript window.document.addEventListener( @@ -32,14 +32,14 @@ window.document.addEventListener( This event is triggered when some blocks are clicked in order to view product (redirect to product page). -### `wc-blocks_viewed_product` - `detail` parameters +### `detail` parameters | Parameter | Type | Default value | Description | | ------------------ | ------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `collection` | string | `undefined` | Collection type. It's `undefined` for "create your own" collections as the type is not specified. For other Core collections it can be one of type: `woocommerce/product-collection/best-sellers`, `woocommerce/product-collection/featured`, `woocommerce/product-collection/new-arrivals`, `woocommerce/product-collection/on-sale`, `woocommerce/product-collection/top-rated`. For custom collections it will hold their name. | | `productId` | number | | Product ID | -### `wc-blocks_viewed_product` Example usage +### Example usage ```javascript window.document.addEventListener( diff --git a/docs/product-collection-block/register-product-collection.md b/docs/product-collection-block/register-product-collection.md index 677668ba71e..b754746a679 100644 --- a/docs/product-collection-block/register-product-collection.md +++ b/docs/product-collection-block/register-product-collection.md @@ -36,7 +36,7 @@ We will explain important arguments that can be passed to `__experimentalRegiste A Collection is defined by an object that can contain the following fields: -- `name` (type `string`): A unique and machine-readable collection name. We recommend using the format `/product-collection/`. Both `` and `` should consist only of alphanumeric characters and hyphens (e.g., `my-plugin/product-collection/my-collection`). +- `name` (type `string`): A unique and machine-readable collection name. We recommend using the format `<plugin-name>/product-collection/<collection-name>`. Both `<plugin-name>` and `<collection-name>` should consist only of alphanumeric characters and hyphens (e.g., `my-plugin/product-collection/my-collection`). - `title` (type `string`): The title of the collection, which will be displayed in various places including the block inserter and collection chooser. - `description` (optional, type `string`): A human-readable description of the collection. - `innerBlocks` (optional, type `Array[]`): An array of inner blocks that will be added to the collection. If not provided, the default inner blocks will be used. diff --git a/docs/product-editor-development/how-to-guides/custom-field-tutorial.md b/docs/product-editor-development/how-to-guides/custom-field-tutorial.md index 64fb3203025..ae243d3c9dc 100644 --- a/docs/product-editor-development/how-to-guides/custom-field-tutorial.md +++ b/docs/product-editor-development/how-to-guides/custom-field-tutorial.md @@ -62,7 +62,7 @@ In React: import { registerBlockType } from '@wordpress/blocks'; function Edit() { - return

Hello World (from the editor).

; + return <p>Hello World (from the editor).</p>; } registerBlockType( 'tutorial/new-product-form-field', { @@ -217,8 +217,8 @@ function Edit( { attributes } ) { const blockProps = useWooBlockProps( attributes ); return ( -
- -
+ /> + </div> ); } ``` diff --git a/docs/quality-and-best-practices/core-critical-flows.md b/docs/quality-and-best-practices/core-critical-flows.md index d959b4d54e6..11fb8ec2466 100644 --- a/docs/quality-and-best-practices/core-critical-flows.md +++ b/docs/quality-and-best-practices/core-critical-flows.md @@ -147,13 +147,14 @@ These flows will continually evolve as the platform evolves with flows updated, ### Merchant - Settings | User Type | Flow Area | Flow Name | Test File | -| --------- | --------- | -------------------------------------- | ---------------------------------------- | +| --------- | --------- |----------------------------------------|------------------------------------------| | Merchant | Settings | Update General Settings | merchant/settings-general.spec.js | | Merchant | Settings | Add Tax Rates | merchant/settings-tax.spec.js | | Merchant | Settings | Add Shipping Zones | merchant/create-shipping-zones.spec.js | | Merchant | Settings | Add Shipping Classes | merchant/create-shipping-classes.spec.js | | Merchant | Settings | Enable local pickup for checkout block | merchant/settings-shipping.spec.js | | Merchant | Settings | Update payment settings | admin-tasks/payment.spec.js | +| Merchant | Settings | Handle Product Brands | merchant/create-product-brand.spec.js | ### Merchant - Coupons diff --git a/docs/reporting/extending-woocommerce-admin-reports.md b/docs/reporting/extending-woocommerce-admin-reports.md index 2abe7862fe3..fe328313aee 100644 --- a/docs/reporting/extending-woocommerce-admin-reports.md +++ b/docs/reporting/extending-woocommerce-admin-reports.md @@ -32,7 +32,7 @@ npm run create-wc-extension After choosing a name, move into that folder and start webpack to watch and build files. ```sh -cd ../ +cd ../<my-plugin-name> npm install npm start ``` diff --git a/docs/theme-development/classic-theme-developer-handbook.md b/docs/theme-development/classic-theme-developer-handbook.md index dcdd64c0b6f..3f34881afe9 100644 --- a/docs/theme-development/classic-theme-developer-handbook.md +++ b/docs/theme-development/classic-theme-developer-handbook.md @@ -73,11 +73,11 @@ add_action('woocommerce_before_main_content', 'my_theme_wrapper_start', 10); add_action('woocommerce_after_main_content', 'my_theme_wrapper_end', 10); function my_theme_wrapper_start() { - echo '
'; + echo '<section id="main">'; } function my_theme_wrapper_end() { - echo '
'; + echo '</section>'; } ``` diff --git a/docs/wc-cli/wc-cli-commands.md b/docs/wc-cli/wc-cli-commands.md index 06606bdb500..aa7d3e3a646 100644 --- a/docs/wc-cli/wc-cli-commands.md +++ b/docs/wc-cli/wc-cli-commands.md @@ -117,7 +117,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope ### wc customer_download -#### wc customer_download list +#### wc customer_download list <customer_id> - `--customer_id` - Unique identifier for the resource. - `--context` - Scope under which the request is made; determines fields present in response. @@ -198,7 +198,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope ### wc order_note -#### wc order_note list +#### wc order_note list <order_id> - `--order_id` - The order ID. - `--context` - Scope under which the request is made; determines fields present in response. @@ -211,14 +211,14 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc order_note create +#### wc order_note create <order_id> - `--order_id` - The order ID. - `--note` - Order note content. (*Required*) - `--customer_note` - If true, the note will be shown to customers and they will be notified. If false, the note will be for admin reference only. - `--porcelain` - Output just the id when the operation is successful. -#### wc order_note get [id] +#### wc order_note get <order_id> [id] - `--id` - Unique identifier for the resource. - `--order_id` - The order ID. @@ -231,7 +231,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc order_note delete [id] +#### wc order_note delete <order_id> [id] - `--id` - Unique identifier for the resource. - `--order_id` - The order ID. @@ -240,7 +240,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope ### wc shop_order_refund -#### wc shop_order_refund list +#### wc shop_order_refund list <order_id> - `--order_id` - The order ID. - `--context` - Scope under which the request is made; determines fields present in response. @@ -265,7 +265,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc shop_order_refund create +#### wc shop_order_refund create <order_id> - `--order_id` - The order ID. - `--amount` - Refund amount. @@ -276,7 +276,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--api_refund` - When true, the payment gateway API is used to generate the refund. - `--porcelain` - Output just the id when the operation is successful. -#### wc shop_order_refund get [id] +#### wc shop_order_refund get <order_id> [id] - `--order_id` - The order ID. - `--id` - Unique identifier for the resource. @@ -289,7 +289,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc shop_order_refund delete [id] +#### wc shop_order_refund delete <order_id> [id] - `--order_id` - The order ID. - `--id` - Unique identifier for the resource. @@ -386,7 +386,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope ### wc product_attribute_term -#### wc product_attribute_term list +#### wc product_attribute_term list <attribute_id> - `--attribute_id` - Unique identifier for the attribute of the terms. - `--context` - Scope under which the request is made; determines fields present in response. @@ -409,7 +409,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc product_attribute_term create +#### wc product_attribute_term create <attribute_id> - `--attribute_id` - Unique identifier for the attribute of the terms. - `--name` - Name for the resource. (*Required*) @@ -418,7 +418,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--menu_order` - Menu order, used to custom sort the resource. - `--porcelain` - Output just the id when the operation is successful. -#### wc product_attribute_term get [id] +#### wc product_attribute_term get <attribute_id> [id] - `--id` - Unique identifier for the resource. - `--attribute_id` - Unique identifier for the attribute of the terms. @@ -431,7 +431,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc product_attribute_term update [id] +#### wc product_attribute_term update <attribute_id> [id] - `--id` - Unique identifier for the resource. - `--attribute_id` - Unique identifier for the attribute of the terms. @@ -441,7 +441,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--menu_order` - Menu order, used to custom sort the resource. - `--porcelain` - Output just the id when the operation is successful. -#### wc product_attribute_term delete [id] +#### wc product_attribute_term delete <attribute_id> [id] - `--id` - Unique identifier for the resource. - `--attribute_id` - Unique identifier for the attribute of the terms. @@ -565,7 +565,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope ### wc product_review -#### wc product_review list +#### wc product_review list <product_id> - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the variation. @@ -578,7 +578,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc product_review create +#### wc product_review create <product_id> - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the variation. @@ -590,7 +590,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--email` - Email of the reviewer. (*Required*) - `--porcelain` - Output just the id when the operation is successful. -#### wc product_review get [id] +#### wc product_review get <product_id> [id] - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the resource. @@ -603,7 +603,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc product_review update [id] +#### wc product_review update <product_id> [id] - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the resource. @@ -615,7 +615,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--email` - Reviewer email. - `--porcelain` - Output just the id when the operation is successful. -#### wc product_review delete [id] +#### wc product_review delete <product_id> [id] - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the resource. @@ -893,7 +893,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope ### wc product_variation -#### wc product_variation list +#### wc product_variation list <product_id> - `--product_id` - Unique identifier for the variable product. - `--context` - Scope under which the request is made; determines fields present in response. @@ -932,7 +932,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc product_variation create +#### wc product_variation create <product_id> - `--product_id` - Unique identifier for the variable product. - `--description` - Variation description. @@ -964,7 +964,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--meta_data` - Meta data. - `--porcelain` - Output just the id when the operation is successful. -#### wc product_variation get [id] +#### wc product_variation get <product_id> [id] - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the variation. @@ -977,7 +977,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc product_variation update [id] +#### wc product_variation update <product_id> [id] - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the variation. @@ -1010,7 +1010,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--meta_data` - Meta data. - `--porcelain` - Output just the id when the operation is successful. -#### wc product_variation delete [id] +#### wc product_variation delete <product_id> [id] - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the variation. diff --git a/packages/js/data/changelog/fix-51395-reporttable-page b/packages/js/data/changelog/fix-51395-reporttable-page new file mode 100644 index 00000000000..9b02145b01f --- /dev/null +++ b/packages/js/data/changelog/fix-51395-reporttable-page @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Use page query param consistently as string for `getReportTableQuery`. diff --git a/packages/js/data/changelog/fix-no-permissions-api-error b/packages/js/data/changelog/fix-no-permissions-api-error new file mode 100644 index 00000000000..0695a289f9a --- /dev/null +++ b/packages/js/data/changelog/fix-no-permissions-api-error @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Added pre-API call permission checks for some API calls that were being called on non-admin accessible screens diff --git a/packages/js/data/changelog/fix-remove-help-panel-user-meta b/packages/js/data/changelog/fix-remove-help-panel-user-meta new file mode 100644 index 00000000000..9b1882ac793 --- /dev/null +++ b/packages/js/data/changelog/fix-remove-help-panel-user-meta @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Removed the leftover user meta from the help panel spotlight diff --git a/packages/js/data/src/notes/resolvers.ts b/packages/js/data/src/notes/resolvers.ts index 5a0dec47e6e..1da6d7d9845 100644 --- a/packages/js/data/src/notes/resolvers.ts +++ b/packages/js/data/src/notes/resolvers.ts @@ -10,11 +10,14 @@ import { apiFetch } from '@wordpress/data-controls'; import { NAMESPACE } from '../constants'; import { setNotes, setNotesQuery, setError } from './actions'; import { NoteQuery, Note } from './types'; +import { checkUserCapability } from '../utils'; export function* getNotes( query: NoteQuery = {} ) { const url = addQueryArgs( `${ NAMESPACE }/admin/notes`, query ); try { + yield checkUserCapability( 'manage_woocommerce' ); + const notes: Note[] = yield apiFetch( { path: url, } ); diff --git a/packages/js/data/src/onboarding/resolvers.ts b/packages/js/data/src/onboarding/resolvers.ts index a74e7be4760..814094413b7 100644 --- a/packages/js/data/src/onboarding/resolvers.ts +++ b/packages/js/data/src/onboarding/resolvers.ts @@ -31,6 +31,7 @@ import { TaskListType, } from './types'; import { Plugin } from '../plugins/types'; +import { checkUserCapability } from '../utils'; const resolveSelect = controls && controls.resolveSelect ? controls.resolveSelect : select; @@ -68,6 +69,8 @@ export function* getEmailPrefill() { export function* getTaskLists() { const deprecatedTasks = new DeprecatedTasks(); try { + yield checkUserCapability( 'manage_woocommerce' ); + const results: TaskListType[] = yield apiFetch( { path: WC_ADMIN_NAMESPACE + '/onboarding/tasks', method: deprecatedTasks.hasDeprecatedTasks() ? 'POST' : 'GET', diff --git a/packages/js/data/src/plugins/resolvers.ts b/packages/js/data/src/plugins/resolvers.ts index f656099ed95..c25fdfeeea5 100644 --- a/packages/js/data/src/plugins/resolvers.ts +++ b/packages/js/data/src/plugins/resolvers.ts @@ -27,6 +27,7 @@ import { RecommendedTypes, JetpackConnectionDataResponse, } from './types'; +import { checkUserCapability } from '../utils'; // Can be removed in WP 5.9, wp.data is supported in >5.7. const resolveSelect = @@ -61,6 +62,8 @@ type ConnectJetpackResponse = { export function* getActivePlugins() { yield setIsRequesting( 'getActivePlugins', true ); try { + yield checkUserCapability( 'manage_woocommerce' ); + const url = WC_ADMIN_NAMESPACE + '/plugins/active'; const results: PluginGetResponse = yield apiFetch( { path: url, @@ -77,6 +80,8 @@ export function* getInstalledPlugins() { yield setIsRequesting( 'getInstalledPlugins', true ); try { + yield checkUserCapability( 'manage_woocommerce' ); + const url = WC_ADMIN_NAMESPACE + '/plugins/installed'; const results: PluginGetResponse = yield apiFetch( { path: url, @@ -111,6 +116,8 @@ export function* getJetpackConnectionData() { yield setIsRequesting( 'getJetpackConnectionData', true ); try { + yield checkUserCapability( 'manage_woocommerce' ); + const url = JETPACK_NAMESPACE + '/connection/data'; const results: JetpackConnectionDataResponse = yield apiFetch( { diff --git a/packages/js/data/src/reports/utils.ts b/packages/js/data/src/reports/utils.ts index 065c4847653..39cf4869798 100644 --- a/packages/js/data/src/reports/utils.ts +++ b/packages/js/data/src/reports/utils.ts @@ -561,7 +561,7 @@ export function getReportTableQuery( before: noIntervals ? undefined : appendTimestamp( datesFromQuery.primary.before, 'end' ), - page: query.paged || 1, + page: query.paged || '1', per_page: query.per_page || QUERY_DEFAULTS.pageSize, ...filterQuery, ...tableQuery, diff --git a/packages/js/data/src/user/types.ts b/packages/js/data/src/user/types.ts index 34328a41f18..428b8e1a5e1 100644 --- a/packages/js/data/src/user/types.ts +++ b/packages/js/data/src/user/types.ts @@ -15,7 +15,6 @@ export type UserPreferences = { dashboard_chart_type?: string; dashboard_leaderboard_rows?: string; dashboard_sections?: string; - help_panel_highlight_shown?: string; homepage_layout?: string; homepage_stats?: string; orders_report_columns?: string; diff --git a/packages/js/data/src/utils.ts b/packages/js/data/src/utils.ts index eff7bf1a483..1c0b80b7792 100644 --- a/packages/js/data/src/utils.ts +++ b/packages/js/data/src/utils.ts @@ -2,14 +2,15 @@ * External dependencies */ import { addQueryArgs } from '@wordpress/url'; -import { apiFetch } from '@wordpress/data-controls'; +import { apiFetch, select } from '@wordpress/data-controls'; /** * Internal dependencies */ import { BaseQueryParams } from './types/query-params'; import { fetchWithHeaders } from './controls'; - +import { USER_STORE_NAME } from './user'; +import { WCUser } from './user/types'; function replacer( _: string, value: unknown ) { if ( value ) { if ( Array.isArray( value ) ) { @@ -100,3 +101,20 @@ export function* request< Query extends BaseQueryParams, DataType >( return { items: response.data, totalCount }; } } + +/** + * Utility function to check if the current user has a specific capability. + * + * @param {string} capability - The capability to check (e.g. 'manage_woocommerce'). + * @throws {Error} If the user does not have the required capability. + */ +export function* checkUserCapability( capability: string ) { + const currentUser: WCUser< 'capabilities' > = yield select( + USER_STORE_NAME, + 'getCurrentUser' + ); + + if ( ! currentUser.capabilities[ capability ] ) { + throw new Error( `User does not have ${ capability } capability.` ); + } +} diff --git a/plugins/woocommerce-admin/client/activity-panel/activity-panel.js b/plugins/woocommerce-admin/client/activity-panel/activity-panel.js index 65fb6d08588..47373cf8e88 100644 --- a/plugins/woocommerce-admin/client/activity-panel/activity-panel.js +++ b/plugins/woocommerce-admin/client/activity-panel/activity-panel.js @@ -269,7 +269,8 @@ export const ActivityPanel = ( { isEmbedded, query } ) => { visible: ( isEmbedded || ! isHomescreen ) && ! isPerformingSetupTask() && - ! isProductScreen(), + ! isProductScreen() && + currentUserCan( 'manage_woocommerce' ), }; const feedback = { diff --git a/plugins/woocommerce-admin/client/core-profiler/index.tsx b/plugins/woocommerce-admin/client/core-profiler/index.tsx index 428566a4578..8dbcd742f3f 100644 --- a/plugins/woocommerce-admin/client/core-profiler/index.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/index.tsx @@ -300,15 +300,28 @@ const exitToWooHome = fromPromise( async () => { window.location.href = getNewPath( {}, '/', {} ); } ); +const getPluginNameParam = ( + pluginsSelected: CoreProfilerStateMachineContext[ 'pluginsSelected' ] +) => { + if ( pluginsSelected.includes( 'woocommerce-payments' ) ) { + return 'woocommerce-payments'; + } + return 'jetpack-ai'; +}; + const redirectToJetpackAuthPage = ( { event, + context, }: { context: CoreProfilerStateMachineContext; event: { output: { url: string } }; } ) => { const url = new URL( event.output.url ); url.searchParams.set( 'installed_ext_success', '1' ); - url.searchParams.set( 'plugin_name', 'jetpack-ai' ); + url.searchParams.set( + 'plugin_name', + getPluginNameParam( context.pluginsSelected ) + ); window.location.href = url.toString(); }; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/editor.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/editor.tsx index c2f8c454009..93920c3f71f 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/editor.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/editor.tsx @@ -23,6 +23,7 @@ import { GlobalStylesRenderer } from '@wordpress/edit-site/build-module/componen /** * Internal dependencies */ +import { trackEvent } from '../tracking'; import { editorIsLoaded } from '../utils'; import { BlockEditorContainer } from './block-editor-container'; @@ -63,6 +64,7 @@ export const Editor = ( { isLoading }: { isLoading: boolean } ) => { useEffect( () => { if ( ! isLoading ) { editorIsLoaded(); + trackEvent( 'customize_your_store_assembler_hub_editor_loaded' ); } }, [ isLoading ] ); diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx index ab05865014a..60313651ee7 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx @@ -14,6 +14,11 @@ import { mergeBaseAndUserConfigs } from '@wordpress/edit-site/build-module/compo import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; import { isEqual, noop } from 'lodash'; +/** + * Internal dependencies + */ +import { trackEvent } from '~/customize-store/tracking'; + const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); // Removes the typography settings from the styles when the user is changing @@ -100,6 +105,24 @@ export const VariationContainer = ( { variation, children } ) => { ), }; } ); + + if ( variation.settings.color?.palette ) { + trackEvent( + 'customize_your_store_assembler_hub_color_palette_item_click', + { + item: variation.title, + } + ); + } + + if ( variation.settings.typography ) { + trackEvent( + 'customize_your_store_assembler_hub_typography_item_click', + { + item: variation.title, + } + ); + } }; const selectOnEnter = ( event ) => { diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx index b1b10bab2e6..c851175894a 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx @@ -17,6 +17,7 @@ import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; import { CustomizeStoreContext } from '../'; import { SidebarNavigationScreen } from './sidebar-navigation-screen'; import { ColorPalette, ColorPanel } from './global-styles'; +import { trackEvent } from '~/customize-store/tracking'; import { FlowType } from '~/customize-store/types'; const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); @@ -27,6 +28,14 @@ const SidebarNavigationScreenColorPaletteContent = () => { const hasCreatedOwnColors = !! ( user.settings.color && user.settings.color.palette.hasCreatedOwnColors ); + + function handlePanelBodyToggle( open?: boolean ) { + trackEvent( + 'customize_your_store_assembler_hub_color_palette_create_toggle', + { open } + ); + } + // Wrap in a BlockEditorProvider to ensure that the Iframe's dependencies are // loaded. This is necessary because the Iframe component waits until // the block editor store's `__internalIsInitialized` is true before @@ -45,6 +54,7 @@ const SidebarNavigationScreenColorPaletteContent = () => { className="woocommerce-customize-store__color-panel-container" title={ __( 'or create your own', 'woocommerce' ) } initialOpen={ hasCreatedOwnColors } + onToggle={ handlePanelBodyToggle } > diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-logo.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-logo.tsx index 0a55417170d..10f4c6a5f02 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-logo.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-logo.tsx @@ -297,16 +297,26 @@ const LogoEdit = ( { ); } + function handleMediaUploadSelect( media: { id: string; url: string } ) { + onInitialSelectLogo( media ); + trackEvent( 'customize_your_store_assembler_hub_logo_select' ); + } + if ( ! logoUrl ) { return ( void } ) => ( + ) => void; setEmail: ( value: string ) => void; useShippingAsBilling: boolean; + editingBillingAddress: boolean; + editingShippingAddress: boolean; setUseShippingAsBilling: ( useShippingAsBilling: boolean ) => void; + setEditingBillingAddress: ( isEditing: boolean ) => void; + setEditingShippingAddress: ( isEditing: boolean ) => void; defaultFields: AddressFields; showShippingFields: boolean; showBillingFields: boolean; @@ -40,15 +44,25 @@ interface CheckoutAddress { */ export const useCheckoutAddress = (): CheckoutAddress => { const { needsShipping } = useShippingData(); - const { useShippingAsBilling, prefersCollection } = useSelect( - ( select ) => ( { - useShippingAsBilling: - select( CHECKOUT_STORE_KEY ).getUseShippingAsBilling(), - prefersCollection: select( CHECKOUT_STORE_KEY ).prefersCollection(), - } ) - ); - const { __internalSetUseShippingAsBilling } = - useDispatch( CHECKOUT_STORE_KEY ); + const { + useShippingAsBilling, + prefersCollection, + editingBillingAddress, + editingShippingAddress, + } = useSelect( ( select ) => ( { + useShippingAsBilling: + select( CHECKOUT_STORE_KEY ).getUseShippingAsBilling(), + prefersCollection: select( CHECKOUT_STORE_KEY ).prefersCollection(), + editingBillingAddress: + select( CHECKOUT_STORE_KEY ).getEditingBillingAddress(), + editingShippingAddress: + select( CHECKOUT_STORE_KEY ).getEditingShippingAddress(), + } ) ); + const { + __internalSetUseShippingAsBilling, + setEditingBillingAddress, + setEditingShippingAddress, + } = useDispatch( CHECKOUT_STORE_KEY ); const { billingAddress, setBillingAddress, @@ -77,6 +91,10 @@ export const useCheckoutAddress = (): CheckoutAddress => { defaultFields, useShippingAsBilling, setUseShippingAsBilling: __internalSetUseShippingAsBilling, + editingBillingAddress, + editingShippingAddress, + setEditingBillingAddress, + setEditingShippingAddress, needsShipping, showShippingFields: ! forcedBillingAddress && needsShipping && ! prefersCollection, diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/index.tsx index c6b97fe1e51..e4ad1823ecc 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/index.tsx @@ -11,6 +11,7 @@ import { } from '@woocommerce/types'; import { FormFieldsConfig, getSetting } from '@woocommerce/settings'; import { formatAddress } from '@woocommerce/blocks/checkout/utils'; +import { Button } from '@ariakit/react'; /** * Internal dependencies @@ -82,7 +83,8 @@ const AddressCard = ( { ) } { onEdit && ( - + ) } ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx index bcdcbac3e69..d56938d94df 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx @@ -7,14 +7,12 @@ import { useCheckoutAddress, useEditorContext, noticeContexts, - useShippingData, } from '@woocommerce/base-context'; import Noninteractive from '@woocommerce/base-components/noninteractive'; import type { ShippingAddress, FormFieldsConfig } from '@woocommerce/settings'; import { StoreNoticesContainer } from '@woocommerce/blocks-components'; import { useSelect } from '@wordpress/data'; import { CART_STORE_KEY } from '@woocommerce/block-data'; -import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies @@ -36,14 +34,9 @@ const Block = ( { showPhoneField: boolean; requirePhoneField: boolean; } ): JSX.Element => { - const { - shippingAddress, - billingAddress, - setShippingAddress, - useBillingAsShipping, - } = useCheckoutAddress(); + const { billingAddress, setShippingAddress, useBillingAsShipping } = + useCheckoutAddress(); const { isEditor } = useEditorContext(); - const { needsShipping } = useShippingData(); // Syncs shipping address with billing address if "Force shipping to the customer billing address" is enabled. useEffectOnce( () => { @@ -101,19 +94,6 @@ const Block = ( { }; } ); - // Default editing state for CustomerAddress component comes from the current address and whether or not we're in the editor. - const hasAddress = !! ( - billingAddress.address_1 && - ( billingAddress.first_name || billingAddress.last_name ) - ); - const { email, ...billingAddressWithoutEmail } = billingAddress; - const billingMatchesShipping = isShallowEqual( - billingAddressWithoutEmail, - shippingAddress - ); - const defaultEditingAddress = - isEditor || ! hasAddress || ( needsShipping && billingMatchesShipping ); - return ( <> @@ -121,7 +101,6 @@ const Block = ( { { cartDataLoaded ? ( ) : null } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx index b36708f4314..668fe20cf61 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { useState, useCallback, useEffect } from '@wordpress/element'; +import { useCallback, useEffect } from '@wordpress/element'; import { Form } from '@woocommerce/base-components/cart-checkout'; import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context'; import type { @@ -20,19 +20,18 @@ import AddressCard from '../../address-card'; const CustomerAddress = ( { addressFieldsConfig, - defaultEditing = false, }: { addressFieldsConfig: FormFieldsConfig; - defaultEditing?: boolean; } ) => { const { billingAddress, setShippingAddress, setBillingAddress, useBillingAsShipping, + editingBillingAddress: editing, + setEditingBillingAddress: setEditing, } = useCheckoutAddress(); const { dispatchCheckoutEvent } = useStoreEvents(); - const [ editing, setEditing ] = useState( defaultEditing ); // Forces editing state if store has errors. const { hasValidationErrors, invalidProps } = useSelect( ( select ) => { @@ -55,7 +54,7 @@ const CustomerAddress = ( { if ( invalidProps.length > 0 && editing === false ) { setEditing( true ); } - }, [ editing, hasValidationErrors, invalidProps.length ] ); + }, [ editing, hasValidationErrors, invalidProps.length, setEditing ] ); const onChangeAddress = useCallback( ( values: AddressFormValues ) => { @@ -86,7 +85,7 @@ const CustomerAddress = ( { isExpanded={ editing } /> ), - [ billingAddress, addressFieldsConfig, editing ] + [ billingAddress, addressFieldsConfig, editing, setEditing ] ); const renderAddressFormComponent = useCallback( diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx index 2ddbb0d7744..e749fa33afe 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx @@ -47,6 +47,7 @@ const Block = ( { billingAddress, useShippingAsBilling, setUseShippingAsBilling, + setEditingBillingAddress, } = useCheckoutAddress(); const { isEditor } = useEditorContext(); const isGuest = getSetting( 'currentUserId' ) === 0; @@ -116,10 +117,6 @@ const Block = ( { const noticeContext = useShippingAsBilling ? [ noticeContexts.SHIPPING_ADDRESS, noticeContexts.BILLING_ADDRESS ] : [ noticeContexts.SHIPPING_ADDRESS ]; - const hasAddress = !! ( - shippingAddress.address_1 && - ( shippingAddress.first_name || shippingAddress.last_name ) - ); const { cartDataLoaded } = useSelect( ( select ) => { const store = select( CART_STORE_KEY ); @@ -128,9 +125,6 @@ const Block = ( { }; } ); - // Default editing state for CustomerAddress component comes from the current address and whether or not we're in the editor. - const defaultEditingAddress = isEditor || ! hasAddress; - return ( <> @@ -138,7 +132,6 @@ const Block = ( { { cartDataLoaded ? ( ) : null } @@ -151,6 +144,7 @@ const Block = ( { if ( checked ) { syncBillingWithShipping(); } else { + setEditingBillingAddress( true ); clearBillingAddress( billingAddress ); } } } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/customer-address.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/customer-address.tsx index 1c21625c188..fed4e47c7ba 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/customer-address.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/customer-address.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { useState, useCallback, useEffect } from '@wordpress/element'; +import { useCallback, useEffect } from '@wordpress/element'; import { Form } from '@woocommerce/base-components/cart-checkout'; import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context'; import type { @@ -20,19 +20,18 @@ import AddressCard from '../../address-card'; const CustomerAddress = ( { addressFieldsConfig, - defaultEditing = false, }: { addressFieldsConfig: FormFieldsConfig; - defaultEditing?: boolean; } ) => { const { shippingAddress, setShippingAddress, setBillingAddress, useShippingAsBilling, + editingShippingAddress: editing, + setEditingShippingAddress: setEditing, } = useCheckoutAddress(); const { dispatchCheckoutEvent } = useStoreEvents(); - const [ editing, setEditing ] = useState( defaultEditing ); // Forces editing state if store has errors. const { hasValidationErrors, invalidProps } = useSelect( ( select ) => { @@ -54,7 +53,7 @@ const CustomerAddress = ( { if ( invalidProps.length > 0 && editing === false ) { setEditing( true ); } - }, [ editing, hasValidationErrors, invalidProps.length ] ); + }, [ editing, hasValidationErrors, invalidProps.length, setEditing ] ); const onChangeAddress = useCallback( ( values: AddressFormValues ) => { @@ -85,7 +84,7 @@ const CustomerAddress = ( { isExpanded={ editing } /> ), - [ shippingAddress, addressFieldsConfig, editing ] + [ shippingAddress, addressFieldsConfig, editing, setEditing ] ); const renderAddressFormComponent = useCallback( diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.tsx index f6b033aa709..a42bdd5a61d 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.tsx @@ -10,6 +10,7 @@ import { CART_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data'; import { useDispatch, useSelect } from '@wordpress/data'; import { isPackageRateCollectable } from '@woocommerce/base-utils'; import { getSetting } from '@woocommerce/settings'; +import { Button } from '@ariakit/react'; /** * Internal dependencies @@ -18,7 +19,6 @@ import { RatePrice, getLocalPickupPrices, getShippingPrices } from './shared'; import type { minMaxPrices } from './shared'; import { defaultLocalPickupText, defaultShippingText } from './constants'; import { shippingAddressHasValidationErrors } from '../../../../data/cart/utils'; -import Button from '../../../../base/components/button'; const SHIPPING_RATE_ERROR = { hidden: true, @@ -44,8 +44,8 @@ const LocalPickupSelector = ( { } ) => { return ( - - -); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/constants.ts index 8c47d8c30e4..e535dfebc16 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/constants.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/constants.ts @@ -5,49 +5,71 @@ import { __ } from '@wordpress/i18n'; export const attributeOptionsPreview = [ { - id: 23, - name: __( 'Blue', 'woocommerce' ), - slug: 'blue', - attr_slug: 'blue', - description: '', - parent: 0, - count: 4, + label: __( 'Blue', 'woocommerce' ), + value: 'blue', + rawData: { + id: 23, + name: __( 'Blue', 'woocommerce' ), + slug: 'blue', + attr_slug: 'blue', + description: '', + parent: 0, + count: 4, + }, }, { - id: 29, - name: __( 'Gray', 'woocommerce' ), - slug: 'gray', - attr_slug: 'gray', - description: '', - parent: 0, - count: 3, + label: __( 'Gray', 'woocommerce' ), + value: 'gray', + selected: true, + rawData: { + id: 29, + name: __( 'Gray', 'woocommerce' ), + slug: 'gray', + attr_slug: 'gray', + description: '', + parent: 0, + count: 3, + }, }, { - id: 24, - name: __( 'Green', 'woocommerce' ), - slug: 'green', - attr_slug: 'green', - description: '', - parent: 0, - count: 3, + label: __( 'Green', 'woocommerce' ), + value: 'green', + rawData: { + id: 24, + name: __( 'Green', 'woocommerce' ), + slug: 'green', + attr_slug: 'green', + description: '', + parent: 0, + count: 3, + }, }, { - id: 25, - name: __( 'Red', 'woocommerce' ), - slug: 'red', - attr_slug: 'red', - description: '', - parent: 0, - count: 4, + label: __( 'Red', 'woocommerce' ), + value: 'red', + selected: true, + rawData: { + id: 25, + name: __( 'Red', 'woocommerce' ), + slug: 'red', + attr_slug: 'red', + description: '', + parent: 0, + count: 4, + }, }, { - id: 30, - name: __( 'Yellow', 'woocommerce' ), - slug: 'yellow', - attr_slug: 'yellow', - description: '', - parent: 0, - count: 1, + label: __( 'Yellow', 'woocommerce' ), + value: 'yellow', + rawData: { + id: 30, + name: __( 'Yellow', 'woocommerce' ), + slug: 'yellow', + attr_slug: 'yellow', + description: '', + parent: 0, + count: 1, + }, }, ]; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx index 9fb37b004b1..5f709d0089d 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx @@ -5,29 +5,34 @@ import { useCollection, useCollectionData, } from '@woocommerce/base-context/hooks'; -import { getSetting } from '@woocommerce/settings'; import { AttributeSetting, AttributeTerm, objectHasProp, } from '@woocommerce/types'; -import { useBlockProps } from '@wordpress/block-editor'; -import { Disabled, Notice, withSpokenMessages } from '@wordpress/components'; -import { useEffect, useState, useMemo } from '@wordpress/element'; +import { + useBlockProps, + useInnerBlocksProps, + BlockContextProvider, +} from '@wordpress/block-editor'; +import { withSpokenMessages } from '@wordpress/components'; +import { useEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { getSetting } from '@woocommerce/settings'; /** * Internal dependencies */ -import { AttributeDropdown } from './components/attribute-dropdown'; -import { Preview as CheckboxListPreview } from './components/checkbox-list-editor'; -import { Inspector } from './components/inspector'; -import { NoAttributesPlaceholder } from './components/placeholder'; +import { Inspector } from './inspector'; import { attributeOptionsPreview } from './constants'; import './style.scss'; import { EditProps, isAttributeCounts } from './types'; import { getAttributeFromId } from './utils'; -import './editor.scss'; +import { getAllowedBlocks } from '../../utils'; +import { EXCLUDED_BLOCKS } from '../../constants'; +import { FilterOptionItem } from '../../types'; +import { InitialDisabled } from '../../components/initial-disabled'; +import { Notice } from '../../components/notice'; const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] ); @@ -47,8 +52,10 @@ const Edit = ( props: EditProps ) => { const attributeObject = getAttributeFromId( attributeId ); const [ attributeOptions, setAttributeOptions ] = useState< - AttributeTerm[] + FilterOptionItem[] >( [] ); + const [ isOptionsLoading, setIsOptionsLoading ] = + useState< boolean >( true ); const { results: attributeTerms, isLoading: isTermsLoading } = useCollection< AttributeTerm >( { @@ -59,7 +66,7 @@ const Edit = ( props: EditProps ) => { query: { orderby: 'menu_order', hide_empty: hideEmpty }, } ); - const { results: filteredCounts, isLoading: isCountsLoading } = + const { results: filteredCounts, isLoading: isFilterCountsLoading } = useCollectionData( { queryAttribute: { taxonomy: attributeObject?.taxonomy || '', @@ -69,90 +76,137 @@ const Edit = ( props: EditProps ) => { isEditor: true, } ); - const isLoading = isTermsLoading || isCountsLoading; - useEffect( () => { + if ( isTermsLoading || isFilterCountsLoading ) return; + const termIdHasProducts = objectHasProp( filteredCounts, 'attribute_counts' ) && isAttributeCounts( filteredCounts.attribute_counts ) ? filteredCounts.attribute_counts.map( ( term ) => term.term ) : []; - if ( termIdHasProducts.length === 0 && hideEmpty ) - return setAttributeOptions( [] ); + if ( termIdHasProducts.length === 0 && hideEmpty ) { + setAttributeOptions( [] ); + } else { + setAttributeOptions( + attributeTerms + .filter( ( term ) => { + if ( hideEmpty ) + return termIdHasProducts.includes( term.id ); + return true; + } ) + .sort( ( a, b ) => { + switch ( sortOrder ) { + case 'name-asc': + return a.name > b.name ? 1 : -1; + case 'name-desc': + return a.name < b.name ? 1 : -1; + case 'count-asc': + return a.count > b.count ? 1 : -1; + case 'count-desc': + default: + return a.count < b.count ? 1 : -1; + } + } ) + .map( ( term, index ) => ( { + label: showCounts + ? `${ term.name } (${ term.count })` + : term.name, + value: term.id.toString(), + selected: index === 1, + rawData: term, + } ) ) + ); + } - setAttributeOptions( - attributeTerms - .filter( ( term ) => { - if ( hideEmpty ) - return termIdHasProducts.includes( term.id ); - return true; - } ) - .sort( ( a, b ) => { - switch ( sortOrder ) { - case 'name-asc': - return a.name > b.name ? 1 : -1; - case 'name-desc': - return a.name < b.name ? 1 : -1; - case 'count-asc': - return a.count > b.count ? 1 : -1; - case 'count-desc': - default: - return a.count < b.count ? 1 : -1; - } - } ) - ); - }, [ attributeTerms, filteredCounts, sortOrder, hideEmpty ] ); + setIsOptionsLoading( false ); + }, [ + showCounts, + attributeTerms, + filteredCounts, + sortOrder, + hideEmpty, + isTermsLoading, + isFilterCountsLoading, + ] ); - const Wrapper = ( { children }: { children: React.ReactNode } ) => ( -
- - { children } -
+ const { children, ...innerBlocksProps } = useInnerBlocksProps( + useBlockProps(), + { + allowedBlocks: getAllowedBlocks( EXCLUDED_BLOCKS ), + template: [ + [ + 'core/group', + { + layout: { + type: 'flex', + flexWrap: 'nowrap', + }, + metadata: { + name: __( 'Header', 'woocommerce' ), + }, + style: { + spacing: { + blockGap: '0', + }, + }, + }, + [ + [ + 'core/heading', + { + level: 3, + content: + attributeObject?.label || + __( 'Attribute', 'woocommerce' ), + }, + ], + [ + 'woocommerce/product-filter-clear-button', + { + lock: { + remove: true, + move: false, + }, + }, + ], + ], + ], + [ + displayStyle, + { + lock: { + remove: true, + }, + }, + ], + ], + } ); - const loadingState = useMemo( () => { - return [ ...Array( 5 ) ].map( ( x, i ) => ( -
  • -   -
  • - ) ); - }, [] ); + const isLoading = + isTermsLoading || isFilterCountsLoading || isOptionsLoading; - if ( isPreview ) { - return ( - - - { - if ( showCounts ) - return `${ term.name } (${ term.count })`; - return term.name; - } ) } - /> - - - ); - } - - // Block rendering starts. if ( Object.keys( ATTRIBUTES ).length === 0 ) return ( - - - +
    + + +

    + { __( + "Attributes are needed for filtering your products. You haven't created any attributes yet.", + 'woocommerce' + ) } +

    +
    +
    ); if ( ! attributeId || ! attributeObject ) return ( - - +
    + +

    { __( 'Please select an attribute to use this filter!', @@ -160,22 +214,14 @@ const Edit = ( props: EditProps ) => { ) }

    - +
    ); - if ( isLoading ) + if ( ! isLoading && attributeTerms.length === 0 ) return ( - -
      - { loadingState } -
    -
    - ); - - if ( attributeTerms.length === 0 ) - return ( - - +
    + +

    { __( 'There are no products with the selected attributes.', @@ -183,30 +229,28 @@ const Edit = ( props: EditProps ) => { ) }

    - +
    ); return ( - - - { displayStyle === 'dropdown' ? ( - - ) : ( - { - if ( showCounts ) - return `${ term.name } (${ term.count })`; - return term.name; - } ) } - /> - ) } - - +
    + + + + { children } + + +
    ); }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/editor.scss deleted file mode 100644 index 0580af4584f..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/editor.scss +++ /dev/null @@ -1,8 +0,0 @@ -.wp-block-woocommerce-product-filter-attribute__loading { - padding: 0; - - li { - @include placeholder(); - margin: 5px 0; - } -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/frontend.ts index 0f9eb935586..4416accc8df 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/frontend.ts @@ -1,14 +1,12 @@ /** * External dependencies */ -import { store, getContext } from '@woocommerce/interactivity'; -import { DropdownContext } from '@woocommerce/interactivity-components/dropdown'; -import { HTMLElementEvent } from '@woocommerce/types'; +import { store, getContext, getElement } from '@woocommerce/interactivity'; /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { ProductFiltersContext } from '../../frontend'; type AttributeFilterContext = { attributeSlug: string; @@ -16,95 +14,72 @@ type AttributeFilterContext = { selectType: 'single' | 'multiple'; }; -interface ActiveAttributeFilterContext extends AttributeFilterContext { - value: string; -} - -function nonNullable< T >( value: T ): value is NonNullable< T > { - return value !== null && value !== undefined; -} - -function getUrl( - selectedTerms: string[], - slug: string, - queryType: 'or' | 'and' -) { - const url = new URL( window.location.href ); - const { searchParams } = url; - - if ( selectedTerms.length > 0 ) { - searchParams.set( `filter_${ slug }`, selectedTerms.join( ',' ) ); - searchParams.set( `query_type_${ slug }`, queryType ); - } else { - searchParams.delete( `filter_${ slug }` ); - searchParams.delete( `query_type_${ slug }` ); - } - - return url.href; -} - -function getSelectedTermsFromUrl( slug: string ) { - const url = new URL( window.location.href ); - return ( url.searchParams.get( `filter_${ slug }` ) || '' ) - .split( ',' ) - .filter( Boolean ); -} - store( 'woocommerce/product-filter-attribute', { actions: { - navigate: () => { - const dropdownContext = getContext< DropdownContext >( - 'woocommerce/interactivity-dropdown' - ); - const context = getContext< AttributeFilterContext >(); - const filters = dropdownContext.selectedItems - .map( ( item ) => item.value ) - .filter( nonNullable ); + toggleFilter: () => { + const { ref } = getElement(); + const targetAttribute = + ref.getAttribute( 'data-attribute-value' ) ?? 'value'; + const termSlug = ref.getAttribute( targetAttribute ); - navigate( - getUrl( filters, context.attributeSlug, context.queryType ) - ); - }, - updateProducts: ( event: HTMLElementEvent< HTMLInputElement > ) => { - if ( ! event.target.value ) return; + if ( ! termSlug ) return; - const context = getContext< AttributeFilterContext >(); - - let selectedTerms = getSelectedTermsFromUrl( - context.attributeSlug + const { attributeSlug, queryType } = + getContext< AttributeFilterContext >(); + const productFiltersContext = getContext< ProductFiltersContext >( + 'woocommerce/product-filters' ); if ( - event.target.checked && - ! selectedTerms.includes( event.target.value ) + ! ( + `filter_${ attributeSlug }` in productFiltersContext.params + ) ) { - if ( context.selectType === 'multiple' ) - selectedTerms.push( event.target.value ); - if ( context.selectType === 'single' ) - selectedTerms = [ event.target.value ]; - } else { - selectedTerms = selectedTerms.filter( - ( value ) => value !== event.target.value - ); + productFiltersContext.params = { + ...productFiltersContext.params, + [ `filter_${ attributeSlug }` ]: termSlug, + [ `query_type_${ attributeSlug }` ]: queryType, + }; + return; } - navigate( - getUrl( - selectedTerms, - context.attributeSlug, - context.queryType - ) - ); + const selectedTerms = + productFiltersContext.params[ + `filter_${ attributeSlug }` + ].split( ',' ); + if ( selectedTerms.includes( termSlug ) ) { + const remainingSelectedTerms = selectedTerms.filter( + ( term ) => term !== termSlug + ); + if ( remainingSelectedTerms.length > 0 ) { + productFiltersContext.params[ + `filter_${ attributeSlug }` + ] = remainingSelectedTerms.join( ',' ); + } else { + const updatedParams = productFiltersContext.params; + + delete updatedParams[ `filter_${ attributeSlug }` ]; + delete updatedParams[ `query_type_${ attributeSlug }` ]; + + productFiltersContext.params = updatedParams; + } + } else { + productFiltersContext.params[ `filter_${ attributeSlug }` ] = + selectedTerms.concat( termSlug ).join( ',' ); + } }, - removeFilter: () => { - const { attributeSlug, queryType, value } = - getContext< ActiveAttributeFilterContext >(); - let selectedTerms = getSelectedTermsFromUrl( attributeSlug ); + clearFilters: () => { + const { attributeSlug } = getContext< AttributeFilterContext >(); + const productFiltersContext = getContext< ProductFiltersContext >( + 'woocommerce/product-filters' + ); + const updatedParams = productFiltersContext.params; - selectedTerms = selectedTerms.filter( ( item ) => item !== value ); + delete updatedParams[ `filter_${ attributeSlug }` ]; + delete updatedParams[ `query_type_${ attributeSlug }` ]; - navigate( getUrl( selectedTerms, attributeSlug, queryType ) ); + productFiltersContext.params = updatedParams; }, }, } ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/index.tsx index 6c1c49b3fb7..66a1c806d0b 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/index.tsx @@ -2,18 +2,21 @@ * External dependencies */ import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; -import { productFilterOptions } from '@woocommerce/icons'; +import { productFilterAttribute } from '@woocommerce/icons'; import { getSetting } from '@woocommerce/settings'; -import { registerBlockType } from '@wordpress/blocks'; import { AttributeSetting } from '@woocommerce/types'; +import { registerBlockType } from '@wordpress/blocks'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ import metadata from './block.json'; import Edit from './edit'; +import Save from './save'; import './style.scss'; +const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] ); if ( isExperimentalBlocksEnabled() ) { const defaultAttribute = getSetting< AttributeSetting >( 'defaultProductFilterAttribute' @@ -21,7 +24,7 @@ if ( isExperimentalBlocksEnabled() ) { registerBlockType( metadata, { edit: Edit, - icon: productFilterOptions, + icon: productFilterAttribute, attributes: { ...metadata.attributes, attributeId: { @@ -29,5 +32,25 @@ if ( isExperimentalBlocksEnabled() ) { default: parseInt( defaultAttribute.attribute_id, 10 ), }, }, + save: Save, + variations: ATTRIBUTES.map( ( attribute, index ) => { + return { + name: `product-filter-attribute-${ attribute.attribute_name }`, + title: `${ attribute.attribute_label } (Experimental)`, + description: sprintf( + // translators: %s is the attribute label. + __( + `Enable customers to filter the product collection by selecting one or more %s attributes.`, + 'woocommerce' + ), + attribute.attribute_label + ), + attributes: { + attributeId: parseInt( attribute.attribute_id, 10 ), + }, + isActive: [ 'attributeId' ], + isDefault: index === 0, + }; + } ), } ); } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/inspector.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/inspector.tsx similarity index 68% rename from plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/inspector.tsx rename to plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/inspector.tsx index 21b62ac0857..d2efdccbc07 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/inspector.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/inspector.tsx @@ -5,8 +5,9 @@ import { getSetting } from '@woocommerce/settings'; import { AttributeSetting } from '@woocommerce/types'; import { InspectorControls } from '@wordpress/block-editor'; import { dispatch, useSelect } from '@wordpress/data'; -import { createInterpolateElement } from '@wordpress/element'; +import { createInterpolateElement, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { Block, getBlockTypes, createBlock } from '@wordpress/blocks'; import { ComboboxControl, PanelBody, @@ -23,12 +24,15 @@ import { /** * Internal dependencies */ -import { sortOrderOptions } from '../constants'; -import { BlockAttributes, EditProps } from '../types'; -import { getAttributeFromId } from '../utils'; +import { sortOrderOptions } from './constants'; +import { BlockAttributes, EditProps } from './types'; +import { getAttributeFromId } from './utils'; +import { getInnerBlockByName } from '../../utils'; const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] ); +let displayStyleOptions: Block[] = []; + export const Inspector = ( { clientId, attributes, @@ -43,47 +47,29 @@ export const Inspector = ( { hideEmpty, clearButton, } = attributes; - const { updateBlockAttributes } = dispatch( 'core/block-editor' ); - const { productFilterWrapperBlockId, productFilterWrapperHeadingBlockId } = - useSelect( - ( select ) => { - if ( ! clientId ) - return { - productFilterWrapperBlockId: undefined, - productFilterWrapperHeadingBlockId: undefined, - }; + const { updateBlockAttributes, insertBlock, replaceBlock } = + dispatch( 'core/block-editor' ); + const filterBlock = useSelect( + ( select ) => { + return select( 'core/block-editor' ).getBlock( clientId ); + }, + [ clientId ] + ); + const [ displayStyleBlocksAttributes, setDisplayStyleBlocksAttributes ] = + useState< Record< string, unknown > >( {} ); - const { getBlockParentsByBlockName, getBlock } = - select( 'core/block-editor' ); + const filterHeadingBlock = getInnerBlockByName( + filterBlock, + 'core/heading' + ); - const parentBlocksByBlockName = getBlockParentsByBlockName( - clientId, - 'woocommerce/product-filter' - ); - - if ( parentBlocksByBlockName.length === 0 ) - return { - productFilterWrapperBlockId: undefined, - productFilterWrapperHeadingBlockId: undefined, - }; - - const parentBlockId = parentBlocksByBlockName[ 0 ]; - - const parentBlock = getBlock( parentBlockId ); - const headerGroupBlock = parentBlock?.innerBlocks.find( - ( block ) => block.name === 'core/group' - ); - const headingBlock = headerGroupBlock?.innerBlocks.find( - ( block ) => block.name === 'core/heading' - ); - - return { - productFilterWrapperBlockId: parentBlockId, - productFilterWrapperHeadingBlockId: headingBlock?.clientId, - }; - }, - [ clientId ] + if ( displayStyleOptions.length === 0 ) { + displayStyleOptions = getBlockTypes().filter( ( blockType ) => + blockType.ancestor?.includes( + 'woocommerce/product-filter-attribute' + ) ); + } return ( <> @@ -102,17 +88,9 @@ export const Inspector = ( { } ); const attributeObject = getAttributeFromId( numericId ); - if ( productFilterWrapperBlockId ) { + if ( filterHeadingBlock ) { updateBlockAttributes( - productFilterWrapperBlockId, - { - attributeId: numericId, - } - ); - } - if ( productFilterWrapperHeadingBlockId ) { - updateBlockAttributes( - productFilterWrapperHeadingBlockId, + filterHeadingBlock.clientId, { content: attributeObject?.label ?? @@ -188,17 +166,46 @@ export const Inspector = ( { value={ displayStyle } onChange={ ( value: BlockAttributes[ 'displayStyle' ] - ) => setAttributes( { displayStyle: value } ) } + ) => { + if ( ! filterBlock ) return; + const currentStyleBlock = getInnerBlockByName( + filterBlock, + displayStyle + ); + + if ( currentStyleBlock ) { + setDisplayStyleBlocksAttributes( { + ...displayStyleBlocksAttributes, + [ displayStyle ]: + currentStyleBlock.attributes, + } ); + replaceBlock( + currentStyleBlock.clientId, + createBlock( + value, + displayStyleBlocksAttributes[ value ] || + {} + ) + ); + } else { + insertBlock( + createBlock( value ), + filterBlock.innerBlocks.length, + filterBlock.clientId, + false + ); + } + setAttributes( { displayStyle: value } ); + } } style={ { width: '100%' } } > - - + { displayStyleOptions.map( ( blockType ) => ( + + ) ) } { + const blockProps = useBlockProps.save(); + const innerBlocksProps = useInnerBlocksProps.save( blockProps ); + return
    ; +}; + +export default Save; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json new file mode 100644 index 00000000000..75be211eb62 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "name": "woocommerce/product-filter-checkbox-list", + "version": "1.0.0", + "title": "List", + "description": "Display a list of filter options.", + "category": "woocommerce", + "keywords": [ + "WooCommerce" + ], + "textdomain": "woocommerce", + "apiVersion": 3, + "ancestor": [ + "woocommerce/product-filter-attribute" + ], + "supports": { + "color": { + "enableContrastChecker": false + } + }, + "usesContext": [ + "filterData" + ], + "attributes": { + "optionElementBorder": { + "type": "string", + "default": "" + }, + "customOptionElementBorder": { + "type": "string", + "default": "" + }, + "optionElementSelected": { + "type": "string", + "default": "" + }, + "customOptionElementSelected": { + "type": "string", + "default": "" + }, + "optionElement": { + "type": "string", + "default": "" + }, + "customOptionElement": { + "type": "string", + "default": "" + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx new file mode 100644 index 00000000000..0d290cf8bfa --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx @@ -0,0 +1,212 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; +import { __ } from '@wordpress/i18n'; +import { Icon } from '@wordpress/components'; +import { checkMark } from '@woocommerce/icons'; +import { useMemo } from '@wordpress/element'; +import { + useBlockProps, + withColors, + InspectorControls, + // @ts-expect-error - no types. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, + // @ts-expect-error - no types. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, +} from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import './style.scss'; +import './editor.scss'; +import { EditProps } from './types'; +import { getColorClasses, getColorVars } from './utils'; + +const Edit = ( props: EditProps ): JSX.Element => { + const { + clientId, + context, + attributes, + setAttributes, + optionElementBorder, + setOptionElementBorder, + optionElementSelected, + setOptionElementSelected, + optionElement, + setOptionElement, + } = props; + + const { + customOptionElementBorder, + customOptionElementSelected, + customOptionElement, + } = attributes; + const { filterData } = context; + const { isLoading, items } = filterData; + + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + const blockProps = useBlockProps( { + className: clsx( 'wc-block-product-filter-checkbox-list', { + 'is-loading': isLoading, + ...getColorClasses( attributes ), + } ), + style: getColorVars( attributes ), + } ); + + const loadingState = useMemo( () => { + return [ ...Array( 5 ) ].map( ( x, i ) => ( +
  • +   +
  • + ) ); + }, [] ); + + if ( ! items ) { + return <>; + } + + const threshold = 15; + const isLongList = items.length > threshold; + + return ( + <> +
    +
      + { isLoading && loadingState } + { ! isLoading && + ( isLongList + ? items.slice( 0, threshold ) + : items + ).map( ( item, index ) => ( +
    • + +
    • + ) ) } +
    + { ! isLoading && isLongList && ( + + ) } +
    + + { colorGradientSettings.hasColorsOrGradients && ( + { + setOptionElementBorder( colorValue ); + setAttributes( { + customOptionElementBorder: colorValue, + } ); + }, + resetAllFilter: () => { + setOptionElementBorder( '' ); + setAttributes( { + customOptionElementBorder: '', + } ); + }, + }, + { + label: __( + 'Option Element (Selected)', + 'woocommerce' + ), + colorValue: + optionElementSelected.color || + customOptionElementSelected, + isShownByDefault: true, + enableAlpha: true, + onColorChange: ( colorValue: string ) => { + setOptionElementSelected( colorValue ); + setAttributes( { + customOptionElementSelected: colorValue, + } ); + }, + resetAllFilter: () => { + setOptionElementSelected( '' ); + setAttributes( { + customOptionElementSelected: '', + } ); + }, + }, + { + label: __( 'Option Element', 'woocommerce' ), + colorValue: + optionElement.color || customOptionElement, + isShownByDefault: true, + enableAlpha: true, + onColorChange: ( colorValue: string ) => { + setOptionElement( colorValue ); + setAttributes( { + customOptionElement: colorValue, + } ); + }, + resetAllFilter: () => { + setOptionElement( '' ); + setAttributes( { + customOptionElement: '', + } ); + }, + }, + ] } + panelId={ clientId } + { ...colorGradientSettings } + /> + ) } + + + ); +}; + +export default withColors( { + optionElementBorder: 'option-element-border', + optionElementSelected: 'option-element-border', + optionElement: 'option-element', +} )( Edit ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/editor.scss new file mode 100644 index 00000000000..dbca49525b7 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/editor.scss @@ -0,0 +1,10 @@ +.wc-block-product-filter-checkbox-list.is-loading { + .wc-block-product-filter-checkbox-list__list { + padding: 0; + + li { + @include placeholder(); + margin: 5px 0; + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts new file mode 100644 index 00000000000..f88d26cf9e2 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import { getContext, store } from '@woocommerce/interactivity'; +import { HTMLElementEvent } from '@woocommerce/types'; + +/** + * Internal dependencies + */ + +export type CheckboxListContext = { + items: { + id: string; + label: string; + value: string; + checked: boolean; + }[]; + showAll: boolean; +}; + +store( 'woocommerce/product-filter-checkbox-list', { + actions: { + showAllItems: () => { + const context = getContext< CheckboxListContext >(); + context.showAll = true; + }, + + selectCheckboxItem: ( event: HTMLElementEvent< HTMLInputElement > ) => { + const context = getContext< CheckboxListContext >(); + const value = event.target.value; + + context.items = context.items.map( ( item ) => { + if ( item.value.toString() === value ) { + return { + ...item, + checked: ! item.checked, + }; + } + return item; + } ); + }, + }, +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/index.tsx new file mode 100644 index 00000000000..75205c4b208 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/index.tsx @@ -0,0 +1,22 @@ +/** + * External dependencies + */ +import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; +import { productFilterOptions } from '@woocommerce/icons'; +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import Edit from './edit'; +import './style.scss'; +import Save from './save'; + +if ( isExperimentalBlocksEnabled() ) { + registerBlockType( metadata, { + edit: Edit, + icon: productFilterOptions, + save: Save, + } ); +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/save.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/save.tsx new file mode 100644 index 00000000000..e470a5c0f49 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/save.tsx @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { useBlockProps } from '@wordpress/block-editor'; +import clsx from 'clsx'; + +/** + * Internal dependencies + */ +import { BlockAttributes } from './types'; +import { getColorClasses, getColorVars } from './utils'; + +const Save = ( { + attributes, + style, +}: { + attributes: BlockAttributes; + style: Record< string, string >; +} ) => { + const blockProps = useBlockProps.save( { + className: clsx( + 'wc-block-product-filter-checkbox-list', + attributes.className, + getColorClasses( attributes ) + ), + style: { + ...style, + ...getColorVars( attributes ), + }, + } ); + + return
    ; +}; + +export default Save; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/style.scss new file mode 100644 index 00000000000..8e0691d3af1 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/style.scss @@ -0,0 +1,86 @@ +:where(.wc-block-product-filter-checkbox-list__list) { + list-style: none outside; + margin: 0; + padding: 0; +} + +:where(.wc-block-product-filter-checkbox-list__label) { + align-items: center; + display: flex; + gap: 0.625em; +} + +.wc-block-product-filter-checkbox-list__item .wc-block-product-filter-checkbox-list__label { + margin-bottom: 0; +} + +:where(.wc-block-product-filter-checkbox-list__input-wrapper) { + display: block; + position: relative; +} + +.wc-block-product-filter-checkbox-list__input-wrapper::before { + content: ""; + left: 0; + position: absolute; + top: 0; + background: currentColor; + opacity: 0.1; + width: 1em; + height: 1em; + border-radius: 2px; + pointer-events: none; + + .has-option-element-color & { + display: none; + } +} + +:where(.wc-block-product-filter-checkbox-list__input) { + appearance: none; + border-radius: 2px; + border: 1px solid var(--wc-product-filter-checkbox-list-option-element-border, transparent); + color: inherit; + display: block; + font-size: inherit; + height: 1em; + margin: 0; + width: 1em; + background: var(--wc-product-filter-checkbox-list-option-element, transparent); + cursor: pointer; +} + +.wc-block-product-filter-checkbox-list__input:checked + .wc-block-product-filter-checkbox-list__mark { + display: block; + pointer-events: none; +} + +.wc-block-product-filter-checkbox-list__input:focus { + outline-width: 1px; + outline-color: var(--wc-product-filter-checkbox-list-option-element-border, currentColor); +} + +:where(.wc-block-product-filter-checkbox-list__mark) { + box-sizing: border-box; + display: none; + height: 1em; + left: 0; + padding: 0.2em; + position: absolute; + top: 0; + width: 1em; + color: var(--wc-product-filter-checkbox-list-option-element-selected, currentColor); +} + +:where(.wc-block-product-filter-checkbox-list__text) { + font-size: 0.875em; +} + +:where(.wc-block-product-filter-checkbox-list__show-more) { + text-decoration: underline; + appearance: none; + background: transparent; + border: none; + padding: 0; +} + diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/types.ts new file mode 100644 index 00000000000..ef10e7f6bd4 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/types.ts @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import { BlockEditProps } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { FilterBlockContext } from '../../types'; + +export type Color = { + slug: string; + class: string; + name: string; + color: string; +}; + +export type BlockAttributes = { + className: string; + optionElementBorder: string; + customOptionElementBorder: string; + optionElementSelected: string; + customOptionElementSelected: string; + optionElement: string; + customOptionElement: string; +}; + +export type EditProps = BlockEditProps< BlockAttributes > & { + context: FilterBlockContext; + optionElementBorder: Color; + setOptionElementBorder: ( value: string ) => void; + optionElementSelected: Color; + setOptionElementSelected: ( value: string ) => void; + optionElement: Color; + setOptionElement: ( value: string ) => void; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/utils.ts new file mode 100644 index 00000000000..df761d0aed6 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/utils.ts @@ -0,0 +1,66 @@ +/** + * Internal dependencies + */ +import { BlockAttributes } from './types'; + +function getCSSVar( slug: string | undefined, value: string | undefined ) { + if ( slug ) { + return `var(--wp--preset--color--${ slug })`; + } + return value || ''; +} + +export function getColorVars( attributes: BlockAttributes ) { + const { + optionElement, + optionElementBorder, + optionElementSelected, + customOptionElement, + customOptionElementBorder, + customOptionElementSelected, + } = attributes; + + const vars: Record< string, string > = { + '--wc-product-filter-checkbox-list-option-element': getCSSVar( + optionElement, + customOptionElement + ), + '--wc-product-filter-checkbox-list-option-element-border': getCSSVar( + optionElementBorder, + customOptionElementBorder + ), + '--wc-product-filter-checkbox-list-option-element-selected': getCSSVar( + optionElementSelected, + customOptionElementSelected + ), + }; + + return Object.keys( vars ).reduce( + ( acc: Record< string, string >, key ) => { + if ( vars[ key ] ) { + acc[ key ] = vars[ key ]; + } + return acc; + }, + {} + ); +} + +export function getColorClasses( attributes: BlockAttributes ) { + const { + optionElement, + optionElementBorder, + optionElementSelected, + customOptionElement, + customOptionElementBorder, + customOptionElementSelected, + } = attributes; + + return { + 'has-option-element-color': optionElement || customOptionElement, + 'has-option-element-border-color': + optionElementBorder || customOptionElementBorder, + 'has-option-element-selected-color': + optionElementSelected || customOptionElementSelected, + }; +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json new file mode 100644 index 00000000000..7ef9d364b96 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "name": "woocommerce/product-filter-chips", + "version": "1.0.0", + "title": "Chips", + "description": "Display filter options as chips.", + "category": "woocommerce", + "keywords": [ + "WooCommerce" + ], + "textdomain": "woocommerce", + "apiVersion": 3, + "ancestor": [ + "woocommerce/product-filter-attribute" + ], + "supports": {}, + "usesContext": [ + "filterData" + ], + "attributes": { + "chipText":{ + "type": "string" + }, + "customChipText":{ + "type": "string" + }, + "chipBackground":{ + "type": "string" + }, + "customChipBackground":{ + "type": "string" + }, + "chipBorder":{ + "type": "string" + }, + "customChipBorder":{ + "type": "string" + }, + "selectedChipText":{ + "type": "string" + }, + "customSelectedChipText":{ + "type": "string" + }, + "selectedChipBackground":{ + "type": "string" + }, + "customSelectedChipBackground":{ + "type": "string" + }, + "selectedChipBorder":{ + "type": "string" + }, + "customSelectedChipBorder":{ + "type": "string" + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx new file mode 100644 index 00000000000..b2782b3c323 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx @@ -0,0 +1,260 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useMemo } from '@wordpress/element'; +import clsx from 'clsx'; +import { + InspectorControls, + useBlockProps, + withColors, + // @ts-expect-error - no types. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, + // @ts-expect-error - no types. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, +} from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { EditProps } from './types'; +import './editor.scss'; +import { getColorClasses, getColorVars } from './utils'; + +const Edit = ( props: EditProps ): JSX.Element => { + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + const { + context, + clientId, + attributes, + setAttributes, + chipText, + setChipText, + chipBackground, + setChipBackground, + chipBorder, + setChipBorder, + selectedChipText, + setSelectedChipText, + selectedChipBackground, + setSelectedChipBackground, + selectedChipBorder, + setSelectedChipBorder, + } = props; + const { + customChipText, + customChipBackground, + customChipBorder, + customSelectedChipText, + customSelectedChipBackground, + customSelectedChipBorder, + } = attributes; + const { filterData } = context; + const { isLoading, items } = filterData; + + const blockProps = useBlockProps( { + className: clsx( 'wc-block-product-filter-chips', { + 'is-loading': isLoading, + ...getColorClasses( attributes ), + } ), + style: getColorVars( attributes ), + } ); + + const loadingState = useMemo( () => { + return [ ...Array( 10 ) ].map( ( _, i ) => ( +
    +   +
    + ) ); + }, [] ); + + if ( ! items ) { + return <>; + } + + const threshold = 15; + const isLongList = items.length > threshold; + + return ( + <> +
    +
    + { isLoading && loadingState } + { ! isLoading && + ( isLongList + ? items.slice( 0, threshold ) + : items + ).map( ( item, index ) => ( +
    + + { item.label } + +
    + ) ) } +
    + { ! isLoading && isLongList && ( + + ) } +
    + + { colorGradientSettings.hasColorsOrGradients && ( + { + setChipText( colorValue ); + setAttributes( { + customChipText: colorValue, + } ); + }, + resetAllFilter: () => { + setChipText( '' ); + setAttributes( { + customChipText: '', + } ); + }, + }, + { + label: __( + 'Unselected Chip Border', + 'woocommerce' + ), + colorValue: + chipBorder.color || customChipBorder, + onColorChange: ( colorValue: string ) => { + setChipBorder( colorValue ); + setAttributes( { + customChipBorder: colorValue, + } ); + }, + resetAllFilter: () => { + setChipBorder( '' ); + setAttributes( { + customChipBorder: '', + } ); + }, + }, + { + label: __( + 'Unselected Chip Background', + 'woocommerce' + ), + colorValue: + chipBackground.color || + customChipBackground, + onColorChange: ( colorValue: string ) => { + setChipBackground( colorValue ); + setAttributes( { + customChipBackground: colorValue, + } ); + }, + resetAllFilter: () => { + setChipBackground( '' ); + setAttributes( { + customChipBackground: '', + } ); + }, + }, + { + label: __( + 'Selected Chip Text', + 'woocommerce' + ), + colorValue: + selectedChipText.color || + customSelectedChipText, + onColorChange: ( colorValue: string ) => { + setSelectedChipText( colorValue ); + setAttributes( { + customSelectedChipText: colorValue, + } ); + }, + resetAllFilter: () => { + setSelectedChipText( '' ); + setAttributes( { + customSelectedChipText: '', + } ); + }, + }, + { + label: __( + 'Selected Chip Border', + 'woocommerce' + ), + colorValue: + selectedChipBorder.color || + customSelectedChipBorder, + onColorChange: ( colorValue: string ) => { + setSelectedChipBorder( colorValue ); + setAttributes( { + customSelectedChipBorder: colorValue, + } ); + }, + resetAllFilter: () => { + setSelectedChipBorder( '' ); + setAttributes( { + customSelectedChipBorder: '', + } ); + }, + }, + { + label: __( + 'Selected Chip Background', + 'woocommerce' + ), + colorValue: + selectedChipBackground.color || + customSelectedChipBackground, + onColorChange: ( colorValue: string ) => { + setSelectedChipBackground( colorValue ); + setAttributes( { + customSelectedChipBackground: + colorValue, + } ); + }, + resetAllFilter: () => { + setSelectedChipBackground( '' ); + setAttributes( { + customSelectedChipBackground: '', + } ); + }, + }, + ] } + panelId={ clientId } + { ...colorGradientSettings } + /> + ) } + + + ); +}; + +export default withColors( { + chipText: 'chip-text', + chipBorder: 'chip-border', + chipBackground: 'chip-background', + selectedChipText: 'selected-chip-text', + selectedChipBorder: 'selected-chip-border', + selectedChipBackground: 'selected-chip-background', +} )( Edit ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/editor.scss new file mode 100644 index 00000000000..ec741894be7 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/editor.scss @@ -0,0 +1,6 @@ +.wc-block-product-filter-chips.is-loading { + .wc-block-product-filter-chips__item { + @include placeholder(); + margin: 5px 0; + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts new file mode 100644 index 00000000000..45a5de9f317 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { getElement, getContext, store } from '@woocommerce/interactivity'; + +/** + * Internal dependencies + */ + +export type ChipsContext = { + items: { + id: string; + label: string; + value: string; + checked: boolean; + }[]; + showAll: boolean; +}; + +store( 'woocommerce/product-filter-chips', { + actions: { + showAllItems: () => { + const context = getContext< ChipsContext >(); + context.showAll = true; + }, + + selectItem: () => { + const { ref } = getElement(); + const value = ref.getAttribute( 'value' ); + + if ( ! value ) return; + + const context = getContext< ChipsContext >(); + + context.items = context.items.map( ( item ) => { + if ( item.value.toString() === value ) { + return { + ...item, + checked: ! item.checked, + }; + } + return item; + } ); + }, + }, +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/index.tsx new file mode 100644 index 00000000000..df088f6f16f --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/index.tsx @@ -0,0 +1,22 @@ +/** + * External dependencies + */ +import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; +import { productFilterOptions } from '@woocommerce/icons'; +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import Edit from './edit'; +import Save from './save'; +import './style.scss'; + +if ( isExperimentalBlocksEnabled() ) { + registerBlockType( metadata, { + edit: Edit, + icon: productFilterOptions, + save: Save, + } ); +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/save.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/save.tsx new file mode 100644 index 00000000000..5a5f9a3d110 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/save.tsx @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { useBlockProps } from '@wordpress/block-editor'; +import clsx from 'clsx'; + +/** + * Internal dependencies + */ +import { BlockAttributes } from './types'; +import { getColorClasses, getColorVars } from './utils'; + +const Save = ( { + attributes, + style, +}: { + attributes: BlockAttributes; + style: Record< string, string >; +} ) => { + const blockProps = useBlockProps.save( { + className: clsx( + 'wc-block-product-filter-chips', + attributes.className, + getColorClasses( attributes ) + ), + style: { + ...style, + ...getColorVars( attributes ), + }, + } ); + + return
    ; +}; + +export default Save; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss new file mode 100644 index 00000000000..a26f333241b --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss @@ -0,0 +1,55 @@ +:where(.wc-block-product-filter-chips__items) { + display: flex; + flex-wrap: wrap; + gap: $gap-smallest; +} + +:where(.wc-block-product-filter-chips__item) { + border: 1px solid color-mix(in srgb, currentColor 20%, transparent); + padding: $gap-smallest $gap-smaller; + appearance: none; + background: transparent; + border-radius: 2px; + font-size: 0.875em; + cursor: pointer; + + .has-chip-text & { + color: var(--wc-product-filter-chips-text); + } + .has-chip-background & { + background: var(--wc-product-filter-chips-background); + } + .has-chip-border & { + border-color: var(--wc-product-filter-chips-border); + } +} + +:where(.wc-block-product-filter-chips__item[aria-checked="true"]) { + background: currentColor; + + .has-selected-chip-text & { + color: var(--wc-product-filter-chips-selected-text); + } + .has-selected-chip-background & { + background: var(--wc-product-filter-chips-selected-background); + } + .has-selected-chip-border & { + border-color: var(--wc-product-filter-chips-selected-border); + } +} + +:where( +.wc-block-product-filter-chips:not(.has-selected-chip-text) +.wc-block-product-filter-chips__item[aria-checked="true"] +> .wc-block-product-filter-chips__label +) { + filter: invert(100%); +} + +:where(.wc-block-product-filter-chips__show-more) { + text-decoration: underline; + appearance: none; + background: transparent; + border: none; + padding: 0; +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/types.ts new file mode 100644 index 00000000000..096019cb760 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/types.ts @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import { BlockEditProps } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { FilterBlockContext } from '../../types'; + +export type Color = { + slug?: string; + name?: string; + class?: string; + color: string; +}; + +export type BlockAttributes = { + className: string; + chipText?: string; + customChipText?: string; + chipBackground?: string; + customChipBackground?: string; + chipBorder?: string; + customChipBorder?: string; + selectedChipText?: string; + customSelectedChipText?: string; + selectedChipBackground?: string; + customSelectedChipBackground?: string; + selectedChipBorder?: string; + customSelectedChipBorder?: string; +}; + +export type EditProps = BlockEditProps< BlockAttributes > & { + style: Record< string, string >; + context: FilterBlockContext; + chipText: Color; + setChipText: ( value: string ) => void; + chipBackground: Color; + setChipBackground: ( value: string ) => void; + chipBorder: Color; + setChipBorder: ( value: string ) => void; + selectedChipText: Color; + setSelectedChipText: ( value: string ) => void; + selectedChipBackground: Color; + setSelectedChipBackground: ( value: string ) => void; + selectedChipBorder: Color; + setSelectedChipBorder: ( value: string ) => void; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/utils.ts new file mode 100644 index 00000000000..0d9d462600e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/utils.ts @@ -0,0 +1,91 @@ +/** + * Internal dependencies + */ +import { BlockAttributes } from './types'; + +function getCSSVar( slug: string | undefined, value: string | undefined ) { + if ( slug ) { + return `var(--wp--preset--color--${ slug })`; + } + return value || ''; +} + +export function getColorVars( attributes: BlockAttributes ) { + const { + chipText, + chipBackground, + chipBorder, + selectedChipText, + selectedChipBackground, + selectedChipBorder, + customChipText, + customChipBackground, + customChipBorder, + customSelectedChipText, + customSelectedChipBackground, + customSelectedChipBorder, + } = attributes; + + const vars: Record< string, string > = { + '--wc-product-filter-chips-text': getCSSVar( chipText, customChipText ), + '--wc-product-filter-chips-background': getCSSVar( + chipBackground, + customChipBackground + ), + '--wc-product-filter-chips-border': getCSSVar( + chipBorder, + customChipBorder + ), + '--wc-product-filter-chips-selected-text': getCSSVar( + selectedChipText, + customSelectedChipText + ), + '--wc-product-filter-chips-selected-background': getCSSVar( + selectedChipBackground, + customSelectedChipBackground + ), + '--wc-product-filter-chips-selected-border': getCSSVar( + selectedChipBorder, + customSelectedChipBorder + ), + }; + + return Object.keys( vars ).reduce( + ( acc: Record< string, string >, key ) => { + if ( vars[ key ] ) { + acc[ key ] = vars[ key ]; + } + return acc; + }, + {} + ); +} + +export function getColorClasses( attributes: BlockAttributes ) { + const { + chipText, + chipBackground, + chipBorder, + selectedChipText, + selectedChipBackground, + selectedChipBorder, + customChipText, + customChipBackground, + customChipBorder, + customSelectedChipText, + customSelectedChipBackground, + customSelectedChipBorder, + } = attributes; + + return { + 'has-chip-text-color': chipText || customChipText, + 'has-chip-background-color': chipBackground || customChipBackground, + 'has-chip-border-color': chipBorder || customChipBorder, + 'has-selected-chip-text-color': + selectedChipText || customSelectedChipText, + 'has-selected-chip-background-color': + selectedChipBackground || customSelectedChipBackground, + 'has-selected-chip-border-color': + selectedChipBorder || customSelectedChipBorder, + }; +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/frontend.ts index 0bfc299b660..7fccd55836c 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/frontend.ts @@ -1,3 +1,8 @@ +/** + * Logic in this file is unused and should be moved to product-fitlers block. + * + * @see https://github.com/woocommerce/woocommerce/issues/50868 + */ /** * External dependencies */ @@ -6,7 +11,7 @@ import { store, getContext } from '@woocommerce/interactivity'; /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; const getQueryParams = ( e: Event ) => { const filterNavContainer = ( e.target as HTMLElement )?.closest( diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts index 844e3c364f6..482e3e51117 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts @@ -9,7 +9,7 @@ import { debounce } from '@woocommerce/base-utils'; /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; import type { PriceFilterContext, PriceFilterStore } from './types'; const getUrl = ( context: PriceFilterContext ) => { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block-variations.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block-variations.tsx deleted file mode 100644 index 7d33338bea6..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block-variations.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/** - * External dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { BlockVariation } from '@wordpress/blocks'; -import { - productFilterActive, - productFilterAttribute, - productFilterPrice, - productFilterRating, - productFilterStockStatus, -} from '@woocommerce/icons'; -import { getSetting } from '@woocommerce/settings'; -import { AttributeSetting, objectHasProp } from '@woocommerce/types'; - -const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] ); - -const variations: BlockVariation[] = [ - { - name: 'product-filter-active', - title: __( 'Active (Experimental)', 'woocommerce' ), - description: __( - 'Display the currently active filters.', - 'woocommerce' - ), - attributes: { - heading: __( 'Active filters', 'woocommerce' ), - filterType: 'active-filters', - }, - icon: productFilterActive, - isDefault: true, - }, - { - name: 'product-filter-price', - title: __( 'Price (Experimental)', 'woocommerce' ), - description: __( - 'Enable customers to filter the product collection by choosing a price range.', - 'woocommerce' - ), - attributes: { - filterType: 'price-filter', - heading: __( 'Price', 'woocommerce' ), - }, - icon: productFilterPrice, - }, - { - name: 'product-filter-stock-status', - title: __( 'Status (Experimental)', 'woocommerce' ), - description: __( - 'Enable customers to filter the product collection by stock status.', - 'woocommerce' - ), - attributes: { - filterType: 'stock-filter', - heading: __( 'Status', 'woocommerce' ), - }, - icon: productFilterStockStatus, - }, - { - name: 'product-filter-rating', - title: __( 'Rating (Experimental)', 'woocommerce' ), - description: __( - 'Enable customers to filter the product collection by rating.', - 'woocommerce' - ), - attributes: { - filterType: 'rating-filter', - heading: __( 'Rating', 'woocommerce' ), - }, - icon: productFilterRating, - }, -]; - -ATTRIBUTES.forEach( ( attribute ) => { - variations.push( { - name: `product-filter-attribute-${ attribute.attribute_name }`, - title: `${ attribute.attribute_label } (Experimental)`, - description: sprintf( - // translators: %s is the attribute label. - __( - `Enable customers to filter the product collection by selecting one or more %s attributes.`, - 'woocommerce' - ), - attribute.attribute_label - ), - attributes: { - filterType: 'attribute-filter', - heading: attribute.attribute_label, - attributeId: parseInt( attribute.attribute_id, 10 ), - }, - icon: productFilterAttribute, - // Can be `isActive: [ 'filterType', 'attributeId' ]`, but the API is available from 6.6. - isActive: ( blockAttributes, variationAttributes ) => { - return ( - blockAttributes.filterType === variationAttributes.filterType && - blockAttributes.attributeId === variationAttributes.attributeId - ); - }, - } ); -} ); - -variations.push( { - name: 'product-filter-attribute', - title: __( 'Attribute (Experimental)', 'woocommerce' ), - description: __( - 'Enable customers to filter the product collection by selecting one or more attributes, such as color.', - 'woocommerce' - ), - attributes: { - filterType: 'attribute-filter', - heading: __( 'Attribute', 'woocommerce' ), - attributeId: 0, - }, - icon: productFilterAttribute, -} ); - -/** - * Add `isActive` function to all Product Filter block variations. - * `isActive` function is used to find a variation match from a created - * Block by providing its attributes. - */ -variations.forEach( ( variation ) => { - if ( ! objectHasProp( variation, 'isActive' ) ) { - // @ts-expect-error: `isActive` is currently typed wrong in `@wordpress/blocks`. - variation.isActive = [ 'filterType' ]; - } -} ); - -export const blockVariations = variations; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block.json deleted file mode 100644 index 673300a288c..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "woocommerce/product-filter", - "version": "1.0.0", - "title": "Product Filter (Experimental)", - "description": "A block that adds product filters to the product collection.", - "category": "woocommerce", - "keywords": [ - "WooCommerce", - "Filters" - ], - "textdomain": "woocommerce", - "supports": { - "html": false, - "reusable": false, - "inserter": true - }, - "ancestor": [ - "woocommerce/product-filters" - ], - "usesContext": [ - "query", - "queryId" - ], - "attributes": { - "filterType": { - "type": "string" - }, - "heading": { - "type": "string" - }, - "isPreview": { - "type": "boolean", - "default": false - }, - "attributeId": { - "type": "number", - "default": 0 - } - }, - "example": { - "attributes": { - "isPreview": true - } - }, - "apiVersion": 3, - "$schema": "https://schemas.wp.org/trunk/block.json" -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/components/warning.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/components/warning.tsx deleted file mode 100644 index 33b16eaf5cf..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/components/warning.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * External dependencies - */ -import { getSetting } from '@woocommerce/settings'; -import { Notice } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; - -const Warning = () => { - const isWidgetEditor = getSetting< boolean >( 'isWidgetEditor' ); - if ( isWidgetEditor ) { - return ( - - { __( - 'The widget area containing Collection Filters block needs to be placed on a product archive page for filters to function properly.', - 'woocommerce' - ) } - - ); - } - - const isSiteEditor = getSetting< boolean >( 'isSiteEditor' ); - if ( ! isSiteEditor ) { - return ( - - { __( - 'When added to a post or page, Collection Filters block needs to be nested inside a Product Collection block to function properly.', - 'woocommerce' - ) } - - ); - } - - return null; -}; - -export default Warning; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/constants.ts deleted file mode 100644 index ba212b495f2..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const BLOCK_NAME_MAP = { - 'active-filters': 'woocommerce/product-filter-active', - 'price-filter': 'woocommerce/product-filter-price', - 'stock-filter': 'woocommerce/product-filter-stock-status', - 'rating-filter': 'woocommerce/product-filter-rating', - 'attribute-filter': 'woocommerce/product-filter-attribute', - 'clear-button': 'woocommerce/product-filter-clear-button', -}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/edit.tsx deleted file mode 100644 index 9706f037f9f..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/edit.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/** - * External dependencies - */ -import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; -import { BlockEditProps } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import Warning from './components/warning'; -import './editor.scss'; -import { getAllowedBlocks } from './utils'; -import { BLOCK_NAME_MAP } from './constants'; -import type { FilterType } from './types'; - -const Edit = ( { - attributes, - clientId, -}: BlockEditProps< { - heading: string; - filterType: FilterType; - isPreview: boolean; - attributeId: number | undefined; -} > ) => { - const blockProps = useBlockProps(); - - const isNested = useSelect( ( select ) => { - const { getBlockParentsByBlockName } = select( 'core/block-editor' ); - return !! getBlockParentsByBlockName( - clientId, - 'woocommerce/product-collection' - ).length; - } ); - - return ( - - ); -}; - -export default Edit; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/editor.scss deleted file mode 100644 index b739f84f377..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/editor.scss +++ /dev/null @@ -1,5 +0,0 @@ -.wp-block-woocommerce-collection-filters { - .components-notice { - margin: 0; - } -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/frontend.ts deleted file mode 100644 index 13a489f89c0..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/frontend.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * External dependencies - */ -import { navigate as navigateFn } from '@woocommerce/interactivity'; -import { getSetting } from '@woocommerce/settings'; - -const isBlockTheme = getSetting< boolean >( 'isBlockTheme' ); -const isProductArchive = getSetting< boolean >( 'isProductArchive' ); -const needsRefresh = getSetting< boolean >( - 'needsRefreshForInteractivityAPI', - false -); - -export function navigate( href: string, options = {} ) { - /** - * We may need to reset the current page when changing filters. - * This is because the current page may not exist for this set - * of filters and will 404 when the user navigates to it. - * - * There are different pagination formats to consider, as documented here: - * https://github.com/WordPress/gutenberg/blob/317eb8f14c8e1b81bf56972cca2694be250580e3/packages/block-library/src/query-pagination-numbers/index.php#L22-L85 - */ - const url = new URL( href ); - // When pretty permalinks are enabled, the page number may be in the path name. - url.pathname = url.pathname.replace( /\/page\/[0-9]+/i, '' ); - // When plain permalinks are enabled, the page number may be in the "paged" query parameter. - url.searchParams.delete( 'paged' ); - // On posts and pages the page number will be in a query parameter that - // identifies which block we are paginating. - url.searchParams.forEach( ( _, key ) => { - if ( key.match( /^query(?:-[0-9]+)?-page$/ ) ) { - url.searchParams.delete( key ); - } - } ); - // Make sure to update the href with the changes. - href = url.href; - - if ( needsRefresh || ( ! isBlockTheme && isProductArchive ) ) { - return ( window.location.href = href ); - } - return navigateFn( href, options ); -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/index.tsx deleted file mode 100644 index 850ac787f5d..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/** - * External dependencies - */ -import { - BlockInstance, - createBlock, - registerBlockType, -} from '@wordpress/blocks'; -import { Icon, more } from '@wordpress/icons'; -import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; -/** - * Internal dependencies - */ -import metadata from './block.json'; -import edit from './edit'; -import save from './save'; -import { BLOCK_NAME_MAP } from './constants'; -import { BlockAttributes } from './types'; -import { blockVariations } from './block-variations'; - -if ( isExperimentalBlocksEnabled() ) { - registerBlockType( metadata, { - icon: { - src: ( - - ), - }, - edit, - save, - variations: blockVariations, - transforms: { - from: [ - { - type: 'block', - blocks: [ 'woocommerce/filter-wrapper' ], - transform: ( - attributes: BlockAttributes, - innerBlocks: BlockInstance[] - ) => { - const newInnerBlocks: BlockInstance[] = []; - // Loop through inner blocks to preserve the block order. - innerBlocks.forEach( ( block ) => { - if ( - block.name === - `woocommerce/${ attributes.filterType }` - ) { - newInnerBlocks.push( - createBlock( - BLOCK_NAME_MAP[ attributes.filterType ], - block.attributes - ) - ); - } - - if ( block.name === 'core/heading' ) { - newInnerBlocks.push( block ); - } - } ); - - return createBlock( - 'woocommerce/product-filter', - attributes, - newInnerBlocks - ); - }, - }, - ], - }, - } ); -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/save.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/save.tsx deleted file mode 100644 index 992874cb932..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/save.tsx +++ /dev/null @@ -1,8 +0,0 @@ -/** - * External dependencies - */ -import { InnerBlocks } from '@wordpress/block-editor'; - -export default function save() { - return ; -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/types.ts deleted file mode 100644 index 726784edfd3..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * External dependencies - */ -import { BlockEditProps } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { BLOCK_NAME_MAP } from './constants'; - -export type FilterType = keyof typeof BLOCK_NAME_MAP; - -export type BlockAttributes = { - filterType: FilterType; - heading: string; -}; - -export type EditProps = BlockEditProps< BlockAttributes >; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/utils.ts deleted file mode 100644 index 10a27961150..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * External dependencies - */ -import { getBlockTypes } from '@wordpress/blocks'; - -/** - * Returns an array of allowed block names excluding the disallowedBlocks array. - * - * @param disallowedBlocks Array of block names to disallow. - * @return Array of allowed block names. - */ -export const getAllowedBlocks = ( disallowedBlocks: string[] ) => { - const allBlocks = getBlockTypes(); - - return allBlocks - .map( ( block ) => block.name ) - .filter( ( name ) => ! disallowedBlocks.includes( name ) ); -}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/frontend.ts index 3bd9f961704..8d865131b33 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/frontend.ts @@ -8,7 +8,7 @@ import { DropdownContext } from '@woocommerce/interactivity-components/dropdown' /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; function getUrl( filters: Array< string | null > ) { filters = filters.filter( Boolean ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/stock-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/stock-filter/frontend.ts index 9833ff753c1..f0037014671 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/stock-filter/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/stock-filter/frontend.ts @@ -9,7 +9,7 @@ import { CheckboxListContext } from '@woocommerce/interactivity-components/check /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; const getUrl = ( activeFilters: string ) => { const url = new URL( window.location.href ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts index f7ad8b247e9..0709a1490b8 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts @@ -7,8 +7,8 @@ export type BlockOverlayAttributeOptions = ( typeof BlockOverlayAttribute )[ keyof typeof BlockOverlayAttribute ]; export interface BlockAttributes { - productId?: string; setAttributes: ( attributes: ProductFiltersBlockAttributes ) => void; + productId?: string; overlay: BlockOverlayAttributeOptions; overlayIcon: | 'filter-icon-1' @@ -18,3 +18,23 @@ export interface BlockAttributes { overlayButtonStyle: 'label-icon' | 'label' | 'icon'; overlayIconSize?: number; } + +export type FilterOptionItem = { + label: string; + value: string; + selected?: boolean; + rawData?: Record< string, unknown >; +}; + +export type FilterBlockContext = { + filterData: { + isLoading: boolean; + items?: FilterOptionItem[]; + range?: { + min: number; + max: number; + step: number; + }; + }; + isParentSelected: boolean; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/utils.ts new file mode 100644 index 00000000000..d21e622fc3e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/utils.ts @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { BlockInstance, getBlockTypes } from '@wordpress/blocks'; + +/** + * Returns an array of allowed block names excluding the disallowedBlocks array. + * + * @param disallowedBlocks Array of block names to disallow. + * @return Array of allowed block names. + */ +export const getAllowedBlocks = ( disallowedBlocks: string[] ) => { + const allBlocks = getBlockTypes(); + + return allBlocks + .map( ( block ) => block.name ) + .filter( ( name ) => ! disallowedBlocks.includes( name ) ); +}; + +export const getInnerBlockByName = ( + block: BlockInstance | null, + name: string +): BlockInstance | null => { + if ( ! block ) return null; + + if ( block.innerBlocks.length === 0 ) return null; + + for ( const innerBlock of block.innerBlocks ) { + if ( innerBlock.name === name ) return innerBlock; + const innerInnerBlock = getInnerBlockByName( innerBlock, name ); + if ( innerInnerBlock ) return innerInnerBlock; + } + + return null; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/edit.tsx index f6e3898701f..d220d0fdb67 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/edit.tsx @@ -10,6 +10,10 @@ import { import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks'; import { useEffect } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; +import ErrorPlaceholder, { + ErrorObject, +} from '@woocommerce/editor-components/error-placeholder'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -132,14 +136,16 @@ export const Edit = ( { useEffect( () => { const mode = getMode( currentTemplateId, templateType ); + const newProductGalleryClientId = + attributes.productGalleryClientId || clientId; setAttributes( { ...attributes, mode, - productGalleryClientId: clientId, + productGalleryClientId: newProductGalleryClientId, } ); // Move the Thumbnails block to the correct above or below the Large Image based on the thumbnailsPosition attribute. - moveInnerBlocksToPosition( attributes, clientId ); + moveInnerBlocksToPosition( attributes, newProductGalleryClientId ); }, [ setAttributes, attributes, @@ -148,6 +154,18 @@ export const Edit = ( { templateType, ] ); + if ( attributes.productGalleryClientId !== clientId ) { + const error = { + message: __( + 'productGalleryClientId and clientId codes mismatch.', + 'woocommerce' + ), + type: 'general', + } as ErrorObject; + + return ; + } + return (
    diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/utils.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/utils.tsx index eb2e15258b2..d5b97d3ea2e 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/utils.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/utils.tsx @@ -136,10 +136,10 @@ export const moveInnerBlocksToPosition = ( ): void => { const { getBlock, getBlockRootClientId, getBlockIndex } = select( 'core/block-editor' ); - const { moveBlockToPosition } = dispatch( 'core/block-editor' ); const productGalleryBlock = getBlock( clientId ); - if ( productGalleryBlock ) { + if ( productGalleryBlock?.name === 'woocommerce/product-gallery' ) { + const { moveBlockToPosition } = dispatch( 'core/block-editor' ); const previousLayout = productGalleryBlock.innerBlocks.length ? productGalleryBlock.innerBlocks[ 0 ].attributes.layout : null; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-search/constants.ts new file mode 100644 index 00000000000..ae42399cf3d --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/constants.ts @@ -0,0 +1,10 @@ +export const SEARCH_BLOCK_NAME = 'core/search'; +export const SEARCH_VARIATION_NAME = 'woocommerce/product-search'; + +export enum PositionOptions { + OUTSIDE = 'button-outside', + INSIDE = 'button-inside', + NO_BUTTON = 'no-button', + BUTTON_ONLY = 'button-only', + INPUT_AND_BUTTON = 'input-and-button', +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx index 9691e2cdf9a..89bd2ac4dac 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx @@ -2,6 +2,7 @@ /** * External dependencies */ +import { addFilter } from '@wordpress/hooks'; import { store as blockEditorStore, Warning } from '@wordpress/block-editor'; import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; @@ -9,6 +10,7 @@ import { Icon, search } from '@wordpress/icons'; import { getSettingWithCoercion } from '@woocommerce/settings'; import { isBoolean } from '@woocommerce/types'; import { Button } from '@wordpress/components'; +import type { Block as BlockType } from '@wordpress/blocks'; import { // @ts-ignore waiting for @types/wordpress__blocks update registerBlockVariation, @@ -21,8 +23,10 @@ import { */ import './style.scss'; import './editor.scss'; +import { withProductSearchControls } from './inspector-controls'; import Block from './block'; import Edit from './edit'; +import { SEARCH_BLOCK_NAME, SEARCH_VARIATION_NAME } from './constants'; const isBlockVariationAvailable = getSettingWithCoercion( 'isBlockVariationAvailable', @@ -71,6 +75,7 @@ const PRODUCT_SEARCH_ATTRIBUTES = { query: { post_type: 'product', }, + namespace: SEARCH_VARIATION_NAME, }; const DeprecatedBlockEdit = ( { clientId }: { clientId: string } ) => { @@ -115,7 +120,7 @@ const DeprecatedBlockEdit = ( { clientId }: { clientId: string } ) => { ); }; -registerBlockType( 'woocommerce/product-search', { +registerBlockType( SEARCH_VARIATION_NAME, { title: __( 'Product Search', 'woocommerce' ), apiVersion: 3, icon: { @@ -146,7 +151,7 @@ registerBlockType( 'woocommerce/product-search', { isMatch: ( { idBase, instance } ) => idBase === 'woocommerce_product_search' && !! instance?.raw, transform: ( { instance } ) => - createBlock( 'woocommerce/product-search', { + createBlock( SEARCH_VARIATION_NAME, { label: instance.raw.title || PRODUCT_SEARCH_ATTRIBUTES.label, @@ -172,9 +177,31 @@ registerBlockType( 'woocommerce/product-search', { }, } ); +function registerProductSearchNamespace( props: BlockType, blockName: string ) { + if ( blockName === 'core/search' ) { + // Gracefully handle if settings.attributes is undefined. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore -- We need this because `attributes` is marked as `readonly` + props.attributes = { + ...props.attributes, + namespace: { + type: 'string', + }, + }; + } + + return props; +} + +addFilter( + 'blocks.registerBlockType', + SEARCH_VARIATION_NAME, + registerProductSearchNamespace +); + if ( isBlockVariationAvailable ) { registerBlockVariation( 'core/search', { - name: 'woocommerce/product-search', + name: SEARCH_VARIATION_NAME, title: __( 'Product Search', 'woocommerce' ), icon: { src: ( @@ -199,4 +226,9 @@ if ( isBlockVariationAvailable ) { ), attributes: PRODUCT_SEARCH_ATTRIBUTES, } ); + addFilter( + 'editor.BlockEdit', + SEARCH_BLOCK_NAME, + withProductSearchControls + ); } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/inspector-controls.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-search/inspector-controls.tsx new file mode 100644 index 00000000000..3212dd9e76e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/inspector-controls.tsx @@ -0,0 +1,156 @@ +/** + * External dependencies + */ +import { type ElementType, useEffect, useState } from '@wordpress/element'; +import { EditorBlock } from '@woocommerce/types'; +import { __ } from '@wordpress/i18n'; +import { InspectorControls } from '@wordpress/block-editor'; +import { + PanelBody, + RadioControl, + ToggleControl, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - Ignoring because `__experimentalToggleGroupControl` is not yet in the type definitions. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalToggleGroupControl as ToggleGroupControl, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - Ignoring because `__experimentalToggleGroupControlOption` is not yet in the type definitions. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalToggleGroupControlOption as ToggleGroupControlOption, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { + getInputAndButtonOption, + getSelectedRadioControlOption, + isInputAndButtonOption, + isWooSearchBlockVariation, +} from './utils'; +import { ButtonPositionProps, ProductSearchBlockProps } from './types'; +import { PositionOptions } from './constants'; + +const ProductSearchControls = ( props: ProductSearchBlockProps ) => { + const { attributes, setAttributes } = props; + const { buttonPosition, buttonUseIcon, showLabel } = attributes; + const [ initialPosition, setInitialPosition ] = + useState< ButtonPositionProps >( buttonPosition ); + + useEffect( () => { + if ( + isInputAndButtonOption( buttonPosition ) && + initialPosition !== buttonPosition + ) { + setInitialPosition( buttonPosition ); + } + }, [ buttonPosition ] ); + + return ( + + + & + PositionOptions.INPUT_AND_BUTTON + ) => { + if ( selected !== PositionOptions.INPUT_AND_BUTTON ) { + setAttributes( { + buttonPosition: selected, + } ); + } else { + const newButtonPosition = + getInputAndButtonOption( initialPosition ); + setAttributes( { + buttonPosition: newButtonPosition, + } ); + } + } } + /> + { buttonPosition !== PositionOptions.NO_BUTTON && ( + <> + { buttonPosition !== PositionOptions.BUTTON_ONLY && ( + { + setAttributes( { + buttonPosition: value, + } ); + } } + value={ getInputAndButtonOption( + buttonPosition + ) } + > + + + + ) } + { + setAttributes( { + buttonUseIcon: value, + } ); + } } + value={ buttonUseIcon } + > + + + + + ) } + + setAttributes( { + showLabel: showInputLabel, + } ) + } + /> + + + ); +}; + +export const withProductSearchControls = + < T extends EditorBlock< T > >( BlockEdit: ElementType ) => + ( props: ProductSearchBlockProps ) => { + return isWooSearchBlockVariation( props ) ? ( + <> + + + + ) : ( + + ); + }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-search/types.ts new file mode 100644 index 00000000000..290523ae727 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/types.ts @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import type { EditorBlock } from '@woocommerce/types'; + +export type ButtonPositionProps = + | 'button-outside' + | 'button-inside' + | 'no-button' + | 'button-only'; + +export interface SearchBlockAttributes { + buttonPosition: ButtonPositionProps; + buttonText?: string; + buttonUseIcon: boolean; + isSearchFieldHidden: boolean; + label?: string; + namespace?: string; + placeholder?: string; + showLabel: boolean; +} + +export type ProductSearchBlockProps = EditorBlock< SearchBlockAttributes >; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-search/utils.ts new file mode 100644 index 00000000000..5b70f82a27a --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/utils.ts @@ -0,0 +1,75 @@ +/** + * Internal dependencies + */ +import { + PositionOptions, + SEARCH_BLOCK_NAME, + SEARCH_VARIATION_NAME, +} from './constants'; +import { ButtonPositionProps, ProductSearchBlockProps } from './types'; + +/** + * Identifies if a block is a Search block variation from our conventions + * + * We are extending Gutenberg's core Search block with our variations, and + * also adding extra namespaced attributes. If those namespaced attributes + * are present, we can be fairly sure it is our own registered variation. + * + * @param {ProductSearchBlockProps} block - A WooCommerce block. + */ +export function isWooSearchBlockVariation( block: ProductSearchBlockProps ) { + return ( + block.name === SEARCH_BLOCK_NAME && + block.attributes?.namespace === SEARCH_VARIATION_NAME + ); +} + +/** + * Checks if the given button position is a valid option for input and button placement. + * + * The function verifies if the provided `buttonPosition` matches one of the predefined + * values for placing a button either inside or outside an input field. + * + * @param {string} buttonPosition - The position of the button to check. + */ +export function isInputAndButtonOption( buttonPosition: string ): boolean { + return ( + buttonPosition === 'button-outside' || + buttonPosition === 'button-inside' + ); +} + +/** + * Returns the option for the selected button position + * + * Based on the provided `buttonPosition`, the function returns a predefined option + * if the position is valid for input and button placement. If the position is not + * one of the predefined options, it returns the original `buttonPosition`. + * + * @param {string} buttonPosition - The position of the button to evaluate. + */ +export function getSelectedRadioControlOption( + buttonPosition: string +): string { + if ( isInputAndButtonOption( buttonPosition ) ) { + return PositionOptions.INPUT_AND_BUTTON; + } + return buttonPosition; +} + +/** + * Returns the appropriate option for input and button placement based on the given value + * + * This function checks if the provided `value` is a valid option for placing a button either + * inside or outside an input field. If the `value` is valid, it is returned as is. If the `value` + * is not valid, the function returns a default option. + * + * @param {ButtonPositionProps} value - The position of the button to evaluate. + */ +export function getInputAndButtonOption( value: ButtonPositionProps ) { + if ( isInputAndButtonOption( value ) ) { + return value; + } + // The default value is 'inside' for input and button. + return PositionOptions.OUTSIDE; +} diff --git a/plugins/woocommerce-blocks/assets/js/data/checkout/action-types.ts b/plugins/woocommerce-blocks/assets/js/data/checkout/action-types.ts index 327cd10bca1..3b1b82a5254 100644 --- a/plugins/woocommerce-blocks/assets/js/data/checkout/action-types.ts +++ b/plugins/woocommerce-blocks/assets/js/data/checkout/action-types.ts @@ -17,4 +17,6 @@ export const ACTION_TYPES = { SET_REDIRECT_URL: 'SET_REDIRECT_URL', SET_SHOULD_CREATE_ACCOUNT: 'SET_SHOULD_CREATE_ACCOUNT', SET_USE_SHIPPING_AS_BILLING: 'SET_USE_SHIPPING_AS_BILLING', + SET_EDITING_BILLING_ADDRESS: 'SET_EDITING_BILLING_ADDRESS', + SET_EDITING_SHIPPING_ADDRESS: 'SET_EDITING_SHIPPING_ADDRESS', } as const; diff --git a/plugins/woocommerce-blocks/assets/js/data/checkout/actions.ts b/plugins/woocommerce-blocks/assets/js/data/checkout/actions.ts index 8cc1f724274..eee54051f65 100644 --- a/plugins/woocommerce-blocks/assets/js/data/checkout/actions.ts +++ b/plugins/woocommerce-blocks/assets/js/data/checkout/actions.ts @@ -118,6 +118,30 @@ export const __internalSetUseShippingAsBilling = ( useShippingAsBilling, } ); +/** + * Set whether the billing address is being edited + * + * @param isEditing True if the billing address is being edited, false otherwise + */ +export const setEditingBillingAddress = ( isEditing: boolean ) => { + return { + type: types.SET_EDITING_BILLING_ADDRESS, + isEditing, + }; +}; + +/** + * Set whether the shipping address is being edited + * + * @param isEditing True if the shipping address is being edited, false otherwise + */ +export const setEditingShippingAddress = ( isEditing: boolean ) => { + return { + type: types.SET_EDITING_SHIPPING_ADDRESS, + isEditing, + }; +}; + /** * Whether an account should be created for the user while checking out * @@ -182,6 +206,8 @@ export type CheckoutAction = | typeof __internalSetCustomerId | typeof __internalSetCustomerPassword | typeof __internalSetUseShippingAsBilling + | typeof setEditingBillingAddress + | typeof setEditingShippingAddress | typeof __internalSetShouldCreateAccount | typeof __internalSetOrderNotes | typeof setPrefersCollection diff --git a/plugins/woocommerce-blocks/assets/js/data/checkout/default-state.ts b/plugins/woocommerce-blocks/assets/js/data/checkout/default-state.ts index 7891c255565..1a82d4d5056 100644 --- a/plugins/woocommerce-blocks/assets/js/data/checkout/default-state.ts +++ b/plugins/woocommerce-blocks/assets/js/data/checkout/default-state.ts @@ -23,8 +23,28 @@ export type CheckoutState = { shouldCreateAccount: boolean; // Should a user account be created? status: STATUS; // Status of the checkout useShippingAsBilling: boolean; // Should the billing form be hidden and inherit the shipping address? + editingBillingAddress: boolean; // Is the billing address being edited? + editingShippingAddress: boolean; // Is the shipping address being edited? }; +// Default editing state for CustomerAddress component comes from the current address and whether or not we're in the editor. +const hasBillingAddress = !! ( + checkoutData.billing_address.address_1 && + ( checkoutData.billing_address.first_name || + checkoutData.billing_address.last_name ) +); + +const hasShippingAddress = !! ( + checkoutData.shipping_address.address_1 && + ( checkoutData.shipping_address.first_name || + checkoutData.shipping_address.last_name ) +); + +const billingMatchesShipping = isSameAddress( + checkoutData.billing_address, + checkoutData.shipping_address +); + export const defaultState: CheckoutState = { additionalFields: checkoutData.additional_fields || {}, calculatingCount: 0, @@ -38,8 +58,7 @@ export const defaultState: CheckoutState = { redirectUrl: '', shouldCreateAccount: false, status: STATUS.IDLE, - useShippingAsBilling: isSameAddress( - checkoutData.billing_address, - checkoutData.shipping_address - ), + useShippingAsBilling: billingMatchesShipping, + editingBillingAddress: ! hasBillingAddress, + editingShippingAddress: ! hasShippingAddress, }; diff --git a/plugins/woocommerce-blocks/assets/js/data/checkout/reducers.ts b/plugins/woocommerce-blocks/assets/js/data/checkout/reducers.ts index 1a01c5772df..01bf428d245 100644 --- a/plugins/woocommerce-blocks/assets/js/data/checkout/reducers.ts +++ b/plugins/woocommerce-blocks/assets/js/data/checkout/reducers.ts @@ -130,6 +130,20 @@ const reducer = ( state = defaultState, action: CheckoutAction ) => { } break; + case types.SET_EDITING_BILLING_ADDRESS: + newState = { + ...state, + editingBillingAddress: action.isEditing, + }; + break; + + case types.SET_EDITING_SHIPPING_ADDRESS: + newState = { + ...state, + editingShippingAddress: action.isEditing, + }; + break; + case types.SET_SHOULD_CREATE_ACCOUNT: if ( action.shouldCreateAccount !== undefined && diff --git a/plugins/woocommerce-blocks/assets/js/data/checkout/selectors.ts b/plugins/woocommerce-blocks/assets/js/data/checkout/selectors.ts index c472bc25bc8..759049db2ba 100644 --- a/plugins/woocommerce-blocks/assets/js/data/checkout/selectors.ts +++ b/plugins/woocommerce-blocks/assets/js/data/checkout/selectors.ts @@ -36,6 +36,14 @@ export const getUseShippingAsBilling = ( state: CheckoutState ) => { return state.useShippingAsBilling; }; +export const getEditingBillingAddress = ( state: CheckoutState ) => { + return state.editingBillingAddress; +}; + +export const getEditingShippingAddress = ( state: CheckoutState ) => { + return state.editingShippingAddress; +}; + export const getExtensionData = ( state: CheckoutState ) => { return state.extensionData; }; diff --git a/plugins/woocommerce-blocks/assets/js/editor-components/product-rating/index.tsx b/plugins/woocommerce-blocks/assets/js/editor-components/product-rating/index.tsx new file mode 100644 index 00000000000..f98bd4be1f5 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/editor-components/product-rating/index.tsx @@ -0,0 +1,184 @@ +/** + * External dependencies + */ +import { __, _n, sprintf } from '@wordpress/i18n'; +import clsx from 'clsx'; +import type { CSSProperties } from 'react'; +import { isNumber, ProductResponseItem } from '@woocommerce/types'; + +type RatingProps = { + className: string; + reviews: number; + rating: number; + parentClassName?: string; +}; + +export const getAverageRating = ( + product: Omit< ProductResponseItem, 'average_rating' > & { + average_rating: string; + } +) => { + const rating = parseFloat( product.average_rating ); + + return Number.isFinite( rating ) && rating > 0 ? rating : 0; +}; + +export const getRatingCount = ( product: ProductResponseItem ) => { + const count = isNumber( product.review_count ) + ? product.review_count + : parseInt( product.review_count, 10 ); + + return Number.isFinite( count ) && count > 0 ? count : 0; +}; + +const getStarStyle = ( rating: number ) => ( { + width: ( rating / 5 ) * 100 + '%', +} ); + +const NoRating = ( { + className, + parentClassName, +}: { + className: string; + parentClassName: string; +} ) => { + const starStyle = getStarStyle( 0 ); + + return ( +
    +
    + +
    + { __( 'No Reviews', 'woocommerce' ) } +
    + ); +}; + +const Rating = ( props: RatingProps ): JSX.Element => { + const { className, rating, reviews, parentClassName } = props; + + const starStyle = getStarStyle( rating ); + + const ratingText = sprintf( + /* translators: %f is referring to the average rating value */ + __( 'Rated %f out of 5', 'woocommerce' ), + rating + ); + + const ratingHTML = { + __html: sprintf( + /* translators: %1$s is referring to the average rating value, %2$s is referring to the number of ratings */ + _n( + 'Rated %1$s out of 5 based on %2$s customer rating', + 'Rated %1$s out of 5 based on %2$s customer ratings', + reviews, + 'woocommerce' + ), + sprintf( '%f', rating ), + sprintf( '%d', reviews ) + ), + }; + return ( +
    + +
    + ); +}; + +const ReviewsCount = ( props: { + className: string; + reviews: number; +} ): JSX.Element => { + const { className, reviews } = props; + + const reviewsCount = sprintf( + /* translators: %s is referring to the total of reviews for a product */ + _n( + '(%s customer review)', + '(%s customer reviews)', + reviews, + 'woocommerce' + ), + reviews + ); + + return ( + + { reviewsCount } + + ); +}; + +type ProductRatingProps = { + className: string; + showReviewCount?: boolean; + showMockedReviews?: boolean; + parentClassName?: string; + rating: number; + reviews: number; + styleProps: { + className: string; + style: CSSProperties; + }; + textAlign?: string; +}; + +export const ProductRating = ( + props: ProductRatingProps +): JSX.Element | null => { + const { + className = 'wc-block-components-product-rating', + showReviewCount, + showMockedReviews, + parentClassName = '', + rating, + reviews, + styleProps, + textAlign, + } = props; + + const wrapperClassName = clsx( styleProps.className, className, { + [ `${ parentClassName }__product-rating` ]: parentClassName, + [ `has-text-align-${ textAlign }` ]: textAlign, + } ); + + const mockedRatings = showMockedReviews && ( + + ); + + const content = reviews ? ( + + ) : ( + mockedRatings + ); + + const isReviewCountVisible = reviews && showReviewCount; + + return ( +
    +
    + { content } + { isReviewCountVisible ? ( + + ) : null } +
    +
    + ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/icons/index.js b/plugins/woocommerce-blocks/assets/js/icons/index.js index 446bbadcebb..6cfa7e5f667 100644 --- a/plugins/woocommerce-blocks/assets/js/icons/index.js +++ b/plugins/woocommerce-blocks/assets/js/icons/index.js @@ -4,6 +4,7 @@ export { default as bagAlt } from './library/bag-alt'; export { default as barcode } from './library/barcode'; export { default as cart } from './library/cart'; export { default as cartOutline } from './library/cart-outline'; +export { default as checkMark } from './library/check-mark'; export { default as checkPayment } from './library/check-payment'; export { default as closeSquareShadow } from './library/close-square-shadow'; export { default as customerAccount } from './library/customer-account'; diff --git a/plugins/woocommerce-blocks/assets/js/icons/library/check-mark.tsx b/plugins/woocommerce-blocks/assets/js/icons/library/check-mark.tsx new file mode 100644 index 00000000000..cd7e6b1bffa --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/icons/library/check-mark.tsx @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { IconProps } from '@wordpress/icons/build-types/icon'; +import { Path, SVG } from '@wordpress/primitives'; + +const CheckMark = ( props: IconProps ) => ( + + + +); + +export default CheckMark; diff --git a/plugins/woocommerce-blocks/assets/js/middleware/index.js b/plugins/woocommerce-blocks/assets/js/middleware/index.js index f5d970a90d3..0d071b0b069 100644 --- a/plugins/woocommerce-blocks/assets/js/middleware/index.js +++ b/plugins/woocommerce-blocks/assets/js/middleware/index.js @@ -2,3 +2,4 @@ * Internal dependencies */ import './store-api-nonce'; +import './remove-user-locale'; diff --git a/plugins/woocommerce-blocks/assets/js/middleware/remove-user-locale.ts b/plugins/woocommerce-blocks/assets/js/middleware/remove-user-locale.ts new file mode 100644 index 00000000000..d53a7cb6666 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/middleware/remove-user-locale.ts @@ -0,0 +1,38 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import { isStoreApiRequest } from './store-api-nonce'; + +/** + * Middleware to add the '_locale=site' query parameter from API requests. + * + * TODO: Remove once https://github.com/WordPress/gutenberg/issues/16805 is fixed and replace by removing userLocaleMiddleware middleware. + * + * @param {Object} options Fetch options. + * @param {Object} options.url The URL of the request. + * @param {Object} options.path The path of the request. + * @param {Function} next The next middleware or fetchHandler to call. + * @return {*} The evaluated result of the remaining middleware chain. + */ +const removeUserLocaleMiddleware = ( + options: { url?: string; path?: string }, + next: ( options: { url?: string; path?: string } ) => Promise< unknown > +): Promise< unknown > => { + if ( typeof options.url === 'string' && isStoreApiRequest( options ) ) { + options.url = addQueryArgs( options.url, { _locale: 'site' } ); + } + + if ( typeof options.path === 'string' && isStoreApiRequest( options ) ) { + options.path = addQueryArgs( options.path, { _locale: 'site' } ); + } + + return next( options ); +}; + +apiFetch.use( removeUserLocaleMiddleware ); diff --git a/plugins/woocommerce-blocks/assets/js/middleware/store-api-nonce.js b/plugins/woocommerce-blocks/assets/js/middleware/store-api-nonce.js index a5b121d8693..dc813d71653 100644 --- a/plugins/woocommerce-blocks/assets/js/middleware/store-api-nonce.js +++ b/plugins/woocommerce-blocks/assets/js/middleware/store-api-nonce.js @@ -23,7 +23,7 @@ try { * * @return {boolean} Returns true if this is a store request. */ -const isStoreApiRequest = ( options ) => { +export const isStoreApiRequest = ( options ) => { const url = options.url || options.path; if ( ! url || ! options.method || options.method === 'GET' ) { return false; diff --git a/plugins/woocommerce-blocks/bin/webpack-entries.js b/plugins/woocommerce-blocks/bin/webpack-entries.js index 11a84832ef5..aeeb511c7eb 100644 --- a/plugins/woocommerce-blocks/bin/webpack-entries.js +++ b/plugins/woocommerce-blocks/bin/webpack-entries.js @@ -88,10 +88,6 @@ const blocks = { 'product-filters': { isExperimental: true, }, - 'product-filter': { - isExperimental: true, - customDir: 'product-filters/inner-blocks/product-filter', - }, 'product-filters-overlay': { isExperimental: true, customDir: 'product-filters/inner-blocks/overlay', @@ -124,6 +120,14 @@ const blocks = { customDir: 'product-filters/inner-blocks/clear-button', isExperimental: true, }, + 'product-filter-checkbox-list': { + customDir: 'product-filters/inner-blocks/checkbox-list', + isExperimental: true, + }, + 'product-filter-chips': { + customDir: 'product-filters/inner-blocks/chips', + isExperimental: true, + }, 'order-confirmation-summary': { customDir: 'order-confirmation/summary', }, diff --git a/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/data-store/checkout.md b/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/data-store/checkout.md index 5badd9d879e..b4a99cc4ecb 100644 --- a/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/data-store/checkout.md +++ b/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/data-store/checkout.md @@ -1,5 +1,7 @@ # Checkout Store (`wc/store/checkout`) + + > 💡 What's the difference between the Cart Store and the Checkout Store? > > The **Cart Store (`wc/store/cart`)** manages and retrieves data about the shopping cart, including items, customer data, and interactions like coupons. @@ -173,6 +175,36 @@ const store = select( CHECKOUT_STORE_KEY ); const useShippingAsBilling = store.getUseShippingAsBilling(); ``` +### getEditingBillingAddress + +Returns true if the billing address is being edited. + +#### _Returns_ + +- `boolean`: True if the billing address is being edited. + +#### _Example_ + +```js +const store = select( CHECKOUT_STORE_KEY ); +const editingBillingAddress = store.getEditingBillingAddress(); +``` + +### getEditingShippingAddress + +Returns true if the shipping address is being edited. + +#### _Returns_ + +- `boolean`: True if the shipping address is being edited. + +#### _Example_ + +```js +const store = select( CHECKOUT_STORE_KEY ); +const editingShippingAddress = store.getEditingShippingAddress(); +``` + ### hasError Returns true if an error occurred, and false otherwise. @@ -293,7 +325,6 @@ const store = select( CHECKOUT_STORE_KEY ); const isCalculating = store.isCalculating(); ``` - ### prefersCollection Returns true if the customer prefers to collect their order, and false otherwise. @@ -326,6 +357,36 @@ const store = dispatch( CHECKOUT_STORE_KEY ); store.setPrefersCollection( true ); ``` +### setEditingBillingAddress + +Set the billing address to editing state or collapsed state. Note that if the address has invalid fields, it will not be set to collapsed state. + +#### _Parameters_ + +- _isEditing_ `boolean`: True to set the billing address to editing state, false to set it to collapsed state. + +#### _Example_ + +```js +const store = dispatch( CHECKOUT_STORE_KEY ); +store.setEditingBillingAddress( true ); +``` + +### setEditingShippingAddress + +Set the shipping address to editing state or collapsed state. Note that if the address has invalid fields, it will not be set to collapsed state. + +#### _Parameters_ + +- _isEditing_ `boolean`: True to set the shipping address to editing state, false to set it to collapsed state. + +#### _Example_ + +```js +const store = dispatch( CHECKOUT_STORE_KEY ); +store.setEditingShippingAddress( true ); +``` + --- diff --git a/plugins/woocommerce-blocks/package.json b/plugins/woocommerce-blocks/package.json index cd63f8a5ca4..f6e4ce66b83 100644 --- a/plugins/woocommerce-blocks/package.json +++ b/plugins/woocommerce-blocks/package.json @@ -256,7 +256,7 @@ "pnpm": "9.1.3" }, "dependencies": { - "@ariakit/react": "^0.4.4", + "@ariakit/react": "^0.4.5", "@dnd-kit/core": "^6.1.0", "@dnd-kit/modifiers": "^6.0.1", "@dnd-kit/sortable": "^7.0.2", @@ -288,7 +288,7 @@ "fast-deep-equal": "^3.1.3", "fast-sort": "^3.4.0", "html-react-parser": "3.0.4", - "postcode-validator": "3.8.15", + "postcode-validator": "3.9.2", "preact": "^10.19.3", "prop-types": "^15.8.1", "react-number-format": "4.9.3", diff --git a/plugins/woocommerce-blocks/packages/checkout/utils/validation/is-postcode.ts b/plugins/woocommerce-blocks/packages/checkout/utils/validation/is-postcode.ts index ec2ca97a1c3..2ab21d238b9 100644 --- a/plugins/woocommerce-blocks/packages/checkout/utils/validation/is-postcode.ts +++ b/plugins/woocommerce-blocks/packages/checkout/utils/validation/is-postcode.ts @@ -13,6 +13,7 @@ const CUSTOM_REGEXES = new Map< string, RegExp >( [ [ 'JP', /^([0-9]{3})([-]?)([0-9]{4})$/ ], [ 'KH', /^[0-9]{6}$/ ], // Cambodia (6-digit postal code). [ 'LI', /^(94[8-9][0-9])$/ ], + [ 'MN', /^[0-9]{5}(-[0-9]{4})?$/ ], // Mongolia (5-digit postal code or 5-digit postal code followed by a hyphen and 4-digit postal code). [ 'NI', /^[1-9]{1}[0-9]{4}$/ ], // Nicaragua (5-digit postal code) [ 'NL', /^([1-9][0-9]{3})(\s?)(?!SA|SD|SS)[A-Z]{2}$/i ], [ 'SI', /^([1-9][0-9]{3})$/ ], diff --git a/plugins/woocommerce-blocks/packages/components/panel/index.tsx b/plugins/woocommerce-blocks/packages/components/panel/index.tsx index 35b62a99ea9..af21136a68e 100644 --- a/plugins/woocommerce-blocks/packages/components/panel/index.tsx +++ b/plugins/woocommerce-blocks/packages/components/panel/index.tsx @@ -5,7 +5,8 @@ import { useState } from '@wordpress/element'; import clsx from 'clsx'; import { Icon, chevronUp, chevronDown } from '@wordpress/icons'; import type { ReactNode, ReactElement } from 'react'; - +import { Button } from '@ariakit/react'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies */ @@ -27,7 +28,11 @@ const Panel = ( { initialOpen = false, hasBorder = false, title, - titleTag: TitleTag = 'div', + /** + * @deprecated The `titleTag` prop is deprecated and will be removed in a future version. + * Use the `title` prop to pass a custom React element instead. + */ + titleTag, state, }: PanelProps ): ReactElement => { let [ isOpen, setIsOpen ] = useState< boolean >( initialOpen ); @@ -36,26 +41,31 @@ const Panel = ( { [ isOpen, setIsOpen ] = state; } + if ( titleTag ) { + deprecated( "Panel component's titleTag prop", { + since: '9.4.0', + } ); + } + return (
    - - - + { isOpen && (
    { children } diff --git a/plugins/woocommerce-blocks/packages/components/panel/style.scss b/plugins/woocommerce-blocks/packages/components/panel/style.scss index ac4f5722bde..d4651f9b918 100644 --- a/plugins/woocommerce-blocks/packages/components/panel/style.scss +++ b/plugins/woocommerce-blocks/packages/components/panel/style.scss @@ -14,31 +14,27 @@ } .wc-block-components-panel__button { - @include reset-box(); + box-sizing: border-box; height: auto; - line-height: 1; margin-top: em(6px); padding-right: #{24px + $gap-smaller}; padding-top: em($gap-small - 6px); + padding-left: 0 !important; position: relative; text-align: left; width: 100%; word-break: break-word; &[aria-expanded="true"] { - padding-bottom: $gap-smaller; - margin-bottom: $gap-smaller; + margin-bottom: $gap-smaller * 2; } &, &:hover, &:focus, &:active { - @include reset-color(); - @include reset-typography(); - background: transparent; - box-shadow: none; cursor: pointer; + padding-left: 0 !important; } > .wc-block-components-panel__button-icon { @@ -58,21 +54,3 @@ display: none; } } - -// Extra classes for specificity. -.theme-twentytwentyone.theme-twentytwentyone.theme-twentytwentyone -.wc-block-components-panel__button { - background-color: inherit; - color: inherit; -} - -.theme-twentytwenty .wc-block-components-panel__button, -.theme-twentyseventeen .wc-block-components-panel__button { - background: none transparent; - color: inherit; - - &.wc-block-components-panel__button:hover, - &.wc-block-components-panel__button:focus { - background: none transparent; - } -} diff --git a/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars b/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars index f99258d7b35..1434a83d574 100644 --- a/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars +++ b/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars @@ -1,11 +1,12 @@ - - -

    Active filters

    - + +
    {{#> wp-block blockName='woocommerce/product-filter-active' attributes=attributes }} {{/ wp-block }} +
    + +
    diff --git a/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_attribute-filter.handlebars b/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_attribute-filter.handlebars index 1ae53f9c3d8..805634b68a7 100644 --- a/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_attribute-filter.handlebars +++ b/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_attribute-filter.handlebars @@ -1,18 +1,26 @@ - - -

    Filter by Attribute

    - + +
    +{{#> wp-block blockName='woocommerce/product-filter-attribute' attributes=attributes }} +
    +
    +

    Attribute

    + + + -
    + - - +
    + -{{#> wp-block blockName='woocommerce/product-filter-attribute' attributes=attributes }} +
    {{/ wp-block }} +
    + +
    diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/active-filters.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/active-filters.block_theme.spec.ts deleted file mode 100644 index b4c78fd0ab1..00000000000 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/active-filters.block_theme.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * External dependencies - */ -import { TemplateCompiler, test as base, expect } from '@woocommerce/e2e-utils'; - -const test = base.extend< { templateCompiler: TemplateCompiler } >( { - templateCompiler: async ( { requestUtils }, use ) => { - const compiler = await requestUtils.createTemplateFromFile( - 'archive-product_active-filters' - ); - await use( compiler ); - }, -} ); - -test.describe( 'Product Filter: Active Filters Block', () => { - test.describe( 'frontend', () => { - test.beforeEach( async ( { requestUtils } ) => { - await requestUtils.activatePlugin( - 'woocommerce-blocks-test-enable-experimental-features' - ); - } ); - - test( 'Without any filters selected, only a wrapper block is rendered', async ( { - page, - templateCompiler, - } ) => { - await templateCompiler.compile(); - - await page.goto( '/shop' ); - - const locator = page.locator( - '.wp-block-woocommerce-product-filter' - ); - - await expect( locator ).toHaveCount( 1 ); - - const html = await locator.innerHTML(); - expect( html.trim() ).toBe( '' ); - } ); - - test( 'With rating filters applied it shows the correct active filters', async ( { - page, - templateCompiler, - } ) => { - await templateCompiler.compile(); - - await page.goto( `${ '/shop' }?rating_filter=1,2,5` ); - - await expect( page.getByText( 'Rating:' ) ).toBeVisible(); - await expect( page.getByText( 'Rated 1 out of 5' ) ).toBeVisible(); - await expect( page.getByText( 'Rated 2 out of 5' ) ).toBeVisible(); - await expect( page.getByText( 'Rated 5 out of 5' ) ).toBeVisible(); - } ); - - test( 'With stock filters applied it shows the correct active filters', async ( { - page, - templateCompiler, - } ) => { - await templateCompiler.compile(); - - await page.goto( - `${ '/shop' }?filter_stock_status=instock,onbackorder` - ); - - await expect( page.getByText( 'Stock Status:' ) ).toBeVisible(); - await expect( page.getByText( 'In stock' ) ).toBeVisible(); - await expect( page.getByText( 'On backorder' ) ).toBeVisible(); - } ); - - test( 'With attribute filters applied it shows the correct active filters', async ( { - page, - templateCompiler, - } ) => { - await templateCompiler.compile(); - - await page.goto( - `${ '/shop' }?filter_color=blue,gray&query_type_color=or` - ); - - await expect( page.getByText( 'Color:' ) ).toBeVisible(); - await expect( page.getByText( 'Blue' ) ).toBeVisible(); - await expect( page.getByText( 'Gray' ) ).toBeVisible(); - } ); - - test( 'With price filters applied it shows the correct active filters', async ( { - page, - templateCompiler, - } ) => { - await templateCompiler.compile(); - - await page.goto( `${ '/shop' }?min_price=17&max_price=71` ); - - await expect( page.getByText( 'Price:' ) ).toBeVisible(); - await expect( - page.getByText( 'Between $17 and $71' ) - ).toBeVisible(); - } ); - } ); -} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/attribute-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/attribute-filter.block_theme.spec.ts deleted file mode 100644 index d39c14ff018..00000000000 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/attribute-filter.block_theme.spec.ts +++ /dev/null @@ -1,316 +0,0 @@ -/** - * External dependencies - */ -import { TemplateCompiler, test as base, expect } from '@woocommerce/e2e-utils'; - -const COLOR_ATTRIBUTE_VALUES = [ 'Blue', 'Gray', 'Green', 'Red', 'Yellow' ]; -const COLOR_ATTRIBUTES_WITH_COUNTS = [ - 'Blue (4)', - 'Gray (2)', - 'Green (3)', - 'Red (4)', - 'Yellow (1)', -]; - -const test = base.extend< { templateCompiler: TemplateCompiler } >( { - templateCompiler: async ( { requestUtils }, use ) => { - const compiler = await requestUtils.createTemplateFromFile( - 'archive-product_attribute-filter' - ); - await use( compiler ); - }, -} ); - -test.describe( 'Product Filter: Attribute Block', () => { - test.describe( 'With default display style', () => { - test.beforeEach( async ( { requestUtils, templateCompiler } ) => { - await requestUtils.activatePlugin( - 'woocommerce-blocks-test-enable-experimental-features' - ); - await templateCompiler.compile( { - attributes: { - attributeId: 1, - }, - } ); - } ); - - test( 'clear button is not shown on initial page load', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await expect( button ).toBeHidden(); - } ); - - test( 'renders a checkbox list with the available attribute filters', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const attributes = page.locator( - '.wc-block-interactivity-components-checkbox-list__label' - ); - - await expect( attributes ).toHaveCount( 5 ); - - for ( let i = 0; i < COLOR_ATTRIBUTE_VALUES.length; i++ ) { - await expect( attributes.nth( i ) ).toHaveText( - COLOR_ATTRIBUTE_VALUES[ i ] - ); - } - } ); - - test( 'filters the list of products by selecting an attribute', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const grayCheckbox = page.getByText( 'Gray' ); - await grayCheckbox.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=gray.*/ ); - - const products = page.locator( '.wc-block-product' ); - - await expect( products ).toHaveCount( 2 ); - } ); - - test( 'clear button appears after a filter is applied', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const grayCheckbox = page.getByText( 'Gray' ); - await grayCheckbox.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=gray.*/ ); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await expect( button ).toBeVisible(); - } ); - - test( 'clear button hides after deselecting all filters', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const grayCheckbox = page.getByText( 'Gray' ); - await grayCheckbox.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=gray.*/ ); - - await grayCheckbox.click(); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await expect( button ).toBeHidden(); - } ); - - test( 'filters are cleared after clear button is clicked', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const grayCheckbox = page.getByText( 'Gray' ); - await grayCheckbox.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=gray.*/ ); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await button.click(); - - COLOR_ATTRIBUTE_VALUES.map( async ( color ) => { - const element = page.locator( - `input[value="${ color.toLowerCase() }"]` - ); - - await expect( element ).not.toBeChecked(); - } ); - } ); - } ); - - test.describe( 'With show counts enabled', () => { - test.beforeEach( async ( { requestUtils, templateCompiler } ) => { - await requestUtils.activatePlugin( - 'woocommerce-blocks-test-enable-experimental-features' - ); - await templateCompiler.compile( { - attributes: { - attributeId: 1, - showCounts: true, - }, - } ); - } ); - - test( 'Renders checkboxes with associated product counts', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const attributes = page.locator( - '.wc-block-interactivity-components-checkbox-list__label' - ); - - await expect( attributes ).toHaveCount( 5 ); - - for ( let i = 0; i < COLOR_ATTRIBUTES_WITH_COUNTS.length; i++ ) { - await expect( attributes.nth( i ) ).toHaveText( - COLOR_ATTRIBUTES_WITH_COUNTS[ i ] - ); - } - } ); - } ); - - test.describe( "With display style 'dropdown'", () => { - test.beforeEach( async ( { requestUtils, templateCompiler } ) => { - await requestUtils.activatePlugin( - 'woocommerce-blocks-test-enable-experimental-features' - ); - await templateCompiler.compile( { - attributes: { - attributeId: 1, - displayStyle: 'dropdown', - }, - } ); - } ); - - test( 'clear button is not shown on initial page load', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await expect( button ).toBeHidden(); - } ); - - test( 'renders a dropdown list with the available attribute filters', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const dropdownLocator = page.locator( - '.wc-interactivity-dropdown' - ); - - await expect( dropdownLocator ).toBeVisible(); - await dropdownLocator.click(); - - for ( let i = 0; i < COLOR_ATTRIBUTE_VALUES.length; i++ ) { - await expect( - dropdownLocator.getByText( COLOR_ATTRIBUTE_VALUES[ i ] ) - ).toBeVisible(); - } - } ); - - test( 'Clicking a dropdown option should filter the displayed products', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const dropdownLocator = page.locator( - '.wc-interactivity-dropdown' - ); - - await expect( dropdownLocator ).toBeVisible(); - await dropdownLocator.click(); - - const yellowOption = page.getByText( 'Yellow' ); - await yellowOption.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=yellow.*/ ); - - const products = page.locator( '.wc-block-product' ); - - await expect( products ).toHaveCount( 1 ); - } ); - - test( 'clear button appears after a filter is applied', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const dropdownLocator = page.locator( - '.wc-interactivity-dropdown' - ); - - await expect( dropdownLocator ).toBeVisible(); - await dropdownLocator.click(); - - const yellowOption = page.getByText( 'Yellow' ); - await yellowOption.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=yellow.*/ ); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await expect( button ).toBeVisible(); - } ); - - test( 'clear button hides after deselecting all filters', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const dropdownLocator = page.locator( - '.wc-interactivity-dropdown' - ); - - await dropdownLocator.click(); - - const yellowOption = page.getByText( 'Yellow' ); - await yellowOption.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=yellow.*/ ); - - await dropdownLocator.click(); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - const removeFilter = page.locator( - '.wc-interactivity-dropdown__badge-remove' - ); - - await removeFilter.click(); - - await expect( button ).toBeHidden(); - } ); - - test( 'filters are cleared after clear button is clicked', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const dropdownLocator = page.locator( - '.wc-interactivity-dropdown' - ); - - await dropdownLocator.click(); - - const yellowOption = page.getByText( 'Yellow' ); - await yellowOption.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=yellow.*/ ); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await button.click(); - - const placeholder = page.getByText( 'Select Color' ); - - await expect( placeholder ).toBeVisible(); - } ); - } ); -} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/basic.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/basic.block_theme.spec.ts deleted file mode 100644 index db9f8dd5b10..00000000000 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/basic.block_theme.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * External dependencies - */ -import { test, expect } from '@woocommerce/e2e-utils'; - -const filterBlocks = [ - { - name: 'woocommerce/product-filter-price', - title: 'Product Filter: Price (Experimental)', - heading: 'Filter by Price', - }, - { - name: 'woocommerce/product-filter-stock-status', - title: 'Product Filter: Stock Status (Experimental)', - heading: 'Filter by Stock Status', - }, - { - name: 'woocommerce/product-filter-rating', - title: 'Product Filter: Rating (Experimental)', - heading: 'Filter by Rating', - }, - { - name: 'woocommerce/product-filter-attribute', - title: 'Product Filter: Attribute (Experimental)', - heading: 'Filter by Attribute', - }, - { - name: 'woocommerce/product-filter-active', - title: 'Product Filter: Active Filters (Experimental)', - heading: 'Active Filters', - }, -]; - -test.describe( 'Filter blocks registration', () => { - test.beforeEach( async ( { admin } ) => { - await admin.createNewPost(); - } ); - - test( 'Variations cannot be inserted through the inserter.', async ( { - page, - editor, - } ) => { - for ( const block of filterBlocks ) { - await editor.openGlobalBlockInserter(); - await page.getByPlaceholder( 'Search' ).fill( block.title ); - const filterBlock = page.getByRole( 'option', { - name: block.title, - exact: true, - } ); - - await expect( filterBlock ).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 1a87ebeb605..3b94f037eeb 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 @@ -207,7 +207,8 @@ class ProductCollectionPage { } async chooseProductInEditorProductPickerIfAvailable( - pageReference: Page | FrameLocator + pageReference: Page | FrameLocator, + productName = 'Album' ) { const editorProductPicker = pageReference.locator( SELECTORS.productPicker @@ -217,7 +218,7 @@ class ProductCollectionPage { await editorProductPicker .locator( 'label' ) .filter( { - hasText: 'Album', + hasText: productName, } ) .click(); } 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-tester.block_theme.spec.ts index a7ea710f8a4..6fd09e4050c 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-tester.block_theme.spec.ts @@ -356,4 +356,84 @@ test.describe( 'Product Collection registration', () => { await expect( previewButtonLocator ).toBeHidden(); } ); } ); + + test( 'Product picker should be shown when selected product is deleted', async ( { + pageObject, + admin, + editor, + requestUtils, + page, + } ) => { + // Add a new test product to the database + let testProductId: number | null = null; + const newProduct = await requestUtils.rest( { + method: 'POST', + path: 'wc/v3/products', + data: { + name: 'A Test Product', + price: 10, + }, + } ); + testProductId = newProduct.id; + + 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, + 'A Test Product' + ); + await expect( editorProductPicker ).toBeHidden(); + + await editor.saveDraft(); + + // Delete the product + await requestUtils.rest( { + method: 'DELETE', + path: `wc/v3/products/${ testProductId }`, + } ); + + // Product picker should be shown in Editor + await admin.page.reload(); + const deletedProductPicker = editor.canvas.getByText( + 'Previously selected product' + ); + await expect( deletedProductPicker ).toBeVisible(); + + // Change status from "trash" to "publish" + await requestUtils.rest( { + method: 'PUT', + path: `wc/v3/products/${ testProductId }`, + data: { + status: 'publish', + }, + } ); + + // Product Picker shouldn't be shown as product is available now + await page.reload(); + await expect( editorProductPicker ).toBeHidden(); + + // Delete the product from database, instead of trashing it + await requestUtils.rest( { + method: 'DELETE', + path: `wc/v3/products/${ testProductId }`, + params: { + // Bypass trash and permanently delete the product + force: true, + }, + } ); + + // Product picker should be shown in Editor + await expect( deletedProductPicker ).toBeVisible(); + } ); } ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts index 4dae7c49c84..97ae2631985 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts @@ -105,10 +105,6 @@ test.describe( 'Filters Overlay Template Part', () => { templatePartData.selectors.editor.blocks.activeFilters .blockLabel ) - .getByLabel( - templatePartData.selectors.editor.blocks.filterOptions - .blockLabel - ) .click(); await editor.openDocumentSettingsSidebar(); @@ -201,10 +197,6 @@ test.describe( 'Filters Overlay Template Part', () => { templatePartData.selectors.editor.blocks.activeFilters .blockLabel ) - .getByLabel( - templatePartData.selectors.editor.blocks.filterOptions - .blockLabel - ) .click(); await editor.openDocumentSettingsSidebar(); @@ -221,12 +213,6 @@ test.describe( 'Filters Overlay Template Part', () => { 'OverlayNeverMobileAlways' ); await layoutSettings.getByLabel( 'Never' ).click(); - await editor.page - .getByRole( 'link', { - name: templatePartData.selectors.editor.blocks - .productFiltersOverlayNavigation.title, - } ) - .click(); await editor.saveSiteEditorEntities( { isOnlyCurrentEntityDirty: true, @@ -271,10 +257,6 @@ test.describe( 'Filters Overlay Template Part', () => { templatePartData.selectors.editor.blocks.activeFilters .blockLabel ) - .getByLabel( - templatePartData.selectors.editor.blocks.filterOptions - .blockLabel - ) .click(); await editor.openDocumentSettingsSidebar(); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/active-filter-frontend.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/active-filter-frontend.block_theme.spec.ts new file mode 100644 index 00000000000..341a57ee21e --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/active-filter-frontend.block_theme.spec.ts @@ -0,0 +1,115 @@ +/** + * External dependencies + */ +import { TemplateCompiler, test as base, expect } from '@woocommerce/e2e-utils'; + +const test = base.extend< { templateCompiler: TemplateCompiler } >( { + templateCompiler: async ( { requestUtils }, use ) => { + const compiler = await requestUtils.createTemplateFromFile( + 'archive-product_active-filters' + ); + await use( compiler ); + }, +} ); + +test.describe( 'woocommerce/product-filter-active - Frontend', () => { + test.beforeEach( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( + 'woocommerce-blocks-test-enable-experimental-features' + ); + } ); + + test( 'Without any filters selected, only a wrapper block is rendered', async ( { + page, + templateCompiler, + } ) => { + await templateCompiler.compile( { + attributes: { + displayStyle: 'list', + }, + } ); + + await page.goto( '/shop' ); + + const locator = page.locator( + '.wp-block-woocommerce-product-filter-active' + ); + + await expect( locator ).toHaveCount( 1 ); + + const html = await locator.innerHTML(); + expect( html.trim() ).toBe( '' ); + } ); + + test( 'With rating filters applied it shows the correct active filters', async ( { + page, + templateCompiler, + } ) => { + await templateCompiler.compile( { + attributes: { + displayStyle: 'list', + }, + } ); + + await page.goto( `${ '/shop' }?rating_filter=1,2,5` ); + + await expect( page.getByText( 'Rating:' ) ).toBeVisible(); + await expect( page.getByText( 'Rated 1 out of 5' ) ).toBeVisible(); + await expect( page.getByText( 'Rated 2 out of 5' ) ).toBeVisible(); + await expect( page.getByText( 'Rated 5 out of 5' ) ).toBeVisible(); + } ); + + test( 'With stock filters applied it shows the correct active filters', async ( { + page, + templateCompiler, + } ) => { + await templateCompiler.compile( { + attributes: { + displayStyle: 'list', + }, + } ); + + await page.goto( + `${ '/shop' }?filter_stock_status=instock,onbackorder` + ); + + await expect( page.getByText( 'Stock Status:' ) ).toBeVisible(); + await expect( page.getByText( 'In stock' ) ).toBeVisible(); + await expect( page.getByText( 'On backorder' ) ).toBeVisible(); + } ); + + test( 'With attribute filters applied it shows the correct active filters', async ( { + page, + templateCompiler, + } ) => { + await templateCompiler.compile( { + attributes: { + displayStyle: 'list', + }, + } ); + + await page.goto( + `${ '/shop' }?filter_color=blue,gray&query_type_color=or` + ); + + await expect( page.getByText( 'Color:' ) ).toBeVisible(); + await expect( page.getByText( 'Blue' ) ).toBeVisible(); + await expect( page.getByText( 'Gray' ) ).toBeVisible(); + } ); + + test( 'With price filters applied it shows the correct active filters', async ( { + page, + templateCompiler, + } ) => { + await templateCompiler.compile( { + attributes: { + displayStyle: 'list', + }, + } ); + + await page.goto( `${ '/shop' }?min_price=17&max_price=71` ); + + await expect( page.getByText( 'Price:' ) ).toBeVisible(); + await expect( page.getByText( 'Between $17 and $71' ) ).toBeVisible(); + } ); +} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-editor.block_theme.spec.ts similarity index 91% rename from plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts rename to plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-editor.block_theme.spec.ts index 54220f04b92..77f9b13f090 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-editor.block_theme.spec.ts @@ -48,9 +48,7 @@ test.describe( `${ blockData.name }`, () => { } ) => { await pageObject.addProductFiltersBlock( { cleanContent: true } ); - const block = editor.canvas - .getByLabel( 'Block: Color (Experimental)' ) - .getByLabel( 'Block: Filter Options' ); + const block = editor.canvas.getByLabel( 'Block: Color (Experimental)' ); await expect( block ).toBeVisible(); @@ -82,9 +80,7 @@ test.describe( `${ blockData.name }`, () => { } ) => { await pageObject.addProductFiltersBlock( { cleanContent: true } ); - const block = editor.canvas - .getByLabel( 'Block: Color (Experimental)' ) - .getByLabel( 'Block: Filter Options' ); + const block = editor.canvas.getByLabel( 'Block: Color (Experimental)' ); await expect( block ).toBeVisible(); @@ -112,9 +108,7 @@ test.describe( `${ blockData.name }`, () => { } ) => { await pageObject.addProductFiltersBlock( { cleanContent: true } ); - const block = editor.canvas - .getByLabel( 'Block: Color (Experimental)' ) - .getByLabel( 'Block: Filter Options' ); + const block = editor.canvas.getByLabel( 'Block: Color (Experimental)' ); await editor.openDocumentSettingsSidebar(); await block.click(); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-frontend.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-frontend.block_theme.spec.ts new file mode 100644 index 00000000000..8f79924b291 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-frontend.block_theme.spec.ts @@ -0,0 +1,171 @@ +/** + * External dependencies + */ +import { TemplateCompiler, test as base, expect } from '@woocommerce/e2e-utils'; + +const COLOR_ATTRIBUTE_VALUES = [ 'Blue', 'Gray', 'Green', 'Red', 'Yellow' ]; +const COLOR_ATTRIBUTES_WITH_COUNTS = [ + 'Blue (4)', + 'Gray (2)', + 'Green (3)', + 'Red (4)', + 'Yellow (1)', +]; + +const test = base.extend< { templateCompiler: TemplateCompiler } >( { + templateCompiler: async ( { requestUtils }, use ) => { + const compiler = await requestUtils.createTemplateFromFile( + 'archive-product_attribute-filter' + ); + await use( compiler ); + }, +} ); + +test.describe( 'woocommerce/product-filter-attribute - Frontend', () => { + test.describe( 'With default display style', () => { + test.beforeEach( async ( { requestUtils, templateCompiler } ) => { + await requestUtils.activatePlugin( + 'woocommerce-blocks-test-enable-experimental-features' + ); + await templateCompiler.compile( { + attributes: { + attributeId: 1, + }, + } ); + } ); + + test( 'clear button is not shown on initial page load', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeHidden(); + } ); + + test( 'renders a checkbox list with the available attribute filters', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const listItems = page + .getByLabel( 'Filter Options' ) + .getByRole( 'listitem' ); + + await expect( listItems ).toHaveCount( 5 ); + + for ( let i = 0; i < COLOR_ATTRIBUTE_VALUES.length; i++ ) { + await expect( listItems.nth( i ) ).toHaveText( + COLOR_ATTRIBUTE_VALUES[ i ] + ); + } + } ); + + test( 'filters the list of products by selecting an attribute', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const grayCheckbox = page.getByText( 'Gray' ); + await grayCheckbox.click(); + + // wait for navigation + await page.waitForURL( /.*filter_color=gray.*/ ); + + const products = page.locator( '.wc-block-product' ); + + await expect( products ).toHaveCount( 2 ); + } ); + + test( 'clear button appears after a filter is applied', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const grayCheckbox = page.getByText( 'Gray' ); + await grayCheckbox.click(); + + // wait for navigation + await page.waitForURL( /.*filter_color=gray.*/ ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeVisible(); + } ); + + test( 'clear button hides after deselecting all filters', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const grayCheckbox = page.getByText( 'Gray' ); + await grayCheckbox.click(); + + // wait for navigation + await page.waitForURL( /.*filter_color=gray.*/ ); + + await grayCheckbox.click(); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeHidden(); + } ); + + test( 'filters are cleared after clear button is clicked', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const grayCheckbox = page.getByText( 'Gray' ); + await grayCheckbox.click(); + + // wait for navigation + await page.waitForURL( /.*filter_color=gray.*/ ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await button.click(); + + COLOR_ATTRIBUTE_VALUES.map( async ( color ) => { + const element = page.locator( + `input[value="${ color.toLowerCase() }"]` + ); + + await expect( element ).not.toBeChecked(); + } ); + } ); + } ); + + test.describe( 'With show counts enabled', () => { + test.beforeEach( async ( { requestUtils, templateCompiler } ) => { + await requestUtils.activatePlugin( + 'woocommerce-blocks-test-enable-experimental-features' + ); + await templateCompiler.compile( { + attributes: { + attributeId: 1, + showCounts: true, + }, + } ); + } ); + + test( 'Renders checkboxes with associated product counts', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const listItems = page + .getByLabel( 'Filter Options' ) + .getByRole( 'listitem' ); + + await expect( listItems ).toHaveCount( 5 ); + + for ( let i = 0; i < COLOR_ATTRIBUTES_WITH_COUNTS.length; i++ ) { + await expect( listItems.nth( i ) ).toHaveText( + COLOR_ATTRIBUTES_WITH_COUNTS[ i ] + ); + } + } ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/price-filter-frontend.block_theme.spec.ts similarity index 99% rename from plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.block_theme.spec.ts rename to plugins/woocommerce-blocks/tests/e2e/tests/product-filters/price-filter-frontend.block_theme.spec.ts index 796fd4af488..191c16f53f4 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/price-filter-frontend.block_theme.spec.ts @@ -12,7 +12,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( { }, } ); -test.describe( 'Product Filter: Price Filter Block', () => { +test.describe.skip( 'Product Filter: Price Filter Block', () => { test.describe( 'frontend', () => { test.beforeEach( async ( { requestUtils, templateCompiler } ) => { await requestUtils.activatePlugin( diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts index d065baf73dc..3d16f0c9e89 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts @@ -52,13 +52,7 @@ test.describe( 'Product Filters Template Part', () => { const block = editor.canvas.getByLabel( `Block: ${ blockData.name }` ); await expect( block ).toBeVisible(); - const searchTerms = [ - 'Status (Experimental)', - 'Price (Experimental)', - 'Rating (Experimental)', - 'Attribute (Experimental)', - 'Active (Experimental)', - ]; + const searchTerms = [ 'Color (Experimental)', 'Active (Experimental)' ]; for ( const filter of searchTerms ) { await editor.selectBlocks( blockData.selectors.editor.block ); @@ -78,13 +72,7 @@ test.describe( 'Product Filters Template Part', () => { await searchResult.click(); - let _locator = `[aria-label="Block: ${ filter }"]`; - - // We need to treat the attributes filter different because - // the variation of the block label depends on the product attribute. - if ( filter === 'Attribute (Experimental)' ) { - _locator = '.wp-block-woocommerce-product-filter-attribute'; - } + const _locator = `[aria-label="Block: ${ filter }"]`; await expect( editor.canvas.locator( _locator ) ).toHaveCount( 2 ); } diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts index 887dcf60e20..e6838d7c37d 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts @@ -68,53 +68,17 @@ test.describe( `${ blockData.name }`, () => { ); await expect( block ).toBeVisible(); - const activeHeading = block.getByText( 'Active', { exact: true } ); - const activeFilterBlock = block - .getByLabel( 'Block: Filter Options' ) - .and( - editor.canvas.locator( - '[data-type="woocommerce/product-filter-active"]' - ) - ); - await expect( activeHeading ).toBeVisible(); + const activeFilterBlock = block.getByLabel( + 'Block: Active (Experimental)' + ); await expect( activeFilterBlock ).toBeVisible(); - const priceHeading = block.getByText( 'Price', { - exact: true, - } ); - const priceFilterBlock = block - .getByLabel( 'Block: Filter Options' ) - .and( - editor.canvas.locator( - '[data-type="woocommerce/product-filter-price"]' - ) - ); - await expect( priceHeading ).toBeVisible(); - await expect( priceFilterBlock ).toBeVisible(); - - const statusHeading = block.getByText( 'Status', { - exact: true, - } ); - const statusFilterBlock = block - .getByLabel( 'Block: Filter Options' ) - .and( - editor.canvas.locator( - '[data-type="woocommerce/product-filter-stock-status"]' - ) - ); - await expect( statusHeading ).toBeVisible(); - await expect( statusFilterBlock ).toBeVisible(); - const colorHeading = block.getByText( 'Color', { exact: true, } ); - const colorFilterBlock = block - .getByLabel( 'Block: Filter Options' ) - .and( - editor.canvas.locator( - '[data-type="woocommerce/product-filter-attribute"]' - ) - ); + const colorFilterBlock = block.getByLabel( + 'Block: Color (Experimental)' + ); const expectedColorFilterOptions = [ 'Blue', 'Green', @@ -122,27 +86,11 @@ test.describe( `${ blockData.name }`, () => { 'Red', 'Yellow', ]; - const colorFilterOptions = ( - await colorFilterBlock.allInnerTexts() - )[ 0 ].split( '\n' ); await expect( colorHeading ).toBeVisible(); await expect( colorFilterBlock ).toBeVisible(); - expect( colorFilterOptions ).toEqual( - expect.arrayContaining( expectedColorFilterOptions ) - ); - - const ratingHeading = block.getByText( 'Rating', { - exact: true, - } ); - const ratingFilterBlock = block - .getByLabel( 'Block: Filter Options' ) - .and( - editor.canvas.locator( - '[data-type="woocommerce/product-filter-rating"]' - ) - ); - await expect( ratingHeading ).toBeVisible(); - await expect( ratingFilterBlock ).toBeVisible(); + for ( const option of expectedColorFilterOptions ) { + await expect( colorFilterBlock ).toContainText( option ); + } } ); test( 'should contain the correct inner block names in the list view', async ( { @@ -181,25 +129,10 @@ test.describe( `${ blockData.name }`, () => { ); await expect( productFilterActiveBlocksListItem ).toBeVisible(); - const productFilterPriceBlockListItem = listView.getByText( - 'Price (Experimental)' - ); - await expect( productFilterPriceBlockListItem ).toBeVisible(); - - const productFilterStatusBlockListItem = listView.getByText( - 'Status (Experimental)' - ); - await expect( productFilterStatusBlockListItem ).toBeVisible(); - const productFilterAttributeBlockListItem = listView.getByText( 'Color (Experimental)' // it must select the attribute with the highest product count ); await expect( productFilterAttributeBlockListItem ).toBeVisible(); - - const productFilterRatingBlockListItem = listView.getByText( - 'Rating (Experimental)' - ); - await expect( productFilterRatingBlockListItem ).toBeVisible(); } ); test( 'should display the correct inspector style controls', async ( { @@ -370,7 +303,7 @@ test.describe( `${ blockData.name }`, () => { ).toHaveCSS( 'align-items', 'center' ); } ); - test( 'Layout > Orientation: changing option should update the preview', async ( { + test.skip( 'Layout > Orientation: changing option should update the preview', async ( { editor, pageObject, } ) => { diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/rating-filter-frontend.block_theme.spec.ts similarity index 97% rename from plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.block_theme.spec.ts rename to plugins/woocommerce-blocks/tests/e2e/tests/product-filters/rating-filter-frontend.block_theme.spec.ts index 279fd391567..272aeb4966f 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/rating-filter-frontend.block_theme.spec.ts @@ -12,7 +12,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( { }, } ); -test.describe( 'Product Filter: Rating Filter Block', () => { +test.describe.skip( 'Product Filter: Rating Filter Block', () => { test.describe( 'frontend', () => { test.beforeEach( async ( { requestUtils, templateCompiler } ) => { await requestUtils.activatePlugin( diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/stock-status-frontend.block_theme.spec.ts similarity index 98% rename from plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.block_theme.spec.ts rename to plugins/woocommerce-blocks/tests/e2e/tests/product-filters/stock-status-frontend.block_theme.spec.ts index c465257eea2..4012ea324af 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/stock-status-frontend.block_theme.spec.ts @@ -12,7 +12,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( { }, } ); -test.describe( 'Product Filter: Stock Status Block', () => { +test.describe.skip( 'Product Filter: Stock Status Block', () => { test.describe( 'With default display style', () => { test.beforeEach( async ( { requestUtils, templateCompiler } ) => { await requestUtils.activatePlugin( diff --git a/plugins/woocommerce/bin/composer/phpcs/composer.lock b/plugins/woocommerce/bin/composer/phpcs/composer.lock index a42b55c51d6..d1c2a664f30 100644 --- a/plugins/woocommerce/bin/composer/phpcs/composer.lock +++ b/plugins/woocommerce/bin/composer/phpcs/composer.lock @@ -874,5 +874,5 @@ "platform-overrides": { "php": "7.2" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/plugins/woocommerce/bin/composer/phpunit/composer.lock b/plugins/woocommerce/bin/composer/phpunit/composer.lock index f0b64b9cbe4..89456bd9d80 100644 --- a/plugins/woocommerce/bin/composer/phpunit/composer.lock +++ b/plugins/woocommerce/bin/composer/phpunit/composer.lock @@ -315,35 +315,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.31", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -352,7 +352,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -381,7 +381,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -389,7 +389,7 @@ "type": "github" } ], - "time": "2024-03-02T06:37:42+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1759,5 +1759,5 @@ "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/plugins/woocommerce/bin/composer/wp/composer.lock b/plugins/woocommerce/bin/composer/wp/composer.lock index aadb4231f84..64558c071e9 100644 --- a/plugins/woocommerce/bin/composer/wp/composer.lock +++ b/plugins/woocommerce/bin/composer/wp/composer.lock @@ -635,5 +635,5 @@ "platform-overrides": { "php": "7.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/plugins/woocommerce/changelog/50279-fix-mongolia-postcode-5 b/plugins/woocommerce/changelog/50279-fix-mongolia-postcode-5 new file mode 100644 index 00000000000..b6f3ac11940 --- /dev/null +++ b/plugins/woocommerce/changelog/50279-fix-mongolia-postcode-5 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Adjust Mongolia postcode validation to be 5 digits or 5 digits followed by 4 digits. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51060-fix-21455-notice-plugin-install-products-without-subscription b/plugins/woocommerce/changelog/51060-fix-21455-notice-plugin-install-products-without-subscription new file mode 100644 index 00000000000..8e432f67f7a --- /dev/null +++ b/plugins/woocommerce/changelog/51060-fix-21455-notice-plugin-install-products-without-subscription @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add a new notice to the plugins list for products used without subscriptions reminding them to purchase. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51114-add-44878-product-collection-handling-missing-product-context b/plugins/woocommerce/changelog/51114-add-44878-product-collection-handling-missing-product-context new file mode 100644 index 00000000000..5e5b6821ab3 --- /dev/null +++ b/plugins/woocommerce/changelog/51114-add-44878-product-collection-handling-missing-product-context @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Product Collection: Added Editor UI for missing product reference \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51163-fix-21486-iam-discover-tracks b/plugins/woocommerce/changelog/51163-fix-21486-iam-discover-tracks new file mode 100644 index 00000000000..f2459934217 --- /dev/null +++ b/plugins/woocommerce/changelog/51163-fix-21486-iam-discover-tracks @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak +Comment: Fix missing Tracks event from the Extensions > Discover page + diff --git a/plugins/woocommerce/changelog/51164-fix-extra-search-request-in-my-subscriptions b/plugins/woocommerce/changelog/51164-fix-extra-search-request-in-my-subscriptions new file mode 100644 index 00000000000..85c7d2e021f --- /dev/null +++ b/plugins/woocommerce/changelog/51164-fix-extra-search-request-in-my-subscriptions @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Prevent search request in the Extensions > My Subscriptions page \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51187-update-21566-in-app-marketaplace-header-sticky b/plugins/woocommerce/changelog/51187-update-21566-in-app-marketaplace-header-sticky new file mode 100644 index 00000000000..844123a8234 --- /dev/null +++ b/plugins/woocommerce/changelog/51187-update-21566-in-app-marketaplace-header-sticky @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Make In-App Marketplace header sticky to improve discoverability. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51277-bugfix-46283 b/plugins/woocommerce/changelog/51277-bugfix-46283 new file mode 100644 index 00000000000..cc81d2fcd5b --- /dev/null +++ b/plugins/woocommerce/changelog/51277-bugfix-46283 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix a styling bug where the guest order confirmation email input was too wide. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51281-phpcs-fixes b/plugins/woocommerce/changelog/51281-phpcs-fixes new file mode 100644 index 00000000000..9db48f6d016 --- /dev/null +++ b/plugins/woocommerce/changelog/51281-phpcs-fixes @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Fix PHPCS warnings in OrdersTableQuery.php and ProductQuery.php diff --git a/plugins/woocommerce/changelog/51284-e2e-external-pressable-include-api-tests b/plugins/woocommerce/changelog/51284-e2e-external-pressable-include-api-tests new file mode 100644 index 00000000000..0351b91c1d8 --- /dev/null +++ b/plugins/woocommerce/changelog/51284-e2e-external-pressable-include-api-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Include API tests into test suites for Pressable and WPCOM. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51286-51176-fixed-fatal-is_visible-func b/plugins/woocommerce/changelog/51286-51176-fixed-fatal-is_visible-func new file mode 100644 index 00000000000..b604b3112e5 --- /dev/null +++ b/plugins/woocommerce/changelog/51286-51176-fixed-fatal-is_visible-func @@ -0,0 +1,4 @@ +Significance: patch +Type: fix +Comment: Fixed call to a member function is_visible() on string | content-product.php:23 + diff --git a/plugins/woocommerce/changelog/51288-update-wccom-21570-in-app-product-cards-padding b/plugins/woocommerce/changelog/51288-update-wccom-21570-in-app-product-cards-padding new file mode 100644 index 00000000000..1f91bb228e1 --- /dev/null +++ b/plugins/woocommerce/changelog/51288-update-wccom-21570-in-app-product-cards-padding @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Update product card content padding to 24px and add 0 margin bottom to product cards \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51303-e2e-external-expand-wpcom-suite b/plugins/woocommerce/changelog/51303-e2e-external-expand-wpcom-suite new file mode 100644 index 00000000000..975d531637f --- /dev/null +++ b/plugins/woocommerce/changelog/51303-e2e-external-expand-wpcom-suite @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Expand the e2e suite we're running on WPCOM. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51315-update-wccom-21574-in-app-marketplace-footer b/plugins/woocommerce/changelog/51315-update-wccom-21574-in-app-marketplace-footer new file mode 100644 index 00000000000..7f77d1fe125 --- /dev/null +++ b/plugins/woocommerce/changelog/51315-update-wccom-21574-in-app-marketplace-footer @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Update footer design, add one more element to footer content and remove `woo-marketplace` copy at footer bottom. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51332-fix-focus-style-select-checkout-block b/plugins/woocommerce/changelog/51332-fix-focus-style-select-checkout-block new file mode 100644 index 00000000000..da9cb8df7b4 --- /dev/null +++ b/plugins/woocommerce/changelog/51332-fix-focus-style-select-checkout-block @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add focus styles to the Checkout block select field. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51375-update-switch-to-ariakit-buttons-in-checkout b/plugins/woocommerce/changelog/51375-update-switch-to-ariakit-buttons-in-checkout new file mode 100644 index 00000000000..ccedf5ae319 --- /dev/null +++ b/plugins/woocommerce/changelog/51375-update-switch-to-ariakit-buttons-in-checkout @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Harden styles for interactive elements in Checkout block to prevent style leakage. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51379-51134-product-attribute-placeholder-change b/plugins/woocommerce/changelog/51379-51134-product-attribute-placeholder-change new file mode 100644 index 00000000000..35d09da84a8 --- /dev/null +++ b/plugins/woocommerce/changelog/51379-51134-product-attribute-placeholder-change @@ -0,0 +1,4 @@ +Significance: patch +Type: fix +Comment: Changed Product attributes placeholder to e.g. length or weight + diff --git a/plugins/woocommerce/changelog/51386-add-control-address-edit-from-data-store b/plugins/woocommerce/changelog/51386-add-control-address-edit-from-data-store new file mode 100644 index 00000000000..38a81b13fff --- /dev/null +++ b/plugins/woocommerce/changelog/51386-add-control-address-edit-from-data-store @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Move address card state management to data stores in Checkout block. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51392-update-add-locale-param-to-jetpack-auth b/plugins/woocommerce/changelog/51392-update-add-locale-param-to-jetpack-auth new file mode 100644 index 00000000000..8e1be059b7d --- /dev/null +++ b/plugins/woocommerce/changelog/51392-update-add-locale-param-to-jetpack-auth @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add `locale` param when redirecting to the Jetpack auth page. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51414-e2e-external-expand-wpcom-suite-part2 b/plugins/woocommerce/changelog/51414-e2e-external-expand-wpcom-suite-part2 new file mode 100644 index 00000000000..806a0b91679 --- /dev/null +++ b/plugins/woocommerce/changelog/51414-e2e-external-expand-wpcom-suite-part2 @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Expand the e2e suite we're running on WPCOM part #2. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51421-update-use-horizon-env b/plugins/woocommerce/changelog/51421-update-use-horizon-env new file mode 100644 index 00000000000..0258515d62b --- /dev/null +++ b/plugins/woocommerce/changelog/51421-update-use-horizon-env @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add use-wp-horizon feature flag to set calpyso_env to horizon \ No newline at end of file diff --git a/plugins/woocommerce/changelog/51449-dev-harden-added-to-cart b/plugins/woocommerce/changelog/51449-dev-harden-added-to-cart new file mode 100644 index 00000000000..99351de4130 --- /dev/null +++ b/plugins/woocommerce/changelog/51449-dev-harden-added-to-cart @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix bug where manually triggering `added_to_cart` event without a button element caused an Exception. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/add-47890_inspector_control_to_product_search b/plugins/woocommerce/changelog/add-47890_inspector_control_to_product_search new file mode 100644 index 00000000000..351aa293cfd --- /dev/null +++ b/plugins/woocommerce/changelog/add-47890_inspector_control_to_product_search @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add inspector controls to Product Search block #51247 diff --git a/plugins/woocommerce/changelog/add-50832 b/plugins/woocommerce/changelog/add-50832 new file mode 100644 index 00000000000..e50768a52a4 --- /dev/null +++ b/plugins/woocommerce/changelog/add-50832 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Track data for font and color pairings including 'Create your own' option in CYS diff --git a/plugins/woocommerce/changelog/add-50832-loading-time b/plugins/woocommerce/changelog/add-50832-loading-time new file mode 100644 index 00000000000..3b1f7bef150 --- /dev/null +++ b/plugins/woocommerce/changelog/add-50832-loading-time @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Track customize_your_store_assembler_hub_editor_loaded event to measure CYS loading time diff --git a/plugins/woocommerce/changelog/add-chips-style-and-new-interactitity-implementation b/plugins/woocommerce/changelog/add-chips-style-and-new-interactitity-implementation new file mode 100644 index 00000000000..e9c85e6063a --- /dev/null +++ b/plugins/woocommerce/changelog/add-chips-style-and-new-interactitity-implementation @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: [Experimental] Product Filters Chips style and new interactivity API implementation + + diff --git a/plugins/woocommerce/changelog/add-missing-wp-block-classnames-49739 b/plugins/woocommerce/changelog/add-missing-wp-block-classnames-49739 new file mode 100644 index 00000000000..dc4c7059a56 --- /dev/null +++ b/plugins/woocommerce/changelog/add-missing-wp-block-classnames-49739 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Added missing wp-block- classes to order confirmation, store notices, and breadcrumb blocks. diff --git a/plugins/woocommerce/changelog/add-tax-task-completion-filter b/plugins/woocommerce/changelog/add-tax-task-completion-filter new file mode 100644 index 00000000000..89ab40a7240 --- /dev/null +++ b/plugins/woocommerce/changelog/add-tax-task-completion-filter @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Adds a filter for third party tax plugins to indicate that they have completed the tax task diff --git a/plugins/woocommerce/changelog/dev-42582_use_one_rating_component b/plugins/woocommerce/changelog/dev-42582_use_one_rating_component new file mode 100644 index 00000000000..e50c3ca32e2 --- /dev/null +++ b/plugins/woocommerce/changelog/dev-42582_use_one_rating_component @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +[Enhancement] Abstract rating block #50810 diff --git a/plugins/woocommerce/changelog/doc-update-markdown-lint b/plugins/woocommerce/changelog/doc-update-markdown-lint new file mode 100644 index 00000000000..4d3d00c11ea --- /dev/null +++ b/plugins/woocommerce/changelog/doc-update-markdown-lint @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Updating Markdown linter rule + + diff --git a/plugins/woocommerce/changelog/enhance-improve-log-structure b/plugins/woocommerce/changelog/enhance-improve-log-structure new file mode 100644 index 00000000000..f3fe5a6215f --- /dev/null +++ b/plugins/woocommerce/changelog/enhance-improve-log-structure @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Improve remote logging structure and content diff --git a/plugins/woocommerce/changelog/enhance-reduce-remote-logging-noise b/plugins/woocommerce/changelog/enhance-reduce-remote-logging-noise new file mode 100644 index 00000000000..736e9c0d3f8 --- /dev/null +++ b/plugins/woocommerce/changelog/enhance-reduce-remote-logging-noise @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Reducing noise in remote logging diff --git a/plugins/woocommerce/changelog/enhance-remote-logging-reduce-dependency b/plugins/woocommerce/changelog/enhance-remote-logging-reduce-dependency new file mode 100644 index 00000000000..2a0eca872f0 --- /dev/null +++ b/plugins/woocommerce/changelog/enhance-remote-logging-reduce-dependency @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Reduce dependency of remote logging on WC_Tracks diff --git a/plugins/woocommerce/changelog/fix-49759-removed-taxname-export b/plugins/woocommerce/changelog/fix-49759-removed-taxname-export new file mode 100644 index 00000000000..2d76f39f150 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-49759-removed-taxname-export @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Improve performance of tax report export generation and fix tax_code for removed rates. diff --git a/plugins/woocommerce/changelog/fix-50875-disable-client-side-nav-for-filters-in-pc-block b/plugins/woocommerce/changelog/fix-50875-disable-client-side-nav-for-filters-in-pc-block new file mode 100644 index 00000000000..e219a09d7aa --- /dev/null +++ b/plugins/woocommerce/changelog/fix-50875-disable-client-side-nav-for-filters-in-pc-block @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Product Collection: Disable client-side nav if filter blocks are detected inside diff --git a/plugins/woocommerce/changelog/fix-51154-product-gallery-fatal b/plugins/woocommerce/changelog/fix-51154-product-gallery-fatal new file mode 100644 index 00000000000..3ed4766c3d0 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-51154-product-gallery-fatal @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix error when adding the Product Gallery (Beta) block into a pattern diff --git a/plugins/woocommerce/changelog/fix-cart-url-page-check-50524 b/plugins/woocommerce/changelog/fix-cart-url-page-check-50524 new file mode 100644 index 00000000000..dbd4b4aa4bc --- /dev/null +++ b/plugins/woocommerce/changelog/fix-cart-url-page-check-50524 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +wc_get_cart_url should only return current URL if on the cart page. This excludes the usage of WOOCOMMERCE_CART. diff --git a/plugins/woocommerce/changelog/fix-correct-language-in-frontend b/plugins/woocommerce/changelog/fix-correct-language-in-frontend new file mode 100644 index 00000000000..00edcbc0400 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-correct-language-in-frontend @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Use the correct language in Cart/Checkout when the user language is different from the store. diff --git a/plugins/woocommerce/changelog/fix-no-permissions-api-error b/plugins/woocommerce/changelog/fix-no-permissions-api-error new file mode 100644 index 00000000000..0695a289f9a --- /dev/null +++ b/plugins/woocommerce/changelog/fix-no-permissions-api-error @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Added pre-API call permission checks for some API calls that were being called on non-admin accessible screens diff --git a/plugins/woocommerce/changelog/fix-remote-logger-undefined-wp-current-user b/plugins/woocommerce/changelog/fix-remote-logger-undefined-wp-current-user new file mode 100644 index 00000000000..be10eb2ec34 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-remote-logger-undefined-wp-current-user @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Adds a function_exists() check so that we don't add a secondary fatal in remote logging if wp_get_current_user does not exist yet diff --git a/plugins/woocommerce/changelog/fix-remove-help-panel-user-meta b/plugins/woocommerce/changelog/fix-remove-help-panel-user-meta new file mode 100644 index 00000000000..9b1882ac793 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-remove-help-panel-user-meta @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Removed the leftover user meta from the help panel spotlight diff --git a/plugins/woocommerce/changelog/fix-type-mismatch-updateproducts b/plugins/woocommerce/changelog/fix-type-mismatch-updateproducts new file mode 100644 index 00000000000..1af042a90a0 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-type-mismatch-updateproducts @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Fix a type mismatch in UpdateProducts.php + + diff --git a/plugins/woocommerce/changelog/fix-unit-test-trac-61739 b/plugins/woocommerce/changelog/fix-unit-test-trac-61739 new file mode 100644 index 00000000000..45cd4544e59 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-unit-test-trac-61739 @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Update unit test to account for WordPress nightly change. See core trac ticket 61739 + + diff --git a/plugins/woocommerce/changelog/fix-use-customer-email-if-available b/plugins/woocommerce/changelog/fix-use-customer-email-if-available new file mode 100644 index 00000000000..58490385da9 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-use-customer-email-if-available @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Set customer email in reports if customer data is available diff --git a/plugins/woocommerce/changelog/fix-wccom-20223-in-app-subs-error-handling b/plugins/woocommerce/changelog/fix-wccom-20223-in-app-subs-error-handling new file mode 100644 index 00000000000..0815a090e50 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-wccom-20223-in-app-subs-error-handling @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Introduce error handling on the in-app my subscriptions page diff --git a/plugins/woocommerce/changelog/merge-brands-in-core b/plugins/woocommerce/changelog/merge-brands-in-core new file mode 100644 index 00000000000..65fd35876a3 --- /dev/null +++ b/plugins/woocommerce/changelog/merge-brands-in-core @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Introduced Product Brands. diff --git a/plugins/woocommerce/changelog/pr-51020 b/plugins/woocommerce/changelog/pr-51020 new file mode 100644 index 00000000000..ac4e7978690 --- /dev/null +++ b/plugins/woocommerce/changelog/pr-51020 @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Fix version number in a template diff --git a/plugins/woocommerce/changelog/try-new-improved-filter-blocks-structure b/plugins/woocommerce/changelog/try-new-improved-filter-blocks-structure new file mode 100644 index 00000000000..61232016454 --- /dev/null +++ b/plugins/woocommerce/changelog/try-new-improved-filter-blocks-structure @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: Filter blocks: new and improve filter blocks structure. + + diff --git a/plugins/woocommerce/changelog/tweak-disable-remote-logging-by-default b/plugins/woocommerce/changelog/tweak-disable-remote-logging-by-default deleted file mode 100644 index cd22a7c5643..00000000000 --- a/plugins/woocommerce/changelog/tweak-disable-remote-logging-by-default +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: tweak - -Disable remote logging feature by default diff --git a/plugins/woocommerce/changelog/tweak-gtin-structured-data b/plugins/woocommerce/changelog/tweak-gtin-structured-data new file mode 100644 index 00000000000..69fc8c38c2e --- /dev/null +++ b/plugins/woocommerce/changelog/tweak-gtin-structured-data @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Validate and prepare GTIN in structured data diff --git a/plugins/woocommerce/changelog/tweak-reenable-remote-logging-by-default b/plugins/woocommerce/changelog/tweak-reenable-remote-logging-by-default new file mode 100644 index 00000000000..efcc6da22eb --- /dev/null +++ b/plugins/woocommerce/changelog/tweak-reenable-remote-logging-by-default @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Reenable remote logging feature by default diff --git a/plugins/woocommerce/changelog/update-action-scheduler-3.8.2 b/plugins/woocommerce/changelog/update-action-scheduler-3.8.2 new file mode 100644 index 00000000000..8265892ca69 --- /dev/null +++ b/plugins/woocommerce/changelog/update-action-scheduler-3.8.2 @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update the Action Scheduler package to version 3.8.2 diff --git a/plugins/woocommerce/changelog/update-deprecate-banner-layout b/plugins/woocommerce/changelog/update-deprecate-banner-layout new file mode 100644 index 00000000000..4af22693683 --- /dev/null +++ b/plugins/woocommerce/changelog/update-deprecate-banner-layout @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Deprecate unsupported Inbox note banner layout diff --git a/plugins/woocommerce/changelog/update-dynamically-set-plugin-name b/plugins/woocommerce/changelog/update-dynamically-set-plugin-name new file mode 100644 index 00000000000..079eb126b45 --- /dev/null +++ b/plugins/woocommerce/changelog/update-dynamically-set-plugin-name @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Dynamically set plugin_name when redirecting to Jetpack Auth from core profiler diff --git a/plugins/woocommerce/changelog/update-refine-error-counting b/plugins/woocommerce/changelog/update-refine-error-counting new file mode 100644 index 00000000000..905988307d8 --- /dev/null +++ b/plugins/woocommerce/changelog/update-refine-error-counting @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Refine PHP Fatal Error Counting in MC Stat diff --git a/plugins/woocommerce/changelog/update-remove-deprecated-rin-classes b/plugins/woocommerce/changelog/update-remove-deprecated-rin-classes new file mode 100644 index 00000000000..2fc99a0873a --- /dev/null +++ b/plugins/woocommerce/changelog/update-remove-deprecated-rin-classes @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Remove deprecated RemoteInboxNotification classes diff --git a/plugins/woocommerce/client/admin/config/core.json b/plugins/woocommerce/client/admin/config/core.json index 3c201f07922..6bfd9954259 100644 --- a/plugins/woocommerce/client/admin/config/core.json +++ b/plugins/woocommerce/client/admin/config/core.json @@ -39,6 +39,7 @@ "launch-your-store": true, "product-editor-template-system": false, "blueprint": false, - "reactify-classic-payments-settings": false + "reactify-classic-payments-settings": false, + "use-wp-horizon": false } } diff --git a/plugins/woocommerce/client/admin/config/development.json b/plugins/woocommerce/client/admin/config/development.json index 802c62fe6a3..da6f07d82dd 100644 --- a/plugins/woocommerce/client/admin/config/development.json +++ b/plugins/woocommerce/client/admin/config/development.json @@ -39,6 +39,7 @@ "launch-your-store": true, "product-editor-template-system": false, "blueprint": true, - "reactify-classic-payments-settings": false + "reactify-classic-payments-settings": false, + "use-wp-horizon": false } } diff --git a/plugins/woocommerce/client/legacy/css/brands-admin.scss b/plugins/woocommerce/client/legacy/css/brands-admin.scss new file mode 100644 index 00000000000..5f9e47fed78 --- /dev/null +++ b/plugins/woocommerce/client/legacy/css/brands-admin.scss @@ -0,0 +1,3 @@ +table.wp-list-table .column-taxonomy-product_brand { + width: 10%; +} diff --git a/plugins/woocommerce/client/legacy/css/brands.scss b/plugins/woocommerce/client/legacy/css/brands.scss new file mode 100644 index 00000000000..060d28a0278 --- /dev/null +++ b/plugins/woocommerce/client/legacy/css/brands.scss @@ -0,0 +1,173 @@ +/* Brand description on archives */ +.tax-product_brand .brand-description { + overflow: hidden; + zoom: 1; +} +.tax-product_brand .brand-description img.brand-thumbnail { + width: 25%; + float: right; +} +.tax-product_brand .brand-description .text { + width: 72%; + float: left; +} + +/* Brand description widget */ +.widget_brand_description img { + -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ + -moz-box-sizing: border-box; /* Firefox, other Gecko */ + box-sizing: border-box; /* Opera/IE 8+ */ + width: 100%; + max-width: none; + height: auto; + margin: 0 0 1em; +} + +/* Brand thumbnails widget */ +ul.brand-thumbnails { + margin-left: 0; + margin-bottom: 0; + clear: both; + list-style: none; +} + +ul.brand-thumbnails:before { + clear: both; + content: ""; + display: table; +} + +ul.brand-thumbnails:after { + clear: both; + content: ""; + display: table; +} + +ul.brand-thumbnails li { + float: left; + margin: 0 3.8% 1em 0; + padding: 0; + position: relative; + width: 22.05%; /* 4 columns */ +} + +ul.brand-thumbnails.fluid-columns li { + width: auto; +} + +ul.brand-thumbnails:not(.fluid-columns) li.first { + clear: both; +} + +ul.brand-thumbnails:not(.fluid-columns) li.last { + margin-right: 0; +} + +ul.brand-thumbnails.columns-1 li { + width: 100%; + margin-right: 0; +} + +ul.brand-thumbnails.columns-2 li { + width: 48%; +} + +ul.brand-thumbnails.columns-3 li { + width: 30.75%; +} + +ul.brand-thumbnails.columns-5 li { + width: 16.95%; +} + +ul.brand-thumbnails.columns-6 li { + width: 13.5%; +} + +.brand-thumbnails li img { + -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ + -moz-box-sizing: border-box; /* Firefox, other Gecko */ + box-sizing: border-box; /* Opera/IE 8+ */ + width: 100%; + max-width: none; + height: auto; + margin: 0; +} + +@media screen and (max-width: 768px) { + ul.brand-thumbnails:not(.fluid-columns) li { + width: 48% !important; + } + + ul.brand-thumbnails:not(.fluid-columns) li.first { + clear: none; + } + + ul.brand-thumbnails:not(.fluid-columns) li.last { + margin-right: 3.8% + } + + ul.brand-thumbnails:not(.fluid-columns) li:nth-of-type(odd) { + clear: both; + } + + ul.brand-thumbnails:not(.fluid-columns) li:nth-of-type(even) { + margin-right: 0; + } +} + +/* Brand thumbnails description */ +.brand-thumbnails-description li { + text-align: center; +} + +.brand-thumbnails-description li .term-thumbnail img { + display: inline; +} + +.brand-thumbnails-description li .term-description { + margin-top: 1em; + text-align: left; +} + +/* A-Z Shortcode */ +#brands_a_z h3:target { + text-decoration: underline; +} +ul.brands_index { + list-style: none outside; + overflow: hidden; + zoom: 1; +} +ul.brands_index li { + float: left; + margin: 0 2px 2px 0; +} +ul.brands_index li a, ul.brands_index li span { + border: 1px solid #ccc; + padding: 6px; + line-height: 1em; + float: left; + text-decoration: none; +} +ul.brands_index li span { + border-color: #eee; + color: #ddd; +} +ul.brands_index li a:hover { + border-width: 2px; + padding: 5px; + text-decoration: none; +} +ul.brands_index li a.active { + border-width: 2px; + padding: 5px; +} +div#brands_a_z a.top { + border: 1px solid #ccc; + padding: 4px; + line-height: 1em; + float: right; + text-decoration: none; + font-size: 0.8em; +} diff --git a/plugins/woocommerce/client/legacy/js/admin/wc-brands-enhanced-select.js b/plugins/woocommerce/client/legacy/js/admin/wc-brands-enhanced-select.js new file mode 100644 index 00000000000..270d5b8dc1c --- /dev/null +++ b/plugins/woocommerce/client/legacy/js/admin/wc-brands-enhanced-select.js @@ -0,0 +1,94 @@ +/* global wc_enhanced_select_params */ +/* global wpApiSettings */ +jQuery( function( $ ) { + + function getEnhancedSelectFormatString() { + return { + 'language': { + errorLoading: function() { + // Workaround for https://github.com/select2/select2/issues/4355 instead of i18n_ajax_error. + return wc_enhanced_select_params.i18n_searching; + }, + inputTooLong: function( args ) { + var overChars = args.input.length - args.maximum; + + if ( 1 === overChars ) { + return wc_enhanced_select_params.i18n_input_too_long_1; + } + + return wc_enhanced_select_params.i18n_input_too_long_n.replace( '%qty%', overChars ); + }, + inputTooShort: function( args ) { + var remainingChars = args.minimum - args.input.length; + + if ( 1 === remainingChars ) { + return wc_enhanced_select_params.i18n_input_too_short_1; + } + + return wc_enhanced_select_params.i18n_input_too_short_n.replace( '%qty%', remainingChars ); + }, + loadingMore: function() { + return wc_enhanced_select_params.i18n_load_more; + }, + maximumSelected: function( args ) { + if ( args.maximum === 1 ) { + return wc_enhanced_select_params.i18n_selection_too_long_1; + } + + return wc_enhanced_select_params.i18n_selection_too_long_n.replace( '%qty%', args.maximum ); + }, + noResults: function() { + return wc_enhanced_select_params.i18n_no_matches; + }, + searching: function() { + return wc_enhanced_select_params.i18n_searching; + } + } + }; + } + + try { + $( document.body ) + .on( 'wc-enhanced-select-init', function() { + // Ajax category search boxes + $( ':input.wc-brands-search' ).filter( ':not(.enhanced)' ).each( function() { + var select2_args = $.extend( { + allowClear : $( this ).data( 'allow_clear' ) ? true : false, + placeholder : $( this ).data( 'placeholder' ), + minimumInputLength: $( this ).data( 'minimum_input_length' ) ? $( this ).data( 'minimum_input_length' ) : 3, + escapeMarkup : function( m ) { + return m; + }, + ajax: { + url: wpApiSettings.root + 'wc/v3/products/brands', + dataType: 'json', + delay: 250, + headers: { + 'X-WP-Nonce': wpApiSettings.nonce + }, + data: function( params ) { + return { + hide_empty: 1, + search: params.term + }; + }, + processResults: function( data ) { + const results = data + .map( term => ({ id: term.slug, text: term.name + ' (' + term.count + ')' }) ) + return { + results + }; + }, + cache: true + } + }, getEnhancedSelectFormatString() ); + + $( this ).selectWoo( select2_args ).addClass( 'enhanced' ); + }); + }) + .trigger( 'wc-enhanced-select-init' ); + } catch( err ) { + // If select2 failed (conflict?) log the error but don't stop other scripts breaking. + window.console.log( err ); + } +}); \ No newline at end of file diff --git a/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js index a99e4394750..dd2e0872c86 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js +++ b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js @@ -173,6 +173,8 @@ jQuery( function( $ ) { * Update cart page elements after add to cart events. */ AddToCartHandler.prototype.updateButton = function( e, fragments, cart_hash, $button ) { + // Some themes and plugins manually trigger added_to_cart without passing a button element, which in turn calls this function. + // If there is no button we don't want to crash. $button = typeof $button === 'undefined' ? false : $button; if ( $button ) { @@ -222,19 +224,25 @@ jQuery( function( $ ) { * Update cart live region message after add/remove cart events. */ AddToCartHandler.prototype.alertCartUpdated = function( e, fragments, cart_hash, $button ) { - var message = $button.data( 'success_message' ); + // Some themes and plugins manually trigger added_to_cart without passing a button element, which in turn calls this function. + // If there is no button we don't want to crash. + $button = typeof $button === 'undefined' ? false : $button; - if ( !message ) { - return; - } + if ( $button ) { + var message = $button.data( 'success_message' ); + + if ( !message ) { + return; + } - // If the response after adding/removing an item to/from the cart is really fast, - // screen readers may not have time to identify the changes in the live region element. - // So, we add a delay to ensure an interval between messages. - e.data.addToCartHandler.$liveRegion - .delay(1000) - .text( message ) - .attr( 'aria-relevant', 'all' ); + // If the response after adding/removing an item to/from the cart is really fast, + // screen readers may not have time to identify the changes in the live region element. + // So, we add a delay to ensure an interval between messages. + e.data.addToCartHandler.$liveRegion + .delay(1000) + .text( message ) + .attr( 'aria-relevant', 'all' ); + } }; /** diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index cdc8c4f1dc1..a42b16a87a7 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -34,7 +34,7 @@ "composer/installers": "^1.9", "maxmind-db/reader": "^1.11", "pelago/emogrifier": "^6.0", - "woocommerce/action-scheduler": "3.8.1", + "woocommerce/action-scheduler": "3.8.2", "woocommerce/blueprint": "*" }, "require-dev": { diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock index 12b5fc18ba9..230b3d42e91 100644 --- a/plugins/woocommerce/composer.lock +++ b/plugins/woocommerce/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "15d426b01c8ba919cabdd7078845595e", + "content-hash": "e068848765b4c2df4244eac75f7ea46d", "packages": [ { "name": "automattic/jetpack-a8c-mc-stats", @@ -1251,20 +1251,20 @@ }, { "name": "woocommerce/action-scheduler", - "version": "3.8.1", + "version": "3.8.2", "source": { "type": "git", "url": "https://github.com/woocommerce/action-scheduler.git", - "reference": "e331b534d7de10402d7545a0de50177b874c0779" + "reference": "2bc91d88fdbc2c07ab899cbb56b983e11e62cf69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/e331b534d7de10402d7545a0de50177b874c0779", - "reference": "e331b534d7de10402d7545a0de50177b874c0779", + "url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/2bc91d88fdbc2c07ab899cbb56b983e11e62cf69", + "reference": "2bc91d88fdbc2c07ab899cbb56b983e11e62cf69", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=7.0" }, "require-dev": { "phpunit/phpunit": "^7.5", @@ -1288,9 +1288,9 @@ "homepage": "https://actionscheduler.org/", "support": { "issues": "https://github.com/woocommerce/action-scheduler/issues", - "source": "https://github.com/woocommerce/action-scheduler/tree/3.8.1" + "source": "https://github.com/woocommerce/action-scheduler/tree/3.8.2" }, - "time": "2024-06-20T19:53:06+00:00" + "time": "2024-09-12T23:12:58+00:00" }, { "name": "woocommerce/blueprint", @@ -4984,7 +4984,7 @@ }, { "name": "woocommerce/monorepo-plugin", - "version": "dev-tweak/bump-jetpack-connection", + "version": "dev-update/action-scheduler-3.8.2", "dist": { "type": "path", "url": "../../packages/php/monorepo-plugin", @@ -5143,5 +5143,5 @@ "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-brands.php b/plugins/woocommerce/includes/admin/class-wc-admin-brands.php new file mode 100644 index 00000000000..a5f65745625 --- /dev/null +++ b/plugins/woocommerce/includes/admin/class-wc-admin-brands.php @@ -0,0 +1,792 @@ +settings_tabs = array( + 'brands' => __( 'Brands', 'woocommerce' ), + ); + + // Hiding setting for future depreciation. Only users who have touched this settings should see it. + $setting_value = get_option( 'wc_brands_show_description' ); + if ( is_string( $setting_value ) ) { + // Add the settings fields to each tab. + $this->init_form_fields(); + add_action( 'woocommerce_get_sections_products', array( $this, 'add_settings_tab' ) ); + add_action( 'woocommerce_get_settings_products', array( $this, 'add_settings_section' ), null, 2 ); + } + + add_action( 'woocommerce_update_options_catalog', array( $this, 'save_admin_settings' ) ); + + /* 2.1 */ + add_action( 'woocommerce_update_options_products', array( $this, 'save_admin_settings' ) ); + + // Add brands filtering to the coupon creation screens. + add_action( 'woocommerce_coupon_options_usage_restriction', array( $this, 'add_coupon_brands_fields' ) ); + add_action( 'woocommerce_coupon_options_save', array( $this, 'save_coupon_brands' ) ); + + // Permalinks. + add_filter( 'pre_update_option_woocommerce_permalinks', array( $this, 'validate_product_base' ) ); + + add_action( 'current_screen', array( $this, 'add_brand_base_setting' ) ); + + // CSV Import/Export Support. + // https://github.com/woocommerce/woocommerce/wiki/Product-CSV-Importer-&-Exporter + // Import. + add_filter( 'woocommerce_csv_product_import_mapping_options', array( $this, 'add_column_to_importer_exporter' ), 10 ); + add_filter( 'woocommerce_csv_product_import_mapping_default_columns', array( $this, 'add_default_column_mapping' ), 10 ); + add_filter( 'woocommerce_product_import_inserted_product_object', array( $this, 'process_import' ), 10, 2 ); + + // Export. + add_filter( 'woocommerce_product_export_column_names', array( $this, 'add_column_to_importer_exporter' ), 10 ); + add_filter( 'woocommerce_product_export_product_default_columns', array( $this, 'add_column_to_importer_exporter' ), 10 ); + add_filter( 'woocommerce_product_export_product_column_brand_ids', array( $this, 'get_column_value_brand_ids' ), 10, 2 ); + } + + /** + * Add the settings for the new "Brands" subtab. + * + * @since 9.4.0 + * + * @param array $settings Settings. + * @param array $current_section Current section. + */ + public function add_settings_section( $settings, $current_section ) { + if ( 'brands' === $current_section ) { + $settings = $this->settings; + } + return $settings; + } + + /** + * Add a new "Brands" subtab to the "Products" tab. + * + * @since 9.4.0 + * @param array $sections Sections. + */ + public function add_settings_tab( $sections ) { + $sections = array_merge( $sections, $this->settings_tabs ); + return $sections; + } + + /** + * Display coupon filter fields relating to brands. + * + * @since 9.4.0 + * @return void + */ + public function add_coupon_brands_fields() { + global $post; + // Brands. + ?> +

    + + +

    + + settings = apply_filters( + 'woocommerce_brands_settings_fields', + array( + array( + 'name' => __( 'Brands Archives', 'woocommerce' ), + 'type' => 'title', + 'desc' => '', + 'id' => 'brands_archives', + ), + array( + 'name' => __( 'Show description', 'woocommerce' ), + 'desc' => __( 'Choose to show the brand description on the archive page. Turn this off if you intend to use the description widget instead. Please note: this is only for themes that do not show the description.', 'woocommerce' ), + 'tip' => '', + 'id' => 'wc_brands_show_description', + 'css' => '', + 'std' => 'yes', + 'type' => 'checkbox', + ), + array( + 'type' => 'sectionend', + 'id' => 'brands_archives', + ), + ) + ); + } + + /** + * Enqueue scripts. + * + * @return void + */ + public function scripts() { + $screen = get_current_screen(); + $version = Constants::get_constant( 'WC_VERSION' ); + $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + + if ( 'edit-product' === $screen->id ) { + wp_register_script( + 'wc-brands-enhanced-select', + WC()->plugin_url() . '/assets/js/admin/wc-brands-enhanced-select' . $suffix . '.js', + array( 'jquery', 'selectWoo', 'wc-enhanced-select', 'wp-api' ), + $version, + true + ); + wp_localize_script( + 'wc-brands-enhanced-select', + 'wc_brands_enhanced_select_params', + array( 'ajax_url' => get_rest_url() . 'brands/search' ) + ); + wp_enqueue_script( 'wc-brands-enhanced-select' ); + } + + if ( in_array( $screen->id, array( 'edit-product_brand' ), true ) ) { + wp_enqueue_media(); + wp_enqueue_style( 'woocommerce_admin_styles' ); + } + } + + /** + * Enqueue styles. + * + * @return void + */ + public function styles() { + $version = Constants::get_constant( 'WC_VERSION' ); + wp_enqueue_style( 'brands-admin-styles', WC()->plugin_url() . '/assets/css/brands-admin.css', array(), $version ); + } + + /** + * Admin settings function. + */ + public function admin_settings() { + woocommerce_admin_fields( $this->settings ); + } + + /** + * Save admin settings function. + */ + public function save_admin_settings() { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET['section'] ) && 'brands' === $_GET['section'] ) { + woocommerce_update_options( $this->settings ); + } + } + + /** + * Category thumbnails. + */ + public function add_thumbnail_field() { + global $woocommerce; + ?> +

    + +
    +
    + + + +
    + +
    +
    + term_id, 'thumbnail_id', true ); + if ( $thumbnail_id ) { + $image = wp_get_attachment_url( $thumbnail_id ); + } + if ( empty( $image ) ) { + $image = wc_placeholder_img_src(); + } + ?> + + + +
    +
    + + + +
    + +
    + + + $brands_column ), + array_slice( $columns, -2, null, true ) + ); + } + + + /** + * Columns function. + * + * @param mixed $columns Columns. + */ + public function columns( $columns ) { + if ( empty( $columns ) ) { + return $columns; + } + + $new_columns = array(); + $new_columns['cb'] = $columns['cb']; + $new_columns['thumb'] = __( 'Image', 'woocommerce' ); + unset( $columns['cb'] ); + $columns = array_merge( $new_columns, $columns ); + return $columns; + } + + /** + * Column function. + * + * @param mixed $columns Columns. + * @param mixed $column Column. + * @param mixed $id ID. + */ + public function column( $columns, $column, $id ) { + if ( 'thumb' === $column ) { + global $woocommerce; + + $image = ''; + $thumbnail_id = get_term_meta( $id, 'thumbnail_id', true ); + + if ( $thumbnail_id ) { + $image = wp_get_attachment_url( $thumbnail_id ); + } + if ( empty( $image ) ) { + $image = wc_placeholder_img_src(); + } + + $columns .= 'Thumbnail'; + + } + return $columns; + } + + /** + * Renders either dropdown or a search field for brands depending on the threshold value of + * woocommerce_product_brand_filter_threshold filter. + */ + public function render_product_brand_filter() { + // phpcs:disable WordPress.Security.NonceVerification + $brands_count = (int) wp_count_terms( 'product_brand' ); + $current_brand_slug = wc_clean( wp_unslash( $_GET['product_brand'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + /** + * Filter the brands threshold count. + * + * @since 9.4.0 + * + * @param int $value Threshold. + */ + if ( $brands_count <= apply_filters( 'woocommerce_product_brand_filter_threshold', 100 ) ) { + wc_product_dropdown_categories( + array( + 'pad_counts' => true, + 'show_count' => true, + 'orderby' => 'name', + 'selected' => $current_brand_slug, + 'show_option_none' => __( 'Filter by brand', 'woocommerce' ), + 'option_none_value' => '', + 'value_field' => 'slug', + 'taxonomy' => 'product_brand', + 'name' => 'product_brand', + 'class' => 'dropdown_product_brand', + ) + ); + } else { + $current_brand = $current_brand_slug ? get_term_by( 'slug', $current_brand_slug, 'product_brand' ) : ''; + $selected_option = ''; + if ( $current_brand_slug && $current_brand ) { + $selected_option = ''; + } + $placeholder = esc_attr__( 'Filter by brand', 'woocommerce' ); + ?> + + id ) { + return; + } + + add_settings_field( + 'woocommerce_product_brand_slug', + __( 'Product brand base', 'woocommerce' ), + array( $this, 'product_brand_slug_input' ), + 'permalink', + 'optional' + ); + + $this->save_permalink_settings(); + } + + /** + * Add a slug input box. + */ + public function product_brand_slug_input() { + $permalink = get_option( 'woocommerce_brand_permalink', '' ); + ?> + + 'brand_ids' ); + return array_merge( $mappings, $new_mapping ); + } + + /** + * Add brands to newly imported product. + * + * @param WC_Product $product Product being imported. + * @param array $data Raw CSV data. + */ + public function process_import( $product, $data ) { + if ( empty( $data['brand_ids'] ) ) { + return; + } + + $brand_ids = array_map( 'intval', $this->parse_brands_field( $data['brand_ids'] ) ); + + wp_set_object_terms( $product->get_id(), $brand_ids, 'product_brand' ); + } + + /** + * Parse brands field from a CSV during import. + * + * Based on WC_Product_CSV_Importer::parse_categories_field() + * + * @param string $value Field value. + * @return array + */ + public function parse_brands_field( $value ) { + + // Based on WC_Product_Importer::explode_values(). + $values = str_replace( '\\,', '::separator::', explode( ',', $value ) ); + $row_terms = array(); + foreach ( $values as $row_value ) { + $row_terms[] = trim( str_replace( '::separator::', ',', $row_value ) ); + } + + $brands = array(); + foreach ( $row_terms as $row_term ) { + $parent = null; + + // WC Core uses '>', but for some reason it's already escaped at this point. + $_terms = array_map( 'trim', explode( '>', $row_term ) ); + $total = count( $_terms ); + + foreach ( $_terms as $index => $_term ) { + $term = term_exists( $_term, 'product_brand', $parent ); + + if ( is_array( $term ) ) { + $term_id = $term['term_id']; + } else { + $term = wp_insert_term( $_term, 'product_brand', array( 'parent' => intval( $parent ) ) ); + + if ( is_wp_error( $term ) ) { + break; // We cannot continue if the term cannot be inserted. + } + + $term_id = $term['term_id']; + } + + // Only requires assign the last category. + if ( ( 1 + $index ) === $total ) { + $brands[] = $term_id; + } else { + // Store parent to be able to insert or query brands based in parent ID. + $parent = $term_id; + } + } + } + + return $brands; + } + + /** + * Get brands column value for csv export. + * + * @param string $value What will be exported. + * @param WC_Product $product Product being exported. + * @return string Brands separated by commas and child brands as "parent > child". + */ + public function get_column_value_brand_ids( $value, $product ) { + $brand_ids = wp_parse_id_list( wp_get_post_terms( $product->get_id(), 'product_brand', array( 'fields' => 'ids' ) ) ); + + if ( ! count( $brand_ids ) ) { + return ''; + } + + // Based on WC_CSV_Exporter::format_term_ids(). + $formatted_brands = array(); + foreach ( $brand_ids as $brand_id ) { + $formatted_term = array(); + $ancestor_ids = array_reverse( get_ancestors( $brand_id, 'product_brand' ) ); + + foreach ( $ancestor_ids as $ancestor_id ) { + $term = get_term( $ancestor_id, 'product_brand' ); + if ( $term && ! is_wp_error( $term ) ) { + $formatted_term[] = $term->name; + } + } + + $term = get_term( $brand_id, 'product_brand' ); + + if ( $term && ! is_wp_error( $term ) ) { + $formatted_term[] = $term->name; + } + + $formatted_brands[] = implode( ' > ', $formatted_term ); + } + + // Based on WC_CSV_Exporter::implode_values(). + $values_to_implode = array(); + foreach ( $formatted_brands as $brand ) { + $brand = (string) is_scalar( $brand ) ? $brand : ''; + $values_to_implode[] = str_replace( ',', '\\,', $brand ); + } + + return implode( ', ', $values_to_implode ); + } +} + +$GLOBALS['WC_Brands_Admin'] = new WC_Brands_Admin(); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-importers.php b/plugins/woocommerce/includes/admin/class-wc-admin-importers.php index 6dfec075b89..3c16a0b07d9 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-importers.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-importers.php @@ -215,114 +215,12 @@ class WC_Admin_Importers { * Ajax callback for importing one batch of products from a CSV. */ public function do_ajax_product_import() { - global $wpdb; - - check_ajax_referer( 'wc-product-import', 'security' ); - - if ( ! $this->import_allowed() || ! isset( $_POST['file'] ) ) { // PHPCS: input var ok. + if ( ! $this->import_allowed() ) { wp_send_json_error( array( 'message' => __( 'Insufficient privileges to import products.', 'woocommerce' ) ) ); } include_once WC_ABSPATH . 'includes/admin/importers/class-wc-product-csv-importer-controller.php'; - include_once WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php'; - - $file = wc_clean( wp_unslash( $_POST['file'] ) ); // PHPCS: input var ok. - $params = array( - 'delimiter' => ! empty( $_POST['delimiter'] ) ? wc_clean( wp_unslash( $_POST['delimiter'] ) ) : ',', // PHPCS: input var ok. - 'start_pos' => isset( $_POST['position'] ) ? absint( $_POST['position'] ) : 0, // PHPCS: input var ok. - 'mapping' => isset( $_POST['mapping'] ) ? (array) wc_clean( wp_unslash( $_POST['mapping'] ) ) : array(), // PHPCS: input var ok. - 'update_existing' => isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false, // PHPCS: input var ok. - 'character_encoding' => isset( $_POST['character_encoding'] ) ? wc_clean( wp_unslash( $_POST['character_encoding'] ) ) : '', - - /** - * Batch size for the product import process. - * - * @param int $size Batch size. - * - * @since 3.1.0 - */ - 'lines' => apply_filters( 'woocommerce_product_import_batch_size', 30 ), - 'parse' => true, - ); - - // Log failures. - if ( 0 !== $params['start_pos'] ) { - $error_log = array_filter( (array) get_user_option( 'product_import_error_log' ) ); - } else { - $error_log = array(); - } - - $importer = WC_Product_CSV_Importer_Controller::get_importer( $file, $params ); - $results = $importer->import(); - $percent_complete = $importer->get_percent_complete(); - $error_log = array_merge( $error_log, $results['failed'], $results['skipped'] ); - - update_user_option( get_current_user_id(), 'product_import_error_log', $error_log ); - - if ( 100 === $percent_complete ) { - // @codingStandardsIgnoreStart. - $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_original_id' ) ); - $wpdb->delete( $wpdb->posts, array( - 'post_type' => 'product', - 'post_status' => 'importing', - ) ); - $wpdb->delete( $wpdb->posts, array( - 'post_type' => 'product_variation', - 'post_status' => 'importing', - ) ); - // @codingStandardsIgnoreEnd. - - // Clean up orphaned data. - $wpdb->query( - " - DELETE {$wpdb->posts}.* FROM {$wpdb->posts} - LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->posts}.post_parent - WHERE wp.ID IS NULL AND {$wpdb->posts}.post_type = 'product_variation' - " - ); - $wpdb->query( - " - DELETE {$wpdb->postmeta}.* FROM {$wpdb->postmeta} - LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->postmeta}.post_id - WHERE wp.ID IS NULL - " - ); - // @codingStandardsIgnoreStart. - $wpdb->query( " - DELETE tr.* FROM {$wpdb->term_relationships} tr - LEFT JOIN {$wpdb->posts} wp ON wp.ID = tr.object_id - LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id - WHERE wp.ID IS NULL - AND tt.taxonomy IN ( '" . implode( "','", array_map( 'esc_sql', get_object_taxonomies( 'product' ) ) ) . "' ) - " ); - // @codingStandardsIgnoreEnd. - - // Send success. - wp_send_json_success( - array( - 'position' => 'done', - 'percentage' => 100, - 'url' => add_query_arg( array( '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ) ), admin_url( 'edit.php?post_type=product&page=product_importer&step=done' ) ), - 'imported' => is_countable( $results['imported'] ) ? count( $results['imported'] ) : 0, - 'imported_variations' => is_countable( $results['imported_variations'] ) ? count( $results['imported_variations'] ) : 0, - 'failed' => is_countable( $results['failed'] ) ? count( $results['failed'] ) : 0, - 'updated' => is_countable( $results['updated'] ) ? count( $results['updated'] ) : 0, - 'skipped' => is_countable( $results['skipped'] ) ? count( $results['skipped'] ) : 0, - ) - ); - } else { - wp_send_json_success( - array( - 'position' => $importer->get_file_position(), - 'percentage' => $percent_complete, - 'imported' => is_countable( $results['imported'] ) ? count( $results['imported'] ) : 0, - 'imported_variations' => is_countable( $results['imported_variations'] ) ? count( $results['imported_variations'] ) : 0, - 'failed' => is_countable( $results['failed'] ) ? count( $results['failed'] ) : 0, - 'updated' => is_countable( $results['updated'] ) ? count( $results['updated'] ) : 0, - 'skipped' => is_countable( $results['skipped'] ) ? count( $results['skipped'] ) : 0, - ) - ); - } + WC_Product_CSV_Importer_Controller::dispatch_ajax(); } /** diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php index 96cc160502c..15ed3ffe3bb 100644 --- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php +++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php @@ -85,6 +85,7 @@ class WC_Helper_Admin { if ( WC_Helper::is_site_connected() ) { $settings['wccomHelper']['subscription_expired_notice'] = PluginsHelper::get_expired_subscription_notice( false ); $settings['wccomHelper']['subscription_expiring_notice'] = PluginsHelper::get_expiring_subscription_notice( false ); + $settings['wccomHelper']['subscription_missing_notice'] = PluginsHelper::get_missing_subscription_notice(); } return $settings; diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php index 02eeb89e0aa..086071717d4 100644 --- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php +++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php @@ -40,11 +40,13 @@ class WC_Helper_Updater { } if ( WC_Helper::is_site_connected() ) { add_action( 'load-plugins.php', array( __CLASS__, 'setup_message_for_expired_and_expiring_subscriptions' ), 11 ); + add_action( 'load-plugins.php', array( __CLASS__, 'setup_message_for_plugins_without_subscription' ), 11 ); } } /** * Add the hook for modifying default WPCore update notices on the plugins management page. + * This is for plugins with expired or expiring subscriptions. */ public static function setup_message_for_expired_and_expiring_subscriptions() { foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) { @@ -52,6 +54,16 @@ class WC_Helper_Updater { } } + /** + * Add the hook for modifying default WPCore update notices on the plugins management page. + * This is for plugins without a subscription. + */ + public static function setup_message_for_plugins_without_subscription() { + foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) { + add_action( 'in_plugin_update_message-' . $plugin['_filename'], array( __CLASS__, 'display_notice_for_plugins_without_subscription' ), 10, 2 ); + } + } + /** * Runs in a cron thread, or in a visitor thread if triggered * by _maybe_update_plugins(), or in an auto-update thread. @@ -294,10 +306,11 @@ class WC_Helper_Updater { $renew_link = add_query_arg( array( + 'add-to-cart' => $product_id, 'utm_source' => 'pu', 'utm_campaign' => 'pu_plugin_screen_renew', ), - PluginsHelper::WOO_SUBSCRIPTION_PAGE_URL + PluginsHelper::WOO_CART_PAGE_URL ); /* translators: 1: Product regular price */ @@ -340,6 +353,52 @@ class WC_Helper_Updater { } } + /** + * Runs on in_plugin_update_message-{file-name}, show a message if plugin is without a subscription. + * Only Woo local plugins are passed to this function. + * + * @see setup_message_for_plugins_without_subscription + * @param object $plugin_data An array of plugin metadata. + * @param object $response An object of metadata about the available plugin update. + * + * @return void. + */ + public static function display_notice_for_plugins_without_subscription( $plugin_data, $response ) { + // Extract product ID from the response. + $product_id = preg_replace( '/[^0-9]/', '', $response->id ); + + if ( WC_Helper::has_product_subscription( $product_id ) ) { + return; + } + + // Prepare the expiry notice based on subscription status. + $purchase_link = add_query_arg( + array( + 'add-to-cart' => $product_id, + 'utm_source' => 'pu', + 'utm_campaign' => 'pu_plugin_screen_purchase', + ), + PluginsHelper::WOO_CART_PAGE_URL, + ); + + $notice = sprintf( + /* translators: 1: URL to My Subscriptions page */ + __( ' You don\'t have a subscription,
    subscribe to update.', 'woocommerce' ), + esc_url( $purchase_link ), + ); + + // Display the expiry notice. + echo wp_kses( + $notice, + array( + 'a' => array( + 'href' => array(), + 'class' => array(), + ), + ) + ); + } + /** * Get update data for all plugins. * diff --git a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php index e5fac1cecb9..cadf53a7bc5 100644 --- a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php +++ b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php @@ -103,6 +103,56 @@ class WC_Product_CSV_Importer_Controller { return wc_is_file_valid_csv( $file, $check_path ); } + /** + * Runs before controller actions to check that the file used during the import is valid. + * + * @since 9.3.0 + * + * @param string $path Path to test. + * + * @throws \Exception When file validation fails. + */ + protected static function check_file_path( string $path ): void { + $is_valid_file = false; + + if ( ! empty( $path ) ) { + $path = realpath( $path ); + $is_valid_file = false !== $path; + } + + // File must be readable. + $is_valid_file = $is_valid_file && is_readable( $path ); + + // Check that file is within an allowed location. + if ( $is_valid_file ) { + $in_valid_location = false; + $valid_locations = array(); + $valid_locations[] = ABSPATH; + + $upload_dir = wp_get_upload_dir(); + if ( false === $upload_dir['error'] ) { + $valid_locations[] = $upload_dir['basedir']; + } + + foreach ( $valid_locations as $valid_location ) { + if ( 0 === stripos( $path, trailingslashit( realpath( $valid_location ) ) ) ) { + $in_valid_location = true; + break; + } + } + + $is_valid_file = $in_valid_location; + } + + if ( ! $is_valid_file ) { + throw new \Exception( esc_html__( 'File path provided for import is invalid.', 'woocommerce' ) ); + } + + if ( ! self::is_file_valid_csv( $path ) ) { + throw new \Exception( esc_html__( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); + } + } + /** * Get all the valid filetypes for a CSV file. * @@ -263,17 +313,151 @@ class WC_Product_CSV_Importer_Controller { * Dispatch current step and show correct view. */ public function dispatch() { - // phpcs:ignore WordPress.Security.NonceVerification.Missing - if ( ! empty( $_POST['save_step'] ) && ! empty( $this->steps[ $this->step ]['handler'] ) ) { - call_user_func( $this->steps[ $this->step ]['handler'], $this ); + $output = ''; + + try { + // phpcs:ignore WordPress.Security.NonceVerification.Missing + if ( ! empty( $_POST['save_step'] ) && ! empty( $this->steps[ $this->step ]['handler'] ) ) { + if ( is_callable( $this->steps[ $this->step ]['handler'] ) ) { + call_user_func( $this->steps[ $this->step ]['handler'], $this ); + } + } + + ob_start(); + + if ( is_callable( $this->steps[ $this->step ]['view'] ) ) { + call_user_func( $this->steps[ $this->step ]['view'], $this ); + } + + $output = ob_get_clean(); + } catch ( \Exception $e ) { + $this->add_error( $e->getMessage() ); } + $this->output_header(); $this->output_steps(); $this->output_errors(); - call_user_func( $this->steps[ $this->step ]['view'], $this ); + echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output is HTML we've generated ourselves. $this->output_footer(); } + /** + * Processes AJAX requests related to a product CSV import. + * + * @since 9.3.0 + */ + public static function dispatch_ajax() { + global $wpdb; + + check_ajax_referer( 'wc-product-import', 'security' ); + + try { + $file = wc_clean( wp_unslash( $_POST['file'] ?? '' ) ); // PHPCS: input var ok. + self::check_file_path( $file ); + + $params = array( + 'delimiter' => ! empty( $_POST['delimiter'] ) ? wc_clean( wp_unslash( $_POST['delimiter'] ) ) : ',', // PHPCS: input var ok. + 'start_pos' => isset( $_POST['position'] ) ? absint( $_POST['position'] ) : 0, // PHPCS: input var ok. + 'mapping' => isset( $_POST['mapping'] ) ? (array) wc_clean( wp_unslash( $_POST['mapping'] ) ) : array(), // PHPCS: input var ok. + 'update_existing' => isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false, // PHPCS: input var ok. + 'character_encoding' => isset( $_POST['character_encoding'] ) ? wc_clean( wp_unslash( $_POST['character_encoding'] ) ) : '', + + /** + * Batch size for the product import process. + * + * @param int $size Batch size. + * + * @since 3.1.0 + */ + 'lines' => apply_filters( 'woocommerce_product_import_batch_size', 1 ), + 'parse' => true, + ); + + // Log failures. + if ( 0 !== $params['start_pos'] ) { + $error_log = array_filter( (array) get_user_option( 'product_import_error_log' ) ); + } else { + $error_log = array(); + } + + include_once WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php'; + + $importer = self::get_importer( $file, $params ); + $results = $importer->import(); + $percent_complete = $importer->get_percent_complete(); + $error_log = array_merge( $error_log, $results['failed'], $results['skipped'] ); + + update_user_option( get_current_user_id(), 'product_import_error_log', $error_log ); + + if ( 100 === $percent_complete ) { + // @codingStandardsIgnoreStart. + $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_original_id' ) ); + $wpdb->delete( $wpdb->posts, array( + 'post_type' => 'product', + 'post_status' => 'importing', + ) ); + $wpdb->delete( $wpdb->posts, array( + 'post_type' => 'product_variation', + 'post_status' => 'importing', + ) ); + // @codingStandardsIgnoreEnd. + + // Clean up orphaned data. + $wpdb->query( + " + DELETE {$wpdb->posts}.* FROM {$wpdb->posts} + LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->posts}.post_parent + WHERE wp.ID IS NULL AND {$wpdb->posts}.post_type = 'product_variation' + " + ); + $wpdb->query( + " + DELETE {$wpdb->postmeta}.* FROM {$wpdb->postmeta} + LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->postmeta}.post_id + WHERE wp.ID IS NULL + " + ); + // @codingStandardsIgnoreStart. + $wpdb->query( " + DELETE tr.* FROM {$wpdb->term_relationships} tr + LEFT JOIN {$wpdb->posts} wp ON wp.ID = tr.object_id + LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id + WHERE wp.ID IS NULL + AND tt.taxonomy IN ( '" . implode( "','", array_map( 'esc_sql', get_object_taxonomies( 'product' ) ) ) . "' ) + " ); + // @codingStandardsIgnoreEnd. + + // Send success. + wp_send_json_success( + array( + 'position' => 'done', + 'percentage' => 100, + 'url' => add_query_arg( array( '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ) ), admin_url( 'edit.php?post_type=product&page=product_importer&step=done' ) ), + 'imported' => is_countable( $results['imported'] ) ? count( $results['imported'] ) : 0, + 'imported_variations' => is_countable( $results['imported_variations'] ) ? count( $results['imported_variations'] ) : 0, + 'failed' => is_countable( $results['failed'] ) ? count( $results['failed'] ) : 0, + 'updated' => is_countable( $results['updated'] ) ? count( $results['updated'] ) : 0, + 'skipped' => is_countable( $results['skipped'] ) ? count( $results['skipped'] ) : 0, + ) + ); + } else { + wp_send_json_success( + array( + 'position' => $importer->get_file_position(), + 'percentage' => $percent_complete, + 'imported' => is_countable( $results['imported'] ) ? count( $results['imported'] ) : 0, + 'imported_variations' => is_countable( $results['imported_variations'] ) ? count( $results['imported_variations'] ) : 0, + 'failed' => is_countable( $results['failed'] ) ? count( $results['failed'] ) : 0, + 'updated' => is_countable( $results['updated'] ) ? count( $results['updated'] ) : 0, + 'skipped' => is_countable( $results['skipped'] ) ? count( $results['skipped'] ) : 0, + ) + ); + } + } catch ( \Exception $e ) { + wp_send_json_error( array( 'message' => $e->getMessage() ) ); + } + } + /** * Output information about the uploading process. */ @@ -314,60 +498,20 @@ class WC_Product_CSV_Importer_Controller { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce already verified in WC_Product_CSV_Importer_Controller::upload_form_handler() $file_url = isset( $_POST['file_url'] ) ? wc_clean( wp_unslash( $_POST['file_url'] ) ) : ''; - if ( empty( $file_url ) ) { - if ( ! isset( $_FILES['import'] ) ) { - return new WP_Error( 'woocommerce_product_csv_importer_upload_file_empty', __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.', 'woocommerce' ) ); + try { + if ( ! empty( $file_url ) ) { + $path = ABSPATH . $file_url; + self::check_file_path( $path ); + } else { + $csv_import_util = wc_get_container()->get( Automattic\WooCommerce\Internal\Admin\ImportExport\CSVUploadHelper::class ); + $upload = $csv_import_util->handle_csv_upload( 'product', 'import', self::get_valid_csv_filetypes() ); + $path = $upload['file']; } - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated - if ( ! self::is_file_valid_csv( wc_clean( wp_unslash( $_FILES['import']['name'] ) ), false ) ) { - return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); - } - - $overrides = array( - 'test_form' => false, - 'mimes' => self::get_valid_csv_filetypes(), - ); - $import = $_FILES['import']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash - $upload = wp_handle_upload( $import, $overrides ); - - if ( isset( $upload['error'] ) ) { - return new WP_Error( 'woocommerce_product_csv_importer_upload_error', $upload['error'] ); - } - - // Construct the object array. - $object = array( - 'post_title' => basename( $upload['file'] ), - 'post_content' => $upload['url'], - 'post_mime_type' => $upload['type'], - 'guid' => $upload['url'], - 'context' => 'import', - 'post_status' => 'private', - ); - - // Save the data. - $id = wp_insert_attachment( $object, $upload['file'] ); - - /* - * Schedule a cleanup for one day from now in case of failed - * import or missing wp_import_cleanup() call. - */ - wp_schedule_single_event( time() + DAY_IN_SECONDS, 'importer_scheduled_cleanup', array( $id ) ); - - return $upload['file']; - } elseif ( - ( 0 === stripos( realpath( ABSPATH . $file_url ), ABSPATH ) ) && - file_exists( ABSPATH . $file_url ) - ) { - if ( ! self::is_file_valid_csv( ABSPATH . $file_url ) ) { - return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); - } - - return ABSPATH . $file_url; + return $path; + } catch ( \Exception $e ) { + return new \WP_Error( 'woocommerce_product_csv_importer_upload_invalid_file', $e->getMessage() ); } - // phpcs:enable - - return new WP_Error( 'woocommerce_product_csv_importer_upload_invalid_file', __( 'Please upload or provide the link to a valid CSV file.', 'woocommerce' ) ); } /** @@ -375,6 +519,8 @@ class WC_Product_CSV_Importer_Controller { */ protected function mapping_form() { check_admin_referer( 'woocommerce-csv-importer' ); + self::check_file_path( $this->file ); + $args = array( 'lines' => 1, 'delimiter' => $this->delimiter, @@ -412,18 +558,7 @@ class WC_Product_CSV_Importer_Controller { // Displaying this page triggers Ajax action to run the import with a valid nonce, // therefore this page needs to be nonce protected as well. check_admin_referer( 'woocommerce-csv-importer' ); - - if ( ! self::is_file_valid_csv( $this->file ) ) { - $this->add_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); - $this->output_errors(); - return; - } - - if ( ! is_file( $this->file ) ) { - $this->add_error( __( 'The file does not exist, please try again.', 'woocommerce' ) ); - $this->output_errors(); - return; - } + self::check_file_path( $this->file ); if ( ! empty( $_POST['map_from'] ) && ! empty( $_POST['map_to'] ) ) { $mapping_from = wc_clean( wp_unslash( $_POST['map_from'] ) ); diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php index ef38a24cb54..94112796bb9 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php @@ -20,7 +20,7 @@ if ( ! defined( 'ABSPATH' ) ) { get_name() ) ); ?> - + diff --git a/plugins/woocommerce/includes/blocks/class-wc-brands-block-template-utils-duplicated.php b/plugins/woocommerce/includes/blocks/class-wc-brands-block-template-utils-duplicated.php new file mode 100644 index 00000000000..19844bc9d66 --- /dev/null +++ b/plugins/woocommerce/includes/blocks/class-wc-brands-block-template-utils-duplicated.php @@ -0,0 +1,369 @@ + 'block-templates', + 'DEPRECATED_TEMPLATE_PARTS' => 'block-template-parts', + 'TEMPLATES' => 'templates', + 'TEMPLATE_PARTS' => 'parts', + ); + + /** + * WooCommerce plugin slug + * + * This is used to save templates to the DB which are stored against this value in the wp_terms table. + * + * @var string + */ + protected const PLUGIN_SLUG = 'woocommerce/woocommerce'; + + /** + * Returns an array containing the references of + * the passed blocks and their inner blocks. + * + * @param array $blocks array of blocks. + * + * @return array block references to the passed blocks and their inner blocks. + */ + public static function gutenberg_flatten_blocks( &$blocks ) { + $all_blocks = array(); + $queue = array(); + foreach ( $blocks as &$block ) { + $queue[] = &$block; + } + $queue_count = count( $queue ); + + while ( $queue_count > 0 ) { + $block = &$queue[0]; + array_shift( $queue ); + $all_blocks[] = &$block; + + if ( ! empty( $block['innerBlocks'] ) ) { + foreach ( $block['innerBlocks'] as &$inner_block ) { + $queue[] = &$inner_block; + } + } + + $queue_count = count( $queue ); + } + + return $all_blocks; + } + + /** + * Parses wp_template content and injects the current theme's + * stylesheet as a theme attribute into each wp_template_part + * + * @param string $template_content serialized wp_template content. + * + * @return string Updated wp_template content. + */ + public static function gutenberg_inject_theme_attribute_in_content( $template_content ) { + $has_updated_content = false; + $new_content = ''; + $template_blocks = parse_blocks( $template_content ); + + $blocks = self::gutenberg_flatten_blocks( $template_blocks ); + foreach ( $blocks as &$block ) { + if ( + 'core/template-part' === $block['blockName'] && + ! isset( $block['attrs']['theme'] ) + ) { + $block['attrs']['theme'] = wp_get_theme()->get_stylesheet(); + $has_updated_content = true; + } + } + + if ( $has_updated_content ) { + foreach ( $template_blocks as &$block ) { + $new_content .= serialize_block( $block ); + } + + return $new_content; + } + + return $template_content; + } + + /** + * Build a unified template object based a post Object. + * + * @param \WP_Post $post Template post. + * + * @return \WP_Block_Template|\WP_Error Template. + */ + public static function gutenberg_build_template_result_from_post( $post ) { + $terms = get_the_terms( $post, 'wp_theme' ); + + if ( is_wp_error( $terms ) ) { + return $terms; + } + + if ( ! $terms ) { + return new \WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.', 'woocommerce' ) ); + } + + $theme = $terms[0]->name; + $has_theme_file = true; + + $template = new \WP_Block_Template(); + $template->wp_id = $post->ID; + $template->id = $theme . '//' . $post->post_name; + $template->theme = $theme; + $template->content = $post->post_content; + $template->slug = $post->post_name; + $template->source = 'custom'; + $template->type = $post->post_type; + $template->description = $post->post_excerpt; + $template->title = $post->post_title; + $template->status = $post->post_status; + $template->has_theme_file = $has_theme_file; + $template->is_custom = false; + $template->post_types = array(); // Don't appear in any Edit Post template selector dropdown. + + if ( 'wp_template_part' === $post->post_type ) { + $type_terms = get_the_terms( $post, 'wp_template_part_area' ); + if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) { + $template->area = $type_terms[0]->name; + } + } + + // We are checking 'woocommerce' to maintain legacy templates which are saved to the DB, + // prior to updating to use the correct slug. + // More information found here: https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5423. + if ( self::PLUGIN_SLUG === $theme || 'woocommerce' === strtolower( $theme ) ) { + $template->origin = 'plugin'; + } + + return $template; + } + + /** + * Build a unified template object based on a theme file. + * + * @param array|object $template_file Theme file. + * @param string $template_type wp_template or wp_template_part. + * + * @return \WP_Block_Template Template. + */ + public static function gutenberg_build_template_result_from_file( $template_file, $template_type ) { + $template_file = (object) $template_file; + + // If the theme has an archive-products.html template but does not have product taxonomy templates + // then we will load in the archive-product.html template from the theme to use for product taxonomies on the frontend. + $template_is_from_theme = 'theme' === $template_file->source; + $theme_name = wp_get_theme()->get( 'TextDomain' ); + + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + $template_content = file_get_contents( $template_file->path ); + $template = new \WP_Block_Template(); + $template->id = $template_is_from_theme ? $theme_name . '//' . $template_file->slug : self::PLUGIN_SLUG . '//' . $template_file->slug; + $template->theme = $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG; + $template->content = self::gutenberg_inject_theme_attribute_in_content( $template_content ); + // Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909. + $template->source = $template_file->source ? $template_file->source : 'plugin'; + $template->slug = $template_file->slug; + $template->type = $template_type; + $template->title = ! empty( $template_file->title ) ? $template_file->title : self::convert_slug_to_title( $template_file->slug ); + $template->status = 'publish'; + $template->has_theme_file = true; + $template->origin = $template_file->source; + $template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are. + $template->post_types = array(); // Don't appear in any Edit Post template selector dropdown. + $template->area = 'uncategorized'; + return $template; + } + + /** + * Build a new template object so that we can make Woo Blocks default templates available in the current theme should they not have any. + * + * @param string $template_file Block template file path. + * @param string $template_type wp_template or wp_template_part. + * @param string $template_slug Block template slug e.g. single-product. + * @param bool $template_is_from_theme If the block template file is being loaded from the current theme instead of Woo Blocks. + * + * @return object Block template object. + */ + public static function create_new_block_template_object( $template_file, $template_type, $template_slug, $template_is_from_theme = false ) { + $theme_name = wp_get_theme()->get( 'TextDomain' ); + + $new_template_item = array( + 'slug' => $template_slug, + 'id' => $template_is_from_theme ? $theme_name . '//' . $template_slug : self::PLUGIN_SLUG . '//' . $template_slug, + 'path' => $template_file, + 'type' => $template_type, + 'theme' => $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG, + // Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909. + 'source' => $template_is_from_theme ? 'theme' : 'plugin', + 'title' => self::convert_slug_to_title( $template_slug ), + 'description' => '', + 'post_types' => array(), // Don't appear in any Edit Post template selector dropdown. + ); + + return (object) $new_template_item; + } + + + /** + * Converts template slugs into readable titles. + * + * @param string $template_slug The templates slug (e.g. single-product). + * @return string Human friendly title converted from the slug. + */ + public static function convert_slug_to_title( $template_slug ) { + switch ( $template_slug ) { + case 'single-product': + return __( 'Single Product', 'woocommerce' ); + case 'archive-product': + return __( 'Product Archive', 'woocommerce' ); + case 'taxonomy-product_cat': + return __( 'Product Category', 'woocommerce' ); + case 'taxonomy-product_tag': + return __( 'Product Tag', 'woocommerce' ); + default: + // Replace all hyphens and underscores with spaces. + return ucwords( preg_replace( '/[\-_]/', ' ', $template_slug ) ); + } + } + + + /** + * Gets the first matching template part within themes directories + * + * Since [Gutenberg 12.1.0](https://github.com/WordPress/gutenberg/releases/tag/v12.1.0), the conventions for + * block templates and parts directory has changed from `block-templates` and `block-templates-parts` + * to `templates` and `parts` respectively. + * + * This function traverses all possible combinations of directory paths where a template or part + * could be located and returns the first one which is readable, prioritizing the new convention + * over the deprecated one, but maintaining that one for backwards compatibility. + * + * @param string $template_slug The slug of the template (i.e. without the file extension). + * @param string $template_type Either `wp_template` or `wp_template_part`. + * + * @return string|null The matched path or `null` if no match was found. + */ + public static function get_theme_template_path( $template_slug, $template_type = 'wp_template' ) { + $template_filename = $template_slug . '.html'; + $possible_templates_dir = 'wp_template' === $template_type ? array( + self::DIRECTORY_NAMES['TEMPLATES'], + self::DIRECTORY_NAMES['DEPRECATED_TEMPLATES'], + ) : array( + self::DIRECTORY_NAMES['TEMPLATE_PARTS'], + self::DIRECTORY_NAMES['DEPRECATED_TEMPLATE_PARTS'], + ); + + // Combine the possible root directory names with either the template directory + // or the stylesheet directory for child themes. + $possible_paths = array_reduce( + $possible_templates_dir, + function ( $carry, $item ) use ( $template_filename ) { + $filepath = DIRECTORY_SEPARATOR . $item . DIRECTORY_SEPARATOR . $template_filename; + + $carry[] = get_template_directory() . $filepath; + $carry[] = get_stylesheet_directory() . $filepath; + + return $carry; + }, + array() + ); + + // Return the first matching. + foreach ( $possible_paths as $path ) { + if ( is_readable( $path ) ) { + return $path; + } + } + + return null; + } + + /** + * Check if the theme has a template. So we know if to load our own in or not. + * + * @param string $template_name name of the template file without .html extension e.g. 'single-product'. + * @return boolean + */ + public static function theme_has_template( $template_name ) { + return (bool) self::get_theme_template_path( $template_name, 'wp_template' ); + } + + /** + * Check if the theme has a template. So we know if to load our own in or not. + * + * @param string $template_name name of the template file without .html extension e.g. 'single-product'. + * @return boolean + */ + public static function theme_has_template_part( $template_name ) { + return (bool) self::get_theme_template_path( $template_name, 'wp_template_part' ); + } + + /** + * Checks to see if they are using a compatible version of WP, or if not they have a compatible version of the Gutenberg plugin installed. + * + * @return boolean + */ + public static function supports_block_templates() { + if ( + ( ! function_exists( 'wp_is_block_theme' ) || ! wp_is_block_theme() ) && + ( ! function_exists( 'gutenberg_supports_block_templates' ) || ! gutenberg_supports_block_templates() ) + ) { + return false; + } + + return true; + } + + /** + * Returns whether the blockified templates should be used or not. + * + * First, we need to make sure WordPress version is higher than 6.1 (lowest that supports Products block). + * Then, if the option is not stored on the db, we need to check if the current theme is a block one or not. + * + * @return boolean + */ + public static function should_use_blockified_product_grid_templates() { + $minimum_wp_version = '6.1'; + + if ( version_compare( $GLOBALS['wp_version'], $minimum_wp_version, '<' ) ) { + return false; + } + + $use_blockified_templates = wc_string_to_bool( get_option( Options::WC_BLOCK_USE_BLOCKIFIED_PRODUCT_GRID_BLOCK_AS_TEMPLATE ) ); + + if ( false === $use_blockified_templates ) { + return function_exists( 'wc_current_theme_is_fse_theme' ) && wc_current_theme_is_fse_theme(); + } + + return $use_blockified_templates; + } +} diff --git a/plugins/woocommerce/includes/blocks/class-wc-brands-block-templates.php b/plugins/woocommerce/includes/blocks/class-wc-brands-block-templates.php new file mode 100644 index 00000000000..efba2807519 --- /dev/null +++ b/plugins/woocommerce/includes/blocks/class-wc-brands-block-templates.php @@ -0,0 +1,156 @@ + 'taxonomy-product_brand', + 'post_type' => 'wp_template', + 'post_status' => 'publish', + 'posts_per_page' => 1, + ) + ); + + if ( count( $posts ) ) { + return $posts[0]; + } + + return null; + } + + /** + * Fixes a bug regarding taxonomies and FSE. + * Without this, the system will always load archive-product.php version instead of taxonomy_product_brand.html + * it will show a deprecation error if that happens. + * + * Triggered by woocommerce_has_block_template filter + * + * @param bool $has_template True if the template is available. + * @param string $template_name The name of the template. + * + * @return bool True if the system is checking archive-product + */ + public function has_block_template( $has_template, $template_name ) { + if ( 'archive-product' === $template_name || 'taxonomy-product_brand' === $template_name ) { + $has_template = true; + } + + return $has_template; + } + + /** + * Get the block template for Taxonomy Product Brand. First it attempts to load the last version from DB + * Otherwise it loads the file based template. + * + * @param string $template_type The post_type for the template. Normally wp_template or wp_template_part. + * + * @return WP_Block_Template The taxonomy-product_brand template. + */ + private function get_product_brands_template( $template_type ) { + $template_db = $this->get_product_brand_template_db(); + + if ( $template_db ) { + return BlockTemplateUtilsDuplicated::gutenberg_build_template_result_from_post( $template_db ); + } + + $template_path = BlockTemplateUtilsDuplicated::should_use_blockified_product_grid_templates() + ? WC()->plugin_path() . '/templates/templates/blockified/taxonomy-product_brand.html' + : WC()->plugin_path() . '/templates/templates/taxonomy-product_brand.html'; + + $template_file = BlockTemplateUtilsDuplicated::create_new_block_template_object( $template_path, $template_type, 'taxonomy-product_brand', false ); + + return BlockTemplateUtilsDuplicated::gutenberg_build_template_result_from_file( $template_file, $template_type ); + } + + /** + * Function to check if a template name is woocommerce/taxonomy-product_brand + * + * Notice depending on the version of WooCommerce this could be: + * + * woocommerce//taxonomy-product_brand + * woocommerce/woocommerce//taxonomy-product_brand + * + * @param String $id The string to check if contains the template name. + * + * @return bool True if the template is woocommerce/taxonomy-product_brand + */ + private function is_taxonomy_product_brand_template( $id ) { + return strpos( $id, 'woocommerce//taxonomy-product_brand' ) !== false; + } + + /** + * Get the block template for Taxonomy Product Brand if requested. + * Triggered by get_block_file_template action + * + * @param WP_Block_Template|null $block_template The current Block Template loaded, if any. + * @param string $id The template id normally in the format theme-slug//template-slug. + * @param string $template_type The post_type for the template. Normally wp_template or wp_template_part. + * + * @return WP_Block_Template|null The taxonomy-product_brand template. + */ + public function get_block_file_template( $block_template, $id, $template_type ) { + if ( $this->is_taxonomy_product_brand_template( $id ) && is_null( $block_template ) ) { + $block_template = $this->get_product_brands_template( $template_type ); + } + + return $block_template; + } + + /** + * Add the Block template in the template query results needed by FSE + * Triggered by get_block_templates action + * + * @param array $query_result The list of templates to render in the query. + * @param array $query The current query parameters. + * @param string $template_type The post_type for the template. Normally wp_template or wp_template_part. + * + * @return WP_Block_Template[] Array of the matched Block Templates to render. + */ + public function get_block_templates( $query_result, $query, $template_type ) { + // We don't want to run this if we are looking for template-parts. Like the header. + if ( 'wp_template' !== $template_type ) { + return $query_result; + } + + $post_id = isset( $_REQUEST['postId'] ) ? wc_clean( wp_unslash( $_REQUEST['postId'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $slugs = $query['slug__in'] ?? array(); + + // Only add the template if asking for Product Brands. + if ( + in_array( 'taxonomy-product_brand', $slugs, true ) || + ( ! $post_id && ! count( $slugs ) ) || + ( ! count( $slugs ) && $this->is_taxonomy_product_brand_template( $post_id ) ) + ) { + $query_result[] = $this->get_product_brands_template( $template_type ); + } + + return $query_result; + } +} + +new WC_Brands_Block_Templates(); diff --git a/plugins/woocommerce/includes/class-wc-autoloader.php b/plugins/woocommerce/includes/class-wc-autoloader.php index 6f3b3f51be1..3c9c9570eee 100644 --- a/plugins/woocommerce/includes/class-wc-autoloader.php +++ b/plugins/woocommerce/includes/class-wc-autoloader.php @@ -77,6 +77,11 @@ class WC_Autoloader { return; } + // If the class is already loaded from a merged package, prevent autoloader from loading it as well. + if ( \Automattic\WooCommerce\Packages::should_load_class( $class ) ) { + return; + } + $file = $this->get_file_name_from_class( $class ); $path = ''; diff --git a/plugins/woocommerce/includes/class-wc-brands-brand-settings-manager.php b/plugins/woocommerce/includes/class-wc-brands-brand-settings-manager.php new file mode 100644 index 00000000000..5b68df7e4df --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-brands-brand-settings-manager.php @@ -0,0 +1,68 @@ +get_id(); + + // Check if the brand settings are already set for this coupon. + if ( isset( self::$brand_settings[ $coupon_id ] ) ) { + return; + } + + $included_brands = get_post_meta( $coupon_id, 'product_brands', true ); + $included_brands = ! empty( $included_brands ) ? $included_brands : array(); + + $excluded_brands = get_post_meta( $coupon_id, 'exclude_product_brands', true ); + $excluded_brands = ! empty( $excluded_brands ) ? $excluded_brands : array(); + + // Store these settings in the static array. + self::$brand_settings[ $coupon_id ] = array( + 'included_brands' => $included_brands, + 'excluded_brands' => $excluded_brands, + ); + } + + /** + * Get brand settings for a coupon. + * + * @param WC_Coupon $coupon Coupon object. + * @return array Brand settings (included and excluded brands). + */ + public static function get_brand_settings_on_coupon( $coupon ) { + $coupon_id = $coupon->get_id(); + + if ( isset( self::$brand_settings[ $coupon_id ] ) ) { + return self::$brand_settings[ $coupon_id ]; + } + + // Default return value if no settings are found. + return array( + 'included_brands' => array(), + 'excluded_brands' => array(), + ); + } +} diff --git a/plugins/woocommerce/includes/class-wc-brands-coupons.php b/plugins/woocommerce/includes/class-wc-brands-coupons.php new file mode 100644 index 00000000000..065ec3e0de7 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-brands-coupons.php @@ -0,0 +1,189 @@ +set_brand_settings_on_coupon( $coupon ); + + // Only check if coupon has brand restrictions on it. + $brand_coupon_settings = WC_Brands_Brand_Settings_Manager::get_brand_settings_on_coupon( $coupon ); + + $brand_restrictions = ! empty( $brand_coupon_settings['included_brands'] ) || ! empty( $brand_coupon_settings['excluded_brands'] ); + if ( ! $brand_restrictions ) { + return $valid; + } + + $included_brands_match = false; + $excluded_brands_matches = 0; + + $items = $discounts->get_items(); + + foreach ( $items as $item ) { + $product_brands = $this->get_product_brands( $this->get_product_id( $item->product ) ); + + if ( ! empty( array_intersect( $product_brands, $brand_coupon_settings['included_brands'] ) ) ) { + $included_brands_match = true; + } + + if ( ! empty( array_intersect( $product_brands, $brand_coupon_settings['excluded_brands'] ) ) ) { + ++$excluded_brands_matches; + } + } + + // 1) Coupon has a brand requirement but no products in the cart have the brand. + if ( ! $included_brands_match && ! empty( $brand_coupon_settings['included_brands'] ) ) { + throw new Exception( WC_Coupon::E_WC_COUPON_NOT_APPLICABLE ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + } + + // 2) All products in the cart match brand exclusion rule. + if ( count( $items ) === $excluded_brands_matches ) { + throw new Exception( self::E_WC_COUPON_EXCLUDED_BRANDS ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + } + + // 3) For a cart discount, there is at least one product in cart that matches exclusion rule. + if ( $coupon->is_type( 'fixed_cart' ) && $excluded_brands_matches > 0 ) { + throw new Exception( self::E_WC_COUPON_EXCLUDED_BRANDS ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + } + + return $valid; + } + + /** + * Check if a coupon is valid for a product. + * + * This allows percentage and product discounts to apply to only + * the correct products in the cart. + * + * @param bool $valid Whether the product should get the coupon's discounts. + * @param WC_Product $product WC Product Object. + * @param WC_Coupon $coupon Coupon object. + * @return bool $valid + */ + public function is_valid_for_product( $valid, $product, $coupon ) { + + if ( ! is_a( $product, 'WC_Product' ) ) { + return $valid; + } + $this->set_brand_settings_on_coupon( $coupon ); + + $product_id = $this->get_product_id( $product ); + $product_brands = $this->get_product_brands( $product_id ); + + // Check if coupon has a brand requirement and if this product has that brand attached. + $brand_coupon_settings = WC_Brands_Brand_Settings_Manager::get_brand_settings_on_coupon( $coupon ); + if ( ! empty( $brand_coupon_settings['included_brands'] ) && empty( array_intersect( $product_brands, $brand_coupon_settings['included_brands'] ) ) ) { + return false; + } + + // Check if coupon has a brand exclusion and if this product has that brand attached. + if ( ! empty( $brand_coupon_settings['excluded_brands'] ) && ! empty( array_intersect( $product_brands, $brand_coupon_settings['excluded_brands'] ) ) ) { + return false; + } + + return $valid; + } + + /** + * Display a custom error message when a cart discount coupon does not validate + * because an excluded brand was found in the cart. + * + * @param string $err The error message. + * @param string $err_code The error code. + * @return string + */ + public function brand_exclusion_error( $err, $err_code ) { + if ( self::E_WC_COUPON_EXCLUDED_BRANDS !== $err_code ) { + return $err; + } + + return __( 'Sorry, this coupon is not applicable to the brands of selected products.', 'woocommerce' ); + } + + /** + * Get a list of brands that are assigned to a specific product + * + * @param int $product_id Product id. + * @return array brands + */ + private function get_product_brands( $product_id ) { + return wp_get_post_terms( $product_id, 'product_brand', array( 'fields' => 'ids' ) ); + } + + /** + * Set brand settings as properties on coupon object. These properties are + * lists of included product brand IDs and list of excluded brand IDs. + * + * @param WC_Coupon $coupon Coupon object. + * + * @return void + */ + private function set_brand_settings_on_coupon( $coupon ) { + $brand_coupon_settings = WC_Brands_Brand_Settings_Manager::get_brand_settings_on_coupon( $coupon ); + + if ( ! empty( $brand_coupon_settings['included_brands'] ) && ! empty( $brand_coupon_settings['excluded_brands'] ) ) { + return; + } + + $included_brands = get_post_meta( $coupon->get_id(), 'product_brands', true ); + if ( empty( $included_brands ) ) { + $included_brands = array(); + } + + $excluded_brands = get_post_meta( $coupon->get_id(), 'exclude_product_brands', true ); + if ( empty( $excluded_brands ) ) { + $excluded_brands = array(); + } + + // Store these for later to avoid multiple look-ups. + WC_Brands_Brand_Settings_Manager::set_brand_settings_on_coupon( $coupon ); + } + + /** + * Returns the product (or variant) ID. + * + * @param WC_Product $product WC Product Object. + * @return int Product ID + */ + private function get_product_id( $product ) { + return $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(); + } +} + +new WC_Brands_Coupons(); diff --git a/plugins/woocommerce/includes/class-wc-brands.php b/plugins/woocommerce/includes/class-wc-brands.php new file mode 100644 index 00000000000..71e1fa71299 --- /dev/null +++ b/plugins/woocommerce/includes/class-wc-brands.php @@ -0,0 +1,1070 @@ +template_url = apply_filters( 'woocommerce_template_url', 'woocommerce/' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + + add_action( 'plugins_loaded', array( $this, 'register_hooks' ), 2 ); + + $this->register_shortcodes(); + } + + /** + * Register our hooks + */ + public function register_hooks() { + add_action( 'woocommerce_register_taxonomy', array( __CLASS__, 'init_taxonomy' ) ); + add_action( 'widgets_init', array( $this, 'init_widgets' ) ); + + if ( ! wc_current_theme_is_fse_theme() ) { + add_filter( 'template_include', array( $this, 'template_loader' ) ); + } + + add_action( 'wp_enqueue_scripts', array( $this, 'styles' ) ); + add_action( 'wp', array( $this, 'body_class' ) ); + + add_action( 'woocommerce_product_meta_end', array( $this, 'show_brand' ) ); + add_filter( 'woocommerce_structured_data_product', array( $this, 'add_structured_data' ), 20 ); + + // duplicate product brands. + add_action( 'woocommerce_product_duplicate_before_save', array( $this, 'duplicate_store_temporary_brands' ), 10, 2 ); + add_action( 'woocommerce_new_product', array( $this, 'duplicate_add_product_brand_terms' ) ); + add_action( 'woocommerce_new_product', array( $this, 'invalidate_wc_layered_nav_counts_cache' ), 10, 0 ); + add_action( 'woocommerce_update_product', array( $this, 'invalidate_wc_layered_nav_counts_cache' ), 10, 0 ); + add_action( 'transition_post_status', array( $this, 'reset_layered_nav_counts_on_status_change' ), 10, 3 ); + + add_filter( 'post_type_link', array( $this, 'post_type_link' ), 11, 2 ); + + if ( 'yes' === get_option( 'wc_brands_show_description' ) ) { + add_action( 'woocommerce_archive_description', array( $this, 'brand_description' ) ); + } + + add_filter( 'woocommerce_product_query_tax_query', array( $this, 'update_product_query_tax_query' ), 10, 1 ); + + // REST API. + add_action( 'rest_api_init', array( $this, 'rest_api_register_routes' ) ); + add_action( 'woocommerce_rest_insert_product', array( $this, 'rest_api_maybe_set_brands' ), 10, 2 ); + add_filter( 'woocommerce_rest_prepare_product', array( $this, 'rest_api_prepare_brands_to_product' ), 10, 2 ); // WC 2.6.x. + add_filter( 'woocommerce_rest_prepare_product_object', array( $this, 'rest_api_prepare_brands_to_product' ), 10, 2 ); // WC 3.x. + add_action( 'woocommerce_rest_insert_product', array( $this, 'rest_api_add_brands_to_product' ), 10, 3 ); // WC 2.6.x. + add_action( 'woocommerce_rest_insert_product_object', array( $this, 'rest_api_add_brands_to_product' ), 10, 3 ); // WC 3.x. + add_filter( 'woocommerce_rest_product_object_query', array( $this, 'rest_api_filter_products_by_brand' ), 10, 2 ); + add_filter( 'rest_product_collection_params', array( $this, 'rest_api_product_collection_params' ), 10, 2 ); + + // Layered nav widget compatibility. + add_filter( 'woocommerce_layered_nav_term_html', array( $this, 'woocommerce_brands_update_layered_nav_link' ), 10, 4 ); + + // Filter the list of taxonomies overridden for the original term count. + add_filter( 'woocommerce_change_term_counts', array( $this, 'add_brands_to_terms' ) ); + add_action( 'woocommerce_product_set_stock_status', array( $this, 'recount_after_stock_change' ) ); + add_action( 'woocommerce_update_options_products_inventory', array( $this, 'recount_all_brands' ) ); + + // Product Editor compatibility. + add_action( 'woocommerce_layout_template_after_instantiation', array( $this, 'wc_brands_on_block_template_register' ), 10, 3 ); + } + + /** + * Add product_brand to the taxonomies overridden for the original term count. + * + * @param array $taxonomies List of taxonomies. + * + * @return array + */ + public function add_brands_to_terms( $taxonomies ) { + $taxonomies[] = 'product_brand'; + return $taxonomies; + } + + /** + * Recount the brands after the stock amount changes. + * + * @param int $product_id Product ID. + */ + public function recount_after_stock_change( $product_id ) { + if ( 'yes' !== get_option( 'woocommerce_hide_out_of_stock_items' ) || empty( $product_id ) ) { + return; + } + + $product_terms = get_the_terms( $product_id, 'product_brand' ); + + if ( $product_terms ) { + $product_brands = array(); + + foreach ( $product_terms as $term ) { + $product_brands[ $term->term_id ] = $term->parent; + } + + _wc_term_recount( $product_brands, get_taxonomy( 'product_brand' ), false, false ); + } + } + + /** + * Recount all brands. + */ + public function recount_all_brands() { + $product_brands = get_terms( + array( + 'taxonomy' => 'product_brand', + 'hide_empty' => false, + 'fields' => 'id=>parent', + ) + ); + _wc_term_recount( $product_brands, get_taxonomy( 'product_brand' ), true, false ); + } + + /** + * Update the main product fetch query to filter by selected brands. + * + * @param array $tax_query array of current taxonomy filters. + * + * @return array + */ + public function update_product_query_tax_query( array $tax_query ) { + if ( isset( $_GET['filter_product_brand'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $filter_product_brand = wc_clean( wp_unslash( $_GET['filter_product_brand'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $brands_filter = array_filter( array_map( 'absint', explode( ',', $filter_product_brand ) ) ); + + if ( $brands_filter ) { + $tax_query[] = array( + 'taxonomy' => 'product_brand', + 'terms' => $brands_filter, + 'operator' => 'IN', + ); + } + } + + return $tax_query; + } + + /** + * Filter to allow product_brand in the permalinks for products. + * + * @param string $permalink The existing permalink URL. + * @param WP_Post $post The post. + * @return string + */ + public function post_type_link( $permalink, $post ) { + // Abort if post is not a product. + if ( 'product' !== $post->post_type ) { + return $permalink; + } + + // Abort early if the placeholder rewrite tag isn't in the generated URL. + if ( false === strpos( $permalink, '%' ) ) { + return $permalink; + } + + // Get the custom taxonomy terms in use by this post. + $terms = get_the_terms( $post->ID, 'product_brand' ); + + if ( empty( $terms ) ) { + // If no terms are assigned to this post, use a string instead (can't leave the placeholder there). + $product_brand = _x( 'uncategorized', 'slug', 'woocommerce' ); + } else { + // Replace the placeholder rewrite tag with the first term's slug. + $first_term = array_shift( $terms ); + $product_brand = $first_term->slug; + } + + $find = array( + '%product_brand%', + ); + + $replace = array( + $product_brand, + ); + + $replace = array_map( 'sanitize_title', $replace ); + + $permalink = str_replace( $find, $replace, $permalink ); + + return $permalink; + } + + /** + * Adds filter for introducing CSS classes. + */ + public function body_class() { + if ( is_tax( 'product_brand' ) ) { + add_filter( 'body_class', array( $this, 'add_body_class' ) ); + } + } + + /** + * Adds classes to brand taxonomy pages. + * + * @param array $classes Classes array. + */ + public function add_body_class( $classes ) { + $classes[] = 'woocommerce'; + $classes[] = 'woocommerce-page'; + return $classes; + } + + /** + * Enqueues styles. + */ + public function styles() { + $version = Constants::get_constant( 'WC_VERSION' ); + wp_enqueue_style( 'brands-styles', WC()->plugin_url() . '/assets/css/brands.css', array(), $version ); + } + + /** + * Initializes brand taxonomy. + */ + public static function init_taxonomy() { + $shop_page_id = wc_get_page_id( 'shop' ); + + $base_slug = $shop_page_id > 0 && get_page( $shop_page_id ) ? get_page_uri( $shop_page_id ) : 'shop'; + $category_base = get_option( 'woocommerce_prepend_shop_page_to_urls' ) === 'yes' ? trailingslashit( $base_slug ) : ''; + + $slug = $category_base . __( 'brand', 'woocommerce' ); + if ( '' === $category_base ) { + $slug = get_option( 'woocommerce_brand_permalink', '' ); + } + + // Can't provide transatable string as get_option default. + if ( '' === $slug ) { + $slug = __( 'brand', 'woocommerce' ); + } + + register_taxonomy( + 'product_brand', + array( 'product' ), + /** + * Filter the brand taxonomy. + * + * @since 9.4.0 + * + * @param array $args Args. + */ + apply_filters( + 'register_taxonomy_product_brand', + array( + 'hierarchical' => true, + 'update_count_callback' => '_update_post_term_count', + 'label' => __( 'Brands', 'woocommerce' ), + 'labels' => array( + 'name' => __( 'Brands', 'woocommerce' ), + 'singular_name' => __( 'Brand', 'woocommerce' ), + 'search_items' => __( 'Search Brands', 'woocommerce' ), + 'all_items' => __( 'All Brands', 'woocommerce' ), + 'parent_item' => __( 'Parent Brand', 'woocommerce' ), + 'parent_item_colon' => __( 'Parent Brand:', 'woocommerce' ), + 'edit_item' => __( 'Edit Brand', 'woocommerce' ), + 'update_item' => __( 'Update Brand', 'woocommerce' ), + 'add_new_item' => __( 'Add New Brand', 'woocommerce' ), + 'new_item_name' => __( 'New Brand Name', 'woocommerce' ), + 'not_found' => __( 'No Brands Found', 'woocommerce' ), + 'back_to_items' => __( '← Go to Brands', 'woocommerce' ), + ), + + 'show_ui' => true, + 'show_admin_column' => true, + 'show_in_nav_menus' => true, + 'show_in_rest' => true, + 'capabilities' => array( + 'manage_terms' => 'manage_product_terms', + 'edit_terms' => 'edit_product_terms', + 'delete_terms' => 'delete_product_terms', + 'assign_terms' => 'assign_product_terms', + ), + + 'rewrite' => array( + 'slug' => $slug, + 'with_front' => false, + 'hierarchical' => true, + ), + ) + ) + ); + } + + /** + * Initializes brand widgets. + */ + public function init_widgets() { + // Include. + require_once WC()->plugin_path() . '/includes/widgets/class-wc-widget-brand-description.php'; + require_once WC()->plugin_path() . '/includes/widgets/class-wc-widget-brand-nav.php'; + require_once WC()->plugin_path() . '/includes/widgets/class-wc-widget-brand-thumbnails.php'; + + // Register. + register_widget( 'WC_Widget_Brand_Description' ); + register_widget( 'WC_Widget_Brand_Nav' ); + register_widget( 'WC_Widget_Brand_Thumbnails' ); + } + + /** + * + * Handles template usage so that we can use our own templates instead of the themes. + * + * Templates are in the 'templates' folder. woocommerce looks for theme + * overides in /theme/woocommerce/ by default + * + * For beginners, it also looks for a woocommerce.php template first. If the user adds + * this to the theme (containing a woocommerce() inside) this will be used for all + * woocommerce templates. + * + * @param string $template Template. + */ + public function template_loader( $template ) { + $find = array( 'woocommerce.php' ); + $file = ''; + + if ( is_tax( 'product_brand' ) ) { + + $term = get_queried_object(); + + $file = 'taxonomy-' . $term->taxonomy . '.php'; + $find[] = 'taxonomy-' . $term->taxonomy . '-' . $term->slug . '.php'; + $find[] = $this->template_url . 'taxonomy-' . $term->taxonomy . '-' . $term->slug . '.php'; + $find[] = $file; + $find[] = $this->template_url . $file; + + } + + if ( $file ) { + $template = locate_template( $find ); + if ( ! $template ) { + $template = WC()->plugin_path() . '/templates/brands/' . $file; + } + } + + return $template; + } + + /** + * Displays brand description. + */ + public function brand_description() { + if ( ! is_tax( 'product_brand' ) ) { + return; + } + + if ( ! get_query_var( 'term' ) ) { + return; + } + + $thumbnail = ''; + + $term = get_term_by( 'slug', get_query_var( 'term' ), 'product_brand' ); + $thumbnail = wc_get_brand_thumbnail_url( $term->term_id, 'full' ); + + wc_get_template( + 'brand-description.php', + array( + 'thumbnail' => $thumbnail, + ), + 'woocommerce', + WC()->plugin_path() . '/templates/brands/' + ); + } + + /** + * Displays brand. + */ + public function show_brand() { + global $post; + + if ( is_singular( 'product' ) ) { + $terms = get_the_terms( $post->ID, 'product_brand' ); + $brand_count = is_array( $terms ) ? count( $terms ) : 0; + + $taxonomy = get_taxonomy( 'product_brand' ); + $labels = $taxonomy->labels; + + /* translators: %s - Label name */ + echo wc_get_brands( $post->ID, ', ', ' ' . sprintf( _n( '%s: ', '%s: ', $brand_count, 'woocommerce' ), $labels->singular_name, $labels->name ), '' ); // phpcs:ignore WordPress.Security.EscapeOutput + } + } + + /** + * Add structured data to product page. + * + * @param array $markup Markup. + * @return array $markup + */ + public function add_structured_data( $markup ) { + global $post; + + if ( array_key_exists( 'brand', $markup ) ) { + return $markup; + } + + $brands = get_the_terms( $post->ID, 'product_brand' ); + + if ( ! empty( $brands ) && is_array( $brands ) ) { + // Can only return one brand, so pick the first. + $markup['brand'] = array( + '@type' => 'Brand', + 'name' => $brands[0]->name, + ); + } + + return $markup; + } + + /** + * Registers shortcodes. + */ + public function register_shortcodes() { + add_shortcode( 'product_brand', array( $this, 'output_product_brand' ) ); + add_shortcode( 'product_brand_thumbnails', array( $this, 'output_product_brand_thumbnails' ) ); + add_shortcode( 'product_brand_thumbnails_description', array( $this, 'output_product_brand_thumbnails_description' ) ); + add_shortcode( 'product_brand_list', array( $this, 'output_product_brand_list' ) ); + add_shortcode( 'brand_products', array( $this, 'output_brand_products' ) ); + } + + /** + * Displays product brand. + * + * @param array $atts Attributes from the shortcode. + * @return string The generated output. + */ + public function output_product_brand( $atts ) { + global $post; + + $args = shortcode_atts( + array( + 'width' => '', + 'height' => '', + 'class' => 'aligncenter', + 'post_id' => '', + ), + $atts + ); + + if ( ! $args['post_id'] && ! $post ) { + return ''; + } + + if ( ! $args['post_id'] ) { + $args['post_id'] = $post->ID; + } + + $brands = wp_get_post_terms( $args['post_id'], 'product_brand', array( 'fields' => 'ids' ) ); + + // Bail early if we don't have any brands registered. + if ( 0 === count( $brands ) ) { + return ''; + } + + ob_start(); + + foreach ( $brands as $brand ) { + $thumbnail = wc_get_brand_thumbnail_url( $brand ); + if ( empty( $thumbnail ) ) { + continue; + } + + $args['thumbnail'] = $thumbnail; + $args['term'] = get_term_by( 'id', $brand, 'product_brand' ); + + if ( $args['width'] || $args['height'] ) { + $args['width'] = ! empty( $args['width'] ) ? $args['width'] : 'auto'; + $args['height'] = ! empty( $args['height'] ) ? $args['height'] : 'auto'; + } + + wc_get_template( + 'shortcodes/single-brand.php', + $args, + 'woocommerce', + WC()->plugin_path() . '/templates/brands/' + ); + } + + return ob_get_clean(); + } + + /** + * Displays product brand list. + * + * @param array $atts Attributes from the shortcode. + * @return string + */ + public function output_product_brand_list( $atts ) { + $args = shortcode_atts( + array( + 'show_top_links' => true, + 'show_empty' => true, + 'show_empty_brands' => false, + ), + $atts + ); + + $show_top_links = $args['show_top_links']; + $show_empty = $args['show_empty']; + $show_empty_brands = $args['show_empty_brands']; + + if ( 'false' === $show_top_links ) { + $show_top_links = false; + } + + if ( 'false' === $show_empty ) { + $show_empty = false; + } + + if ( 'false' === $show_empty_brands ) { + $show_empty_brands = false; + } + + $product_brands = array(); + //phpcs:disable + $terms = get_terms( array( 'taxonomy' => 'product_brand', 'hide_empty' => ( $show_empty_brands ? false : true ) ) ); + $alphabet = apply_filters( 'woocommerce_brands_list_alphabet', range( 'a', 'z' ) ); + $numbers = apply_filters( 'woocommerce_brands_list_numbers', '0-9' ); + + /** + * Check for empty brands and remove them from the list. + */ + if ( ! $show_empty_brands ) { + $terms = $this->remove_terms_with_empty_products( $terms ); + } + + foreach ( $terms as $term ) { + $term_letter = $this->get_brand_name_first_character( $term->name ); + + // Allow a locale to be set for ctype_alpha(). + if ( has_filter( 'woocommerce_brands_list_locale' ) ) { + setLocale( LC_CTYPE, apply_filters( 'woocommerce_brands_list_locale', 'en_US.UTF-8' ) ); + } + + if ( ctype_alpha( $term_letter ) ) { + + foreach ( $alphabet as $i ) { + if ( $i == $term_letter ) { + $product_brands[ $i ][] = $term; + break; + } + } + } else { + $product_brands[ $numbers ][] = $term; + } + } + + ob_start(); + + wc_get_template( + 'shortcodes/brands-a-z.php', + array( + 'terms' => $terms, + 'index' => array_merge( $alphabet, array( $numbers ) ), + 'product_brands' => $product_brands, + 'show_empty' => $show_empty, + 'show_top_links' => $show_top_links, + ), + 'woocommerce', + WC()->plugin_path() . '/templates/brands/' + ); + + return ob_get_clean(); + } + + /** + * Get the first letter of the brand name, returning lowercase and without accents. + * + * @param string $name + * + * @return string + * @since 9.4.0 + */ + private function get_brand_name_first_character( $name ) { + // Convert to lowercase and remove accents. + $clean_name = strtolower( sanitize_title( $name ) ); + // Return the first letter of the name. + return substr( $clean_name, 0, 1 ); + } + + /** + * Displays brand thumbnails. + * + * @param mixed $atts + * @return void + */ + public function output_product_brand_thumbnails( $atts ) { + $args = shortcode_atts( + array( + 'show_empty' => true, + 'columns' => 4, + 'hide_empty' => 0, + 'orderby' => 'name', + 'exclude' => '', + 'number' => '', + 'fluid_columns' => false, + ), + $atts + ); + + $exclude = array_map( 'intval', explode( ',', $args['exclude'] ) ); + $order = 'name' === $args['orderby'] ? 'asc' : 'desc'; + + if ( 'true' === $args['show_empty'] ) { + $hide_empty = false; + } else { + $hide_empty = true; + } + + $brands = get_terms( + 'product_brand', + array( + 'hide_empty' => $hide_empty, + 'orderby' => $args['orderby'], + 'exclude' => $exclude, + 'number' => $args['number'], + 'order' => $order, + ) + ); + + if ( ! $brands ) { + return; + } + + if ( $hide_empty ) { + $brands = $this->remove_terms_with_empty_products( $brands ); + } + + ob_start(); + + wc_get_template( + 'widgets/brand-thumbnails.php', + array( + 'brands' => $brands, + 'columns' => is_numeric( $args['columns'] ) ? intval( $args['columns'] ) : 4, + 'fluid_columns' => wp_validate_boolean( $args['fluid_columns'] ), + ), + 'woocommerce', + WC()->plugin_path() . '/templates/brands/' + ); + + return ob_get_clean(); + } + + /** + * Displays brand thumbnails description. + * + * @param mixed $atts + * @return void + */ + public function output_product_brand_thumbnails_description( $atts ) { + $args = shortcode_atts( + array( + 'show_empty' => true, + 'columns' => 1, + 'hide_empty' => 0, + 'orderby' => 'name', + 'exclude' => '', + 'number' => '', + ), + $atts + ); + + $exclude = array_map( 'intval', explode( ',', $args['exclude'] ) ); + $order = 'name' === $args['orderby'] ? 'asc' : 'desc'; + + if ( 'true' === $args['show_empty'] ) { + $hide_empty = false; + } else { + $hide_empty = true; + } + + $brands = get_terms( + 'product_brand', + array( + 'hide_empty' => $args['hide_empty'], + 'orderby' => $args['orderby'], + 'exclude' => $exclude, + 'number' => $args['number'], + 'order' => $order, + ) + ); + + if ( ! $brands ) { + return; + } + + if ( $hide_empty ) { + $brands = $this->remove_terms_with_empty_products( $brands ); + } + + ob_start(); + + wc_get_template( + 'widgets/brand-thumbnails-description.php', + array( + 'brands' => $brands, + 'columns' => $args['columns'], + ), + 'woocommerce', + WC()->plugin_path() . '/templates/brands/' + ); + + return ob_get_clean(); + } + + /** + * Displays brand products. + * + * @param array $atts + * @return string + */ + public function output_brand_products( $atts ) { + if ( empty( $atts['brand'] ) ) { + return ''; + } + + // Add the brand attributes and query arguments. + add_filter( 'shortcode_atts_brand_products', array( __CLASS__, 'add_brand_products_shortcode_atts' ), 10, 4 ); + add_filter( 'woocommerce_shortcode_products_query', array( __CLASS__, 'get_brand_products_query_args' ), 10, 3 ); + + $shortcode = new WC_Shortcode_Products( $atts, 'brand_products' ); + + // Remove the brand attributes and query arguments. + remove_filter( 'shortcode_atts_brand_products', array( __CLASS__, 'add_brand_products_shortcode_atts' ), 10 ); + remove_filter( 'woocommerce_shortcode_products_query', array( __CLASS__, 'get_brand_products_query_args' ), 10 ); + + return $shortcode->get_content(); + } + + /** + * Adds the taxonomy query to the WooCommerce products shortcode query arguments. + * + * @param array $query_args + * @param array $attributes + * @param string $type + * + * @return array + */ + public static function get_brand_products_query_args( $query_args, $attributes, $type ) { + if ( 'brand_products' !== $type || empty( $attributes['brand'] ) ) { + return $query_args; + } + + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_brand', + 'terms' => array_map( 'sanitize_title', explode( ',', $attributes['brand'] ) ), + 'field' => 'slug', + 'operator' => 'IN', + ); + + return $query_args; + } + + /** + * Adds the "brand" attribute to the list of WooCommerce products shortcode attributes. + * + * @param array $out The output array of shortcode attributes. + * @param array $pairs The supported attributes and their defaults. + * @param array $atts The user defined shortcode attributes. + * @param string $shortcode The shortcode name. + * + * @return array The output array of shortcode attributes. + */ + public static function add_brand_products_shortcode_atts( $out, $pairs, $atts, $shortcode ) { + $out['brand'] = array_key_exists( 'brand', $atts ) ? $atts['brand'] : ''; + + return $out; + } + + /** + * Register REST API route for /products/brands. + * + * @since 9.4.0 + * + * @return void + */ + public function rest_api_register_routes() { + require_once WC()->plugin_path() . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-brands-v2-controller.php'; + require_once WC()->plugin_path() . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-brands-controller.php'; + + $controllers = array( + 'WC_REST_Product_Brands_V2_Controller', + 'WC_REST_Product_Brands_Controller' + ); + + foreach ( $controllers as $controller ) { + ( new $controller() )->register_routes(); + } + } + + /** + * Maybe set brands when requesting PUT /products/. + * + * @since 9.4.0 + * + * @param WP_Post $post Post object + * @param WP_REST_Request $request Request object + * + * @return void + */ + public function rest_api_maybe_set_brands( $post, $request ) { + if ( isset( $request['brands'] ) && is_array( $request['brands'] ) ) { + $terms = array_map( 'absint', $request['brands'] ); + wp_set_object_terms( $post->ID, $terms, 'product_brand' ); + } + } + + /** + * Prepare brands in product response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post|WC_Data $post Post object or WC object. + * @version 9.4.0 + * @return WP_REST_Response + */ + public function rest_api_prepare_brands_to_product( $response, $post ) { + $post_id = is_callable( array( $post, 'get_id' ) ) ? $post->get_id() : ( ! empty( $post->ID ) ? $post->ID : null ); + + if ( empty( $response->data['brands'] ) ) { + $terms = array(); + + foreach ( wp_get_post_terms( $post_id, 'product_brand' ) as $term ) { + $terms[] = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + ); + } + + $response->data['brands'] = $terms; + } + + return $response; + } + + /** + * Add brands in product response. + * + * @param WC_Data $product Inserted product object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating object, false when updating. + * @version 9.4.0 + */ + public function rest_api_add_brands_to_product( $product, $request, $creating = true ) { + $product_id = is_callable( array( $product, 'get_id' ) ) ? $product->get_id() : ( ! empty( $product->ID ) ? $product->ID : null ); + $params = $request->get_params(); + $brands = isset( $params['brands'] ) ? $params['brands'] : array(); + + if ( ! empty( $brands ) ) { + if ( is_array( $brands[0] ) && array_key_exists( 'id', $brands[0] ) ) { + $brands = array_map( + function ( $brand ) { + return absint( $brand['id'] ); + }, + $brands + ); + } else { + $brands = array_map( 'absint', $brands ); + } + wp_set_object_terms( $product_id, $brands, 'product_brand' ); + } + } + + /** + * Filters products by taxonomy product_brand. + * + * @param array $args Request args. + * @param WP_REST_Request $request Request data. + * @return array Request args. + * @version 9.4.0 + */ + public function rest_api_filter_products_by_brand( $args, $request ) { + if ( ! empty( $request['brand'] ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'product_brand', + 'field' => 'term_id', + 'terms' => $request['brand'], + ); + } + + return $args; + } + + /** + * Documents additional query params for collections of products. + * + * @param array $params JSON Schema-formatted collection parameters. + * @param WP_Post_Type $post_type Post type object. + * @return array JSON Schema-formatted collection parameters. + * @version 9.4.0 + */ + public function rest_api_product_collection_params( $params, $post_type ) { + $params['brand'] = array( + 'description' => __( 'Limit result set to products assigned a specific brand ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } + + /** + * Injects Brands filters into layered nav links. + * + * @param string $term_html Original link html. + * @param mixed $term Term that is currently added. + * @param string $link Original layered nav item link. + * @param number $count Number of items in that filter. + * @return string Term html. + * @version 9.4.0 + */ + public function woocommerce_brands_update_layered_nav_link( $term_html, $term, $link, $count ) { + if ( empty( $_GET['filter_product_brand'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return $term_html; + } + + $filter_product_brand = wc_clean( wp_unslash( $_GET['filter_product_brand'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $current_attributes = array_map( 'intval', explode( ',', $filter_product_brand ) ); + $current_values = ! empty( $current_attributes ) ? $current_attributes : array(); + $link = add_query_arg( + array( + 'filtering' => '1', + 'filter_product_brand' => implode( ',', $current_values ), + ), + wp_specialchars_decode( $link ) + ); + $term_html = '' . esc_html( $term->name ) . ''; + $term_html .= ' ' . apply_filters( 'woocommerce_layered_nav_count', '(' . absint( $count ) . ')', $count, $term ); + return $term_html; + } + + /** + * Temporarily tag a post with meta before it is saved in order + * to allow us to be able to use the meta when the product is saved to add + * the brands when an ID has been generated. + * + * + * @param WC_Product $duplicate + * @return WC_Product $original + */ + public function duplicate_store_temporary_brands( $duplicate, $original ) { + $terms = get_the_terms( $original->get_id(), 'product_brand' ); + if ( ! is_array( $terms ) ) { + return; + } + + $ids = array(); + foreach ( $terms as $term ) { + $ids[] = $term->term_id; + } + $duplicate->add_meta_data( 'duplicate_temp_brand_ids', $ids ); + } + + /** + * After product was added check if there are temporary brands and + * add them officially and remove the temporary brands. + * + * @since 9.4.0 + * + * @param int $product_id + */ + public function duplicate_add_product_brand_terms( $product_id ) { + $product = wc_get_product( $product_id ); + // Bail if product isn't found. + if ( ! $product instanceof WC_Product ) { + return; + } + $term_ids = $product->get_meta( 'duplicate_temp_brand_ids' ); + if ( empty( $term_ids ) ) { + return; + } + $term_taxonomy_ids = wp_set_object_terms( $product_id, $term_ids, 'product_brand' ); + $product->delete_meta_data( 'duplicate_temp_brand_ids' ); + $product->save(); + } + + /** + * Remove terms with empty products. + * + * @param WP_Term[] $terms The terms array that needs to be removed of empty products. + * + * @return WP_Term[] + */ + private function remove_terms_with_empty_products( $terms ) { + return array_filter( + $terms, + function ( $term ) { + return $term->count > 0; + } + ); + } + + /** + * Invalidates the layered nav counts cache. + * + * @return void + */ + public function invalidate_wc_layered_nav_counts_cache() { + $taxonomy = 'product_brand'; + delete_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ) ); + } + + /** + * Reset Layered Nav cached counts on product status change. + * + * @param $new_status + * @param $old_status + * @param $post + * + * @return void + */ + function reset_layered_nav_counts_on_status_change( $new_status, $old_status, $post ) { + if ( $post->post_type === 'product' && $old_status !== $new_status ) { + $this->invalidate_wc_layered_nav_counts_cache(); + } + } + + /** + * Add a new block to the template. + * + * @param string $template_id Template ID. + * @param string $template_area Template area. + * @param BlockTemplateInterface $template Template instance. + */ + public function wc_brands_on_block_template_register( $template_id, $template_area, $template ) { + + if ( 'simple-product' === $template->get_id() ) { + $section = $template->get_section_by_id( 'product-catalog-section' ); + if ( $section !== null ) { + $section->add_block( + array( + 'id' => 'woocommerce-brands-select', + 'blockName' => 'woocommerce/product-taxonomy-field', + 'order' => 15, + 'attributes' => array( + 'label' => __( 'Brands', 'woocommerce-brands' ), + 'createTitle' => __( 'Create new brand', 'woocommerce-brands' ), + 'slug' => 'product_brand', + 'property' => 'brands', + ), + ) + ); + } + } + } +} + +$GLOBALS['WC_Brands'] = new WC_Brands(); diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php index 002844d49c4..ec17ff0c71c 100644 --- a/plugins/woocommerce/includes/class-wc-install.php +++ b/plugins/woocommerce/includes/class-wc-install.php @@ -268,6 +268,7 @@ class WC_Install { ), '9.4.0' => array( 'wc_update_940_add_phone_to_order_address_fts_index', + 'wc_update_940_remove_help_panel_highlight_shown', ), ); diff --git a/plugins/woocommerce/includes/class-wc-structured-data.php b/plugins/woocommerce/includes/class-wc-structured-data.php index 98acb513224..12f1861ad9e 100644 --- a/plugins/woocommerce/includes/class-wc-structured-data.php +++ b/plugins/woocommerce/includes/class-wc-structured-data.php @@ -214,9 +214,9 @@ class WC_Structured_Data { $markup['sku'] = $product->get_id(); } - // Add GTIN only if it's a valid number. - $gtin = $product->get_global_unique_id(); - if ( $gtin && is_numeric( $gtin ) ) { + // Prepare GTIN and load it if it's valid. + $gtin = $this->prepare_gtin( $product->get_global_unique_id() ); + if ( $this->is_valid_gtin( $gtin ) ) { $markup['gtin'] = $gtin; } @@ -577,4 +577,30 @@ class WC_Structured_Data { $this->set_data( apply_filters( 'woocommerce_structured_data_order', $markup, $sent_to_admin, $order ), true ); } + + /** + * Check if a GTIN is valid. + * A valid GTIN is a string containing 8,12,13 or 14 digits. + * + * @see https://schema.org/gtin + * @param string $gtin The GTIN to check. + * @return bool True if valid. False otherwise. + */ + public function is_valid_gtin( $gtin ) { + return is_string( $gtin ) && preg_match( '/^(\d{8}|\d{12,14})$/', $gtin ); + } + + /** + * Prepare a GTIN input removing everything except numbers. + * + * @param string $gtin The GTIN to prepare. + * @return string Empty string if no GTIN is provided or the string with the replacements. + */ + public function prepare_gtin( $gtin ) { + if ( ! $gtin || ! is_string( $gtin ) ) { + return ''; + } + + return preg_replace( '/[^0-9]/', '', $gtin ); + } } diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php index 41104fcbd65..85ce78999c1 100644 --- a/plugins/woocommerce/includes/class-woocommerce.php +++ b/plugins/woocommerce/includes/class-woocommerce.php @@ -27,7 +27,6 @@ use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use Automattic\WooCommerce\Internal\Utilities\LegacyRestApiStub; use Automattic\WooCommerce\Internal\Utilities\WebhookUtil; use Automattic\WooCommerce\Internal\Admin\Marketplace; -use Automattic\WooCommerce\Internal\McStats; use Automattic\WooCommerce\Proxies\LegacyProxy; use Automattic\WooCommerce\Utilities\{LoggingUtil, RestApiUtil, TimeUtil}; use Automattic\WooCommerce\Internal\Logging\RemoteLogger; @@ -384,8 +383,10 @@ final class WooCommerce { unset( $error_copy['message'] ); $context = array( - 'source' => 'fatal-errors', - 'error' => $error_copy, + 'source' => 'fatal-errors', + 'error' => $error_copy, + // Indicate that this error should be logged remotely if remote logging is enabled. + 'remote-logging' => true, ); if ( false !== strpos( $message, 'Stack trace:' ) ) { @@ -407,12 +408,6 @@ final class WooCommerce { $context ); - // Record fatal error stats. - $container = wc_get_container(); - $mc_stats = $container->get( McStats::class ); - $mc_stats->add( 'error', 'fatal-errors-during-shutdown' ); - $mc_stats->do_server_side_stats(); - /** * Action triggered when there are errors during shutdown. * diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-brands-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-brands-v2-controller.php new file mode 100644 index 00000000000..dc66380c2cd --- /dev/null +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-product-brands-v2-controller.php @@ -0,0 +1,40 @@ +term_id, 'thumbnail_id', true ); + + if ( '' === $size || 'brand-thumb' === $size ) { + /** + * Filter the brand's thumbnail size. + * + * @since 9.4.0 + * + * @param string $size Brand's thumbnail size. + */ + $size = apply_filters( 'woocommerce_brand_thumbnail_size', 'shop_catalog' ); + } + + if ( $thumbnail_id ) { + $image_src = wp_get_attachment_image_src( $thumbnail_id, $size ); + $image_src = $image_src[0]; + $dimensions = wc_get_image_size( $size ); + $image_srcset = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $thumbnail_id, $size ) : false; + $image_sizes = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $thumbnail_id, $size ) : false; + } else { + $image_src = wc_placeholder_img_src(); + $dimensions = wc_get_image_size( $size ); + $image_srcset = false; + $image_sizes = false; + } + + // Add responsive image markup if available. + if ( $image_srcset && $image_sizes ) { + $image = '' . esc_attr( $brand->name ) . ''; + } else { + $image = '' . esc_attr( $brand->name ) . ''; + } + + return $image; +} + +/** + * Retrieves product's brands. + * + * @param int $post_id Post ID (default: 0). + * @param string $sep Seperator (default: '). + * @param string $before Before item (default: ''). + * @param string $after After item (default: ''). + * @return array List of terms + */ +function wc_get_brands( $post_id = 0, $sep = ', ', $before = '', $after = '' ) { + global $post; + + if ( ! $post_id ) { + $post_id = $post->ID; + } + + return get_the_term_list( $post_id, 'product_brand', $before, $sep, $after ); +} + +/** + * Polyfills for backwards compatibility with the WooCommerce Brands plugin. + */ + +if ( ! function_exists( 'get_brand_thumbnail_url' ) ) { + + /** + * Polyfill for get_brand_thumbnail_image. + * + * @param int $brand_id Brand ID. + * @param string $size Thumbnail image size. + * @return string + */ + function get_brand_thumbnail_url( $brand_id, $size = 'full' ) { + return wc_get_brand_thumbnail_url( $brand_id, $size ); + } +} + +if ( ! function_exists( 'get_brand_thumbnail_image' ) ) { + + /** + * Polyfill for get_brand_thumbnail_image. + * + * @param object $brand Brand term. + * @param string $size Thumbnail image size. + * @return string + */ + function get_brand_thumbnail_image( $brand, $size = '' ) { + return wc_get_brand_thumbnail_image( $brand, $size ); + } +} + +if ( ! function_exists( 'get_brands' ) ) { + + /** + * Polyfill for get_brands. + * + * @param int $post_id Post ID (default: 0). + * @param string $sep Seperator (default: '). + * @param string $before Before item (default: ''). + * @param string $after After item (default: ''). + * @return array List of terms + */ + function get_brands( $post_id = 0, $sep = ', ', $before = '', $after = '' ) { + return wc_get_brands( $post_id, $sep, $before, $after ); + } +} diff --git a/plugins/woocommerce/includes/wc-core-functions.php b/plugins/woocommerce/includes/wc-core-functions.php index 4158fd900e0..34d4781f00c 100644 --- a/plugins/woocommerce/includes/wc-core-functions.php +++ b/plugins/woocommerce/includes/wc-core-functions.php @@ -1484,7 +1484,11 @@ function wc_transaction_query( $type = 'start', $force = false ) { * @return string Url to cart page */ function wc_get_cart_url() { - if ( is_cart() && isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) { + // We don't use is_cart() here because that also checks for a defined constant. We are only interested in the page. + $page_id = wc_get_page_id( 'cart' ); + $is_cart_page = ( $page_id && is_page( $page_id ) ) || wc_post_content_has_shortcode( 'woocommerce_cart' ); + + if ( $is_cart_page && isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) { $protocol = is_ssl() ? 'https' : 'http'; $current_url = esc_url_raw( $protocol . '://' . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ) ); $cart_url = remove_query_arg( array( 'remove_item', 'add-to-cart', 'added-to-cart', 'order_again', '_wpnonce' ), $current_url ); diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php index e4f880a2a76..79c42ac89fe 100644 --- a/plugins/woocommerce/includes/wc-update-functions.php +++ b/plugins/woocommerce/includes/wc-update-functions.php @@ -2872,3 +2872,51 @@ function wc_update_940_add_phone_to_order_address_fts_index(): void { } } } + +/** + * Remove user meta associated with the key 'woocommerce_admin_help_panel_highlight_shown'. + * + * This key is no longer needed since the help panel spotlight tour has been removed. + * + * @return void + */ +function wc_update_940_remove_help_panel_highlight_shown() { + global $wpdb; + + $meta_key = 'woocommerce_admin_help_panel_highlight_shown'; + + $deletions = $wpdb->query( + $wpdb->prepare( + "DELETE FROM $wpdb->usermeta WHERE meta_key = %s", + $meta_key + ) + ); + + // Get the WooCommerce logger to track the results of the deletion. + $logger = wc_get_logger(); + + if ( null === $logger ) { + return; + } + + if ( false === $deletions ) { + $logger->notice( + 'During the update to 9.4.0, WooCommerce attempted to remove user meta with the key "woocommerce_admin_help_panel_highlight_shown", but was unable to do so.', + array( + 'source' => 'wc-updater', + ) + ); + } else { + $logger->info( + sprintf( + 1 === $deletions + ? 'During the update to 9.4.0, WooCommerce removed %d user meta row associated with the meta key "woocommerce_admin_help_panel_highlight_shown".' + : 'During the update to 9.4.0, WooCommerce removed %d user meta rows associated with the meta key "woocommerce_admin_help_panel_highlight_shown".', + number_format_i18n( $deletions ) + ), + array( + 'source' => 'wc-updater', + ) + ); + } +} diff --git a/plugins/woocommerce/includes/widgets/class-wc-widget-brand-description.php b/plugins/woocommerce/includes/widgets/class-wc-widget-brand-description.php new file mode 100644 index 00000000000..6f117274f5c --- /dev/null +++ b/plugins/woocommerce/includes/widgets/class-wc-widget-brand-description.php @@ -0,0 +1,130 @@ +woo_widget_name = __( 'WooCommerce Brand Description', 'woocommerce' ); + $this->woo_widget_description = __( 'When viewing a brand archive, show the current brands description.', 'woocommerce' ); + $this->woo_widget_idbase = 'wc_brands_brand_description'; + $this->woo_widget_cssclass = 'widget_brand_description'; + + /* Widget settings. */ + $widget_ops = array( + 'classname' => $this->woo_widget_cssclass, + 'description' => $this->woo_widget_description, + ); + + /* Create the widget. */ + parent::__construct( $this->woo_widget_idbase, $this->woo_widget_name, $widget_ops ); + } + + /** + * Echoes the widget content. + * + * @see WP_Widget + * + * @param array $args Display arguments including 'before_title', 'after_title', + * 'before_widget', and 'after_widget'. + * @param array $instance The settings for the particular instance of the widget. + */ + public function widget( $args, $instance ) { + extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract + + if ( ! is_tax( 'product_brand' ) ) { + return; + } + + if ( ! get_query_var( 'term' ) ) { + return; + } + + $thumbnail = ''; + $term = get_term_by( 'slug', get_query_var( 'term' ), 'product_brand' ); + + $thumbnail = wc_get_brand_thumbnail_url( $term->term_id, 'large' ); + + echo $before_widget . $before_title . $term->name . $after_title; // phpcs:ignore WordPress.Security.EscapeOutput + + wc_get_template( + 'widgets/brand-description.php', + array( + 'thumbnail' => $thumbnail, + 'brand' => $term, + ), + 'woocommerce', + WC()->plugin_path() . '/templates/brands/' + ); + + echo $after_widget; // phpcs:ignore WordPress.Security.EscapeOutput + } + + /** + * Updates widget instance. + * + * @see WP_Widget->update + * + * @param array $new_instance New widget instance. + * @param array $old_instance Old widget instance. + */ + public function update( $new_instance, $old_instance ) { + $instance['title'] = wp_strip_all_tags( stripslashes( $new_instance['title'] ) ); + return $instance; + } + + /** + * Outputs the settings update form. + * + * @param array $instance Current settings. + */ + public function form( $instance ) { + ?> +

    + + +

    + widget_cssclass = 'woocommerce widget_brand_nav widget_layered_nav'; + $this->widget_description = __( 'Shows brands in a widget which lets you narrow down the list of products when viewing products.', 'woocommerce' ); + $this->widget_id = 'woocommerce_brand_nav'; + $this->widget_name = __( 'WooCommerce Brand Layered Nav', 'woocommerce' ); + + add_filter( 'woocommerce_product_subcategories_args', array( $this, 'filter_out_cats' ) ); + + /* Create the widget. */ + parent::__construct(); + } + + /** + * Filter out all categories and not display them + * + * @param array $cat_args Category arguments. + */ + public function filter_out_cats( $cat_args ) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( ! empty( $_GET['filter_product_brand'] ) ) { + return array( 'taxonomy' => '' ); + } + + return $cat_args; + } + + /** + * Return the currently viewed taxonomy name. + * + * @return string + */ + protected function get_current_taxonomy() { + return is_tax() ? get_queried_object()->taxonomy : ''; + } + + /** + * Return the currently viewed term ID. + * + * @return int + */ + protected function get_current_term_id() { + return absint( is_tax() ? get_queried_object()->term_id : 0 ); + } + + /** + * Return the currently viewed term slug. + * + * @return int + */ + protected function get_current_term_slug() { + return absint( is_tax() ? get_queried_object()->slug : 0 ); + } + + /** + * Widget function. + * + * @see WP_Widget + * + * @param array $args Arguments. + * @param array $instance Widget instance. + * @return void + */ + public function widget( $args, $instance ) { + $attribute_array = array(); + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + if ( ! empty( $attribute_taxonomies ) ) { + foreach ( $attribute_taxonomies as $tax ) { + if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) { + $attribute_array[ $tax->attribute_name ] = $tax->attribute_name; + } + } + } + + if ( ! is_post_type_archive( 'product' ) && ! is_tax( array_merge( is_array( $attribute_array ) ? $attribute_array : array(), array( 'product_cat', 'product_tag' ) ) ) ) { + return; + } + + $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); + + $current_term = $attribute_array && is_tax( $attribute_array ) ? get_queried_object()->term_id : ''; + $current_tax = $attribute_array && is_tax( $attribute_array ) ? get_queried_object()->taxonomy : ''; + + /** + * Filter the widget's title. + * + * @since 9.4.0 + * + * @param string $title Widget title + * @param array $instance The settings for the particular instance of the widget. + * @param string $woo_widget_idbase The widget's id base. + */ + $title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base ); + $taxonomy = 'product_brand'; + $display_type = isset( $instance['display_type'] ) ? $instance['display_type'] : 'list'; + + if ( ! taxonomy_exists( $taxonomy ) ) { + return; + } + + // Get only parent terms. Methods will recursively retrieve children. + $terms = get_terms( + array( + 'taxonomy' => $taxonomy, + 'hide_empty' => true, + 'parent' => 0, + ) + ); + + if ( empty( $terms ) ) { + return; + } + + ob_start(); + + $this->widget_start( $args, $instance ); + + if ( 'dropdown' === $display_type ) { + $found = $this->layered_nav_dropdown( $terms, $taxonomy ); + } else { + $found = $this->layered_nav_list( $terms, $taxonomy ); + } + + $this->widget_end( $args ); + + // Force found when option is selected - do not force found on taxonomy attributes. + if ( ! is_tax() && is_array( $_chosen_attributes ) && array_key_exists( $taxonomy, $_chosen_attributes ) ) { + $found = true; + } + + if ( ! $found ) { + ob_end_clean(); + } else { + echo ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput + } + } + + /** + * Update function. + * + * @see WP_Widget->update + * + * @param array $new_instance The new settings for the particular instance of the widget. + * @param array $old_instance The old settings for the particular instance of the widget. + * @return array + */ + public function update( $new_instance, $old_instance ) { + global $woocommerce; + + if ( empty( $new_instance['title'] ) ) { + $new_instance['title'] = __( 'Brands', 'woocommerce' ); + } + + $instance['title'] = wp_strip_all_tags( stripslashes( $new_instance['title'] ) ); + $instance['display_type'] = stripslashes( $new_instance['display_type'] ); + + return $instance; + } + + /** + * Form function. + * + * @see WP_Widget->form + * + * @param array $instance Widget instance. + * @return void + */ + public function form( $instance ) { + global $woocommerce; + + if ( ! isset( $instance['display_type'] ) ) { + $instance['display_type'] = 'list'; + } + ?> +

    + +

    + +

    +

    + $data ) { + if ( $name === $taxonomy ) { + continue; + } + $filter_name = sanitize_title( str_replace( 'pa_', '', $name ) ); + if ( ! empty( $data['terms'] ) ) { + $link = add_query_arg( 'filter_' . $filter_name, implode( ',', $data['terms'] ), $link ); + } + if ( 'or' === $data['query_type'] ) { + $link = add_query_arg( 'query_type_' . $filter_name, 'or', $link ); + } + } + } + + // phpcs:enable WordPress.Security.NonceVerification.Recommended + return esc_url( $link ); + } + + /** + * Gets the currently selected attributes + * + * @return array + */ + public function get_chosen_attributes() { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( ! empty( $_GET['filter_product_brand'] ) ) { + $filter_product_brand = wc_clean( wp_unslash( $_GET['filter_product_brand'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return array_map( 'intval', explode( ',', $filter_product_brand ) ); + } + + return array(); + } + + /** + * Show dropdown layered nav. + * + * @param array $terms Terms. + * @param string $taxonomy Taxonomy. + * @param int $depth Depth. + * @return bool Will nav display? + */ + protected function layered_nav_dropdown( $terms, $taxonomy, $depth = 0 ) { + $found = false; + + if ( $taxonomy !== $this->get_current_taxonomy() ) { + $term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, 'or' ); + $_chosen_attributes = $this->get_chosen_attributes(); + + if ( 0 === $depth ) { + echo ''; + + wc_enqueue_js( + " + jQuery( '.wc-brand-dropdown-layered-nav-" . esc_js( $taxonomy ) . "' ).change( function() { + var slug = jQuery( this ).val(); + location.href = '" . preg_replace( '%\/page\/[0-9]+%', '', str_replace( array( '&', '%2C' ), array( '&', ',' ), esc_js( add_query_arg( 'filtering', '1', $link ) ) ) ) . '&filter_' . esc_js( $taxonomy ) . "=' + jQuery( '.wc-brand-dropdown-layered-nav-" . esc_js( $taxonomy ) . "' ).val(); + }); + " + ); + } + } + + return $found; + } + + /** + * Show list based layered nav. + * + * @param array $terms Terms. + * @param string $taxonomy Taxonomy. + * @param int $depth Depth. + * @return bool Will nav display? + */ + protected function layered_nav_list( $terms, $taxonomy, $depth = 0 ) { + // List display. + echo '
      '; + + $term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, 'or' ); + $_chosen_attributes = $this->get_chosen_attributes(); + $current_values = ! empty( $_chosen_attributes ) ? $_chosen_attributes : array(); + $found = false; + + $filter_name = 'filter_' . $taxonomy; + + foreach ( $terms as $term ) { + $option_is_set = in_array( $term->term_id, $current_values, true ); + $count = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0; + + // skip the term for the current archive. + if ( $this->get_current_term_id() === $term->term_id ) { + continue; + } + + // Only show options with count > 0. + if ( 0 < $count ) { + $found = true; + } elseif ( 0 === $count && ! $option_is_set ) { + continue; + } + + $current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $current_filter = array_map( 'intval', $current_filter ); + + if ( ! in_array( $term->term_id, $current_filter, true ) ) { + $current_filter[] = $term->term_id; + } + + $link = $this->get_page_base_url( $taxonomy ); + + // Add current filters to URL. + foreach ( $current_filter as $key => $value ) { + // Exclude query arg for current term archive term. + if ( $value === $this->get_current_term_id() ) { + unset( $current_filter[ $key ] ); + } + + // Exclude self so filter can be unset on click. + if ( $option_is_set && $value === $term->term_id ) { + unset( $current_filter[ $key ] ); + } + } + + if ( ! empty( $current_filter ) ) { + $link = add_query_arg( + array( + 'filtering' => '1', + $filter_name => implode( ',', $current_filter ), + ), + $link + ); + } + + echo '
    • '; + + echo ( $count > 0 || $option_is_set ) ? '' : ''; // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + + echo esc_html( $term->name ); + + echo ( $count > 0 || $option_is_set ) ? ' ' : ' '; + + echo wp_kses_post( apply_filters( 'woocommerce_layered_nav_count', '(' . absint( $count ) . ')', $count, $term ) );// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + + $child_terms = get_terms( + array( + 'taxonomy' => $taxonomy, + 'hide_empty' => true, + 'parent' => $term->term_id, + ) + ); + + if ( ! empty( $child_terms ) ) { + $found |= $this->layered_nav_list( $child_terms, $taxonomy, $depth + 1 ); + } + + echo '
    • '; + } + + echo '
    '; + + return $found; + } + + /** + * Count products within certain terms, taking the main WP query into consideration. + * + * @param array $term_ids Term IDs. + * @param string $taxonomy Taxonomy. + * @param string $query_type Query type. + * @return array + */ + protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type = 'and' ) { + global $wpdb; + + $tax_query = WC_Query::get_main_tax_query(); + $meta_query = WC_Query::get_main_meta_query(); + + if ( 'or' === $query_type ) { + foreach ( $tax_query as $key => $query ) { + if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) { + unset( $tax_query[ $key ] ); + } + } + } + + $meta_query = new WP_Meta_Query( $meta_query ); + $tax_query = new WP_Tax_Query( $tax_query ); + $meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' ); + $tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' ); + + // Generate query. + $query = array(); + $query['select'] = "SELECT COUNT( DISTINCT {$wpdb->posts}.ID ) as term_count, terms.term_id as term_count_id"; + $query['from'] = "FROM {$wpdb->posts}"; + $query['join'] = " + INNER JOIN {$wpdb->term_relationships} AS term_relationships ON {$wpdb->posts}.ID = term_relationships.object_id + INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id ) + INNER JOIN {$wpdb->terms} AS terms USING( term_id ) + " . $tax_query_sql['join'] . $meta_query_sql['join']; + $query['where'] = " + WHERE {$wpdb->posts}.post_type IN ( 'product' ) + AND {$wpdb->posts}.post_status = 'publish' + " . $tax_query_sql['where'] . $meta_query_sql['where'] . ' + AND terms.term_id IN (' . implode( ',', array_map( 'absint', $term_ids ) ) . ') + '; + $query['group_by'] = 'GROUP BY terms.term_id'; + $query = apply_filters( 'woocommerce_get_filtered_term_product_counts_query', $query ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + $query = implode( ' ', $query ); + + // We have a query - let's see if cached results of this query already exist. + $query_hash = md5( $query ); + + $cache = apply_filters( 'woocommerce_layered_nav_count_maybe_cache', true ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + if ( true === $cache ) { + $cached_counts = (array) get_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ) ); + } else { + $cached_counts = array(); + } + + if ( ! isset( $cached_counts[ $query_hash ] ) ) { + $results = $wpdb->get_results( $query, ARRAY_A ); // @codingStandardsIgnoreLine + $counts = array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) ); + $cached_counts[ $query_hash ] = $counts; + if ( true === $cache ) { + set_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ), $cached_counts, HOUR_IN_SECONDS ); + } + } + + return array_map( 'absint', (array) $cached_counts[ $query_hash ] ); + } +} diff --git a/plugins/woocommerce/includes/widgets/class-wc-widget-brand-thumbnails.php b/plugins/woocommerce/includes/widgets/class-wc-widget-brand-thumbnails.php new file mode 100644 index 00000000000..fd6a07e38f8 --- /dev/null +++ b/plugins/woocommerce/includes/widgets/class-wc-widget-brand-thumbnails.php @@ -0,0 +1,235 @@ +woo_widget_name = __( 'WooCommerce Brand Thumbnails', 'woocommerce' ); + $this->woo_widget_description = __( 'Show a grid of brand thumbnails.', 'woocommerce' ); + $this->woo_widget_idbase = 'wc_brands_brand_thumbnails'; + $this->woo_widget_cssclass = 'widget_brand_thumbnails'; + + /* Widget settings. */ + $widget_ops = array( + 'classname' => $this->woo_widget_cssclass, + 'description' => $this->woo_widget_description, + ); + + /* Create the widget. */ + parent::__construct( $this->woo_widget_idbase, $this->woo_widget_name, $widget_ops ); + } + + /** + * Echoes the widget content. + * + * @see WP_Widget + * + * @param array $args Display arguments including 'before_title', 'after_title', + * 'before_widget', and 'after_widget'. + * @param array $instance The settings for the particular instance of the widget. + */ + public function widget( $args, $instance ) { + $instance = wp_parse_args( + $instance, + array( + 'title' => '', + 'columns' => 1, + 'exclude' => '', + 'orderby' => 'name', + 'hide_empty' => 0, + 'number' => '', + ) + ); + + $exclude = array_map( 'intval', explode( ',', $instance['exclude'] ) ); + $order = 'name' === $instance['orderby'] ? 'asc' : 'desc'; + + $brands = get_terms( + array( + 'taxonomy' => 'product_brand', + 'hide_empty' => $instance['hide_empty'], + 'orderby' => $instance['orderby'], + 'exclude' => $exclude, + 'number' => $instance['number'], + 'order' => $order, + ) + ); + + if ( ! $brands ) { + return; + } + + /** + * Filter the widget's title. + * + * @since 9.4.0 + * + * @param string $title Widget title + * @param array $instance The settings for the particular instance of the widget. + * @param string $woo_widget_idbase The widget's id base. + */ + $title = apply_filters( 'widget_title', $instance['title'], $instance, $this->woo_widget_idbase ); + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput + if ( '' !== $title ) { + echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput + } + + wc_get_template( + 'widgets/brand-thumbnails.php', + array( + 'brands' => $brands, + 'columns' => (int) $instance['columns'], + 'fluid_columns' => ! empty( $instance['fluid_columns'] ) ? true : false, + ), + 'woocommerce', + WC()->plugin_path() . '/templates/brands/' + ); + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput + } + + /** + * Update widget instance. + * + * @param array $new_instance The new settings for the particular instance of the widget. + * @param array $old_instance The old settings for the particular instance of the widget. + * + * @see WP_Widget->update + */ + public function update( $new_instance, $old_instance ) { + $instance['title'] = wp_strip_all_tags( stripslashes( $new_instance['title'] ) ); + $instance['columns'] = wp_strip_all_tags( stripslashes( $new_instance['columns'] ) ); + $instance['fluid_columns'] = ! empty( $new_instance['fluid_columns'] ) ? true : false; + $instance['orderby'] = wp_strip_all_tags( stripslashes( $new_instance['orderby'] ) ); + $instance['exclude'] = wp_strip_all_tags( stripslashes( $new_instance['exclude'] ) ); + $instance['hide_empty'] = wp_strip_all_tags( stripslashes( (string) $new_instance['hide_empty'] ) ); + $instance['number'] = wp_strip_all_tags( stripslashes( $new_instance['number'] ) ); + + if ( ! $instance['columns'] ) { + $instance['columns'] = 1; + } + + if ( ! $instance['orderby'] ) { + $instance['orderby'] = 'name'; + } + + if ( ! $instance['exclude'] ) { + $instance['exclude'] = ''; + } + + if ( ! $instance['hide_empty'] ) { + $instance['hide_empty'] = 0; + } + + if ( ! $instance['number'] ) { + $instance['number'] = ''; + } + + return $instance; + } + + /** + * Outputs the settings update form. + * + * @param array $instance Current settings. + */ + public function form( $instance ) { + if ( ! isset( $instance['hide_empty'] ) ) { + $instance['hide_empty'] = 0; + } + + if ( ! isset( $instance['orderby'] ) ) { + $instance['orderby'] = 'name'; + } + + if ( empty( $instance['fluid_columns'] ) ) { + $instance['fluid_columns'] = false; + } + + ?> +

    + + +

    + +

    + + +

    + +

    + + id="get_field_id( 'fluid_columns' ) ); ?>" name="get_field_name( 'fluid_columns' ) ); ?>" /> +

    + +

    + + +

    + +

    + + +

    + +

    + + +

    + +

    + + +

    + get_param( 'redirect_url' ); - $calypso_env = defined( 'WOOCOMMERCE_CALYPSO_ENVIRONMENT' ) && in_array( WOOCOMMERCE_CALYPSO_ENVIRONMENT, [ 'development', 'wpcalypso', 'horizon', 'stage' ], true ) ? WOOCOMMERCE_CALYPSO_ENVIRONMENT : 'production'; + $calypso_env = defined( 'WOOCOMMERCE_CALYPSO_ENVIRONMENT' ) && in_array( WOOCOMMERCE_CALYPSO_ENVIRONMENT, array( 'development', 'wpcalypso', 'horizon', 'stage' ), true ) ? WOOCOMMERCE_CALYPSO_ENVIRONMENT : 'production'; + + $authorization_url = $manager->get_authorization_url( null, $redirect_url ); + $authorization_url = add_query_arg( 'locale', $this->get_wpcom_locale(), $authorization_url ); + + if ( Features::is_enabled( 'use-wp-horizon' ) ) { + $calypso_env = 'horizon'; + } return [ 'success' => ! $errors->has_errors(), 'errors' => $errors->get_error_messages(), 'url' => add_query_arg( - [ + array( 'from' => $request->get_param( 'from' ), 'calypso_env' => $calypso_env, - ], - $manager->get_authorization_url( null, $redirect_url ) + ), + $authorization_url, ), ]; } + /** + * Return a locale string for wpcom. + * + * @return string + */ + private function get_wpcom_locale() { + // List of locales that should be used with region code. + $locale_to_lang = array( + 'bre' => 'br', + 'de_AT' => 'de-at', + 'de_CH' => 'de-ch', + 'de' => 'de_formal', + 'el' => 'el-po', + 'en_GB' => 'en-gb', + 'es_CL' => 'es-cl', + 'es_MX' => 'es-mx', + 'fr_BE' => 'fr-be', + 'fr_CA' => 'fr-ca', + 'nl_BE' => 'nl-be', + 'nl' => 'nl_formal', + 'pt_BR' => 'pt-br', + 'sr' => 'sr_latin', + 'zh_CN' => 'zh-cn', + 'zh_HK' => 'zh-hk', + 'zh_SG' => 'zh-sg', + 'zh_TW' => 'zh-tw', + ); + + $system_locale = get_locale(); + if ( isset( $locale_to_lang[ $system_locale ] ) ) { + // Return the locale with region code if it's in the list. + return $locale_to_lang[ $system_locale ]; + } + + // If the locale is not in the list, return the language code only. + return explode( '_', $system_locale )[0]; + } + /** * Check whether the current user has permission to install plugins * @@ -400,7 +446,7 @@ class OnboardingPlugins extends WC_REST_Data_Controller { ), $slug ), - 'type' => 'plugin_info_api_error', + 'type' => 'plugin_info_api_error', 'slug' => $slug, 'api_version' => $api->version, 'api_download_link' => $api->download_link, diff --git a/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php index e13ed41c47c..7f740eff74e 100644 --- a/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php +++ b/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php @@ -615,6 +615,11 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { if ( is_null( $customer_user ) ) { $customer_user = new \WC_Customer( $user_id ); } + + // Set email as customer email instead of Order Billing Email if we have a customer. + $data['email'] = $customer_user->get_email( 'edit' ); + + // Adding other relevant customer data. $data['user_id'] = $user_id; $data['username'] = $customer_user->get_username( 'edit' ); $data['date_registered'] = $customer_user->get_date_created( 'edit' ) ? $customer_user->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ) : null; diff --git a/plugins/woocommerce/src/Admin/API/Reports/Taxes/Controller.php b/plugins/woocommerce/src/Admin/API/Reports/Taxes/Controller.php index d31db463c0c..a794fee018f 100644 --- a/plugins/woocommerce/src/Admin/API/Reports/Taxes/Controller.php +++ b/plugins/woocommerce/src/Admin/API/Reports/Taxes/Controller.php @@ -244,7 +244,15 @@ class Controller extends GenericController implements ExportableInterface { */ public function prepare_item_for_export( $item ) { return array( - 'tax_code' => \WC_Tax::get_rate_code( $item['tax_rate_id'] ), + 'tax_code' => \WC_Tax::get_rate_code( + (object) array( + 'tax_rate_id' => $item['tax_rate_id'], + 'tax_rate_country' => $item['country'], + 'tax_rate_state' => $item['state'], + 'tax_rate_name' => $item['name'], + 'tax_rate_priority' => $item['priority'], + ) + ), 'rate' => $item['tax_rate'], 'total_tax' => self::csv_number_format( $item['total_tax'] ), 'order_tax' => self::csv_number_format( $item['order_tax'] ), diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php index ba33d9b2362..b0a65b969e6 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php @@ -15,6 +15,7 @@ class Tax extends Task { /** * Used to cache is_complete() method result. + * * @var null */ private $is_complete_result = null; @@ -109,12 +110,16 @@ class Tax extends Task { */ public function is_complete() { if ( $this->is_complete_result === null ) { - $wc_connect_taxes_enabled = get_option( 'wc_connect_taxes_enabled' ); + $wc_connect_taxes_enabled = get_option( 'wc_connect_taxes_enabled' ); $is_wc_connect_taxes_enabled = ( $wc_connect_taxes_enabled === 'yes' ) || ( $wc_connect_taxes_enabled === true ); // seems that in some places boolean is used, and other places 'yes' | 'no' is used + // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- We will replace this with a formal system by WC 9.6 so lets not advertise it yet. + $third_party_complete = apply_filters( 'woocommerce_admin_third_party_tax_setup_complete', false ); + $this->is_complete_result = $is_wc_connect_taxes_enabled || count( TaxDataStore::get_taxes( array() ) ) > 0 || - get_option( 'woocommerce_no_sales_tax' ) !== false; + get_option( 'woocommerce_no_sales_tax' ) !== false || + $third_party_complete; } return $this->is_complete_result; diff --git a/plugins/woocommerce/src/Admin/Notes/Note.php b/plugins/woocommerce/src/Admin/Notes/Note.php index af986572efa..9058906d87b 100644 --- a/plugins/woocommerce/src/Admin/Notes/Note.php +++ b/plugins/woocommerce/src/Admin/Notes/Note.php @@ -579,6 +579,11 @@ class Note extends \WC_Data { $layout = 'plain'; } $valid_layouts = array( 'banner', 'plain', 'thumbnail' ); + + if ( 'banner' === $layout ) { + wc_deprecated_argument( 'Note::set_layout', '9.4.0', 'The "banner" layout is deprecated. Please use "thumbnail" instead to display a image.' ); + } + if ( in_array( $layout, $valid_layouts, true ) ) { $this->set_prop( 'layout', $layout ); } else { diff --git a/plugins/woocommerce/src/Admin/PluginsHelper.php b/plugins/woocommerce/src/Admin/PluginsHelper.php index efcf9152f9e..1900bfdf493 100644 --- a/plugins/woocommerce/src/Admin/PluginsHelper.php +++ b/plugins/woocommerce/src/Admin/PluginsHelper.php @@ -33,17 +33,24 @@ if ( ! function_exists( 'get_plugins' ) ) { class PluginsHelper { /** - * Indicates whether the expiration notice for subscriptions can be displayed. + * Subscription notices in Woo screens are shown in clear priority order, first + * expired, and if those don't exist, expiring, and finally if none of those exist, + * then missing. This keeps track of whether we can show the next set of notices. * * @var bool */ - public static $can_show_expiring_subs_notice = true; + public static $subscription_usage_notices_already_shown = false; /** * The URL for the WooCommerce subscription page. */ const WOO_SUBSCRIPTION_PAGE_URL = 'https://woocommerce.com/my-account/my-subscriptions/'; + /** + * The URL for the WooCommerce.com cart page. + */ + const WOO_CART_PAGE_URL = 'https://woocommerce.com/cart/'; + /** * The URL for the WooCommerce.com add payment method page. */ @@ -59,6 +66,11 @@ class PluginsHelper { */ const DISMISS_EXPIRING_SUBS_NOTICE = 'woo_subscription_expiring_notice_dismiss'; + /** + * Meta key for dismissing missing subscription notices + */ + const DISMISS_MISSING_SUBS_NOTICE = 'woo_subscription_missing_notice_dismiss'; + /** * Initialize hooks. */ @@ -67,10 +79,7 @@ class PluginsHelper { add_action( 'woocommerce_plugins_install_and_activate_async_callback', array( __CLASS__, 'install_and_activate_plugins_async_callback' ), 10, 2 ); add_action( 'woocommerce_plugins_activate_callback', array( __CLASS__, 'activate_plugins' ), 10, 2 ); add_action( 'admin_notices', array( __CLASS__, 'maybe_show_connect_notice_in_plugin_list' ) ); - add_action( 'admin_notices', array( __CLASS__, 'maybe_show_expired_subscriptions_notice' ), 10 ); - add_action( 'admin_notices', array( __CLASS__, 'maybe_show_expiring_subscriptions_notice' ), 11 ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_scripts_for_connect_notice' ) ); - add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_scripts_for_subscription_notice' ) ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_scripts_for_notices_in_plugins' ) ); } @@ -659,6 +668,7 @@ class PluginsHelper { wp_enqueue_script( 'woo-plugin-update-connect-notice' ); wp_enqueue_script( 'woo-enable-autorenew' ); wp_enqueue_script( 'woo-renew-subscription' ); + wp_enqueue_script( 'woo-purchase-subscription' ); } /** @@ -733,11 +743,15 @@ class PluginsHelper { * @return array notice data to return. Contains type, parsed_message and product_id. */ public static function get_subscriptions_notice_data( array $all_subs, array $subs_to_show, int $total, array $messages, string $type ) { + $utm_campaign = 'expired' === $type ? + 'pu_settings_screen_renew' : + ( 'missing' === $type ? 'pu_settings_screen_purchase' : 'pu_settings_screen_enable_autorenew' ); + if ( 1 < $total ) { $hyperlink_url = add_query_arg( array( 'utm_source' => 'pu', - 'utm_campaign' => 'expired' === $type ? 'pu_settings_screen_renew' : 'pu_settings_screen_enable_autorenew', + 'utm_campaign' => $utm_campaign, ), self::WOO_SUBSCRIPTION_PAGE_URL @@ -750,10 +764,18 @@ class PluginsHelper { esc_attr( $total ), ); + // All product ids. + $product_ids = array_map( + function ( $sub ) { + return $sub['product_id']; + }, + $subs_to_show + ); + return array( 'type' => 'different_subscriptions', 'parsed_message' => $parsed_message, - 'product_id' => '', + 'product_ids' => $product_ids, ); } @@ -769,8 +791,9 @@ class PluginsHelper { ) ); - $message_key = $has_multiple_subs_for_product ? 'multiple_manage' : 'single_manage'; - $renew_string = __( 'Renew', 'woocommerce' ); + $message_key = $has_multiple_subs_for_product ? 'multiple_manage' : 'single_manage'; + $renew_string = __( 'Renew', 'woocommerce' ); + $subscribe_string = __( 'Subscribe', 'woocommerce' ); if ( isset( $subscription['product_regular_price'] ) ) { /* translators: 1: Product price */ $renew_string = sprintf( __( 'Renew for %1$s', 'woocommerce' ), $subscription['product_regular_price'] ); @@ -781,7 +804,7 @@ class PluginsHelper { 'product_id' => $product_id, 'type' => $type, 'utm_source' => 'pu', - 'utm_campaign' => 'expired' === $type ? 'pu_settings_screen_renew' : 'pu_settings_screen_enable_autorenew', + 'utm_campaign' => $utm_campaign, ), self::WOO_SUBSCRIPTION_PAGE_URL @@ -798,7 +821,8 @@ class PluginsHelper { esc_attr( $subscription['product_name'] ), esc_attr( $expiry_date ), esc_url( $hyperlink_url ), - esc_attr( $renew_string ), + // Show subscribe for missing subscriptions, renew otherwise. + 'missing' === $type ? esc_attr( $subscribe_string ) : esc_attr( $renew_string ), ); return array( @@ -826,7 +850,7 @@ class PluginsHelper { return array(); } - if ( ! self::$can_show_expiring_subs_notice ) { + if ( self::$subscription_usage_notices_already_shown ) { return array(); } @@ -851,6 +875,9 @@ class PluginsHelper { $total_expiring_subscriptions = count( $expiring_subscriptions ); + // Don't show missing notice if there are expiring subscriptions. + self::$subscription_usage_notices_already_shown = true; + // When payment method is missing on WooCommerce.com. $helper_notices = WC_Helper::get_notices(); if ( ! empty( $helper_notices['missing_payment_method_notice'] ) ) { @@ -927,8 +954,8 @@ class PluginsHelper { return array(); } - $total_expired_subscriptions = count( $expired_subscriptions ); - self::$can_show_expiring_subs_notice = false; + $total_expired_subscriptions = count( $expired_subscriptions ); + self::$subscription_usage_notices_already_shown = true; $notice_data = self::get_subscriptions_notice_data( $subscriptions, @@ -947,17 +974,17 @@ class PluginsHelper { $button_link = add_query_arg( array( + 'add-to-cart' => $notice_data['product_ids'], 'utm_source' => 'pu', 'utm_campaign' => $allowed_link ? 'pu_settings_screen_renew' : 'pu_in_apps_screen_renew', ), - self::WOO_SUBSCRIPTION_PAGE_URL + self::WOO_CART_PAGE_URL ); if ( in_array( $notice_data['type'], array( 'single_manage', 'multiple_manage' ), true ) ) { $button_link = add_query_arg( array( - 'product_id' => $notice_data['product_id'], - 'type' => 'expiring', + 'add-to-cart' => $notice_data['product_id'], ), $button_link ); @@ -970,6 +997,86 @@ class PluginsHelper { ); } + /** + * Get formatted notice information for missing subscription. + * + * @return array notice information. + */ + public static function get_missing_subscription_notice() { + if ( ! WC_Helper::is_site_connected() ) { + return array(); + } + + if ( self::$subscription_usage_notices_already_shown ) { + return array(); + } + + if ( ! self::should_show_notice( self::DISMISS_MISSING_SUBS_NOTICE ) ) { + return array(); + } + + $subscriptions = WC_Helper::get_subscription_list_data(); + $missing_subscriptions = array_filter( + $subscriptions, + function ( $sub ) { + return ( ! empty( $sub['local']['installed'] ) && empty( $sub['product_key'] ) ); + }, + ); + + // Remove WUM from missing subscriptions list. + $missing_subscriptions = array_filter( + $missing_subscriptions, + function ( $sub ) { + return 'woo-update-manager' !== $sub['zip_slug']; + } + ); + + if ( ! $missing_subscriptions ) { + return array(); + } + + $total_missing_subscriptions = count( $missing_subscriptions ); + + $notice_data = self::get_subscriptions_notice_data( + $subscriptions, + $missing_subscriptions, + $total_missing_subscriptions, + array( + /* translators: 1) product name */ + 'single_manage' => __( 'You don\'t have a subscription for %1$s. Subscribe to receive updates and streamlined support.', 'woocommerce' ), + /* translators: 1) total expired subscriptions */ + 'different_subscriptions' => __( 'You don\'t have subscriptions for %1$s Woo extensions. Subscribe to receive updates and streamlined support.', 'woocommerce' ), + ), + 'missing', + ); + + $button_link = add_query_arg( + array( + 'add-to-cart' => $notice_data['product_ids'], + 'utm_source' => 'pu', + 'utm_campaign' => 'pu_in_apps_screen_purchase', + ), + self::WOO_CART_PAGE_URL + ); + + if ( in_array( $notice_data['type'], array( 'single_manage', 'multiple_manage' ), true ) ) { + $button_link = add_query_arg( + array( + 'add-to-cart' => $notice_data['product_id'], + ), + $button_link + ); + } + + $button_text = __( 'Subscribe', 'woocommerce' ); + + return array( + 'description' => $notice_data['parsed_message'], + 'button_text' => $button_text, + 'button_link' => $button_link, + ); + } + /** * Determine whether a specific notice should be shown to the current user. * diff --git a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/BaseLocationCountryRuleProcessor.php b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/BaseLocationCountryRuleProcessor.php deleted file mode 100644 index 3854f372b83..00000000000 --- a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/BaseLocationCountryRuleProcessor.php +++ /dev/null @@ -1,33 +0,0 @@ -source ) ) { $note->set_source( $spec->source ); } + if ( isset( $spec->layout ) ) { + $note->set_layout( $spec->layout ); + } // Recreate actions. $note->set_actions( self::get_actions( $spec ) ); diff --git a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/StoredStateRuleProcessor.php b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/StoredStateRuleProcessor.php deleted file mode 100644 index bc4f2c5e9fb..00000000000 --- a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/StoredStateRuleProcessor.php +++ /dev/null @@ -1,33 +0,0 @@ - "media" ), - array("industry" => "software" ) -); -``` - -Use `array_column` to extract `array("media", "software")` then choose the first element with `dot_notation`. - -```php -"transformers": [ - { - "use": "array_column", - "arguments": { - "key": "industry" - } - }, - { - "use": "dot_notation", - "arguments": { - "key": "0" - } - } -], -``` - -**Output**: "media" - - - - - -## array_flatten - -Flattens a nested array. - -#### Arguments: N/A - -#### Definition: - -```php -"transformers": [ - { - "use": "array_flatten" - } -], -``` - -#### Example: - -Given the following data - -```php -array( - array( - 'member1', - ), - array( - 'member2', - ), - array( - 'member3', - ), -); -``` - -Use `array_flatten` to extract `array("member1", "member2", "member3")` then use `array_search` to make sure it has `member2` - - -```php -"transformers": [ - { - "use": "array_flatten", - }, - { - "use": "array_search", - "arguments": { - "key": "member2" - } - } -], -``` - -**Output**: true - -## array_keys - -PHP's built-in `array_keys` to return keys from an array. For more information about how `array_keys` works, please see PHP’s [official documentation](https://www.php.net/manual/en/function.array-column.php). - -#### Arguments: N/A - -####Definition: - -```php -"transformers": [ - { - "use": "array_keys" - } -], -``` - -#### Example: - -Given the following data - -```php -array( - "name" => "tester", - "address" => "test", - "supports_version_2" => true -) -``` - -Use `array_keys` to extract `array("name", "address", "supports_version_2")` and then use `array_search` to make sure it has `supports_version_2` - -```php -"transformers": [ - { - "use": "array_keys", - }, - { - "use": "array_search", - "arguments": { - "key": "member2" - } - } -], -``` - -**Output**: true - -## array_search - -PHP's built-in `array_search` to search a value in an array. For more information about how `array_search` works, please see PHP’s [official documentation](https://www.php.net/manual/en/function.array-search.php). - -#### Arguments: - -|name|description| -|----|---------| -| value | a value to search in the given array | - -#### Definition: - -```php -"transformers": [ - { - "use": "array_search", - "arguments": { - "value": "test" - } - } -], -``` - -#### Examples - -See examples from [array_flatten](#array_flatten) and [array_keys](#array_keys) - -## array_values - -PHP's built-in array_values to return values from an array. For more information about how `array_values` works, please see PHP’s [official documentation](https://www.php.net/manual/en/function.array-values). - - -#### Arguments: N/A - -#### Definition: - -```php -"transformers": [ - { - "use": "array_values" - } -], -``` - -#### Example: - -Given the following data - -```php -array ( - "size" => "x-large" -) -``` - -Use `array_values` to extract `array("x-large")` - -```php -"transformers": [ - { - "use": "array_values", - } -], -``` - -**Output:** "x-large" - - -## dot_notation - -Uses dot notation to select a value in an array. Dot notation lets you access an array as if it is an object. - -#### Arguments: N/A - -#### Definition: - - -```php -"transformers": [ - { - "use": "dot_notation", - "arguments": { - "path": "name" - } - } -], -``` - -#### Example: - - - -Given the following data - -```php -array( - 'name' => 'john', - 'members' => ['member1', 'member2'] -); -``` - -Select `name` field. - -```php -"transformers": [ - { - "use": "dot_notation", - "arguments": { - "path": "name" - } - } -], -``` - -**Output:** "john" - -Select `member2`. You can access array items with an index. - -```php -"transformers": [ - { - "use": "dot_notation", - "arguments": { - "path": "members.1" - } - } -], -``` - -**Output:**: "member2" - -## count - -PHP's built-in count to return the number of values from a countable, such as an array. - -#### Arguments: N/A - -#### Definition: - -```php -"transformers": [ - { - "use": "count" - } -], -``` - -#### Example: - -Given the following list of usernames - -```php -array( - "username1", - "username2", - "username3" -) -``` - -Let's count # of users with `count` - -```php -"transformers": [ - { - "use": "count", - } -], -``` - -**Output:** 3 - -## prepare_url - -This prepares the site URL by removing the protocol and the last slash. - -#### Arguments: N/A - -####Definition: - -```php -"transformers": [ - { - "use": "prepare_url" - } -], -``` - -#### Example: - -Given the following data - -```php -$siteurl = "https://mysite.com/" -``` - -Removes the protocol and the last slash. - -```php -"transformers": [ - { - "use": "prepare_url", - } -], -``` - -**Output:** "mysite.com" diff --git a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/WCAdminActiveForProvider.php b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/WCAdminActiveForProvider.php deleted file mode 100644 index 21d53b4c0c1..00000000000 --- a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/WCAdminActiveForProvider.php +++ /dev/null @@ -1,31 +0,0 @@ -%4$s
    ', - esc_attr( $classes_and_styles['classes'] ), - esc_attr( $classname ), - esc_attr( $classes_and_styles['styles'] ), + '
    %2$s
    ', + get_block_wrapper_attributes( + array( + 'class' => 'wc-block-breadcrumbs woocommerce ' . esc_attr( $classes_and_styles['classes'] ), + 'style' => $classes_and_styles['styles'], + ) + ), $breadcrumb ); } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlock.php index 4887dd04e30..66b263dbe82 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlock.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlock.php @@ -55,11 +55,12 @@ abstract class AbstractOrderConfirmationBlock extends AbstractBlock { } return $block_content ? sprintf( - '
    %3$s
    ', + '
    %3$s
    ', esc_attr( trim( $classname ) ), esc_attr( $classes_and_styles['styles'] ), $block_content, - esc_attr( $this->block_name ) + esc_attr( $this->block_name ), + esc_attr( $this->namespace ) ) : ''; } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php index 9c38143c870..047be224e28 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php @@ -2,8 +2,6 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation; -use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils; - /** * Status class. */ diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php index 0681a7b249a..a93b9c2ce34 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php @@ -499,11 +499,18 @@ class ProductCollection extends AbstractBlock { */ private function is_block_compatible( $block_name ) { // Check for explicitly unsupported blocks. - if ( - 'core/post-content' === $block_name || - 'woocommerce/mini-cart' === $block_name || - 'woocommerce/featured-product' === $block_name - ) { + $unsupported_blocks = array( + 'core/post-content', + 'woocommerce/mini-cart', + 'woocommerce/featured-product', + 'woocommerce/active-filters', + 'woocommerce/price-filter', + 'woocommerce/stock-filter', + 'woocommerce/attribute-filter', + 'woocommerce/rating-filter', + ); + + if ( in_array( $block_name, $unsupported_blocks, true ) ) { return false; } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php deleted file mode 100644 index 6cfc9a1465a..00000000000 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php +++ /dev/null @@ -1,157 +0,0 @@ -register_block_type() - * @param string $key Data to get, or default to everything. - * @return array|string|null - */ - protected function get_block_type_script( $key = null ) { - return null; - } - - /** - * Extra data passed through from server to client for block. - * - * @param array $attributes Any attributes that currently are available from the block. - * Note, this will be empty in the editor context when the block is - * not in the post content on editor load. - */ - protected function enqueue_data( array $attributes = [] ) { - global $pagenow; - parent::enqueue_data( $attributes ); - - $this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() ); - $this->asset_data_registry->add( 'isProductArchive', is_shop() || is_product_taxonomy() ); - $this->asset_data_registry->add( 'isSiteEditor', 'site-editor.php' === $pagenow ); - $this->asset_data_registry->add( 'isWidgetEditor', 'widgets.php' === $pagenow || 'customize.php' === $pagenow ); - } - - /** - * Check array for checked item. - * - * @param array $items Items to check. - */ - private function hasSelectedFilter( $items ) { - foreach ( $items as $key => $value ) { - if ( 'checked' === $key && true === $value ) { - return true; - } - - if ( is_array( $value ) && $this->hasSelectedFilter( $value ) ) { - return true; - } - } - - return false; - } - - /** - * Render the block. - * - * @param array $attributes Block attributes. - * @param string $content Block content. - * @param WP_Block $block Block instance. - * @return string Rendered block type output. - */ - protected function render( $attributes, $content, $block ) { - if ( is_admin() ) { - return $content; - } - - $tags = new WP_HTML_Tag_Processor( $content ); - $has_selected_filter = false; - - while ( $tags->next_tag( 'div' ) ) { - $items = $tags->get_attribute( 'data-wc-context' ) ? json_decode( $tags->get_attribute( 'data-wc-context' ), true ) : null; - - // For checked box filters. - if ( $items && array_key_exists( 'items', $items ) ) { - $has_selected_filter = $this->hasSelectedFilter( $items['items'] ); - break; - } - - // For price range filter. - if ( $items && array_key_exists( 'minPrice', $items ) ) { - if ( $items['minPrice'] > $items['minRange'] || $items['maxPrice'] < $items['maxRange'] ) { - $has_selected_filter = true; - break; - } - } - - // For dropdown filters. - if ( $items && array_key_exists( 'selectedItems', $items ) ) { - if ( count( $items['selectedItems'] ) > 0 ) { - $has_selected_filter = true; - break; - } - } - } - - $attributes_data = array( - 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), - 'data-wc-context' => wp_json_encode( array( 'hasSelectedFilter' => $has_selected_filter ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), - 'class' => 'wc-block-product-filters', - ); - - if ( ! isset( $block->context['queryId'] ) ) { - $attributes_data['data-wc-navigation-id'] = $this->generate_navigation_id( $block ); - } - - $tags = new WP_HTML_Tag_Processor( $content ); - - while ( $tags->next_tag( 'div' ) ) { - if ( 'yes' === $tags->get_attribute( 'data-has-filter' ) ) { - return sprintf( - '', - get_block_wrapper_attributes( $attributes_data ), - $content - ); - } - } - - return sprintf( - '', - get_block_wrapper_attributes( $attributes_data ), - ); - } - - /** - * Generate a unique navigation ID for the block. - * - * @param mixed $block - Block instance. - * @return string - Unique navigation ID. - */ - private function generate_navigation_id( $block ) { - return sprintf( - 'wc-product-filter-%s', - md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) ) - ); - } -} diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php index 679227f3a09..e8d24554024 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php @@ -48,16 +48,9 @@ final class ProductFilterActive extends AbstractBlock { */ $active_filters = apply_filters( 'collection_active_filters_data', array(), $this->get_filter_query_params( $query_id ) ); - $context = array( - 'queryId' => $query_id, - 'params' => array_keys( $this->get_filter_query_params( $query_id ) ), - ); - $wrapper_attributes = get_block_wrapper_attributes( array( 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), - 'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), - 'data-has-filter' => empty( $active_filters ) ? 'no' : 'yes', ) ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php index d997799c772..0ef49f27988 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php @@ -1,10 +1,10 @@ $term_object->name, 'attributes' => array( - 'data-wc-on--click' => "$action_namespace::actions.removeFilter", + 'value' => $term, + 'data-wc-on--click' => "$action_namespace::actions.toggleFilter", 'data-wc-context' => "$action_namespace::" . wp_json_encode( array( - 'value' => $term, 'attributeSlug' => $product_attribute, 'queryType' => get_query_var( "query_type_{$product_attribute}" ), ), @@ -156,24 +157,24 @@ final class ProductFilterAttribute extends AbstractBlock { /** * Render the block. * - * @param array $attributes Block attributes. - * @param string $content Block content. - * @param WP_Block $block Block instance. + * @param array $block_attributes Block attributes. + * @param string $content Block content. + * @param WP_Block $block Block instance. * @return string Rendered block type output. */ - protected function render( $attributes, $content, $block ) { - if ( empty( $attributes['attributeId'] ) ) { - $default_product_attribute = $this->get_default_product_attribute(); - $attributes['attributeId'] = $default_product_attribute->attribute_id; + protected function render( $block_attributes, $content, $block ) { + if ( empty( $block_attributes['attributeId'] ) ) { + $default_product_attribute = $this->get_default_product_attribute(); + $block_attributes['attributeId'] = $default_product_attribute->attribute_id; } // don't render if its admin, or ajax in progress. - if ( is_admin() || wp_doing_ajax() || empty( $attributes['attributeId'] ) ) { + if ( is_admin() || wp_doing_ajax() || empty( $block_attributes['attributeId'] ) ) { return ''; } - $product_attribute = wc_get_attribute( $attributes['attributeId'] ); - $attribute_counts = $this->get_attribute_counts( $block, $product_attribute->slug, $attributes['queryType'] ); + $product_attribute = wc_get_attribute( $block_attributes['attributeId'] ); + $attribute_counts = $this->get_attribute_counts( $block, $product_attribute->slug, $block_attributes['queryType'] ); if ( empty( $attribute_counts ) ) { return sprintf( @@ -181,7 +182,6 @@ final class ProductFilterAttribute extends AbstractBlock { get_block_wrapper_attributes( array( 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), - 'data-has-filter' => 'no', ) ), ); @@ -202,118 +202,56 @@ final class ProductFilterAttribute extends AbstractBlock { ); $attribute_options = array_map( - function ( $term ) use ( $attribute_counts, $selected_terms ) { + function ( $term ) use ( $block_attributes, $attribute_counts, $selected_terms ) { $term = (array) $term; $term['count'] = $attribute_counts[ $term['term_id'] ]; $term['selected'] = in_array( $term['slug'], $selected_terms, true ); - return $term; + return array( + 'label' => $block_attributes['showCounts'] ? sprintf( '%1$s (%2$d)', $term['name'], $term['count'] ) : $term['name'], + 'value' => $term['slug'], + 'selected' => $term['selected'], + 'rawData' => $term, + ); }, $attribute_terms ); $filtered_options = array_filter( $attribute_options, - function ( $option ) { - return $option['count'] > 0; + function ( $option ) use ( $block_attributes ) { + $hide_empty = $block_attributes['hideEmpty'] ?? true; + if ( $hide_empty ) { + return $option['rawData']['count'] > 0; + } + return true; } ); - $filter_content = 'dropdown' === $attributes['displayStyle'] ? - $this->render_attribute_dropdown( $filtered_options, $attributes ) : - $this->render_attribute_checkbox_list( $filtered_options, $attributes ); + $filter_context = array( + 'action' => "{$this->get_full_block_name()}::actions.toggleFilter", + 'items' => $filtered_options, + ); + + foreach ( $block->parsed_block['innerBlocks'] as $inner_block ) { + $content .= ( new \WP_Block( $inner_block, array( 'filterData' => $filter_context ) ) )->render(); + } $context = array( - 'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ), - 'queryType' => $attributes['queryType'], - 'selectType' => 'multiple', + 'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ), + 'queryType' => $block_attributes['queryType'], + 'selectType' => 'multiple', + 'hasSelectedFilters' => count( $selected_terms ) > 0, ); return sprintf( - '
    %2$s%3$s
    ', + '
    %2$s
    ', get_block_wrapper_attributes( array( 'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), - 'data-has-filter' => 'yes', ) ), - $content, - $filter_content - ); - } - - /** - * Render the dropdown. - * - * @param array $options Data to render the dropdown. - * @param bool $attributes Block attributes. - */ - private function render_attribute_dropdown( $options, $attributes ) { - if ( empty( $options ) ) { - return ''; - } - - $list_items = array(); - $selected_items = array(); - - $product_attribute = wc_get_attribute( $attributes['attributeId'] ); - - foreach ( $options as $option ) { - $item = array( - 'label' => $attributes['showCounts'] ? sprintf( '%1$s (%2$d)', $option['name'], $option['count'] ) : $option['name'], - 'value' => $option['slug'], - ); - - $list_items[] = $item; - - if ( $option['selected'] ) { - $selected_items[] = $item; - } - } - - return Dropdown::render( - array( - 'items' => $list_items, - 'action' => "{$this->get_full_block_name()}::actions.navigate", - 'selected_items' => $selected_items, - 'select_type' => 'multiple', - // translators: %s is a product attribute name. - 'placeholder' => sprintf( __( 'Select %s', 'woocommerce' ), $product_attribute->name ), - ) - ); - } - - /** - * Render the attribute filter checkbox list. - * - * @param mixed $options Attribute filter options to render in the checkbox list. - * @param mixed $attributes Block attributes. - * @return string - */ - private function render_attribute_checkbox_list( $options, $attributes ) { - if ( empty( $options ) ) { - return ''; - } - - $show_counts = $attributes['showCounts'] ?? false; - - $list_options = array_map( - function ( $option ) use ( $show_counts ) { - return array( - 'id' => $option['slug'] . '-' . $option['term_id'], - 'checked' => $option['selected'], - 'label' => $show_counts ? sprintf( '%1$s (%2$d)', $option['name'], $option['count'] ) : $option['name'], - 'value' => $option['slug'], - ); - }, - $options - ); - - return CheckboxList::render( - array( - 'items' => $list_options, - 'on_change' => "{$this->get_full_block_name()}::actions.updateProducts", - ) + $content ); } @@ -380,7 +318,16 @@ final class ProductFilterAttribute extends AbstractBlock { $cached = get_transient( 'wc_block_product_filter_attribute_default_attribute' ); - if ( $cached ) { + if ( + $cached && + isset( $cached->attribute_id ) && + isset( $cached->attribute_name ) && + isset( $cached->attribute_label ) && + isset( $cached->attribute_type ) && + isset( $cached->attribute_orderby ) && + isset( $cached->attribute_public ) && + '0' !== $cached->attribute_id + ) { return $cached; } @@ -428,10 +375,9 @@ final class ProductFilterAttribute extends AbstractBlock { if ( $attribute_id ) { $default_attribute = $attributes[ $attribute_id ]; + set_transient( 'wc_block_product_filter_attribute_default_attribute', $default_attribute, DAY_IN_SECONDS ); } - set_transient( 'wc_block_product_filter_attribute_default_attribute', $default_attribute ); - return $default_attribute; } @@ -447,32 +393,29 @@ final class ProductFilterAttribute extends AbstractBlock { 'inserter' => false, 'content' => strtr( ' - - -
    - -

    {{attribute_label}}

    - + +
    + +
    + +

    {{attribute_label}}

    + + + + +
    + +
    + +
    + + + +
    + - - -
    - -
    - Clear -
    - -
    - -
    - - - - + ', array( '{{attribute_id}}' => intval( $default_attribute->attribute_id ), @@ -482,4 +425,18 @@ final class ProductFilterAttribute extends AbstractBlock { ) ); } + + /** + * Skip default rendering routine for inner blocks. + * + * @param array $settings Array of determined settings for registering a block type. + * @param array $metadata Metadata provided for registering a block type. + * @return array + */ + public function add_block_type_metadata_settings( $settings, $metadata ) { + if ( ! empty( $metadata['name'] ) && "woocommerce/{$this->block_name}" === $metadata['name'] ) { + $settings['skip_inner_blocks'] = true; + } + return $settings; + } } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php new file mode 100644 index 00000000000..3c0fa38eddb --- /dev/null +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php @@ -0,0 +1,124 @@ +context['filterData']; + $items = $context['items'] ?? array(); + $checkbox_list_context = array( 'items' => $items ); + $action = $context['action'] ?? ''; + $namespace = wp_json_encode( array( 'namespace' => 'woocommerce/product-filter-checkbox-list' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ); + $classes = ''; + $style = ''; + + $tags = new \WP_HTML_Tag_Processor( $content ); + if ( $tags->next_tag( array( 'class_name' => 'wc-block-product-filter-checkbox-list' ) ) ) { + $classes = $tags->get_attribute( 'class' ); + $style = $tags->get_attribute( 'style' ); + } + + $checked_items = array_filter( + $items, + function ( $item ) { + return $item['selected']; + } + ); + $show_initially = $context['show_initially'] ?? 15; + $remaining_initial_unchecked = count( $checked_items ) > $show_initially ? count( $checked_items ) : $show_initially - count( $checked_items ); + $count = 0; + + $wrapper_attributes = array( + 'data-wc-interactive' => esc_attr( $namespace ), + 'data-wc-context' => wp_json_encode( $checkbox_list_context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), + 'class' => esc_attr( $classes ), + 'style' => esc_attr( $style ), + ); + + ob_start(); + ?> +
    > +
      + + +
    • = $remaining_initial_unchecked ) : + ?> + class="wc-block-product-filter-checkbox-list__item" + data-wc-bind--hidden="!context.showAll" + hidden + + + + + class="wc-block-product-filter-checkbox-list__item" + > + +
    • + +
    + $show_initially ) : ?> + + +
    + context['filterData']; + $items = $context['items'] ?? array(); + $checkbox_list_context = array( 'items' => $items ); + $action = $context['action'] ?? ''; + $namespace = wp_json_encode( array( 'namespace' => 'woocommerce/product-filter-chips' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ); + + $tags = new \WP_HTML_Tag_Processor( $content ); + if ( $tags->next_tag( array( 'class_name' => 'wc-block-product-filter-chips' ) ) ) { + $classes = $tags->get_attribute( 'class' ); + $style = $tags->get_attribute( 'style' ); + } + + $checked_items = array_filter( + $items, + function ( $item ) { + return $item['selected']; + } + ); + $show_initially = $context['show_initially'] ?? 15; + $remaining_initial_unchecked = count( $checked_items ) > $show_initially ? count( $checked_items ) : $show_initially - count( $checked_items ); + $count = 0; + + $wrapper_attributes = array( + 'data-wc-interactive' => esc_attr( $namespace ), + 'data-wc-context' => wp_json_encode( $checkbox_list_context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), + 'class' => esc_attr( $classes ), + 'style' => esc_attr( $style ), + ); + + ob_start(); + ?> +
    > +
    + + + + +
    + $show_initially ) : ?> + + +
    + '!context.hasSelectedFilter', + 'data-wc-bind--hidden' => '!context.hasSelectedFilters', ) ); $p = new \WP_HTML_Tag_Processor( $content ); if ( $p->next_tag( array( 'class_name' => 'wp-block-button__link' ) ) ) { - $p->set_attribute( 'data-wc-on--click', 'actions.clear' ); + $p->set_attribute( 'data-wc-on--click', 'actions.clearFilters' ); $style = $p->get_attribute( 'style' ); $p->set_attribute( 'style', 'outline:none;' . $style ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php index 9c149ef6b3c..fdfa5f22452 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php @@ -23,6 +23,23 @@ class ProductFilters extends AbstractBlock { return array( 'postId' ); } + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = array() ) { + global $pagenow; + parent::enqueue_data( $attributes ); + + $this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() ); + $this->asset_data_registry->add( 'isProductArchive', is_shop() || is_product_taxonomy() ); + $this->asset_data_registry->add( 'isSiteEditor', 'site-editor.php' === $pagenow ); + $this->asset_data_registry->add( 'isWidgetEditor', 'widgets.php' === $pagenow || 'customize.php' === $pagenow ); + } + /** * Return the dialog content. * @@ -116,28 +133,86 @@ class ProductFilters extends AbstractBlock { * @return string Rendered block type output. */ protected function render( $attributes, $content, $block ) { - $html = $content; - $p = new \WP_HTML_Tag_Processor( $html ); - - if ( $p->next_tag() ) { - $p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-filters' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); - $p->set_attribute( + $tags = new \WP_HTML_Tag_Processor( $content ); + if ( $tags->next_tag() ) { + $tags->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/' . $this->block_name ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); + $tags->set_attribute( 'data-wc-context', wp_json_encode( array( 'isDialogOpen' => false, 'hasPageWithWordPressAdminBar' => false, + 'params' => $this->get_filter_query_params( 0 ), + 'originalParams' => $this->get_filter_query_params( 0 ), ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); - $html = $p->get_updated_html(); + $tags->set_attribute( 'data-wc-navigation-id', $this->generate_navigation_id( $block ) ); + $tags->set_attribute( 'data-wc-watch', 'callbacks.maybeNavigate' ); + + if ( + 'always' === $attributes['overlay'] || + ( 'mobile' === $attributes['overlay'] && wp_is_mobile() ) + ) { + return $this->inject_dialog( $tags->get_updated_html(), $this->render_dialog() ); + } + + return $tags->get_updated_html(); + } + } + + /** + * Generate a unique navigation ID for the block. + * + * @param mixed $block - Block instance. + * @return string - Unique navigation ID. + */ + private function generate_navigation_id( $block ) { + return sprintf( + 'wc-product-filters-%s', + md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) ) + ); + } + + /** + * Parse the filter parameters from the URL. + * For now we only get the global query params from the URL. In the future, + * we should get the query params based on $query_id. + * + * @param int $query_id Query ID. + * @return array Parsed filter params. + */ + private function get_filter_query_params( $query_id ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; + + $parsed_url = wp_parse_url( esc_url_raw( $request_uri ) ); + + if ( empty( $parsed_url['query'] ) ) { + return array(); } - $dialog_html = $this->render_dialog(); + parse_str( $parsed_url['query'], $url_query_params ); - $html = $this->inject_dialog( $html, $dialog_html ); + /** + * Filters the active filter data provided by filter blocks. + * + * @since 11.7.0 + * + * @param array $filter_param_keys The active filters data + * @param array $url_param_keys The query param parsed from the URL. + * + * @return array Active filters params. + */ + $filter_param_keys = array_unique( apply_filters( 'collection_filter_query_param_keys', array(), array_keys( $url_query_params ) ) ); - return $html; + return array_filter( + $url_query_params, + function ( $key ) use ( $filter_param_keys ) { + return in_array( $key, $filter_param_keys, true ); + }, + ARRAY_FILTER_USE_KEY + ); } } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php index 812a3baaf98..9d52abacc8a 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php @@ -110,11 +110,14 @@ class ProductGallery extends AbstractBlock { * @return string Rendered block type output. */ protected function render( $attributes, $content, $block ) { - $post_id = $block->context['postId'] ?? ''; + $post_id = $block->context['postId'] ?? ''; + $product = wc_get_product( $post_id ); + if ( ! $product instanceof \WC_Product ) { + return ''; + } + $product_gallery_images = ProductGalleryUtils::get_product_gallery_images( $post_id, 'thumbnail', array() ); $classname_single_image = ''; - // This is a temporary solution. We have to refactor this code when the block will have to be addable on every page/post https://github.com/woocommerce/woocommerce-blocks/issues/10882. - global $product; if ( count( $product_gallery_images ) < 2 ) { // The gallery consists of a single image. @@ -124,8 +127,6 @@ class ProductGallery extends AbstractBlock { $number_of_thumbnails = $block->attributes['thumbnailsNumberOfThumbnails'] ?? 0; $classname = $attributes['className'] ?? ''; $dialog = isset( $attributes['mode'] ) && 'full' !== $attributes['mode'] ? $this->render_dialog() : ''; - $post_id = $block->context['postId'] ?? ''; - $product = wc_get_product( $post_id ); $product_gallery_first_image = ProductGalleryUtils::get_product_gallery_image_ids( $product, 1 ); $product_gallery_first_image_id = reset( $product_gallery_first_image ); $product_id = strval( $product->get_id() ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/StoreNotices.php b/plugins/woocommerce/src/Blocks/BlockTypes/StoreNotices.php index c9c20d21548..9addff1a630 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/StoreNotices.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/StoreNotices.php @@ -45,17 +45,15 @@ class StoreNotices extends AbstractBlock { return; } - $classname = isset( $attributes['className'] ) ? $attributes['className'] : ''; $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); - if ( isset( $attributes['align'] ) ) { - $classname .= " align{$attributes['align']}"; - } - return sprintf( - '
    %3$s
    ', - esc_attr( $classes_and_styles['classes'] ), - esc_attr( $classname ), + '
    %2$s
    ', + get_block_wrapper_attributes( + array( + 'class' => 'wc-block-store-notices woocommerce ' . esc_attr( $classes_and_styles['classes'] ), + ) + ), wc_kses_notice( $notices ) ); } diff --git a/plugins/woocommerce/src/Blocks/BlockTypesController.php b/plugins/woocommerce/src/Blocks/BlockTypesController.php index 059152fd7fc..03e87ecbbad 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypesController.php +++ b/plugins/woocommerce/src/Blocks/BlockTypesController.php @@ -404,7 +404,6 @@ final class BlockTypesController { // Update plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md // when modifying this list. if ( Features::is_enabled( 'experimental-blocks' ) ) { - $block_types[] = 'ProductFilter'; $block_types[] = 'ProductFilters'; $block_types[] = 'ProductFiltersOverlay'; $block_types[] = 'ProductFiltersOverlayNavigation'; @@ -414,6 +413,8 @@ final class BlockTypesController { $block_types[] = 'ProductFilterRating'; $block_types[] = 'ProductFilterActive'; $block_types[] = 'ProductFilterClearButton'; + $block_types[] = 'ProductFilterCheckboxList'; + $block_types[] = 'ProductFilterChips'; $block_types[] = 'OrderConfirmation\CreateAccount'; } diff --git a/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php b/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php index 78c6cefe9e0..bb8eee65ae9 100644 --- a/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php +++ b/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php @@ -26,7 +26,7 @@ class ProductGalleryUtils { $product_gallery_images = array(); $product = wc_get_product( $post_id ); - if ( $product ) { + if ( $product instanceof \WC_Product ) { $all_product_gallery_image_ids = self::get_product_gallery_image_ids( $product ); if ( 'full' === $size || 'full' !== $size && count( $all_product_gallery_image_ids ) > 1 ) { diff --git a/plugins/woocommerce/src/Container.php b/plugins/woocommerce/src/Container.php index 56d3b5b1e98..17ceac62783 100644 --- a/plugins/woocommerce/src/Container.php +++ b/plugins/woocommerce/src/Container.php @@ -32,6 +32,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\BatchP use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\LayoutTemplatesServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ComingSoonServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\StatsServiceProvider; +use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ImportExportServiceProvider; /** * PSR11 compliant dependency injection container for WooCommerce. @@ -83,6 +84,7 @@ final class Container { EnginesServiceProvider::class, ComingSoonServiceProvider::class, StatsServiceProvider::class, + ImportExportServiceProvider::class, ); /** diff --git a/plugins/woocommerce/src/Internal/Admin/Homescreen.php b/plugins/woocommerce/src/Internal/Admin/Homescreen.php index 1a9cd90f776..dfb0cde522e 100644 --- a/plugins/woocommerce/src/Internal/Admin/Homescreen.php +++ b/plugins/woocommerce/src/Internal/Admin/Homescreen.php @@ -166,7 +166,6 @@ class Homescreen { 'homepage_layout', 'homepage_stats', 'task_list_tracked_started_tasks', - 'help_panel_highlight_shown', ) ); } diff --git a/plugins/woocommerce/src/Internal/Admin/ImportExport/CSVUploadHelper.php b/plugins/woocommerce/src/Internal/Admin/ImportExport/CSVUploadHelper.php new file mode 100644 index 00000000000..c1759bc5e93 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Admin/ImportExport/CSVUploadHelper.php @@ -0,0 +1,175 @@ +get_import_subdir_name(); + if ( $create ) { + FilesystemUtil::mkdir_p_not_indexable( $upload_dir ); + } + return $upload_dir; + } + + /** + * Handles a CSV file upload. + * + * @param string $import_type Type of upload or context. + * @param string $files_index $_FILES index that contains the file to upload. + * @param array|null $allowed_mime_types List of allowed MIME types. + * @return array { + * Details for the uploaded file. + * + * @type int $id Attachment ID. + * @type string $file Full path to uploaded file. + * } + * + * @throws \Exception In case of error. + */ + public function handle_csv_upload( string $import_type, string $files_index = 'import', ?array $allowed_mime_types = null ): array { + $import_type = sanitize_key( $import_type ); + if ( ! $import_type ) { + throw new \Exception( 'Import type is invalid.' ); + } + + if ( ! $allowed_mime_types ) { + $allowed_mime_types = array( + 'csv' => 'text/csv', + 'txt' => 'text/plain', + ); + } + + $file = $_FILES[ $files_index ] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing + if ( ! isset( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) { + throw new \Exception( esc_html__( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.', 'woocommerce' ) ); + } + + if ( ! function_exists( 'wp_import_handle_upload' ) ) { + require_once ABSPATH . 'wp-admin/includes/import.php'; + } + + // Make sure upload dir exists. + $this->get_import_dir(); + + // Add prefix. + $file['name'] = $import_type . '-' . $file['name']; + + $overrides_callback = function ( $overrides_ ) use ( $allowed_mime_types ) { + $overrides_['test_form'] = false; + $overrides_['test_type'] = true; + $overrides_['mimes'] = $allowed_mime_types; + return $overrides_; + }; + + self::add_filter( 'upload_dir', array( $this, 'override_upload_dir' ) ); + self::add_filter( 'wp_unique_filename', array( $this, 'override_unique_filename' ), 0, 2 ); + self::add_filter( 'wp_handle_upload_overrides', $overrides_callback, 999 ); + self::add_filter( 'wp_handle_upload_prefilter', array( $this, 'remove_txt_from_uploaded_file' ), 0 ); + + $orig_files_import = $_FILES['import'] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing + $_FILES['import'] = $file; // wp_import_handle_upload() expects the file to be in 'import'. + + $upload = wp_import_handle_upload(); + + remove_filter( 'upload_dir', array( $this, 'override_upload_dir' ) ); + remove_filter( 'wp_unique_filename', array( $this, 'override_unique_filename' ), 0 ); + remove_filter( 'wp_handle_upload_overrides', $overrides_callback, 999 ); + remove_filter( 'wp_handle_upload_prefilter', array( $this, 'remove_txt_from_uploaded_file' ), 0 ); + + if ( $orig_files_import ) { + $_FILES['import'] = $orig_files_import; + } else { + unset( $_FILES['import'] ); + } + + if ( ! empty( $upload['error'] ) ) { + throw new \Exception( esc_html( $upload['error'] ) ); + } + + if ( ! wc_is_file_valid_csv( $upload['file'], false ) ) { + wp_delete_attachment( $file['id'], true ); + throw new \Exception( esc_html__( 'Invalid file type for a CSV import.', 'woocommerce' ) ); + } + + return $upload; + } + + /** + * Hooked onto 'upload_dir' to override the default upload directory for a CSV upload. + * + * @param array $uploads WP upload dir details. + * @return array + */ + private function override_upload_dir( $uploads ): array { + $new_subdir = '/' . $this->get_import_subdir_name(); + + $uploads['path'] = $uploads['basedir'] . $new_subdir; + $uploads['url'] = $uploads['baseurl'] . $new_subdir; + $uploads['subdir'] = $new_subdir; + + return $uploads; + } + + /** + * Adds a random string to the name of an uploaded CSV file to make it less discoverable. Hooked onto 'wp_unique_filename'. + * + * @param string $filename File name. + * @param string $ext File extension. + * @return string + */ + private function override_unique_filename( string $filename, string $ext ): string { + $length = min( 10, 255 - strlen( $filename ) - 1 ); + if ( 1 < $length ) { + $suffix = strtolower( wp_generate_password( $length, false, false ) ); + $filename = substr( $filename, 0, strlen( $filename ) - strlen( $ext ) ) . '-' . $suffix . $ext; + } + + return $filename; + } + + /** + * `wp_import_handle_upload()` appends .txt to any file name. This function is hooked onto 'wp_handle_upload_prefilter' + * to remove those extra characters. + * + * @param array $file File details in the form of a $_FILES entry. + * @return array Modified file details. + */ + private function remove_txt_from_uploaded_file( array $file ): array { + $file['name'] = substr( $file['name'], 0, -4 ); + return $file; + } +} diff --git a/plugins/woocommerce/src/Internal/Brands.php b/plugins/woocommerce/src/Internal/Brands.php new file mode 100644 index 00000000000..7ac3cdbab05 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Brands.php @@ -0,0 +1,61 @@ + '', 'month' => '', 'day' => '', @@ -373,10 +373,12 @@ class OrdersTableQuery { /** * Returns UTC-based date query arguments for a combination of local time dates and a date shorthand operator. * - * @param array $dates_raw Array of dates (in local time) to use in combination with the operator. + * @param array $dates_raw Array of dates (in local time) to use in combination with the operator. * @param string $operator One of the operators supported by date queries (<, <=, =, ..., >, >=). * @return array Partial date query arg with relevant dates now UTC-based. * + * @throws \Exception If an invalid date shorthand operator is specified. + * * @since 8.2.0 */ private function local_time_to_gmt_date_query( $dates_raw, $operator ) { @@ -387,7 +389,7 @@ class OrdersTableQuery { $raw_date = is_numeric( $raw_date ) ? $raw_date : strtotime( get_gmt_from_date( date( 'Y-m-d', strtotime( $raw_date ) ) ) ); } - $date1 = end( $dates_raw ); + $date1 = end( $dates_raw ); switch ( $operator ) { case '>': @@ -410,9 +412,9 @@ class OrdersTableQuery { 'inclusive' => true, ), array( - 'before' => $this->date_to_date_query_arg( $date1 + DAY_IN_SECONDS ), - 'inclusive' => false, - ) + 'before' => $this->date_to_date_query_arg( $date1 + DAY_IN_SECONDS ), + 'inclusive' => false, + ), ); break; case '<=': @@ -474,7 +476,6 @@ class OrdersTableQuery { foreach ( $date_keys as $date_key ) { $is_local = in_array( $date_key, $local_date_keys, true ); $date_value = $this->args[ $date_key ]; - $operator = '='; $dates_raw = array(); $dates = array(); diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/ImportExportServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/ImportExportServiceProvider.php new file mode 100644 index 00000000000..58984a41fbf --- /dev/null +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/ImportExportServiceProvider.php @@ -0,0 +1,35 @@ +share( CSVUploadHelper::class ); + } +} diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php index 1b9b4515c33..e777fc63fb8 100644 --- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php +++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php @@ -241,7 +241,7 @@ class FeaturesController { 'Enable this feature to log errors and related data to Automattic servers for debugging purposes and to improve WooCommerce', 'woocommerce' ), - 'enabled_by_default' => false, + 'enabled_by_default' => true, 'disable_ui' => true, 'is_legacy' => false, 'is_experimental' => true, diff --git a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php index 3043cd110fe..19a04d84fec 100644 --- a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php +++ b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php @@ -5,8 +5,10 @@ namespace Automattic\WooCommerce\Internal\Logging; use Automattic\WooCommerce\Utilities\FeaturesUtil; use Automattic\WooCommerce\Utilities\StringUtil; +use Automattic\WooCommerce\Internal\McStats; use WC_Rate_Limiter; use WC_Log_Levels; +use Jetpack_Options; /** * WooCommerce Remote Logger @@ -71,9 +73,16 @@ class RemoteLogger extends \WC_Log_Handler { 'php_version' => phpversion(), 'wp_version' => get_bloginfo( 'version' ), 'request_uri' => $this->sanitize_request_uri( filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL ) ), + 'store_id' => get_option( \WC_Install::STORE_ID_OPTION, null ), ), ); + $blog_id = class_exists( 'Jetpack_Options' ) ? Jetpack_Options::get_option( 'id' ) : null; + + if ( ! empty( $blog_id ) && is_int( $blog_id ) ) { + $log_data['blog_id'] = $blog_id; + } + if ( isset( $context['backtrace'] ) ) { if ( is_array( $context['backtrace'] ) || is_string( $context['backtrace'] ) ) { $log_data['trace'] = $this->sanitize_trace( $context['backtrace'] ); @@ -88,25 +97,15 @@ class RemoteLogger extends \WC_Log_Handler { unset( $context['tags'] ); } - if ( class_exists( '\WC_Tracks' ) ) { - $user = wp_get_current_user(); - $blog_details = \WC_Tracks::get_blog_details( $user->ID ); - - if ( is_numeric( $blog_details['blog_id'] ) && $blog_details['blog_id'] > 0 ) { - $log_data['blog_id'] = $blog_details['blog_id']; - } - - if ( ! empty( $blog_details['store_id'] ) ) { - $log_data['properties']['store_id'] = $blog_details['store_id']; - } - } - - if ( isset( $context['error'] ) && is_array( $context['error'] ) && ! empty( $context['error']['file'] ) ) { - $context['error']['file'] = $this->sanitize( $context['error']['file'] ); + if ( isset( $context['error']['file'] ) && is_string( $context['error']['file'] ) && '' !== $context['error']['file'] ) { + $log_data['file'] = $this->sanitize( $context['error']['file'] ); + unset( $context['error']['file'] ); } $extra_attrs = $context['extra'] ?? array(); unset( $context['extra'] ); + unset( $context['remote-logging'] ); + // Merge the extra attributes with the remaining context since we can't send arbitrary fields to Logstash. $log_data['extra'] = array_merge( $extra_attrs, $context ); @@ -166,9 +165,15 @@ class RemoteLogger extends \WC_Log_Handler { * @return bool True if the log should be handled. */ protected function should_handle( $level, $message, $context ) { + // Ignore logs that are not opted in for remote logging. + if ( ! isset( $context['remote-logging'] ) || false === $context['remote-logging'] ) { + return false; + } + if ( ! $this->is_remote_logging_allowed() ) { return false; } + // Ignore logs that are less severe than critical. This is temporary to prevent sending too many logs to the remote logging service. We can consider remove this if the remote logging service can handle more logs. if ( WC_Log_Levels::get_level_severity( $level ) < WC_Log_Levels::get_level_severity( WC_Log_Levels::CRITICAL ) ) { return false; @@ -178,6 +183,15 @@ class RemoteLogger extends \WC_Log_Handler { return false; } + try { + // Record fatal error stats. + $mc_stats = wc_get_container()->get( McStats::class ); + $mc_stats->add( 'error', 'critical-errors' ); + $mc_stats->do_server_side_stats(); + } catch ( \Throwable $e ) { + error_log( 'Warning: Failed to record fatal error stats: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + } + if ( WC_Rate_Limiter::retried_too_soon( self::RATE_LIMIT_ID ) ) { error_log( 'Remote logging throttled.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log return false; @@ -233,7 +247,7 @@ class RemoteLogger extends \WC_Log_Handler { } return true; - } catch ( \Exception $e ) { + } catch ( \Throwable $e ) { // Log the error locally if the remote logging fails. error_log( 'Remote logging failed: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log return false; @@ -358,7 +372,7 @@ class RemoteLogger extends \WC_Log_Handler { * * The trace is sanitized by: * - * 1. Remove the absolute path to the WooCommerce plugin directory. + * 1. Remove the absolute path to the plugin directory based on WC_ABSPATH. This is more accurate than using WP_PLUGIN_DIR when the plugin is symlinked. * 2. Remove the absolute path to the WordPress root directory. * * For example, the trace: @@ -374,12 +388,12 @@ class RemoteLogger extends \WC_Log_Handler { return $message; } - $wc_path = StringUtil::normalize_local_path_slashes( WC_ABSPATH ); - $wp_path = StringUtil::normalize_local_path_slashes( ABSPATH ); + $plugin_path = StringUtil::normalize_local_path_slashes( trailingslashit( dirname( WC_ABSPATH ) ) ); + $wp_path = StringUtil::normalize_local_path_slashes( trailingslashit( ABSPATH ) ); $sanitized = str_replace( - array( $wc_path, $wp_path ), - array( '**/' . dirname( WC_PLUGIN_BASENAME ) . '/', '**/' ), + array( $plugin_path, $wp_path ), + array( './', './' ), $message ); diff --git a/plugins/woocommerce/src/Internal/McStats.php b/plugins/woocommerce/src/Internal/McStats.php index 89320e1ee6a..2f05d355451 100644 --- a/plugins/woocommerce/src/Internal/McStats.php +++ b/plugins/woocommerce/src/Internal/McStats.php @@ -60,4 +60,17 @@ class McStats extends A8c_Mc_Stats { return parent::do_server_side_stat( $url ); } + + /** + * Pings the stats server for the current stats and empty the stored stats from the object + * + * @return void + */ + public function do_server_side_stats() { + if ( ! \WC_Site_Tracking::is_tracking_enabled() ) { + return; + } + + parent::do_server_side_stats(); + } } diff --git a/plugins/woocommerce/src/Internal/Utilities/FilesystemUtil.php b/plugins/woocommerce/src/Internal/Utilities/FilesystemUtil.php index 4bac73265b3..3b91fcbc721 100644 --- a/plugins/woocommerce/src/Internal/Utilities/FilesystemUtil.php +++ b/plugins/woocommerce/src/Internal/Utilities/FilesystemUtil.php @@ -31,6 +31,36 @@ class FilesystemUtil { return $wp_filesystem; } + /** + * Recursively creates a directory (if it doesn't exist) and adds an empty index.html and a .htaccess to prevent + * directory listing. + * + * @since 9.3.0 + * + * @param string $path Directory to create. + * @throws \Exception In case of error. + */ + public static function mkdir_p_not_indexable( string $path ): void { + $wp_fs = self::get_wp_filesystem(); + + if ( $wp_fs->is_dir( $path ) ) { + return; + } + + if ( ! wp_mkdir_p( $path ) ) { + throw new \Exception( esc_html( sprintf( 'Could not create directory: %s.', wp_basename( $path ) ) ) ); + } + + $files = array( + '.htaccess' => 'deny from all', + 'index.html' => '', + ); + + foreach ( $files as $name => $content ) { + $wp_fs->put_contents( trailingslashit( $path ) . $name, $content ); + } + } + /** * Wrapper to initialize the WP filesystem with defined credentials if they are available. * diff --git a/plugins/woocommerce/src/Packages.php b/plugins/woocommerce/src/Packages.php index b9255a4580c..eb251bbc908 100644 --- a/plugins/woocommerce/src/Packages.php +++ b/plugins/woocommerce/src/Packages.php @@ -37,23 +37,43 @@ class Packages { * initialization for the now-merged feature plugin. * * Once a package has been merged into WooCommerce Core it should have its slug added here. This will ensure - * that we deactivate the feature plugin automaticatlly to prevent any problems caused by conflicts between + * that we deactivate the feature plugin automatically to prevent any problems caused by conflicts between * the two versions caused by them both being active. * + * The packages included in this array cannot be deactivated and will always load with WooCommerce core. + * + * @var array Key is the package name/directory, value is the main package class which handles init. + */ + protected static $base_packages = array( + 'woocommerce-admin' => '\\Automattic\\WooCommerce\\Admin\\Composer\\Package', + 'woocommerce-gutenberg-products-block' => '\\Automattic\\WooCommerce\\Blocks\\Package', + ); + + /** + * Similar to $base_packages, but + * the packages included in this array can be deactivated via the 'woocommerce_merged_packages' filter. + * * @var array Key is the package name/directory, value is the main package class which handles init. */ protected static $merged_packages = array( - 'woocommerce-admin' => '\\Automattic\\WooCommerce\\Admin\\Composer\\Package', - 'woocommerce-gutenberg-products-block' => '\\Automattic\\WooCommerce\\Blocks\\Package', + 'woocommerce-brands' => '\\Automattic\\WooCommerce\\Internal\\Brands', ); + /** * Init the package loader. * * @since 3.7.0 */ public static function init() { - add_action( 'plugins_loaded', array( __CLASS__, 'on_init' ) ); + add_action( 'plugins_loaded', array( __CLASS__, 'on_init' ), 0 ); + + // Prevent plugins already merged into WooCommerce core from getting activated as standalone plugins. + add_action( 'activate_plugin', array( __CLASS__, 'deactivate_merged_plugins' ) ); + + // Display a notice in the Plugins tab next to plugins already merged into WooCommerce core. + add_filter( 'all_plugins', array( __CLASS__, 'mark_merged_plugins_as_pending_update' ), 10, 1 ); + add_action( 'after_plugin_row', array( __CLASS__, 'display_notice_for_merged_plugins' ), 10, 1 ); } /** @@ -74,6 +94,61 @@ class Packages { return file_exists( dirname( __DIR__ ) . '/packages/' . $package ); } + /** + * Checks a package exists by looking for it's directory. + * + * @param string $class_name Class name. + * @return boolean + */ + public static function should_load_class( $class_name ) { + + foreach ( self::$merged_packages as $merged_package_name => $merged_package_class ) { + if ( str_replace( 'woocommerce-', 'wc_', $merged_package_name ) === $class_name ) { + return true; + } + } + + return false; + } + + /** + * Gets all merged, enabled packages. + * + * @return array + */ + protected static function get_enabled_packages() { + $enabled_packages = array(); + + foreach ( self::$merged_packages as $merged_package_name => $package_class ) { + + // For gradual rollouts, ensure that a package is enabled for user's remote variant number. + $experimental_package_enabled = method_exists( $package_class, 'is_enabled' ) ? + call_user_func( array( $package_class, 'is_enabled' ) ) : + false; + + if ( ! $experimental_package_enabled ) { + continue; + } + + $option = 'wc_feature_' . str_replace( '-', '_', $merged_package_name ) . '_enabled'; + if ( 'yes' === get_option( $option, 'no' ) ) { + $enabled_packages[ $merged_package_name ] = $package_class; + } + } + + return array_merge( $enabled_packages, self::$base_packages ); + } + + /** + * Checks if a package is enabled. + * + * @param string $package Package name. + * @return boolean + */ + public static function is_package_enabled( $package ) { + return array_key_exists( $package, self::get_enabled_packages() ); + } + /** * Deactivates merged feature plugins. * @@ -93,7 +168,8 @@ class Packages { // Deactivate the plugin if possible so that there are no conflicts. foreach ( $active_plugins as $active_plugin_path ) { $plugin_file = basename( plugin_basename( $active_plugin_path ), '.php' ); - if ( ! isset( self::$merged_packages[ $plugin_file ] ) ) { + + if ( ! self::is_package_enabled( $plugin_file ) ) { continue; } @@ -107,7 +183,7 @@ class Packages { function() use ( $plugin_data ) { echo '

    '; printf( - /* translators: %s: is referring to the plugin's name. */ + /* translators: %s: is referring to the plugin's name. */ esc_html__( 'The %1$s plugin has been deactivated as the latest improvements are now included with the %2$s plugin.', 'woocommerce' ), '' . esc_html( $plugin_data['Name'] ) . '', 'WooCommerce' @@ -118,13 +194,71 @@ class Packages { } } + /** + * Prevent plugins already merged into WooCommerce core from getting activated as standalone plugins. + * + * @param string $plugin Plugin name. + */ + public static function deactivate_merged_plugins( $plugin ) { + $plugin_dir = basename( dirname( $plugin ) ); + + if ( self::is_package_enabled( $plugin_dir ) ) { + $plugins_url = esc_url( admin_url( 'plugins.php' ) ); + wp_die( + esc_html__( 'This plugin cannot be activated because its functionality is now included in WooCommerce core.', 'woocommerce' ), + esc_html__( 'Plugin Activation Error', 'woocommerce' ), + array( + 'link_url' => esc_url( $plugins_url ), + 'link_text' => esc_html__( 'Return to the Plugins page', 'woocommerce' ), + ), + ); + } + } + + /** + * Mark merged plugins as pending update. + * This is required for correctly displaying maintenance notices. + * + * @param array $plugins Plugins list. + */ + public static function mark_merged_plugins_as_pending_update( $plugins ) { + foreach ( $plugins as $plugin_name => $plugin_data ) { + $plugin_dir = basename( dirname( $plugin_name ) ); + if ( self::is_package_enabled( $plugin_dir ) ) { + // Necessary to properly display notice within row. + $plugins[ $plugin_name ]['update'] = 1; + } + } + return $plugins; + } + + /** + * Displays a maintenance notice next to merged plugins, to inform users + * that the plugin functionality is now offered by WooCommerce core. + * + * Requires 'mark_merged_plugins_as_pending_update' to properly display this notice. + * + * @param string $plugin_file Plugin file. + */ + public static function display_notice_for_merged_plugins( $plugin_file ) { + global $wp_list_table; + + $plugin_dir = basename( dirname( $plugin_file ) ); + $columns_count = $wp_list_table->get_column_count(); + $notice = __( 'This plugin can no longer be activated because its functionality is now included in WooCommerce. It is recommended to delete it.', 'woocommerce' ); + + if ( self::is_package_enabled( $plugin_dir ) ) { + echo '

    ' . wp_kses_post( $notice ) . '

    '; + } + } + /** * Loads packages after plugins_loaded hook. * * Each package should include an init file which loads the package so it can be used by core. */ protected static function initialize_packages() { - foreach ( self::$merged_packages as $package_name => $package_class ) { + foreach ( self::get_enabled_packages() as $package_name => $package_class ) { call_user_func( array( $package_class, 'init' ) ); } @@ -172,7 +306,7 @@ class Packages { } add_action( 'admin_notices', - function() use ( $package ) { + function () use ( $package ) { ?>

    diff --git a/plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php b/plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php index 25f915d2862..3fce3214ab0 100644 --- a/plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php +++ b/plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php @@ -16,7 +16,7 @@ class ProductQuery { * @return array */ public function prepare_objects_query( $request ) { - $args = [ + $args = array( 'offset' => $request['offset'], 'order' => $request['order'], 'orderby' => $request['orderby'], @@ -31,17 +31,17 @@ class ProductQuery { 'fields' => 'ids', 'ignore_sticky_posts' => true, 'post_status' => 'publish', - 'date_query' => [], + 'date_query' => array(), 'post_type' => 'product', - ]; + ); // If searching for a specific SKU or slug, allow any post type. if ( ! empty( $request['sku'] ) || ! empty( $request['slug'] ) ) { - $args['post_type'] = [ 'product', 'product_variation' ]; + $args['post_type'] = array( 'product', 'product_variation' ); } // Taxonomy query to filter products by type, category, tag, shipping class, and attribute. - $tax_query = []; + $tax_query = array(); // Filter product type by slug. if ( ! empty( $request['type'] ) ) { @@ -49,11 +49,11 @@ class ProductQuery { $args['post_type'] = 'product_variation'; } else { $args['post_type'] = 'product'; - $tax_query[] = [ + $tax_query[] = array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $request['type'], - ]; + ); } } @@ -77,12 +77,12 @@ class ProductQuery { } // Set custom args to handle later during clauses. - $custom_keys = [ + $custom_keys = array( 'sku', 'min_price', 'max_price', 'stock_status', - ]; + ); foreach ( $custom_keys as $key ) { if ( ! empty( $request[ $key ] ) ) { @@ -90,11 +90,11 @@ class ProductQuery { } } - $operator_mapping = [ + $operator_mapping = array( 'in' => 'IN', 'not_in' => 'NOT IN', 'and' => 'AND', - ]; + ); // Gets all registered product taxonomies and prefixes them with `tax_`. // This is needed to avoid situations where a user registers a new product taxonomy with the same name as default field. @@ -107,10 +107,10 @@ class ProductQuery { ); // Map between taxonomy name and arg key. - $default_taxonomies = [ + $default_taxonomies = array( 'product_cat' => 'category', 'product_tag' => 'tag', - ]; + ); $taxonomies = array_merge( $all_product_taxonomies, $default_taxonomies ); @@ -118,18 +118,18 @@ class ProductQuery { foreach ( $taxonomies as $taxonomy => $key ) { if ( ! empty( $request[ $key ] ) ) { $operator = $request->get_param( $key . '_operator' ) && isset( $operator_mapping[ $request->get_param( $key . '_operator' ) ] ) ? $operator_mapping[ $request->get_param( $key . '_operator' ) ] : 'IN'; - $tax_query[] = [ + $tax_query[] = array( 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $request[ $key ], 'operator' => $operator, - ]; + ); } } // Filter by attributes. if ( ! empty( $request['attributes'] ) ) { - $att_queries = []; + $att_queries = array(); foreach ( $request['attributes'] as $attribute ) { if ( empty( $attribute['term_id'] ) && empty( $attribute['slug'] ) ) { @@ -137,22 +137,22 @@ class ProductQuery { } if ( in_array( $attribute['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { $operator = isset( $attribute['operator'], $operator_mapping[ $attribute['operator'] ] ) ? $operator_mapping[ $attribute['operator'] ] : 'IN'; - $att_queries[] = [ + $att_queries[] = array( 'taxonomy' => $attribute['attribute'], 'field' => ! empty( $attribute['term_id'] ) ? 'term_id' : 'slug', 'terms' => ! empty( $attribute['term_id'] ) ? $attribute['term_id'] : $attribute['slug'], 'operator' => $operator, - ]; + ); } } if ( 1 < count( $att_queries ) ) { // Add relation arg when using multiple attributes. $relation = $request->get_param( 'attribute_relation' ) && isset( $operator_mapping[ $request->get_param( 'attribute_relation' ) ] ) ? $operator_mapping[ $request->get_param( 'attribute_relation' ) ] : 'IN'; - $tax_query[] = [ + $tax_query[] = array( 'relation' => $relation, $att_queries, - ]; + ); } else { $tax_query = array_merge( $tax_query, $att_queries ); } @@ -176,12 +176,12 @@ class ProductQuery { // Filter featured. if ( is_bool( $request['featured'] ) ) { - $args['tax_query'][] = [ + $args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'name', 'terms' => 'featured', 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', - ]; + ); } // Filter by on sale products. @@ -190,7 +190,7 @@ class ProductQuery { $on_sale_ids = wc_get_product_ids_on_sale(); // Use 0 when there's no on sale products to avoid return all products. - $on_sale_ids = empty( $on_sale_ids ) ? [ 0 ] : $on_sale_ids; + $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; $args[ $on_sale_key ] += $on_sale_ids; } @@ -203,25 +203,25 @@ class ProductQuery { $exclude_from_catalog = 'search' === $catalog_visibility ? '' : 'exclude-from-catalog'; $exclude_from_search = 'catalog' === $catalog_visibility ? '' : 'exclude-from-search'; - $args['tax_query'][] = [ + $args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'name', - 'terms' => [ $exclude_from_catalog, $exclude_from_search ], + 'terms' => array( $exclude_from_catalog, $exclude_from_search ), 'operator' => 'hidden' === $catalog_visibility ? 'AND' : 'NOT IN', 'rating_filter' => true, - ]; + ); } if ( $rating ) { - $rating_terms = []; + $rating_terms = array(); foreach ( $rating as $value ) { $rating_terms[] = 'rated-' . $value; } - $args['tax_query'][] = [ + $args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'name', 'terms' => $rating_terms, - ]; + ); } $orderby = $request->get_param( 'orderby' ); @@ -283,7 +283,7 @@ class ProductQuery { public function get_results( $request ) { $query_args = $this->prepare_objects_query( $request ); - add_filter( 'posts_clauses', [ $this, 'add_query_clauses' ], 10, 2 ); + add_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10, 2 ); $query = new \WP_Query(); $results = $query->query( $query_args ); @@ -297,13 +297,13 @@ class ProductQuery { $total_posts = $count_query->found_posts; } - remove_filter( 'posts_clauses', [ $this, 'add_query_clauses' ], 10 ); + remove_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10 ); - return [ + return array( 'results' => $results, 'total' => (int) $total_posts, 'pages' => $query->query_vars['posts_per_page'] > 0 ? (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ) : 1, - ]; + ); } /** @@ -315,11 +315,11 @@ class ProductQuery { public function get_objects( $request ) { $results = $this->get_results( $request ); - return [ + return array( 'objects' => array_map( 'wc_get_product', $results['results'] ), 'total' => $results['total'], 'pages' => $results['pages'], - ]; + ); } /** @@ -442,7 +442,7 @@ class ProductQuery { return ''; } - $or_queries = []; + $or_queries = array(); // We need to adjust the filter for each possible tax class and combine the queries into one. foreach ( $product_tax_classes as $tax_class ) { diff --git a/plugins/woocommerce/templates/brands/brand-description.php b/plugins/woocommerce/templates/brands/brand-description.php new file mode 100644 index 00000000000..a72a251a3f6 --- /dev/null +++ b/plugins/woocommerce/templates/brands/brand-description.php @@ -0,0 +1,35 @@ + +

    + + + + Thumbnail + + + +
    + + + +
    + +
    diff --git a/plugins/woocommerce/templates/brands/shortcodes/brands-a-z.php b/plugins/woocommerce/templates/brands/shortcodes/brands-a-z.php new file mode 100644 index 00000000000..ef2d9042a5f --- /dev/null +++ b/plugins/woocommerce/templates/brands/shortcodes/brands-a-z.php @@ -0,0 +1,63 @@ + +
    + + + + + +

    + +
      + %s', + esc_url( get_term_link( $brand->slug, 'product_brand' ) ), + esc_html( $brand->name ) + ); + } + ?> +
    + + + + + + + +
    diff --git a/plugins/woocommerce/templates/brands/shortcodes/single-brand.php b/plugins/woocommerce/templates/brands/shortcodes/single-brand.php new file mode 100644 index 00000000000..556ae2055e9 --- /dev/null +++ b/plugins/woocommerce/templates/brands/shortcodes/single-brand.php @@ -0,0 +1,38 @@ + + + <?php echo esc_attr( $term->name ); ?> + diff --git a/plugins/woocommerce/templates/brands/taxonomy-product_brand.php b/plugins/woocommerce/templates/brands/taxonomy-product_brand.php new file mode 100644 index 00000000000..56898bf0cb3 --- /dev/null +++ b/plugins/woocommerce/templates/brands/taxonomy-product_brand.php @@ -0,0 +1,12 @@ + +
      + + $brand ) : + + /** + * Filter the brand's thumbnail size. + * + * @since 9.4.0 + * @param string $size Defaults to 'shop_catalog' + */ + $thumbnail = wc_get_brand_thumbnail_url( $brand->term_id, apply_filters( 'woocommerce_brand_thumbnail_size', 'shop_catalog' ) ); + + if ( ! $thumbnail ) { + $thumbnail = wc_placeholder_img_src(); + } + + $class = ''; + + if ( 0 === $index || 0 === $index % $columns ) { + $class = 'first'; + } elseif ( 0 === ( $index + 1 ) % $columns ) { + $class = 'last'; + } + + $width = floor( ( ( 100 - ( ( $columns - 1 ) * 2 ) ) / $columns ) * 100 ) / 100; + ?> +
    • + + <?php echo esc_attr( $brand->name ); ?> + +
      + description ) ) ); ?> +
      +
    • + + + +
    diff --git a/plugins/woocommerce/templates/brands/widgets/brand-thumbnails.php b/plugins/woocommerce/templates/brands/widgets/brand-thumbnails.php new file mode 100644 index 00000000000..bbfdf43f236 --- /dev/null +++ b/plugins/woocommerce/templates/brands/widgets/brand-thumbnails.php @@ -0,0 +1,45 @@ + +
      + + $brand ) : + $class = ''; + if ( 0 === $index || 0 === $index % $columns ) { + $class = 'first'; + } elseif ( 0 === ( $index + 1 ) % $columns ) { + $class = 'last'; + } + ?> + +
    • + + + +
    • + + + +
    diff --git a/plugins/woocommerce/templates/cart/mini-cart.php b/plugins/woocommerce/templates/cart/mini-cart.php index ecf51eb96eb..27ae9454260 100644 --- a/plugins/woocommerce/templates/cart/mini-cart.php +++ b/plugins/woocommerce/templates/cart/mini-cart.php @@ -14,7 +14,7 @@ * * @see https://woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 9.2.0 + * @version 9.3.0 */ defined( 'ABSPATH' ) || exit; diff --git a/plugins/woocommerce/templates/content-product.php b/plugins/woocommerce/templates/content-product.php index 7423164e81c..b3bc12ad92c 100644 --- a/plugins/woocommerce/templates/content-product.php +++ b/plugins/woocommerce/templates/content-product.php @@ -12,15 +12,15 @@ * * @see https://woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.6.0 + * @version 9.4.0 */ defined( 'ABSPATH' ) || exit; global $product; -// Ensure visibility. -if ( empty( $product ) || ! $product->is_visible() ) { +// Check if the product is a valid WooCommerce product and ensure its visibility before proceeding. +if ( ! is_a( $product, WC_Product::class ) || ! $product->is_visible() ) { return; } ?> diff --git a/plugins/woocommerce/templates/parts/product-filters.html b/plugins/woocommerce/templates/parts/product-filters.html index 9e699ccd71d..a4e82272829 100644 --- a/plugins/woocommerce/templates/parts/product-filters.html +++ b/plugins/woocommerce/templates/parts/product-filters.html @@ -1,79 +1,28 @@ - -
    - +
    + +

    Filters

    + -
    -

    Filters

    - + - - -

    Active

    - + - - - - - -
    -

    Price

    - - - - -
    - -
    - -
    - - - - - - - -
    -

    Status

    - - - - -
    - -
    - -
    - - - - - - - - - -
    -

    Rating

    - - - - -
    - -
    - -
    - - - - - - -
    - -
    -
    + +
    + +
    + Apply +
    + +
    + +
    diff --git a/plugins/woocommerce/templates/templates/blockified/taxonomy-product_brand.html b/plugins/woocommerce/templates/templates/blockified/taxonomy-product_brand.html new file mode 100644 index 00000000000..aa23a7d2ccc --- /dev/null +++ b/plugins/woocommerce/templates/templates/blockified/taxonomy-product_brand.html @@ -0,0 +1,42 @@ + + + +
    + + + + + + + + + +
    + + +
    + + +
    + + + + + + + + + + + + + + + + +
    + +
    + + + diff --git a/plugins/woocommerce/templates/templates/taxonomy-product_brand.html b/plugins/woocommerce/templates/templates/taxonomy-product_brand.html new file mode 100644 index 00000000000..4cf01077d40 --- /dev/null +++ b/plugins/woocommerce/templates/templates/taxonomy-product_brand.html @@ -0,0 +1,5 @@ + + +
    + + diff --git a/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/playwright.config.js index 259c06b7fc6..4de1709668c 100644 --- a/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/playwright.config.js +++ b/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/playwright.config.js @@ -16,6 +16,7 @@ config = { '**/customize-store/**/*.spec.js', '**/merchant/**/*.spec.js', '**/shopper/**/*.spec.js', + '**/api-tests/**/*.test.js', ], grepInvert: /@skip-on-default-pressable/, }, 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 62169c1d0dc..27ec548c34a 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 @@ -7,7 +7,23 @@ config = { { name: 'default wpcom', use: { ...devices[ 'Desktop Chrome' ] }, - testMatch: [ '**/basic.spec.js', '**/shopper/**/*.spec.js' ], + testMatch: [ + '**/basic.spec.js', + '**/activate-and-setup/**/*.spec.js', + '**/admin-analytics/**/*.spec.js', + '**/admin-marketing/**/*.spec.js', + '**/admin-tasks/**/*.spec.js', + '**/shopper/**/*.spec.js', + '**/api-tests/**/*.test.js', + '**/merchant/products/add-variable-product/**/*.spec.js', + '**/merchant/command-palette.spec.js', + '**/merchant/create-cart-block.spec.js', + '**/merchant/create-checkout-block.spec.js', + '**/merchant/create-coupon.spec.js', + '**/merchant/create-order.spec.js', + '**/merchant/create-page.spec.js', + '**/merchant/create-post.spec.js', + ], grepInvert: /@skip-on-default-wpcom/, }, ], diff --git a/plugins/woocommerce/tests/e2e-pw/tests/activate-and-setup/core-profiler.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/activate-and-setup/core-profiler.spec.js index f0cf8016e40..a4679791f03 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/activate-and-setup/core-profiler.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/activate-and-setup/core-profiler.spec.js @@ -3,7 +3,7 @@ const { setOption } = require( '../../utils/options' ); test.describe( 'Store owner can complete the core profiler', - { tag: '@skip-on-default-pressable' }, + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, () => { test.use( { storageState: process.env.ADMINSTATE } ); @@ -450,7 +450,7 @@ test.describe( test.describe( 'Store owner can skip the core profiler', - { tag: '@skip-on-default-pressable' }, + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, () => { test.use( { storageState: process.env.ADMINSTATE } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/admin-analytics/analytics-data.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/admin-analytics/analytics-data.spec.js index 6cfd1d35fd2..9d8ebd604a9 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/admin-analytics/analytics-data.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/admin-analytics/analytics-data.spec.js @@ -25,7 +25,14 @@ const test = baseTest.extend( { test.describe( 'Analytics-related tests', - { tag: [ '@payments', '@services', '@skip-on-default-pressable' ] }, + { + tag: [ + '@payments', + '@services', + '@skip-on-default-pressable', + '@skip-on-default-wpcom', + ], + }, () => { let categoryIds, productIds, orderIds, setupPage; diff --git a/plugins/woocommerce/tests/e2e-pw/tests/admin-marketing/overview.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/admin-marketing/overview.spec.js index 2ababaf97ec..773f160caf2 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/admin-marketing/overview.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/admin-marketing/overview.spec.js @@ -15,40 +15,44 @@ test.describe( 'Marketing page', () => { ).toBeVisible(); } ); - test( 'Marketing Overview page have relevant content', async ( { - page, - } ) => { - // Go to the Marketing page. - await page.goto( 'wp-admin/admin.php?page=wc-admin&path=%2Fmarketing' ); + test( + 'Marketing Overview page have relevant content', + { tag: '@skip-on-default-wpcom' }, + async ( { page } ) => { + // Go to the Marketing page. + await page.goto( + 'wp-admin/admin.php?page=wc-admin&path=%2Fmarketing' + ); - // Heading should be overview - await expect( - page.getByRole( 'heading', { name: 'Overview' } ) - ).toBeVisible(); + // Heading should be overview + await expect( + page.getByRole( 'heading', { name: 'Overview' } ) + ).toBeVisible(); - // Sections present - await expect( - page.getByText( 'Channels', { exact: true } ) - ).toBeVisible(); - await expect( - page.getByText( 'Discover more marketing tools' ) - ).toBeVisible(); - await expect( - page.getByRole( 'tab', { name: 'Email' } ) - ).toBeVisible(); - await expect( - page.getByRole( 'tab', { name: 'Automations' } ) - ).toBeVisible(); - await expect( - page.getByRole( 'tab', { name: 'Conversion' } ) - ).toBeVisible(); - await expect( - page.getByRole( 'tab', { name: 'CRM', exact: true } ) - ).toBeVisible(); - await expect( - page.getByText( 'Learn about marketing a store' ) - ).toBeVisible(); - } ); + // Sections present + await expect( + page.getByText( 'Channels', { exact: true } ) + ).toBeVisible(); + await expect( + page.getByText( 'Discover more marketing tools' ) + ).toBeVisible(); + await expect( + page.getByRole( 'tab', { name: 'Email' } ) + ).toBeVisible(); + await expect( + page.getByRole( 'tab', { name: 'Automations' } ) + ).toBeVisible(); + await expect( + page.getByRole( 'tab', { name: 'Conversion' } ) + ).toBeVisible(); + await expect( + page.getByRole( 'tab', { name: 'CRM', exact: true } ) + ).toBeVisible(); + await expect( + page.getByText( 'Learn about marketing a store' ) + ).toBeVisible(); + } + ); test( 'Introduction can be dismissed', diff --git a/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js index eec70d721a8..035b63be270 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js @@ -73,7 +73,7 @@ test.describe( 'Payment setup task', () => { await page .locator( '//input[@placeholder="BIC / Swift"]' ) .fill( 'ABBA' ); - await page.locator( 'text=Save' ).click(); + await page.getByRole( 'button', { name: 'Save' } ).click(); // Check that bank transfers were set up. await expect( @@ -93,7 +93,7 @@ test.describe( 'Payment setup task', () => { page, } ) => { await page.goto( 'wp-admin/admin.php?page=wc-admin' ); - await page.locator( 'text=Get paid' ).click(); + await page.getByRole( 'button', { name: '3 Get paid' } ).click(); await expect( page.locator( '.woocommerce-layout__header-wrapper > h1' ) ).toHaveText( 'Get paid' ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/coupons/coupons.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/coupons/coupons.test.js index 7c9a8a06565..a4a7cc9abe9 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/coupons/coupons.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/coupons/coupons.test.js @@ -69,26 +69,30 @@ test.describe( 'Coupons API tests', () => { ); } ); - test( 'can permanently delete a coupon', async ( { request } ) => { - //call API to delete previously created coupon - const response = await request.delete( - `/wp-json/wc/v3/coupons/${ couponId }`, - { - data: { force: true }, - } - ); + test( + 'can permanently delete a coupon', + { tag: '@skip-on-default-wpcom' }, + async ( { request } ) => { + //call API to delete previously created coupon + const response = await request.delete( + `/wp-json/wc/v3/coupons/${ couponId }`, + { + data: { force: true }, + } + ); - //validate response - expect( response.status() ).toEqual( 200 ); + //validate response + expect( response.status() ).toEqual( 200 ); - //call API to retrieve previously deleted coupon - const getCouponResponse = await request.get( - `/wp-json/wc/v3/coupons/${ couponId }` - ); + //call API to retrieve previously deleted coupon + const getCouponResponse = await request.get( + `/wp-json/wc/v3/coupons/${ couponId }` + ); - //validate response - expect( getCouponResponse.status() ).toEqual( 404 ); - } ); + //validate response + expect( getCouponResponse.status() ).toEqual( 404 ); + } + ); } ); test.describe( 'Batch update coupons', () => { @@ -180,38 +184,42 @@ test.describe( 'Batch update coupons', () => { expect( updatedCoupons[ 1 ].amount ).toEqual( '25.00' ); } ); - test( 'can batch delete coupons', async ( { request } ) => { - // Batch delete the 2 coupons. - const couponIdsToDelete = expectedCoupons.map( ( { id } ) => id ); - const batchDeletePayload = { - delete: couponIdsToDelete, - }; + test( + 'can batch delete coupons', + { tag: '@skip-on-default-wpcom' }, + async ( { request } ) => { + // Batch delete the 2 coupons. + const couponIdsToDelete = expectedCoupons.map( ( { id } ) => id ); + const batchDeletePayload = { + delete: couponIdsToDelete, + }; - //Call API to batch delete the coupons - const batchDeleteResponse = await request.post( - 'wp-json/wc/v3/coupons/batch', - { - data: batchDeletePayload, - } - ); - const batchDeletePayloadJSON = await batchDeleteResponse.json(); - - // Verify that the response shows the 2 coupons. - const deletedCouponIds = batchDeletePayloadJSON.delete.map( - ( { id } ) => id - ); - expect( batchDeleteResponse.status() ).toEqual( 200 ); - expect( deletedCouponIds ).toEqual( couponIdsToDelete ); - - // Verify that the 2 deleted coupons cannot be retrieved. - for ( const couponId of couponIdsToDelete ) { - //Call the API to attempte to retrieve the coupons - const response = await request.get( - `wp-json/wc/v3/coupons/${ couponId }` + //Call API to batch delete the coupons + const batchDeleteResponse = await request.post( + 'wp-json/wc/v3/coupons/batch', + { + data: batchDeletePayload, + } ); - expect( response.status() ).toEqual( 404 ); + const batchDeletePayloadJSON = await batchDeleteResponse.json(); + + // Verify that the response shows the 2 coupons. + const deletedCouponIds = batchDeletePayloadJSON.delete.map( + ( { id } ) => id + ); + expect( batchDeleteResponse.status() ).toEqual( 200 ); + expect( deletedCouponIds ).toEqual( couponIdsToDelete ); + + // Verify that the 2 deleted coupons cannot be retrieved. + for ( const couponId of couponIdsToDelete ) { + //Call the API to attempte to retrieve the coupons + const response = await request.get( + `wp-json/wc/v3/coupons/${ couponId }` + ); + expect( response.status() ).toEqual( 404 ); + } } - } ); + ); } ); test.describe( 'List coupons', () => { diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/customers/customers-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/customers/customers-crud.test.js index 17be7e0a3bc..a93fef4198a 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/customers/customers-crud.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/customers/customers-crud.test.js @@ -2,500 +2,537 @@ const { test, expect } = require( '../../../fixtures/api-tests-fixtures' ); const { admin } = require( '../../../test-data/data' ); const { customer } = require( '../../../data' ); -test.describe( 'Customers API tests: CRUD', () => { - let customerId; - let subscriberUserId; - let subscriberUserCreatedDuringTests = false; +test.describe( + 'Customers API tests: CRUD', + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, + () => { + let customerId; + let subscriberUserId; + let subscriberUserCreatedDuringTests = false; - test.beforeAll( async ( { request } ) => { - // Call the API to return all users and determine if a - // subscriber user has been created - const customersResponse = await request.get( - '/wp-json/wc/v3/customers', - { - params: { - role: 'all', - }, - } - ); - const customersResponseJSON = await customersResponse.json(); - - for ( const element of customersResponseJSON ) { - if ( element.role === 'subscriber' ) { - subscriberUserId = element.id; - break; - } - } - - // If a subscriber user has not been created then create one - if ( ! subscriberUserId ) { - const now = Date.now(); - const userResponse = await request.post( '/wp-json/wp/v2/users', { - data: { - username: `customer_${ now }`, - email: `customer_${ now }@woocommercecoretestsuite.com`, - first_name: 'Jane', - last_name: 'Smith', - roles: [ 'subscriber' ], - password: 'password', - name: 'Jane', - }, - } ); - const userResponseJSON = await userResponse.json(); - // set subscriber user id to newly created user - subscriberUserId = userResponseJSON.id; - subscriberUserCreatedDuringTests = true; - } - - // Verify the subscriber user has been created - const response = await request.get( - `/wp-json/wc/v3/customers/${ subscriberUserId }` - ); - const responseJSON = await response.json(); - // eslint-disable-next-line jest/no-standalone-expect - expect( response.status() ).toEqual( 200 ); - // eslint-disable-next-line jest/no-standalone-expect - expect( responseJSON.role ).toEqual( 'subscriber' ); - } ); - - test.afterAll( async ( { request } ) => { - // delete subscriber user if one was created during the execution of these tests - if ( subscriberUserCreatedDuringTests ) { - await request.delete( - `/wp-json/wc/v3/customers/${ subscriberUserId }`, + test.beforeAll( async ( { request } ) => { + // Call the API to return all users and determine if a + // subscriber user has been created + const customersResponse = await request.get( + '/wp-json/wc/v3/customers', { - data: { - force: true, + params: { + role: 'all', }, } ); - } - } ); + const customersResponseJSON = await customersResponse.json(); - test.describe( 'Retrieve after env setup', () => { - /** - * when the environment is created, - * (https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/e2e-pw#woocommerce-playwright-end-to-end-tests), - * we have an admin user and a subscriber user that can both be - * accessed through their ids - * admin user will have id 1 and subscriber user will have id 2 - * neither of these are returned as part of the get all customers call - * unless the role 'all' is passed as a search param - * but they can be accessed by specific id reference - */ - test( 'can retrieve admin user', async ( { request } ) => { - // call API to retrieve the previously saved customer - const response = await request.get( '/wp-json/wc/v3/customers/1' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.is_paying_customer ).toEqual( false ); - expect( responseJSON.role ).toEqual( 'administrator' ); - // this test was updated to allow for local test setup and other test sites. - expect( responseJSON.username ).toEqual( admin.username ); - } ); + for ( const element of customersResponseJSON ) { + if ( element.role === 'subscriber' ) { + subscriberUserId = element.id; + break; + } + } - test( 'can retrieve subscriber user', async ( { request } ) => { - // if environment was created with subscriber user - // call API to retrieve the customer with id 2 + // If a subscriber user has not been created then create one + if ( ! subscriberUserId ) { + const now = Date.now(); + const userResponse = await request.post( + '/wp-json/wp/v2/users', + { + data: { + username: `customer_${ now }`, + email: `customer_${ now }@woocommercecoretestsuite.com`, + first_name: 'Jane', + last_name: 'Smith', + roles: [ 'subscriber' ], + password: 'password', + name: 'Jane', + }, + } + ); + const userResponseJSON = await userResponse.json(); + // set subscriber user id to newly created user + subscriberUserId = userResponseJSON.id; + subscriberUserCreatedDuringTests = true; + } + + // Verify the subscriber user has been created const response = await request.get( `/wp-json/wc/v3/customers/${ subscriberUserId }` ); const responseJSON = await response.json(); + // eslint-disable-next-line jest/no-standalone-expect expect( response.status() ).toEqual( 200 ); - expect( responseJSON.is_paying_customer ).toEqual( false ); + // eslint-disable-next-line jest/no-standalone-expect expect( responseJSON.role ).toEqual( 'subscriber' ); } ); - test( 'retrieve user with id 0 is invalid', async ( { request } ) => { - // call API to retrieve the previously saved customer - const response = await request.get( '/wp-json/wc/v3/customers/0' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 404 ); - expect( responseJSON.code ).toEqual( - 'woocommerce_rest_invalid_id' - ); - expect( responseJSON.message ).toEqual( 'Invalid resource ID.' ); + test.afterAll( async ( { request } ) => { + // delete subscriber user if one was created during the execution of these tests + if ( subscriberUserCreatedDuringTests ) { + await request.delete( + `/wp-json/wc/v3/customers/${ subscriberUserId }`, + { + data: { + force: true, + }, + } + ); + } } ); - test( 'can retrieve customers', async ( { request } ) => { - // call API to retrieve all customers should initially return empty array - const response = await request.get( '/wp-json/wc/v3/customers' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); - expect( responseJSON.length ).toEqual( 1 ); - } ); - - // however, if we pass in the search string for role 'all' then all users are returned - test( 'can retrieve all customers', async ( { request } ) => { - // call API to retrieve all customers should initially return empty array - // unless the role 'all' is passed as a search string, in which case the admin - // and subscriber users will be returned - const response = await request.get( '/wp-json/wc/v3/customers', { - params: { - role: 'all', - }, - } ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); - expect( responseJSON.length ).toBeGreaterThanOrEqual( 3 ); - } ); - } ); - - test.describe( 'Create a customer', () => { - test( 'can create a customer', async ( { request } ) => { - // call API to create a customer - const response = await request.post( '/wp-json/wc/v3/customers', { - data: customer, - } ); - const responseJSON = await response.json(); - - // Save the customer ID. It will be used by the retrieve, update, and delete tests. - customerId = responseJSON.id; - - expect( response.status() ).toEqual( 201 ); - expect( typeof responseJSON.id ).toEqual( 'number' ); - // Verify that the customer role is 'customer' - expect( responseJSON.role ).toEqual( 'customer' ); - } ); - } ); - - test.describe( 'Retrieve after create', () => { - test( 'can retrieve a customer', async ( { request } ) => { - // call API to retrieve the previously saved customer - const response = await request.get( - `/wp-json/wc/v3/customers/${ customerId }` - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.id ).toEqual( customerId ); - expect( responseJSON.is_paying_customer ).toEqual( false ); - expect( responseJSON.role ).toEqual( 'customer' ); - } ); - - test( 'can retrieve all customers after create', async ( { - request, - } ) => { - // call API to retrieve all customers - const response = await request.get( '/wp-json/wc/v3/customers' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); - expect( responseJSON.length ).toBeGreaterThan( 0 ); - } ); - } ); - - test.describe( 'Update a customer', () => { - test( `can update the admin user/customer`, async ( { request } ) => { + test.describe( 'Retrieve after env setup', () => { /** - * update customer names (regular, billing and shipping) to admin - * (these were initialised blank when the environment is created, - * (https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/e2e-pw#woocommerce-playwright-end-to-end-tests + * when the environment is created, + * (https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/e2e-pw#woocommerce-playwright-end-to-end-tests), + * we have an admin user and a subscriber user that can both be + * accessed through their ids + * admin user will have id 1 and subscriber user will have id 2 + * neither of these are returned as part of the get all customers call + * unless the role 'all' is passed as a search param + * but they can be accessed by specific id reference */ - const response = await request.put( `/wp-json/wc/v3/customers/1`, { - data: { - first_name: 'admin', + test( 'can retrieve admin user', async ( { request } ) => { + // call API to retrieve the previously saved customer + const response = await request.get( + '/wp-json/wc/v3/customers/1' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.is_paying_customer ).toEqual( false ); + expect( responseJSON.role ).toEqual( 'administrator' ); + // this test was updated to allow for local test setup and other test sites. + expect( responseJSON.username ).toEqual( admin.username ); + } ); + + test( 'can retrieve subscriber user', async ( { request } ) => { + // if environment was created with subscriber user + // call API to retrieve the customer with id 2 + const response = await request.get( + `/wp-json/wc/v3/customers/${ subscriberUserId }` + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.is_paying_customer ).toEqual( false ); + expect( responseJSON.role ).toEqual( 'subscriber' ); + } ); + + test( 'retrieve user with id 0 is invalid', async ( { + request, + } ) => { + // call API to retrieve the previously saved customer + const response = await request.get( + '/wp-json/wc/v3/customers/0' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 404 ); + expect( responseJSON.code ).toEqual( + 'woocommerce_rest_invalid_id' + ); + expect( responseJSON.message ).toEqual( + 'Invalid resource ID.' + ); + } ); + + test( 'can retrieve customers', async ( { request } ) => { + // call API to retrieve all customers should initially return empty array + const response = await request.get( + '/wp-json/wc/v3/customers' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); + expect( responseJSON.length ).toEqual( 1 ); + } ); + + // however, if we pass in the search string for role 'all' then all users are returned + test( 'can retrieve all customers', async ( { request } ) => { + // call API to retrieve all customers should initially return empty array + // unless the role 'all' is passed as a search string, in which case the admin + // and subscriber users will be returned + const response = await request.get( + '/wp-json/wc/v3/customers', + { + params: { + role: 'all', + }, + } + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); + expect( responseJSON.length ).toBeGreaterThanOrEqual( 3 ); + } ); + } ); + + test.describe( 'Create a customer', () => { + test( 'can create a customer', async ( { request } ) => { + // call API to create a customer + const response = await request.post( + '/wp-json/wc/v3/customers', + { + data: customer, + } + ); + const responseJSON = await response.json(); + + // Save the customer ID. It will be used by the retrieve, update, and delete tests. + customerId = responseJSON.id; + + expect( response.status() ).toEqual( 201 ); + expect( typeof responseJSON.id ).toEqual( 'number' ); + // Verify that the customer role is 'customer' + expect( responseJSON.role ).toEqual( 'customer' ); + } ); + } ); + + test.describe( 'Retrieve after create', () => { + test( 'can retrieve a customer', async ( { request } ) => { + // call API to retrieve the previously saved customer + const response = await request.get( + `/wp-json/wc/v3/customers/${ customerId }` + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.id ).toEqual( customerId ); + expect( responseJSON.is_paying_customer ).toEqual( false ); + expect( responseJSON.role ).toEqual( 'customer' ); + } ); + + test( 'can retrieve all customers after create', async ( { + request, + } ) => { + // call API to retrieve all customers + const response = await request.get( + '/wp-json/wc/v3/customers' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); + expect( responseJSON.length ).toBeGreaterThan( 0 ); + } ); + } ); + + test.describe( 'Update a customer', () => { + test( `can update the admin user/customer`, async ( { + request, + } ) => { + /** + * update customer names (regular, billing and shipping) to admin + * (these were initialised blank when the environment is created, + * (https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/e2e-pw#woocommerce-playwright-end-to-end-tests + */ + const response = await request.put( + `/wp-json/wc/v3/customers/1`, + { + data: { + first_name: 'admin', + billing: { + first_name: 'admin', + }, + shipping: { + first_name: 'admin', + }, + }, + } + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.first_name ).toEqual( 'admin' ); + expect( responseJSON.billing.first_name ).toEqual( 'admin' ); + expect( responseJSON.shipping.first_name ).toEqual( 'admin' ); + } ); + + test( 'retrieve after update admin', async ( { request } ) => { + // call API to retrieve the admin customer we updated above + const response = await request.get( + '/wp-json/wc/v3/customers/1' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.first_name ).toEqual( 'admin' ); + expect( responseJSON.billing.first_name ).toEqual( 'admin' ); + expect( responseJSON.shipping.first_name ).toEqual( 'admin' ); + } ); + + test( `can update the subscriber user/customer`, async ( { + request, + } ) => { + // update customer names (billing and shipping) to Jane + // (these were initialised blank, only regular first_name was populated) + const response = await request.put( + `/wp-json/wc/v3/customers/${ subscriberUserId }`, + { + data: { + billing: { + first_name: 'Jane', + }, + shipping: { + first_name: 'Jane', + }, + }, + } + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.first_name ).toEqual( 'Jane' ); + expect( responseJSON.billing.first_name ).toEqual( 'Jane' ); + expect( responseJSON.shipping.first_name ).toEqual( 'Jane' ); + } ); + + test( 'retrieve after update subscriber', async ( { request } ) => { + // call API to retrieve the subscriber customer we updated above + const response = await request.get( + `/wp-json/wc/v3/customers/${ subscriberUserId }` + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.first_name ).toEqual( 'Jane' ); + expect( responseJSON.billing.first_name ).toEqual( 'Jane' ); + expect( responseJSON.shipping.first_name ).toEqual( 'Jane' ); + } ); + + test( `can update a customer`, async ( { request } ) => { + // update customer names (regular, billing and shipping) from John to Jack + const response = await request.put( + `/wp-json/wc/v3/customers/${ customerId }`, + { + data: { + first_name: 'Jack', + billing: { + first_name: 'Jack', + }, + shipping: { + first_name: 'Jack', + }, + }, + } + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.first_name ).toEqual( 'Jack' ); + expect( responseJSON.billing.first_name ).toEqual( 'Jack' ); + expect( responseJSON.shipping.first_name ).toEqual( 'Jack' ); + } ); + + test( 'retrieve after update customer', async ( { request } ) => { + // call API to retrieve the updated customer we created above + const response = await request.get( + `/wp-json/wc/v3/customers/${ customerId }` + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.first_name ).toEqual( 'Jack' ); + expect( responseJSON.billing.first_name ).toEqual( 'Jack' ); + expect( responseJSON.shipping.first_name ).toEqual( 'Jack' ); + } ); + } ); + + test.describe( 'Delete a customer', () => { + test( 'can permanently delete an customer', async ( { + request, + } ) => { + // Delete the customer. + const response = await request.delete( + `/wp-json/wc/v3/customers/${ customerId }`, + { + data: { + force: true, + }, + } + ); + expect( response.status() ).toEqual( 200 ); + + // Verify that the customer can no longer be retrieved. + const getDeletedCustomerResponse = await request.get( + `/wp-json/wc/v3/customers/${ customer }` + ); + expect( getDeletedCustomerResponse.status() ).toEqual( 404 ); + } ); + } ); + + test.describe( 'Batch update customers', () => { + /** + * 2 Customers to be created in one batch. + */ + const expectedCustomers = [ + { + email: 'john.doe2@example.com', + first_name: 'John', + last_name: 'Doe', + username: 'john.doe2', billing: { - first_name: 'admin', + first_name: 'John', + last_name: 'Doe', + company: '', + address_1: '969 Market', + address_2: '', + city: 'San Francisco', + state: 'CA', + postcode: '94103', + country: 'US', + email: 'john.doe2@example.com', + phone: '(555) 555-5555', }, shipping: { - first_name: 'admin', + first_name: 'John', + last_name: 'Doe', + company: '', + address_1: '969 Market', + address_2: '', + city: 'San Francisco', + state: 'CA', + postcode: '94103', + country: 'US', }, }, - } ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.first_name ).toEqual( 'admin' ); - expect( responseJSON.billing.first_name ).toEqual( 'admin' ); - expect( responseJSON.shipping.first_name ).toEqual( 'admin' ); - } ); - - test( 'retrieve after update admin', async ( { request } ) => { - // call API to retrieve the admin customer we updated above - const response = await request.get( '/wp-json/wc/v3/customers/1' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.first_name ).toEqual( 'admin' ); - expect( responseJSON.billing.first_name ).toEqual( 'admin' ); - expect( responseJSON.shipping.first_name ).toEqual( 'admin' ); - } ); - - test( `can update the subscriber user/customer`, async ( { - request, - } ) => { - // update customer names (billing and shipping) to Jane - // (these were initialised blank, only regular first_name was populated) - const response = await request.put( - `/wp-json/wc/v3/customers/${ subscriberUserId }`, { - data: { - billing: { - first_name: 'Jane', - }, - shipping: { - first_name: 'Jane', - }, - }, - } - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.first_name ).toEqual( 'Jane' ); - expect( responseJSON.billing.first_name ).toEqual( 'Jane' ); - expect( responseJSON.shipping.first_name ).toEqual( 'Jane' ); - } ); - - test( 'retrieve after update subscriber', async ( { request } ) => { - // call API to retrieve the subscriber customer we updated above - const response = await request.get( - `/wp-json/wc/v3/customers/${ subscriberUserId }` - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.first_name ).toEqual( 'Jane' ); - expect( responseJSON.billing.first_name ).toEqual( 'Jane' ); - expect( responseJSON.shipping.first_name ).toEqual( 'Jane' ); - } ); - - test( `can update a customer`, async ( { request } ) => { - // update customer names (regular, billing and shipping) from John to Jack - const response = await request.put( - `/wp-json/wc/v3/customers/${ customerId }`, - { - data: { - first_name: 'Jack', - billing: { - first_name: 'Jack', - }, - shipping: { - first_name: 'Jack', - }, - }, - } - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.first_name ).toEqual( 'Jack' ); - expect( responseJSON.billing.first_name ).toEqual( 'Jack' ); - expect( responseJSON.shipping.first_name ).toEqual( 'Jack' ); - } ); - - test( 'retrieve after update customer', async ( { request } ) => { - // call API to retrieve the updated customer we created above - const response = await request.get( - `/wp-json/wc/v3/customers/${ customerId }` - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.first_name ).toEqual( 'Jack' ); - expect( responseJSON.billing.first_name ).toEqual( 'Jack' ); - expect( responseJSON.shipping.first_name ).toEqual( 'Jack' ); - } ); - } ); - - test.describe( 'Delete a customer', () => { - test( 'can permanently delete an customer', async ( { request } ) => { - // Delete the customer. - const response = await request.delete( - `/wp-json/wc/v3/customers/${ customerId }`, - { - data: { - force: true, - }, - } - ); - expect( response.status() ).toEqual( 200 ); - - // Verify that the customer can no longer be retrieved. - const getDeletedCustomerResponse = await request.get( - `/wp-json/wc/v3/customers/${ customer }` - ); - expect( getDeletedCustomerResponse.status() ).toEqual( 404 ); - } ); - } ); - - test.describe( 'Batch update customers', () => { - /** - * 2 Customers to be created in one batch. - */ - const expectedCustomers = [ - { - email: 'john.doe2@example.com', - first_name: 'John', - last_name: 'Doe', - username: 'john.doe2', - billing: { - first_name: 'John', - last_name: 'Doe', - company: '', - address_1: '969 Market', - address_2: '', - city: 'San Francisco', - state: 'CA', - postcode: '94103', - country: 'US', - email: 'john.doe2@example.com', - phone: '(555) 555-5555', - }, - shipping: { - first_name: 'John', - last_name: 'Doe', - company: '', - address_1: '969 Market', - address_2: '', - city: 'San Francisco', - state: 'CA', - postcode: '94103', - country: 'US', - }, - }, - { - email: 'joao.silva2@example.com', - first_name: 'João', - last_name: 'Silva', - username: 'joao.silva2', - billing: { - first_name: 'João', - last_name: 'Silva', - company: '', - address_1: 'Av. Brasil, 432', - address_2: '', - city: 'Rio de Janeiro', - state: 'RJ', - postcode: '12345-000', - country: 'BR', email: 'joao.silva2@example.com', - phone: '(55) 5555-5555', - }, - shipping: { first_name: 'João', last_name: 'Silva', - company: '', - address_1: 'Av. Brasil, 432', - address_2: '', - city: 'Rio de Janeiro', - state: 'RJ', - postcode: '12345-000', - country: 'BR', + username: 'joao.silva2', + billing: { + first_name: 'João', + last_name: 'Silva', + company: '', + address_1: 'Av. Brasil, 432', + address_2: '', + city: 'Rio de Janeiro', + state: 'RJ', + postcode: '12345-000', + country: 'BR', + email: 'joao.silva2@example.com', + phone: '(55) 5555-5555', + }, + shipping: { + first_name: 'João', + last_name: 'Silva', + company: '', + address_1: 'Av. Brasil, 432', + address_2: '', + city: 'Rio de Janeiro', + state: 'RJ', + postcode: '12345-000', + country: 'BR', + }, }, - }, - ]; + ]; - // set payload to use batch create: action - const batchCreate2CustomersPayload = { - create: expectedCustomers, - }; - - test( 'can batch create customers', async ( { request } ) => { - // Batch create 2 new customers. - // call API to batch create customers - const response = await request.post( - 'wp-json/wc/v3/customers/batch', - { - data: batchCreate2CustomersPayload, - } - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - - // Verify that the 2 new customers were created - const actualCustomers = responseJSON.create; - expect( actualCustomers ).toHaveLength( expectedCustomers.length ); - - for ( let i = 0; i < actualCustomers.length; i++ ) { - const { id, first_name } = actualCustomers[ i ]; - const expectedCustomerName = expectedCustomers[ i ].first_name; - - expect( id ).toBeDefined(); - expect( first_name ).toEqual( expectedCustomerName ); - - // Save the customer id - expectedCustomers[ i ].id = id; - } - } ); - - test( 'can batch update customers', async ( { request } ) => { - // set payload to use batch update: action - const batchUpdatePayload = { - update: [ - { - id: expectedCustomers[ 0 ].id, - email: 'emailupdated@example.com', - }, - { - id: expectedCustomers[ 1 ].id, - billing: { - address_1: '123 Addressupdate Street', - }, - }, - ], + // set payload to use batch create: action + const batchCreate2CustomersPayload = { + create: expectedCustomers, }; - // Call API to update the customers - const response = await request.post( - 'wp-json/wc/v3/customers/batch', - { - data: batchUpdatePayload, - } - ); - const responseJSON = await response.json(); - - // Verify the response code and the number of customers that were updated. - const updatedCustomers = responseJSON.update; - expect( response.status() ).toEqual( 200 ); - expect( updatedCustomers ).toHaveLength( 2 ); - - // Verify that the 1st customer was updated to have a new email address. - expect( updatedCustomers[ 0 ].id ).toEqual( - expectedCustomers[ 0 ].id - ); - expect( updatedCustomers[ 0 ].email ).toEqual( - 'emailupdated@example.com' - ); - - // Verify that the amount of the 2nd customer was updated to have a new billing address. - expect( updatedCustomers[ 1 ].id ).toEqual( - expectedCustomers[ 1 ].id - ); - expect( updatedCustomers[ 1 ].billing.address_1 ).toEqual( - '123 Addressupdate Street' - ); - } ); - - test( 'can batch delete customers', async ( { request } ) => { - // Batch delete the 2 customers. - const customerIdsToDelete = expectedCustomers.map( - ( { id } ) => id - ); - const batchDeletePayload = { - delete: customerIdsToDelete, - }; - - //Call API to batch delete the customers - const response = await request.post( - 'wp-json/wc/v3/customers/batch', - { - data: batchDeletePayload, - } - ); - const responseJSON = await response.json(); - - // Verify that the response shows the 2 customers. - const deletedCustomerIds = responseJSON.delete.map( - ( { id } ) => id - ); - expect( response.status() ).toEqual( 200 ); - expect( deletedCustomerIds ).toEqual( customerIdsToDelete ); - - // Verify that the 2 deleted customers cannot be retrieved. - for ( const id of customerIdsToDelete ) { - //Call the API to attempte to retrieve the customers - const r = await request.get( - `wp-json/wc/v3/customers/${ id }` + test( 'can batch create customers', async ( { request } ) => { + // Batch create 2 new customers. + // call API to batch create customers + const response = await request.post( + 'wp-json/wc/v3/customers/batch', + { + data: batchCreate2CustomersPayload, + } ); - expect( r.status() ).toEqual( 404 ); - } + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + + // Verify that the 2 new customers were created + const actualCustomers = responseJSON.create; + expect( actualCustomers ).toHaveLength( + expectedCustomers.length + ); + + for ( let i = 0; i < actualCustomers.length; i++ ) { + const { id, first_name } = actualCustomers[ i ]; + const expectedCustomerName = + expectedCustomers[ i ].first_name; + + expect( id ).toBeDefined(); + expect( first_name ).toEqual( expectedCustomerName ); + + // Save the customer id + expectedCustomers[ i ].id = id; + } + } ); + + test( 'can batch update customers', async ( { request } ) => { + // set payload to use batch update: action + const batchUpdatePayload = { + update: [ + { + id: expectedCustomers[ 0 ].id, + email: 'emailupdated@example.com', + }, + { + id: expectedCustomers[ 1 ].id, + billing: { + address_1: '123 Addressupdate Street', + }, + }, + ], + }; + + // Call API to update the customers + const response = await request.post( + 'wp-json/wc/v3/customers/batch', + { + data: batchUpdatePayload, + } + ); + const responseJSON = await response.json(); + + // Verify the response code and the number of customers that were updated. + const updatedCustomers = responseJSON.update; + expect( response.status() ).toEqual( 200 ); + expect( updatedCustomers ).toHaveLength( 2 ); + + // Verify that the 1st customer was updated to have a new email address. + expect( updatedCustomers[ 0 ].id ).toEqual( + expectedCustomers[ 0 ].id + ); + expect( updatedCustomers[ 0 ].email ).toEqual( + 'emailupdated@example.com' + ); + + // Verify that the amount of the 2nd customer was updated to have a new billing address. + expect( updatedCustomers[ 1 ].id ).toEqual( + expectedCustomers[ 1 ].id + ); + expect( updatedCustomers[ 1 ].billing.address_1 ).toEqual( + '123 Addressupdate Street' + ); + } ); + + test( 'can batch delete customers', async ( { request } ) => { + // Batch delete the 2 customers. + const customerIdsToDelete = expectedCustomers.map( + ( { id } ) => id + ); + const batchDeletePayload = { + delete: customerIdsToDelete, + }; + + //Call API to batch delete the customers + const response = await request.post( + 'wp-json/wc/v3/customers/batch', + { + data: batchDeletePayload, + } + ); + const responseJSON = await response.json(); + + // Verify that the response shows the 2 customers. + const deletedCustomerIds = responseJSON.delete.map( + ( { id } ) => id + ); + expect( response.status() ).toEqual( 200 ); + expect( deletedCustomerIds ).toEqual( customerIdsToDelete ); + + // Verify that the 2 deleted customers cannot be retrieved. + for ( const id of customerIdsToDelete ) { + //Call the API to attempte to retrieve the customers + const r = await request.get( + `wp-json/wc/v3/customers/${ id }` + ); + expect( r.status() ).toEqual( 404 ); + } + } ); } ); - } ); -} ); + } +); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-complex.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-complex.test.js index 5308ecdcfa1..90df59a1b9c 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-complex.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-complex.test.js @@ -208,48 +208,52 @@ test.describe( 'Orders API test', () => { } ); } ); - test( 'can add complex order', async ( { request } ) => { - //ensure tax calculations are enabled - await request.put( - '/wp-json/wc/v3/settings/general/woocommerce_calc_taxes', - { - data: { - value: 'yes', - }, + test( + 'can add complex order', + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, + async ( { request } ) => { + //ensure tax calculations are enabled + await request.put( + '/wp-json/wc/v3/settings/general/woocommerce_calc_taxes', + { + data: { + value: 'yes', + }, + } + ); + + // Create the complex order and save its ID. + const response = await request.post( '/wp-json/wc/v3/orders', { + data: order, + } ); + const responseJSON = await response.json(); + + order.id = responseJSON.id; + + expect( response.status() ).toEqual( 201 ); + + // Verify order and tax totals + expect( responseJSON.total ).toEqual( expectedOrderTotal ); + expect( responseJSON.total_tax ).toEqual( expectedTaxTotal ); + + // Verify total tax of each product line item + const expectedTaxTotalsPerLineItem = [ + [ simpleProduct, expectedSimpleProductTaxTotal ], + [ variableProduct, expectedVariableProductTaxTotal ], + [ groupedProduct, expectedSimpleProductTaxTotal ], + [ externalProduct, expectedExternalProductTaxTotal ], + ]; + for ( const [ + product, + expectedLineTaxTotal, + ] of expectedTaxTotalsPerLineItem ) { + const { total_tax: actualLineTaxTotal } = + responseJSON.line_items.find( + ( { product_id } ) => product_id === product.id + ); + + expect( actualLineTaxTotal ).toEqual( expectedLineTaxTotal ); } - ); - - // Create the complex order and save its ID. - const response = await request.post( '/wp-json/wc/v3/orders', { - data: order, - } ); - const responseJSON = await response.json(); - - order.id = responseJSON.id; - - expect( response.status() ).toEqual( 201 ); - - // Verify order and tax totals - expect( responseJSON.total ).toEqual( expectedOrderTotal ); - expect( responseJSON.total_tax ).toEqual( expectedTaxTotal ); - - // Verify total tax of each product line item - const expectedTaxTotalsPerLineItem = [ - [ simpleProduct, expectedSimpleProductTaxTotal ], - [ variableProduct, expectedVariableProductTaxTotal ], - [ groupedProduct, expectedSimpleProductTaxTotal ], - [ externalProduct, expectedExternalProductTaxTotal ], - ]; - for ( const [ - product, - expectedLineTaxTotal, - ] of expectedTaxTotalsPerLineItem ) { - const { total_tax: actualLineTaxTotal } = - responseJSON.line_items.find( - ( { product_id } ) => product_id === product.id - ); - - expect( actualLineTaxTotal ).toEqual( expectedLineTaxTotal ); } - } ); + ); } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders.test.js index f0164b40982..b6a556ce2de 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders.test.js @@ -34,3232 +34,3274 @@ const updatedCustomerShipping = { phone: '123456789', }; -test.describe.serial( 'Orders API tests', () => { - let orderId, sampleData; +test.describe.serial( + 'Orders API tests', + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, + () => { + let orderId, sampleData; - test.beforeAll( async ( { request } ) => { - const createSampleCategories = async () => { - const clothing = await request.post( - '/wp-json/wc/v3/products/categories', - { - data: { - name: 'Clothing', - }, - } - ); - const clothingJSON = await clothing.json(); - - const accessories = await request.post( - '/wp-json/wc/v3/products/categories', - { - data: { - name: 'Accessories', - parent: clothingJSON.id, - }, - } - ); - const accessoriesJSON = await accessories.json(); - - const hoodies = await request.post( - '/wp-json/wc/v3/products/categories', - { - data: { - name: 'Hoodies', - parent: clothingJSON.id, - }, - } - ); - const hoodiesJSON = await hoodies.json(); - - const tshirts = await request.post( - '/wp-json/wc/v3/products/categories', - { - data: { - name: 'Tshirts', - parent: clothingJSON.id, - }, - } - ); - const tshirtsJSON = await tshirts.json(); - - const decor = await request.post( - '/wp-json/wc/v3/products/categories', - { - data: { - name: 'Decor', - }, - } - ); - const decorJSON = await decor.json(); - - const music = await request.post( - '/wp-json/wc/v3/products/categories', - { - data: { - name: 'Music', - }, - } - ); - const musicJSON = await music.json(); - - return { - clothingJSON, - accessoriesJSON, - hoodiesJSON, - tshirtsJSON, - decorJSON, - musicJSON, - }; - }; - - const createSampleAttributes = async () => { - const color = await request.post( - '/wp-json/wc/v3/products/attributes', - { - data: { - name: 'Color', - }, - } - ); - const colorJSON = await color.json(); - - const size = await request.post( - '/wp-json/wc/v3/products/attributes', - { - data: { - name: 'Size', - }, - } - ); - const sizeJSON = await size.json(); - - const colorNames = [ 'Blue', 'Gray', 'Green', 'Red', 'Yellow' ]; - - const colorNamesObjectArray = colorNames.map( ( name ) => ( { - name, - } ) ); - - const colors = await request.post( - `/wp-json/wc/v3/products/attributes/${ colorJSON.id }/terms/batch`, - { - data: { - create: colorNamesObjectArray, - }, - } - ); - - const colorsJSON = await colors.json(); - - const sizeNames = [ 'Large', 'Medium', 'Small' ]; - - const sizeNamesObjectArray = sizeNames.map( ( name ) => ( { - name, - } ) ); - - const sizes = await request.post( - `/wp-json/wc/v3/products/attributes/${ sizeJSON.id }/terms/batch`, - { - data: { - create: sizeNamesObjectArray, - }, - } - ); - const sizesJSON = await sizes.json(); - - return { - colorJSON, - colors: colorsJSON.create, - sizeJSON, - sizes: sizesJSON.create, - }; - }; - - const createSampleTags = async () => { - const cool = await request.post( '/wp-json/wc/v3/products/tags', { - data: { - name: 'Cool', - }, - } ); - const coolJSON = await cool.json(); - - return { - coolJSON, - }; - }; - - const createSampleShippingClasses = async () => { - const freight = await request.post( - '/wp-json/wc/v3/products/shipping_classes', - { - data: { - name: 'Freight', - }, - } - ); - const freightJSON = await freight.json(); - - return { - freightJSON, - }; - }; - - const createSampleTaxClasses = async () => { - //check to see if Reduced Rate tax class exists - if not, create it - let reducedRate = await request.get( - '/wp-json/wc/v3/taxes/classes/reduced-rate' - ); - let reducedRateJSON = await reducedRate.json(); - expect( Array.isArray( reducedRateJSON ) ).toBe( true ); - - //if tax class does not exist then create it - if ( reducedRateJSON.length < 1 ) { - reducedRate = await request.post( - '/wp-json/wc/v3/taxes/classes', + test.beforeAll( async ( { request } ) => { + const createSampleCategories = async () => { + const clothing = await request.post( + '/wp-json/wc/v3/products/categories', { data: { - name: 'Reduced Rate', + name: 'Clothing', }, } ); - reducedRateJSON = await reducedRate.json(); - return { reducedRateJSON }; - } + const clothingJSON = await clothing.json(); - // return an empty object as nothing new was created so nothing will - // need deleted during cleanup - return {}; - }; - - const createSampleSimpleProducts = async ( - categories, - attributes, - tags - ) => { - const description = - '

    Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. ' + - 'Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. ' + - 'Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

    \n'; - - const simpleProducts = await request.post( - '/wp-json/wc/v3/products/batch', - { - data: { - create: [ - { - name: 'Beanie with Logo oxo', - date_created_gmt: '2021-09-01T15:50:20', - type: 'simple', - status: 'publish', - featured: false, - catalog_visibility: 'visible', - description, - short_description: - '

    This is a simple product.

    \n', - sku: 'Woo-beanie-logo', - price: '18', - regular_price: '20', - sale_price: '18', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: true, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '0.2', - dimensions: { - length: '6', - width: '4', - height: '1', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.accessoriesJSON.id, - }, - ], - tags: [], - attributes: [ - { - id: attributes.colorJSON.id, - position: 0, - visible: true, - variation: false, - options: [ 'Red' ], - }, - ], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 62, 63, 61, 60 ], - stock_status: 'instock', - }, - { - name: 'T-Shirt with Logo oxo', - date_created_gmt: '2021-09-02T15:50:20', - type: 'simple', - status: 'publish', - featured: false, - catalog_visibility: 'visible', - description, - short_description: - '

    This is a simple product.

    \n', - sku: 'Woo-tshirt-logo', - price: '18', - regular_price: '18', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '0.5', - dimensions: { - length: '10', - width: '12', - height: '0.5', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.tshirtsJSON.id, - }, - ], - tags: [], - attributes: [ - { - id: attributes.colorJSON.id, - position: 0, - visible: true, - variation: false, - options: [ 'Gray' ], - }, - ], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 59, 67, 66, 56 ], - stock_status: 'instock', - }, - { - name: 'Single oxo', - date_created_gmt: '2021-09-03T15:50:19', - type: 'simple', - status: 'publish', - featured: false, - catalog_visibility: 'visible', - description, - short_description: - '

    This is a simple, virtual product.

    \n', - sku: 'woo-single', - price: '2', - regular_price: '3', - sale_price: '2', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: true, - purchasable: true, - total_sales: 0, - virtual: true, - downloadable: true, - downloads: [ - { - id: '2579cf07-8b08-4c25-888a-b6258dd1f035', - name: 'Single', - file: 'https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg', - }, - ], - download_limit: 1, - download_expiry: 1, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '', - dimensions: { - length: '', - width: '', - height: '', - }, - shipping_required: false, - shipping_taxable: false, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.musicJSON.id, - }, - ], - tags: [], - attributes: [], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 68 ], - stock_status: 'instock', - }, - { - name: 'Album oxo', - date_created_gmt: '2021-09-04T15:50:19', - type: 'simple', - status: 'publish', - featured: false, - catalog_visibility: 'visible', - description, - short_description: - '

    This is a simple, virtual product.

    \n', - sku: 'woo-album', - price: '15', - regular_price: '15', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - purchasable: true, - total_sales: 0, - virtual: true, - downloadable: true, - downloads: [ - { - id: 'cc10249f-1de2-44d4-93d3-9f88ae629f76', - name: 'Single 1', - file: 'https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg', - }, - { - id: 'aea8ef69-ccdc-4d83-8e21-3c395ebb9411', - name: 'Single 2', - file: 'https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/album.jpg', - }, - ], - download_limit: 1, - download_expiry: 1, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '', - dimensions: { - length: '', - width: '', - height: '', - }, - shipping_required: false, - shipping_taxable: false, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.musicJSON.id, - }, - ], - tags: [], - attributes: [], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 69 ], - stock_status: 'instock', - }, - { - name: 'Polo oxo', - date_created_gmt: '2021-09-05T15:50:19', - type: 'simple', - status: 'pending', - featured: false, - catalog_visibility: 'visible', - description, - short_description: - '

    This is a simple product.

    \n', - sku: 'woo-polo', - price: '20', - regular_price: '20', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '0.8', - dimensions: { - length: '6', - width: '5', - height: '1', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.tshirtsJSON.id, - }, - ], - tags: [], - attributes: [ - { - id: attributes.colorJSON.id, - position: 0, - visible: true, - variation: false, - options: [ 'Blue' ], - }, - ], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 59, 56, 66, 76 ], - stock_status: 'instock', - }, - { - name: 'Long Sleeve Tee oxo', - date_created_gmt: '2021-09-06T15:50:19', - type: 'simple', - status: 'publish', - featured: false, - catalog_visibility: 'visible', - description, - short_description: - '

    This is a simple product.

    \n', - sku: 'woo-long-sleeve-tee', - price: '25', - regular_price: '25', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '1', - dimensions: { - length: '7', - width: '5', - height: '1', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: 'freight', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.tshirtsJSON.id, - }, - ], - tags: [], - attributes: [ - { - id: attributes.colorJSON.id, - position: 0, - visible: true, - variation: false, - options: [ 'Green' ], - }, - ], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 59, 56, 76, 67 ], - stock_status: 'instock', - }, - { - name: 'Hoodie with Zipper oxo', - date_created_gmt: '2021-09-07T15:50:19', - type: 'simple', - status: 'publish', - featured: true, - catalog_visibility: 'visible', - description, - short_description: - '

    This is a simple product.

    \n', - sku: 'woo-hoodie-with-zipper', - price: '45', - regular_price: '45', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '2', - dimensions: { - length: '8', - width: '6', - height: '2', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.hoodiesJSON.id, - }, - ], - tags: [], - attributes: [], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 57, 58 ], - stock_status: 'instock', - }, - { - name: 'Hoodie with Pocket oxo', - date_created_gmt: '2021-09-08T15:50:19', - type: 'simple', - status: 'publish', - featured: true, - catalog_visibility: 'hidden', - description, - short_description: - '

    This is a simple product.

    \n', - sku: 'woo-hoodie-with-pocket', - price: '35', - regular_price: '45', - sale_price: '35', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: true, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '3', - dimensions: { - length: '10', - width: '8', - height: '2', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.hoodiesJSON.id, - }, - ], - tags: [ - { - id: tags.coolJSON.id, - }, - ], - attributes: [ - { - id: attributes.colorJSON.id, - position: 0, - visible: true, - variation: false, - options: [ 'Gray' ], - }, - ], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 65, 57, 58 ], - stock_status: 'instock', - }, - { - name: 'Sunglasses oxo', - date_created_gmt: '2021-09-09T15:50:19', - type: 'simple', - status: 'publish', - featured: true, - catalog_visibility: 'visible', - description, - short_description: - '

    This is a simple product.

    \n', - sku: 'woo-sunglasses', - price: '90', - regular_price: '90', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: 'reduced-rate', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '0.2', - dimensions: { - length: '4', - width: '1.4', - height: '1', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.accessoriesJSON.id, - }, - ], - tags: [ - { - id: tags.coolJSON.id, - }, - ], - attributes: [], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 60, 62, 77, 61 ], - stock_status: 'instock', - }, - { - name: 'Cap oxo', - date_created_gmt: '2021-09-10T15:50:19', - type: 'simple', - status: 'publish', - featured: true, - catalog_visibility: 'visible', - description, - short_description: - '

    This is a simple product.

    \n', - sku: 'woo-cap', - price: '16', - regular_price: '18', - sale_price: '16', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: true, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '0.6', - dimensions: { - length: '8', - width: '6.5', - height: '4', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.accessoriesJSON.id, - }, - ], - tags: [], - attributes: [ - { - id: attributes.colorJSON.id, - position: 0, - visible: true, - variation: false, - options: [ 'Yellow' ], - }, - ], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 60, 77, 61, 63 ], - stock_status: 'instock', - }, - { - name: 'Belt oxo', - date_created_gmt: '2021-09-12T15:50:19', - type: 'simple', - status: 'publish', - featured: false, - catalog_visibility: 'visible', - description, - short_description: - '

    This is a simple product.

    \n', - sku: 'woo-belt', - price: '55', - regular_price: '65', - sale_price: '55', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: true, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '1.2', - dimensions: { - length: '12', - width: '2', - height: '1.5', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.accessoriesJSON.id, - }, - ], - tags: [], - attributes: [], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 63, 77, 62, 60 ], - stock_status: 'instock', - }, - { - name: 'Beanie oxo', - date_created_gmt: '2021-09-13T15:50:19', - type: 'simple', - status: 'publish', - featured: false, - catalog_visibility: 'visible', - description, - short_description: - '

    This is a simple product.

    \n', - sku: 'woo-beanie', - price: '18', - regular_price: '20', - sale_price: '18', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: true, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '0.2', - dimensions: { - length: '4', - width: '5', - height: '0.5', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.accessoriesJSON.id, - }, - ], - tags: [ - { - id: tags.coolJSON.id, - }, - ], - attributes: [ - { - id: attributes.colorJSON.id, - position: 0, - visible: true, - variation: false, - options: [ 'Red' ], - }, - ], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 63, 62, 61, 77 ], - stock_status: 'instock', - }, - { - name: 'T-Shirt oxo', - date_created_gmt: '2021-09-14T15:50:19', - type: 'simple', - status: 'publish', - featured: false, - catalog_visibility: 'visible', - description, - short_description: - '

    This is a simple product.

    \n', - sku: 'woo-tshirt', - price: '18', - regular_price: '18', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '0.8', - dimensions: { - length: '8', - width: '6', - height: '1', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.tshirtsJSON.id, - }, - ], - tags: [], - attributes: [ - { - id: attributes.colorJSON.id, - position: 0, - visible: true, - variation: false, - options: [ 'Gray' ], - }, - ], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 67, 76, 56, 66 ], - stock_status: 'onbackorder', - }, - { - name: 'Hoodie with Logo oxo', - date_created_gmt: '2021-09-15T15:50:19', - type: 'simple', - status: 'publish', - featured: false, - catalog_visibility: 'visible', - description, - short_description: - '

    This is a simple product.

    \n', - sku: 'woo-hoodie-with-logo', - price: '45', - regular_price: '45', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '2', - dimensions: { - length: '10', - width: '6', - height: '3', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.hoodiesJSON.id, - }, - ], - tags: [], - attributes: [ - { - id: attributes.colorJSON.id, - position: 0, - visible: true, - variation: false, - options: [ 'Blue' ], - }, - ], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [ 57, 65 ], - stock_status: 'instock', - }, - ], - }, - } - ); - const simpleProductsJSON = await simpleProducts.json(); - - return simpleProductsJSON.create; - }; - - const createSampleExternalProducts = async ( categories ) => { - const externalProducts = await request.post( - '/wp-json/wc/v3/products/batch', - { - data: { - create: [ - { - name: 'WordPress Pennant oxo', - date_created_gmt: '2021-09-16T15:50:20', - type: 'external', - status: 'publish', - featured: false, - catalog_visibility: 'visible', - description: - '

    Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. ' + - 'Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. ' + - 'Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

    \n', - short_description: - '

    This is an external product.

    \n', - sku: 'wp-pennant', - price: '11.05', - regular_price: '11.05', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - purchasable: false, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: - 'https://mercantile.wordpress.org/product/wordpress-pennant/', - button_text: 'Buy on the WordPress swag store!', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '', - dimensions: { - length: '', - width: '', - height: '', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.decorJSON.id, - }, - ], - tags: [], - attributes: [], - default_attributes: [], - variations: [], - grouped_products: [], - menu_order: 0, - related_ids: [], - stock_status: 'instock', - }, - ], - }, - } - ); - const externalProductsJSON = await externalProducts.json(); - - return externalProductsJSON.create; - }; - - const createSampleGroupedProduct = async ( categories ) => { - const logoProducts = await request.get( '/wp-json/wc/v3/products', { - params: { - search: 'logo', - _fields: [ 'id' ], - }, - } ); - const logoProductsJSON = await logoProducts.json(); - - const groupedProducts = await request.post( - '/wp-json/wc/v3/products/batch', - { - data: { - create: [ - { - name: 'Logo Collection oxo', - date_created_gmt: '2021-09-17T15:50:20', - type: 'grouped', - status: 'publish', - featured: false, - catalog_visibility: 'visible', - description: - '

    Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. ' + - 'Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. ' + - 'Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

    \n', - short_description: - '

    This is a grouped product.

    \n', - sku: 'logo-collection', - price: '18', - regular_price: '', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: true, - purchasable: false, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '', - dimensions: { - length: '', - width: '', - height: '', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.clothingJSON.id, - }, - ], - tags: [], - attributes: [], - default_attributes: [], - variations: [], - grouped_products: logoProductsJSON.map( - ( p ) => p.id - ), - menu_order: 0, - related_ids: [], - stock_status: 'instock', - }, - ], - }, - } - ); - const groupedProductsJSON = await groupedProducts.json(); - - return groupedProductsJSON.create; - }; - - const createSampleVariableProducts = async ( - categories, - attributes - ) => { - const description = - '

    Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. ' + - 'Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. ' + - 'Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

    \n'; - - const hoodie = await request.post( '/wp-json/wc/v3/products', { - data: { - name: 'Hoodie oxo', - date_created_gmt: '2021-09-18T15:50:19', - type: 'variable', - status: 'publish', - featured: false, - catalog_visibility: 'visible', - description, - short_description: '

    This is a variable product.

    \n', - sku: 'woo-hoodie', - price: '42', - regular_price: '', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: true, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '1.5', - dimensions: { - length: '10', - width: '8', - height: '3', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.hoodiesJSON.id, + const accessories = await request.post( + '/wp-json/wc/v3/products/categories', + { + data: { + name: 'Accessories', + parent: clothingJSON.id, }, - ], - tags: [], - attributes: [ - { - id: attributes.colorJSON.id, - position: 0, - visible: true, - variation: true, - options: [ 'Blue', 'Green', 'Red' ], - }, - { - id: 0, - name: 'Logo', - position: 1, - visible: true, - variation: true, - options: [ 'Yes', 'No' ], - }, - ], - default_attributes: [], - grouped_products: [], - menu_order: 0, - stock_status: 'instock', - }, - } ); - const hoodieJSON = await hoodie.json(); + } + ); + const accessoriesJSON = await accessories.json(); - const variationDescription = - '

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. ' + - 'Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. ' + - 'Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. ' + - 'Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. ' + - 'Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. ' + - 'Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.

    \n'; - - const hoodieVariations = await request.post( - `/wp-json/wc/v3/products/${ hoodieJSON.id }/variations/batch`, - { - data: { - create: [ - { - date_created_gmt: '2021-09-19T15:50:20', - description: variationDescription, - sku: 'woo-hoodie-blue-logo', - price: '45', - regular_price: '45', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - status: 'publish', - purchasable: true, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - stock_status: 'instock', - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - weight: '1.5', - dimensions: { - length: '10', - width: '8', - height: '3', - }, - shipping_class: '', - attributes: [ - { - id: attributes.colorJSON.id, - option: 'Blue', - }, - { - id: 0, - name: 'Logo', - option: 'Yes', - }, - ], - menu_order: 0, - }, - { - date_created_gmt: '2021-09-20T15:50:20', - description: variationDescription, - sku: 'woo-hoodie-blue', - price: '45', - regular_price: '45', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - status: 'publish', - purchasable: true, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - stock_status: 'instock', - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - weight: '1.5', - dimensions: { - length: '10', - width: '8', - height: '3', - }, - shipping_class: '', - attributes: [ - { - id: attributes.colorJSON.id, - option: 'Blue', - }, - { - id: 0, - name: 'Logo', - option: 'No', - }, - ], - menu_order: 3, - }, - { - date_created_gmt: '2021-09-21T15:50:20', - description: variationDescription, - sku: 'woo-hoodie-green', - price: '45', - regular_price: '45', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - status: 'publish', - purchasable: true, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - stock_status: 'instock', - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - weight: '1.5', - dimensions: { - length: '10', - width: '8', - height: '3', - }, - shipping_class: '', - attributes: [ - { - id: attributes.colorJSON.id, - option: 'Green', - }, - { - id: 0, - name: 'Logo', - option: 'No', - }, - ], - menu_order: 2, - }, - { - date_created_gmt: '2021-09-22T15:50:19', - description: variationDescription, - sku: 'woo-hoodie-red', - price: '42', - regular_price: '45', - sale_price: '42', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: true, - status: 'publish', - purchasable: true, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - stock_status: 'instock', - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - weight: '1.5', - dimensions: { - length: '10', - width: '8', - height: '3', - }, - shipping_class: '', - attributes: [ - { - id: attributes.colorJSON.id, - option: 'Red', - }, - { - id: 0, - name: 'Logo', - option: 'No', - }, - ], - menu_order: 1, - }, - ], - }, - } - ); - const hoodieVariationsJSON = await hoodieVariations.json(); - - const vneck = await request.post( '/wp-json/wc/v3/products', { - data: { - name: 'V-Neck T-Shirt oxo', - date_created_gmt: '2021-09-23T15:50:19', - type: 'variable', - status: 'publish', - featured: true, - catalog_visibility: 'visible', - description, - short_description: '

    This is a variable product.

    \n', - sku: 'woo-vneck-tee', - price: '15', - regular_price: '', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - purchasable: true, - total_sales: 0, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - external_url: '', - button_text: '', - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - sold_individually: false, - weight: '0.5', - dimensions: { - length: '24', - width: '1', - height: '2', - }, - shipping_required: true, - shipping_taxable: true, - shipping_class: '', - reviews_allowed: true, - average_rating: '0.00', - rating_count: 0, - upsell_ids: [], - cross_sell_ids: [], - parent_id: 0, - purchase_note: '', - categories: [ - { - id: categories.tshirtsJSON.id, + const hoodies = await request.post( + '/wp-json/wc/v3/products/categories', + { + data: { + name: 'Hoodies', + parent: clothingJSON.id, }, - ], - tags: [], - attributes: [ - { - id: attributes.colorJSON.id, - position: 0, - visible: true, - variation: true, - options: [ 'Blue', 'Green', 'Red' ], - }, - { - id: attributes.sizeJSON.id, - position: 1, - visible: true, - variation: true, - options: [ 'Large', 'Medium', 'Small' ], - }, - ], - default_attributes: [], - grouped_products: [], - menu_order: 0, - stock_status: 'instock', - }, - } ); - const vneckJSON = await vneck.json(); + } + ); + const hoodiesJSON = await hoodies.json(); - const vneckVariations = await request.post( - `/wp-json/wc/v3/products/${ vneckJSON.id }/variations/batch`, - { - data: { - create: [ - { - date_created_gmt: '2021-09-24T15:50:19', - description: variationDescription, - sku: 'woo-vneck-tee-blue', - price: '15', - regular_price: '15', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - status: 'publish', - purchasable: true, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - stock_status: 'instock', - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - weight: '0.5', - dimensions: { - length: '24', - width: '1', - height: '2', - }, - shipping_class: '', - attributes: [ - { - id: attributes.colorJSON.id, - option: 'Blue', - }, - ], - menu_order: 0, - }, - { - date_created_gmt: '2021-09-25T15:50:19', - description: variationDescription, - sku: 'woo-vneck-tee-green', - price: '20', - regular_price: '20', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - status: 'publish', - purchasable: true, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - stock_status: 'instock', - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - weight: '0.5', - dimensions: { - length: '24', - width: '1', - height: '2', - }, - shipping_class: '', - attributes: [ - { - id: attributes.colorJSON.id, - option: 'Green', - }, - ], - menu_order: 0, - }, - { - date_created_gmt: '2021-09-26T15:50:19', - description: variationDescription, - sku: 'woo-vneck-tee-red', - price: '20', - regular_price: '20', - sale_price: '', - date_on_sale_from_gmt: null, - date_on_sale_to_gmt: null, - on_sale: false, - status: 'publish', - purchasable: true, - virtual: false, - downloadable: false, - downloads: [], - download_limit: 0, - download_expiry: 0, - tax_status: 'taxable', - tax_class: '', - manage_stock: false, - stock_quantity: null, - stock_status: 'instock', - backorders: 'no', - backorders_allowed: false, - backordered: false, - low_stock_amount: null, - weight: '0.5', - dimensions: { - length: '24', - width: '1', - height: '2', - }, - shipping_class: '', - attributes: [ - { - id: attributes.colorJSON.id, - option: 'Red', - }, - ], - menu_order: 0, - }, - ], - }, - } - ); - const vneckVariationsJSON = await vneckVariations.json(); + const tshirts = await request.post( + '/wp-json/wc/v3/products/categories', + { + data: { + name: 'Tshirts', + parent: clothingJSON.id, + }, + } + ); + const tshirtsJSON = await tshirts.json(); - return { - hoodieJSON, - hoodieVariations: hoodieVariationsJSON.create, - vneckJSON, - vneckVariations: vneckVariationsJSON.create, + const decor = await request.post( + '/wp-json/wc/v3/products/categories', + { + data: { + name: 'Decor', + }, + } + ); + const decorJSON = await decor.json(); + + const music = await request.post( + '/wp-json/wc/v3/products/categories', + { + data: { + name: 'Music', + }, + } + ); + const musicJSON = await music.json(); + + return { + clothingJSON, + accessoriesJSON, + hoodiesJSON, + tshirtsJSON, + decorJSON, + musicJSON, + }; }; - }; - const createSampleHierarchicalProducts = async () => { - const parent = await request.post( '/wp-json/wc/v3/products', { - data: { - name: 'Parent Product oxo', - date_created_gmt: '2021-09-27T15:50:19', - }, - } ); - const parentJSON = await parent.json(); + const createSampleAttributes = async () => { + const color = await request.post( + '/wp-json/wc/v3/products/attributes', + { + data: { + name: 'Color', + }, + } + ); + const colorJSON = await color.json(); - const child = await request.post( '/wp-json/wc/v3/products', { - data: { - name: 'Child Product oxo', - parent_id: parentJSON.id, - date_created_gmt: '2021-09-28T15:50:19', - }, - } ); - const childJSON = await child.json(); + const size = await request.post( + '/wp-json/wc/v3/products/attributes', + { + data: { + name: 'Size', + }, + } + ); + const sizeJSON = await size.json(); - return { - parentJSON, - childJSON, + const colorNames = [ 'Blue', 'Gray', 'Green', 'Red', 'Yellow' ]; + + const colorNamesObjectArray = colorNames.map( ( name ) => ( { + name, + } ) ); + + const colors = await request.post( + `/wp-json/wc/v3/products/attributes/${ colorJSON.id }/terms/batch`, + { + data: { + create: colorNamesObjectArray, + }, + } + ); + + const colorsJSON = await colors.json(); + + const sizeNames = [ 'Large', 'Medium', 'Small' ]; + + const sizeNamesObjectArray = sizeNames.map( ( name ) => ( { + name, + } ) ); + + const sizes = await request.post( + `/wp-json/wc/v3/products/attributes/${ sizeJSON.id }/terms/batch`, + { + data: { + create: sizeNamesObjectArray, + }, + } + ); + const sizesJSON = await sizes.json(); + + return { + colorJSON, + colors: colorsJSON.create, + sizeJSON, + sizes: sizesJSON.create, + }; }; - }; - const createSampleProductReviews = async ( simpleProducts ) => { - const cap = simpleProducts.find( ( p ) => p.name === 'Cap oxo' ); - - const shirt = simpleProducts.find( - ( p ) => p.name === 'T-Shirt oxo' - ); - - const sunglasses = simpleProducts.find( - ( p ) => p.name === 'Sunglasses oxo' - ); - - const review1 = await request.post( - '/wp-json/wc/v3/products/reviews', - { - data: { - product_id: cap.id, - rating: 3, - review: 'Decent cap.', - reviewer: 'John Doe', - reviewer_email: 'john.doe@example.com', - }, - } - ); - const review1JSON = await review1.json(); - - // We need to update the review in order for the product's - // average_rating to be recalculated. - // See: https://github.com/woocommerce/woocommerce/issues/29906. - //await updateProductReview(review1.id); - await request.post( - `/wp-json/wc/v3/products/reviews/${ review1JSON.id }`, - { - data: {}, - } - ); - - const review2 = await request.post( - '/wp-json/wc/v3/products/reviews', - { - data: { - product_id: shirt.id, - rating: 5, - review: 'The BEST shirt ever!!', - reviewer: 'Shannon Smith', - reviewer_email: 'shannon.smith@example.com', - }, - } - ); - const review2JSON = await review2.json(); - - //await updateProductReview(review2.id); - await request.post( - `/wp-json/wc/v3/products/reviews/${ review2JSON.id }`, - { - data: {}, - } - ); - - const review3 = await request.post( - '/wp-json/wc/v3/products/reviews', - { - data: { - product_id: sunglasses.id, - rating: 1, - review: 'These are way too expensive.', - reviewer: 'Tim Frugalman', - reviewer_email: 'timmyfrufru@example.com', - }, - } - ); - const review3JSON = await review3.json(); - - await request.post( - `/wp-json/wc/v3/products/reviews/${ review3JSON.id }`, - { - data: {}, - } - ); - - return [ review1JSON.id, review2JSON.id, review3JSON.id ]; - }; - - const createSampleProductOrders = async ( simpleProducts ) => { - const single = simpleProducts.find( - ( p ) => p.name === 'Single oxo' - ); - const beanie = simpleProducts.find( - ( p ) => p.name === 'Beanie with Logo oxo' - ); - const shirt = simpleProducts.find( - ( p ) => p.name === 'T-Shirt oxo' - ); - - const order1 = await request.post( '/wp-json/wc/v3/orders', { - data: { - set_paid: true, - status: 'completed', - line_items: [ - { - product_id: single.id, - quantity: 2, + const createSampleTags = async () => { + const cool = await request.post( + '/wp-json/wc/v3/products/tags', + { + data: { + name: 'Cool', }, - { - product_id: beanie.id, - quantity: 3, + } + ); + const coolJSON = await cool.json(); + + return { + coolJSON, + }; + }; + + const createSampleShippingClasses = async () => { + const freight = await request.post( + '/wp-json/wc/v3/products/shipping_classes', + { + data: { + name: 'Freight', }, + } + ); + const freightJSON = await freight.json(); + + return { + freightJSON, + }; + }; + + const createSampleTaxClasses = async () => { + //check to see if Reduced Rate tax class exists - if not, create it + let reducedRate = await request.get( + '/wp-json/wc/v3/taxes/classes/reduced-rate' + ); + let reducedRateJSON = await reducedRate.json(); + expect( Array.isArray( reducedRateJSON ) ).toBe( true ); + + //if tax class does not exist then create it + if ( reducedRateJSON.length < 1 ) { + reducedRate = await request.post( + '/wp-json/wc/v3/taxes/classes', { - product_id: shirt.id, - quantity: 1, - }, - ], - }, - } ); - const orderJSON = await order1.json(); + data: { + name: 'Reduced Rate', + }, + } + ); + reducedRateJSON = await reducedRate.json(); + return { reducedRateJSON }; + } - return [ orderJSON ]; - }; + // return an empty object as nothing new was created so nothing will + // need deleted during cleanup + return {}; + }; - const productsTestSetupCreateSampleData = async () => { - const categories = await createSampleCategories(); - - const attributes = await createSampleAttributes(); - - const tags = await createSampleTags(); - - const shippingClasses = await createSampleShippingClasses(); - - const taxClasses = await createSampleTaxClasses(); - - const simpleProducts = await createSampleSimpleProducts( + const createSampleSimpleProducts = async ( categories, attributes, tags - ); + ) => { + const description = + '

    Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. ' + + 'Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. ' + + 'Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

    \n'; - const externalProducts = await createSampleExternalProducts( - categories - ); + const simpleProducts = await request.post( + '/wp-json/wc/v3/products/batch', + { + data: { + create: [ + { + name: 'Beanie with Logo oxo', + date_created_gmt: '2021-09-01T15:50:20', + type: 'simple', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a simple product.

    \n', + sku: 'Woo-beanie-logo', + price: '18', + regular_price: '20', + sale_price: '18', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: true, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '0.2', + dimensions: { + length: '6', + width: '4', + height: '1', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.accessoriesJSON.id, + }, + ], + tags: [], + attributes: [ + { + id: attributes.colorJSON.id, + position: 0, + visible: true, + variation: false, + options: [ 'Red' ], + }, + ], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 62, 63, 61, 60 ], + stock_status: 'instock', + }, + { + name: 'T-Shirt with Logo oxo', + date_created_gmt: '2021-09-02T15:50:20', + type: 'simple', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a simple product.

    \n', + sku: 'Woo-tshirt-logo', + price: '18', + regular_price: '18', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '0.5', + dimensions: { + length: '10', + width: '12', + height: '0.5', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.tshirtsJSON.id, + }, + ], + tags: [], + attributes: [ + { + id: attributes.colorJSON.id, + position: 0, + visible: true, + variation: false, + options: [ 'Gray' ], + }, + ], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 59, 67, 66, 56 ], + stock_status: 'instock', + }, + { + name: 'Single oxo', + date_created_gmt: '2021-09-03T15:50:19', + type: 'simple', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a simple, virtual product.

    \n', + sku: 'woo-single', + price: '2', + regular_price: '3', + sale_price: '2', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: true, + purchasable: true, + total_sales: 0, + virtual: true, + downloadable: true, + downloads: [ + { + id: '2579cf07-8b08-4c25-888a-b6258dd1f035', + name: 'Single', + file: 'https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg', + }, + ], + download_limit: 1, + download_expiry: 1, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '', + dimensions: { + length: '', + width: '', + height: '', + }, + shipping_required: false, + shipping_taxable: false, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.musicJSON.id, + }, + ], + tags: [], + attributes: [], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 68 ], + stock_status: 'instock', + }, + { + name: 'Album oxo', + date_created_gmt: '2021-09-04T15:50:19', + type: 'simple', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a simple, virtual product.

    \n', + sku: 'woo-album', + price: '15', + regular_price: '15', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + purchasable: true, + total_sales: 0, + virtual: true, + downloadable: true, + downloads: [ + { + id: 'cc10249f-1de2-44d4-93d3-9f88ae629f76', + name: 'Single 1', + file: 'https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg', + }, + { + id: 'aea8ef69-ccdc-4d83-8e21-3c395ebb9411', + name: 'Single 2', + file: 'https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/album.jpg', + }, + ], + download_limit: 1, + download_expiry: 1, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '', + dimensions: { + length: '', + width: '', + height: '', + }, + shipping_required: false, + shipping_taxable: false, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.musicJSON.id, + }, + ], + tags: [], + attributes: [], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 69 ], + stock_status: 'instock', + }, + { + name: 'Polo oxo', + date_created_gmt: '2021-09-05T15:50:19', + type: 'simple', + status: 'pending', + featured: false, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a simple product.

    \n', + sku: 'woo-polo', + price: '20', + regular_price: '20', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '0.8', + dimensions: { + length: '6', + width: '5', + height: '1', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.tshirtsJSON.id, + }, + ], + tags: [], + attributes: [ + { + id: attributes.colorJSON.id, + position: 0, + visible: true, + variation: false, + options: [ 'Blue' ], + }, + ], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 59, 56, 66, 76 ], + stock_status: 'instock', + }, + { + name: 'Long Sleeve Tee oxo', + date_created_gmt: '2021-09-06T15:50:19', + type: 'simple', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a simple product.

    \n', + sku: 'woo-long-sleeve-tee', + price: '25', + regular_price: '25', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '1', + dimensions: { + length: '7', + width: '5', + height: '1', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: 'freight', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.tshirtsJSON.id, + }, + ], + tags: [], + attributes: [ + { + id: attributes.colorJSON.id, + position: 0, + visible: true, + variation: false, + options: [ 'Green' ], + }, + ], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 59, 56, 76, 67 ], + stock_status: 'instock', + }, + { + name: 'Hoodie with Zipper oxo', + date_created_gmt: '2021-09-07T15:50:19', + type: 'simple', + status: 'publish', + featured: true, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a simple product.

    \n', + sku: 'woo-hoodie-with-zipper', + price: '45', + regular_price: '45', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '2', + dimensions: { + length: '8', + width: '6', + height: '2', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.hoodiesJSON.id, + }, + ], + tags: [], + attributes: [], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 57, 58 ], + stock_status: 'instock', + }, + { + name: 'Hoodie with Pocket oxo', + date_created_gmt: '2021-09-08T15:50:19', + type: 'simple', + status: 'publish', + featured: true, + catalog_visibility: 'hidden', + description, + short_description: + '

    This is a simple product.

    \n', + sku: 'woo-hoodie-with-pocket', + price: '35', + regular_price: '45', + sale_price: '35', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: true, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '3', + dimensions: { + length: '10', + width: '8', + height: '2', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.hoodiesJSON.id, + }, + ], + tags: [ + { + id: tags.coolJSON.id, + }, + ], + attributes: [ + { + id: attributes.colorJSON.id, + position: 0, + visible: true, + variation: false, + options: [ 'Gray' ], + }, + ], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 65, 57, 58 ], + stock_status: 'instock', + }, + { + name: 'Sunglasses oxo', + date_created_gmt: '2021-09-09T15:50:19', + type: 'simple', + status: 'publish', + featured: true, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a simple product.

    \n', + sku: 'woo-sunglasses', + price: '90', + regular_price: '90', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: 'reduced-rate', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '0.2', + dimensions: { + length: '4', + width: '1.4', + height: '1', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.accessoriesJSON.id, + }, + ], + tags: [ + { + id: tags.coolJSON.id, + }, + ], + attributes: [], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 60, 62, 77, 61 ], + stock_status: 'instock', + }, + { + name: 'Cap oxo', + date_created_gmt: '2021-09-10T15:50:19', + type: 'simple', + status: 'publish', + featured: true, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a simple product.

    \n', + sku: 'woo-cap', + price: '16', + regular_price: '18', + sale_price: '16', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: true, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '0.6', + dimensions: { + length: '8', + width: '6.5', + height: '4', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.accessoriesJSON.id, + }, + ], + tags: [], + attributes: [ + { + id: attributes.colorJSON.id, + position: 0, + visible: true, + variation: false, + options: [ 'Yellow' ], + }, + ], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 60, 77, 61, 63 ], + stock_status: 'instock', + }, + { + name: 'Belt oxo', + date_created_gmt: '2021-09-12T15:50:19', + type: 'simple', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a simple product.

    \n', + sku: 'woo-belt', + price: '55', + regular_price: '65', + sale_price: '55', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: true, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '1.2', + dimensions: { + length: '12', + width: '2', + height: '1.5', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.accessoriesJSON.id, + }, + ], + tags: [], + attributes: [], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 63, 77, 62, 60 ], + stock_status: 'instock', + }, + { + name: 'Beanie oxo', + date_created_gmt: '2021-09-13T15:50:19', + type: 'simple', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a simple product.

    \n', + sku: 'woo-beanie', + price: '18', + regular_price: '20', + sale_price: '18', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: true, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '0.2', + dimensions: { + length: '4', + width: '5', + height: '0.5', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.accessoriesJSON.id, + }, + ], + tags: [ + { + id: tags.coolJSON.id, + }, + ], + attributes: [ + { + id: attributes.colorJSON.id, + position: 0, + visible: true, + variation: false, + options: [ 'Red' ], + }, + ], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 63, 62, 61, 77 ], + stock_status: 'instock', + }, + { + name: 'T-Shirt oxo', + date_created_gmt: '2021-09-14T15:50:19', + type: 'simple', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a simple product.

    \n', + sku: 'woo-tshirt', + price: '18', + regular_price: '18', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '0.8', + dimensions: { + length: '8', + width: '6', + height: '1', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.tshirtsJSON.id, + }, + ], + tags: [], + attributes: [ + { + id: attributes.colorJSON.id, + position: 0, + visible: true, + variation: false, + options: [ 'Gray' ], + }, + ], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 67, 76, 56, 66 ], + stock_status: 'onbackorder', + }, + { + name: 'Hoodie with Logo oxo', + date_created_gmt: '2021-09-15T15:50:19', + type: 'simple', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a simple product.

    \n', + sku: 'woo-hoodie-with-logo', + price: '45', + regular_price: '45', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '2', + dimensions: { + length: '10', + width: '6', + height: '3', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.hoodiesJSON.id, + }, + ], + tags: [], + attributes: [ + { + id: attributes.colorJSON.id, + position: 0, + visible: true, + variation: false, + options: [ 'Blue' ], + }, + ], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [ 57, 65 ], + stock_status: 'instock', + }, + ], + }, + } + ); + const simpleProductsJSON = await simpleProducts.json(); - const groupedProducts = await createSampleGroupedProduct( - categories - ); + return simpleProductsJSON.create; + }; - const variableProducts = await createSampleVariableProducts( + const createSampleExternalProducts = async ( categories ) => { + const externalProducts = await request.post( + '/wp-json/wc/v3/products/batch', + { + data: { + create: [ + { + name: 'WordPress Pennant oxo', + date_created_gmt: '2021-09-16T15:50:20', + type: 'external', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description: + '

    Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. ' + + 'Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. ' + + 'Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

    \n', + short_description: + '

    This is an external product.

    \n', + sku: 'wp-pennant', + price: '11.05', + regular_price: '11.05', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + purchasable: false, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: + 'https://mercantile.wordpress.org/product/wordpress-pennant/', + button_text: + 'Buy on the WordPress swag store!', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '', + dimensions: { + length: '', + width: '', + height: '', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.decorJSON.id, + }, + ], + tags: [], + attributes: [], + default_attributes: [], + variations: [], + grouped_products: [], + menu_order: 0, + related_ids: [], + stock_status: 'instock', + }, + ], + }, + } + ); + const externalProductsJSON = await externalProducts.json(); + + return externalProductsJSON.create; + }; + + const createSampleGroupedProduct = async ( categories ) => { + const logoProducts = await request.get( + '/wp-json/wc/v3/products', + { + params: { + search: 'logo', + _fields: [ 'id' ], + }, + } + ); + const logoProductsJSON = await logoProducts.json(); + + const groupedProducts = await request.post( + '/wp-json/wc/v3/products/batch', + { + data: { + create: [ + { + name: 'Logo Collection oxo', + date_created_gmt: '2021-09-17T15:50:20', + type: 'grouped', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description: + '

    Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. ' + + 'Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. ' + + 'Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

    \n', + short_description: + '

    This is a grouped product.

    \n', + sku: 'logo-collection', + price: '18', + regular_price: '', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: true, + purchasable: false, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '', + dimensions: { + length: '', + width: '', + height: '', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.clothingJSON.id, + }, + ], + tags: [], + attributes: [], + default_attributes: [], + variations: [], + grouped_products: logoProductsJSON.map( + ( p ) => p.id + ), + menu_order: 0, + related_ids: [], + stock_status: 'instock', + }, + ], + }, + } + ); + const groupedProductsJSON = await groupedProducts.json(); + + return groupedProductsJSON.create; + }; + + const createSampleVariableProducts = async ( categories, attributes - ); + ) => { + const description = + '

    Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. ' + + 'Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. ' + + 'Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

    \n'; - const hierarchicalProducts = - await createSampleHierarchicalProducts(); + const hoodie = await request.post( '/wp-json/wc/v3/products', { + data: { + name: 'Hoodie oxo', + date_created_gmt: '2021-09-18T15:50:19', + type: 'variable', + status: 'publish', + featured: false, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a variable product.

    \n', + sku: 'woo-hoodie', + price: '42', + regular_price: '', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: true, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '1.5', + dimensions: { + length: '10', + width: '8', + height: '3', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.hoodiesJSON.id, + }, + ], + tags: [], + attributes: [ + { + id: attributes.colorJSON.id, + position: 0, + visible: true, + variation: true, + options: [ 'Blue', 'Green', 'Red' ], + }, + { + id: 0, + name: 'Logo', + position: 1, + visible: true, + variation: true, + options: [ 'Yes', 'No' ], + }, + ], + default_attributes: [], + grouped_products: [], + menu_order: 0, + stock_status: 'instock', + }, + } ); + const hoodieJSON = await hoodie.json(); - const reviewIds = await createSampleProductReviews( - simpleProducts - ); + const variationDescription = + '

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. ' + + 'Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. ' + + 'Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. ' + + 'Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. ' + + 'Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. ' + + 'Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.

    \n'; - const orders = await createSampleProductOrders( simpleProducts ); + const hoodieVariations = await request.post( + `/wp-json/wc/v3/products/${ hoodieJSON.id }/variations/batch`, + { + data: { + create: [ + { + date_created_gmt: '2021-09-19T15:50:20', + description: variationDescription, + sku: 'woo-hoodie-blue-logo', + price: '45', + regular_price: '45', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + status: 'publish', + purchasable: true, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + stock_status: 'instock', + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + weight: '1.5', + dimensions: { + length: '10', + width: '8', + height: '3', + }, + shipping_class: '', + attributes: [ + { + id: attributes.colorJSON.id, + option: 'Blue', + }, + { + id: 0, + name: 'Logo', + option: 'Yes', + }, + ], + menu_order: 0, + }, + { + date_created_gmt: '2021-09-20T15:50:20', + description: variationDescription, + sku: 'woo-hoodie-blue', + price: '45', + regular_price: '45', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + status: 'publish', + purchasable: true, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + stock_status: 'instock', + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + weight: '1.5', + dimensions: { + length: '10', + width: '8', + height: '3', + }, + shipping_class: '', + attributes: [ + { + id: attributes.colorJSON.id, + option: 'Blue', + }, + { + id: 0, + name: 'Logo', + option: 'No', + }, + ], + menu_order: 3, + }, + { + date_created_gmt: '2021-09-21T15:50:20', + description: variationDescription, + sku: 'woo-hoodie-green', + price: '45', + regular_price: '45', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + status: 'publish', + purchasable: true, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + stock_status: 'instock', + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + weight: '1.5', + dimensions: { + length: '10', + width: '8', + height: '3', + }, + shipping_class: '', + attributes: [ + { + id: attributes.colorJSON.id, + option: 'Green', + }, + { + id: 0, + name: 'Logo', + option: 'No', + }, + ], + menu_order: 2, + }, + { + date_created_gmt: '2021-09-22T15:50:19', + description: variationDescription, + sku: 'woo-hoodie-red', + price: '42', + regular_price: '45', + sale_price: '42', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: true, + status: 'publish', + purchasable: true, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + stock_status: 'instock', + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + weight: '1.5', + dimensions: { + length: '10', + width: '8', + height: '3', + }, + shipping_class: '', + attributes: [ + { + id: attributes.colorJSON.id, + option: 'Red', + }, + { + id: 0, + name: 'Logo', + option: 'No', + }, + ], + menu_order: 1, + }, + ], + }, + } + ); + const hoodieVariationsJSON = await hoodieVariations.json(); - return { - categories, - attributes, - tags, - shippingClasses, - taxClasses, - simpleProducts, - externalProducts, - groupedProducts, - variableProducts, - hierarchicalProducts, - reviewIds, - orders, + const vneck = await request.post( '/wp-json/wc/v3/products', { + data: { + name: 'V-Neck T-Shirt oxo', + date_created_gmt: '2021-09-23T15:50:19', + type: 'variable', + status: 'publish', + featured: true, + catalog_visibility: 'visible', + description, + short_description: + '

    This is a variable product.

    \n', + sku: 'woo-vneck-tee', + price: '15', + regular_price: '', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + purchasable: true, + total_sales: 0, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + external_url: '', + button_text: '', + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + sold_individually: false, + weight: '0.5', + dimensions: { + length: '24', + width: '1', + height: '2', + }, + shipping_required: true, + shipping_taxable: true, + shipping_class: '', + reviews_allowed: true, + average_rating: '0.00', + rating_count: 0, + upsell_ids: [], + cross_sell_ids: [], + parent_id: 0, + purchase_note: '', + categories: [ + { + id: categories.tshirtsJSON.id, + }, + ], + tags: [], + attributes: [ + { + id: attributes.colorJSON.id, + position: 0, + visible: true, + variation: true, + options: [ 'Blue', 'Green', 'Red' ], + }, + { + id: attributes.sizeJSON.id, + position: 1, + visible: true, + variation: true, + options: [ 'Large', 'Medium', 'Small' ], + }, + ], + default_attributes: [], + grouped_products: [], + menu_order: 0, + stock_status: 'instock', + }, + } ); + const vneckJSON = await vneck.json(); + + const vneckVariations = await request.post( + `/wp-json/wc/v3/products/${ vneckJSON.id }/variations/batch`, + { + data: { + create: [ + { + date_created_gmt: '2021-09-24T15:50:19', + description: variationDescription, + sku: 'woo-vneck-tee-blue', + price: '15', + regular_price: '15', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + status: 'publish', + purchasable: true, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + stock_status: 'instock', + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + weight: '0.5', + dimensions: { + length: '24', + width: '1', + height: '2', + }, + shipping_class: '', + attributes: [ + { + id: attributes.colorJSON.id, + option: 'Blue', + }, + ], + menu_order: 0, + }, + { + date_created_gmt: '2021-09-25T15:50:19', + description: variationDescription, + sku: 'woo-vneck-tee-green', + price: '20', + regular_price: '20', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + status: 'publish', + purchasable: true, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + stock_status: 'instock', + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + weight: '0.5', + dimensions: { + length: '24', + width: '1', + height: '2', + }, + shipping_class: '', + attributes: [ + { + id: attributes.colorJSON.id, + option: 'Green', + }, + ], + menu_order: 0, + }, + { + date_created_gmt: '2021-09-26T15:50:19', + description: variationDescription, + sku: 'woo-vneck-tee-red', + price: '20', + regular_price: '20', + sale_price: '', + date_on_sale_from_gmt: null, + date_on_sale_to_gmt: null, + on_sale: false, + status: 'publish', + purchasable: true, + virtual: false, + downloadable: false, + downloads: [], + download_limit: 0, + download_expiry: 0, + tax_status: 'taxable', + tax_class: '', + manage_stock: false, + stock_quantity: null, + stock_status: 'instock', + backorders: 'no', + backorders_allowed: false, + backordered: false, + low_stock_amount: null, + weight: '0.5', + dimensions: { + length: '24', + width: '1', + height: '2', + }, + shipping_class: '', + attributes: [ + { + id: attributes.colorJSON.id, + option: 'Red', + }, + ], + menu_order: 0, + }, + ], + }, + } + ); + const vneckVariationsJSON = await vneckVariations.json(); + + return { + hoodieJSON, + hoodieVariations: hoodieVariationsJSON.create, + vneckJSON, + vneckVariations: vneckVariationsJSON.create, + }; }; - }; - // create Sample Data function - const createSampleData = async () => { - const testProductData = await productsTestSetupCreateSampleData(); - const orderedProducts = { - pocketHoodie: testProductData.simpleProducts.find( - ( p ) => p.name === 'Hoodie with Pocket oxo' - ), - sunglasses: testProductData.simpleProducts.find( + const createSampleHierarchicalProducts = async () => { + const parent = await request.post( '/wp-json/wc/v3/products', { + data: { + name: 'Parent Product oxo', + date_created_gmt: '2021-09-27T15:50:19', + }, + } ); + const parentJSON = await parent.json(); + + const child = await request.post( '/wp-json/wc/v3/products', { + data: { + name: 'Child Product oxo', + parent_id: parentJSON.id, + date_created_gmt: '2021-09-28T15:50:19', + }, + } ); + const childJSON = await child.json(); + + return { + parentJSON, + childJSON, + }; + }; + + const createSampleProductReviews = async ( simpleProducts ) => { + const cap = simpleProducts.find( + ( p ) => p.name === 'Cap oxo' + ); + + const shirt = simpleProducts.find( + ( p ) => p.name === 'T-Shirt oxo' + ); + + const sunglasses = simpleProducts.find( ( p ) => p.name === 'Sunglasses oxo' - ), - beanie: testProductData.simpleProducts.find( - ( p ) => p.name === 'Beanie oxo' - ), - blueVneck: - testProductData.variableProducts.vneckVariations.find( - ( p ) => p.sku === 'woo-vneck-tee-blue' + ); + + const review1 = await request.post( + '/wp-json/wc/v3/products/reviews', + { + data: { + product_id: cap.id, + rating: 3, + review: 'Decent cap.', + reviewer: 'John Doe', + reviewer_email: 'john.doe@example.com', + }, + } + ); + const review1JSON = await review1.json(); + + // We need to update the review in order for the product's + // average_rating to be recalculated. + // See: https://github.com/woocommerce/woocommerce/issues/29906. + //await updateProductReview(review1.id); + await request.post( + `/wp-json/wc/v3/products/reviews/${ review1JSON.id }`, + { + data: {}, + } + ); + + const review2 = await request.post( + '/wp-json/wc/v3/products/reviews', + { + data: { + product_id: shirt.id, + rating: 5, + review: 'The BEST shirt ever!!', + reviewer: 'Shannon Smith', + reviewer_email: 'shannon.smith@example.com', + }, + } + ); + const review2JSON = await review2.json(); + + //await updateProductReview(review2.id); + await request.post( + `/wp-json/wc/v3/products/reviews/${ review2JSON.id }`, + { + data: {}, + } + ); + + const review3 = await request.post( + '/wp-json/wc/v3/products/reviews', + { + data: { + product_id: sunglasses.id, + rating: 1, + review: 'These are way too expensive.', + reviewer: 'Tim Frugalman', + reviewer_email: 'timmyfrufru@example.com', + }, + } + ); + const review3JSON = await review3.json(); + + await request.post( + `/wp-json/wc/v3/products/reviews/${ review3JSON.id }`, + { + data: {}, + } + ); + + return [ review1JSON.id, review2JSON.id, review3JSON.id ]; + }; + + const createSampleProductOrders = async ( simpleProducts ) => { + const single = simpleProducts.find( + ( p ) => p.name === 'Single oxo' + ); + const beanie = simpleProducts.find( + ( p ) => p.name === 'Beanie with Logo oxo' + ); + const shirt = simpleProducts.find( + ( p ) => p.name === 'T-Shirt oxo' + ); + + const order1 = await request.post( '/wp-json/wc/v3/orders', { + data: { + set_paid: true, + status: 'completed', + line_items: [ + { + product_id: single.id, + quantity: 2, + }, + { + product_id: beanie.id, + quantity: 3, + }, + { + product_id: shirt.id, + quantity: 1, + }, + ], + }, + } ); + const orderJSON = await order1.json(); + + return [ orderJSON ]; + }; + + const productsTestSetupCreateSampleData = async () => { + const categories = await createSampleCategories(); + + const attributes = await createSampleAttributes(); + + const tags = await createSampleTags(); + + const shippingClasses = await createSampleShippingClasses(); + + const taxClasses = await createSampleTaxClasses(); + + const simpleProducts = await createSampleSimpleProducts( + categories, + attributes, + tags + ); + + const externalProducts = await createSampleExternalProducts( + categories + ); + + const groupedProducts = await createSampleGroupedProduct( + categories + ); + + const variableProducts = await createSampleVariableProducts( + categories, + attributes + ); + + const hierarchicalProducts = + await createSampleHierarchicalProducts(); + + const reviewIds = await createSampleProductReviews( + simpleProducts + ); + + const orders = await createSampleProductOrders( + simpleProducts + ); + + return { + categories, + attributes, + tags, + shippingClasses, + taxClasses, + simpleProducts, + externalProducts, + groupedProducts, + variableProducts, + hierarchicalProducts, + reviewIds, + orders, + }; + }; + + // create Sample Data function + const createSampleData = async () => { + const testProductData = + await productsTestSetupCreateSampleData(); + const orderedProducts = { + pocketHoodie: testProductData.simpleProducts.find( + ( p ) => p.name === 'Hoodie with Pocket oxo' ), - pennant: testProductData.externalProducts[ 0 ], - }; + sunglasses: testProductData.simpleProducts.find( + ( p ) => p.name === 'Sunglasses oxo' + ), + beanie: testProductData.simpleProducts.find( + ( p ) => p.name === 'Beanie oxo' + ), + blueVneck: + testProductData.variableProducts.vneckVariations.find( + ( p ) => p.sku === 'woo-vneck-tee-blue' + ), + pennant: testProductData.externalProducts[ 0 ], + }; - const johnAddress = { - first_name: 'John', - last_name: 'Doe', - company: 'Automattic', - country: 'US', - address_1: '60 29th Street', - address_2: '#343', - city: 'San Francisco', - state: 'CA', - postcode: '94110', - phone: '123456789', - }; - const tinaAddress = { - first_name: 'Tina', - last_name: 'Clark', - company: 'Automattic', - country: 'US', - address_1: 'Oxford Ave', - address_2: '', - city: 'Buffalo', - state: 'NY', - postcode: '14201', - phone: '123456789', - }; - const guestShippingAddress = { - first_name: 'Ano', - last_name: 'Nymous', - company: '', - country: 'US', - address_1: '0 Incognito St', - address_2: '', - city: 'Erie', - state: 'PA', - postcode: '16515', - phone: '123456789', - }; - const guestBillingAddress = { - first_name: 'Ben', - last_name: 'Efactor', - company: '', - country: 'US', - address_1: '200 W University Avenue', - address_2: '', - city: 'Gainesville', - state: 'FL', - postcode: '32601', - phone: '123456789', - email: 'ben.efactor@email.net', - }; - - const john = await request.post( '/wp-json/wc/v3/customers', { - data: { + const johnAddress = { first_name: 'John', last_name: 'Doe', - username: 'john.doe', - email: 'john.doe@example.com', - billing: { - ...johnAddress, - email: 'john.doe@example.com', - }, - shipping: johnAddress, - }, - } ); - const johnJSON = await john.json(); - - const tina = await request.post( '/wp-json/wc/v3/customers', { - data: { + company: 'Automattic', + country: 'US', + address_1: '60 29th Street', + address_2: '#343', + city: 'San Francisco', + state: 'CA', + postcode: '94110', + phone: '123456789', + }; + const tinaAddress = { first_name: 'Tina', last_name: 'Clark', - username: 'tina.clark', - email: 'tina.clark@example.com', - billing: { - ...tinaAddress, - email: 'tina.clark@example.com', - }, - shipping: tinaAddress, - }, - } ); - const tinaJSON = await tina.json(); + company: 'Automattic', + country: 'US', + address_1: 'Oxford Ave', + address_2: '', + city: 'Buffalo', + state: 'NY', + postcode: '14201', + phone: '123456789', + }; + const guestShippingAddress = { + first_name: 'Ano', + last_name: 'Nymous', + company: '', + country: 'US', + address_1: '0 Incognito St', + address_2: '', + city: 'Erie', + state: 'PA', + postcode: '16515', + phone: '123456789', + }; + const guestBillingAddress = { + first_name: 'Ben', + last_name: 'Efactor', + company: '', + country: 'US', + address_1: '200 W University Avenue', + address_2: '', + city: 'Gainesville', + state: 'FL', + postcode: '32601', + phone: '123456789', + email: 'ben.efactor@email.net', + }; - const orderBaseData = { - payment_method: 'cod', - payment_method_title: 'Cash on Delivery', - status: 'processing', - set_paid: false, - currency: 'USD', - customer_id: 0, - }; - - const orders = []; - // Have "John" order all products. - Object.values( orderedProducts ).forEach( async ( product ) => { - const order2 = await request.post( '/wp-json/wc/v3/orders', { + const john = await request.post( '/wp-json/wc/v3/customers', { data: { - ...orderBaseData, - customer_id: johnJSON.id, + first_name: 'John', + last_name: 'Doe', + username: 'john.doe', + email: 'john.doe@example.com', billing: { ...johnAddress, email: 'john.doe@example.com', }, shipping: johnAddress, - line_items: [ - { - product_id: product.id, - quantity: 1, - }, - ], }, } ); - const orderJSON = await order2.json(); + const johnJSON = await john.json(); - orders.push( orderJSON ); - } ); - - // Have "Tina" order some sunglasses and make a child order. - // This somewhat resembles a subscription renewal, but we're just testing the `parent` field. - const order2 = await request.post( '/wp-json/wc/v3/orders', { - data: { - ...orderBaseData, - status: 'completed', - set_paid: true, - customer_id: tinaJSON.id, - billing: { - ...tinaAddress, - email: 'tina.clark@example.com', - }, - shipping: tinaAddress, - line_items: [ - { - product_id: orderedProducts.sunglasses.id, - quantity: 1, - }, - ], - }, - } ); - const order2JSON = await order2.json(); - - orders.push( order2JSON ); - - // create child order by referencing a parent_id - const order3 = await request.post( '/wp-json/wc/v3/orders', { - data: { - ...orderBaseData, - parent_id: order2JSON.id, - customer_id: tinaJSON.id, - billing: { - ...tinaAddress, - email: 'tina.clark@example.com', - }, - shipping: tinaAddress, - line_items: [ - { - product_id: orderedProducts.sunglasses.id, - quantity: 1, - }, - ], - }, - } ); - const order3JSON = await order3.json(); - - orders.push( order3JSON ); - - // Guest order. - const guestOrder = await request.post( '/wp-json/wc/v3/orders', { - data: { - ...orderBaseData, - billing: guestBillingAddress, - shipping: guestShippingAddress, - line_items: [ - { - product_id: orderedProducts.pennant.id, - quantity: 2, - }, - { - product_id: orderedProducts.beanie.id, - quantity: 1, - }, - ], - }, - } ); - const guestOrderJSON = await guestOrder.json(); - - // Create an order with all possible numerical fields (taxes, fees, refunds, etc). - await request.put( - '/wp-json/wc/v3/settings/general/woocommerce_calc_taxes', - { + const tina = await request.post( '/wp-json/wc/v3/customers', { data: { - value: 'yes', + first_name: 'Tina', + last_name: 'Clark', + username: 'tina.clark', + email: 'tina.clark@example.com', + billing: { + ...tinaAddress, + email: 'tina.clark@example.com', + }, + shipping: tinaAddress, }, - } - ); + } ); + const tinaJSON = await tina.json(); - await request.post( '/wp-json/wc/v3/taxes', { - data: { - country: '*', - state: '*', - postcode: '*', - city: '*', - name: 'Tax', - rate: '5.5', - shipping: true, - }, - } ); + const orderBaseData = { + payment_method: 'cod', + payment_method_title: 'Cash on Delivery', + status: 'processing', + set_paid: false, + currency: 'USD', + customer_id: 0, + }; - const coupon = await request.post( '/wp-json/wc/v3/coupons', { - data: { - code: 'save5', - amount: '5', - }, - } ); - const couponJSON = await coupon.json(); - - const order4 = await request.post( '/wp-json/wc/v3/orders', { - data: { - ...orderBaseData, - line_items: [ + const orders = []; + // Have "John" order all products. + Object.values( orderedProducts ).forEach( async ( product ) => { + const order2 = await request.post( + '/wp-json/wc/v3/orders', { - product_id: orderedProducts.blueVneck.id, - quantity: 1, - }, - ], - coupon_lines: [ - { - code: 'save5', - }, - ], - shipping_lines: [ - { - method_id: 'flat_rate', - total: '5.00', - }, - ], - fee_lines: [ - { - total: '1.00', - name: 'Test Fee', - }, - ], - }, - } ); - const order4JSON = await order4.json(); - - await request.post( - `/wp-json/wc/v3/orders/${ order4JSON.id }/refunds`, - { - data: { - api_refund: false, // Prevent an actual refund request (fails with CoD), - line_items: [ - { - id: order4JSON.line_items[ 0 ].id, - quantity: 1, - refund_total: order4JSON.line_items[ 0 ].total, - refund_tax: [ + data: { + ...orderBaseData, + customer_id: johnJSON.id, + billing: { + ...johnAddress, + email: 'john.doe@example.com', + }, + shipping: johnAddress, + line_items: [ { - id: order4JSON.line_items[ 0 ] - .taxes[ 0 ].id, - refund_total: - order4JSON.line_items[ 0 ] - .total_tax, + product_id: product.id, + quantity: 1, }, ], }, + } + ); + const orderJSON = await order2.json(); + + orders.push( orderJSON ); + } ); + + // Have "Tina" order some sunglasses and make a child order. + // This somewhat resembles a subscription renewal, but we're just testing the `parent` field. + const order2 = await request.post( '/wp-json/wc/v3/orders', { + data: { + ...orderBaseData, + status: 'completed', + set_paid: true, + customer_id: tinaJSON.id, + billing: { + ...tinaAddress, + email: 'tina.clark@example.com', + }, + shipping: tinaAddress, + line_items: [ + { + product_id: orderedProducts.sunglasses.id, + quantity: 1, + }, ], }, - } - ); - orders.push( order4JSON ); + } ); + const order2JSON = await order2.json(); - return { - customers: { - johnJSON, - tinaJSON, - }, - orders, - precisionOrder: order4JSON, - hierarchicalOrders: { - parent: order2JSON, - child: order3JSON, - }, - guestOrderJSON, - testProductData, - couponJSON, + orders.push( order2JSON ); + + // create child order by referencing a parent_id + const order3 = await request.post( '/wp-json/wc/v3/orders', { + data: { + ...orderBaseData, + parent_id: order2JSON.id, + customer_id: tinaJSON.id, + billing: { + ...tinaAddress, + email: 'tina.clark@example.com', + }, + shipping: tinaAddress, + line_items: [ + { + product_id: orderedProducts.sunglasses.id, + quantity: 1, + }, + ], + }, + } ); + const order3JSON = await order3.json(); + + orders.push( order3JSON ); + + // Guest order. + const guestOrder = await request.post( + '/wp-json/wc/v3/orders', + { + data: { + ...orderBaseData, + billing: guestBillingAddress, + shipping: guestShippingAddress, + line_items: [ + { + product_id: orderedProducts.pennant.id, + quantity: 2, + }, + { + product_id: orderedProducts.beanie.id, + quantity: 1, + }, + ], + }, + } + ); + const guestOrderJSON = await guestOrder.json(); + + // Create an order with all possible numerical fields (taxes, fees, refunds, etc). + await request.put( + '/wp-json/wc/v3/settings/general/woocommerce_calc_taxes', + { + data: { + value: 'yes', + }, + } + ); + + await request.post( '/wp-json/wc/v3/taxes', { + data: { + country: '*', + state: '*', + postcode: '*', + city: '*', + name: 'Tax', + rate: '5.5', + shipping: true, + }, + } ); + + const coupon = await request.post( '/wp-json/wc/v3/coupons', { + data: { + code: 'save5', + amount: '5', + }, + } ); + const couponJSON = await coupon.json(); + + const order4 = await request.post( '/wp-json/wc/v3/orders', { + data: { + ...orderBaseData, + line_items: [ + { + product_id: orderedProducts.blueVneck.id, + quantity: 1, + }, + ], + coupon_lines: [ + { + code: 'save5', + }, + ], + shipping_lines: [ + { + method_id: 'flat_rate', + total: '5.00', + }, + ], + fee_lines: [ + { + total: '1.00', + name: 'Test Fee', + }, + ], + }, + } ); + const order4JSON = await order4.json(); + + await request.post( + `/wp-json/wc/v3/orders/${ order4JSON.id }/refunds`, + { + data: { + api_refund: false, // Prevent an actual refund request (fails with CoD), + line_items: [ + { + id: order4JSON.line_items[ 0 ].id, + quantity: 1, + refund_total: + order4JSON.line_items[ 0 ].total, + refund_tax: [ + { + id: order4JSON.line_items[ 0 ] + .taxes[ 0 ].id, + refund_total: + order4JSON.line_items[ 0 ] + .total_tax, + }, + ], + }, + ], + }, + } + ); + orders.push( order4JSON ); + + return { + customers: { + johnJSON, + tinaJSON, + }, + orders, + precisionOrder: order4JSON, + hierarchicalOrders: { + parent: order2JSON, + child: order3JSON, + }, + guestOrderJSON, + testProductData, + couponJSON, + }; }; - }; - sampleData = await createSampleData(); - }, 100000 ); + sampleData = await createSampleData(); + }, 100000 ); - test.afterAll( async ( { request } ) => { - const productsTestSetupDeleteSampleData = async ( _sampleData ) => { - const { - categories, - attributes, - tags, - shippingClasses, - taxClasses, - simpleProducts, - externalProducts, - groupedProducts, - variableProducts, - hierarchicalProducts, - orders, - } = _sampleData; + test.afterAll( async ( { request } ) => { + const productsTestSetupDeleteSampleData = async ( _sampleData ) => { + const { + categories, + attributes, + tags, + shippingClasses, + taxClasses, + simpleProducts, + externalProducts, + groupedProducts, + variableProducts, + hierarchicalProducts, + orders, + } = _sampleData; - const productIds = [] - .concat( simpleProducts.map( ( p ) => p.id ) ) - .concat( externalProducts.map( ( p ) => p.id ) ) - .concat( groupedProducts.map( ( p ) => p.id ) ) - .concat( [ - variableProducts.hoodieJSON.id, - variableProducts.vneckJSON.id, - ] ) - .concat( [ - hierarchicalProducts.parentJSON.id, - hierarchicalProducts.childJSON.id, - ] ); + const productIds = [] + .concat( simpleProducts.map( ( p ) => p.id ) ) + .concat( externalProducts.map( ( p ) => p.id ) ) + .concat( groupedProducts.map( ( p ) => p.id ) ) + .concat( [ + variableProducts.hoodieJSON.id, + variableProducts.vneckJSON.id, + ] ) + .concat( [ + hierarchicalProducts.parentJSON.id, + hierarchicalProducts.childJSON.id, + ] ); - for ( const _order of orders ) { - await request.delete( `/wp-json/wc/v3/orders/${ _order.id }`, { - data: { - force: true, - }, - } ); - } - - for ( const productId of productIds ) { - await request.delete( - `/wp-json/wc/v3/products/${ productId }`, - { - data: { - force: true, - }, - } - ); - } - - await request.delete( - `/wp-json/wc/v3/products/attributes/${ attributes.colorJSON.id }`, - { - data: { - force: true, - }, + for ( const _order of orders ) { + await request.delete( + `/wp-json/wc/v3/orders/${ _order.id }`, + { + data: { + force: true, + }, + } + ); } - ); - await request.delete( - `/wp-json/wc/v3/products/attributes/${ attributes.sizeJSON.id }`, - { - data: { - force: true, - }, + for ( const productId of productIds ) { + await request.delete( + `/wp-json/wc/v3/products/${ productId }`, + { + data: { + force: true, + }, + } + ); } - ); - for ( const category of Object.values( categories ) ) { await request.delete( - `/wp-json/wc/v3/products/categories/${ category.id }`, + `/wp-json/wc/v3/products/attributes/${ attributes.colorJSON.id }`, { data: { force: true, }, } ); - } - for ( const tag of Object.values( tags ) ) { await request.delete( - `/wp-json/wc/v3/products/tags/${ tag.id }`, + `/wp-json/wc/v3/products/attributes/${ attributes.sizeJSON.id }`, { data: { force: true, }, } ); - } - for ( const shippingClass of Object.values( shippingClasses ) ) { - await request.delete( - `/wp-json/wc/v3/products/shipping_classes/${ shippingClass.id }`, - { - data: { - force: true, - }, - } + for ( const category of Object.values( categories ) ) { + await request.delete( + `/wp-json/wc/v3/products/categories/${ category.id }`, + { + data: { + force: true, + }, + } + ); + } + + for ( const tag of Object.values( tags ) ) { + await request.delete( + `/wp-json/wc/v3/products/tags/${ tag.id }`, + { + data: { + force: true, + }, + } + ); + } + + for ( const shippingClass of Object.values( + shippingClasses + ) ) { + await request.delete( + `/wp-json/wc/v3/products/shipping_classes/${ shippingClass.id }`, + { + data: { + force: true, + }, + } + ); + } + + for ( const taxClass of Object.values( taxClasses ) ) { + await request.delete( + `/wp-json/wc/v3/taxes/classes/${ taxClass.slug }`, + { + data: { + force: true, + }, + } + ); + } + }; + + const deleteSampleData = async ( _sampleData ) => { + await productsTestSetupDeleteSampleData( + _sampleData.testProductData ); - } - for ( const taxClass of Object.values( taxClasses ) ) { - await request.delete( - `/wp-json/wc/v3/taxes/classes/${ taxClass.slug }`, - { - data: { - force: true, - }, - } - ); - } - }; + for ( const _order of _sampleData.orders.concat( [ + _sampleData.guestOrderJSON, + ] ) ) { + await request.delete( + `/wp-json/wc/v3/orders/${ _order.id }`, + { + data: { + force: true, + }, + } + ); + } - const deleteSampleData = async ( _sampleData ) => { - await productsTestSetupDeleteSampleData( - _sampleData.testProductData - ); + for ( const customer of Object.values( + _sampleData.customers + ) ) { + await request.delete( + `/wp-json/wc/v3/customers/${ customer.id }`, + { + data: { + force: true, + }, + } + ); + } + }; - for ( const _order of _sampleData.orders.concat( [ - _sampleData.guestOrderJSON, - ] ) ) { - await request.delete( `/wp-json/wc/v3/orders/${ _order.id }`, { - data: { - force: true, - }, - } ); - } + await deleteSampleData( sampleData ); + }, 10000 ); - for ( const customer of Object.values( _sampleData.customers ) ) { - await request.delete( - `/wp-json/wc/v3/customers/${ customer.id }`, - { - data: { - force: true, - }, - } - ); - } - }; - - await deleteSampleData( sampleData ); - }, 10000 ); - - test( 'can create an order', async ( { request } ) => { - const response = await request.post( '/wp-json/wc/v3/orders', { - data: order, - } ); - const responseJSON = await response.json(); - - expect( response.status() ).toEqual( 201 ); - expect( responseJSON.id ).toBeDefined(); - orderId = responseJSON.id; - - // Validate the data type and verify the order is in a pending state - expect( typeof responseJSON.status ).toBe( 'string' ); - expect( responseJSON.status ).toEqual( 'pending' ); - } ); - - test( 'can retrieve an order', async ( { request } ) => { - const response = await request.get( - `/wp-json/wc/v3/orders/${ orderId }` - ); - const responseJSON = await response.json(); - - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.id ).toEqual( orderId ); - } ); - - test( 'can add shipping and billing contacts to an order', async ( { - request, - } ) => { - // Update the billing and shipping fields on the order - order.billing = updatedCustomerBilling; - order.shipping = updatedCustomerShipping; - - const response = await request.put( - `/wp-json/wc/v3/orders/${ orderId }`, - { + test( 'can create an order', async ( { request } ) => { + const response = await request.post( '/wp-json/wc/v3/orders', { data: order, - } - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - - expect( responseJSON.billing ).toEqual( updatedCustomerBilling ); - expect( responseJSON.shipping ).toEqual( updatedCustomerShipping ); - } ); - - test( 'can permanently delete an order', async ( { request } ) => { - const response = await request.delete( - `/wp-json/wc/v3/orders/${ orderId }`, - { - data: { - force: true, - }, - } - ); - expect( response.status() ).toEqual( 200 ); - - const getOrderResponse = await request.get( - `/wp-json/wc/v3/orders/${ orderId }` - ); - expect( getOrderResponse.status() ).toEqual( 404 ); - } ); - - test.describe( 'List all orders', () => { - const ORDERS_COUNT = 10; - - test( 'pagination', async ( { request } ) => { - const pageSize = 4; - const page1 = await request.get( '/wp-json/wc/v3/orders', { - params: { - per_page: pageSize, - search: 'oxo', - }, } ); - const page1JSON = await page1.json(); + const responseJSON = await response.json(); - const page2 = await request.get( '/wp-json/wc/v3/orders', { - params: { - per_page: pageSize, - page: 2, - search: 'oxo', - }, - } ); - const page2JSON = await page2.json(); + expect( response.status() ).toEqual( 201 ); + expect( responseJSON.id ).toBeDefined(); + orderId = responseJSON.id; - expect( page1.status() ).toEqual( 200 ); - expect( page2.status() ).toEqual( 200 ); - - // Verify total page count. - expect( page1.headers()[ 'x-wp-total' ] ).toEqual( - ORDERS_COUNT.toString() - ); - expect( page1.headers()[ 'x-wp-totalpages' ] ).toEqual( '3' ); - - // Verify we get pageSize'd arrays. - expect( Array.isArray( page1JSON ) ).toBe( true ); - expect( Array.isArray( page2JSON ) ).toBe( true ); - expect( page1JSON ).toHaveLength( pageSize ); - expect( page2JSON ).toHaveLength( pageSize ); - - // Ensure all of the order IDs are unique (no page overlap). - const allOrderIds = page1JSON - .concat( page2JSON ) - .reduce( ( acc, { id } ) => { - acc[ id ] = 1; - return acc; - }, {} ); - expect( Object.keys( allOrderIds ) ).toHaveLength( pageSize * 2 ); - - // Verify that offset takes precedent over page number. - const page2Offset = await request.get( 'wp-json/wc/v3/orders', { - params: { - per_page: pageSize, - page: 2, - offset: pageSize + 1, - search: 'oxo', - }, - } ); - const page2OffsetJSON = await page2Offset.json(); - - // The offset pushes the result set 1 order past the start of page 2. - expect( page2OffsetJSON ).toEqual( - expect.not.arrayContaining( [ - expect.objectContaining( { - id: page2JSON[ 0 ].id, - } ), - ] ) - ); - expect( page2OffsetJSON[ 0 ].id ).toEqual( page2JSON[ 1 ].id ); - - // Verify the last page only has 1 order as we expect. - const lastPage = await request.get( 'wp-json/wc/v3/orders', { - params: { - per_page: pageSize, - page: 3, - search: 'oxo', - }, - } ); - const lastPageJSON = await lastPage.json(); - - expect( Array.isArray( lastPageJSON ) ).toBe( true ); - expect( lastPageJSON ).toHaveLength( 2 ); - - // Verify a page outside the total page count is empty. - const page6 = await request.get( 'wp-json/wc/v3/orders', { - params: { - page: 6, - search: 'oxo', - }, - } ); - const page6JSON = await page6.json(); - - expect( Array.isArray( page6JSON ) ).toBe( true ); - expect( page6JSON ).toHaveLength( 0 ); + // Validate the data type and verify the order is in a pending state + expect( typeof responseJSON.status ).toBe( 'string' ); + expect( responseJSON.status ).toEqual( 'pending' ); } ); - test( 'inclusion / exclusion', async ( { request } ) => { - const allOrders = await request.get( 'wp-json/wc/v3/orders', { - params: { - per_page: 10, - search: 'oxo', - }, - } ); - const allOrdersJSON = await allOrders.json(); - - expect( allOrders.status() ).toEqual( 200 ); - const allOrdersIds = allOrdersJSON.map( ( _order ) => _order.id ); - expect( allOrdersIds ).toHaveLength( ORDERS_COUNT ); - - const ordersToFilter = [ - allOrdersIds[ 0 ], - allOrdersIds[ 2 ], - allOrdersIds[ 4 ], - allOrdersIds[ 7 ], - ]; - - const included = await request.get( 'wp-json/wc/v3/orders', { - params: { - per_page: 20, - include: ordersToFilter.join( ',' ), - }, - } ); - const includedJSON = await included.json(); - - expect( included.status() ).toEqual( 200 ); - expect( includedJSON ).toHaveLength( ordersToFilter.length ); - expect( includedJSON ).toEqual( - expect.arrayContaining( - ordersToFilter.map( ( id ) => - expect.objectContaining( { - id, - } ) - ) - ) + test( 'can retrieve an order', async ( { request } ) => { + const response = await request.get( + `/wp-json/wc/v3/orders/${ orderId }` ); + const responseJSON = await response.json(); - const excluded = await request.get( 'wp-json/wc/v3/orders', { - params: { - per_page: 20, - exclude: ordersToFilter.join( ',' ), - }, - } ); - const excludedJSON = await excluded.json(); - - expect( excluded.status() ).toEqual( 200 ); - expect( excludedJSON.length ).toBeGreaterThanOrEqual( - Number( ORDERS_COUNT - ordersToFilter.length ) - ); - expect( excludedJSON ).toEqual( - expect.not.arrayContaining( - ordersToFilter.map( ( id ) => - expect.objectContaining( { - id, - } ) - ) - ) - ); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.id ).toEqual( orderId ); } ); - test( 'parent', async ( { request } ) => { - const result1 = await request.get( 'wp-json/wc/v3/orders', { - params: { - parent: sampleData.hierarchicalOrders.parent.id, - }, - } ); - const result1JSON = await result1.json(); + test( 'can add shipping and billing contacts to an order', async ( { + request, + } ) => { + // Update the billing and shipping fields on the order + order.billing = updatedCustomerBilling; + order.shipping = updatedCustomerShipping; - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( 1 ); - expect( result1JSON[ 0 ].id ).toBe( - sampleData.hierarchicalOrders.child.id + const response = await request.put( + `/wp-json/wc/v3/orders/${ orderId }`, + { + data: order, + } ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); - const result2 = await request.get( 'wp-json/wc/v3/orders', { - params: { - parent_exclude: sampleData.hierarchicalOrders.parent.id, - }, - } ); - const result2JSON = await result2.json(); - - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toEqual( - expect.not.arrayContaining( [ - expect.objectContaining( { - id: sampleData.hierarchicalOrders.child.id, - } ), - ] ) - ); + expect( responseJSON.billing ).toEqual( updatedCustomerBilling ); + expect( responseJSON.shipping ).toEqual( updatedCustomerShipping ); } ); - test( 'status', async ( { request } ) => { - const result1 = await request.get( 'wp-json/wc/v3/orders', { - params: { - status: 'completed', - search: 'oxo', - }, - } ); - const result1JSON = await result1.json(); - - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( 2 ); - expect( result1JSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - status: 'completed', - customer_id: 0, - line_items: expect.arrayContaining( [ - expect.objectContaining( { - name: 'Single oxo', - quantity: 2, - } ), - expect.objectContaining( { - name: 'Beanie with Logo oxo', - quantity: 3, - } ), - expect.objectContaining( { - name: 'T-Shirt oxo', - quantity: 1, - } ), - ] ), - } ), - expect.objectContaining( { - status: 'completed', - customer_id: sampleData.customers.tinaJSON.id, - line_items: expect.arrayContaining( [ - expect.objectContaining( { - name: 'Sunglasses oxo', - quantity: 1, - } ), - ] ), - } ), - ] ) + test( 'can permanently delete an order', async ( { request } ) => { + const response = await request.delete( + `/wp-json/wc/v3/orders/${ orderId }`, + { + data: { + force: true, + }, + } ); + expect( response.status() ).toEqual( 200 ); - const result2 = await request.get( 'wp-json/wc/v3/orders', { - params: { - status: 'processing', - search: 'oxo', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toHaveLength( 8 ); - expect( result2JSON ).toEqual( - expect.not.arrayContaining( - result1JSON.map( ( { id } ) => - expect.objectContaining( { - id, - } ) - ) - ) + const getOrderResponse = await request.get( + `/wp-json/wc/v3/orders/${ orderId }` ); + expect( getOrderResponse.status() ).toEqual( 404 ); } ); - test( 'customer', async ( { request } ) => { - const result1 = await request.get( 'wp-json/wc/v3/orders', { - params: { - customer: sampleData.customers.johnJSON.id, - }, - } ); - const result1JSON = await result1.json(); + test.describe( 'List all orders', () => { + const ORDERS_COUNT = 10; - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( 5 ); - result1JSON.forEach( ( _order ) => - expect( _order ).toEqual( - expect.objectContaining( { - customer_id: sampleData.customers.johnJSON.id, - } ) - ) - ); + test( 'pagination', async ( { request } ) => { + const pageSize = 4; + const page1 = await request.get( '/wp-json/wc/v3/orders', { + params: { + per_page: pageSize, + search: 'oxo', + }, + } ); + const page1JSON = await page1.json(); - const result2 = await request.get( 'wp-json/wc/v3/orders', { - params: { - customer: 0, - search: 'oxo', - }, - } ); - const result2JSON = await result2.json(); + const page2 = await request.get( '/wp-json/wc/v3/orders', { + params: { + per_page: pageSize, + page: 2, + search: 'oxo', + }, + } ); + const page2JSON = await page2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toHaveLength( 3 ); - result2JSON.forEach( ( _order ) => - expect( _order ).toEqual( - expect.objectContaining( { - customer_id: 0, - } ) - ) - ); - } ); + expect( page1.status() ).toEqual( 200 ); + expect( page2.status() ).toEqual( 200 ); - test( 'product', async ( { request } ) => { - const beanie = sampleData.testProductData.simpleProducts.find( - ( p ) => p.name === 'Beanie oxo' - ); - const result1 = await request.get( 'wp-json/wc/v3/orders', { - params: { - product: beanie.id, - }, - } ); - const result1JSON = await result1.json(); - - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( 2 ); - result1JSON.forEach( ( _order ) => - expect( _order ).toEqual( - expect.objectContaining( { - line_items: expect.arrayContaining( [ - expect.objectContaining( { - name: 'Beanie oxo', - } ), - ] ), - } ) - ) - ); - } ); - - // NOTE: This does not verify the `taxes` array nested in line items. - // While the precision parameter doesn't affect those values, after some - // discussion it seems `dp` may not be supported in v4 of the API. - test( 'dp (precision)', async ( { request } ) => { - const expectPrecisionToMatch = ( value, dp ) => { - expect( value ).toEqual( - Number.parseFloat( value ).toFixed( dp ) + // Verify total page count. + expect( page1.headers()[ 'x-wp-total' ] ).toEqual( + ORDERS_COUNT.toString() ); - }; + expect( page1.headers()[ 'x-wp-totalpages' ] ).toEqual( '3' ); - const verifyOrderPrecision = ( _order, dp ) => { - expectPrecisionToMatch( _order.discount_total, dp ); - expectPrecisionToMatch( _order.discount_tax, dp ); - expectPrecisionToMatch( _order.shipping_total, dp ); - expectPrecisionToMatch( _order.shipping_tax, dp ); - expectPrecisionToMatch( _order.cart_tax, dp ); - expectPrecisionToMatch( _order.total, dp ); - expectPrecisionToMatch( _order.total_tax, dp ); + // Verify we get pageSize'd arrays. + expect( Array.isArray( page1JSON ) ).toBe( true ); + expect( Array.isArray( page2JSON ) ).toBe( true ); + expect( page1JSON ).toHaveLength( pageSize ); + expect( page2JSON ).toHaveLength( pageSize ); - _order.line_items.forEach( ( lineItem ) => { - expectPrecisionToMatch( lineItem.total, dp ); - expectPrecisionToMatch( lineItem.total_tax, dp ); - } ); + // Ensure all of the order IDs are unique (no page overlap). + const allOrderIds = page1JSON + .concat( page2JSON ) + .reduce( ( acc, { id } ) => { + acc[ id ] = 1; + return acc; + }, {} ); + expect( Object.keys( allOrderIds ) ).toHaveLength( + pageSize * 2 + ); - _order.tax_lines.forEach( ( taxLine ) => { - expectPrecisionToMatch( taxLine.tax_total, dp ); - expectPrecisionToMatch( taxLine.shipping_tax_total, dp ); - } ); - - _order.shipping_lines.forEach( ( shippingLine ) => { - expectPrecisionToMatch( shippingLine.total, dp ); - expectPrecisionToMatch( shippingLine.total_tax, dp ); - } ); - - _order.fee_lines.forEach( ( feeLine ) => { - expectPrecisionToMatch( feeLine.total, dp ); - expectPrecisionToMatch( feeLine.total_tax, dp ); - } ); - - _order.refunds.forEach( ( refund ) => { - expectPrecisionToMatch( refund.total, dp ); - } ); - }; - - const result1 = await request.get( - `wp-json/wc/v3/orders/${ sampleData.precisionOrder.id }`, - { + // Verify that offset takes precedent over page number. + const page2Offset = await request.get( 'wp-json/wc/v3/orders', { params: { - dp: 1, + per_page: pageSize, + page: 2, + offset: pageSize + 1, + search: 'oxo', }, - } - ); - const result1JSON = await result1.json(); + } ); + const page2OffsetJSON = await page2Offset.json(); - expect( result1.status() ).toEqual( 200 ); - verifyOrderPrecision( result1JSON, 1 ); + // The offset pushes the result set 1 order past the start of page 2. + expect( page2OffsetJSON ).toEqual( + expect.not.arrayContaining( [ + expect.objectContaining( { + id: page2JSON[ 0 ].id, + } ), + ] ) + ); + expect( page2OffsetJSON[ 0 ].id ).toEqual( page2JSON[ 1 ].id ); - const result2 = await request.get( - `wp-json/wc/v3/orders/${ sampleData.precisionOrder.id }`, - { + // Verify the last page only has 1 order as we expect. + const lastPage = await request.get( 'wp-json/wc/v3/orders', { params: { - dp: 3, + per_page: pageSize, + page: 3, + search: 'oxo', }, - } - ); - const result2JSON = await result2.json(); + } ); + const lastPageJSON = await lastPage.json(); - expect( result2.status() ).toEqual( 200 ); - verifyOrderPrecision( result2JSON, 3 ); + expect( Array.isArray( lastPageJSON ) ).toBe( true ); + expect( lastPageJSON ).toHaveLength( 2 ); - const result3 = await request.get( - `wp-json/wc/v3/orders/${ sampleData.precisionOrder.id }` - ); - const result3JSON = await result3.json(); + // Verify a page outside the total page count is empty. + const page6 = await request.get( 'wp-json/wc/v3/orders', { + params: { + page: 6, + search: 'oxo', + }, + } ); + const page6JSON = await page6.json(); - expect( result3.status() ).toEqual( 200 ); - verifyOrderPrecision( result3JSON, 2 ); // The default value for 'dp' is 2. - } ); - - test( 'search', async ( { request } ) => { - // By default, 'search' looks in: - // - _billing_address_index - // - _shipping_address_index - // - _billing_last_name - // - _billing_email - // - order_item_name - - // Test billing email. - const result1 = await request.get( 'wp-json/wc/v3/orders', { - params: { - search: 'example.com', - }, + expect( Array.isArray( page6JSON ) ).toBe( true ); + expect( page6JSON ).toHaveLength( 0 ); } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON.length ).toBeGreaterThanOrEqual( 1 ); - result1JSON.forEach( ( _order ) => - expect( _order.billing.email ).toContain( 'example.com' ) - ); + test( 'inclusion / exclusion', async ( { request } ) => { + const allOrders = await request.get( 'wp-json/wc/v3/orders', { + params: { + per_page: 10, + search: 'oxo', + }, + } ); + const allOrdersJSON = await allOrders.json(); - // Test billing address. - const result2 = await request.get( 'wp-json/wc/v3/orders', { - params: { - search: 'gainesville', - }, - } ); - const result2JSON = await result2.json(); + expect( allOrders.status() ).toEqual( 200 ); + const allOrdersIds = allOrdersJSON.map( + ( _order ) => _order.id + ); + expect( allOrdersIds ).toHaveLength( ORDERS_COUNT ); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toHaveLength( 1 ); - expect( result2JSON[ 0 ].id ).toEqual( - sampleData.guestOrderJSON.id - ); + const ordersToFilter = [ + allOrdersIds[ 0 ], + allOrdersIds[ 2 ], + allOrdersIds[ 4 ], + allOrdersIds[ 7 ], + ]; - // Test shipping address. - const result3 = await request.get( 'wp-json/wc/v3/orders', { - params: { - search: 'Incognito', - }, - } ); - const result3JSON = await result3.json(); + const included = await request.get( 'wp-json/wc/v3/orders', { + params: { + per_page: 20, + include: ordersToFilter.join( ',' ), + }, + } ); + const includedJSON = await included.json(); - expect( result3.status() ).toEqual( 200 ); - expect( result3JSON ).toHaveLength( 1 ); - expect( result3JSON[ 0 ].id ).toEqual( - sampleData.guestOrderJSON.id - ); - - // Test billing last name. - const result4 = await request.get( 'wp-json/wc/v3/orders', { - params: { - search: 'Doe', - }, - } ); - const result4JSON = await result4.json(); - - expect( result4.status() ).toEqual( 200 ); - expect( result4JSON.length ).toBeGreaterThanOrEqual( 1 ); - result4JSON.forEach( ( _order ) => - expect( _order.billing.last_name ).toEqual( 'Doe' ) - ); - - // Test order item name. - const result5 = await request.get( 'wp-json/wc/v3/orders', { - params: { - search: 'Pennant oxo', - }, - } ); - const result5JSON = await result5.json(); - - expect( result5.status() ).toEqual( 200 ); - expect( result5JSON ).toHaveLength( 2 ); - result5JSON.forEach( ( _order ) => - expect( _order ).toEqual( - expect.objectContaining( { - line_items: expect.arrayContaining( [ + expect( included.status() ).toEqual( 200 ); + expect( includedJSON ).toHaveLength( ordersToFilter.length ); + expect( includedJSON ).toEqual( + expect.arrayContaining( + ordersToFilter.map( ( id ) => expect.objectContaining( { - name: 'WordPress Pennant oxo', - } ), - ] ), - } ) - ) - ); - } ); - } ); + id, + } ) + ) + ) + ); - test.describe( 'orderby', () => { - // The orders endpoint `orderby` parameter uses WP_Query, so our tests won't - // include slug and title, since they are programmatically generated. - test( 'default', async ( { request } ) => { - // Default = date desc. - const result = await request.get( 'wp-json/wc/v3/orders' ); - const resultJSON = await result.json(); - expect( result.status() ).toEqual( 200 ); + const excluded = await request.get( 'wp-json/wc/v3/orders', { + params: { + per_page: 20, + exclude: ordersToFilter.join( ',' ), + }, + } ); + const excludedJSON = await excluded.json(); - // Verify all dates are in descending order. - let lastDate = Date.now(); - resultJSON.forEach( ( { date_created } ) => { - const created = Date.parse( date_created + '.000Z' ); - expect( lastDate ).toBeGreaterThanOrEqual( created ); - lastDate = created; + expect( excluded.status() ).toEqual( 200 ); + expect( excludedJSON.length ).toBeGreaterThanOrEqual( + Number( ORDERS_COUNT - ordersToFilter.length ) + ); + expect( excludedJSON ).toEqual( + expect.not.arrayContaining( + ordersToFilter.map( ( id ) => + expect.objectContaining( { + id, + } ) + ) + ) + ); + } ); + + test( 'parent', async ( { request } ) => { + const result1 = await request.get( 'wp-json/wc/v3/orders', { + params: { + parent: sampleData.hierarchicalOrders.parent.id, + }, + } ); + const result1JSON = await result1.json(); + + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( 1 ); + expect( result1JSON[ 0 ].id ).toBe( + sampleData.hierarchicalOrders.child.id + ); + + const result2 = await request.get( 'wp-json/wc/v3/orders', { + params: { + parent_exclude: sampleData.hierarchicalOrders.parent.id, + }, + } ); + const result2JSON = await result2.json(); + + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toEqual( + expect.not.arrayContaining( [ + expect.objectContaining( { + id: sampleData.hierarchicalOrders.child.id, + } ), + ] ) + ); + } ); + + test( 'status', async ( { request } ) => { + const result1 = await request.get( 'wp-json/wc/v3/orders', { + params: { + status: 'completed', + search: 'oxo', + }, + } ); + const result1JSON = await result1.json(); + + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( 2 ); + expect( result1JSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + status: 'completed', + customer_id: 0, + line_items: expect.arrayContaining( [ + expect.objectContaining( { + name: 'Single oxo', + quantity: 2, + } ), + expect.objectContaining( { + name: 'Beanie with Logo oxo', + quantity: 3, + } ), + expect.objectContaining( { + name: 'T-Shirt oxo', + quantity: 1, + } ), + ] ), + } ), + expect.objectContaining( { + status: 'completed', + customer_id: sampleData.customers.tinaJSON.id, + line_items: expect.arrayContaining( [ + expect.objectContaining( { + name: 'Sunglasses oxo', + quantity: 1, + } ), + ] ), + } ), + ] ) + ); + + const result2 = await request.get( 'wp-json/wc/v3/orders', { + params: { + status: 'processing', + search: 'oxo', + }, + } ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toHaveLength( 8 ); + expect( result2JSON ).toEqual( + expect.not.arrayContaining( + result1JSON.map( ( { id } ) => + expect.objectContaining( { + id, + } ) + ) + ) + ); + } ); + + test( 'customer', async ( { request } ) => { + const result1 = await request.get( 'wp-json/wc/v3/orders', { + params: { + customer: sampleData.customers.johnJSON.id, + }, + } ); + const result1JSON = await result1.json(); + + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( 5 ); + result1JSON.forEach( ( _order ) => + expect( _order ).toEqual( + expect.objectContaining( { + customer_id: sampleData.customers.johnJSON.id, + } ) + ) + ); + + const result2 = await request.get( 'wp-json/wc/v3/orders', { + params: { + customer: 0, + search: 'oxo', + }, + } ); + const result2JSON = await result2.json(); + + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toHaveLength( 3 ); + result2JSON.forEach( ( _order ) => + expect( _order ).toEqual( + expect.objectContaining( { + customer_id: 0, + } ) + ) + ); + } ); + + test( 'product', async ( { request } ) => { + const beanie = sampleData.testProductData.simpleProducts.find( + ( p ) => p.name === 'Beanie oxo' + ); + const result1 = await request.get( 'wp-json/wc/v3/orders', { + params: { + product: beanie.id, + }, + } ); + const result1JSON = await result1.json(); + + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( 2 ); + result1JSON.forEach( ( _order ) => + expect( _order ).toEqual( + expect.objectContaining( { + line_items: expect.arrayContaining( [ + expect.objectContaining( { + name: 'Beanie oxo', + } ), + ] ), + } ) + ) + ); + } ); + + // NOTE: This does not verify the `taxes` array nested in line items. + // While the precision parameter doesn't affect those values, after some + // discussion it seems `dp` may not be supported in v4 of the API. + test( 'dp (precision)', async ( { request } ) => { + const expectPrecisionToMatch = ( value, dp ) => { + expect( value ).toEqual( + Number.parseFloat( value ).toFixed( dp ) + ); + }; + + const verifyOrderPrecision = ( _order, dp ) => { + expectPrecisionToMatch( _order.discount_total, dp ); + expectPrecisionToMatch( _order.discount_tax, dp ); + expectPrecisionToMatch( _order.shipping_total, dp ); + expectPrecisionToMatch( _order.shipping_tax, dp ); + expectPrecisionToMatch( _order.cart_tax, dp ); + expectPrecisionToMatch( _order.total, dp ); + expectPrecisionToMatch( _order.total_tax, dp ); + + _order.line_items.forEach( ( lineItem ) => { + expectPrecisionToMatch( lineItem.total, dp ); + expectPrecisionToMatch( lineItem.total_tax, dp ); + } ); + + _order.tax_lines.forEach( ( taxLine ) => { + expectPrecisionToMatch( taxLine.tax_total, dp ); + expectPrecisionToMatch( + taxLine.shipping_tax_total, + dp + ); + } ); + + _order.shipping_lines.forEach( ( shippingLine ) => { + expectPrecisionToMatch( shippingLine.total, dp ); + expectPrecisionToMatch( shippingLine.total_tax, dp ); + } ); + + _order.fee_lines.forEach( ( feeLine ) => { + expectPrecisionToMatch( feeLine.total, dp ); + expectPrecisionToMatch( feeLine.total_tax, dp ); + } ); + + _order.refunds.forEach( ( refund ) => { + expectPrecisionToMatch( refund.total, dp ); + } ); + }; + + const result1 = await request.get( + `wp-json/wc/v3/orders/${ sampleData.precisionOrder.id }`, + { + params: { + dp: 1, + }, + } + ); + const result1JSON = await result1.json(); + + expect( result1.status() ).toEqual( 200 ); + verifyOrderPrecision( result1JSON, 1 ); + + const result2 = await request.get( + `wp-json/wc/v3/orders/${ sampleData.precisionOrder.id }`, + { + params: { + dp: 3, + }, + } + ); + const result2JSON = await result2.json(); + + expect( result2.status() ).toEqual( 200 ); + verifyOrderPrecision( result2JSON, 3 ); + + const result3 = await request.get( + `wp-json/wc/v3/orders/${ sampleData.precisionOrder.id }` + ); + const result3JSON = await result3.json(); + + expect( result3.status() ).toEqual( 200 ); + verifyOrderPrecision( result3JSON, 2 ); // The default value for 'dp' is 2. + } ); + + test( 'search', async ( { request } ) => { + // By default, 'search' looks in: + // - _billing_address_index + // - _shipping_address_index + // - _billing_last_name + // - _billing_email + // - order_item_name + + // Test billing email. + const result1 = await request.get( 'wp-json/wc/v3/orders', { + params: { + search: 'example.com', + }, + } ); + const result1JSON = await result1.json(); + + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON.length ).toBeGreaterThanOrEqual( 1 ); + result1JSON.forEach( ( _order ) => + expect( _order.billing.email ).toContain( 'example.com' ) + ); + + // Test billing address. + const result2 = await request.get( 'wp-json/wc/v3/orders', { + params: { + search: 'gainesville', + }, + } ); + const result2JSON = await result2.json(); + + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toHaveLength( 1 ); + expect( result2JSON[ 0 ].id ).toEqual( + sampleData.guestOrderJSON.id + ); + + // Test shipping address. + const result3 = await request.get( 'wp-json/wc/v3/orders', { + params: { + search: 'Incognito', + }, + } ); + const result3JSON = await result3.json(); + + expect( result3.status() ).toEqual( 200 ); + expect( result3JSON ).toHaveLength( 1 ); + expect( result3JSON[ 0 ].id ).toEqual( + sampleData.guestOrderJSON.id + ); + + // Test billing last name. + const result4 = await request.get( 'wp-json/wc/v3/orders', { + params: { + search: 'Doe', + }, + } ); + const result4JSON = await result4.json(); + + expect( result4.status() ).toEqual( 200 ); + expect( result4JSON.length ).toBeGreaterThanOrEqual( 1 ); + result4JSON.forEach( ( _order ) => + expect( _order.billing.last_name ).toEqual( 'Doe' ) + ); + + // Test order item name. + const result5 = await request.get( 'wp-json/wc/v3/orders', { + params: { + search: 'Pennant oxo', + }, + } ); + const result5JSON = await result5.json(); + + expect( result5.status() ).toEqual( 200 ); + expect( result5JSON ).toHaveLength( 2 ); + result5JSON.forEach( ( _order ) => + expect( _order ).toEqual( + expect.objectContaining( { + line_items: expect.arrayContaining( [ + expect.objectContaining( { + name: 'WordPress Pennant oxo', + } ), + ] ), + } ) + ) + ); } ); } ); - test( 'date', async ( { request } ) => { - const result = await request.get( 'wp-json/wc/v3/orders', { - params: { - order: 'asc', - orderby: 'date', - }, + test.describe( 'orderby', () => { + // The orders endpoint `orderby` parameter uses WP_Query, so our tests won't + // include slug and title, since they are programmatically generated. + test( 'default', async ( { request } ) => { + // Default = date desc. + const result = await request.get( 'wp-json/wc/v3/orders' ); + const resultJSON = await result.json(); + expect( result.status() ).toEqual( 200 ); + + // Verify all dates are in descending order. + let lastDate = Date.now(); + resultJSON.forEach( ( { date_created } ) => { + const created = Date.parse( date_created + '.000Z' ); + expect( lastDate ).toBeGreaterThanOrEqual( created ); + lastDate = created; + } ); } ); - const resultJSON = await result.json(); - expect( result.status() ).toEqual( 200 ); + test( 'date', async ( { request } ) => { + const result = await request.get( 'wp-json/wc/v3/orders', { + params: { + order: 'asc', + orderby: 'date', + }, + } ); + const resultJSON = await result.json(); - // Verify all dates are in ascending order. - let lastDate = 0; - resultJSON.forEach( ( { date_created } ) => { - const created = Date.parse( date_created + '.000Z' ); - expect( created ).toBeGreaterThanOrEqual( lastDate ); - lastDate = created; + expect( result.status() ).toEqual( 200 ); + + // Verify all dates are in ascending order. + let lastDate = 0; + resultJSON.forEach( ( { date_created } ) => { + const created = Date.parse( date_created + '.000Z' ); + expect( created ).toBeGreaterThanOrEqual( lastDate ); + lastDate = created; + } ); + } ); + + test( 'id', async ( { request } ) => { + const result1 = await request.get( 'wp-json/wc/v3/orders', { + params: { + order: 'asc', + orderby: 'id', + }, + } ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + + // Verify all results are in ascending order. + let lastId = 0; + result1JSON.forEach( ( { id } ) => { + expect( id ).toBeGreaterThan( lastId ); + lastId = id; + } ); + + const result2 = await request.get( 'wp-json/wc/v3/orders', { + params: { + order: 'desc', + orderby: 'id', + }, + } ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + + // Verify all results are in descending order. + lastId = Number.MAX_SAFE_INTEGER; + result2JSON.forEach( ( { id } ) => { + expect( lastId ).toBeGreaterThan( id ); + lastId = id; + } ); + } ); + + test( 'include', async ( { request } ) => { + const includeIds = [ + sampleData.precisionOrder.id, + sampleData.hierarchicalOrders.parent.id, + sampleData.guestOrderJSON.id, + ]; + + const result1 = await request.get( 'wp-json/wc/v3/orders', { + params: { + order: 'asc', + orderby: 'include', + include: includeIds.join( ',' ), + }, + } ); + const result1JSON = await result1.json(); + + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( includeIds.length ); + + // Verify all results are in proper order. + result1JSON.forEach( ( { id }, idx ) => { + expect( id ).toBe( includeIds[ idx ] ); + } ); + + const result2 = await request.get( 'wp-json/wc/v3/orders', { + params: { + order: 'desc', + orderby: 'include', + include: includeIds.join( ',' ), + }, + } ); + const result2JSON = await result2.json(); + + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toHaveLength( includeIds.length ); + + // Verify all results are in proper order. + result2JSON.forEach( ( { id }, idx ) => { + expect( id ).toBe( includeIds[ idx ] ); + } ); } ); } ); - - test( 'id', async ( { request } ) => { - const result1 = await request.get( 'wp-json/wc/v3/orders', { - params: { - order: 'asc', - orderby: 'id', - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - - // Verify all results are in ascending order. - let lastId = 0; - result1JSON.forEach( ( { id } ) => { - expect( id ).toBeGreaterThan( lastId ); - lastId = id; - } ); - - const result2 = await request.get( 'wp-json/wc/v3/orders', { - params: { - order: 'desc', - orderby: 'id', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - - // Verify all results are in descending order. - lastId = Number.MAX_SAFE_INTEGER; - result2JSON.forEach( ( { id } ) => { - expect( lastId ).toBeGreaterThan( id ); - lastId = id; - } ); - } ); - - test( 'include', async ( { request } ) => { - const includeIds = [ - sampleData.precisionOrder.id, - sampleData.hierarchicalOrders.parent.id, - sampleData.guestOrderJSON.id, - ]; - - const result1 = await request.get( 'wp-json/wc/v3/orders', { - params: { - order: 'asc', - orderby: 'include', - include: includeIds.join( ',' ), - }, - } ); - const result1JSON = await result1.json(); - - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( includeIds.length ); - - // Verify all results are in proper order. - result1JSON.forEach( ( { id }, idx ) => { - expect( id ).toBe( includeIds[ idx ] ); - } ); - - const result2 = await request.get( 'wp-json/wc/v3/orders', { - params: { - order: 'desc', - orderby: 'include', - include: includeIds.join( ',' ), - }, - } ); - const result2JSON = await result2.json(); - - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toHaveLength( includeIds.length ); - - // Verify all results are in proper order. - result2JSON.forEach( ( { id }, idx ) => { - expect( id ).toBe( includeIds[ idx ] ); - } ); - } ); - } ); -} ); + } +); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/payment-gateways/payment-gateways-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/payment-gateways/payment-gateways-crud.test.js index c4d021c65f5..a0cd439f3e4 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/payment-gateways/payment-gateways-crud.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/payment-gateways/payment-gateways-crud.test.js @@ -1,170 +1,176 @@ const { test, expect } = require( '../../../fixtures/api-tests-fixtures' ); test.describe( 'Payment Gateways API tests', () => { - test( 'can view all payment gateways', async ( { request } ) => { - // call API to retrieve the payment gateways - const response = await request.get( '/wp-json/wc/v3/payment_gateways' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); + test( + 'can view all payment gateways', + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, + async ( { request } ) => { + // call API to retrieve the payment gateways + const response = await request.get( + '/wp-json/wc/v3/payment_gateways' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); - const localPickupKey = - // eslint-disable-next-line playwright/no-conditional-in-test - process.env.BASE_URL && - ! process.env.BASE_URL.includes( 'localhost' ) - ? 'pickup_location' - : 'local_pickup'; - console.log( 'localPickupKey=', localPickupKey ); + const localPickupKey = + // eslint-disable-next-line playwright/no-conditional-in-test + process.env.BASE_URL && + ! process.env.BASE_URL.includes( 'localhost' ) + ? 'pickup_location' + : 'local_pickup'; + console.log( 'localPickupKey=', localPickupKey ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'bacs', - title: 'Direct bank transfer', - description: - 'Make your payment directly into our bank account. Please use your Order ID as the payment reference. Your order will not be shipped until the funds have cleared in our account.', - order: '', - enabled: false, - method_title: 'Direct bank transfer', - method_description: - 'Take payments in person via BACS. More commonly known as direct bank/wire transfer.', - method_supports: [ 'products' ], - settings: { - title: { - id: 'title', - label: 'Title', - description: - 'This controls the title which the user sees during checkout.', - type: 'safe_text', - value: 'Direct bank transfer', - default: 'Direct bank transfer', - tip: 'This controls the title which the user sees during checkout.', - placeholder: '', + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'bacs', + title: 'Direct bank transfer', + description: + 'Make your payment directly into our bank account. Please use your Order ID as the payment reference. Your order will not be shipped until the funds have cleared in our account.', + order: '', + enabled: false, + method_title: 'Direct bank transfer', + method_description: + 'Take payments in person via BACS. More commonly known as direct bank/wire transfer.', + method_supports: [ 'products' ], + settings: { + title: { + id: 'title', + label: 'Title', + description: + 'This controls the title which the user sees during checkout.', + type: 'safe_text', + value: 'Direct bank transfer', + default: 'Direct bank transfer', + tip: 'This controls the title which the user sees during checkout.', + placeholder: '', + }, + instructions: { + id: 'instructions', + label: 'Instructions', + description: + 'Instructions that will be added to the thank you page and emails.', + type: 'textarea', + value: '', + default: '', + tip: 'Instructions that will be added to the thank you page and emails.', + placeholder: '', + }, }, - instructions: { - id: 'instructions', - label: 'Instructions', - description: - 'Instructions that will be added to the thank you page and emails.', - type: 'textarea', - value: '', - default: '', - tip: 'Instructions that will be added to the thank you page and emails.', - placeholder: '', - }, - }, - } ), + } ), - expect.objectContaining( { - id: 'cheque', - title: 'Check payments', - description: - 'Please send a check to Store Name, Store Street, Store Town, Store State / County, Store Postcode.', - order: '', - enabled: false, - method_title: 'Check payments', - method_description: - 'Take payments in person via checks. This offline gateway can also be useful to test purchases.', - method_supports: [ 'products' ], - settings: { - title: { - id: 'title', - label: 'Title', - description: - 'This controls the title which the user sees during checkout.', - type: 'safe_text', - value: 'Check payments', - default: 'Check payments', - tip: 'This controls the title which the user sees during checkout.', - placeholder: '', + expect.objectContaining( { + id: 'cheque', + title: 'Check payments', + description: + 'Please send a check to Store Name, Store Street, Store Town, Store State / County, Store Postcode.', + order: '', + enabled: false, + method_title: 'Check payments', + method_description: + 'Take payments in person via checks. This offline gateway can also be useful to test purchases.', + method_supports: [ 'products' ], + settings: { + title: { + id: 'title', + label: 'Title', + description: + 'This controls the title which the user sees during checkout.', + type: 'safe_text', + value: 'Check payments', + default: 'Check payments', + tip: 'This controls the title which the user sees during checkout.', + placeholder: '', + }, + instructions: { + id: 'instructions', + label: 'Instructions', + description: + 'Instructions that will be added to the thank you page and emails.', + type: 'textarea', + value: '', + default: '', + tip: 'Instructions that will be added to the thank you page and emails.', + placeholder: '', + }, }, - instructions: { - id: 'instructions', - label: 'Instructions', - description: - 'Instructions that will be added to the thank you page and emails.', - type: 'textarea', - value: '', - default: '', - tip: 'Instructions that will be added to the thank you page and emails.', - placeholder: '', - }, - }, - } ), + } ), - expect.objectContaining( { - id: 'cod', - title: 'Cash on delivery', - description: 'Pay with cash upon delivery.', - order: '', - enabled: false, - method_title: 'Cash on delivery', - method_description: - 'Have your customers pay with cash (or by other means) upon delivery.', - method_supports: [ 'products' ], - settings: { - title: { - id: 'title', - label: 'Title', - description: - 'Payment method description that the customer will see on your checkout.', - type: 'safe_text', - value: 'Cash on delivery', - default: 'Cash on delivery', - tip: 'Payment method description that the customer will see on your checkout.', - placeholder: '', - }, - instructions: { - id: 'instructions', - label: 'Instructions', - description: - 'Instructions that will be added to the thank you page.', - type: 'textarea', - value: 'Pay with cash upon delivery.', - default: 'Pay with cash upon delivery.', - tip: 'Instructions that will be added to the thank you page.', - placeholder: '', - }, - enable_for_methods: { - id: 'enable_for_methods', - label: 'Enable for shipping methods', - description: - 'If COD is only available for certain methods, set it up here. Leave blank to enable for all methods.', - type: 'multiselect', - value: '', - default: '', - tip: 'If COD is only available for certain methods, set it up here. Leave blank to enable for all methods.', - placeholder: '', - options: expect.objectContaining( { - 'Flat rate': { - flat_rate: - 'Any "Flat rate" method', - }, - 'Free shipping': { - free_shipping: - 'Any "Free shipping" method', - }, - 'Local pickup': expect.objectContaining( { - [ localPickupKey ]: - 'Any "Local pickup" method', + expect.objectContaining( { + id: 'cod', + title: 'Cash on delivery', + description: 'Pay with cash upon delivery.', + order: '', + enabled: false, + method_title: 'Cash on delivery', + method_description: + 'Have your customers pay with cash (or by other means) upon delivery.', + method_supports: [ 'products' ], + settings: { + title: { + id: 'title', + label: 'Title', + description: + 'Payment method description that the customer will see on your checkout.', + type: 'safe_text', + value: 'Cash on delivery', + default: 'Cash on delivery', + tip: 'Payment method description that the customer will see on your checkout.', + placeholder: '', + }, + instructions: { + id: 'instructions', + label: 'Instructions', + description: + 'Instructions that will be added to the thank you page.', + type: 'textarea', + value: 'Pay with cash upon delivery.', + default: 'Pay with cash upon delivery.', + tip: 'Instructions that will be added to the thank you page.', + placeholder: '', + }, + enable_for_methods: { + id: 'enable_for_methods', + label: 'Enable for shipping methods', + description: + 'If COD is only available for certain methods, set it up here. Leave blank to enable for all methods.', + type: 'multiselect', + value: '', + default: '', + tip: 'If COD is only available for certain methods, set it up here. Leave blank to enable for all methods.', + placeholder: '', + options: expect.objectContaining( { + 'Flat rate': { + flat_rate: + 'Any "Flat rate" method', + }, + 'Free shipping': { + free_shipping: + 'Any "Free shipping" method', + }, + 'Local pickup': expect.objectContaining( { + [ localPickupKey ]: + 'Any "Local pickup" method', + } ), } ), - } ), + }, + enable_for_virtual: { + id: 'enable_for_virtual', + label: 'Accept COD if the order is virtual', + description: '', + type: 'checkbox', + value: 'yes', + default: 'yes', + tip: '', + placeholder: '', + }, }, - enable_for_virtual: { - id: 'enable_for_virtual', - label: 'Accept COD if the order is virtual', - description: '', - type: 'checkbox', - value: 'yes', - default: 'yes', - tip: '', - placeholder: '', - }, - }, - } ), - ] ) - ); - } ); + } ), + ] ) + ); + } + ); test( 'can view a payment gateway', async ( { request } ) => { // call API to retrieve a single payment gateway diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/product-list.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/product-list.test.js index 03f51112b37..d9faf15633c 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/product-list.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/product-list.test.js @@ -2230,1077 +2230,1170 @@ test.describe( 'Products API tests: List All Products', () => { await deleteSampleData( sampleData ); }, 10000 ); - test.describe( 'List all products', () => { - test( 'defaults', async ( { request } ) => { - const result = await request.get( 'wp-json/wc/v3/products', { - params: { - search: 'xxx', - }, + test.describe( + 'List all products', + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, + () => { + test( 'defaults', async ( { request } ) => { + const result = await request.get( 'wp-json/wc/v3/products', { + params: { + search: 'xxx', + }, + } ); + + expect( result.status() ).toEqual( 200 ); + expect( result.headers()[ 'x-wp-total' ] ).toEqual( + PRODUCTS_COUNT.toString() + ); + expect( result.headers()[ 'x-wp-totalpages' ] ).toEqual( '2' ); } ); - expect( result.status() ).toEqual( 200 ); - expect( result.headers()[ 'x-wp-total' ] ).toEqual( - PRODUCTS_COUNT.toString() - ); - expect( result.headers()[ 'x-wp-totalpages' ] ).toEqual( '2' ); - } ); + test( 'pagination', async ( { request } ) => { + const pageSize = 6; + const page1 = await request.get( 'wp-json/wc/v3/products', { + params: { + per_page: pageSize, + search: 'xxx', + }, + } ); + const page1JSON = await page1.json(); + const page2 = await request.get( 'wp-json/wc/v3/products', { + params: { + per_page: pageSize, + page: 2, + search: 'xxx', + }, + } ); + const page2JSON = await page2.json(); + expect( page1.status() ).toEqual( 200 ); + expect( page2.status() ).toEqual( 200 ); - test( 'pagination', async ( { request } ) => { - const pageSize = 6; - const page1 = await request.get( 'wp-json/wc/v3/products', { - params: { - per_page: pageSize, - search: 'xxx', - }, - } ); - const page1JSON = await page1.json(); - const page2 = await request.get( 'wp-json/wc/v3/products', { - params: { - per_page: pageSize, - page: 2, - search: 'xxx', - }, - } ); - const page2JSON = await page2.json(); - expect( page1.status() ).toEqual( 200 ); - expect( page2.status() ).toEqual( 200 ); + // Verify total page count. + expect( page1.headers()[ 'x-wp-total' ] ).toEqual( + PRODUCTS_COUNT.toString() + ); + expect( page1.headers()[ 'x-wp-totalpages' ] ).toEqual( '4' ); - // Verify total page count. - expect( page1.headers()[ 'x-wp-total' ] ).toEqual( - PRODUCTS_COUNT.toString() - ); - expect( page1.headers()[ 'x-wp-totalpages' ] ).toEqual( '4' ); + // Verify we get pageSize'd arrays. + expect( Array.isArray( page1JSON ) ).toBe( true ); + expect( Array.isArray( page2JSON ) ).toBe( true ); + expect( page1JSON ).toHaveLength( pageSize ); + expect( page2JSON ).toHaveLength( pageSize ); - // Verify we get pageSize'd arrays. - expect( Array.isArray( page1JSON ) ).toBe( true ); - expect( Array.isArray( page2JSON ) ).toBe( true ); - expect( page1JSON ).toHaveLength( pageSize ); - expect( page2JSON ).toHaveLength( pageSize ); + // Ensure all of the product IDs are unique (no page overlap). + const allProductIds = page1JSON + .concat( page2JSON ) + .reduce( ( acc, product ) => { + acc[ product.id ] = 1; + return acc; + }, {} ); + expect( Object.keys( allProductIds ) ).toHaveLength( + pageSize * 2 + ); - // Ensure all of the product IDs are unique (no page overlap). - const allProductIds = page1JSON - .concat( page2JSON ) - .reduce( ( acc, product ) => { - acc[ product.id ] = 1; - return acc; - }, {} ); - expect( Object.keys( allProductIds ) ).toHaveLength( pageSize * 2 ); - - // Verify that offset takes precedent over page number. - const page2Offset = await request.get( 'wp-json/wc/v3/products', { - params: { - per_page: pageSize, - page: 2, - offset: pageSize + 1, - search: 'xxx', - }, - } ); - const page2OffsetJSON = await page2Offset.json(); - // The offset pushes the result set 1 product past the start of page 2. - expect( page2OffsetJSON ).toEqual( - expect.not.arrayContaining( [ - expect.objectContaining( { - id: page2JSON[ 0 ].id, - } ), - ] ) - ); - expect( page2OffsetJSON[ 0 ].id ).toEqual( page2JSON[ 1 ].id ); - - // Verify the last page only has 2 products as we expect. - const lastPage = await request.get( 'wp-json/wc/v3/products', { - params: { - per_page: pageSize, - page: 4, - search: 'xxx', - }, - } ); - const lastPageJSON = await lastPage.json(); - expect( Array.isArray( lastPageJSON ) ).toBe( true ); - expect( lastPageJSON ).toHaveLength( 2 ); - - // Verify a page outside the total page count is empty. - const page6 = await request.get( 'wp-json/wc/v3/products', { - params: { - per_page: pageSize, - page: 6, - search: 'xxx', - }, - } ); - const page6JSON = await page6.json(); - expect( Array.isArray( page6JSON ) ).toBe( true ); - expect( page6JSON ).toHaveLength( 0 ); - } ); - - test( 'search', async ( { request } ) => { - // Match in the short description. - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - search: 'external', - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON.length ).toBeGreaterThanOrEqual( 1 ); - expect( result1JSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - name: 'WordPress Pennant xxx', - } ), - ] ) - ); - - // Match in the product name. - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - search: 'pocket xxx', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toHaveLength( 1 ); - expect( result2JSON[ 0 ].name ).toBe( 'Hoodie with Pocket xxx' ); - } ); - - test( 'inclusion / exclusion', async ( { request } ) => { - const allProducts = await request.get( 'wp-json/wc/v3/products', { - params: { - per_page: 20, - search: 'xxx', - }, - } ); - const allProductsJSON = await allProducts.json(); - expect( allProducts.status() ).toEqual( 200 ); - const allProductIds = allProductsJSON.map( - ( product ) => product.id - ); - expect( allProductIds ).toHaveLength( PRODUCTS_COUNT ); - - const productsToFilter = [ - allProductIds[ 2 ], - allProductIds[ 4 ], - allProductIds[ 7 ], - allProductIds[ 13 ], - ]; - - const included = await request.get( 'wp-json/wc/v3/products', { - params: { - per_page: 20, - include: productsToFilter.join( ',' ), - }, - } ); - const includedJSON = await included.json(); - expect( included.status() ).toEqual( 200 ); - expect( includedJSON ).toHaveLength( productsToFilter.length ); - expect( includedJSON ).toEqual( - expect.arrayContaining( - productsToFilter.map( ( id ) => + // Verify that offset takes precedent over page number. + const page2Offset = await request.get( + 'wp-json/wc/v3/products', + { + params: { + per_page: pageSize, + page: 2, + offset: pageSize + 1, + search: 'xxx', + }, + } + ); + const page2OffsetJSON = await page2Offset.json(); + // The offset pushes the result set 1 product past the start of page 2. + expect( page2OffsetJSON ).toEqual( + expect.not.arrayContaining( [ expect.objectContaining( { - id, - } ) - ) - ) - ); + id: page2JSON[ 0 ].id, + } ), + ] ) + ); + expect( page2OffsetJSON[ 0 ].id ).toEqual( page2JSON[ 1 ].id ); - const excluded = await request.get( 'wp-json/wc/v3/products', { - params: { - per_page: 20, - exclude: productsToFilter.join( ',' ), - }, + // Verify the last page only has 2 products as we expect. + const lastPage = await request.get( 'wp-json/wc/v3/products', { + params: { + per_page: pageSize, + page: 4, + search: 'xxx', + }, + } ); + const lastPageJSON = await lastPage.json(); + expect( Array.isArray( lastPageJSON ) ).toBe( true ); + expect( lastPageJSON ).toHaveLength( 2 ); + + // Verify a page outside the total page count is empty. + const page6 = await request.get( 'wp-json/wc/v3/products', { + params: { + per_page: pageSize, + page: 6, + search: 'xxx', + }, + } ); + const page6JSON = await page6.json(); + expect( Array.isArray( page6JSON ) ).toBe( true ); + expect( page6JSON ).toHaveLength( 0 ); } ); - const excludedJSON = await excluded.json(); - expect( excluded.status() ).toEqual( 200 ); - expect( excludedJSON.length ).toBeGreaterThanOrEqual( - Number( PRODUCTS_COUNT - productsToFilter.length ) - ); - expect( excludedJSON ).toEqual( - expect.not.arrayContaining( - productsToFilter.map( ( id ) => + + test( 'search', async ( { request } ) => { + // Match in the short description. + const result1 = await request.get( 'wp-json/wc/v3/products', { + params: { + search: 'external', + }, + } ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON.length ).toBeGreaterThanOrEqual( 1 ); + expect( result1JSON ).toEqual( + expect.arrayContaining( [ expect.objectContaining( { - id, - } ) + name: 'WordPress Pennant xxx', + } ), + ] ) + ); + + // Match in the product name. + const result2 = await request.get( 'wp-json/wc/v3/products', { + params: { + search: 'pocket xxx', + }, + } ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toHaveLength( 1 ); + expect( result2JSON[ 0 ].name ).toBe( + 'Hoodie with Pocket xxx' + ); + } ); + + test( 'inclusion / exclusion', async ( { request } ) => { + const allProducts = await request.get( + 'wp-json/wc/v3/products', + { + params: { + per_page: 20, + search: 'xxx', + }, + } + ); + const allProductsJSON = await allProducts.json(); + expect( allProducts.status() ).toEqual( 200 ); + const allProductIds = allProductsJSON.map( + ( product ) => product.id + ); + expect( allProductIds ).toHaveLength( PRODUCTS_COUNT ); + + const productsToFilter = [ + allProductIds[ 2 ], + allProductIds[ 4 ], + allProductIds[ 7 ], + allProductIds[ 13 ], + ]; + + const included = await request.get( 'wp-json/wc/v3/products', { + params: { + per_page: 20, + include: productsToFilter.join( ',' ), + }, + } ); + const includedJSON = await included.json(); + expect( included.status() ).toEqual( 200 ); + expect( includedJSON ).toHaveLength( productsToFilter.length ); + expect( includedJSON ).toEqual( + expect.arrayContaining( + productsToFilter.map( ( id ) => + expect.objectContaining( { + id, + } ) + ) ) - ) + ); + + const excluded = await request.get( 'wp-json/wc/v3/products', { + params: { + per_page: 20, + exclude: productsToFilter.join( ',' ), + }, + } ); + const excludedJSON = await excluded.json(); + expect( excluded.status() ).toEqual( 200 ); + expect( excludedJSON.length ).toBeGreaterThanOrEqual( + Number( PRODUCTS_COUNT - productsToFilter.length ) + ); + expect( excludedJSON ).toEqual( + expect.not.arrayContaining( + productsToFilter.map( ( id ) => + expect.objectContaining( { + id, + } ) + ) + ) + ); + } ); + + test( 'slug', async ( { request } ) => { + // Match by slug. + const result1 = await request.get( 'wp-json/wc/v3/products', { + params: { + slug: 't-shirt-with-logo-xxx', + }, + } ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( 1 ); + expect( result1JSON[ 0 ].slug ).toBe( 't-shirt-with-logo-xxx' ); + + // No matches + const result2 = await request.get( 'wp-json/wc/v3/products', { + params: { + slug: 'no-product-with-this-slug', + }, + } ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toHaveLength( 0 ); + } ); + + test( 'sku', async ( { request } ) => { + // Match by SKU. + const result1 = await request.get( 'wp-json/wc/v3/products', { + params: { + sku: 'woo-sunglasses-product', + }, + } ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( 1 ); + expect( result1JSON[ 0 ].sku ).toBe( 'woo-sunglasses-product' ); + + // No matches + const result2 = await request.get( 'wp-json/wc/v3/products', { + params: { + sku: 'no-product-with-this-sku', + }, + } ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toHaveLength( 0 ); + } ); + + test( 'type', async ( { request } ) => { + const result1 = await request.get( 'wp-json/wc/v3/products', { + params: { + type: 'simple', + search: 'xxx', + }, + } ); + expect( result1.status() ).toEqual( 200 ); + expect( result1.headers()[ 'x-wp-total' ] ).toEqual( '16' ); + + const result2 = await request.get( 'wp-json/wc/v3/products', { + params: { + type: 'external', + search: 'xxx', + }, + } ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toHaveLength( 1 ); + expect( result2JSON[ 0 ].name ).toBe( 'WordPress Pennant xxx' ); + + const result3 = await request.get( 'wp-json/wc/v3/products', { + params: { + type: 'variable', + search: 'xxx', + }, + } ); + const result3JSON = await result3.json(); + expect( result3.status() ).toEqual( 200 ); + expect( result3JSON ).toHaveLength( 2 ); + + const result4 = await request.get( 'wp-json/wc/v3/products', { + params: { + type: 'grouped', + search: 'xxx', + }, + } ); + const result4JSON = await result4.json(); + expect( result4.status() ).toEqual( 200 ); + expect( result4JSON ).toHaveLength( 1 ); + expect( result4JSON[ 0 ].name ).toBe( 'Logo Collection xxx' ); + } ); + + test( 'featured', async ( { request } ) => { + const featured = [ + expect.objectContaining( { + name: 'Hoodie with Zipper xxx', + } ), + expect.objectContaining( { + name: 'Hoodie with Pocket xxx', + } ), + expect.objectContaining( { + name: 'Sunglasses xxx', + } ), + expect.objectContaining( { + name: 'Cap xxx', + } ), + expect.objectContaining( { + name: 'V-Neck T-Shirt xxx', + } ), + ]; + + const result1 = await request.get( 'wp-json/wc/v3/products', { + params: { + featured: true, + search: 'xxx', + }, + } ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( featured.length ); + expect( result1JSON ).toEqual( + expect.arrayContaining( featured ) + ); + + const result2 = await request.get( 'wp-json/wc/v3/products', { + params: { + featured: false, + search: 'xxx', + }, + } ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toEqual( + expect.not.arrayContaining( featured ) + ); + } ); + + test( + 'categories', + { tag: '@skip-on-default-wpcom' }, + async ( { request } ) => { + const accessory = [ + expect.objectContaining( { + name: 'Beanie xxx', + } ), + ]; + const hoodies = [ + expect.objectContaining( { + name: 'Hoodie with Zipper xxx', + } ), + expect.objectContaining( { + name: 'Hoodie with Pocket xxx', + } ), + expect.objectContaining( { + name: 'Hoodie with Logo xxx', + } ), + expect.objectContaining( { + name: 'Hoodie xxx', + } ), + ]; + + // Verify that subcategories are included. + const result1 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + per_page: 20, + category: sampleData.categories.clothingJSON.id, + }, + } + ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toEqual( + expect.arrayContaining( accessory ) + ); + expect( result1JSON ).toEqual( + expect.arrayContaining( hoodies ) + ); + + // Verify sibling categories are not. + const result2 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + category: sampleData.categories.hoodiesJSON.id, + }, + } + ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toEqual( + expect.not.arrayContaining( accessory ) + ); + expect( result2JSON ).toEqual( + expect.arrayContaining( hoodies ) + ); + } ); - } ); - test( 'slug', async ( { request } ) => { - // Match by slug. - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - slug: 't-shirt-with-logo-xxx', - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( 1 ); - expect( result1JSON[ 0 ].slug ).toBe( 't-shirt-with-logo-xxx' ); - - // No matches - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - slug: 'no-product-with-this-slug', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toHaveLength( 0 ); - } ); - - test( 'sku', async ( { request } ) => { - // Match by SKU. - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - sku: 'woo-sunglasses-product', - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( 1 ); - expect( result1JSON[ 0 ].sku ).toBe( 'woo-sunglasses-product' ); - - // No matches - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - sku: 'no-product-with-this-sku', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toHaveLength( 0 ); - } ); - - test( 'type', async ( { request } ) => { - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - type: 'simple', - search: 'xxx', - }, - } ); - expect( result1.status() ).toEqual( 200 ); - expect( result1.headers()[ 'x-wp-total' ] ).toEqual( '16' ); - - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - type: 'external', - search: 'xxx', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toHaveLength( 1 ); - expect( result2JSON[ 0 ].name ).toBe( 'WordPress Pennant xxx' ); - - const result3 = await request.get( 'wp-json/wc/v3/products', { - params: { - type: 'variable', - search: 'xxx', - }, - } ); - const result3JSON = await result3.json(); - expect( result3.status() ).toEqual( 200 ); - expect( result3JSON ).toHaveLength( 2 ); - - const result4 = await request.get( 'wp-json/wc/v3/products', { - params: { - type: 'grouped', - search: 'xxx', - }, - } ); - const result4JSON = await result4.json(); - expect( result4.status() ).toEqual( 200 ); - expect( result4JSON ).toHaveLength( 1 ); - expect( result4JSON[ 0 ].name ).toBe( 'Logo Collection xxx' ); - } ); - - test( 'featured', async ( { request } ) => { - const featured = [ - expect.objectContaining( { - name: 'Hoodie with Zipper xxx', - } ), - expect.objectContaining( { - name: 'Hoodie with Pocket xxx', - } ), - expect.objectContaining( { - name: 'Sunglasses xxx', - } ), - expect.objectContaining( { - name: 'Cap xxx', - } ), - expect.objectContaining( { - name: 'V-Neck T-Shirt xxx', - } ), - ]; - - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - featured: true, - search: 'xxx', - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( featured.length ); - expect( result1JSON ).toEqual( expect.arrayContaining( featured ) ); - - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - featured: false, - search: 'xxx', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toEqual( - expect.not.arrayContaining( featured ) - ); - } ); - - test( 'categories', async ( { request } ) => { - const accessory = [ - expect.objectContaining( { - name: 'Beanie xxx', - } ), - ]; - const hoodies = [ - expect.objectContaining( { - name: 'Hoodie with Zipper xxx', - } ), - expect.objectContaining( { - name: 'Hoodie with Pocket xxx', - } ), - expect.objectContaining( { - name: 'Hoodie with Logo xxx', - } ), - expect.objectContaining( { - name: 'Hoodie xxx', - } ), - ]; - - // Verify that subcategories are included. - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - per_page: 20, - category: sampleData.categories.clothingJSON.id, - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toEqual( - expect.arrayContaining( accessory ) - ); - expect( result1JSON ).toEqual( expect.arrayContaining( hoodies ) ); - - // Verify sibling categories are not. - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - category: sampleData.categories.hoodiesJSON.id, - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toEqual( - expect.not.arrayContaining( accessory ) - ); - expect( result2JSON ).toEqual( expect.arrayContaining( hoodies ) ); - } ); - - test( 'on sale', async ( { request } ) => { - const onSale = [ - expect.objectContaining( { - name: 'Beanie with Logo xxx', - } ), - expect.objectContaining( { - name: 'Hoodie with Pocket xxx', - } ), - expect.objectContaining( { - name: 'Single xxx', - } ), - expect.objectContaining( { - name: 'Cap xxx', - } ), - expect.objectContaining( { - name: 'Belt xxx', - } ), - expect.objectContaining( { - name: 'Beanie xxx', - } ), - expect.objectContaining( { - name: 'Hoodie xxx', - } ), - ]; - - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - on_sale: true, - search: 'xxx', - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( onSale.length ); - expect( result1JSON ).toEqual( expect.arrayContaining( onSale ) ); - - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - on_sale: false, - search: 'xxx', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toEqual( - expect.not.arrayContaining( onSale ) - ); - } ); - - test( 'price', async ( { request } ) => { - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - min_price: 21, - max_price: 28, - search: 'xxx', - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( 1 ); - expect( result1JSON[ 0 ].name ).toBe( 'Long Sleeve Tee xxx' ); - expect( result1JSON[ 0 ].price ).toBe( '25' ); - - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - max_price: 5, - search: 'xxx', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toHaveLength( 1 ); - expect( result2JSON[ 0 ].name ).toBe( 'Single xxx' ); - expect( result2JSON[ 0 ].price ).toBe( '2' ); - - const result3 = await request.get( 'wp-json/wc/v3/products', { - params: { - min_price: 5, - order: 'asc', - orderby: 'price', - search: 'xxx', - }, - } ); - const result3JSON = await result3.json(); - expect( result3.status() ).toEqual( 200 ); - expect( result3JSON ).toEqual( - expect.not.arrayContaining( [ + test( 'on sale', async ( { request } ) => { + const onSale = [ + expect.objectContaining( { + name: 'Beanie with Logo xxx', + } ), + expect.objectContaining( { + name: 'Hoodie with Pocket xxx', + } ), expect.objectContaining( { name: 'Single xxx', } ), - ] ) - ); - } ); + expect.objectContaining( { + name: 'Cap xxx', + } ), + expect.objectContaining( { + name: 'Belt xxx', + } ), + expect.objectContaining( { + name: 'Beanie xxx', + } ), + expect.objectContaining( { + name: 'Hoodie xxx', + } ), + ]; - test( 'before / after', async ( { request } ) => { - const before = [ - expect.objectContaining( { - name: 'Album xxx', - } ), - expect.objectContaining( { - name: 'Single xxx', - } ), - expect.objectContaining( { - name: 'T-Shirt with Logo xxx', - } ), - expect.objectContaining( { - name: 'Beanie with Logo xxx', - } ), - ]; - const after = [ - expect.objectContaining( { - name: 'Hoodie xxx', - } ), - expect.objectContaining( { - name: 'V-Neck T-Shirt xxx', - } ), - expect.objectContaining( { - name: 'Parent Product xxx', - } ), - expect.objectContaining( { - name: 'Child Product xxx', - } ), - ]; + const result1 = await request.get( 'wp-json/wc/v3/products', { + params: { + on_sale: true, + search: 'xxx', + }, + } ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( onSale.length ); + expect( result1JSON ).toEqual( + expect.arrayContaining( onSale ) + ); - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - before: '2021-09-05T15:50:19', - search: 'xxx', - }, + const result2 = await request.get( 'wp-json/wc/v3/products', { + params: { + on_sale: false, + search: 'xxx', + }, + } ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toEqual( + expect.not.arrayContaining( onSale ) + ); } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( before.length ); - expect( result1JSON ).toEqual( expect.arrayContaining( before ) ); - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - after: '2021-09-18T15:50:18', - search: 'xxx', - }, + test( 'price', async ( { request } ) => { + const result1 = await request.get( 'wp-json/wc/v3/products', { + params: { + min_price: 21, + max_price: 28, + search: 'xxx', + }, + } ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( 1 ); + expect( result1JSON[ 0 ].name ).toBe( 'Long Sleeve Tee xxx' ); + expect( result1JSON[ 0 ].price ).toBe( '25' ); + + const result2 = await request.get( 'wp-json/wc/v3/products', { + params: { + max_price: 5, + search: 'xxx', + }, + } ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toHaveLength( 1 ); + expect( result2JSON[ 0 ].name ).toBe( 'Single xxx' ); + expect( result2JSON[ 0 ].price ).toBe( '2' ); + + const result3 = await request.get( 'wp-json/wc/v3/products', { + params: { + min_price: 5, + order: 'asc', + orderby: 'price', + search: 'xxx', + }, + } ); + const result3JSON = await result3.json(); + expect( result3.status() ).toEqual( 200 ); + expect( result3JSON ).toEqual( + expect.not.arrayContaining( [ + expect.objectContaining( { + name: 'Single xxx', + } ), + ] ) + ); } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toEqual( - expect.not.arrayContaining( before ) - ); - expect( result2JSON ).toHaveLength( after.length ); - expect( result2JSON ).toEqual( expect.arrayContaining( after ) ); - } ); - test( 'attributes', async ( { request } ) => { - const red = sampleData.attributes.colors.find( - ( term ) => term.name === 'Red' - ); - - const redProducts = [ - expect.objectContaining( { - name: 'V-Neck T-Shirt xxx', - } ), - expect.objectContaining( { - name: 'Hoodie xxx', - } ), - expect.objectContaining( { - name: 'Beanie xxx', - } ), - expect.objectContaining( { - name: 'Beanie with Logo xxx', - } ), - ]; - - const result = await request.get( 'wp-json/wc/v3/products', { - params: { - attribute: 'pa_colorxxx', - attribute_term: red.id, - }, - } ); - const resultJSON = await result.json(); - - expect( result.status() ).toEqual( 200 ); - expect( resultJSON ).toHaveLength( redProducts.length ); - expect( resultJSON ).toEqual( - expect.arrayContaining( redProducts ) - ); - } ); - - test( 'status', async ( { request } ) => { - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - status: 'pending', - search: 'xxx', - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( 1 ); - expect( result1JSON[ 0 ].name ).toBe( 'Polo xxx' ); - - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - status: 'draft', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toHaveLength( 0 ); - } ); - - test( 'shipping class', async ( { request } ) => { - const result = await request.get( 'wp-json/wc/v3/products', { - params: { - shipping_class: sampleData.shippingClasses.freightJSON.id, - }, - } ); - const resultJSON = await result.json(); - expect( result.status() ).toEqual( 200 ); - expect( resultJSON ).toHaveLength( 1 ); - expect( resultJSON[ 0 ].name ).toBe( 'Long Sleeve Tee xxx' ); - } ); - - test( 'tax class', async ( { request } ) => { - const result = await request.get( 'wp-json/wc/v3/products', { - params: { - tax_class: 'reduced-rate', - search: 'xxx', - }, - } ); - const resultJSON = await result.json(); - expect( result.status() ).toEqual( 200 ); - expect( resultJSON ).toHaveLength( 1 ); - expect( resultJSON[ 0 ].name ).toBe( 'Sunglasses xxx' ); - } ); - - test( 'stock status', async ( { request } ) => { - const result = await request.get( 'wp-json/wc/v3/products', { - params: { - stock_status: 'onbackorder', - search: 'xxx', - }, - } ); - const resultJSON = await result.json(); - expect( result.status() ).toEqual( 200 ); - expect( resultJSON ).toHaveLength( 1 ); - expect( resultJSON[ 0 ].name ).toBe( 'T-Shirt xxx' ); - } ); - - test( 'tags', async ( { request } ) => { - const coolProducts = [ - expect.objectContaining( { - name: 'Sunglasses xxx', - } ), - expect.objectContaining( { - name: 'Hoodie with Pocket xxx', - } ), - expect.objectContaining( { - name: 'Beanie xxx', - } ), - ]; - - const result = await request.get( 'wp-json/wc/v3/products', { - params: { - tag: sampleData.tags.coolJSON.id, - }, - } ); - const resultJSON = await result.json(); - - expect( result.status() ).toEqual( 200 ); - expect( resultJSON ).toHaveLength( coolProducts.length ); - expect( resultJSON ).toEqual( - expect.arrayContaining( coolProducts ) - ); - } ); - - test( 'parent', async ( { request } ) => { - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - parent: sampleData.hierarchicalProducts.parentJSON.id, - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( 1 ); - expect( result1JSON[ 0 ].name ).toBe( 'Child Product xxx' ); - - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - parent_exclude: - sampleData.hierarchicalProducts.parentJSON.id, - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toEqual( - expect.not.arrayContaining( [ + test( 'before / after', async ( { request } ) => { + const before = [ + expect.objectContaining( { + name: 'Album xxx', + } ), + expect.objectContaining( { + name: 'Single xxx', + } ), + expect.objectContaining( { + name: 'T-Shirt with Logo xxx', + } ), + expect.objectContaining( { + name: 'Beanie with Logo xxx', + } ), + ]; + const after = [ + expect.objectContaining( { + name: 'Hoodie xxx', + } ), + expect.objectContaining( { + name: 'V-Neck T-Shirt xxx', + } ), + expect.objectContaining( { + name: 'Parent Product xxx', + } ), expect.objectContaining( { name: 'Child Product xxx', } ), - ] ) - ); - } ); - - test.describe( 'orderby', () => { - const productNamesAsc = [ - 'Album xxx', - 'Beanie with Logo xxx', - 'Beanie xxx', - 'Belt xxx', - 'Cap xxx', - 'Child Product xxx', - 'Hoodie with Logo xxx', - 'Hoodie with Pocket xxx', - 'Hoodie with Zipper xxx', - 'Hoodie xxx', - 'Logo Collection xxx', - 'Long Sleeve Tee xxx', - 'Parent Product xxx', - 'Polo xxx', - 'Single xxx', - 'Sunglasses xxx', - 'T-Shirt with Logo xxx', - 'T-Shirt xxx', - 'V-Neck T-Shirt xxx', - 'WordPress Pennant xxx', - ]; - const productNamesDesc = [ ...productNamesAsc ].reverse(); - const productNamesByRatingAsc = [ - 'Sunglasses xxx', - 'Cap xxx', - 'T-Shirt xxx', - ]; - const productNamesByRatingDesc = [ - ...productNamesByRatingAsc, - ].reverse(); - const productNamesByPopularityDesc = [ - 'Beanie with Logo xxx', - 'Single xxx', - 'T-Shirt xxx', - ]; - const productNamesByPopularityAsc = [ - ...productNamesByPopularityDesc, - ].reverse(); - - test( 'default', async ( { request } ) => { - // Default = date desc. - const result = await request.get( 'wp-json/wc/v3/products', { - params: { - search: 'xxx', - }, - } ); - const resultJSON = await result.json(); - expect( result.status() ).toEqual( 200 ); - - // Verify all dates are in descending order. - let lastDate = Date.now(); - resultJSON.forEach( ( { date_created_gmt } ) => { - const created = Date.parse( date_created_gmt + '.000Z' ); - expect( lastDate ).toBeGreaterThan( created ); - lastDate = created; - } ); - } ); - - test( 'date', async ( { request } ) => { - const result = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'asc', - orderby: 'date', - search: 'xxx', - }, - } ); - const resultJSON = await result.json(); - expect( result.status() ).toEqual( 200 ); - - // Verify all dates are in ascending order. - let lastDate = 0; - resultJSON.forEach( ( { date_created_gmt } ) => { - const created = Date.parse( date_created_gmt + '.000Z' ); - expect( created ).toBeGreaterThan( lastDate ); - lastDate = created; - } ); - } ); - - test( 'id', async ( { request } ) => { - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'asc', - orderby: 'id', - search: 'xxx', - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - - // Verify all results are in ascending order. - let lastId = 0; - result1JSON.forEach( ( { id } ) => { - expect( id ).toBeGreaterThan( lastId ); - lastId = id; - } ); - - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'desc', - orderby: 'id', - search: 'xxx', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - - // Verify all results are in descending order. - lastId = Number.MAX_SAFE_INTEGER; - result2JSON.forEach( ( { id } ) => { - expect( lastId ).toBeGreaterThan( id ); - lastId = id; - } ); - } ); - - test( 'title', async ( { request } ) => { - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'asc', - orderby: 'title', - per_page: productNamesAsc.length, - search: 'xxx', - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - - // Verify all results are in ascending order. - result1JSON.forEach( ( { name }, idx ) => { - expect( name ).toBe( productNamesAsc[ idx ] ); - } ); - - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'desc', - orderby: 'title', - per_page: productNamesDesc.length, - search: 'xxx', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - - // Verify all results are in descending order. - result2JSON.forEach( ( { name }, idx ) => { - expect( name ).toBe( productNamesDesc[ idx ] ); - } ); - } ); - - test( 'slug orderby', async ( { request } ) => { - const productNamesBySlugAsc = [ - 'Polo xxx', // The Polo isn't published so it has an empty slug. - ...productNamesAsc.filter( ( p ) => p !== 'Polo xxx' ), ]; - const productNamesBySlugDesc = [ - ...productNamesBySlugAsc, + + const result1 = await request.get( 'wp-json/wc/v3/products', { + params: { + before: '2021-09-05T15:50:19', + search: 'xxx', + }, + } ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( before.length ); + expect( result1JSON ).toEqual( + expect.arrayContaining( before ) + ); + + const result2 = await request.get( 'wp-json/wc/v3/products', { + params: { + after: '2021-09-18T15:50:18', + search: 'xxx', + }, + } ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toEqual( + expect.not.arrayContaining( before ) + ); + expect( result2JSON ).toHaveLength( after.length ); + expect( result2JSON ).toEqual( + expect.arrayContaining( after ) + ); + } ); + + test( 'attributes', async ( { request } ) => { + const red = sampleData.attributes.colors.find( + ( term ) => term.name === 'Red' + ); + + const redProducts = [ + expect.objectContaining( { + name: 'V-Neck T-Shirt xxx', + } ), + expect.objectContaining( { + name: 'Hoodie xxx', + } ), + expect.objectContaining( { + name: 'Beanie xxx', + } ), + expect.objectContaining( { + name: 'Beanie with Logo xxx', + } ), + ]; + + const result = await request.get( 'wp-json/wc/v3/products', { + params: { + attribute: 'pa_colorxxx', + attribute_term: red.id, + }, + } ); + const resultJSON = await result.json(); + + expect( result.status() ).toEqual( 200 ); + expect( resultJSON ).toHaveLength( redProducts.length ); + expect( resultJSON ).toEqual( + expect.arrayContaining( redProducts ) + ); + } ); + + test( 'status', async ( { request } ) => { + const result1 = await request.get( 'wp-json/wc/v3/products', { + params: { + status: 'pending', + search: 'xxx', + }, + } ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( 1 ); + expect( result1JSON[ 0 ].name ).toBe( 'Polo xxx' ); + + const result2 = await request.get( 'wp-json/wc/v3/products', { + params: { + status: 'draft', + }, + } ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toHaveLength( 0 ); + } ); + + test( 'shipping class', async ( { request } ) => { + const result = await request.get( 'wp-json/wc/v3/products', { + params: { + shipping_class: + sampleData.shippingClasses.freightJSON.id, + }, + } ); + const resultJSON = await result.json(); + expect( result.status() ).toEqual( 200 ); + expect( resultJSON ).toHaveLength( 1 ); + expect( resultJSON[ 0 ].name ).toBe( 'Long Sleeve Tee xxx' ); + } ); + + test( 'tax class', async ( { request } ) => { + const result = await request.get( 'wp-json/wc/v3/products', { + params: { + tax_class: 'reduced-rate', + search: 'xxx', + }, + } ); + const resultJSON = await result.json(); + expect( result.status() ).toEqual( 200 ); + expect( resultJSON ).toHaveLength( 1 ); + expect( resultJSON[ 0 ].name ).toBe( 'Sunglasses xxx' ); + } ); + + test( 'stock status', async ( { request } ) => { + const result = await request.get( 'wp-json/wc/v3/products', { + params: { + stock_status: 'onbackorder', + search: 'xxx', + }, + } ); + const resultJSON = await result.json(); + expect( result.status() ).toEqual( 200 ); + expect( resultJSON ).toHaveLength( 1 ); + expect( resultJSON[ 0 ].name ).toBe( 'T-Shirt xxx' ); + } ); + + test( 'tags', async ( { request } ) => { + const coolProducts = [ + expect.objectContaining( { + name: 'Sunglasses xxx', + } ), + expect.objectContaining( { + name: 'Hoodie with Pocket xxx', + } ), + expect.objectContaining( { + name: 'Beanie xxx', + } ), + ]; + + const result = await request.get( 'wp-json/wc/v3/products', { + params: { + tag: sampleData.tags.coolJSON.id, + }, + } ); + const resultJSON = await result.json(); + + expect( result.status() ).toEqual( 200 ); + expect( resultJSON ).toHaveLength( coolProducts.length ); + expect( resultJSON ).toEqual( + expect.arrayContaining( coolProducts ) + ); + } ); + + test( 'parent', async ( { request } ) => { + const result1 = await request.get( 'wp-json/wc/v3/products', { + params: { + parent: sampleData.hierarchicalProducts.parentJSON.id, + }, + } ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( 1 ); + expect( result1JSON[ 0 ].name ).toBe( 'Child Product xxx' ); + + const result2 = await request.get( 'wp-json/wc/v3/products', { + params: { + parent_exclude: + sampleData.hierarchicalProducts.parentJSON.id, + }, + } ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toEqual( + expect.not.arrayContaining( [ + expect.objectContaining( { + name: 'Child Product xxx', + } ), + ] ) + ); + } ); + + test.describe( 'orderby', () => { + const productNamesAsc = [ + 'Album xxx', + 'Beanie with Logo xxx', + 'Beanie xxx', + 'Belt xxx', + 'Cap xxx', + 'Child Product xxx', + 'Hoodie with Logo xxx', + 'Hoodie with Pocket xxx', + 'Hoodie with Zipper xxx', + 'Hoodie xxx', + 'Logo Collection xxx', + 'Long Sleeve Tee xxx', + 'Parent Product xxx', + 'Polo xxx', + 'Single xxx', + 'Sunglasses xxx', + 'T-Shirt with Logo xxx', + 'T-Shirt xxx', + 'V-Neck T-Shirt xxx', + 'WordPress Pennant xxx', + ]; + const productNamesDesc = [ ...productNamesAsc ].reverse(); + const productNamesByRatingAsc = [ + 'Sunglasses xxx', + 'Cap xxx', + 'T-Shirt xxx', + ]; + const productNamesByRatingDesc = [ + ...productNamesByRatingAsc, + ].reverse(); + const productNamesByPopularityDesc = [ + 'Beanie with Logo xxx', + 'Single xxx', + 'T-Shirt xxx', + ]; + const productNamesByPopularityAsc = [ + ...productNamesByPopularityDesc, ].reverse(); - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'asc', - orderby: 'slug', - per_page: productNamesBySlugAsc.length, - search: 'xxx', - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); + test( 'default', async ( { request } ) => { + // Default = date desc. + const result = await request.get( + 'wp-json/wc/v3/products', + { + params: { + search: 'xxx', + }, + } + ); + const resultJSON = await result.json(); + expect( result.status() ).toEqual( 200 ); - // Verify all results are in ascending order. - result1JSON.forEach( ( { name }, idx ) => { - expect( name ).toBe( productNamesBySlugAsc[ idx ] ); + // Verify all dates are in descending order. + let lastDate = Date.now(); + resultJSON.forEach( ( { date_created_gmt } ) => { + const created = Date.parse( + date_created_gmt + '.000Z' + ); + expect( lastDate ).toBeGreaterThan( created ); + lastDate = created; + } ); } ); - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'desc', - orderby: 'slug', - per_page: productNamesBySlugDesc.length, - search: 'xxx', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); + test( 'date', async ( { request } ) => { + const result = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'asc', + orderby: 'date', + search: 'xxx', + }, + } + ); + const resultJSON = await result.json(); + expect( result.status() ).toEqual( 200 ); - // Verify all results are in descending order. - result2JSON.forEach( ( { name }, idx ) => { - expect( name ).toBe( productNamesBySlugDesc[ idx ] ); + // Verify all dates are in ascending order. + let lastDate = 0; + resultJSON.forEach( ( { date_created_gmt } ) => { + const created = Date.parse( + date_created_gmt + '.000Z' + ); + expect( created ).toBeGreaterThan( lastDate ); + lastDate = created; + } ); + } ); + + test( 'id', async ( { request } ) => { + const result1 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'asc', + orderby: 'id', + search: 'xxx', + }, + } + ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + + // Verify all results are in ascending order. + let lastId = 0; + result1JSON.forEach( ( { id } ) => { + expect( id ).toBeGreaterThan( lastId ); + lastId = id; + } ); + + const result2 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'desc', + orderby: 'id', + search: 'xxx', + }, + } + ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + + // Verify all results are in descending order. + lastId = Number.MAX_SAFE_INTEGER; + result2JSON.forEach( ( { id } ) => { + expect( lastId ).toBeGreaterThan( id ); + lastId = id; + } ); + } ); + + test( 'title', async ( { request } ) => { + const result1 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'asc', + orderby: 'title', + per_page: productNamesAsc.length, + search: 'xxx', + }, + } + ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + + // Verify all results are in ascending order. + result1JSON.forEach( ( { name }, idx ) => { + expect( name ).toBe( productNamesAsc[ idx ] ); + } ); + + const result2 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'desc', + orderby: 'title', + per_page: productNamesDesc.length, + search: 'xxx', + }, + } + ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + + // Verify all results are in descending order. + result2JSON.forEach( ( { name }, idx ) => { + expect( name ).toBe( productNamesDesc[ idx ] ); + } ); + } ); + + test( 'slug orderby', async ( { request } ) => { + const productNamesBySlugAsc = [ + 'Polo xxx', // The Polo isn't published so it has an empty slug. + ...productNamesAsc.filter( ( p ) => p !== 'Polo xxx' ), + ]; + const productNamesBySlugDesc = [ + ...productNamesBySlugAsc, + ].reverse(); + + const result1 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'asc', + orderby: 'slug', + per_page: productNamesBySlugAsc.length, + search: 'xxx', + }, + } + ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + + // Verify all results are in ascending order. + result1JSON.forEach( ( { name }, idx ) => { + expect( name ).toBe( productNamesBySlugAsc[ idx ] ); + } ); + + const result2 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'desc', + orderby: 'slug', + per_page: productNamesBySlugDesc.length, + search: 'xxx', + }, + } + ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + + // Verify all results are in descending order. + result2JSON.forEach( ( { name }, idx ) => { + expect( name ).toBe( productNamesBySlugDesc[ idx ] ); + } ); + } ); + + test( 'price orderby', async ( { request } ) => { + const productNamesMinPriceAsc = [ + 'Parent Product xxx', + 'Child Product xxx', + 'Single xxx', + 'WordPress Pennant xxx', + 'Album xxx', + 'V-Neck T-Shirt xxx', + 'Cap xxx', + 'Beanie with Logo xxx', + 'T-Shirt with Logo xxx', + 'Beanie xxx', + 'T-Shirt xxx', + 'Logo Collection xxx', + 'Polo xxx', + 'Long Sleeve Tee xxx', + 'Hoodie with Pocket xxx', + 'Hoodie xxx', + 'Hoodie with Zipper xxx', + 'Hoodie with Logo xxx', + 'Belt xxx', + 'Sunglasses xxx', + ]; + const result1 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'asc', + orderby: 'price', + per_page: productNamesMinPriceAsc.length, + search: 'xxx', + }, + } + ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( + productNamesMinPriceAsc.length + ); + + // Verify all results are in ascending order. + // The query uses the min price calculated in the product meta lookup table, + // so we can't just check the price property of the response. + result1JSON.forEach( ( { name }, idx ) => { + expect( name ).toBe( productNamesMinPriceAsc[ idx ] ); + } ); + + const productNamesMaxPriceDesc = [ + 'Sunglasses xxx', + 'Belt xxx', + 'Hoodie xxx', + 'Logo Collection xxx', + 'Hoodie with Logo xxx', + 'Hoodie with Zipper xxx', + 'Hoodie with Pocket xxx', + 'Long Sleeve Tee xxx', + 'V-Neck T-Shirt xxx', + 'Polo xxx', + 'T-Shirt xxx', + 'Beanie xxx', + 'T-Shirt with Logo xxx', + 'Beanie with Logo xxx', + 'Cap xxx', + 'Album xxx', + 'WordPress Pennant xxx', + 'Single xxx', + 'Child Product xxx', + 'Parent Product xxx', + ]; + + const result2 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'desc', + orderby: 'price', + per_page: productNamesMaxPriceDesc.length, + search: 'xxx', + }, + } + ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toHaveLength( + productNamesMaxPriceDesc.length + ); + + // Verify all results are in descending order. + // The query uses the max price calculated in the product meta lookup table, + // so we can't just check the price property of the response. + result2JSON.forEach( ( { name }, idx ) => { + expect( name ).toBe( productNamesMaxPriceDesc[ idx ] ); + } ); + } ); + + test( 'include', async ( { request } ) => { + const includeIds = [ + sampleData.groupedProducts[ 0 ].id, + sampleData.simpleProducts[ 3 ].id, + sampleData.hierarchicalProducts.parentJSON.id, + ]; + + const result1 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'asc', + orderby: 'include', + include: includeIds.join( ',' ), + }, + } + ); + const result1JSON = await result1.json(); + + expect( result1.status() ).toEqual( 200 ); + expect( result1JSON ).toHaveLength( includeIds.length ); + + // Verify all results are in proper order. + result1JSON.forEach( ( { id }, idx ) => { + expect( id ).toBe( includeIds[ idx ] ); + } ); + + const result2 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'desc', + orderby: 'include', + include: includeIds.join( ',' ), + }, + } + ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + expect( result2JSON ).toHaveLength( includeIds.length ); + + // Verify all results are in proper order. + result2JSON.forEach( ( { id }, idx ) => { + expect( id ).toBe( includeIds[ idx ] ); + } ); + } ); + + test( 'rating (desc)', async ( { request } ) => { + const result2 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'desc', + orderby: 'rating', + per_page: productNamesByRatingDesc.length, + search: 'xxx', + }, + } + ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + + // Verify all results are in descending order. + result2JSON.forEach( ( { name }, idx ) => { + expect( name ).toBe( productNamesByRatingDesc[ idx ] ); + } ); + } ); + + // This case will remain skipped until ratings can be sorted ascending. + // See: https://github.com/woocommerce/woocommerce/issues/30354#issuecomment-925955099. + test.skip( 'rating (asc)', async ( { request } ) => { + const result1 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'asc', + orderby: 'rating', + per_page: productNamesByRatingAsc.length, + search: 'xxx', + }, + } + ); + expect( result1.status() ).toEqual( 200 ); + const result1JSON = await result1.json(); + + // Verify all results are in ascending order. + result1JSON.forEach( ( { name }, idx ) => { + expect( name ).toBe( productNamesByRatingAsc[ idx ] ); + } ); + } ); + + // This case will remain skipped until popularity can be sorted ascending. + // See: https://github.com/woocommerce/woocommerce/issues/30354#issuecomment-925955099. + test.skip( 'popularity (asc)', async ( { request } ) => { + const result1 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'asc', + orderby: 'popularity', + per_page: productNamesByPopularityAsc.length, + search: 'xxx', + }, + } + ); + const result1JSON = await result1.json(); + expect( result1.status() ).toEqual( 200 ); + + // Verify all results are in ascending order. + result1JSON.forEach( ( { name }, idx ) => { + expect( name ).toBe( + productNamesByPopularityAsc[ idx ] + ); + } ); + } ); + + test( 'popularity (desc)', async ( { request } ) => { + const result2 = await request.get( + 'wp-json/wc/v3/products', + { + params: { + order: 'desc', + orderby: 'popularity', + per_page: productNamesByPopularityDesc.length, + search: 'xxx', + }, + } + ); + const result2JSON = await result2.json(); + expect( result2.status() ).toEqual( 200 ); + + // Verify all results are in descending order. + result2JSON.forEach( ( { name }, idx ) => { + expect( name ).toBe( + productNamesByPopularityDesc[ idx ] + ); + } ); } ); } ); - - test( 'price orderby', async ( { request } ) => { - const productNamesMinPriceAsc = [ - 'Parent Product xxx', - 'Child Product xxx', - 'Single xxx', - 'WordPress Pennant xxx', - 'Album xxx', - 'V-Neck T-Shirt xxx', - 'Cap xxx', - 'Beanie with Logo xxx', - 'T-Shirt with Logo xxx', - 'Beanie xxx', - 'T-Shirt xxx', - 'Logo Collection xxx', - 'Polo xxx', - 'Long Sleeve Tee xxx', - 'Hoodie with Pocket xxx', - 'Hoodie xxx', - 'Hoodie with Zipper xxx', - 'Hoodie with Logo xxx', - 'Belt xxx', - 'Sunglasses xxx', - ]; - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'asc', - orderby: 'price', - per_page: productNamesMinPriceAsc.length, - search: 'xxx', - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( - productNamesMinPriceAsc.length - ); - - // Verify all results are in ascending order. - // The query uses the min price calculated in the product meta lookup table, - // so we can't just check the price property of the response. - result1JSON.forEach( ( { name }, idx ) => { - expect( name ).toBe( productNamesMinPriceAsc[ idx ] ); - } ); - - const productNamesMaxPriceDesc = [ - 'Sunglasses xxx', - 'Belt xxx', - 'Hoodie xxx', - 'Logo Collection xxx', - 'Hoodie with Logo xxx', - 'Hoodie with Zipper xxx', - 'Hoodie with Pocket xxx', - 'Long Sleeve Tee xxx', - 'V-Neck T-Shirt xxx', - 'Polo xxx', - 'T-Shirt xxx', - 'Beanie xxx', - 'T-Shirt with Logo xxx', - 'Beanie with Logo xxx', - 'Cap xxx', - 'Album xxx', - 'WordPress Pennant xxx', - 'Single xxx', - 'Child Product xxx', - 'Parent Product xxx', - ]; - - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'desc', - orderby: 'price', - per_page: productNamesMaxPriceDesc.length, - search: 'xxx', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toHaveLength( - productNamesMaxPriceDesc.length - ); - - // Verify all results are in descending order. - // The query uses the max price calculated in the product meta lookup table, - // so we can't just check the price property of the response. - result2JSON.forEach( ( { name }, idx ) => { - expect( name ).toBe( productNamesMaxPriceDesc[ idx ] ); - } ); - } ); - - test( 'include', async ( { request } ) => { - const includeIds = [ - sampleData.groupedProducts[ 0 ].id, - sampleData.simpleProducts[ 3 ].id, - sampleData.hierarchicalProducts.parentJSON.id, - ]; - - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'asc', - orderby: 'include', - include: includeIds.join( ',' ), - }, - } ); - const result1JSON = await result1.json(); - - expect( result1.status() ).toEqual( 200 ); - expect( result1JSON ).toHaveLength( includeIds.length ); - - // Verify all results are in proper order. - result1JSON.forEach( ( { id }, idx ) => { - expect( id ).toBe( includeIds[ idx ] ); - } ); - - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'desc', - orderby: 'include', - include: includeIds.join( ',' ), - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - expect( result2JSON ).toHaveLength( includeIds.length ); - - // Verify all results are in proper order. - result2JSON.forEach( ( { id }, idx ) => { - expect( id ).toBe( includeIds[ idx ] ); - } ); - } ); - - test( 'rating (desc)', async ( { request } ) => { - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'desc', - orderby: 'rating', - per_page: productNamesByRatingDesc.length, - search: 'xxx', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - - // Verify all results are in descending order. - result2JSON.forEach( ( { name }, idx ) => { - expect( name ).toBe( productNamesByRatingDesc[ idx ] ); - } ); - } ); - - // This case will remain skipped until ratings can be sorted ascending. - // See: https://github.com/woocommerce/woocommerce/issues/30354#issuecomment-925955099. - test.skip( 'rating (asc)', async ( { request } ) => { - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'asc', - orderby: 'rating', - per_page: productNamesByRatingAsc.length, - search: 'xxx', - }, - } ); - expect( result1.status() ).toEqual( 200 ); - const result1JSON = await result1.json(); - - // Verify all results are in ascending order. - result1JSON.forEach( ( { name }, idx ) => { - expect( name ).toBe( productNamesByRatingAsc[ idx ] ); - } ); - } ); - - // This case will remain skipped until popularity can be sorted ascending. - // See: https://github.com/woocommerce/woocommerce/issues/30354#issuecomment-925955099. - test.skip( 'popularity (asc)', async ( { request } ) => { - const result1 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'asc', - orderby: 'popularity', - per_page: productNamesByPopularityAsc.length, - search: 'xxx', - }, - } ); - const result1JSON = await result1.json(); - expect( result1.status() ).toEqual( 200 ); - - // Verify all results are in ascending order. - result1JSON.forEach( ( { name }, idx ) => { - expect( name ).toBe( productNamesByPopularityAsc[ idx ] ); - } ); - } ); - - test( 'popularity (desc)', async ( { request } ) => { - const result2 = await request.get( 'wp-json/wc/v3/products', { - params: { - order: 'desc', - orderby: 'popularity', - per_page: productNamesByPopularityDesc.length, - search: 'xxx', - }, - } ); - const result2JSON = await result2.json(); - expect( result2.status() ).toEqual( 200 ); - - // Verify all results are in descending order. - result2JSON.forEach( ( { name }, idx ) => { - expect( name ).toBe( productNamesByPopularityDesc[ idx ] ); - } ); - } ); - } ); - } ); + } + ); } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/products-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/products-crud.test.js index 64bf5072cfd..cbb39c3aed7 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/products-crud.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/products-crud.test.js @@ -589,278 +589,301 @@ test.describe( 'Products API tests: CRUD', () => { } ); } ); - test.describe( 'Product review tests: CRUD', () => { - let productReviewId; - let reviewsTestProduct; + test.describe( + 'Product review tests: CRUD', + { tag: '@skip-on-default-wpcom' }, + () => { + let productReviewId; + let reviewsTestProduct; - test.beforeAll( async ( { simpleTestProduct } ) => { - reviewsTestProduct = simpleTestProduct; - } ); - - test( 'can add a product review', async ( { request } ) => { - const response = await request.post( - 'wp-json/wc/v3/products/reviews', - { - data: { - product_id: reviewsTestProduct.id, - review: 'Nice simple product!', - reviewer: 'John Doe', - reviewer_email: 'john.doe@example.com', - rating: 5, - }, - } - ); - const responseJSON = await response.json(); - productReviewId = responseJSON.id; - - expect( response.status() ).toEqual( 201 ); - expect( typeof productReviewId ).toEqual( 'number' ); - expect( responseJSON.id ).toEqual( productReviewId ); - expect( responseJSON.product_name ).toEqual( 'A Simple Product' ); - expect( responseJSON.status ).toEqual( 'approved' ); - expect( responseJSON.reviewer ).toEqual( 'John Doe' ); - expect( responseJSON.reviewer_email ).toEqual( - 'john.doe@example.com' - ); - expect( responseJSON.review ).toEqual( 'Nice simple product!' ); - expect( responseJSON.rating ).toEqual( 5 ); - expect( responseJSON.verified ).toEqual( false ); - } ); - - test( 'cannot add a product review with invalid product_id', async ( { - request, - } ) => { - const response = await request.post( - 'wp-json/wc/v3/products/reviews', - { - data: { - product_id: 999, - review: 'A non existent product!', - reviewer: 'John Do Not', - reviewer_email: 'john.do.not@example.com', - rating: 5, - }, - } - ); - const responseJSON = await response.json(); - - expect( response.status() ).toEqual( 404 ); - expect( responseJSON.code ).toEqual( - 'woocommerce_rest_product_invalid_id' - ); - expect( responseJSON.message ).toEqual( 'Invalid product ID.' ); - } ); - - test( 'cannot add a duplicate product review', async ( { - request, - } ) => { - const response = await request.post( - 'wp-json/wc/v3/products/reviews', - { - data: { - product_id: reviewsTestProduct.id, - review: 'Nice simple product!', - reviewer: 'John Doe', - reviewer_email: 'john.doe@example.com', - rating: 5, - }, - } - ); - const responseJSON = await response.json(); - - expect( response.status() ).toEqual( 409 ); - expect( responseJSON.code ).toEqual( - 'woocommerce_rest_comment_duplicate' - ); - expect( responseJSON.message ).toEqual( - 'Duplicate comment detected; it looks as though you’ve already said that!' - ); - } ); - - test( 'can retrieve a product review', async ( { request } ) => { - const response = await request.get( - `wp-json/wc/v3/products/reviews/${ productReviewId }` - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.id ).toEqual( productReviewId ); - expect( responseJSON.product_id ).toEqual( reviewsTestProduct.id ); - expect( responseJSON.product_name ).toEqual( 'A Simple Product' ); - expect( responseJSON.status ).toEqual( 'approved' ); - expect( responseJSON.reviewer ).toEqual( 'John Doe' ); - expect( responseJSON.reviewer_email ).toEqual( - 'john.doe@example.com' - ); - expect( responseJSON.review ).toEqual( - '

    Nice simple product!

    \n' - ); - expect( responseJSON.rating ).toEqual( 5 ); - expect( responseJSON.verified ).toEqual( false ); - } ); - - test( 'can retrieve all product reviews', async ( { request } ) => { - // call API to retrieve all product reviews - const response = await request.get( - '/wp-json/wc/v3/products/reviews' - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); - expect( responseJSON.length ).toBeGreaterThan( 0 ); - } ); - - test( 'can update a product review', async ( { request } ) => { - // call API to retrieve all product reviews - const response = await request.put( - `wp-json/wc/v3/products/reviews/${ productReviewId }`, - { - data: { - rating: 1, - }, - } - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.id ).toEqual( productReviewId ); - expect( responseJSON.product_id ).toEqual( reviewsTestProduct.id ); - expect( responseJSON.product_name ).toEqual( 'A Simple Product' ); - expect( responseJSON.status ).toEqual( 'approved' ); - expect( responseJSON.reviewer ).toEqual( 'John Doe' ); - expect( responseJSON.reviewer_email ).toEqual( - 'john.doe@example.com' - ); - expect( responseJSON.review ).toEqual( 'Nice simple product!' ); - expect( responseJSON.rating ).toEqual( 1 ); - expect( responseJSON.verified ).toEqual( false ); - } ); - - test( 'can permanently delete a product review', async ( { - request, - } ) => { - // Delete the product review. - const response = await request.delete( - `wp-json/wc/v3/products/reviews/${ productReviewId }`, - { - data: { - force: true, - }, - } - ); - expect( response.status() ).toEqual( 200 ); - - // Verify that the product review can no longer be retrieved. - const getDeletedProductReviewResponse = await request.get( - `wp-json/wc/v3/products/reviews/${ productReviewId }` - ); - expect( getDeletedProductReviewResponse.status() ).toEqual( 404 ); - } ); - - test( 'can batch update product reviews', async ( { request } ) => { - // Batch create product reviews. - const response = await request.post( - `wp-json/wc/v3/products/reviews/batch`, - { - data: { - create: [ - { - product_id: reviewsTestProduct.id, - review: 'Nice product!', - reviewer: 'John Doe', - reviewer_email: 'john.doe@example.com', - rating: 4, - }, - { - product_id: reviewsTestProduct.id, - review: 'I love this thing!', - reviewer: 'Jane Doe', - reviewer_email: 'Jane.doe@example.com', - rating: 5, - }, - ], - }, - } - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.create[ 0 ].product_id ).toEqual( - reviewsTestProduct.id - ); - expect( responseJSON.create[ 0 ].review ).toEqual( - 'Nice product!' - ); - expect( responseJSON.create[ 0 ].reviewer ).toEqual( 'John Doe' ); - expect( responseJSON.create[ 0 ].reviewer_email ).toEqual( - 'john.doe@example.com' - ); - expect( responseJSON.create[ 0 ].rating ).toEqual( 4 ); - - expect( responseJSON.create[ 1 ].product_id ).toEqual( - reviewsTestProduct.id - ); - expect( responseJSON.create[ 1 ].review ).toEqual( - 'I love this thing!' - ); - expect( responseJSON.create[ 1 ].reviewer ).toEqual( 'Jane Doe' ); - expect( responseJSON.create[ 1 ].reviewer_email ).toEqual( - 'Jane.doe@example.com' - ); - expect( responseJSON.create[ 1 ].rating ).toEqual( 5 ); - const review1Id = responseJSON.create[ 0 ].id; - const review2Id = responseJSON.create[ 1 ].id; - - // Batch create a new review, update a review and delete another. - const responseBatchUpdate = await request.post( - `wp-json/wc/v3/products/reviews/batch`, - { - data: { - create: [ - { - product_id: reviewsTestProduct.id, - review: 'Ok product.', - reviewer: 'Jack Doe', - reviewer_email: 'jack.doe@example.com', - rating: 3, - }, - ], - update: [ - { - id: review1Id, - review: 'On reflection, I hate this thing!', - rating: 1, - }, - ], - delete: [ review2Id ], - }, - } - ); - const responseBatchUpdateJSON = await responseBatchUpdate.json(); - const review3Id = responseBatchUpdateJSON.create[ 0 ].id; - expect( response.status() ).toEqual( 200 ); - - const responseUpdatedReview = await request.get( - `wp-json/wc/v3/products/reviews/${ review1Id }` - ); - const responseUpdatedReviewJSON = - await responseUpdatedReview.json(); - expect( responseUpdatedReviewJSON.review ).toEqual( - '

    On reflection, I hate this thing!

    \n' - ); - expect( responseUpdatedReviewJSON.rating ).toEqual( 1 ); - - // Verify that the deleted review can no longer be retrieved. - const getDeletedProductReviewResponse = await request.get( - `wp-json/wc/v3/products/reviews/${ review2Id }` - ); - expect( getDeletedProductReviewResponse.status() ).toEqual( 404 ); - - // Batch delete the created tags - await request.post( `wp-json/wc/v3/products/reviews/batch`, { - data: { - delete: [ review1Id, review3Id ], - }, + test.beforeAll( async ( { simpleTestProduct } ) => { + reviewsTestProduct = simpleTestProduct; } ); - } ); - } ); + + test( 'can add a product review', async ( { request } ) => { + const response = await request.post( + 'wp-json/wc/v3/products/reviews', + { + data: { + product_id: reviewsTestProduct.id, + review: 'Nice simple product!', + reviewer: 'John Doe', + reviewer_email: 'john.doe@example.com', + rating: 5, + }, + } + ); + const responseJSON = await response.json(); + productReviewId = responseJSON.id; + + expect( response.status() ).toEqual( 201 ); + expect( typeof productReviewId ).toEqual( 'number' ); + expect( responseJSON.id ).toEqual( productReviewId ); + expect( responseJSON.product_name ).toEqual( + 'A Simple Product' + ); + expect( responseJSON.status ).toEqual( 'approved' ); + expect( responseJSON.reviewer ).toEqual( 'John Doe' ); + expect( responseJSON.reviewer_email ).toEqual( + 'john.doe@example.com' + ); + expect( responseJSON.review ).toEqual( 'Nice simple product!' ); + expect( responseJSON.rating ).toEqual( 5 ); + expect( responseJSON.verified ).toEqual( false ); + } ); + + test( 'cannot add a product review with invalid product_id', async ( { + request, + } ) => { + const response = await request.post( + 'wp-json/wc/v3/products/reviews', + { + data: { + product_id: 999, + review: 'A non existent product!', + reviewer: 'John Do Not', + reviewer_email: 'john.do.not@example.com', + rating: 5, + }, + } + ); + const responseJSON = await response.json(); + + expect( response.status() ).toEqual( 404 ); + expect( responseJSON.code ).toEqual( + 'woocommerce_rest_product_invalid_id' + ); + expect( responseJSON.message ).toEqual( 'Invalid product ID.' ); + } ); + + test( 'cannot add a duplicate product review', async ( { + request, + } ) => { + const response = await request.post( + 'wp-json/wc/v3/products/reviews', + { + data: { + product_id: reviewsTestProduct.id, + review: 'Nice simple product!', + reviewer: 'John Doe', + reviewer_email: 'john.doe@example.com', + rating: 5, + }, + } + ); + const responseJSON = await response.json(); + + expect( response.status() ).toEqual( 409 ); + expect( responseJSON.code ).toEqual( + 'woocommerce_rest_comment_duplicate' + ); + expect( responseJSON.message ).toEqual( + 'Duplicate comment detected; it looks as though you’ve already said that!' + ); + } ); + + test( 'can retrieve a product review', async ( { request } ) => { + const response = await request.get( + `wp-json/wc/v3/products/reviews/${ productReviewId }` + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.id ).toEqual( productReviewId ); + expect( responseJSON.product_id ).toEqual( + reviewsTestProduct.id + ); + expect( responseJSON.product_name ).toEqual( + 'A Simple Product' + ); + expect( responseJSON.status ).toEqual( 'approved' ); + expect( responseJSON.reviewer ).toEqual( 'John Doe' ); + expect( responseJSON.reviewer_email ).toEqual( + 'john.doe@example.com' + ); + expect( responseJSON.review ).toEqual( + '

    Nice simple product!

    \n' + ); + expect( responseJSON.rating ).toEqual( 5 ); + expect( responseJSON.verified ).toEqual( false ); + } ); + + test( 'can retrieve all product reviews', async ( { request } ) => { + // call API to retrieve all product reviews + const response = await request.get( + '/wp-json/wc/v3/products/reviews' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); + expect( responseJSON.length ).toBeGreaterThan( 0 ); + } ); + + test( 'can update a product review', async ( { request } ) => { + // call API to retrieve all product reviews + const response = await request.put( + `wp-json/wc/v3/products/reviews/${ productReviewId }`, + { + data: { + rating: 1, + }, + } + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.id ).toEqual( productReviewId ); + expect( responseJSON.product_id ).toEqual( + reviewsTestProduct.id + ); + expect( responseJSON.product_name ).toEqual( + 'A Simple Product' + ); + expect( responseJSON.status ).toEqual( 'approved' ); + expect( responseJSON.reviewer ).toEqual( 'John Doe' ); + expect( responseJSON.reviewer_email ).toEqual( + 'john.doe@example.com' + ); + expect( responseJSON.review ).toEqual( 'Nice simple product!' ); + expect( responseJSON.rating ).toEqual( 1 ); + expect( responseJSON.verified ).toEqual( false ); + } ); + + test( 'can permanently delete a product review', async ( { + request, + } ) => { + // Delete the product review. + const response = await request.delete( + `wp-json/wc/v3/products/reviews/${ productReviewId }`, + { + data: { + force: true, + }, + } + ); + expect( response.status() ).toEqual( 200 ); + + // Verify that the product review can no longer be retrieved. + const getDeletedProductReviewResponse = await request.get( + `wp-json/wc/v3/products/reviews/${ productReviewId }` + ); + expect( getDeletedProductReviewResponse.status() ).toEqual( + 404 + ); + } ); + + test( 'can batch update product reviews', async ( { request } ) => { + // Batch create product reviews. + const response = await request.post( + `wp-json/wc/v3/products/reviews/batch`, + { + data: { + create: [ + { + product_id: reviewsTestProduct.id, + review: 'Nice product!', + reviewer: 'John Doe', + reviewer_email: 'john.doe@example.com', + rating: 4, + }, + { + product_id: reviewsTestProduct.id, + review: 'I love this thing!', + reviewer: 'Jane Doe', + reviewer_email: 'Jane.doe@example.com', + rating: 5, + }, + ], + }, + } + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.create[ 0 ].product_id ).toEqual( + reviewsTestProduct.id + ); + expect( responseJSON.create[ 0 ].review ).toEqual( + 'Nice product!' + ); + expect( responseJSON.create[ 0 ].reviewer ).toEqual( + 'John Doe' + ); + expect( responseJSON.create[ 0 ].reviewer_email ).toEqual( + 'john.doe@example.com' + ); + expect( responseJSON.create[ 0 ].rating ).toEqual( 4 ); + + expect( responseJSON.create[ 1 ].product_id ).toEqual( + reviewsTestProduct.id + ); + expect( responseJSON.create[ 1 ].review ).toEqual( + 'I love this thing!' + ); + expect( responseJSON.create[ 1 ].reviewer ).toEqual( + 'Jane Doe' + ); + expect( responseJSON.create[ 1 ].reviewer_email ).toEqual( + 'Jane.doe@example.com' + ); + expect( responseJSON.create[ 1 ].rating ).toEqual( 5 ); + const review1Id = responseJSON.create[ 0 ].id; + const review2Id = responseJSON.create[ 1 ].id; + + // Batch create a new review, update a review and delete another. + const responseBatchUpdate = await request.post( + `wp-json/wc/v3/products/reviews/batch`, + { + data: { + create: [ + { + product_id: reviewsTestProduct.id, + review: 'Ok product.', + reviewer: 'Jack Doe', + reviewer_email: 'jack.doe@example.com', + rating: 3, + }, + ], + update: [ + { + id: review1Id, + review: 'On reflection, I hate this thing!', + rating: 1, + }, + ], + delete: [ review2Id ], + }, + } + ); + const responseBatchUpdateJSON = + await responseBatchUpdate.json(); + const review3Id = responseBatchUpdateJSON.create[ 0 ].id; + expect( response.status() ).toEqual( 200 ); + + const responseUpdatedReview = await request.get( + `wp-json/wc/v3/products/reviews/${ review1Id }` + ); + const responseUpdatedReviewJSON = + await responseUpdatedReview.json(); + expect( responseUpdatedReviewJSON.review ).toEqual( + '

    On reflection, I hate this thing!

    \n' + ); + expect( responseUpdatedReviewJSON.rating ).toEqual( 1 ); + + // Verify that the deleted review can no longer be retrieved. + const getDeletedProductReviewResponse = await request.get( + `wp-json/wc/v3/products/reviews/${ review2Id }` + ); + expect( getDeletedProductReviewResponse.status() ).toEqual( + 404 + ); + + // Batch delete the created tags + await request.post( `wp-json/wc/v3/products/reviews/batch`, { + data: { + delete: [ review1Id, review3Id ], + }, + } ); + } ); + } + ); test.describe( 'Product shipping classes tests: CRUD', () => { let productShippingClassId; @@ -958,83 +981,91 @@ test.describe( 'Products API tests: CRUD', () => { ); } ); - test( 'can batch update product shipping classes', async ( { - request, - } ) => { - // Batch create product shipping classes. - const response = await request.post( - `wp-json/wc/v3/products/shipping_classes/batch`, - { - data: { - create: [ - { - name: 'Small Items', - }, - { - name: 'Large Items', - }, - ], - }, - } - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.create[ 0 ].name ).toEqual( 'Small Items' ); - expect( responseJSON.create[ 1 ].name ).toEqual( 'Large Items' ); - const shippingClass1Id = responseJSON.create[ 0 ].id; - const shippingClass2Id = responseJSON.create[ 1 ].id; + test( + 'can batch update product shipping classes', + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, + async ( { request } ) => { + // Batch create product shipping classes. + const response = await request.post( + `wp-json/wc/v3/products/shipping_classes/batch`, + { + data: { + create: [ + { + name: 'Small Items', + }, + { + name: 'Large Items', + }, + ], + }, + } + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.create[ 0 ].name ).toEqual( + 'Small Items' + ); + expect( responseJSON.create[ 1 ].name ).toEqual( + 'Large Items' + ); + const shippingClass1Id = responseJSON.create[ 0 ].id; + const shippingClass2Id = responseJSON.create[ 1 ].id; - // Batch create a new shipping class, update a shipping class and delete another. - const responseBatchUpdate = await request.post( - `wp-json/wc/v3/products/shipping_classes/batch`, - { - data: { - create: [ - { - name: 'Express', - }, - ], - update: [ - { - id: shippingClass1Id, - description: 'Priority shipping.', - }, - ], - delete: [ shippingClass2Id ], - }, - } - ); - const responseBatchUpdateJSON = await responseBatchUpdate.json(); - const shippingClass3Id = responseBatchUpdateJSON.create[ 0 ].id; - expect( response.status() ).toEqual( 200 ); + // Batch create a new shipping class, update a shipping class and delete another. + const responseBatchUpdate = await request.post( + `wp-json/wc/v3/products/shipping_classes/batch`, + { + data: { + create: [ + { + name: 'Express', + }, + ], + update: [ + { + id: shippingClass1Id, + description: 'Priority shipping.', + }, + ], + delete: [ shippingClass2Id ], + }, + } + ); + const responseBatchUpdateJSON = + await responseBatchUpdate.json(); + const shippingClass3Id = responseBatchUpdateJSON.create[ 0 ].id; + expect( response.status() ).toEqual( 200 ); - const responseUpdatedShippingClass = await request.get( - `wp-json/wc/v3/products/shipping_classes/${ shippingClass1Id }` - ); - const responseUpdatedShippingClassJSON = - await responseUpdatedShippingClass.json(); - expect( responseUpdatedShippingClassJSON.description ).toEqual( - 'Priority shipping.' - ); + const responseUpdatedShippingClass = await request.get( + `wp-json/wc/v3/products/shipping_classes/${ shippingClass1Id }` + ); + const responseUpdatedShippingClassJSON = + await responseUpdatedShippingClass.json(); + expect( responseUpdatedShippingClassJSON.description ).toEqual( + 'Priority shipping.' + ); - // Verify that the product tag can no longer be retrieved. - const getDeletedProductShippingClassResponse = await request.get( - `wp-json/wc/v3/products/shipping_classes/${ shippingClass2Id }` - ); - expect( getDeletedProductShippingClassResponse.status() ).toEqual( - 404 - ); + // Verify that the product tag can no longer be retrieved. + const getDeletedProductShippingClassResponse = + await request.get( + `wp-json/wc/v3/products/shipping_classes/${ shippingClass2Id }` + ); + expect( + getDeletedProductShippingClassResponse.status() + ).toEqual( 404 ); - // Batch delete the created tags - await request.post( - `wp-json/wc/v3/products/shipping_classes/batch`, - { - data: { - delete: [ shippingClass1Id, shippingClass3Id ], - }, - } - ); - } ); + // Batch delete the created tags + await request.post( + `wp-json/wc/v3/products/shipping_classes/batch`, + { + data: { + delete: [ shippingClass1Id, shippingClass3Id ], + }, + } + ); + } + ); } ); test.describe( 'Product tags tests: CRUD', () => { diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/settings/settings-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/settings/settings-crud.test.js index 376d606c745..e7caee7e0b4 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/settings/settings-crud.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/settings/settings-crud.test.js @@ -280,7 +280,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => { type: 'text', default: '', tip: 'The street address for your business location.', - value: '', + value: expect.any( String ), } ), ] ) ); @@ -310,7 +310,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => { type: 'text', default: '', tip: 'The city in which your business is located.', - value: '', + value: expect.any( String ), } ), ] ) ); @@ -341,7 +341,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => { type: 'text', default: '', tip: 'The postal code, if any, in which your business is located.', - value: '', + value: expect.any( String ), } ), ] ) ); @@ -1019,157 +1019,164 @@ test.describe.serial( 'Settings API tests: CRUD', () => { } ); test.describe( 'List all Tax settings options', () => { - test( 'can retrieve all tax settings', async ( { request } ) => { - // call API to retrieve all settings options - const response = await request.get( '/wp-json/wc/v3/settings/tax' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); - expect( responseJSON.length ).toBeGreaterThan( 0 ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_prices_include_tax', - label: 'Prices entered with tax', - description: '', - type: 'radio', - default: 'no', - options: { - yes: 'Yes, I will enter prices inclusive of tax', - no: 'No, I will enter prices exclusive of tax', - }, - tip: 'This option is important as it will affect how you input prices. Changing it will not update existing products.', - value: 'no', - } ), - ] ) - ); + test( + 'can retrieve all tax settings', + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, + async ( { request } ) => { + // call API to retrieve all settings options + const response = await request.get( + '/wp-json/wc/v3/settings/tax' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); + expect( responseJSON.length ).toBeGreaterThan( 0 ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_prices_include_tax', + label: 'Prices entered with tax', + description: '', + type: 'radio', + default: 'no', + options: { + yes: 'Yes, I will enter prices inclusive of tax', + no: 'No, I will enter prices exclusive of tax', + }, + tip: 'This option is important as it will affect how you input prices. Changing it will not update existing products.', + value: 'no', + } ), + ] ) + ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_tax_based_on', - label: 'Calculate tax based on', - description: '', - type: 'select', - default: 'shipping', - options: { - shipping: 'Customer shipping address', - billing: 'Customer billing address', - base: 'Shop base address', - }, - tip: 'This option determines which address is used to calculate tax.', - value: 'shipping', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_shipping_tax_class', - label: 'Shipping tax class', - description: - 'Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.', - type: 'select', - default: 'inherit', - options: { - inherit: 'Shipping tax class based on cart items', - '': 'Standard', - 'reduced-rate': 'Reduced rate', - 'zero-rate': 'Zero rate', - }, - tip: 'Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.', - value: 'inherit', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_tax_round_at_subtotal', - label: 'Rounding', - description: - 'Round tax at subtotal level, instead of rounding per line', - type: 'checkbox', - default: 'no', - value: 'no', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_tax_classes', - label: 'Additional tax classes', - description: '', - type: 'textarea', - default: '', - tip: 'List additional tax classes you need below (1 per line, e.g. Reduced Rates). These are in addition to "Standard rate" which exists by default.', - value: '', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_tax_display_shop', - label: 'Display prices in the shop', - description: '', - type: 'select', - default: 'excl', - options: { - incl: 'Including tax', - excl: 'Excluding tax', - }, - value: 'excl', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_tax_display_cart', - label: 'Display prices during cart and checkout', - description: '', - type: 'select', - default: 'excl', - options: { - incl: 'Including tax', - excl: 'Excluding tax', - }, - value: 'excl', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_price_display_suffix', - label: 'Price display suffix', - description: '', - type: 'text', - default: '', - tip: 'Define text to show after your product prices. This could be, for example, "inc. Vat" to explain your pricing. You can also have prices substituted here using one of the following: {price_including_tax}, {price_excluding_tax}.', - value: '', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_tax_total_display', - label: 'Display tax totals', - description: '', - type: 'select', - default: 'itemized', - options: { - single: 'As a single total', - itemized: 'Itemized', - }, - value: 'itemized', - } ), - ] ) - ); - } ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_tax_based_on', + label: 'Calculate tax based on', + description: '', + type: 'select', + default: 'shipping', + options: { + shipping: 'Customer shipping address', + billing: 'Customer billing address', + base: 'Shop base address', + }, + tip: 'This option determines which address is used to calculate tax.', + value: 'shipping', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_shipping_tax_class', + label: 'Shipping tax class', + description: + 'Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.', + type: 'select', + default: 'inherit', + options: { + inherit: + 'Shipping tax class based on cart items', + '': 'Standard', + 'reduced-rate': 'Reduced rate', + 'zero-rate': 'Zero rate', + }, + tip: 'Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.', + value: 'inherit', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_tax_round_at_subtotal', + label: 'Rounding', + description: + 'Round tax at subtotal level, instead of rounding per line', + type: 'checkbox', + default: 'no', + value: 'no', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_tax_classes', + label: 'Additional tax classes', + description: '', + type: 'textarea', + default: '', + tip: 'List additional tax classes you need below (1 per line, e.g. Reduced Rates). These are in addition to "Standard rate" which exists by default.', + value: '', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_tax_display_shop', + label: 'Display prices in the shop', + description: '', + type: 'select', + default: 'excl', + options: { + incl: 'Including tax', + excl: 'Excluding tax', + }, + value: 'excl', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_tax_display_cart', + label: 'Display prices during cart and checkout', + description: '', + type: 'select', + default: 'excl', + options: { + incl: 'Including tax', + excl: 'Excluding tax', + }, + value: 'excl', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_price_display_suffix', + label: 'Price display suffix', + description: '', + type: 'text', + default: '', + tip: 'Define text to show after your product prices. This could be, for example, "inc. Vat" to explain your pricing. You can also have prices substituted here using one of the following: {price_including_tax}, {price_excluding_tax}.', + value: '', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_tax_total_display', + label: 'Display tax totals', + description: '', + type: 'select', + default: 'itemized', + options: { + single: 'As a single total', + itemized: 'Itemized', + }, + value: 'itemized', + } ), + ] ) + ); + } + ); } ); test.describe( 'List all Shipping settings options', () => { @@ -1613,286 +1620,294 @@ test.describe.serial( 'Settings API tests: CRUD', () => { } ); } ); - test.describe( 'List all Advanced settings options', () => { - test( 'can retrieve all advanced settings', async ( { request } ) => { - // call API to retrieve all settings options - const response = await request.get( - '/wp-json/wc/v3/settings/advanced' - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); + test.describe( + 'List all Advanced settings options', + { tag: '@skip-on-default-wpcom' }, + () => { + test( 'can retrieve all advanced settings', async ( { + request, + } ) => { + // call API to retrieve all settings options + const response = await request.get( + '/wp-json/wc/v3/settings/advanced' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); + + // not present in external host + // eslint-disable-next-line playwright/no-conditional-in-test + if ( ! shouldSkip ) { + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_cart_page_id', + label: 'Cart page', + description: + 'Page where shoppers review their shopping cart', + type: 'select', + default: '', + tip: 'Page where shoppers review their shopping cart', + value: expect.any( String ), + options: expect.any( Object ), + } ), + ] ) + ); + } + + // not present in external host + // eslint-disable-next-line playwright/no-conditional-in-test + if ( ! shouldSkip ) { + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_checkout_page_id', + label: 'Checkout page', + description: + 'Page where shoppers go to finalize their purchase', + type: 'select', + default: expect.any( Number ), + tip: 'Page where shoppers go to finalize their purchase', + value: expect.any( String ), + options: expect.any( Object ), + } ), + ] ) + ); + } - // not present in external host - // eslint-disable-next-line playwright/no-conditional-in-test - if ( ! shouldSkip ) { expect( responseJSON ).toEqual( expect.arrayContaining( [ expect.objectContaining( { - id: 'woocommerce_cart_page_id', - label: 'Cart page', + id: 'woocommerce_myaccount_page_id', + label: 'My account page', description: - 'Page where shoppers review their shopping cart', + 'Page contents: [woocommerce_my_account]', type: 'select', default: '', - tip: 'Page where shoppers review their shopping cart', + tip: 'Page contents: [woocommerce_my_account]', value: expect.any( String ), options: expect.any( Object ), } ), ] ) ); - } - - // not present in external host - // eslint-disable-next-line playwright/no-conditional-in-test - if ( ! shouldSkip ) { expect( responseJSON ).toEqual( expect.arrayContaining( [ expect.objectContaining( { - id: 'woocommerce_checkout_page_id', - label: 'Checkout page', + id: 'woocommerce_checkout_pay_endpoint', + label: 'Pay', description: - 'Page where shoppers go to finalize their purchase', - type: 'select', - default: expect.any( Number ), - tip: 'Page where shoppers go to finalize their purchase', - value: expect.any( String ), - options: expect.any( Object ), + 'Endpoint for the "Checkout → Pay" page.', + type: 'text', + default: 'order-pay', + tip: 'Endpoint for the "Checkout → Pay" page.', + value: 'order-pay', } ), ] ) ); - } - - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_page_id', - label: 'My account page', - description: 'Page contents: [woocommerce_my_account]', - type: 'select', - default: '', - tip: 'Page contents: [woocommerce_my_account]', - value: expect.any( String ), - options: expect.any( Object ), - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_checkout_pay_endpoint', - label: 'Pay', - description: - 'Endpoint for the "Checkout → Pay" page.', - type: 'text', - default: 'order-pay', - tip: 'Endpoint for the "Checkout → Pay" page.', - value: 'order-pay', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_checkout_order_received_endpoint', - label: 'Order received', - description: - 'Endpoint for the "Checkout → Order received" page.', - type: 'text', - default: 'order-received', - tip: 'Endpoint for the "Checkout → Order received" page.', - value: 'order-received', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_add_payment_method_endpoint', - label: 'Add payment method', - description: - 'Endpoint for the "Checkout → Add payment method" page.', - type: 'text', - default: 'add-payment-method', - tip: 'Endpoint for the "Checkout → Add payment method" page.', - value: 'add-payment-method', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_delete_payment_method_endpoint', - label: 'Delete payment method', - description: - 'Endpoint for the delete payment method page.', - type: 'text', - default: 'delete-payment-method', - tip: 'Endpoint for the delete payment method page.', - value: 'delete-payment-method', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_orders_endpoint', - label: 'Orders', - description: - 'Endpoint for the "My account → Orders" page.', - type: 'text', - default: 'orders', - tip: 'Endpoint for the "My account → Orders" page.', - value: 'orders', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_view_order_endpoint', - label: 'View order', - description: - 'Endpoint for the "My account → View order" page.', - type: 'text', - default: 'view-order', - tip: 'Endpoint for the "My account → View order" page.', - value: 'view-order', - } ), - ] ) - ); - - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_downloads_endpoint', - label: 'Downloads', - description: - 'Endpoint for the "My account → Downloads" page.', - type: 'text', - default: 'downloads', - tip: 'Endpoint for the "My account → Downloads" page.', - value: 'downloads', - } ), - ] ) - ); - - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_edit_account_endpoint', - label: 'Edit account', - description: - 'Endpoint for the "My account → Edit account" page.', - type: 'text', - default: 'edit-account', - tip: 'Endpoint for the "My account → Edit account" page.', - value: 'edit-account', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_edit_address_endpoint', - label: 'Addresses', - description: - 'Endpoint for the "My account → Addresses" page.', - type: 'text', - default: 'edit-address', - tip: 'Endpoint for the "My account → Addresses" page.', - value: 'edit-address', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_payment_methods_endpoint', - label: 'Payment methods', - description: - 'Endpoint for the "My account → Payment methods" page.', - type: 'text', - default: 'payment-methods', - tip: 'Endpoint for the "My account → Payment methods" page.', - value: 'payment-methods', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_lost_password_endpoint', - label: 'Lost password', - description: - 'Endpoint for the "My account → Lost password" page.', - type: 'text', - default: 'lost-password', - tip: 'Endpoint for the "My account → Lost password" page.', - value: 'lost-password', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_logout_endpoint', - label: 'Logout', - description: - 'Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true', - type: 'text', - default: 'customer-logout', - tip: 'Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true', - value: 'customer-logout', - } ), - ] ) - ); - // eslint-disable-next-line playwright/no-conditional-in-test - if ( ! shouldSkip ) { expect( responseJSON ).toEqual( expect.arrayContaining( [ expect.objectContaining( { - id: 'woocommerce_allow_tracking', - label: 'Enable tracking', + id: 'woocommerce_checkout_order_received_endpoint', + label: 'Order received', description: - 'Allow usage of WooCommerce to be tracked', + 'Endpoint for the "Checkout → Order received" page.', + type: 'text', + default: 'order-received', + tip: 'Endpoint for the "Checkout → Order received" page.', + value: 'order-received', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_add_payment_method_endpoint', + label: 'Add payment method', + description: + 'Endpoint for the "Checkout → Add payment method" page.', + type: 'text', + default: 'add-payment-method', + tip: 'Endpoint for the "Checkout → Add payment method" page.', + value: 'add-payment-method', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_delete_payment_method_endpoint', + label: 'Delete payment method', + description: + 'Endpoint for the delete payment method page.', + type: 'text', + default: 'delete-payment-method', + tip: 'Endpoint for the delete payment method page.', + value: 'delete-payment-method', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_orders_endpoint', + label: 'Orders', + description: + 'Endpoint for the "My account → Orders" page.', + type: 'text', + default: 'orders', + tip: 'Endpoint for the "My account → Orders" page.', + value: 'orders', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_view_order_endpoint', + label: 'View order', + description: + 'Endpoint for the "My account → View order" page.', + type: 'text', + default: 'view-order', + tip: 'Endpoint for the "My account → View order" page.', + value: 'view-order', + } ), + ] ) + ); + + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_downloads_endpoint', + label: 'Downloads', + description: + 'Endpoint for the "My account → Downloads" page.', + type: 'text', + default: 'downloads', + tip: 'Endpoint for the "My account → Downloads" page.', + value: 'downloads', + } ), + ] ) + ); + + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_edit_account_endpoint', + label: 'Edit account', + description: + 'Endpoint for the "My account → Edit account" page.', + type: 'text', + default: 'edit-account', + tip: 'Endpoint for the "My account → Edit account" page.', + value: 'edit-account', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_edit_address_endpoint', + label: 'Addresses', + description: + 'Endpoint for the "My account → Addresses" page.', + type: 'text', + default: 'edit-address', + tip: 'Endpoint for the "My account → Addresses" page.', + value: 'edit-address', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_payment_methods_endpoint', + label: 'Payment methods', + description: + 'Endpoint for the "My account → Payment methods" page.', + type: 'text', + default: 'payment-methods', + tip: 'Endpoint for the "My account → Payment methods" page.', + value: 'payment-methods', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_lost_password_endpoint', + label: 'Lost password', + description: + 'Endpoint for the "My account → Lost password" page.', + type: 'text', + default: 'lost-password', + tip: 'Endpoint for the "My account → Lost password" page.', + value: 'lost-password', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_logout_endpoint', + label: 'Logout', + description: + 'Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true', + type: 'text', + default: 'customer-logout', + tip: 'Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true', + value: 'customer-logout', + } ), + ] ) + ); + // eslint-disable-next-line playwright/no-conditional-in-test + if ( ! shouldSkip ) { + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_allow_tracking', + label: 'Enable tracking', + description: + 'Allow usage of WooCommerce to be tracked', + type: 'checkbox', + default: 'no', + tip: 'To opt out, leave this box unticked. Your store remains untracked, and no data will be collected. Read about what usage data is tracked at: WooCommerce.com Usage Tracking Documentation.', + value: 'no', + } ), + ] ) + ); + } else { + // Test is failing on external hosts + } + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_show_marketplace_suggestions', + label: 'Show Suggestions', + description: + 'Display suggestions within WooCommerce', type: 'checkbox', - default: 'no', - tip: 'To opt out, leave this box unticked. Your store remains untracked, and no data will be collected. Read about what usage data is tracked at: WooCommerce.com Usage Tracking Documentation.', - value: 'no', + default: 'yes', + tip: 'Leave this box unchecked if you do not want to pull suggested extensions from WooCommerce.com. You will see a static list of extensions instead.', + value: 'yes', } ), ] ) ); - } else { - // Test is failing on external hosts - } - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_show_marketplace_suggestions', - label: 'Show Suggestions', - description: 'Display suggestions within WooCommerce', - type: 'checkbox', - default: 'yes', - tip: 'Leave this box unchecked if you do not want to pull suggested extensions from WooCommerce.com. You will see a static list of extensions instead.', - value: 'yes', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_analytics_enabled', - label: 'Analytics', - description: 'Enable WooCommerce Analytics', - type: 'checkbox', - default: 'yes', - value: 'yes', - } ), - ] ) - ); - } ); - } ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_analytics_enabled', + label: 'Analytics', + description: 'Enable WooCommerce Analytics', + type: 'checkbox', + default: 'yes', + value: 'yes', + } ), + ] ) + ); + } ); + } + ); test.describe( 'List all Email New Order settings', () => { test( 'can retrieve all email new order settings', async ( { @@ -1930,7 +1945,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => { tip: expect.stringContaining( 'Enter recipients (comma separated) for this email. Defaults to' ), - value: '', + value: expect.any( String ), } ), ] ) ); @@ -1998,107 +2013,110 @@ test.describe.serial( 'Settings API tests: CRUD', () => { } ); test.describe( 'List all Email Failed Order settings', () => { - test( 'can retrieve all email failed order settings', async ( { - request, - } ) => { - // call API to retrieve all settings options - const response = await request.get( - '/wp-json/wc/v3/settings/email_failed_order' - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'enabled', - label: 'Enable/Disable', - description: '', - type: 'checkbox', - default: 'yes', - value: 'yes', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'recipient', - label: 'Recipient(s)', - description: expect.stringContaining( - 'Enter recipients (comma separated) for this email. Defaults to' - ), - type: 'text', - default: '', - tip: expect.stringContaining( - 'Enter recipients (comma separated) for this email. Defaults to' - ), - value: '', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'subject', - label: 'Subject', - description: - 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', - type: 'text', - default: '', - tip: 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', - value: '', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'heading', - label: 'Email heading', - description: - 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', - type: 'text', - default: '', - tip: 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', - value: '', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'additional_content', - label: 'Additional content', - description: - 'Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', - type: 'textarea', - default: - 'Hopefully they’ll be back. Read more about troubleshooting failed payments.', - tip: 'Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', - value: 'Hopefully they’ll be back. Read more about troubleshooting failed payments.', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'email_type', - label: 'Email type', - description: 'Choose which format of email to send.', - type: 'select', - default: 'html', - options: { - plain: 'Plain text', - html: 'HTML', - multipart: 'Multipart', - }, - tip: 'Choose which format of email to send.', - value: 'html', - } ), - ] ) - ); - } ); + test( + 'can retrieve all email failed order settings', + { tag: '@skip-on-default-pressable' }, + async ( { request } ) => { + // call API to retrieve all settings options + const response = await request.get( + '/wp-json/wc/v3/settings/email_failed_order' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'enabled', + label: 'Enable/Disable', + description: '', + type: 'checkbox', + default: 'yes', + value: 'yes', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'recipient', + label: 'Recipient(s)', + description: expect.stringContaining( + 'Enter recipients (comma separated) for this email. Defaults to' + ), + type: 'text', + default: '', + tip: expect.stringContaining( + 'Enter recipients (comma separated) for this email. Defaults to' + ), + value: '', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'subject', + label: 'Subject', + description: + 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', + type: 'text', + default: '', + tip: 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', + value: '', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'heading', + label: 'Email heading', + description: + 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', + type: 'text', + default: '', + tip: 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', + value: '', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'additional_content', + label: 'Additional content', + description: + 'Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', + type: 'textarea', + default: + 'Hopefully they’ll be back. Read more about troubleshooting failed payments.', + tip: 'Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', + value: 'Hopefully they’ll be back. Read more about troubleshooting failed payments.', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'email_type', + label: 'Email type', + description: + 'Choose which format of email to send.', + type: 'select', + default: 'html', + options: { + plain: 'Plain text', + html: 'HTML', + multipart: 'Multipart', + }, + tip: 'Choose which format of email to send.', + value: 'html', + } ), + ] ) + ); + } + ); } ); test.describe( 'List all Email Customer On Hold Order settings', () => { diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/system-status/system-status-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/system-status/system-status-crud.test.js index 65d56a07196..005c48a99cd 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/system-status/system-status-crud.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/system-status/system-status-crud.test.js @@ -3,582 +3,590 @@ const { BASE_URL } = process.env; const shouldSkip = BASE_URL !== undefined && ! BASE_URL.includes( 'localhost' ); test.describe( 'System Status API tests', () => { - test( 'can view all system status items', async ( { request } ) => { - // call API to view all system status items - const response = await request.get( '/wp-json/wc/v3/system_status' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); + test( + 'can view all system status items', + { tag: '@skip-on-default-wpcom' }, + async ( { request } ) => { + // call API to view all system status items + const response = await request.get( + '/wp-json/wc/v3/system_status' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + + // local environment differs from external hosts. Local listed first. + // eslint-disable-next-line playwright/no-conditional-in-test + if ( ! shouldSkip ) { + expect( responseJSON ).toEqual( + expect.objectContaining( { + environment: expect.objectContaining( { + home_url: expect.any( String ), + site_url: expect.any( String ), + version: expect.any( String ), + log_directory: expect.any( String ), + log_directory_writable: expect.any( Boolean ), + wp_version: expect.any( String ), + wp_multisite: expect.any( Boolean ), + wp_memory_limit: expect.any( Number ), + wp_debug_mode: expect.any( Boolean ), + wp_cron: expect.any( Boolean ), + language: expect.any( String ), + external_object_cache: null, + server_info: expect.any( String ), + php_version: expect.any( String ), + php_post_max_size: expect.any( Number ), + php_max_execution_time: expect.any( Number ), + php_max_input_vars: expect.any( Number ), + curl_version: expect.any( String ), + suhosin_installed: expect.any( Boolean ), + max_upload_size: expect.any( Number ), + mysql_version: expect.any( String ), + mysql_version_string: expect.any( String ), + default_timezone: expect.any( String ), + fsockopen_or_curl_enabled: expect.any( Boolean ), + soapclient_enabled: expect.any( Boolean ), + domdocument_enabled: expect.any( Boolean ), + gzip_enabled: expect.any( Boolean ), + mbstring_enabled: expect.any( Boolean ), + remote_post_successful: expect.any( Boolean ), + remote_post_response: expect.any( String ), + remote_get_successful: expect.any( Boolean ), + remote_get_response: expect.any( String ), + } ), + } ) + ); + } else { + expect( responseJSON ).toEqual( + expect.objectContaining( { + environment: expect.objectContaining( { + home_url: expect.any( String ), + site_url: expect.any( String ), + version: expect.any( String ), + log_directory: expect.any( String ), + log_directory_writable: expect.any( Boolean ), + wp_version: expect.any( String ), + wp_multisite: expect.any( Boolean ), + wp_memory_limit: expect.any( Number ), + wp_debug_mode: expect.any( Boolean ), + wp_cron: expect.any( Boolean ), + language: expect.any( String ), + external_object_cache: expect.any( Boolean ), + server_info: expect.any( String ), + php_version: expect.any( String ), + php_post_max_size: expect.any( Number ), + php_max_execution_time: expect.any( Number ), + php_max_input_vars: expect.any( Number ), + curl_version: expect.any( String ), + suhosin_installed: expect.any( Boolean ), + max_upload_size: expect.any( Number ), + mysql_version: expect.any( String ), + mysql_version_string: expect.any( String ), + default_timezone: expect.any( String ), + fsockopen_or_curl_enabled: expect.any( Boolean ), + soapclient_enabled: expect.any( Boolean ), + domdocument_enabled: expect.any( Boolean ), + gzip_enabled: expect.any( Boolean ), + mbstring_enabled: expect.any( Boolean ), + remote_post_successful: expect.any( Boolean ), + remote_post_response: expect.any( Number ), + remote_get_successful: expect.any( Boolean ), + remote_get_response: expect.any( Number ), + } ), + } ) + ); + } - // local environment differs from external hosts. Local listed first. - // eslint-disable-next-line playwright/no-conditional-in-test - if ( ! shouldSkip ) { expect( responseJSON ).toEqual( expect.objectContaining( { - environment: expect.objectContaining( { - home_url: expect.any( String ), - site_url: expect.any( String ), - version: expect.any( String ), - log_directory: expect.any( String ), - log_directory_writable: expect.any( Boolean ), - wp_version: expect.any( String ), - wp_multisite: expect.any( Boolean ), - wp_memory_limit: expect.any( Number ), - wp_debug_mode: expect.any( Boolean ), - wp_cron: expect.any( Boolean ), - language: expect.any( String ), - external_object_cache: null, - server_info: expect.any( String ), - php_version: expect.any( String ), - php_post_max_size: expect.any( Number ), - php_max_execution_time: expect.any( Number ), - php_max_input_vars: expect.any( Number ), - curl_version: expect.any( String ), - suhosin_installed: expect.any( Boolean ), - max_upload_size: expect.any( Number ), - mysql_version: expect.any( String ), - mysql_version_string: expect.any( String ), - default_timezone: expect.any( String ), - fsockopen_or_curl_enabled: expect.any( Boolean ), - soapclient_enabled: expect.any( Boolean ), - domdocument_enabled: expect.any( Boolean ), - gzip_enabled: expect.any( Boolean ), - mbstring_enabled: expect.any( Boolean ), - remote_post_successful: expect.any( Boolean ), - remote_post_response: expect.any( String ), - remote_get_successful: expect.any( Boolean ), - remote_get_response: expect.any( String ), - } ), - } ) - ); - } else { - expect( responseJSON ).toEqual( - expect.objectContaining( { - environment: expect.objectContaining( { - home_url: expect.any( String ), - site_url: expect.any( String ), - version: expect.any( String ), - log_directory: expect.any( String ), - log_directory_writable: expect.any( Boolean ), - wp_version: expect.any( String ), - wp_multisite: expect.any( Boolean ), - wp_memory_limit: expect.any( Number ), - wp_debug_mode: expect.any( Boolean ), - wp_cron: expect.any( Boolean ), - language: expect.any( String ), - external_object_cache: expect.any( Boolean ), - server_info: expect.any( String ), - php_version: expect.any( String ), - php_post_max_size: expect.any( Number ), - php_max_execution_time: expect.any( Number ), - php_max_input_vars: expect.any( Number ), - curl_version: expect.any( String ), - suhosin_installed: expect.any( Boolean ), - max_upload_size: expect.any( Number ), - mysql_version: expect.any( String ), - mysql_version_string: expect.any( String ), - default_timezone: expect.any( String ), - fsockopen_or_curl_enabled: expect.any( Boolean ), - soapclient_enabled: expect.any( Boolean ), - domdocument_enabled: expect.any( Boolean ), - gzip_enabled: expect.any( Boolean ), - mbstring_enabled: expect.any( Boolean ), - remote_post_successful: expect.any( Boolean ), - remote_post_response: expect.any( Number ), - remote_get_successful: expect.any( Boolean ), - remote_get_response: expect.any( Number ), - } ), - } ) - ); - } - - expect( responseJSON ).toEqual( - expect.objectContaining( { - database: expect.objectContaining( { - wc_database_version: expect.any( String ), - database_prefix: expect.any( String ), - maxmind_geoip_database: expect.any( String ), - database_tables: expect.objectContaining( { - woocommerce: expect.objectContaining( { - wp_woocommerce_sessions: expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_api_keys: expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_attribute_taxonomies: - expect.objectContaining( { + database: expect.objectContaining( { + wc_database_version: expect.any( String ), + database_prefix: expect.any( String ), + maxmind_geoip_database: expect.any( String ), + database_tables: expect.objectContaining( { + woocommerce: expect.objectContaining( { + wp_woocommerce_sessions: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_api_keys: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_attribute_taxonomies: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_downloadable_product_permissions: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_order_items: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_order_itemmeta: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_tax_rates: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_tax_rate_locations: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_shipping_zones: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_shipping_zone_locations: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_shipping_zone_methods: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_payment_tokens: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_payment_tokenmeta: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_log: expect.objectContaining( { data: expect.any( String ), index: expect.any( String ), engine: expect.any( String ), } ), - wp_woocommerce_downloadable_product_permissions: - expect.objectContaining( { + } ), + other: expect.objectContaining( { + wp_actionscheduler_actions: { data: expect.any( String ), index: expect.any( String ), engine: expect.any( String ), - } ), - wp_woocommerce_order_items: expect.objectContaining( + }, + wp_actionscheduler_claims: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_actionscheduler_groups: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_actionscheduler_logs: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_commentmeta: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_comments: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_links: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_options: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_postmeta: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_posts: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_termmeta: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_terms: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_term_relationships: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_term_taxonomy: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_usermeta: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_users: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_admin_notes: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_admin_note_actions: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_category_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_customer_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_download_log: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_order_coupon_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_order_product_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_order_stats: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_order_tax_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_product_attributes_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_product_download_directories: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_product_meta_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_rate_limits: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_reserved_stock: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_tax_rate_classes: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_webhooks: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + } ), + } ), + database_size: { + data: expect.any( Number ), + index: expect.any( Number ), + }, + } ), + } ) + ); + + expect( responseJSON ).toEqual( + expect.objectContaining( { + active_plugins: expect.arrayContaining( [ + { + plugin: expect.any( String ), + name: expect.any( String ), + version: expect.any( String ), + version_latest: expect.any( String ), + url: expect.any( String ), + author_name: expect.any( String ), + author_url: expect.any( String ), + network_activated: expect.any( Boolean ), + }, + { + plugin: expect.any( String ), + name: expect.any( String ), + version: expect.any( String ), + version_latest: expect.any( String ), + url: expect.any( String ), + author_name: expect.any( String ), + author_url: expect.any( String ), + network_activated: expect.any( Boolean ), + }, + { + plugin: expect.any( String ), + name: expect.any( String ), + version: expect.any( String ), + version_latest: expect.any( String ), + url: expect.any( String ), + author_name: expect.any( String ), + author_url: expect.any( String ), + network_activated: expect.any( Boolean ), + }, + { + plugin: expect.any( String ), + name: expect.any( String ), + version: expect.any( String ), + version_latest: expect.any( String ), + url: expect.any( String ), + author_name: expect.any( String ), + author_url: expect.any( String ), + network_activated: expect.any( Boolean ), + }, + ] ), + } ) + ); + + // local environment differs from external hosts. Local listed first. + // eslint-disable-next-line playwright/no-conditional-in-test + if ( ! shouldSkip ) { + expect( responseJSON ).toEqual( + expect.objectContaining( { + dropins_mu_plugins: expect.objectContaining( { + dropins: expect.arrayContaining( [] ), + mu_plugins: expect.arrayContaining( [] ), + } ), + } ) + ); + } else { + expect( responseJSON ).toEqual( + expect.objectContaining( { + dropins_mu_plugins: expect.objectContaining( { + dropins: expect.arrayContaining( [ { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } - ), - wp_woocommerce_order_itemmeta: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_tax_rates: expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_tax_rate_locations: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_shipping_zones: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_shipping_zone_locations: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_shipping_zone_methods: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_payment_tokens: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_payment_tokenmeta: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_log: expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), + name: expect.any( String ), + plugin: expect.any( String ), + }, + { + name: expect.any( String ), + plugin: expect.any( String ), + }, + ] ), + mu_plugins: [], } ), - other: expect.objectContaining( { - wp_actionscheduler_actions: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_actionscheduler_claims: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_actionscheduler_groups: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_actionscheduler_logs: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_commentmeta: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_comments: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_links: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_options: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_postmeta: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_posts: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_termmeta: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_terms: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_term_relationships: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_term_taxonomy: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_usermeta: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_users: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_admin_notes: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_admin_note_actions: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_category_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_customer_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_download_log: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_order_coupon_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_order_product_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_order_stats: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_order_tax_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_product_attributes_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_product_download_directories: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_product_meta_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_rate_limits: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_reserved_stock: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_tax_rate_classes: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_webhooks: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - } ), - } ), - database_size: { - data: expect.any( Number ), - index: expect.any( Number ), - }, - } ), - } ) - ); - - expect( responseJSON ).toEqual( - expect.objectContaining( { - active_plugins: expect.arrayContaining( [ - { - plugin: expect.any( String ), - name: expect.any( String ), - version: expect.any( String ), - version_latest: expect.any( String ), - url: expect.any( String ), - author_name: expect.any( String ), - author_url: expect.any( String ), - network_activated: expect.any( Boolean ), - }, - { - plugin: expect.any( String ), - name: expect.any( String ), - version: expect.any( String ), - version_latest: expect.any( String ), - url: expect.any( String ), - author_name: expect.any( String ), - author_url: expect.any( String ), - network_activated: expect.any( Boolean ), - }, - { - plugin: expect.any( String ), - name: expect.any( String ), - version: expect.any( String ), - version_latest: expect.any( String ), - url: expect.any( String ), - author_name: expect.any( String ), - author_url: expect.any( String ), - network_activated: expect.any( Boolean ), - }, - { - plugin: expect.any( String ), - name: expect.any( String ), - version: expect.any( String ), - version_latest: expect.any( String ), - url: expect.any( String ), - author_name: expect.any( String ), - author_url: expect.any( String ), - network_activated: expect.any( Boolean ), - }, - ] ), - } ) - ); - - // local environment differs from external hosts. Local listed first. - // eslint-disable-next-line playwright/no-conditional-in-test - if ( ! shouldSkip ) { + } ) + ); + } expect( responseJSON ).toEqual( expect.objectContaining( { - dropins_mu_plugins: expect.objectContaining( { - dropins: expect.arrayContaining( [] ), - mu_plugins: expect.arrayContaining( [] ), + theme: expect.objectContaining( { + name: expect.any( String ), + version: expect.any( String ), + version_latest: expect.any( String ), + author_url: expect.any( String ), + is_child_theme: expect.any( Boolean ), + has_woocommerce_support: expect.any( Boolean ), + has_woocommerce_file: expect.any( Boolean ), + has_outdated_templates: expect.any( Boolean ), + overrides: expect.any( Array ), + parent_name: expect.any( String ), + parent_version: expect.any( String ), + parent_version_latest: expect.any( String ), + parent_author_url: expect.any( String ), } ), } ) ); - } else { expect( responseJSON ).toEqual( expect.objectContaining( { - dropins_mu_plugins: expect.objectContaining( { - dropins: expect.arrayContaining( [ - { - name: expect.any( String ), - plugin: expect.any( String ), - }, - { - name: expect.any( String ), - plugin: expect.any( String ), - }, - ] ), - mu_plugins: [], + settings: expect.objectContaining( { + api_enabled: expect.any( Boolean ), + force_ssl: expect.any( Boolean ), + currency: expect.any( String ), + currency_symbol: expect.any( String ), + currency_position: expect.any( String ), + thousand_separator: expect.any( String ), + decimal_separator: expect.any( String ), + number_of_decimals: expect.any( Number ), + geolocation_enabled: expect.any( Boolean ), + taxonomies: { + external: expect.any( String ), + grouped: expect.any( String ), + simple: expect.any( String ), + variable: expect.any( String ), + }, + product_visibility_terms: { + 'exclude-from-catalog': expect.any( String ), + 'exclude-from-search': expect.any( String ), + featured: expect.any( String ), + outofstock: expect.any( String ), + 'rated-1': expect.any( String ), + 'rated-2': expect.any( String ), + 'rated-3': expect.any( String ), + 'rated-4': expect.any( String ), + 'rated-5': expect.any( String ), + }, + woocommerce_com_connected: expect.any( String ), } ), } ) ); + expect( responseJSON ).toEqual( + expect.objectContaining( { + security: expect.objectContaining( { + secure_connection: expect.any( Boolean ), + hide_errors: expect.any( Boolean ), + } ), + } ) + ); + expect( responseJSON ).toEqual( + expect.objectContaining( { + pages: expect.arrayContaining( [ + { + page_name: expect.any( String ), + page_id: expect.any( String ), + page_set: expect.any( Boolean ), + page_exists: expect.any( Boolean ), + page_visible: expect.any( Boolean ), + shortcode: expect.any( String ), + block: expect.any( String ), + shortcode_required: expect.any( Boolean ), + shortcode_present: expect.any( Boolean ), + block_present: expect.any( Boolean ), + block_required: expect.any( Boolean ), + }, + { + page_name: expect.any( String ), + page_id: expect.any( String ), + page_set: expect.any( Boolean ), + page_exists: expect.any( Boolean ), + page_visible: expect.any( Boolean ), + shortcode: expect.any( String ), + block: expect.any( String ), + shortcode_required: expect.any( Boolean ), + shortcode_present: expect.any( Boolean ), + block_present: expect.any( Boolean ), + block_required: expect.any( Boolean ), + }, + { + page_name: expect.any( String ), + page_id: expect.any( String ), + page_set: expect.any( Boolean ), + page_exists: expect.any( Boolean ), + page_visible: expect.any( Boolean ), + shortcode: expect.any( String ), + block: expect.any( String ), + shortcode_required: expect.any( Boolean ), + shortcode_present: expect.any( Boolean ), + block_present: expect.any( Boolean ), + block_required: expect.any( Boolean ), + }, + { + page_name: expect.any( String ), + page_id: expect.any( String ), + page_set: expect.any( Boolean ), + page_exists: expect.any( Boolean ), + page_visible: expect.any( Boolean ), + shortcode: expect.any( String ), + block: expect.any( String ), + shortcode_required: expect.any( Boolean ), + shortcode_present: expect.any( Boolean ), + block_present: expect.any( Boolean ), + block_required: expect.any( Boolean ), + }, + { + page_name: expect.any( String ), + page_id: expect.any( String ), + page_set: expect.any( Boolean ), + page_exists: expect.any( Boolean ), + page_visible: expect.any( Boolean ), + shortcode: expect.any( String ), + block: expect.any( String ), + shortcode_required: expect.any( Boolean ), + shortcode_present: expect.any( Boolean ), + block_present: expect.any( Boolean ), + block_required: expect.any( Boolean ), + }, + ] ), + } ) + ); + expect( responseJSON ).toEqual( + expect.objectContaining( { + post_type_counts: expect.arrayContaining( [ + { + type: expect.any( String ), + count: expect.any( String ), + }, + { + type: expect.any( String ), + count: expect.any( String ), + }, + { + type: expect.any( String ), + count: expect.any( String ), + }, + ] ), + } ) + ); } - expect( responseJSON ).toEqual( - expect.objectContaining( { - theme: expect.objectContaining( { - name: expect.any( String ), - version: expect.any( String ), - version_latest: expect.any( String ), - author_url: expect.any( String ), - is_child_theme: expect.any( Boolean ), - has_woocommerce_support: expect.any( Boolean ), - has_woocommerce_file: expect.any( Boolean ), - has_outdated_templates: expect.any( Boolean ), - overrides: expect.any( Array ), - parent_name: expect.any( String ), - parent_version: expect.any( String ), - parent_version_latest: expect.any( String ), - parent_author_url: expect.any( String ), - } ), - } ) - ); - expect( responseJSON ).toEqual( - expect.objectContaining( { - settings: expect.objectContaining( { - api_enabled: expect.any( Boolean ), - force_ssl: expect.any( Boolean ), - currency: expect.any( String ), - currency_symbol: expect.any( String ), - currency_position: expect.any( String ), - thousand_separator: expect.any( String ), - decimal_separator: expect.any( String ), - number_of_decimals: expect.any( Number ), - geolocation_enabled: expect.any( Boolean ), - taxonomies: { - external: expect.any( String ), - grouped: expect.any( String ), - simple: expect.any( String ), - variable: expect.any( String ), - }, - product_visibility_terms: { - 'exclude-from-catalog': expect.any( String ), - 'exclude-from-search': expect.any( String ), - featured: expect.any( String ), - outofstock: expect.any( String ), - 'rated-1': expect.any( String ), - 'rated-2': expect.any( String ), - 'rated-3': expect.any( String ), - 'rated-4': expect.any( String ), - 'rated-5': expect.any( String ), - }, - woocommerce_com_connected: expect.any( String ), - } ), - } ) - ); - expect( responseJSON ).toEqual( - expect.objectContaining( { - security: expect.objectContaining( { - secure_connection: expect.any( Boolean ), - hide_errors: expect.any( Boolean ), - } ), - } ) - ); - expect( responseJSON ).toEqual( - expect.objectContaining( { - pages: expect.arrayContaining( [ - { - page_name: expect.any( String ), - page_id: expect.any( String ), - page_set: expect.any( Boolean ), - page_exists: expect.any( Boolean ), - page_visible: expect.any( Boolean ), - shortcode: expect.any( String ), - block: expect.any( String ), - shortcode_required: expect.any( Boolean ), - shortcode_present: expect.any( Boolean ), - block_present: expect.any( Boolean ), - block_required: expect.any( Boolean ), - }, - { - page_name: expect.any( String ), - page_id: expect.any( String ), - page_set: expect.any( Boolean ), - page_exists: expect.any( Boolean ), - page_visible: expect.any( Boolean ), - shortcode: expect.any( String ), - block: expect.any( String ), - shortcode_required: expect.any( Boolean ), - shortcode_present: expect.any( Boolean ), - block_present: expect.any( Boolean ), - block_required: expect.any( Boolean ), - }, - { - page_name: expect.any( String ), - page_id: expect.any( String ), - page_set: expect.any( Boolean ), - page_exists: expect.any( Boolean ), - page_visible: expect.any( Boolean ), - shortcode: expect.any( String ), - block: expect.any( String ), - shortcode_required: expect.any( Boolean ), - shortcode_present: expect.any( Boolean ), - block_present: expect.any( Boolean ), - block_required: expect.any( Boolean ), - }, - { - page_name: expect.any( String ), - page_id: expect.any( String ), - page_set: expect.any( Boolean ), - page_exists: expect.any( Boolean ), - page_visible: expect.any( Boolean ), - shortcode: expect.any( String ), - block: expect.any( String ), - shortcode_required: expect.any( Boolean ), - shortcode_present: expect.any( Boolean ), - block_present: expect.any( Boolean ), - block_required: expect.any( Boolean ), - }, - { - page_name: expect.any( String ), - page_id: expect.any( String ), - page_set: expect.any( Boolean ), - page_exists: expect.any( Boolean ), - page_visible: expect.any( Boolean ), - shortcode: expect.any( String ), - block: expect.any( String ), - shortcode_required: expect.any( Boolean ), - shortcode_present: expect.any( Boolean ), - block_present: expect.any( Boolean ), - block_required: expect.any( Boolean ), - }, - ] ), - } ) - ); - expect( responseJSON ).toEqual( - expect.objectContaining( { - post_type_counts: expect.arrayContaining( [ - { - type: expect.any( String ), - count: expect.any( String ), - }, - { - type: expect.any( String ), - count: expect.any( String ), - }, - { - type: expect.any( String ), - count: expect.any( String ), - }, - ] ), - } ) - ); - } ); + ); test( 'can view all system status tools', async ( { request } ) => { // call API to view system status tools diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-checkout-block.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-checkout-block.spec.js index c5f0d7ec448..7279c31fbea 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-checkout-block.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-checkout-block.spec.js @@ -149,9 +149,11 @@ test.describe( .locator( 'legend' ) ).toBeVisible(); await expect( - page.locator( - '.wp-block-woocommerce-checkout-order-summary-block' - ) + page + .locator( + '.wp-block-woocommerce-checkout-order-summary-block' + ) + .first() ).toBeVisible(); await expect( page.locator( '.wc-block-components-address-form' ).first() diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-coupon.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-coupon.spec.js index 2630674a42b..1d38fd68d5c 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-coupon.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-coupon.spec.js @@ -39,102 +39,110 @@ const test = baseTest.extend( { }, } ); -test.describe( 'Coupon management', { tag: '@services' }, () => { - for ( const couponType of Object.keys( couponData ) ) { - test( `can create new ${ couponType } coupon`, async ( { - page, - coupon, - } ) => { - await test.step( 'add new coupon', async () => { - await page.goto( - 'wp-admin/post-new.php?post_type=shop_coupon' - ); - await page - .getByLabel( 'Coupon code' ) - .fill( couponData[ couponType ].code ); - await page - .getByPlaceholder( 'Description (optional)' ) - .fill( couponData[ couponType ].description ); - await page - .getByPlaceholder( '0' ) - .fill( couponData[ couponType ].amount ); +test.describe( + 'Coupon management', + { tag: [ '@services', '@skip-on-default-wpcom' ] }, + () => { + for ( const couponType of Object.keys( couponData ) ) { + test( `can create new ${ couponType } coupon`, async ( { + page, + coupon, + } ) => { + await test.step( 'add new coupon', async () => { + await page.goto( + 'wp-admin/post-new.php?post_type=shop_coupon' + ); + await page + .getByLabel( 'Coupon code' ) + .fill( couponData[ couponType ].code ); + await page + .getByPlaceholder( 'Description (optional)' ) + .fill( couponData[ couponType ].description ); + await page + .getByPlaceholder( '0' ) + .fill( couponData[ couponType ].amount ); - // set expiry date if it was provided + // set expiry date if it was provided + if ( couponData[ couponType ].expiryDate ) { + await page + .getByPlaceholder( 'yyyy-mm-dd' ) + .fill( couponData[ couponType ].expiryDate ); + } + + // be explicit about whether free shipping is allowed + if ( couponData[ couponType ].freeShipping ) { + await page.getByLabel( 'Allow free shipping' ).check(); + } else { + await page + .getByLabel( 'Allow free shipping' ) + .uncheck(); + } + } ); + + // publish the coupon and retrieve the id + await test.step( 'publish the coupon', async () => { + await expect( + page.getByRole( 'link', { name: 'Move to Trash' } ) + ).toBeVisible(); + await page + .getByRole( 'button', { name: 'Publish', exact: true } ) + .click(); + await expect( + page.getByText( 'Coupon updated.' ) + ).toBeVisible(); + coupon.id = page.url().match( /(?<=post=)\d+/ )[ 0 ]; + expect( coupon.id ).toBeDefined(); + } ); + + // verify the creation of the coupon and details + await test.step( 'verify coupon creation', async () => { + await page.goto( + 'wp-admin/edit.php?post_type=shop_coupon' + ); + await expect( + page.getByRole( 'cell', { + name: couponData[ couponType ].code, + } ) + ).toBeVisible(); + await expect( + page.getByRole( 'cell', { + name: couponData[ couponType ].description, + } ) + ).toBeVisible(); + await expect( + page.getByRole( 'cell', { + name: couponData[ couponType ].amount, + exact: true, + } ) + ).toBeVisible(); + } ); + + // check expiry date if it was set if ( couponData[ couponType ].expiryDate ) { - await page - .getByPlaceholder( 'yyyy-mm-dd' ) - .fill( couponData[ couponType ].expiryDate ); + await test.step( 'verify coupon expiry date', async () => { + await page + .getByText( couponData[ couponType ].code ) + .last() + .click(); + await expect( + page.getByPlaceholder( 'yyyy-mm-dd' ) + ).toHaveValue( couponData[ couponType ].expiryDate ); + } ); } - // be explicit about whether free shipping is allowed + // if it was a free shipping coupon check that if ( couponData[ couponType ].freeShipping ) { - await page.getByLabel( 'Allow free shipping' ).check(); - } else { - await page.getByLabel( 'Allow free shipping' ).uncheck(); + await test.step( 'verify free shipping', async () => { + await page + .getByText( couponData[ couponType ].code ) + .last() + .click(); + await expect( + page.getByLabel( 'Allow free shipping' ) + ).toBeChecked(); + } ); } } ); - - // publish the coupon and retrieve the id - await test.step( 'publish the coupon', async () => { - await expect( - page.getByRole( 'link', { name: 'Move to Trash' } ) - ).toBeVisible(); - await page - .getByRole( 'button', { name: 'Publish', exact: true } ) - .click(); - await expect( - page.getByText( 'Coupon updated.' ) - ).toBeVisible(); - coupon.id = page.url().match( /(?<=post=)\d+/ )[ 0 ]; - expect( coupon.id ).toBeDefined(); - } ); - - // verify the creation of the coupon and details - await test.step( 'verify coupon creation', async () => { - await page.goto( 'wp-admin/edit.php?post_type=shop_coupon' ); - await expect( - page.getByRole( 'cell', { - name: couponData[ couponType ].code, - } ) - ).toBeVisible(); - await expect( - page.getByRole( 'cell', { - name: couponData[ couponType ].description, - } ) - ).toBeVisible(); - await expect( - page.getByRole( 'cell', { - name: couponData[ couponType ].amount, - exact: true, - } ) - ).toBeVisible(); - } ); - - // check expiry date if it was set - if ( couponData[ couponType ].expiryDate ) { - await test.step( 'verify coupon expiry date', async () => { - await page - .getByText( couponData[ couponType ].code ) - .last() - .click(); - await expect( - page.getByPlaceholder( 'yyyy-mm-dd' ) - ).toHaveValue( couponData[ couponType ].expiryDate ); - } ); - } - - // if it was a free shipping coupon check that - if ( couponData[ couponType ].freeShipping ) { - await test.step( 'verify free shipping', async () => { - await page - .getByText( couponData[ couponType ].code ) - .last() - .click(); - await expect( - page.getByLabel( 'Allow free shipping' ) - ).toBeChecked(); - } ); - } - } ); + } } -} ); +); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-product-brand.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-product-brand.spec.js new file mode 100644 index 00000000000..2288819aa86 --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-product-brand.spec.js @@ -0,0 +1,181 @@ +const { test, expect } = require( '@playwright/test' ); + +test.use( { storageState: process.env.ADMINSTATE } ); + +test.skip( 'Merchant can add brands', async ( { page } ) => { + /** + * Go to the Brands page. + * + * This will visit the Products page first, and then click on the Brands link. + * This is to workaround the hover menu for now. + */ + const goToBrandsPage = async () => { + await page.goto( + 'wp-admin/edit-tags.php?taxonomy=product_brand&post_type=product' + ); + + // Wait for the Brands page to load. + // This is needed so that checking for existing brands would work. + await page.waitForSelector( '.wp-list-table' ); + }; + + const createBrandIfNotExist = async ( + name, + slug, + parentBrand, + description, + thumbnailFileName + ) => { + // Create "WooCommerce" brand if it does not exist. + const cellVisible = await page + .locator( '#posts-filter' ) + .getByRole( 'cell', { name: slug, exact: true } ) + .isVisible(); + + if ( cellVisible ) { + return; + } + + await page.getByRole( 'textbox', { name: 'Name' } ).click(); + await page.getByRole( 'textbox', { name: 'Name' } ).fill( name ); + await page.getByRole( 'textbox', { name: 'Slug' } ).click(); + await page.getByRole( 'textbox', { name: 'Slug' } ).fill( slug ); + + await page + .getByRole( 'combobox', { name: 'Parent Brand' } ) + .selectOption( { label: parentBrand } ); + + await page.getByRole( 'textbox', { name: 'Description' } ).click(); + await page + .getByRole( 'textbox', { name: 'Description' } ) + .fill( description ); + await page.getByRole( 'button', { name: 'Upload/Add image' } ).click(); + await page.getByRole( 'tab', { name: 'Media Library' } ).click(); + await page.getByRole( 'checkbox', { name: thumbnailFileName } ).click(); + await page.getByRole( 'button', { name: 'Use image' } ).click(); + await page.getByRole( 'button', { name: 'Add New Brand' } ).click(); + + // We should see an "Item added." notice message at the top of the page. + await expect( + page.locator( '#ajax-response' ).getByText( 'Item added.' ) + ).toBeVisible(); + + // We should see the newly created brand in the Brands table. + await expect( + page + .locator( '#posts-filter' ) + .getByRole( 'cell', { name: slug, exact: true } ) + ).toHaveCount( 1 ); + }; + + /** + * Edit a brand. + * + * You must be in the Brands page before calling this function. + * To do so, call `goToBrandsPage()` first. + * + * After a brand is edited, you will be redirected to the Brands page. + */ + const editBrand = async ( + currentName, + { name, slug, parentBrand, description, thumbnailFileName } + ) => { + await page.getByLabel( `“${ currentName }” (Edit)` ).click(); + await page.getByLabel( 'Name' ).fill( name ); + await page.getByLabel( 'Slug' ).fill( slug ); + await page + .getByLabel( 'Parent Brand' ) + .selectOption( { label: parentBrand } ); + await page.getByLabel( 'Description' ).fill( description ); + + await page.getByRole( 'button', { name: 'Upload/Add image' } ).click(); + await page.getByRole( 'tab', { name: 'Media Library' } ).click(); + await page.getByLabel( thumbnailFileName ).click(); + await page.getByRole( 'button', { name: 'Use image' } ).click(); + + await page.getByRole( 'button', { name: 'Update' } ).click(); + + // We should see an "Item updated." notice message at the top of the page. + await expect( + page.locator( '#message' ).getByText( 'Item updated.' ) + ).toBeVisible(); + + // navigate back to Brands page. + await page.getByRole( 'link', { name: '← Go to Brands' } ).click(); + + // confirm that the brand has been updated. + await expect( + page + .locator( '#posts-filter' ) + .getByRole( 'cell', { name: slug, exact: true } ) + ).toHaveCount( 1 ); + }; + + /** + * Delete a brand. + * + * You must be in the Brands page before calling this function. + * To do so, call `goToBrandsPage()` first. + * + * After a brand is deleted, you will be redirected to the Brands page. + */ + const deleteBrand = async ( name ) => { + await page.getByLabel( `“${ name }” (Edit)` ).click(); + + // After clicking the "Delete" button, there will be a confirmation dialog. + page.once( 'dialog', ( dialog ) => { + // Click "OK" to confirm the deletion. + dialog.accept(); + } ); + + // Click on the "Delete" button. + await page.getByRole( 'link', { name: 'Delete' } ).click(); + + // We should now be in the Brands page. + // Confirm that the brand has been deleted and is no longer in the Brands table. + await expect( + page + .locator( '#posts-filter' ) + .getByRole( 'cell', { name, exact: true } ) + ).toHaveCount( 0 ); + }; + + await goToBrandsPage(); + await createBrandIfNotExist( + 'WooCommerce', + 'woocommerce', + 'None', + 'All things WooCommerce!', + 'image-01' + ); + + // Create child brand under the "WooCommerce" parent brand. + await createBrandIfNotExist( + 'WooCommerce Apparels', + 'woocommerce-apparels', + 'WooCommerce', + 'Cool WooCommerce clothings!', + 'image-02' + ); + + // Create a dummy child brand called "WooCommerce Dummy" under the "WooCommerce" parent brand. + await createBrandIfNotExist( + 'WooCommerce Dummy', + 'woocommerce-dummy', + 'WooCommerce', + 'Dummy WooCommerce brand!', + 'image-02' + ); + + // Edit the dummy child brand from "WooCommerce Dummy" to "WooCommerce Dummy Edited". + await editBrand( 'WooCommerce Dummy', { + name: 'WooCommerce Dummy Edited', + slug: 'woocommerce-dummy-edited', + parentBrand: 'WooCommerce', + description: 'Dummy WooCommerce brand edited!', + thumbnailFileName: 'image-03', + } ); + + // Delete the dummy child brand "WooCommerce Dummy Edited". + await deleteBrand( 'WooCommerce Dummy Edited' ); +} ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-restricted-coupons.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-restricted-coupons.spec.js index 2f26691761b..3fd0b4c1717 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-restricted-coupons.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-restricted-coupons.spec.js @@ -37,6 +37,12 @@ const couponData = { amount: '60', excludeProductCategories: [ 'Uncategorized' ], }, + excludeProductBrands: { + code: `excludeProductBrands-${ new Date().getTime().toString() }`, + description: 'Exclude product brands coupon', + amount: '65', + excludeProductBrands: [ 'WooCommerce Apparels' ], + }, products: { code: `products-${ new Date().getTime().toString() }`, description: 'Products coupon', @@ -202,6 +208,26 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => { .click(); } ); } + + // Skip Brands tests while behind a feature flag. + const skipBrandsTests = true; + + // set exclude product brands + if ( couponType === 'excludeProductBrands' && ! skipBrandsTests ) { + await test.step( 'set exclude product brands coupon', async () => { + await page + .getByRole( 'link', { + name: 'Usage restriction', + } ) + .click(); + await page + .getByPlaceholder( 'No brands' ) + .pressSequentially( 'WooCommerce Apparels' ); + await page + .getByRole( 'option', { name: 'WooCommerce Apparels' } ) + .click(); + } ); + } // set products if ( couponType === 'products' ) { await test.step( 'set products coupon', async () => { diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-create-simple.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-create-simple.spec.js index 77b8a6ebefa..46934a82a1d 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-create-simple.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-create-simple.spec.js @@ -112,7 +112,7 @@ for ( const productType of Object.keys( productData ) ) { .getByRole( 'link', { name: 'Attributes' } ) .click(); await page - .getByPlaceholder( 'f.e. size or color' ) + .getByPlaceholder( 'e.g. length or weight' ) .fill( attributeName ); await page .getByPlaceholder( 'Enter some descriptive text.' ) @@ -183,7 +183,7 @@ for ( const productType of Object.keys( productData ) ) { .getByPlaceholder( '0' ) .fill( productData[ productType ].shipping.weight ); await page - .getByPlaceholder( 'Length' ) + .getByPlaceholder( 'Length', { exact: true } ) .fill( productData[ productType ].shipping.length ); await page .getByPlaceholder( 'Width' ) diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-import-csv.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-import-csv.spec.js index 103d50fd8f6..507568812a0 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-import-csv.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-import-csv.spec.js @@ -89,8 +89,7 @@ const productCategories = [ ]; const productAttributes = [ 'Color', 'Size' ]; -const errorMessage = - 'Invalid file type. The importer supports CSV and TXT file formats.'; +const errorMessage = 'File is empty. Please upload something more substantial.'; test.describe.serial( 'Import Products from a CSV file', diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/add-variable-product/create-product-attributes.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/add-variable-product/create-product-attributes.spec.js index 77db0953f5c..9be4189e4b9 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/add-variable-product/create-product-attributes.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/add-variable-product/create-product-attributes.spec.js @@ -39,8 +39,9 @@ test.describe( 'Add product attributes', { tag: '@gutenberg' }, () => { } ); test( 'can add custom product attributes', async ( { page } ) => { - const textbox_attributeName = - page.getByPlaceholder( 'f.e. size or color' ); + const textbox_attributeName = page.getByPlaceholder( + 'e.g. length or weight' + ); const textbox_attributeValues = page.getByPlaceholder( 'Enter options for customers to choose from' ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/add-variable-product/create-variations.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/add-variable-product/create-variations.spec.js index b1271496bf6..16f2965b3b4 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/add-variable-product/create-variations.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/add-variable-product/create-variations.spec.js @@ -84,105 +84,117 @@ test.describe( 'Add variations', { tag: '@gutenberg' }, () => { } } ); - test( 'can manually add a variation', async ( { page } ) => { - await test.step( `Open "Edit product" page of product id ${ productId_addManually }`, async () => { - await page.goto( - `/wp-admin/post.php?post=${ productId_addManually }&action=edit` - ); - } ); - - // hook up the woocommerce_variations_added jQuery trigger so we can check if it's fired - await test.step( 'Hook up the woocommerce_variations_added jQuery trigger', async () => { - await page.evaluate( () => { - window.woocommerceVariationsAddedFunctionCalls = []; - - window - .jQuery( '#variable_product_options' ) - .on( 'woocommerce_variations_added', ( event, data ) => { - window.woocommerceVariationsAddedFunctionCalls.push( [ - event, - data, - ] ); - } ); + test( + 'can manually add a variation', + { tag: '@skip-on-default-wpcom' }, + async ( { page } ) => { + await test.step( `Open "Edit product" page of product id ${ productId_addManually }`, async () => { + await page.goto( + `/wp-admin/post.php?post=${ productId_addManually }&action=edit` + ); } ); - } ); - await test.step( 'Click on the "Variations" tab.', async () => { - await page.locator( '.variations_tab' ).click(); - } ); + // hook up the woocommerce_variations_added jQuery trigger so we can check if it's fired + await test.step( 'Hook up the woocommerce_variations_added jQuery trigger', async () => { + await page.evaluate( () => { + window.woocommerceVariationsAddedFunctionCalls = []; - await test.step( `Manually add ${ variationsToManuallyCreate.length } variations`, async () => { - const variationRows = page.locator( '.woocommerce_variation h3' ); - let variationRowsCount = await variationRows.count(); - const originalVariationRowsCount = variationRowsCount; - - for ( const variationToCreate of variationsToManuallyCreate ) { - await test.step( 'Click "Add manually"', async () => { - const addManuallyButton = page.getByRole( 'button', { - name: 'Add manually', - } ); - - await addManuallyButton.click(); - - await expect( variationRows ).toHaveCount( - ++variationRowsCount - ); - - // verify that the woocommerce_variations_added jQuery trigger was fired - const woocommerceVariationsAddedFunctionCalls = - await page.evaluate( - () => window.woocommerceVariationsAddedFunctionCalls + window + .jQuery( '#variable_product_options' ) + .on( + 'woocommerce_variations_added', + ( event, data ) => { + window.woocommerceVariationsAddedFunctionCalls.push( + [ event, data ] + ); + } ); - expect( - woocommerceVariationsAddedFunctionCalls.length - ).toEqual( - variationRowsCount - originalVariationRowsCount - ); } ); + } ); - for ( const attributeValue of variationToCreate ) { - const attributeName = productAttributes.find( - ( { options } ) => options.includes( attributeValue ) - ).name; - const addAttributeMenu = variationRows - .nth( 0 ) - .locator( 'select', { - has: page.locator( 'option', { - hasText: attributeValue, - } ), + await test.step( 'Click on the "Variations" tab.', async () => { + await page.locator( '.variations_tab' ).click(); + } ); + + await test.step( `Manually add ${ variationsToManuallyCreate.length } variations`, async () => { + const variationRows = page.locator( + '.woocommerce_variation h3' + ); + let variationRowsCount = await variationRows.count(); + const originalVariationRowsCount = variationRowsCount; + + for ( const variationToCreate of variationsToManuallyCreate ) { + await test.step( 'Click "Add manually"', async () => { + const addManuallyButton = page.getByRole( 'button', { + name: 'Add manually', } ); - await test.step( `Select "${ attributeValue }" from the "${ attributeName }" attribute menu`, async () => { - await addAttributeMenu.selectOption( attributeValue ); + await addManuallyButton.click(); + + await expect( variationRows ).toHaveCount( + ++variationRowsCount + ); + + // verify that the woocommerce_variations_added jQuery trigger was fired + const woocommerceVariationsAddedFunctionCalls = + await page.evaluate( + () => + window.woocommerceVariationsAddedFunctionCalls + ); + expect( + woocommerceVariationsAddedFunctionCalls.length + ).toEqual( + variationRowsCount - originalVariationRowsCount + ); } ); - } - - await test.step( 'Click "Save changes"', async () => { - await page - .getByRole( 'button', { - name: 'Save changes', - } ) - .click(); - } ); - - await test.step( `Expect the variation ${ variationToCreate.join( - ', ' - ) } to be successfully saved.`, async () => { - let newlyAddedVariationRow; for ( const attributeValue of variationToCreate ) { - newlyAddedVariationRow = ( - newlyAddedVariationRow || variationRows - ).filter( { - has: page.locator( 'option[selected]', { - hasText: attributeValue, - } ), + const attributeName = productAttributes.find( + ( { options } ) => + options.includes( attributeValue ) + ).name; + const addAttributeMenu = variationRows + .nth( 0 ) + .locator( 'select', { + has: page.locator( 'option', { + hasText: attributeValue, + } ), + } ); + + await test.step( `Select "${ attributeValue }" from the "${ attributeName }" attribute menu`, async () => { + await addAttributeMenu.selectOption( + attributeValue + ); } ); } - await expect( newlyAddedVariationRow ).toBeVisible(); - } ); - } - } ); - } ); + await test.step( 'Click "Save changes"', async () => { + await page + .getByRole( 'button', { + name: 'Save changes', + } ) + .click(); + } ); + + await test.step( `Expect the variation ${ variationToCreate.join( + ', ' + ) } to be successfully saved.`, async () => { + let newlyAddedVariationRow; + + for ( const attributeValue of variationToCreate ) { + newlyAddedVariationRow = ( + newlyAddedVariationRow || variationRows + ).filter( { + has: page.locator( 'option[selected]', { + hasText: attributeValue, + } ), + } ); + } + + await expect( newlyAddedVariationRow ).toBeVisible(); + } ); + } + } ); + } + ); } ); diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-admin-notes.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-admin-notes.php index 66f12f2a352..247d3c5fa84 100644 --- a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-admin-notes.php +++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-admin-notes.php @@ -60,7 +60,7 @@ class WC_Helper_Admin_Notes { $note_2->set_source( 'PHPUNIT_TEST' ); $note_2->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED ); $note_2->set_is_snoozable( true ); - $note_2->set_layout( 'banner' ); + $note_2->set_layout( 'thumbnail' ); $note_2->set_image( 'https://an-image.jpg' ); // This note has no actions. $note_2->save(); @@ -100,7 +100,6 @@ class WC_Helper_Admin_Notes { '?s=PHPUNIT_TEST_NOTE_4_ACTION_2_URL' ); $note_4->save(); - } /** diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/product-reviews.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/product-reviews.php index df53aeece57..a68ad775f87 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/product-reviews.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/product-reviews.php @@ -1,4 +1,7 @@ assertEquals( 200, $response->get_status() ); $this->assertEquals( 10, count( $product_reviews ) ); - $this->assertContains( - array( - 'id' => $review_id, - 'date_created' => $product_reviews[0]['date_created'], - 'date_created_gmt' => $product_reviews[0]['date_created_gmt'], - 'product_id' => $product->get_id(), - 'product_name' => $product->get_name(), - 'product_permalink' => $product->get_permalink(), - 'status' => 'approved', - 'reviewer' => 'admin', - 'reviewer_email' => 'woo@woo.local', - 'review' => "

    Review content here

    \n", - 'rating' => 0, - 'verified' => false, - 'reviewer_avatar_urls' => $product_reviews[0]['reviewer_avatar_urls'], - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v3/products/reviews/' . $review_id ), + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => $review_id, + 'date_created' => $product_reviews[0]['date_created'], + 'date_created_gmt' => $product_reviews[0]['date_created_gmt'], + 'product_id' => $product->get_id(), + 'product_name' => $product->get_name(), + 'product_permalink' => $product->get_permalink(), + 'status' => 'approved', + 'reviewer' => 'admin', + 'reviewer_email' => 'woo@woo.local', + 'review' => "

    Review content here

    \n", + 'rating' => 0, + 'verified' => false, + 'reviewer_avatar_urls' => $product_reviews[0]['reviewer_avatar_urls'], + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/products/reviews/' . $review_id ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v3/products/reviews' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/products/reviews' ), + ), ), - ), - 'up' => array( - array( - 'href' => rest_url( '/wc/v3/products/' . $product->get_id() ), + 'up' => array( + array( + 'href' => rest_url( '/wc/v3/products/' . $product->get_id() ), + ), ), ), ), - ), - $product_reviews + $product_reviews[0] + ) ); } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php index 32a96fb95dc..f185a811097 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php @@ -6,6 +6,7 @@ * @since 3.0.0 */ +use Automattic\WooCommerce\Utilities\ArrayUtil; use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; /** @@ -482,29 +483,39 @@ class Settings_V2 extends WC_REST_Unit_Test_Case { $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/products' ) ); $data = $response->get_data(); $this->assertTrue( is_array( $data ) ); - $this->assertContains( - array( - 'id' => 'woocommerce_downloads_require_login', - 'label' => 'Access restriction', - 'description' => 'Downloads require login', - 'type' => 'checkbox', - 'default' => 'no', - 'tip' => 'This setting does not apply to guest purchases.', - 'value' => 'no', - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v2/settings/products/woocommerce_downloads_require_login' ), + $data_download_required_login = null; + foreach ( $data as $setting ) { + if ( 'woocommerce_downloads_require_login' === $setting['id'] ) { + $data_download_required_login = $setting; + break; + } + } + $this->assertNotEmpty( $data_download_required_login ); + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => 'woocommerce_downloads_require_login', + 'label' => 'Access restriction', + 'description' => 'Downloads require login', + 'type' => 'checkbox', + 'default' => 'no', + 'tip' => 'This setting does not apply to guest purchases.', + 'value' => 'no', + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/settings/products/woocommerce_downloads_require_login' ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v2/settings/products' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/settings/products' ), + ), ), ), ), - ), - $data + $data_download_required_login + ) ); // test get single. @@ -540,29 +551,41 @@ class Settings_V2 extends WC_REST_Unit_Test_Case { $this->assertEquals( 200, $response->get_status() ); - $this->assertContains( - array( - 'id' => 'recipient', - 'label' => 'Recipient(s)', - 'description' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org.', - 'type' => 'text', - 'default' => '', - 'tip' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org.', - 'value' => '', - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v2/settings/email_new_order/recipient' ), + $recipient_setting = null; + foreach ( $settings as $setting ) { + if ( 'recipient' === $setting['id'] ) { + $recipient_setting = $setting; + break; + } + } + + $this->assertNotEmpty( $recipient_setting ); + + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => 'recipient', + 'label' => 'Recipient(s)', + 'description' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org.', + 'type' => 'text', + 'default' => '', + 'tip' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org.', + 'value' => '', + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/settings/email_new_order/recipient' ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v2/settings/email_new_order' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/settings/email_new_order' ), + ), ), ), ), - ), - $settings + $recipient_setting + ) ); // test get single. diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-methods.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-methods.php index 3f13ecb2ee1..d27efd5b67b 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-methods.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-methods.php @@ -1,4 +1,7 @@ get_data(); $this->assertEquals( 200, $response->get_status() ); - $this->assertContains( - array( - 'id' => 'free_shipping', - 'title' => 'Free shipping', - 'description' => 'Free shipping is a special method which can be triggered with coupons and minimum spends.', - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v2/shipping_methods/free_shipping' ), + + $free_shipping_method = null; + foreach ( $methods as $method ) { + if ( 'free_shipping' === $method['id'] ) { + $free_shipping_method = $method; + break; + } + } + $this->assertNotEmpty( $free_shipping_method ); + + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => 'free_shipping', + 'title' => 'Free shipping', + 'description' => 'Free shipping is a special method which can be triggered with coupons and minimum spends.', + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/shipping_methods/free_shipping' ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v2/shipping_methods' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping_methods' ), + ), ), ), ), - ), - $methods + $free_shipping_method + ) ); } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-zones.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-zones.php index bc020791d02..1e15bb91e02 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-zones.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-zones.php @@ -1,5 +1,7 @@ assertEquals( 200, $response->get_status() ); $this->assertEquals( count( $data ), 1 ); - $this->assertContains( - array( - 'id' => $data[0]['id'], - 'name' => 'Locations not covered by your other zones', - 'order' => 0, - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[0]['id'] ), + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => $data[0]['id'], + 'name' => 'Locations not covered by your other zones', + 'order' => 0, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[0]['id'] ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones' ), + ), ), - ), - 'describedby' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[0]['id'] . '/locations' ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[0]['id'] . '/locations' ), + ), ), ), ), - ), - $data + $data[0] + ) ); // Create a zone and make sure it's in the response @@ -108,30 +112,32 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( count( $data ), 2 ); - $this->assertContains( - array( - 'id' => $data[1]['id'], - 'name' => 'Zone 1', - 'order' => 0, - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[1]['id'] ), + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => $data[1]['id'], + 'name' => 'Zone 1', + 'order' => 0, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[1]['id'] ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones' ), + ), ), - ), - 'describedby' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[1]['id'] . '/locations' ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[1]['id'] . '/locations' ), + ), ), ), ), - ), - $data + $data[1] + ) ); } @@ -195,30 +201,32 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case { $data = $response->get_data(); $this->assertEquals( 201, $response->get_status() ); - $this->assertEquals( - array( - 'id' => $data['id'], - 'name' => 'Test Zone', - 'order' => 1, - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones/' . $data['id'] ), + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => $data['id'], + 'name' => 'Test Zone', + 'order' => 1, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $data['id'] ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones' ), + ), ), - ), - 'describedby' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones/' . $data['id'] . '/locations' ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $data['id'] . '/locations' ), + ), ), ), ), - ), - $data + $data + ) ); } @@ -260,30 +268,32 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case { $data = $response->get_data(); $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( - array( - 'id' => $zone->get_id(), - 'name' => 'Zone Test', - 'order' => 2, - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ), + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => $zone->get_id(), + 'name' => 'Zone Test', + 'order' => 2, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones' ), + ), ), - ), - 'describedby' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ), + ), ), ), ), - ), - $data + $data + ) ); } @@ -359,30 +369,32 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case { $data = $response->get_data(); $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( - array( - 'id' => $zone->get_id(), - 'name' => 'Test Zone', - 'order' => 0, - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ), + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => $zone->get_id(), + 'name' => 'Test Zone', + 'order' => 0, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones' ), + ), ), - ), - 'describedby' => array( - array( - 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ), + ), ), ), ), - ), - $data + $data + ) ); } @@ -624,13 +636,13 @@ class WC_Tests_API_Shipping_Zones_V2 extends WC_REST_Unit_Test_Case { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( count( $data ), 1 ); - $this->assertContains( $expected, $data ); + $this->assertEmpty( ArrayUtil::deep_assoc_array_diff( $expected, $data[0] ) ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ) ); $data = $response->get_data(); $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( $expected, $data ); + $this->assertEmpty( ArrayUtil::deep_assoc_array_diff( $expected, $data ) ); } /** diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/product-reviews.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/product-reviews.php index 11aa94c16b7..b655ffb538a 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/product-reviews.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/product-reviews.php @@ -1,4 +1,7 @@ assertEquals( 200, $response->get_status() ); $this->assertEquals( 10, count( $product_reviews ) ); - $this->assertContains( - array( - 'id' => $review_id, - 'date_created' => $product_reviews[0]['date_created'], - 'date_created_gmt' => $product_reviews[0]['date_created_gmt'], - 'product_id' => $product->get_id(), - 'product_name' => $product->get_name(), - 'product_permalink' => $product->get_permalink(), - 'status' => 'approved', - 'reviewer' => 'admin', - 'reviewer_email' => 'woo@woo.local', - 'review' => "

    Review content here

    \n", - 'rating' => 0, - 'verified' => false, - 'reviewer_avatar_urls' => $product_reviews[0]['reviewer_avatar_urls'], - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v3/products/reviews/' . $review_id ), + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => $review_id, + 'date_created' => $product_reviews[0]['date_created'], + 'date_created_gmt' => $product_reviews[0]['date_created_gmt'], + 'product_id' => $product->get_id(), + 'product_name' => $product->get_name(), + 'product_permalink' => $product->get_permalink(), + 'status' => 'approved', + 'reviewer' => 'admin', + 'reviewer_email' => 'woo@woo.local', + 'review' => "

    Review content here

    \n", + 'rating' => 0, + 'verified' => false, + 'reviewer_avatar_urls' => $product_reviews[0]['reviewer_avatar_urls'], + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/products/reviews/' . $review_id ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v3/products/reviews' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/products/reviews' ), + ), ), - ), - 'up' => array( - array( - 'href' => rest_url( '/wc/v3/products/' . $product->get_id() ), + 'up' => array( + array( + 'href' => rest_url( '/wc/v3/products/' . $product->get_id() ), + ), ), ), ), - ), - $product_reviews + $product_reviews[0] + ) ); } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php index d8890569eff..d10062ec2ee 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php @@ -6,6 +6,7 @@ * @since 3.5.0 */ +use Automattic\WooCommerce\Utilities\ArrayUtil; use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; /** @@ -481,29 +482,42 @@ class Settings extends WC_REST_Unit_Test_Case { $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/products' ) ); $data = $response->get_data(); $this->assertTrue( is_array( $data ) ); - $this->assertContains( - array( - 'id' => 'woocommerce_downloads_require_login', - 'label' => 'Access restriction', - 'description' => 'Downloads require login', - 'type' => 'checkbox', - 'default' => 'no', - 'tip' => 'This setting does not apply to guest purchases.', - 'value' => 'no', - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v3/settings/products/woocommerce_downloads_require_login' ), + + $setting_downloads_required = null; + foreach ( $data as $setting ) { + if ( 'woocommerce_downloads_require_login' === $setting['id'] ) { + $setting_downloads_required = $setting; + break; + } + } + + $this->assertNotEmpty( $setting_downloads_required ); + + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => 'woocommerce_downloads_require_login', + 'label' => 'Access restriction', + 'description' => 'Downloads require login', + 'type' => 'checkbox', + 'default' => 'no', + 'tip' => 'This setting does not apply to guest purchases.', + 'value' => 'no', + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/settings/products/woocommerce_downloads_require_login' ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v3/settings/products' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/settings/products' ), + ), ), ), ), - ), - $data + $setting_downloads_required + ) ); // test get single. @@ -539,29 +553,41 @@ class Settings extends WC_REST_Unit_Test_Case { $this->assertEquals( 200, $response->get_status() ); - $this->assertContains( - array( - 'id' => 'recipient', - 'label' => 'Recipient(s)', - 'description' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org.', - 'type' => 'text', - 'default' => '', - 'tip' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org.', - 'value' => '', - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v3/settings/email_new_order/recipient' ), + $recipient_setting = null; + foreach ( $settings as $setting ) { + if ( 'recipient' === $setting['id'] ) { + $recipient_setting = $setting; + break; + } + } + + $this->assertNotEmpty( $recipient_setting ); + + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => 'recipient', + 'label' => 'Recipient(s)', + 'description' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org.', + 'type' => 'text', + 'default' => '', + 'tip' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org.', + 'value' => '', + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/settings/email_new_order/recipient' ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v3/settings/email_new_order' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/settings/email_new_order' ), + ), ), ), ), - ), - $settings + $recipient_setting + ) ); // test get single. diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-methods.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-methods.php index 31dc36c1b14..05d38ad0517 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-methods.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-methods.php @@ -1,4 +1,7 @@ get_data(); $this->assertEquals( 200, $response->get_status() ); - $this->assertContains( - array( - 'id' => 'free_shipping', - 'title' => 'Free shipping', - 'description' => 'Free shipping is a special method which can be triggered with coupons and minimum spends.', - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v3/shipping_methods/free_shipping' ), + + $free_shipping = null; + foreach ( $methods as $method ) { + if ( 'free_shipping' === $method['id'] ) { + $free_shipping = $method; + break; + } + } + $this->assertNotEmpty( $free_shipping ); + + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => 'free_shipping', + 'title' => 'Free shipping', + 'description' => 'Free shipping is a special method which can be triggered with coupons and minimum spends.', + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/shipping_methods/free_shipping' ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v3/shipping_methods' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping_methods' ), + ), ), ), ), - ), - $methods + $free_shipping + ) ); } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-zones.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-zones.php index 1dd58034653..3c49902d989 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-zones.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-zones.php @@ -1,5 +1,7 @@ assertEquals( 200, $response->get_status() ); $this->assertEquals( count( $data ), 1 ); - $this->assertContains( - array( - 'id' => $data[0]['id'], - 'name' => 'Locations not covered by your other zones', - 'order' => 0, - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[0]['id'] ), + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => $data[0]['id'], + 'name' => 'Locations not covered by your other zones', + 'order' => 0, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[0]['id'] ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones' ), + ), ), - ), - 'describedby' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[0]['id'] . '/locations' ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[0]['id'] . '/locations' ), + ), ), ), ), - ), - $data + $data[0] + ) ); // Create a zone and make sure it's in the response @@ -111,30 +115,32 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( count( $data ), 2 ); - $this->assertContains( - array( - 'id' => $data[1]['id'], - 'name' => 'Zone 1', - 'order' => 0, - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[1]['id'] ), + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => $data[1]['id'], + 'name' => 'Zone 1', + 'order' => 0, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[1]['id'] ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones' ), + ), ), - ), - 'describedby' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[1]['id'] . '/locations' ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[1]['id'] . '/locations' ), + ), ), ), ), - ), - $data + $data[1] + ) ); } @@ -202,30 +208,32 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case { $data = $response->get_data(); $this->assertEquals( 201, $response->get_status() ); - $this->assertEquals( - array( - 'id' => $data['id'], - 'name' => 'Test Zone', - 'order' => 1, - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones/' . $data['id'] ), + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => $data['id'], + 'name' => 'Test Zone', + 'order' => 1, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $data['id'] ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones' ), + ), ), - ), - 'describedby' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones/' . $data['id'] . '/locations' ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $data['id'] . '/locations' ), + ), ), ), ), - ), - $data + $data + ) ); } @@ -269,30 +277,32 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case { $data = $response->get_data(); $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( - array( - 'id' => $zone->get_id(), - 'name' => 'Zone Test', - 'order' => 2, - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ), + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => $zone->get_id(), + 'name' => 'Zone Test', + 'order' => 2, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones' ), + ), ), - ), - 'describedby' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ), + ), ), ), ), - ), - $data + $data + ) ); } @@ -373,30 +383,32 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case { $data = $response->get_data(); $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( - array( - 'id' => $zone->get_id(), - 'name' => 'Test Zone', - 'order' => 0, - '_links' => array( - 'self' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ), + $this->assertEmpty( + ArrayUtil::deep_assoc_array_diff( + array( + 'id' => $zone->get_id(), + 'name' => 'Test Zone', + 'order' => 0, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ), + ), ), - ), - 'collection' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones' ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones' ), + ), ), - ), - 'describedby' => array( - array( - 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ), + ), ), ), ), - ), - $data + $data + ) ); } @@ -644,13 +656,12 @@ class WC_Tests_API_Shipping_Zones extends WC_REST_Unit_Test_Case { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( count( $data ), 1 ); - $this->assertContains( $expected, $data ); - + $this->assertEmpty( ArrayUtil::deep_assoc_array_diff( $expected, $data[0] ) ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ) ); $data = $response->get_data(); $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( $expected, $data ); + $this->assertEmpty( ArrayUtil::deep_assoc_array_diff( $expected, $data ) ); } /** diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/base-location-country-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/base-location-country-rule-processor.php similarity index 94% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/base-location-country-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/base-location-country-rule-processor.php index b49df23970d..031da61b9f2 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/base-location-country-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/base-location-country-rule-processor.php @@ -2,16 +2,18 @@ /** * Base Location country rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\BaseLocationCountryRuleProcessor; use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile; /** - * class WC_Admin_Tests_RemoteInboxNotifications_BaseLocationCountryRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_BaseLocationCountryRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_BaseLocationCountryRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_BaseLocationCountryRuleProcessor extends WC_Unit_Test_Case { /** * Get the publish_before rule. * diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/base-location-state-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/base-location-state-rule-processor.php similarity index 84% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/base-location-state-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/base-location-state-rule-processor.php index 9135ab0b05d..d69972954c0 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/base-location-state-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/base-location-state-rule-processor.php @@ -2,16 +2,18 @@ /** * Base Location state rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\BaseLocationStateRuleProcessor; use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile; /** - * class WC_Admin_Tests_RemoteInboxNotifications_BaseLocationStateRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_BaseLocationStateRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_BaseLocationStateRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_BaseLocationStateRuleProcessor extends WC_Unit_Test_Case { /** * Get the base_location_state rule. diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/comparison-operation.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/comparison-operation.php similarity index 90% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/comparison-operation.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/comparison-operation.php index 85fa28f8e9f..7ea8bd7dd8b 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/comparison-operation.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/comparison-operation.php @@ -5,12 +5,14 @@ * @package WooCommerce\Admin\Tests\RemoteInboxNotification */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\ComparisonOperation; /** - * class WC_Admin_Tests_RemoteInboxNotifications_Comparison_Operation + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_Comparison_Operation */ -class WC_Admin_Tests_RemoteInboxNotifications_Comparison_Operation extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_Comparison_Operation extends WC_Unit_Test_Case { /** * @var ComparisonOperation $operation */ @@ -36,7 +38,6 @@ class WC_Admin_Tests_RemoteInboxNotifications_Comparison_Operation extends WC_Un $this->assertFalse( $this->operation->compare( 11, array( 1, 10 ), 'range' ) ); $this->assertFalse( $this->operation->compare( 11, array( 1, 10, 2 ), 'range' ) ); $this->assertFalse( $this->operation->compare( 11, 'string', 'range' ) ); - } /** diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/evaluate-and-get-status.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/evaluate-and-get-status.php similarity index 95% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/evaluate-and-get-status.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/evaluate-and-get-status.php index 7de3b0c6251..7ec67d2f995 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/evaluate-and-get-status.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/evaluate-and-get-status.php @@ -2,15 +2,17 @@ /** * Evaluate and get status tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\EvaluateAndGetStatus; /** - * class WC_Admin_Tests_RemoteInboxNotifications_EvaluateAndGetStatus + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_EvaluateAndGetStatus */ -class WC_Admin_Tests_RemoteInboxNotifications_EvaluateAndGetStatus extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_EvaluateAndGetStatus extends WC_Unit_Test_Case { /** * Build up a spec given the supplied parameters. * diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/failing-rule-evaluator.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/failing-rule-evaluator.php similarity index 58% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/failing-rule-evaluator.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/failing-rule-evaluator.php index 8d856895e5b..b4c79cffb31 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/failing-rule-evaluator.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/failing-rule-evaluator.php @@ -2,9 +2,11 @@ /** * FailingRuleEvaluator * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + /** * class FailingRuleEvaluator */ @@ -16,7 +18,7 @@ class FailingRuleEvaluator { * * @return bool The evaluated result. */ - public function evaluate( $rules ) { + public function evaluate( $rules ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found return false; } } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/get-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/get-rule-processor.php similarity index 70% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/get-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/get-rule-processor.php index b5eee559d84..fb2721229eb 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/get-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/get-rule-processor.php @@ -2,15 +2,17 @@ /** * Get rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\GetRuleProcessor; /** - * class WC_Admin_Tests_RemoteInboxNotifications_GetRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_GetRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_GetRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_GetRuleProcessor extends WC_Unit_Test_Case { /** * Tests that an unknown rule processor returns a FailRuleProcessor * diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/is-woo-express-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/is-woo-express-rule-processor.php similarity index 91% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/is-woo-express-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/is-woo-express-rule-processor.php index 5db49ff717f..ba2bca4b7cf 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/is-woo-express-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/is-woo-express-rule-processor.php @@ -2,15 +2,17 @@ /** * Is WooExpress rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\IsWooExpressRuleProcessor; /** - * class WC_Admin_Tests_RemoteInboxNotifications_IsWooExpressRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_IsWooExpressRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_IsWooExpressRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_IsWooExpressRuleProcessor extends WC_Unit_Test_Case { /** * Set Up Before Class. */ diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-date-time-provider.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-date-time-provider.php similarity index 87% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-date-time-provider.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-date-time-provider.php index e250192bb23..cc8c32321c2 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-date-time-provider.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-date-time-provider.php @@ -2,9 +2,11 @@ /** * Mock DateTime Provider. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\DateTimeProvider\DateTimeProviderInterface; /** @@ -29,4 +31,3 @@ class MockDateTimeProvider implements DateTimeProviderInterface { return $this->now; } } - diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-get-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-get-rule-processor.php similarity index 91% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-get-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-get-rule-processor.php index 5de623920a5..805e6aabca2 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-get-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-get-rule-processor.php @@ -2,9 +2,11 @@ /** * MockGetRuleProcessor. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\PublishAfterTimeRuleProcessor; use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\FailRuleProcessor; @@ -29,4 +31,3 @@ class MockGetRuleProcessor { return new FailRuleProcessor(); } } - diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-plugins-provider.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-plugins-provider.php similarity index 96% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-plugins-provider.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-plugins-provider.php index 72e4bc22b62..f9a14565af9 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-plugins-provider.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-plugins-provider.php @@ -2,9 +2,11 @@ /** * Mock plugins Provider. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\PluginsProvider\PluginsProviderInterface; /** diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-product-query.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-product-query.php similarity index 85% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-product-query.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-product-query.php index 681309d4045..8b3c0381e77 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-product-query.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-product-query.php @@ -2,9 +2,11 @@ /** * Mock product query. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + /** * Mock product query. */ @@ -29,4 +31,3 @@ class MockProductQuery { ); } } - diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-wc-admin-active-for-provider.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-wc-admin-active-for-provider.php similarity index 80% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-wc-admin-active-for-provider.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-wc-admin-active-for-provider.php index 37932001686..3f65b3f1030 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/mock-wc-admin-active-for-provider.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/mock-wc-admin-active-for-provider.php @@ -2,9 +2,11 @@ /** * Mock WCAdminActiveForProvider * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + /** * Mock WCAdminActiveForProvider */ diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/not-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/not-rule-processor.php similarity index 88% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/not-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/not-rule-processor.php index 9088cec3242..f2628792362 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/not-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/not-rule-processor.php @@ -2,16 +2,18 @@ /** * Not rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\NotRuleProcessor; use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\RuleEvaluator; /** - * class WC_Admin_Tests_RemoteInboxNotifications_NotRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_NotRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_NotRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_NotRuleProcessor extends WC_Unit_Test_Case { /** * An empty operand evaluates to false, so negating that should * evaluate to true. diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/onboarding-profile-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/onboarding-profile-rule-processor.php similarity index 87% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/onboarding-profile-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/onboarding-profile-rule-processor.php index 097670b0115..9f59c1ab06f 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/onboarding-profile-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/onboarding-profile-rule-processor.php @@ -2,16 +2,18 @@ /** * Onboarding profile rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\OnboardingProfileRuleProcessor; use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile; /** - * class WC_Admin_Tests_RemoteInboxNotifications_OnboardingProfileRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_OnboardingProfileRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_OnboardingProfileRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_OnboardingProfileRuleProcessor extends WC_Unit_Test_Case { /** * Get the publish_before rule. * diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/option-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/option-rule-processor.php similarity index 93% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/option-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/option-rule-processor.php index 6e062f0ec7a..20f5dccad84 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/option-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/option-rule-processor.php @@ -2,15 +2,17 @@ /** * Option rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\OptionRuleProcessor; /** - * class WC_Admin_Tests_RemoteInboxNotifications_OptionRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_OptionRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_OptionRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_OptionRuleProcessor extends WC_Unit_Test_Case { /** * No default option resolves to false. * diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/or-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/or-rule-processor.php similarity index 93% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/or-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/or-rule-processor.php index 6c93dfd1fb8..90c95605e1a 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/or-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/or-rule-processor.php @@ -2,16 +2,18 @@ /** * Or rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\OrRuleProcessor; use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\RuleEvaluator; /** - * class WC_Admin_Tests_RemoteInboxNotifications_OrRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_OrRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_OrRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_OrRuleProcessor extends WC_Unit_Test_Case { /** * Both operands evaluating to false and ORed together evaluates to false. * diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/passing-rule-evaluator.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/passing-rule-evaluator.php similarity index 58% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/passing-rule-evaluator.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/passing-rule-evaluator.php index 9a504e784cd..f0d730f1ced 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/passing-rule-evaluator.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/passing-rule-evaluator.php @@ -2,9 +2,11 @@ /** * PassingRuleEvaluator * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + /** * class PassingRuleEvaluator */ @@ -16,7 +18,7 @@ class PassingRuleEvaluator { * * @return bool The evaluated result. */ - public function evaluate( $rules ) { + public function evaluate( $rules ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found return true; } } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/plugin-version-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/plugin-version-rule-processor.php similarity index 94% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/plugin-version-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/plugin-version-rule-processor.php index a38fdbb9a63..cd2d9c5811e 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/plugin-version-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/plugin-version-rule-processor.php @@ -2,15 +2,17 @@ /** * Plugin version rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\PluginVersionRuleProcessor; /** - * class WC_Admin_Tests_RemoteInboxNotifications_PluginVersionRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_PluginVersionRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_PluginVersionRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_PluginVersionRuleProcessor extends WC_Unit_Test_Case { /** * Test that the processor does not pass if the plugin is not activated. * diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/plugins-activated-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/plugins-activated-rule-processor.php similarity index 93% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/plugins-activated-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/plugins-activated-rule-processor.php index 41f478cedce..6250102b9bc 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/plugins-activated-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/plugins-activated-rule-processor.php @@ -2,16 +2,18 @@ /** * Plugins activated rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\PluginsActivatedRuleProcessor; use Automattic\WooCommerce\Admin\PluginsProvider\PluginsProviderInterface; /** - * class WC_Admin_Tests_RemoteInboxNotifications_PluginsActivatedRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_PluginsActivatedRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_PluginsActivatedRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_PluginsActivatedRuleProcessor extends WC_Unit_Test_Case { /** * Tests that the processor does not pass a plugins_activated rule with * no plugins to verify. diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/product-count-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/product-count-rule-processor.php similarity index 81% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/product-count-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/product-count-rule-processor.php index a3ca1a8cb74..2e4b0e1891f 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/product-count-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/product-count-rule-processor.php @@ -2,15 +2,17 @@ /** * Product count rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\ProductCountRuleProcessor; /** - * class WC_Admin_Tests_RemoteInboxNotifications_ProductCountRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_ProductCountRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_ProductCountRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_ProductCountRuleProcessor extends WC_Unit_Test_Case { /** * Get a product_count rule that passes when the product count is > 5. * diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/publish-after-time-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/publish-after-time-rule-processor.php similarity index 90% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/publish-after-time-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/publish-after-time-rule-processor.php index 9aec0acfcfd..4057a9f1daf 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/publish-after-time-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/publish-after-time-rule-processor.php @@ -2,16 +2,18 @@ /** * Publish after time rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\PublishAfterTimeRuleProcessor; use Automattic\WooCommerce\Admin\DateTimeProvider\DateTimeProviderInterface; /** - * class WC_Admin_Tests_RemoteInboxNotifications_PublishAfterTimeRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_PublishAfterTimeRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_PublishAfterTimeRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_PublishAfterTimeRuleProcessor extends WC_Unit_Test_Case { /** * Get the publish_after rule. * diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/publish-before-time-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/publish-before-time-rule-processor.php similarity index 90% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/publish-before-time-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/publish-before-time-rule-processor.php index 0234df59c87..60e23fbf5ec 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/publish-before-time-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/publish-before-time-rule-processor.php @@ -2,16 +2,18 @@ /** * Publish before time rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\PublishBeforeTimeRuleProcessor; use Automattic\WooCommerce\Admin\DateTimeProvider\DateTimeProviderInterface; /** - * class WC_Admin_Tests_RemoteInboxNotifications_PublishBeforeTimeRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_PublishBeforeTimeRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_PublishBeforeTimeRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_PublishBeforeTimeRuleProcessor extends WC_Unit_Test_Case { /** * Get the publish_before rule. * diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/stored-state-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/stored-state-rule-processor.php similarity index 97% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/stored-state-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/stored-state-rule-processor.php index 47567a6627d..ed1f616e67c 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/stored-state-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/stored-state-rule-processor.php @@ -2,15 +2,17 @@ /** * Stored state rule processor tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\StoredStateRuleProcessor; /** - * class WC_Admin_Tests_RemoteInboxNotifications_StoredStateRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_StoredStateRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_StoredStateRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_StoredStateRuleProcessor extends WC_Unit_Test_Case { /** * Empty $stored_state evaluates to false. * diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/total-payments-volume-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/total-payments-volume-processor.php similarity index 93% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/total-payments-volume-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/total-payments-volume-processor.php index d177dd8f975..7d11b482a78 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/total-payments-volume-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/total-payments-volume-processor.php @@ -5,13 +5,15 @@ * @package WooCommerce\Admin\Tests\RemoteInboxNotification */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\TotalPaymentsVolumeProcessor; use Automattic\WooCommerce\Admin\API\Reports\Revenue\Query as RevenueQuery; /** - * class WC_Admin_Tests_RemoteInboxNotifications_TotalPaymentsVolumeProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_TotalPaymentsVolumeProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_TotalPaymentsVolumeProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_TotalPaymentsVolumeProcessor extends WC_Unit_Test_Case { /** * Greater than 1000 total payments volume evaluates to false. diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/transformer-service.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformer-service.php similarity index 95% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/transformer-service.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformer-service.php index 224e7ba86e4..c5e17aee945 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/transformer-service.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformer-service.php @@ -2,16 +2,18 @@ /** * TransformerService tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\Transformers\ArrayKeys; use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\Transformers\TransformerService; /** - * class WC_Admin_Tests_RemoteInboxNotifications_TransformerService + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_TransformerService */ -class WC_Admin_Tests_RemoteInboxNotifications_TransformerService extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_TransformerService extends WC_Unit_Test_Case { /** * Test it creates a transformer with snake case 'use' value */ diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-column.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-column.php similarity index 82% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-column.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-column.php index e5303725b37..bb1ed192b74 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-column.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-column.php @@ -2,23 +2,25 @@ /** * ArrayColumn tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\Transformers\ArrayColumn; /** - * class WC_Admin_Tests_RemoteInboxNotifications_Transformers_ArrayColumn + * class WC_Admin_Tests_RemoteSpecs_Transformers_ArrayColumn */ -class WC_Admin_Tests_RemoteInboxNotifications_Transformers_ArrayColumn extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_Transformers_ArrayColumn extends WC_Unit_Test_Case { /** * Test validate method returns false when 'key' argument is missing */ public function test_validate_returns_false_when_key_argument_is_missing() { $array_column = new ArrayColumn(); $result = $array_column->validate( (object) array() ); - $this->assertFalse( false, $result ); + $this->assertFalse( $result ); } /** @@ -31,14 +33,14 @@ class WC_Admin_Tests_RemoteInboxNotifications_Transformers_ArrayColumn extends W 'key' => true, ) ); - $this->assertFalse( false, $result ); + $this->assertFalse( $result ); $result = $array_column->validate( (object) array( 'key' => array(), ) ); - $this->assertFalse( false, $result ); + $this->assertFalse( $result ); } /** diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-flatten.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-flatten.php similarity index 92% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-flatten.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-flatten.php index b02ba40fff9..5e5d76c2834 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-flatten.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-flatten.php @@ -2,9 +2,11 @@ /** * ArrayKeys tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\Transformers\ArrayFlatten; /** diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-keys.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-keys.php similarity index 77% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-keys.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-keys.php index b8e7d036192..f9e7b5df6c3 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-keys.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-keys.php @@ -2,15 +2,17 @@ /** * ArrayKeys tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\Transformers\ArrayKeys; /** - * class WC_Admin_Tests_RemoteInboxNotifications_Transformers_ArrayKeys + * class WC_Admin_Tests_RemoteSpecs_Transformers_ArrayKeys */ -class WC_Admin_Tests_RemoteInboxNotifications_Transformers_ArrayKeys extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_Transformers_ArrayKeys extends WC_Unit_Test_Case { /** * Test it returns default value when value is not an array */ diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-search.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-search.php similarity index 84% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-search.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-search.php index 46167d4b93e..17172146021 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-search.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-search.php @@ -2,22 +2,24 @@ /** * ArraySearch tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\Transformers\ArraySearch; /** - * class WC_Admin_Tests_RemoteInboxNotifications_Transformers_ArraySearch + * class WC_Admin_Tests_RemoteSpecs_Transformers_ArraySearch */ -class WC_Admin_Tests_RemoteInboxNotifications_Transformers_ArraySearch extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_Transformers_ArraySearch extends WC_Unit_Test_Case { /** * Test validate method returns false when 'value' argument is missing */ public function test_validate_returns_false_when_value_argument_is_missing() { $array_column = new ArraySearch(); $result = $array_column->validate( (object) array() ); - $this->assertFalse( false, $result ); + $this->assertFalse( $result ); } /** diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-values.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-values.php similarity index 77% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-values.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-values.php index efef87142be..18d96425cf4 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/array-values.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/array-values.php @@ -2,15 +2,17 @@ /** * ArrayValues tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\Transformers\ArrayValues; /** - * class WC_Admin_Tests_RemoteInboxNotifications_Transformers_ArrayValues + * class WC_Admin_Tests_RemoteSpecs_Transformers_ArrayValues */ -class WC_Admin_Tests_RemoteInboxNotifications_Transformers_ArrayValues extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_Transformers_ArrayValues extends WC_Unit_Test_Case { /** * Test it returns default value when value is not an array */ diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/count.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/count.php similarity index 76% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/count.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/count.php index 703a5804909..38f0f65ccb5 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/count.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/count.php @@ -2,15 +2,17 @@ /** * ArrayValues tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\Transformers\Count; /** - * class WC_Admin_Tests_RemoteInboxNotifications_Transformers_ArrayValues + * class WC_Admin_Tests_RemoteSpecs_Transformers_ArrayValues */ -class WC_Admin_Tests_RemoteInboxNotifications_Transformers_ArrayCount extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_Transformers_ArrayCount extends WC_Unit_Test_Case { /** * Test it returns default value when value is not an array. */ diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/dot-notation.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/dot-notation.php similarity index 88% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/dot-notation.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/dot-notation.php index 1e74188467a..32f76474014 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/dot-notation.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/dot-notation.php @@ -2,16 +2,18 @@ /** * DotNotation tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\Transformers\DotNotation; /** - * class WC_Admin_Tests_RemoteInboxNotifications_Transformers_DotNotation + * class WC_Admin_Tests_RemoteSpecs_Transformers_DotNotation */ -class WC_Admin_Tests_RemoteInboxNotifications_Transformers_DotNotation extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_Transformers_DotNotation extends WC_Unit_Test_Case { /** * Test validate method returns false when 'path' argument is missing @@ -19,7 +21,7 @@ class WC_Admin_Tests_RemoteInboxNotifications_Transformers_DotNotation extends W public function test_validate_returns_false_when_path_argument_is_missing() { $array_column = new DotNotation(); $result = $array_column->validate( (object) array() ); - $this->assertFalse( false, $result ); + $this->assertFalse( $result ); } /** diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/prepare-url.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/prepare-url.php similarity index 84% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/prepare-url.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/prepare-url.php index a6f9d42478a..0e8c2145358 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/Transformers/prepare-url.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/transformers/prepare-url.php @@ -2,15 +2,17 @@ /** * PrepareUrl tests. * - * @package WooCommerce\Admin\Tests\RemoteInboxNotifications + * @package WooCommerce\Admin\Tests\RemoteSpecs */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\Transformers\PrepareUrl; /** - * class WC_Admin_Tests_RemoteInboxNotifications_Transformers_PrepareUrl + * class WC_Admin_Tests_RemoteSpecs_Transformers_PrepareUrl */ -class WC_Admin_Tests_RemoteInboxNotifications_Transformers_PrepareUrl extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_Transformers_PrepareUrl extends WC_Unit_Test_Case { /** * Test it returns default value when url is not string. */ diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/wcadmin-active-for-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/wcadmin-active-for-rule-processor.php similarity index 93% rename from plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/wcadmin-active-for-rule-processor.php rename to plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/wcadmin-active-for-rule-processor.php index dca95841a92..8cd0349f66f 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-inbox-notifications/wcadmin-active-for-rule-processor.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/wcadmin-active-for-rule-processor.php @@ -5,12 +5,14 @@ * @package WooCommerce\Admin\Tests\RemoteInboxNotification */ +declare( strict_types = 1 ); + use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\WCAdminActiveForRuleProcessor; /** - * class WC_Admin_Tests_RemoteInboxNotifications_WCAdminActiveForRuleProcessor + * class WC_Admin_Tests_RemoteSpecs_RuleProcessors_WCAdminActiveForRuleProcessor */ -class WC_Admin_Tests_RemoteInboxNotifications_WCAdminActiveForRuleProcessor extends WC_Unit_Test_Case { +class WC_Admin_Tests_RemoteSpecs_RuleProcessors_WCAdminActiveForRuleProcessor extends WC_Unit_Test_Case { /** * Greater than 7 days evaluates to true * diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-brands-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-brands-test.php new file mode 100644 index 00000000000..5ca5953daf5 --- /dev/null +++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-brands-test.php @@ -0,0 +1,116 @@ +factory()->term->create( + array( + 'taxonomy' => 'product_brand', + 'name' => 'Blah_A', + ) + ); + $term_b_id = $this->factory()->term->create( + array( + 'taxonomy' => 'product_brand', + 'name' => 'Foo_A', + ) + ); + $term_c_id = $this->factory()->term->create( + array( + 'taxonomy' => 'product_brand', + 'name' => 'Blah_B', + ) + ); + + wp_set_post_terms( $simple_product->get_id(), array( $term_a_id, $term_b_id, $term_c_id ), 'product_brand' ); + + add_filter( + 'woocommerce_product_brand_filter_threshold', + function () { + return 3; + } + ); + + $brands_admin = new WC_Brands_Admin(); + ob_start(); + $brands_admin->render_product_brand_filter(); + $output = ob_get_contents(); + ob_end_clean(); + + $this->assertStringContainsString( + 'structured_data = new WC_Structured_Data(); + parent::setUp(); + } + + /** + * Test is_valid_gtin function + * + * @return void + */ + public function test_is_valid_gtin(): void { + + $valid_gtins = array( + '12345678', + '123456789012', + '1234567890123', + '12345678901234', + ); + + $invalid_gtins = array( + '', + null, + false, + 12345678, + 123.4e-5, + +1234567, + 'abcdefgh', + '-9999999', + '12-45-66', + '123', + '123456789012345', + '123456789', + '1234567890', + '12 34 56 78', + '12 34 56', + '+12345678', + '123.4e-5', + ); + + foreach ( $valid_gtins as $valid_gtin ) { + $this->assertTrue( $this->structured_data->is_valid_gtin( $valid_gtin ) ); + } + + foreach ( $invalid_gtins as $invalid_gtin ) { + $this->assertFalse( $this->structured_data->is_valid_gtin( $invalid_gtin ) ); + } + } + + /** + * Test prepare_gtin function + * + * @return void + */ + public function test_prepare_gtin(): void { + $this->assertEquals( $this->structured_data->prepare_gtin( '123-456-78' ), '12345678' ); + $this->assertEquals( $this->structured_data->prepare_gtin( '-123-456-78' ), '12345678' ); + $this->assertEquals( $this->structured_data->prepare_gtin( 'GTIN: 123-456-78' ), '12345678' ); + $this->assertEquals( $this->structured_data->prepare_gtin( '123 456 78' ), '12345678' ); + $this->assertEquals( $this->structured_data->prepare_gtin( null ), '' ); + $this->assertEquals( $this->structured_data->prepare_gtin( 'GTIN' ), '' ); + $this->assertEquals( $this->structured_data->prepare_gtin( 123 ), '' ); + $this->assertEquals( $this->structured_data->prepare_gtin( array( '123-456-78', '123-456-78' ) ), '' ); + $this->assertEquals( $this->structured_data->prepare_gtin( '+12345678' ), '12345678' ); + $this->assertEquals( $this->structured_data->prepare_gtin( '123.4e-5' ), '12345' ); + } +} diff --git a/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php b/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php index 1133880bad2..88218f1da75 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php @@ -228,7 +228,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging { array( 'feature' => 'woocommerce_core', 'severity' => 'error', - 'message' => 'Fatal error occurred at line 123 in **/wp-content/file.php', + 'message' => 'Fatal error occurred at line 123 in ./wp-content/file.php', 'tags' => array( 'woocommerce', 'php', 'tag1', 'tag2' ), ), ), @@ -236,7 +236,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging { 'error', 'Test error message', array( 'backtrace' => ABSPATH . 'wp-content/plugins/woocommerce/file.php' ), - array( 'trace' => '**/woocommerce/file.php' ), + array( 'trace' => './woocommerce/file.php' ), ), 'log with extra attributes' => array( 'error', @@ -254,6 +254,14 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging { ), ), ), + 'log with error file' => array( + 'error', + 'Test error message', + array( 'error' => array( 'file' => WC_ABSPATH . 'includes/class-wc-test.php' ) ), + array( + 'file' => './woocommerce/includes/class-wc-test.php', + ), + ), ); } @@ -348,7 +356,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging { $setup( $this ); - $result = $this->invoke_private_method( $this->sut, 'should_handle', array( $level, 'Test message', array() ) ); + $result = $this->invoke_private_method( $this->sut, 'should_handle', array( $level, 'Test message', array( 'remote-logging' => true ) ) ); $this->assertEquals( $expected, $result ); } @@ -377,6 +385,14 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging { ); } + /** + * @testdox Test should_handle returns false without remote-logging context + */ + public function test_should_handle_no_remote_logging_context() { + $result = $this->invoke_private_method( $this->sut, 'should_handle', array( 'error', 'Test message', array() ) ); + $this->assertFalse( $result, 'should_handle should return false without remote-logging context' ); + } + /** * @testdox handle method applies filter and doesn't send logs when filtered to null */ @@ -390,7 +406,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging { add_filter( 'woocommerce_remote_logger_formatted_log_data', fn() => null, 10, 4 ); add_filter( 'pre_http_request', fn() => $this->fail( 'wp_safe_remote_post should not be called' ), 10, 3 ); - $this->assertFalse( $this->sut->handle( time(), 'error', 'Test message', array() ) ); + $this->assertFalse( $this->sut->handle( time(), 'error', 'Test message', array( 'remote-logging' => true ) ) ); } /** @@ -404,7 +420,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging { $this->sut->set_is_dev_or_local( true ); $this->sut->method( 'is_remote_logging_allowed' )->willReturn( true ); - $this->assertFalse( $this->sut->handle( time(), 'error', 'Test message', array() ) ); + $this->assertFalse( $this->sut->handle( time(), 'error', 'Test message', array( 'remote-logging' => true ) ) ); } /** @@ -435,7 +451,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging { 3 ); - $this->assertTrue( $this->sut->handle( time(), 'critical', 'Test message', array() ) ); + $this->assertTrue( $this->sut->handle( time(), 'critical', 'Test message', array( 'remote-logging' => true ) ) ); $this->assertTrue( WC_Rate_Limiter::retried_too_soon( RemoteLogger::RATE_LIMIT_ID ) ); } @@ -462,7 +478,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging { 3 ); - $this->assertFalse( $this->sut->handle( time(), 'critical', 'Test message', array() ) ); + $this->assertFalse( $this->sut->handle( time(), 'critical', 'Test message', array( 'remote-logging' => true ) ) ); $this->assertTrue( WC_Rate_Limiter::retried_too_soon( RemoteLogger::RATE_LIMIT_ID ) ); } @@ -528,7 +544,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging { */ public function test_sanitize() { $message = WC_ABSPATH . 'includes/class-wc-test.php on line 123'; - $expected = '**/woocommerce/includes/class-wc-test.php on line 123'; + $expected = './woocommerce/includes/class-wc-test.php on line 123'; $result = $this->invoke_private_method( $this->sut, 'sanitize', array( $message ) ); $this->assertEquals( $expected, $result ); } @@ -541,7 +557,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging { WC_ABSPATH . 'includes/class-wc-test.php:123', ABSPATH . 'wp-includes/plugin.php:456', ); - $expected = "**/woocommerce/includes/class-wc-test.php:123\n**/wp-includes/plugin.php:456"; + $expected = "./woocommerce/includes/class-wc-test.php:123\n./wp-includes/plugin.php:456"; $result = $this->invoke_private_method( $this->sut, 'sanitize_trace', array( $trace ) ); $this->assertEquals( $expected, $result ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38a0e0fbccb..a8837891f4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -256,7 +256,7 @@ importers: version: 10.5.0(sass@1.69.5)(webpack@5.89.0(webpack-cli@3.3.12)) ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) + version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) typescript: specifier: ^5.3.3 version: 5.3.3 @@ -292,7 +292,7 @@ importers: version: 2.3.2 debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@9.4.0) + version: 4.3.4(supports-color@8.1.1) dompurify: specifier: ^2.4.7 version: 2.4.7 @@ -453,7 +453,7 @@ importers: version: 27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)) ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) + version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) typescript: specifier: ^5.3.3 version: 5.3.3 @@ -2866,7 +2866,7 @@ importers: dependencies: debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@9.4.0) + version: 4.3.4(supports-color@8.1.1) devDependencies: '@babel/core': specifier: ^7.23.5 @@ -2975,7 +2975,7 @@ importers: version: 1.2.5(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2) debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@9.4.0) + version: 4.3.4(supports-color@8.1.1) prop-types: specifier: ^15.8.1 version: 15.8.1 @@ -2991,10 +2991,10 @@ importers: devDependencies: '@babel/preset-react': specifier: 7.23.3 - version: 7.23.3(@babel/core@7.25.2) + version: 7.23.3(@babel/core@7.24.7) '@babel/preset-typescript': specifier: 7.23.2 - version: 7.23.2(@babel/core@7.25.2) + version: 7.23.2(@babel/core@7.24.7) '@svgr/webpack': specifier: ^8.1.0 version: 8.1.0(typescript@5.3.3) @@ -3045,10 +3045,10 @@ importers: version: 2.17.0(wp-prettier@2.8.5) '@wordpress/scripts': specifier: ^19.2.4 - version: 19.2.4(@babel/core@7.25.2)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4) + version: 19.2.4(@babel/core@7.24.7)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4) babel-jest: specifier: ~27.5.1 - version: 27.5.1(@babel/core@7.25.2) + version: 27.5.1(@babel/core@7.24.7) eslint: specifier: ^8.55.0 version: 8.55.0 @@ -3352,7 +3352,7 @@ importers: version: 3.34.0 debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@9.4.0) + version: 4.3.4(supports-color@8.1.1) dompurify: specifier: ^2.4.7 version: 2.4.7 @@ -3906,7 +3906,7 @@ importers: version: 2.17.0(wp-prettier@2.8.5) '@wordpress/scripts': specifier: ^19.2.4 - version: 19.2.4(@babel/core@7.25.2)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4) + version: 19.2.4(@babel/core@7.25.2)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4) eslint: specifier: ^8.55.0 version: 8.55.0 @@ -3929,7 +3929,7 @@ importers: plugins/woocommerce-blocks: dependencies: '@ariakit/react': - specifier: ^0.4.4 + specifier: ^0.4.5 version: 0.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@dnd-kit/core': specifier: ^6.1.0 @@ -4025,8 +4025,8 @@ importers: specifier: 3.0.4 version: 3.0.4(react@18.3.1) postcode-validator: - specifier: 3.8.15 - version: 3.8.15 + specifier: 3.9.2 + version: 3.9.2 preact: specifier: ^10.19.3 version: 10.19.3 @@ -4642,7 +4642,7 @@ importers: version: 27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)) ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) + version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3) @@ -4658,6 +4658,12 @@ importers: tools/compare-perf: dependencies: + '@tsconfig/node16': + specifier: ^1.0.4 + version: 1.0.4 + '@types/node': + specifier: ^16.18.68 + version: 16.18.68 '@wordpress/env': specifier: ^10.1.0 version: 10.5.0 @@ -4673,6 +4679,12 @@ importers: simple-git: specifier: ^3.21.0 version: 3.21.0 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3) + tslib: + specifier: ^2.6.2 + version: 2.6.3 tools/monorepo-merge: dependencies: @@ -4827,7 +4839,7 @@ importers: version: 1.2.2 ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) + version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) ts-loader: specifier: ^9.5.1 version: 9.5.1(typescript@5.3.3)(webpack@5.89.0(webpack-cli@3.3.12)) @@ -20611,8 +20623,8 @@ packages: resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} engines: {node: '>=0.10.0'} - postcode-validator@3.8.15: - resolution: {integrity: sha512-B2oeZ4E9D7JBHk0GEo0lv9aqHGd6vv+VsWoTG6Jt0tMtc5RUzSXOSQixBZgAAn4A/Dbajp8GbwzMtUkkyYw2Ig==} + postcode-validator@3.9.2: + resolution: {integrity: sha512-C+oaXif+z+mAN1EWDZG/EM2dnrUxRQR0gpMq8VeLSZwHuT3Bqo4hR3+k7OaEevPl0VK/7W27JwEQb24CbdtzuQ==} postcss-calc@7.0.5: resolution: {integrity: sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==} @@ -21617,7 +21629,7 @@ packages: engines: {node: '>=18'} hasBin: true peerDependencies: - react: ^17.0.2 + react: 18.2.0 react-number-format@4.9.3: resolution: {integrity: sha512-am1A1xYAbENuKJ+zpM7V+B1oRTSeOHYltqVKExznIVFweBzhLmOBmyb1DfIKjHo90E0bo1p3nzVJ2NgS5xh+sQ==} @@ -25591,7 +25603,7 @@ snapshots: '@wordpress/primitives': 3.55.0 '@wordpress/react-i18n': 3.55.0 classnames: 2.3.2 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) react: 17.0.2 react-dom: 17.0.2(react@17.0.2) react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -25616,7 +25628,7 @@ snapshots: '@wordpress/primitives': 3.55.0 '@wordpress/react-i18n': 3.55.0 classnames: 2.3.2 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) react: 17.0.2 react-dom: 17.0.2(react@17.0.2) react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -25720,7 +25732,7 @@ snapshots: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 lodash: 4.17.21 @@ -25743,7 +25755,7 @@ snapshots: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -25763,7 +25775,7 @@ snapshots: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -25826,6 +25838,14 @@ snapshots: eslint-visitor-keys: 2.1.0 semver: 6.3.1 + '@babel/eslint-parser@7.23.3(@babel/core@7.24.7)(eslint@7.32.0)': + dependencies: + '@babel/core': 7.24.7 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 7.32.0 + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 + '@babel/eslint-parser@7.23.3(@babel/core@7.25.2)(eslint@7.32.0)': dependencies: '@babel/core': 7.25.2 @@ -25990,6 +26010,19 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.7 semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.23.6(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.24.7 + semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.23.6(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -26981,7 +27014,6 @@ snapshots: dependencies: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.8 - optional: true '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.25.2)': dependencies: @@ -27218,6 +27250,11 @@ snapshots: '@babel/core': 7.23.2 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -28829,6 +28866,11 @@ snapshots: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-react-display-name@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-react-display-name@7.24.7(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -28848,6 +28890,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/plugin-transform-react-jsx': 7.25.2(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -28939,6 +28988,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/types': 7.25.2 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -28962,6 +29022,12 @@ snapshots: '@babel/helper-annotate-as-pure': 7.24.7 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-react-pure-annotations@7.23.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-react-pure-annotations@7.23.3(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -29274,13 +29340,13 @@ snapshots: '@babel/helper-plugin-utils': 7.24.8 '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.2) - '@babel/plugin-transform-typescript@7.23.6(@babel/core@7.25.2)': + '@babel/plugin-transform-typescript@7.23.6(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.24.7 '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-create-class-features-plugin': 7.23.6(@babel/core@7.25.2) + '@babel/helper-create-class-features-plugin': 7.23.6(@babel/core@7.24.7) '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.24.7) '@babel/plugin-transform-typescript@7.25.2(@babel/core@7.12.9)': dependencies: @@ -30021,8 +30087,8 @@ snapshots: '@babel/preset-flow@7.23.3(@babel/core@7.23.2)': dependencies: '@babel/core': 7.23.2 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-validator-option': 7.24.8 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.23.5 '@babel/plugin-transform-flow-strip-types': 7.23.3(@babel/core@7.23.2) '@babel/preset-flow@7.23.3(@babel/core@7.23.5)': @@ -30107,6 +30173,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-react@7.23.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-validator-option': 7.24.8 + '@babel/plugin-transform-react-display-name': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx': 7.25.2(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx-development': 7.22.5(@babel/core@7.24.7) + '@babel/plugin-transform-react-pure-annotations': 7.23.3(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + '@babel/preset-react@7.23.3(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -30130,14 +30208,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.23.2(@babel/core@7.25.2)': + '@babel/preset-typescript@7.23.2(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.25.2) - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-typescript': 7.23.6(@babel/core@7.25.2) + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.24.7) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.24.7) + '@babel/plugin-transform-typescript': 7.23.6(@babel/core@7.24.7) transitivePeerDependencies: - supports-color @@ -32095,7 +32173,7 @@ snapshots: '@oclif/color': 1.0.13 '@oclif/core': 2.15.0(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3) chalk: 4.1.2 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) fs-extra: 9.1.0 http-call: 5.3.0 load-json-file: 5.3.0 @@ -32584,7 +32662,7 @@ snapshots: '@puppeteer/browsers@1.4.6(typescript@5.3.2)': dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.0 @@ -32598,7 +32676,7 @@ snapshots: '@puppeteer/browsers@1.4.6(typescript@5.3.3)': dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.0 @@ -32612,7 +32690,7 @@ snapshots: '@puppeteer/browsers@1.9.0': dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.1 @@ -38337,7 +38415,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.56.0 '@typescript-eslint/type-utils': 5.56.0(eslint@8.55.0)(typescript@5.3.2) '@typescript-eslint/utils': 5.56.0(eslint@8.55.0)(typescript@5.3.2) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 grapheme-splitter: 1.0.4 ignore: 5.3.0 @@ -38356,7 +38434,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.55.0)(typescript@5.3.2) '@typescript-eslint/utils': 5.62.0(eslint@8.55.0)(typescript@5.3.2) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -38375,7 +38453,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.55.0)(typescript@5.3.3) '@typescript-eslint/utils': 5.62.0(eslint@8.55.0)(typescript@5.3.3) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -38455,7 +38533,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.56.0 '@typescript-eslint/types': 5.56.0 '@typescript-eslint/typescript-estree': 5.56.0(typescript@5.3.2) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 optionalDependencies: typescript: 5.3.2 @@ -39006,7 +39084,7 @@ snapshots: webpack: 5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@4.10.0) webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack-dev-server@4.15.1)(webpack@5.91.0) - '@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0))': + '@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0(webpack@5.89.0))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0))': dependencies: webpack: 5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0) webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0) @@ -39026,7 +39104,7 @@ snapshots: envinfo: 7.13.0 webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack-dev-server@4.15.1)(webpack@5.91.0) - '@webpack-cli/info@1.5.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0))': + '@webpack-cli/info@1.5.0(webpack-cli@4.10.0(webpack@5.89.0))': dependencies: envinfo: 7.13.0 webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0) @@ -42101,6 +42179,33 @@ snapshots: - supports-color - typescript + '@wordpress/eslint-plugin@9.3.0(@babel/core@7.24.7)(eslint@7.32.0)(typescript@5.3.3)': + dependencies: + '@babel/eslint-parser': 7.23.3(@babel/core@7.24.7)(eslint@7.32.0) + '@typescript-eslint/eslint-plugin': 4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.3.3))(eslint@7.32.0)(typescript@5.3.3) + '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.3.3) + '@wordpress/prettier-config': 1.4.0(wp-prettier@2.2.1-beta-1) + cosmiconfig: 7.1.0 + eslint: 7.32.0 + eslint-config-prettier: 7.2.0(eslint@7.32.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.3.3))(eslint@7.32.0) + eslint-plugin-jest: 24.7.0(@typescript-eslint/eslint-plugin@4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.3.3))(eslint@7.32.0)(typescript@5.3.3))(eslint@7.32.0)(typescript@5.3.3) + eslint-plugin-jsdoc: 36.1.1(eslint@7.32.0) + eslint-plugin-jsx-a11y: 6.8.0(eslint@7.32.0) + eslint-plugin-prettier: 3.4.1(eslint-config-prettier@7.2.0(eslint@7.32.0))(eslint@7.32.0)(wp-prettier@2.2.1-beta-1) + eslint-plugin-react: 7.33.2(eslint@7.32.0) + eslint-plugin-react-hooks: 4.6.0(eslint@7.32.0) + globals: 12.4.0 + prettier: wp-prettier@2.2.1-beta-1 + requireindex: 1.2.0 + optionalDependencies: + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + '@wordpress/eslint-plugin@9.3.0(@babel/core@7.25.2)(eslint@7.32.0)(typescript@5.3.3)': dependencies: '@babel/eslint-parser': 7.23.3(@babel/core@7.25.2)(eslint@7.32.0) @@ -42455,6 +42560,20 @@ snapshots: - react-dom - supports-color + '@wordpress/jest-preset-default@7.1.3(@babel/core@7.24.7)(jest@26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + '@wojtekmaj/enzyme-adapter-react-17': 0.6.7(enzyme@3.11.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@wordpress/jest-console': 4.1.1(jest@26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))) + babel-jest: 26.6.3(@babel/core@7.24.7) + enzyme: 3.11.0 + enzyme-to-json: 3.6.2(enzyme@3.11.0) + jest: 26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)) + transitivePeerDependencies: + - '@babel/core' + - react + - react-dom + - supports-color + '@wordpress/jest-preset-default@7.1.3(@babel/core@7.25.2)(jest@26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@wojtekmaj/enzyme-adapter-react-17': 0.6.7(enzyme@3.11.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -43204,7 +43323,86 @@ snapshots: - utf-8-validate - webpack-command - '@wordpress/scripts@19.2.4(@babel/core@7.25.2)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4)': + '@wordpress/scripts@19.2.4(@babel/core@7.24.7)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4)': + dependencies: + '@svgr/webpack': 5.5.0 + '@wordpress/babel-preset-default': 6.17.0 + '@wordpress/browserslist-config': 4.1.3 + '@wordpress/dependency-extraction-webpack-plugin': 3.7.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + '@wordpress/eslint-plugin': 9.3.0(@babel/core@7.24.7)(eslint@7.32.0)(typescript@5.3.3) + '@wordpress/jest-preset-default': 7.1.3(@babel/core@7.24.7)(jest@26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@wordpress/npm-package-json-lint-config': 4.32.0(npm-package-json-lint@5.4.2) + '@wordpress/postcss-plugins-preset': 3.6.1(postcss@8.4.32) + '@wordpress/prettier-config': 1.4.0(wp-prettier@2.2.1-beta-1) + '@wordpress/stylelint-config': 19.1.0(stylelint@13.13.1) + babel-jest: 26.6.3(@babel/core@7.24.7) + babel-loader: 8.3.0(@babel/core@7.24.7)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + browserslist: 4.19.3 + chalk: 4.1.2 + check-node-version: 4.2.1 + clean-webpack-plugin: 3.0.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + cross-spawn: 5.1.0 + css-loader: 6.8.1(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + cssnano: 5.1.12(postcss@8.4.32) + cwd: 0.10.0 + dir-glob: 3.0.1 + eslint: 7.32.0 + eslint-plugin-markdown: 2.2.1(eslint@7.32.0) + expect-puppeteer: 4.4.0 + filenamify: 4.3.0 + jest: 26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)) + jest-circus: 26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)) + jest-dev-server: 5.0.3(debug@4.3.4) + jest-environment-node: 26.6.2 + markdownlint: 0.23.1 + markdownlint-cli: 0.27.1 + merge-deep: 3.0.3 + mini-css-extract-plugin: 2.7.6(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + minimist: 1.2.8 + npm-package-json-lint: 5.4.2 + postcss: 8.4.32 + postcss-loader: 6.2.1(postcss@8.4.32)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + prettier: wp-prettier@2.2.1-beta-1 + puppeteer-core: 10.4.0 + read-pkg-up: 1.0.1 + resolve-bin: 0.4.3 + sass: 1.69.5 + sass-loader: 12.6.0(sass@1.69.5)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + source-map-loader: 3.0.2(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + stylelint: 13.13.1 + terser-webpack-plugin: 5.3.6(uglify-js@3.17.4)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + webpack: 5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0) + webpack-bundle-analyzer: 4.7.0 + webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0) + webpack-livereload-plugin: 3.0.2(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + transitivePeerDependencies: + - '@babel/core' + - '@swc/core' + - '@webpack-cli/generators' + - '@webpack-cli/migrate' + - bufferutil + - canvas + - debug + - esbuild + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - fibers + - file-loader + - node-sass + - postcss-jsx + - postcss-markdown + - react + - react-dom + - sass-embedded + - supports-color + - ts-node + - typescript + - uglify-js + - utf-8-validate + - webpack-dev-server + + '@wordpress/scripts@19.2.4(@babel/core@7.25.2)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4)': dependencies: '@svgr/webpack': 5.5.0 '@wordpress/babel-preset-default': 6.17.0 @@ -44556,6 +44754,20 @@ snapshots: transitivePeerDependencies: - supports-color + babel-jest@26.6.3(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@jest/transform': 26.6.2 + '@jest/types': 26.6.2 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 26.6.2(@babel/core@7.24.7) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + babel-jest@26.6.3(@babel/core@7.25.2): dependencies: '@babel/core': 7.25.2 @@ -44584,6 +44796,20 @@ snapshots: transitivePeerDependencies: - supports-color + babel-jest@27.5.1(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@jest/transform': 27.5.1 + '@jest/types': 27.5.1 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 27.5.1(@babel/core@7.24.7) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + babel-jest@27.5.1(@babel/core@7.25.2): dependencies: '@babel/core': 7.25.2 @@ -44688,6 +44914,15 @@ snapshots: schema-utils: 2.7.1 webpack: 5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@4.10.0) + babel-loader@8.3.0(@babel/core@7.24.7)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)): + dependencies: + '@babel/core': 7.24.7 + find-cache-dir: 3.3.2 + loader-utils: 2.0.4 + make-dir: 3.1.0 + schema-utils: 2.7.1 + webpack: 5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0) + babel-loader@8.3.0(@babel/core@7.25.2)(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))): dependencies: '@babel/core': 7.25.2 @@ -45131,7 +45366,6 @@ snapshots: '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.7) - optional: true babel-preset-current-node-syntax@1.0.1(@babel/core@7.25.2): dependencies: @@ -45179,6 +45413,12 @@ snapshots: babel-plugin-jest-hoist: 26.6.2 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.5) + babel-preset-jest@26.6.2(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + babel-plugin-jest-hoist: 26.6.2 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.7) + babel-preset-jest@26.6.2(@babel/core@7.25.2): dependencies: '@babel/core': 7.25.2 @@ -45191,6 +45431,12 @@ snapshots: babel-plugin-jest-hoist: 27.5.1 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.5) + babel-preset-jest@27.5.1(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + babel-plugin-jest-hoist: 27.5.1 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.7) + babel-preset-jest@27.5.1(@babel/core@7.25.2): dependencies: '@babel/core': 7.25.2 @@ -48161,7 +48407,7 @@ snapshots: eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.56.0(eslint@8.55.0)(typescript@5.3.2))(eslint-import-resolver-webpack@0.13.2)(eslint-plugin-import@2.28.1)(eslint@8.55.0): dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) enhanced-resolve: 5.15.0 eslint: 8.55.0 eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.56.0(eslint@8.55.0)(typescript@5.3.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.56.0(eslint@8.55.0)(typescript@5.3.2))(eslint-import-resolver-webpack@0.13.2)(eslint-plugin-import@2.28.1)(eslint@8.55.0))(eslint-import-resolver-webpack@0.13.2(eslint-plugin-import@2.28.1)(webpack@5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)))(eslint@8.55.0) @@ -48178,7 +48424,7 @@ snapshots: eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.55.0)(typescript@5.3.3))(eslint-import-resolver-webpack@0.13.8)(eslint-plugin-import@2.29.0)(eslint@8.55.0): dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) enhanced-resolve: 5.15.0 eslint: 8.55.0 eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.55.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.55.0)(typescript@5.3.3))(eslint-import-resolver-webpack@0.13.8)(eslint-plugin-import@2.29.0)(eslint@8.55.0))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.0)(webpack@5.89.0(webpack-cli@4.10.0)))(eslint@8.55.0) @@ -49586,7 +49832,7 @@ snapshots: follow-redirects@1.15.6(debug@4.3.4): optionalDependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) follow-redirects@1.5.10: dependencies: @@ -52779,7 +53025,9 @@ snapshots: pretty-format: 24.9.0 throat: 4.1.0 transitivePeerDependencies: + - bufferutil - supports-color + - utf-8-validate jest-jasmine2@25.5.4: dependencies: @@ -54451,7 +54699,7 @@ snapshots: chalk: 5.2.0 cli-truncate: 3.1.0 commander: 10.0.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) execa: 7.2.0 lilconfig: 2.1.0 listr2: 5.0.8(enquirer@2.4.1) @@ -55748,7 +55996,7 @@ snapshots: dependencies: carlo: 0.9.46 chokidar: 3.5.3 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) isbinaryfile: 3.0.3 mime: 2.6.0 opn: 5.5.0 @@ -56276,7 +56524,7 @@ snapshots: '@oclif/plugin-warn-if-update-available': 2.1.1(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3) aws-sdk: 2.1515.0 concurrently: 7.6.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) find-yarn-workspace-root: 2.0.0 fs-extra: 8.1.0 github-slugger: 1.5.0 @@ -56921,7 +57169,7 @@ snapshots: posix-character-classes@0.1.1: {} - postcode-validator@3.8.15: {} + postcode-validator@3.9.2: {} postcss-calc@7.0.5: dependencies: @@ -57786,7 +58034,7 @@ snapshots: puppeteer-core@13.7.0(encoding@0.1.13): dependencies: cross-fetch: 3.1.5(encoding@0.1.13) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.981744 extract-zip: 2.0.1 https-proxy-agent: 5.0.1 @@ -57825,7 +58073,7 @@ snapshots: '@puppeteer/browsers': 1.4.6(typescript@5.3.2) chromium-bidi: 0.4.16(devtools-protocol@0.0.1147663) cross-fetch: 4.0.0(encoding@0.1.13) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.1147663 ws: 8.13.0 optionalDependencies: @@ -57841,7 +58089,7 @@ snapshots: '@puppeteer/browsers': 1.4.6(typescript@5.3.3) chromium-bidi: 0.4.16(devtools-protocol@0.0.1147663) cross-fetch: 4.0.0(encoding@0.1.13) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.1147663 ws: 8.13.0 optionalDependencies: @@ -57857,7 +58105,7 @@ snapshots: '@puppeteer/browsers': 1.9.0 chromium-bidi: 0.5.1(devtools-protocol@0.0.1203626) cross-fetch: 4.0.0(encoding@0.1.13) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.1203626 ws: 8.14.2 transitivePeerDependencies: @@ -57893,7 +58141,7 @@ snapshots: puppeteer@17.1.3(encoding@0.1.13): dependencies: cross-fetch: 3.1.5(encoding@0.1.13) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.1036444 extract-zip: 2.0.1 https-proxy-agent: 5.0.1 @@ -58221,7 +58469,7 @@ snapshots: react-docgen-typescript-plugin@1.0.5(typescript@5.3.2)(webpack@5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)): dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -60098,7 +60346,7 @@ snapshots: dependencies: '@kwsites/file-exists': 1.1.1 '@kwsites/promise-deferred': 1.1.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -60847,7 +61095,7 @@ snapshots: colord: 2.9.3 cosmiconfig: 7.1.0 css-functions-list: 3.2.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) fast-glob: 3.3.2 fastest-levenshtein: 1.0.16 file-entry-cache: 6.0.1 @@ -61651,7 +61899,7 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.24.7) - ts-jest@29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3): + ts-jest@29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -62757,8 +63005,8 @@ snapshots: webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - '@webpack-cli/info': 1.5.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0)) + '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0(webpack@5.89.0))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + '@webpack-cli/info': 1.5.0(webpack-cli@4.10.0(webpack@5.89.0)) '@webpack-cli/serve': 1.7.0(webpack-cli@4.10.0(webpack@5.89.0)) colorette: 2.0.20 commander: 7.2.0 diff --git a/tools/compare-perf/index.js b/tools/compare-perf/index.js index d14680f9bf0..beca567b34c 100755 --- a/tools/compare-perf/index.js +++ b/tools/compare-perf/index.js @@ -15,12 +15,17 @@ const catchException = ( command ) => { }; }; -const ciOption = [ '-c, --ci', 'Run in CI (non interactive)' ]; - program .command( 'compare-performance [branches...]' ) .alias( 'perf' ) - .option( ...ciOption ) + .option( + '-c, --ci', + 'Run in CI (non interactive)' + ) + .option( + '--skip-benchmarking', + 'Skips benchmarking and gets straight to reporting phase (tests results already available)' + ) .option( '--rounds ', 'Run each test suite this many times for each branch; results are summarized, default = 1' diff --git a/tools/compare-perf/package.json b/tools/compare-perf/package.json index 430d64146e5..424decbe36d 100644 --- a/tools/compare-perf/package.json +++ b/tools/compare-perf/package.json @@ -7,15 +7,19 @@ "license": "GPLv2", "repository": "woocommerce/woocommerce", "scripts": { - "compare": "node index.js", + "compare": "node -r ts-node/register index.js", "log": "node log-to-codevitals.js" }, "dependencies": { + "@types/node": "^16.18.68", + "@tsconfig/node16": "^1.0.4", "@wordpress/env": "^10.1.0", "commander": "9.5.0", "chalk": "^4.1.2", "inquirer": "^7.1.0", - "simple-git": "^3.21.0" + "simple-git": "^3.21.0", + "ts-node": "^10.9.2", + "tslib": "^2.6.2" }, "engines": { "node": "^20.11.1", diff --git a/tools/compare-perf/performance.js b/tools/compare-perf/performance.js index 47315f382ef..527ad2a914e 100644 --- a/tools/compare-perf/performance.js +++ b/tools/compare-perf/performance.js @@ -13,71 +13,27 @@ const formats = { }; const { runShellScript, - readJSONFile, askForConfirmation, getFilesFromDir, + logAtIndent, + sanitizeBranchName, } = require( './utils' ); const config = require( './config' ); +const { processPerformanceReports } = require( './process-reports.ts' ); const ARTIFACTS_PATH = process.env.WP_ARTIFACTS_PATH || path.join( process.cwd(), 'artifacts' ); -const RESULTS_FILE_SUFFIX = '.performance-results.json'; /** * @typedef WPPerformanceCommandOptions * - * @property {boolean=} ci Run on CI. - * @property {number=} rounds Run each test suite this many times for each branch. - * @property {string=} testsBranch The branch whose performance test files will be used for testing. - * @property {string=} wpVersion The WordPress version to be used as the base install for testing. + * @property {boolean=} ci Run on CI. + * @property {number=} rounds Run each test suite this many times for each branch. + * @property {string=} testsBranch The branch whose performance test files will be used for testing. + * @property {boolean=} skipBenchmarking Skip benchmarking and get to report processing (reports supplied from outside). + * @property {string=} wpVersion The WordPress version to be used as the base install for testing. */ -/** - * A logging helper for printing steps and their substeps. - * - * @param {number} indent Value to indent the log. - * @param {any} msg Message to log. - * @param {...any} args Rest of the arguments to pass to console.log. - */ -function logAtIndent( indent, msg, ...args ) { - const prefix = indent === 0 ? '▶ ' : '> '; - const newline = indent === 0 ? '\n' : ''; - return console.log( - newline + ' '.repeat( indent ) + prefix + msg, - ...args - ); -} - -/** - * Sanitizes branch name to be used in a path or a filename. - * - * @param {string} branch - * - * @return {string} Sanitized branch name. - */ -function sanitizeBranchName( branch ) { - return branch.replace( /[^a-zA-Z0-9-]/g, '-' ); -} - -/** - * Computes the median number from an array numbers. - * - * @param {number[]} array - * - * @return {number|undefined} Median value or undefined if array empty. - */ -function median( array ) { - if ( ! array || ! array.length ) return undefined; - - const numbers = [ ...array ].sort( ( a, b ) => a - b ); - const middleIndex = Math.floor( numbers.length / 2 ); - - if ( numbers.length % 2 === 0 ) { - return ( numbers[ middleIndex - 1 ] + numbers[ middleIndex ] ) / 2; - } - return numbers[ middleIndex ]; -} - /** * Runs the performance tests on the current branch. * @@ -106,6 +62,7 @@ async function runTestSuite( testSuite, testRunnerDir, runKey ) { */ async function runPerformanceTests( branches, options ) { const runningInCI = !! process.env.CI || !! options.ci; + const skipBenchmarking = !! options.skipBenchmarking; const TEST_ROUNDS = options.rounds || 1; // The default value doesn't work because commander provides an array. @@ -129,6 +86,20 @@ async function runPerformanceTests( branches, options ) { await askForConfirmation( 'Ready to go? ' ); } + if ( skipBenchmarking ) { + // When benchmarking is skipped, it's expected that artifacts folder contains reports for the branches. + // If so, we'll process reports and pick test suites as per current state of codebase. + const testSuites = getFilesFromDir( + path.resolve( __dirname, '../..' ) + config.testsPath + ).map( ( file ) => { + logAtIndent( 1, 'Found:', formats.success( file ) ); + return path.basename( file, '.spec.js' ); + } ); + + await processPerformanceReports( testSuites, branches ); + return; + } + logAtIndent( 0, 'Setting up' ); /** @@ -140,7 +111,6 @@ async function runPerformanceTests( branches, options ) { } const baseDir = path.join( os.tmpdir(), 'wp-performance-tests' ); - if ( fs.existsSync( baseDir ) ) { logAtIndent( 1, 'Removing existing files' ); fs.rmSync( baseDir, { recursive: true } ); @@ -350,82 +320,7 @@ async function runPerformanceTests( branches, options ) { } } - logAtIndent( 0, 'Calculating results' ); - - const resultFiles = getFilesFromDir( ARTIFACTS_PATH ).filter( ( file ) => - file.endsWith( RESULTS_FILE_SUFFIX ) - ); - /** @type {Record>>} */ - const results = {}; - - // Calculate medians from all rounds. - for ( const testSuite of testSuites ) { - logAtIndent( 1, 'Test suite:', formats.success( testSuite ) ); - - results[ testSuite ] = {}; - for ( const branch of branches ) { - const sanitizedBranchName = sanitizeBranchName( branch ); - const resultsRounds = resultFiles - .filter( ( file ) => - file.includes( - `/${ testSuite }_${ sanitizedBranchName }_round-` - ) - ) - .map( ( file ) => { - logAtIndent( 2, 'Reading from:', formats.success( file ) ); - return readJSONFile( file ); - } ); - - const metrics = Object.keys( resultsRounds[ 0 ] ); - results[ testSuite ][ branch ] = {}; - - for ( const metric of metrics ) { - const values = resultsRounds - .map( ( round ) => round[ metric ] ) - .filter( ( value ) => typeof value === 'number' ); - - const value = median( values ); - if ( value !== undefined ) { - results[ testSuite ][ branch ][ metric ] = value; - } - } - } - const calculatedResultsPath = path.join( - ARTIFACTS_PATH, - testSuite + RESULTS_FILE_SUFFIX - ); - - logAtIndent( - 2, - 'Saving curated results to:', - formats.success( calculatedResultsPath ) - ); - fs.writeFileSync( - calculatedResultsPath, - JSON.stringify( results[ testSuite ], null, 2 ) - ); - } - - logAtIndent( 0, 'Printing results' ); - - for ( const testSuite of testSuites ) { - logAtIndent( 0, formats.success( testSuite ) ); - - // Invert the results so we can display them in a table. - /** @type {Record>} */ - const invertedResult = {}; - for ( const [ branch, metrics ] of Object.entries( - results[ testSuite ] - ) ) { - for ( const [ metric, value ] of Object.entries( metrics ) ) { - invertedResult[ metric ] = invertedResult[ metric ] || {}; - invertedResult[ metric ][ branch ] = `${ value } ms`; - } - } - - // Print the results. - console.table( invertedResult ); - } + await processPerformanceReports( testSuites, branches ); } module.exports = { diff --git a/tools/compare-perf/process-reports.ts b/tools/compare-perf/process-reports.ts new file mode 100644 index 00000000000..ec2684161e9 --- /dev/null +++ b/tools/compare-perf/process-reports.ts @@ -0,0 +1,119 @@ +/** + * External dependencies + */ +const bold = require( 'chalk' ); +const fs = require( 'fs' ); +const path = require( 'path' ); + +/** + * Internal dependencies + */ +const { + getFilesFromDir, + readJSONFile, + logAtIndent, + sanitizeBranchName, + median +} = require( './utils' ) ; + +const formats = { + success: bold.green, +}; + +const ARTIFACTS_PATH = + process.env.WP_ARTIFACTS_PATH || path.join( process.cwd(), 'artifacts' ); +const RESULTS_FILE_SUFFIX = '.performance-results.json'; + +/** + * Calculates and prints results from the generated reports. + * + * @param {string[]} testSuites Test suites we are aiming. + * @param {string[]} branches Branches we are aiming. + */ +async function processPerformanceReports( + testSuites: string[], + branches: string[] +): Promise< void > { + logAtIndent( 0, 'Calculating results' ); + + const resultFiles = getFilesFromDir( ARTIFACTS_PATH ).filter( + ( file: string ) => file.endsWith( RESULTS_FILE_SUFFIX ) + ); + const results: Record< + string, + Record< string, Record< string, number > > + > = {}; + + // Calculate medians from all rounds. + for ( const testSuite of testSuites ) { + logAtIndent( 1, 'Test suite:', formats.success( testSuite ) ); + + results[ testSuite ] = {}; + for ( const branch of branches ) { + const sanitizedBranchName = sanitizeBranchName( branch ); + const resultsRounds: any[] = resultFiles + .filter( ( file: string ) => + file.includes( + `/${ testSuite }_${ sanitizedBranchName }_round-` + ) + ) + .map( ( file: string ) => { + logAtIndent( 2, 'Reading from:', formats.success( file ) ); + return readJSONFile( file ); + } ); + + const metrics = Object.keys( resultsRounds[ 0 ] ); + results[ testSuite ][ branch ] = {}; + + for ( const metric of metrics ) { + const values = resultsRounds + .map( ( round ) => round[ metric ] ) + .filter( ( value ) => typeof value === 'number' ); + + const value = median( values ); + if ( value !== undefined ) { + results[ testSuite ][ branch ][ metric ] = value; + } + } + } + const calculatedResultsPath = path.join( + ARTIFACTS_PATH, + testSuite + RESULTS_FILE_SUFFIX + ); + + logAtIndent( + 2, + 'Saving curated results to:', + formats.success( calculatedResultsPath ) + ); + fs.writeFileSync( + calculatedResultsPath, + JSON.stringify( results[ testSuite ], null, 2 ) + ); + } + + logAtIndent( 0, 'Printing results' ); + + for ( const testSuite of testSuites ) { + logAtIndent( 0, formats.success( testSuite ) ); + + // Invert the results, so we can display them in a table. + const invertedResult: Record< string, Record< string, string > > = {}; + for ( const [ branch, metrics ] of Object.entries( + results[ testSuite ] + ) ) { + for ( const [ metric, value ] of Object.entries( metrics ) ) { + invertedResult[ metric ] = invertedResult[ metric ] || {}; + invertedResult[ metric ][ branch ] = `${ value } ms`; + } + } + + // Print the results. + // eslint-disable-next-line no-console + console.table( invertedResult ); + } +} + +module.exports = { + processPerformanceReports, +}; diff --git a/tools/compare-perf/tsconfig.json b/tools/compare-perf/tsconfig.json new file mode 100644 index 00000000000..d491017bfb9 --- /dev/null +++ b/tools/compare-perf/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "./node_modules/@tsconfig/node16/tsconfig.json", + "compilerOptions": { + "module": "Node16", + "moduleResolution": "Node16", + "typeRoots": [ + "./typings", + "./node_modules/@types" + ] + }, + "ts-node": { + "transpileOnly": true, + "files": true, + }, +} diff --git a/tools/compare-perf/utils.js b/tools/compare-perf/utils.js index 2cca3b1a781..4a47cb41ded 100644 --- a/tools/compare-perf/utils.js +++ b/tools/compare-perf/utils.js @@ -97,9 +97,58 @@ function getFilesFromDir( dir ) { .map( ( dirent ) => path.join( dir, dirent.name ) ); } +/** + * A logging helper for printing steps and their substeps. + * + * @param {number} indent Value to indent the log. + * @param {any} msg Message to log. + * @param {...any} args Rest of the arguments to pass to console.log. + */ +function logAtIndent( indent, msg, ...args ) { + const prefix = indent === 0 ? '▶ ' : '> '; + const newline = indent === 0 ? '\n' : ''; + return console.log( + newline + ' '.repeat( indent ) + prefix + msg, + ...args + ); +} + +/** + * Sanitizes branch name to be used in a path or a filename. + * + * @param {string} branch + * + * @return {string} Sanitized branch name. + */ +function sanitizeBranchName( branch ) { + return branch.replace( /[^a-zA-Z0-9-]/g, '-' ); +} + +/** + * Computes the median number from an array numbers. + * + * @param {number[]} array + * + * @return {number|undefined} Median value or undefined if array empty. + */ +function median( array ) { + if ( ! array || ! array.length ) return undefined; + + const numbers = [ ...array ].sort( ( a, b ) => a - b ); + const middleIndex = Math.floor( numbers.length / 2 ); + + if ( numbers.length % 2 === 0 ) { + return ( numbers[ middleIndex - 1 ] + numbers[ middleIndex ] ) / 2; + } + return numbers[ middleIndex ]; +} + module.exports = { askForConfirmation, readJSONFile, runShellScript, getFilesFromDir, + logAtIndent, + sanitizeBranchName, + median };