diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7f0abf47ce2..7114392b602 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,18 +14,27 @@ Closes # . + + +- [ ] This PR is a very minor change/addition and does not require testing instructions (if checked you can ignore/remove the next section). + + + ### How to test the changes in this Pull Request: + + 1. 2. 3. + + ### Other information: - [ ] Have you added an explanation of what your changes do and why you'd like us to include them? - [ ] Have you written new tests for your changes, as applicable? -- [ ] Have you successfully run tests with your changes locally? -- [ ] Have you created a changelog file for each project being changed, ie `pnpm --filter= run changelog add`? +- [ ] Have you created a changelog file for each project being changed, ie `pnpm --filter= changelog add`? diff --git a/.github/actions/setup-woocommerce-monorepo/action.yml b/.github/actions/setup-woocommerce-monorepo/action.yml index a97ef63b1cd..b31441bce31 100644 --- a/.github/actions/setup-woocommerce-monorepo/action.yml +++ b/.github/actions/setup-woocommerce-monorepo/action.yml @@ -35,12 +35,14 @@ runs: with: node-version-file: .nvmrc cache: pnpm + registry-url: 'https://registry.npmjs.org' - name: Setup PHP uses: shivammathur/setup-php@e04e1d97f0c0481c6e1ba40f8a538454fe5d7709 with: php-version: ${{ inputs.php-version }} coverage: none + tools: phpcs, sirbrillig/phpcs-changed - name: Cache Composer Dependencies uses: actions/cache@fd5de65bc895cf536527842281bea11763fefd77 diff --git a/.github/project-community-pr-assigner.yml b/.github/project-community-pr-assigner.yml index f28936a753b..7454a24859d 100644 --- a/.github/project-community-pr-assigner.yml +++ b/.github/project-community-pr-assigner.yml @@ -1,3 +1,85 @@ +# See https://github.com/shufo/auto-assign-reviewer-by-files/blob/main/README.md for configuration format ".github/*": - - atlas + - team: atlas + +"packages/js/api/**/*": + - team: solaris + +"packages/js/e2e-utils/**/*": + - team: solaris + +"packages/js/e2e-environment/**/*": + - team: solaris + +"packages/js/api-core-tests/**/*": + - team: solaris + +"packages/js/e2e-core-tests/**/*": + - team: solaris + +"packages/js/admin-e2e-tests/**/*": + - team: solaris + +"packages/js/components/**/*": + - team: mothra + - team: ghidorah + +"packages/js/csv-export/**/*": + - team: mothra + +"packages/js/currency/**/*": + - team: mothra + +"packages/js/customer-effort-score/**/*": + - team: mothra + +"packages/js/data/**/*": + - team: mothra + - team: ghidorah + +"packages/js/date/**/*": + - team: mothra + +"packages/js/dependency-extraction-webpack-plugin/**/*": + - team: mothra + +"packages/js/eslint-plugin/**/*": + - team: mothra + +"packages/js/experimental/**/*": + - team: mothra + +"packages/js/explat/**/*": + - team: mothra + - team: ghidorah + +"packages/js/navigation/**/*": + - team: mothra + +"packages/js/number/**/*": + - team: mothra + +"packages/js/onboarding/**/*": + - team: ghidorah + +"packages/js/tracks/**/*": + - team: mothra + +"plugins/woocommerce/**/*": + - team: proton + +"plugins/woocommerce/src/Admin/**/*": + - team: mothra + - team: ghidorah + +"plugins/woocommerce/src/Internal/Admin/**/*": + - team: mothra + - team: ghidorah + +"plugins/woocommerce-admin/**/*": + - team: mothra + - team: ghidorah + +"plugins/woocommerce-beta-tester/**/*": + - team: atlas diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index f1455a8085c..ba2b99d4814 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -27,3 +27,19 @@ jobs: asset_path: plugins/woocommerce/woocommerce.zip asset_name: woocommerce.zip asset_content_type: application/zip + + + update-code-reference: + if: github.event.release.prerelease == false && github.event.release.draft == false && github.repository_owner == 'woocommerce' + name: Update Code Reference + needs: build + runs-on: ubuntu-20.04 + steps: + - name: Invoke Code Reference build and deploy workflow + uses: aurelien-baudet/workflow-dispatch@v2 + with: + workflow: GitHub Pages deploy + repo: woocommerce/code-reference + token: ${{ secrets.CUSTOM_GH_TOKEN }} + ref: refs/heads/trunk + inputs: '{ "version": "${{ github.event.release.tag_name }}" }' diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml index 81071f1e5ef..d9a22a4fb85 100644 --- a/.github/workflows/cherry-pick.yml +++ b/.github/workflows/cherry-pick.yml @@ -211,45 +211,45 @@ jobs: if ( changelogEntry.match( /comment:/i ) ) { changelogEntry = false; } - } ); - if ( changelogEntry === false ) { - continue; - } - - fs.readFile( './plugins/woocommerce/readme.txt', 'utf-8', function( err, data ) { - if ( err ) { - console.error( err ); + if ( ! changelogEntry ) { + return; } - changelogTxt = data.split( "\n" ); - let isInRange = false; - let newChangelogTxt = []; - - for ( const line of changelogTxt ) { - if ( isInRange === false && line === '== Changelog ==' ) { - isInRange = true; - } - - if ( isInRange === true && line.match( /\*\*WooCommerce Blocks/ ) ) { - isInRange = false; - } - - // Find the first match of the entry "Type". - if ( isInRange && line.match( `\\* ${changelogEntryType} -` ) ) { - newChangelogTxt.push( '* ' + changelogEntryType + ' - ' + changelogEntry + ` [#${{ needs.prep.outputs.pr }}](https://github.com/woocommerce/woocommerce/pull/${{ needs.prep.outputs.pr }})` ); - newChangelogTxt.push( line ); - isInRange = false; - continue; - } - - newChangelogTxt.push( line ); - } - - fs.writeFile( './plugins/woocommerce/readme.txt', newChangelogTxt.join( "\n" ), err => { + fs.readFile( './plugins/woocommerce/readme.txt', 'utf-8', function( err, data ) { if ( err ) { - console.error( `Unable to generate the changelog entry for PR ${{ needs.prep.outputs.pr }}` ); + console.error( err ); } + + changelogTxt = data.split( "\n" ); + let isInRange = false; + let newChangelogTxt = []; + + for ( const line of changelogTxt ) { + if ( isInRange === false && line === '== Changelog ==' ) { + isInRange = true; + } + + if ( isInRange === true && line.match( /\*\*WooCommerce Blocks/ ) ) { + isInRange = false; + } + + // Find the first match of the entry "Type". + if ( isInRange && line.match( `\\* ${changelogEntryType} -` ) ) { + newChangelogTxt.push( '* ' + changelogEntryType + ' - ' + changelogEntry + ` [#${{ needs.prep.outputs.pr }}](https://github.com/woocommerce/woocommerce/pull/${{ needs.prep.outputs.pr }})` ); + newChangelogTxt.push( line ); + isInRange = false; + continue; + } + + newChangelogTxt.push( line ); + } + + fs.writeFile( './plugins/woocommerce/readme.txt', newChangelogTxt.join( "\n" ), err => { + if ( err ) { + console.error( `Unable to generate the changelog entry for PR ${{ needs.prep.outputs.pr }}` ); + } + } ); } ); } ); } diff --git a/.github/workflows/community-label.yml b/.github/workflows/community-label.yml index 9a388fca414..221753b62e3 100644 --- a/.github/workflows/community-label.yml +++ b/.github/workflows/community-label.yml @@ -32,7 +32,7 @@ jobs: - name: "If community PR, assign a reviewer" if: github.event.pull_request && steps.check.outputs.is-community == 'yes' - uses: shufo/auto-assign-reviewer-by-files@24a9fcbd5c51c4403b64c8b6e087c824b67a5c35 + uses: shufo/auto-assign-reviewer-by-files@f5f3db9ef06bd72ab6978996988c6462cbdaabf6 with: config: ".github/project-community-pr-assigner.yml" - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.PR_ASSIGN_TOKEN }} diff --git a/.github/workflows/cot-build-and-e2e-tests-daily.yml b/.github/workflows/cot-build-and-e2e-tests-daily.yml index ccea0f0dd25..51cd250ce12 100644 --- a/.github/workflows/cot-build-and-e2e-tests-daily.yml +++ b/.github/workflows/cot-build-and-e2e-tests-daily.yml @@ -1,25 +1,30 @@ name: Run daily tests in an environment with COT enabled on: - schedule: - - cron: "30 2 * * *" - workflow_dispatch: + schedule: + - cron: '30 2 * * *' + workflow_dispatch: concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: cot-e2e-tests-run: name: Runs E2E tests with COT enabled. runs-on: ubuntu-20.04 + env: + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report steps: - uses: actions/checkout@v3 - + - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo - name: Load docker images and start containers with COT enabled. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 1 run: pnpm env:test:cot --filter=woocommerce - name: Download and install Chromium browser. @@ -30,7 +35,7 @@ jobs: timeout-minutes: 60 id: run_playwright_e2e_tests env: - USE_WP_ENV: 1 + USE_WP_ENV: 1 working-directory: plugins/woocommerce run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js @@ -43,7 +48,7 @@ jobs: steps.run_playwright_e2e_tests.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean e2e/allure-results --output e2e/allure-report + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - name: Archive Playwright E2E test report if: | @@ -53,8 +58,8 @@ jobs: with: name: e2e-test-report---pr-${{ github.event.number }} path: | - plugins/woocommerce/e2e/allure-results - plugins/woocommerce/e2e/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 @@ -62,15 +67,18 @@ jobs: name: Runs API tests with COT enabled. runs-on: ubuntu-20.04 env: - API_TEST_REPORT_DIR: ${{ github.workspace }}/api-test-report + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report steps: - uses: actions/checkout@v3 - + - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo - name: Load docker images and start containers with COT enabled. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 1 run: pnpm env:test:cot --filter=woocommerce - name: Run Playwright API tests. @@ -81,6 +89,7 @@ jobs: USER_KEY: admin USER_SECRET: password run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js + - name: Generate Playwright API Test report. id: generate_api_report if: | @@ -90,7 +99,8 @@ jobs: steps.run_playwright_api_tests.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean api-test-report/allure-results --output api-test-report/allure-report + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} + - name: Archive Playwright API test report if: | always() && @@ -99,8 +109,8 @@ jobs: with: name: api-test-report---pr-${{ github.event.number }} path: | - plugins/woocommerce/api-test-report/allure-results - plugins/woocommerce/api-test-report/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 diff --git a/.github/workflows/cot-pr-build-and-e2e-tests.yml b/.github/workflows/cot-pr-build-and-e2e-tests.yml index 13ff7080081..e5e6e4bd4cc 100644 --- a/.github/workflows/cot-pr-build-and-e2e-tests.yml +++ b/.github/workflows/cot-pr-build-and-e2e-tests.yml @@ -13,14 +13,19 @@ jobs: name: Runs E2E tests with COT enabled. if: "${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'focus: custom order tables' }}" runs-on: ubuntu-20.04 + env: + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report steps: - uses: actions/checkout@v3 - + - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo - name: Load docker images and start containers with COT enabled. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 1 run: pnpm env:test:cot --filter=woocommerce - name: Download and install Chromium browser. @@ -31,7 +36,7 @@ jobs: timeout-minutes: 60 id: run_playwright_e2e_tests env: - USE_WP_ENV: 1 + USE_WP_ENV: 1 working-directory: plugins/woocommerce run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js @@ -44,7 +49,7 @@ jobs: steps.run_playwright_e2e_tests.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean e2e/allure-results --output e2e/allure-report + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - name: Archive Playwright E2E test report if: | @@ -54,8 +59,8 @@ jobs: with: name: e2e-test-report---pr-${{ github.event.number }} path: | - plugins/woocommerce/e2e/allure-results - plugins/woocommerce/e2e/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 @@ -64,15 +69,18 @@ jobs: if: "${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'focus: custom order tables' }}" runs-on: ubuntu-20.04 env: - API_TEST_REPORT_DIR: ${{ github.workspace }}/api-test-report + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report steps: - uses: actions/checkout@v3 - + - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo - name: Load docker images and start containers with COT enabled. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 1 run: pnpm env:test:cot --filter=woocommerce - name: Run Playwright API tests. @@ -93,8 +101,8 @@ jobs: steps.run_playwright_api_tests.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean api-test-report/allure-results --output api-test-report/allure-report - + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} + - name: Archive Playwright API test report if: | always() && @@ -103,8 +111,8 @@ jobs: with: name: api-test-report---pr-${{ github.event.number }} path: | - plugins/woocommerce/api-test-report/allure-results - plugins/woocommerce/api-test-report/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index 31bb7dc23c5..c9454db35ef 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -10,52 +10,112 @@ env: GIT_AUTHOR_EMAIL: 'no-reply@woocommerce.com' jobs: - update-changelog-in-trunk: - name: Update changelog in trunk + changelog-version-update: + name: Update changelog and version runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - - name: Get tag name - id: tag - uses: actions/github-script@v6 - with: - script: | - const tag = ${{ toJSON( github.event.release.tag_name ) }} - - console.log( `::set-output name=tag::release/${ tag.substring( 0, 3 ) }` ) - name: Git fetch trunk branch run: git fetch origin trunk - - name: Copy changelog.txt to vm root - run: cp changelog.txt ../../changelog.txt + - name: Copy readme.txt to vm root + run: cp ./plugins/woocommerce/readme.txt ../../readme.txt - name: Switch to trunk branch run: git checkout trunk - - - name: Create a new branch based on trunk - run: git checkout -b update/changelog-from-release-${{ github.event.release.tag_name }} - - name: Copy saved changelog.txt to monorepo - run: cp ../../changelog.txt ./changelog.txt + - name: Create a new branch based on trunk + run: git checkout -b prep/post-release-tasks-${{ github.event.release.tag_name }} + + - name: Check if we need to continue processing + uses: actions/github-script@v6 + id: check + with: + script: | + const fs = require( 'node:fs' ); + const version = ${{ toJSON( github.event.release.tag_name ) }} + + fs.readFile( './plugins/woocommerce/readme.txt', 'utf-8', function( err, data ) { + if ( err ) { + console.error( err ); + } + + const regex = /Stable\stag:\s(\d+\.\d+\.\d+)/; + + const stableVersion = data.match( regex )[1]; + + // If the release version is less than stable version we can bail. + if ( version.localeCompare( stableVersion, undefined, { numeric: true, sensitivity: 'base' } ) == -1 ) { + console.log( 'Release version is less than stable version. No automated action taken. A manual process is required.' ); + console.log( `::set-output name=continue::false` ) + return; + } else { + console.log( `::set-output name=continue::true` ) + } + } ) + + - name: Update changelog.txt entries + uses: actions/github-script@v6 + id: update-entries + if: steps.check.outputs.continue == 'true' + with: + script: | + const fs = require( 'node:fs' ); + const version = ${{ toJSON( github.event.release.tag_name ) }} + + // Read the saved readme.txt file from earlier. + fs.readFile( '../../readme.txt', 'utf-8', function( err, readme ) { + if ( err ) { + console.log( `::set-output name=continue::false` ) + console.error( err ); + } + + const regex = /(== Changelog ==[\s\S]+)\s{2}\[See changelog for all versions\]\(https:\/\/raw\.githubusercontent\.com\/woocommerce\/woocommerce\/trunk\/changelog\.txt\)\./; + + const entries = readme.match( regex )[1]; + + fs.readFile( './changelog.txt', 'utf-8', function( err, changelog ) { + if ( err ) { + console.log( `::set-output name=continue::false` ) + console.error( err ); + } + + const regex = /== Changelog ==/; + + const updatedChangelog = changelog.replace( regex, entries ); + + fs.writeFile( './changelog.txt', updatedChangelog, err => { + if ( err ) { + console.log( `::set-output name=continue::false` ) + console.error( 'Unable to update changelog entries in changelog.txt' ); + } + + console.log( `::set-output name=continue::true` ) + } ) + } ) + } ) - name: Commit changes - run: git commit -am "Update changelog.txt from release ${{ github.event.release.tag_name }}" + if: steps.update-entries.outputs.continue == 'true' + run: git commit -am "Prep trunk post release ${{ github.event.release.tag_name }}" - name: Push branch up - run: git push origin update/changelog-from-release-${{ github.event.release.tag_name }} + if: steps.update-entries.outputs.continue == 'true' + run: git push origin prep/post-release-tasks-${{ github.event.release.tag_name }} - name: Create the PR + if: steps.update-entries.outputs.continue == 'true' uses: actions/github-script@v6 with: script: | - const body = "This PR updates the changelog.txt based on the latest release: ${{ github.event.release.tag_name }}" + const body = "This PR updates the changelog.txt entries based on the latest release: ${{ github.event.release.tag_name }}" const pr = await github.rest.pulls.create({ owner: context.repo.owner, repo: context.repo.repo, title: "Update changelog.txt from release ${{ github.event.release.tag_name }}", - head: "update/changelog-from-release-${{ github.event.release.tag_name }}", + head: "prep/post-release-tasks-${{ github.event.release.tag_name }}", base: "trunk", body: body }) diff --git a/.github/workflows/pr-build-and-e2e-tests.yml b/.github/workflows/pr-build-and-e2e-tests.yml index 00757a05801..a5f7a7afe31 100644 --- a/.github/workflows/pr-build-and-e2e-tests.yml +++ b/.github/workflows/pr-build-and-e2e-tests.yml @@ -11,6 +11,9 @@ jobs: e2e-tests-run: name: Runs E2E tests. runs-on: ubuntu-20.04 + env: + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report outputs: E2E_GRAND_TOTAL: ${{ steps.count_e2e_total.outputs.E2E_GRAND_TOTAL }} steps: @@ -21,6 +24,8 @@ jobs: - name: Load docker images and start containers. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 0 run: pnpm env:test --filter=woocommerce - name: Download and install Chromium browser. @@ -40,9 +45,9 @@ jobs: timeout-minutes: 60 id: run_playwright_e2e_tests env: - USE_WP_ENV: 1 - E2E_MAX_FAILURES: 15 - FORCE_COLOR: 1 + USE_WP_ENV: 1 + E2E_MAX_FAILURES: 15 + FORCE_COLOR: 1 working-directory: plugins/woocommerce run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js @@ -55,7 +60,7 @@ jobs: steps.run_playwright_e2e_tests.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean e2e/allure-results --output e2e/allure-report + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - name: Archive Playwright E2E test report if: | @@ -65,8 +70,8 @@ jobs: with: name: e2e-test-report---pr-${{ github.event.number }} path: | - plugins/woocommerce/e2e/allure-results - plugins/woocommerce/e2e/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 @@ -74,7 +79,8 @@ jobs: name: Runs API tests. runs-on: ubuntu-20.04 env: - API_TEST_REPORT_DIR: ${{ github.workspace }}/api-test-report + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report steps: - uses: actions/checkout@v3 @@ -83,6 +89,8 @@ jobs: - name: Load docker images and start containers. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 0 run: pnpm env:test --filter=woocommerce - name: Run Playwright API tests. @@ -103,7 +111,7 @@ jobs: steps.run_playwright_api_tests.conclusion != 'skipped' ) working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean api-test-report/allure-results --output api-test-report/allure-report + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - name: Archive Playwright API test report if: | always() && @@ -112,8 +120,8 @@ jobs: with: name: api-test-report---pr-${{ github.event.number }} path: | - plugins/woocommerce/api-test-report/allure-results - plugins/woocommerce/api-test-report/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 @@ -128,6 +136,8 @@ jobs: - name: Load docker images and start containers. working-directory: plugins/woocommerce + env: + ENABLE_HPOS: 0 run: | pnpm env:dev --filter=woocommerce pnpm env:performance-init --filter=woocommerce diff --git a/.github/workflows/pr-code-sniff.yml b/.github/workflows/pr-code-sniff.yml index 638a0b46d7f..2eee0a02a3a 100644 --- a/.github/workflows/pr-code-sniff.yml +++ b/.github/workflows/pr-code-sniff.yml @@ -1,35 +1,46 @@ name: Run code sniff on PR -on: - pull_request +on: pull_request defaults: - run: - shell: bash -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + run: + shell: bash +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +env: + PHPCS: ./plugins/woocommerce/vendor/bin/phpcs # Run WooCommerce phpcs setup in phpcs-changed instead of default jobs: - test: - name: Code sniff (PHP 7.4, WP Latest) - timeout-minutes: 15 - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 + test: + name: Code sniff (PHP 7.4, WP Latest) + timeout-minutes: 15 + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 - - name: Setup WooCommerce Monorepo - uses: ./.github/actions/setup-woocommerce-monorepo - with: - build: false + - name: Get Changed Files + id: changed-files + uses: tj-actions/changed-files@v32 + with: + files: | + **/*.php - - name: Tool versions - run: | - php --version - composer --version + - name: Setup WooCommerce Monorepo + if: steps.changed-files.outputs.any_changed == 'true' + uses: ./.github/actions/setup-woocommerce-monorepo + with: + build: false - - name: Run code sniffer - uses: thenabeel/action-phpcs@v8 - with: - files: "**.php" - phpcs_path: plugins/woocommerce/vendor/bin/phpcs - standard: phpcs.xml + - name: Tool versions + if: steps.changed-files.outputs.any_changed == 'true' + run: | + php --version + composer --version + phpcs-changed --version + + - name: Run PHPCS + if: steps.changed-files.outputs.any_changed == 'true' + run: | + HEAD_REF=$(git rev-parse HEAD) + git checkout $HEAD_REF + phpcs-changed --git --git-base ${{ github.base_ref }} ${{ steps.changed-files.outputs.all_changed_files }} diff --git a/.github/workflows/pr-highlight-changes.yml b/.github/workflows/pr-highlight-changes.yml index df85cdcec51..c6a0186279e 100644 --- a/.github/workflows/pr-highlight-changes.yml +++ b/.github/workflows/pr-highlight-changes.yml @@ -18,8 +18,9 @@ jobs: id: run working-directory: tools/code-analyzer run: | - version=$(pnpm run analyzer major-minor "${{ github.head_ref || github.ref_name }}" "plugins/woocommerce/woocommerce.php" | tail -n 1) - pnpm run analyzer "$GITHUB_HEAD_REF" $version -o "github" + HEAD_REF=$(git rev-parse HEAD) + version=$(pnpm run analyzer major-minor "$HEAD_REF" "plugins/woocommerce/woocommerce.php" | tail -n 1) + pnpm run analyzer "$HEAD_REF" $version -o "github" - name: Print results id: results run: echo "::set-output name=results::${{ steps.run.outputs.templates }}${{ steps.run.outputs.wphooks }}${{ steps.run.outputs.schema }}${{ steps.run.outputs.database }}" diff --git a/.github/workflows/pull-request-post-merge-processing.yml b/.github/workflows/pull-request-post-merge-processing.yml index 62705adf07f..9ce002c31b6 100644 --- a/.github/workflows/pull-request-post-merge-processing.yml +++ b/.github/workflows/pull-request-post-merge-processing.yml @@ -37,8 +37,3 @@ jobs: env: PULL_REQUEST_ID: ${{ github.event.pull_request.node_id }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: "Run the script to post a comment with next steps hint" - run: php add-post-merge-comment.php - env: - PULL_REQUEST_ID: ${{ github.event.pull_request.node_id }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scripts/prepare-test-summary-daily.js b/.github/workflows/scripts/prepare-test-summary-daily.js new file mode 100644 index 00000000000..784e42bf396 --- /dev/null +++ b/.github/workflows/scripts/prepare-test-summary-daily.js @@ -0,0 +1,154 @@ +/** + * Script to generate the test results summary. + */ +const { API_SUMMARY_PATH, E2E_PW_SUMMARY_PATH } = process.env; + +/** + * Convert the given `duration` from milliseconds to a more user-friendly string. + * For example, if `duration = 323000`, this function would return `5m 23s`. + * + * @param {Number} duration Duration in millisecods, as read from either the `summary.json` file in the Allure report, or from the `test-results.json` file from the Jest-Puppeteer report. + * @returns String in "5m 23s" format. + */ +const getFormattedDuration = ( duration ) => { + const durationMinutes = Math.floor( duration / 1000 / 60 ); + const durationSeconds = Math.floor( ( duration / 1000 ) % 60 ); + return `${ durationMinutes }m ${ durationSeconds }s`; +}; + +/** + * Extract the test report statistics (the number of tests that passed, failed, skipped, etc.) from Allure report's `summary.json` file. + * + * @param {string} summaryJSONPath Path to the Allure report's `summary.json` file. + * @returns An object containing relevant statistics from the Allure report. + */ +const getAllureSummaryStats = ( summaryJSONPath ) => { + const summary = require( summaryJSONPath ); + const { statistic, time } = summary; + const { passed, failed, skipped, broken, unknown, total } = statistic; + const { duration } = time; + + return { + passed, + failed, + skipped, + broken, + unknown, + total, + duration, + }; +}; + +/** + * Construct the array to be used for the API table row. + * + * @returns Array of API test result stats. + */ +const createAPITableRow = () => { + const { passed, failed, skipped, broken, unknown, total, duration } = + getAllureSummaryStats( API_SUMMARY_PATH ); + const durationFormatted = getFormattedDuration( duration ); + + return [ + 'API Tests', + passed.toString(), + failed.toString(), + broken.toString(), + skipped.toString(), + unknown.toString(), + total.toString(), + durationFormatted, + ]; +}; + +/** + * Construct the array to be used for the E2E table row. + * + * @returns Array of E2E test result stats. + */ +const createE2ETableRow = () => { + const { passed, failed, skipped, broken, unknown, total, duration } = + getAllureSummaryStats( E2E_PW_SUMMARY_PATH ); + const durationFormatted = getFormattedDuration( duration ); + + return [ + 'E2E Tests', + passed.toString(), + failed.toString(), + broken.toString(), + skipped.toString(), + unknown.toString(), + total.toString(), + durationFormatted, + ]; +}; + +/** + * Create the heading and test results table. + * + * @param core The GitHub Actions toolkit core object + */ +const addSummaryHeadingAndTable = ( core ) => { + const apiTableRow = createAPITableRow(); + const e2eTableRow = createE2ETableRow(); + + core.summary.addHeading( 'Smoke tests on trunk' ).addTable( [ + [ + { data: 'Test :test_tube:', header: true }, + { data: 'Passed :white_check_mark:', header: true }, + { data: 'Failed :rotating_light:', header: true }, + { data: 'Broken :construction:', header: true }, + { data: 'Skipped :next_track_button:', header: true }, + { data: 'Unknown :grey_question:', header: true }, + { data: 'Total :bar_chart:', header: true }, + { data: 'Duration :stopwatch:', header: true }, + ], + apiTableRow, + e2eTableRow, + ] ); +}; + +/** + * Add the summary footer. + * + * @param core The GitHub Actions toolkit core object + */ +const addSummaryFooter = ( core ) => { + core.summary + .addSeparator() + .addRaw( 'To view the full API test report, click ' ) + .addLink( + 'here.', + 'https://woocommerce.github.io/woocommerce-test-reports/daily/api' + ) + .addBreak() + .addRaw( 'To view the full E2E test report, click ' ) + .addLink( + 'here.', + 'https://woocommerce.github.io/woocommerce-test-reports/daily/e2e' + ) + .addBreak() + .addRaw( 'To view all test reports, visit the ' ) + .addLink( + 'WooCommerce Test Reports Dashboard.', + 'https://woocommerce.github.io/woocommerce-test-reports/' + ); +}; + +/** + * Generate the contents of the test results summary and post it on the workflow run. + * + * @param {*} params Objects passed from the calling GitHub Action workflow. + * @returns Stringified content of the test results summary. + */ +module.exports = async ( { core } ) => { + addSummaryHeadingAndTable( core ); + + addSummaryFooter( core ); + + const summary = core.summary.stringify(); + + await core.summary.write(); + + return summary; +}; diff --git a/.github/workflows/smoke-test-daily.yml b/.github/workflows/smoke-test-daily.yml index 7c12d9f1f6c..640cd61cd27 100644 --- a/.github/workflows/smoke-test-daily.yml +++ b/.github/workflows/smoke-test-daily.yml @@ -1,99 +1,175 @@ name: Smoke test daily on: - schedule: - - cron: '25 3 * * *' + # schedule: + # - cron: '25 3 * * *' workflow_dispatch: +env: + API_ARTIFACT: api-daily--run-${{ github.run_number }} + E2E_ARTIFACT: e2e-daily--run-${{ github.run_number }} + FORCE_COLOR: 1 + BRANCH_NAME: ${{ github.ref_name }} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - login-run: - name: Daily smoke test on trunk. + e2e-tests: + name: E2E tests on trunk runs-on: ubuntu-20.04 + if: always() env: - API_TEST_REPORT_DIR: ${{ github.workspace }}/api-test-report - outputs: - commit_message: ${{ steps.get_commit_message.outputs.commit_message }} + BASE_URL: ${{ secrets.SMOKE_TEST_URL }} + ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }} + ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} + ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }} + CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }} + CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }} + DEFAULT_TIMEOUT_OVERRIDE: 120000 steps: - uses: actions/checkout@v3 with: - ref: trunk + ref: ${{ env.BRANCH_NAME }} - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo + with: + install-filters: woocommerce + build: false - - name: Install Jest - run: npm install -g jest - - - name: Get latest commit message - id: get_commit_message - run: | - COMMIT_MESSAGE=$(git log --pretty=format:%s -1) - echo "::set-output name=commit_message::$COMMIT_MESSAGE" - - - name: Run E2E smoke test. + - name: Download and install Chromium browser. working-directory: plugins/woocommerce - env: - SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_URL }} - SMOKE_TEST_ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }} - SMOKE_TEST_ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} - SMOKE_TEST_ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }} - SMOKE_TEST_CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }} - SMOKE_TEST_CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }} - WC_E2E_SCREENSHOTS: 1 - E2E_RETEST: 1 - E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} - E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }} - UPDATE_WC: 1 - DEFAULT_TIMEOUT_OVERRIDE: 120000 - run: | - pnpm exec wc-e2e docker:up - pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/update-woocommerce.js - pnpm exec wc-e2e test:e2e + run: pnpm exec playwright install chromium - - name: Run API smoke tests + - name: Run 'Update WooCommerce' test. + working-directory: plugins/woocommerce + id: e2e-update + env: + UPDATE_WC: true + run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js update-woocommerce.spec.js + + - name: Run the rest of E2E tests. + timeout-minutes: 60 + working-directory: plugins/woocommerce + id: e2e + env: + E2E_MAX_FAILURES: 15 + run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js basic.spec.js + + - name: Generate Playwright E2E Test report. + id: generate_e2e_report + if: | + always() && + ( + steps.e2e-update.conclusion != 'cancelled' || + steps.e2e-update.conclusion != 'skipped' || + steps.e2e.conclusion != 'cancelled' || + steps.e2e.conclusion != 'skipped' + ) + working-directory: plugins/woocommerce + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} + + - name: Archive E2E test report + if: | + always() && + steps.generate_e2e_report.conclusion == 'success' + uses: actions/upload-artifact@v3 + with: + name: ${{ env.E2E_ARTIFACT }} + path: | + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} + if-no-files-found: ignore + retention-days: 5 + + api-tests: + name: API tests on trunk + runs-on: ubuntu-20.04 + needs: [e2e-tests] + if: always() + env: + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ env.BRANCH_NAME }} + + - name: Setup WooCommerce Monorepo + uses: ./.github/actions/setup-woocommerce-monorepo + with: + install-filters: woocommerce + build: false + + - name: Run API tests. if: always() - id: run_api_tests + id: run_playwright_api_tests working-directory: plugins/woocommerce env: BASE_URL: ${{ secrets.SMOKE_TEST_URL }} USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }} USER_SECRET: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} DEFAULT_TIMEOUT_OVERRIDE: 120000 - run: pnpm exec wc-api-tests test api + run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js hello.test.js + + - name: Generate API Test report. + id: generate_api_report + if: | + always() && + ( + steps.run_playwright_api_tests.conclusion != 'cancelled' || + steps.run_playwright_api_tests.conclusion != 'skipped' + ) + working-directory: plugins/woocommerce + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - name: Archive API test report if: | always() && - ( - steps.run_api_tests.conclusion != 'cancelled' || - steps.run_api_tests.conclusion != 'skipped' - ) + steps.generate_api_report.conclusion == 'success' uses: actions/upload-artifact@v3 with: - name: api-test-report---daily + name: ${{ env.API_ARTIFACT }} path: | - ${{ env.API_TEST_REPORT_DIR }}/allure-results - ${{ env.API_TEST_REPORT_DIR }}/allure-report + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} + if-no-files-found: ignore retention-days: 5 + k6-tests: + name: k6 tests on trunk + runs-on: ubuntu-20.04 + needs: [api-tests] + if: always() + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ env.BRANCH_NAME }} + + - name: Setup WooCommerce Monorepo + uses: ./.github/actions/setup-woocommerce-monorepo + with: + install-filters: woocommerce + build: false + + - name: Download and install Chromium browser. + working-directory: plugins/woocommerce + run: pnpm exec playwright install chromium + - name: Update performance test site with E2E test if: always() working-directory: plugins/woocommerce env: - SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_PERF_URL }}/ - SMOKE_TEST_ADMIN_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }} - SMOKE_TEST_ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }} - SMOKE_TEST_ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }} - SMOKE_TEST_CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }} - SMOKE_TEST_CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }} - WC_E2E_SCREENSHOTS: 1 - E2E_RETEST: 1 - E2E_RETRY_TIMES: 0 - E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} - E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }} - UPDATE_WC: 1 - DEFAULT_TIMEOUT_OVERRIDE: 120000 + BASE_URL: ${{ secrets.SMOKE_TEST_PERF_URL }}/ + ADMIN_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }} + ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }} + CUSTOMER_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }} + CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }} + UPDATE_WC: true + DEFAULT_TIMEOUT_OVERRIDE: 120000 run: | - pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/update-woocommerce.js + pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js update-woocommerce.spec.js continue-on-error: true - name: Install k6 @@ -114,34 +190,15 @@ jobs: run: | ./k6 run plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js - build: - name: Build zip for PR - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - - - name: Setup WooCommerce Monorepo - uses: ./.github/actions/setup-woocommerce-monorepo - with: - build: false - - - name: Build zip - working-directory: plugins/woocommerce - run: bash bin/build-zip.sh - - - name: Upload the zip file as an artifact - uses: actions/upload-artifact@v3 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - name: woocommerce - path: plugins/woocommerce/woocommerce.zip - retention-days: 7 - test-plugins: name: Smoke tests with ${{ matrix.plugin }} plugin installed runs-on: ubuntu-20.04 - needs: [build] + needs: [k6-tests] + if: always() + env: + USE_WP_ENV: 1 + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report strategy: fail-fast: false matrix: @@ -160,59 +217,164 @@ jobs: - plugin: 'Contact Form 7' repo: 'takayukister/contact-form-7' steps: - - name: Create dirs. - run: | - mkdir -p package/woocommerce - mkdir -p tmp/woocommerce - - uses: actions/checkout@v3 with: - path: package/woocommerce + ref: ${{ env.BRANCH_NAME }} - - name: Download WooCommerce ZIP. - uses: actions/download-artifact@v3 - with: - name: woocommerce - path: tmp + - name: Setup WooCommerce Monorepo + uses: ./.github/actions/setup-woocommerce-monorepo - - name: Extract and replace WooCommerce zip. - working-directory: tmp - run: | - unzip woocommerce.zip -d . - rsync -a woocommerce/* ../package/woocommerce/plugins/woocommerce/ + - name: Launch wp-env e2e environment + working-directory: plugins/woocommerce + run: pnpm env:test --filter=woocommerce - - name: Load docker images and start containers. - working-directory: package/woocommerce - run: pnpm docker:up --filter=woocommerce + - name: Download and install Chromium browser. + working-directory: plugins/woocommerce + run: pnpm exec playwright install chromium - - name: Run tests command. - working-directory: package/woocommerce/plugins/woocommerce + - name: Run 'Upload plugin' test + id: e2e-upload + working-directory: plugins/woocommerce env: - WC_E2E_SCREENSHOTS: 1 - E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} - E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }} PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }} PLUGIN_NAME: ${{ matrix.plugin }} GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }} - run: | - pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/upload-plugin.js - pnpm exec wc-e2e test:e2e + run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js upload-plugin.spec.js - publish-test-reports: - name: Publish test reports - if: always() + - name: Run the rest of E2E tests + id: e2e + working-directory: plugins/woocommerce + env: + E2E_MAX_FAILURES: 15 + run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js basic.spec.js + + - name: Generate E2E Test report. + id: report + if: | + always() && + ( + steps.e2e-upload.conclusion != 'cancelled' || + steps.e2e-upload.conclusion != 'skipped' || + steps.e2e.conclusion != 'cancelled' || + steps.e2e.conclusion != 'skipped' + ) + working-directory: plugins/woocommerce + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} + + - name: Archive E2E test report + if: | + always() && + steps.report.conclusion == 'success' + uses: actions/upload-artifact@v3 + with: + name: Smoke tests with ${{ matrix.plugin }} plugin installed (run ${{ github.run_number }}) + path: | + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} + if-no-files-found: ignore + retention-days: 5 + + trunk-results: + name: Publish report on smoke tests on trunk + if: always() && + ! github.event.pull_request.head.repo.fork runs-on: ubuntu-20.04 - needs: [login-run, build, test-plugins] - env: - GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} - RUN_ID: ${{ github.run_id }} - API_ARTIFACT: api-test-report---daily - COMMIT_MESSAGE: ${{ needs.login-run.outputs.commit_message }} + needs: [test-plugins] steps: - - name: Publish API test report + - name: Create dirs + run: | + mkdir -p repo + mkdir -p artifacts/api + mkdir -p artifacts/e2e + mkdir -p output + + - name: Checkout code + uses: actions/checkout@v3 + with: + path: repo + ref: ${{ env.BRANCH_NAME }} + + - name: Download API test report artifact + uses: actions/download-artifact@v3 + with: + name: ${{ env.API_ARTIFACT }} + path: artifacts/api + + - name: Download E2E test report artifact + uses: actions/download-artifact@v3 + with: + name: ${{ env.E2E_ARTIFACT }} + path: artifacts/e2e + + - name: Post test summary + uses: actions/github-script@v6 + env: + API_SUMMARY_PATH: ${{ github.workspace }}/artifacts/api/allure-report/widgets/summary.json + E2E_PW_SUMMARY_PATH: ${{ github.workspace }}/artifacts/e2e/allure-report/widgets/summary.json + with: + result-encoding: string + script: | + const script = require( './repo/.github/workflows/scripts/prepare-test-summary-daily.js' ) + return await script( { core } ) + + - name: Publish report + env: + GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} + RUN_ID: ${{ github.run_id }} run: | gh workflow run publish-test-reports-daily.yml \ -f run_id=$RUN_ID \ - -f api_artifact=$API_ARTIFACT \ - -f commit_message="$COMMIT_MESSAGE" \ + -f api_artifact="$API_ARTIFACT" \ + -f e2e_artifact="$E2E_ARTIFACT" \ + -f s3_root=public \ + --repo woocommerce/woocommerce-test-reports + + plugins-results: + name: Publish report on smoke tests with plugins + if: | + always() && + ! github.event.pull_request.head.repo.fork + runs-on: ubuntu-20.04 + needs: [test-plugins] + env: + GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} + RUN_ID: ${{ github.run_id }} + ARTIFACT: Smoke tests with ${{ matrix.plugin }} plugin installed (run ${{ github.run_number }}) + strategy: + fail-fast: false + matrix: + include: + - plugin: 'WooCommerce Payments' + repo: 'automattic/woocommerce-payments' + - plugin: 'WooCommerce PayPal Payments' + repo: 'woocommerce/woocommerce-paypal-payments' + - plugin: 'WooCommerce Shipping & Tax' + repo: 'automattic/woocommerce-services' + - plugin: 'WordPress SEO' # Yoast SEO in the UI, but the slug is wordpress-seo + repo: 'Yoast/wordpress-seo' + - plugin: 'Contact Form 7' + repo: 'takayukister/contact-form-7' + steps: + - name: Download test report artifact + uses: actions/download-artifact@v3 + with: + name: ${{ env.ARTIFACT }} + + # TODO: Add step to post job summary + + - name: Get slug + id: get-slug + uses: actions/github-script@v6 + with: + result-encoding: string + script: return "${{ matrix.repo }}".split( '/' ).pop() + + - name: Publish reports + run: | + gh workflow run publish-test-reports-daily-plugins.yml \ + -f run_id=$RUN_ID \ + -f artifact="${{ env.ARTIFACT }}" \ + -f plugin="${{ matrix.plugin }}" \ + -f slug="${{ steps.get-slug.outputs.result }}" \ + -f s3_root=public \ --repo woocommerce/woocommerce-test-reports diff --git a/changelog.txt b/changelog.txt index e30f9f9135c..3c9793b7d46 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,134 @@ == Changelog == += 7.1.0 2022-11-08 = + +**WooCommerce** + +* Fix - Fix business details step when Gutenberg is active [#35448](https://github.com/woocommerce/woocommerce/pull/35448) +* Fix - Check order type is set before returning to prevent notice. [#35349](https://github.com/woocommerce/woocommerce/pull/35349) +* Fix - When HPOS is enabled, posts are authoritative, and sync is enabled, ensure the HPOS record correctly tracks the CPT order record. [#35402](https://github.com/woocommerce/woocommerce/pull/35402) +* Fix - Allow line breaks in order note again. [#35366](https://github.com/woocommerce/woocommerce/pull/35366) +* Fix - Sync orders for stats table. [#35118](https://github.com/woocommerce/woocommerce/pull/35118) +* Fix - Fix (un)trashing of orders when using HPOS [#35125](https://github.com/woocommerce/woocommerce/pull/35125) +* Fix - Check whether order has classname before returning. [#35207](https://github.com/woocommerce/woocommerce/pull/35207) +* Fix - Add billing and shipping address indexes on order update. [#35121](https://github.com/woocommerce/woocommerce/pull/35121) +* Fix - Use correct datastore when backfilling orders. [#35176](https://github.com/woocommerce/woocommerce/pull/35176) +* Fix - (HPOS) Ensure we use GMT when populating the `date_created_gmt` column for orders. [#34875](https://github.com/woocommerce/woocommerce/pull/34875) +* Fix - Admin list table for orders (in HPOS mode) should check in case the user pages beyond the available range. [#34793](https://github.com/woocommerce/woocommerce/pull/34793) +* Fix - Allow features to declare their initial enabled state. [#34867](https://github.com/woocommerce/woocommerce/pull/34867) +* Fix - Customers should be able to pay for orders so long as any required stock reductions have already taken place. [#33575](https://github.com/woocommerce/woocommerce/pull/33575) +* Fix - Do no override order defaults with NULL values (HPOS) [#34822](https://github.com/woocommerce/woocommerce/pull/34822) +* Fix - Fix "Industry" options fails to save in the Industry step after reloading the page for OBW [#34847](https://github.com/woocommerce/woocommerce/pull/34847) +* Fix - Fix a fatal error thrown by init_theorder_object due to the return type declaration [#34730](https://github.com/woocommerce/woocommerce/pull/34730) +* Fix - fixed mismatching jetpack user should not see mobile app task list item [#35052](https://github.com/woocommerce/woocommerce/pull/35052) +* Fix - Fix enable guided mode button not trigger when its text is translated [#34843](https://github.com/woocommerce/woocommerce/pull/34843) +* Fix - Fixes test environment setup setting datetime for customer user creation [#34888](https://github.com/woocommerce/woocommerce/pull/34888) +* Fix - Fix JSON schema for product's image properties. [#34852](https://github.com/woocommerce/woocommerce/pull/34852) +* Fix - Fix obw validation issue to truly disable the continue buttons [#34895](https://github.com/woocommerce/woocommerce/pull/34895) +* Fix - Fix onboarding wizard popover padding for WP6.1 [#34896](https://github.com/woocommerce/woocommerce/pull/34896) +* Fix - Fix order refund removal when the HPOS datastore is in use. [#34785](https://github.com/woocommerce/woocommerce/pull/34785) +* Fix - Handle loading and error states for magic link button [#35068](https://github.com/woocommerce/woocommerce/pull/35068) +* Fix - Implement missing method of calculating shipping and total tax. [#34805](https://github.com/woocommerce/woocommerce/pull/34805) +* Fix - Serialize meta value before rendering so that it's rendered properly. [#34952](https://github.com/woocommerce/woocommerce/pull/34952) +* Fix - Set correct timezone when backfilling data. [#35033](https://github.com/woocommerce/woocommerce/pull/35033) +* Add - Twenty Twenty-Three theme compatibility. [#35306](https://github.com/woocommerce/woocommerce/pull/35306) +* Add - Add handling for plugin-feature incompatibilities [#34879](https://github.com/woocommerce/woocommerce/pull/34879) +* Add - Add inventory stock management to new product management experience [#34984](https://github.com/woocommerce/woocommerce/pull/34984) +* Add - Add new attributes section and field for the new Product Management Experience. [#34751](https://github.com/woocommerce/woocommerce/pull/34751) +* Add - Add order preview functionality to HPOS list table. [#34770](https://github.com/woocommerce/woocommerce/pull/34770) +* Add - Add playwright api-core-tests for customers crud operations [#34945](https://github.com/woocommerce/woocommerce/pull/34945) +* Add - Add playwright api-core-tests for order notes crud operations [#34979](https://github.com/woocommerce/woocommerce/pull/34979) +* Add - Add playwright api-core-tests for product properties crud operations [#34998](https://github.com/woocommerce/woocommerce/pull/34998) +* Add - Add playwright api-core-tests for tax rates crud operations [#34960](https://github.com/woocommerce/woocommerce/pull/34960) +* Add - Add shipping class section and dropdown [#34684](https://github.com/woocommerce/woocommerce/pull/34684) +* Add - Add shipping dimensions section to product page #34329 [#34856](https://github.com/woocommerce/woocommerce/pull/34856) +* Add - Add SKU field to new product management experience [#34978](https://github.com/woocommerce/woocommerce/pull/34978) +* Add - Add the WooCommerce features engine [#34727](https://github.com/woocommerce/woocommerce/pull/34727) +* Add - Deprecate existing `wp wc cot migrate` command and replace with `wp wc cot sync`. [#34676](https://github.com/woocommerce/woocommerce/pull/34676) +* Add - Disable action buttons when product form is invalid [#34658](https://github.com/woocommerce/woocommerce/pull/34658) +* Add - Expand attributes list to display attributes list and allow removal and re-ordering. [#34841](https://github.com/woocommerce/woocommerce/pull/34841) +* Add - Images Product management [#34769](https://github.com/woocommerce/woocommerce/pull/34769) +* Add - Improve on feature incompatibility plugin screens. [#35063](https://github.com/woocommerce/woocommerce/pull/35063) +* Add - Render columns via action so that they can be hooked into. [#34900](https://github.com/woocommerce/woocommerce/pull/34900) +* Add - Support `wc_customer_bought_product` function in HPOS. [#34931](https://github.com/woocommerce/woocommerce/pull/34931) +* Add - The updates will mean that the github workflows use the playwright versions of the api-core-tests rather than the supertest versions of the tests. [#34935](https://github.com/woocommerce/woocommerce/pull/34935) +* Update - Don't show feature compatibility warnings for inactive plugins [#35333](https://github.com/woocommerce/woocommerce/pull/35333) +* Update - Update WooCommerce Blocks to 8.7.5 [#35428](https://github.com/woocommerce/woocommerce/pull/35428) +* Update - Improve the warnings about incompatibilities between plugins and features [#35198](https://github.com/woocommerce/woocommerce/pull/35198) +* Update - Additional payment methods on new WCPay promotion page (payment-welcome) [#34581](https://github.com/woocommerce/woocommerce/pull/34581) +* Update - Add Tiktok to free grow extensions list [#34953](https://github.com/woocommerce/woocommerce/pull/34953) +* Update - Allowing generic item type in new experimental SelectControl. [#34547](https://github.com/woocommerce/woocommerce/pull/34547) +* Update - Change order data store internal key to props for better representation. [#34627](https://github.com/woocommerce/woocommerce/pull/34627) +* Update - Changing inbox display to only 5 notes with the ability to load more. [#35003](https://github.com/woocommerce/woocommerce/pull/35003) +* Update - Deploy spotlight product tour treatment [#34859](https://github.com/woocommerce/woocommerce/pull/34859) +* Update - Track orders origin in WC_Tracker. [#35069](https://github.com/woocommerce/woocommerce/pull/35069) +* Update - Updates a few css selectors to be more robust [#34790](https://github.com/woocommerce/woocommerce/pull/34790) +* Update - Update WCPay promo requirements and ensure it's dismissed on every scenario [#35030](https://github.com/woocommerce/woocommerce/pull/35030) +* Dev - Add api-core-tests for playwright [#34835](https://github.com/woocommerce/woocommerce/pull/34835) +* Dev - Add fail-fast configuration to Playwright E2E tests. [#33977](https://github.com/woocommerce/woocommerce/pull/33977) +* Dev - Add new shippping class modal to a shipping class section in product page [#34937](https://github.com/woocommerce/woocommerce/pull/34937) +* Dev - Add shipping dimensions image to visualize the sizes of the product #34329 [#34857](https://github.com/woocommerce/woocommerce/pull/34857) +* Dev - Add tests for UI Revamp on Marketing Page. [#34840](https://github.com/woocommerce/woocommerce/pull/34840) +* Dev - Exclude "debug" module from babel compile to fix the tour kit stories loading error [#34831](https://github.com/woocommerce/woocommerce/pull/34831) +* Dev - Fix node and pnpm versions via engines [#34773](https://github.com/woocommerce/woocommerce/pull/34773) +* Dev - Improve the matching of plugins during the compatibility check. [#35070](https://github.com/woocommerce/woocommerce/pull/35070) +* Dev - Load size units to show it as a suffix of shipping dimensions fields #34329 [#34856](https://github.com/woocommerce/woocommerce/pull/34856) +* Dev - Match TypeScript version with syncpack [#34787](https://github.com/woocommerce/woocommerce/pull/34787) +* Dev - set the store country in the test step [#34972](https://github.com/woocommerce/woocommerce/pull/34972) +* Dev - Update Playwright to 1.26.0 and fix a few flaky tests [#34790](https://github.com/woocommerce/woocommerce/pull/34790) +* Dev - Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues [#35007](https://github.com/woocommerce/woocommerce/pull/35007) +* Tweak - Add hooks that fire before an HPOS order is deleted or trashed. [#34858](https://github.com/woocommerce/woocommerce/pull/34858) +* Tweak - Disable new-product-management-experience feature flag in development. [#34836](https://github.com/woocommerce/woocommerce/pull/34836) +* Tweak - Update copy in the payments welcome modal [#35031](https://github.com/woocommerce/woocommerce/pull/35031) +* Tweak - Update subdivision codes for New Zealand, to match current CLDR specification. [#35011](https://github.com/woocommerce/woocommerce/pull/35011) +* Tweak - When the primary order store is the posts table, and sync is enabled, propagate changes outside of dedicated migrations. [#34863](https://github.com/woocommerce/woocommerce/pull/34863) +* Performance - Support fetching order types in bulk to improve performance. [#34976](https://github.com/woocommerce/woocommerce/pull/34976) +* Enhancement - Add support for complex field queries for orders. [#34533](https://github.com/woocommerce/woocommerce/pull/34533) +* Enhancement - Also read from posts when reading from orders as a mittigation to direct write. [#34465](https://github.com/woocommerce/woocommerce/pull/34465) +* Enhancement - Enable async typeahead fields for the attribute and term fields within products. [#34744](https://github.com/woocommerce/woocommerce/pull/34744) +* Enhancement - Enchance tour experience for store location [#34697](https://github.com/woocommerce/woocommerce/pull/34697) + +**WooCommerce Blocks 8.7.0 & 8.7.1 & 8.7.2 & 8.7.3 & 8.7.4 & 8.7.5** + +* Enhancement - Improve visual consistency between block links. ([7340](https://github.com/woocommerce/woocommerce-blocks/pull/7340)) +* Enhancement - Update the titles of some inner blocks of the Cart block and remove the lock of the Cross-Sells parent block. ([7232](https://github.com/woocommerce/woocommerce-blocks/pull/7232)) +* Enhancement - Add filter for place order button label. ([7154](https://github.com/woocommerce/woocommerce-blocks/pull/7154)) +* Enhancement - Exposed data related to the checkout through wordpress/data stores. ([6612](https://github.com/woocommerce/woocommerce-blocks/pull/6612)) +* Enhancement - Add simple, large & two menus footer patterns. ([7306](https://github.com/woocommerce/woocommerce-blocks/pull/7306)) +* Enhancement - Add minimal, large, and essential header patterns. ([7292](https://github.com/woocommerce/woocommerce-blocks/pull/7292)) +* Enhancement - Add `showRemoveItemLink` as a new checkout filter to allow extensions to toggle the visibility of the `Remove item` button under each cart line item. ([7242](https://github.com/woocommerce/woocommerce-blocks/pull/7242)) +* Enhancement - Add support for a GT tracking ID for Google Analytics. ([7213](https://github.com/woocommerce/woocommerce-blocks/pull/7213)) +* Enhancement - Separate filter titles and filter controls by converting filter blocks to use Inner Blocks. ([6978](https://github.com/woocommerce/woocommerce-blocks/pull/6978)) +* Enhancement - StoreApi requests will return a `Cart-Token` header that can be used to retrieve the cart from the corresponding session via **GET** `/wc/store/v1/cart`. ([5953](https://github.com/woocommerce/woocommerce-blocks/pull/5953)) +* Fix - Fixed HTML rendering in description of active payment integrations. ([7313](https://github.com/woocommerce/woocommerce-blocks/pull/7313)) +* Fix - Hide the shipping address form from the Checkout when the "Force shipping to the customer billing address" is enabled. ([7268](https://github.com/woocommerce/woocommerce-blocks/pull/7268)) +* Fix - Fixed an error where adding new pages would cause an infinite loop and large amounts of memory use in redux. ([7256](https://github.com/woocommerce/woocommerce-blocks/pull/7256)) +* Fix - Ensure error messages containing HTML are shown correctly in the Cart and Checkout blocks. ([7231](https://github.com/woocommerce/woocommerce-blocks/pull/7231)) +* Fix - Prevent locked inner blocks from sometimes displaying twice. ([6676](https://github.com/woocommerce/woocommerce-blocks/pull/6676)) +* Fix - StoreApi `/checkout` endpoint now returns HTTP 402 instead of HTTP 400 when payment fails. ([7273](https://github.com/woocommerce/woocommerce-blocks/pull/7273)) +* Fix - Fix a problem that causes an infinite loop when inserting Cart block in wordpress.com. ([7367](https://github.com/woocommerce/woocommerce-blocks/pull/7367)) +* Fix - Fixed an issue where JavaScript errors would occur when more than one extension tried to filter specific payment methods in the Cart and Checkout blocks. ([7377](https://github.com/woocommerce/woocommerce-blocks/pull/7377)) +* Fix - Fixed a problem where Custom Order Tables compatibility declaration could fail due to the unpredictable plugin order load. ([7395](https://github.com/woocommerce/woocommerce-blocks/pull/7395)) +* Fix - Refactor useCheckoutAddress hook to enable "Use same address for billing" option in Editor ([7393](https://github.com/woocommerce/woocommerce-blocks/pull/7393)) +* Fix - Fixed an issue where the argument passed to `canMakePayment` contained the incorrect keys. Also fixed the current user's customer data appearing in the editor when editing the Checkout block. +* Fix - Compatibility fix for Cart and Checkout inner blocks for WordPress 6.1. + += 7.0.1 2022-11-01 = + +**WooCommerce** + +* Dev - Twenty Twenty-Three theme compatibility. [#35306](https://github.com/woocommerce/woocommerce/pull/35306) +* Dev - Simplify and reduce size of payload supplied by the woocommerce_get_customer_details ajax endpoint. + +**WooCommerce Blocks 8.5.2** + +* Enhancement - Fix Mini Cart Global Styles. [7515](https://github.com/woocommerce/woocommerce-blocks/pull/7515) +* Enhancement - Fix inconsistent button styling with TT3. ([7516](https://github.com/woocommerce/woocommerce-blocks/pull/7516)) +* Enhancement - Make the Filter by Price block range color dependent of the theme color. [7525](https://github.com/woocommerce/woocommerce-blocks/pull/7525) +* Enhancement - Filter by Price block: fix price slider visibility on dark themes. [7527](https://github.com/woocommerce/woocommerce-blocks/pull/7527) +* Enhancement - Update the Mini Cart block drawer to honor the theme's background. [7510](https://github.com/woocommerce/woocommerce-blocks/pull/7510) +* Enhancement - Add white background to Filter by Attribute block dropdown so text is legible in dark backgrounds. [7506](https://github.com/woocommerce/woocommerce-blocks/pull/7506) + = 7.0.0 2022-10-11 = **WooCommerce** diff --git a/packages/js/api-core-tests/CHANGELOG.md b/packages/js/api-core-tests/CHANGELOG.md index fd5c09f3ee7..630728985c5 100644 --- a/packages/js/api-core-tests/CHANGELOG.md +++ b/packages/js/api-core-tests/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +# 1.0.0 + ## Changed - Bumped jest version to v27 - Used the jest packaged bundled in this module to run tests diff --git a/packages/js/api-core-tests/package.json b/packages/js/api-core-tests/package.json index 8ee776540d6..d176de4d7b6 100644 --- a/packages/js/api-core-tests/package.json +++ b/packages/js/api-core-tests/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/api-core-tests", - "version": "0.1.0", + "version": "1.0.0", "description": "API tests for WooCommerce", "main": "index.js", "engines": { diff --git a/packages/js/components/CHANGELOG.md b/packages/js/components/CHANGELOG.md index d911fd0bd50..a8bb8684c2a 100644 --- a/packages/js/components/CHANGELOG.md +++ b/packages/js/components/CHANGELOG.md @@ -2,6 +2,59 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [11.1.0](https://www.npmjs.com/package/@woocommerce/components/v/11.1.0) - 2022-10-24 + +- Minor - Allow passing of additional props to form inputs [#35160] + +## [11.0.0](https://www.npmjs.com/package/@woocommerce/components/v/11.0.0) - 2022-10-20 + +- Patch - Export StepperProps for external usage [#35140] +- Patch - Fixed the initial setting of DateTimePickerControl's input field. [#35140] +- Patch - Fix EnrichedLabel Storybook story styles so they don't affect other stories. [#35140] +- Patch - Fixes DateTimePickerControl's debounce handling to work even if onChange prop changes. [#35140] +- Patch - Fix issue with form onChange handler, passing outdated values. [#35140] +- Patch - Update tag component styling [#35140] +- Patch - Add missing type definitions and add babel config for tests [#35140] +- Patch - Merging trunk with local [#35140] +- Patch - Removed unfinished and unused SplitDropdown component. [#35140] +- Patch - Assume ambiguous dates passed into DateTimePickerControl are UTC. [#35140] +- Patch - Remove default selected sortable item. [#35140] +- Minor - Fix Enriched-label styles +- Minor - Fix initially selected items in SelectControl component [#35140] +- Minor - Add date-only mode to DateTimePickerControl. [#35140] +- Minor - Add disabled option to the Select Control input component and alter the onInputChange callback [#35140] +- Minor - Add form input name dot notation name="product.dimensions.width" [#35140] +- Minor - Add FormSection component [#35140] +- Minor - Add ImageGallery component [#35140] +- Minor - Adding datetimepicker component. [#35140] +- Minor - Adding on-click toolbar to image gallery component items. [#35140] +- Minor - Add label prop to rich text editor [#35140] +- Minor - Add MediaUploader component [#35140] +- Minor - Add rich text editor component [#35140] +- Minor - Add SortableList component [#35140] +- Minor - Allow external tags in SelectControl component [#35140] +- Minor - Export ImportProps type. Add DateTimePickerControl to Form stories and tests. [#35140] +- Minor - Images Product management [#35140] +- Minor - Remove EnrichedLabel component in favor of Tooltip component [#35140] +- Minor - Update resetForm arguments, adding changed fields, touched fields and errors. [#35140] +- Minor - [PM Components] Create SplitDropdown component. #34180 [#35140] +- Minor - Add label, placeholder, and help props to DateTimePickerControl. [#35140] +- Minor - Adds setValues support to FormContext [#35140] +- Minor - Add support in SelectControl for using the popover slot for the popover. [#35140] +- Minor - Update experimental SelectControl compoment to expose a couple extra combobox functions from Downshift. [#35140] +- Minor - Update experimental SelectControl compoment to expose combobox functions from Downshift and provide additional options. [#35140] +- Minor - Update text input placement in SelectControl [#35140] +- Minor - Add component EnrichedLabel #34214 [#35140] +- Minor - Add new shippping class modal to a shipping class section in product page [#35140] +- Minor - Adjust build/test scripts to remove -- -- that was required for pnpm 6. [#35140] +- Minor - Fix node and pnpm versions via engines [#35140] +- Minor - Update Plugin installer component to TS [#35140] +- Minor - Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues [#35140] +- Minor - Fix DateTimePickerControl's onChange date arg to only be a string (TypeScript). [#35140] +- Minor - Improve experimental SelectControl accessibility [#35140] +- Minor - Improve Sortable component acessibility [#35140] +- - Create new experimental SelectControl component [#35140] + ## [10.3.0](https://www.npmjs.com/package/@woocommerce/components/v/10.3.0) - 2022-08-12 - Patch - Added in missing TS definitions in package.json [#34279] diff --git a/packages/js/components/changelog/add-28_product_details b/packages/js/components/changelog/add-28_product_details deleted file mode 100644 index 8c362772846..00000000000 --- a/packages/js/components/changelog/add-28_product_details +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Add component EnrichedLabel #34214 diff --git a/packages/js/components/changelog/add-29_product_link_slug b/packages/js/components/changelog/add-29_product_link_slug deleted file mode 100644 index a0e4f41a088..00000000000 --- a/packages/js/components/changelog/add-29_product_link_slug +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Update resetForm arguments, adding changed fields, touched fields and errors. diff --git a/packages/js/components/changelog/add-30_list_price_field b/packages/js/components/changelog/add-30_list_price_field deleted file mode 100644 index 6b6de73827b..00000000000 --- a/packages/js/components/changelog/add-30_list_price_field +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Fix Enriched-label styles - #34382 diff --git a/packages/js/components/changelog/update-64-tag-styling b/packages/js/components/changelog/add-34332-add-attribute-edit similarity index 53% rename from packages/js/components/changelog/update-64-tag-styling rename to packages/js/components/changelog/add-34332-add-attribute-edit index a3bd088afd8..42678d69bf9 100644 --- a/packages/js/components/changelog/update-64-tag-styling +++ b/packages/js/components/changelog/add-34332-add-attribute-edit @@ -1,4 +1,4 @@ Significance: patch Type: update -Update tag component styling +Updating downshift to 6.1.12. diff --git a/packages/js/components/changelog/add-34333_attribute_list b/packages/js/components/changelog/add-34333_attribute_list deleted file mode 100644 index 7655dcde6df..00000000000 --- a/packages/js/components/changelog/add-34333_attribute_list +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: tweak - -Remove default selected sortable item. diff --git a/packages/js/components/changelog/add-34437-gallery-toolbar b/packages/js/components/changelog/add-34437-gallery-toolbar deleted file mode 100644 index d16e53c3f42..00000000000 --- a/packages/js/components/changelog/add-34437-gallery-toolbar +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Adding on-click toolbar to image gallery component items. diff --git a/packages/js/components/changelog/add-34657-add-new-shipping-class b/packages/js/components/changelog/add-34657-add-new-shipping-class deleted file mode 100644 index 3a6e304d072..00000000000 --- a/packages/js/components/changelog/add-34657-add-new-shipping-class +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Add new shippping class modal to a shipping class section in product page diff --git a/packages/js/components/changelog/add-34_category_field b/packages/js/components/changelog/add-34_category_field deleted file mode 100644 index 13d991676ee..00000000000 --- a/packages/js/components/changelog/add-34_category_field +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Update experimental SelectControl compoment to expose a couple extra combobox functions from Downshift. diff --git a/packages/js/components/changelog/add-35046 b/packages/js/components/changelog/add-35046 new file mode 100644 index 00000000000..e9d7806593d --- /dev/null +++ b/packages/js/components/changelog/add-35046 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add experimental ConditionalWrapper component diff --git a/packages/js/components/changelog/add-39_image_section_details b/packages/js/components/changelog/add-39_image_section_details deleted file mode 100644 index ed8d9ab775a..00000000000 --- a/packages/js/components/changelog/add-39_image_section_details +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Images Product management diff --git a/packages/js/components/changelog/add-component-datetime-picker b/packages/js/components/changelog/add-component-datetime-picker deleted file mode 100644 index b41e9de5483..00000000000 --- a/packages/js/components/changelog/add-component-datetime-picker +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Adding datetimepicker component. diff --git a/packages/js/components/changelog/add-draggable-list b/packages/js/components/changelog/add-draggable-list deleted file mode 100644 index 470f358d6a5..00000000000 --- a/packages/js/components/changelog/add-draggable-list +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add SortableList component diff --git a/packages/js/components/changelog/add-form_input_name_dot_notation b/packages/js/components/changelog/add-form_input_name_dot_notation deleted file mode 100644 index dadd617537a..00000000000 --- a/packages/js/components/changelog/add-form_input_name_dot_notation +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add form input name dot notation name="product.dimensions.width" diff --git a/packages/js/components/changelog/add-gb-text-editor b/packages/js/components/changelog/add-gb-text-editor deleted file mode 100644 index 8ade85318e8..00000000000 --- a/packages/js/components/changelog/add-gb-text-editor +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add rich text editor component diff --git a/packages/js/components/changelog/add-image-gallery b/packages/js/components/changelog/add-image-gallery deleted file mode 100644 index 9c475bb48ed..00000000000 --- a/packages/js/components/changelog/add-image-gallery +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add ImageGallery component diff --git a/packages/js/components/changelog/add-media-uploader b/packages/js/components/changelog/add-media-uploader deleted file mode 100644 index b0418ffd014..00000000000 --- a/packages/js/components/changelog/add-media-uploader +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add MediaUploader component diff --git a/packages/js/components/changelog/add-product_schedule_sale b/packages/js/components/changelog/add-product_schedule_sale deleted file mode 100644 index c745e217882..00000000000 --- a/packages/js/components/changelog/add-product_schedule_sale +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Adds setValues support to FormContext diff --git a/packages/js/components/changelog/add-section-component b/packages/js/components/changelog/add-section-component deleted file mode 100644 index 7277b31d330..00000000000 --- a/packages/js/components/changelog/add-section-component +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add FormSection component diff --git a/packages/js/components/changelog/add-sortable-accessibility b/packages/js/components/changelog/add-sortable-accessibility deleted file mode 100644 index 407bba232b4..00000000000 --- a/packages/js/components/changelog/add-sortable-accessibility +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: enhancement - -Improve Sortable component acessibility diff --git a/packages/js/components/changelog/add-split-dropdown-component b/packages/js/components/changelog/add-split-dropdown-component deleted file mode 100644 index 5c63071d0fc..00000000000 --- a/packages/js/components/changelog/add-split-dropdown-component +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -[PM Components] Create SplitDropdown component. #34180 diff --git a/packages/js/components/changelog/dev-bump-pnpm-version-restraint b/packages/js/components/changelog/dev-bump-pnpm-version-restraint deleted file mode 100644 index f7511cb6974..00000000000 --- a/packages/js/components/changelog/dev-bump-pnpm-version-restraint +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues diff --git a/packages/js/components/changelog/dev-fix-admin-tests-pnpm7 b/packages/js/components/changelog/dev-fix-admin-tests-pnpm7 deleted file mode 100644 index d8b487150a2..00000000000 --- a/packages/js/components/changelog/dev-fix-admin-tests-pnpm7 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Add missing type definitions and add babel config for tests diff --git a/packages/js/components/changelog/dev-fix-pnpm-version-engines b/packages/js/components/changelog/dev-fix-pnpm-version-engines deleted file mode 100644 index a1804a282f0..00000000000 --- a/packages/js/components/changelog/dev-fix-pnpm-version-engines +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Fix node and pnpm versions via engines diff --git a/packages/js/components/changelog/enhancement-35190-update-tooltip-styles b/packages/js/components/changelog/enhancement-35190-update-tooltip-styles new file mode 100644 index 00000000000..4d21861d602 --- /dev/null +++ b/packages/js/components/changelog/enhancement-35190-update-tooltip-styles @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Update font size and spacing in the tooltip component diff --git a/packages/js/components/changelog/fix-34112 b/packages/js/components/changelog/fix-34112 deleted file mode 100644 index 2c06f8f49fb..00000000000 --- a/packages/js/components/changelog/fix-34112 +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: tweak -Comment: Minor update of react and react-dom to 17.0.2. - - diff --git a/packages/js/components/changelog/fix-34584_form_onchange_callback b/packages/js/components/changelog/fix-34584_form_onchange_callback deleted file mode 100644 index a060d4c1b74..00000000000 --- a/packages/js/components/changelog/fix-34584_form_onchange_callback +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix issue with form onChange handler, passing outdated values. diff --git a/packages/js/components/changelog/fix-date-time-picker-control-debounce b/packages/js/components/changelog/fix-date-time-picker-control-debounce deleted file mode 100644 index e134508881a..00000000000 --- a/packages/js/components/changelog/fix-date-time-picker-control-debounce +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fixes DateTimePickerControl's debounce handling to work even if onChange prop changes. diff --git a/packages/js/components/changelog/fix-date-time-picker-control-initial-setting b/packages/js/components/changelog/fix-date-time-picker-control-initial-setting deleted file mode 100644 index 589414fa14e..00000000000 --- a/packages/js/components/changelog/fix-date-time-picker-control-initial-setting +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fixed the initial setting of DateTimePickerControl's input field. diff --git a/packages/js/components/changelog/fix-date-time-picker-control-onchange b/packages/js/components/changelog/fix-date-time-picker-control-onchange new file mode 100644 index 00000000000..f7074ae252b --- /dev/null +++ b/packages/js/components/changelog/fix-date-time-picker-control-onchange @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +DateTimePickerControl's onChange now only fires when there is an actual change to the datetime. diff --git a/packages/js/components/changelog/fix-date-time-picker-control-suffix-style b/packages/js/components/changelog/fix-date-time-picker-control-suffix-style new file mode 100644 index 00000000000..97b797ed6cb --- /dev/null +++ b/packages/js/components/changelog/fix-date-time-picker-control-suffix-style @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Just a minor tweak to the CSS for the DateTimePickerControl suffix. + + diff --git a/packages/js/components/changelog/fix-enriched-label-storybook b/packages/js/components/changelog/fix-enriched-label-storybook deleted file mode 100644 index 05370ad6e76..00000000000 --- a/packages/js/components/changelog/fix-enriched-label-storybook +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix EnrichedLabel Storybook story styles so they don't affect other stories. diff --git a/packages/js/components/changelog/fix-export-stepper-props b/packages/js/components/changelog/fix-export-stepper-props deleted file mode 100644 index 7d1cda9f63f..00000000000 --- a/packages/js/components/changelog/fix-export-stepper-props +++ /dev/null @@ -1,6 +0,0 @@ -Significance: patch -Type: fix - -Export StepperProps for external usage - - diff --git a/packages/js/components/changelog/fix-form_ts_error b/packages/js/components/changelog/fix-form_ts_error new file mode 100644 index 00000000000..fc0ec73f0ee --- /dev/null +++ b/packages/js/components/changelog/fix-form_ts_error @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Update variable name within useFormContext. diff --git a/packages/js/components/changelog/fix-plugin-installer-ts b/packages/js/components/changelog/fix-plugin-installer-ts deleted file mode 100644 index 623f429f0e2..00000000000 --- a/packages/js/components/changelog/fix-plugin-installer-ts +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Update Plugin installer component to TS diff --git a/packages/js/components/changelog/fix-revert_on_change_third_param_update b/packages/js/components/changelog/fix-revert_on_change_third_param_update deleted file mode 100644 index cd8bd7ed107..00000000000 --- a/packages/js/components/changelog/fix-revert_on_change_third_param_update +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: tweak -Comment: Reverted change of last PR as part of #34614 - - diff --git a/packages/js/components/changelog/fix-rich-text-editor-selection b/packages/js/components/changelog/fix-rich-text-editor-selection new file mode 100644 index 00000000000..59caa9a16d8 --- /dev/null +++ b/packages/js/components/changelog/fix-rich-text-editor-selection @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Fix up initial block selection in RichTextEditor and add media blocks diff --git a/packages/js/components/changelog/fix-select-control-extensibility b/packages/js/components/changelog/fix-select-control-extensibility deleted file mode 100644 index 40dee5ca1b0..00000000000 --- a/packages/js/components/changelog/fix-select-control-extensibility +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: enhancement - -Improve experimental SelectControl accessibility diff --git a/packages/js/components/changelog/fix-select-control-popover-slots b/packages/js/components/changelog/fix-select-control-popover-slots new file mode 100644 index 00000000000..81d59f026ab --- /dev/null +++ b/packages/js/components/changelog/fix-select-control-popover-slots @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add name to exported popover slot used to display SelectControl Menu, so it is only used for SelectControl menus. diff --git a/packages/js/components/changelog/fix-select-control-selection b/packages/js/components/changelog/fix-select-control-selection deleted file mode 100644 index 438b5c29632..00000000000 --- a/packages/js/components/changelog/fix-select-control-selection +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Fix initially selected items in SelectControl component diff --git a/packages/js/components/changelog/remove-split-dropdown b/packages/js/components/changelog/remove-split-dropdown deleted file mode 100644 index 87f296ba40f..00000000000 --- a/packages/js/components/changelog/remove-split-dropdown +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Removed unfinished and unused SplitDropdown component. diff --git a/packages/js/components/changelog/try-downshift b/packages/js/components/changelog/try-downshift deleted file mode 100644 index 38315113e38..00000000000 --- a/packages/js/components/changelog/try-downshift +++ /dev/null @@ -1,4 +0,0 @@ -Significance: major -Type: add - -Create new experimental SelectControl component diff --git a/packages/js/components/changelog/update-date-time-picker-control b/packages/js/components/changelog/update-date-time-picker-control deleted file mode 100644 index 20911f9f710..00000000000 --- a/packages/js/components/changelog/update-date-time-picker-control +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Add label, placeholder, and help props to DateTimePickerControl. diff --git a/packages/js/components/changelog/update-date-time-picker-control-force-time-to b/packages/js/components/changelog/update-date-time-picker-control-force-time-to new file mode 100644 index 00000000000..fcf4278fc5f --- /dev/null +++ b/packages/js/components/changelog/update-date-time-picker-control-force-time-to @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Added ability to force time when DateTimePickerControl is date-only (timeForDateOnly prop). diff --git a/packages/js/components/changelog/update-date-time-picker-control-formatting b/packages/js/components/changelog/update-date-time-picker-control-formatting new file mode 100644 index 00000000000..800e0dbc01d --- /dev/null +++ b/packages/js/components/changelog/update-date-time-picker-control-formatting @@ -0,0 +1,4 @@ +Significance: major +Type: update + +Switch DateTimePickerControl formatting to PHP style, for WP compatibility. diff --git a/packages/js/components/changelog/update-date-time-picker-control-onchange b/packages/js/components/changelog/update-date-time-picker-control-onchange deleted file mode 100644 index 21e87a4f6b3..00000000000 --- a/packages/js/components/changelog/update-date-time-picker-control-onchange +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: tweak - -Fix DateTimePickerControl's onChange date arg to only be a string (TypeScript). diff --git a/packages/js/components/changelog/update-date-time-picker-control-picker-classname b/packages/js/components/changelog/update-date-time-picker-control-picker-classname new file mode 100644 index 00000000000..4f444ed8862 --- /dev/null +++ b/packages/js/components/changelog/update-date-time-picker-control-picker-classname @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix DateTimePickerControl's popover styling when slot-fill is used. diff --git a/packages/js/components/changelog/update-experimental_select_control b/packages/js/components/changelog/update-experimental_select_control deleted file mode 100644 index d64304cd04c..00000000000 --- a/packages/js/components/changelog/update-experimental_select_control +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Update experimental SelectControl compoment to expose combobox functions from Downshift and provide additional options. diff --git a/packages/js/components/changelog/update-select-control-input-placement b/packages/js/components/changelog/update-select-control-input-placement deleted file mode 100644 index 8fd514c91c5..00000000000 --- a/packages/js/components/changelog/update-select-control-input-placement +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Update text input placement in SelectControl diff --git a/packages/js/components/changelog/update-select-control-tag-location b/packages/js/components/changelog/update-select-control-tag-location deleted file mode 100644 index 4d8382fc1f8..00000000000 --- a/packages/js/components/changelog/update-select-control-tag-location +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Allow external tags in SelectControl component diff --git a/packages/js/components/changelog/upgrade-pnpm-7 b/packages/js/components/changelog/upgrade-pnpm-7 deleted file mode 100644 index 10ee28d636f..00000000000 --- a/packages/js/components/changelog/upgrade-pnpm-7 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Adjust build/test scripts to remove -- -- that was required for pnpm 6. diff --git a/packages/js/components/package.json b/packages/js/components/package.json index d30756a9036..2a07a218727 100644 --- a/packages/js/components/package.json +++ b/packages/js/components/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/components", - "version": "10.3.0", + "version": "11.1.0", "description": "UI components for WooCommerce.", "author": "Automattic", "license": "GPL-3.0-or-later", @@ -74,7 +74,7 @@ "d3-shape": "^1.3.7", "d3-time-format": "^2.3.0", "dompurify": "^2.3.6", - "downshift": "^6.1.9", + "downshift": "^6.1.12", "emoji-flags": "^1.3.0", "gridicons": "^3.4.0", "memoize-one": "^6.0.0", @@ -161,4 +161,4 @@ "pnpm test-staged" ] } -} \ No newline at end of file +} diff --git a/packages/js/components/src/util/conditional-wrapper/conditional-wrapper.tsx b/packages/js/components/src/conditional-wrapper/conditional-wrapper.tsx similarity index 100% rename from packages/js/components/src/util/conditional-wrapper/conditional-wrapper.tsx rename to packages/js/components/src/conditional-wrapper/conditional-wrapper.tsx diff --git a/packages/js/components/src/util/conditional-wrapper/index.ts b/packages/js/components/src/conditional-wrapper/index.ts similarity index 100% rename from packages/js/components/src/util/conditional-wrapper/index.ts rename to packages/js/components/src/conditional-wrapper/index.ts diff --git a/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss b/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss index 740ed510c67..a3df6290728 100644 --- a/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss +++ b/packages/js/components/src/date-time-picker-control/date-time-picker-control.scss @@ -1,7 +1,13 @@ .woocommerce-date-time-picker-control { display: block; - .woocommerce-date-time-picker-control__input-control__suffix { - padding-right: 8px; + .components-input-control__suffix { + margin-right: 8px; + } + + &__popover { + .components-datetime__date { + border-top: 0; + } } } diff --git a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx index 186604c4069..42832b6e800 100644 --- a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx +++ b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx @@ -1,12 +1,13 @@ /** * External dependencies */ +import { format as formatDate } from '@wordpress/date'; import { createElement, + useCallback, useState, useEffect, - useLayoutEffect, - useCallback, + useMemo, useRef, } from '@wordpress/element'; import { Icon, calendar } from '@wordpress/icons'; @@ -16,17 +17,21 @@ import { sprintf, __ } from '@wordpress/i18n'; import { useDebounce, useInstanceId } from '@wordpress/compose'; import { BaseControl, - Dropdown, + DatePicker, DateTimePicker as WpDateTimePicker, + Dropdown, // @ts-expect-error `__experimentalInputControl` does exist. __experimentalInputControl as InputControl, } from '@wordpress/components'; -export const default12HourDateTimeFormat = 'MM/DD/YYYY h:mm a'; -export const default24HourDateTimeFormat = 'MM/DD/YYYY H:mm'; +// PHP style formatting: +// https://wordpress.org/support/article/formatting-date-and-time/ +export const defaultDateFormat = 'm/d/Y'; +export const default12HourDateTimeFormat = 'm/d/Y h:i a'; +export const default24HourDateTimeFormat = 'm/d/Y H:i'; export type DateTimePickerControlOnChangeHandler = ( - date: string, + dateTimeIsoString: string, isValid: boolean ) => void; @@ -34,7 +39,9 @@ export type DateTimePickerControlProps = { currentDate?: string | null; dateTimeFormat?: string; disabled?: boolean; - is12Hour?: boolean; + isDateOnlyPicker?: boolean; + is12HourPicker?: boolean; + timeForDateOnly?: 'start-of-day' | 'end-of-day'; onChange?: DateTimePickerControlOnChangeHandler; onBlur?: () => void; label?: string; @@ -45,10 +52,10 @@ export type DateTimePickerControlProps = { export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { currentDate, - is12Hour = true, - dateTimeFormat = is12Hour - ? default12HourDateTimeFormat - : default24HourDateTimeFormat, + isDateOnlyPicker = false, + is12HourPicker = true, + timeForDateOnly = 'start-of-day', + dateTimeFormat, disabled = false, onChange, onBlur, @@ -62,35 +69,59 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { const id = `inspector-date-time-picker-control-${ instanceId }`; const inputControl = useRef< InputControl >(); - const isMounted = useRef( false ); - useEffect( () => { - isMounted.current = true; - return () => { - isMounted.current = false; - }; - } ); - const [ inputString, setInputString ] = useState( '' ); - const [ lastValidDate, setLastValidDate ] = useState< Moment | null >( - null + + const displayFormat = useMemo( () => { + if ( dateTimeFormat ) { + return dateTimeFormat; + } + + if ( isDateOnlyPicker ) { + return defaultDateFormat; + } + + if ( is12HourPicker ) { + return default12HourDateTimeFormat; + } + + return default24HourDateTimeFormat; + }, [ dateTimeFormat, isDateOnlyPicker, is12HourPicker ] ); + + function parseAsISODateTime( + dateString?: string | null, + assumeLocalTime = false + ): Moment { + return assumeLocalTime + ? moment( dateString, moment.ISO_8601, true ).utc() + : moment.utc( dateString, moment.ISO_8601, true ); + } + + function parseAsLocalDateTime( dateString: string | null ): Moment { + // parse input date string as local time; + // be lenient of user input and try to match any format Moment can + return moment( dateString ); + } + + const maybeForceTime = useCallback( + ( momentDate: Moment ) => { + if ( ! isDateOnlyPicker || ! momentDate.isValid() ) + return momentDate; + + // We want to set to the start/end of the local time, so + // we need to put our Moment instance into "local" mode + const updatedMomentDate = momentDate.clone().local(); + + if ( timeForDateOnly === 'start-of-day' ) { + updatedMomentDate.startOf( 'day' ); + } else if ( timeForDateOnly === 'end-of-day' ) { + updatedMomentDate.endOf( 'day' ); + } + + return updatedMomentDate; + }, + [ isDateOnlyPicker, timeForDateOnly ] ); - function parseMomentIso( dateString?: string | null ): Moment { - return moment( dateString, moment.ISO_8601, true ); - } - - function parseMoment( dateString?: string | null ): Moment { - return moment( dateString, dateTimeFormat ); - } - - function formatMomentIso( momentDate: Moment ): string { - return momentDate.toISOString(); - } - - function formatMoment( momentDate: Moment ): string { - return momentDate.format( dateTimeFormat ); - } - function hasFocusLeftInputAndDropdownContent( event: React.FocusEvent< HTMLInputElement > ): boolean { @@ -99,89 +130,67 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { ); } - // We setup the debounced handling of the input string changes using - // useRef because useCallback does *not* guarantee that the resulting - // callback function will not be recreated, even if the dependencies - // haven't changed (this is because of it's use of useMemo under the - // hood, which also makes not guarantee). And, even if it did, the - // equality check for useCallback dependencies is by reference. So, if - // the "same" function is passed in, but it is a different instance, it - // will trigger the recreation of the callback. - // - // With useDebounce, if the callback function changes, the current - // debounce is canceled. This results in the callback function never being - // called. - // - // We *need* to ensure that our handler is called at least once, - // and also that we call the passed in onChange callback. - // - // We guarantee this by keeping references to both our handler and the - // passed in prop. - // - // The consumer of DateTimePickerControl should ensure that the - // function passed into onChange does not change (using references or - // useCallbackOne). But, even if they do not, and the function changes, - // things will likely function as expected unless the consumer is doing - // something really convoluted. - // - // See also: - // - [note regarding useMemo not being a semantic guarantee](https://reactjs.org/docs/hooks-reference.html#usememo) - // - [useDebounce hook loses function calls if the dependency changes](https://github.com/WordPress/gutenberg/issues/35505) - // - [useMemoOne and useCallbackOne](https://github.com/alexreardon/use-memo-one) - - const onChangePropFunctionRef = useRef< - DateTimePickerControlOnChangeHandler | undefined - >(); - useLayoutEffect( () => { - onChangePropFunctionRef.current = onChange; - }, [ onChange ] ); - - const inputStringChangeHandlerFunctionRef = useRef< - ( newInputString: string, fireOnChange: boolean ) => void - >( ( newInputString: string, fireOnChange: boolean ) => { - if ( ! isMounted.current ) return; - - const newDateTime = parseMoment( newInputString ); - const isValid = newDateTime.isValid(); - - if ( isValid ) { - setLastValidDate( newDateTime ); - } - - if ( - fireOnChange && - typeof onChangePropFunctionRef.current === 'function' - ) { - onChangePropFunctionRef.current( - isValid ? formatMomentIso( newDateTime ) : newInputString, - isValid - ); - } - } ); - - const debouncedInputStringChangeHandler = useDebounce( - inputStringChangeHandlerFunctionRef.current, - onChangeDebounceWait + const formatDateTimeForDisplay = useCallback( + ( dateTime: Moment ) => { + return dateTime.isValid() + ? formatDate( displayFormat, dateTime.local() ) + : dateTime.creationData().input?.toString() || ''; + }, + [ displayFormat ] ); - function change( newInputString: string ) { - setInputString( newInputString ); - debouncedInputStringChangeHandler( newInputString, true ); + function formatDateTimeAsISO( dateTime: Moment ): string { + return dateTime.isValid() + ? dateTime.utc().toISOString() + : dateTime.creationData().input?.toString() || ''; } - function changeImmediate( newInputString: string, fireOnChange: boolean ) { - setInputString( newInputString ); - inputStringChangeHandlerFunctionRef.current( - newInputString, - fireOnChange - ); - } + const inputStringDateTime = useMemo( () => { + return maybeForceTime( parseAsLocalDateTime( inputString ) ); + }, [ inputString, maybeForceTime ] ); - function blur() { - if ( onBlur ) { - onBlur(); - } - } + // We keep a ref to the onChange prop so that we can be sure we are + // always using the more up-to-date value, even if it changes + // it while a debounced onChange handler is in progress + const onChangeRef = useRef< + DateTimePickerControlOnChangeHandler | undefined + >(); + useEffect( () => { + onChangeRef.current = onChange; + }, [ onChange ] ); + + const setInputStringAndMaybeCallOnChange = useCallback( + ( newInputString: string, isUserTypedInput: boolean ) => { + const newDateTime = maybeForceTime( + isUserTypedInput + ? parseAsLocalDateTime( newInputString ) + : parseAsISODateTime( newInputString, true ) + ); + const isDateTimeSame = newDateTime.isSame( inputStringDateTime ); + + if ( isUserTypedInput ) { + setInputString( newInputString ); + } else if ( ! isDateTimeSame ) { + setInputString( formatDateTimeForDisplay( newDateTime ) ); + } + + if ( + typeof onChangeRef.current === 'function' && + ! isDateTimeSame + ) { + onChangeRef.current( + formatDateTimeAsISO( newDateTime ), + newDateTime.isValid() + ); + } + }, + [ formatDateTimeForDisplay, inputStringDateTime, maybeForceTime ] + ); + + const debouncedSetInputStringAndMaybeCallOnChange = useDebounce( + setInputStringAndMaybeCallOnChange, + onChangeDebounceWait + ); function focusInputControl() { if ( inputControl.current ) { @@ -189,21 +198,23 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { } } - const isInitialUpdate = useRef( true ); - useEffect( () => { - const fireOnChange = ! isInitialUpdate.current; - if ( isInitialUpdate.current ) { - isInitialUpdate.current = false; + function getUserInputOrUpdatedCurrentDate() { + const newDateTime = maybeForceTime( + parseAsISODateTime( currentDate, false ) + ); + + if ( + ! newDateTime.isValid() || + newDateTime.isSame( + maybeForceTime( parseAsLocalDateTime( inputString ) ) + ) + ) { + // keep the input string as the user entered it + return inputString; } - const newDate = parseMomentIso( currentDate ); - - if ( newDate.isValid() ) { - changeImmediate( formatMoment( newDate ), fireOnChange ); - } else { - changeImmediate( currentDate || '', fireOnChange ); - } - }, [ currentDate, dateTimeFormat ] ); + return formatDateTimeForDisplay( newDateTime ); + } return ( = ( { focusOnMount={ false } // @ts-expect-error `onToggle` does exist. onToggle={ ( willOpen ) => { - if ( ! willOpen ) { - blur(); + if ( ! willOpen && typeof onBlur === 'function' ) { + onBlur(); } } } renderToggle={ ( { isOpen, onToggle } ) => ( @@ -225,8 +236,13 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { id={ id } ref={ inputControl } disabled={ disabled } - value={ inputString } - onChange={ change } + value={ getUserInputOrUpdatedCurrentDate() } + onChange={ ( newValue: string ) => + debouncedSetInputStringAndMaybeCallOnChange( + newValue, + true + ) + } onBlur={ ( event: React.FocusEvent< HTMLInputElement > ) => { @@ -264,22 +280,30 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( { /> ) } - renderContent={ () => ( - { - const formattedDate = formatMoment( - parseMomentIso( date ) - ); - changeImmediate( formattedDate, true ); - } } - is12Hour={ is12Hour } - /> - ) } + popoverProps={ { + className: 'woocommerce-date-time-picker-control__popover', + } } + renderContent={ () => { + const Picker = isDateOnlyPicker ? DatePicker : WpDateTimePicker; + const inputDateTime = parseAsLocalDateTime( inputString ); + + return ( + + setInputStringAndMaybeCallOnChange( + newDateTimeISOString, + false + ) + } + is12Hour={ is12HourPicker } + /> + ); + } } /> ); }; diff --git a/packages/js/components/src/date-time-picker-control/stories/index.tsx b/packages/js/components/src/date-time-picker-control/stories/index.tsx index 23bd6600c7f..4ce10f12129 100644 --- a/packages/js/components/src/date-time-picker-control/stories/index.tsx +++ b/packages/js/components/src/date-time-picker-control/stories/index.tsx @@ -2,13 +2,13 @@ * External dependencies */ import React from 'react'; -import { Button } from '@wordpress/components'; +import { Button, Popover, SlotFillProvider } from '@wordpress/components'; import { createElement, useState } from '@wordpress/element'; /** * Internal dependencies */ -import { DateTimePickerControl } from '../'; +import { DateTimePickerControl, defaultDateFormat } from '../'; export default { title: 'WooCommerce Admin/components/DateTimePickerControl', @@ -28,16 +28,25 @@ Basic.args = { help: 'Type a date and time or use the picker', }; +const customFormat = 'Y-m-d H:i'; + export const CustomDateTimeFormat = Template.bind( {} ); CustomDateTimeFormat.args = { ...Basic.args, - help: 'Format: YYYY-MM-DD HH:mm', - dateTimeFormat: 'YYYY-MM-DD HH:mm', + help: 'Format: ' + customFormat, + dateTimeFormat: customFormat, }; function ControlledContainer( { children, ...props } ) { + function nowWithZeroedSeconds() { + const now = new Date(); + now.setSeconds( 0 ); + now.setMilliseconds( 0 ); + return now; + } + const [ controlledDate, setControlledDate ] = useState( - new Date().toISOString() + nowWithZeroedSeconds().toISOString() ); return ( @@ -46,11 +55,19 @@ function ControlledContainer( { children, ...props } ) {
+
+
+ Controlled date: +
{ controlledDate } +
+
); @@ -68,25 +85,92 @@ CustomClassName.args = { className: 'custom-class-name', }; +function ControlledDecorator( Story, props ) { + function nowWithZeroedSeconds() { + const now = new Date(); + now.setSeconds( 0 ); + now.setMilliseconds( 0 ); + return now; + } + + const [ controlledDate, setControlledDate ] = useState( + nowWithZeroedSeconds().toISOString() + ); + + return ( +
+ +
+ +
+
+ Controlled date: +
{ controlledDate } +
+
+
+
+ ); +} + export const Controlled = Template.bind( {} ); Controlled.args = { ...Basic.args, help: "I'm controlled by a container that uses React state", }; -Controlled.decorators = [ - ( story, props ) => { - return ( - - { ( controlledDate, setControlledDate ) => - story( { - args: { - currentDate: controlledDate, - onChange: setControlledDate, +Controlled.decorators = [ ControlledDecorator ]; + +export const ControlledDateOnly = Template.bind( {} ); +ControlledDateOnly.args = { + ...Controlled.args, + isDateOnlyPicker: true, +}; +ControlledDateOnly.decorators = Controlled.decorators; + +export const ControlledDateOnlyEndOfDay = Template.bind( {} ); +ControlledDateOnlyEndOfDay.args = { + ...ControlledDateOnly.args, + timeForDateOnly: 'end-of-day', +}; +ControlledDateOnlyEndOfDay.decorators = Controlled.decorators; + +function PopoverSlotDecorator( Story, props ) { + return ( +
+ +
+ - ); - }, -]; + } } + /> +
+ +
+
+ ); +} + +export const WithPopoverSlot = Template.bind( {} ); +WithPopoverSlot.args = { + ...Basic.args, + label: 'Start date', + placeholder: 'Enter the start date', + help: 'There is a SlotFillProvider and Popover.Slot on the page', + isDateOnlyPicker: true, +}; +WithPopoverSlot.decorators = [ PopoverSlotDecorator ]; diff --git a/packages/js/components/src/date-time-picker-control/test/index.tsx b/packages/js/components/src/date-time-picker-control/test/index.tsx index 7050c2e92eb..1088e875feb 100644 --- a/packages/js/components/src/date-time-picker-control/test/index.tsx +++ b/packages/js/components/src/date-time-picker-control/test/index.tsx @@ -3,6 +3,7 @@ */ import { render, waitFor, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { format as formatDate } from '@wordpress/date'; import { createElement, Fragment } from '@wordpress/element'; import moment from 'moment'; @@ -96,13 +97,53 @@ describe( 'DateTimePickerControl', () => { const { container } = render( ); const input = container.querySelector( 'input' ); expect( input?.value ).toBe( - dateTime.format( default24HourDateTimeFormat ) + formatDate( default24HourDateTimeFormat, dateTime ) + ); + } ); + + it( 'should assume ambiguous dates are UTC', () => { + const ambiguousISODateTimeString = '2202-09-15T22:30:40'; + + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + + expect( input?.value ).toBe( + formatDate( + default24HourDateTimeFormat, + moment.utc( ambiguousISODateTimeString ).local() + ) + ); + } ); + + it( 'should handle unambiguous UTC dates', () => { + const unambiguousISODateTimeString = '2202-09-15T22:30:40Z'; + + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + + expect( input?.value ).toBe( + formatDate( + default24HourDateTimeFormat, + moment.utc( unambiguousISODateTimeString ).local() + ) ); } ); @@ -112,13 +153,13 @@ describe( 'DateTimePickerControl', () => { const { container } = render( ); const input = container.querySelector( 'input' ); expect( input?.value ).toBe( - dateTime.format( default12HourDateTimeFormat ) + formatDate( default12HourDateTimeFormat, dateTime ) ); } ); @@ -134,7 +175,7 @@ describe( 'DateTimePickerControl', () => { ); const input = container.querySelector( 'input' ); - expect( input?.value ).toBe( dateTime.format( dateTimeFormat ) ); + expect( input?.value ).toBe( formatDate( dateTimeFormat, dateTime ) ); } ); it( 'should update the input when currentDate is changed', () => { @@ -144,20 +185,20 @@ describe( 'DateTimePickerControl', () => { const { container, rerender } = render( ); rerender( ); const input = container.querySelector( 'input' ); expect( input?.value ).toBe( - updatedDateTime.format( default24HourDateTimeFormat ) + formatDate( default24HourDateTimeFormat, updatedDateTime ) ); } ); @@ -189,9 +230,23 @@ describe( 'DateTimePickerControl', () => { ); } ); - it( 'should set the date time picker popup to 12 hour mode', async () => { + it( 'should set the picker popup to date and time by default', async () => { + const { container } = render( ); + + const input = container.querySelector( 'input' ); + + userEvent.click( input! ); + + await waitFor( () => + expect( + container.querySelector( '.components-datetime' ) + ).toBeInTheDocument() + ); + } ); + + it( 'should set the picker to 12 hour mode', async () => { const { container } = render( - + ); const input = container.querySelector( 'input' ); @@ -207,6 +262,25 @@ describe( 'DateTimePickerControl', () => { ); } ); + it( 'should set the picker popup to date only', async () => { + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + + userEvent.click( input! ); + + await waitFor( () => { + expect( + container.querySelector( '.components-datetime' ) + ).not.toBeInTheDocument(); + expect( + container.querySelector( '.components-datetime__date' ) + ).toBeInTheDocument(); + } ); + } ); + it( 'should call onBlur when losing focus', async () => { const onBlurHandler = jest.fn(); @@ -232,9 +306,9 @@ describe( 'DateTimePickerControl', () => { // TypeError: Cannot read properties of null (reading 'createEvent') it( 'should call onChange when the input is changed', async () => { const originalDateTime = moment( '2022-09-15 02:30:40' ); - const dateTimeFormat = 'HH:mm, MM-DD-YYYY'; - const newDateTimeInputString = '02:04, 06-08-2010'; - const newDateTime = moment( newDateTimeInputString, dateTimeFormat ); + const dateTimeFormat = 'm-d-Y, H:i'; + const newDateTimeInputString = '06-08-2010, 02:04'; + const newDateTime = moment( newDateTimeInputString ); const onChangeHandler = jest.fn(); const { container } = render( @@ -262,6 +336,122 @@ describe( 'DateTimePickerControl', () => { ); }, 10000 ); + // We need to bump up the timeout for this test because: + // 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577) + // 2. moment.js is slow + // Otherwise, the following error can occur on slow machines (such as our CI), because Jest times out and starts + // tearing down the component while test microtasks are still being executed + // (see https://github.com/facebook/jest/issues/12670) + // TypeError: Cannot read properties of null (reading 'createEvent') + it( 'should force time to the start of the day if date only', async () => { + const originalDateTime = moment( '09-15-2022' ); + const newDateTimeInputString = '06-08-2010'; + const newDateTime = moment( newDateTimeInputString ).startOf( 'day' ); + const onChangeHandler = jest.fn(); + + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + userEvent.type( + input!, + '{selectall}{backspace}' + newDateTimeInputString + ); + + await waitFor( + () => + expect( onChangeHandler ).toHaveBeenLastCalledWith( + newDateTime.toISOString(), + true + ), + { timeout: 100 } + ); + }, 10000 ); + + // We need to bump up the timeout for this test because: + // 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577) + // 2. moment.js is slow + // Otherwise, the following error can occur on slow machines (such as our CI), because Jest times out and starts + // tearing down the component while test microtasks are still being executed + // (see https://github.com/facebook/jest/issues/12670) + // TypeError: Cannot read properties of null (reading 'createEvent') + it( 'should force time to the end of the day if date only', async () => { + const originalDateTime = moment( '09-15-2022' ); + const newDateTimeInputString = '06-08-2010'; + const newDateTime = moment( newDateTimeInputString ).endOf( 'day' ); + const onChangeHandler = jest.fn(); + + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + userEvent.type( + input!, + '{selectall}{backspace}' + newDateTimeInputString + ); + + await waitFor( + () => + expect( onChangeHandler ).toHaveBeenLastCalledWith( + newDateTime.toISOString(), + true + ), + { timeout: 100 } + ); + }, 10000 ); + + // We need to bump up the timeout for this test because: + // 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577) + // 2. moment.js is slow + // Otherwise, the following error can occur on slow machines (such as our CI), because Jest times out and starts + // tearing down the component while test microtasks are still being executed + // (see https://github.com/facebook/jest/issues/12670) + // TypeError: Cannot read properties of null (reading 'createEvent') + it( 'should not force time to the start of the day if not date only', async () => { + const originalDateTime = moment( '09-15-2022' ); + const newDateTimeInputString = '06-08-2010 7:00'; + const newDateTime = moment( newDateTimeInputString ); + const onChangeHandler = jest.fn(); + + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + userEvent.type( + input!, + '{selectall}{backspace}' + newDateTimeInputString + ); + + await waitFor( + () => + expect( onChangeHandler ).toHaveBeenLastCalledWith( + newDateTime.toISOString(), + true + ), + { timeout: 100 } + ); + }, 10000 ); + // We need to bump up the timeout for this test because: // 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577) // 2. moment.js is slow @@ -304,9 +494,9 @@ describe( 'DateTimePickerControl', () => { // TypeError: Cannot read properties of null (reading 'createEvent') it( 'should call the current onChange when the input is changed', async () => { const originalDateTime = moment( '2022-09-15 02:30:40' ); - const dateTimeFormat = 'HH:mm, MM-DD-YYYY'; - const newDateTimeInputString = '02:04, 06-08-2010'; - const newDateTime = moment( newDateTimeInputString, dateTimeFormat ); + const dateTimeFormat = 'm-d-Y, H:i'; + const newDateTimeInputString = '06-08-2010, 02:04'; + const newDateTime = moment( newDateTimeInputString ); const originalOnChangeHandler = jest.fn(); const newOnChangeHandler = jest.fn(); @@ -469,4 +659,112 @@ describe( 'DateTimePickerControl', () => { await waitFor( () => expect( onChangeHandler ).not.toHaveBeenCalled() ); } ); + + it( 'should not call onChange if currentDate is set to an equivalent UTC date without Zulu offset specifier', async () => { + const originalDateTime = '2023-01-01T00:00:00Z'; + const equivalentDateTimeWithoutZulu = '2023-01-01T00:00:00'; + const onChangeHandler = jest.fn(); + + const { rerender } = render( + + ); + + // re-render the component; we do this to then test whether our onChange still gets called + rerender( + + ); + + await waitFor( () => expect( onChangeHandler ).not.toHaveBeenCalled() ); + } ); + + it( 'should not call onChange if currentDate is set to an equivalent UTC date without time', async () => { + const originalDateTime = '2023-01-01T00:00:00Z'; + const equivalentDateTimeWithoutTime = '2023-01-01'; + const onChangeHandler = jest.fn(); + + const { rerender } = render( + + ); + + // re-render the component; we do this to then test whether our onChange still gets called + rerender( + + ); + + await waitFor( () => expect( onChangeHandler ).not.toHaveBeenCalled() ); + } ); + + it( 'should not call onChange when the dateTimeFormat changes', async () => { + // we are specifically using a date with seconds in it, with a format + // without seconds in it; this helps us to determine if the currentDate + // is getting re-parsed from the input string (if it does this, it + // would result in a different date) + const originalDateTime = moment( '2022-11-15 02:30:40' ); + const originalDateTimeFormat = 'm-d-Y, H:i'; + const newDateTimeFormat = 'Y-m-d H:i'; + const onChangeHandler = jest.fn(); + + const { rerender } = render( + + ); + + // re-render the component; we do this to then test whether our onChange still gets called + rerender( + + ); + + await waitFor( () => expect( onChangeHandler ).not.toHaveBeenCalled() ); + } ); + + // We need to bump up the timeout for this test because: + // 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577) + // 2. moment.js is slow + // Otherwise, the following error can occur on slow machines (such as our CI), because Jest times out and starts + // tearing down the component while test microtasks are still being executed + // (see https://github.com/facebook/jest/issues/12670) + // TypeError: Cannot read properties of null (reading 'createEvent') + it( 'should not call onChange when the input is changed to an equivalent date', async () => { + const originalDateTime = moment( '2022-09-15' ); + const newDateTimeInputString = 'September 9, 2022'; + const onChangeHandler = jest.fn(); + + const { container } = render( + + ); + + const input = container.querySelector( 'input' ); + userEvent.type( + input!, + '{selectall}{backspace}' + newDateTimeInputString + ); + + await waitFor( () => expect( onChangeHandler ).not.toHaveBeenCalled(), { + timeout: 10000, + } ); + }, 10000 ); } ); diff --git a/packages/js/components/src/enriched-label/README.md b/packages/js/components/src/enriched-label/README.md deleted file mode 100644 index a48c1ef982d..00000000000 --- a/packages/js/components/src/enriched-label/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# EnrichedLabel - -Use `EnrichedLabel` to create a label with a tooltip. - -## Usage - -```jsx - alert( 'Learn More clicked' ) } -/> -``` - -### Props - -| Name | Type | Default | Description | -| --------------------- | -------- | ------- | ----------------------------------------------------------------------- | -| `helpDescription` | String | `null` | Text that will be shown in the tooltip. | -| `label` | String | `null` | Text that will be shown in the label. | -| `moreUrl` | String | `null` | URL that will be added to the link `Learn More`, shown after the label. | -| `tooltipLinkCallback` | Function | `noop` | Callback that will be triggered after clicking the `Learn More` link. | diff --git a/packages/js/components/src/enriched-label/enriched-label.tsx b/packages/js/components/src/enriched-label/enriched-label.tsx deleted file mode 100644 index 93055a774bb..00000000000 --- a/packages/js/components/src/enriched-label/enriched-label.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Button, Popover } from '@wordpress/components'; -import { createElement, Fragment, useState } from '@wordpress/element'; -import interpolateComponents from '@automattic/interpolate-components'; -import { Icon, help } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import Link from '../link'; - -type EnrichedLabelProps = { - helpDescription: string; - label: string; - moreUrl: string; - tooltipLinkCallback: () => void; -}; - -export const EnrichedLabel: React.FC< EnrichedLabelProps > = ( { - helpDescription, - label, - moreUrl, - tooltipLinkCallback, -} ) => { - const [ isPopoverVisible, setIsPopoverVisible ] = useState( false ); - - return ( - <> - { label } - { helpDescription && ( -
setIsPopoverVisible( false ) } - > - - - { isPopoverVisible && ( - - { interpolateComponents( { - mixedString: - helpDescription + - ( moreUrl ? ' {{moreLink/}}' : '' ), - components: { - moreLink: moreUrl ? ( - - { __( - 'Learn more', - 'woocommerce' - ) } - - ) : ( -
- ), - }, - } ) } - - ) } -
- ) } - - ); -}; diff --git a/packages/js/components/src/enriched-label/index.js b/packages/js/components/src/enriched-label/index.js deleted file mode 100644 index 52c22688576..00000000000 --- a/packages/js/components/src/enriched-label/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './enriched-label'; diff --git a/packages/js/components/src/enriched-label/stories/index.js b/packages/js/components/src/enriched-label/stories/index.js deleted file mode 100644 index ae0d300bf95..00000000000 --- a/packages/js/components/src/enriched-label/stories/index.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * External dependencies - */ -import { CheckboxControl } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { EnrichedLabel } from '../'; -import './style.scss'; - -export default { - title: 'WooCommerce Admin/components/EnrichedLabel', - component: EnrichedLabel, - argTypes: { - tooltipLinkCallback: { action: 'tooltipLinkCallback' }, - }, -}; - -const Template = ( args ) => ( - { - // eslint-disable-next-line no-alert - window.alert( 'Learn More clicked' ); - } } - { ...args } - /> -); - -export const Basic = Template.bind( {} ); -Basic.decorators = [ - ( story, props ) => { - return ( - {} } - /> - ); - }, -]; diff --git a/packages/js/components/src/enriched-label/stories/style.scss b/packages/js/components/src/enriched-label/stories/style.scss deleted file mode 100644 index 1143aceb998..00000000000 --- a/packages/js/components/src/enriched-label/stories/style.scss +++ /dev/null @@ -1,23 +0,0 @@ -.woocommerce-enriched-label-story__checkbox-control { - .woocommerce-enriched-label__help-wrapper { - .components-popover { - margin: 0; - } - } - .components-base-control__field { - display: flex; - .components-checkbox-control { - &__label { - display: flex; - } - - &__input-container { - align-self: center; - } - - .woocommerce-enriched-label__text { - align-self: center; - } - } - } -} diff --git a/packages/js/components/src/enriched-label/style.scss b/packages/js/components/src/enriched-label/style.scss deleted file mode 100644 index a2592f13d75..00000000000 --- a/packages/js/components/src/enriched-label/style.scss +++ /dev/null @@ -1,18 +0,0 @@ -.woocommerce-enriched-label__text { - align-self: center; -} -.woocommerce-enriched-label__help-wrapper { - .components-button { - padding: 0; - height: 28px; - } - .components-popover { - .components-popover__content { - min-width: 360px; - > div { - padding: $gap $gap-large; - font-size: 16px; - } - } - } -} diff --git a/packages/js/components/src/experimental-select-control/menu.scss b/packages/js/components/src/experimental-select-control/menu.scss index 3592eec2d9b..7c1f126b447 100644 --- a/packages/js/components/src/experimental-select-control/menu.scss +++ b/packages/js/components/src/experimental-select-control/menu.scss @@ -5,13 +5,25 @@ left: 0; margin-top: $gap-smaller; box-sizing: border-box; - display: none; +} + +.components-popover.woocommerce-experimental-select-control__popover-menu { background: $studio-white; border: 1px solid $studio-gray-5; border-radius: 3px; - z-index: 10; - + display: none; &.is-open.has-results { display: block; } } +.woocommerce-experimental-select-control__popover-menu-container { + margin: 0; + max-height: 300px; + overflow-y: scroll; + + > .category-field-dropdown__item:not( :first-child ) { + .category-field-dropdown__item-content { + border-top: 1px solid $gray-200; + } + } +} diff --git a/packages/js/components/src/experimental-select-control/menu.tsx b/packages/js/components/src/experimental-select-control/menu.tsx index 22e2adb552b..60a8cbb6e4e 100644 --- a/packages/js/components/src/experimental-select-control/menu.tsx +++ b/packages/js/components/src/experimental-select-control/menu.tsx @@ -1,8 +1,16 @@ /** * External dependencies */ +import { Popover } from '@wordpress/components'; import classnames from 'classnames'; -import { createElement } from '@wordpress/element'; +import { + createElement, + useEffect, + useRef, + useState, + createPortal, + Children, +} from '@wordpress/element'; /** * Internal dependencies @@ -22,21 +30,65 @@ export const Menu = ( { isOpen, className, }: MenuProps ) => { + const [ boundingRect, setBoundingRect ] = useState< DOMRect >(); + const selectControlMenuRef = useRef< HTMLDivElement >( null ); + + useEffect( () => { + if ( selectControlMenuRef.current?.parentElement ) { + setBoundingRect( + selectControlMenuRef.current.parentElement.getBoundingClientRect() + ); + } + }, [ selectControlMenuRef.current ] ); + + /* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ + /* Disabled because of the onmouseup on the ul element below. */ return ( -
    - { isOpen && children } -
+ 0, + } + ) } + position="bottom center" + animate={ false } + > +
    + // Fix to prevent select control dropdown from closing when selecting within the Popover. + e.stopPropagation() + } + > + { isOpen && children } +
+
+
); + /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ }; + +export const MenuSlot: React.FC = () => + createPortal( +
+ { /* @ts-expect-error name does exist on PopoverSlot see: https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/popover/index.tsx#L555 */ } + +
, + document.body + ); diff --git a/packages/js/components/src/experimental-select-control/select-control.tsx b/packages/js/components/src/experimental-select-control/select-control.tsx index f6bc95457d5..8545e652112 100644 --- a/packages/js/components/src/experimental-select-control/select-control.tsx +++ b/packages/js/components/src/experimental-select-control/select-control.tsx @@ -48,7 +48,10 @@ type SelectControlProps< ItemType > = { ) => ItemType[]; hasExternalTags?: boolean; multiple?: boolean; - onInputChange?: ( value: string | undefined ) => void; + onInputChange?: ( + value: string | undefined, + changes: Partial< Omit< UseComboboxState< ItemType >, 'inputValue' > > + ) => void; onRemove?: ( item: ItemType ) => void; onSelect?: ( selected: ItemType ) => void; onFocus?: ( data: { inputValue: string } ) => void; @@ -59,6 +62,7 @@ type SelectControlProps< ItemType > = { placeholder?: string; selected: ItemType | ItemType[] | null; className?: string; + disabled?: boolean; }; export const selectControlStateChangeTypes = useCombobox.stateChangeTypes; @@ -102,6 +106,7 @@ function SelectControl< ItemType = DefaultItemType >( { placeholder, selected, className, + disabled, }: SelectControlProps< ItemType > ) { const [ isFocused, setIsFocused ] = useState( false ); const [ inputValue, setInputValue ] = useState( '' ); @@ -150,16 +155,14 @@ function SelectControl< ItemType = DefaultItemType >( { initialSelectedItem: singleSelectedItem, inputValue, items: filteredItems, - selectedItem: multiple ? null : undefined, + selectedItem: multiple ? null : singleSelectedItem, itemToString: getItemLabel, onSelectedItemChange: ( { selectedItem } ) => selectedItem && onSelect( selectedItem ), - onInputValueChange: ( changes ) => { - if ( changes.inputValue !== undefined ) { - setInputValue( changes.inputValue ); - if ( changes.isOpen ) { - onInputChange( changes.inputValue ); - } + onInputValueChange: ( { inputValue: value, ...changes } ) => { + if ( value !== undefined ) { + setInputValue( value ); + onInputChange( value, changes ); } }, stateReducer: ( state, actionAndChanges ) => { @@ -225,12 +228,14 @@ function SelectControl< ItemType = DefaultItemType >( { > { /* Downshift's getLabelProps handles the necessary label attributes. */ } { /* eslint-disable jsx-a11y/label-has-for */ } - + { label && ( + + ) } { /* eslint-enable jsx-a11y/label-has-for */ } ( { }, onBlur: () => setIsFocused( false ), placeholder, + disabled, } ) } > <> diff --git a/packages/js/components/src/experimental-select-control/stories/index.tsx b/packages/js/components/src/experimental-select-control/stories/index.tsx index c36cae46cfd..c13f9c88ebd 100644 --- a/packages/js/components/src/experimental-select-control/stories/index.tsx +++ b/packages/js/components/src/experimental-select-control/stories/index.tsx @@ -1,7 +1,13 @@ /** * External dependencies */ -import { CheckboxControl, Spinner } from '@wordpress/components'; +import { + Button, + CheckboxControl, + Modal, + SlotFillProvider, + Spinner, +} from '@wordpress/components'; import React from 'react'; import { createElement, useState } from '@wordpress/element'; @@ -11,7 +17,7 @@ import { createElement, useState } from '@wordpress/element'; import { SelectedType, DefaultItemType, getItemLabelType } from '../types'; import { MenuItem } from '../menu-item'; import { SelectControl, selectControlStateChangeTypes } from '../'; -import { Menu } from '../menu'; +import { Menu, MenuSlot } from '../menu'; const sampleItems = [ { value: 'apple', label: 'Apple' }, @@ -365,6 +371,45 @@ export const CustomItemType: React.FC = () => { ); }; +export const SingleWithinModalUsingBodyDropdownPlacement: React.FC = () => { + const [ isOpen, setOpen ] = useState( true ); + const [ selected, setSelected ] = + useState< SelectedType< DefaultItemType > >(); + const [ selectedTwo, setSelectedTwo ] = + useState< SelectedType< DefaultItemType > >(); + + return ( + + Selected: { JSON.stringify( selected ) } + + { isOpen && ( + setOpen( false ) } + > + item && setSelected( item ) } + onRemove={ () => setSelected( null ) } + /> + item && setSelectedTwo( item ) } + onRemove={ () => setSelectedTwo( null ) } + /> + + ) } + + + ); +}; + export default { title: 'WooCommerce Admin/experimental/SelectControl', component: SelectControl, diff --git a/packages/js/components/src/form/form-context.ts b/packages/js/components/src/form/form-context.ts index 678640f2631..d7ca500810d 100644 --- a/packages/js/components/src/form/form-context.ts +++ b/packages/js/components/src/form/form-context.ts @@ -1,9 +1,18 @@ /** * External dependencies */ -import { ChangeEvent } from 'react'; import { createContext, useContext } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { + CheckboxProps, + ConsumerInputProps, + InputProps, + SelectControlProps, +} from './form'; + export type FormErrors< Values > = { [ P in keyof Values ]?: FormErrors< Values[ P ] > | string; }; @@ -21,17 +30,18 @@ export type FormContext< Values extends Record< string, any > > = { setValue: ( name: string, value: any ) => void; setValues: ( valuesToSet: Values ) => void; handleSubmit: () => Promise< Values >; + getCheckboxControlProps< Value extends Values[ keyof Values ] >( + name: string, + inputProps?: ConsumerInputProps< Values > + ): CheckboxProps< Values, Value >; + getSelectControlProps< Value extends Values[ keyof Values ] >( + name: string, + inputProps?: ConsumerInputProps< Values > + ): SelectControlProps< Values, Value >; getInputProps< Value extends Values[ keyof Values ] >( - name: string - ): { - value: Value; - checked: boolean; - selected?: boolean; - onChange: ( value: ChangeEvent< HTMLInputElement > | Value ) => void; - onBlur: () => void; - className: string | undefined; - help: string | null | undefined; - }; + name: string, + inputProps?: ConsumerInputProps< Values > + ): InputProps< Values, Value >; isValidForm: boolean; resetForm: ( initialValues: Values, @@ -48,7 +58,7 @@ export const FormContext = createContext< FormContext< any > >( // eslint-disable-next-line @typescript-eslint/no-explicit-any export function useFormContext< Values extends Record< string, any > >() { - const formik = useContext< FormContext< Values > >( FormContext ); + const formContext = useContext< FormContext< Values > >( FormContext ); - return formik; + return formContext; } diff --git a/packages/js/components/src/form/form.tsx b/packages/js/components/src/form/form.tsx index 4298978038c..878c6ce6ef0 100644 --- a/packages/js/components/src/form/form.tsx +++ b/packages/js/components/src/form/form.tsx @@ -1,6 +1,7 @@ /** * External dependencies */ +import classnames from 'classnames'; import { cloneElement, useState, @@ -17,6 +18,7 @@ import _setWith from 'lodash/setWith'; import _get from 'lodash/get'; import _clone from 'lodash/clone'; import _isEqual from 'lodash/isEqual'; +import _omit from 'lodash/omit'; /** * Internal dependencies @@ -87,6 +89,40 @@ export type FormRef< Values > = { resetForm: ( initialValues: Values ) => void; }; +export type InputProps< Values, Value > = { + value: Value; + checked: boolean; + selected?: boolean; + onChange: ( + value: ChangeEvent< HTMLInputElement > | Values[ keyof Values ] + ) => void; + onBlur: () => void; + className: string | undefined; + help: string | null | undefined; +}; + +export type CheckboxProps< Values, Value > = Omit< + InputProps< Values, Value >, + 'value' | 'selected' +>; + +export type SelectControlProps< Values, Value > = Omit< + InputProps< Values, Value >, + 'value' +> & { + value: string | undefined; +}; + +export type ConsumerInputProps< Values > = { + className?: string; + onChange?: ( + value: ChangeEvent< HTMLInputElement > | Values[ keyof Values ] + ) => void; + onBlur?: () => void; + [ key: string ]: unknown; + sanitize?: ( value: Values[ keyof Values ] ) => Values[ keyof Values ]; +}; + /** * A form component to handle form state and provide input helper props. */ @@ -268,21 +304,19 @@ function FormComponent< Values extends Record< string, any > >( }; function getInputProps< Value = Values[ keyof Values ] >( - name: string - ): { - value: Value; - checked: boolean; - selected?: boolean; - onChange: ( - value: ChangeEvent< HTMLInputElement > | Values[ keyof Values ] - ) => void; - onBlur: () => void; - className: string | undefined; - help: string | null | undefined; - } { + name: string, + inputProps: ConsumerInputProps< Values > = {} + ): InputProps< Values, Value > { const inputValue = _get( values, name ); const isTouched = touched[ name ]; const inputError = _get( errors, name ); + const { + className: classNameProp, + onBlur: onBlurProp, + onChange: onChangeProp, + sanitize, + ...additionalProps + } = inputProps; return { value: inputValue, @@ -290,10 +324,50 @@ function FormComponent< Values extends Record< string, any > >( selected: inputValue, onChange: ( value: ChangeEvent< HTMLInputElement > | Values[ keyof Values ] - ) => handleChange( name, value ), - onBlur: () => handleBlur( name ), - className: isTouched && inputError ? 'has-error' : undefined, + ) => { + handleChange( name, value ); + if ( onChangeProp ) { + onChangeProp( value ); + } + }, + onBlur: () => { + if ( sanitize ) { + handleChange( name, sanitize( inputValue ) ); + } + handleBlur( name ); + if ( onBlurProp ) { + onBlurProp(); + } + }, + className: classnames( classNameProp, { + 'has-error': isTouched && inputError, + } ), help: isTouched ? ( inputError as string ) : null, + ...additionalProps, + }; + } + + function getCheckboxControlProps< Value = Values[ keyof Values ] >( + name: string, + inputProps: ConsumerInputProps< Values > = {} + ): CheckboxProps< Values, Value > { + return _omit( getInputProps( name, inputProps ), [ + 'selected', + 'value', + ] ); + } + + function getSelectControlProps< Value = Values[ keyof Values ] >( + name: string, + inputProps: ConsumerInputProps< Values > = {} + ): SelectControlProps< Values, Value > { + const selectControlProps = getInputProps( name, inputProps ); + return { + ...selectControlProps, + value: + selectControlProps.value === undefined + ? undefined + : String( selectControlProps.value ), }; } @@ -312,7 +386,9 @@ function FormComponent< Values extends Record< string, any > >( setValue, setValues, handleSubmit, + getCheckboxControlProps, getInputProps, + getSelectControlProps, isValidForm: ! Object.keys( errors ).length, resetForm, }; diff --git a/packages/js/components/src/form/stories/index.js b/packages/js/components/src/form/stories/index.js index 903d6bbec56..6aed2db896b 100644 --- a/packages/js/components/src/form/stories/index.js +++ b/packages/js/components/src/form/stories/index.js @@ -9,7 +9,8 @@ import { TextControl, } from '@wordpress/components'; import { useState } from '@wordpress/element'; -import { Form } from '@woocommerce/components'; +import { Form, DateTimePickerControl } from '@woocommerce/components'; +import moment from 'moment'; const validate = ( values ) => { const errors = {}; @@ -19,6 +20,9 @@ const validate = ( values ) => { if ( values.lastName.length < 3 ) { errors.lastName = 'Last name must be at least 3 characters'; } + if ( ! moment( values.date, moment.ISO_8601, true ).isValid() ) { + errors.date = 'Invalid date'; + } return errors; }; @@ -28,6 +32,7 @@ const initialValues = { firstName: '', lastName: '', select: '3', + date: '2014-10-24T13:02', checkbox: true, radio: 'one', }; @@ -68,6 +73,13 @@ export const Basic = () => { ] } { ...getInputProps( 'select' ) } /> + { const formProps = useFormContext< { foo: string } >(); @@ -407,6 +408,68 @@ describe( 'Form', () => { ); } ); + // We need to bump up the timeout for this test because: + // 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577) + // 2. moment.js is slow + // Otherwise, the following error can occur on slow machines (such as our CI), because Jest times out and starts + // tearing down the component while test microtasks are still being executed + // (see https://github.com/facebook/jest/issues/12670) + // TypeError: Cannot read properties of null (reading 'createEvent') + it( 'should provide props that automatically handle DateTimePickerControl changes', async () => { + const newDateTimeInputString = 'invalid input'; + + type TestData = { date: string }; + + const mockOnChange = jest.fn(); + + function validate(): Record< string, string > { + return { date: 'This is a bad date' }; + } + + const { container, queryByText } = render( + onChange={ mockOnChange } validate={ validate }> + { ( { getInputProps, values }: FormContext< TestData > ) => { + return ( + + ); + } } + + ); + + const controlRoot = container.querySelector( + '.woocommerce-date-time-picker-control' + ); + + const input = controlRoot?.querySelector( 'input' ); + userEvent.type( + input!, + '{selectall}{backspace}' + newDateTimeInputString + ); + fireEvent.blur( input! ); + + await waitFor( + () => { + expect( mockOnChange ).toHaveBeenLastCalledWith( + { name: 'date', value: newDateTimeInputString }, + { date: newDateTimeInputString }, + false + ); + expect( controlRoot?.classList.contains( 'has-error' ) ).toBe( + true + ); + expect( + queryByText( 'This is a bad date' ) + ).toBeInTheDocument(); + }, + { timeout: 100 } + ); + }, 10000 ); + describe( 'FormContext', () => { it( 'should allow nested field to use useFormContext to set field value', async () => { const mockOnChange = jest.fn(); diff --git a/packages/js/components/src/image-gallery/image-gallery-item.tsx b/packages/js/components/src/image-gallery/image-gallery-item.tsx index 5c781929e0b..8a775661722 100644 --- a/packages/js/components/src/image-gallery/image-gallery-item.tsx +++ b/packages/js/components/src/image-gallery/image-gallery-item.tsx @@ -9,7 +9,7 @@ import { createElement, Fragment } from '@wordpress/element'; */ import Pill from '../pill'; import { SortableHandle, NonSortableItem } from '../sortable'; -import { ConditionalWrapper } from '../util/conditional-wrapper'; +import { ConditionalWrapper } from '../conditional-wrapper'; export type ImageGalleryItemProps = { id?: string; diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts index 19663facc15..38a03508a74 100644 --- a/packages/js/components/src/index.ts +++ b/packages/js/components/src/index.ts @@ -4,6 +4,7 @@ export { default as AnimationSlider } from './animation-slider'; export { default as Chart } from './chart'; export { default as ChartPlaceholder } from './chart/placeholder'; export { CompareButton, CompareFilter } from './compare-filter'; +export { ConditionalWrapper as __experimentalConditionalWrapper } from './conditional-wrapper'; export { default as Date } from './date'; export { default as DateRangeFilterPicker } from './date-range-filter-picker'; export { default as DateRange } from './calendar/date-range'; @@ -49,7 +50,10 @@ export { MenuItem as __experimentalSelectControlMenuItem, MenuItemProps as __experimentalSelectControlMenuItemProps, } from './experimental-select-control/menu-item'; -export { Menu as __experimentalSelectControlMenu } from './experimental-select-control/menu'; +export { + Menu as __experimentalSelectControlMenu, + MenuSlot as __experimentalSelectControlMenuSlot, +} from './experimental-select-control/menu'; export { default as ScrollTo } from './scroll-to'; export { Sortable } from './sortable'; export { ListItem } from './list-item'; @@ -71,11 +75,11 @@ export { default as Tag } from './tag'; export { default as TextControl } from './text-control'; export { default as TextControlWithAffixes } from './text-control-with-affixes'; export { default as Timeline } from './timeline'; +export { Tooltip as __experimentalTooltip } from './tooltip'; export { default as ViewMoreList } from './view-more-list'; export { default as WebPreview } from './web-preview'; export { Badge } from './badge'; export { DynamicForm } from './dynamic-form'; -export { EnrichedLabel } from './enriched-label'; export { default as TourKit } from './tour-kit'; export * as TourKitTypes from './tour-kit/types'; export { CollapsibleContent } from './collapsible-content'; diff --git a/packages/js/components/src/rich-text-editor/editor-writing-flow.tsx b/packages/js/components/src/rich-text-editor/editor-writing-flow.tsx index b56a6ef08fd..2734c566484 100644 --- a/packages/js/components/src/rich-text-editor/editor-writing-flow.tsx +++ b/packages/js/components/src/rich-text-editor/editor-writing-flow.tsx @@ -3,8 +3,8 @@ */ import { useSelect, useDispatch } from '@wordpress/data'; import { useInstanceId } from '@wordpress/compose'; +import { BlockInstance, createBlock } from '@wordpress/blocks'; import { createElement, useEffect } from '@wordpress/element'; -import { createBlock } from '@wordpress/blocks'; import { BlockList, ObserveTyping, @@ -15,37 +15,50 @@ import { WritingFlow, } from '@wordpress/block-editor'; -export const EditorWritingFlow: React.VFC = () => { +type EditorWritingFlowProps = { + blocks: BlockInstance[]; + onChange: ( changes: BlockInstance[] ) => void; + placeholder?: string; +}; + +export const EditorWritingFlow = ( { + blocks, + onChange, + placeholder = '', +}: EditorWritingFlowProps ) => { const instanceId = useInstanceId( EditorWritingFlow ); + const firstBlock = blocks[ 0 ]; + const isEmpty = ! blocks.length; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore This action is available in the block editor data store. - const { insertBlock } = useDispatch( blockEditorStore ); - - const { isEmpty } = useSelect( ( select ) => { - const blocks = select( 'core/block-editor' ).getBlocks(); - + const { insertBlock, selectBlock } = useDispatch( blockEditorStore ); + const { selectedBlockClientIds } = useSelect( ( select ) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore This selector is available in the block editor data store. const { getSelectedBlockClientIds } = select( blockEditorStore ); return { - isEmpty: blocks.length - ? blocks.length <= 1 && - blocks[ 0 ].attributes?.content?.trim() === '' - : true, - firstBlock: blocks[ 0 ], selectedBlockClientIds: getSelectedBlockClientIds(), }; } ); + useEffect( () => { + if ( selectedBlockClientIds?.length || ! firstBlock ) { + return; + } + selectBlock( firstBlock.clientId ); + }, [ firstBlock, selectedBlockClientIds ] ); + useEffect( () => { if ( isEmpty ) { const initialBlock = createBlock( 'core/paragraph', { content: '', + placeholder, } ); insertBlock( initialBlock ); + onChange( [ initialBlock ] ); } - }, [] ); + }, [ isEmpty ] ); return ( /* Gutenberg handles the keyboard events when focusing the content editable area. */ @@ -60,8 +73,6 @@ export const EditorWritingFlow: React.VFC = () => { - { /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ } - { /* @ts-ignore This action is available in the block editor data store. */ } diff --git a/packages/js/components/src/rich-text-editor/rich-text-editor.tsx b/packages/js/components/src/rich-text-editor/rich-text-editor.tsx index 0db10c3439d..3920a283970 100644 --- a/packages/js/components/src/rich-text-editor/rich-text-editor.tsx +++ b/packages/js/components/src/rich-text-editor/rich-text-editor.tsx @@ -1,18 +1,14 @@ /** * External dependencies */ +import { BaseControl, Popover, SlotFillProvider } from '@wordpress/components'; import { BlockEditorProvider } from '@wordpress/block-editor'; import { BlockInstance } from '@wordpress/blocks'; -import { SlotFillProvider } from '@wordpress/components'; +import { createElement, useEffect, useState, useRef } from '@wordpress/element'; import { debounce } from 'lodash'; -import { - createElement, - useCallback, - useEffect, - useState, - useRef, -} from '@wordpress/element'; import React from 'react'; +import { uploadMedia } from '@wordpress/media-utils'; +import { useUser } from '@woocommerce/data'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore No types for this exist yet. // eslint-disable-next-line @woocommerce/dependency-group @@ -28,16 +24,20 @@ registerBlocks(); type RichTextEditorProps = { blocks: BlockInstance[]; + label?: string; onChange: ( changes: BlockInstance[] ) => void; entryId?: string; + placeholder?: string; }; export const RichTextEditor: React.VFC< RichTextEditorProps > = ( { blocks, + label, onChange, + placeholder = '', } ) => { const blocksRef = useRef( blocks ); - + const { currentUserCan } = useUser(); const [ , setRefresh ] = useState( 0 ); // If there is a props change we need to update the ref and force re-render. @@ -59,8 +59,29 @@ export const RichTextEditor: React.VFC< RichTextEditorProps > = ( { forceRerender(); }, 200 ); + const mediaUpload = currentUserCan( 'upload_files' ) + ? ( { + onError, + ...rest + }: { + onError: ( message: string ) => void; + } ) => { + uploadMedia( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore The upload function passes the remaining required props. + { + onError: ( { message } ) => onError( message ), + ...rest, + } + ); + } + : undefined; + return (
+ { label && ( + { label } + ) } = ( { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore This property was recently added in the block editor data store. __experimentalClearBlockSelection: false, + mediaUpload, } } onInput={ debounceChange } onChange={ debounceChange } > - + +
diff --git a/packages/js/components/src/rich-text-editor/style.scss b/packages/js/components/src/rich-text-editor/style.scss index c1263c39b85..722e98d31bf 100644 --- a/packages/js/components/src/rich-text-editor/style.scss +++ b/packages/js/components/src/rich-text-editor/style.scss @@ -1,27 +1,22 @@ +$toolbar-height: 40px; + .woocommerce-rich-text-editor { - border: 1px solid $gray-600; - border-radius: 2px; - background: $white; + .woocommerce-rich-text-editor__writing-flow { + border: 1px solid $gray-600; + border-radius: 2px; + background: $white; + } * { box-sizing: border-box; } - .block-editor-inserter { - display: none; - } - .block-editor-block-contextual-toolbar.is-fixed, .block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar .components-toolbar-group, .block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar .components-toolbar { border-color: $gray-600; } - /* Hide rich text placeholder text */ - .rich-text [data-rich-text-placeholder] { - display: none !important; - } - /* hide block boundary background styling */ .rich-text:focus *[data-rich-text-format-boundary] { background: none !important; @@ -37,11 +32,6 @@ outline: none; } - .block-editor-block-list__empty-block-inserter, - .block-editor-block-list__insertion-point { - display: none !important; - } - .block-editor-writing-flow { padding: $gap-small; } @@ -54,11 +44,25 @@ } .components-accessible-toolbar { + height: $toolbar-height; width: 100%; background-color: $white; border-color: $gray-700; border-bottom-left-radius: 0; border-bottom-right-radius: 0; + + .components-button { + height: $toolbar-height; + } + } + + .block-editor-block-mover:not(.is-horizontal) .block-editor-block-mover__move-button-container > * { + height: calc( $toolbar-height / 2 ); + } + + .block-editor-block-contextual-toolbar.is-fixed, + .components-toolbar-group { + min-height: $toolbar-height; } .wp-block-quote { diff --git a/packages/js/components/src/rich-text-editor/utils/register-blocks.ts b/packages/js/components/src/rich-text-editor/utils/register-blocks.ts index cdcf514835e..566ff239bef 100644 --- a/packages/js/components/src/rich-text-editor/utils/register-blocks.ts +++ b/packages/js/components/src/rich-text-editor/utils/register-blocks.ts @@ -14,6 +14,8 @@ export const HEADING_BLOCK_ID = 'core/heading'; export const LIST_BLOCK_ID = 'core/list'; export const LIST_ITEM_BLOCK_ID = 'core/list-item'; export const QUOTE_BLOCK_ID = 'core/quote'; +export const IMAGE_BLOCK_ID = 'core/image'; +export const VIDEO_BLOCK_ID = 'core/video'; const ALLOWED_CORE_BLOCKS = [ PARAGRAPH_BLOCK_ID, @@ -21,6 +23,8 @@ const ALLOWED_CORE_BLOCKS = [ LIST_BLOCK_ID, LIST_ITEM_BLOCK_ID, QUOTE_BLOCK_ID, + IMAGE_BLOCK_ID, + VIDEO_BLOCK_ID, ]; const registerCoreBlocks = () => { diff --git a/packages/js/components/src/style.scss b/packages/js/components/src/style.scss index e10f13b53f6..a3d439bc9e2 100644 --- a/packages/js/components/src/style.scss +++ b/packages/js/components/src/style.scss @@ -46,11 +46,11 @@ @import 'tag/style.scss'; @import 'text-control/style.scss'; @import 'text-control-with-affixes/style.scss'; +@import 'tooltip/style.scss'; @import 'timeline/style.scss'; @import 'view-more-list/style.scss'; @import 'web-preview/style.scss'; @import 'badge/style.scss'; @import 'dynamic-form/style.scss'; -@import 'enriched-label/style.scss'; @import 'tour-kit/style.scss'; @import 'collapsible-content/style.scss'; diff --git a/packages/js/components/src/tooltip/index.ts b/packages/js/components/src/tooltip/index.ts new file mode 100644 index 00000000000..ed8326d5e7c --- /dev/null +++ b/packages/js/components/src/tooltip/index.ts @@ -0,0 +1 @@ +export * from './tooltip'; diff --git a/packages/js/components/src/tooltip/stories/index.tsx b/packages/js/components/src/tooltip/stories/index.tsx new file mode 100644 index 00000000000..fe5e3015565 --- /dev/null +++ b/packages/js/components/src/tooltip/stories/index.tsx @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; +import { Icon, warning } from '@wordpress/icons'; +import React from 'react'; + +/** + * Internal dependencies + */ +import { Tooltip } from '../'; + +export const Basic = () => { + return ( + + This is a tooltip! + + } + /> + ); +}; + +export const CustomIcon = () => { + return ( + + + + ); +}; + +export default { + title: 'WooCommerce Admin/experimental/Tooltip', + component: Tooltip, +}; diff --git a/packages/js/components/src/tooltip/style.scss b/packages/js/components/src/tooltip/style.scss new file mode 100644 index 00000000000..1d94d5fb767 --- /dev/null +++ b/packages/js/components/src/tooltip/style.scss @@ -0,0 +1,14 @@ +.woocommerce-tooltip { + display: inline-flex; + + .woocommerce-tooltip__button { + height: auto; + } + + &__text .components-popover__content { + font-size: $default-font-size; + padding: $gap; + width: max-content; + } +} + diff --git a/packages/js/components/src/tooltip/tooltip.tsx b/packages/js/components/src/tooltip/tooltip.tsx new file mode 100644 index 00000000000..b6b6a38d336 --- /dev/null +++ b/packages/js/components/src/tooltip/tooltip.tsx @@ -0,0 +1,83 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Button, Popover } from '@wordpress/components'; +import { createElement, Fragment, useState } from '@wordpress/element'; +import { FocusEvent, KeyboardEvent } from 'react'; +import { Icon, help } from '@wordpress/icons'; + +type Position = + | 'top left' + | 'top right' + | 'top center' + | 'middle left' + | 'middle right' + | 'middle center' + | 'bottom left' + | 'bottom right' + | 'bottom center'; + +type TooltipProps = { + children?: JSX.Element | string; + position?: Position; + text: JSX.Element | string; +}; + +export const Tooltip: React.FC< TooltipProps > = ( { + children = , + position = 'top center', + text, +} ) => { + const [ isPopoverVisible, setIsPopoverVisible ] = useState( false ); + + return ( + <> +
+ + + { isPopoverVisible && ( + { + if ( + event.relatedTarget?.classList.contains( + 'woocommerce-tooltip__button' + ) + ) { + return; + } + setIsPopoverVisible( false ); + } } + onKeyDown={ ( + event: KeyboardEvent< HTMLDivElement > + ) => { + if ( event.key !== 'Escape' ) { + return; + } + setIsPopoverVisible( false ); + } } + > + { text } + + ) } +
+ + ); +}; diff --git a/packages/js/components/src/util/index.ts b/packages/js/components/src/util/index.ts deleted file mode 100644 index ed673a8ca5b..00000000000 --- a/packages/js/components/src/util/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './conditional-wrapper'; diff --git a/packages/js/data/changelog/add-34331_add_attributes_modal b/packages/js/data/changelog/add-34331_add_attributes_modal new file mode 100644 index 00000000000..49cb315e249 --- /dev/null +++ b/packages/js/data/changelog/add-34331_add_attributes_modal @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update product attribute type name and export the product attribute types. diff --git a/packages/js/data/changelog/add-product-management-description b/packages/js/data/changelog/add-product-management-description new file mode 100644 index 00000000000..e8d1c078d67 --- /dev/null +++ b/packages/js/data/changelog/add-product-management-description @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Retrieve product information using edit context diff --git a/packages/js/data/changelog/dev-clean-up-unused-task-properties-methods b/packages/js/data/changelog/dev-clean-up-unused-task-properties-methods new file mode 100644 index 00000000000..49ebaf5e3ee --- /dev/null +++ b/packages/js/data/changelog/dev-clean-up-unused-task-properties-methods @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Update task list types and getVisibleTasks logic diff --git a/packages/js/components/changelog/update-jest-merge b/packages/js/data/changelog/dev-migrate-reports-to-ts similarity index 55% rename from packages/js/components/changelog/update-jest-merge rename to packages/js/data/changelog/dev-migrate-reports-to-ts index 3ffb0e90b2a..c3d939a24f0 100644 --- a/packages/js/components/changelog/update-jest-merge +++ b/packages/js/data/changelog/dev-migrate-reports-to-ts @@ -1,4 +1,4 @@ Significance: patch Type: dev -Merging trunk with local +Migrate reports to TS diff --git a/packages/js/data/changelog/update-34996 b/packages/js/data/changelog/update-34996 new file mode 100644 index 00000000000..f7695b9e16c --- /dev/null +++ b/packages/js/data/changelog/update-34996 @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Add missing shipping class property diff --git a/packages/js/data/changelog/update-data-products-exclude-non-gmt-scheduled-sale b/packages/js/data/changelog/update-data-products-exclude-non-gmt-scheduled-sale new file mode 100644 index 00000000000..1db6a633809 --- /dev/null +++ b/packages/js/data/changelog/update-data-products-exclude-non-gmt-scheduled-sale @@ -0,0 +1,4 @@ +Significance: major +Type: update + +Remove `Product` `date_on_sale_from` and `date_on_sale_to` properties. Use `date_on_sale_from_gmt` and `date_on_sale_to_gmt` instead. diff --git a/packages/js/data/src/crud/resolvers.ts b/packages/js/data/src/crud/resolvers.ts index 6ab06885867..ecfeef5b258 100644 --- a/packages/js/data/src/crud/resolvers.ts +++ b/packages/js/data/src/crud/resolvers.ts @@ -67,7 +67,7 @@ export const createResolvers = ( { } try { - const path = getRestPath( namespace, {}, urlParameters ); + const path = getRestPath( namespace, query || {}, urlParameters ); const { items, totalCount }: { items: Item[]; totalCount: number } = yield request< ItemQuery, Item >( path, resourceQuery ); diff --git a/packages/js/data/src/index.ts b/packages/js/data/src/index.ts index 8cde4e16128..be23c55eff3 100644 --- a/packages/js/data/src/index.ts +++ b/packages/js/data/src/index.ts @@ -76,7 +76,15 @@ export * from './countries/types'; export * from './onboarding/types'; export * from './plugins/types'; export * from './products/types'; +export { + QueryProductAttribute, + ProductAttributeSelectors, +} from './product-attributes/types'; export * from './product-shipping-classes/types'; +export { + ProductAttributeTerm, + ProductAttributeTermsSelectors, +} from './product-attribute-terms/types'; export * from './orders/types'; export { ProductCategory, diff --git a/packages/js/data/src/onboarding/types.ts b/packages/js/data/src/onboarding/types.ts index 15c9bd5fc27..24060daa19d 100644 --- a/packages/js/data/src/onboarding/types.ts +++ b/packages/js/data/src/onboarding/types.ts @@ -53,15 +53,6 @@ export type DeprecatedTaskType = { type?: string; }; -export type TaskListSection = { - id: string; - title: string; - description: string; - image: string; - tasks: string[]; - isComplete: boolean; -}; - export type TaskListType = { id: string; title: string; @@ -73,7 +64,6 @@ export type TaskListType = { displayProgressHeader: boolean; keepCompletedTaskList: 'yes' | 'no'; showCESFeedback?: boolean; - sections?: TaskListSection[]; isToggleable?: boolean; isCollapsible?: boolean; isExpandable?: boolean; diff --git a/packages/js/data/src/onboarding/utils.ts b/packages/js/data/src/onboarding/utils.ts index 9623eeff560..3fbca5364d2 100644 --- a/packages/js/data/src/onboarding/utils.ts +++ b/packages/js/data/src/onboarding/utils.ts @@ -7,8 +7,4 @@ import { TaskType } from './types'; * Filters tasks to only visible tasks, taking in account snoozed tasks. */ export const getVisibleTasks = ( tasks: TaskType[] ) => - tasks.filter( - ( task ) => - ! task.isDismissed && - ( ! task.isSnoozed || task.snoozedUntil < Date.now() ) - ); + tasks.filter( ( task ) => ! task.isDismissed ); diff --git a/packages/js/data/src/product-attribute-terms/types.ts b/packages/js/data/src/product-attribute-terms/types.ts index eb451eafc8a..6f539fb3df4 100644 --- a/packages/js/data/src/product-attribute-terms/types.ts +++ b/packages/js/data/src/product-attribute-terms/types.ts @@ -8,7 +8,7 @@ import { DispatchFromMap } from '@automattic/data-stores'; */ import { CrudActions, CrudSelectors } from '../crud/types'; -type ProductAttributeTerm = { +export type ProductAttributeTerm = { id: number; slug: string; name: string; diff --git a/packages/js/data/src/product-attributes/types.ts b/packages/js/data/src/product-attributes/types.ts index 11f38ddad18..7222846dead 100644 --- a/packages/js/data/src/product-attributes/types.ts +++ b/packages/js/data/src/product-attributes/types.ts @@ -8,7 +8,7 @@ import { DispatchFromMap } from '@automattic/data-stores'; */ import { CrudActions, CrudSelectors } from '../crud/types'; -type ProductAttribute = { +export type QueryProductAttribute = { id: number; slug: string; name: string; @@ -24,19 +24,19 @@ type Query = { type ReadOnlyProperties = 'id'; type MutableProperties = Partial< - Omit< ProductAttribute, ReadOnlyProperties > + Omit< QueryProductAttribute, ReadOnlyProperties > >; type ProductAttributeActions = CrudActions< 'ProductAttribute', - ProductAttribute, + QueryProductAttribute, MutableProperties >; export type ProductAttributeSelectors = CrudSelectors< 'ProductAttribute', 'ProductAttributes', - ProductAttribute, + QueryProductAttribute, Query, MutableProperties >; diff --git a/packages/js/data/src/products/resolvers.ts b/packages/js/data/src/products/resolvers.ts index d8996091664..14d07648549 100644 --- a/packages/js/data/src/products/resolvers.ts +++ b/packages/js/data/src/products/resolvers.ts @@ -1,6 +1,7 @@ /** * External dependencies */ +import { addQueryArgs } from '@wordpress/url'; import { apiFetch } from '@wordpress/data-controls'; /** @@ -48,7 +49,9 @@ export function* getProducts( query: Partial< ProductQuery > ) { export function* getProduct( productId: number ) { try { const product: Product = yield apiFetch( { - path: `${ WC_PRODUCT_NAMESPACE }/${ productId }`, + path: addQueryArgs( `${ WC_PRODUCT_NAMESPACE }/${ productId }`, { + context: 'edit', + } ), method: 'GET', } ); diff --git a/packages/js/data/src/products/types.ts b/packages/js/data/src/products/types.ts index d42c781ed12..961261a771f 100644 --- a/packages/js/data/src/products/types.ts +++ b/packages/js/data/src/products/types.ts @@ -59,9 +59,7 @@ export type Product< Status = ProductStatus, Type = ProductType > = Omit< description: string; short_description: string; sku: string; - date_on_sale_from: string | null; date_on_sale_from_gmt: string | null; - date_on_sale_to: string | null; date_on_sale_to_gmt: string | null; virtual: boolean; downloadable: boolean; @@ -88,6 +86,7 @@ export type Product< Status = ProductStatus, Type = ProductType > = Omit< backordered: boolean; shipping_required: boolean; shipping_taxable: boolean; + shipping_class: string; shipping_class_id: number; average_rating: string; rating_count: number; diff --git a/packages/js/data/src/reports/action-types.js b/packages/js/data/src/reports/action-types.ts similarity index 93% rename from packages/js/data/src/reports/action-types.js rename to packages/js/data/src/reports/action-types.ts index 5ca88da5207..082280de8a0 100644 --- a/packages/js/data/src/reports/action-types.js +++ b/packages/js/data/src/reports/action-types.ts @@ -3,6 +3,6 @@ const TYPES = { SET_STAT_ERROR: 'SET_STAT_ERROR', SET_REPORT_ITEMS: 'SET_REPORT_ITEMS', SET_REPORT_STATS: 'SET_REPORT_STATS', -}; +} as const; export default TYPES; diff --git a/packages/js/data/src/reports/actions.js b/packages/js/data/src/reports/actions.js deleted file mode 100644 index b6449e659f0..00000000000 --- a/packages/js/data/src/reports/actions.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Internal dependencies - */ -import { getResourceName } from '../utils'; -import TYPES from './action-types'; - -export function setReportItemsError( endpoint, query, error ) { - const resourceName = getResourceName( endpoint, query ); - - return { - type: TYPES.SET_ITEM_ERROR, - resourceName, - error, - }; -} - -export function setReportItems( endpoint, query, items ) { - const resourceName = getResourceName( endpoint, query ); - - return { - type: TYPES.SET_REPORT_ITEMS, - resourceName, - items, - }; -} - -export function setReportStats( endpoint, query, stats ) { - const resourceName = getResourceName( endpoint, query ); - - return { - type: TYPES.SET_REPORT_STATS, - resourceName, - stats, - }; -} - -export function setReportStatsError( endpoint, query, error ) { - const resourceName = getResourceName( endpoint, query ); - - return { - type: TYPES.SET_STAT_ERROR, - resourceName, - error, - }; -} diff --git a/packages/js/data/src/reports/actions.ts b/packages/js/data/src/reports/actions.ts new file mode 100644 index 00000000000..ffebf70e60c --- /dev/null +++ b/packages/js/data/src/reports/actions.ts @@ -0,0 +1,76 @@ +/** + * Internal dependencies + */ +import { getResourceName } from '../utils'; +import TYPES from './action-types'; +import { + ReportItemsEndpoint, + ReportStatEndpoint, + ReportQueryParams, + ReportStatQueryParams, + ReportItemObject, + ReportStatObject, +} from './types'; + +export function setReportItemsError( + endpoint: ReportItemsEndpoint, + query: ReportQueryParams, + error: unknown +) { + const resourceName = getResourceName( endpoint, query ); + + return { + type: TYPES.SET_ITEM_ERROR, + resourceName, + error, + }; +} + +export function setReportItems( + endpoint: ReportItemsEndpoint, + query: ReportQueryParams, + items: ReportItemObject +) { + const resourceName = getResourceName( endpoint, query ); + + return { + type: TYPES.SET_REPORT_ITEMS, + resourceName, + items, + }; +} + +export function setReportStats( + endpoint: ReportStatEndpoint, + query: ReportStatQueryParams, + stats: ReportStatObject +) { + const resourceName = getResourceName( endpoint, query ); + + return { + type: TYPES.SET_REPORT_STATS, + resourceName, + stats, + }; +} + +export function setReportStatsError( + endpoint: ReportStatEndpoint, + query: ReportStatQueryParams, + error: unknown +) { + const resourceName = getResourceName( endpoint, query ); + + return { + type: TYPES.SET_STAT_ERROR, + resourceName, + error, + }; +} + +export type Action = ReturnType< + | typeof setReportItems + | typeof setReportItemsError + | typeof setReportStats + | typeof setReportStatsError +>; diff --git a/packages/js/data/src/reports/constants.ts b/packages/js/data/src/reports/constants.ts index fdf8db22cf4..b0249f3966c 100644 --- a/packages/js/data/src/reports/constants.ts +++ b/packages/js/data/src/reports/constants.ts @@ -1,4 +1,4 @@ /** * Internal dependencies */ -export const STORE_NAME = 'wc/admin/reports'; +export const STORE_NAME = 'wc/admin/reports' as const; diff --git a/packages/js/data/src/reports/index.js b/packages/js/data/src/reports/index.js deleted file mode 100644 index 0172e6a3cdc..00000000000 --- a/packages/js/data/src/reports/index.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * External dependencies - */ - -import { registerStore } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { STORE_NAME } from './constants'; -import * as selectors from './selectors'; -import * as actions from './actions'; -import * as resolvers from './resolvers'; -import controls from '../controls'; -import reducer from './reducer'; - -registerStore( STORE_NAME, { - reducer, - actions, - controls, - selectors, - resolvers, -} ); - -export const REPORTS_STORE_NAME = STORE_NAME; diff --git a/packages/js/data/src/reports/index.ts b/packages/js/data/src/reports/index.ts new file mode 100644 index 00000000000..7d738c15c78 --- /dev/null +++ b/packages/js/data/src/reports/index.ts @@ -0,0 +1,60 @@ +/** + * External dependencies + */ + +import { registerStore } from '@wordpress/data'; +import { Reducer, AnyAction } from 'redux'; +import { SelectFromMap, DispatchFromMap } from '@automattic/data-stores'; + +/** + * Internal dependencies + */ +import { STORE_NAME } from './constants'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import * as resolvers from './resolvers'; +import controls from '../controls'; +import reducer, { State } from './reducer'; +import { WPDataActions, WPDataSelectors } from '../types'; +import { + ReportItemObjectInfer, + ReportItemsEndpoint, + ReportQueryParams, + ReportStatEndpoint, + ReportStatObjectInfer, + ReportStatQueryParams, +} from './types'; +export * from './types'; +export type { State }; + +registerStore( STORE_NAME, { + reducer: reducer as Reducer< State, AnyAction >, + actions, + controls, + selectors, + resolvers, +} ); + +export const REPORTS_STORE_NAME = STORE_NAME; + +export type ReportsSelect = WPDataSelectors & + Omit< + SelectFromMap< typeof selectors >, + 'getReportItems' | 'getReportStats' + > & { + getReportItems: < T >( + endpoint: ReportItemsEndpoint, + query: ReportQueryParams + ) => ReportItemObjectInfer< T >; + getReportStats: < T >( + endpoint: ReportStatEndpoint, + query: ReportStatQueryParams + ) => ReportStatObjectInfer< T >; + }; + +declare module '@wordpress/data' { + function dispatch( + key: typeof STORE_NAME + ): DispatchFromMap< typeof actions & WPDataActions >; + function select( key: typeof STORE_NAME ): ReportsSelect; +} diff --git a/packages/js/data/src/reports/reducer.js b/packages/js/data/src/reports/reducer.js deleted file mode 100644 index 562aea0dc23..00000000000 --- a/packages/js/data/src/reports/reducer.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Internal dependencies - */ -import TYPES from './action-types'; - -const reports = ( - state = { - itemErrors: {}, - items: {}, - statErrors: {}, - stats: {}, - }, - { type, items, stats, error, resourceName } -) => { - switch ( type ) { - case TYPES.SET_REPORT_ITEMS: - return { - ...state, - items: { ...state.items, [ resourceName ]: items }, - }; - case TYPES.SET_REPORT_STATS: - return { - ...state, - stats: { ...state.stats, [ resourceName ]: stats }, - }; - case TYPES.SET_ITEM_ERROR: - return { - ...state, - itemErrors: { - ...state.itemErrors, - [ resourceName ]: error, - }, - }; - case TYPES.SET_STAT_ERROR: - return { - ...state, - statErrors: { - ...state.statErrors, - [ resourceName ]: error, - }, - }; - default: - return state; - } -}; - -export default reports; diff --git a/packages/js/data/src/reports/reducer.ts b/packages/js/data/src/reports/reducer.ts new file mode 100644 index 00000000000..1614c794d50 --- /dev/null +++ b/packages/js/data/src/reports/reducer.ts @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import type { Reducer } from 'redux'; + +/** + * Internal dependencies + */ +import TYPES from './action-types'; +import { Action } from './actions'; +import { ReportState } from './types'; + +const initialState: ReportState = { + itemErrors: {}, + items: {}, + statErrors: {}, + stats: {}, +}; +const reducer: Reducer< ReportState, Action > = ( + state = initialState, + action +) => { + switch ( action.type ) { + case TYPES.SET_REPORT_ITEMS: + return { + ...state, + items: { + ...state.items, + [ action.resourceName ]: action.items, + }, + }; + case TYPES.SET_REPORT_STATS: + return { + ...state, + stats: { + ...state.stats, + [ action.resourceName ]: action.stats, + }, + }; + case TYPES.SET_ITEM_ERROR: + return { + ...state, + itemErrors: { + ...state.itemErrors, + [ action.resourceName ]: action.error, + }, + }; + case TYPES.SET_STAT_ERROR: + return { + ...state, + statErrors: { + ...state.statErrors, + [ action.resourceName ]: action.error, + }, + }; + default: + return state; + } +}; + +export type State = ReturnType< typeof reducer >; +export default reducer; diff --git a/packages/js/data/src/reports/resolvers.js b/packages/js/data/src/reports/resolvers.js deleted file mode 100644 index 80224a3eaed..00000000000 --- a/packages/js/data/src/reports/resolvers.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * External dependencies - */ -import { addQueryArgs } from '@wordpress/url'; - -/** - * Internal dependencies - */ -import { fetchWithHeaders } from '../controls'; -import { NAMESPACE } from '../constants'; -import { - setReportItemsError, - setReportStatsError, - setReportItems, - setReportStats, -} from './actions'; - -export function* getReportItems( endpoint, query ) { - const fetchArgs = { - parse: false, - path: addQueryArgs( `${ NAMESPACE }/reports/${ endpoint }`, query ), - }; - - try { - const response = yield fetchWithHeaders( fetchArgs ); - const data = response.data; - const totalResults = parseInt( - response.headers.get( 'x-wp-total' ), - 10 - ); - const totalPages = parseInt( - response.headers.get( 'x-wp-totalpages' ), - 10 - ); - - yield setReportItems( endpoint, query, { - data, - totalResults, - totalPages, - } ); - } catch ( error ) { - yield setReportItemsError( endpoint, query, error ); - } -} - -export function* getReportStats( endpoint, query ) { - const fetchArgs = { - parse: false, - path: addQueryArgs( - `${ NAMESPACE }/reports/${ endpoint }/stats`, - query - ), - }; - - try { - const response = yield fetchWithHeaders( fetchArgs ); - const data = response.data; - const totalResults = parseInt( - response.headers.get( 'x-wp-total' ), - 10 - ); - const totalPages = parseInt( - response.headers.get( 'x-wp-totalpages' ), - 10 - ); - - yield setReportStats( endpoint, query, { - data, - totalResults, - totalPages, - } ); - } catch ( error ) { - yield setReportStatsError( endpoint, query, error ); - } -} diff --git a/packages/js/data/src/reports/resolvers.ts b/packages/js/data/src/reports/resolvers.ts new file mode 100644 index 00000000000..249fe9ddc83 --- /dev/null +++ b/packages/js/data/src/reports/resolvers.ts @@ -0,0 +1,110 @@ +/** + * External dependencies + */ +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import { fetchWithHeaders } from '../controls'; +import { NAMESPACE } from '../constants'; +import { + setReportItemsError, + setReportStatsError, + setReportItems, + setReportStats, +} from './actions'; +import { + ReportItemsEndpoint, + ReportStatEndpoint, + ReportQueryParams, + ReportStatQueryParams, + ReportItemObject, + ReportStatObject, +} from './types'; + +const getIntHeaderValues = ( + endpoint: string, + response: { + headers: Map< string, string >; + data: unknown; + }, + keys: string[] +) => { + return keys.map( ( key ) => { + const value = response.headers.get( key ); + if ( value === undefined ) { + throw new Error( + `Malformed response from server. '${ key }' header is missing when retriving ./report/${ endpoint }.` + ); + } + return parseInt( value, 10 ); + } ); +}; + +export function* getReportItems( + endpoint: ReportItemsEndpoint, + query: ReportQueryParams +) { + const fetchArgs = { + parse: false, + path: addQueryArgs( `${ NAMESPACE }/reports/${ endpoint }`, query ), + }; + + try { + const response: { + headers: Map< string, string >; + data: ReportItemObject[ 'data' ]; + } = yield fetchWithHeaders( fetchArgs ); + const data = response.data; + + const [ totalResults, totalPages ] = getIntHeaderValues( + endpoint, + response, + [ 'x-wp-total', 'x-wp-totalpages' ] + ); + + yield setReportItems( endpoint, query, { + data, + totalResults, + totalPages, + } ); + } catch ( error ) { + yield setReportItemsError( endpoint, query, error ); + } +} + +export function* getReportStats( + endpoint: ReportStatEndpoint, + query: ReportStatQueryParams +) { + const fetchArgs = { + parse: false, + path: addQueryArgs( + `${ NAMESPACE }/reports/${ endpoint }/stats`, + query + ), + }; + + try { + const response: { + headers: Map< string, string >; + data: ReportStatObject[ 'data' ]; + } = yield fetchWithHeaders( fetchArgs ); + const data = response.data; + + const [ totalResults, totalPages ] = getIntHeaderValues( + endpoint, + response, + [ 'x-wp-total', 'x-wp-totalpages' ] + ); + + yield setReportStats( endpoint, query, { + data, + totalResults, + totalPages, + } ); + } catch ( error ) { + yield setReportStatsError( endpoint, query, error ); + } +} diff --git a/packages/js/data/src/reports/selectors.js b/packages/js/data/src/reports/selectors.js deleted file mode 100644 index 6ac94167cc1..00000000000 --- a/packages/js/data/src/reports/selectors.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Internal dependencies - */ -import { getResourceName } from '../utils'; - -const EMPTY_OBJECT = {}; - -export const getReportItemsError = ( state, endpoint, query ) => { - const resourceName = getResourceName( endpoint, query ); - return state.itemErrors[ resourceName ] || false; -}; - -export const getReportItems = ( state, endpoint, query ) => { - const resourceName = getResourceName( endpoint, query ); - return state.items[ resourceName ] || EMPTY_OBJECT; -}; - -export const getReportStats = ( state, endpoint, query ) => { - const resourceName = getResourceName( endpoint, query ); - return state.stats[ resourceName ] || EMPTY_OBJECT; -}; - -export const getReportStatsError = ( state, endpoint, query ) => { - const resourceName = getResourceName( endpoint, query ); - return state.statErrors[ resourceName ] || false; -}; diff --git a/packages/js/data/src/reports/selectors.ts b/packages/js/data/src/reports/selectors.ts new file mode 100644 index 00000000000..94f44df437d --- /dev/null +++ b/packages/js/data/src/reports/selectors.ts @@ -0,0 +1,57 @@ +/** + * Internal dependencies + */ +import { getResourceName } from '../utils'; +import { + ReportState, + ReportItemsEndpoint, + ReportStatEndpoint, + ReportQueryParams, + ReportStatQueryParams, + ReportItemObjectInfer, + ReportStatObjectInfer, +} from './types'; + +const EMPTY_OBJECT = {} as const; + +export const getReportItemsError = ( + state: ReportState, + endpoint: ReportItemsEndpoint, + query: ReportQueryParams +) => { + const resourceName = getResourceName( endpoint, query ); + return state.itemErrors[ resourceName ] || false; +}; + +export const getReportItems = < T >( + state: ReportState, + endpoint: ReportItemsEndpoint, + query: ReportQueryParams +): ReportItemObjectInfer< T > => { + const resourceName = getResourceName( endpoint, query ); + return ( + ( state.items[ resourceName ] as ReportItemObjectInfer< T > ) || + EMPTY_OBJECT + ); +}; + +export const getReportStats = < T >( + state: ReportState, + endpoint: ReportStatEndpoint, + query: ReportStatQueryParams +): ReportStatObjectInfer< T > => { + const resourceName = getResourceName( endpoint, query ); + return ( + ( state.stats[ resourceName ] as ReportStatObjectInfer< T > ) || + EMPTY_OBJECT + ); +}; + +export const getReportStatsError = ( + state: ReportState, + endpoint: ReportStatEndpoint, + query: ReportStatQueryParams +) => { + const resourceName = getResourceName( endpoint, query ); + return state.statErrors[ resourceName ] || false; +}; diff --git a/packages/js/data/src/reports/test/reducer.js b/packages/js/data/src/reports/test/reducer.ts similarity index 80% rename from packages/js/data/src/reports/test/reducer.js rename to packages/js/data/src/reports/test/reducer.ts index 9303f407840..83c78d602a9 100644 --- a/packages/js/data/src/reports/test/reducer.js +++ b/packages/js/data/src/reports/test/reducer.ts @@ -7,6 +7,7 @@ */ import reducer from '../reducer'; import TYPES from '../action-types'; +import { Action } from '../actions'; const defaultState = { itemErrors: {}, @@ -17,7 +18,7 @@ const defaultState = { describe( 'reports reducer', () => { it( 'should return a default state', () => { - const state = reducer( undefined, {} ); + const state = reducer( undefined, {} as Action ); expect( state ).toEqual( defaultState ); expect( state ).not.toBe( defaultState ); } ); @@ -26,6 +27,7 @@ describe( 'reports reducer', () => { const state = reducer( defaultState, { type: TYPES.SET_REPORT_ITEMS, resourceName: 'test-resource-items', + // @ts-expect-error This is a test. items: [ 1, 2 ], } ); @@ -38,6 +40,7 @@ describe( 'reports reducer', () => { const state = reducer( defaultState, { type: TYPES.SET_REPORT_STATS, resourceName: 'test-resource-stats', + // @ts-expect-error This is a test. stats: [ 3, 4 ], } ); @@ -53,9 +56,10 @@ describe( 'reports reducer', () => { error: { code: 'error' }, } ); - expect( state.itemErrors[ 'test-resource-items' ].code ).toBe( - 'error' - ); + expect( + ( state.itemErrors[ 'test-resource-items' ] as { code: string } ) + .code + ).toBe( 'error' ); } ); it( 'should handle SET_STAT_ERROR', () => { @@ -65,8 +69,9 @@ describe( 'reports reducer', () => { error: { code: 'error' }, } ); - expect( state.statErrors[ 'test-resource-stats' ].code ).toBe( - 'error' - ); + expect( + ( state.statErrors[ 'test-resource-stats' ] as { code: string } ) + .code + ).toBe( 'error' ); } ); } ); diff --git a/packages/js/data/src/reports/types.ts b/packages/js/data/src/reports/types.ts new file mode 100644 index 00000000000..e1859d58c79 --- /dev/null +++ b/packages/js/data/src/reports/types.ts @@ -0,0 +1,553 @@ +/** + * Internal dependencies + */ +import { getReportTableQuery, getRequestQuery } from './utils'; + +export type ReportItemsEndpoint = + | 'customers' + | 'products' + | 'varitations' + | 'orders' + | 'categories' + | 'taxes' + | 'coupons' + | 'stock' + | 'downloads' + | 'performance_indicator'; + +export type ReportStatEndpoint = + | 'products' + | 'variations' + | 'revenue' + | 'orders' + | 'taxes' + | 'coupons' + | 'customers'; + +export type ReportQueryParams = ReturnType< typeof getReportTableQuery >; +export type ReportStatQueryParams = ReturnType< typeof getRequestQuery >; + +export type CustomerReport = { + /** Customer ID. */ + id: number; + /** User ID. */ + user_id: number; + /** Name. */ + name: string; + /** Username. */ + username: string; + /** Country / Region. */ + country: string; + /** City. */ + city: string; + /** Region. */ + state: string; + /** Postal code. */ + postcode: string; + /** Date registered. */ + date_registered: string | null; + /** Date registered GMT. */ + date_registered_gmt: string | null; + /** Date last active. */ + date_last_active: string | null; + /** Date last active GMT. */ + date_last_active_gmt: string | null; + /** Order count. */ + orders_count: number; + /** Total spend. */ + total_spend: number; + /** Avg order value. */ + avg_order_value: number; +}; + +export type ProductReport = { + /** Product ID. */ + product_id: number; + /** Number of items sold. */ + items_sold: number; + /** Total Net sales of all items sold. */ + net_revenue: number; + /** Number of orders product appeared in. */ + orders_count: number; + extended_info: { + /** Product name. */ + name: string; + /** Product price. */ + price: number; + /** Product image. */ + image: string; + /** Product link. */ + permalink: string; + /** Product category IDs. */ + category_ids: Array< number >; + /** Product inventory status. */ + stock_status: string; + /** Product inventory quantity. */ + stock_quantity: number; + /** Product inventory threshold for low stock. */ + low_stock_amount: number; + /** Product variations IDs. */ + variations: Array< number >; + /** Product SKU. */ + sku: string; + }; +}; + +export type VariationReport = { + /** Product ID. */ + product_id: number; + /** Product ID. */ + variation_id: number; + /** Number of items sold. */ + items_sold: number; + /** Total Net sales of all items sold. */ + net_revenue: number; + /** Number of orders product appeared in. */ + orders_count: number; + extended_info: { + /** Product name. */ + name: string; + /** Product price. */ + price: number; + /** Product image. */ + image: string; + /** Product link. */ + permalink: string; + /** Product attributes. */ + attributes: Array< { + id: number; + name: string; + position: number; + visible: boolean; + variation: boolean; + options: string[]; + } >; + /** Product inventory status. */ + stock_status: string; + /** Product inventory quantity. */ + stock_quantity: number; + /** Product inventory threshold for low stock. */ + low_stock_amount: number; + }; +}; + +export type OrderReport = { + /** Order ID. */ + order_id: number; + /** Order Number. */ + order_number: string; + /** Date the order was created, in the site's timezone. */ + date_created: string | null; + /** Date the order was created, as GMT. */ + date_created_gmt: string | null; + /** Order status. */ + status: string; + /** Customer ID. */ + customer_id: number; + /** Number of items sold. */ + num_items_sold: number; + /** Net total revenue. */ + net_total: number; + /** Net total revenue (formatted). */ + total_formatted: string; + /** Returning or new customer. */ + customer_type: string; + extended_info: { + /** List of order product IDs, names, quantities. */ + products: Array< { + id: string; + name: string; + quantity: string; + } >; + /** List of order coupons. */ + coupons: Array< { + id: string; + code: string; + } >; + /** Order customer information. */ + customer: { + customer_id: number; + user_id: string; + username: string; + first_name: string; + last_name: string; + email: string; + date_last_active: string; + date_registered: string; + country: string; + postcode: string; + city: string; + state: string; + }; + }; +}; + +export type CategoriesReport = { + /** Category ID. */ + category_id: number; + /** Amount of items sold. */ + items_sold: number; + /** Total sales. */ + net_revenue: number; + /** Number of orders. */ + orders_count: number; + /** Amount of products. */ + products_count: number; + extended_info: { + /** Category name. */ + name: string; + }; +}; + +export type TaxesReport = { + /** Tax rate ID. */ + tax_rate_id: number; + /** Tax rate name. */ + name: string; + /** Tax rate. */ + tax_rate: number; + /** Country / Region. */ + country: string; + /** State. */ + state: string; + /** Priority. */ + priority: number; + /** Total tax. */ + total_tax: number; + /** Order tax. */ + order_tax: number; + /** Shipping tax. */ + shipping_tax: number; + /** Number of orders. */ + orders_count: number; +}; + +export type CouponReport = { + /** Coupon ID. */ + coupon_id: number; + /** Net discount amount. */ + amount: number; + /** Number of orders. */ + orders_count: number; + /** undefined */ + extended_info: { + /** Coupon code. */ + code: string; + /** Coupon creation date. */ + date_created: string | null; + /** Coupon creation date in GMT. */ + date_created_gmt: string | null; + /** Coupon expiration date. */ + date_expires: string | null; + /** Coupon expiration date in GMT. */ + date_expires_gmt: string | null; + /** Coupon discount type. */ + discount_type: 'percent' | 'fixed_cart' | 'fixed_product'; + }; +}; + +export type StockReport = { + /** Unique identifier for the resource. */ + id: number; + /** Product parent ID. */ + parent_id: number; + /** Product name. */ + name: string; + /** Unique identifier. */ + sku: string; + /** Stock status. */ + stock_status: 'instock' | 'outofstock' | 'onbackorder'; + /** Stock quantity. */ + stock_quantity: number; + /** Manage stock. */ + manage_stock: boolean; +}; + +export type DownloadReport = { + /** ID. */ + id: number; + /** Product ID. */ + product_id: number; + /** The date of the download, in the site's timezone. */ + date: string | null; + /** The date of the download, as GMT. */ + date_gmt: string | null; + /** Download ID. */ + download_id: string; + /** File name. */ + file_name: string; + /** File URL. */ + file_path: string; + /** Order ID. */ + order_id: number; + /** Order Number. */ + order_number: string; + /** User ID for the downloader. */ + user_id: number; + /** User name of the downloader. */ + username: string; + /** IP address for the downloader. */ + ip_address: string; +}; + +export type PerformanceIndicatorReport = { + /** Unique identifier for the resource. */ + stat: + | 'revenue/total_sales' + | 'revenue/net_revenue' + | 'revenue/shipping' + | 'revenue/refunds' + | 'revenue/gross_sales' + | 'orders/orders_count' + | 'orders/avg_order_value' + | 'products/items_sold' + | 'variations/items_sold' + | 'coupons/amount' + | 'coupons/orders_count' + | 'taxes/total_tax' + | 'taxes/order_tax' + | 'taxes/shipping_tax' + | 'downloads/download_count'; + /** The specific chart this stat referrers to. */ + chart: string; + /** Human readable label for the stat. */ + label: string; +}; + +export type ReportItemObject = { + data: + | CustomerReport + | ProductReport + | VariationReport + | CouponReport + | TaxesReport + | StockReport + | DownloadReport + | OrderReport + | CategoriesReport + | PerformanceIndicatorReport; + totalResults: number; + totalPages: number; +}; + +export type ReportItemObjectInfer< T > = { + data: T extends 'customers' + ? CustomerReport + : T extends 'products' + ? ProductReport + : T extends 'variations' + ? VariationReport + : T extends 'orders' + ? OrderReport + : T extends 'categories' + ? CategoriesReport + : T extends 'taxes' + ? TaxesReport + : T extends 'coupons' + ? CouponReport + : T extends 'stock' + ? StockReport + : T extends 'downloads' + ? DownloadReport + : T extends 'performance_indicators' + ? PerformanceIndicatorReport + : never; + totalResults: number; + totalPages: number; +}; + +type SubTotals = { + /** Number of product items sold. */ + items_sold: number; + /** Net sales. */ + net_revenue: number; + /** Number of orders. */ + orders_count: number; +}; + +type Interval = { + /** Type of interval. */ + interval: 'day' | 'week' | 'month' | 'year'; + /** The date the report start, in the site's timezone. */ + date_start: string | null; + /** The date the report start, as GMT. */ + date_start_gmt: string | null; + /** The date the report end, in the site's timezone. */ + date_end: string | null; + /** The date the report end, as GMT. */ + date_end_gmt: string | null; + /** Interval subtotals. */ + subtotals: SubTotals & { + /** Reports data grouped by segment condition. */ + segments: Array< Segment >; + }; +}; + +export type Segment = { + /** Segment identificator. */ + segment_id: number; + /** Human readable segment label, either product or variation name. */ + segment_label: 'day' | 'week' | 'month' | 'year'; + /** Interval subtotals. */ + subtotals: SubTotals; +}; + +export type ProductReportStat = { + totals: { + /** Number of product items sold. */ + items_sold: number; + /** Net sales. */ + net_revenue: number; + /** Number of orders. */ + orders_count: number; + /** Reports data grouped by segment condition. */ + segments: Array< Segment >; + }; + intervals: Array< Interval >; +}; + +export type VariationsReportStat = { + totals: { + /** Number of variation items sold. */ + items_sold: number; + /** Net sales. */ + net_revenue: number; + /** Number of orders. */ + orders_count: number; + /** Reports data grouped by segment condition. */ + segments: Array< Segment >; + }; + intervals: Array< Interval >; +}; + +export type RevenueReportStat = { + totals: { + /** Total sales. */ + total_sales: number; + /** Net sales. */ + net_revenue: number; + /** Amount discounted by coupons. */ + coupons: number; + /** Unique coupons count. */ + coupons_count: number; + /** Total of shipping. */ + shipping: number; + /** Total of taxes. */ + taxes: number; + /** Total of returns. */ + refunds: number; + /** Number of orders. */ + orders_count: number; + /** Items sold. */ + num_items_sold: number; + /** Products sold. */ + products: number; + /** Gross sales. */ + gross_sales: number; + /** Reports data grouped by segment condition. */ + segments: Array< Segment >; + }; + intervals: Array< Interval >; +}; + +export type OrderReportStat = { + totals: { + /** Number of downloads. */ + download_count: number; + }; + intervals: Array< Interval >; +}; + +export type TaxesReportStat = { + totals: { + /** Total tax. */ + total_tax: number; + /** Order tax. */ + order_tax: number; + /** Shipping tax. */ + shipping_tax: number; + /** Number of orders. */ + orders_count: number; + /** Amount of tax codes. */ + tax_codes: number; + /** Reports data grouped by segment condition. */ + segments: Array< Segment >; + }; + intervals: Array< Interval >; +}; + +export type CouponsReportStat = { + totals: { + /** Net discount amount. */ + amount: number; + /** Number of coupons. */ + coupons_count: number; + /** Number of discounted orders. */ + orders_count: number; + /** Reports data grouped by segment condition. */ + segments: Array< Segment >; + }; + intervals: Array< Interval >; +}; + +export type CustomersReportStat = { + totals: { + /** Number of customers. */ + customers_count: number; + /** Average number of orders. */ + avg_orders_count: number; + /** Average total spend per customer. */ + avg_total_spend: number; + /** Average AOV per customer. */ + avg_avg_order_value: number; + }; + intervals: Array< Interval >; +}; + +export type ReportStatObject = { + data: + | ProductReportStat + | VariationsReportStat + | RevenueReportStat + | OrderReportStat + | TaxesReportStat + | CouponsReportStat + | CustomersReportStat; + totalResults: number; + totalPages: number; +}; + +export type ReportStatObjectInfer< T > = { + data: T extends 'products' + ? ProductReportStat + : T extends 'variations' + ? VariationsReportStat + : T extends 'revenue' + ? RevenueReportStat + : T extends 'orders' + ? OrderReportStat + : T extends 'taxes' + ? TaxesReportStat + : T extends 'coupons' + ? CouponsReportStat + : T extends 'customers' + ? CustomersReportStat + : never; + totalResults: number; + totalPages: number; +}; + +export type ReportState = { + itemErrors: { + [ resourceName: string ]: unknown; + }; + items: { + [ resourceName: string ]: ReportItemObject; + }; + statErrors: { + [ resourceName: string ]: unknown; + }; + stats: { + [ resourceName: string ]: ReportStatObject; + }; +}; diff --git a/packages/js/data/src/reports/utils.js b/packages/js/data/src/reports/utils.ts similarity index 79% rename from packages/js/data/src/reports/utils.js rename to packages/js/data/src/reports/utils.ts index fbe58240fba..065c4847653 100644 --- a/packages/js/data/src/reports/utils.js +++ b/packages/js/data/src/reports/utils.ts @@ -14,6 +14,7 @@ import { getQueryFromActiveFilters, } from '@woocommerce/navigation'; import deprecated from '@wordpress/deprecated'; +import { select as WPSelect } from '@wordpress/data'; /** * Internal dependencies @@ -22,46 +23,43 @@ import * as reportsUtils from './utils'; import { MAX_PER_PAGE, QUERY_DEFAULTS } from '../constants'; import { STORE_NAME } from './constants'; import { getResourceName } from '../utils'; +import { + ReportItemsEndpoint, + ReportStatEndpoint, + ReportStatObject, +} from './types'; +import type { ReportsSelect } from './'; -/** - * Add filters and advanced filters values to a query object. - * - * @param {Object} options arguments - * @param {string} options.endpoint Report API Endpoint - * @param {Object} options.query Query parameters in the url - * @param {Array} options.limitBy Properties used to limit the results. It will be used in the API call to send the IDs. - * @param {Array} [options.filters] config filters - * @param {Object} [options.advancedFilters] config advanced filters - * @return {Object} A query object with the values from filters and advanced fitlters applied. - */ -export function getFilterQuery( options ) { - const { - endpoint, - query, - limitBy, - filters = [], - advancedFilters = {}, - } = options; - if ( query.search ) { - const limitProperties = limitBy || [ endpoint ]; - return limitProperties.reduce( ( result, limitProperty ) => { - result[ limitProperty ] = query[ limitProperty ]; - return result; - }, {} ); - } +type Filter = { + param: string; + filters: Array< Record< string, unknown > >; +}; - return filters - .map( ( filter ) => - getQueryFromConfig( filter, advancedFilters, query ) - ) - .reduce( - ( result, configQuery ) => Object.assign( result, configQuery ), - {} - ); -} +type AdvancedFilters = + | { + filters: { + [ key: string ]: { + input: { + component: string; + }; + }; + }; + } + | Record< string, never >; -// Some stats endpoints don't have interval data, so they can ignore after/before params and omit that part of the response. -const noIntervalEndpoints = [ 'stock', 'customers' ]; +type QueryOptions = { + endpoint: ReportStatEndpoint; + dataType: 'primary' | 'secondary'; + query: Record< string, string >; + limitBy: string[]; + filters: Array< Filter >; + advancedFilters: AdvancedFilters; + defaultDateRange: string; + tableQuery: Record< string, string >; + fields: string[]; + selector: ReportsSelect; + select: typeof WPSelect; +}; /** * Add timestamp to advanced filter parameters involving date. The api @@ -71,7 +69,10 @@ const noIntervalEndpoints = [ 'stock', 'customers' ]; * @param {Object} activeFilter - an active filter. * @return {Object} - an active filter with timestamp added to date values. */ -export function timeStampFilterDates( config, activeFilter ) { +export function timeStampFilterDates( + config: AdvancedFilters, + activeFilter: ActiveFilter +) { const advancedFilterConfig = config.filters[ activeFilter.key ]; if ( get( advancedFilterConfig, [ 'input', 'component' ] ) !== 'Date' ) { return activeFilter; @@ -99,7 +100,11 @@ export function timeStampFilterDates( config, activeFilter ) { } ); } -export function getQueryFromConfig( config, advancedFilters, query ) { +export function getQueryFromConfig( + config: Filter, + advancedFilters: QueryOptions[ 'advancedFilters' ], + query: QueryOptions[ 'query' ] +) { const queryValue = query[ config.param ]; if ( ! queryValue ) { @@ -155,6 +160,59 @@ export function getQueryFromConfig( config, advancedFilters, query ) { }; } +/** + * Add filters and advanced filters values to a query object. + * + * @param {Object} options arguments + * @param {string} options.endpoint Report API Endpoint + * @param {Object} options.query Query parameters in the url + * @param {Array} options.limitBy Properties used to limit the results. It will be used in the API call to send the IDs. + * @param {Array} [options.filters] config filters + * @param {Object} [options.advancedFilters] config advanced filters + * @return {Object} A query object with the values from filters and advanced fitlters applied. + */ +export function getFilterQuery( + options: Omit< QueryOptions, 'endpoint' > & { + endpoint: ReportItemsEndpoint | ReportStatEndpoint; + } +) { + const { + endpoint, + query, + limitBy, + filters = [], + advancedFilters = {}, + } = options; + if ( query.search ) { + const limitProperties = limitBy || [ endpoint ]; + return limitProperties.reduce< Record< string, string > >( + ( result, limitProperty ) => { + result[ limitProperty ] = query[ limitProperty ]; + return result; + }, + {} + ); + } + + return filters + .map( ( filter ) => + getQueryFromConfig( filter, advancedFilters, query ) + ) + .reduce( + ( result, configQuery ) => Object.assign( result, configQuery ), + {} + ); +} + +// Some stats endpoints don't have interval data, so they can ignore after/before params and omit that part of the response. +const noIntervalEndpoints = [ 'stock', 'customers' ] as const; + +type ActiveFilter = { + key: string; + rule: 'after' | 'before'; + value: string; +}; + /** * Returns true if a report object is empty. * @@ -162,7 +220,10 @@ export function getQueryFromConfig( config, advancedFilters, query ) { * @param {string} endpoint Endpoint slug * @return {boolean} True if report is data is empty. */ -export function isReportDataEmpty( report, endpoint ) { +export function isReportDataEmpty( + report: ReportStatObject, + endpoint: ReportStatEndpoint +) { if ( ! report ) { return true; } @@ -186,15 +247,17 @@ export function isReportDataEmpty( report, endpoint ) { /** * Constructs and returns a query associated with a Report data request. * - * @param {Object} options arguments - * @param {string} options.endpoint Report API Endpoint - * @param {string} options.dataType 'primary' or 'secondary'. - * @param {Object} options.query Query parameters in the url. - * @param {Array} options.limitBy Properties used to limit the results. It will be used in the API call to send the IDs. - * @param {string} options.defaultDateRange User specified default date range. + * @param {Object} options arguments + * @param {string} options.endpoint Report API Endpoint + * @param {string} options.dataType 'primary' or 'secondary'. + * @param {Object} options.query Query parameters in the url. + * @param {Array} [options.filters] config filters + * @param {Object} [options.advancedFilters] config advanced filters + * @param {Array} options.limitBy Properties used to limit the results. It will be used in the API call to send the IDs. + * @param {string} options.defaultDateRange User specified default date range. * @return {Object} data request query parameters. */ -function getRequestQuery( options ) { +export function getRequestQuery( options: QueryOptions ) { const { endpoint, dataType, query, fields, defaultDateRange } = options; const datesFromQuery = getCurrentDates( query, defaultDateRange ); const interval = getIntervalForQuery( query, defaultDateRange ); @@ -222,15 +285,19 @@ function getRequestQuery( options ) { /** * Returns summary number totals needed to render a report page. * - * @param {Object} options arguments - * @param {string} options.endpoint Report API Endpoint - * @param {Object} options.query Query parameters in the url - * @param {Object} options.select Instance of @wordpress/select - * @param {Array} options.limitBy Properties used to limit the results. It will be used in the API call to send the IDs. - * @param {string} options.defaultDateRange User specified default date range. + * @param {Object} options arguments + * @param {string} options.endpoint Report API Endpoint + * @param {Object} options.query Query parameters in the url + * @param {Object} options.select Instance of @wordpress/select + * @param {Array} [options.filters] config filters + * @param {Object} [options.advancedFilters] config advanced filters + * @param {Array} options.limitBy Properties used to limit the results. It will be used in the API call to send the IDs. + * @param {string} options.defaultDateRange User specified default date range. * @return {Object} Object containing summary number responses. */ -export function getSummaryNumbers( options ) { +export function getSummaryNumbers< T extends ReportStatEndpoint >( + options: QueryOptions +) { const { endpoint, select } = options; const { getReportStats, getReportStatsError, isResolving } = select( STORE_NAME ); @@ -248,7 +315,7 @@ export function getSummaryNumbers( options ) { // Disable eslint rule requiring `getReportStats` to be defined below because the next two statements // depend on `getReportStats` to have been called. // eslint-disable-next-line @wordpress/no-unused-vars-before-return - const primary = getReportStats( endpoint, primaryQuery ); + const primary = getReportStats< T >( endpoint, primaryQuery ); if ( isResolving( 'getReportStats', [ endpoint, primaryQuery ] ) ) { return { ...response, isRequesting: true }; @@ -267,7 +334,7 @@ export function getSummaryNumbers( options ) { // Disable eslint rule requiring `getReportStats` to be defined below because the next two statements // depend on `getReportStats` to have been called. // eslint-disable-next-line @wordpress/no-unused-vars-before-return - const secondary = getReportStats( endpoint, secondaryQuery ); + const secondary = getReportStats< T >( endpoint, secondaryQuery ); if ( isResolving( 'getReportStats', [ endpoint, secondaryQuery ] ) ) { return { ...response, isRequesting: true }; @@ -317,7 +384,7 @@ const reportChartDataResponses = { }, }; -const EMPTY_ARRAY = []; +const EMPTY_ARRAY = [] as const; /** * Cache helper for returning the full chart dataset after multiple @@ -325,7 +392,7 @@ const EMPTY_ARRAY = []; * all the requests have resolved successfully. */ const getReportChartDataResponse = memoize( - ( requestString, totals, intervals ) => ( { + ( _requestString, totals, intervals ) => ( { isEmpty: false, isError: false, isRequesting: false, @@ -348,7 +415,9 @@ const getReportChartDataResponse = memoize( * @param {string} options.defaultDateRange User specified default date range. * @return {Object} Object containing API request information (response, fetching, and error details) */ -export function getReportChartData( options ) { +export function getReportChartData< T extends ReportStatEndpoint >( + options: QueryOptions +) { const { endpoint } = options; let reportSelectors = options.selector; if ( options.select && ! options.selector ) { @@ -365,7 +434,7 @@ export function getReportChartData( options ) { // Disable eslint rule requiring `stats` to be defined below because the next two if statements // depend on `getReportStats` to have been called. // eslint-disable-next-line @wordpress/no-unused-vars-before-return - const stats = getReportStats( endpoint, requestQuery ); + const stats = getReportStats< T >( endpoint, requestQuery ); if ( isResolving( 'getReportStats', [ endpoint, requestQuery ] ) ) { return reportChartDataResponses.requesting; @@ -394,7 +463,7 @@ export function getReportChartData( options ) { for ( let i = 2; i <= totalPages; i++ ) { const nextQuery = { ...requestQuery, page: i }; - const _data = getReportStats( endpoint, nextQuery ); + const _data = getReportStats< T >( endpoint, nextQuery ); if ( isResolving( 'getReportStats', [ endpoint, nextQuery ] ) ) { continue; } @@ -444,7 +513,10 @@ export function getReportChartData( options ) { * @param {Function} formatAmount format currency function * @return {string|Function} returns a number format based on the type or an overriding formatting function */ -export function getTooltipValueFormat( type, formatAmount ) { +export function getTooltipValueFormat( + type: string, + formatAmount: ( amount: number ) => string +) { switch ( type ) { case 'currency': return formatAmount; @@ -463,12 +535,17 @@ export function getTooltipValueFormat( type, formatAmount ) { * Returns query needed for a request to populate a table. * * @param {Object} options arguments + * @param {string} options.endpoint Report API Endpoint * @param {Object} options.query Query parameters in the url * @param {Object} options.tableQuery Query parameters specific for that endpoint * @param {string} options.defaultDateRange User specified default date range. * @return {Object} Object Table data response */ -export function getReportTableQuery( options ) { +export function getReportTableQuery( + options: Omit< QueryOptions, 'endpoint' > & { + endpoint: ReportItemsEndpoint; + } +) { const { query, tableQuery = {} } = options; const filterQuery = getFilterQuery( options ); const datesFromQuery = getCurrentDates( query, options.defaultDateRange ); @@ -503,7 +580,11 @@ export function getReportTableQuery( options ) { * @param {string} options.defaultDateRange User specified default date range. * @return {Object} Object Table data response */ -export function getReportTableData( options ) { +export function getReportTableData< T extends ReportItemsEndpoint >( + options: Omit< QueryOptions, 'endpoint' > & { + endpoint: ReportItemsEndpoint; + } +) { const { endpoint } = options; let reportSelectors = options.selector; if ( options.select && ! options.selector ) { @@ -530,7 +611,7 @@ export function getReportTableData( options ) { // Disable eslint rule requiring `items` to be defined below because the next two if statements // depend on `getReportItems` to have been called. // eslint-disable-next-line @wordpress/no-unused-vars-before-return - const items = getReportItems( endpoint, tableQuery ); + const items = getReportItems< T >( endpoint, tableQuery ); const queryResolved = hasFinishedResolution( 'getReportItems', [ endpoint, diff --git a/packages/js/extend-cart-checkout-block/$slug-blocks-integration.php.mustache b/packages/js/extend-cart-checkout-block/$slug-blocks-integration.php.mustache index 26e74cac439..89762e199dd 100644 --- a/packages/js/extend-cart-checkout-block/$slug-blocks-integration.php.mustache +++ b/packages/js/extend-cart-checkout-block/$slug-blocks-integration.php.mustache @@ -19,9 +19,18 @@ class {{slugPascalCase}}_Blocks_Integration implements IntegrationInterface { /** * When called invokes any initialization/setup for the integration. - * */ public function initialize() { + $this->register_newsletter_block_frontend_scripts(); + $this->register_newsletter_block_editor_scripts(); + $this->register_newsletter_block_editor_styles(); + $this->register_main_integration(); + } + + /** + * Registers the main JS file required to add filters and Slot/Fills. + */ + public function register_main_integration() { $script_path = '/build/index.js'; $style_path = '/build/style-index.css'; @@ -53,7 +62,6 @@ class {{slugPascalCase}}_Blocks_Integration implements IntegrationInterface { wp_set_script_translations( '{{slug}}-blocks-integration', '{{slug}}', - '{{slug}}', dirname( __FILE__ ) . '/languages' ); } @@ -64,7 +72,7 @@ class {{slugPascalCase}}_Blocks_Integration implements IntegrationInterface { * @return string[] */ public function get_script_handles() { - return array( '{{slug}}-blocks-integration' ); + return array( '{{slug}}-blocks-integration', '{{slug}}-checkout-newsletter-subscription-block-frontend' ); } /** @@ -73,7 +81,7 @@ class {{slugPascalCase}}_Blocks_Integration implements IntegrationInterface { * @return string[] */ public function get_editor_script_handles() { - return array( '{{slug}}-blocks-integration' ); + return array( '{{slug}}-blocks-integration', '{{slug}}-checkout-newsletter-subscription-block-editor' ); } /** @@ -84,13 +92,77 @@ class {{slugPascalCase}}_Blocks_Integration implements IntegrationInterface { public function get_script_data() { $data = array( '{{slug}}-active' => true, - 'example-data' => 'This is some example data from the server', + 'example-data' => __( 'This is some example data from the server', '{{slug}}' ), + 'optInDefaultText' => __( 'I want to receive updates about products and promotions.', '{{ slug }}' ), ); return $data; } + public function register_newsletter_block_editor_styles() { + $style_path = '/build/style-{{slug}}-checkout-newsletter-subscription-block.css'; + + $style_url = plugins_url( $style_path, __FILE__ ); + wp_enqueue_style( + '{{slug}}-blocks-integration', + $style_url, + [], + $this->get_file_version( $style_path ) + ); + } + + public function register_newsletter_block_editor_scripts() { + $script_path = '/build/{{slug}}-checkout-newsletter-subscription-block.js'; + $script_url = plugins_url( $script_path, __FILE__ ); + $script_asset_path = dirname( __FILE__ ) . '/build/{{slug}}-checkout-newsletter-subscription-block.asset.php'; + $script_asset = file_exists( $script_asset_path ) + ? require $script_asset_path + : array( + 'dependencies' => array(), + 'version' => $this->get_file_version( $script_asset_path ), + ); + + wp_register_script( + '{{slug}}-checkout-newsletter-subscription-block-editor', + $script_url, + $script_asset['dependencies'], + $script_asset['version'], + true + ); + + wp_set_script_translations( + '{{slug}}-newsletter-block-editor', // script handle + '{{slug}}', // text domain + dirname( __FILE__ ) . '/languages' + ); + } + + public function register_newsletter_block_frontend_scripts() { + $script_path = '/build/{{slug}}-checkout-newsletter-subscription-block-frontend.js'; + $script_url = plugins_url( $script_path, __FILE__ ); + $script_asset_path = dirname( __FILE__ ) . '/build/newsletter-block-frontend.asset.php'; + $script_asset = file_exists( $script_asset_path ) + ? require $script_asset_path + : array( + 'dependencies' => array(), + 'version' => $this->get_file_version( $script_asset_path ), + ); + + wp_register_script( + '{{slug}}-checkout-newsletter-subscription-block-frontend', + $script_url, + $script_asset['dependencies'], + $script_asset['version'], + true + ); + wp_set_script_translations( + '{{slug}}-checkout-newsletter-subscription-block-frontend', // script handle + '{{slug}}', // text domain + dirname( __FILE__ ) . '/languages' + ); + } + /** * Get the file modified time as a cache buster if we're in dev mode. * @@ -103,4 +175,4 @@ class {{slugPascalCase}}_Blocks_Integration implements IntegrationInterface { } return {{slugPascalCase}}_VERSION; } -} \ No newline at end of file +} diff --git a/packages/js/extend-cart-checkout-block/$slug.php.mustache b/packages/js/extend-cart-checkout-block/$slug.php.mustache index c9f5d74c578..efe1a073e49 100644 --- a/packages/js/extend-cart-checkout-block/$slug.php.mustache +++ b/packages/js/extend-cart-checkout-block/$slug.php.mustache @@ -33,3 +33,19 @@ add_action('woocommerce_blocks_loaded', function() { } ); }); + +/** + * Registers the slug as a block category with WordPress. + */ +function register_{{slugPascalCase}}_block_category( $categories ) { + return array_merge( + $categories, + [ + [ + 'slug' => '{{slug}}', + 'title' => __( '{{slugPascalCase}} Blocks', '{{slug}}' ), + ], + ] + ); +} +add_action( 'block_categories_all', 'register_{{slugPascalCase}}_block_category', 10, 2 ); diff --git a/packages/js/extend-cart-checkout-block/changelog/add-inner-block-example b/packages/js/extend-cart-checkout-block/changelog/add-inner-block-example new file mode 100644 index 00000000000..1a22ae4197f --- /dev/null +++ b/packages/js/extend-cart-checkout-block/changelog/add-inner-block-example @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Include an example of adding an inner block to the WooCommerce Blocks Checkout Block diff --git a/packages/js/extend-cart-checkout-block/src/index.js.mustache b/packages/js/extend-cart-checkout-block/src/index.js.mustache index fe131bcb220..b6da8429a0d 100644 --- a/packages/js/extend-cart-checkout-block/src/index.js.mustache +++ b/packages/js/extend-cart-checkout-block/src/index.js.mustache @@ -1 +1,2 @@ -import './js/index'; \ No newline at end of file +import './js/index'; +import './js/checkout-newsletter-subscription-block'; diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.js.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.js.mustache new file mode 100644 index 00000000000..eab6fe1bf50 --- /dev/null +++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.js.mustache @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import { useEffect, useState } from '@wordpress/element'; +import { CheckboxControl } from '@woocommerce/blocks-checkout'; +import { getSetting } from '@woocommerce/settings'; +import { useSelect, useDispatch } from '@wordpress/data'; + +const { optInDefaultText } = getSetting( '{{slug}}_data', '' ); + +const Block = ( { children, checkoutExtensionData } ) => { + const [ checked, setChecked ] = useState( false ); + const { setExtensionData } = checkoutExtensionData; + + const { setValidationErrors, clearValidationError } = useDispatch( + 'wc/store/validation' + ); + + useEffect( () => { + setExtensionData( '{{slug}}', 'optin', checked ); + if ( ! checked ) { + setValidationErrors( { + '{{slug}}': { + message: 'Please tick the box', + hidden: false, + }, + } ); + return; + } + clearValidationError( '{{slug}}' ); + }, [ + clearValidationError, + setValidationErrors, + checked, + setExtensionData, + ] ); + + const { validationError } = useSelect( ( select ) => { + const store = select( 'wc/store/validation' ); + return { + validationError: store.getValidationError( '{{slug}}' ), + }; + } ); + + return ( + <> + + { children || optInDefaultText } + + + { validationError?.hidden === false && ( +
+ + ⚠️ + + { validationError?.message } +
+ ) } + + ); +}; + +export default Block; diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.json.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.json.mustache new file mode 100644 index 00000000000..ed70cbd7b67 --- /dev/null +++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.json.mustache @@ -0,0 +1,34 @@ +{ + "apiVersion": 2, + "name": "{{slug}}/checkout-newsletter-subscription", + "version": "2.0.0", + "title": "Newsletter Subscription!", + "category": "{{slug}}", + "description": "Adds a newsletter subscription checkbox to the checkout.", + "supports": { + "html": false, + "align": false, + "multiple": false, + "reusable": false + }, + "parent": [ + "woocommerce/checkout-contact-information-block" + ], + "attributes": { + "lock": { + "type": "object", + "default": { + "remove": true, + "move": true + } + }, + "text": { + "type": "string", + "source": "html", + "selector": ".wp-block-{{slug}}-checkout-newsletter-subscription", + "default": "" + } + }, + "textdomain": "{{slug}}", + "editorStyle": "file:../../../build/style-{{slug}}-checkout-newsletter-subscription-block.css" +} diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/edit.js.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/edit.js.mustache new file mode 100644 index 00000000000..f55abeb2b72 --- /dev/null +++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/edit.js.mustache @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + RichText, + InspectorControls, +} from '@wordpress/block-editor'; +import { PanelBody } from '@wordpress/components'; +import { CheckboxControl } from '@woocommerce/blocks-checkout'; +import { getSetting } from '@woocommerce/settings'; +/** + * Internal dependencies + */ +import './style.scss'; +const { optInDefaultText } = getSetting( '{{slug}}_data', '' ); + +export const Edit = ( { attributes, setAttributes } ) => { + const { text } = attributes; + const blockProps = useBlockProps(); + return ( +
+ + + Options for the block go here. + + + + setAttributes( { text: value } ) } + /> +
+ ); +}; + +export const Save = ( { attributes } ) => { + const { text } = attributes; + return ( +
+ +
+ ); +}; \ No newline at end of file diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/frontend.js.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/frontend.js.mustache new file mode 100644 index 00000000000..b93fa66ec9f --- /dev/null +++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/frontend.js.mustache @@ -0,0 +1,14 @@ +/** + * External dependencies + */ +import { registerCheckoutBlock } from '@woocommerce/blocks-checkout'; +/** + * Internal dependencies + */ +import Block from './block'; +import metadata from './block.json'; + +registerCheckoutBlock( { + metadata, + component: Block, +} ); \ No newline at end of file diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/index.js.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/index.js.mustache new file mode 100644 index 00000000000..7a6ea521ade --- /dev/null +++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/index.js.mustache @@ -0,0 +1,33 @@ +/** + * External dependencies + */ +import { SVG } from '@wordpress/components'; +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { Edit, Save } from './edit'; +import metadata from './block.json'; +registerBlockType( metadata, { + icon: { + src: ( + + + + + + + ), + foreground: '#874FB9', + }, + edit: Edit, + save: Save, +} ); diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/style.scss.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/style.scss.mustache new file mode 100644 index 00000000000..395ee85d530 --- /dev/null +++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/style.scss.mustache @@ -0,0 +1,27 @@ +.wp-block-{{slug}}-checkout-newsletter-subscription { + margin: 20px 0; + padding-top: 4px; + padding-bottom: 4px; + display: flex; + align-items: flex-start; + + .block-editor-rich-text__editable { + vertical-align: middle; + line-height: 24px; + } + + .wc-block-components-checkbox { + margin-right: 16px; + margin-top: 0; + } +} + +.editor-styles-wrapper { + + .wp-block-{{slug}}-checkout-newsletter-subscription { + .wc-block-components-checkbox { + margin-right: 0; + margin-top: 0; + } + } +} diff --git a/packages/js/extend-cart-checkout-block/src/js/index.js.mustache b/packages/js/extend-cart-checkout-block/src/js/index.js.mustache index 238e1c1983a..a2ab7d7fa0d 100644 --- a/packages/js/extend-cart-checkout-block/src/js/index.js.mustache +++ b/packages/js/extend-cart-checkout-block/src/js/index.js.mustache @@ -9,14 +9,11 @@ import { getSetting } from '@woocommerce/settings'; */ import './style.scss'; -const exampleDataFromSettings = getSetting( '{{slug}}_data' ); - -/** - * Internal dependencies - */ import { registerFilters } from './filters'; import { ExampleComponent } from './ExampleComponent'; +const exampleDataFromSettings = getSetting( '{{slug}}_data' ); + const render = () => { return ( <> diff --git a/packages/js/extend-cart-checkout-block/webpack.config.js.mustache b/packages/js/extend-cart-checkout-block/webpack.config.js.mustache index c25a9a2f6f6..ce8447b6078 100644 --- a/packages/js/extend-cart-checkout-block/webpack.config.js.mustache +++ b/packages/js/extend-cart-checkout-block/webpack.config.js.mustache @@ -10,6 +10,23 @@ const defaultRules = defaultConfig.module.rules.filter( ( rule ) => { module.exports = { ...defaultConfig, + entry: { + index: path.resolve(process.cwd(), 'src', 'js', 'index.js'), + '{{slug}}-checkout-newsletter-subscription-block': path.resolve( + process.cwd(), + 'src', + 'js', + 'checkout-newsletter-subscription-block', + 'index.js' + ), + '{{slug}}-checkout-newsletter-subscription-block-frontend': path.resolve( + process.cwd(), + 'src', + 'js', + 'checkout-newsletter-subscription-block', + 'frontend.js' + ), + }, module: { ...defaultConfig.module, rules: [ diff --git a/plugins/woocommerce-admin/client/activity-panel/activity-panel.js b/plugins/woocommerce-admin/client/activity-panel/activity-panel.js index 0a5690591fc..d94961584fd 100644 --- a/plugins/woocommerce-admin/client/activity-panel/activity-panel.js +++ b/plugins/woocommerce-admin/client/activity-panel/activity-panel.js @@ -45,6 +45,7 @@ import { ABBREVIATED_NOTIFICATION_SLOT_NAME } from './panels/inbox/abbreviated-n import { getAdminSetting } from '~/utils/admin-settings'; import { useActiveSetupTasklist } from '~/tasks'; import { LayoutContext } from '~/layout'; +import { getSegmentsFromPath } from '~/utils/url-helpers'; const HelpPanel = lazy( () => import( /* webpackChunkName: "activity-panels-help" */ './panels/help' ) @@ -236,6 +237,13 @@ export const ActivityPanel = ( { isEmbedded, query } ) => { return query.page === 'wc-admin' && ! query.path; }; + const isProductPage = () => { + const [ firstPathSegment ] = getSegmentsFromPath( query.path ); + return ( + firstPathSegment === 'add-product' || firstPathSegment === 'product' + ); + }; + const isPerformingSetupTask = () => { return ( query.task && @@ -254,7 +262,9 @@ export const ActivityPanel = ( { isEmbedded, query } ) => { icon: , unread: hasUnreadNotes || hasAbbreviatedNotifications, visible: - ( isEmbedded || ! isHomescreen() ) && ! isPerformingSetupTask(), + ( isEmbedded || ! isHomescreen() ) && + ! isPerformingSetupTask() && + ! isProductPage(), }; const setup = { @@ -273,7 +283,8 @@ export const ActivityPanel = ( { isEmbedded, query } ) => { ! requestingTaskListOptions && ! setupTaskListComplete && ! setupTaskListHidden && - ! isHomescreen(), + ! isHomescreen() && + ! isProductPage(), }; const help = { diff --git a/plugins/woocommerce-admin/client/layout/transient-notices/style.scss b/plugins/woocommerce-admin/client/layout/transient-notices/style.scss index 25dfad3a708..ce44b0bd7a9 100644 --- a/plugins/woocommerce-admin/client/layout/transient-notices/style.scss +++ b/plugins/woocommerce-admin/client/layout/transient-notices/style.scss @@ -4,7 +4,7 @@ position: fixed; bottom: $gap-small; left: $admin-menu-width + $gap; - z-index: 99999; + z-index: calc(z-index('.components-snackbar-list') + 1); width: auto; @media ( max-width: 960px ) { diff --git a/plugins/woocommerce-admin/client/products/add-product-page.tsx b/plugins/woocommerce-admin/client/products/add-product-page.tsx index e594bbf2bfd..5c2ed73dd1e 100644 --- a/plugins/woocommerce-admin/client/products/add-product-page.tsx +++ b/plugins/woocommerce-admin/client/products/add-product-page.tsx @@ -28,7 +28,12 @@ const AddProductPage: React.FC = () => { return (
> - initialValues={ { stock_quantity: 0 } } + initialValues={ { + name: '', + sku: '', + stock_quantity: 0, + stock_status: 'instock', + } } errors={ {} } validate={ validate } > diff --git a/plugins/woocommerce-admin/client/products/constants.js b/plugins/woocommerce-admin/client/products/constants.js deleted file mode 100644 index 2942144a2a7..00000000000 --- a/plugins/woocommerce-admin/client/products/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -export const NUMBERS_AND_ALLOWED_CHARS = '[^-0-9%s1%s2]'; -export const NUMBERS_AND_DECIMAL_SEPARATOR = '[^-\\d\\%s]+'; -export const ONLY_ONE_DECIMAL_SEPARATOR = '[%s](?=%s*[%s])'; diff --git a/plugins/woocommerce-admin/client/products/constants.ts b/plugins/woocommerce-admin/client/products/constants.ts new file mode 100644 index 00000000000..5a340309aea --- /dev/null +++ b/plugins/woocommerce-admin/client/products/constants.ts @@ -0,0 +1,7 @@ +export const NUMBERS_AND_ALLOWED_CHARS = '[^-0-9%s1%s2]'; +export const NUMBERS_AND_DECIMAL_SEPARATOR = '[^-\\d\\%s]+'; +export const ONLY_ONE_DECIMAL_SEPARATOR = '[%s](?=%s*[%s])'; +// This should never be a real slug value of any existing shipping class +export const ADD_NEW_SHIPPING_CLASS_OPTION_VALUE = + '__ADD_NEW_SHIPPING_CLASS_OPTION__'; +export const UNCATEGORIZED_CATEGORY_SLUG = 'uncategorized'; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.scss b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.scss new file mode 100644 index 00000000000..3453461ef8a --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.scss @@ -0,0 +1,63 @@ +.woocommerce-add-attribute-modal { + .components-notice.is-info { + margin-left: 0; + margin-right: 0; + background-color: #f0f6fc; + } + + &__add-attribute { + margin-top: $gap-small; + } + + &__buttons { + margin-top: $gap-larger; + display: flex; + flex-direction: row; + gap: 8px; + justify-content: flex-end; + } + + .components-modal__content { + display: flex; + flex-direction: column; + } + + &__body { + min-height: 200px; + flex: 1 1 auto; + overflow: auto; + } + + &__table { + width: 100%; + margin-top: $gap-large; + + th { + text-align: left; + color: $gray-700; + font-weight: normal; + text-transform: uppercase; + } + } + &__table-header { + padding: 0 0 $gap; + } + &__table-header, + &__table-row { + display: grid; + grid-template-columns: 40% 55% 5%; + border-bottom: 1px solid $gray-300; + align-items: center; + } + &__table-row { + padding: $gap-large 0; + td:not(:last-child) { + margin-right: $gap; + } + } + + &__table-attribute-trash-column { + display: flex; + justify-content: center; + } +} diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx new file mode 100644 index 00000000000..20bb4c5ab23 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx @@ -0,0 +1,343 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { trash } from '@wordpress/icons'; +import { + Form, + __experimentalSelectControlMenuSlot as SelectControlMenuSlot, +} from '@woocommerce/components'; + +import { + Button, + Modal, + Notice, + // @ts-expect-error ConfirmDialog is not part of the typescript definition yet. + __experimentalConfirmDialog as ConfirmDialog, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import './add-attribute-modal.scss'; +import { AttributeInputField } from '../attribute-input-field'; +import { AttributeTermInputField } from '../attribute-term-input-field'; +import { HydratedAttributeType } from '../attribute-field'; + +type AddAttributeModalProps = { + onCancel: () => void; + onAdd: ( newCategories: HydratedAttributeType[] ) => void; + selectedAttributeIds?: number[]; +}; + +type AttributeForm = { + attributes: Array< HydratedAttributeType | { id: undefined; terms: [] } >; +}; + +export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( { + onCancel, + onAdd, + selectedAttributeIds = [], +} ) => { + const [ showConfirmClose, setShowConfirmClose ] = useState( false ); + const addAnother = ( + values: AttributeForm, + setValue: ( + name: string, + value: AttributeForm[ keyof AttributeForm ] + ) => void + ) => { + setValue( 'attributes', [ + ...values.attributes, + { + id: undefined, + terms: [], + }, + ] ); + }; + + const onAddingAttributes = ( values: AttributeForm ) => { + const newAttributesToAdd: HydratedAttributeType[] = []; + values.attributes.forEach( ( attr ) => { + if ( attr.id && attr.name && ( attr.terms || [] ).length > 0 ) { + newAttributesToAdd.push( { + ...( attr as HydratedAttributeType ), + } ); + } + } ); + onAdd( newAttributesToAdd ); + }; + + const onRemove = ( + index: number, + values: AttributeForm, + setValue: ( + name: string, + value: AttributeForm[ keyof AttributeForm ] + ) => void + ) => { + if ( values.attributes.length > 1 ) { + setValue( + 'attributes', + values.attributes.filter( ( val, i ) => i !== index ) + ); + } else { + setValue( `attributes[${ index }]`, [ + { id: undefined, terms: [] }, + ] ); + } + }; + + const focusValueField = ( index: number ) => { + const valueInputField: HTMLInputElement | null = document.querySelector( + '.woocommerce-add-attribute-modal__table-row-' + + index + + ' .woocommerce-add-attribute-modal__table-attribute-value-column .woocommerce-experimental-select-control__input' + ); + if ( valueInputField ) { + setTimeout( () => { + valueInputField.focus(); + }, 0 ); + } + }; + + const onClose = ( values: AttributeForm ) => { + const hasValuesSet = values.attributes.some( + ( value ) => value?.id && value?.terms && value?.terms.length > 0 + ); + if ( hasValuesSet ) { + setShowConfirmClose( true ); + } else { + onCancel(); + } + }; + + return ( + <> + + initialValues={ { + attributes: [ { id: undefined, terms: [] } ], + } } + > + { ( { + values, + setValue, + }: { + values: AttributeForm; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setValue: ( name: string, value: any ) => void; + } ) => { + return ( + + | React.MouseEvent< Element > + | React.FocusEvent< Element > + ) => { + if ( ! event.isPropagationStopped() ) { + onClose( values ); + } + } } + className="woocommerce-add-attribute-modal" + > + +

+ { __( + 'By default, attributes are filterable and visible on the product page. You can change these settings for each attribute separately later.', + 'woocommerce' + ) } +

+
+ +
+ + + + + + + + + { values.attributes.map( + ( formAttr, index ) => ( + + + + + + ) + ) } + +
AttributeValues
+ { + setValue( + 'attributes[' + + index + + ']', + { + ...val, + terms: [], + options: + undefined, + } + ); + if ( val ) { + focusValueField( + index + ); + } + } } + ignoredAttributeIds={ [ + ...selectedAttributeIds, + ...values.attributes + .map( + ( + attr + ) => + attr?.id + ) + .filter( + ( + attrId + ): attrId is number => + attrId !== + undefined + ), + ] } + /> + + + setValue( + 'attributes[' + + index + + '].terms', + val + ) + } + /> + + +
+
+
+ +
+
+ + +
+
+ ); + } } + + { /* Add slot so select control menu renders correctly within Modal */ } + + { showConfirmClose && ( + setShowConfirmClose( false ) } + onConfirm={ onCancel } + > + { __( + 'You have some attributes added to the list, are you sure you want to cancel?', + 'woocommerce' + ) } + + ) } + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.scss b/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.scss index 275bc8869c1..b056534865b 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.scss +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.scss @@ -41,7 +41,7 @@ padding: 0 $gap-large; &:last-child { - margin: -1px; + margin-bottom: -1px; } } diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx index 314c33ca411..9fea1840def 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx @@ -2,62 +2,201 @@ * External dependencies */ import { sprintf, __ } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; -import { ProductAttribute } from '@woocommerce/data'; +import { Button, Card, CardBody } from '@wordpress/components'; +import { useState, useCallback, useEffect } from '@wordpress/element'; +import { + ProductAttribute, + EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME, + ProductAttributeTerm, +} from '@woocommerce/data'; +import { resolveSelect } from '@wordpress/data'; import { Text } from '@woocommerce/experimental'; -import { Sortable, ListItem } from '@woocommerce/components'; -import { trash } from '@wordpress/icons'; +import { + Sortable, + ListItem, + __experimentalSelectControlMenuSlot as SelectControlMenuSlot, +} from '@woocommerce/components'; +import { closeSmall } from '@wordpress/icons'; /** * Internal dependencies */ import './attribute-field.scss'; import AttributeEmptyStateLogo from './attribute-empty-state-logo.svg'; +import { AddAttributeModal } from './add-attribute-modal'; +import { EditAttributeModal } from './edit-attribute-modal'; import { reorderSortableProductAttributePositions } from './utils'; +import { sift } from '../../../utils'; type AttributeFieldProps = { value: ProductAttribute[]; onChange: ( value: ProductAttribute[] ) => void; + productId?: number; +}; + +export type HydratedAttributeType = Omit< ProductAttribute, 'options' > & { + options?: string[]; + terms?: ProductAttributeTerm[]; }; export const AttributeField: React.FC< AttributeFieldProps > = ( { value, onChange, + productId, } ) => { + const [ showAddAttributeModal, setShowAddAttributeModal ] = + useState( false ); + const [ hydrationComplete, setHydrationComplete ] = useState< boolean >( + value ? false : true + ); + const [ hydratedAttributes, setHydratedAttributes ] = useState< + HydratedAttributeType[] + >( [] ); + const [ editingAttributeId, setEditingAttributeId ] = useState< + null | string + >( null ); + + const fetchTerms = useCallback( + ( attributeId: number ) => { + return resolveSelect( + EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME + ) + .getProductAttributeTerms< ProductAttributeTerm[] >( { + attribute_id: attributeId, + product: productId, + } ) + .then( + ( attributeTerms ) => { + return attributeTerms; + }, + ( error ) => { + return error; + } + ); + }, + [ productId ] + ); + + useEffect( () => { + if ( ! value || hydrationComplete ) { + return; + } + + const [ customAttributes, globalAttributes ]: ProductAttribute[][] = + sift( value, ( attr: ProductAttribute ) => attr.id === 0 ); + + Promise.all( + globalAttributes.map( ( attr ) => fetchTerms( attr.id ) ) + ).then( ( allResults ) => { + setHydratedAttributes( [ + ...globalAttributes.map( ( attr, index ) => { + const newAttr = { + ...attr, + terms: allResults[ index ], + options: undefined, + }; + + return newAttr; + } ), + ...customAttributes, + ] ); + setHydrationComplete( true ); + } ); + }, [ productId, value, hydrationComplete ] ); + + const fetchAttributeId = ( attribute: { id: number; name: string } ) => + `${ attribute.id }-${ attribute.name }`; + + const updateAttributes = ( attributes: HydratedAttributeType[] ) => { + setHydratedAttributes( attributes ); + onChange( + attributes.map( ( attr ) => { + return { + ...attr, + options: attr.terms + ? attr.terms.map( ( term ) => term.name ) + : ( attr.options as string[] ), + terms: undefined, + }; + } ) + ); + }; + const onRemove = ( attribute: ProductAttribute ) => { // eslint-disable-next-line no-alert if ( window.confirm( __( 'Remove this attribute?', 'woocommerce' ) ) ) { - onChange( value.filter( ( attr ) => attr.id !== attribute.id ) ); + updateAttributes( + hydratedAttributes.filter( + ( attr ) => + fetchAttributeId( attr ) !== + fetchAttributeId( attribute ) + ) + ); } }; - if ( ! value || value.length === 0 ) { + const onAddNewAttributes = ( newAttributes: HydratedAttributeType[] ) => { + updateAttributes( [ + ...( hydratedAttributes || [] ), + ...newAttributes + .filter( + ( newAttr ) => + ! ( value || [] ).find( + ( attr ) => attr.id === newAttr.id + ) + ) + .map( ( newAttr, index ) => { + newAttr.position = ( value || [] ).length + index; + return newAttr; + } ), + ] ); + setShowAddAttributeModal( false ); + }; + + if ( ! value || value.length === 0 || hydratedAttributes.length === 0 ) { return ( -
-
- Completed - - { __( 'No attributes yet', 'woocommerce' ) } - - -
-
+ + +
+
+ Completed + + { __( 'No attributes yet', 'woocommerce' ) } + + +
+ { showAddAttributeModal && ( + + setShowAddAttributeModal( false ) + } + onAdd={ onAddNewAttributes } + selectedAttributeIds={ ( value || [] ).map( + ( attr ) => attr.id + ) } + /> + ) } +
+
+
); } @@ -72,6 +211,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( { }, {} as Record< number, ProductAttribute > ); + return (
= ( { } } > { sortedAttributes.map( ( attribute ) => ( - +
{ attribute.name }
{ attribute.options @@ -108,11 +248,18 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( { ) }
- + { showAddAttributeModal && ( + setShowAddAttributeModal( false ) } + onAdd={ onAddNewAttributes } + selectedAttributeIds={ value.map( ( attr ) => attr.id ) } + /> + ) } + + { editingAttributeId && ( + setEditingAttributeId( null ) } + onEdit={ ( changedAttribute ) => { + const newAttributesSet = [ ...hydratedAttributes ]; + const changedAttributeIndex: number = + newAttributesSet.findIndex( + ( attr ) => attr.id === changedAttribute.id + ); + + newAttributesSet.splice( + changedAttributeIndex, + 1, + changedAttribute + ); + + updateAttributes( newAttributesSet ); + setEditingAttributeId( null ); + } } + attribute={ + hydratedAttributes.find( + ( attr ) => + fetchAttributeId( attr ) === editingAttributeId + ) as HydratedAttributeType + } + /> + ) } +
); }; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.scss b/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.scss new file mode 100644 index 00000000000..420debdc77a --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.scss @@ -0,0 +1,35 @@ +.woocommerce-edit-attribute-modal { + overflow: visible; +} + +.woocommerce-edit-attribute-modal__body { + width: 500px; + max-width: 100%; + + .woocommerce-experimental-select-control + .woocommerce-experimental-select-control { + margin-top: 1.3em; + } + + .woocommerce-experimental-select-control__label, + .components-base-control__label { + font-size: 14px; + color: #757575; + font-weight: bold; + text-transform: none; + } + + .woocommerce-edit-attribute-modal__option-container { + display: flex; + flex-direction: row; + align-items: center; + } + + .woocommerce-attribute-term-field { + margin-bottom: 1.5em; + } + + .woocommerce-edit-attribute-modal__helper-text { + color: #757575; + margin: 0.5em 0 1.5em 0; + } +} diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.tsx new file mode 100644 index 00000000000..19e82720c5e --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.tsx @@ -0,0 +1,184 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + Button, + Modal, + CheckboxControl, + TextControl, +} from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { + __experimentalTooltip as Tooltip, + Link, +} from '@woocommerce/components'; +import interpolateComponents from '@automattic/interpolate-components'; +import { getAdminLink } from '@woocommerce/settings'; + +/** + * Internal dependencies + */ +import { + AttributeTermInputField, + CustomAttributeTermInputField, +} from '../attribute-term-input-field'; +import { HydratedAttributeType } from './attribute-field'; + +import './edit-attribute-modal.scss'; + +type EditAttributeModalProps = { + onCancel: () => void; + onEdit: ( alteredAttribute: HydratedAttributeType ) => void; + attribute: HydratedAttributeType; +}; + +export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( { + onCancel, + onEdit, + attribute, +} ) => { + const [ editableAttribute, setEditableAttribute ] = useState< + HydratedAttributeType | undefined + >( { ...attribute } ); + + const isCustomAttribute = editableAttribute?.id === 0; + + return ( + onCancel() } + className="woocommerce-edit-attribute-modal" + > +
+ + setEditableAttribute( { + ...( editableAttribute as HydratedAttributeType ), + name: val, + } ) + } + /> +

+ { ! isCustomAttribute + ? interpolateComponents( { + mixedString: __( + `You can change the attribute's name in {{link}}Attributes{{/link}}.`, + 'woocommerce' + ), + components: { + link: ( + + <> + + ), + }, + } ) + : __( + 'Your customers will see this on the product page', + 'woocommerce' + ) } +

+ { attribute.terms ? ( + { + setEditableAttribute( { + ...( editableAttribute as HydratedAttributeType ), + terms: val, + } ); + } } + /> + ) : ( + { + setEditableAttribute( { + ...( editableAttribute as HydratedAttributeType ), + options: val, + } ); + } } + /> + ) } + +
+ + setEditableAttribute( { + ...( editableAttribute as HydratedAttributeType ), + visible: val, + } ) + } + checked={ editableAttribute?.visible } + label={ __( 'Visible to customers', 'woocommerce' ) } + /> + +
+
+ + setEditableAttribute( { + ...( editableAttribute as HydratedAttributeType ), + variation: val, + } ) + } + checked={ editableAttribute?.variation } + label={ __( 'Used for filters', 'woocommerce' ) } + /> + +
+
+
+ + +
+
+ ); +}; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/test/add-attribute-modal.spec.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/test/add-attribute-modal.spec.tsx new file mode 100644 index 00000000000..d91848deb76 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/test/add-attribute-modal.spec.tsx @@ -0,0 +1,291 @@ +/** + * External dependencies + */ +import { render } from '@testing-library/react'; +import { ProductAttribute, ProductAttributeTerm } from '@woocommerce/data'; + +/** + * Internal dependencies + */ +import { AddAttributeModal } from '../add-attribute-modal'; + +let attributeOnChange: ( val: ProductAttribute ) => void; +jest.mock( '../../attribute-input-field', () => ( { + AttributeInputField: ( { + onChange, + }: { + onChange: ( + value?: Omit< + ProductAttribute, + 'position' | 'visible' | 'variation' + > + ) => void; + } ) => { + attributeOnChange = onChange; + return
attribute_input_field
; + }, +} ) ); +let attributeTermOnChange: ( val: ProductAttributeTerm[] ) => void; +jest.mock( '../../attribute-term-input-field', () => ( { + AttributeTermInputField: ( { + onChange, + disabled, + }: { + onChange: ( value: ProductAttributeTerm[] ) => void; + disabled: boolean; + } ) => { + attributeTermOnChange = onChange; + return ( +
+ attribute_term_input_field: disabled:{ disabled.toString() } +
+ ); + }, +} ) ); + +const attributeList: ProductAttribute[] = [ + { + id: 15, + name: 'Automotive', + position: 0, + visible: true, + variation: false, + options: [ 'test' ], + }, + { + id: 1, + name: 'Color', + position: 2, + visible: true, + variation: true, + options: [ + 'Beige', + 'black', + 'Blue', + 'brown', + 'Gray', + 'Green', + 'mint', + 'orange', + 'pink', + 'Red', + 'white', + 'Yellow', + ], + }, +]; + +const attributeTermList: ProductAttributeTerm[] = [ + { + id: 23, + name: 'XXS', + slug: 'xxs', + description: '', + menu_order: 1, + count: 1, + }, + { + id: 22, + name: 'XS', + slug: 'xs', + description: '', + menu_order: 2, + count: 1, + }, + { + id: 17, + name: 'S', + slug: 's', + description: '', + menu_order: 3, + count: 1, + }, + { + id: 18, + name: 'M', + slug: 'm', + description: '', + menu_order: 4, + count: 1, + }, + { + id: 19, + name: 'L', + slug: 'l', + description: '', + menu_order: 5, + count: 1, + }, +]; + +describe( 'AddAttributeModal', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should render at-least one row with the attribute dropdown fields', () => { + const { queryAllByText } = render( + {} } + onAdd={ () => {} } + selectedAttributeIds={ [] } + /> + ); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 1 ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ).length + ).toEqual( 1 ); + } ); + + it( 'should enable attribute term field once attribute is selected', () => { + const { queryAllByText } = render( + {} } + onAdd={ () => {} } + selectedAttributeIds={ [] } + /> + ); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 1 ); + attributeOnChange( attributeList[ 0 ] ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:false' ) + .length + ).toEqual( 1 ); + } ); + + it( 'should allow us to add multiple new rows with the attribute fields', () => { + const { queryAllByText, queryByRole } = render( + {} } + onAdd={ () => {} } + selectedAttributeIds={ [] } + /> + ); + queryByRole( 'button', { name: 'Add another attribute' } )?.click(); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 2 ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ).length + ).toEqual( 2 ); + queryByRole( 'button', { name: 'Add another attribute' } )?.click(); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 3 ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ).length + ).toEqual( 3 ); + } ); + + it( 'should allow us to remove the added fields', () => { + const { queryAllByText, queryByRole, queryAllByLabelText } = render( + {} } + onAdd={ () => {} } + selectedAttributeIds={ [] } + /> + ); + + queryByRole( 'button', { name: 'Add another attribute' } )?.click(); + queryByRole( 'button', { name: 'Add another attribute' } )?.click(); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 3 ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ).length + ).toEqual( 3 ); + + const removeButtons = queryAllByLabelText( 'Remove attribute' ); + + removeButtons[ 0 ].click(); + removeButtons[ 1 ].click(); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 1 ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ).length + ).toEqual( 1 ); + } ); + + it( 'should not allow us to remove all the rows', () => { + const { queryAllByText, queryAllByLabelText } = render( + {} } + onAdd={ () => {} } + selectedAttributeIds={ [] } + /> + ); + + const removeButtons = queryAllByLabelText( 'Remove attribute' ); + + removeButtons[ 0 ].click(); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 1 ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ).length + ).toEqual( 1 ); + } ); + + describe( 'onAdd', () => { + it( 'should not return empty attribute rows', () => { + const onAddMock = jest.fn(); + const { queryAllByText, queryByLabelText, queryByRole } = render( + {} } + onAdd={ onAddMock } + selectedAttributeIds={ [] } + /> + ); + + const addAnotherButton = queryByLabelText( + 'Add another attribute' + ); + addAnotherButton?.click(); + addAnotherButton?.click(); + expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( + 3 + ); + expect( + queryAllByText( 'attribute_term_input_field: disabled:true' ) + .length + ).toEqual( 3 ); + queryByRole( 'button', { name: 'Add attributes' } )?.click(); + expect( onAddMock ).toHaveBeenCalledWith( [] ); + } ); + + it( 'should not add attribute if no terms were selected', () => { + const onAddMock = jest.fn(); + const { queryByRole } = render( + {} } + onAdd={ onAddMock } + selectedAttributeIds={ [] } + /> + ); + + attributeOnChange( attributeList[ 0 ] ); + queryByRole( 'button', { name: 'Add attributes' } )?.click(); + expect( onAddMock ).toHaveBeenCalledWith( [] ); + } ); + + it( 'should add attribute with array of terms', () => { + const onAddMock = jest.fn(); + const { queryByRole } = render( + {} } + onAdd={ onAddMock } + selectedAttributeIds={ [] } + /> + ); + + attributeOnChange( attributeList[ 0 ] ); + attributeTermOnChange( [ + attributeTermList[ 0 ], + attributeTermList[ 1 ], + ] ); + queryByRole( 'button', { name: 'Add attributes' } )?.click(); + + const onAddMockCalls = onAddMock.mock.calls[ 0 ][ 0 ]; + + expect( onAddMockCalls ).toHaveLength( 1 ); + expect( onAddMockCalls[ 0 ].id ).toEqual( attributeList[ 0 ].id ); + expect( onAddMockCalls[ 0 ].terms[ 0 ].name ).toEqual( + attributeTermList[ 0 ].name + ); + expect( onAddMockCalls[ 0 ].terms[ 1 ].name ).toEqual( + attributeTermList[ 1 ].name + ); + } ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/test/attribute-field.spec.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-field/test/attribute-field.spec.tsx index d6152e19562..542a1fe7e07 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/test/attribute-field.spec.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/test/attribute-field.spec.tsx @@ -1,48 +1,16 @@ /** * External dependencies */ -import { render } from '@testing-library/react'; +import { render, act, screen, waitFor } from '@testing-library/react'; import { useState, useEffect } from '@wordpress/element'; import { ProductAttribute } from '@woocommerce/data'; +import { resolveSelect } from '@wordpress/data'; /** * Internal dependencies */ import { AttributeField } from '../attribute-field'; -let triggerDrag: ( items: Array< { key: string } > ) => void; - -jest.mock( '@woocommerce/components', () => ( { - __esModule: true, - ListItem: ( { children }: { children: JSX.Element } ) => children, - Sortable: ( { - onOrderChange, - children, - }: { - onOrderChange: ( items: Array< { key: string } > ) => void; - children: JSX.Element[]; - } ) => { - const [ items, setItems ] = useState< JSX.Element[] >( [] ); - useEffect( () => { - if ( ! children ) { - return; - } - setItems( Array.isArray( children ) ? children : [ children ] ); - }, [ children ] ); - - triggerDrag = ( newItems: Array< { key: string } > ) => { - onOrderChange( newItems ); - }; - return ( - <> - { items.map( ( child, index ) => ( -
{ child }
- ) ) } - - ); - }, -} ) ); - const attributeList: ProductAttribute[] = [ { id: 15, @@ -75,6 +43,66 @@ const attributeList: ProductAttribute[] = [ }, ]; +let triggerDrag: ( items: Array< { key: string } > ) => void; + +jest.mock( '@wordpress/data', () => ( { + ...jest.requireActual( '@wordpress/data' ), + resolveSelect: jest.fn().mockReturnValue( { + getProductAttributeTerms: ( { + attribute_id, + }: { + attribute_id: number; + } ) => + new Promise( ( resolve ) => { + const attr = attributeList.find( + ( item ) => item.id === attribute_id + ); + resolve( + attr?.options.map( ( itemName, index ) => ( { + id: ++index, + slug: itemName.toLowerCase(), + name: itemName, + description: '', + menu_order: ++index, + count: ++index, + } ) ) + ); + } ), + } ), +} ) ); + +jest.mock( '@woocommerce/components', () => ( { + __esModule: true, + ListItem: ( { children }: { children: JSX.Element } ) => children, + __experimentalSelectControlMenuSlot: () => null, + Sortable: ( { + onOrderChange, + children, + }: { + onOrderChange: ( items: Array< { key: string } > ) => void; + children: JSX.Element[]; + } ) => { + const [ items, setItems ] = useState< JSX.Element[] >( [] ); + useEffect( () => { + if ( ! children ) { + return; + } + setItems( Array.isArray( children ) ? children : [ children ] ); + }, [ children ] ); + + triggerDrag = ( newItems: Array< { key: string } > ) => { + onOrderChange( newItems ); + }; + return ( + <> + { items.map( ( child, index ) => ( +
{ child }
+ ) ) } + + ); + }, +} ) ); + describe( 'AttributeField', () => { beforeEach( () => { jest.clearAllMocks(); @@ -90,103 +118,138 @@ describe( 'AttributeField', () => { } ); } ); - it( 'should render the list of existing attributes', () => { - const { queryByText } = render( - {} } - /> - ); - expect( queryByText( 'No attributes yet' ) ).not.toBeInTheDocument(); - expect( queryByText( 'Add first attribute' ) ).not.toBeInTheDocument(); - expect( queryByText( attributeList[ 0 ].name ) ).toBeInTheDocument(); - expect( queryByText( attributeList[ 1 ].name ) ).toBeInTheDocument(); - } ); + it( 'should render the list of existing attributes', async () => { + act( () => { + render( + {} } + /> + ); + } ); - it( 'should render the first two terms of each attribute, and show "+ n more" for the rest', () => { - const { queryByText } = render( - {} } - /> - ); expect( - queryByText( attributeList[ 0 ].options[ 0 ] ) - ).toBeInTheDocument(); - expect( - queryByText( attributeList[ 1 ].options[ 0 ] ) - ).toBeInTheDocument(); - expect( - queryByText( attributeList[ 1 ].options[ 1 ] ) - ).toBeInTheDocument(); - expect( - queryByText( attributeList[ 1 ].options[ 2 ] ) + await screen.findByText( 'No attributes yet' ) ).not.toBeInTheDocument(); expect( - queryByText( + await screen.findByText( attributeList[ 0 ].name ) + ).toBeInTheDocument(); + expect( + await screen.findByText( attributeList[ 1 ].name ) + ).toBeInTheDocument(); + } ); + + it( 'should render the first two terms of each attribute, and show "+ n more" for the rest', async () => { + act( () => { + render( + {} } + /> + ); + } ); + + expect( + await screen.findByText( attributeList[ 0 ].options[ 0 ] ) + ).toBeInTheDocument(); + expect( + await screen.findByText( attributeList[ 1 ].options[ 0 ] ) + ).toBeInTheDocument(); + expect( + await screen.findByText( attributeList[ 1 ].options[ 1 ] ) + ).toBeInTheDocument(); + expect( + await screen.queryByText( attributeList[ 1 ].options[ 2 ] ) + ).not.toBeInTheDocument(); + expect( + await screen.queryByText( `+ ${ attributeList[ 1 ].options.length - 2 } more` ) ).not.toBeInTheDocument(); } ); describe( 'deleting', () => { - it( 'should show a window confirm when trash icon is clicked', () => { + it( 'should show a window confirm when trash icon is clicked', async () => { jest.spyOn( global, 'confirm' ).mockReturnValueOnce( false ); - const { queryAllByLabelText } = render( - {} } - /> - ); - queryAllByLabelText( 'Remove attribute' )[ 0 ].click(); + act( () => { + render( + {} } + /> + ); + } ); + ( + await screen.findAllByLabelText( 'Remove attribute' ) + )[ 0 ].click(); expect( global.confirm ).toHaveBeenCalled(); } ); - it( 'should trigger onChange with removed item when user clicks ok on alert', () => { + it( 'should trigger onChange with removed item when user clicks ok on alert', async () => { jest.spyOn( global, 'confirm' ).mockReturnValueOnce( true ); const onChange = jest.fn(); - const { queryAllByLabelText } = render( - - ); - queryAllByLabelText( 'Remove attribute' )[ 0 ].click(); + + act( () => { + render( + + ); + } ); + + ( + await screen.findAllByLabelText( 'Remove attribute' ) + )[ 0 ].click(); + expect( global.confirm ).toHaveBeenCalled(); expect( onChange ).toHaveBeenCalledWith( [ attributeList[ 1 ] ] ); } ); - it( 'should not trigger onChange with removed item when user cancel', () => { + it( 'should not trigger onChange with removed item when user cancel', async () => { jest.spyOn( global, 'confirm' ).mockReturnValueOnce( false ); const onChange = jest.fn(); - const { queryAllByLabelText } = render( - - ); - queryAllByLabelText( 'Remove attribute' )[ 0 ].click(); + act( () => { + render( + + ); + } ); + ( + await screen.findAllByLabelText( 'Remove attribute' ) + )[ 0 ].click(); expect( global.confirm ).toHaveBeenCalled(); expect( onChange ).not.toHaveBeenCalled(); } ); } ); describe( 'dragging', () => { - it( 'should trigger onChange with new order when onOrderChange triggered', () => { + it.skip( 'should trigger onChange with new order when onOrderChange triggered', async () => { + jest.spyOn( global, 'confirm' ).mockReturnValueOnce( true ); const onChange = jest.fn(); - const { queryAllByLabelText } = render( - - ); + + act( () => { + render( + + ); + } ); + if ( triggerDrag ) { triggerDrag( [ { key: attributeList[ 1 ].id.toString() }, { key: attributeList[ 0 ].id.toString() }, ] ); } - queryAllByLabelText( 'Remove attribute' )[ 0 ].click(); + + ( + await screen.findAllByLabelText( 'Remove attribute' ) + )[ 0 ].click(); + expect( onChange ).toHaveBeenCalledWith( [ { ...attributeList[ 1 ], position: 0 }, { ...attributeList[ 0 ], position: 1 }, diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/utils.ts b/plugins/woocommerce-admin/client/products/fields/attribute-field/utils.ts index 2fc1aa6dfb3..5ff34d939e7 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-field/utils.ts +++ b/plugins/woocommerce-admin/client/products/fields/attribute-field/utils.ts @@ -6,8 +6,8 @@ import { ProductAttribute } from '@woocommerce/data'; /** * Updates the position of a product attribute from the new items JSX.Element list. * - * @param { JSX.Element[] } items list of JSX elements coming back from sortable container. - * @param { Object } attributeKeyValues key value pair of product attributes. + * @param { JSX.Element[] } items list of JSX elements coming back from sortable container. + * @param { Object } attributeKeyValues key value pair of product attributes. */ export function reorderSortableProductAttributePositions( items: JSX.Element[], diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx new file mode 100644 index 00000000000..68210642193 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx @@ -0,0 +1,116 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; +import { Spinner } from '@wordpress/components'; +import { + EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME, + QueryProductAttribute, + ProductAttribute, + WCDataSelector, +} from '@woocommerce/data'; +import { + __experimentalSelectControl as SelectControl, + __experimentalSelectControlMenu as Menu, + __experimentalSelectControlMenuItem as MenuItem, +} from '@woocommerce/components'; + +type NarrowedQueryAttribute = Pick< QueryProductAttribute, 'id' | 'name' >; + +type AttributeInputFieldProps = { + value?: Pick< QueryProductAttribute, 'id' | 'name' > | null; + onChange: ( + value?: Omit< ProductAttribute, 'position' | 'visible' | 'variation' > + ) => void; + label?: string; + placeholder?: string; + disabled?: boolean; + ignoredAttributeIds?: number[]; +}; + +export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( { + value = null, + onChange, + placeholder, + label, + disabled, + ignoredAttributeIds = [], +} ) => { + const { attributes, isLoading } = useSelect( ( select: WCDataSelector ) => { + const { getProductAttributes, hasFinishedResolution } = select( + EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME + ); + return { + isLoading: ! hasFinishedResolution( 'getProductAttributes' ), + attributes: getProductAttributes(), + }; + } ); + + const getFilteredItems = ( + allItems: NarrowedQueryAttribute[], + inputValue: string + ) => { + const ignoreIdsFilter = ( item: NarrowedQueryAttribute ) => + ignoredAttributeIds.length + ? ! ignoredAttributeIds.includes( item.id ) + : true; + + return allItems.filter( + ( item ) => + ignoreIdsFilter( item ) && + ( item.name || '' ) + .toLowerCase() + .startsWith( inputValue.toLowerCase() ) + ); + }; + + return ( + + items={ attributes || [] } + label={ label || '' } + disabled={ disabled } + getFilteredItems={ getFilteredItems } + placeholder={ placeholder } + getItemLabel={ ( item ) => item?.name || '' } + getItemValue={ ( item ) => item?.id || '' } + selected={ value } + onSelect={ ( attribute ) => { + onChange( { + id: attribute.id, + name: attribute.name, + options: [], + } ); + } } + onRemove={ () => onChange() } + > + { ( { + items: renderItems, + highlightedIndex, + getItemProps, + getMenuProps, + isOpen, + } ) => { + return ( + + { isLoading ? ( + + ) : ( + renderItems.map( ( item, index: number ) => ( + + { item.name } + + ) ) + ) } + + ); + } } + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-input-field/index.ts b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/index.ts new file mode 100644 index 00000000000..6000ad95f49 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/index.ts @@ -0,0 +1 @@ +export * from './attribute-input-field'; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-input-field/test/attribute-input-field.spec.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/test/attribute-input-field.spec.tsx new file mode 100644 index 00000000000..0458350ae90 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/test/attribute-input-field.spec.tsx @@ -0,0 +1,226 @@ +/** + * External dependencies + */ +import { render } from '@testing-library/react'; +import { useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { ProductAttribute, QueryProductAttribute } from '@woocommerce/data'; + +/** + * Internal dependencies + */ +import { AttributeInputField } from '../attribute-input-field'; + +jest.mock( '@wordpress/data', () => ( { + ...jest.requireActual( '@wordpress/data' ), + useSelect: jest.fn(), +} ) ); + +jest.mock( '@wordpress/components', () => ( { + __esModule: true, + Spinner: () =>
spinner
, +} ) ); + +jest.mock( '@woocommerce/components', () => { + return { + __esModule: true, + __experimentalSelectControlMenu: ( { + children, + }: { + children: JSX.Element; + } ) => children, + __experimentalSelectControlMenuItem: ( { + children, + }: { + children: JSX.Element; + } ) =>
{ children }
, + __experimentalSelectControl: ( { + children, + items, + getFilteredItems, + onSelect, + onRemove, + }: { + children: ( options: { + isOpen: boolean; + items: QueryProductAttribute[]; + getMenuProps: () => Record< string, string >; + getItemProps: () => Record< string, string >; + } ) => JSX.Element; + items: QueryProductAttribute[]; + onSelect: ( item: QueryProductAttribute ) => void; + onRemove: ( item: QueryProductAttribute ) => void; + getFilteredItems: ( + allItems: QueryProductAttribute[], + inputValue: string, + selectedItems: QueryProductAttribute[] + ) => QueryProductAttribute[]; + } ) => { + const [ input, setInput ] = useState( '' ); + return ( +
+ attribute_input_field + + + +
+ { children( { + isOpen: true, + items: getFilteredItems( items, input, [] ), + getMenuProps: () => ( {} ), + getItemProps: () => ( {} ), + } ) } +
+
+ ); + }, + }; +} ); + +const attributeList: ProductAttribute[] = [ + { + id: 15, + name: 'Automotive', + position: 0, + visible: true, + variation: false, + options: [ 'test' ], + }, + { + id: 1, + name: 'Color', + position: 2, + visible: true, + variation: true, + options: [ + 'Beige', + 'black', + 'Blue', + 'brown', + 'Gray', + 'Green', + 'mint', + 'orange', + 'pink', + 'Red', + 'white', + 'Yellow', + ], + }, +]; + +describe( 'AttributeInputField', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should show spinner while attributes are loading', () => { + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: true, + attributes: undefined, + } ); + const { queryByText } = render( + + ); + expect( queryByText( 'spinner' ) ).toBeInTheDocument(); + } ); + + it( 'should render attributes when finished loading', () => { + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: false, + attributes: attributeList, + } ); + const { queryByText } = render( + + ); + expect( queryByText( 'spinner' ) ).not.toBeInTheDocument(); + expect( queryByText( attributeList[ 0 ].name ) ).toBeInTheDocument(); + expect( queryByText( attributeList[ 1 ].name ) ).toBeInTheDocument(); + } ); + + it( 'should filter out attribute ids passed into ignoredAttributeIds', () => { + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: false, + attributes: attributeList, + } ); + const { queryByText } = render( + + ); + expect( queryByText( 'spinner' ) ).not.toBeInTheDocument(); + expect( + queryByText( attributeList[ 0 ].name ) + ).not.toBeInTheDocument(); + expect( queryByText( attributeList[ 1 ].name ) ).toBeInTheDocument(); + } ); + + it( 'should filter attributes by name case insensitive', () => { + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: false, + attributes: attributeList, + } ); + const { queryByText } = render( + + ); + queryByText( 'Update Input' )?.click(); + expect( + queryByText( attributeList[ 0 ].name ) + ).not.toBeInTheDocument(); + expect( queryByText( attributeList[ 1 ].name ) ).toBeInTheDocument(); + } ); + + it( 'should filter out attributes ids from ignoredAttributeIds', () => { + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: false, + attributes: attributeList, + } ); + const { queryByText } = render( + + ); + expect( queryByText( attributeList[ 0 ].name ) ).toBeInTheDocument(); + expect( + queryByText( attributeList[ 1 ].name ) + ).not.toBeInTheDocument(); + } ); + + it( 'should trigger onChange when onSelect is triggered with attribute value', () => { + const onChangeMock = jest.fn(); + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: false, + attributes: attributeList, + } ); + const { queryByText } = render( + + ); + queryByText( 'select attribute' )?.click(); + expect( onChangeMock ).toHaveBeenCalledWith( { + id: attributeList[ 0 ].id, + name: attributeList[ 0 ].name, + options: [], + } ); + } ); + + it( 'should trigger onChange when onRemove is triggered with undefined', () => { + const onChangeMock = jest.fn(); + ( useSelect as jest.Mock ).mockReturnValue( { + isLoading: false, + attributes: attributeList, + } ); + const { queryByText } = render( + + ); + queryByText( 'remove attribute' )?.click(); + expect( onChangeMock ).toHaveBeenCalledWith(); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss new file mode 100644 index 00000000000..1bc3b232ed4 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.scss @@ -0,0 +1,18 @@ +.woocommerce-attribute-term-field { + &__loading-spinner { + padding: 12px 0; + } + &__add-new { + display: flex; + align-items: center; + font-weight: 600; + } + &__add-new-icon { + margin-right: $gap-small; + } +} + +.woocommerce-attribute-term-field__add-new { + display: flex; + align-items: center; +} diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx new file mode 100644 index 00000000000..32fdc11d0d0 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/attribute-term-input-field.tsx @@ -0,0 +1,288 @@ +/** + * External dependencies + */ +import { sprintf, __ } from '@wordpress/i18n'; +import { CheckboxControl, Icon, Spinner } from '@wordpress/components'; +import { resolveSelect } from '@wordpress/data'; +import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; +import { useDebounce } from '@wordpress/compose'; +import { plus } from '@wordpress/icons'; +import { + EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME, + ProductAttributeTerm, +} from '@woocommerce/data'; +import { + selectControlStateChangeTypes, + __experimentalSelectControl as SelectControl, + __experimentalSelectControlMenu as Menu, + __experimentalSelectControlMenuItem as MenuItem, +} from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import './attribute-term-input-field.scss'; +import { CreateAttributeTermModal } from './create-attribute-term-modal'; + +type AttributeTermInputFieldProps = { + value?: ProductAttributeTerm[]; + onChange: ( value: ProductAttributeTerm[] ) => void; + attributeId?: number; + placeholder?: string; + disabled?: boolean; + label?: string; +}; + +let uniqueId = 0; + +export const AttributeTermInputField: React.FC< + AttributeTermInputFieldProps +> = ( { + value = [], + onChange, + placeholder, + disabled, + attributeId, + label = '', +} ) => { + const attributeTermInputId = useRef( + `woocommerce-attribute-term-field-${ ++uniqueId }` + ); + const [ fetchedItems, setFetchedItems ] = useState< + ProductAttributeTerm[] + >( [] ); + const [ isFetching, setIsFetching ] = useState( false ); + const [ addNewAttributeTermName, setAddNewAttributeTermName ] = + useState< string >(); + + const fetchItems = useCallback( + ( searchString?: string | undefined ) => { + setIsFetching( true ); + return resolveSelect( + EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME + ) + .getProductAttributeTerms< ProductAttributeTerm[] >( { + search: searchString || '', + attribute_id: attributeId, + } ) + .then( + ( attributeTerms ) => { + setFetchedItems( attributeTerms ); + setIsFetching( false ); + return attributeTerms; + }, + ( error ) => { + setIsFetching( false ); + return error; + } + ); + }, + [ attributeId ] + ); + + const debouncedSearch = useDebounce( fetchItems, 250 ); + + useEffect( () => { + if ( + ! disabled && + attributeId !== undefined && + ! fetchedItems.length + ) { + fetchItems(); + } + }, [ disabled, attributeId ] ); + + const onRemove = ( item: ProductAttributeTerm ) => { + onChange( value.filter( ( opt ) => opt.slug !== item.slug ) ); + }; + + const onSelect = ( item: ProductAttributeTerm ) => { + // Add new item. + if ( item.id === -99 ) { + setAddNewAttributeTermName( item.name ); + return; + } + const isSelected = value.find( ( i ) => i.slug === item.slug ); + if ( isSelected ) { + onRemove( item ); + return; + } + onChange( [ ...value, item ] ); + }; + + const focusSelectControl = () => { + const selectControlInputField: HTMLInputElement | null = + document.querySelector( + '.' + + attributeTermInputId.current + + ' .woocommerce-experimental-select-control__input' + ); + if ( selectControlInputField ) { + setTimeout( () => { + selectControlInputField.focus(); + }, 0 ); + } + }; + + const selectedTermSlugs = ( value || [] ).map( ( term ) => term.slug ); + + return ( + <> + + items={ fetchedItems } + multiple + disabled={ disabled || ! attributeId } + label={ label } + getFilteredItems={ ( allItems, inputValue ) => { + if ( + inputValue.length > 0 && + ! allItems.find( + ( item ) => + item.name.toLowerCase() === + inputValue.toLowerCase() + ) + ) { + return [ + ...allItems, + { + id: -99, + name: inputValue, + } as ProductAttributeTerm, + ]; + } + return allItems; + } } + onInputChange={ debouncedSearch } + placeholder={ placeholder || '' } + getItemLabel={ ( item ) => item?.name || '' } + getItemValue={ ( item ) => item?.slug || '' } + stateReducer={ ( state, actionAndChanges ) => { + const { changes, type } = actionAndChanges; + switch ( type ) { + case selectControlStateChangeTypes.ControlledPropUpdatedSelectedItem: + return { + ...changes, + inputValue: state.inputValue, + }; + case selectControlStateChangeTypes.ItemClick: + if ( + changes.selectedItem && + changes.selectedItem.id === -99 + ) { + return changes; + } + return { + ...changes, + isOpen: true, + inputValue: state.inputValue, + highlightedIndex: state.highlightedIndex, + }; + default: + return changes; + } + } } + selected={ value } + onSelect={ onSelect } + onRemove={ onRemove } + className={ + 'woocommerce-attribute-term-field ' + + attributeTermInputId.current + } + > + { ( { + items, + highlightedIndex, + getItemProps, + getMenuProps, + isOpen, + } ) => { + return ( + + { [ + isFetching ? ( +
+ +
+ ) : null, + ...items.map( ( item, menuIndex ) => { + const isSelected = + selectedTermSlugs.includes( item.slug ); + + return ( + + { item.id !== -99 ? ( + null } + checked={ isSelected } + label={ + + { item.name } + + } + /> + ) : ( +
+ + + { sprintf( + /* translators: The name of the new attribute term to be created */ + __( + 'Create "%s"', + 'woocommerce' + ), + item.name + ) } + +
+ ) } +
+ ); + } ), + ].filter( + ( child ): child is JSX.Element => + child !== null + ) } +
+ ); + } } + + { addNewAttributeTermName && attributeId !== undefined && ( + { + setAddNewAttributeTermName( undefined ); + focusSelectControl(); + } } + attributeId={ attributeId } + onCreated={ ( newAttribute ) => { + onSelect( newAttribute ); + setAddNewAttributeTermName( undefined ); + focusSelectControl(); + } } + /> + ) } + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.scss b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.scss new file mode 100644 index 00000000000..87c014b3880 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.scss @@ -0,0 +1,22 @@ +.woocommerce-create-attribute-term-modal { + @mixin break-medium { + min-width: 650px; + } + + &__buttons { + margin-top: $gap-larger; + display: flex; + flex-direction: row; + gap: 8px; + justify-content: flex-end; + } + .has-error { + .components-base-control__help { + color: $studio-red-50; + } + } + .components-base-control:not(:first-child) { + margin-top: 16px; + margin-bottom: 0; + } +} diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.tsx new file mode 100644 index 00000000000..107b6ee6755 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.tsx @@ -0,0 +1,175 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + Button, + Modal, + TextareaControl, + TextControl, +} from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; +import { cleanForSlug } from '@wordpress/url'; +import { Form, FormContext, FormErrors } from '@woocommerce/components'; +import { recordEvent } from '@woocommerce/tracks'; +import { + EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME, + ProductAttributeTerm, + QueryProductAttribute, +} from '@woocommerce/data'; + +/** + * Internal dependencies + */ +import './create-attribute-term-modal.scss'; + +type CreateAttributeTermModalProps = { + initialAttributeTermName: string; + attributeId: number; + onCancel?: () => void; + onCreated?: ( newAttribute: ProductAttributeTerm ) => void; +}; + +export const CreateAttributeTermModal: React.FC< + CreateAttributeTermModalProps +> = ( { + initialAttributeTermName, + attributeId, + onCancel = () => {}, + onCreated = () => {}, +} ) => { + const { createNotice } = useDispatch( 'core/notices' ); + const [ isCreating, setIsCreating ] = useState( false ); + const { createProductAttributeTerm, invalidateResolutionForStoreSelector } = + useDispatch( EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME ); + + const onAdd = async ( attribute: Partial< ProductAttributeTerm > ) => { + recordEvent( 'product_attribute_term_add', { + new_product_page: true, + } ); + setIsCreating( true ); + try { + const newAttribute: ProductAttributeTerm = + await createProductAttributeTerm( { + ...attribute, + attribute_id: attributeId, + } ); + recordEvent( 'product_attribute_term_add_success', { + new_product_page: true, + } ); + invalidateResolutionForStoreSelector( 'getProductAttributes' ); + setIsCreating( false ); + onCreated( newAttribute ); + } catch ( e ) { + recordEvent( 'product_attribute_term_add_failed', { + new_product_page: true, + } ); + createNotice( + 'error', + __( 'Failed to create attribute term.', 'woocommerce' ) + ); + setIsCreating( false ); + onCancel(); + } + }; + + function validateForm( + values: Partial< ProductAttributeTerm > + ): FormErrors< ProductAttributeTerm > { + const errors: FormErrors< ProductAttributeTerm > = {}; + + if ( ! values.name?.length ) { + errors.name = __( + 'The attribute term name is required.', + 'woocommerce' + ); + } + + return errors; + } + + return ( + + | React.MouseEvent< Element > + | React.FocusEvent< Element > + ) => { + event.stopPropagation(); + onCancel(); + } } + className="woocommerce-create-attribute-term-modal" + > + > + initialValues={ { + name: initialAttributeTermName, + slug: cleanForSlug( initialAttributeTermName ), + } } + validate={ validateForm } + errors={ {} } + onSubmit={ onAdd } + > + { ( { + getInputProps, + handleSubmit, + isValidForm, + setValue, + values, + }: FormContext< QueryProductAttribute > ) => { + const nameInputProps = getInputProps< string >( 'name' ); + return ( + <> + { + nameInputProps.onBlur(); + setValue( + 'slug', + cleanForSlug( values.name ) + ); + } } + /> + + +
+ + +
+ + ); + } } + +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/custom-attribute-term-input-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/custom-attribute-term-input-field.tsx new file mode 100644 index 00000000000..8ca43d15916 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/custom-attribute-term-input-field.tsx @@ -0,0 +1,182 @@ +/** + * External dependencies + */ +import { sprintf, __ } from '@wordpress/i18n'; +import { CheckboxControl, Icon } from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { plus } from '@wordpress/icons'; +import { + __experimentalSelectControl as SelectControl, + __experimentalSelectControlMenu as Menu, + __experimentalSelectControlMenuItem as MenuItem, +} from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import './attribute-term-input-field.scss'; + +type CustomAttributeTermInputFieldProps = { + value?: string[]; + onChange: ( value: string[] ) => void; + placeholder?: string; + label?: string; + disabled?: boolean; +}; + +type NewTermItem = { + id: string; + label: string; +}; + +function isNewTermItem( + item: NewTermItem | string | null +): item is NewTermItem { + return item !== null && typeof item === 'object' && !! item.label; +} + +export const CustomAttributeTermInputField: React.FC< + CustomAttributeTermInputFieldProps +> = ( { value = [], onChange, placeholder, disabled, label } ) => { + const [ listItems, setListItems ] = + useState< Array< string | NewTermItem > >( value ); + + const onRemove = ( item: string | NewTermItem ) => { + onChange( value.filter( ( opt ) => opt !== item ) ); + }; + + const onSelect = ( item: string | NewTermItem ) => { + // Add new item. + if ( isNewTermItem( item ) ) { + setListItems( [ ...listItems, item.label ] ); + onChange( [ ...value, item.label ] ); + return; + } + const isSelected = value.includes( item ); + if ( isSelected ) { + onRemove( item ); + return; + } + onChange( [ ...value, item ] ); + }; + + return ( + <> + + items={ listItems } + multiple + disabled={ disabled } + label={ label || '' } + placeholder={ placeholder || '' } + getItemLabel={ ( item ) => + isNewTermItem( item ) ? item.label : item || '' + } + getItemValue={ ( item ) => + isNewTermItem( item ) ? item.id : item || '' + } + getFilteredItems={ ( allItems, inputValue ) => { + const filteredItems = allItems.filter( + ( item ) => + ! inputValue.length || + ( ! isNewTermItem( item ) && + item + .toLowerCase() + .includes( inputValue.toLowerCase() ) ) + ); + if ( + inputValue.length > 0 && + ! filteredItems.find( + ( item ) => + ! isNewTermItem( item ) && + item.toLowerCase() === inputValue.toLowerCase() + ) + ) { + return [ + ...filteredItems, + { + id: 'is-new', + label: inputValue, + }, + ]; + } + return filteredItems; + } } + selected={ value } + onSelect={ onSelect } + onRemove={ onRemove } + className="woocommerce-attribute-term-field" + > + { ( { + items, + highlightedIndex, + getItemProps, + getMenuProps, + isOpen, + } ) => { + return ( + + { items.map( ( item, menuIndex ) => { + return ( + + { isNewTermItem( item ) ? ( +
+ + + { sprintf( + /* translators: The name of the new attribute term to be created */ + __( + 'Create "%s"', + 'woocommerce' + ), + item.label + ) } + +
+ ) : ( + null } + checked={ value.includes( + item + ) } + label={ + + { item } + + } + /> + ) } +
+ ); + } ) } +
+ ); + } } + + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/index.ts b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/index.ts new file mode 100644 index 00000000000..f7a35f4a1e1 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/index.ts @@ -0,0 +1,2 @@ +export * from './attribute-term-input-field'; +export * from './custom-attribute-term-input-field'; diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/test/attribute-term-input-field.spec.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/test/attribute-term-input-field.spec.tsx new file mode 100644 index 00000000000..94e9bae3d38 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/test/attribute-term-input-field.spec.tsx @@ -0,0 +1,208 @@ +/** + * External dependencies + */ +import { act, render, waitFor, screen } from '@testing-library/react'; +import { useState } from '@wordpress/element'; +import { resolveSelect } from '@wordpress/data'; +import { ProductAttribute, ProductAttributeTerm } from '@woocommerce/data'; + +/** + * Internal dependencies + */ +import { AttributeTermInputField } from '../attribute-term-input-field'; + +jest.mock( '@wordpress/data', () => ( { + ...jest.requireActual( '@wordpress/data' ), + resolveSelect: jest.fn(), +} ) ); + +jest.mock( '@wordpress/components', () => { + return { + __esModule: true, + Spinner: () =>
spinner
, + }; +} ); + +jest.mock( '@woocommerce/components', () => { + return { + __esModule: true, + __experimentalSelectControlMenu: ( { + children, + }: { + children: JSX.Element; + } ) => children, + __experimentalSelectControlMenuItem: ( { + children, + }: { + children: JSX.Element; + } ) =>
{ children }
, + __experimentalSelectControl: ( { + children, + items, + getFilteredItems, + }: { + children: ( options: { + isOpen: boolean; + items: ProductAttributeTerm[]; + getMenuProps: () => Record< string, string >; + getItemProps: () => Record< string, string >; + } ) => JSX.Element; + items: ProductAttributeTerm[]; + getFilteredItems: ( + allItems: ProductAttributeTerm[], + inputValue: string, + selectedItems: ProductAttributeTerm[] + ) => ProductAttributeTerm[]; + } ) => { + const [ input, setInput ] = useState( '' ); + return ( +
+ attribute_input_field + +
+ { children( { + isOpen: true, + items: getFilteredItems( items, input, [] ), + getMenuProps: () => ( {} ), + getItemProps: () => ( {} ), + } ) } +
+
+ ); + }, + }; +} ); + +const attributeList: ProductAttribute[] = [ + { + id: 15, + name: 'Automotive', + position: 0, + visible: true, + variation: false, + options: [ 'test' ], + }, + { + id: 1, + name: 'Color', + position: 2, + visible: true, + variation: true, + options: [ + 'Beige', + 'black', + 'Blue', + 'brown', + 'Gray', + 'Green', + 'mint', + 'orange', + 'pink', + 'Red', + 'white', + 'Yellow', + ], + }, +]; + +const attributeTermList: ProductAttributeTerm[] = [ + { + id: 23, + name: 'XXS', + slug: 'xxs', + description: '', + menu_order: 1, + count: 1, + }, + { + id: 22, + name: 'XS', + slug: 'xs', + description: '', + menu_order: 2, + count: 1, + }, + { + id: 17, + name: 'S', + slug: 's', + description: '', + menu_order: 3, + count: 1, + }, + { + id: 18, + name: 'M', + slug: 'm', + description: '', + menu_order: 4, + count: 1, + }, + { + id: 19, + name: 'L', + slug: 'l', + description: '', + menu_order: 5, + count: 1, + }, +]; + +describe( 'AttributeTermInputField', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should not trigger resolveSelect if attributeId is not defined', () => { + render( ); + expect( resolveSelect ).not.toHaveBeenCalled(); + } ); + + it( 'should not trigger resolveSelect if attributeId is defined but field disabled', () => { + render( + + ); + expect( resolveSelect ).not.toHaveBeenCalled(); + } ); + + it( 'should trigger resolveSelect if attributeId is defined and field not disabled', () => { + const getProductAttributesMock = jest.fn().mockResolvedValue( [] ); + ( resolveSelect as jest.Mock ).mockReturnValue( { + getProductAttributeTerms: getProductAttributesMock, + } ); + render( + + ); + expect( getProductAttributesMock ).toHaveBeenCalledWith( { + search: '', + attribute_id: 2, + } ); + } ); + + it( 'should render spinner while retrieving products', async () => { + const getProductAttributesMock = jest + .fn() + .mockReturnValue( { then: () => {} } ); + ( resolveSelect as jest.Mock ).mockReturnValue( { + getProductAttributeTerms: getProductAttributesMock, + } ); + await act( async () => { + render( + + ); + } ); + // debug(); + await waitFor( () => { + expect( screen.queryByText( 'spinner' ) ).toBeInTheDocument(); + } ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/products/fields/category-field/category-field.scss b/plugins/woocommerce-admin/client/products/fields/category-field/category-field.scss index 09eb2478645..a80d03fe3eb 100644 --- a/plugins/woocommerce-admin/client/products/fields/category-field/category-field.scss +++ b/plugins/woocommerce-admin/client/products/fields/category-field/category-field.scss @@ -58,9 +58,6 @@ } .woocommerce-experimental-select-control { - &__input { - height: 30px; - } &__combox-box-icon { box-sizing: unset; } diff --git a/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss b/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss index 2e49589b949..10288998954 100644 --- a/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss +++ b/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss @@ -2,14 +2,17 @@ max-width: 1032px; margin: 0 auto; - h4 { + h4, + .components-radio-control .components-base-control__label { font-size: 14px; font-weight: 600; margin-bottom: 18px; margin-top: 26px; + text-transform: none; } - .components-card__body h4:first-child { + .components-card__body h4:first-child, + .components-radio-control:first-child .components-base-control__label { margin-top: 0; } diff --git a/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss b/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss index 0759fa9922a..15b02e61f1f 100644 --- a/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss +++ b/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss @@ -13,12 +13,29 @@ } } - .components-base-control { - &:not(:first-child) { + .components-base-control, + .woocommerce-rich-text-editor { + &:not(:first-child):not(.components-radio-control) { margin-top: $gap-large - $gap-smaller; margin-bottom: 0; } } + + .woocommerce-product-form__field { + margin-top: $gap-large; + + > .components-base-control { + margin-bottom: 0; + } + } + + .components-radio-control .components-v-stack { + gap: $gap-small; + } + + .woocommerce-collapsible-content { + margin-top: $gap-large; + } } &__header { diff --git a/plugins/woocommerce-admin/client/products/product-form-actions.scss b/plugins/woocommerce-admin/client/products/product-form-actions.scss index 2a7f384dd8c..47d6ee040e7 100644 --- a/plugins/woocommerce-admin/client/products/product-form-actions.scss +++ b/plugins/woocommerce-admin/client/products/product-form-actions.scss @@ -1,11 +1,12 @@ -$gutenberg-blue: #007cba; -$gutenberg-blue-darker: #0063a1; +$gutenberg-blue: var(--wp-admin-theme-color); +$gutenberg-blue-darker: var(--wp-admin-theme-color-darker-20); .woocommerce-product-form-actions { display: flex; flex-direction: row; align-items: center; justify-content: flex-end; + padding-right: var(--large-gap); > .components-button { margin-right: $gap-smaller; diff --git a/plugins/woocommerce-admin/client/products/product-form-actions.tsx b/plugins/woocommerce-admin/client/products/product-form-actions.tsx index 1e53fdab129..ac0507aee39 100644 --- a/plugins/woocommerce-admin/client/products/product-form-actions.tsx +++ b/plugins/woocommerce-admin/client/products/product-form-actions.tsx @@ -10,6 +10,7 @@ import { MenuItem, } from '@wordpress/components'; import { chevronDown, check, Icon } from '@wordpress/icons'; +import { registerPlugin } from '@wordpress/plugins'; import { useFormContext } from '@woocommerce/components'; import { Product } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; @@ -17,8 +18,9 @@ import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies */ -import './product-form-actions.scss'; +import { WooHeaderItem } from '~/header/utils'; import { useProductHelper } from './use-product-helper'; +import './product-form-actions.scss'; export const ProductFormActions: React.FC = () => { const { @@ -122,120 +124,139 @@ export const ProductFormActions: React.FC = () => { const isPublished = values.id && values.status === 'publish'; return ( -
- - - - - - { () => ( - <> - - - { isPublished - ? __( - 'Update & duplicate', + + { () => ( +
+ + + + + + { () => ( + <> + + + { isPublished + ? __( + 'Update & duplicate', + 'woocommerce' + ) + : __( + 'Publish & duplicate', + 'woocommerce' + ) } + + + { __( + 'Copy to a new draft', 'woocommerce' - ) - : __( - 'Publish & duplicate', - 'woocommerce' - ) } - - - { __( - 'Copy to a new draft', - 'woocommerce' - ) } - - { values.id && ( - - { __( 'Move to trash', 'woocommerce' ) } - - ) } - - - ) } - - -
+ ) } +
+ { values.id && ( + + { __( + 'Move to trash', + 'woocommerce' + ) } + + ) } +
+ + ) } +
+
+
+ ) } + ); }; + +registerPlugin( 'action-buttons-header-item', { + render: ProductFormActions, + icon: 'admin-generic', +} ); diff --git a/plugins/woocommerce-admin/client/products/product-page.scss b/plugins/woocommerce-admin/client/products/product-page.scss index 6c8f9adeb0f..dae149dad32 100644 --- a/plugins/woocommerce-admin/client/products/product-page.scss +++ b/plugins/woocommerce-admin/client/products/product-page.scss @@ -3,7 +3,7 @@ .woocommerce-product-form-actions { margin-top: $gap-largest + $gap-smaller; } - .woocommerce-product__checkbox, + .components-checkbox-control, .components-toggle-control { & > * { margin-bottom: 0; @@ -17,6 +17,23 @@ margin-right: $gap-smaller; } } + .components-checkbox-control { + &__label { + display: flex; + align-items: center; + } + + &__input-container { + align-self: center; + } + + .components-base-control__field { + display: flex; + } + } + .woocommerce-tooltip { + margin-left: $gap-smaller; + } .woocommerce-product-form { &__custom-label-input { display: flex; @@ -41,9 +58,28 @@ margin-bottom: 0; } } - .woocommerce-enriched-label__help-wrapper { - .components-popover { - margin-top: 0; + + // This is needed because Gutenberg disables tooltip events on disabled elements. + // We are explicitly using this on a disabled item so this overlay prevents + // the tooltip from seeing the disabled property and allows mouse events to occur. + // See https://github.com/WordPress/gutenberg/blob/411b6eee8376e31bf9db4c15c92a80524ae38e9b/packages/components/src/tooltip/index.js#L99-L102 + .woocommerce-product-form__tooltip-disabled-overlay { + position: relative; + display: inline-block; + &::after { + content: ''; + display: inline-block; + position: absolute; + width: 100%; + height: 100%; + background: transparent; + left: 0; + top: 0; + cursor: default; + } + + .components-base-control { + margin-bottom: 0; } } } diff --git a/plugins/woocommerce-admin/client/products/sections/attributes-section.tsx b/plugins/woocommerce-admin/client/products/sections/attributes-section.tsx index cc569e491e3..e2141df7e9c 100644 --- a/plugins/woocommerce-admin/client/products/sections/attributes-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/attributes-section.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { Card, CardBody } from '@wordpress/components'; import { Link, useFormContext } from '@woocommerce/components'; import { Product } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; @@ -15,7 +14,10 @@ import { ProductSectionLayout } from '../layout/product-section-layout'; import { AttributeField } from '../fields/attribute-field'; export const AttributesSection: React.FC = () => { - const { getInputProps } = useFormContext< Product >(); + const { + getInputProps, + values: { id: productId }, + } = useFormContext< Product >(); return ( { } > - - - - - + ); }; diff --git a/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx b/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx index 956efb3e7b2..d5f530eb2f8 100644 --- a/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/pricing-section.tsx @@ -20,8 +20,8 @@ import { * Internal dependencies */ import './pricing-section.scss'; +import { formatCurrencyDisplayValue, getCurrencySymbolProps } from './utils'; import { ProductSectionLayout } from '../layout/product-section-layout'; -import { getInputControlProps } from './utils'; import { ADMIN_URL } from '../../utils/admin-settings'; import { CurrencyContext } from '../../lib/currency-context'; import { useProductHelper } from '../use-product-helper'; @@ -44,6 +44,8 @@ export const PricingSection: React.FC = () => { const pricesIncludeTax = taxSettings.woocommerce_prices_include_tax === 'yes'; const context = useContext( CurrencyContext ); + const { getCurrencyConfig, formatAmount } = context; + const currencyConfig = getCurrencyConfig(); const taxIncludedInPriceText = __( 'Per your {{link}}store settings{{/link}}, tax is {{strong}}included{{/strong}} in the price.', @@ -77,24 +79,17 @@ export const PricingSection: React.FC = () => { }, } ); - const salePriceTitle = interpolateComponents( { - mixedString: __( - 'Sale price {{span}}(optional){{/span}}', - 'woocommerce' - ), - components: { - span: , + const currencyInputProps = { + ...getCurrencySymbolProps( currencyConfig ), + sanitize: ( value: Product[ keyof Product ] ) => { + return sanitizePrice( String( value ) ); }, - } ); - - const regularPriceProps = getInputControlProps( { - ...getInputProps( 'regular_price' ), - context, - } ); - const salePriceProps = getInputControlProps( { - ...getInputProps( 'sale_price' ), - context, - } ); + }; + const regularPriceProps = getInputProps( + 'regular_price', + currencyInputProps + ); + const salePriceProps = getInputProps( 'sale_price', currencyInputProps ); return ( { { - const sanitizedValue = sanitizePrice( value ); - regularPriceProps?.onChange( sanitizedValue ); - } } + value={ formatCurrencyDisplayValue( + String( regularPriceProps?.value ), + currencyConfig, + formatAmount + ) } /> { ! isTaxSettingsResolving && ( @@ -154,12 +149,12 @@ export const PricingSection: React.FC = () => { > { - const sanitizedValue = sanitizePrice( value ); - salePriceProps?.onChange( sanitizedValue ); - } } + label={ __( 'Sale price', 'woocommerce' ) } + value={ formatCurrencyDisplayValue( + String( salePriceProps?.value ), + currencyConfig, + formatAmount + ) } /> diff --git a/plugins/woocommerce-admin/client/products/sections/product-details-section.scss b/plugins/woocommerce-admin/client/products/sections/product-details-section.scss index f276a13c49b..d5f37a1907f 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-details-section.scss +++ b/plugins/woocommerce-admin/client/products/sections/product-details-section.scss @@ -12,23 +12,4 @@ margin-left: $gap-smaller; } } - - &__feature-checkbox { - .components-base-control__field { - display: flex; - .components-checkbox-control { - &__label { - display: flex; - } - - &__input-container { - align-self: center; - } - } - .woocommerce-enriched-label__text { - align-self: center; - margin-right: $gap-smaller; - } - } - } } diff --git a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx index 43fffdc0f4f..b452d8d3478 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx @@ -12,7 +12,13 @@ import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; import { cleanForSlug } from '@wordpress/url'; -import { EnrichedLabel, useFormContext } from '@woocommerce/components'; +import { + Link, + useFormContext, + __experimentalRichTextEditor as RichTextEditor, + __experimentalTooltip as Tooltip, +} from '@woocommerce/components'; +import interpolateComponents from '@automattic/interpolate-components'; import { Product, ProductCategory, @@ -20,23 +26,36 @@ import { WCDataSelector, } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; +import { BlockInstance, serialize, parse } from '@wordpress/blocks'; /** * Internal dependencies */ import './product-details-section.scss'; -import { getCheckboxProps, getTextControlProps } from './utils'; -import { ProductSectionLayout } from '../layout/product-section-layout'; -import { EditProductLinkModal } from '../shared/edit-product-link-modal'; import { CategoryField } from '../fields/category-field'; +import { EditProductLinkModal } from '../shared/edit-product-link-modal'; +import { getCheckboxTracks } from './utils'; +import { ProductSectionLayout } from '../layout/product-section-layout'; const PRODUCT_DETAILS_SLUG = 'product-details'; export const ProductDetailsSection: React.FC = () => { - const { getInputProps, values, touched, errors, setValue } = - useFormContext< Product >(); + const { + getCheckboxControlProps, + getInputProps, + values, + touched, + errors, + setValue, + } = useFormContext< Product >(); const [ showProductLinkEditModal, setShowProductLinkEditModal ] = useState( false ); + const [ descriptionBlocks, setDescriptionBlocks ] = useState< + BlockInstance[] + >( parse( values.description || '' ) ); + const [ summaryBlocks, setSummaryBlocks ] = useState< BlockInstance[] >( + parse( values.short_description || '' ) + ); const { permalinkPrefix, permalinkSuffix } = useSelect( ( select: WCDataSelector ) => { const { getPermalinkParts } = select( PRODUCTS_STORE_NAME ); @@ -56,7 +75,7 @@ export const ProductDetailsSection: React.FC = () => { }; const setSkuIfEmpty = () => { - if ( values.sku || ! values.name.length ) { + if ( values.sku || ! values.name?.length ) { return; } setValue( 'sku', cleanForSlug( values.name ) ); @@ -64,7 +83,7 @@ export const ProductDetailsSection: React.FC = () => { return ( {
+ { __( + '(required)', + 'woocommerce' + ) } + + ), + }, + } ) } name={ `${ PRODUCT_DETAILS_SLUG }-name` } placeholder={ __( 'e.g. 12 oz Coffee Mug', 'woocommerce' ) } - { ...getTextControlProps( - getInputProps( 'name' ) - ) } - onBlur={ () => { - setSkuIfEmpty(); - getInputProps( 'name' ).onBlur(); - } } + { ...getInputProps( 'name', { + onBlur: setSkuIfEmpty, + } ) } /> { values.id && ! hasNameError() && permalinkPrefix && ( @@ -125,29 +155,45 @@ export const ProductDetailsSection: React.FC = () => { /> - recordEvent( 'add_product_learn_more', { - category: PRODUCT_DETAILS_SLUG, - } ) - } - /> + <> + { __( 'Feature this product', 'woocommerce' ) } + + recordEvent( + 'add_product_learn_more', + { + category: + PRODUCT_DETAILS_SLUG, + } + ) + } + > + { __( + 'Learn more', + 'woocommerce' + ) } + + ), + }, + } ) } + /> + } - { ...getCheckboxProps( { - ...getInputProps( 'featured' ), - name: 'featured', - className: - 'product-details-section__feature-checkbox', - } ) } + { ...getCheckboxControlProps( + 'featured', + getCheckboxTracks( 'featured' ) + ) } /> { showProductLinkEditModal && ( { } /> ) } + { + setSummaryBlocks( blocks ); + setValue( + 'short_description', + serialize( blocks ) + ); + } } + /> + { + setDescriptionBlocks( blocks ); + setValue( 'description', serialize( blocks ) ); + } } + placeholder={ __( + 'Describe this product. What makes it unique? What are its most important features?', + 'woocommerce' + ) } + /> diff --git a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/advanced-stock-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/advanced-stock-section.tsx new file mode 100644 index 00000000000..957d31a84b8 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/advanced-stock-section.tsx @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { CheckboxControl, RadioControl } from '@wordpress/components'; +import { Product } from '@woocommerce/data'; +import { useFormContext } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import { getCheckboxTracks } from '../utils'; + +export const AdvancedStockSection: React.FC = () => { + const { getCheckboxControlProps, getInputProps, values } = + useFormContext< Product >(); + + const backordersProp = getInputProps( 'backorders' ); + // These properties cause issues with the RadioControl component. + // A fix to form upstream would help if we can identify what type of input is used. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete backordersProp.checked; + delete backordersProp.value; + + return ( + <> + { values.manage_stock && ( + + ) } +

{ __( 'Restrictions', 'woocommerce' ) }

+ + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/manage-stock-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/manage-stock-section.tsx index e4130e3f3ed..47391bf291f 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/manage-stock-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/manage-stock-section.tsx @@ -13,7 +13,6 @@ import { recordEvent } from '@woocommerce/tracks'; * Internal dependencies */ import { getAdminSetting } from '~/utils/admin-settings'; -import { getTextControlProps } from '../utils'; export const ManageStockSection: React.FC = () => { const { getInputProps } = useFormContext< Product >(); @@ -25,9 +24,7 @@ export const ManageStockSection: React.FC = () => { { __( '%d (store default)', 'woocommerce' ), notifyLowStockAmount ) } - { ...getTextControlProps( { - ...getInputProps( 'low_stock_amount' ), - } ) } + { ...getInputProps( 'low_stock_amount' ) } min={ 0 } /> diff --git a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/manual-stock-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/manual-stock-section.tsx new file mode 100644 index 00000000000..f815f8ef5c3 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/manual-stock-section.tsx @@ -0,0 +1,39 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { RadioControl } from '@wordpress/components'; +import { useFormContext } from '@woocommerce/components'; +import { Product } from '@woocommerce/data'; + +export const ManualStockSection: React.FC = () => { + const { getInputProps } = useFormContext< Product >(); + const inputProps = getInputProps( 'stock_status' ); + // These properties cause issues with the RadioControl component. + // A fix to form upstream would help if we can identify what type of input is used. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete inputProps.checked; + delete inputProps.value; + + return ( + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx index 1ab01f095c2..a7d2c01b5a1 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/product-inventory-section.tsx @@ -2,27 +2,36 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; +import { + CollapsibleContent, + __experimentalConditionalWrapper as ConditionalWrapper, + Link, + useFormContext, +} from '@woocommerce/components'; import { Card, CardBody, ToggleControl, TextControl, + Tooltip, } from '@wordpress/components'; import { getAdminLink } from '@woocommerce/settings'; -import { Link, useFormContext } from '@woocommerce/components'; import { Product } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies */ -import { getCheckboxProps, getTextControlProps } from '../utils'; +import { AdvancedStockSection } from './advanced-stock-section'; +import { getCheckboxTracks } from '../utils'; import { getAdminSetting } from '~/utils/admin-settings'; import { ProductSectionLayout } from '../../layout/product-section-layout'; import { ManageStockSection } from './manage-stock-section'; +import { ManualStockSection } from './manual-stock-section'; export const ProductInventorySection: React.FC = () => { - const { getInputProps, values } = useFormContext< Product >(); + const { getCheckboxControlProps, getInputProps, values } = + useFormContext< Product >(); const canManageStock = getAdminSetting( 'manageStock', 'yes' ) === 'yes'; return ( @@ -62,26 +71,50 @@ export const ProductInventorySection: React.FC = () => { 'SKU (Stock Keeping Unit)', 'woocommerce' ) } - placeholder={ __( - 'washed-oxford-button-down-shirt', - 'woocommerce' - ) } - { ...getTextControlProps( getInputProps( 'sku' ) ) } + { ...getInputProps( 'sku' ) } /> - { canManageStock && ( - <> +
+ ( + +
+ { children } +
+
+ ) } + > - { values.manage_stock && } - +
+
+ { values.manage_stock ? ( + + ) : ( + ) } + + + diff --git a/plugins/woocommerce-admin/client/products/sections/product-inventory-section/test/product-inventory-section.spec.tsx b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/test/product-inventory-section.spec.tsx new file mode 100644 index 00000000000..a3c80a454b5 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/sections/product-inventory-section/test/product-inventory-section.spec.tsx @@ -0,0 +1,176 @@ +/** + * External dependencies + */ +import { Form } from '@woocommerce/components'; +import { Product } from '@woocommerce/data'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +/** + * Internal dependencies + */ +import { getAdminSetting } from '~/utils/admin-settings'; +import { ProductInventorySection } from '../'; + +jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) ); +jest.mock( '~/utils/admin-settings', () => ( { + getAdminSetting: jest.fn(), +} ) ); + +describe( 'ProductInventorySection', () => { + beforeEach( () => { + jest.clearAllMocks(); + ( getAdminSetting as jest.Mock ).mockImplementation( + ( key, value = false ) => { + const values = { + manageStock: 'yes', + notifyLowStockAmount: 5, + }; + if ( values.hasOwnProperty( key ) ) { + return values[ key as keyof typeof values ]; + } + return value; + } + ); + } ); + + const product: Partial< Product > = { + id: 1, + name: 'Lorem', + slug: 'lorem', + manage_stock: false, + }; + + it( 'should render the sku field', () => { + render( +
+ + + ); + + expect( + screen.getByLabelText( 'SKU (Stock Keeping Unit)' ) + ).toBeInTheDocument(); + } ); + + it( 'should disable the manage stock section if inventory management is turned off', () => { + ( getAdminSetting as jest.Mock ).mockImplementation( ( key, value ) => { + const values = { + manageStock: 'no', + }; + if ( values.hasOwnProperty( key ) ) { + return values[ key as keyof typeof values ]; + } + return value; + } ); + + render( +
+ + + ); + + expect( + screen.getByText( 'Track quantity for this product' ) + .previousSibling + ).toHaveClass( 'is-disabled' ); + } ); + + it( 'should not disable the manage stock section if inventory management is turned on', () => { + render( +
+ + + ); + + expect( + screen.getByText( 'Track quantity for this product' ) + .previousSibling + ).not.toHaveClass( 'is-disabled' ); + } ); + + it( 'should render the quantity field when product stock is being managed', () => { + render( +
+ + + ); + + expect( + screen.getByLabelText( 'Current quantity' ) + ).toBeInTheDocument(); + } ); + + it( 'should not render the quantity field when product stock is not being managed', () => { + render( +
+ + + ); + + expect( + screen.queryByLabelText( 'Current quantity' ) + ).not.toBeInTheDocument(); + } ); + + it( 'should render the default low stock amount in placeholder', () => { + render( +
+ + + ); + + expect( + screen.getByPlaceholderText( '5 (store default)' ) + ).toBeInTheDocument(); + } ); + + it( 'should not render the advanced section until clicked', () => { + render( +
+ + + ); + + expect( + screen.queryByLabelText( 'Limit purchases to 1 item per order' ) + ).not.toBeInTheDocument(); + } ); + + it( 'should render the advanced section after clicked', () => { + render( +
+ + + ); + + userEvent.click( screen.getByText( 'Advanced' ) ); + expect( + screen.getByLabelText( 'Limit purchases to 1 item per order' ) + ).toBeInTheDocument(); + } ); + + it( 'should not allow backorder settings when not managing stock', () => { + render( +
+ + + ); + + userEvent.click( screen.getByText( 'Advanced' ) ); + expect( + screen.queryByText( 'When out of stock' ) + ).not.toBeInTheDocument(); + } ); + + it( 'should allow backorder settings when managing stock', () => { + render( +
+ + + ); + + userEvent.click( screen.getByText( 'Advanced' ) ); + expect( screen.queryByText( 'When out of stock' ) ).toBeInTheDocument(); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.scss b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.scss index 08c59941265..58261f87ab1 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.scss +++ b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.scss @@ -15,7 +15,7 @@ &__dimensions { &-body { display: grid; - gap: $gap-large; + gap: $gap-largest; grid-template-columns: 1fr; @media ( min-width: #{ ($break-medium) } ) { grid-template-columns: 1fr 1fr; @@ -28,15 +28,14 @@ } } } + &-image { + width: 100%; + height: 100%; + } .product-shipping-section__spinner-wrapper { @media ( min-width: #{ ($break-medium) } ) { min-height: 326px; } } } - &-image { - @media ( min-width: #{ ($break-medium) } ) { - width: 100%; - } - } } diff --git a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx index 2f57386860c..679d6e433e9 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx @@ -32,17 +32,21 @@ import { } from '../fields/shipping-dimensions-image'; import { useProductHelper } from '../use-product-helper'; import { AddNewShippingClassModal } from '../shared/add-new-shipping-class-modal'; -import { getTextControlProps } from './utils'; import './product-shipping-section.scss'; +import { + ADD_NEW_SHIPPING_CLASS_OPTION_VALUE, + UNCATEGORIZED_CATEGORY_SLUG, +} from '../constants'; export type ProductShippingSectionProps = { product?: PartialProduct; }; -// This should never be a real slug value of any existing shipping class -const ADD_NEW_SHIPPING_CLASS_OPTION_VALUE = '__ADD_NEW_SHIPPING_CLASS_OPTION__'; +type ServerErrorResponse = { + code: string; +}; -const DEFAULT_SHIPPING_CLASS_OPTIONS: SelectControl.Option[] = [ +export const DEFAULT_SHIPPING_CLASS_OPTIONS: SelectControl.Option[] = [ { value: '', label: __( 'No shipping class', 'woocommerce' ) }, { value: ADD_NEW_SHIPPING_CLASS_OPTION_VALUE, @@ -50,8 +54,6 @@ const DEFAULT_SHIPPING_CLASS_OPTIONS: SelectControl.Option[] = [ }, ]; -const UNCATEGORIZED_CATEGORY_SLUG = 'uncategorized'; - function mapShippingClassToSelectOption( shippingClasses: ProductShippingClass[] ): SelectControl.Option[] { @@ -72,19 +74,26 @@ function getInterpolatedSizeLabel( mixedString: string ) { /** * This extracts a shipping class from the product categories. Using - * the first category different to `Uncategorized`. + * the first category different to `Uncategorized` and check if the + * category was not added to the shipping class list * * @see https://github.com/woocommerce/woocommerce/issues/34657 + * @see https://github.com/woocommerce/woocommerce/issues/35037 * @param product The product + * @param shippingClasses The shipping classes * @return The default shipping class */ function extractDefaultShippingClassFromProduct( - product: PartialProduct + product?: PartialProduct, + shippingClasses?: ProductShippingClass[] ): Partial< ProductShippingClass > | undefined { const category = product?.categories?.find( ( { slug } ) => slug !== UNCATEGORIZED_CATEGORY_SLUG ); - if ( category ) { + if ( + category && + ! shippingClasses?.some( ( { slug } ) => slug === category.slug ) + ) { return { name: category.name, slug: category.slug, @@ -95,7 +104,8 @@ function extractDefaultShippingClassFromProduct( export function ProductShippingSection( { product, }: ProductShippingSectionProps ) { - const { getInputProps } = useFormContext< PartialProduct >(); + const { getInputProps, getSelectControlProps, setValue } = + useFormContext< PartialProduct >(); const { formatNumber, parseNumber } = useProductHelper(); const [ highlightSide, setHighlightSide ] = useState< ShippingDimensionsImageProps[ 'highlight' ] >(); @@ -140,20 +150,53 @@ export function ProductShippingSection( { const { createProductShippingClass, invalidateResolution } = useDispatch( EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME ); + const { createErrorNotice } = useDispatch( 'core/notices' ); - const selectShippingClassProps = getTextControlProps( - getInputProps( 'shipping_class' ) + const dimensionProps = { + onBlur: () => { + setHighlightSide( undefined ); + }, + sanitize: ( value: PartialProduct[ keyof PartialProduct ] ) => + parseNumber( String( value ) ), + suffix: dimensionUnit, + }; + + const inputWidthProps = getInputProps( 'dimensions.width', dimensionProps ); + const inputLengthProps = getInputProps( + 'dimensions.length', + dimensionProps ); - const inputWidthProps = getTextControlProps( - getInputProps( 'dimensions.width' ) + const inputHeightProps = getInputProps( + 'dimensions.height', + dimensionProps ); - const inputLengthProps = getTextControlProps( - getInputProps( 'dimensions.length' ) - ); - const inputHeightProps = getTextControlProps( - getInputProps( 'dimensions.height' ) - ); - const inputWeightProps = getTextControlProps( getInputProps( 'weight' ) ); + const inputWeightProps = getInputProps( 'weight', { + sanitize: ( value: PartialProduct[ keyof PartialProduct ] ) => + parseNumber( String( value ) ), + } ); + const shippingClassProps = getInputProps( 'shipping_class' ); + + function handleShippingClassServerError( + error: ServerErrorResponse + ): Promise< ProductShippingClass > { + let message = __( + 'We couldn’t add this shipping class. Try again in a few seconds.', + 'woocommerce' + ); + + if ( error.code === 'term_exists' ) { + message = __( + 'A shipping class with that slug already exists.', + 'woocommerce' + ); + } + + createErrorNotice( message, { + explicitDismiss: true, + } ); + + throw error; + } return ( { if ( value === @@ -184,8 +221,14 @@ export function ProductShippingSection( { setShowShippingClassModal( true ); return; } - selectShippingClassProps?.onChange( value ); + shippingClassProps.onChange( value ); } } + options={ [ + ...DEFAULT_SHIPPING_CLASS_OPTIONS, + ...mapShippingClassToSelectOption( + shippingClasses ?? [] + ), + ] } /> { interpolateComponents( { @@ -236,7 +279,7 @@ export function ProductShippingSection( { - inputWidthProps?.onChange( - parseNumber( value ) - ) - } onFocus={ () => { setHighlightSide( 'A' ); } } - onBlur={ () => { - setHighlightSide( undefined ); - inputWidthProps?.onBlur(); - } } - suffix={ dimensionUnit } /> @@ -268,7 +301,7 @@ export function ProductShippingSection( { - inputLengthProps?.onChange( - parseNumber( value ) - ) - } onFocus={ () => { setHighlightSide( 'B' ); } } - onBlur={ () => { - setHighlightSide( undefined ); - inputLengthProps?.onBlur(); - } } - suffix={ dimensionUnit } /> @@ -300,7 +323,7 @@ export function ProductShippingSection( { - inputHeightProps?.onChange( - parseNumber( value ) - ) - } onFocus={ () => { setHighlightSide( 'C' ); } } - onBlur={ () => { - setHighlightSide( undefined ); - inputHeightProps?.onBlur(); - } } - suffix={ dimensionUnit } /> @@ -332,17 +345,12 @@ export function ProductShippingSection( { - inputWeightProps?.onChange( - parseNumber( value ) - ) - } suffix={ weightUnit } /> @@ -365,17 +373,22 @@ export function ProductShippingSection( { { showShippingClassModal && ( + shippingClass={ extractDefaultShippingClassFromProduct( + product, + shippingClasses + ) } + onAdd={ ( shippingClassValues ) => createProductShippingClass< Promise< ProductShippingClass > - >( values ).then( ( value ) => { - invalidateResolution( 'getProductShippingClasses' ); - return value; - } ) + >( shippingClassValues ) + .then( ( value ) => { + invalidateResolution( + 'getProductShippingClasses' + ); + setValue( 'shipping_class', value.slug ); + return value; + } ) + .catch( handleShippingClassServerError ) } onCancel={ () => setShowShippingClassModal( false ) } /> diff --git a/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx b/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx index 77ec61bf9c7..fed5d6a532a 100644 --- a/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx +++ b/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx @@ -1,11 +1,20 @@ /** * External dependencies */ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { useSelect } from '@wordpress/data'; +import { createRegistry, RegistryProvider, useSelect } from '@wordpress/data'; import { Form } from '@woocommerce/components'; import { Product } from '@woocommerce/data'; +import { render, screen } from '@testing-library/react'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { store as blockEditorStore } from '@wordpress/block-editor'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { store as coreDataStore } from '@wordpress/core-data'; +// eslint-disable-next-line @woocommerce/dependency-group +import userEvent from '@testing-library/user-event'; /** * Internal dependencies @@ -19,6 +28,14 @@ jest.mock( '@wordpress/data', () => ( { useSelect: jest.fn(), } ) ); +const registry = createRegistry(); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +registry.register( coreDataStore ); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +registry.register( blockEditorStore ); + describe( 'ProductDetailsSection', () => { const useSelectMock = useSelect as jest.Mock; @@ -43,9 +60,11 @@ describe( 'ProductDetailsSection', () => { it( 'should render the product link', () => { render( -
- - + +
+ + +
); expect( screen.queryByText( linkUrl ) ).toBeInTheDocument(); @@ -53,11 +72,15 @@ describe( 'ProductDetailsSection', () => { it( 'should hide the product link if field name has errors', () => { render( -
- - + +
+ + +
+ ); + userEvent.clear( + screen.getByLabelText( 'Name', { exact: false } ) ); - userEvent.clear( screen.getByLabelText( 'Name' ) ); userEvent.tab(); expect( screen.queryByText( linkUrl ) ).not.toBeInTheDocument(); diff --git a/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx b/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx new file mode 100644 index 00000000000..9818765b7c0 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/sections/test/product-shipping-section.spec.tsx @@ -0,0 +1,204 @@ +/** + * External dependencies + */ +import { act, render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { PartialProduct, ProductShippingClass } from '@woocommerce/data'; +import { __ } from '@wordpress/i18n'; +import { Form } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import { ProductShippingSection } from '../product-shipping-section'; +import { validate } from '../../product-validation'; +import { ADD_NEW_SHIPPING_CLASS_OPTION_VALUE } from '~/products/constants'; + +jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) ); +jest.mock( '@wordpress/data', () => ( { + ...jest.requireActual( '@wordpress/data' ), + useSelect: jest.fn(), + useDispatch: jest.fn(), +} ) ); + +function getShippingClassDialog() { + return screen.getByRole( 'dialog' ); +} + +function getShippingClassSelect() { + return screen.getByLabelText( __( 'Shipping class', 'woocommerce' ) ); +} + +async function getShippingClassNameInput() { + const dialog = getShippingClassDialog(); + return within( dialog ).getByLabelText( 'Name', { exact: false } ); +} + +async function getShippingClassSlugInput() { + const dialog = getShippingClassDialog(); + return within( dialog ).getByLabelText( __( 'Slug', 'woocommerce' ) ); +} + +async function openShippingClassDialog() { + const select = getShippingClassSelect(); + + await act( async () => + userEvent.selectOptions( select, ADD_NEW_SHIPPING_CLASS_OPTION_VALUE ) + ); + + return getShippingClassDialog(); +} + +async function submitShippingClassDialog() { + const dialog = getShippingClassDialog(); + const buttonAdd = within( dialog ).getByText( __( 'Add', 'woocommerce' ) ); + await act( async () => userEvent.click( buttonAdd ) ); +} + +async function addNewShippingClass( name?: string, slug?: string ) { + await openShippingClassDialog(); + + if ( name ) { + const inputName = await getShippingClassNameInput(); + userEvent.type( inputName, name ); + } + + if ( slug ) { + const inputSlug = await getShippingClassSlugInput(); + userEvent.type( inputSlug, slug ); + } + + await submitShippingClassDialog(); +} + +describe( 'ProductShippingSection', () => { + const useSelectMock = useSelect as jest.Mock; + const useDispatchMock = useDispatch as jest.Mock; + const createProductShippingClass = jest.fn(); + const invalidateResolution = jest.fn(); + const createErrorNotice = jest.fn(); + let shippingClasses: Partial< ProductShippingClass >[]; + + beforeEach( () => { + shippingClasses = []; + + useSelectMock.mockReturnValue( { + shippingClasses, + hasResolvedShippingClasses: true, + } ); + + useDispatchMock.mockReturnValue( { + createProductShippingClass, + invalidateResolution, + createErrorNotice, + } ); + } ); + + afterEach( () => { + jest.clearAllMocks(); + } ); + + describe( 'when creating a product', () => { + beforeEach( () => { + render( +
+ + + ); + } ); + + describe( 'when creating a shipping class', () => { + const newShippingClass = { + name: 'New shipping class', + slug: 'new-shipping-class', + }; + + it( 'should be selected as the current option', async () => { + createProductShippingClass.mockImplementation( ( value ) => { + shippingClasses.push( value ); + return Promise.resolve( value ); + } ); + + await addNewShippingClass( + newShippingClass.name, + newShippingClass.slug + ); + + const select = getShippingClassSelect(); + + expect( select ).toHaveDisplayValue( [ + newShippingClass.name, + ] ); + } ); + + it( 'should show a snackbar message when server responds an error', async () => { + createProductShippingClass.mockRejectedValue( + new Error( 'Server Error' ) + ); + + await addNewShippingClass( newShippingClass.name ); + + expect( createErrorNotice ).toHaveBeenNthCalledWith( + 1, + __( + 'We couldn’t add this shipping class. Try again in a few seconds.', + 'woocommerce' + ), + expect.objectContaining( { explicitDismiss: true } ) + ); + } ); + } ); + } ); + + describe( 'when editing a product', () => { + const product: PartialProduct = { + id: 1, + categories: [ + { + id: 1, + name: 'Category 1', + slug: 'category-1', + }, + ], + }; + + beforeEach( () => { + render( +
+ + + ); + } ); + + describe( 'when creating a shipping class', () => { + it( 'should add the first cat as a shipping class only once', async () => { + const category = product?.categories?.at( 0 ); + const newShippingClass: Partial< ProductShippingClass > = { + name: category?.name, + slug: category?.slug, + }; + createProductShippingClass.mockImplementation( ( value ) => { + shippingClasses.push( value ); + return Promise.resolve( value ); + } ); + + await openShippingClassDialog(); + + let inputName = await getShippingClassNameInput(); + expect( inputName ).toHaveValue( newShippingClass.name ); + + await submitShippingClassDialog(); + expect( createProductShippingClass ).toHaveBeenNthCalledWith( + 1, + expect.objectContaining( newShippingClass ) + ); + + await openShippingClassDialog(); + + inputName = await getShippingClassNameInput(); + expect( inputName ).not.toHaveValue( newShippingClass.name ); + } ); + } ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/products/sections/utils.ts b/plugins/woocommerce-admin/client/products/sections/utils.ts index a00789de2da..d2e6e418355 100644 --- a/plugins/woocommerce-admin/client/products/sections/utils.ts +++ b/plugins/woocommerce-admin/client/products/sections/utils.ts @@ -1,7 +1,8 @@ /** * External dependencies */ -import classnames from 'classnames'; +import { ChangeEvent } from 'react'; +import { Product } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; /** @@ -9,85 +10,67 @@ import { recordEvent } from '@woocommerce/tracks'; */ import { NUMBERS_AND_ALLOWED_CHARS } from '../constants'; -type gettersProps = { - context?: { - formatAmount: ( number: number | string ) => string; - getCurrencyConfig: () => { - code: string; - symbol: string; - symbolPosition: string; - decimalSeparator: string; - priceFormat: string; - thousandSeparator: string; - precision: number; - }; - }; - value: string; - name?: string; - checked: boolean; - selected?: boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onChange: ( value: any ) => void; - onBlur: () => void; - className: string | undefined; - help: string | null | undefined; +type CurrencyConfig = { + code: string; + symbol: string; + symbolPosition: string; + decimalSeparator: string; + priceFormat: string; + thousandSeparator: string; + precision: number; }; -export const getCheckboxProps = ( { - checked = false, - className, - name, - onBlur, - onChange, -}: gettersProps ) => { +/** + * Get additional props to be passed to all checkbox inputs. + * + * @param {string} name Name of the checkbox + * @return {Object} Props. + */ +export const getCheckboxTracks = ( name: string ) => { return { - checked, - className: classnames( 'woocommerce-product__checkbox', className ), - onChange: ( isChecked: boolean ) => { + onChange: ( + isChecked: + | ChangeEvent< HTMLInputElement > + | Product[ keyof Product ] + ) => { recordEvent( `product_checkbox_${ name }`, { checked: isChecked, } ); - return onChange( isChecked ); }, - onBlur, }; }; -export const getTextControlProps = ( { - className, - onBlur, - onChange, - value = '', - help, -}: gettersProps ) => { - return { - value, - className: classnames( 'woocommerce-product__text', className ), - onChange, - onBlur, - help, - }; -}; - -export const getInputControlProps = ( { - className, - context, - onBlur, - onChange, - value = '', - help, -}: gettersProps ) => { - if ( ! context ) { - return; - } - const { formatAmount, getCurrencyConfig } = context; - const { decimalSeparator, symbol, symbolPosition, thousandSeparator } = - getCurrencyConfig(); +/** + * Get input props for currency related values and symbol positions. + * + * @param {Object} currencyConfig - Currency context + * @return {Object} Props. + */ +export const getCurrencySymbolProps = ( currencyConfig: CurrencyConfig ) => { + const { symbol, symbolPosition } = currencyConfig; const currencyPosition = symbolPosition.includes( 'left' ) ? 'prefix' : 'suffix'; - // Cleans the value to show. + return { + [ currencyPosition ]: symbol, + }; +}; + +/** + * Cleans and formats the currency value shown to the user. + * + * @param {string} value Form value. + * @param {Object} currencyConfig Currency context. + * @return {string} Display value. + */ +export const formatCurrencyDisplayValue = ( + value: string, + currencyConfig: CurrencyConfig, + format: ( number: number | string ) => string +) => { + const { decimalSeparator, thousandSeparator } = currencyConfig; + const regex = new RegExp( NUMBERS_AND_ALLOWED_CHARS.replace( '%s1', decimalSeparator ).replace( '%s2', @@ -95,16 +78,6 @@ export const getInputControlProps = ( { ), 'g' ); - const currencyString = - value === undefined - ? value - : formatAmount( value ).replace( regex, '' ); - return { - value: currencyString, - [ currencyPosition ]: symbol, - className: classnames( 'woocommerce-product__input', className ), - onChange, - onBlur, - help, - }; + + return value === undefined ? value : format( value ).replace( regex, '' ); }; diff --git a/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx index 080e77127c5..b4b12e3d859 100644 --- a/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx +++ b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx @@ -11,7 +11,6 @@ import { ProductShippingClass } from '@woocommerce/data'; /** * Internal dependencies */ -import { getTextControlProps } from '../../sections/utils'; import './add-new-shipping-class-modal.scss'; export type ShippingClassFormProps = { @@ -20,16 +19,10 @@ export type ShippingClassFormProps = { }; function ShippingClassForm( { onAdd, onCancel }: ShippingClassFormProps ) { - const { getInputProps, isValidForm } = + const { errors, getInputProps, isValidForm } = useFormContext< ProductShippingClass >(); const [ isLoading, setIsLoading ] = useState( false ); - const inputNameProps = getTextControlProps( getInputProps( 'name' ) ); - const inputSlugProps = getTextControlProps( getInputProps( 'slug' ) ); - const inputDescriptionProps = getTextControlProps( - getInputProps( 'description' ) - ); - function handleAdd() { setIsLoading( true ); onAdd() @@ -45,38 +38,28 @@ function ShippingClassForm( { onAdd, onCancel }: ShippingClassFormProps ) { return (
- + required: ( + + { __( '(required)', 'woocommerce' ) } + ), }, } ) } /> - ), - }, - } ) } + { ...getInputProps( 'slug' ) } + label={ __( 'Slug', 'woocommerce' ) } + /> + void; }; -const INITIAL_VALUES = { - name: __( 'New shipping class', 'woocommerce' ), - slug: __( 'new-shipping-class', 'woocommerce' ), -}; +const INITIAL_VALUES = { name: '', slug: '', description: '' }; export function AddNewShippingClassModal( { shippingClass, diff --git a/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx b/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx index e5f9789fd9d..565189510a0 100644 --- a/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx +++ b/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx @@ -1,7 +1,9 @@ /** * External dependencies */ +import { PropsWithChildren } from 'react'; import { render, waitFor, screen, within } from '@testing-library/react'; +import { Fragment } from '@wordpress/element'; import { Form, FormContext } from '@woocommerce/components'; import { Product } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; @@ -18,7 +20,13 @@ const updateProductWithStatus = jest.fn(); const copyProductWithStatus = jest.fn(); const deleteProductAndRedirect = jest.fn(); +jest.mock( '@wordpress/plugins', () => ( { registerPlugin: jest.fn() } ) ); jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) ); +jest.mock( '~/header/utils', () => ( { + WooHeaderItem: ( props: { children: () => React.ReactElement } ) => ( + { props.children() } + ), +} ) ); jest.mock( '../use-product-helper', () => { return { useProductHelper: () => ( { diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js index ba3aba4599b..08e18e6bd38 100644 --- a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js +++ b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js @@ -766,7 +766,11 @@ class BusinessDetails extends Component { activeClass="is-active" initialTabName="current-tab" onSelect={ ( tabName ) => { - if ( this.state.currentTab !== tabName ) { + if ( + this.state.currentTab !== tabName && + // TabPanel calls onSelect on mount when initialTabName is provided, so we need to check if the tabName is valid. + tabName !== 'current-tab' + ) { this.setState( { currentTab: tabName, savedValues: @@ -819,7 +823,7 @@ export const BusinessFeaturesList = compose( ? getInstallableExtensions( { freeExtensionBundleByCategory: freeExtensions, country, - productTypes: profileItems.product_types, + productTypes: profileItems.product_types || [], } ) : []; const hasInstallableExtensions = installableExtensions.some( diff --git a/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss b/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss index 09265e4c1a6..887d6dc4656 100644 --- a/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss +++ b/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss @@ -34,9 +34,9 @@ color: $gray-900; } - select.components-select-control__input { + .components-base-control select.components-select-control__input { max-width: 100%; - line-height: 1; + line-height: normal; } .components-panel__body > .components-panel__body-title, diff --git a/plugins/woocommerce-admin/client/tasks/tasks.tsx b/plugins/woocommerce-admin/client/tasks/tasks.tsx index 7c6368a2fa2..84dd5c6ec65 100644 --- a/plugins/woocommerce-admin/client/tasks/tasks.tsx +++ b/plugins/woocommerce-admin/client/tasks/tasks.tsx @@ -20,15 +20,13 @@ import { recordEvent } from '@woocommerce/tracks'; */ import { DisplayOption } from '~/activity-panel/display-options'; import { Task } from './task'; -import { TasksPlaceholder, TasksPlaceholderProps } from './placeholder'; +import { TasksPlaceholder } from './placeholder'; import './tasks.scss'; import { TaskList } from './task-list'; import { TaskList as TwoColumnTaskList } from '../two-column-tasks/task-list'; -import { SectionedTaskList } from '../two-column-tasks/sectioned-task-list'; import TwoColumnTaskListPlaceholder from '../two-column-tasks/placeholder'; import '../two-column-tasks/style.scss'; import { getAdminSetting } from '~/utils/admin-settings'; -import { SectionedTaskListPlaceholder } from '~/two-column-tasks/sectioned-task-list-placeholder'; export type TasksProps = { query: { task?: string }; diff --git a/plugins/woocommerce-admin/client/tasks/test/task-list.test.tsx b/plugins/woocommerce-admin/client/tasks/test/task-list.test.tsx index fe6cd2e29b7..169c7b42b6e 100644 --- a/plugins/woocommerce-admin/client/tasks/test/task-list.test.tsx +++ b/plugins/woocommerce-admin/client/tasks/test/task-list.test.tsx @@ -235,56 +235,4 @@ describe( 'TaskList', () => { queryByText( dismissedTask[ 0 ].title ) ).not.toBeInTheDocument(); } ); - - it( 'should not display isSnoozed tasks', () => { - const dismissedTask = [ - { - ...tasks.setup[ 0 ], - isSnoozed: true, - snoozedUntil: Date.now() + 10000, - }, - ]; - const { queryByText } = render( - - ); - expect( - queryByText( dismissedTask[ 0 ].title ) - ).not.toBeInTheDocument(); - } ); - - it( 'should display a snoozed task if snoozedUntil passed the current timestamp', () => { - const dismissedTask = [ - { - ...tasks.setup[ 0 ], - isSnoozed: true, - snoozedUntil: Date.now() - 1000, - }, - ]; - const { queryByText } = render( - - ); - expect( queryByText( dismissedTask[ 0 ].title ) ).toBeInTheDocument(); - } ); } ); diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss deleted file mode 100644 index 9a0ab073071..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss +++ /dev/null @@ -1,36 +0,0 @@ -.woocommerce-task-section-header__container { - display: flex; - - .woocommerce-task-header__illustration { - max-width: 150px; - width: 34%; - margin-left: auto; - margin-right: 7%; - display: flex; - align-items: center; - - .illustration-background { - max-width: 100%; - } - } - - @at-root .woocommerce-setup-panel & .woocommerce-task-header__contents p { - font-size: 13px; - } - - - .woocommerce-task-header__contents p { - font-size: 16px; - } - - @at-root .woocommerce-setup-panel & .woocommerce-task-header__contents h1 { - font-size: 14px; - font-weight: 600; - } - - .woocommerce-task-header__contents h1 { - font-size: 20px; - line-height: 28px; - padding: 0; - } -} diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx deleted file mode 100644 index c84a892084e..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Internal dependencies - */ -import './section-header.scss'; - -type Props = { - title: string; - description: string; - image: string; -}; - -const SectionHeader: React.FC< Props > = ( { title, description, image } ) => { - return ( -
-
-

{ title }

-

{ description }

-
-
- { -
-
- ); -}; - -export default SectionHeader; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx b/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx deleted file mode 100644 index c2e0927d0b7..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/** - * External dependencies - */ -import { Badge } from '@woocommerce/components'; -import { TaskListSection, TaskType } from '@woocommerce/data'; -import { Icon, check } from '@wordpress/icons'; -import { Text } from '@woocommerce/experimental'; - -/** - * Internal dependencies - */ -import SectionHeader from './headers/section-header'; - -type SectionPanelTitleProps = { - section: TaskListSection; - active: boolean; - tasks: TaskType[]; -}; - -export const SectionPanelTitle: React.FC< SectionPanelTitleProps > = ( { - section, - active, - tasks, -} ) => { - if ( active ) { - return ( -
-
- -
-
- ); - } - - const uncompletedTasksCount = tasks.filter( - ( task ) => ! task.isComplete && section.tasks.includes( task.id ) - ).length; - const isComplete = section.isComplete || uncompletedTasksCount === 0; - - return ( - <> - - { section.title } - - { ! isComplete && } - { isComplete && ( -
- -
- ) } - - ); -}; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list-placeholder.tsx b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list-placeholder.tsx deleted file mode 100644 index 70b234eaf0a..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list-placeholder.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Internal dependencies - */ -import './style.scss'; - -type TasksPlaceholderProps = { - numTasks?: number; - query: { - task?: string; - }; -}; - -const SectionedTaskListPlaceholder: React.FC< TasksPlaceholderProps > = ( - props -) => { - const { numTasks = 3 } = props; - - return ( -
-
-
-
-
-
-
-
    - { Array.from( new Array( numTasks ) ).map( ( v, i ) => ( -
  • -
    -
    -
    -
    -
    -
    -
  • - ) ) } -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ); -}; - -export { SectionedTaskListPlaceholder }; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss deleted file mode 100644 index a55dea2a5aa..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss +++ /dev/null @@ -1,165 +0,0 @@ -.woocommerce-sectioned-task-list { - .components-panel { - width: 100%; - background: transparent; - border: 0; - } - - .components-panel__body { - padding-bottom: 0; - margin-bottom: $gap-smaller; - background: #fff; - border: 1px solid $gray-200; - - &.is-opened { - padding-bottom: 0; - } - - .components-panel__body-title { - margin-bottom: 0; - border-bottom: 1px solid #e0e0e0; - - &:hover { - border-bottom: 1px solid #e0e0e0; - } - - @at-root .woocommerce-setup-panel & > .components-button { - font-size: 14px; - } - - > .components-button { - font-size: 20px; - font-weight: 400; - padding-top: 20px; - padding-bottom: 20px; - } - - .components-panel__arrow { - right: $gap-large; - } - - .woocommerce-task-header__contents p:first-of-type { - margin-top: $gap-small; - } - } - .wooocommerce-task-card__header-container { - width: 100%; - border-bottom: none; - } - .components-panel__body-toggle { - box-shadow: none; - padding-left: $gap-large; - } - &.is-opened .components-panel__body-toggle { - width: 100%; - padding: 0; - .components-panel__arrow { - top: 32px; - } - } - - .woocommerce-experimental-list { - width: calc(100% + 32px); - margin: 0 -16px; - } - } - ul li.woocommerce-task-list__item { - padding-top: $gap; - padding-bottom: $gap; - min-height: 72px; - - &.is-disabled { - pointer-events: none; - } - - &:not(.complete) - .woocommerce-task-list__item-before - .woocommerce-task__icon { - border-color: $gray-300; - } - .woocommerce-task-list__item-expandable-content { - line-height: $gap; - } - } - - .woocommerce-task-list__item.complete .woocommerce-task__icon { - background-color: $alert-green; - } - - .components-panel__body-title { - .woocommerce-badge { - width: 28px; - height: 28px; - margin-left: $gap-small; - } - .woocommerce-task__icon { - margin-left: $gap; - background-color: $alert-green; - border-radius: 50%; - width: 24px; - height: 24px; - svg { - fill: #fff; - position: relative; - } - } - } - - > .is-loading { - border: none; - margin-bottom: 8px; - - .woocommerce-task-list__item .woocommerce-task-list__item-before { - padding: 0 0 0 $gap-large; - } - - &.components-panel__body .components-panel__body-title .woocommerce-task-list__item-text { - width: 50%; - - .is-placeholder { - width: 100%; - } - } - - &.components-panel__body .woocommerce-task-list__item-after { - margin-left: $gap; - - .is-placeholder { - height: 24px; - width: 24px; - border-radius: 50%; - } - } - } -} - -.woocommerce-setup-panel { - .two-column-experiment { - h1 { - font-size: 16px; - font-weight: 600; - } - } - - .woocommerce-task-header-collapsed { - font-size: 14px; - line-height: 20px; - font-weight: bold; - } - - .woocommerce-task-progress-header { - padding: $gap; - margin-bottom: $gap-smaller; - background: #fff; - border: 1px solid $gray-200; - - .woocommerce-task-progress-header__title { - padding-top: 4px; - } - - .woocommerce-ellipsis-menu { - display: none; - } - - } -} diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx deleted file mode 100644 index 147757bdae4..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx +++ /dev/null @@ -1,209 +0,0 @@ -/** - * External dependencies - */ -import { useEffect, useRef, useState, useContext } from '@wordpress/element'; -import { Panel, PanelBody, PanelRow } from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { ONBOARDING_STORE_NAME, getVisibleTasks } from '@woocommerce/data'; -import { recordEvent } from '@woocommerce/tracks'; -import { List } from '@woocommerce/experimental'; -import classnames from 'classnames'; - -/** - * Internal dependencies - */ -import '../tasks/task-list.scss'; -import './sectioned-task-list.scss'; -import TaskListCompleted from './completed'; -import { TaskListProps } from '~/tasks/task-list'; -import { ProgressHeader } from '~/task-lists/progress-header'; -import { SectionPanelTitle } from './section-panel-title'; -import { TaskListItem } from './task-list-item'; -import { TaskListCompletedHeader } from './completed-header'; -import { LayoutContext } from '~/layout'; - -type PanelBodyProps = Omit< PanelBody.Props, 'title' | 'onToggle' > & { - title: string | React.ReactNode | undefined; - onToggle?: ( isOpen: boolean ) => void; -}; -const PanelBodyWithUpdatedType = - PanelBody as React.ComponentType< PanelBodyProps >; - -export const SectionedTaskList: React.FC< TaskListProps > = ( { - query, - id, - eventPrefix, - tasks, - keepCompletedTaskList, - isComplete, - sections, - displayProgressHeader, - cesHeader = true, -} ) => { - const { profileItems } = useSelect( ( select ) => { - const { getProfileItems } = select( ONBOARDING_STORE_NAME ); - return { - profileItems: getProfileItems(), - }; - } ); - const { hideTaskList, keepCompletedTaskList: keepCompletedTasks } = - useDispatch( ONBOARDING_STORE_NAME ); - const [ openPanel, setOpenPanel ] = useState< string | null >( - sections?.find( ( section ) => ! section.isComplete )?.id || null - ); - const layoutContext = useContext( LayoutContext ); - - const prevQueryRef = useRef( query ); - - const visibleTasks = getVisibleTasks( tasks ); - - const recordTaskListView = () => { - if ( query.task ) { - return; - } - - recordEvent( `${ eventPrefix }view`, { - number_tasks: visibleTasks.length, - store_connected: profileItems.wccom_connected, - context: layoutContext.toString(), - } ); - }; - - useEffect( () => { - recordTaskListView(); - }, [] ); - - useEffect( () => { - const { task: prevTask } = prevQueryRef.current; - const { task } = query; - - if ( prevTask !== task ) { - window.document.documentElement.scrollTop = 0; - prevQueryRef.current = query; - } - }, [ query ] ); - - const hideTasks = () => { - hideTaskList( id ); - }; - - const keepTasks = () => { - keepCompletedTasks( id ); - }; - - let selectedHeaderCard = visibleTasks.find( - ( listTask ) => listTask.isComplete === false - ); - - // If nothing is selected, default to the last task since everything is completed. - if ( ! selectedHeaderCard ) { - selectedHeaderCard = visibleTasks[ visibleTasks.length - 1 ]; - } - - const getSectionTasks = ( sectionTaskIds: string[] ) => { - return visibleTasks.filter( ( task ) => - sectionTaskIds.includes( task.id ) - ); - }; - - if ( ! visibleTasks.length ) { - return
; - } - - if ( isComplete && keepCompletedTaskList !== 'yes' ) { - return ( - <> - { cesHeader ? ( - - ) : ( - - ) } - - ); - } - - return ( - <> - { displayProgressHeader ? ( - - ) : null } -
- - { ( sections || [] ).map( ( section ) => ( - - } - opened={ openPanel === section.id } - onToggle={ ( isOpen: boolean ) => { - if ( ! isOpen && openPanel === section.id ) { - recordEvent( - `${ eventPrefix }section_closed`, - { - id: section.id, - all: true, - } - ); - setOpenPanel( null ); - } else { - if ( openPanel ) { - recordEvent( - `${ eventPrefix }section_closed`, - { - id: openPanel, - all: false, - } - ); - } - setOpenPanel( section.id ); - } - if ( isOpen ) { - recordEvent( - `${ eventPrefix }section_opened`, - { - id: section.id, - } - ); - } - } } - initialOpen={ false } - > - - - { getSectionTasks( section.tasks ).map( - ( task ) => ( - - ) - ) } - - - - ) ) } - -
- - ); -}; - -export default SectionedTaskList; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/task-list-item.tsx b/plugins/woocommerce-admin/client/two-column-tasks/task-list-item.tsx deleted file mode 100644 index bc280ce4a10..00000000000 --- a/plugins/woocommerce-admin/client/two-column-tasks/task-list-item.tsx +++ /dev/null @@ -1,189 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { getNewPath, navigateTo } from '@woocommerce/navigation'; -import { - ONBOARDING_STORE_NAME, - TaskType, - useUserPreferences, -} from '@woocommerce/data'; -import { recordEvent } from '@woocommerce/tracks'; -import { TaskItem, useSlot } from '@woocommerce/experimental'; -import { useCallback, useContext } from '@wordpress/element'; -import { useDispatch } from '@wordpress/data'; -import { WooOnboardingTaskListItem } from '@woocommerce/onboarding'; -import classnames from 'classnames'; - -/** - * Internal dependencies - */ -import { LayoutContext } from '~/layout'; - -export type TaskListItemProps = { - task: TaskType; - eventPrefix?: string; -}; - -export const TaskListItem: React.FC< TaskListItemProps > = ( { - task, - eventPrefix, -} ) => { - const { createNotice } = useDispatch( 'core/notices' ); - - const { - visitedTask, - dismissTask, - undoDismissTask, - snoozeTask, - undoSnoozeTask, - } = useDispatch( ONBOARDING_STORE_NAME ); - - const layoutContext = useContext( LayoutContext ); - - const slot = useSlot( - `woocommerce_onboarding_task_list_item_${ task.id }` - ); - const hasFills = Boolean( slot?.fills?.length ); - - const userPreferences = useUserPreferences(); - - const getTaskStartedCount = () => { - const trackedStartedTasks = - userPreferences.task_list_tracked_started_tasks; - if ( ! trackedStartedTasks || ! trackedStartedTasks[ task.id ] ) { - return 0; - } - return trackedStartedTasks[ task.id ]; - }; - - const updateTrackStartedCount = () => { - const newCount = getTaskStartedCount() + 1; - const trackedStartedTasks = - userPreferences.task_list_tracked_started_tasks || {}; - - visitedTask( task.id ); - userPreferences.updateUserPreferences( { - task_list_tracked_started_tasks: { - ...( trackedStartedTasks || {} ), - [ task.id ]: newCount, - }, - } ); - }; - - const trackClick = () => { - recordEvent( `${ eventPrefix }click`, { - task_name: task.id, - context: layoutContext.toString(), - } ); - - if ( ! task.isComplete ) { - updateTrackStartedCount(); - } - }; - - const onTaskSelected = () => { - trackClick(); - - if ( task.actionUrl ) { - navigateTo( { - url: task.actionUrl, - } ); - return; - } - - navigateTo( { url: getNewPath( { task: task.id }, '/', {} ) } ); - }; - - const onDismiss = useCallback( () => { - dismissTask( task.id ); - createNotice( 'success', __( 'Task dismissed', 'woocommerce' ), { - actions: [ - { - label: __( 'Undo', 'woocommerce' ), - onClick: () => undoDismissTask( task.id ), - }, - ], - } ); - }, [ task.id ] ); - - const onSnooze = useCallback( () => { - snoozeTask( task.id ); - createNotice( - 'success', - __( 'Task postponed until tomorrow', 'woocommerce' ), - { - actions: [ - { - label: __( 'Undo', 'woocommerce' ), - onClick: () => undoSnoozeTask( task.id ), - }, - ], - } - ); - }, [ task.id ] ); - - const className = classnames( 'woocommerce-task-list__item', { - complete: task.isComplete, - 'is-disabled': task.isDisabled, - } ); - - const taskItemProps = { - completed: task.isComplete, - onSnooze: task.isSnoozeable && onSnooze, - onDismiss: task.isDismissable && onDismiss, - }; - - const DefaultTaskItem = useCallback( - ( props ) => { - const onClickActions = () => { - if ( props.onClick ) { - trackClick(); - return props.onClick(); - } - return onTaskSelected(); - }; - return ( - {} } - actionLabel={ task.actionLabel } - { ...props } - onClick={ ( e: React.ChangeEvent ) => { - if ( task.isDisabled || e.target.tagName === 'A' ) { - return; - } - onClickActions(); - } } - /> - ); - }, - [ - task.id, - task.title, - task.content, - task.time, - task.actionLabel, - task.isComplete, - ] - ); - - return hasFills ? ( - - ) : ( - - ); -}; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx b/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx index 1f7b5d69d7a..48a3366a71a 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx @@ -259,56 +259,4 @@ describe( 'TaskList', () => { queryByText( dismissedTask[ 0 ].title ) ).not.toBeInTheDocument(); } ); - - it( 'should not display isSnoozed tasks', () => { - const dismissedTask = [ - { - ...tasks.setup[ 0 ], - isSnoozed: true, - snoozedUntil: Date.now() + 10000, - }, - ]; - const { queryByText } = render( - - ); - expect( - queryByText( dismissedTask[ 0 ].title ) - ).not.toBeInTheDocument(); - } ); - - it( 'should display a snoozed task if snoozedUntil passed the current timestamp', () => { - const dismissedTask = [ - { - ...tasks.setup[ 0 ], - isSnoozed: true, - snoozedUntil: Date.now() - 1000, - }, - ]; - const { queryByText } = render( - - ); - expect( queryByText( dismissedTask[ 0 ].title ) ).toBeInTheDocument(); - } ); } ); diff --git a/plugins/woocommerce-admin/client/utils/test/url-helpers.spec.ts b/plugins/woocommerce-admin/client/utils/test/url-helpers.spec.ts new file mode 100644 index 00000000000..de094bc823e --- /dev/null +++ b/plugins/woocommerce-admin/client/utils/test/url-helpers.spec.ts @@ -0,0 +1,25 @@ +/** + * Internal dependencies + */ +import { getSegmentsFromPath } from '../url-helpers'; + +describe( 'URL Helpers', () => { + it( 'should extract segments from query path param correctly', () => { + const paths = [ + { input: undefined, output: [] }, + { input: '', output: [ '' ] }, + { input: 'product', output: [ 'product' ] }, + { input: 'product/', output: [ 'product' ] }, + { input: '/product', output: [ 'product' ] }, + { input: '/product/', output: [ 'product' ] }, + { input: 'product/123', output: [ 'product', '123' ] }, + { input: 'product/123/', output: [ 'product', '123' ] }, + { input: '/product/123', output: [ 'product', '123' ] }, + { input: '/product/123/', output: [ 'product', '123' ] }, + ]; + + paths.forEach( ( { input, output } ) => { + expect( getSegmentsFromPath( input ) ).toStrictEqual( output ); + } ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/utils/url-helpers.ts b/plugins/woocommerce-admin/client/utils/url-helpers.ts new file mode 100644 index 00000000000..2a4de62a627 --- /dev/null +++ b/plugins/woocommerce-admin/client/utils/url-helpers.ts @@ -0,0 +1,11 @@ +/** + * Extracts all segments from the path query param as a string array. + * + * @param path The query path param + * @return The list of segments from the path + */ +export function getSegmentsFromPath( path?: string ): string[] { + const firstIndex = path?.startsWith( '/' ) ? 1 : 0; + const lastIndex = path?.endsWith( '/' ) ? -1 : undefined; + return path?.slice( firstIndex, lastIndex )?.split( '/' ) ?? []; +} diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index dd9131c0f09..3a602997d23 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -50,12 +50,14 @@ "@automattic/explat-client-react-helpers": "^0.0.4", "@automattic/interpolate-components": "^1.2.0", "@react-spring/web": "^9.4.3", + "@types/wordpress__blocks": "^11.0.7", "@woocommerce/api": "^0.2.0", "@woocommerce/e2e-environment": "^0.3.0", "@woocommerce/e2e-utils": "^0.2.0", "@wordpress/a11y": "^3.5.0", "@wordpress/api-fetch": "^6.0.1", "@wordpress/base-styles": "^4.3.0", + "@wordpress/blocks": "^11.17.0", "@wordpress/components": "^19.5.0", "@wordpress/compose": "^5.1.2", "@wordpress/core-data": "^4.1.2", @@ -118,6 +120,7 @@ "@types/jest": "^27.4.1", "@types/lodash": "^4.14.179", "@types/puppeteer": "^4.0.2", + "@types/qs": "^6.9.7", "@types/react": "^17.0.0", "@types/react-router-dom": "^5.3.3", "@types/react-transition-group": "^4.4.4", @@ -129,7 +132,6 @@ "@types/wordpress__media-utils": "^3.0.0", "@types/wordpress__notices": "^3.3.0", "@types/wordpress__plugins": "^3.0.0", - "@types/qs": "^6.9.7", "@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/parser": "^5.14.0", "@woocommerce/admin-e2e-tests": "workspace:*", @@ -151,6 +153,7 @@ "@woocommerce/onboarding": "workspace:*", "@woocommerce/tracks": "workspace:*", "@wordpress/babel-preset-default": "^6.5.1", + "@wordpress/block-editor": "^9.8.0", "@wordpress/browserslist-config": "^4.1.1", "@wordpress/custom-templated-path-webpack-plugin": "^2.1.2", "@wordpress/jest-preset-default": "^8.0.1", diff --git a/plugins/woocommerce-beta-tester/EXTENDING-WC-ADMIN-HELPER.md b/plugins/woocommerce-beta-tester/EXTENDING-WC-ADMIN-HELPER.md new file mode 100644 index 00000000000..0060ed451e8 --- /dev/null +++ b/plugins/woocommerce-beta-tester/EXTENDING-WC-ADMIN-HELPER.md @@ -0,0 +1,91 @@ +## Extending + +There are two client-side filters available if you want to extend the test +helper with your own plugin's test setup code. + +This example adds a new tab: + +``` +import { addFilter } from '@wordpress/hooks'; + +const SuperSekret = () => ( + <> +

Super sekret

+

This section contains super sekret tools.

+ + +); +addFilter( + 'woocommerce_admin_test_helper_tabs', + 'wath', + ( tabs ) => [ + ...tabs, + { + name: 'super-sekret', + title: 'Super sekret', + content: , + } + ] +); +``` + +This example adds a new tool to the existing Options tab: + +``` +import { addFilter } from '@wordpress/hooks'; + +const NewTool = () => ( + <> + New tool +

Description

+ + +); +addFilter( + 'woocommerce_admin_test_helper_tab_options', + 'wath', + ( entries ) => [ + ...entries, + + ] +); +``` + +Register a REST API endpoint to perform server-side actions in the usual way: + +``` +add_action( 'rest_api_init', function() { + register_rest_route( + 'your-plugin/v1', + '/area/action', + array( + 'methods' => 'POST', + 'callback' => 'your_plugin_area_action', + 'permission_callback' => function( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit ) ) { + return new \WP_Error( + 'woocommerce_rest_cannot_edit', + __( 'Sorry, you cannot perform this action', 'your-plugin' ) + ); + } + return true; + } + ) + ); +} ); + +function your_plugin_area_action() { + return []; +} +``` + +This would be used on the client like this: + +``` +import apiFetch from '@wordpress/api-fetch'; +... +const response = await apiFetch( { + path: '/your-plugin/v1/area/action', + method: 'POST', +} ); +``` diff --git a/plugins/woocommerce-beta-tester/README.md b/plugins/woocommerce-beta-tester/README.md index 3b36bd7fd56..65a1889ebfb 100644 --- a/plugins/woocommerce-beta-tester/README.md +++ b/plugins/woocommerce-beta-tester/README.md @@ -1,137 +1,26 @@ # WooCommerce Beta Tester -A plugin that makes it easy to test out pre-releases such as betas release canadidates and even final releases. +A plugin that makes it easy to test out pre-releases such as betas release candidates and even final releases. It also comes with WooCommerce Admin Test Helper that helps test WooCommerce Admin functionalities. -## Usage +## Installation -You can get to the settings and features from your top admin bar under the name WC Beta Tester. - -# WooCommerce Admin Test Helper - -A plugin that makes it easier to test the WooCommerce Admin plugin. +You can either install the latest version from [wp.org](https://wordpress.org/plugins/woocommerce-beta-tester/) or symlink this directory by running `ln -s ./ :path-to-your-wp-plugin-directory/woocommerce-beta-tester` ## Development To get started, run the following commands: ```text -npm install -npm start +pnpm install +pnpm run start ``` See [wp-scripts](https://github.com/WordPress/gutenberg/tree/master/packages/scripts) for more usage information. -## Extending +## Usage -There are two client-side filters available if you want to extend the test -helper with your own plugin's test setup code. +You can get to the settings and features from your top admin bar under the name WC Beta Tester. -This example adds a new tab: +For more information about WooCommerce Admin Test Helper usage, click [here](./EXTENDING-WC-ADMIN-HELPER.md). -``` -import { addFilter } from '@wordpress/hooks'; - -const SuperSekret = () => ( - <> -

Super sekret

-

This section contains super sekret tools.

- - -); -addFilter( - 'woocommerce_admin_test_helper_tabs', - 'wath', - ( tabs ) => [ - ...tabs, - { - name: 'super-sekret', - title: 'Super sekret', - content: , - } - ] -); -``` - -This example adds a new tool to the existing Options tab: - -``` -import { addFilter } from '@wordpress/hooks'; - -const NewTool = () => ( - <> - New tool -

Description

- - -); -addFilter( - 'woocommerce_admin_test_helper_tab_options', - 'wath', - ( entries ) => [ - ...entries, - - ] -); -``` - -Register a REST API endpoint to perform server-side actions in the usual way: - -``` -add_action( 'rest_api_init', function() { - register_rest_route( - 'your-plugin/v1', - '/area/action', - array( - 'methods' => 'POST', - 'callback' => 'your_plugin_area_action', - 'permission_callback' => function( $request ) { - if ( ! wc_rest_check_manager_permissions( 'settings', 'edit ) ) { - return new \WP_Error( - 'woocommerce_rest_cannot_edit', - __( 'Sorry, you cannot perform this action', 'your-plugin' ) - ); - } - return true; - } - ) - ); -} ); - -function your_plugin_area_action() { - return []; -} -``` - -This would be used on the client like this: - -``` -import apiFetch from '@wordpress/api-fetch'; -... -const response = await apiFetch( { - path: '/your-plugin/v1/area/action', - method: 'POST', -} ); -``` - -### Deploying - -Prerequisites: - -- [Hub](https://github.com/github/hub) -- Write access to this repository - -You can create a test ZIP of the plugin using this command: - -``` -npm run build -``` - -This creates `woocommerce-admin-test-helper.zip` in the project root. - -We release the plugin using GitHub Releases. There is a script to automate this: - -0. Make sure the version is updated in `woocommerce-admin-test-helper.php` and `package.json` -1. Commit and push to `trunk` -2. Run `npm run release` -3. Make sure you provide the correct version number when prompted -4. That's it! +Run `./bin/build-zip.sh` to make a zip file. diff --git a/plugins/woocommerce-beta-tester/changelog/update-woocommerce-beta-tester-readme b/plugins/woocommerce-beta-tester/changelog/update-woocommerce-beta-tester-readme new file mode 100644 index 00000000000..ce36805e219 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/update-woocommerce-beta-tester-readme @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update README to separate WooCommerce Admin Tester diff --git a/plugins/woocommerce/README.md b/plugins/woocommerce/README.md index a6e050a91b7..04fe227e5b9 100644 --- a/plugins/woocommerce/README.md +++ b/plugins/woocommerce/README.md @@ -2,7 +2,6 @@

license -Latest Stable Version WordPress.org downloads WordPress.org rating Build Status diff --git a/plugins/woocommerce/bin/composer/mozart/composer.lock b/plugins/woocommerce/bin/composer/mozart/composer.lock index eea80f8bd52..1b31825ab7b 100644 --- a/plugins/woocommerce/bin/composer/mozart/composer.lock +++ b/plugins/woocommerce/bin/composer/mozart/composer.lock @@ -13,12 +13,12 @@ "source": { "type": "git", "url": "https://github.com/coenjacobs/mozart.git", - "reference": "75ae1f91f04bbbd4b6edff282a483dfe611b2cea" + "reference": "4f9d00fbc3b3e39f4e334434fe058e516ad82291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/coenjacobs/mozart/zipball/75ae1f91f04bbbd4b6edff282a483dfe611b2cea", - "reference": "75ae1f91f04bbbd4b6edff282a483dfe611b2cea", + "url": "https://api.github.com/repos/coenjacobs/mozart/zipball/4f9d00fbc3b3e39f4e334434fe058e516ad82291", + "reference": "4f9d00fbc3b3e39f4e334434fe058e516ad82291", "shasum": "" }, "require": { @@ -29,9 +29,11 @@ }, "require-dev": { "mheap/phpunit-github-actions-printer": "^1.4", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-deprecation-rules": "^1.0", "phpunit/phpunit": "^8.5", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.4" + "squizlabs/php_codesniffer": "^3.5" }, "default-branch": true, "bin": [ @@ -64,20 +66,20 @@ "type": "github" } ], - "time": "2021-08-03T18:56:55+00:00" + "time": "2022-10-22T08:08:20+00:00" }, { "name": "league/flysystem", - "version": "1.1.9", + "version": "1.1.10", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "094defdb4a7001845300334e7c1ee2335925ef99" + "reference": "3239285c825c152bcc315fe0e87d6b55f5972ed1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/094defdb4a7001845300334e7c1ee2335925ef99", - "reference": "094defdb4a7001845300334e7c1ee2335925ef99", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/3239285c825c152bcc315fe0e87d6b55f5972ed1", + "reference": "3239285c825c152bcc315fe0e87d6b55f5972ed1", "shasum": "" }, "require": { @@ -150,7 +152,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/1.1.9" + "source": "https://github.com/thephpleague/flysystem/tree/1.1.10" }, "funding": [ { @@ -158,7 +160,7 @@ "type": "other" } ], - "time": "2021-12-09T09:40:50+00:00" + "time": "2022-10-04T09:16:37+00:00" }, { "name": "league/mime-type-detection", @@ -266,16 +268,16 @@ }, { "name": "symfony/console", - "version": "v5.4.12", + "version": "v5.4.15", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1" + "reference": "ea59bb0edfaf9f28d18d8791410ee0355f317669" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c072aa8f724c3af64e2c7a96b796a4863d24dba1", - "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1", + "url": "https://api.github.com/repos/symfony/console/zipball/ea59bb0edfaf9f28d18d8791410ee0355f317669", + "reference": "ea59bb0edfaf9f28d18d8791410ee0355f317669", "shasum": "" }, "require": { @@ -345,7 +347,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.12" + "source": "https://github.com/symfony/console/tree/v5.4.15" }, "funding": [ { @@ -361,7 +363,7 @@ "type": "tidelift" } ], - "time": "2022-08-17T13:18:05+00:00" + "time": "2022-10-26T21:41:52+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1070,16 +1072,16 @@ }, { "name": "symfony/string", - "version": "v5.4.12", + "version": "v5.4.15", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058" + "reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/2fc515e512d721bf31ea76bd02fe23ada4640058", - "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058", + "url": "https://api.github.com/repos/symfony/string/zipball/571334ce9f687e3e6af72db4d3b2a9431e4fd9ed", + "reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed", "shasum": "" }, "require": { @@ -1136,7 +1138,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.12" + "source": "https://github.com/symfony/string/tree/v5.4.15" }, "funding": [ { @@ -1152,7 +1154,7 @@ "type": "tidelift" } ], - "time": "2022-08-12T17:03:11+00:00" + "time": "2022-10-05T15:16:54+00:00" } ], "aliases": [], diff --git a/plugins/woocommerce/bin/composer/phpcs/composer.lock b/plugins/woocommerce/bin/composer/phpcs/composer.lock index ef7a75941e4..0e1a6def19c 100644 --- a/plugins/woocommerce/bin/composer/phpcs/composer.lock +++ b/plugins/woocommerce/bin/composer/phpcs/composer.lock @@ -146,16 +146,16 @@ }, { "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.1", + "version": "1.3.2", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "ddabec839cc003651f2ce695c938686d1086cf43" + "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/ddabec839cc003651f2ce695c938686d1086cf43", - "reference": "ddabec839cc003651f2ce695c938686d1086cf43", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", "shasum": "" }, "require": { @@ -192,26 +192,27 @@ "paragonie", "phpcs", "polyfill", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" }, - "time": "2021-02-15T10:24:51+00:00" + "time": "2022-10-25T01:46:02+00:00" }, { "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.3", + "version": "2.1.4", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "d55de55f88697b9cdb94bccf04f14eb3b11cf308" + "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/d55de55f88697b9cdb94bccf04f14eb3b11cf308", - "reference": "d55de55f88697b9cdb94bccf04f14eb3b11cf308", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", "shasum": "" }, "require": { @@ -246,13 +247,14 @@ "compatibility", "phpcs", "standards", + "static analysis", "wordpress" ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" }, - "time": "2021-12-30T16:37:40+00:00" + "time": "2022-10-24T09:00:36+00:00" }, { "name": "sirbrillig/phpcs-changed", diff --git a/plugins/woocommerce/bin/composer/phpunit/composer.lock b/plugins/woocommerce/bin/composer/phpunit/composer.lock index 19d941bd66a..1983c0a3a0b 100644 --- a/plugins/woocommerce/bin/composer/phpunit/composer.lock +++ b/plugins/woocommerce/bin/composer/phpunit/composer.lock @@ -1112,16 +1112,16 @@ }, { "name": "sebastian/exporter", - "version": "3.1.4", + "version": "3.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "0c32ea2e40dbf59de29f3b49bf375176ce7dd8db" + "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0c32ea2e40dbf59de29f3b49bf375176ce7dd8db", - "reference": "0c32ea2e40dbf59de29f3b49bf375176ce7dd8db", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/73a9676f2833b9a7c36968f9d882589cd75511e6", + "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6", "shasum": "" }, "require": { @@ -1177,7 +1177,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.4" + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.5" }, "funding": [ { @@ -1185,7 +1185,7 @@ "type": "github" } ], - "time": "2021-11-11T13:51:24+00:00" + "time": "2022-09-14T06:00:17+00:00" }, { "name": "sebastian/global-state", diff --git a/plugins/woocommerce/bin/composer/wp/composer.lock b/plugins/woocommerce/bin/composer/wp/composer.lock index 021717abf55..7ef28cb0bfd 100644 --- a/plugins/woocommerce/bin/composer/wp/composer.lock +++ b/plugins/woocommerce/bin/composer/wp/composer.lock @@ -148,16 +148,16 @@ }, { "name": "gettext/languages", - "version": "2.9.0", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/php-gettext/Languages.git", - "reference": "ed56dd2c7f4024cc953ed180d25f02f2640e3ffa" + "reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-gettext/Languages/zipball/ed56dd2c7f4024cc953ed180d25f02f2640e3ffa", - "reference": "ed56dd2c7f4024cc953ed180d25f02f2640e3ffa", + "url": "https://api.github.com/repos/php-gettext/Languages/zipball/4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab", + "reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab", "shasum": "" }, "require": { @@ -206,7 +206,7 @@ ], "support": { "issues": "https://github.com/php-gettext/Languages/issues", - "source": "https://github.com/php-gettext/Languages/tree/2.9.0" + "source": "https://github.com/php-gettext/Languages/tree/2.10.0" }, "funding": [ { @@ -218,20 +218,20 @@ "type": "github" } ], - "time": "2021-11-11T17:30:39+00:00" + "time": "2022-10-18T15:00:10+00:00" }, { "name": "mck89/peast", - "version": "v1.14.0", + "version": "v1.15.0", "source": { "type": "git", "url": "https://github.com/mck89/peast.git", - "reference": "70a728d598017e237118652b2fa30fbaa9d4ef6d" + "reference": "733cd8f62dcb8239094688063a92766bbfcbf523" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mck89/peast/zipball/70a728d598017e237118652b2fa30fbaa9d4ef6d", - "reference": "70a728d598017e237118652b2fa30fbaa9d4ef6d", + "url": "https://api.github.com/repos/mck89/peast/zipball/733cd8f62dcb8239094688063a92766bbfcbf523", + "reference": "733cd8f62dcb8239094688063a92766bbfcbf523", "shasum": "" }, "require": { @@ -244,7 +244,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.14.0-dev" + "dev-master": "1.15.0-dev" } }, "autoload": { @@ -266,9 +266,9 @@ "description": "Peast is PHP library that generates AST for JavaScript code", "support": { "issues": "https://github.com/mck89/peast/issues", - "source": "https://github.com/mck89/peast/tree/v1.14.0" + "source": "https://github.com/mck89/peast/tree/v1.15.0" }, - "time": "2022-05-01T15:09:54+00:00" + "time": "2022-09-13T15:56:53+00:00" }, { "name": "mustache/mustache", @@ -553,16 +553,16 @@ }, { "name": "wp-cli/php-cli-tools", - "version": "v0.11.15", + "version": "v0.11.16", "source": { "type": "git", "url": "https://github.com/wp-cli/php-cli-tools.git", - "reference": "b6edd35988892ea1451392eb7a26d9dbe98c836d" + "reference": "c32e51a5c9993ad40591bc426b21f5422a5ed293" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/b6edd35988892ea1451392eb7a26d9dbe98c836d", - "reference": "b6edd35988892ea1451392eb7a26d9dbe98c836d", + "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/c32e51a5c9993ad40591bc426b21f5422a5ed293", + "reference": "c32e51a5c9993ad40591bc426b21f5422a5ed293", "shasum": "" }, "require": { @@ -601,22 +601,22 @@ ], "support": { "issues": "https://github.com/wp-cli/php-cli-tools/issues", - "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.15" + "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.16" }, - "time": "2022-08-15T10:15:55+00:00" + "time": "2022-11-03T15:19:26+00:00" }, { "name": "wp-cli/wp-cli", - "version": "v2.6.0", + "version": "v2.7.1", "source": { "type": "git", "url": "https://github.com/wp-cli/wp-cli.git", - "reference": "dee13c2baf6bf972484a63f8b8dab48f7220f095" + "reference": "1ddc754f1c15e56fb2cdd1a4e82bd0ec6ca32a76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/dee13c2baf6bf972484a63f8b8dab48f7220f095", - "reference": "dee13c2baf6bf972484a63f8b8dab48f7220f095", + "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/1ddc754f1c15e56fb2cdd1a4e82bd0ec6ca32a76", + "reference": "1ddc754f1c15e56fb2cdd1a4e82bd0ec6ca32a76", "shasum": "" }, "require": { @@ -634,7 +634,7 @@ "wp-cli/entity-command": "^1.2 || ^2", "wp-cli/extension-command": "^1.1 || ^2", "wp-cli/package-command": "^1 || ^2", - "wp-cli/wp-cli-tests": "^3.1.3" + "wp-cli/wp-cli-tests": "^3.1.6" }, "suggest": { "ext-readline": "Include for a better --prompt implementation", @@ -647,7 +647,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6.x-dev" + "dev-master": "2.8.x-dev" } }, "autoload": { @@ -674,7 +674,7 @@ "issues": "https://github.com/wp-cli/wp-cli/issues", "source": "https://github.com/wp-cli/wp-cli" }, - "time": "2022-01-25T16:31:27+00:00" + "time": "2022-10-17T23:10:42+00:00" } ], "aliases": [], diff --git a/plugins/woocommerce/changelog/add-33554_variation-price b/plugins/woocommerce/changelog/add-33554_variation-price new file mode 100644 index 00000000000..f6d8cbb756a --- /dev/null +++ b/plugins/woocommerce/changelog/add-33554_variation-price @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add variation price shortcut diff --git a/plugins/woocommerce/changelog/add-34331_add_attributes_modal b/plugins/woocommerce/changelog/add-34331_add_attributes_modal new file mode 100644 index 00000000000..05ccdce5eb3 --- /dev/null +++ b/plugins/woocommerce/changelog/add-34331_add_attributes_modal @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add add attribute modal to the attribute field in the new product management MVP diff --git a/plugins/woocommerce/changelog/add-34331_create_attribute_term_modal b/plugins/woocommerce/changelog/add-34331_create_attribute_term_modal new file mode 100644 index 00000000000..b297668b20a --- /dev/null +++ b/plugins/woocommerce/changelog/add-34331_create_attribute_term_modal @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add option and modal to create new attribute terms within MVP attribute modal. diff --git a/plugins/woocommerce/changelog/add-34332-add-attribute-edit b/plugins/woocommerce/changelog/add-34332-add-attribute-edit new file mode 100644 index 00000000000..a2a242fb00e --- /dev/null +++ b/plugins/woocommerce/changelog/add-34332-add-attribute-edit @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adding attribute edit modal for new product screen. diff --git a/plugins/woocommerce/changelog/add-34388 b/plugins/woocommerce/changelog/add-34388 new file mode 100644 index 00000000000..7330ad35258 --- /dev/null +++ b/plugins/woocommerce/changelog/add-34388 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add manual stock management section to product management experience diff --git a/plugins/woocommerce/changelog/add-34389 b/plugins/woocommerce/changelog/add-34389 new file mode 100644 index 00000000000..abd13470fa6 --- /dev/null +++ b/plugins/woocommerce/changelog/add-34389 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add product inventory advanced section diff --git a/plugins/woocommerce/changelog/add-34864-order-data-store-in-ssr b/plugins/woocommerce/changelog/add-34864-order-data-store-in-ssr new file mode 100644 index 00000000000..63ceec99d1b --- /dev/null +++ b/plugins/woocommerce/changelog/add-34864-order-data-store-in-ssr @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Include order datastore information in status report. diff --git a/plugins/woocommerce/changelog/add-34997 b/plugins/woocommerce/changelog/add-34997 new file mode 100644 index 00000000000..019d0a61d8f --- /dev/null +++ b/plugins/woocommerce/changelog/add-34997 @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Use new Tooltip component instead of EnrichedLabel diff --git a/plugins/woocommerce/changelog/add-35036-select-created-class b/plugins/woocommerce/changelog/add-35036-select-created-class new file mode 100644 index 00000000000..bc8c95890d3 --- /dev/null +++ b/plugins/woocommerce/changelog/add-35036-select-created-class @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Select the current new added shipping class diff --git a/plugins/woocommerce/changelog/add-35037-error-handling b/plugins/woocommerce/changelog/add-35037-error-handling new file mode 100644 index 00000000000..7df34f3d8c1 --- /dev/null +++ b/plugins/woocommerce/changelog/add-35037-error-handling @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Show a dismissible snackbar if the server responds with an error diff --git a/plugins/woocommerce/changelog/add-35046 b/plugins/woocommerce/changelog/add-35046 new file mode 100644 index 00000000000..0276e8b8ba5 --- /dev/null +++ b/plugins/woocommerce/changelog/add-35046 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Disable inventory stock toggle when product stock management is disabled diff --git a/plugins/woocommerce/changelog/add-35139-header-action-button b/plugins/woocommerce/changelog/add-35139-header-action-button new file mode 100644 index 00000000000..b7055d8a883 --- /dev/null +++ b/plugins/woocommerce/changelog/add-35139-header-action-button @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Move product action buttons to header menu diff --git a/plugins/woocommerce/changelog/add-35165 b/plugins/woocommerce/changelog/add-35165 new file mode 100644 index 00000000000..33982e23520 --- /dev/null +++ b/plugins/woocommerce/changelog/add-35165 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add summary to new product page experience diff --git a/plugins/woocommerce/changelog/add-api-core-tests-data-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-data-crud-tests new file mode 100644 index 00000000000..d2bec88cc88 --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-data-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for data crud operations \ No newline at end of file diff --git a/plugins/woocommerce/changelog/add-api-core-tests-payment-gateways-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-payment-gateways-crud-tests new file mode 100644 index 00000000000..d578dc26973 --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-payment-gateways-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for payment gateways crud operations \ No newline at end of file diff --git a/plugins/woocommerce/changelog/add-api-core-tests-product-reviews-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-product-reviews-crud-tests new file mode 100644 index 00000000000..ee5edb1ab30 --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-product-reviews-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for product reviews crud operations \ No newline at end of file diff --git a/plugins/woocommerce/changelog/add-api-core-tests-product-variations-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-product-variations-crud-tests new file mode 100644 index 00000000000..56315464db6 --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-product-variations-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for product variations crud operations \ No newline at end of file diff --git a/plugins/woocommerce/changelog/add-api-core-tests-reports-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-reports-crud-tests new file mode 100644 index 00000000000..ded7697523f --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-reports-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for reports crud operations \ No newline at end of file diff --git a/plugins/woocommerce/changelog/add-api-core-tests-settings-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-settings-crud-tests new file mode 100644 index 00000000000..e8bacdc4e91 --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-settings-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for settingss crud operations \ No newline at end of file diff --git a/plugins/woocommerce/changelog/add-api-core-tests-system-status-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-system-status-crud-tests new file mode 100644 index 00000000000..1a782cf170c --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-system-status-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for system status crud operations \ No newline at end of file diff --git a/plugins/woocommerce/changelog/add-api-core-tests-webhooks-crud-tests b/plugins/woocommerce/changelog/add-api-core-tests-webhooks-crud-tests new file mode 100644 index 00000000000..87cb2494912 --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-core-tests-webhooks-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add playwright api-core-tests for webhooks crud operations \ No newline at end of file diff --git a/plugins/woocommerce/changelog/add-api-pw-npm-script b/plugins/woocommerce/changelog/add-api-pw-npm-script new file mode 100644 index 00000000000..ad00e99c15a --- /dev/null +++ b/plugins/woocommerce/changelog/add-api-pw-npm-script @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Added npm script for Playwright API Core Tests diff --git a/plugins/woocommerce/changelog/add-k6-baseline-test b/plugins/woocommerce/changelog/add-k6-baseline-test new file mode 100644 index 00000000000..9d95650169c --- /dev/null +++ b/plugins/woocommerce/changelog/add-k6-baseline-test @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: Change to k6 test scenario + + diff --git a/plugins/woocommerce/changelog/add-product-management-description b/plugins/woocommerce/changelog/add-product-management-description new file mode 100644 index 00000000000..97acec67012 --- /dev/null +++ b/plugins/woocommerce/changelog/add-product-management-description @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add product management description to new product management experience diff --git a/plugins/woocommerce/changelog/add-setup-permalinks b/plugins/woocommerce/changelog/add-setup-permalinks new file mode 100644 index 00000000000..6378e6aef57 --- /dev/null +++ b/plugins/woocommerce/changelog/add-setup-permalinks @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Uses the globa-setup.js to setup permalinks structure diff --git a/plugins/woocommerce/changelog/dev-clean-up-unused-task-properties-methods b/plugins/woocommerce/changelog/dev-clean-up-unused-task-properties-methods new file mode 100644 index 00000000000..245dc1feca4 --- /dev/null +++ b/plugins/woocommerce/changelog/dev-clean-up-unused-task-properties-methods @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Cleanup and deprecate unused Task properties and methods diff --git a/plugins/woocommerce/changelog/e2e-daily-tests-playwright b/plugins/woocommerce/changelog/e2e-daily-tests-playwright new file mode 100644 index 00000000000..6edb1b9980b --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-daily-tests-playwright @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Enable Playwright tests on Daily Smoke Test workflow and upload its Allure reports to S3 bucket. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/e2e-use-e2e-pw-folder-for-outputs b/plugins/woocommerce/changelog/e2e-use-e2e-pw-folder-for-outputs new file mode 100644 index 00000000000..6ef49004835 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-use-e2e-pw-folder-for-outputs @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Use plugins/woocommerce/tests/e2e-pw folder for saving test outputs diff --git a/plugins/woocommerce/changelog/enhancement-35092-replace-trash-icon b/plugins/woocommerce/changelog/enhancement-35092-replace-trash-icon new file mode 100644 index 00000000000..00b27cbc363 --- /dev/null +++ b/plugins/woocommerce/changelog/enhancement-35092-replace-trash-icon @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Replace the trash can icon in the attribute list diff --git a/plugins/woocommerce/changelog/enhancement-35171-product-title b/plugins/woocommerce/changelog/enhancement-35171-product-title new file mode 100644 index 00000000000..5ed684aaeec --- /dev/null +++ b/plugins/woocommerce/changelog/enhancement-35171-product-title @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Change the product info section title to Product Details diff --git a/plugins/woocommerce/changelog/enhancement-35174-remove-placeholders b/plugins/woocommerce/changelog/enhancement-35174-remove-placeholders new file mode 100644 index 00000000000..a4ebb4853b5 --- /dev/null +++ b/plugins/woocommerce/changelog/enhancement-35174-remove-placeholders @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Remove some placeholder values diff --git a/plugins/woocommerce/changelog/enhancement-35188-fix-display-letters b/plugins/woocommerce/changelog/enhancement-35188-fix-display-letters new file mode 100644 index 00000000000..ed7c1c39b94 --- /dev/null +++ b/plugins/woocommerce/changelog/enhancement-35188-fix-display-letters @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Fix the display of letter descenders in the shipping class dropdown menu diff --git a/plugins/woocommerce/changelog/enhancement-35189-increase-gap b/plugins/woocommerce/changelog/enhancement-35189-increase-gap new file mode 100644 index 00000000000..95d6b3736f0 --- /dev/null +++ b/plugins/woocommerce/changelog/enhancement-35189-increase-gap @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Increase the spacing between the shipping box illustration and the dimensions fields diff --git a/plugins/woocommerce/changelog/enhancement-35191-improve-req-opt-labels b/plugins/woocommerce/changelog/enhancement-35191-improve-req-opt-labels new file mode 100644 index 00000000000..81e2302df00 --- /dev/null +++ b/plugins/woocommerce/changelog/enhancement-35191-improve-req-opt-labels @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Improve the communication around required and optional diff --git a/plugins/woocommerce/changelog/fix-103-install-plugin-error-track b/plugins/woocommerce/changelog/fix-103-install-plugin-error-track new file mode 100644 index 00000000000..73059808c1f --- /dev/null +++ b/plugins/woocommerce/changelog/fix-103-install-plugin-error-track @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix invalid wcadmin_install_plugin_error event props diff --git a/plugins/woocommerce/changelog/fix-29844-replay b/plugins/woocommerce/changelog/fix-29844-replay new file mode 100644 index 00000000000..c217cff0d73 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-29844-replay @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Added states for Senegal. diff --git a/plugins/woocommerce/changelog/fix-33537_revert_closing_short_description_by_default b/plugins/woocommerce/changelog/fix-33537_revert_closing_short_description_by_default new file mode 100644 index 00000000000..87159e7afe8 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-33537_revert_closing_short_description_by_default @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Revert change that auto collapses the product short description field. diff --git a/plugins/woocommerce/changelog/fix-34224 b/plugins/woocommerce/changelog/fix-34224 new file mode 100644 index 00000000000..2eabccdcbd0 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-34224 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixed "Unsupported operand types" error. diff --git a/plugins/woocommerce/changelog/fix-34833-slow-onboarding-task-list-query b/plugins/woocommerce/changelog/fix-34833-slow-onboarding-task-list-query new file mode 100644 index 00000000000..981c1a4a9e4 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-34833-slow-onboarding-task-list-query @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Optimize query usage in the Onboarding tasks diff --git a/plugins/woocommerce/changelog/fix-34974-obw-steps-break-via-url b/plugins/woocommerce/changelog/fix-34974-obw-steps-break-via-url new file mode 100644 index 00000000000..5f06728d05e --- /dev/null +++ b/plugins/woocommerce/changelog/fix-34974-obw-steps-break-via-url @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix JS error when the business step is accessed directly via URL without completing the previous steps diff --git a/plugins/woocommerce/changelog/fix-35074-hpos-cpt-admin-redirects b/plugins/woocommerce/changelog/fix-35074-hpos-cpt-admin-redirects new file mode 100644 index 00000000000..9a4fa61de72 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35074-hpos-cpt-admin-redirects @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +When custom order tables are not authoritative, admin UI requests will be redirected to the matching legacy order screen as appropriate. diff --git a/plugins/woocommerce/changelog/fix-35211_shipping_label_banner_add_meta_boxes_function b/plugins/woocommerce/changelog/fix-35211_shipping_label_banner_add_meta_boxes_function new file mode 100644 index 00000000000..a27f7cc1b08 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35211_shipping_label_banner_add_meta_boxes_function @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update ShippingLabelBanner add_meta_box action to only trigger on shop_order pages and remove deprecated function call. diff --git a/plugins/woocommerce/changelog/fix-35414-custom-bulk-actions b/plugins/woocommerce/changelog/fix-35414-custom-bulk-actions new file mode 100644 index 00000000000..8436c71c4a3 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35414-custom-bulk-actions @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Make it possible to add custom bulk action handling to the admin order list screen (when HPOS is enabled). diff --git a/plugins/woocommerce/changelog/fix-cot-env-setup b/plugins/woocommerce/changelog/fix-cot-env-setup new file mode 100644 index 00000000000..7c977609bf4 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-cot-env-setup @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Updated the COT plugin URL now that this feature can be enabled in a different way. diff --git a/plugins/woocommerce/changelog/fix-duplicate_meta b/plugins/woocommerce/changelog/fix-duplicate_meta new file mode 100644 index 00000000000..7eb2163f78e --- /dev/null +++ b/plugins/woocommerce/changelog/fix-duplicate_meta @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Override filter_meta_data method, since it should be a no-op anyway. diff --git a/plugins/woocommerce/changelog/fix-invalid-payment-error-upon-double-click-delete b/plugins/woocommerce/changelog/fix-invalid-payment-error-upon-double-click-delete new file mode 100644 index 00000000000..64a2fd56147 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-invalid-payment-error-upon-double-click-delete @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix 'Invalid payment method' error upon double click on Delete button of Payment methods table diff --git a/plugins/woocommerce/changelog/fix-missing-use-fqcn-wp_error b/plugins/woocommerce/changelog/fix-missing-use-fqcn-wp_error new file mode 100644 index 00000000000..f09f3d26109 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-missing-use-fqcn-wp_error @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +FQCN for WP_Error in PHPDoc. diff --git a/plugins/woocommerce/changelog/fix-misspelled-first-downlaodable-product b/plugins/woocommerce/changelog/fix-misspelled-first-downlaodable-product new file mode 100644 index 00000000000..85864007047 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-misspelled-first-downlaodable-product @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix class name for class FirstDownlaodableProduct diff --git a/plugins/woocommerce/changelog/fix-rich-text-editor-selection b/plugins/woocommerce/changelog/fix-rich-text-editor-selection new file mode 100644 index 00000000000..88bcd847753 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-rich-text-editor-selection @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add placeholder to description field diff --git a/plugins/woocommerce/changelog/fix-select-control-popover-slots b/plugins/woocommerce/changelog/fix-select-control-popover-slots new file mode 100644 index 00000000000..061d348413d --- /dev/null +++ b/plugins/woocommerce/changelog/fix-select-control-popover-slots @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Remove Popover.Slot usage and make use of exported SelectControlMenuSlot. diff --git a/plugins/woocommerce/changelog/fix-skip-failing-api-test b/plugins/woocommerce/changelog/fix-skip-failing-api-test new file mode 100644 index 00000000000..86578e5f08d --- /dev/null +++ b/plugins/woocommerce/changelog/fix-skip-failing-api-test @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Skip flaky settings API test diff --git a/plugins/woocommerce/changelog/fix-test-install-script b/plugins/woocommerce/changelog/fix-test-install-script new file mode 100644 index 00000000000..f6182141871 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-test-install-script @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update unit test install script for db sockets. diff --git a/plugins/woocommerce/changelog/fix-version-typo-form-login b/plugins/woocommerce/changelog/fix-version-typo-form-login new file mode 100644 index 00000000000..04268cb2b3b --- /dev/null +++ b/plugins/woocommerce/changelog/fix-version-typo-form-login @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Fix @version header in form-login.php diff --git a/plugins/woocommerce/changelog/fix-wrong-return-type-get-shipping-tax b/plugins/woocommerce/changelog/fix-wrong-return-type-get-shipping-tax new file mode 100644 index 00000000000..bdc41c8d4e7 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-wrong-return-type-get-shipping-tax @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix inconsistent return type of class WC_Shipping_Rate->get_shipping_tax() diff --git a/plugins/woocommerce/changelog/improve-cat-dashboard-loading-time b/plugins/woocommerce/changelog/improve-cat-dashboard-loading-time new file mode 100644 index 00000000000..8732f051e96 --- /dev/null +++ b/plugins/woocommerce/changelog/improve-cat-dashboard-loading-time @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Improve the loading time of WooCommerce setup widget for large databases diff --git a/plugins/woocommerce/changelog/libyan-dinar b/plugins/woocommerce/changelog/libyan-dinar new file mode 100644 index 00000000000..c97817a14c0 --- /dev/null +++ b/plugins/woocommerce/changelog/libyan-dinar @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Corrects the currency symbol for Libyan Dinar (LYD). diff --git a/plugins/woocommerce/changelog/patch-improve-pr-template-for-testing-instructions b/plugins/woocommerce/changelog/patch-improve-pr-template-for-testing-instructions new file mode 100644 index 00000000000..435ea696c2e --- /dev/null +++ b/plugins/woocommerce/changelog/patch-improve-pr-template-for-testing-instructions @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Tweaks the PR template for GitHub pull requests diff --git a/plugins/woocommerce/changelog/playwright-1_27_1 b/plugins/woocommerce/changelog/playwright-1_27_1 new file mode 100644 index 00000000000..77fabbb7835 --- /dev/null +++ b/plugins/woocommerce/changelog/playwright-1_27_1 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Update Playwright from 1.26.1 to 1.27.1 diff --git a/plugins/woocommerce/changelog/pr-29985 b/plugins/woocommerce/changelog/pr-29985 new file mode 100644 index 00000000000..09bc666d6f0 --- /dev/null +++ b/plugins/woocommerce/changelog/pr-29985 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Added default additional content to emails via filter woocommerce_email_additional_content_. diff --git a/plugins/woocommerce/changelog/pr-34834 b/plugins/woocommerce/changelog/pr-34834 new file mode 100644 index 00000000000..8e62b02db29 --- /dev/null +++ b/plugins/woocommerce/changelog/pr-34834 @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Added Ukrainian subdivisions. diff --git a/plugins/woocommerce/changelog/remove-adding-and-managing-products-note b/plugins/woocommerce/changelog/remove-adding-and-managing-products-note new file mode 100644 index 00000000000..b9cf435de9e --- /dev/null +++ b/plugins/woocommerce/changelog/remove-adding-and-managing-products-note @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove adding and managing products note diff --git a/plugins/woocommerce/changelog/remove-first-downloadable-product-note b/plugins/woocommerce/changelog/remove-first-downloadable-product-note new file mode 100644 index 00000000000..dd51ea60e1f --- /dev/null +++ b/plugins/woocommerce/changelog/remove-first-downloadable-product-note @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove first downloadable product note diff --git a/plugins/woocommerce/changelog/remove-insight-first-product-and-payment-note b/plugins/woocommerce/changelog/remove-insight-first-product-and-payment-note new file mode 100644 index 00000000000..d14bff55fda --- /dev/null +++ b/plugins/woocommerce/changelog/remove-insight-first-product-and-payment-note @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove InsightFirstProductAndPayment note diff --git a/plugins/woocommerce/changelog/remove-insight-first-sale-note b/plugins/woocommerce/changelog/remove-insight-first-sale-note new file mode 100644 index 00000000000..91145d27806 --- /dev/null +++ b/plugins/woocommerce/changelog/remove-insight-first-sale-note @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove insight on first sale note diff --git a/plugins/woocommerce/changelog/remove-manage-store-activity-from-home-screen-note b/plugins/woocommerce/changelog/remove-manage-store-activity-from-home-screen-note new file mode 100644 index 00000000000..81c212c40a3 --- /dev/null +++ b/plugins/woocommerce/changelog/remove-manage-store-activity-from-home-screen-note @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove manage store activity note diff --git a/plugins/woocommerce/changelog/remove-update-store-details-note b/plugins/woocommerce/changelog/remove-update-store-details-note new file mode 100644 index 00000000000..45edb557672 --- /dev/null +++ b/plugins/woocommerce/changelog/remove-update-store-details-note @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove update store details note diff --git a/plugins/woocommerce/changelog/revert-setup-permalinks b/plugins/woocommerce/changelog/revert-setup-permalinks new file mode 100644 index 00000000000..5000ded536d --- /dev/null +++ b/plugins/woocommerce/changelog/revert-setup-permalinks @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Revert the changes introduced in PR #35282 diff --git a/plugins/woocommerce/changelog/try-add-phpcs-changed b/plugins/woocommerce/changelog/try-add-phpcs-changed new file mode 100644 index 00000000000..a235f670cdf --- /dev/null +++ b/plugins/woocommerce/changelog/try-add-phpcs-changed @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Just whitespace change, no entry required + + diff --git a/plugins/woocommerce/changelog/typo b/plugins/woocommerce/changelog/typo new file mode 100644 index 00000000000..01309888727 --- /dev/null +++ b/plugins/woocommerce/changelog/typo @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +typo fix diff --git a/plugins/woocommerce/changelog/uniform-user-customer-search-and-display b/plugins/woocommerce/changelog/uniform-user-customer-search-and-display new file mode 100644 index 00000000000..76817ba3651 --- /dev/null +++ b/plugins/woocommerce/changelog/uniform-user-customer-search-and-display @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Make the user search metabox for orders show the same information for the loaded user and for search results diff --git a/plugins/woocommerce/changelog/update-33538_update_long_description_copy_and_layout b/plugins/woocommerce/changelog/update-33538_update_long_description_copy_and_layout new file mode 100644 index 00000000000..f6970dbec40 --- /dev/null +++ b/plugins/woocommerce/changelog/update-33538_update_long_description_copy_and_layout @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add Product description title in old editor for clarification. diff --git a/plugins/woocommerce/changelog/update-34996 b/plugins/woocommerce/changelog/update-34996 new file mode 100644 index 00000000000..ce04fb51cbc --- /dev/null +++ b/plugins/woocommerce/changelog/update-34996 @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Unwrap product page input props and pass via getInputProps diff --git a/plugins/woocommerce/changelog/update-api-core-tests-shipping-crud-tests b/plugins/woocommerce/changelog/update-api-core-tests-shipping-crud-tests new file mode 100644 index 00000000000..cff840b283b --- /dev/null +++ b/plugins/woocommerce/changelog/update-api-core-tests-shipping-crud-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update playwright api-core-tests for shipping crud operations \ No newline at end of file diff --git a/plugins/woocommerce/changelog/update-array-checks-api-core-tests b/plugins/woocommerce/changelog/update-array-checks-api-core-tests new file mode 100644 index 00000000000..a3f9948da49 --- /dev/null +++ b/plugins/woocommerce/changelog/update-array-checks-api-core-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update Array checks in playwright api-core-tests as some of the existing tests would produce false positives \ No newline at end of file diff --git a/plugins/woocommerce/changelog/update-pw-api-tests-readme b/plugins/woocommerce/changelog/update-pw-api-tests-readme new file mode 100644 index 00000000000..79cdb05228e --- /dev/null +++ b/plugins/woocommerce/changelog/update-pw-api-tests-readme @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update api-core-tests readme for consistency with new command and updates to other commands too. diff --git a/plugins/woocommerce/changelog/update-readme-stable b/plugins/woocommerce/changelog/update-readme-stable new file mode 100644 index 00000000000..2df31d83341 --- /dev/null +++ b/plugins/woocommerce/changelog/update-readme-stable @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Update the README, no changelog required + + diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.2 b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.2 new file mode 100644 index 00000000000..8ba98efe1ee --- /dev/null +++ b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.2 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Comment: Update WooCommerce Blocks to 8.7.2 diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.3 b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.3 new file mode 100644 index 00000000000..640eae41f1a --- /dev/null +++ b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.7.3 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Comment: Update WooCommerce Blocks to 8.7.3 diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-8.9.0 b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.9.0 new file mode 100644 index 00000000000..f49cff81ad5 --- /dev/null +++ b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.9.0 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update WooCommerce Blocks to 8.9.0 diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss index bc570ef2d2e..f50bd3dbf54 100644 --- a/plugins/woocommerce/client/legacy/css/admin.scss +++ b/plugins/woocommerce/client/legacy/css/admin.scss @@ -952,7 +952,43 @@ #variable_product_options #message, #variable_product_options .notice { + display: flex; margin: 10px; + background-color: #FCFAE8; + > p { + width: 85%; + } + .woocommerce-add-variation-price-container { + width: 15%; + display: flex; + justify-content: flex-end; + > button { + align-self: center; + } + } +} + +.woocommerce-set-price-variations { + .woocommerce-usage-modal__wrapper{ + .woocommerce-usage-modal__message { + height: 60px; + flex-wrap: wrap; + display: flex; + > span { + padding-bottom: 16px; + } + } + .woocommerce-usage-modal__actions { + display: flex; + justify-content: flex-end; + margin-top: 20px; + > button{ + margin-left: 16px; + width: 88px; + display: unset; + } + } + } } #variable_product_options { @@ -5090,7 +5126,6 @@ img.help_tip { font-size: 15px; font-weight: 400; margin-right: 0.5em; - visibility: hidden; text-align: center; vertical-align: middle; @@ -5110,6 +5145,9 @@ img.help_tip { color: #777; } } + .edit_variation { + margin-left: 0.5em; + } h3:hover, &.ui-sortable-helper { @@ -5119,6 +5157,14 @@ img.help_tip { } } +.woocommerce_attribute { + h3 { + .sort, a.delete { + visibility: hidden; + } + } +} + .woocommerce_options_panel { min-height: 175px; box-sizing: border-box; @@ -5442,7 +5488,8 @@ img.help_tip { cursor: move; button, - a.delete { + a.delete, + a.edit { float: right; } @@ -5452,7 +5499,17 @@ img.help_tip { line-height: 26px; text-decoration: none; position: relative; - visibility: hidden; + } + + a.edit { + font-weight: normal; + line-height: 26px; + text-decoration: none; + position: relative; + } + + a.remove_variation { + margin: 0 0.5em; } strong { @@ -5484,11 +5541,19 @@ img.help_tip { padding: 0.5em 0.75em 0.5em 1em !important; a.delete, + a.edit, .handlediv, .sort { margin-top: 0.25em; } } + &.woocommerce_variation h3 { + a.delete, + a.edit, + .sort { + margin-top: 0.45em; + } + } h3:hover, &.ui-sortable-helper { @@ -7780,3 +7845,21 @@ table.bar_chart { } } } + +#postdivrich.woocommerce-product-description { + margin-top: 20px; + margin-bottom: 0px; + + .wp-editor-tools { + background: none; + padding-top: 0px; + width: 100%; + } + .wp-editor-wrap { + margin: 6px 12px 0; + } + #post-status-info { + margin: 0px 12px 12px; + width: calc( 100% - 24px ); + } +} \ No newline at end of file diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss new file mode 100644 index 00000000000..862f78ea0ab --- /dev/null +++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss @@ -0,0 +1,1174 @@ +/** +* Fonts +*/ +@font-face { + font-family: star; + src: url(../fonts/star.eot); + src: + url(../fonts/star.eot?#iefix) format("embedded-opentype"), + url(../fonts/star.woff) format("woff"), + url(../fonts/star.ttf) format("truetype"), + url(../fonts/star.svg#star) format("svg"); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: WooCommerce; + src: url(../fonts/WooCommerce.eot); + src: + url(../fonts/WooCommerce.eot?#iefix) format("embedded-opentype"), + url(../fonts/WooCommerce.woff) format("woff"), + url(../fonts/WooCommerce.ttf) format("truetype"), + url(../fonts/WooCommerce.svg#WooCommerce) format("svg"); + font-weight: 400; + font-style: normal; +} + +@import "mixins"; +@import "animation"; + + +/* + Layout fix. + */ +.woocommerce-page { + + main { + // This is to allow .woocommerce div to have width of 1000px on styles with full width layout (such as Pitch). + max-width: calc(1000px + var(--wp--style--root--padding-right) + var(--wp--style--root--padding-left)); + margin-left: auto; + margin-right: auto; + + .woocommerce { + @include clearfix(); + } + } +} + +.theme-twentytwentythree { + .container-colors { + display: flex; + flex-direction: row; + } + + .cube { + width: 20%; + height: 100px; + text-align: center; + vert-align: middle; + } + + .base { + background-color: var(--wp--preset--color--base); + } + + .contrast { + background-color: var(--wp--preset--color--contrast); + color: var(--wp--preset--color--base); + } + + .primary { + background-color: var(--wp--preset--color--primary); + } + + .secondary { + background-color: var(--wp--preset--color--secondary); + } + + .tertiary { + background-color: var(--wp--preset--color--tertiary); + } + + +} + +.woocommerce { + + /* + Common/global + */ + + // Make quantity selector less wide. + .quantity { + input[type="number"] { + width: 3em; + } + + input[type="number"]::-webkit-inner-spin-button, + input[type="number"]::-webkit-outer-spin-button { + opacity: 1; + } + } + + // Breadcrumbs are unnecessary on the shop page. + &.woocommerce-shop .woocommerce-breadcrumb { + display: none; + } + + // Make sure breadcrumbs are not overlapping with Sale badge on the Single product page, etc. + .woocommerce-breadcrumb { + margin-bottom: 1rem; + } + + /* + Notice messages (like 'Added to cart', 'Billing address needs to be filled in', etc. + */ + .woocommerce-message, + .woocommerce-error, + .woocommerce-info { + background-color: rgba(176, 176, 176, 0.6); + color: #222; + border-top-color: var(--wp--preset--color--primary); + border-top-style: solid; + border-top-width: 2px; + padding: 1rem 1.5rem; + margin-bottom: 2rem; + list-style: none; + font-size: var(--wp--preset--font-size--small); + display: flow-root; + + &[role="alert"]::before { + background: #d5d5d5; + color: black; + border-radius: 5rem; + font-size: 1rem; + padding-left: 3px; + padding-right: 3px; + margin-right: 1rem; + } + + a { + color: var(--wp--preset--color--contrast); + + .button { + margin-top: -0.5rem; + border: none; + padding: 0.5rem 1rem; + } + } + } + + .woocommerce-error[role="alert"] { + margin: 0; + + &::before { + content: "X"; + padding-right: 4px; + padding-left: 4px; + } + + li { + display: inline-block; + } + } + + .woocommerce-message { + &[role="alert"]::before { + content: "\2713"; + } + } + + // Checkout notice group styling. + .woocommerce-NoticeGroup-checkout { + ul.woocommerce-error[role="alert"] { + color: var(--wp--preset--color--contrast); + background: var(--wp--preset--color--primary); + + &::before { + display: none; + } + li { + display: inherit; + margin-bottom: 1rem; + } + } + } + + /* + Shop page. + */ + + // Styling the buttons on the Shop page. + a.button, + button[name="add-to-cart"], + input[name="submit"], + button.single_add_to_cart_button, + button[type="submit"]:not(.wp-block-search__button) { + display: inline-block; + text-align: center; + word-break: break-word; + padding: 1rem 2rem; + margin-top: 1rem; + text-decoration: none; + font-size: medium; + cursor: pointer; + + } + + // Style the 'Showing A-B of X results' text. + .woocommerce-result-count { + margin-top: 0; + } + + // The 'order by' dropdown on the Shop page is rather tiny unless the font size is increased. + select.orderby { + font-size: var(--wp--preset--font-size--medium); + } + + // Products. + ul.products { + + padding-inline-start: 0; + display: flex; + align-items: stretch; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + @media only screen and (max-width: 768px) { + justify-content: space-between; + } + + li.product { + list-style: none; + margin-top: var(--wp--style--block-gap); + text-align: center; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + + a.woocommerce-loop-product__link { + text-decoration: none; + display: block; + border: 0; + } + + h2.woocommerce-loop-product__title { + color: var(--wp--preset--color--contrast); + font-family: var(--wp--preset--font-family--system-font); + text-decoration: none; + margin-bottom: 0; + } + + h2.woocommerce-loop-category__title { + font-size: revert; + } + + // Add to cart/Select options/Read more buttons. + a.button { + padding: 0.8rem 10%; + margin-left: auto; + margin-right: auto; + + &.loading { + opacity: 0.5; + } + } + + // View cart link. + a.added_to_cart { + margin: 1rem auto; + } + + } + } + + // Position page numbers under list of products horizontally in the centre of the page. + ul.page-numbers { + text-align: center; + } + + // On sale badge. + span.onsale { + top: -1rem; + right: -1rem; + position: absolute; + background: var(--wp--preset--color--tertiary); + color: var(--wp--preset--color--contrast); + border-radius: 2rem; + line-height: 2.6rem; + font-size: 0.8rem; + padding: 0 0.5rem 0 0.5rem; + } + + /* + Single product page. + */ + + div.product { + position: relative; + + > span.onsale { + position: absolute; + left: -1rem; + top: -1rem; + width: 1.8rem; + z-index: 1; + } + + // How price gets displayed + .entry-summary { + .woocommerce-Price-amount, + del, + .price { + font-size: var(--wp--preset--font-size--large); + } + } + + div.woocommerce-product-gallery { + position: relative; + + a.woocommerce-product-gallery__trigger { + position: absolute; + top: 1rem; + right: 1rem; + z-index: 1; + text-decoration: none; + border-radius: 1rem; + border-style: solid; + line-height: 1.5rem; + padding: 0; + font-size: 0.6rem; + background: var(--wp--preset--color--white); + border-color: var(--wp--preset--color--white); + height: 1.5rem; + width: 1.5rem; + overflow: hidden; + + &::before { + content: url('data:image/svg+xml;utf8,'); + display: block; + transform: rotateY(180deg); + margin-left: 1.55rem; + } + } + + figure.woocommerce-product-gallery__wrapper { + margin: 0; + } + + } + + div.summary { + font-size: 1rem; + + h1.product_title { + font-size: var(--wp--preset--font-size--huge); + margin: 0; + } + + figure.woocommerce-product-gallery__wrapper { + margin: 0; + } + + .woocommerce-product-rating { + .star-rating { + display: inline-block; + } + .woocommerce-review-link { + display: inline-block; + overflow: hidden; + position: relative; + top: -0.5em; + font-size: 1em; + } + } + + .quantity { + display: inline-block; + } + } + + table.variations tr { + + display: table-row; + margin-bottom: 0; + text-align: left; + + td select { + margin: calc(var(--wp--style--block-gap) / 4) 0; + } + } + + .single_variation_wrap { + margin-top: var(--wp--style--block-gap); + } + + button[name="add-to-cart"], + button.single_add_to_cart_button { + margin-top: 0.5rem; + margin-bottom: var(--wp--style--block-gap); + } + + ol.flex-control-thumbs { + padding-left: 0; + float: left; + + li { + list-style: none; + cursor: pointer; + float: left; + width: 18%; + margin-right: 1rem; + } + + } + + a.reset_variations { + margin-left: 0.5em; + } + + table.group_table { + td { + padding-right: 0.5rem; + padding-bottom: 1rem; + } + + margin-bottom: var(--wp--style--block-gap); + } + + .related.products { + margin-top: 7rem; + } + + } + + // Description/Additional info/Reviews tabs. + .woocommerce-tabs { + padding-top: var(--wp--style--block-gap); + + ul.wc-tabs { + padding: 0; + border-bottom-style: solid; + border-bottom-width: 1px; + border-bottom-color: #eae9eb; + + li { + opacity: 0.5; + color: var(--wp--preset--color--contrast); + margin: 0; + padding: 0.5em 1em 0.5em 1em; + border-color: #eae9eb; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + float: left; + border-style: solid; + border-width: 1px; + font-weight: 600; + font-size: var(--wp--preset--font-size--medium); + + &:first-child { + border-left-color: #eae9eb; + margin-left: 1em; + } + + &.active { + background: var(--wp--preset--color--tertiary); + color: var(--wp--preset--color--contrast); + opacity: 1; + } + + a { + text-decoration: none; + color: var(--wp--preset--color--contrast); + } + } + } + + .woocommerce-Tabs-panel { + padding-top: var(--wp--style--block-gap); + font-size: var(--wp--preset--font-size--small); + margin-left: 1em; + + h2 { + display: none; + } + + table.woocommerce-product-attributes { + text-align: left; + } + } + } + + // Reviews tab. + .woocommerce-Reviews { + ol.commentlist { + list-style: none; + padding-left: 0; + + li { + margin-bottom: var(--wp--style--block-gap); + } + + img.avatar { + float: left; + } + + p.meta { + font-size: 1rem; + } + + .comment-text { + display: flow-root; + padding-left: var(--wp--style--block-gap); + + .star-rating { + margin-top: 0; + margin-right: unset; + margin-left: unset; + } + } + } + + .comment-form-rating { + label { + display: inline-block; + padding-right: var(--wp--style--block-gap); + padding-top: var(--wp--style--block-gap); + } + + p.stars { + display: inline; + a::before { + color: var(--wp--preset--color--contrast); + } + } + } + + .comment-form-comment { + label { + float: left; + padding-right: var(--wp--style--block-gap); + } + } + + #review_form_wrapper { + margin-top: 5rem; + } + } + + // Rating: show stars instead of 1, 2, 3, 4, 5. + .star-rating { + overflow: hidden; + position: relative; + height: 1em; + line-height: 1; + width: 5.4rem; + font-family: star; + font-style: normal; + color: var(--wp--preset--color--contrast); + margin: 1rem auto 0.7rem auto; + + &::before { + content: "\73\73\73\73\73"; + float: left; + top: 0; + left: 0; + position: absolute; + font-size: 1rem; + } + + span { + overflow: hidden; + float: left; + top: 0; + left: 0; + position: absolute; + padding-top: 1.5em; + } + + span::before { + content: "\53\53\53\53\53"; + top: 0; + position: absolute; + left: 0; + font-size: 1rem; + } + } + + // Rating stars. + p.stars { + margin-top: 0; + + a { + position: relative; + height: 1em; + width: 1em; + text-indent: -999em; + display: inline-block; + text-decoration: none; + box-shadow: none; + font-style: normal; + + &::before { + display: block; + position: absolute; + top: 0; + left: 0; + width: 1em; + height: 1em; + line-height: 1; + font-family: WooCommerce; + content: "\e021"; + text-indent: 0; + } + + &:hover { + + ~ a::before { + content: "\e021"; + } + } + } + + &:hover { + + a { + + &::before { + content: "\e020"; + } + } + } + + &.selected { + + a.active { + + &::before { + content: "\e020"; + } + + ~ a::before { + content: "\e021"; + } + } + + a:not(.active) { + + &::before { + content: "\e020"; + } + } + } + } + + // Highlights for specific info, e.g. on the My Account > Orders > Order X: Order ___ was placed on ____ + mark { + font-weight: bold; + background-color: transparent; + } + +} + +.woocommerce-page { + .woocommerce-cart-form { + + // Make coupon code input less ginormous. + #coupon_code { + padding: 0 1rem; + } + + .actions { + button.button { + height: initial; + } + } + + // Cart table, aka review of cart items. + table.shop_table_responsive { + + td, + th { + padding: 1rem 0 0.5rem 1rem; + } + + tbody { + + tr:last-of-type { + border-top: none; + } + + @media only screen and (max-width: 768px) { + td { + padding-left: 0; + } + + .product-remove { + text-align: left !important; + } + + #coupon_code { + float: left; + margin-bottom: 1rem; + } + } + } + + .product-remove { + font-size: var(--wp--preset--font-size--large); + + a { + text-decoration: none; + } + } + } + } + + // Elements around "Proceed to Checkout" button. + .cart-collaterals { + margin-top: 1.5rem; + + h2 { + text-transform: uppercase; + font-family: inherit; + } + + table.shop_table_responsive { + + tr { + border-top: none; + } + + th { + width: 11rem; + } + + td, + th { + padding: 1rem 0; + vertical-align: text-top; + } + } + + button[name="calc_shipping"] { + padding: 1rem 2rem; + } + + .woocommerce-Price-amount { + font-weight: normal; + } + } + + // Style the payment gateway selection input--improve the size of the click target, etc + input[type="radio"][name="payment_method"], + input[type="radio"].shipping_method { + display: none; + + & + label { + + &::before { + content: ""; + display: inline-block; + width: 1rem; + height: 1rem; + border: 2px solid var(--wp--preset--color--black); + background: var(--wp--preset--color--white); + margin-left: 4px; + margin-right: 1.2rem; + border-radius: 100%; + transform: translateY(0.2rem); + } + } + + & ~ .payment_box { + padding-left: 3rem; + margin-top: 1rem; + } + + &:checked + label { + + &::before { + background: radial-gradient(circle at center, black 45%, white 0); + } + } + } + + // Style labels like "Remember me?" or "Ship to different address". + label.woocommerce-form__label-for-checkbox { + font-weight: normal; + cursor: pointer; + + span { + + &::before { + content: ""; + display: inline-block; + height: 1rem; + width: 1rem; + border: 2px solid var(--wp--preset--color--black); + background: var(--wp--preset--color--white); + margin-right: 0.5rem; + transform: translateY(0.2rem); + } + } + + input[type="checkbox"] { + display: none; + } + + input[type="checkbox"]:checked + span::before { + background: var(--wp--preset--color--black); + box-shadow: inset 0.2rem 0.2rem var(--wp--preset--color--white), inset -0.2rem -0.2rem var(--wp--preset--color--white); + } + } + + // Cart totals, Cart page table or Order list in My Account. + table.shop_table_responsive { + text-align: left; + + th, + td { + font-size: var(--wp--preset--font-size--small); + font-weight: normal; + } + + th { + padding-bottom: 1rem; + } + + tbody { + + tr { + border-top: 1px solid var(--wp--preset--color--contrast); + } + + td { + a.button, + button { + margin-bottom: 1rem; + padding: 0.5rem 1rem 0.5rem 1rem; + } + + &.woocommerce-orders-table__cell-order-actions { + a.button { + display: block; + + @media only screen and (max-width: 768px) { + width: 50%; + margin-left: auto; + } + } + } + } + } + } + + table.shop_table, + table.shop_table_responsive { + tbody { + .product-name { + + .variation { + dt { + font-style: italic; + margin-right: 0.25rem; + float: left; + } + + dd { + font-style: normal; + + a { + font-style: normal; + } + } + } + } + } + + td, + th { + padding: 0.5rem; + } + } + + // Improve styling of the 'Have a coupon?' section on the checkout page. + form.checkout_coupon { + padding-left: 1.5rem; + // 1.5 rem is to account for extra padding we added above. + width: calc(100% - 1.5rem); + + .form-row { + button[name="apply_coupon"] { + margin-top: 0; + } + } + } + + // Hide the dot to the left of list items as we style the checkboxes: Shipping method and Payment method selection. + ul.wc_payment_methods, + ul.woocommerce-shipping-methods { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; + padding-left: 0; + + input[type="radio"] { + margin-right: 0.6rem; + } + + li.wc_payment_method { + margin-bottom: 1rem; + } + } + + // Layout of the checkout: Billing vs Shipping address, Cart overview, etc. + .woocommerce-checkout, + &.woocommerce-order-pay { + display: table; + + h3 { + font-family: inherit; + font-size: var(--wp--preset--font-size--large); + font-weight: 700; + } + + .col2-set { + width: 43%; + float: right; + } + + .blockUI.blockOverlay { + position: relative; + @include loader(); + } + + #customer_details { + width: 53%; + float: left; + + .col-1, + .col-2 { + width: 100%; + float: none; + } + } + + @media only screen and (max-width: 768px) { + .col2-set, + #customer_details { + width: 100%; + float: none; + } + } + + .woocommerce-billing-fields__field-wrapper, + .woocommerce-checkout-review-order-table, + .woocommerce-checkout-payment, + #payment { + margin-top: 4rem; + } + + .woocommerce-checkout-review-order-table, + #order_review .shop_table { + border-collapse: collapse; + width: 100%; + + thead { + display: none; + } + + th { + text-align: left; + font-weight: normal; + } + + th, + td { + padding: 1rem 1rem 1rem 0; + vertical-align: text-top; + } + + tbody { + border-bottom: 1px solid #d2ced2; + } + + tr.order-total { + border-top: 1px solid #d2ced2; + } + + .product-quantity { + font-weight: normal; + } + + .product-total, + .cart-subtotal, + .order-total, + .tax-rate, + input[type="radio"].shipping_method:checked + label, + input[type="hidden"].shipping_method + label { + .woocommerce-Price-amount { + font-weight: bold; + } + } + } + + button#place_order { + width: 100%; + text-transform: uppercase; + } + } + + /* + Thank you page (after checkout). + */ + + // Adds a tiny bit of vertical spacing on the Thank you (after checkout) page. + .woocommerce-order > * { + margin-bottom: var(--wp--style--block-gap); + } + + // Improves the presentation of order overview (order #, date, email, etc, not the line items) on the Thank you page. + ul.woocommerce-order-overview { + + li { + display: inline; + text-transform: uppercase; + + strong { + text-transform: none; + display: block; + } + } + } + + // Bottom section of the Thank you page---customer details: align, add border, make it look nice. + .woocommerce-customer-details { + address { + border: 1px solid var(--wp--preset--color--black); + font-style: inherit; + + p[class^="woocommerce-customer-details--"] { + &:first-of-type { + margin-top: 2rem; + } + + margin-top: 1rem; + margin-bottom: 0; + } + + .woocommerce-customer-details--phone::before { + content: "\01F4DE"; + margin-right: 1rem; + } + + .woocommerce-customer-details--email::before { + content: "\2709"; + margin-right: 1rem; + font-size: 1.8rem; + } + } + } + + // Better styling for Order line items on the Thank you page: create a table and align it to the left side. + .woocommerce-table--order-details { + border: 1px solid var(--wp--preset--color--black); + + th, + td { + text-align: left; + border-top: 1px solid var(--wp--preset--color--black); + border-bottom: 1px solid var(--wp--preset--color--black); + font-weight: normal; + } + + thead th { + text-transform: uppercase; + } + } +} + +/* + My Account + */ + +.woocommerce-account { + + // Make sure the floated content of My Account section doesn't overlap with the footer. + .woocommerce { + overflow: auto; + + table.woocommerce-table--order-downloads, + table.woocommerce-MyAccount-orders { + thead tr { + border-top: 2px solid var(--wp--preset--color--contrast); + + span { + font-weight: bold; + } + + } + + tbody a.button { + margin: calc(var(--wp--style--block-gap) / 6) 0; + } + } + + .woocommerce-MyAccount-navigation li { + &.is-active a { + &::before { + content: "> "; + } + } + + a { + text-decoration: initial; + + &:hover { + text-decoration: initial; + } + } + } + + + } + + // Make the Log in form on My Account page less wide. + .woocommerce-form-login { + max-width: 516px; + margin: 0 auto; + } + +} + +// TODO: This could look nicer. +.select2-container { + + .select2-selection, + .select2-dropdown { + border: 1px solid var(--wp--preset--color--black); + border-radius: 0; + } + + .select2-dropdown { + border-top: 0; + + .select2-search__field { + border: 1px solid var(--wp--preset--color--black); + border-radius: 0; + } + } +} + + +// Store-wide notice +.theme-twentytwentythree .woocommerce-store-notice { + color: black; + border-top: 2px solid var(--wp--preset--color--primary); + background: lightgray; + padding: 2rem; + position: fixed; + bottom: 0; + left: 0; + width: 100%; + z-index: 999; + margin: 0; + + .woocommerce-store-notice__dismiss-link { + float: right; + margin-right: 4rem; + color: inherit; + } +} diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss index 7b1b7a888a3..37e8d7a01c0 100644 --- a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss +++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss @@ -27,6 +27,7 @@ @import "mixins"; @import "animation"; +@import "variables"; $tt2-gray: #f7f7f7; @@ -187,6 +188,29 @@ $tt2-gray: #f7f7f7; } } + // Moved from blocktheme.css to make sure TT2 won't be changed. + #respond input#submit, + input.button { + // Style primary WooCommerce CTAs in theme colors by default. + background-color: var(--wp--preset--color--foreground, $primary); + color: var(--wp--preset--color--background, $primarytext); + + &:hover { + background-color: var(--wp--preset--color--foreground, $primary); + color: var(--wp--preset--color--background, $primarytext); + } + + &.disabled, + &:disabled, + &:disabled[disabled], + &.disabled:hover, + &:disabled:hover, + &:disabled[disabled]:hover { + background-color: var(--wp--preset--color--foreground, $primary); + color: var(--wp--preset--color--background, $primarytext); + } + } + #respond input#submit.alt, a.button.alt, button.button.alt, @@ -220,6 +244,23 @@ $tt2-gray: #f7f7f7; padding: 1.5rem 3.5rem; } + // Moved from blockthemes.css to make sure TT2 won't be changed. + button.button, + a.button { + background-color: var(--wp--preset--color--foreground, $primary); + color: var(--wp--preset--color--background, $primarytext); + + &.disabled, + &:disabled, + &:disabled[disabled], + &.disabled:hover, + &:disabled:hover, + &:disabled[disabled]:hover { + background-color: var(--wp--preset--color--foreground, $primary); + color: var(--wp--preset--color--background, $primarytext); + } + } + // Shop page .woocommerce-result-count { @@ -420,6 +461,14 @@ $tt2-gray: #f7f7f7; } } + // Moved from blocktheme.scss to retain full styling. + ul.tabs li.active { + // Style active tab in theme colors. + background: var(--wp--preset--color--background, $contentbg); + border-bottom-color: var(--wp--preset--color--background, $contentbg); + } + + .woocommerce-Tabs-panel { padding-top: var(--wp--style--block-gap); font-size: var(--wp--preset--font-size--small); diff --git a/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss b/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss index 00eabcdf3f4..017ba81fb1f 100644 --- a/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss +++ b/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss @@ -17,16 +17,14 @@ } } +.clear { + clear: both; +} + /** * General */ .woocommerce { - mark { - // Style the mark element in theme colors. - // For details see https://github.com/woocommerce/woocommerce/pull/31631. - background-color: var(--wp--preset--color--foreground, $highlight); - color: var(--wp--preset--color--background, $highlightext); - } /** * Buttons @@ -42,8 +40,6 @@ button.button, a.button { - background-color: var(--wp--preset--color--foreground, $primary); - color: var(--wp--preset--color--background, $primarytext); &.disabled, &:disabled, @@ -51,8 +47,6 @@ &.disabled:hover, &:disabled:hover, &:disabled[disabled]:hover { - background-color: var(--wp--preset--color--foreground, $primary); - color: var(--wp--preset--color--background, $primarytext); opacity: 0.5; } } @@ -60,13 +54,8 @@ #respond input#submit, input.button, a.button.alt { - // Style primary WooCommerce CTAs in theme colors by default. - background-color: var(--wp--preset--color--foreground, $primary); - color: var(--wp--preset--color--background, $primarytext); &:hover { - background-color: var(--wp--preset--color--foreground, $primary); - color: var(--wp--preset--color--background, $primarytext); opacity: 0.9; } @@ -76,8 +65,6 @@ &.disabled:hover, &:disabled:hover, &:disabled[disabled]:hover { - background-color: var(--wp--preset--color--foreground, $primary); - color: var(--wp--preset--color--background, $primarytext); opacity: 0.5; } } @@ -114,9 +101,6 @@ .woocommerce-tabs { ul.tabs li.active { - // Style active tab in theme colors. - background: var(--wp--preset--color--background, $contentbg); - border-bottom-color: var(--wp--preset--color--background, $contentbg); &::before { box-shadow: 2px 2px 0 var(--wp--preset--color--background, $contentbg); diff --git a/plugins/woocommerce/client/legacy/css/woocommerce.scss b/plugins/woocommerce/client/legacy/css/woocommerce.scss index da71b3321ed..efa4f5432d6 100644 --- a/plugins/woocommerce/client/legacy/css/woocommerce.scss +++ b/plugins/woocommerce/client/legacy/css/woocommerce.scss @@ -329,6 +329,7 @@ p.demo_store, li { border: 1px solid darken($secondary, 10%); background-color: $secondary; + color: $secondarytext; display: inline-block; position: relative; z-index: 0; @@ -351,6 +352,7 @@ p.demo_store, &.active { background: $contentbg; + color: $secondarytext; z-index: 2; border-bottom-color: $contentbg; diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js index e6949a5fb4b..84eab9d8bd5 100644 --- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js +++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js @@ -1,27 +1,54 @@ /* global wp, woocommerce_admin_meta_boxes_variations, woocommerce_admin, accounting */ -jQuery( function( $ ) { - 'use strict'; +jQuery( function ( $ ) { + 'use strict'; /** * Variations actions */ var wc_meta_boxes_product_variations_actions = { - /** * Initialize variations actions */ - init: function() { + init: function () { $( '#variable_product_options' ) - .on( 'change', 'input.variable_is_downloadable', this.variable_is_downloadable ) - .on( 'change', 'input.variable_is_virtual', this.variable_is_virtual ) - .on( 'change', 'input.variable_manage_stock', this.variable_manage_stock ) + .on( + 'change', + 'input.variable_is_downloadable', + this.variable_is_downloadable + ) + .on( + 'change', + 'input.variable_is_virtual', + this.variable_is_virtual + ) + .on( + 'change', + 'input.variable_manage_stock', + this.variable_manage_stock + ) .on( 'click', 'button.notice-dismiss', this.notice_dismiss ) .on( 'click', 'h3 .sort', this.set_menu_order ) + .on( + 'click', + 'button.add_price_for_variations', + this.open_modal_to_set_variations_price + ) .on( 'reload', this.reload ); - $( 'input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock' ).trigger( 'change' ); - $( '#woocommerce-product-data' ).on( 'woocommerce_variations_loaded', this.variations_loaded ); - $( document.body ).on( 'woocommerce_variations_added', this.variation_added ); + $( + 'input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock' + ).trigger( 'change' ); + $( '#woocommerce-product-data' ).on( + 'woocommerce_variations_loaded', + this.variations_loaded + ); + $( document.body ) + .on( 'woocommerce_variations_added', this.variation_added ) + .on( + 'keyup', + '.wc_input_variations_price', + this.maybe_enable_button_to_add_price_to_variations + ); }, /** @@ -30,7 +57,7 @@ jQuery( function( $ ) { * @param {Object} event * @param {Int} qty */ - reload: function() { + reload: function () { wc_meta_boxes_product_variations_ajax.load_variations( 1 ); wc_meta_boxes_product_variations_pagenav.set_paginav( 0 ); }, @@ -38,47 +65,89 @@ jQuery( function( $ ) { /** * Check if variation is downloadable and show/hide elements */ - variable_is_downloadable: function() { - $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_downloadable' ).hide(); + variable_is_downloadable: function () { + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.show_if_variation_downloadable' ) + .hide(); if ( $( this ).is( ':checked' ) ) { - $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_downloadable' ).show(); + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.show_if_variation_downloadable' ) + .show(); } }, /** * Check if variation is virtual and show/hide elements */ - variable_is_virtual: function() { - $( this ).closest( '.woocommerce_variation' ).find( '.hide_if_variation_virtual' ).show(); + variable_is_virtual: function () { + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.hide_if_variation_virtual' ) + .show(); if ( $( this ).is( ':checked' ) ) { - $( this ).closest( '.woocommerce_variation' ).find( '.hide_if_variation_virtual' ).hide(); + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.hide_if_variation_virtual' ) + .hide(); + } + }, + + /** + * Maybe enable the button to add a price for every variation + */ + maybe_enable_button_to_add_price_to_variations: function () { + var variation_price = parseInt( + $( '.wc_input_variations_price' ).val(), + 10 + ); + if ( isNaN( variation_price ) ) { + $( '.add_variations_price_button' ).prop( 'disabled', true ); + } else { + $( '.add_variations_price_button' ).prop( 'disabled', false ); } }, /** * Check if variation manage stock and show/hide elements */ - variable_manage_stock: function() { - $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_manage_stock' ).hide(); - $( this ).closest( '.woocommerce_variation' ).find( '.variable_stock_status' ).show(); + variable_manage_stock: function () { + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.show_if_variation_manage_stock' ) + .hide(); + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.variable_stock_status' ) + .show(); if ( $( this ).is( ':checked' ) ) { - $( this ).closest( '.woocommerce_variation' ).find( '.show_if_variation_manage_stock' ).show(); - $( this ).closest( '.woocommerce_variation' ).find( '.variable_stock_status' ).hide(); + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.show_if_variation_manage_stock' ) + .show(); + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.variable_stock_status' ) + .hide(); } // Parent level. if ( $( 'input#_manage_stock:checked' ).length ) { - $( this ).closest( '.woocommerce_variation' ).find( '.variable_stock_status' ).hide(); + $( this ) + .closest( '.woocommerce_variation' ) + .find( '.variable_stock_status' ) + .hide(); } }, /** * Notice dismiss */ - notice_dismiss: function() { + notice_dismiss: function () { $( this ).closest( 'div.notice' ).remove(); }, @@ -88,31 +157,43 @@ jQuery( function( $ ) { * @param {Object} event * @param {Int} needsUpdate */ - variations_loaded: function( event, needsUpdate ) { + variations_loaded: function ( event, needsUpdate ) { needsUpdate = needsUpdate || false; var wrapper = $( '#woocommerce-product-data' ); if ( ! needsUpdate ) { // Show/hide downloadable, virtual and stock fields - $( 'input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock', wrapper ).trigger( 'change' ); + $( + 'input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock', + wrapper + ).trigger( 'change' ); // Open sale schedule fields when have some sale price date - $( '.woocommerce_variation', wrapper ).each( function( index, el ) { - var $el = $( el ), + $( '.woocommerce_variation', wrapper ).each( function ( + index, + el + ) { + var $el = $( el ), date_from = $( '.sale_price_dates_from', $el ).val(), - date_to = $( '.sale_price_dates_to', $el ).val(); + date_to = $( '.sale_price_dates_to', $el ).val(); if ( '' !== date_from || '' !== date_to ) { $( 'a.sale_schedule', $el ).trigger( 'click' ); } - }); + } ); // Remove variation-needs-update classes - $( '.woocommerce_variations .variation-needs-update', wrapper ).removeClass( 'variation-needs-update' ); + $( + '.woocommerce_variations .variation-needs-update', + wrapper + ).removeClass( 'variation-needs-update' ); // Disable cancel and save buttons - $( 'button.cancel-variation-changes, button.save-variation-changes', wrapper ).attr( 'disabled', 'disabled' ); + $( + 'button.cancel-variation-changes, button.save-variation-changes', + wrapper + ).attr( 'disabled', 'disabled' ); } // Init TipTip @@ -120,48 +201,53 @@ jQuery( function( $ ) { $( '#tiptip_arrow' ).removeAttr( 'style' ); $( '.woocommerce_variations .tips, ' + - '.woocommerce_variations .help_tip, ' + - '.woocommerce_variations .woocommerce-help-tip, ' + - '.toolbar-variations-defaults .woocommerce-help-tip', + '.woocommerce_variations .help_tip, ' + + '.woocommerce_variations .woocommerce-help-tip, ' + + '.toolbar-variations-defaults .woocommerce-help-tip', wrapper - ) - .tipTip({ - 'attribute': 'data-tip', - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 200 - }); + ).tipTip( { + attribute: 'data-tip', + fadeIn: 50, + fadeOut: 50, + delay: 200, + } ); // Datepicker fields - $( '.sale_price_dates_fields', wrapper ).find( 'input' ).datepicker({ - defaultDate: '', - dateFormat: 'yy-mm-dd', - numberOfMonths: 1, - showButtonPanel: true, - onSelect: function() { - var option = $( this ).is( '.sale_price_dates_from' ) ? 'minDate' : 'maxDate', - dates = $( this ).closest( '.sale_price_dates_fields' ).find( 'input' ), - date = $( this ).datepicker( 'getDate' ); + $( '.sale_price_dates_fields', wrapper ) + .find( 'input' ) + .datepicker( { + defaultDate: '', + dateFormat: 'yy-mm-dd', + numberOfMonths: 1, + showButtonPanel: true, + onSelect: function () { + var option = $( this ).is( '.sale_price_dates_from' ) + ? 'minDate' + : 'maxDate', + dates = $( this ) + .closest( '.sale_price_dates_fields' ) + .find( 'input' ), + date = $( this ).datepicker( 'getDate' ); - dates.not( this ).datepicker( 'option', option, date ); - $( this ).trigger( 'change' ); - } - }); + dates.not( this ).datepicker( 'option', option, date ); + $( this ).trigger( 'change' ); + }, + } ); // Allow sorting - $( '.woocommerce_variations', wrapper ).sortable({ - items: '.woocommerce_variation', - cursor: 'move', - axis: 'y', - handle: '.sort', - scrollSensitivity: 40, + $( '.woocommerce_variations', wrapper ).sortable( { + items: '.woocommerce_variation', + cursor: 'move', + axis: 'y', + handle: '.sort', + scrollSensitivity: 40, forcePlaceholderSize: true, - helper: 'clone', - opacity: 0.65, - stop: function() { - wc_meta_boxes_product_variations_actions.variation_row_indexes(); - } - }); + helper: 'clone', + opacity: 0.65, + stop: function () { + wc_meta_boxes_product_variations_actions.variation_row_indexes(); + }, + } ); $( document.body ).trigger( 'wc-enhanced-select-init' ); }, @@ -172,32 +258,85 @@ jQuery( function( $ ) { * @param {Object} event * @param {Int} qty */ - variation_added: function( event, qty ) { + variation_added: function ( event, qty ) { if ( 1 === qty ) { - wc_meta_boxes_product_variations_actions.variations_loaded( null, true ); + wc_meta_boxes_product_variations_actions.variations_loaded( + null, + true + ); } }, + /** + * Sets a price for every variation + */ + set_variations_price: function () { + var variation_price = $( '.wc_input_variations_price' ).val(); + var product_type = $( 'select#product-type' ).val(); + var input_type = + 'variable-subscription' === product_type + ? 'variable_subscription_sign_up_fee' + : 'variable_regular_price'; + var input = $( `.wc_input_price[name^=${ input_type }]` ); + + // We don't want to override prices already set + input.each( function ( index, el ) { + if ( '0' === $( el ).val() || '' === $( el ).val() ) { + $( el ).val( variation_price ).trigger( 'change' ); + } + } ); + wc_meta_boxes_product_variations_ajax.save_variations(); + }, + + /** + * Opens the modal to set a price for every variation + */ + open_modal_to_set_variations_price: function () { + $( this ).WCBackboneModal( { + template: 'wc-modal-set-price-variations', + } ); + $( '.add_variations_price_button' ).on( + 'click', + wc_meta_boxes_product_variations_actions.set_variations_price + ); + }, + /** * Lets the user manually input menu order to move items around pages */ - set_menu_order: function( event ) { + set_menu_order: function ( event ) { event.preventDefault(); - var $menu_order = $( this ).closest( '.woocommerce_variation' ).find( '.variation_menu_order' ); - var variation_id = $( this ).closest( '.woocommerce_variation' ).find( '.variable_post_id' ).val(); - var value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_menu_order, $menu_order.val() ); + var $menu_order = $( this ) + .closest( '.woocommerce_variation' ) + .find( '.variation_menu_order' ); + var variation_id = $( this ) + .closest( '.woocommerce_variation' ) + .find( '.variable_post_id' ) + .val(); + var value = window.prompt( + woocommerce_admin_meta_boxes_variations.i18n_enter_menu_order, + $menu_order.val() + ); if ( value != null ) { // Set value, save changes and reload view $menu_order.val( parseInt( value, 10 ) ).trigger( 'change' ); - $( this ).closest( '.woocommerce_variation' ) - .append( '' ); + $( this ) + .closest( '.woocommerce_variation' ) + .append( + '' + ); - $( this ).closest( '.woocommerce_variation' ) - .append( '' ); + $( this ) + .closest( '.woocommerce_variation' ) + .append( + '' + ); wc_meta_boxes_product_variations_ajax.save_variations(); } @@ -206,25 +345,40 @@ jQuery( function( $ ) { /** * Set menu order */ - variation_row_indexes: function() { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + variation_row_indexes: function () { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), current_page = parseInt( wrapper.attr( 'data-page' ), 10 ), - offset = parseInt( ( current_page - 1 ) * woocommerce_admin_meta_boxes_variations.variations_per_page, 10 ); + offset = parseInt( + ( current_page - 1 ) * + woocommerce_admin_meta_boxes_variations.variations_per_page, + 10 + ); - $( '.woocommerce_variations .woocommerce_variation' ).each( function ( index, el ) { - $( '.variation_menu_order', el ) - .val( parseInt( $( el ) - .index( '.woocommerce_variations .woocommerce_variation' ), 10 ) + 1 + offset ) - .trigger( 'change' ); - }); - } + $( '.woocommerce_variations .woocommerce_variation' ).each( + function ( index, el ) { + $( '.variation_menu_order', el ) + .val( + parseInt( + $( el ).index( + '.woocommerce_variations .woocommerce_variation' + ), + 10 + ) + + 1 + + offset + ) + .trigger( 'change' ); + } + ); + }, }; /** * Variations media actions */ var wc_meta_boxes_product_variations_media = { - /** * wp.media frame object * @@ -256,8 +410,12 @@ jQuery( function( $ ) { /** * Initialize media actions */ - init: function() { - $( '#variable_product_options' ).on( 'click', '.upload_image_button', this.add_image ); + init: function () { + $( '#variable_product_options' ).on( + 'click', + '.upload_image_button', + this.add_image + ); $( 'a.add_media' ).on( 'click', this.restore_wp_media_post_id ); }, @@ -266,64 +424,101 @@ jQuery( function( $ ) { * * @param {Object} event */ - add_image: function( event ) { + add_image: function ( event ) { var $button = $( this ), post_id = $button.attr( 'rel' ), $parent = $button.closest( '.upload_image' ); - wc_meta_boxes_product_variations_media.setting_variation_image = $parent; + wc_meta_boxes_product_variations_media.setting_variation_image = $parent; wc_meta_boxes_product_variations_media.setting_variation_image_id = post_id; event.preventDefault(); if ( $button.is( '.remove' ) ) { - - $( '.upload_image_id', wc_meta_boxes_product_variations_media.setting_variation_image ).val( '' ).trigger( 'change' ); - wc_meta_boxes_product_variations_media.setting_variation_image.find( 'img' ).eq( 0 ) - .attr( 'src', woocommerce_admin_meta_boxes_variations.woocommerce_placeholder_img_src ); - wc_meta_boxes_product_variations_media.setting_variation_image.find( '.upload_image_button' ).removeClass( 'remove' ); - + $( + '.upload_image_id', + wc_meta_boxes_product_variations_media.setting_variation_image + ) + .val( '' ) + .trigger( 'change' ); + wc_meta_boxes_product_variations_media.setting_variation_image + .find( 'img' ) + .eq( 0 ) + .attr( + 'src', + woocommerce_admin_meta_boxes_variations.woocommerce_placeholder_img_src + ); + wc_meta_boxes_product_variations_media.setting_variation_image + .find( '.upload_image_button' ) + .removeClass( 'remove' ); } else { - // If the media frame already exists, reopen it. - if ( wc_meta_boxes_product_variations_media.variable_image_frame ) { - wc_meta_boxes_product_variations_media.variable_image_frame.uploader.uploader - .param( 'post_id', wc_meta_boxes_product_variations_media.setting_variation_image_id ); + if ( + wc_meta_boxes_product_variations_media.variable_image_frame + ) { + wc_meta_boxes_product_variations_media.variable_image_frame.uploader.uploader.param( + 'post_id', + wc_meta_boxes_product_variations_media.setting_variation_image_id + ); wc_meta_boxes_product_variations_media.variable_image_frame.open(); return; } else { - wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.setting_variation_image_id; + wp.media.model.settings.post.id = + wc_meta_boxes_product_variations_media.setting_variation_image_id; } // Create the media frame. - wc_meta_boxes_product_variations_media.variable_image_frame = wp.media.frames.variable_image = wp.media({ - // Set the title of the modal. - title: woocommerce_admin_meta_boxes_variations.i18n_choose_image, - button: { - text: woocommerce_admin_meta_boxes_variations.i18n_set_image - }, - states: [ - new wp.media.controller.Library({ - title: woocommerce_admin_meta_boxes_variations.i18n_choose_image, - filterable: 'all' - }) - ] - }); + wc_meta_boxes_product_variations_media.variable_image_frame = wp.media.frames.variable_image = wp.media( + { + // Set the title of the modal. + title: + woocommerce_admin_meta_boxes_variations.i18n_choose_image, + button: { + text: + woocommerce_admin_meta_boxes_variations.i18n_set_image, + }, + states: [ + new wp.media.controller.Library( { + title: + woocommerce_admin_meta_boxes_variations.i18n_choose_image, + filterable: 'all', + } ), + ], + } + ); // When an image is selected, run a callback. - wc_meta_boxes_product_variations_media.variable_image_frame.on( 'select', function () { + wc_meta_boxes_product_variations_media.variable_image_frame.on( + 'select', + function () { + var attachment = wc_meta_boxes_product_variations_media.variable_image_frame + .state() + .get( 'selection' ) + .first() + .toJSON(), + url = + attachment.sizes && attachment.sizes.thumbnail + ? attachment.sizes.thumbnail.url + : attachment.url; - var attachment = wc_meta_boxes_product_variations_media.variable_image_frame.state() - .get( 'selection' ).first().toJSON(), - url = attachment.sizes && attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url; + $( + '.upload_image_id', + wc_meta_boxes_product_variations_media.setting_variation_image + ) + .val( attachment.id ) + .trigger( 'change' ); + wc_meta_boxes_product_variations_media.setting_variation_image + .find( '.upload_image_button' ) + .addClass( 'remove' ); + wc_meta_boxes_product_variations_media.setting_variation_image + .find( 'img' ) + .eq( 0 ) + .attr( 'src', url ); - $( '.upload_image_id', wc_meta_boxes_product_variations_media.setting_variation_image ).val( attachment.id ) - .trigger( 'change' ); - wc_meta_boxes_product_variations_media.setting_variation_image.find( '.upload_image_button' ).addClass( 'remove' ); - wc_meta_boxes_product_variations_media.setting_variation_image.find( 'img' ).eq( 0 ).attr( 'src', url ); - - wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.wp_media_post_id; - }); + wp.media.model.settings.post.id = + wc_meta_boxes_product_variations_media.wp_media_post_id; + } + ); // Finally, open the modal. wc_meta_boxes_product_variations_media.variable_image_frame.open(); @@ -333,41 +528,65 @@ jQuery( function( $ ) { /** * Restore wp.media post ID. */ - restore_wp_media_post_id: function() { - wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.wp_media_post_id; - } + restore_wp_media_post_id: function () { + wp.media.model.settings.post.id = + wc_meta_boxes_product_variations_media.wp_media_post_id; + }, }; /** * Product variations metabox ajax methods */ var wc_meta_boxes_product_variations_ajax = { - /** * Initialize variations ajax methods */ - init: function() { + init: function () { $( 'li.variations_tab a' ).on( 'click', this.initial_load ); $( '#variable_product_options' ) - .on( 'click', 'button.save-variation-changes', this.save_variations ) - .on( 'click', 'button.cancel-variation-changes', this.cancel_variations ) + .on( + 'click', + 'button.save-variation-changes', + this.save_variations + ) + .on( + 'click', + 'button.cancel-variation-changes', + this.cancel_variations + ) .on( 'click', '.remove_variation', this.remove_variation ) - .on( 'click','.downloadable_files a.delete', this.input_changed ); + .on( + 'click', + '.downloadable_files a.delete', + this.input_changed + ); $( document.body ) - .on( 'change input', '#variable_product_options .woocommerce_variations :input', this.input_changed ) - .on( 'change', '.variations-defaults select', this.defaults_changed ); + .on( + 'change input', + '#variable_product_options .woocommerce_variations :input', + this.input_changed + ) + .on( + 'change', + '.variations-defaults select', + this.defaults_changed + ); var postForm = $( 'form#post' ); postForm.on( 'submit', this.save_on_submit ); - $( 'input:submit', postForm ).on( 'click keypress', function() { + $( 'input:submit', postForm ).on( 'click keypress', function () { postForm.data( 'callerid', this.id ); - }); + } ); - $( '.wc-metaboxes-wrapper' ).on( 'click', 'a.do_variation_action', this.do_variation_action ); + $( '.wc-metaboxes-wrapper' ).on( + 'click', + 'a.do_variation_action', + this.do_variation_action + ); }, /** @@ -375,11 +594,17 @@ jQuery( function( $ ) { * * @return {Bool} */ - check_for_changes: function() { - var need_update = $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ); + check_for_changes: function () { + var need_update = $( '#variable_product_options' ).find( + '.woocommerce_variations .variation-needs-update' + ); if ( 0 < need_update.length ) { - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_edited_variations ) ) { + if ( + window.confirm( + woocommerce_admin_meta_boxes_variations.i18n_edited_variations + ) + ) { wc_meta_boxes_product_variations_ajax.save_changes(); } else { need_update.removeClass( 'variation-needs-update' ); @@ -393,20 +618,20 @@ jQuery( function( $ ) { /** * Block edit screen */ - block: function() { - $( '#woocommerce-product-data' ).block({ + block: function () { + $( '#woocommerce-product-data' ).block( { message: null, overlayCSS: { background: '#fff', - opacity: 0.6 - } - }); + opacity: 0.6, + }, + } ); }, /** * Unblock edit screen */ - unblock: function() { + unblock: function () { $( '#woocommerce-product-data' ).unblock(); }, @@ -415,8 +640,13 @@ jQuery( function( $ ) { * * @return {Bool} */ - initial_load: function() { - if ( 0 === $( '#variable_product_options' ).find( '.woocommerce_variations .woocommerce_variation' ).length ) { + initial_load: function () { + if ( + 0 === + $( '#variable_product_options' ).find( + '.woocommerce_variations .woocommerce_variation' + ).length + ) { wc_meta_boxes_product_variations_pagenav.go_to_page(); } }, @@ -427,33 +657,43 @@ jQuery( function( $ ) { * @param {Int} page (default: 1) * @param {Int} per_page (default: 10) */ - load_variations: function( page, per_page ) { - page = page || 1; - per_page = per_page || woocommerce_admin_meta_boxes_variations.variations_per_page; + load_variations: function ( page, per_page ) { + page = page || 1; + per_page = + per_page || + woocommerce_admin_meta_boxes_variations.variations_per_page; - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ); + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ); wc_meta_boxes_product_variations_ajax.block(); - $.ajax({ + $.ajax( { url: woocommerce_admin_meta_boxes_variations.ajax_url, data: { - action: 'woocommerce_load_variations', - security: woocommerce_admin_meta_boxes_variations.load_variations_nonce, + action: 'woocommerce_load_variations', + security: + woocommerce_admin_meta_boxes_variations.load_variations_nonce, product_id: woocommerce_admin_meta_boxes_variations.post_id, attributes: wrapper.data( 'attributes' ), - page: page, - per_page: per_page + page: page, + per_page: per_page, }, type: 'POST', - success: function( response ) { - wrapper.empty().append( response ).attr( 'data-page', page ); + success: function ( response ) { + wrapper + .empty() + .append( response ) + .attr( 'data-page', page ); - $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_loaded' ); + $( '#woocommerce-product-data' ).trigger( + 'woocommerce_variations_loaded' + ); wc_meta_boxes_product_variations_ajax.unblock(); - } - }); + }, + } ); }, /** @@ -463,13 +703,16 @@ jQuery( function( $ ) { * * @return {Object} */ - get_variations_fields: function( fields ) { + get_variations_fields: function ( fields ) { var data = $( ':input', fields ).serializeJSON(); - $( '.variations-defaults select' ).each( function( index, element ) { + $( '.variations-defaults select' ).each( function ( + index, + element + ) { var select = $( element ); data[ select.attr( 'name' ) ] = select.val(); - }); + } ); return data; }, @@ -479,39 +722,49 @@ jQuery( function( $ ) { * * @param {Function} callback Called once saving is complete */ - save_changes: function( callback ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + save_changes: function ( callback ) { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), need_update = $( '.variation-needs-update', wrapper ), - data = {}; + data = {}; // Save only with products need update. if ( 0 < need_update.length ) { wc_meta_boxes_product_variations_ajax.block(); - data = wc_meta_boxes_product_variations_ajax.get_variations_fields( need_update ); - data.action = 'woocommerce_save_variations'; - data.security = woocommerce_admin_meta_boxes_variations.save_variations_nonce; - data.product_id = woocommerce_admin_meta_boxes_variations.post_id; - data['product-type'] = $( '#product-type' ).val(); + data = wc_meta_boxes_product_variations_ajax.get_variations_fields( + need_update + ); + data.action = 'woocommerce_save_variations'; + data.security = + woocommerce_admin_meta_boxes_variations.save_variations_nonce; + data.product_id = + woocommerce_admin_meta_boxes_variations.post_id; + data[ 'product-type' ] = $( '#product-type' ).val(); - $.ajax({ + $.ajax( { url: woocommerce_admin_meta_boxes_variations.ajax_url, data: data, type: 'POST', - success: function( response ) { + success: function ( response ) { // Allow change page, delete and add new variations need_update.removeClass( 'variation-needs-update' ); - $( 'button.cancel-variation-changes, button.save-variation-changes' ).attr( 'disabled', 'disabled' ); + $( + 'button.cancel-variation-changes, button.save-variation-changes' + ).attr( 'disabled', 'disabled' ); - $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_saved' ); + $( '#woocommerce-product-data' ).trigger( + 'woocommerce_variations_saved' + ); if ( typeof callback === 'function' ) { callback( response ); } wc_meta_boxes_product_variations_ajax.unblock(); - } - }); + }, + } ); } }, @@ -520,25 +773,33 @@ jQuery( function( $ ) { * * @return {Bool} */ - save_variations: function() { - $( '#variable_product_options' ).trigger( 'woocommerce_variations_save_variations_button' ); + save_variations: function () { + $( '#variable_product_options' ).trigger( + 'woocommerce_variations_save_variations_button' + ); - wc_meta_boxes_product_variations_ajax.save_changes( function( error ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + wc_meta_boxes_product_variations_ajax.save_changes( function ( + error + ) { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), current = wrapper.attr( 'data-page' ); - $( '#variable_product_options' ).find( '#woocommerce_errors' ).remove(); + $( '#variable_product_options' ) + .find( '#woocommerce_errors' ) + .remove(); if ( error ) { wrapper.before( error ); } - $( '.variations-defaults select' ).each( function() { + $( '.variations-defaults select' ).each( function () { $( this ).attr( 'data-current', $( this ).val() ); - }); + } ); wc_meta_boxes_product_variations_pagenav.go_to_page( current ); - }); + } ); return false; }, @@ -546,27 +807,41 @@ jQuery( function( $ ) { /** * Save on post form submit */ - save_on_submit: function( e ) { - var need_update = $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ); + save_on_submit: function ( e ) { + var need_update = $( '#variable_product_options' ).find( + '.woocommerce_variations .variation-needs-update' + ); if ( 0 < need_update.length ) { e.preventDefault(); - $( '#variable_product_options' ).trigger( 'woocommerce_variations_save_variations_on_submit' ); - wc_meta_boxes_product_variations_ajax.save_changes( wc_meta_boxes_product_variations_ajax.save_on_submit_done ); + $( '#variable_product_options' ).trigger( + 'woocommerce_variations_save_variations_on_submit' + ); + wc_meta_boxes_product_variations_ajax.save_changes( + wc_meta_boxes_product_variations_ajax.save_on_submit_done + ); } }, /** * After saved, continue with form submission */ - save_on_submit_done: function() { + save_on_submit_done: function () { var postForm = $( 'form#post' ), callerid = postForm.data( 'callerid' ); if ( 'publish' === callerid ) { - postForm.append('').trigger( 'submit' ); + postForm + .append( + '' + ) + .trigger( 'submit' ); } else { - postForm.append('').trigger( 'submit' ); + postForm + .append( + '' + ) + .trigger( 'submit' ); } }, @@ -575,14 +850,20 @@ jQuery( function( $ ) { * * @return {Bool} */ - cancel_variations: function() { - var current = parseInt( $( '#variable_product_options' ).find( '.woocommerce_variations' ).attr( 'data-page' ), 10 ); + cancel_variations: function () { + var current = parseInt( + $( '#variable_product_options' ) + .find( '.woocommerce_variations' ) + .attr( 'data-page' ), + 10 + ); - $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ) + $( '#variable_product_options' ) + .find( '.woocommerce_variations .variation-needs-update' ) .removeClass( 'variation-needs-update' ); - $( '.variations-defaults select' ).each( function() { + $( '.variations-defaults select' ).each( function () { $( this ).val( $( this ).attr( 'data-current' ) ); - }); + } ); wc_meta_boxes_product_variations_pagenav.go_to_page( current ); @@ -594,26 +875,38 @@ jQuery( function( $ ) { * * @return {Bool} */ - add_variation: function() { + add_variation: function () { wc_meta_boxes_product_variations_ajax.block(); var data = { action: 'woocommerce_add_variation', post_id: woocommerce_admin_meta_boxes_variations.post_id, loop: $( '.woocommerce_variation' ).length, - security: woocommerce_admin_meta_boxes_variations.add_variation_nonce + security: + woocommerce_admin_meta_boxes_variations.add_variation_nonce, }; - $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function( response ) { - var variation = $( response ); - variation.addClass( 'variation-needs-update' ); + $.post( + woocommerce_admin_meta_boxes_variations.ajax_url, + data, + function ( response ) { + var variation = $( response ); + variation.addClass( 'variation-needs-update' ); - $( '.woocommerce-notice-invalid-variation' ).remove(); - $( '#variable_product_options' ).find( '.woocommerce_variations' ).prepend( variation ); - $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); - $( '#variable_product_options' ).trigger( 'woocommerce_variations_added', 1 ); - wc_meta_boxes_product_variations_ajax.unblock(); - }); + $( '.woocommerce-notice-invalid-variation' ).remove(); + $( '#variable_product_options' ) + .find( '.woocommerce_variations' ) + .prepend( variation ); + $( + 'button.cancel-variation-changes, button.save-variation-changes' + ).prop( 'disabled', false ); + $( '#variable_product_options' ).trigger( + 'woocommerce_variations_added', + 1 + ); + wc_meta_boxes_product_variations_ajax.unblock(); + } + ); return false; }, @@ -623,14 +916,18 @@ jQuery( function( $ ) { * * @return {Bool} */ - remove_variation: function() { + remove_variation: function () { wc_meta_boxes_product_variations_ajax.check_for_changes(); - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_remove_variation ) ) { - var variation = $( this ).attr( 'rel' ), + if ( + window.confirm( + woocommerce_admin_meta_boxes_variations.i18n_remove_variation + ) + ) { + var variation = $( this ).attr( 'rel' ), variation_ids = [], - data = { - action: 'woocommerce_remove_variations' + data = { + action: 'woocommerce_remove_variations', }; wc_meta_boxes_product_variations_ajax.block(); @@ -639,27 +936,52 @@ jQuery( function( $ ) { variation_ids.push( variation ); data.variation_ids = variation_ids; - data.security = woocommerce_admin_meta_boxes_variations.delete_variations_nonce; + data.security = + woocommerce_admin_meta_boxes_variations.delete_variations_nonce; - $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function() { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - current_page = parseInt( wrapper.attr( 'data-page' ), 10 ), - total_pages = Math.ceil( ( - parseInt( wrapper.attr( 'data-total' ), 10 ) - 1 - ) / woocommerce_admin_meta_boxes_variations.variations_per_page ), - page = 1; + $.post( + woocommerce_admin_meta_boxes_variations.ajax_url, + data, + function () { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), + current_page = parseInt( + wrapper.attr( 'data-page' ), + 10 + ), + total_pages = Math.ceil( + ( parseInt( + wrapper.attr( 'data-total' ), + 10 + ) - + 1 ) / + woocommerce_admin_meta_boxes_variations.variations_per_page + ), + page = 1; - $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_removed' ); + $( '#woocommerce-product-data' ).trigger( + 'woocommerce_variations_removed' + ); - if ( current_page === total_pages || current_page <= total_pages ) { - page = current_page; - } else if ( current_page > total_pages && 0 !== total_pages ) { - page = total_pages; + if ( + current_page === total_pages || + current_page <= total_pages + ) { + page = current_page; + } else if ( + current_page > total_pages && + 0 !== total_pages + ) { + page = total_pages; + } + + wc_meta_boxes_product_variations_pagenav.go_to_page( + page, + -1 + ); } - - wc_meta_boxes_product_variations_pagenav.go_to_page( page, -1 ); - }); - + ); } else { wc_meta_boxes_product_variations_ajax.unblock(); } @@ -673,36 +995,61 @@ jQuery( function( $ ) { * * @return {Bool} */ - link_all_variations: function() { + link_all_variations: function () { wc_meta_boxes_product_variations_ajax.check_for_changes(); - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_link_all_variations ) ) { + if ( + window.confirm( + woocommerce_admin_meta_boxes_variations.i18n_link_all_variations + ) + ) { wc_meta_boxes_product_variations_ajax.block(); var data = { action: 'woocommerce_link_all_variations', post_id: woocommerce_admin_meta_boxes_variations.post_id, - security: woocommerce_admin_meta_boxes_variations.link_variation_nonce + security: + woocommerce_admin_meta_boxes_variations.link_variation_nonce, }; - $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function( response ) { - var count = parseInt( response, 10 ); + $.post( + woocommerce_admin_meta_boxes_variations.ajax_url, + data, + function ( response ) { + var count = parseInt( response, 10 ); - if ( 1 === count ) { - window.alert( count + ' ' + woocommerce_admin_meta_boxes_variations.i18n_variation_added ); - } else if ( 0 === count || count > 1 ) { - window.alert( count + ' ' + woocommerce_admin_meta_boxes_variations.i18n_variations_added ); - } else { - window.alert( woocommerce_admin_meta_boxes_variations.i18n_no_variations_added ); - } + if ( 1 === count ) { + window.alert( + count + + ' ' + + woocommerce_admin_meta_boxes_variations.i18n_variation_added + ); + } else if ( 0 === count || count > 1 ) { + window.alert( + count + + ' ' + + woocommerce_admin_meta_boxes_variations.i18n_variations_added + ); + } else { + window.alert( + woocommerce_admin_meta_boxes_variations.i18n_no_variations_added + ); + } - if ( count > 0 ) { - wc_meta_boxes_product_variations_pagenav.go_to_page( 1, count ); - $( '#variable_product_options' ).trigger( 'woocommerce_variations_added', count ); - } else { - wc_meta_boxes_product_variations_ajax.unblock(); + if ( count > 0 ) { + wc_meta_boxes_product_variations_pagenav.go_to_page( + 1, + count + ); + $( '#variable_product_options' ).trigger( + 'woocommerce_variations_added', + count + ); + } else { + wc_meta_boxes_product_variations_ajax.unblock(); + } } - }); + ); } return false; @@ -711,87 +1058,119 @@ jQuery( function( $ ) { /** * Add new class when have changes in some input */ - input_changed: function( event ) { + input_changed: function ( event ) { $( this ) .closest( '.woocommerce_variation' ) .addClass( 'variation-needs-update' ); - $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); + $( + 'button.cancel-variation-changes, button.save-variation-changes' + ).prop( 'disabled', false ); // Do not trigger 'woocommerce_variations_input_changed' for 'input' events for backwards compat. if ( 'input' === event.type && $( this ).is( ':text' ) ) { return; } - $( '#variable_product_options' ).trigger( 'woocommerce_variations_input_changed' ); + $( '#variable_product_options' ).trigger( + 'woocommerce_variations_input_changed' + ); }, /** * Added new .variation-needs-update class when defaults is changed */ - defaults_changed: function() { + defaults_changed: function () { $( this ) .closest( '#variable_product_options' ) .find( '.woocommerce_variation:first' ) .addClass( 'variation-needs-update' ); - $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); + $( + 'button.cancel-variation-changes, button.save-variation-changes' + ).prop( 'disabled', false ); - $( '#variable_product_options' ).trigger( 'woocommerce_variations_defaults_changed' ); + $( '#variable_product_options' ).trigger( + 'woocommerce_variations_defaults_changed' + ); }, /** * Actions */ - do_variation_action: function() { + do_variation_action: function () { var do_variation_action = $( 'select.variation_actions' ).val(), - data = {}, - changes = 0, + data = {}, + changes = 0, value; switch ( do_variation_action ) { - case 'add_variation' : + case 'add_variation': wc_meta_boxes_product_variations_ajax.add_variation(); return; - case 'link_all_variations' : + case 'link_all_variations': wc_meta_boxes_product_variations_ajax.link_all_variations(); return; - case 'delete_all' : - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_delete_all_variations ) ) { - if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_last_warning ) ) { + case 'delete_all': + if ( + window.confirm( + woocommerce_admin_meta_boxes_variations.i18n_delete_all_variations + ) + ) { + if ( + window.confirm( + woocommerce_admin_meta_boxes_variations.i18n_last_warning + ) + ) { data.allowed = true; - changes = parseInt( $( '#variable_product_options' ).find( '.woocommerce_variations' ) - .attr( 'data-total' ), 10 ) * -1; + changes = + parseInt( + $( '#variable_product_options' ) + .find( '.woocommerce_variations' ) + .attr( 'data-total' ), + 10 + ) * -1; } } break; - case 'variable_regular_price_increase' : - case 'variable_regular_price_decrease' : - case 'variable_sale_price_increase' : - case 'variable_sale_price_decrease' : - value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_a_value_fixed_or_percent ); + case 'variable_regular_price_increase': + case 'variable_regular_price_decrease': + case 'variable_sale_price_increase': + case 'variable_sale_price_decrease': + value = window.prompt( + woocommerce_admin_meta_boxes_variations.i18n_enter_a_value_fixed_or_percent + ); if ( value != null ) { if ( value.indexOf( '%' ) >= 0 ) { - data.value = accounting.unformat( value.replace( /\%/, '' ), woocommerce_admin.mon_decimal_point ) + '%'; + data.value = + accounting.unformat( + value.replace( /\%/, '' ), + woocommerce_admin.mon_decimal_point + ) + '%'; } else { - data.value = accounting.unformat( value, woocommerce_admin.mon_decimal_point ); + data.value = accounting.unformat( + value, + woocommerce_admin.mon_decimal_point + ); } } else { return; } break; - case 'variable_regular_price' : - case 'variable_sale_price' : - case 'variable_stock' : - case 'variable_low_stock_amount' : - case 'variable_weight' : - case 'variable_length' : - case 'variable_width' : - case 'variable_height' : - case 'variable_download_limit' : - case 'variable_download_expiry' : - value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_a_value ); + case 'variable_regular_price': + case 'variable_sale_price': + case 'variable_stock': + case 'variable_low_stock_amount': + case 'variable_weight': + case 'variable_length': + case 'variable_width': + case 'variable_height': + case 'variable_download_limit': + case 'variable_download_expiry': + value = window.prompt( + woocommerce_admin_meta_boxes_variations.i18n_enter_a_value + ); if ( value != null ) { data.value = value; @@ -799,9 +1178,13 @@ jQuery( function( $ ) { return; } break; - case 'variable_sale_schedule' : - data.date_from = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_start ); - data.date_to = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_end ); + case 'variable_sale_schedule': + data.date_from = window.prompt( + woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_start + ); + data.date_to = window.prompt( + woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_end + ); if ( null === data.date_from ) { data.date_from = false; @@ -815,9 +1198,14 @@ jQuery( function( $ ) { return; } break; - default : - $( 'select.variation_actions' ).trigger( do_variation_action ); - data = $( 'select.variation_actions' ).triggerHandler( do_variation_action + '_ajax_data', data ); + default: + $( 'select.variation_actions' ).trigger( + do_variation_action + ); + data = $( 'select.variation_actions' ).triggerHandler( + do_variation_action + '_ajax_data', + data + ); if ( null === data ) { return; @@ -826,47 +1214,67 @@ jQuery( function( $ ) { } if ( 'delete_all' === do_variation_action && data.allowed ) { - $( '#variable_product_options' ).find( '.variation-needs-update' ).removeClass( 'variation-needs-update' ); + $( '#variable_product_options' ) + .find( '.variation-needs-update' ) + .removeClass( 'variation-needs-update' ); } else { wc_meta_boxes_product_variations_ajax.check_for_changes(); } wc_meta_boxes_product_variations_ajax.block(); - $.ajax({ + $.ajax( { url: woocommerce_admin_meta_boxes_variations.ajax_url, data: { - action: 'woocommerce_bulk_edit_variations', - security: woocommerce_admin_meta_boxes_variations.bulk_edit_variations_nonce, - product_id: woocommerce_admin_meta_boxes_variations.post_id, + action: 'woocommerce_bulk_edit_variations', + security: + woocommerce_admin_meta_boxes_variations.bulk_edit_variations_nonce, + product_id: woocommerce_admin_meta_boxes_variations.post_id, product_type: $( '#product-type' ).val(), - bulk_action: do_variation_action, - data: data + bulk_action: do_variation_action, + data: data, }, type: 'POST', - success: function() { - wc_meta_boxes_product_variations_pagenav.go_to_page( 1, changes ); - } - }); - } + success: function () { + wc_meta_boxes_product_variations_pagenav.go_to_page( + 1, + changes + ); + }, + } ); + }, }; /** * Product variations pagenav */ var wc_meta_boxes_product_variations_pagenav = { - /** * Initialize products variations meta box */ - init: function() { + init: function () { $( document.body ) - .on( 'woocommerce_variations_added', this.update_single_quantity ) - .on( 'change', '.variations-pagenav .page-selector', this.page_selector ) - .on( 'click', '.variations-pagenav .first-page', this.first_page ) + .on( + 'woocommerce_variations_added', + this.update_single_quantity + ) + .on( + 'change', + '.variations-pagenav .page-selector', + this.page_selector + ) + .on( + 'click', + '.variations-pagenav .first-page', + this.first_page + ) .on( 'click', '.variations-pagenav .prev-page', this.prev_page ) .on( 'click', '.variations-pagenav .next-page', this.next_page ) - .on( 'click', '.variations-pagenav .last-page', this.last_page ); + .on( + 'click', + '.variations-pagenav .last-page', + this.last_page + ); }, /** @@ -876,18 +1284,30 @@ jQuery( function( $ ) { * * @return {Int} */ - update_variations_count: function( qty ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - total = parseInt( wrapper.attr( 'data-total' ), 10 ) + qty, + update_variations_count: function ( qty ) { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), + total = parseInt( wrapper.attr( 'data-total' ), 10 ) + qty, displaying_num = $( '.variations-pagenav .displaying-num' ); // Set the new total of variations wrapper.attr( 'data-total', total ); if ( 1 === total ) { - displaying_num.text( woocommerce_admin_meta_boxes_variations.i18n_variation_count_single.replace( '%qty%', total ) ); + displaying_num.text( + woocommerce_admin_meta_boxes_variations.i18n_variation_count_single.replace( + '%qty%', + total + ) + ); } else { - displaying_num.text( woocommerce_admin_meta_boxes_variations.i18n_variation_count_plural.replace( '%qty%', total ) ); + displaying_num.text( + woocommerce_admin_meta_boxes_variations.i18n_variation_count_plural.replace( + '%qty%', + total + ) + ); } return total; @@ -899,11 +1319,13 @@ jQuery( function( $ ) { * @param {Object} event * @param {Int} qty */ - update_single_quantity: function( event, qty ) { + update_single_quantity: function ( event, qty ) { if ( 1 === qty ) { var page_nav = $( '.variations-pagenav' ); - wc_meta_boxes_product_variations_pagenav.update_variations_count( qty ); + wc_meta_boxes_product_variations_pagenav.update_variations_count( + qty + ); if ( page_nav.is( ':hidden' ) ) { $( 'option, optgroup', '.variation_actions' ).show(); @@ -920,15 +1342,22 @@ jQuery( function( $ ) { * * @param {Int} qty */ - set_paginav: function( qty ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - new_qty = wc_meta_boxes_product_variations_pagenav.update_variations_count( qty ), - toolbar = $( '#variable_product_options' ).find( '.toolbar' ), + set_paginav: function ( qty ) { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), + new_qty = wc_meta_boxes_product_variations_pagenav.update_variations_count( + qty + ), + toolbar = $( '#variable_product_options' ).find( '.toolbar' ), variation_action = $( '.variation_actions' ), - page_nav = $( '.variations-pagenav' ), + page_nav = $( '.variations-pagenav' ), displaying_links = $( '.pagination-links', page_nav ), - total_pages = Math.ceil( new_qty / woocommerce_admin_meta_boxes_variations.variations_per_page ), - options = ''; + total_pages = Math.ceil( + new_qty / + woocommerce_admin_meta_boxes_variations.variations_per_page + ), + options = ''; // Set the new total of pages wrapper.attr( 'data-total_pages', total_pages ); @@ -949,7 +1378,6 @@ jQuery( function( $ ) { $( 'option, optgroup', variation_action ).hide(); $( '.variation_actions' ).val( 'add_variation' ); $( 'option[data-global="true"]', variation_action ).show(); - } else { toolbar.show(); page_nav.show(); @@ -970,18 +1398,18 @@ jQuery( function( $ ) { * * @return {Bool} */ - check_is_enabled: function( current ) { + check_is_enabled: function ( current ) { return ! $( current ).hasClass( 'disabled' ); }, /** * Change "disabled" class on pagenav */ - change_classes: function( selected, total ) { + change_classes: function ( selected, total ) { var first_page = $( '.variations-pagenav .first-page' ), - prev_page = $( '.variations-pagenav .prev-page' ), - next_page = $( '.variations-pagenav .next-page' ), - last_page = $( '.variations-pagenav .last-page' ); + prev_page = $( '.variations-pagenav .prev-page' ), + next_page = $( '.variations-pagenav .next-page' ), + last_page = $( '.variations-pagenav .last-page' ); if ( 1 === selected ) { first_page.addClass( 'disabled' ); @@ -1003,8 +1431,11 @@ jQuery( function( $ ) { /** * Set page */ - set_page: function( page ) { - $( '.variations-pagenav .page-selector' ).val( page ).first().trigger( 'change' ); + set_page: function ( page ) { + $( '.variations-pagenav .page-selector' ) + .val( page ) + .first() + .trigger( 'change' ); }, /** @@ -1013,9 +1444,9 @@ jQuery( function( $ ) { * @param {Int} page * @param {Int} qty */ - go_to_page: function( page, qty ) { + go_to_page: function ( page, qty ) { page = page || 1; - qty = qty || 0; + qty = qty || 0; wc_meta_boxes_product_variations_pagenav.set_paginav( qty ); wc_meta_boxes_product_variations_pagenav.set_page( page ); @@ -1024,14 +1455,19 @@ jQuery( function( $ ) { /** * Paginav pagination selector */ - page_selector: function() { + page_selector: function () { var selected = parseInt( $( this ).val(), 10 ), - wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ); + wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ); $( '.variations-pagenav .page-selector' ).val( selected ); wc_meta_boxes_product_variations_ajax.check_for_changes(); - wc_meta_boxes_product_variations_pagenav.change_classes( selected, parseInt( wrapper.attr( 'data-total_pages' ), 10 ) ); + wc_meta_boxes_product_variations_pagenav.change_classes( + selected, + parseInt( wrapper.attr( 'data-total_pages' ), 10 ) + ); wc_meta_boxes_product_variations_ajax.load_variations( selected ); }, @@ -1040,8 +1476,12 @@ jQuery( function( $ ) { * * @return {Bool} */ - first_page: function() { - if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { + first_page: function () { + if ( + wc_meta_boxes_product_variations_pagenav.check_is_enabled( + this + ) + ) { wc_meta_boxes_product_variations_pagenav.set_page( 1 ); } @@ -1053,11 +1493,17 @@ jQuery( function( $ ) { * * @return {Bool} */ - prev_page: function() { - if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + prev_page: function () { + if ( + wc_meta_boxes_product_variations_pagenav.check_is_enabled( + this + ) + ) { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), prev_page = parseInt( wrapper.attr( 'data-page' ), 10 ) - 1, - new_page = ( 0 < prev_page ) ? prev_page : 1; + new_page = 0 < prev_page ? prev_page : 1; wc_meta_boxes_product_variations_pagenav.set_page( new_page ); } @@ -1070,12 +1516,22 @@ jQuery( function( $ ) { * * @return {Bool} */ - next_page: function() { - if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { - var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), - total_pages = parseInt( wrapper.attr( 'data-total_pages' ), 10 ), - next_page = parseInt( wrapper.attr( 'data-page' ), 10 ) + 1, - new_page = ( total_pages >= next_page ) ? next_page : total_pages; + next_page: function () { + if ( + wc_meta_boxes_product_variations_pagenav.check_is_enabled( + this + ) + ) { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ), + total_pages = parseInt( + wrapper.attr( 'data-total_pages' ), + 10 + ), + next_page = parseInt( wrapper.attr( 'data-page' ), 10 ) + 1, + new_page = + total_pages >= next_page ? next_page : total_pages; wc_meta_boxes_product_variations_pagenav.set_page( new_page ); } @@ -1088,20 +1544,25 @@ jQuery( function( $ ) { * * @return {Bool} */ - last_page: function() { - if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { - var last_page = $( '#variable_product_options' ).find( '.woocommerce_variations' ).attr( 'data-total_pages' ); + last_page: function () { + if ( + wc_meta_boxes_product_variations_pagenav.check_is_enabled( + this + ) + ) { + var last_page = $( '#variable_product_options' ) + .find( '.woocommerce_variations' ) + .attr( 'data-total_pages' ); wc_meta_boxes_product_variations_pagenav.set_page( last_page ); } return false; - } + }, }; wc_meta_boxes_product_variations_actions.init(); wc_meta_boxes_product_variations_media.init(); wc_meta_boxes_product_variations_ajax.init(); wc_meta_boxes_product_variations_pagenav.init(); - -}); +} ); diff --git a/plugins/woocommerce/client/legacy/js/admin/product-editor.js b/plugins/woocommerce/client/legacy/js/admin/product-editor.js new file mode 100644 index 00000000000..8f2bafef16a --- /dev/null +++ b/plugins/woocommerce/client/legacy/js/admin/product-editor.js @@ -0,0 +1,15 @@ +/* global woocommerce_admin_product_editor */ +jQuery( function ( $ ) { + $( function () { + var editorWrapper = $( '#postdivrich' ); + + if ( editorWrapper.length ) { + editorWrapper.addClass( 'postbox woocommerce-product-description' ); + editorWrapper.prepend( + '

' + ); + } + } ); +} ); diff --git a/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js b/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js index 7db81de1724..4b34a0d1302 100644 --- a/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js +++ b/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js @@ -1,32 +1,32 @@ /* global woocommerce_admin */ -( function( $, woocommerce_admin ) { - $( function() { +( function ( $, woocommerce_admin ) { + $( function () { if ( 'undefined' === typeof woocommerce_admin ) { return; } // Add buttons to product screen. var $product_screen = $( '.edit-php.post-type-product' ), - $title_action = $product_screen.find( '.page-title-action:first' ), - $blankslate = $product_screen.find( '.woocommerce-BlankState' ); + $title_action = $product_screen.find( '.page-title-action:first' ), + $blankslate = $product_screen.find( '.woocommerce-BlankState' ); if ( 0 === $blankslate.length ) { if ( woocommerce_admin.urls.export_products ) { $title_action.after( '' + - woocommerce_admin.strings.export_products + - '' + woocommerce_admin.urls.export_products + + '" class="page-title-action">' + + woocommerce_admin.strings.export_products + + '' ); } if ( woocommerce_admin.urls.import_products ) { $title_action.after( '' + - woocommerce_admin.strings.import_products + - '' + woocommerce_admin.urls.import_products + + '" class="page-title-action">' + + woocommerce_admin.strings.import_products + + '' ); } } else { @@ -34,60 +34,103 @@ } // Progress indicators when showing steps. - $( '.woocommerce-progress-form-wrapper .button-next' ).on( 'click', function() { - $('.wc-progress-form-content').block({ - message: null, - overlayCSS: { - background: '#fff', - opacity: 0.6 - } - }); - return true; - } ); + $( '.woocommerce-progress-form-wrapper .button-next' ).on( + 'click', + function () { + $( '.wc-progress-form-content' ).block( { + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6, + }, + } ); + return true; + } + ); // Field validation error tips $( document.body ) - - .on( 'wc_add_error_tip', function( e, element, error_type ) { + .on( 'wc_add_error_tip', function ( e, element, error_type ) { var offset = element.position(); if ( element.parent().find( '.wc_error_tip' ).length === 0 ) { - element.after( '
' + woocommerce_admin[error_type] + '
' ); - element.parent().find( '.wc_error_tip' ) - .css( 'left', offset.left + element.width() - ( element.width() / 2 ) - ( $( '.wc_error_tip' ).width() / 2 ) ) + element.after( + '
' + + woocommerce_admin[ error_type ] + + '
' + ); + element + .parent() + .find( '.wc_error_tip' ) + .css( + 'left', + offset.left + + element.width() - + element.width() / 2 - + $( '.wc_error_tip' ).width() / 2 + ) .css( 'top', offset.top + element.height() ) .fadeIn( '100' ); } - }) + } ) - .on( 'wc_remove_error_tip', function( e, element, error_type ) { - element.parent().find( '.wc_error_tip.' + error_type ).fadeOut( '100', function() { $( this ).remove(); } ); - }) + .on( 'wc_remove_error_tip', function ( e, element, error_type ) { + element + .parent() + .find( '.wc_error_tip.' + error_type ) + .fadeOut( '100', function () { + $( this ).remove(); + } ); + } ) - .on( 'click', function() { - $( '.wc_error_tip' ).fadeOut( '100', function() { $( this ).remove(); } ); - }) + .on( 'click', function () { + $( '.wc_error_tip' ).fadeOut( '100', function () { + $( this ).remove(); + } ); + } ) - .on( 'blur', '.wc_input_decimal[type=text], .wc_input_price[type=text], .wc_input_country_iso[type=text]', function() { - $( '.wc_error_tip' ).fadeOut( '100', function() { $( this ).remove(); } ); - }) + .on( + 'blur', + '.wc_input_decimal[type=text], .wc_input_price[type=text], .wc_input_country_iso[type=text]', + function () { + $( '.wc_error_tip' ).fadeOut( '100', function () { + $( this ).remove(); + } ); + } + ) .on( 'change', - '.wc_input_price[type=text], .wc_input_decimal[type=text], .wc-order-totals #refund_amount[type=text]', - function() { - var regex, decimalRegex, + '.wc_input_price[type=text], .wc_input_decimal[type=text], .wc-order-totals #refund_amount[type=text], ' + + '.wc_input_variations_price[type=text]', + function () { + var regex, + decimalRegex, decimailPoint = woocommerce_admin.decimal_point; - if ( $( this ).is( '.wc_input_price' ) || $( this ).is( '#refund_amount' ) ) { + if ( + $( this ).is( '.wc_input_price' ) || + $( this ).is( '.wc_input_variations_price' ) || + $( this ).is( '#refund_amount' ) + ) { decimailPoint = woocommerce_admin.mon_decimal_point; } - regex = new RegExp( '[^\-0-9\%\\' + decimailPoint + ']+', 'gi' ); - decimalRegex = new RegExp( '\\' + decimailPoint + '+', 'gi' ); + regex = new RegExp( + '[^-0-9%\\' + decimailPoint + ']+', + 'gi' + ); + decimalRegex = new RegExp( + '\\' + decimailPoint + '+', + 'gi' + ); - var value = $( this ).val(); - var newvalue = value.replace( regex, '' ).replace( decimalRegex, decimailPoint ); + var value = $( this ).val(); + var newvalue = value + .replace( regex, '' ) + .replace( decimalRegex, decimailPoint ); if ( value !== newvalue ) { $( this ).val( newvalue ); @@ -98,120 +141,197 @@ .on( 'keyup', // eslint-disable-next-line max-len - '.wc_input_price[type=text], .wc_input_decimal[type=text], .wc_input_country_iso[type=text], .wc-order-totals #refund_amount[type=text]', - function() { + '.wc_input_price[type=text], .wc_input_decimal[type=text], .wc_input_country_iso[type=text], .wc-order-totals #refund_amount[type=text], .wc_input_variations_price[type=text]', + function () { var regex, error, decimalRegex; var checkDecimalNumbers = false; - - if ( $( this ).is( '.wc_input_price' ) || $( this ).is( '#refund_amount' ) ) { + if ( + $( this ).is( '.wc_input_price' ) || + $( this ).is( '.wc_input_variations_price' ) || + $( this ).is( '#refund_amount' ) + ) { checkDecimalNumbers = true; - regex = new RegExp( '[^\-0-9\%\\' + woocommerce_admin.mon_decimal_point + ']+', 'gi' ); - decimalRegex = new RegExp( '[^\\' + woocommerce_admin.mon_decimal_point + ']', 'gi' ); + regex = new RegExp( + '[^-0-9%\\' + + woocommerce_admin.mon_decimal_point + + ']+', + 'gi' + ); + decimalRegex = new RegExp( + '[^\\' + woocommerce_admin.mon_decimal_point + ']', + 'gi' + ); error = 'i18n_mon_decimal_error'; } else if ( $( this ).is( '.wc_input_country_iso' ) ) { regex = new RegExp( '([^A-Z])+|(.){3,}', 'im' ); error = 'i18n_country_iso_error'; } else { checkDecimalNumbers = true; - regex = new RegExp( '[^\-0-9\%\\' + woocommerce_admin.decimal_point + ']+', 'gi' ); - decimalRegex = new RegExp( '[^\\' + woocommerce_admin.decimal_point + ']', 'gi' ); + regex = new RegExp( + '[^-0-9%\\' + + woocommerce_admin.decimal_point + + ']+', + 'gi' + ); + decimalRegex = new RegExp( + '[^\\' + woocommerce_admin.decimal_point + ']', + 'gi' + ); error = 'i18n_decimal_error'; } - var value = $( this ).val(); + var value = $( this ).val(); var newvalue = value.replace( regex, '' ); // Check if newvalue have more than one decimal point. - if ( checkDecimalNumbers && 1 < newvalue.replace( decimalRegex, '' ).length ) { + if ( + checkDecimalNumbers && + 1 < newvalue.replace( decimalRegex, '' ).length + ) { newvalue = newvalue.replace( decimalRegex, '' ); } if ( value !== newvalue ) { - $( document.body ).triggerHandler( 'wc_add_error_tip', [ $( this ), error ] ); + $( document.body ).triggerHandler( 'wc_add_error_tip', [ + $( this ), + error, + ] ); } else { - $( document.body ).triggerHandler( 'wc_remove_error_tip', [ $( this ), error ] ); + $( + document.body + ).triggerHandler( 'wc_remove_error_tip', [ + $( this ), + error, + ] ); } } ) - .on( 'change', '#_sale_price.wc_input_price[type=text], .wc_input_price[name^=variable_sale_price]', function() { - var sale_price_field = $( this ), regular_price_field; + .on( + 'change', + '#_sale_price.wc_input_price[type=text], .wc_input_price[name^=variable_sale_price]', + function () { + var sale_price_field = $( this ), + regular_price_field; - if ( sale_price_field.attr( 'name' ).indexOf( 'variable' ) !== -1 ) { - regular_price_field = sale_price_field - .parents( '.variable_pricing' ) - .find( '.wc_input_price[name^=variable_regular_price]' ); - } else { - regular_price_field = $( '#_regular_price' ); + if ( + sale_price_field + .attr( 'name' ) + .indexOf( 'variable' ) !== -1 + ) { + regular_price_field = sale_price_field + .parents( '.variable_pricing' ) + .find( + '.wc_input_price[name^=variable_regular_price]' + ); + } else { + regular_price_field = $( '#_regular_price' ); + } + + var sale_price = parseFloat( + window.accounting.unformat( + sale_price_field.val(), + woocommerce_admin.mon_decimal_point + ) + ); + var regular_price = parseFloat( + window.accounting.unformat( + regular_price_field.val(), + woocommerce_admin.mon_decimal_point + ) + ); + + if ( sale_price >= regular_price ) { + $( this ).val( '' ); + } } + ) - var sale_price = parseFloat( - window.accounting.unformat( sale_price_field.val(), woocommerce_admin.mon_decimal_point ) - ); - var regular_price = parseFloat( - window.accounting.unformat( regular_price_field.val(), woocommerce_admin.mon_decimal_point ) - ); + .on( + 'keyup', + '#_sale_price.wc_input_price[type=text], .wc_input_price[name^=variable_sale_price]', + function () { + var sale_price_field = $( this ), + regular_price_field; - if ( sale_price >= regular_price ) { - $( this ).val( '' ); + if ( + sale_price_field + .attr( 'name' ) + .indexOf( 'variable' ) !== -1 + ) { + regular_price_field = sale_price_field + .parents( '.variable_pricing' ) + .find( + '.wc_input_price[name^=variable_regular_price]' + ); + } else { + regular_price_field = $( '#_regular_price' ); + } + + var sale_price = parseFloat( + window.accounting.unformat( + sale_price_field.val(), + woocommerce_admin.mon_decimal_point + ) + ); + var regular_price = parseFloat( + window.accounting.unformat( + regular_price_field.val(), + woocommerce_admin.mon_decimal_point + ) + ); + + if ( sale_price >= regular_price ) { + $( document.body ).triggerHandler( 'wc_add_error_tip', [ + $( this ), + 'i18n_sale_less_than_regular_error', + ] ); + } else { + $( + document.body + ).triggerHandler( 'wc_remove_error_tip', [ + $( this ), + 'i18n_sale_less_than_regular_error', + ] ); + } } - }) - - .on( 'keyup', '#_sale_price.wc_input_price[type=text], .wc_input_price[name^=variable_sale_price]', function() { - var sale_price_field = $( this ), regular_price_field; - - if ( sale_price_field.attr( 'name' ).indexOf( 'variable' ) !== -1 ) { - regular_price_field = sale_price_field - .parents( '.variable_pricing' ) - .find( '.wc_input_price[name^=variable_regular_price]' ); - } else { - regular_price_field = $( '#_regular_price' ); - } - - var sale_price = parseFloat( - window.accounting.unformat( sale_price_field.val(), woocommerce_admin.mon_decimal_point ) - ); - var regular_price = parseFloat( - window.accounting.unformat( regular_price_field.val(), woocommerce_admin.mon_decimal_point ) - ); - - if ( sale_price >= regular_price ) { - $( document.body ).triggerHandler( 'wc_add_error_tip', [ $(this), 'i18n_sale_less_than_regular_error' ] ); - } else { - $( document.body ).triggerHandler( 'wc_remove_error_tip', [ $(this), 'i18n_sale_less_than_regular_error' ] ); - } - }) - - .on( 'init_tooltips', function() { + ) + .on( 'init_tooltips', function () { $( '.tips, .help_tip, .woocommerce-help-tip' ).tipTip( { - 'attribute': 'data-tip', - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 200, - 'keepAlive': true + attribute: 'data-tip', + fadeIn: 50, + fadeOut: 50, + delay: 200, + keepAlive: true, } ); $( '.column-wc_actions .wc-action-button' ).tipTip( { - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 200 + fadeIn: 50, + fadeOut: 50, + delay: 200, } ); // Add tiptip to parent element for widefat tables - $( '.parent-tips' ).each( function() { - $( this ).closest( 'a, th' ).attr( 'data-tip', $( this ).data( 'tip' ) ).tipTip( { - 'attribute': 'data-tip', - 'fadeIn': 50, - 'fadeOut': 50, - 'delay': 200, - 'keepAlive': true - } ).css( 'cursor', 'help' ); - }); - }) + $( '.parent-tips' ).each( function () { + $( this ) + .closest( 'a, th' ) + .attr( 'data-tip', $( this ).data( 'tip' ) ) + .tipTip( { + attribute: 'data-tip', + fadeIn: 50, + fadeOut: 50, + delay: 200, + keepAlive: true, + } ) + .css( 'cursor', 'help' ); + } ); + } ) - .on( 'click', '.wc-confirm-delete', function( event ) { - if ( ! window.confirm( woocommerce_admin.i18n_confirm_delete ) ) { + .on( 'click', '.wc-confirm-delete', function ( event ) { + if ( + ! window.confirm( woocommerce_admin.i18n_confirm_delete ) + ) { event.stopPropagation(); } } ); @@ -220,7 +340,7 @@ $( document.body ).trigger( 'init_tooltips' ); // wc_input_table tables - $( '.wc_input_table.sortable tbody' ).sortable({ + $( '.wc_input_table.sortable tbody' ).sortable( { items: 'tr', cursor: 'move', axis: 'y', @@ -229,205 +349,290 @@ helper: 'clone', opacity: 0.65, placeholder: 'wc-metabox-sortable-placeholder', - start: function( event, ui ) { + start: function ( event, ui ) { ui.item.css( 'background-color', '#f6f6f6' ); }, - stop: function( event, ui ) { + stop: function ( event, ui ) { ui.item.removeAttr( 'style' ); - } - }); + }, + } ); // Focus on inputs within the table if clicked instead of trying to sort. - $( '.wc_input_table.sortable tbody input' ).on( 'click', function() { + $( '.wc_input_table.sortable tbody input' ).on( 'click', function () { $( this ).trigger( 'focus' ); } ); - $( '.wc_input_table .remove_rows' ).on( 'click', function() { + $( '.wc_input_table .remove_rows' ).on( 'click', function () { var $tbody = $( this ).closest( '.wc_input_table' ).find( 'tbody' ); if ( $tbody.find( 'tr.current' ).length > 0 ) { var $current = $tbody.find( 'tr.current' ); - $current.each( function() { + $current.each( function () { $( this ).remove(); - }); + } ); } return false; - }); + } ); var controlled = false; - var shifted = false; - var hasFocus = false; + var shifted = false; + var hasFocus = false; - $( document.body ).on( 'keyup keydown', function( e ) { - shifted = e.shiftKey; + $( document.body ).on( 'keyup keydown', function ( e ) { + shifted = e.shiftKey; controlled = e.ctrlKey || e.metaKey; - }); + } ); - $( '.wc_input_table' ).on( 'focus click', 'input', function( e ) { - var $this_table = $( this ).closest( 'table, tbody' ); - var $this_row = $( this ).closest( 'tr' ); + $( '.wc_input_table' ) + .on( 'focus click', 'input', function ( e ) { + var $this_table = $( this ).closest( 'table, tbody' ); + var $this_row = $( this ).closest( 'tr' ); - if ( ( e.type === 'focus' && hasFocus !== $this_row.index() ) || ( e.type === 'click' && $( this ).is( ':focus' ) ) ) { - hasFocus = $this_row.index(); + if ( + ( e.type === 'focus' && hasFocus !== $this_row.index() ) || + ( e.type === 'click' && $( this ).is( ':focus' ) ) + ) { + hasFocus = $this_row.index(); - if ( ! shifted && ! controlled ) { - $( 'tr', $this_table ).removeClass( 'current' ).removeClass( 'last_selected' ); - $this_row.addClass( 'current' ).addClass( 'last_selected' ); - } else if ( shifted ) { - $( 'tr', $this_table ).removeClass( 'current' ); - $this_row.addClass( 'selected_now' ).addClass( 'current' ); + if ( ! shifted && ! controlled ) { + $( 'tr', $this_table ) + .removeClass( 'current' ) + .removeClass( 'last_selected' ); + $this_row + .addClass( 'current' ) + .addClass( 'last_selected' ); + } else if ( shifted ) { + $( 'tr', $this_table ).removeClass( 'current' ); + $this_row + .addClass( 'selected_now' ) + .addClass( 'current' ); - if ( $( 'tr.last_selected', $this_table ).length > 0 ) { - if ( $this_row.index() > $( 'tr.last_selected', $this_table ).index() ) { - $( 'tr', $this_table ) - .slice( $( 'tr.last_selected', $this_table ).index(), $this_row.index() ) - .addClass( 'current' ); + if ( $( 'tr.last_selected', $this_table ).length > 0 ) { + if ( + $this_row.index() > + $( 'tr.last_selected', $this_table ).index() + ) { + $( 'tr', $this_table ) + .slice( + $( + 'tr.last_selected', + $this_table + ).index(), + $this_row.index() + ) + .addClass( 'current' ); + } else { + $( 'tr', $this_table ) + .slice( + $this_row.index(), + $( + 'tr.last_selected', + $this_table + ).index() + 1 + ) + .addClass( 'current' ); + } + } + + $( 'tr', $this_table ).removeClass( 'last_selected' ); + $this_row.addClass( 'last_selected' ); + } else { + $( 'tr', $this_table ).removeClass( 'last_selected' ); + if ( + controlled && + $( this ).closest( 'tr' ).is( '.current' ) + ) { + $this_row.removeClass( 'current' ); } else { - $( 'tr', $this_table ) - .slice( $this_row.index(), $( 'tr.last_selected', $this_table ).index() + 1 ) - .addClass( 'current' ); + $this_row + .addClass( 'current' ) + .addClass( 'last_selected' ); } } - $( 'tr', $this_table ).removeClass( 'last_selected' ); - $this_row.addClass( 'last_selected' ); - } else { - $( 'tr', $this_table ).removeClass( 'last_selected' ); - if ( controlled && $( this ).closest( 'tr' ).is( '.current' ) ) { - $this_row.removeClass( 'current' ); - } else { - $this_row.addClass( 'current' ).addClass( 'last_selected' ); - } - } - - $( 'tr', $this_table ).removeClass( 'selected_now' ); - } - }).on( 'blur', 'input', function() { - hasFocus = false; - }); - - // Additional cost and Attribute term tables - $( '.woocommerce_page_wc-settings .shippingrows tbody tr:even, table.attributes-table tbody tr:nth-child(odd)' ) - .addClass( 'alternate' ); - - // Show order items on orders page - $( document.body ).on( 'click', '.show_order_items', function() { - $( this ).closest( 'td' ).find( 'table' ).toggle(); - return false; - }); - - // Select availability - $( 'select.availability' ).on( 'change', function() { - if ( $( this ).val() === 'all' ) { - $( this ).closest( 'tr' ).next( 'tr' ).hide(); - } else { - $( this ).closest( 'tr' ).next( 'tr' ).show(); - } - }).trigger( 'change' ); - - // Hidden options - $( '.hide_options_if_checked' ).each( function() { - $( this ).find( 'input:eq(0)' ).on( 'change', function() { - if ( $( this ).is( ':checked' ) ) { - $( this ) - .closest( 'fieldset, tr' ) - .nextUntil( '.hide_options_if_checked, .show_options_if_checked', '.hidden_option' ) - .hide(); - } else { - $( this ) - .closest( 'fieldset, tr' ) - .nextUntil( '.hide_options_if_checked, .show_options_if_checked', '.hidden_option' ) - .show(); - } - }).trigger( 'change' ); - }); - - $( '.show_options_if_checked' ).each( function() { - $( this ).find( 'input:eq(0)' ).on( 'change', function() { - if ( $( this ).is( ':checked' ) ) { - $( this ) - .closest( 'fieldset, tr' ) - .nextUntil( '.hide_options_if_checked, .show_options_if_checked', '.hidden_option' ) - .show(); - } else { - $( this ) - .closest( 'fieldset, tr' ) - .nextUntil( '.hide_options_if_checked, .show_options_if_checked', '.hidden_option' ) - .hide(); - } - }).trigger( 'change' ); - }); - - // Reviews. - $( 'input#woocommerce_enable_reviews' ).on( 'change', function() { - if ( $( this ).is( ':checked' ) ) { - $( '#woocommerce_enable_review_rating' ).closest( 'tr' ).show(); - } else { - $( '#woocommerce_enable_review_rating' ).closest( 'tr' ).hide(); - } - }).trigger( 'change' ); - - // Attribute term table - $( 'table.attributes-table tbody tr:nth-child(odd)' ).addClass( 'alternate' ); - - // Toggle gateway on/off. - $( '.wc_gateways' ).on( 'click', '.wc-payment-gateway-method-toggle-enabled', function() { - var $link = $( this ), - $row = $link.closest( 'tr' ), - $toggle = $link.find( '.woocommerce-input-toggle' ); - - var data = { - action: 'woocommerce_toggle_gateway_enabled', - security: woocommerce_admin.nonces.gateway_toggle, - gateway_id: $row.data( 'gateway_id' ) - }; - - $toggle.addClass( 'woocommerce-input-toggle--loading' ); - - $.ajax( { - url: woocommerce_admin.ajax_url, - data: data, - dataType : 'json', - type : 'POST', - success: function( response ) { - if ( true === response.data ) { - $toggle.removeClass( 'woocommerce-input-toggle--enabled, woocommerce-input-toggle--disabled' ); - $toggle.addClass( 'woocommerce-input-toggle--enabled' ); - $toggle.removeClass( 'woocommerce-input-toggle--loading' ); - } else if ( false === response.data ) { - $toggle.removeClass( 'woocommerce-input-toggle--enabled, woocommerce-input-toggle--disabled' ); - $toggle.addClass( 'woocommerce-input-toggle--disabled' ); - $toggle.removeClass( 'woocommerce-input-toggle--loading' ); - } else if ( 'needs_setup' === response.data ) { - window.location.href = $link.attr( 'href' ); - } + $( 'tr', $this_table ).removeClass( 'selected_now' ); } + } ) + .on( 'blur', 'input', function () { + hasFocus = false; } ); - return false; - }); + // Additional cost and Attribute term tables + $( + '.woocommerce_page_wc-settings .shippingrows tbody tr:even, table.attributes-table tbody tr:nth-child(odd)' + ).addClass( 'alternate' ); - $( '#wpbody' ).on( 'click', '#doaction, #doaction2', function() { - var action = $( this ).is( '#doaction' ) ? $( '#bulk-action-selector-top' ).val() : $( '#bulk-action-selector-bottom' ).val(); + // Show order items on orders page + $( document.body ).on( 'click', '.show_order_items', function () { + $( this ).closest( 'td' ).find( 'table' ).toggle(); + return false; + } ); + + // Select availability + $( 'select.availability' ) + .on( 'change', function () { + if ( $( this ).val() === 'all' ) { + $( this ).closest( 'tr' ).next( 'tr' ).hide(); + } else { + $( this ).closest( 'tr' ).next( 'tr' ).show(); + } + } ) + .trigger( 'change' ); + + // Hidden options + $( '.hide_options_if_checked' ).each( function () { + $( this ) + .find( 'input:eq(0)' ) + .on( 'change', function () { + if ( $( this ).is( ':checked' ) ) { + $( this ) + .closest( 'fieldset, tr' ) + .nextUntil( + '.hide_options_if_checked, .show_options_if_checked', + '.hidden_option' + ) + .hide(); + } else { + $( this ) + .closest( 'fieldset, tr' ) + .nextUntil( + '.hide_options_if_checked, .show_options_if_checked', + '.hidden_option' + ) + .show(); + } + } ) + .trigger( 'change' ); + } ); + + $( '.show_options_if_checked' ).each( function () { + $( this ) + .find( 'input:eq(0)' ) + .on( 'change', function () { + if ( $( this ).is( ':checked' ) ) { + $( this ) + .closest( 'fieldset, tr' ) + .nextUntil( + '.hide_options_if_checked, .show_options_if_checked', + '.hidden_option' + ) + .show(); + } else { + $( this ) + .closest( 'fieldset, tr' ) + .nextUntil( + '.hide_options_if_checked, .show_options_if_checked', + '.hidden_option' + ) + .hide(); + } + } ) + .trigger( 'change' ); + } ); + + // Reviews. + $( 'input#woocommerce_enable_reviews' ) + .on( 'change', function () { + if ( $( this ).is( ':checked' ) ) { + $( '#woocommerce_enable_review_rating' ) + .closest( 'tr' ) + .show(); + } else { + $( '#woocommerce_enable_review_rating' ) + .closest( 'tr' ) + .hide(); + } + } ) + .trigger( 'change' ); + + // Attribute term table + $( 'table.attributes-table tbody tr:nth-child(odd)' ).addClass( + 'alternate' + ); + + // Toggle gateway on/off. + $( '.wc_gateways' ).on( + 'click', + '.wc-payment-gateway-method-toggle-enabled', + function () { + var $link = $( this ), + $row = $link.closest( 'tr' ), + $toggle = $link.find( '.woocommerce-input-toggle' ); + + var data = { + action: 'woocommerce_toggle_gateway_enabled', + security: woocommerce_admin.nonces.gateway_toggle, + gateway_id: $row.data( 'gateway_id' ), + }; + + $toggle.addClass( 'woocommerce-input-toggle--loading' ); + + $.ajax( { + url: woocommerce_admin.ajax_url, + data: data, + dataType: 'json', + type: 'POST', + success: function ( response ) { + if ( true === response.data ) { + $toggle.removeClass( + 'woocommerce-input-toggle--enabled, woocommerce-input-toggle--disabled' + ); + $toggle.addClass( + 'woocommerce-input-toggle--enabled' + ); + $toggle.removeClass( + 'woocommerce-input-toggle--loading' + ); + } else if ( false === response.data ) { + $toggle.removeClass( + 'woocommerce-input-toggle--enabled, woocommerce-input-toggle--disabled' + ); + $toggle.addClass( + 'woocommerce-input-toggle--disabled' + ); + $toggle.removeClass( + 'woocommerce-input-toggle--loading' + ); + } else if ( 'needs_setup' === response.data ) { + window.location.href = $link.attr( 'href' ); + } + }, + } ); + + return false; + } + ); + + $( '#wpbody' ).on( 'click', '#doaction, #doaction2', function () { + var action = $( this ).is( '#doaction' ) + ? $( '#bulk-action-selector-top' ).val() + : $( '#bulk-action-selector-bottom' ).val(); if ( 'remove_personal_data' === action ) { - return window.confirm( woocommerce_admin.i18n_remove_personal_data_notice ); + return window.confirm( + woocommerce_admin.i18n_remove_personal_data_notice + ); } - }); + } ); - var marketplaceSectionDropdown = $( '#marketplace-current-section-dropdown' ); + var marketplaceSectionDropdown = $( + '#marketplace-current-section-dropdown' + ); var marketplaceSectionName = $( '#marketplace-current-section-name' ); var marketplaceMenuIsOpen = false; // Add event listener to toggle Marketplace menu on touch devices if ( marketplaceSectionDropdown.length ) { if ( isTouchDevice() ) { - marketplaceSectionName.on( 'click', function() { + marketplaceSectionName.on( 'click', function () { marketplaceMenuIsOpen = ! marketplaceMenuIsOpen; if ( marketplaceMenuIsOpen ) { marketplaceSectionDropdown.addClass( 'is-open' ); $( document ).on( 'click', maybeToggleMarketplaceMenu ); } else { marketplaceSectionDropdown.removeClass( 'is-open' ); - $( document ).off( 'click', maybeToggleMarketplaceMenu ); + $( document ).off( + 'click', + maybeToggleMarketplaceMenu + ); } } ); } else { @@ -438,8 +643,8 @@ // Close menu if the user clicks outside it function maybeToggleMarketplaceMenu( e ) { if ( - ! marketplaceSectionDropdown.is( e.target ) - && marketplaceSectionDropdown.has( e.target ).length === 0 + ! marketplaceSectionDropdown.is( e.target ) && + marketplaceSectionDropdown.has( e.target ).length === 0 ) { marketplaceSectionDropdown.removeClass( 'is-open' ); marketplaceMenuIsOpen = false; @@ -448,11 +653,11 @@ } function isTouchDevice() { - return ( ( 'ontouchstart' in window ) || - ( navigator.maxTouchPoints > 0 ) || - ( navigator.msMaxTouchPoints > 0 ) ); + return ( + 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0 + ); } - - }); - -})( jQuery, woocommerce_admin ); + } ); +} )( jQuery, woocommerce_admin ); diff --git a/plugins/woocommerce/client/legacy/js/frontend/add-payment-method.js b/plugins/woocommerce/client/legacy/js/frontend/add-payment-method.js index b200c6921fe..1ccdefac3cb 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/add-payment-method.js +++ b/plugins/woocommerce/client/legacy/js/frontend/add-payment-method.js @@ -32,4 +32,13 @@ jQuery( function( $ ) { $( document.body ).trigger( 'init_add_payment_method' ); + // Prevent firing multiple requests upon double clicking the buttons in payment methods table + $(' .woocommerce .payment-method-actions .button.delete' ).on( 'click' , function( event ) { + if ( $( this ).hasClass( 'disabled' ) ) { + event.preventDefault(); + } + + $( this ).addClass( 'disabled' ); + }); + }); diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index 19958785f93..48554489572 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -21,7 +21,7 @@ "maxmind-db/reader": "^1.11", "pelago/emogrifier": "^6.0", "woocommerce/action-scheduler": "3.4.2", - "woocommerce/woocommerce-blocks": "8.7.1" + "woocommerce/woocommerce-blocks": "8.9.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4", diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock index 32a6ca40efe..f9ee1390780 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": "9e0e2721da4db7a3c7474c3e2f14c236", + "content-hash": "4ba153cfcffe43c11a5c994e21a822bb", "packages": [ { "name": "automattic/jetpack-autoloader", @@ -628,16 +628,16 @@ }, { "name": "woocommerce/woocommerce-blocks", - "version": "v8.7.1", + "version": "v8.9.0", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-blocks.git", - "reference": "5530ea25a060cb0eaf9e930f9097057fb820f22e" + "reference": "9ed8e59f2f78a2bd0198750ed314590802d8f3d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/5530ea25a060cb0eaf9e930f9097057fb820f22e", - "reference": "5530ea25a060cb0eaf9e930f9097057fb820f22e", + "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/9ed8e59f2f78a2bd0198750ed314590802d8f3d5", + "reference": "9ed8e59f2f78a2bd0198750ed314590802d8f3d5", "shasum": "" }, "require": { @@ -683,9 +683,9 @@ ], "support": { "issues": "https://github.com/woocommerce/woocommerce-blocks/issues", - "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v8.7.1" + "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v8.9.0" }, - "time": "2022-10-12T15:27:50+00:00" + "time": "2022-11-08T11:37:31+00:00" } ], "packages-dev": [ diff --git a/plugins/woocommerce/i18n/states.php b/plugins/woocommerce/i18n/states.php index 9b14647d69d..b43d34cd698 100644 --- a/plugins/woocommerce/i18n/states.php +++ b/plugins/woocommerce/i18n/states.php @@ -1543,6 +1543,22 @@ return array( 'VS' => __( 'Vaslui', 'woocommerce' ), 'VN' => __( 'Vrancea', 'woocommerce' ), ), + 'SN' => array( // Regions of Senegal. Ref: https://github.com/unicode-org/cldr/blob/release-42/common/subdivisions/en.xml#L4801. + 'SNDB' => __( 'Diourbel', 'woocommerce' ), + 'SNDK' => __( 'Dakar', 'woocommerce' ), + 'SNFK' => __( 'Fatick', 'woocommerce' ), + 'SNKA' => __( 'Kaffrine', 'woocommerce' ), + 'SNKD' => __( 'Kolda', 'woocommerce' ), + 'SNKE' => __( 'Kédougou', 'woocommerce' ), + 'SNKL' => __( 'Kaolack', 'woocommerce' ), + 'SNLG' => __( 'Louga', 'woocommerce' ), + 'SNMT' => __( 'Matam', 'woocommerce' ), + 'SNSE' => __( 'Sédhiou', 'woocommerce' ), + 'SNSL' => __( 'Saint-Louis', 'woocommerce' ), + 'SNTC' => __( 'Tambacounda', 'woocommerce' ), + 'SNTH' => __( 'Thiès', 'woocommerce' ), + 'SNZG' => __( 'Ziguinchor', 'woocommerce' ), + ), 'SG' => array(), 'SK' => array(), 'SI' => array(), @@ -1792,31 +1808,34 @@ return array( 'RSVO' => _x( 'Vojvodina', 'district', 'woocommerce' ), ), 'SE' => array(), - 'UA' => array( // Ukrainian oblasts. - 'VN' => __( 'Vinnytsia Oblast', 'woocommerce' ), - 'VL' => __( 'Volyn Oblast', 'woocommerce' ), - 'DP' => __( 'Dnipropetrovsk Oblast', 'woocommerce' ), - 'DT' => __( 'Donetsk Oblast', 'woocommerce' ), - 'ZT' => __( 'Zhytomyr Oblast', 'woocommerce' ), - 'ZK' => __( 'Zakarpattia Oblast', 'woocommerce' ), - 'ZP' => __( 'Zaporizhzhia Oblast', 'woocommerce' ), - 'IF' => __( 'Ivano-Frankivsk Oblast', 'woocommerce' ), - 'KV' => __( 'Kyiv Oblast', 'woocommerce' ), - 'KH' => __( 'Kirovohrad Oblast', 'woocommerce' ), - 'LH' => __( 'Luhansk Oblast', 'woocommerce' ), - 'LV' => __( 'Lviv Oblast', 'woocommerce' ), - 'MY' => __( 'Mykolaiv Oblast', 'woocommerce' ), - 'OD' => __( 'Odessa Oblast', 'woocommerce' ), - 'PL' => __( 'Poltava Oblast', 'woocommerce' ), - 'RV' => __( 'Rivne Oblast', 'woocommerce' ), - 'SM' => __( 'Sumy Oblast', 'woocommerce' ), - 'TP' => __( 'Ternopil Oblast', 'woocommerce' ), - 'KK' => __( 'Kharkiv Oblast', 'woocommerce' ), - 'KS' => __( 'Kherson Oblast', 'woocommerce' ), - 'KM' => __( 'Khmelnytskyi Oblast', 'woocommerce' ), - 'CK' => __( 'Cherkasy Oblast', 'woocommerce' ), - 'CH' => __( 'Chernihiv Oblast', 'woocommerce' ), - 'CV' => __( 'Chernivtsi Oblast', 'woocommerce' ), + 'UA' => array( // Ukrainian oblasts. https://github.com/unicode-org/cldr/blob/release-42/common/subdivisions/en.xml#L5243. + 'UA05' => __( 'Vinnychchyna', 'woocommerce' ), + 'UA07' => __( 'Volyn', 'woocommerce' ), + 'UA09' => __( 'Luhanshchyna', 'woocommerce' ), + 'UA12' => __( 'Dnipropetrovshchyna', 'woocommerce' ), + 'UA14' => __( 'Donechchyna', 'woocommerce' ), + 'UA18' => __( 'Zhytomyrshchyna', 'woocommerce' ), + 'UA21' => __( 'Zakarpattia', 'woocommerce' ), + 'UA23' => __( 'Zaporizhzhya', 'woocommerce' ), + 'UA26' => __( 'Prykarpattia', 'woocommerce' ), + 'UA30' => __( 'Kyiv', 'woocommerce' ), + 'UA32' => __( 'Kyivshchyna', 'woocommerce' ), + 'UA35' => __( 'Kirovohradschyna', 'woocommerce' ), + 'UA40' => __( 'Sevastopol', 'woocommerce' ), + 'UA43' => __( 'Crimea', 'woocommerce' ), + 'UA46' => __( 'Lvivshchyna', 'woocommerce' ), + 'UA48' => __( 'Mykolayivschyna', 'woocommerce' ), + 'UA51' => __( 'Odeshchyna', 'woocommerce' ), + 'UA53' => __( 'Poltavshchyna', 'woocommerce' ), + 'UA56' => __( 'Rivnenshchyna', 'woocommerce' ), + 'UA59' => __( 'Sumshchyna', 'woocommerce' ), + 'UA61' => __( 'Ternopilshchyna', 'woocommerce' ), + 'UA63' => __( 'Kharkivshchyna', 'woocommerce' ), + 'UA65' => __( 'Khersonshchyna', 'woocommerce' ), + 'UA68' => __( 'Khmelnychchyna', 'woocommerce' ), + 'UA71' => __( 'Cherkashchyna', 'woocommerce' ), + 'UA74' => __( 'Chernihivshchyna', 'woocommerce' ), + 'UA77' => __( 'Chernivtsi Oblast', 'woocommerce' ), ), 'UG' => array( // Ugandan districts. 'UG314' => __( 'Abim', 'woocommerce' ), diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php index 74069a01f2d..c3f031fe34f 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php @@ -539,6 +539,18 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { return wc_string_to_bool( $this->get_prop( 'recorded_coupon_usage_counts', $context ) ); } + /** + * Get basic order data in array format. + * + * @return array + */ + public function get_base_data() { + return array_merge( + array( 'id' => $this->get_id() ), + $this->data + ); + } + /* |-------------------------------------------------------------------------- | Setters diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php index 29131b2f075..97e8bb32cc4 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php @@ -84,7 +84,9 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : // @deprecated 2.3. if ( has_action( 'woocommerce_admin_css' ) ) { + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_admin_css' ); + /* phpcs: enable */ wc_deprecated_function( 'The woocommerce_admin_css action', '2.3', 'admin_enqueue_scripts' ); } @@ -182,15 +184,15 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : wp_enqueue_script( 'jquery-ui-sortable' ); wp_enqueue_script( 'jquery-ui-autocomplete' ); - $locale = localeconv(); + $locale = localeconv(); $decimal_point = isset( $locale['decimal_point'] ) ? $locale['decimal_point'] : '.'; - $decimal = ( ! empty( wc_get_price_decimal_separator() ) ) ? wc_get_price_decimal_separator() : $decimal_point; + $decimal = ( ! empty( wc_get_price_decimal_separator() ) ) ? wc_get_price_decimal_separator() : $decimal_point; $params = array( /* translators: %s: decimal */ - 'i18n_decimal_error' => sprintf( __( 'Please enter with one decimal point (%s) without thousand separators.', 'woocommerce' ), $decimal ), + 'i18n_decimal_error' => sprintf( __( 'Please enter a value with one decimal point (%s) without thousand separators.', 'woocommerce' ), $decimal ), /* translators: %s: price decimal separator */ - 'i18n_mon_decimal_error' => sprintf( __( 'Please enter with one monetary decimal point (%s) without thousand separators and currency symbols.', 'woocommerce' ), wc_get_price_decimal_separator() ), + 'i18n_mon_decimal_error' => sprintf( __( 'Please enter a value with one monetary decimal point (%s) without thousand separators and currency symbols.', 'woocommerce' ), wc_get_price_decimal_separator() ), 'i18n_country_iso_error' => __( 'Please enter in country code with two capital letters.', 'woocommerce' ), 'i18n_sale_less_than_regular_error' => __( 'Please enter in a value less than the regular price.', 'woocommerce' ), 'i18n_delete_product_notice' => __( 'This product has produced sales and may be linked to existing orders. Are you sure you want to delete it?', 'woocommerce' ), @@ -233,11 +235,25 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : wp_localize_script( 'woocommerce_quick-edit', 'woocommerce_quick_edit', $params ); } + // Product description. + if ( in_array( $screen_id, array( 'product' ), true ) ) { + wp_enqueue_script( 'wc-admin-product-editor', WC()->plugin_url() . '/assets/js/admin/product-editor' . $suffix . '.js', array( 'jquery' ), $version, false ); + + wp_localize_script( + 'wc-admin-product-editor', + 'woocommerce_admin_product_editor', + array( + 'i18n_description' => esc_js( __( 'Product description', 'woocommerce' ) ), + ) + ); + } + // Meta boxes. + /* phpcs:disable */ if ( in_array( $screen_id, array( 'product', 'edit-product' ) ) ) { wp_enqueue_media(); wp_register_script( 'wc-admin-product-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'media-models' ), $version ); - wp_register_script( 'wc-admin-variation-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product-variation' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'serializejson', 'media-models' ), $version ); + wp_register_script( 'wc-admin-variation-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product-variation' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'serializejson', 'media-models', 'backbone', 'jquery-ui-sortable', 'wc-backbone-modal' ), $version ); wp_enqueue_script( 'wc-admin-product-meta-boxes' ); wp_enqueue_script( 'wc-admin-variation-meta-boxes' ); @@ -276,6 +292,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : wp_localize_script( 'wc-admin-variation-meta-boxes', 'woocommerce_admin_meta_boxes_variations', $params ); } + /* phpcs: enable */ if ( $this->is_order_meta_box_screen( $screen_id ) ) { $default_location = wc_get_customer_default_location(); @@ -293,6 +310,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : ) ); } + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ if ( in_array( $screen_id, array( 'shop_coupon', 'edit-shop_coupon' ) ) ) { wp_enqueue_script( 'wc-admin-coupon-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-coupon' . $suffix . '.js', array( 'wc-admin-meta-boxes' ), $version ); wp_localize_script( @@ -307,6 +325,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : ) ); } + /* phpcs: enable */ if ( in_array( str_replace( 'edit-', '', $screen_id ), array( 'shop_coupon', 'product' ), true ) || $this->is_order_meta_box_screen( $screen_id ) ) { $post_id = isset( $post->ID ) ? $post->ID : ''; $currency = ''; @@ -396,6 +415,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : } // Term ordering - only when sorting by term_order. + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ if ( ( strstr( $screen_id, 'edit-pa_' ) || ( ! empty( $_GET['taxonomy'] ) && in_array( wp_unslash( $_GET['taxonomy'] ), apply_filters( 'woocommerce_sortable_taxonomies', array( 'product_cat' ) ) ) ) ) && ! isset( $_GET['orderby'] ) ) { wp_register_script( 'woocommerce_term_ordering', WC()->plugin_url() . '/assets/js/admin/term-ordering' . $suffix . '.js', array( 'jquery-ui-sortable' ), $version ); @@ -409,6 +429,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : wp_localize_script( 'woocommerce_term_ordering', 'woocommerce_term_ordering_params', $woocommerce_term_order_params ); } + /* phpcs: enable */ // Product sorting - only when sorting by menu order on the products page. if ( current_user_can( 'edit_others_pages' ) && 'edit-product' === $screen_id && isset( $wp_query->query['orderby'] ) && 'menu_order title' === $wp_query->query['orderby'] ) { @@ -417,6 +438,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : } // Reports Pages. + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ if ( in_array( $screen_id, apply_filters( 'woocommerce_reports_screen_ids', array( $wc_screen_id . '_page_wc-reports', 'toplevel_page_wc-reports', 'dashboard' ) ) ) ) { wp_register_script( 'wc-reports', WC()->plugin_url() . '/assets/js/admin/reports' . $suffix . '.js', array( 'jquery', 'jquery-ui-datepicker' ), $version ); @@ -427,6 +449,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : wp_enqueue_script( 'flot-pie' ); wp_enqueue_script( 'flot-stack' ); } + /* phpcs: enable */ // API settings. if ( $wc_screen_id . '_page_wc-settings' === $screen_id && isset( $_GET['section'] ) && 'keys' == $_GET['section'] ) { diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php index f6023c2f35b..dcc2e7e8422 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php @@ -187,7 +187,7 @@ if ( ! class_exists( 'WC_Admin_Dashboard_Setup', false ) ) : return false; } - if ( ! $this->get_task_list() || $this->get_task_list()->is_complete() || $this->get_task_list()->is_hidden() ) { + if ( ! $this->get_task_list() || $this->get_task_list()->is_hidden() || $this->get_task_list()->is_complete() ) { return false; } diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-exporters.php b/plugins/woocommerce/includes/admin/class-wc-admin-exporters.php index 9a0758ff26d..07b4526bc9f 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-exporters.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-exporters.php @@ -199,7 +199,7 @@ class WC_Admin_Exporters { * @return array The product types keys and labels. */ public static function get_product_types() { - $product_types = wc_get_product_types(); + $product_types = wc_get_product_types(); $product_types['variation'] = __( 'Product variations', 'woocommerce' ); /** diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php index 191d65ca586..8b1491042b7 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php @@ -6,6 +6,7 @@ * @version 2.5.0 */ +use Automattic\WooCommerce\Internal\Admin\Orders\COTRedirectionController; use Automattic\WooCommerce\Internal\Admin\Orders\PageController as Custom_Orders_PageController; use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; use Automattic\WooCommerce\Admin\Features\Features; @@ -316,6 +317,8 @@ class WC_Admin_Menus { if ( wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) { $this->orders_page_controller = new Custom_Orders_PageController(); $this->orders_page_controller->setup(); + } else { + wc_get_container()->get( COTRedirectionController::class )->setup(); } } diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php b/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php index 24571dbd9dc..815613fadb4 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php @@ -23,11 +23,6 @@ class WC_Admin_Meta_Boxes { */ public const ERROR_STORE = 'woocommerce_meta_box_errors'; - /** - * The css class used to close the meta box - */ - private const CLOSED_CSS_CLASS = 'closed'; - /** * Is meta boxes saved once? * @@ -138,8 +133,6 @@ class WC_Admin_Meta_Boxes { add_meta_box( 'woocommerce-product-data', __( 'Product data', 'woocommerce' ), 'WC_Meta_Box_Product_Data::output', 'product', 'normal', 'high' ); add_meta_box( 'woocommerce-product-images', __( 'Product gallery', 'woocommerce' ), 'WC_Meta_Box_Product_Images::output', 'product', 'side', 'low' ); - add_filter( 'postbox_classes_product_postexcerpt', array( $this, 'collapse_postexcerpt' ) ); - // Orders. foreach ( wc_get_order_types( 'order-meta-boxes' ) as $type ) { $order_type_object = get_post_type_object( $type ); @@ -155,18 +148,6 @@ class WC_Admin_Meta_Boxes { } } - /** - * Collapse product short description meta box by default - * - * @param array $classes The css class array applied to the meta box. - */ - public function collapse_postexcerpt( $classes ) { - if ( ! in_array( self::CLOSED_CSS_CLASS, $classes, true ) ) { - array_push( $classes, self::CLOSED_CSS_CLASS ); - } - return $classes; - } - /** * Add default sort order for meta boxes on product page. */ diff --git a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php index dfd667d095e..d090cbb0b54 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php @@ -38,6 +38,13 @@ class WC_Meta_Box_Order_Data { */ public static function init_address_fields() { + /** + * Provides an opportunity to modify the list of order billing fields displayed on the admin. + * + * @since 1.4.0 + * + * @param array Billing fields. + */ self::$billing_fields = apply_filters( 'woocommerce_admin_billing_fields', array( @@ -90,6 +97,13 @@ class WC_Meta_Box_Order_Data { ) ); + /** + * Provides an opportunity to modify the list of order shipping fields displayed on the admin. + * + * @since 1.4.0 + * + * @param array Shipping fields. + */ self::$shipping_fields = apply_filters( 'woocommerce_admin_shipping_fields', array( @@ -223,7 +237,6 @@ class WC_Meta_Box_Order_Data { ); } - $ip_address = $order->get_customer_ip_address(); if ( $ip_address ) { $meta_list[] = sprintf( @@ -304,24 +317,32 @@ class WC_Meta_Box_Order_Data { $user_string = ''; $user_id = ''; if ( $order->get_user_id() ) { - $user_id = absint( $order->get_user_id() ); - $user = get_user_by( 'id', $user_id ); + $user_id = absint( $order->get_user_id() ); + $customer = new WC_Customer( $user_id ); + /* translators: 1: user display name 2: user ID 3: user email */ $user_string = sprintf( - /* translators: 1: user display name 2: user ID 3: user email */ + /* translators: 1: customer name, 2 customer id, 3: customer email */ esc_html__( '%1$s (#%2$s – %3$s)', 'woocommerce' ), - $user->display_name, - absint( $user->ID ), - $user->user_email + $customer->get_first_name() . ' ' . $customer->get_last_name(), + $customer->get_id(), + $customer->get_email() ); } ?>

@@ -526,6 +547,13 @@ class WC_Meta_Box_Order_Data { } } + /** + * Allows 3rd parties to alter whether the customer note should be displayed on the admin. + * + * @since 2.1.0 + * + * @param bool TRUE if the note should be displayed. FALSE otherwise. + */ if ( apply_filters( 'woocommerce_enable_order_notes_field', 'yes' === get_option( 'woocommerce_enable_order_comments', 'yes' ) ) ) : ?>

@@ -662,7 +690,7 @@ class WC_Meta_Box_Order_Data { // Customer note. if ( isset( $_POST['customer_note'] ) ) { - $props['customer_note'] = sanitize_text_field( wp_unslash( $_POST['customer_note'] ) ); + $props['customer_note'] = sanitize_textarea_field( wp_unslash( $_POST['customer_note'] ) ); } // Save order data. diff --git a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php index a7751b42284..560468487b4 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php @@ -53,6 +53,7 @@ class WC_Meta_Box_Product_Data { * @return array */ private static function get_product_type_options() { + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ return apply_filters( 'product_type_options', array( @@ -72,6 +73,7 @@ class WC_Meta_Box_Product_Data { ), ) ); + /* phpcs: enable */ } /** @@ -80,6 +82,7 @@ class WC_Meta_Box_Product_Data { * @return array */ private static function get_product_data_tabs() { + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ $tabs = apply_filters( 'woocommerce_product_data_tabs', array( @@ -127,6 +130,7 @@ class WC_Meta_Box_Product_Data { ), ) ); + /* phpcs: enable */ // Sort tabs based on priority. uasort( $tabs, array( __CLASS__, 'product_data_tabs_sort' ) ); @@ -171,11 +175,14 @@ class WC_Meta_Box_Product_Data { public static function output_variations() { global $post, $wpdb, $product_object; + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ $variation_attributes = array_filter( $product_object->get_attributes(), array( __CLASS__, 'filter_variation_attributes' ) ); $default_attributes = $product_object->get_default_attributes(); $variations_count = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_count', $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_parent = %d AND post_type = 'product_variation' AND post_status IN ('publish', 'private')", $post->ID ) ), $post->ID ) ); $variations_per_page = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_per_page', 15 ) ); $variations_total_pages = ceil( $variations_count / $variations_per_page ); + $modal_title = get_bloginfo( 'name' ) . __( ' says', 'woocommerce' ); + /* phpcs: enable */ include __DIR__ . '/views/html-product-data-variations.php'; } @@ -272,7 +279,9 @@ class WC_Meta_Box_Product_Data { $attribute->set_position( $attribute_position[ $i ] ); $attribute->set_visible( isset( $attribute_visibility[ $i ] ) ); $attribute->set_variation( isset( $attribute_variation[ $i ] ) ); + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ $attributes[] = apply_filters( 'woocommerce_admin_meta_boxes_prepare_attribute', $attribute, $data, $i ); + /* phpcs: enable */ } } return $attributes; @@ -425,9 +434,9 @@ class WC_Meta_Box_Product_Data { $product->get_data_store()->sync_variation_names( $product, $original_post_title, $post_title ); } - + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_process_product_meta_' . $product_type, $post_id ); - // phpcs:enable WordPress.Security.NonceVerification.Missing + /* phpcs:enable WordPress.Security.NonceVerification.Missing and WooCommerce.Commenting.CommentHooks.MissingHookComment */ } /** @@ -448,7 +457,7 @@ class WC_Meta_Box_Product_Data { $max_loop = max( array_keys( wp_unslash( $_POST['variable_post_id'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $data_store = $parent->get_data_store(); $data_store->sort_all_product_variations( $parent->get_id() ); - $new_variation_menu_order_id = ! empty( $_POST['new_variation_menu_order_id'] ) ? wc_clean( wp_unslash( $_POST['new_variation_menu_order_id'] ) ) : false; + $new_variation_menu_order_id = ! empty( $_POST['new_variation_menu_order_id'] ) ? wc_clean( wp_unslash( $_POST['new_variation_menu_order_id'] ) ) : false; $new_variation_menu_order_value = ! empty( $_POST['new_variation_menu_order_value'] ) ? wc_clean( wp_unslash( $_POST['new_variation_menu_order_value'] ) ) : false; // Only perform this operation if setting menu order via the prompt. @@ -560,7 +569,9 @@ class WC_Meta_Box_Product_Data { do_action( 'woocommerce_admin_process_variation_object', $variation, $i ); $variation->save(); + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_save_product_variation', $variation_id, $i ); + /* phpcs: enable */ } } // phpcs:enable WordPress.Security.NonceVerification.Missing diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-variations.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-variations.php index 5bff4c42b5a..6f69d8bd31b 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-variations.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-variations.php @@ -16,7 +16,9 @@ if ( ! defined( 'ABSPATH' ) ) {

Attributes tab.', 'woocommerce' ) ); ?>

+

+
@@ -33,11 +35,15 @@ if ( ! defined( 'ABSPATH' ) ) { is_taxonomy() ) : ?> get_terms() as $option ) : ?> + + get_options() as $option ) : ?> + + @@ -48,7 +54,9 @@ if ( ! defined( 'ABSPATH' ) ) {
+ +
@@ -104,7 +114,9 @@ if ( ! defined( 'ABSPATH' ) ) { @@ -116,7 +128,9 @@ if ( ! defined( 'ABSPATH' ) ) {
+
+
@@ -135,7 +149,9 @@ if ( ! defined( 'ABSPATH' ) ) { @@ -150,3 +166,25 @@ if ( ! defined( 'ABSPATH' ) ) {
+ diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php index 5402c3576d1..45202b0e407 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php @@ -14,8 +14,8 @@ defined( 'ABSPATH' ) || exit; ?>

+ -
# is_taxonomy() ) : ?> get_terms() as $option ) : ?> + + get_options() as $option ) : ?> + + @@ -106,7 +110,9 @@ defined( 'ABSPATH' ) || exit; + +

@@ -152,6 +158,7 @@ defined( 'ABSPATH' ) || exit; $sale_price_dates_from = $sale_price_dates_from_timestamp ? date_i18n( 'Y-m-d', $sale_price_dates_from_timestamp ) : ''; $sale_price_dates_to = $sale_price_dates_to_timestamp ? date_i18n( 'Y-m-d', $sale_price_dates_to_timestamp ) : ''; + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ echo ''; + /* phpcs: enable */ /** * Variation options pricing action. @@ -236,7 +244,7 @@ defined( 'ABSPATH' ) || exit; 'custom_attributes' => array( 'step' => 'any', ), - 'wrapper_class' => 'form-row', + 'wrapper_class' => 'form-row', ) ); @@ -409,7 +417,7 @@ defined( 'ABSPATH' ) || exit; if ( $downloadable_files ) { foreach ( $downloadable_files as $key => $file ) { - $disabled_download = isset( $file['enabled'] ) && false === $file['enabled']; + $disabled_download = isset( $file['enabled'] ) && false === $file['enabled']; $disabled_downloads_count += (int) $disabled_download; include __DIR__ . '/html-product-variation-download.php'; } @@ -421,8 +429,8 @@ defined( 'ABSPATH' ) || exit; get( Download_Directories::class )->get_mode() === Download_Directories::MODE_ENABLED ? '' : ''; ?> + + + + get_current_class_name() ); ?> + + get( Automattic\WooCommerce\Internal\Features\FeaturesController::class )->feature_is_enabled( 'custom_order_tables' ) ) : ?> + + + + get( Order_DataSynchronizer::class )->data_sync_is_enabled() ? '' : ''; ?> + + diff --git a/plugins/woocommerce/includes/admin/wc-admin-functions.php b/plugins/woocommerce/includes/admin/wc-admin-functions.php index 185294d1bd2..29121c49fe5 100644 --- a/plugins/woocommerce/includes/admin/wc-admin-functions.php +++ b/plugins/woocommerce/includes/admin/wc-admin-functions.php @@ -55,7 +55,9 @@ function wc_get_screen_ids() { } } + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ return apply_filters( 'woocommerce_screen_ids', $screen_ids ); + /* phpcs: enable */ } /** @@ -107,7 +109,9 @@ function wc_create_page( $slug, $option = '', $page_title = '', $page_content = $valid_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status NOT IN ( 'pending', 'trash', 'future', 'auto-draft' ) AND post_name = %s LIMIT 1;", $slug ) ); } + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ $valid_page_found = apply_filters( 'woocommerce_create_page_id', $valid_page_found, $slug, $page_content ); + /* phpcs: enable */ if ( $valid_page_found ) { if ( $option ) { @@ -145,7 +149,9 @@ function wc_create_page( $slug, $option = '', $page_title = '', $page_content = ); $page_id = wp_insert_post( $page_data ); + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_page_created', $page_id, $page_data ); + /* phpcs: enable */ } if ( $option ) { @@ -287,7 +293,9 @@ function wc_maybe_adjust_line_item_product_stock( $item, $item_quantity = -1 ) { */ function wc_save_order_items( $order_id, $items ) { // Allow other plugins to check change in order items before they are saved. + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_before_save_order_items', $order_id, $items ); + /* phpcs: enable */ $qty_change_order_notes = array(); $order = wc_get_order( $order_id ); @@ -361,7 +369,9 @@ function wc_save_order_items( $order_id, $items ) { } // Allow other plugins to change item object before it is saved. + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_before_save_order_item', $item ); + /* phpcs: enable */ $item->save(); @@ -438,7 +448,9 @@ function wc_save_order_items( $order_id, $items ) { $order->calculate_totals( false ); // Inform other plugins that the items have been saved. + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ do_action( 'woocommerce_saved_order_items', $order_id, $items ); + /* phpcs: enable */ } /** @@ -472,9 +484,11 @@ function wc_render_invalid_variation_notice( $product_object ) { global $wpdb; // Give ability for extensions to hide this notice. + /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ if ( ! apply_filters( 'woocommerce_show_invalid_variations_notice', true, $product_object ) ) { return; } + /* phpcs: enable */ $variation_ids = $product_object ? $product_object->get_children() : array(); @@ -490,8 +504,8 @@ function wc_render_invalid_variation_notice( $product_object ) { " SELECT count(post_id) FROM {$wpdb->postmeta} WHERE post_id in (" . implode( ',', array_map( 'absint', $variation_ids ) ) . ") - AND meta_key='_price' - AND meta_value >= 0 + AND ( meta_key='_subscription_sign_up_fee' OR meta_key='_price' ) + AND meta_value > 0 AND meta_value != '' " ); @@ -499,7 +513,7 @@ function wc_render_invalid_variation_notice( $product_object ) { if ( 0 < ( $variation_count - $invalid_variation_count ) ) { ?> -
+

+
+ +
%s %s', wc_get_cart_url(), __( 'View cart', 'woocommerce' ), $message ) ); + throw new Exception( sprintf( '%s %s', wc_get_cart_url(), esc_attr( $wp_button_class ), __( 'View cart', 'woocommerce' ), $message ) ); } } @@ -1220,10 +1221,12 @@ class WC_Cart extends WC_Legacy_Cart { if ( isset( $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ] ) && ! $product_data->has_enough_stock( $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ] + $quantity ) ) { $stock_quantity = $product_data->get_stock_quantity(); $stock_quantity_in_cart = $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ]; + $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : ''; $message = sprintf( - '%s %s', + '%s %s', wc_get_cart_url(), + esc_attr( $wp_button_class ), __( 'View cart', 'woocommerce' ), /* translators: 1: quantity in stock 2: current quantity */ sprintf( __( 'You cannot add that amount to the cart — we have %1$s in stock and you already have %2$s in your cart.', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_quantity, $product_data ), wc_format_stock_quantity_for_display( $stock_quantity_in_cart, $product_data ) ) @@ -2029,7 +2032,7 @@ class WC_Cart extends WC_Legacy_Cart { } } } else { - $row_price = $price * $quantity; + $row_price = (float) $price * (float) $quantity; $product_subtotal = wc_price( $row_price ); } diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php index d73908d3013..594fb2f7cac 100644 --- a/plugins/woocommerce/includes/class-wc-install.php +++ b/plugins/woocommerce/includes/class-wc-install.php @@ -822,15 +822,21 @@ class WC_Install { global $wpdb; $obsolete_notes_names = array( 'wc-admin-welcome-note', + 'wc-admin-insight-first-product-and-payment', 'wc-admin-store-notice-setting-moved', 'wc-admin-store-notice-giving-feedback', + 'wc-admin-first-downloadable-product', 'wc-admin-learn-more-about-product-settings', + 'wc-admin-adding-and-managing-products', 'wc-admin-onboarding-profiler-reminder', 'wc-admin-historical-data', + 'wc-admin-manage-store-activity-from-home-screen', 'wc-admin-review-shipping-settings', 'wc-admin-home-screen-feedback', + 'wc-admin-update-store-details', 'wc-admin-effortless-payments-by-mollie', 'wc-admin-google-ads-and-marketing', + 'wc-admin-insight-first-sale', 'wc-admin-marketing-intro', 'wc-admin-draw-attention', 'wc-admin-need-some-inspiration', diff --git a/plugins/woocommerce/includes/class-wc-order-factory.php b/plugins/woocommerce/includes/class-wc-order-factory.php index 94e01d390e7..bc096b0016d 100644 --- a/plugins/woocommerce/includes/class-wc-order-factory.php +++ b/plugins/woocommerce/includes/class-wc-order-factory.php @@ -187,7 +187,7 @@ class WC_Order_Factory { * * @return array Array of order_id => class_name. */ - private static function get_class_names_for_order_ids( $order_ids ) { + public static function get_class_names_for_order_ids( $order_ids ) { $order_data_store = WC_Data_Store::load( 'order' ); if ( $order_data_store->has_callable( 'get_orders_type' ) ) { $order_types = $order_data_store->get_orders_type( $order_ids ); @@ -233,7 +233,7 @@ class WC_Order_Factory { */ private static function get_class_name_for_order_id( $order_id ) { $classname = self::get_class_names_for_order_ids( array( $order_id ) ); - return $classname[ $order_id ]; + return $classname[ $order_id ] ?? false; } } diff --git a/plugins/woocommerce/includes/class-wc-shipping-rate.php b/plugins/woocommerce/includes/class-wc-shipping-rate.php index f7eedbf3045..a42893ef5a1 100644 --- a/plugins/woocommerce/includes/class-wc-shipping-rate.php +++ b/plugins/woocommerce/includes/class-wc-shipping-rate.php @@ -226,10 +226,10 @@ class WC_Shipping_Rate { /** * Get shipping tax. * - * @return array + * @return float */ public function get_shipping_tax() { - return apply_filters( 'woocommerce_get_shipping_tax', count( $this->taxes ) > 0 && ! WC()->customer->get_is_vat_exempt() ? array_sum( $this->taxes ) : 0, $this ); + return apply_filters( 'woocommerce_get_shipping_tax', count( $this->taxes ) > 0 && ! WC()->customer->get_is_vat_exempt() ? (float) array_sum( $this->taxes ) : 0.0, $this ); } /** diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php index 7783608ad42..34dbc0160c9 100644 --- a/plugins/woocommerce/includes/class-woocommerce.php +++ b/plugins/woocommerce/includes/class-woocommerce.php @@ -601,6 +601,9 @@ final class WooCommerce { case 'twentytwentytwo': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty-two.php'; break; + case 'twentytwentythree': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty-three.php'; + break; } } } diff --git a/plugins/woocommerce/includes/cli/class-wc-cli-rest-command.php b/plugins/woocommerce/includes/cli/class-wc-cli-rest-command.php index f4e22392c27..f3aac41fde2 100644 --- a/plugins/woocommerce/includes/cli/class-wc-cli-rest-command.php +++ b/plugins/woocommerce/includes/cli/class-wc-cli-rest-command.php @@ -13,7 +13,7 @@ if ( ! defined( 'ABSPATH' ) ) { } /** - * Main Command for WooCommere CLI. + * Main Command for WooCommerce CLI. * * Since a lot of WC operations can be handled via the REST API, we base our CLI * off of Restful to generate commands for each WooCommerce REST API endpoint diff --git a/plugins/woocommerce/includes/emails/class-wc-email.php b/plugins/woocommerce/includes/emails/class-wc-email.php index fdf6605e797..aaa758fe4bb 100644 --- a/plugins/woocommerce/includes/emails/class-wc-email.php +++ b/plugins/woocommerce/includes/emails/class-wc-email.php @@ -400,9 +400,7 @@ class WC_Email extends WC_Settings_API { * @return string */ public function get_additional_content() { - $content = $this->get_option( 'additional_content', '' ); - - return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $content ), $this->object, $this ); + return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $this->get_option( 'additional_content', $this->get_default_additional_content() ) ), $this->object, $this ); } /** diff --git a/plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty-three.php b/plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty-three.php new file mode 100644 index 00000000000..eea85f4ca61 --- /dev/null +++ b/plugins/woocommerce/includes/theme-support/class-wc-twenty-twenty-three.php @@ -0,0 +1,82 @@ + 450, + 'single_image_width' => 600, + ) + ); + + } + + /** + * Enqueue CSS for this theme. + * + * @param array $styles Array of registered styles. + * @return array + */ + public static function enqueue_styles( $styles ) { + unset( $styles['woocommerce-general'] ); + + $styles['woocommerce-general'] = array( + 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty-three.css', + 'deps' => '', + 'version' => Constants::get_constant( 'WC_VERSION' ), + 'media' => 'all', + 'has_rtl' => true, + ); + + return apply_filters( 'woocommerce_twenty_twenty_three_styles', $styles ); + } + + /** + * Wrap checkout order review with a `col2-set` div. + */ + public static function before_order_review() { + echo '
'; + } + + /** + * Close the div wrapper. + */ + public static function after_order_review() { + echo '
'; + } +} + +WC_Twenty_Twenty_Three::init(); diff --git a/plugins/woocommerce/includes/wc-cart-functions.php b/plugins/woocommerce/includes/wc-cart-functions.php index 1cab675f281..94fba81e7fd 100644 --- a/plugins/woocommerce/includes/wc-cart-functions.php +++ b/plugins/woocommerce/includes/wc-cart-functions.php @@ -118,11 +118,12 @@ function wc_add_to_cart_message( $products, $show_qty = false, $return = false ) $added_text = sprintf( _n( '%s has been added to your cart.', '%s have been added to your cart.', $count, 'woocommerce' ), wc_format_list_of_items( $titles ) ); // Output success messages. + $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : ''; if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) { $return_to = apply_filters( 'woocommerce_continue_shopping_redirect', wc_get_raw_referer() ? wp_validate_redirect( wc_get_raw_referer(), false ) : wc_get_page_permalink( 'shop' ) ); - $message = sprintf( '%s %s', esc_url( $return_to ), esc_html__( 'Continue shopping', 'woocommerce' ), esc_html( $added_text ) ); + $message = sprintf( '%s %s', esc_url( $return_to ), esc_attr( $wp_button_class ), esc_html__( 'Continue shopping', 'woocommerce' ), esc_html( $added_text ) ); } else { - $message = sprintf( '%s %s', esc_url( wc_get_cart_url() ), esc_html__( 'View cart', 'woocommerce' ), esc_html( $added_text ) ); + $message = sprintf( '%s %s', esc_url( wc_get_cart_url() ), esc_attr( $wp_button_class ), esc_html__( 'View cart', 'woocommerce' ), esc_html( $added_text ) ); } if ( has_filter( 'wc_add_to_cart_message' ) ) { diff --git a/plugins/woocommerce/includes/wc-conditional-functions.php b/plugins/woocommerce/includes/wc-conditional-functions.php index 780f310095e..aa9d3d522b4 100644 --- a/plugins/woocommerce/includes/wc-conditional-functions.php +++ b/plugins/woocommerce/includes/wc-conditional-functions.php @@ -500,7 +500,7 @@ function wc_is_file_valid_csv( $file, $check_path = true ) { /** * Check if the current theme is a block theme. * - * @since x.x.x + * @since 6.0.0 * @return bool */ function wc_current_theme_is_fse_theme() { @@ -517,10 +517,27 @@ function wc_current_theme_is_fse_theme() { /** * Check if the current theme has WooCommerce support or is a FSE theme. * - * @since x.x.x + * @since 6.0.0 * @return bool */ function wc_current_theme_supports_woocommerce_or_fse() { return (bool) current_theme_supports( 'woocommerce' ) || wc_current_theme_is_fse_theme(); } +/** + * Given an element name, returns a class name. + * + * If the WP-related function is not defined, return empty string. + * + * @param string $element The name of the element. + * + * @since 7.1.0 + * @return string + */ +function wc_wp_theme_get_element_class_name( $element ) { + if ( function_exists( 'wp_theme_get_element_class_name' ) ) { + return wp_theme_get_element_class_name( $element ); + } + + return ''; +} diff --git a/plugins/woocommerce/includes/wc-core-functions.php b/plugins/woocommerce/includes/wc-core-functions.php index 726d681b172..13fc2bee29f 100644 --- a/plugins/woocommerce/includes/wc-core-functions.php +++ b/plugins/woocommerce/includes/wc-core-functions.php @@ -752,7 +752,7 @@ function get_woocommerce_currency_symbols() { 'LKR' => 'රු', 'LRD' => '$', 'LSL' => 'L', - 'LYD' => 'ل.د', + 'LYD' => 'د.ل', 'MAD' => 'د.م.', 'MDL' => 'MDL', 'MGA' => 'Ar', @@ -2397,6 +2397,7 @@ function wc_is_active_theme( $theme ) { function wc_is_wp_default_theme_active() { return wc_is_active_theme( array( + 'twentytwentythree', 'twentytwentytwo', 'twentytwentyone', 'twentytwenty', diff --git a/plugins/woocommerce/includes/wc-template-functions.php b/plugins/woocommerce/includes/wc-template-functions.php index 271e3b9f764..0935b8c85e9 100644 --- a/plugins/woocommerce/includes/wc-template-functions.php +++ b/plugins/woocommerce/includes/wc-template-functions.php @@ -1345,6 +1345,7 @@ if ( ! function_exists( 'woocommerce_template_loop_add_to_cart' ) ) { array_filter( array( 'button', + wc_wp_theme_get_element_class_name( 'button' ), // escaped in the template. 'product_type_' . $product->get_type(), $product->is_purchasable() && $product->is_in_stock() ? 'add_to_cart_button' : '', $product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && $product->is_in_stock() ? 'ajax_add_to_cart' : '', @@ -2193,7 +2194,8 @@ if ( ! function_exists( 'woocommerce_widget_shopping_cart_button_view_cart' ) ) * Output the view cart button. */ function woocommerce_widget_shopping_cart_button_view_cart() { - echo '' . esc_html__( 'View cart', 'woocommerce' ) . ''; + $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : ''; + echo '' . esc_html__( 'View cart', 'woocommerce' ) . ''; } } @@ -2203,7 +2205,8 @@ if ( ! function_exists( 'woocommerce_widget_shopping_cart_proceed_to_checkout' ) * Output the proceed to checkout button. */ function woocommerce_widget_shopping_cart_proceed_to_checkout() { - echo '' . esc_html__( 'Checkout', 'woocommerce' ) . ''; + $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : ''; + echo '' . esc_html__( 'Checkout', 'woocommerce' ) . ''; } } @@ -3207,6 +3210,7 @@ if ( ! function_exists( 'woocommerce_account_orders' ) ) { 'current_page' => absint( $current_page ), 'customer_orders' => $customer_orders, 'has_orders' => 0 < $customer_orders->total, + 'wp_button_class' => wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '', ) ); } diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index 6000ecf111b..9149fffe73b 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -32,7 +32,8 @@ "env:dev": "pnpm wp-env start", "env:test": "pnpm run env:dev && ./tests/e2e-pw/bin/test-env-setup.sh", "e2e-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js", - "env:test:cot": "pnpm run env:dev && ./tests/e2e-pw/bin/test-env-setup.sh --cot", + "test:api-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js", + "env:test:cot": "pnpm run env:dev && ENABLE_HPOS=1 ./tests/e2e-pw/bin/test-env-setup.sh", "env:performance-init": "./tests/performance/bin/init-sample-products.sh", "env:down": "pnpm wp-env stop", "env:destroy": "pnpm wp-env destroy", @@ -49,7 +50,7 @@ "@babel/core": "7.12.9", "@babel/preset-env": "7.12.7", "@babel/register": "7.12.1", - "@playwright/test": "^1.26.1", + "@playwright/test": "^1.27.1", "@typescript-eslint/eslint-plugin": "3.10.1", "@typescript-eslint/experimental-utils": "3.10.1", "@typescript-eslint/parser": "3.10.1", @@ -68,19 +69,20 @@ "allure-commandline": "^2.17.2", "allure-playwright": "^2.0.0-beta.16", "autoprefixer": "9.8.6", + "axios": "^0.24.0", "babel-eslint": "10.1.0", "chai": "4.2.0", "chai-as-promised": "7.1.1", "config": "3.3.3", "cross-env": "6.0.3", "deasync": "0.1.26", + "dotenv": "^10.0.0", "eslint": "^8.12.0", "eslint-config-wpcalypso": "5.0.0", "eslint-plugin-jest": "23.20.0", "istanbul": "1.0.0-alpha.2", "jest": "^27.5.1", "mocha": "7.2.0", - "playwright": "^1.26.1", "prettier": "npm:wp-prettier@2.0.5", "stylelint": "^13.8.0", "typescript": "^4.8.3", diff --git a/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php index 424ab32be94..16b546f4966 100644 --- a/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php +++ b/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php @@ -12,6 +12,7 @@ use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval; use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery; use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache; +use Automattic\WooCommerce\Utilities\OrderUtil; /** * Admin\API\Reports\Customers\DataStore. @@ -64,7 +65,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { 'id' => "{$table_name}.customer_id as id", 'user_id' => 'user_id', 'username' => 'username', - 'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @todo What does this mean for RTL? + 'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @xxx: What does this mean for RTL? 'email' => 'email', 'country' => 'country', 'city' => 'city', @@ -122,7 +123,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { public static function sync_order_customer( $post_id ) { global $wpdb; - if ( 'shop_order' !== get_post_type( $post_id ) && 'shop_order_refund' !== get_post_type( $post_id ) ) { + if ( ! OrderUtil::is_order( $post_id, array( 'shop_order', 'shop_order_refund' ) ) ) { return -1; } diff --git a/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php index 232320d2f08..b4661107704 100644 --- a/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php +++ b/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php @@ -13,6 +13,7 @@ use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval; use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery; use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache; use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore; +use Automattic\WooCommerce\Utilities\OrderUtil; /** * API\Reports\Orders\Stats\DataStore. @@ -113,6 +114,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { * @param array $query_args Query arguments supplied by the user. */ protected function orders_stats_sql_filter( $query_args ) { + // phpcs:ignore Generic.Commenting.Todo.TaskFound // @todo Performance of all of this? global $wpdb; @@ -335,6 +337,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { return new \WP_Error( 'woocommerce_analytics_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) ); } + // phpcs:ignore Generic.Commenting.Todo.TaskFound // @todo Remove these assignements when refactoring segmenter classes to use query objects. $totals_query = array( 'from_clause' => $this->total_query->get_sql_clause( 'join' ), @@ -474,7 +477,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success. */ public static function sync_order( $post_id ) { - if ( 'shop_order' !== get_post_type( $post_id ) && 'shop_order_refund' !== get_post_type( $post_id ) ) { + if ( ! OrderUtil::is_order( $post_id, array( 'shop_order', 'shop_order_refund' ) ) ) { return -1; } @@ -505,6 +508,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { * * @param array $data Data written to order stats lookup table. * @param WC_Order $order Order object. + * + * @since 4.0.0 */ $data = apply_filters( 'woocommerce_analytics_update_order_stats_data', @@ -555,6 +560,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { * Fires when order's stats reports are updated. * * @param int $order_id Order ID. + * + * @since 4.0.0. */ do_action( 'woocommerce_analytics_update_order_stats', $order->get_id() ); @@ -571,7 +578,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { global $wpdb; $order_id = (int) $post_id; - if ( 'shop_order' !== get_post_type( $order_id ) && 'shop_order_refund' !== get_post_type( $order_id ) ) { + if ( ! OrderUtil::is_order( $post_id, array( 'shop_order', 'shop_order_refund' ) ) ) { return; } @@ -586,6 +593,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { * * @param int $order_id Order ID. * @param int $customer_id Customer ID. + * + * @since 4.0.0 */ do_action( 'woocommerce_analytics_delete_order_stats', $order_id, $customer_id ); diff --git a/plugins/woocommerce/src/Admin/Features/NewProductManagementExperience.php b/plugins/woocommerce/src/Admin/Features/NewProductManagementExperience.php new file mode 100644 index 00000000000..f0bcd4da1d3 --- /dev/null +++ b/plugins/woocommerce/src/Admin/Features/NewProductManagementExperience.php @@ -0,0 +1,37 @@ +get_id() ] ) ) { return $snoozed_tasks[ $this->get_id() ]; @@ -290,9 +304,13 @@ abstract class Task { /** * Bool for task snoozed. * + * @deprecated 7.2.0 + * * @return bool */ public function is_snoozed() { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' ); + if ( ! $this->is_snoozeable() ) { return false; } @@ -306,9 +324,14 @@ abstract class Task { * Snooze the task. * * @param string $duration Duration to snooze. day|hour|week. + * + * @deprecated 7.2.0 + * * @return bool */ public function snooze( $duration = 'day' ) { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' ); + if ( ! $this->is_snoozeable() ) { return false; } @@ -330,9 +353,13 @@ abstract class Task { /** * Undo task snooze. * + * @deprecated 7.2.0 + * * @return bool */ public function undo_snooze() { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' ); + $snoozed = get_option( self::SNOOZED_OPTION, array() ); unset( $snoozed[ $this->get_id() ] ); $update = update_option( self::SNOOZED_OPTION, $snoozed ); @@ -406,9 +433,13 @@ abstract class Task { /** * Check if task is disabled. * + * @deprecated 7.2.0 + * * @return bool */ public function is_disabled() { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' ); + return false; } @@ -453,15 +484,15 @@ abstract class Task { 'actionUrl' => $this->get_action_url(), 'isComplete' => $this->is_complete(), 'time' => $this->get_time(), - 'level' => $this->get_level(), + 'level' => 3, 'isActioned' => $this->is_actioned(), 'isDismissed' => $this->is_dismissed(), 'isDismissable' => $this->is_dismissable(), - 'isSnoozed' => $this->is_snoozed(), - 'isSnoozeable' => $this->is_snoozeable(), + 'isSnoozed' => false, + 'isSnoozeable' => false, 'isVisited' => $this->is_visited(), - 'isDisabled' => $this->is_disabled(), - 'snoozedUntil' => $this->get_snoozed_until(), + 'isDisabled' => false, + 'snoozedUntil' => null, 'additionalData' => self::convert_object_to_camelcase( $this->get_additional_data() ), 'eventPrefix' => $this->prefix_event( '' ), ); diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php index e350d5d576c..b769263d19b 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php @@ -99,6 +99,8 @@ class TaskList { /** * Array of TaskListSection. * + * @deprecated 7.2.0 + * * @var array */ private $sections = array(); @@ -106,6 +108,8 @@ class TaskList { /** * Key value map of task class and id used for sections. * + * @deprecated 7.2.0 + * * @var array */ public $task_class_id_map = array(); @@ -126,7 +130,6 @@ class TaskList { 'options' => array(), 'visible' => true, 'display_progress_header' => false, - 'sections' => array(), ); $data = wp_parse_args( $data, $defaults ); @@ -147,12 +150,6 @@ class TaskList { } $this->possibly_remove_reminder_bar(); - $this->sections = array_map( - function( $section ) { - return new TaskListSection( $section, $this ); - }, - $data['sections'] - ); } /** @@ -239,15 +236,13 @@ class TaskList { * @return bool */ public function is_complete() { - $viewable_tasks = $this->get_viewable_tasks(); + foreach ( $this->get_viewable_tasks() as $viewable_task ) { + if ( $viewable_task->is_complete() === false ) { + return false; + } + } - return array_reduce( - $viewable_tasks, - function( $is_complete, $task ) { - return ! $task->is_complete() ? false : $is_complete; - }, - true - ); + return true; } /** @@ -276,9 +271,7 @@ class TaskList { return; } - $task_class_name = substr( get_class( $task ), strrpos( get_class( $task ), '\\' ) + 1 ); - $this->task_class_id_map[ $task_class_name ] = $task->get_id(); - $this->tasks[] = $task; + $this->tasks[] = $task; } /** @@ -317,9 +310,13 @@ class TaskList { /** * Get task list sections. * + * @deprecated 7.2.0 + * * @return array */ public function get_sections() { + wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' ); + return $this->sections; } @@ -422,12 +419,6 @@ class TaskList { 'eventPrefix' => $this->prefix_event( '' ), 'displayProgressHeader' => $this->display_progress_header, 'keepCompletedTaskList' => $this->get_keep_completed_task_list(), - 'sections' => array_map( - function( $section ) { - return $section->get_json(); - }, - $this->sections - ), ); } } diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php index bc343781dd3..65c5f001163 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php @@ -7,6 +7,8 @@ namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks; /** * Task List section class. + * + * @deprecated 7.2.0 */ class TaskListSection { diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php index 31cd3a68bf6..03d5e0bf72d 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php @@ -262,7 +262,7 @@ class TaskLists { * Add a task list. * * @param array $args Task list properties. - * @return WP_Error|TaskList + * @return \WP_Error|TaskList */ public static function add_list( $args ) { if ( isset( self::$lists[ $args['id'] ] ) ) { @@ -281,7 +281,7 @@ class TaskLists { * * @param string $list_id List ID to add the task to. * @param array $args Task properties. - * @return WP_Error|Task + * @return \WP_Error|Task */ public static function add_task( $list_id, $args ) { if ( ! isset( self::$lists[ $list_id ] ) ) { diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php index 36617070dc8..9465bb3e94a 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php @@ -12,6 +12,13 @@ use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayme */ class AdditionalPayments extends Payments { + /** + * Used to cache is_complete() method result. + * @var null + */ + private $is_complete_result = null; + + /** * ID. * @@ -60,7 +67,11 @@ class AdditionalPayments extends Payments { * @return bool */ public function is_complete() { - return self::has_gateways(); + if ( $this->is_complete_result === null ) { + $this->is_complete_result = self::has_gateways(); + } + + return $this->is_complete_result; } /** diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php index e7ba5cdc3e7..cadd67a6d5b 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php @@ -39,9 +39,6 @@ class Appearance extends Task { * @return string */ public function get_title() { - if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Make your store stand out with unique design', 'woocommerce' ); - } if ( $this->get_parent_option( 'use_completed_title' ) === true ) { if ( $this->is_complete() ) { return __( 'You personalized your store', 'woocommerce' ); @@ -57,9 +54,6 @@ class Appearance extends Task { * @return string */ public function get_content() { - if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Upload your logo to adapt the store to your brand’s personality.', 'woocommerce' ); - } return __( 'Add your logo, create a homepage, and start designing your store.', 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php index 9a35e05b5d5..1ec834bf758 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php @@ -25,9 +25,6 @@ class Marketing extends Task { * @return string */ public function get_title() { - if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Grow your business with marketing tools', 'woocommerce' ); - } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You added sales channels', 'woocommerce' ); @@ -43,9 +40,6 @@ class Marketing extends Task { * @return string */ public function get_content() { - if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Promote your store in other sales channels, like email, Google, and Facebook.', 'woocommerce' ); - } return __( 'Add recommended marketing tools to reach new customers and grow your business', 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php index 6cd523c6f63..d12f0846cda 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php @@ -3,13 +3,19 @@ namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks; use Automattic\WooCommerce\Admin\Features\Features; -use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayments; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task; /** * Payments Task */ class Payments extends Task { + + /** + * Used to cache is_complete() method result. + * @var null + */ + private $is_complete_result = null; + /** * ID. * @@ -25,9 +31,6 @@ class Payments extends Task { * @return string */ public function get_title() { - if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Add a way to get paid', 'woocommerce' ); - } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You set up payments', 'woocommerce' ); @@ -43,9 +46,6 @@ class Payments extends Task { * @return string */ public function get_content() { - if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Let your customers pay the way they like.', 'woocommerce' ); - } return __( 'Choose payment providers and enable payment methods at checkout.', 'woocommerce' @@ -67,7 +67,11 @@ class Payments extends Task { * @return bool */ public function is_complete() { - return self::has_gateways(); + if ( $this->is_complete_result === null ) { + $this->is_complete_result = self::has_gateways(); + } + + return $this->is_complete_result; } /** diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php index 56ba733aa56..0b2ee58fbd3 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php @@ -37,9 +37,6 @@ class Products extends Task { * @return string */ public function get_title() { - if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Create or upload your first products', 'woocommerce' ); - } if ( $this->get_parent_option( 'use_completed_title' ) === true ) { if ( $this->is_complete() ) { return __( 'You added products', 'woocommerce' ); @@ -55,9 +52,6 @@ class Products extends Task { * @return string */ public function get_content() { - if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Add products to sell and build your catalog.', 'woocommerce' ); - } return __( 'Start by adding the first product to your store. You can add your products manually, via CSV, or import them from another service.', 'woocommerce' @@ -167,15 +161,7 @@ class Products extends Task { * @return bool */ public static function has_products() { - $product_query = new \WC_Product_Query( - array( - 'limit' => 1, - 'return' => 'ids', - 'status' => array( 'publish' ), - ) - ); - $products = $product_query->get_products(); - - return count( $products ) !== 0; + $counts = wp_count_posts('product'); + return isset( $counts->publish ) && $counts->publish > 0; } } diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php index 7fe8607b68b..d42d2322c26 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php @@ -43,9 +43,6 @@ class Shipping extends Task { * @return string */ public function get_title() { - if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Select how to ship your products', 'woocommerce' ); - } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You added shipping costs', 'woocommerce' ); @@ -61,9 +58,6 @@ class Shipping extends Task { * @return string */ public function get_content() { - if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Set delivery costs and enable extra features, like shipping label printing.', 'woocommerce' ); - } return __( "Set your store location and where you'll ship to.", 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php index ca06c415616..57d52d2d726 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php @@ -13,6 +13,12 @@ use Automattic\WooCommerce\Internal\Admin\WCAdminAssets; */ class Tax extends Task { + /** + * Used to cache is_complete() method result. + * @var null + */ + private $is_complete_result = null; + /** * Constructor * @@ -56,9 +62,6 @@ class Tax extends Task { * @return string */ public function get_title() { - if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Get taxes out of your mind', 'woocommerce' ); - } if ( $this->get_parent_option( 'use_completed_title' ) === true ) { if ( $this->is_complete() ) { return __( 'You added tax rates', 'woocommerce' ); @@ -74,9 +77,6 @@ class Tax extends Task { * @return string */ public function get_content() { - if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Have sales tax calculated automatically, or add the rates manually.', 'woocommerce' ); - } return self::can_use_automated_taxes() ? __( 'Good news! WooCommerce Services and Jetpack can automate your sales tax calculations for you.', @@ -114,9 +114,13 @@ class Tax extends Task { * @return bool */ public function is_complete() { - return get_option( 'wc_connect_taxes_enabled' ) || - count( TaxDataStore::get_taxes( array() ) ) > 0 || - get_option( 'woocommerce_no_sales_tax' ) !== false; + if ( $this->is_complete_result === null ) { + $this->is_complete_result = get_option( 'wc_connect_taxes_enabled' ) || + count( TaxDataStore::get_taxes( array() ) ) > 0 || + get_option( 'woocommerce_no_sales_tax' ) !== false; + } + + return $this->is_complete_result; } /** diff --git a/plugins/woocommerce/src/Admin/Notes/DeprecatedNotes.php b/plugins/woocommerce/src/Admin/Notes/DeprecatedNotes.php index 2920f0880b0..baf2645ada9 100644 --- a/plugins/woocommerce/src/Admin/Notes/DeprecatedNotes.php +++ b/plugins/woocommerce/src/Admin/Notes/DeprecatedNotes.php @@ -223,27 +223,6 @@ class WC_Admin_Notes_Giving_Feedback_Notes extends DeprecatedClassFacade { protected static $deprecated_in_version = '4.8.0'; } -/** - * WC_Admin_Notes_Insight_First_Sale. - * - * @deprecated since 4.8.0, use InsightFirstSale - */ -class WC_Admin_Notes_Insight_First_Sale extends DeprecatedClassFacade { - /** - * The name of the non-deprecated class that this facade covers. - * - * @var string - */ - protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\InsightFirstSale'; - - /** - * The version that this class was deprecated in. - * - * @var string - */ - protected static $deprecated_in_version = '4.8.0'; -} - /** * WC_Admin_Notes_Install_JP_And_WCS_Plugins. * diff --git a/plugins/woocommerce/src/Admin/PluginsHelper.php b/plugins/woocommerce/src/Admin/PluginsHelper.php index 29e22927262..c2c760d2378 100644 --- a/plugins/woocommerce/src/Admin/PluginsHelper.php +++ b/plugins/woocommerce/src/Admin/PluginsHelper.php @@ -150,6 +150,7 @@ class PluginsHelper { * Filter the list of plugins to install. * * @param array $plugins A list of the plugins to install. + * @since 6.4.0 */ $plugins = apply_filters( 'woocommerce_admin_plugins_pre_install', $plugins ); @@ -193,12 +194,19 @@ class PluginsHelper { if ( is_wp_error( $api ) ) { $properties = array( /* translators: %s: plugin slug (example: woocommerce-services) */ - 'error_message' => __( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce' ), - 'api' => $api, - 'slug' => $slug, + 'error_message' => sprintf( __( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce' ), $slug ), + 'api_error_message' => $api->get_error_message(), + 'slug' => $slug, ); wc_admin_record_tracks_event( 'install_plugin_error', $properties ); + /** + * Action triggered when a plugin API call failed. + * + * @param string $slug The plugin slug. + * @param \WP_Error $api The API response. + * @since 6.4.0 + */ do_action( 'woocommerce_plugins_install_api_error', $slug, $api ); $errors->add( @@ -221,14 +229,24 @@ class PluginsHelper { if ( is_wp_error( $result ) || is_null( $result ) ) { $properties = array( /* translators: %s: plugin slug (example: woocommerce-services) */ - 'error_message' => __( 'The requested plugin `%s` could not be installed.', 'woocommerce' ), - 'slug' => $slug, - 'api' => $api, - 'upgrader' => $upgrader, - 'result' => $result, + 'error_message' => sprintf( __( 'The requested plugin `%s` could not be installed.', 'woocommerce' ), $slug ), + 'slug' => $slug, + 'api_version' => $api->version, + 'api_download_link' => $api->download_link, + 'upgrader_skin_message' => implode( ',', $upgrader->skin->get_upgrade_messages() ), + 'result' => $result, ); wc_admin_record_tracks_event( 'install_plugin_error', $properties ); + /** + * Action triggered when a plugin installation fails. + * + * @param string $slug The plugin slug. + * @param object $api The plugin API object. + * @param \WP_Error|null $result The result of the plugin installation. + * @param \Plugin_Upgrader $upgrader The plugin upgrader. + * @since 6.4.0 + */ do_action( 'woocommerce_plugins_install_error', $slug, $api, $result, $upgrader ); $errors->add( @@ -292,6 +310,7 @@ class PluginsHelper { * Filter the list of plugins to activate. * * @param array $plugins A list of the plugins to activate. + * @since 6.4.0 */ $plugins = apply_filters( 'woocommerce_admin_plugins_pre_activate', $plugins ); @@ -314,6 +333,13 @@ class PluginsHelper { $result = activate_plugin( $path ); if ( ! is_null( $result ) ) { + /** + * Action triggered when a plugin activation fails. + * + * @param string $slug The plugin slug. + * @param null|\WP_Error $result The result of the plugin activation. + * @since 6.4.0 + */ do_action( 'woocommerce_plugins_activate_error', $slug, $result ); $errors->add( diff --git a/plugins/woocommerce/src/Internal/Admin/Events.php b/plugins/woocommerce/src/Internal/Admin/Events.php index 58e1fb89586..94799e91ebd 100644 --- a/plugins/woocommerce/src/Internal/Admin/Events.php +++ b/plugins/woocommerce/src/Internal/Admin/Events.php @@ -11,22 +11,17 @@ use \Automattic\WooCommerce\Admin\Features\Features; use \Automattic\WooCommerce\Admin\RemoteInboxNotifications\DataSourcePoller; use \Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsEngine; use \Automattic\WooCommerce\Internal\Admin\Notes\AddFirstProduct; -use \Automattic\WooCommerce\Internal\Admin\Notes\AddingAndManangingProducts; use \Automattic\WooCommerce\Internal\Admin\Notes\ChoosingTheme; use \Automattic\WooCommerce\Internal\Admin\Notes\CouponPageMoved; use \Automattic\WooCommerce\Internal\Admin\Notes\CustomizeStoreWithBlocks; use \Automattic\WooCommerce\Internal\Admin\Notes\CustomizingProductCatalog; use \Automattic\WooCommerce\Internal\Admin\Notes\EditProductsOnTheMove; use \Automattic\WooCommerce\Internal\Admin\Notes\EUVATNumber; -use \Automattic\WooCommerce\Internal\Admin\Notes\FirstDownlaodableProduct; use \Automattic\WooCommerce\Internal\Admin\Notes\FirstProduct; -use \Automattic\WooCommerce\Internal\Admin\Notes\InsightFirstProductAndPayment; -use \Automattic\WooCommerce\Internal\Admin\Notes\InsightFirstSale; use \Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins; use \Automattic\WooCommerce\Internal\Admin\Notes\LaunchChecklist; use \Automattic\WooCommerce\Internal\Admin\Notes\MagentoMigration; use \Automattic\WooCommerce\Internal\Admin\Notes\ManageOrdersOnTheGo; -use \Automattic\WooCommerce\Internal\Admin\Notes\ManageStoreActivityFromHomeScreen; use \Automattic\WooCommerce\Internal\Admin\Notes\MarketingJetpack; use \Automattic\WooCommerce\Internal\Admin\Notes\MerchantEmailNotifications; use \Automattic\WooCommerce\Internal\Admin\Notes\MigrateFromShopify; @@ -43,7 +38,6 @@ use \Automattic\WooCommerce\Internal\Admin\Notes\SellingOnlineCourses; use \Automattic\WooCommerce\Internal\Admin\Notes\TestCheckout; use \Automattic\WooCommerce\Internal\Admin\Notes\TrackingOptIn; use \Automattic\WooCommerce\Internal\Admin\Notes\UnsecuredReportFiles; -use \Automattic\WooCommerce\Internal\Admin\Notes\UpdateStoreDetails; use \Automattic\WooCommerce\Internal\Admin\Notes\WelcomeToWooCommerceForStoreUsers; use \Automattic\WooCommerce\Internal\Admin\Notes\WooCommercePayments; use \Automattic\WooCommerce\Internal\Admin\Notes\WooCommerceSubscriptions; @@ -76,16 +70,12 @@ class Events { */ private static $note_classes_to_added_or_updated = array( AddFirstProduct::class, - AddingAndManangingProducts::class, ChoosingTheme::class, CustomizeStoreWithBlocks::class, CustomizingProductCatalog::class, EditProductsOnTheMove::class, EUVATNumber::class, - FirstDownlaodableProduct::class, FirstProduct::class, - InsightFirstProductAndPayment::class, - InsightFirstSale::class, LaunchChecklist::class, MagentoMigration::class, ManageOrdersOnTheGo::class, @@ -101,7 +91,6 @@ class Events { RealTimeOrderAlerts::class, TestCheckout::class, TrackingOptIn::class, - UpdateStoreDetails::class, WooCommercePayments::class, WooCommerceSubscriptions::class, ); @@ -114,7 +103,6 @@ class Events { private static $other_note_classes = array( CouponPageMoved::class, InstallJPAndWCSPlugins::class, - ManageStoreActivityFromHomeScreen::class, OrderMilestones::class, SellingOnlineCourses::class, UnsecuredReportFiles::class, @@ -129,7 +117,7 @@ class Events { * @return object Instance. */ final public static function instance() { - if ( static::$instance === null ) { + if ( null === static::$instance ) { static::$instance = new static(); } return static::$instance; diff --git a/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php b/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php index f9e94523282..ad67eded950 100644 --- a/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php +++ b/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php @@ -18,7 +18,6 @@ use \Automattic\WooCommerce\Internal\Admin\Notes\TestCheckout; use \Automattic\WooCommerce\Internal\Admin\Notes\SellingOnlineCourses; use \Automattic\WooCommerce\Internal\Admin\Notes\MerchantEmailNotifications; use \Automattic\WooCommerce\Internal\Admin\Notes\WelcomeToWooCommerceForStoreUsers; -use \Automattic\WooCommerce\Internal\Admin\Notes\ManageStoreActivityFromHomeScreen; use \Automattic\WooCommerce\Internal\Admin\Notes\MagentoMigration; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\PluginsHelper; @@ -182,7 +181,6 @@ class FeaturePlugin { new TestCheckout(); new SellingOnlineCourses(); new WelcomeToWooCommerceForStoreUsers(); - new ManageStoreActivityFromHomeScreen(); new MagentoMigration(); // Initialize MerchantEmailNotifications. diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/AddingAndManangingProducts.php b/plugins/woocommerce/src/Internal/Admin/Notes/AddingAndManangingProducts.php deleted file mode 100644 index 6dba24c73d8..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Notes/AddingAndManangingProducts.php +++ /dev/null @@ -1,80 +0,0 @@ - 3 days and - * product count is 0 - * - * @package WooCommerce\Admin - */ - -namespace Automattic\WooCommerce\Internal\Admin\Notes; - -defined( 'ABSPATH' ) || exit; - -use \Automattic\WooCommerce\Admin\Notes\Note; -use \Automattic\WooCommerce\Admin\Notes\NoteTraits; - -/** - * Class AddingAndManangingProducts - * - * @package Automattic\WooCommerce\Admin\Notes - */ -class AddingAndManangingProducts { - /** - * Note traits. - */ - use NoteTraits; - - /** - * Name of the note for use in the database. - */ - const NOTE_NAME = 'wc-admin-adding-and-managing-products'; - - /** - * Get the note. - * - * @return Note|null - */ - public static function get_note() { - // Store must have been at least 3 days. - if ( ! self::is_wc_admin_active_in_date_range( 'week-1', DAY_IN_SECONDS * 3 ) ) { - return; - } - - // Total # of products must be 0. - $query = new \WC_Product_Query( - array( - 'limit' => 1, - 'paginate' => true, - 'return' => 'ids', - 'status' => array( 'publish' ), - ) - ); - - $products = $query->get_products(); - if ( 0 !== $products->total ) { - return; - } - - $note = new Note(); - $note->set_title( __( 'Adding and Managing Products', 'woocommerce' ) ); - $note->set_content( - __( - 'Learn more about how to set up products in WooCommerce through our useful documentation about adding and managing products.', - 'woocommerce' - ) - ); - $note->set_content_data( (object) array() ); - $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); - $note->set_name( self::NOTE_NAME ); - $note->set_source( 'woocommerce-admin' ); - $note->add_action( - 'learn-more', - __( 'Learn more', 'woocommerce' ), - 'https://woocommerce.com/document/managing-products/?utm_source=inbox&utm_medium=product' - ); - - return $note; - } -} diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/FirstDownlaodableProduct.php b/plugins/woocommerce/src/Internal/Admin/Notes/FirstDownlaodableProduct.php deleted file mode 100644 index 4e7fa864528..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Notes/FirstDownlaodableProduct.php +++ /dev/null @@ -1,71 +0,0 @@ - 1, - 'paginate' => true, - 'return' => 'ids', - 'downloadable' => 1, - 'status' => array( 'publish' ), - ) - ); - $products = $query->get_products(); - - // There must be at least 1 downloadable product. - if ( 0 === $products->total ) { - return; - } - - $note = new Note(); - $note->set_title( __( 'Learn more about digital/downloadable products', 'woocommerce' ) ); - $note->set_content( - __( - 'Congrats on adding your first digital product! You can learn more about how to handle digital or downloadable products in our documentation.', - 'woocommerce' - ) - ); - $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); - $note->set_name( self::NOTE_NAME ); - $note->set_content_data( (object) array() ); - $note->set_source( 'woocommerce-admin' ); - $note->add_action( - 'first-downloadable-product-handling', - __( 'Learn more', 'woocommerce' ), - 'https://woocommerce.com/document/digital-downloadable-product-handling/?utm_source=inbox&utm_medium=product' - ); - - return $note; - } -} diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstProductAndPayment.php b/plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstProductAndPayment.php deleted file mode 100644 index 8d78705ac2f..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstProductAndPayment.php +++ /dev/null @@ -1,69 +0,0 @@ -set_title( __( 'Insight', 'woocommerce' ) ); - $note->set_content( __( 'More than 80% of new merchants add the first product and have at least one payment method set up during the first week.

Do you find this type of insight useful?', 'woocommerce' ) ); - $note->set_type( Note::E_WC_ADMIN_NOTE_SURVEY ); - $note->set_name( self::NOTE_NAME ); - $note->set_content_data( (object) array() ); - $note->set_source( 'woocommerce-admin' ); - $note->add_action( - 'affirm-insight-first-product-and-payment', - __( 'Yes', 'woocommerce' ), - false, - Note::E_WC_ADMIN_NOTE_ACTIONED, - false, - __( 'Thanks for your feedback', 'woocommerce' ) - ); - - $note->add_action( - 'affirm-insight-first-product-and-payment', - __( 'No', 'woocommerce' ), - false, - Note::E_WC_ADMIN_NOTE_ACTIONED, - false, - __( 'Thanks for your feedback', 'woocommerce' ) - ); - - return $note; - } -} diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstSale.php b/plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstSale.php deleted file mode 100644 index 1cc28010536..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Notes/InsightFirstSale.php +++ /dev/null @@ -1,71 +0,0 @@ -set_title( __( 'Did you know?', 'woocommerce' ) ); - $note->set_content( __( 'A WooCommerce powered store needs on average 31 days to get the first sale. You\'re on the right track! Do you find this type of insight useful?', 'woocommerce' ) ); - $note->set_type( Note::E_WC_ADMIN_NOTE_SURVEY ); - $note->set_name( self::NOTE_NAME ); - $note->set_content_data( (object) array() ); - $note->set_source( 'woocommerce-admin' ); - - // Note that there is no corresponding function called in response to - // this. Apart from setting the note to actioned a tracks event is - // sent in NoteActions. - $note->add_action( - 'affirm-insight-first-sale', - __( 'Yes', 'woocommerce' ), - false, - Note::E_WC_ADMIN_NOTE_ACTIONED, - false, - __( 'Thanks for your feedback', 'woocommerce' ) - ); - $note->add_action( - 'deny-insight-first-sale', - __( 'No', 'woocommerce' ), - false, - Note::E_WC_ADMIN_NOTE_ACTIONED, - false, - __( 'Thanks for your feedback', 'woocommerce' ) - ); - - return $note; - } -} diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/ManageStoreActivityFromHomeScreen.php b/plugins/woocommerce/src/Internal/Admin/Notes/ManageStoreActivityFromHomeScreen.php deleted file mode 100644 index 026f6bf5024..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Notes/ManageStoreActivityFromHomeScreen.php +++ /dev/null @@ -1,68 +0,0 @@ -set_title( __( 'New! Manage your store activity from the Home screen', 'woocommerce' ) ); - $note->set_content( __( 'Start your day knowing the next steps you need to take with your orders, products, and customer feedback.

Read more about how to use the activity panels on the Home screen.', 'woocommerce' ) ); - $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); - $note->set_name( self::NOTE_NAME ); - $note->set_source( 'woocommerce-admin' ); - $note->add_action( - 'learn-more', - __( 'Learn more', 'woocommerce' ), - 'https://woocommerce.com/document/home-screen/?utm_source=inbox&utm_medium=product', - Note::E_WC_ADMIN_NOTE_ACTIONED - ); - - return $note; - } -} diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/UpdateStoreDetails.php b/plugins/woocommerce/src/Internal/Admin/Notes/UpdateStoreDetails.php deleted file mode 100644 index f200333753b..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Notes/UpdateStoreDetails.php +++ /dev/null @@ -1,60 +0,0 @@ -set_title( __( 'Edit your store details if you need to', 'woocommerce' ) ); - $note->set_content( __( 'Nice work completing your store profile! You can always go back and edit the details you just shared, as needed.', 'woocommerce' ) ); - $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); - $note->set_name( self::NOTE_NAME ); - $note->set_content_data( (object) array() ); - $note->set_source( 'woocommerce-admin' ); - $note->add_action( - 'update-store-details', - __( 'Update store details', 'woocommerce' ), - wc_admin_url( '&path=/setup-wizard' ) - ); - - return $note; - } -} diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/COTRedirectionController.php b/plugins/woocommerce/src/Internal/Admin/Orders/COTRedirectionController.php new file mode 100644 index 00000000000..f307bad74d7 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Admin/Orders/COTRedirectionController.php @@ -0,0 +1,75 @@ +get( COTRedirectionController::class ), 'handle_hpos_admin_requests' ) + * ); + */ +class COTRedirectionController { + use AccessiblePrivateMethods; + + /** + * Add hooks needed to perform our magic. + */ + public function setup(): void { + // Only take action in cases where access to the admin screen would otherwise be denied. + self::add_action( 'admin_page_access_denied', array( $this, 'handle_hpos_admin_requests' ) ); + } + + /** + * Listen for denied admin requests and, if they appear to relate to HPOS admin screens, potentially + * redirect the user to the equivalent CPT-driven screens. + * + * @param array|null $query_params The query parameters to use when determining the redirect. If not provided, the $_GET superglobal will be used. + */ + private function handle_hpos_admin_requests( $query_params = null ) { + $query_params = is_array( $query_params ) ? $query_params : $_GET; + + if ( ! isset( $query_params['page'] ) || 'wc-orders' !== $query_params['page'] ) { + return; + } + + $params = wp_unslash( $query_params ); + $action = $params['action'] ?? ''; + unset( $params['page'] ); + + if ( 'edit' === $action && isset( $params['id'] ) ) { + $params['post'] = $params['id']; + unset( $params['id'] ); + $new_url = add_query_arg( $params, get_admin_url( null, 'post.php' ) ); + } elseif ( 'new' === $action ) { + unset( $params['action'] ); + $params['post_type'] = 'shop_order'; + $new_url = add_query_arg( $params, get_admin_url( null, 'post-new.php' ) ); + } else { + // If nonce parameters are present and valid, rebuild them for the CPT admin list table. + if ( isset( $params['_wpnonce'] ) && check_admin_referer( 'bulk-orders' ) ) { + $params['_wp_http_referer'] = get_admin_url( null, 'edit.php?post_type=shop_order' ); + $params['_wpnonce'] = wp_create_nonce( 'bulk-posts' ); + } + + // If an `order` array parameter is present, rename as `post`. + if ( isset( $params['order'] ) && is_array( $params['order'] ) ) { + $params['post'] = $params['order']; + unset( $params['order'] ); + } + + $params['post_type'] = 'shop_order'; + $new_url = add_query_arg( $params, get_admin_url( null, 'edit.php' ) ); + } + + if ( ! empty( $new_url ) && wp_safe_redirect( $new_url, 301 ) ) { + exit; + } + } +} diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php b/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php index c98f1090b33..c16c42482f5 100644 --- a/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php +++ b/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php @@ -941,9 +941,24 @@ class ListTable extends WP_List_Table { if ( isset( $order_statuses[ 'wc-' . $new_status ] ) ) { $changed = $this->do_bulk_action_mark_orders( $ids, $new_status ); } + } else { + $screen = get_current_screen()->id; + + /** + * This action is documented in /wp-admin/edit.php (it is a core WordPress hook). + * + * @since 7.2.0 + * + * @param string $redirect_to The URL to redirect to after processing the bulk actions. + * @param string $action The current bulk action. + * @param int[] $ids IDs for the orders to be processed. + */ + $custom_sendback = apply_filters( "handle_bulk_actions-{$screen}", $redirect_to, $action, $ids ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } - if ( $changed ) { + if ( ! empty( $custom_sendback ) ) { + $redirect_to = $custom_sendback; + } elseif ( $changed ) { $redirect_to = add_query_arg( array( 'bulk_action' => $report_action, diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php b/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php index 01b48ec8eaf..695fbeef930 100644 --- a/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php +++ b/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php @@ -58,6 +58,9 @@ class PageController { if ( ! current_user_can( 'edit_others_shop_orders' ) && ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to edit this order', 'woocommerce' ) ); } + if ( 'trash' === $this->order->get_status() ) { + wp_die( esc_html__( 'You cannot edit this item because it is in the Trash. Please restore it and try again.', 'woocommerce' ) ); + } } /** diff --git a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php index 9e43152d523..f9592146d94 100644 --- a/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php +++ b/plugins/woocommerce/src/Internal/Admin/Schedulers/OrdersScheduler.php @@ -13,6 +13,9 @@ use \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsData use \Automattic\WooCommerce\Admin\API\Reports\Taxes\DataStore as TaxesDataStore; use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore; use \Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache; +use Automattic\WooCommerce\Admin\Overrides\Order; +use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; +use Automattic\WooCommerce\Utilities\OrderUtil; /** * OrdersScheduler Class. @@ -36,7 +39,8 @@ class OrdersScheduler extends ImportScheduler { \Automattic\WooCommerce\Admin\Overrides\OrderRefund::add_filters(); // Order and refund data must be run on these hooks to ensure meta data is set. - add_action( 'save_post', array( __CLASS__, 'possibly_schedule_import' ) ); + add_action( 'woocommerce_update_order', array( __CLASS__, 'possibly_schedule_import' ) ); + add_action( 'woocommerce_create_order', array( __CLASS__, 'possibly_schedule_import' ) ); add_action( 'woocommerce_refund_created', array( __CLASS__, 'possibly_schedule_import' ) ); OrdersStatsDataStore::init(); @@ -69,6 +73,25 @@ class OrdersScheduler extends ImportScheduler { * @param bool $skip_existing Skip already imported orders. */ public static function get_items( $limit = 10, $page = 1, $days = false, $skip_existing = false ) { + if ( OrderUtil::custom_orders_table_usage_is_enabled() ) { + return self::get_items_from_orders_table( $limit, $page, $days, $skip_existing ); + } else { + return self::get_items_from_posts_table( $limit, $page, $days, $skip_existing ); + } + } + + /** + * Helper method to ger order/refund IDS and total count that needs to be synced. + * + * @internal + * @param int $limit Number of records to retrieve. + * @param int $page Page number. + * @param int|bool $days Number of days prior to current date to limit search results. + * @param bool $skip_existing Skip already imported orders. + * + * @return object Total counts. + */ + private static function get_items_from_posts_table( $limit, $page, $days, $skip_existing ) { global $wpdb; $where_clause = ''; $offset = $page > 1 ? ( $page - 1 ) * $limit : 0; @@ -112,6 +135,65 @@ class OrdersScheduler extends ImportScheduler { ); } + /** + * Helper method to ger order/refund IDS and total count that needs to be synced from HPOS. + * + * @internal + * @param int $limit Number of records to retrieve. + * @param int $page Page number. + * @param int|bool $days Number of days prior to current date to limit search results. + * @param bool $skip_existing Skip already imported orders. + * + * @return object Total counts. + */ + private static function get_items_from_orders_table( $limit, $page, $days, $skip_existing ) { + global $wpdb; + $where_clause = ''; + $offset = $page > 1 ? ( $page - 1 ) * $limit : 0; + $order_table = OrdersTableDataStore::get_orders_table_name(); + + if ( is_int( $days ) ) { + $days_ago = gmdate( 'Y-m-d 00:00:00', time() - ( DAY_IN_SECONDS * $days ) ); + $where_clause .= " AND orders.date_created_gmt >= '{$days_ago}'"; + } + + if ( $skip_existing ) { + $where_clause .= "AND NOT EXiSTS ( + SELECT 1 FROM {$wpdb->prefix}wc_order_stats + WHERE {$wpdb->prefix}wc_order_stats.order_id = orders.id + ) + "; + } + + $count = $wpdb->get_var( + " +SELECT COUNT(*) FROM {$order_table} AS orders +WHERE type in ( 'shop_order', 'shop_order_refund' ) +AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' ) +{$where_clause} +" + ); // phpcs:ignore unprepared SQL ok. + + $order_ids = absint( $count ) > 0 ? $wpdb->get_col( + $wpdb->prepare( + "SELECT id FROM {$order_table} AS orders + WHERE type IN ( 'shop_order', 'shop_order_refund' ) + AND status NOT IN ( 'wc-auto-draft', 'auto-draft', 'trash' ) + {$where_clause} + ORDER BY date_created_gmt ASC + LIMIT %d + OFFSET %d", + $limit, + $offset + ) + ) : array(); // phpcs:ignore unprepared SQL ok. + + return (object) array( + 'total' => absint( $count ), + 'ids' => $order_ids, + ); + } + /** * Get total number of rows imported. * @@ -125,15 +207,16 @@ class OrdersScheduler extends ImportScheduler { /** * Schedule this import if the post is an order or refund. * + * @param int $order_id Post ID. + * * @internal - * @param int $post_id Post ID. */ - public static function possibly_schedule_import( $post_id ) { - if ( 'shop_order' !== get_post_type( $post_id ) && 'woocommerce_refund_created' !== current_filter() ) { + public static function possibly_schedule_import( $order_id ) { + if ( ! OrderUtil::is_order( $order_id, array( 'shop_order' ) ) && 'woocommerce_refund_created' !== current_filter() ) { return; } - self::schedule_action( 'import', array( $post_id ) ); + self::schedule_action( 'import', array( $order_id ) ); } /** diff --git a/plugins/woocommerce/src/Internal/Admin/ShippingLabelBanner.php b/plugins/woocommerce/src/Internal/Admin/ShippingLabelBanner.php index f3b10b9d11f..200dd36c50e 100644 --- a/plugins/woocommerce/src/Internal/Admin/ShippingLabelBanner.php +++ b/plugins/woocommerce/src/Internal/Admin/ShippingLabelBanner.php @@ -46,7 +46,7 @@ class ShippingLabelBanner { } if ( class_exists( Jetpack_Connection_Manager::class ) ) { - $jetpack_connected = ( new Jetpack_Connection_Manager() )->is_active(); + $jetpack_connected = ( new Jetpack_Connection_Manager() )->has_connected_owner(); } if ( class_exists( '\WC_Connect_Loader' ) ) { @@ -82,6 +82,9 @@ class ShippingLabelBanner { * @param \WP_Post $post Current post object. */ public function add_meta_boxes( $post_type, $post ) { + if ( 'shop_order' !== $post_type ) { + return; + } $order = wc_get_order( $post ); if ( $this->should_show_meta_box() ) { add_meta_box( diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php index d9551175a09..2d30539c601 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -7,6 +7,8 @@ namespace Automattic\WooCommerce\Internal\DataStores\Orders; use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController; use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessorInterface; +use Automattic\WooCommerce\Internal\Features\FeaturesController; +use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil; use Automattic\WooCommerce\Proxies\LegacyProxy; @@ -20,6 +22,8 @@ defined( 'ABSPATH' ) || exit; */ class DataSynchronizer implements BatchProcessorInterface { + use AccessiblePrivateMethods; + public const ORDERS_DATA_SYNC_ENABLED_OPTION = 'woocommerce_custom_orders_table_data_sync_enabled'; private const INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION = 'woocommerce_initial_orders_pending_sync_count'; public const PENDING_SYNCHRONIZATION_FINISHED_ACTION = 'woocommerce_orders_sync_finished'; @@ -63,28 +67,10 @@ class DataSynchronizer implements BatchProcessorInterface { * Class constructor. */ public function __construct() { - // When posts is authoritative and sync is enabled, deleting a post also deletes COT data. - add_action( - 'deleted_post', - function( $postid, $post ) { - if ( 'shop_order' === $post->post_type && ! $this->custom_orders_table_is_authoritative() && $this->data_sync_is_enabled() ) { - $this->data_store->delete_order_data_from_custom_order_tables( $postid ); - } - }, - 10, - 2 - ); - - // When posts is authoritative and sync is enabled, updating a post triggers a corresponding change in the COT table. - add_action( - 'woocommerce_update_order', - function ( $order_id ) { - if ( ! $this->custom_orders_table_is_authoritative() && $this->data_sync_is_enabled() ) { - $this->posts_to_cot_migrator->migrate_orders( array( $order_id ) ); - } - }, - 100 - ); + self::add_action( 'deleted_post', array( $this, 'handle_deleted_post' ), 10, 2 ); + self::add_action( 'woocommerce_new_order', array( $this, 'handle_updated_order' ), 100 ); + self::add_action( 'woocommerce_update_order', array( $this, 'handle_updated_order' ), 100 ); + self::add_filter( 'woocommerce_feature_description_tip', array( $this, 'handle_feature_description_tip' ), 10, 3 ); } /** @@ -93,7 +79,8 @@ class DataSynchronizer implements BatchProcessorInterface { * @param OrdersTableDataStore $data_store The data store to use. * @param DatabaseUtil $database_util The database util class to use. * @param PostsToOrdersMigrationController $posts_to_cot_migrator The posts to COT migration class to use. - *@internal + * @param LegacyProxy $legacy_proxy The legacy proxy instance to use. + * @internal */ final public function init( OrdersTableDataStore $data_store, DatabaseUtil $database_util, PostsToOrdersMigrationController $posts_to_cot_migrator, LegacyProxy $legacy_proxy ) { $this->data_store = $data_store; @@ -266,8 +253,10 @@ SELECT( $order_post_types = wc_get_order_types( 'cot-migration' ); $order_post_type_placeholders = implode( ', ', array_fill( 0, count( $order_post_types ), '%s' ) ); + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared switch ( $type ) { case self::ID_TYPE_MISSING_IN_ORDERS_TABLE: + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholders is prepared. $sql = $wpdb->prepare( " SELECT posts.ID FROM $wpdb->posts posts @@ -305,6 +294,7 @@ WHERE default: throw new \Exception( 'Invalid $type, must be one of the ID_TYPE_... constants.' ); } + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // phpcs:ignore WordPress.DB return array_map( 'intval', $wpdb->get_col( $sql . " LIMIT $limit" ) ); @@ -412,4 +402,96 @@ WHERE public function get_description(): string { return 'Synchronizes orders between posts and custom order tables.'; } + + /** + * Handle the 'deleted_post' action. + * + * When posts is authoritative and sync is enabled, deleting a post also deletes COT data. + * + * @param int $postid The post id. + * @param WP_Post $post The deleted post. + */ + private function handle_deleted_post( $postid, $post ): void { + if ( 'shop_order' === $post->post_type && ! $this->custom_orders_table_is_authoritative() && $this->data_sync_is_enabled() ) { + $this->data_store->delete_order_data_from_custom_order_tables( $postid ); + } + } + + /** + * Handle the 'woocommerce_update_order' action. + * + * When posts is authoritative and sync is enabled, updating a post triggers a corresponding change in the COT table. + * + * @param int $order_id The order id. + */ + private function handle_updated_order( $order_id ): void { + if ( ! $this->custom_orders_table_is_authoritative() && $this->data_sync_is_enabled() ) { + $this->posts_to_cot_migrator->migrate_orders( array( $order_id ) ); + } + } + + /** + * Handle the 'woocommerce_feature_description_tip' filter. + * + * When the COT feature is enabled and there are orders pending sync (in either direction), + * show a "you should ync before disabling" warning under the feature in the features page. + * Skip this if the UI prevents changing the feature enable status. + * + * @param string $desc_tip The original description tip for the feature. + * @param string $feature_id The feature id. + * @param bool $ui_disabled True if the UI doesn't allow to enable or disable the feature. + * @return string The new description tip for the feature. + */ + private function handle_feature_description_tip( $desc_tip, $feature_id, $ui_disabled ): string { + if ( 'custom_order_tables' !== $feature_id || $ui_disabled ) { + return $desc_tip; + } + + $features_controller = wc_get_container()->get( FeaturesController::class ); + $feature_is_enabled = $features_controller->feature_is_enabled( 'custom_order_tables' ); + if ( ! $feature_is_enabled ) { + return $desc_tip; + } + + $pending_sync_count = $this->get_current_orders_pending_sync_count(); + if ( ! $pending_sync_count ) { + return $desc_tip; + } + + if ( $this->custom_orders_table_is_authoritative() ) { + $extra_tip = sprintf( + _n( + "⚠ There's one order pending sync from the orders table to the posts table. The feature shouldn't be disabled until this order is synchronized.", + "⚠ There are %1\$d orders pending sync from the orders table to the posts table. The feature shouldn't be disabled until these orders are synchronized.", + $pending_sync_count, + 'woocommerce' + ), + $pending_sync_count + ); + } else { + $extra_tip = sprintf( + _n( + "⚠ There's one order pending sync from the posts table to the orders table. The feature shouldn't be disabled until this order is synchronized.", + "⚠ There are %1\$d orders pending sync from the posts table to the orders table. The feature shouldn't be disabled until these orders are synchronized.", + $pending_sync_count, + 'woocommerce' + ), + $pending_sync_count + ); + } + + $cot_settings_url = add_query_arg( + array( + 'page' => 'wc-settings', + 'tab' => 'advanced', + 'section' => 'custom_data_stores', + ), + admin_url( 'admin.php' ) + ); + + /* translators: %s = URL of the custom data stores settings page */ + $manage_cot_settings_link = sprintf( __( "Manage orders synchronization", 'woocommerce' ), $cot_settings_url ); + + return $desc_tip ? "{$desc_tip}
{$extra_tip} {$manage_cot_settings_link}" : "{$extra_tip} {$manage_cot_settings_link}"; + } } diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index ac83d682818..64885983ab5 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -497,9 +497,9 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * * @return \WC_Order_Data_Store_CPT Data store instance. */ - private function get_cpt_data_store_instance() { + public function get_cpt_data_store_instance() { if ( ! isset( $this->cpt_data_store ) ) { - $this->cpt_data_store = new \WC_Order_Data_Store_CPT(); + $this->cpt_data_store = $this->get_post_data_store_for_backfill(); } return $this->cpt_data_store; } @@ -948,7 +948,7 @@ WHERE */ public function get_order_type( $order_id ) { $type = $this->get_orders_type( array( $order_id ) ); - return $type[ $order_id ]; + return $type[ $order_id ] ?? ''; } /** @@ -985,7 +985,7 @@ WHERE $data_sync_enabled = $data_synchronizer->data_sync_is_enabled() && 0 === $data_synchronizer->get_current_orders_pending_sync_count_cached(); $load_posts_for = array_diff( $order_ids, self::$reading_order_ids ); - $post_orders = $data_sync_enabled ? $this->get_post_orders_for_ids( $load_posts_for ) : array(); + $post_orders = $data_sync_enabled ? $this->get_post_orders_for_ids( array_intersect_key( $orders, array_flip( $load_posts_for ) ) ) : array(); foreach ( $data as $order_data ) { $order_id = absint( $order_data->id ); @@ -1008,8 +1008,8 @@ WHERE * @return bool Whether the order should be synced. */ private function should_sync_order( \WC_Abstract_Order $order ) : bool { - $draft_order = in_array( $order->get_status(), array( 'draft', 'auto-draft' ) ); - $already_synced = in_array( $order->get_id(), self::$reading_order_ids ); + $draft_order = in_array( $order->get_status(), array( 'draft', 'auto-draft' ), true ); + $already_synced = in_array( $order->get_id(), self::$reading_order_ids, true ); return ! $draft_order && ! $already_synced; } @@ -1017,8 +1017,8 @@ WHERE * Helper method to initialize order object from DB data. * * @param \WC_Abstract_Order $order Order object. - * @param int $order_id Order ID. - * @param \stdClass $order_data Order data fetched from DB. + * @param int $order_id Order ID. + * @param \stdClass $order_data Order data fetched from DB. * * @return void */ @@ -1031,6 +1031,35 @@ WHERE $order->set_object_read( true ); } + /** + * For post based data stores, this was used to filter internal meta data. For custom tables, technically there is no internal meta data, + * (i.e. we store all core data as properties for the order, and not in meta data). So this method is a no-op. + * + * Except that some meta such as billing_address_index and shipping_address_index are infact stored in meta data, so we need to filter those out. + * + * However, declaring $internal_meta_keys is still required so that our backfill and other comparison checks works as expected. + * + * @param \WC_Data $object Object to filter meta data for. + * @param array $raw_meta_data Raw meta data. + * + * @return array Filtered meta data. + */ + public function filter_raw_meta_data( &$object, $raw_meta_data ) { + $filtered_meta_data = parent::filter_raw_meta_data( $object, $raw_meta_data ); + $allowed_keys = array( + '_billing_address_index', + '_shipping_address_index', + ); + $allowed_meta = array_filter( + $raw_meta_data, + function( $meta ) use ( $allowed_keys ) { + return in_array( $meta->meta_key, $allowed_keys, true ); + } + ); + + return array_merge( $allowed_meta, $filtered_meta_data ); + } + /** * Sync order to/from posts tables if we are able to detect difference between order and posts but the sync is enabled. * @@ -1059,9 +1088,7 @@ WHERE * * So we write back to the order table when order modified date is more recent than post modified date. Otherwise, we write to the post table. */ - if ( $order_modified_date > $post_order_modified_date ) { - return; - } else { + if ( $post_order_modified_date >= $order_modified_date ) { $this->migrate_post_record( $order, $post_order ); } } @@ -1084,31 +1111,46 @@ WHERE /** * Helper function to get posts data for an order in bullk. We use to this to compute posts object in bulk so that we can compare it with COT data. * - * @param array $order_ids List of order IDs. + * @param array $orders List of orders mapped by $order_id. * * @return array List of posts. */ - private function get_post_orders_for_ids( array $order_ids ): array { - $cpt_data_store = $this->get_cpt_data_store_instance(); + private function get_post_orders_for_ids( array $orders ): array { + $order_ids = array_keys( $orders ); // We have to bust meta cache, otherwise we will just get the meta cached by OrderTableDataStore. foreach ( $order_ids as $order_id ) { wp_cache_delete( WC_Order::generate_meta_cache_key( $order_id, 'orders' ), 'orders' ); } - $query_vars = array( - 'include' => $order_ids, - 'type' => wc_get_order_types(), - 'status' => 'any', - 'limit' => count( $order_ids ), - ); - $cpt_data_store->prime_caches_for_orders( $order_ids, $query_vars ); - $orders = array(); - foreach ( $order_ids as $order_id ) { - $order = new WC_Order(); - $order->set_id( $order_id ); - $cpt_data_store->read( $order ); - $orders[ $order_id ] = $order; + + $cpt_stores = array(); + $cpt_store_orders = array(); + foreach ( $orders as $order_id => $order ) { + $table_data_store = $order->get_data_store(); + $cpt_data_store = $table_data_store->get_cpt_data_store_instance(); + $cpt_store_class_name = get_class( $cpt_data_store ); + if ( ! isset( $cpt_stores[ $cpt_store_class_name ] ) ) { + $cpt_stores[ $cpt_store_class_name ] = $cpt_data_store; + $cpt_store_orders[ $cpt_store_class_name ] = array(); + } + $cpt_store_orders[ $cpt_store_class_name ][ $order_id ] = $order; } - return $orders; + + $cpt_orders = array(); + foreach ( $cpt_stores as $cpt_store_name => $cpt_store ) { + // Prime caches if we can. + if ( method_exists( $cpt_store, 'prime_caches_for_orders' ) ) { + $cpt_store->prime_caches_for_orders( array_keys( $cpt_store_orders[ $cpt_store_name ] ), array() ); + } + + foreach ( $cpt_store_orders[ $cpt_store_name ] as $order_id => $order ) { + $cpt_order_class_name = wc_get_order_type( $order->get_type() )['class_name']; + $cpt_order = new $cpt_order_class_name(); + $cpt_order->set_id( $order_id ); + $cpt_store->read( $cpt_order ); + $cpt_orders[ $order_id ] = $cpt_order; + } + } + return $cpt_orders; } /** @@ -1221,12 +1263,12 @@ WHERE /** * Migrate post record from a given order object. * - * @param \WC_Order $order Order object. - * @param \WC_Order $post_order Order object read from posts. + * @param \WC_Abstract_Order $order Order object. + * @param \WC_Abstract_Order $post_order Order object read from posts. * * @return void */ - private function migrate_post_record( \WC_Order &$order, \WC_Order $post_order ): void { + private function migrate_post_record( \WC_Abstract_Order &$order, \WC_Abstract_Order $post_order ): void { $this->migrate_meta_data_from_post_order( $order, $post_order ); $post_order_base_data = $post_order->get_base_data(); foreach ( $post_order_base_data as $key => $value ) { @@ -1544,6 +1586,9 @@ FROM $order_meta_table throw new \Exception( sprintf( __( 'Could not persist order to database table "%s".', 'woocommerce' ), $update['table'] ) ); } } + + $changes = $order->get_changes(); + $this->update_address_index_meta( $order, $changes ); } /** @@ -1741,7 +1786,7 @@ FROM $order_meta_table } $trash_metadata = array( - '_wp_trash_meta_status' => $order->get_status( 'edit' ), + '_wp_trash_meta_status' => 'wc-' . $order->get_status( 'edit' ), '_wp_trash_meta_time' => time(), ); @@ -1757,13 +1802,21 @@ FROM $order_meta_table $wpdb->update( self::get_orders_table_name(), - array( 'status' => 'trash' ), + array( + 'status' => 'trash', + 'date_updated_gmt' => current_time( 'Y-m-d H:i:s', true ), + ), array( 'id' => $order->get_id() ), - array( '%s' ), + array( '%s', '%s' ), array( '%d' ) ); $order->set_status( 'trash' ); + + $data_synchronizer = wc_get_container()->get( DataSynchronizer::class ); + if ( $data_synchronizer->data_sync_is_enabled() ) { + wp_trash_post( $order->get_id() ); + } } /** @@ -1791,7 +1844,7 @@ FROM $order_meta_table $previous_status = $order->get_meta( '_wp_trash_meta_status' ); $valid_statuses = wc_get_order_statuses(); - $previous_state_is_invalid = ! array_key_exists( 'wc-' . $previous_status, $valid_statuses ); + $previous_state_is_invalid = ! array_key_exists( $previous_status, $valid_statuses ); $pending_is_valid_status = array_key_exists( 'wc-pending', $valid_statuses ); if ( $previous_state_is_invalid && $pending_is_valid_status ) { @@ -1820,13 +1873,41 @@ FROM $order_meta_table return false; } + /** + * Fires before an order is restored from the trash. + * + * @since 7.2.0 + * + * @param int $order_id Order ID. + * @param string $previous_status The status of the order before it was trashed. + */ + do_action( 'woocommerce_untrash_order', $order->get_id(), $previous_status ); + $order->set_status( $previous_status ); $order->save(); // Was the status successfully restored? Let's clean up the meta and indicate success... - if ( $previous_status === $order->get_status() ) { + if ( 'wc-' . $order->get_status() === $previous_status ) { $order->delete_meta_data( '_wp_trash_meta_status' ); $order->delete_meta_data( '_wp_trash_meta_time' ); + $order->delete_meta_data( '_wp_trash_meta_comments_status' ); + $order->save_meta_data(); + + $data_synchronizer = wc_get_container()->get( DataSynchronizer::class ); + if ( $data_synchronizer->data_sync_is_enabled() ) { + //The previous $order->save() will have forced a sync to the posts table, + //this implies that the post status is not "trash" anymore, and thus + //wp_untrash_post would do nothing. + wp_update_post( + array( + 'ID' => $id, + 'post_status' => 'trash', + ) + ); + + wp_untrash_post( $id ); + } + return true; } @@ -2025,7 +2106,17 @@ FROM $order_meta_table */ public function update_order_meta( &$order ) { $changes = $order->get_changes(); + $this->update_address_index_meta( $order, $changes ); + } + /** + * Helper function to update billing and shipping address metadata. + * @param \WC_Abstract_Order $order Order Object + * @param array $changes Array of changes. + * + * @return void + */ + private function update_address_index_meta( $order, $changes ) { // If address changed, store concatenated version to make searches faster. foreach ( array( 'billing', 'shipping' ) as $address_type ) { if ( isset( $changes[ $address_type ] ) ) { diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php index ee473150be7..bbdd2a83d43 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php @@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; +use Automattic\WooCommerce\Internal\Admin\Orders\COTRedirectionController; use Automattic\WooCommerce\Internal\Admin\Orders\Edit; use Automattic\WooCommerce\Internal\Admin\Orders\ListTable; use Automattic\WooCommerce\Internal\Admin\Orders\PageController; @@ -21,6 +22,7 @@ class OrderAdminServiceProvider extends AbstractServiceProvider { * @var string[] */ protected $provides = array( + COTRedirectionController::class, PageController::class, Edit::class, ListTable::class, @@ -32,6 +34,7 @@ class OrderAdminServiceProvider extends AbstractServiceProvider { * @return void */ public function register() { + $this->share( COTRedirectionController::class ); $this->share( PageController::class ); $this->share( Edit::class )->addArgument( PageController::class ); $this->share( ListTable::class )->addArgument( PageController::class ); diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php index 4339f86d16d..bf2622df5c2 100644 --- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php +++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php @@ -656,6 +656,18 @@ class FeaturesController { } } + /** + * Filter to customize the description tip that appears under the description of each feature in the features settings page. + * + * @since 7.1.0 + * + * @param string $desc_tip The original description tip. + * @param string $feature_id The id of the feature for which the description tip is being customized. + * @param bool $disabled True if the UI currently prevents changing the enable/disable status of the feature. + * @return string The new description tip to use. + */ + $desc_tip = apply_filters( 'woocommerce_feature_description_tip', $desc_tip, $feature_id, $disabled ); + return array( 'title' => $feature['name'], 'desc' => $description, @@ -700,7 +712,7 @@ class FeaturesController { } // phpcs:disable WordPress.Security.NonceVerification - if ( get_current_screen() && 'plugins' !== get_current_screen()->id || 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) { + if ( ! function_exists( 'get_current_screen' ) || get_current_screen() && 'plugins' !== get_current_screen()->id || 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) { return $list; } @@ -713,7 +725,7 @@ class FeaturesController { // phpcs:enable WordPress.Security.NonceVerification foreach ( array_keys( $list ) as $plugin_name ) { - if ( ! $this->plugin_util->is_woocommerce_aware_plugin( $plugin_name ) ) { + if ( ! $this->plugin_util->is_woocommerce_aware_plugin( $plugin_name ) || ! $this->proxy->call_function( 'is_plugin_active', $plugin_name ) ) { continue; } @@ -728,10 +740,6 @@ class FeaturesController { } } - if ( 0 === count( $incompatibles ) ) { - return $list; - } - return array_intersect_key( $list, array_flip( $incompatibles ) ); } @@ -743,25 +751,18 @@ class FeaturesController { return; } - if ( 'plugins' !== get_current_screen()->id ) { - return; + $feature_filter_description_shown = $this->maybe_display_current_feature_filter_description(); + if ( ! $feature_filter_description_shown ) { + $this->maybe_display_feature_incompatibility_warning(); } - - $this->maybe_display_feature_incompatibility_warning(); - $this->maybe_display_current_feature_filter_description(); } /** - * Shows a warning (on the plugins screen) when there are any incompatibility between active plugins and enabled - * features. + * Shows a warning when there are any incompatibility between active plugins and enabled features. + * The warning is shown in on any admin screen except the plugins screen itself, since + * there's already a "You are viewing */ private function maybe_display_feature_incompatibility_warning(): void { - $plugin_status = $_GET['plugin_status'] ?? ''; // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput - - if ( ! in_array( $plugin_status, array( '', 'all', 'active' ), true ) ) { - return; - } - $incompatible_plugins = false; foreach ( $this->plugin_util->get_woocommerce_aware_plugins( true ) as $plugin ) { @@ -789,7 +790,7 @@ class FeaturesController { // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped ?> -
+

'. */ - private function maybe_display_current_feature_filter_description(): void { + private function maybe_display_current_feature_filter_description(): bool { + if ( 'plugins' !== get_current_screen()->id ) { + return false; + } + // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput $plugin_status = $_GET['plugin_status'] ?? ''; $feature_id = $_GET['feature_id'] ?? ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput if ( 'incompatible_with_feature' !== $plugin_status ) { - return; + return false; } $feature_id = ( '' === $feature_id ) ? 'all' : $feature_id; if ( 'all' !== $feature_id && ! $this->feature_exists( $feature_id ) ) { - return; + return false; } // phpcs:enable WordPress.Security.NonceVerification @@ -823,10 +828,10 @@ class FeaturesController { $message = 'all' === $feature_id - ? __( 'You are viewing plugins that are incompatible with currently enabled WooCommerce features.', 'woocommerce' ) + ? __( 'You are viewing active plugins that are incompatible with currently enabled WooCommerce features.', 'woocommerce' ) : sprintf( /* translators: %s is a feature name. */ - __( "You are viewing the plugins that are incompatible with the '%s' feature.", 'woocommerce' ), + __( "You are viewing the active plugins that are incompatible with the '%s' feature.", 'woocommerce' ), $this->features[ $feature_id ]['name'] ); @@ -844,6 +849,8 @@ class FeaturesController {
plugin_util->is_woocommerce_aware_plugin( $plugin_data ) ) { return; } - if ( 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) { // phpcs:ignore WordPress.Security.NonceVerification + if ( ! $this->proxy->call_function( 'is_plugin_active', $plugin_file ) ) { return; } @@ -878,7 +889,7 @@ class FeaturesController { $incompatible_features_count = count( $incompatible_features ); if ( $incompatible_features_count > 0 ) { $columns_count = $wp_list_table->get_column_count(); - $is_active = $this->proxy->call_function( 'is_plugin_active', $plugin_file ); + $is_active = true; // For now we are showing active plugins in the "Incompatible with..." view. $is_active_class = $is_active ? 'active' : 'inactive'; $is_active_td_style = $is_active ? " style='border-left: 4px solid #72aee6;'" : ''; diff --git a/plugins/woocommerce/src/Internal/Utilities/COTMigrationUtil.php b/plugins/woocommerce/src/Internal/Utilities/COTMigrationUtil.php index 1c88e5074b5..1f0d52cc547 100644 --- a/plugins/woocommerce/src/Internal/Utilities/COTMigrationUtil.php +++ b/plugins/woocommerce/src/Internal/Utilities/COTMigrationUtil.php @@ -75,7 +75,7 @@ class COTMigrationUtil { */ public function is_custom_order_tables_in_sync() : bool { $sync_status = $this->data_synchronizer->get_sync_status(); - return $sync_status['current_pending_count'] === 0 && $this->data_synchronizer->data_sync_is_enabled(); + return 0 === $sync_status['current_pending_count'] && $this->data_synchronizer->data_sync_is_enabled(); } /** @@ -122,15 +122,15 @@ class COTMigrationUtil { } /** - * Helper function to id from an post or order object. + * Helper function to get ID from a post or order object. * * @param WP_Post/WC_Order $post_or_order_object WP_Post/WC_Order object to get ID for. * * @return int Order or post ID. */ public function get_post_or_order_id( $post_or_order_object ) : int { - if ( is_int( $post_or_order_object ) ) { - return $post_or_order_object; + if ( is_numeric( $post_or_order_object ) ) { + return (int) $post_or_order_object; } elseif ( $post_or_order_object instanceof WC_Order ) { return $post_or_order_object->get_id(); } elseif ( $post_or_order_object instanceof WP_Post ) { @@ -160,7 +160,7 @@ class COTMigrationUtil { * * @return string|null Type of the order. */ - public function get_order_type( $order_id ) : ?string { + public function get_order_type( $order_id ) { $order_id = $this->get_post_or_order_id( $order_id ); $order_data_store = \WC_Data_Store::load( 'order' ); return $order_data_store->get_order_type( $order_id ); diff --git a/plugins/woocommerce/src/Packages.php b/plugins/woocommerce/src/Packages.php index c9948f60815..f99b3a80436 100644 --- a/plugins/woocommerce/src/Packages.php +++ b/plugins/woocommerce/src/Packages.php @@ -1,6 +1,6 @@

- +

diff --git a/plugins/woocommerce/templates/cart/cart-empty.php b/plugins/woocommerce/templates/cart/cart-empty.php index 0a463e9a200..e9f60c1d61d 100644 --- a/plugins/woocommerce/templates/cart/cart-empty.php +++ b/plugins/woocommerce/templates/cart/cart-empty.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.5.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -24,7 +24,7 @@ do_action( 'woocommerce_cart_is_empty' ); if ( wc_get_page_id( 'shop' ) > 0 ) : ?>

- +

- + diff --git a/plugins/woocommerce/templates/cart/proceed-to-checkout-button.php b/plugins/woocommerce/templates/cart/proceed-to-checkout-button.php index d678926e88c..e02f0b8bfd0 100644 --- a/plugins/woocommerce/templates/cart/proceed-to-checkout-button.php +++ b/plugins/woocommerce/templates/cart/proceed-to-checkout-button.php @@ -14,7 +14,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 2.4.0 + * @version 7.0.1 */ if ( ! defined( 'ABSPATH' ) ) { @@ -22,6 +22,6 @@ if ( ! defined( 'ABSPATH' ) ) { } ?> - + diff --git a/plugins/woocommerce/templates/cart/shipping-calculator.php b/plugins/woocommerce/templates/cart/shipping-calculator.php index 09c0c984f01..d30a63695c7 100644 --- a/plugins/woocommerce/templates/cart/shipping-calculator.php +++ b/plugins/woocommerce/templates/cart/shipping-calculator.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 4.0.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -88,7 +88,7 @@ do_action( 'woocommerce_before_shipping_calculator' ); ?>

-

+

diff --git a/plugins/woocommerce/templates/checkout/form-coupon.php b/plugins/woocommerce/templates/checkout/form-coupon.php index 83e5e3cdaab..9b39e8ee43b 100644 --- a/plugins/woocommerce/templates/checkout/form-coupon.php +++ b/plugins/woocommerce/templates/checkout/form-coupon.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.4.4 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -36,7 +36,7 @@ if ( ! wc_coupons_enabled() ) { // @codingStandardsIgnoreLine.

- +

diff --git a/plugins/woocommerce/templates/checkout/form-pay.php b/plugins/woocommerce/templates/checkout/form-pay.php index e7bdd506ff3..8a44c973509 100644 --- a/plugins/woocommerce/templates/checkout/form-pay.php +++ b/plugins/woocommerce/templates/checkout/form-pay.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 5.2.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -88,7 +88,7 @@ $totals = $order->get_order_item_totals(); // phpcs:ignore WordPress.WP.GlobalVa - ' . esc_html( $order_button_text ) . '' ); // @codingStandardsIgnoreLine ?> + ' . esc_html( $order_button_text ) . '' ); // @codingStandardsIgnoreLine ?> diff --git a/plugins/woocommerce/templates/checkout/payment.php b/plugins/woocommerce/templates/checkout/payment.php index 7da3423e324..bbe92b8032c 100644 --- a/plugins/woocommerce/templates/checkout/payment.php +++ b/plugins/woocommerce/templates/checkout/payment.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.5.3 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -41,14 +41,14 @@ if ( ! wp_doing_ajax() ) { /* translators: $1 and $2 opening and closing emphasis tags respectively */ printf( esc_html__( 'Since your browser does not support JavaScript, or it is disabled, please ensure you click the %1$sUpdate Totals%2$s button before placing your order. You may be charged more than the amount stated above if you fail to do so.', 'woocommerce' ), '', '' ); ?> -
+
- ' . esc_html( $order_button_text ) . '' ); // @codingStandardsIgnoreLine ?> + ' . esc_html( $order_button_text ) . '' ); // @codingStandardsIgnoreLine ?> diff --git a/plugins/woocommerce/templates/content-widget-price-filter.php b/plugins/woocommerce/templates/content-widget-price-filter.php index 30610410ede..2bb2541e13f 100644 --- a/plugins/woocommerce/templates/content-widget-price-filter.php +++ b/plugins/woocommerce/templates/content-widget-price-filter.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.7.1 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -29,7 +29,7 @@ defined( 'ABSPATH' ) || exit; - + diff --git a/plugins/woocommerce/templates/global/form-login.php b/plugins/woocommerce/templates/global/form-login.php index a6142132d22..33776e45611 100644 --- a/plugins/woocommerce/templates/global/form-login.php +++ b/plugins/woocommerce/templates/global/form-login.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.6.0 + * @version 7.0.1 */ if ( ! defined( 'ABSPATH' ) ) { @@ -48,7 +48,7 @@ if ( is_user_logged_in() ) { - +

diff --git a/plugins/woocommerce/templates/myaccount/form-add-payment-method.php b/plugins/woocommerce/templates/myaccount/form-add-payment-method.php index 71b859aa4db..ace49da793a 100644 --- a/plugins/woocommerce/templates/myaccount/form-add-payment-method.php +++ b/plugins/woocommerce/templates/myaccount/form-add-payment-method.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 4.3.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -51,7 +51,7 @@ if ( $available_gateways ) : ?>

- +
diff --git a/plugins/woocommerce/templates/myaccount/form-edit-account.php b/plugins/woocommerce/templates/myaccount/form-edit-account.php index eb5dfd180b5..51a2bf9ecee 100644 --- a/plugins/woocommerce/templates/myaccount/form-edit-account.php +++ b/plugins/woocommerce/templates/myaccount/form-edit-account.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.5.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -66,7 +66,7 @@ do_action( 'woocommerce_before_edit_account_form' ); ?>

- +

diff --git a/plugins/woocommerce/templates/myaccount/form-edit-address.php b/plugins/woocommerce/templates/myaccount/form-edit-address.php index 6916bef15c7..ee35dd9f163 100644 --- a/plugins/woocommerce/templates/myaccount/form-edit-address.php +++ b/plugins/woocommerce/templates/myaccount/form-edit-address.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.6.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -43,7 +43,7 @@ do_action( 'woocommerce_before_edit_account_address_form' ); ?>

- +

diff --git a/plugins/woocommerce/templates/myaccount/form-login.php b/plugins/woocommerce/templates/myaccount/form-login.php index cdf38190e69..93c8d44db39 100644 --- a/plugins/woocommerce/templates/myaccount/form-login.php +++ b/plugins/woocommerce/templates/myaccount/form-login.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 6.0.0 + * @version 7.0.1 */ if ( ! defined( 'ABSPATH' ) ) { @@ -51,7 +51,7 @@ do_action( 'woocommerce_before_customer_login_form' ); ?> - +

@@ -104,7 +104,7 @@ do_action( 'woocommerce_before_customer_login_form' ); ?>

- +

diff --git a/plugins/woocommerce/templates/myaccount/form-lost-password.php b/plugins/woocommerce/templates/myaccount/form-lost-password.php index 15fc984a745..96aa173a4d3 100644 --- a/plugins/woocommerce/templates/myaccount/form-lost-password.php +++ b/plugins/woocommerce/templates/myaccount/form-lost-password.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.5.2 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -35,7 +35,7 @@ do_action( 'woocommerce_before_lost_password_form' );

- +

diff --git a/plugins/woocommerce/templates/myaccount/form-reset-password.php b/plugins/woocommerce/templates/myaccount/form-reset-password.php index 3106f6022da..4e49a9f65c8 100644 --- a/plugins/woocommerce/templates/myaccount/form-reset-password.php +++ b/plugins/woocommerce/templates/myaccount/form-reset-password.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.5.5 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -42,7 +42,7 @@ do_action( 'woocommerce_before_reset_password_form' );

- +

diff --git a/plugins/woocommerce/templates/myaccount/orders.php b/plugins/woocommerce/templates/myaccount/orders.php index 10fcadc509d..8ac301bb7e5 100644 --- a/plugins/woocommerce/templates/myaccount/orders.php +++ b/plugins/woocommerce/templates/myaccount/orders.php @@ -14,7 +14,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.7.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -67,7 +67,7 @@ do_action( 'woocommerce_before_account_orders', $has_orders ); ?> if ( ! empty( $actions ) ) { foreach ( $actions as $key => $action ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - echo '' . esc_html( $action['name'] ) . ''; + echo '' . esc_html( $action['name'] ) . ''; } } ?> diff --git a/plugins/woocommerce/templates/order/form-tracking.php b/plugins/woocommerce/templates/order/form-tracking.php index 2b2ce8de6b3..c18595e1c97 100644 --- a/plugins/woocommerce/templates/order/form-tracking.php +++ b/plugins/woocommerce/templates/order/form-tracking.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 6.5.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -46,7 +46,7 @@ global $post; do_action( 'woocommerce_order_tracking_form' ); ?> -

+

"> - + diff --git a/plugins/woocommerce/templates/single-product/add-to-cart/external.php b/plugins/woocommerce/templates/single-product/add-to-cart/external.php index 92039e72f9b..f7063034d01 100644 --- a/plugins/woocommerce/templates/single-product/add-to-cart/external.php +++ b/plugins/woocommerce/templates/single-product/add-to-cart/external.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.4.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -22,7 +22,7 @@ do_action( 'woocommerce_before_add_to_cart_form' ); ?> - + diff --git a/plugins/woocommerce/templates/single-product/add-to-cart/grouped.php b/plugins/woocommerce/templates/single-product/add-to-cart/grouped.php index 719eb22165f..aa76d3cc267 100644 --- a/plugins/woocommerce/templates/single-product/add-to-cart/grouped.php +++ b/plugins/woocommerce/templates/single-product/add-to-cart/grouped.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 4.8.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -117,7 +117,7 @@ do_action( 'woocommerce_before_add_to_cart_form' ); ?> - + diff --git a/plugins/woocommerce/templates/single-product/add-to-cart/simple.php b/plugins/woocommerce/templates/single-product/add-to-cart/simple.php index 494ece1c465..0affa4ffaf0 100644 --- a/plugins/woocommerce/templates/single-product/add-to-cart/simple.php +++ b/plugins/woocommerce/templates/single-product/add-to-cart/simple.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.4.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -46,7 +46,7 @@ if ( $product->is_in_stock() ) : ?> do_action( 'woocommerce_after_add_to_cart_quantity' ); ?> - + diff --git a/plugins/woocommerce/templates/single-product/add-to-cart/variation-add-to-cart-button.php b/plugins/woocommerce/templates/single-product/add-to-cart/variation-add-to-cart-button.php index 03b7aeb02a9..3518a10fcef 100644 --- a/plugins/woocommerce/templates/single-product/add-to-cart/variation-add-to-cart-button.php +++ b/plugins/woocommerce/templates/single-product/add-to-cart/variation-add-to-cart-button.php @@ -4,7 +4,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.4.0 + * @version 7.0.1 */ defined( 'ABSPATH' ) || exit; @@ -28,7 +28,7 @@ global $product; do_action( 'woocommerce_after_add_to_cart_quantity' ); ?> - + diff --git a/plugins/woocommerce/tests/api-core-tests/README.md b/plugins/woocommerce/tests/api-core-tests/README.md index e39b68593e8..6c19aef1464 100644 --- a/plugins/woocommerce/tests/api-core-tests/README.md +++ b/plugins/woocommerce/tests/api-core-tests/README.md @@ -1,6 +1,6 @@ -# WooCommerce Playwright End to End Tests +# WooCommerce Core API Test Suite -This is the documentation for the new api-core-tests setup based on Playwright and wp-env. It superseedes the Puppeteer and e2e-environment [setup](../tests/e2e), which we will gradually deprecate. +This package contains automated API tests for WooCommerce, based on Playwright and `wp-env`. It supersedes the SuperTest based [api-core-tests package](https://www.npmjs.com/package/@woocommerce/api-core-tests) and e2e-environment [setup](../tests/e2e), which we will gradually deprecate. ## Table of contents @@ -21,36 +21,33 @@ This is the documentation for the new api-core-tests setup based on Playwright a - PNPM ([Installation instructions](https://pnpm.io/installation)) - Docker and Docker Compose ([Installation instructions](https://docs.docker.com/engine/install/)) -Note, that if you are on Mac and you install docker through other methods such as homebrew, for example, your steps to set it up might be different. The commands listed in steps below may also vary. +Note, that if you are on Mac and you install Docker through other methods such as homebrew, for example, your steps to set it up might be different. The commands listed in steps below may also vary. -If you are using Windows, we recommend using [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/) for running E2E tests. Follow the [WSL Setup Instructions](../tests/e2e/WSL_SETUP_INSTRUCTIONS.md) first before proceeding with the steps below. +If you are using Windows, we recommend using [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/) for running tests. Follow the [WSL Setup Instructions](../tests/e2e/WSL_SETUP_INSTRUCTIONS.md) first before proceeding with the steps below. ### Introduction -api-core-tests are powered by Playwright. The test site is spinned up using `wp-env` (recommended), but we will continue to support `e2e-environment` in the meantime. +WooCommerce's `api-core-tests` are powered by Playwright. The test site is spun up using `wp-env` (recommended), but we will continue to support `e2e-environment` in the meantime. **Running tests for the first time:** -Note: the commands may need to be executed in `plugins/woocommerce` (or a subdirectory thereof) - `nvm use` - `pnpm install` - `pnpm run build --filter=woocommerce` -- `pnpm env:test --filter=woocommerce` - -Then execute the tests with the following command: - -- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js` (headless) +- `cd plugins/woocommerce` +- `pnpm env:test` +- `pnpm test:api-pw` To run the test again, re-create the environment to start with a fresh state -- `pnpm env:destroy --filter=woocommerce` -- `pnpm env:test --filter=woocommerce` +- `pnpm env:destroy` +- `pnpm env:test` +- `pnpm test:api-pw` Other ways of running tests: -- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js --headed` (headed) -- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js --debug` (debug) -- `cd plugins/woocommerce && USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js ./tests/api-core-tests/tests/hello/hello.test.js` (running a single test) +- `pnpm test:api-pw --debug` (debug) +- `pnpm test:api-pw ./tests/api-core-tests/tests/hello/hello.test.js` (running a single test) To see all options, run `cd plugins/woocommerce && pnpm playwright test --help` @@ -69,8 +66,7 @@ USER_KEY="" USER_SECRET="" ``` -For local setup, create a `.env` file in the `woocommerce/plugins/woocommerce` folder with the three required values described above. -If any of these variables are configured they will override the values automatically set in the `playwright.config.js` +For local setup, create a `.env` file in the `woocommerce/plugins/woocommerce` folder with the three required values described above. If any of these variables are configured they will override the values automatically set in the `playwright.config.js` When using a username and password combination instead of a consumer secret and consumer key, make sure to have the [JSON Basic Authentication plugin](https://github.com/WP-API/Basic-Auth) installed and activated on the test site. @@ -113,11 +109,11 @@ The test environment uses the following test variables: If you need to modify the port for your local test environment (eg. port is already in use) or use, edit [playwright.config.js](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/playwright.config.js). Depending on what environment tool you are using, you will need to also edit the respective `.json` file. -**Modiify the port wp-env** +**Modify the port wp-env** Edit [.wp-env.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/.wp-env.json) and [playwright.config.js](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/playwright.config.js). -**Modiify port for e2e-environment** +**Modify port for e2e-environment** Edit [tests/e2e/config/default.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/config/default.json).**** @@ -125,9 +121,9 @@ Edit [tests/e2e/config/default.json](https://github.com/woocommerce/woocommerce/ After you run a test, it's best to restart the environment to start from a fresh state. We are currently working to reset the state more efficiently to avoid the restart being needed, but this is a work-in-progress. -- `pnpm env:down --filter=woocommerce` to stop the environment -- `pnpm env:destroy --filter=woocommerce` when you make changes to `.wp-env.json` -- `pnpm env:test --filter=woocommerce` to spin up the test environment +- `pnpm env:down` to stop the environment +- `pnpm env:destroy` when you make changes to `.wp-env.json` +- `pnpm env:test` to spin up the test environment ## Guide for writing tests @@ -142,17 +138,11 @@ Based on our example, the test skeleton would look as follows: ```js test.describe( 'Merchant can create virtual product', () => { - test( 'merchant can log in', async () => { + test( 'merchant can log in', async () => { } ); - } ); + test( 'merchant can create virtual product', async () => { } ); - test( 'merchant can create virtual product', async () => { - - } ); - - test( 'merchant can verify that virtual product was created', async () => { - - } ); + test( 'merchant can verify that virtual product was created', async () => { } ); } ); ``` diff --git a/plugins/woocommerce/tests/api-core-tests/data/settings.js b/plugins/woocommerce/tests/api-core-tests/data/settings.js new file mode 100644 index 00000000000..6ab0c22f2f7 --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/data/settings.js @@ -0,0 +1,2519 @@ +/** + * Default shipping zone object. + * + * For more details on shipping zone properties, see: + * + * https://woocommerce.github.io/woocommerce-rest-api-docs/#shipping-zone-properties + * + */ + +const currencies = { + "AED": "United Arab Emirates dirham (د.إ)", + "AFN": "Afghan afghani (؋)", + "ALL": "Albanian lek (L)", + "AMD": "Armenian dram (AMD)", + "ANG": "Netherlands Antillean guilder (ƒ)", + "AOA": "Angolan kwanza (Kz)", + "ARS": "Argentine peso ($)", + "AUD": "Australian dollar ($)", + "AWG": "Aruban florin (Afl.)", + "AZN": "Azerbaijani manat (AZN)", + "BAM": "Bosnia and Herzegovina convertible mark (KM)", + "BBD": "Barbadian dollar ($)", + "BDT": "Bangladeshi taka (৳ )", + "BGN": "Bulgarian lev (лв.)", + "BHD": "Bahraini dinar (.د.ب)", + "BIF": "Burundian franc (Fr)", + "BMD": "Bermudian dollar ($)", + "BND": "Brunei dollar ($)", + "BOB": "Bolivian boliviano (Bs.)", + "BRL": "Brazilian real (R$)", + "BSD": "Bahamian dollar ($)", + "BTC": "Bitcoin (฿)", + "BTN": "Bhutanese ngultrum (Nu.)", + "BWP": "Botswana pula (P)", + "BYR": "Belarusian ruble (old) (Br)", + "BYN": "Belarusian ruble (Br)", + "BZD": "Belize dollar ($)", + "CAD": "Canadian dollar ($)", + "CDF": "Congolese franc (Fr)", + "CHF": "Swiss franc (CHF)", + "CLP": "Chilean peso ($)", + "CNY": "Chinese yuan (¥)", + "COP": "Colombian peso ($)", + "CRC": "Costa Rican colón (₡)", + "CUC": "Cuban convertible peso ($)", + "CUP": "Cuban peso ($)", + "CVE": "Cape Verdean escudo ($)", + "CZK": "Czech koruna (Kč)", + "DJF": "Djiboutian franc (Fr)", + "DKK": "Danish krone (kr.)", + "DOP": "Dominican peso (RD$)", + "DZD": "Algerian dinar (د.ج)", + "EGP": "Egyptian pound (EGP)", + "ERN": "Eritrean nakfa (Nfk)", + "ETB": "Ethiopian birr (Br)", + "EUR": "Euro (€)", + "FJD": "Fijian dollar ($)", + "FKP": "Falkland Islands pound (£)", + "GBP": "Pound sterling (£)", + "GEL": "Georgian lari (₾)", + "GGP": "Guernsey pound (£)", + "GHS": "Ghana cedi (₵)", + "GIP": "Gibraltar pound (£)", + "GMD": "Gambian dalasi (D)", + "GNF": "Guinean franc (Fr)", + "GTQ": "Guatemalan quetzal (Q)", + "GYD": "Guyanese dollar ($)", + "HKD": "Hong Kong dollar ($)", + "HNL": "Honduran lempira (L)", + "HRK": "Croatian kuna (kn)", + "HTG": "Haitian gourde (G)", + "HUF": "Hungarian forint (Ft)", + "IDR": "Indonesian rupiah (Rp)", + "ILS": "Israeli new shekel (₪)", + "IMP": "Manx pound (£)", + "INR": "Indian rupee (₹)", + "IQD": "Iraqi dinar (د.ع)", + "IRR": "Iranian rial (﷼)", + "IRT": "Iranian toman (تومان)", + "ISK": "Icelandic króna (kr.)", + "JEP": "Jersey pound (£)", + "JMD": "Jamaican dollar ($)", + "JOD": "Jordanian dinar (د.ا)", + "JPY": "Japanese yen (¥)", + "KES": "Kenyan shilling (KSh)", + "KGS": "Kyrgyzstani som (сом)", + "KHR": "Cambodian riel (៛)", + "KMF": "Comorian franc (Fr)", + "KPW": "North Korean won (₩)", + "KRW": "South Korean won (₩)", + "KWD": "Kuwaiti dinar (د.ك)", + "KYD": "Cayman Islands dollar ($)", + "KZT": "Kazakhstani tenge (₸)", + "LAK": "Lao kip (₭)", + "LBP": "Lebanese pound (ل.ل)", + "LKR": "Sri Lankan rupee (රු)", + "LRD": "Liberian dollar ($)", + "LSL": "Lesotho loti (L)", + "LYD": "Libyan dinar (د.ل)", + "MAD": "Moroccan dirham (د.م.)", + "MDL": "Moldovan leu (MDL)", + "MGA": "Malagasy ariary (Ar)", + "MKD": "Macedonian denar (ден)", + "MMK": "Burmese kyat (Ks)", + "MNT": "Mongolian tögrög (₮)", + "MOP": "Macanese pataca (P)", + "MRU": "Mauritanian ouguiya (UM)", + "MUR": "Mauritian rupee (₨)", + "MVR": "Maldivian rufiyaa (.ރ)", + "MWK": "Malawian kwacha (MK)", + "MXN": "Mexican peso ($)", + "MYR": "Malaysian ringgit (RM)", + "MZN": "Mozambican metical (MT)", + "NAD": "Namibian dollar (N$)", + "NGN": "Nigerian naira (₦)", + "NIO": "Nicaraguan córdoba (C$)", + "NOK": "Norwegian krone (kr)", + "NPR": "Nepalese rupee (₨)", + "NZD": "New Zealand dollar ($)", + "OMR": "Omani rial (ر.ع.)", + "PAB": "Panamanian balboa (B/.)", + "PEN": "Sol (S/)", + "PGK": "Papua New Guinean kina (K)", + "PHP": "Philippine peso (₱)", + "PKR": "Pakistani rupee (₨)", + "PLN": "Polish złoty (zł)", + "PRB": "Transnistrian ruble (р.)", + "PYG": "Paraguayan guaraní (₲)", + "QAR": "Qatari riyal (ر.ق)", + "RON": "Romanian leu (lei)", + "RSD": "Serbian dinar (рсд)", + "RUB": "Russian ruble (₽)", + "RWF": "Rwandan franc (Fr)", + "SAR": "Saudi riyal (ر.س)", + "SBD": "Solomon Islands dollar ($)", + "SCR": "Seychellois rupee (₨)", + "SDG": "Sudanese pound (ج.س.)", + "SEK": "Swedish krona (kr)", + "SGD": "Singapore dollar ($)", + "SHP": "Saint Helena pound (£)", + "SLL": "Sierra Leonean leone (Le)", + "SOS": "Somali shilling (Sh)", + "SRD": "Surinamese dollar ($)", + "SSP": "South Sudanese pound (£)", + "STN": "São Tomé and Príncipe dobra (Db)", + "SYP": "Syrian pound (ل.س)", + "SZL": "Swazi lilangeni (E)", + "THB": "Thai baht (฿)", + "TJS": "Tajikistani somoni (ЅМ)", + "TMT": "Turkmenistan manat (m)", + "TND": "Tunisian dinar (د.ت)", + "TOP": "Tongan paʻanga (T$)", + "TRY": "Turkish lira (₺)", + "TTD": "Trinidad and Tobago dollar ($)", + "TWD": "New Taiwan dollar (NT$)", + "TZS": "Tanzanian shilling (Sh)", + "UAH": "Ukrainian hryvnia (₴)", + "UGX": "Ugandan shilling (UGX)", + "USD": "United States (US) dollar ($)", + "UYU": "Uruguayan peso ($)", + "UZS": "Uzbekistani som (UZS)", + "VEF": "Venezuelan bolívar (Bs F)", + "VES": "Bolívar soberano (Bs.S)", + "VND": "Vietnamese đồng (₫)", + "VUV": "Vanuatu vatu (Vt)", + "WST": "Samoan tālā (T)", + "XAF": "Central African CFA franc (CFA)", + "XCD": "East Caribbean dollar ($)", + "XOF": "West African CFA franc (CFA)", + "XPF": "CFP franc (Fr)", + "YER": "Yemeni rial (﷼)", + "ZAR": "South African rand (R)", + "ZMW": "Zambian kwacha (ZK)" +}; + +const stateOptions = { + "AF": "Afghanistan", + "AX": "Åland Islands", + "AL:AL-01": "Albania - Berat", + "AL:AL-09": "Albania - Dibër", + "AL:AL-02": "Albania - Durrës", + "AL:AL-03": "Albania - Elbasan", + "AL:AL-04": "Albania - Fier", + "AL:AL-05": "Albania - Gjirokastër", + "AL:AL-06": "Albania - Korçë", + "AL:AL-07": "Albania - Kukës", + "AL:AL-08": "Albania - Lezhë", + "AL:AL-10": "Albania - Shkodër", + "AL:AL-11": "Albania - Tirana", + "AL:AL-12": "Albania - Vlorë", + "DZ:DZ-01": "Algeria - Adrar", + "DZ:DZ-02": "Algeria - Chlef", + "DZ:DZ-03": "Algeria - Laghouat", + "DZ:DZ-04": "Algeria - Oum El Bouaghi", + "DZ:DZ-05": "Algeria - Batna", + "DZ:DZ-06": "Algeria - Béjaïa", + "DZ:DZ-07": "Algeria - Biskra", + "DZ:DZ-08": "Algeria - Béchar", + "DZ:DZ-09": "Algeria - Blida", + "DZ:DZ-10": "Algeria - Bouira", + "DZ:DZ-11": "Algeria - Tamanghasset", + "DZ:DZ-12": "Algeria - Tébessa", + "DZ:DZ-13": "Algeria - Tlemcen", + "DZ:DZ-14": "Algeria - Tiaret", + "DZ:DZ-15": "Algeria - Tizi Ouzou", + "DZ:DZ-16": "Algeria - Algiers", + "DZ:DZ-17": "Algeria - Djelfa", + "DZ:DZ-18": "Algeria - Jijel", + "DZ:DZ-19": "Algeria - Sétif", + "DZ:DZ-20": "Algeria - Saïda", + "DZ:DZ-21": "Algeria - Skikda", + "DZ:DZ-22": "Algeria - Sidi Bel Abbès", + "DZ:DZ-23": "Algeria - Annaba", + "DZ:DZ-24": "Algeria - Guelma", + "DZ:DZ-25": "Algeria - Constantine", + "DZ:DZ-26": "Algeria - Médéa", + "DZ:DZ-27": "Algeria - Mostaganem", + "DZ:DZ-28": "Algeria - M’Sila", + "DZ:DZ-29": "Algeria - Mascara", + "DZ:DZ-30": "Algeria - Ouargla", + "DZ:DZ-31": "Algeria - Oran", + "DZ:DZ-32": "Algeria - El Bayadh", + "DZ:DZ-33": "Algeria - Illizi", + "DZ:DZ-34": "Algeria - Bordj Bou Arréridj", + "DZ:DZ-35": "Algeria - Boumerdès", + "DZ:DZ-36": "Algeria - El Tarf", + "DZ:DZ-37": "Algeria - Tindouf", + "DZ:DZ-38": "Algeria - Tissemsilt", + "DZ:DZ-39": "Algeria - El Oued", + "DZ:DZ-40": "Algeria - Khenchela", + "DZ:DZ-41": "Algeria - Souk Ahras", + "DZ:DZ-42": "Algeria - Tipasa", + "DZ:DZ-43": "Algeria - Mila", + "DZ:DZ-44": "Algeria - Aïn Defla", + "DZ:DZ-45": "Algeria - Naama", + "DZ:DZ-46": "Algeria - Aïn Témouchent", + "DZ:DZ-47": "Algeria - Ghardaïa", + "DZ:DZ-48": "Algeria - Relizane", + "AS": "American Samoa", + "AD": "Andorra", + "AO:BGO": "Angola - Bengo", + "AO:BLU": "Angola - Benguela", + "AO:BIE": "Angola - Bié", + "AO:CAB": "Angola - Cabinda", + "AO:CNN": "Angola - Cunene", + "AO:HUA": "Angola - Huambo", + "AO:HUI": "Angola - Huíla", + "AO:CCU": "Angola - Kuando Kubango", + "AO:CNO": "Angola - Kwanza-Norte", + "AO:CUS": "Angola - Kwanza-Sul", + "AO:LUA": "Angola - Luanda", + "AO:LNO": "Angola - Lunda-Norte", + "AO:LSU": "Angola - Lunda-Sul", + "AO:MAL": "Angola - Malanje", + "AO:MOX": "Angola - Moxico", + "AO:NAM": "Angola - Namibe", + "AO:UIG": "Angola - Uíge", + "AO:ZAI": "Angola - Zaire", + "AI": "Anguilla", + "AQ": "Antarctica", + "AG": "Antigua and Barbuda", + "AR:C": "Argentina - Ciudad Autónoma de Buenos Aires", + "AR:B": "Argentina - Buenos Aires", + "AR:K": "Argentina - Catamarca", + "AR:H": "Argentina - Chaco", + "AR:U": "Argentina - Chubut", + "AR:X": "Argentina - Córdoba", + "AR:W": "Argentina - Corrientes", + "AR:E": "Argentina - Entre Ríos", + "AR:P": "Argentina - Formosa", + "AR:Y": "Argentina - Jujuy", + "AR:L": "Argentina - La Pampa", + "AR:F": "Argentina - La Rioja", + "AR:M": "Argentina - Mendoza", + "AR:N": "Argentina - Misiones", + "AR:Q": "Argentina - Neuquén", + "AR:R": "Argentina - Río Negro", + "AR:A": "Argentina - Salta", + "AR:J": "Argentina - San Juan", + "AR:D": "Argentina - San Luis", + "AR:Z": "Argentina - Santa Cruz", + "AR:S": "Argentina - Santa Fe", + "AR:G": "Argentina - Santiago del Estero", + "AR:V": "Argentina - Tierra del Fuego", + "AR:T": "Argentina - Tucumán", + "AM": "Armenia", + "AW": "Aruba", + "AU:ACT": "Australia - Australian Capital Territory", + "AU:NSW": "Australia - New South Wales", + "AU:NT": "Australia - Northern Territory", + "AU:QLD": "Australia - Queensland", + "AU:SA": "Australia - South Australia", + "AU:TAS": "Australia - Tasmania", + "AU:VIC": "Australia - Victoria", + "AU:WA": "Australia - Western Australia", + "AT": "Austria", + "AZ": "Azerbaijan", + "BS": "Bahamas", + "BH": "Bahrain", + "BD:BD-05": "Bangladesh - Bagerhat", + "BD:BD-01": "Bangladesh - Bandarban", + "BD:BD-02": "Bangladesh - Barguna", + "BD:BD-06": "Bangladesh - Barishal", + "BD:BD-07": "Bangladesh - Bhola", + "BD:BD-03": "Bangladesh - Bogura", + "BD:BD-04": "Bangladesh - Brahmanbaria", + "BD:BD-09": "Bangladesh - Chandpur", + "BD:BD-10": "Bangladesh - Chattogram", + "BD:BD-12": "Bangladesh - Chuadanga", + "BD:BD-11": "Bangladesh - Cox's Bazar", + "BD:BD-08": "Bangladesh - Cumilla", + "BD:BD-13": "Bangladesh - Dhaka", + "BD:BD-14": "Bangladesh - Dinajpur", + "BD:BD-15": "Bangladesh - Faridpur ", + "BD:BD-16": "Bangladesh - Feni", + "BD:BD-19": "Bangladesh - Gaibandha", + "BD:BD-18": "Bangladesh - Gazipur", + "BD:BD-17": "Bangladesh - Gopalganj", + "BD:BD-20": "Bangladesh - Habiganj", + "BD:BD-21": "Bangladesh - Jamalpur", + "BD:BD-22": "Bangladesh - Jashore", + "BD:BD-25": "Bangladesh - Jhalokati", + "BD:BD-23": "Bangladesh - Jhenaidah", + "BD:BD-24": "Bangladesh - Joypurhat", + "BD:BD-29": "Bangladesh - Khagrachhari", + "BD:BD-27": "Bangladesh - Khulna", + "BD:BD-26": "Bangladesh - Kishoreganj", + "BD:BD-28": "Bangladesh - Kurigram", + "BD:BD-30": "Bangladesh - Kushtia", + "BD:BD-31": "Bangladesh - Lakshmipur", + "BD:BD-32": "Bangladesh - Lalmonirhat", + "BD:BD-36": "Bangladesh - Madaripur", + "BD:BD-37": "Bangladesh - Magura", + "BD:BD-33": "Bangladesh - Manikganj ", + "BD:BD-39": "Bangladesh - Meherpur", + "BD:BD-38": "Bangladesh - Moulvibazar", + "BD:BD-35": "Bangladesh - Munshiganj", + "BD:BD-34": "Bangladesh - Mymensingh", + "BD:BD-48": "Bangladesh - Naogaon", + "BD:BD-43": "Bangladesh - Narail", + "BD:BD-40": "Bangladesh - Narayanganj", + "BD:BD-42": "Bangladesh - Narsingdi", + "BD:BD-44": "Bangladesh - Natore", + "BD:BD-45": "Bangladesh - Nawabganj", + "BD:BD-41": "Bangladesh - Netrakona", + "BD:BD-46": "Bangladesh - Nilphamari", + "BD:BD-47": "Bangladesh - Noakhali", + "BD:BD-49": "Bangladesh - Pabna", + "BD:BD-52": "Bangladesh - Panchagarh", + "BD:BD-51": "Bangladesh - Patuakhali", + "BD:BD-50": "Bangladesh - Pirojpur", + "BD:BD-53": "Bangladesh - Rajbari", + "BD:BD-54": "Bangladesh - Rajshahi", + "BD:BD-56": "Bangladesh - Rangamati", + "BD:BD-55": "Bangladesh - Rangpur", + "BD:BD-58": "Bangladesh - Satkhira", + "BD:BD-62": "Bangladesh - Shariatpur", + "BD:BD-57": "Bangladesh - Sherpur", + "BD:BD-59": "Bangladesh - Sirajganj", + "BD:BD-61": "Bangladesh - Sunamganj", + "BD:BD-60": "Bangladesh - Sylhet", + "BD:BD-63": "Bangladesh - Tangail", + "BD:BD-64": "Bangladesh - Thakurgaon", + "BB": "Barbados", + "BY": "Belarus", + "PW": "Belau", + "BE": "Belgium", + "BZ": "Belize", + "BJ:AL": "Benin - Alibori", + "BJ:AK": "Benin - Atakora", + "BJ:AQ": "Benin - Atlantique", + "BJ:BO": "Benin - Borgou", + "BJ:CO": "Benin - Collines", + "BJ:KO": "Benin - Kouffo", + "BJ:DO": "Benin - Donga", + "BJ:LI": "Benin - Littoral", + "BJ:MO": "Benin - Mono", + "BJ:OU": "Benin - Ouémé", + "BJ:PL": "Benin - Plateau", + "BJ:ZO": "Benin - Zou", + "BM": "Bermuda", + "BT": "Bhutan", + "BO:BO-B": "Bolivia - Beni", + "BO:BO-H": "Bolivia - Chuquisaca", + "BO:BO-C": "Bolivia - Cochabamba", + "BO:BO-L": "Bolivia - La Paz", + "BO:BO-O": "Bolivia - Oruro", + "BO:BO-N": "Bolivia - Pando", + "BO:BO-P": "Bolivia - Potosí", + "BO:BO-S": "Bolivia - Santa Cruz", + "BO:BO-T": "Bolivia - Tarija", + "BQ": "Bonaire, Saint Eustatius and Saba", + "BA": "Bosnia and Herzegovina", + "BW": "Botswana", + "BV": "Bouvet Island", + "BR:AC": "Brazil - Acre", + "BR:AL": "Brazil - Alagoas", + "BR:AP": "Brazil - Amapá", + "BR:AM": "Brazil - Amazonas", + "BR:BA": "Brazil - Bahia", + "BR:CE": "Brazil - Ceará", + "BR:DF": "Brazil - Distrito Federal", + "BR:ES": "Brazil - Espírito Santo", + "BR:GO": "Brazil - Goiás", + "BR:MA": "Brazil - Maranhão", + "BR:MT": "Brazil - Mato Grosso", + "BR:MS": "Brazil - Mato Grosso do Sul", + "BR:MG": "Brazil - Minas Gerais", + "BR:PA": "Brazil - Pará", + "BR:PB": "Brazil - Paraíba", + "BR:PR": "Brazil - Paraná", + "BR:PE": "Brazil - Pernambuco", + "BR:PI": "Brazil - Piauí", + "BR:RJ": "Brazil - Rio de Janeiro", + "BR:RN": "Brazil - Rio Grande do Norte", + "BR:RS": "Brazil - Rio Grande do Sul", + "BR:RO": "Brazil - Rondônia", + "BR:RR": "Brazil - Roraima", + "BR:SC": "Brazil - Santa Catarina", + "BR:SP": "Brazil - São Paulo", + "BR:SE": "Brazil - Sergipe", + "BR:TO": "Brazil - Tocantins", + "IO": "British Indian Ocean Territory", + "BN": "Brunei", + "BG:BG-01": "Bulgaria - Blagoevgrad", + "BG:BG-02": "Bulgaria - Burgas", + "BG:BG-08": "Bulgaria - Dobrich", + "BG:BG-07": "Bulgaria - Gabrovo", + "BG:BG-26": "Bulgaria - Haskovo", + "BG:BG-09": "Bulgaria - Kardzhali", + "BG:BG-10": "Bulgaria - Kyustendil", + "BG:BG-11": "Bulgaria - Lovech", + "BG:BG-12": "Bulgaria - Montana", + "BG:BG-13": "Bulgaria - Pazardzhik", + "BG:BG-14": "Bulgaria - Pernik", + "BG:BG-15": "Bulgaria - Pleven", + "BG:BG-16": "Bulgaria - Plovdiv", + "BG:BG-17": "Bulgaria - Razgrad", + "BG:BG-18": "Bulgaria - Ruse", + "BG:BG-27": "Bulgaria - Shumen", + "BG:BG-19": "Bulgaria - Silistra", + "BG:BG-20": "Bulgaria - Sliven", + "BG:BG-21": "Bulgaria - Smolyan", + "BG:BG-23": "Bulgaria - Sofia District", + "BG:BG-22": "Bulgaria - Sofia", + "BG:BG-24": "Bulgaria - Stara Zagora", + "BG:BG-25": "Bulgaria - Targovishte", + "BG:BG-03": "Bulgaria - Varna", + "BG:BG-04": "Bulgaria - Veliko Tarnovo", + "BG:BG-05": "Bulgaria - Vidin", + "BG:BG-06": "Bulgaria - Vratsa", + "BG:BG-28": "Bulgaria - Yambol", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Cambodia", + "CM": "Cameroon", + "CA:AB": "Canada - Alberta", + "CA:BC": "Canada - British Columbia", + "CA:MB": "Canada - Manitoba", + "CA:NB": "Canada - New Brunswick", + "CA:NL": "Canada - Newfoundland and Labrador", + "CA:NT": "Canada - Northwest Territories", + "CA:NS": "Canada - Nova Scotia", + "CA:NU": "Canada - Nunavut", + "CA:ON": "Canada - Ontario", + "CA:PE": "Canada - Prince Edward Island", + "CA:QC": "Canada - Quebec", + "CA:SK": "Canada - Saskatchewan", + "CA:YT": "Canada - Yukon Territory", + "CV": "Cape Verde", + "KY": "Cayman Islands", + "CF": "Central African Republic", + "TD": "Chad", + "CL:CL-AI": "Chile - Aisén del General Carlos Ibañez del Campo", + "CL:CL-AN": "Chile - Antofagasta", + "CL:CL-AP": "Chile - Arica y Parinacota", + "CL:CL-AR": "Chile - La Araucanía", + "CL:CL-AT": "Chile - Atacama", + "CL:CL-BI": "Chile - Biobío", + "CL:CL-CO": "Chile - Coquimbo", + "CL:CL-LI": "Chile - Libertador General Bernardo O'Higgins", + "CL:CL-LL": "Chile - Los Lagos", + "CL:CL-LR": "Chile - Los Ríos", + "CL:CL-MA": "Chile - Magallanes", + "CL:CL-ML": "Chile - Maule", + "CL:CL-NB": "Chile - Ñuble", + "CL:CL-RM": "Chile - Región Metropolitana de Santiago", + "CL:CL-TA": "Chile - Tarapacá", + "CL:CL-VS": "Chile - Valparaíso", + "CN:CN1": "China - Yunnan / 云南", + "CN:CN2": "China - Beijing / 北京", + "CN:CN3": "China - Tianjin / 天津", + "CN:CN4": "China - Hebei / 河北", + "CN:CN5": "China - Shanxi / 山西", + "CN:CN6": "China - Inner Mongolia / 內蒙古", + "CN:CN7": "China - Liaoning / 辽宁", + "CN:CN8": "China - Jilin / 吉林", + "CN:CN9": "China - Heilongjiang / 黑龙江", + "CN:CN10": "China - Shanghai / 上海", + "CN:CN11": "China - Jiangsu / 江苏", + "CN:CN12": "China - Zhejiang / 浙江", + "CN:CN13": "China - Anhui / 安徽", + "CN:CN14": "China - Fujian / 福建", + "CN:CN15": "China - Jiangxi / 江西", + "CN:CN16": "China - Shandong / 山东", + "CN:CN17": "China - Henan / 河南", + "CN:CN18": "China - Hubei / 湖北", + "CN:CN19": "China - Hunan / 湖南", + "CN:CN20": "China - Guangdong / 广东", + "CN:CN21": "China - Guangxi Zhuang / 广西壮族", + "CN:CN22": "China - Hainan / 海南", + "CN:CN23": "China - Chongqing / 重庆", + "CN:CN24": "China - Sichuan / 四川", + "CN:CN25": "China - Guizhou / 贵州", + "CN:CN26": "China - Shaanxi / 陕西", + "CN:CN27": "China - Gansu / 甘肃", + "CN:CN28": "China - Qinghai / 青海", + "CN:CN29": "China - Ningxia Hui / 宁夏", + "CN:CN30": "China - Macao / 澳门", + "CN:CN31": "China - Tibet / 西藏", + "CN:CN32": "China - Xinjiang / 新疆", + "CX": "Christmas Island", + "CC": "Cocos (Keeling) Islands", + "CO:CO-AMA": "Colombia - Amazonas", + "CO:CO-ANT": "Colombia - Antioquia", + "CO:CO-ARA": "Colombia - Arauca", + "CO:CO-ATL": "Colombia - Atlántico", + "CO:CO-BOL": "Colombia - Bolívar", + "CO:CO-BOY": "Colombia - Boyacá", + "CO:CO-CAL": "Colombia - Caldas", + "CO:CO-CAQ": "Colombia - Caquetá", + "CO:CO-CAS": "Colombia - Casanare", + "CO:CO-CAU": "Colombia - Cauca", + "CO:CO-CES": "Colombia - Cesar", + "CO:CO-CHO": "Colombia - Chocó", + "CO:CO-COR": "Colombia - Córdoba", + "CO:CO-CUN": "Colombia - Cundinamarca", + "CO:CO-DC": "Colombia - Capital District", + "CO:CO-GUA": "Colombia - Guainía", + "CO:CO-GUV": "Colombia - Guaviare", + "CO:CO-HUI": "Colombia - Huila", + "CO:CO-LAG": "Colombia - La Guajira", + "CO:CO-MAG": "Colombia - Magdalena", + "CO:CO-MET": "Colombia - Meta", + "CO:CO-NAR": "Colombia - Nariño", + "CO:CO-NSA": "Colombia - Norte de Santander", + "CO:CO-PUT": "Colombia - Putumayo", + "CO:CO-QUI": "Colombia - Quindío", + "CO:CO-RIS": "Colombia - Risaralda", + "CO:CO-SAN": "Colombia - Santander", + "CO:CO-SAP": "Colombia - San Andrés & Providencia", + "CO:CO-SUC": "Colombia - Sucre", + "CO:CO-TOL": "Colombia - Tolima", + "CO:CO-VAC": "Colombia - Valle del Cauca", + "CO:CO-VAU": "Colombia - Vaupés", + "CO:CO-VID": "Colombia - Vichada", + "KM": "Comoros", + "CG": "Congo (Brazzaville)", + "CD": "Congo (Kinshasa)", + "CK": "Cook Islands", + "CR:CR-A": "Costa Rica - Alajuela", + "CR:CR-C": "Costa Rica - Cartago", + "CR:CR-G": "Costa Rica - Guanacaste", + "CR:CR-H": "Costa Rica - Heredia", + "CR:CR-L": "Costa Rica - Limón", + "CR:CR-P": "Costa Rica - Puntarenas", + "CR:CR-SJ": "Costa Rica - San José", + "HR": "Croatia", + "CU": "Cuba", + "CW": "Curaçao", + "CY": "Cyprus", + "CZ": "Czech Republic", + "DK": "Denmark", + "DJ": "Djibouti", + "DM": "Dominica", + "DO:DO-01": "Dominican Republic - Distrito Nacional", + "DO:DO-02": "Dominican Republic - Azua", + "DO:DO-03": "Dominican Republic - Baoruco", + "DO:DO-04": "Dominican Republic - Barahona", + "DO:DO-33": "Dominican Republic - Cibao Nordeste", + "DO:DO-34": "Dominican Republic - Cibao Noroeste", + "DO:DO-35": "Dominican Republic - Cibao Norte", + "DO:DO-36": "Dominican Republic - Cibao Sur", + "DO:DO-05": "Dominican Republic - Dajabón", + "DO:DO-06": "Dominican Republic - Duarte", + "DO:DO-08": "Dominican Republic - El Seibo", + "DO:DO-37": "Dominican Republic - El Valle", + "DO:DO-07": "Dominican Republic - Elías Piña", + "DO:DO-38": "Dominican Republic - Enriquillo", + "DO:DO-09": "Dominican Republic - Espaillat", + "DO:DO-30": "Dominican Republic - Hato Mayor", + "DO:DO-19": "Dominican Republic - Hermanas Mirabal", + "DO:DO-39": "Dominican Republic - Higüamo", + "DO:DO-10": "Dominican Republic - Independencia", + "DO:DO-11": "Dominican Republic - La Altagracia", + "DO:DO-12": "Dominican Republic - La Romana", + "DO:DO-13": "Dominican Republic - La Vega", + "DO:DO-14": "Dominican Republic - María Trinidad Sánchez", + "DO:DO-28": "Dominican Republic - Monseñor Nouel", + "DO:DO-15": "Dominican Republic - Monte Cristi", + "DO:DO-29": "Dominican Republic - Monte Plata", + "DO:DO-40": "Dominican Republic - Ozama", + "DO:DO-16": "Dominican Republic - Pedernales", + "DO:DO-17": "Dominican Republic - Peravia", + "DO:DO-18": "Dominican Republic - Puerto Plata", + "DO:DO-20": "Dominican Republic - Samaná", + "DO:DO-21": "Dominican Republic - San Cristóbal", + "DO:DO-31": "Dominican Republic - San José de Ocoa", + "DO:DO-22": "Dominican Republic - San Juan", + "DO:DO-23": "Dominican Republic - San Pedro de Macorís", + "DO:DO-24": "Dominican Republic - Sánchez Ramírez", + "DO:DO-25": "Dominican Republic - Santiago", + "DO:DO-26": "Dominican Republic - Santiago Rodríguez", + "DO:DO-32": "Dominican Republic - Santo Domingo", + "DO:DO-41": "Dominican Republic - Valdesia", + "DO:DO-27": "Dominican Republic - Valverde", + "DO:DO-42": "Dominican Republic - Yuma", + "EC:EC-A": "Ecuador - Azuay", + "EC:EC-B": "Ecuador - Bolívar", + "EC:EC-F": "Ecuador - Cañar", + "EC:EC-C": "Ecuador - Carchi", + "EC:EC-H": "Ecuador - Chimborazo", + "EC:EC-X": "Ecuador - Cotopaxi", + "EC:EC-O": "Ecuador - El Oro", + "EC:EC-E": "Ecuador - Esmeraldas", + "EC:EC-W": "Ecuador - Galápagos", + "EC:EC-G": "Ecuador - Guayas", + "EC:EC-I": "Ecuador - Imbabura", + "EC:EC-L": "Ecuador - Loja", + "EC:EC-R": "Ecuador - Los Ríos", + "EC:EC-M": "Ecuador - Manabí", + "EC:EC-S": "Ecuador - Morona-Santiago", + "EC:EC-N": "Ecuador - Napo", + "EC:EC-D": "Ecuador - Orellana", + "EC:EC-Y": "Ecuador - Pastaza", + "EC:EC-P": "Ecuador - Pichincha", + "EC:EC-SE": "Ecuador - Santa Elena", + "EC:EC-SD": "Ecuador - Santo Domingo de los Tsáchilas", + "EC:EC-U": "Ecuador - Sucumbíos", + "EC:EC-T": "Ecuador - Tungurahua", + "EC:EC-Z": "Ecuador - Zamora-Chinchipe", + "EG:EGALX": "Egypt - Alexandria", + "EG:EGASN": "Egypt - Aswan", + "EG:EGAST": "Egypt - Asyut", + "EG:EGBA": "Egypt - Red Sea", + "EG:EGBH": "Egypt - Beheira", + "EG:EGBNS": "Egypt - Beni Suef", + "EG:EGC": "Egypt - Cairo", + "EG:EGDK": "Egypt - Dakahlia", + "EG:EGDT": "Egypt - Damietta", + "EG:EGFYM": "Egypt - Faiyum", + "EG:EGGH": "Egypt - Gharbia", + "EG:EGGZ": "Egypt - Giza", + "EG:EGIS": "Egypt - Ismailia", + "EG:EGJS": "Egypt - South Sinai", + "EG:EGKB": "Egypt - Qalyubia", + "EG:EGKFS": "Egypt - Kafr el-Sheikh", + "EG:EGKN": "Egypt - Qena", + "EG:EGLX": "Egypt - Luxor", + "EG:EGMN": "Egypt - Minya", + "EG:EGMNF": "Egypt - Monufia", + "EG:EGMT": "Egypt - Matrouh", + "EG:EGPTS": "Egypt - Port Said", + "EG:EGSHG": "Egypt - Sohag", + "EG:EGSHR": "Egypt - Al Sharqia", + "EG:EGSIN": "Egypt - North Sinai", + "EG:EGSUZ": "Egypt - Suez", + "EG:EGWAD": "Egypt - New Valley", + "SV:SV-AH": "El Salvador - Ahuachapán", + "SV:SV-CA": "El Salvador - Cabañas", + "SV:SV-CH": "El Salvador - Chalatenango", + "SV:SV-CU": "El Salvador - Cuscatlán", + "SV:SV-LI": "El Salvador - La Libertad", + "SV:SV-MO": "El Salvador - Morazán", + "SV:SV-PA": "El Salvador - La Paz", + "SV:SV-SA": "El Salvador - Santa Ana", + "SV:SV-SM": "El Salvador - San Miguel", + "SV:SV-SO": "El Salvador - Sonsonate", + "SV:SV-SS": "El Salvador - San Salvador", + "SV:SV-SV": "El Salvador - San Vicente", + "SV:SV-UN": "El Salvador - La Unión", + "SV:SV-US": "El Salvador - Usulután", + "GQ": "Equatorial Guinea", + "ER": "Eritrea", + "EE": "Estonia", + "SZ": "Eswatini", + "ET": "Ethiopia", + "FK": "Falkland Islands", + "FO": "Faroe Islands", + "FJ": "Fiji", + "FI": "Finland", + "FR": "France", + "GF": "French Guiana", + "PF": "French Polynesia", + "TF": "French Southern Territories", + "GA": "Gabon", + "GM": "Gambia", + "GE": "Georgia", + "DE:DE-BW": "Germany - Baden-Württemberg", + "DE:DE-BY": "Germany - Bavaria", + "DE:DE-BE": "Germany - Berlin", + "DE:DE-BB": "Germany - Brandenburg", + "DE:DE-HB": "Germany - Bremen", + "DE:DE-HH": "Germany - Hamburg", + "DE:DE-HE": "Germany - Hesse", + "DE:DE-MV": "Germany - Mecklenburg-Vorpommern", + "DE:DE-NI": "Germany - Lower Saxony", + "DE:DE-NW": "Germany - North Rhine-Westphalia", + "DE:DE-RP": "Germany - Rhineland-Palatinate", + "DE:DE-SL": "Germany - Saarland", + "DE:DE-SN": "Germany - Saxony", + "DE:DE-ST": "Germany - Saxony-Anhalt", + "DE:DE-SH": "Germany - Schleswig-Holstein", + "DE:DE-TH": "Germany - Thuringia", + "GH:AF": "Ghana - Ahafo", + "GH:AH": "Ghana - Ashanti", + "GH:BA": "Ghana - Brong-Ahafo", + "GH:BO": "Ghana - Bono", + "GH:BE": "Ghana - Bono East", + "GH:CP": "Ghana - Central", + "GH:EP": "Ghana - Eastern", + "GH:AA": "Ghana - Greater Accra", + "GH:NE": "Ghana - North East", + "GH:NP": "Ghana - Northern", + "GH:OT": "Ghana - Oti", + "GH:SV": "Ghana - Savannah", + "GH:UE": "Ghana - Upper East", + "GH:UW": "Ghana - Upper West", + "GH:TV": "Ghana - Volta", + "GH:WP": "Ghana - Western", + "GH:WN": "Ghana - Western North", + "GI": "Gibraltar", + "GR:I": "Greece - Attica", + "GR:A": "Greece - East Macedonia and Thrace", + "GR:B": "Greece - Central Macedonia", + "GR:C": "Greece - West Macedonia", + "GR:D": "Greece - Epirus", + "GR:E": "Greece - Thessaly", + "GR:F": "Greece - Ionian Islands", + "GR:G": "Greece - West Greece", + "GR:H": "Greece - Central Greece", + "GR:J": "Greece - Peloponnese", + "GR:K": "Greece - North Aegean", + "GR:L": "Greece - South Aegean", + "GR:M": "Greece - Crete", + "GL": "Greenland", + "GD": "Grenada", + "GP": "Guadeloupe", + "GU": "Guam", + "GT:GT-AV": "Guatemala - Alta Verapaz", + "GT:GT-BV": "Guatemala - Baja Verapaz", + "GT:GT-CM": "Guatemala - Chimaltenango", + "GT:GT-CQ": "Guatemala - Chiquimula", + "GT:GT-PR": "Guatemala - El Progreso", + "GT:GT-ES": "Guatemala - Escuintla", + "GT:GT-GU": "Guatemala - Guatemala", + "GT:GT-HU": "Guatemala - Huehuetenango", + "GT:GT-IZ": "Guatemala - Izabal", + "GT:GT-JA": "Guatemala - Jalapa", + "GT:GT-JU": "Guatemala - Jutiapa", + "GT:GT-PE": "Guatemala - Petén", + "GT:GT-QZ": "Guatemala - Quetzaltenango", + "GT:GT-QC": "Guatemala - Quiché", + "GT:GT-RE": "Guatemala - Retalhuleu", + "GT:GT-SA": "Guatemala - Sacatepéquez", + "GT:GT-SM": "Guatemala - San Marcos", + "GT:GT-SR": "Guatemala - Santa Rosa", + "GT:GT-SO": "Guatemala - Sololá", + "GT:GT-SU": "Guatemala - Suchitepéquez", + "GT:GT-TO": "Guatemala - Totonicapán", + "GT:GT-ZA": "Guatemala - Zacapa", + "GG": "Guernsey", + "GN": "Guinea", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HT": "Haiti", + "HM": "Heard Island and McDonald Islands", + "HN:HN-AT": "Honduras - Atlántida", + "HN:HN-IB": "Honduras - Bay Islands", + "HN:HN-CH": "Honduras - Choluteca", + "HN:HN-CL": "Honduras - Colón", + "HN:HN-CM": "Honduras - Comayagua", + "HN:HN-CP": "Honduras - Copán", + "HN:HN-CR": "Honduras - Cortés", + "HN:HN-EP": "Honduras - El Paraíso", + "HN:HN-FM": "Honduras - Francisco Morazán", + "HN:HN-GD": "Honduras - Gracias a Dios", + "HN:HN-IN": "Honduras - Intibucá", + "HN:HN-LE": "Honduras - Lempira", + "HN:HN-LP": "Honduras - La Paz", + "HN:HN-OC": "Honduras - Ocotepeque", + "HN:HN-OL": "Honduras - Olancho", + "HN:HN-SB": "Honduras - Santa Bárbara", + "HN:HN-VA": "Honduras - Valle", + "HN:HN-YO": "Honduras - Yoro", + "HK:HONG KONG": "Hong Kong - Hong Kong Island", + "HK:KOWLOON": "Hong Kong - Kowloon", + "HK:NEW TERRITORIES": "Hong Kong - New Territories", + "HU:BK": "Hungary - Bács-Kiskun", + "HU:BE": "Hungary - Békés", + "HU:BA": "Hungary - Baranya", + "HU:BZ": "Hungary - Borsod-Abaúj-Zemplén", + "HU:BU": "Hungary - Budapest", + "HU:CS": "Hungary - Csongrád-Csanád", + "HU:FE": "Hungary - Fejér", + "HU:GS": "Hungary - Győr-Moson-Sopron", + "HU:HB": "Hungary - Hajdú-Bihar", + "HU:HE": "Hungary - Heves", + "HU:JN": "Hungary - Jász-Nagykun-Szolnok", + "HU:KE": "Hungary - Komárom-Esztergom", + "HU:NO": "Hungary - Nógrád", + "HU:PE": "Hungary - Pest", + "HU:SO": "Hungary - Somogy", + "HU:SZ": "Hungary - Szabolcs-Szatmár-Bereg", + "HU:TO": "Hungary - Tolna", + "HU:VA": "Hungary - Vas", + "HU:VE": "Hungary - Veszprém", + "HU:ZA": "Hungary - Zala", + "IS": "Iceland", + "IN:AP": "India - Andhra Pradesh", + "IN:AR": "India - Arunachal Pradesh", + "IN:AS": "India - Assam", + "IN:BR": "India - Bihar", + "IN:CT": "India - Chhattisgarh", + "IN:GA": "India - Goa", + "IN:GJ": "India - Gujarat", + "IN:HR": "India - Haryana", + "IN:HP": "India - Himachal Pradesh", + "IN:JK": "India - Jammu and Kashmir", + "IN:JH": "India - Jharkhand", + "IN:KA": "India - Karnataka", + "IN:KL": "India - Kerala", + "IN:LA": "India - Ladakh", + "IN:MP": "India - Madhya Pradesh", + "IN:MH": "India - Maharashtra", + "IN:MN": "India - Manipur", + "IN:ML": "India - Meghalaya", + "IN:MZ": "India - Mizoram", + "IN:NL": "India - Nagaland", + "IN:OR": "India - Odisha", + "IN:PB": "India - Punjab", + "IN:RJ": "India - Rajasthan", + "IN:SK": "India - Sikkim", + "IN:TN": "India - Tamil Nadu", + "IN:TS": "India - Telangana", + "IN:TR": "India - Tripura", + "IN:UK": "India - Uttarakhand", + "IN:UP": "India - Uttar Pradesh", + "IN:WB": "India - West Bengal", + "IN:AN": "India - Andaman and Nicobar Islands", + "IN:CH": "India - Chandigarh", + "IN:DN": "India - Dadra and Nagar Haveli", + "IN:DD": "India - Daman and Diu", + "IN:DL": "India - Delhi", + "IN:LD": "India - Lakshadeep", + "IN:PY": "India - Pondicherry (Puducherry)", + "ID:AC": "Indonesia - Daerah Istimewa Aceh", + "ID:SU": "Indonesia - Sumatera Utara", + "ID:SB": "Indonesia - Sumatera Barat", + "ID:RI": "Indonesia - Riau", + "ID:KR": "Indonesia - Kepulauan Riau", + "ID:JA": "Indonesia - Jambi", + "ID:SS": "Indonesia - Sumatera Selatan", + "ID:BB": "Indonesia - Bangka Belitung", + "ID:BE": "Indonesia - Bengkulu", + "ID:LA": "Indonesia - Lampung", + "ID:JK": "Indonesia - DKI Jakarta", + "ID:JB": "Indonesia - Jawa Barat", + "ID:BT": "Indonesia - Banten", + "ID:JT": "Indonesia - Jawa Tengah", + "ID:JI": "Indonesia - Jawa Timur", + "ID:YO": "Indonesia - Daerah Istimewa Yogyakarta", + "ID:BA": "Indonesia - Bali", + "ID:NB": "Indonesia - Nusa Tenggara Barat", + "ID:NT": "Indonesia - Nusa Tenggara Timur", + "ID:KB": "Indonesia - Kalimantan Barat", + "ID:KT": "Indonesia - Kalimantan Tengah", + "ID:KI": "Indonesia - Kalimantan Timur", + "ID:KS": "Indonesia - Kalimantan Selatan", + "ID:KU": "Indonesia - Kalimantan Utara", + "ID:SA": "Indonesia - Sulawesi Utara", + "ID:ST": "Indonesia - Sulawesi Tengah", + "ID:SG": "Indonesia - Sulawesi Tenggara", + "ID:SR": "Indonesia - Sulawesi Barat", + "ID:SN": "Indonesia - Sulawesi Selatan", + "ID:GO": "Indonesia - Gorontalo", + "ID:MA": "Indonesia - Maluku", + "ID:MU": "Indonesia - Maluku Utara", + "ID:PA": "Indonesia - Papua", + "ID:PB": "Indonesia - Papua Barat", + "IR:KHZ": "Iran - Khuzestan (خوزستان)", + "IR:THR": "Iran - Tehran (تهران)", + "IR:ILM": "Iran - Ilaam (ایلام)", + "IR:BHR": "Iran - Bushehr (بوشهر)", + "IR:ADL": "Iran - Ardabil (اردبیل)", + "IR:ESF": "Iran - Isfahan (اصفهان)", + "IR:YZD": "Iran - Yazd (یزد)", + "IR:KRH": "Iran - Kermanshah (کرمانشاه)", + "IR:KRN": "Iran - Kerman (کرمان)", + "IR:HDN": "Iran - Hamadan (همدان)", + "IR:GZN": "Iran - Ghazvin (قزوین)", + "IR:ZJN": "Iran - Zanjan (زنجان)", + "IR:LRS": "Iran - Luristan (لرستان)", + "IR:ABZ": "Iran - Alborz (البرز)", + "IR:EAZ": "Iran - East Azarbaijan (آذربایجان شرقی)", + "IR:WAZ": "Iran - West Azarbaijan (آذربایجان غربی)", + "IR:CHB": "Iran - Chaharmahal and Bakhtiari (چهارمحال و بختیاری)", + "IR:SKH": "Iran - South Khorasan (خراسان جنوبی)", + "IR:RKH": "Iran - Razavi Khorasan (خراسان رضوی)", + "IR:NKH": "Iran - North Khorasan (خراسان شمالی)", + "IR:SMN": "Iran - Semnan (سمنان)", + "IR:FRS": "Iran - Fars (فارس)", + "IR:QHM": "Iran - Qom (قم)", + "IR:KRD": "Iran - Kurdistan / کردستان)", + "IR:KBD": "Iran - Kohgiluyeh and BoyerAhmad (کهگیلوییه و بویراحمد)", + "IR:GLS": "Iran - Golestan (گلستان)", + "IR:GIL": "Iran - Gilan (گیلان)", + "IR:MZN": "Iran - Mazandaran (مازندران)", + "IR:MKZ": "Iran - Markazi (مرکزی)", + "IR:HRZ": "Iran - Hormozgan (هرمزگان)", + "IR:SBN": "Iran - Sistan and Baluchestan (سیستان و بلوچستان)", + "IQ": "Iraq", + "IE:CW": "Ireland - Carlow", + "IE:CN": "Ireland - Cavan", + "IE:CE": "Ireland - Clare", + "IE:CO": "Ireland - Cork", + "IE:DL": "Ireland - Donegal", + "IE:D": "Ireland - Dublin", + "IE:G": "Ireland - Galway", + "IE:KY": "Ireland - Kerry", + "IE:KE": "Ireland - Kildare", + "IE:KK": "Ireland - Kilkenny", + "IE:LS": "Ireland - Laois", + "IE:LM": "Ireland - Leitrim", + "IE:LK": "Ireland - Limerick", + "IE:LD": "Ireland - Longford", + "IE:LH": "Ireland - Louth", + "IE:MO": "Ireland - Mayo", + "IE:MH": "Ireland - Meath", + "IE:MN": "Ireland - Monaghan", + "IE:OY": "Ireland - Offaly", + "IE:RN": "Ireland - Roscommon", + "IE:SO": "Ireland - Sligo", + "IE:TA": "Ireland - Tipperary", + "IE:WD": "Ireland - Waterford", + "IE:WH": "Ireland - Westmeath", + "IE:WX": "Ireland - Wexford", + "IE:WW": "Ireland - Wicklow", + "IM": "Isle of Man", + "IL": "Israel", + "IT:AG": "Italy - Agrigento", + "IT:AL": "Italy - Alessandria", + "IT:AN": "Italy - Ancona", + "IT:AO": "Italy - Aosta", + "IT:AR": "Italy - Arezzo", + "IT:AP": "Italy - Ascoli Piceno", + "IT:AT": "Italy - Asti", + "IT:AV": "Italy - Avellino", + "IT:BA": "Italy - Bari", + "IT:BT": "Italy - Barletta-Andria-Trani", + "IT:BL": "Italy - Belluno", + "IT:BN": "Italy - Benevento", + "IT:BG": "Italy - Bergamo", + "IT:BI": "Italy - Biella", + "IT:BO": "Italy - Bologna", + "IT:BZ": "Italy - Bolzano", + "IT:BS": "Italy - Brescia", + "IT:BR": "Italy - Brindisi", + "IT:CA": "Italy - Cagliari", + "IT:CL": "Italy - Caltanissetta", + "IT:CB": "Italy - Campobasso", + "IT:CE": "Italy - Caserta", + "IT:CT": "Italy - Catania", + "IT:CZ": "Italy - Catanzaro", + "IT:CH": "Italy - Chieti", + "IT:CO": "Italy - Como", + "IT:CS": "Italy - Cosenza", + "IT:CR": "Italy - Cremona", + "IT:KR": "Italy - Crotone", + "IT:CN": "Italy - Cuneo", + "IT:EN": "Italy - Enna", + "IT:FM": "Italy - Fermo", + "IT:FE": "Italy - Ferrara", + "IT:FI": "Italy - Firenze", + "IT:FG": "Italy - Foggia", + "IT:FC": "Italy - Forlì-Cesena", + "IT:FR": "Italy - Frosinone", + "IT:GE": "Italy - Genova", + "IT:GO": "Italy - Gorizia", + "IT:GR": "Italy - Grosseto", + "IT:IM": "Italy - Imperia", + "IT:IS": "Italy - Isernia", + "IT:SP": "Italy - La Spezia", + "IT:AQ": "Italy - L'Aquila", + "IT:LT": "Italy - Latina", + "IT:LE": "Italy - Lecce", + "IT:LC": "Italy - Lecco", + "IT:LI": "Italy - Livorno", + "IT:LO": "Italy - Lodi", + "IT:LU": "Italy - Lucca", + "IT:MC": "Italy - Macerata", + "IT:MN": "Italy - Mantova", + "IT:MS": "Italy - Massa-Carrara", + "IT:MT": "Italy - Matera", + "IT:ME": "Italy - Messina", + "IT:MI": "Italy - Milano", + "IT:MO": "Italy - Modena", + "IT:MB": "Italy - Monza e della Brianza", + "IT:NA": "Italy - Napoli", + "IT:NO": "Italy - Novara", + "IT:NU": "Italy - Nuoro", + "IT:OR": "Italy - Oristano", + "IT:PD": "Italy - Padova", + "IT:PA": "Italy - Palermo", + "IT:PR": "Italy - Parma", + "IT:PV": "Italy - Pavia", + "IT:PG": "Italy - Perugia", + "IT:PU": "Italy - Pesaro e Urbino", + "IT:PE": "Italy - Pescara", + "IT:PC": "Italy - Piacenza", + "IT:PI": "Italy - Pisa", + "IT:PT": "Italy - Pistoia", + "IT:PN": "Italy - Pordenone", + "IT:PZ": "Italy - Potenza", + "IT:PO": "Italy - Prato", + "IT:RG": "Italy - Ragusa", + "IT:RA": "Italy - Ravenna", + "IT:RC": "Italy - Reggio Calabria", + "IT:RE": "Italy - Reggio Emilia", + "IT:RI": "Italy - Rieti", + "IT:RN": "Italy - Rimini", + "IT:RM": "Italy - Roma", + "IT:RO": "Italy - Rovigo", + "IT:SA": "Italy - Salerno", + "IT:SS": "Italy - Sassari", + "IT:SV": "Italy - Savona", + "IT:SI": "Italy - Siena", + "IT:SR": "Italy - Siracusa", + "IT:SO": "Italy - Sondrio", + "IT:SU": "Italy - Sud Sardegna", + "IT:TA": "Italy - Taranto", + "IT:TE": "Italy - Teramo", + "IT:TR": "Italy - Terni", + "IT:TO": "Italy - Torino", + "IT:TP": "Italy - Trapani", + "IT:TN": "Italy - Trento", + "IT:TV": "Italy - Treviso", + "IT:TS": "Italy - Trieste", + "IT:UD": "Italy - Udine", + "IT:VA": "Italy - Varese", + "IT:VE": "Italy - Venezia", + "IT:VB": "Italy - Verbano-Cusio-Ossola", + "IT:VC": "Italy - Vercelli", + "IT:VR": "Italy - Verona", + "IT:VV": "Italy - Vibo Valentia", + "IT:VI": "Italy - Vicenza", + "IT:VT": "Italy - Viterbo", + "CI": "Ivory Coast", + "JM:JM-01": "Jamaica - Kingston", + "JM:JM-02": "Jamaica - Saint Andrew", + "JM:JM-03": "Jamaica - Saint Thomas", + "JM:JM-04": "Jamaica - Portland", + "JM:JM-05": "Jamaica - Saint Mary", + "JM:JM-06": "Jamaica - Saint Ann", + "JM:JM-07": "Jamaica - Trelawny", + "JM:JM-08": "Jamaica - Saint James", + "JM:JM-09": "Jamaica - Hanover", + "JM:JM-10": "Jamaica - Westmoreland", + "JM:JM-11": "Jamaica - Saint Elizabeth", + "JM:JM-12": "Jamaica - Manchester", + "JM:JM-13": "Jamaica - Clarendon", + "JM:JM-14": "Jamaica - Saint Catherine", + "JP:JP01": "Japan - Hokkaido", + "JP:JP02": "Japan - Aomori", + "JP:JP03": "Japan - Iwate", + "JP:JP04": "Japan - Miyagi", + "JP:JP05": "Japan - Akita", + "JP:JP06": "Japan - Yamagata", + "JP:JP07": "Japan - Fukushima", + "JP:JP08": "Japan - Ibaraki", + "JP:JP09": "Japan - Tochigi", + "JP:JP10": "Japan - Gunma", + "JP:JP11": "Japan - Saitama", + "JP:JP12": "Japan - Chiba", + "JP:JP13": "Japan - Tokyo", + "JP:JP14": "Japan - Kanagawa", + "JP:JP15": "Japan - Niigata", + "JP:JP16": "Japan - Toyama", + "JP:JP17": "Japan - Ishikawa", + "JP:JP18": "Japan - Fukui", + "JP:JP19": "Japan - Yamanashi", + "JP:JP20": "Japan - Nagano", + "JP:JP21": "Japan - Gifu", + "JP:JP22": "Japan - Shizuoka", + "JP:JP23": "Japan - Aichi", + "JP:JP24": "Japan - Mie", + "JP:JP25": "Japan - Shiga", + "JP:JP26": "Japan - Kyoto", + "JP:JP27": "Japan - Osaka", + "JP:JP28": "Japan - Hyogo", + "JP:JP29": "Japan - Nara", + "JP:JP30": "Japan - Wakayama", + "JP:JP31": "Japan - Tottori", + "JP:JP32": "Japan - Shimane", + "JP:JP33": "Japan - Okayama", + "JP:JP34": "Japan - Hiroshima", + "JP:JP35": "Japan - Yamaguchi", + "JP:JP36": "Japan - Tokushima", + "JP:JP37": "Japan - Kagawa", + "JP:JP38": "Japan - Ehime", + "JP:JP39": "Japan - Kochi", + "JP:JP40": "Japan - Fukuoka", + "JP:JP41": "Japan - Saga", + "JP:JP42": "Japan - Nagasaki", + "JP:JP43": "Japan - Kumamoto", + "JP:JP44": "Japan - Oita", + "JP:JP45": "Japan - Miyazaki", + "JP:JP46": "Japan - Kagoshima", + "JP:JP47": "Japan - Okinawa", + "JE": "Jersey", + "JO": "Jordan", + "KZ": "Kazakhstan", + "KE:KE01": "Kenya - Baringo", + "KE:KE02": "Kenya - Bomet", + "KE:KE03": "Kenya - Bungoma", + "KE:KE04": "Kenya - Busia", + "KE:KE05": "Kenya - Elgeyo-Marakwet", + "KE:KE06": "Kenya - Embu", + "KE:KE07": "Kenya - Garissa", + "KE:KE08": "Kenya - Homa Bay", + "KE:KE09": "Kenya - Isiolo", + "KE:KE10": "Kenya - Kajiado", + "KE:KE11": "Kenya - Kakamega", + "KE:KE12": "Kenya - Kericho", + "KE:KE13": "Kenya - Kiambu", + "KE:KE14": "Kenya - Kilifi", + "KE:KE15": "Kenya - Kirinyaga", + "KE:KE16": "Kenya - Kisii", + "KE:KE17": "Kenya - Kisumu", + "KE:KE18": "Kenya - Kitui", + "KE:KE19": "Kenya - Kwale", + "KE:KE20": "Kenya - Laikipia", + "KE:KE21": "Kenya - Lamu", + "KE:KE22": "Kenya - Machakos", + "KE:KE23": "Kenya - Makueni", + "KE:KE24": "Kenya - Mandera", + "KE:KE25": "Kenya - Marsabit", + "KE:KE26": "Kenya - Meru", + "KE:KE27": "Kenya - Migori", + "KE:KE28": "Kenya - Mombasa", + "KE:KE29": "Kenya - Murang’a", + "KE:KE30": "Kenya - Nairobi County", + "KE:KE31": "Kenya - Nakuru", + "KE:KE32": "Kenya - Nandi", + "KE:KE33": "Kenya - Narok", + "KE:KE34": "Kenya - Nyamira", + "KE:KE35": "Kenya - Nyandarua", + "KE:KE36": "Kenya - Nyeri", + "KE:KE37": "Kenya - Samburu", + "KE:KE38": "Kenya - Siaya", + "KE:KE39": "Kenya - Taita-Taveta", + "KE:KE40": "Kenya - Tana River", + "KE:KE41": "Kenya - Tharaka-Nithi", + "KE:KE42": "Kenya - Trans Nzoia", + "KE:KE43": "Kenya - Turkana", + "KE:KE44": "Kenya - Uasin Gishu", + "KE:KE45": "Kenya - Vihiga", + "KE:KE46": "Kenya - Wajir", + "KE:KE47": "Kenya - West Pokot", + "KI": "Kiribati", + "KW": "Kuwait", + "KG": "Kyrgyzstan", + "LA:AT": "Laos - Attapeu", + "LA:BK": "Laos - Bokeo", + "LA:BL": "Laos - Bolikhamsai", + "LA:CH": "Laos - Champasak", + "LA:HO": "Laos - Houaphanh", + "LA:KH": "Laos - Khammouane", + "LA:LM": "Laos - Luang Namtha", + "LA:LP": "Laos - Luang Prabang", + "LA:OU": "Laos - Oudomxay", + "LA:PH": "Laos - Phongsaly", + "LA:SL": "Laos - Salavan", + "LA:SV": "Laos - Savannakhet", + "LA:VI": "Laos - Vientiane Province", + "LA:VT": "Laos - Vientiane", + "LA:XA": "Laos - Sainyabuli", + "LA:XE": "Laos - Sekong", + "LA:XI": "Laos - Xiangkhouang", + "LA:XS": "Laos - Xaisomboun", + "LV": "Latvia", + "LB": "Lebanon", + "LS": "Lesotho", + "LR:BM": "Liberia - Bomi", + "LR:BN": "Liberia - Bong", + "LR:GA": "Liberia - Gbarpolu", + "LR:GB": "Liberia - Grand Bassa", + "LR:GC": "Liberia - Grand Cape Mount", + "LR:GG": "Liberia - Grand Gedeh", + "LR:GK": "Liberia - Grand Kru", + "LR:LO": "Liberia - Lofa", + "LR:MA": "Liberia - Margibi", + "LR:MY": "Liberia - Maryland", + "LR:MO": "Liberia - Montserrado", + "LR:NM": "Liberia - Nimba", + "LR:RV": "Liberia - Rivercess", + "LR:RG": "Liberia - River Gee", + "LR:SN": "Liberia - Sinoe", + "LY": "Libya", + "LI": "Liechtenstein", + "LT": "Lithuania", + "LU": "Luxembourg", + "MO": "Macao", + "MG": "Madagascar", + "MW": "Malawi", + "MY:JHR": "Malaysia - Johor", + "MY:KDH": "Malaysia - Kedah", + "MY:KTN": "Malaysia - Kelantan", + "MY:LBN": "Malaysia - Labuan", + "MY:MLK": "Malaysia - Malacca (Melaka)", + "MY:NSN": "Malaysia - Negeri Sembilan", + "MY:PHG": "Malaysia - Pahang", + "MY:PNG": "Malaysia - Penang (Pulau Pinang)", + "MY:PRK": "Malaysia - Perak", + "MY:PLS": "Malaysia - Perlis", + "MY:SBH": "Malaysia - Sabah", + "MY:SWK": "Malaysia - Sarawak", + "MY:SGR": "Malaysia - Selangor", + "MY:TRG": "Malaysia - Terengganu", + "MY:PJY": "Malaysia - Putrajaya", + "MY:KUL": "Malaysia - Kuala Lumpur", + "MV": "Maldives", + "ML": "Mali", + "MT": "Malta", + "MH": "Marshall Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MU": "Mauritius", + "YT": "Mayotte", + "MX:DF": "Mexico - Ciudad de México", + "MX:JA": "Mexico - Jalisco", + "MX:NL": "Mexico - Nuevo León", + "MX:AG": "Mexico - Aguascalientes", + "MX:BC": "Mexico - Baja California", + "MX:BS": "Mexico - Baja California Sur", + "MX:CM": "Mexico - Campeche", + "MX:CS": "Mexico - Chiapas", + "MX:CH": "Mexico - Chihuahua", + "MX:CO": "Mexico - Coahuila", + "MX:CL": "Mexico - Colima", + "MX:DG": "Mexico - Durango", + "MX:GT": "Mexico - Guanajuato", + "MX:GR": "Mexico - Guerrero", + "MX:HG": "Mexico - Hidalgo", + "MX:MX": "Mexico - Estado de México", + "MX:MI": "Mexico - Michoacán", + "MX:MO": "Mexico - Morelos", + "MX:NA": "Mexico - Nayarit", + "MX:OA": "Mexico - Oaxaca", + "MX:PU": "Mexico - Puebla", + "MX:QT": "Mexico - Querétaro", + "MX:QR": "Mexico - Quintana Roo", + "MX:SL": "Mexico - San Luis Potosí", + "MX:SI": "Mexico - Sinaloa", + "MX:SO": "Mexico - Sonora", + "MX:TB": "Mexico - Tabasco", + "MX:TM": "Mexico - Tamaulipas", + "MX:TL": "Mexico - Tlaxcala", + "MX:VE": "Mexico - Veracruz", + "MX:YU": "Mexico - Yucatán", + "MX:ZA": "Mexico - Zacatecas", + "FM": "Micronesia", + "MD:C": "Moldova - Chișinău", + "MD:BL": "Moldova - Bălți", + "MD:AN": "Moldova - Anenii Noi", + "MD:BS": "Moldova - Basarabeasca", + "MD:BR": "Moldova - Briceni", + "MD:CH": "Moldova - Cahul", + "MD:CT": "Moldova - Cantemir", + "MD:CL": "Moldova - Călărași", + "MD:CS": "Moldova - Căușeni", + "MD:CM": "Moldova - Cimișlia", + "MD:CR": "Moldova - Criuleni", + "MD:DN": "Moldova - Dondușeni", + "MD:DR": "Moldova - Drochia", + "MD:DB": "Moldova - Dubăsari", + "MD:ED": "Moldova - Edineț", + "MD:FL": "Moldova - Fălești", + "MD:FR": "Moldova - Florești", + "MD:GE": "Moldova - UTA Găgăuzia", + "MD:GL": "Moldova - Glodeni", + "MD:HN": "Moldova - Hîncești", + "MD:IL": "Moldova - Ialoveni", + "MD:LV": "Moldova - Leova", + "MD:NS": "Moldova - Nisporeni", + "MD:OC": "Moldova - Ocnița", + "MD:OR": "Moldova - Orhei", + "MD:RZ": "Moldova - Rezina", + "MD:RS": "Moldova - Rîșcani", + "MD:SG": "Moldova - Sîngerei", + "MD:SR": "Moldova - Soroca", + "MD:ST": "Moldova - Strășeni", + "MD:SD": "Moldova - Șoldănești", + "MD:SV": "Moldova - Ștefan Vodă", + "MD:TR": "Moldova - Taraclia", + "MD:TL": "Moldova - Telenești", + "MD:UN": "Moldova - Ungheni", + "MC": "Monaco", + "MN": "Mongolia", + "ME": "Montenegro", + "MS": "Montserrat", + "MA": "Morocco", + "MZ:MZP": "Mozambique - Cabo Delgado", + "MZ:MZG": "Mozambique - Gaza", + "MZ:MZI": "Mozambique - Inhambane", + "MZ:MZB": "Mozambique - Manica", + "MZ:MZL": "Mozambique - Maputo Province", + "MZ:MZMPM": "Mozambique - Maputo", + "MZ:MZN": "Mozambique - Nampula", + "MZ:MZA": "Mozambique - Niassa", + "MZ:MZS": "Mozambique - Sofala", + "MZ:MZT": "Mozambique - Tete", + "MZ:MZQ": "Mozambique - Zambézia", + "MM": "Myanmar", + "NA:ER": "Namibia - Erongo", + "NA:HA": "Namibia - Hardap", + "NA:KA": "Namibia - Karas", + "NA:KE": "Namibia - Kavango East", + "NA:KW": "Namibia - Kavango West", + "NA:KH": "Namibia - Khomas", + "NA:KU": "Namibia - Kunene", + "NA:OW": "Namibia - Ohangwena", + "NA:OH": "Namibia - Omaheke", + "NA:OS": "Namibia - Omusati", + "NA:ON": "Namibia - Oshana", + "NA:OT": "Namibia - Oshikoto", + "NA:OD": "Namibia - Otjozondjupa", + "NA:CA": "Namibia - Zambezi", + "NR": "Nauru", + "NP:BAG": "Nepal - Bagmati", + "NP:BHE": "Nepal - Bheri", + "NP:DHA": "Nepal - Dhaulagiri", + "NP:GAN": "Nepal - Gandaki", + "NP:JAN": "Nepal - Janakpur", + "NP:KAR": "Nepal - Karnali", + "NP:KOS": "Nepal - Koshi", + "NP:LUM": "Nepal - Lumbini", + "NP:MAH": "Nepal - Mahakali", + "NP:MEC": "Nepal - Mechi", + "NP:NAR": "Nepal - Narayani", + "NP:RAP": "Nepal - Rapti", + "NP:SAG": "Nepal - Sagarmatha", + "NP:SET": "Nepal - Seti", + "NL": "Netherlands", + "NC": "New Caledonia", + "NZ:NTL": "New Zealand - Northland", + "NZ:AUK": "New Zealand - Auckland", + "NZ:WKO": "New Zealand - Waikato", + "NZ:BOP": "New Zealand - Bay of Plenty", + "NZ:TKI": "New Zealand - Taranaki", + "NZ:GIS": "New Zealand - Gisborne", + "NZ:HKB": "New Zealand - Hawke’s Bay", + "NZ:MWT": "New Zealand - Manawatu-Wanganui", + "NZ:WGN": "New Zealand - Wellington", + "NZ:NSN": "New Zealand - Nelson", + "NZ:MBH": "New Zealand - Marlborough", + "NZ:TAS": "New Zealand - Tasman", + "NZ:WTC": "New Zealand - West Coast", + "NZ:CAN": "New Zealand - Canterbury", + "NZ:OTA": "New Zealand - Otago", + "NZ:STL": "New Zealand - Southland", + "NI:NI-AN": "Nicaragua - Atlántico Norte", + "NI:NI-AS": "Nicaragua - Atlántico Sur", + "NI:NI-BO": "Nicaragua - Boaco", + "NI:NI-CA": "Nicaragua - Carazo", + "NI:NI-CI": "Nicaragua - Chinandega", + "NI:NI-CO": "Nicaragua - Chontales", + "NI:NI-ES": "Nicaragua - Estelí", + "NI:NI-GR": "Nicaragua - Granada", + "NI:NI-JI": "Nicaragua - Jinotega", + "NI:NI-LE": "Nicaragua - León", + "NI:NI-MD": "Nicaragua - Madriz", + "NI:NI-MN": "Nicaragua - Managua", + "NI:NI-MS": "Nicaragua - Masaya", + "NI:NI-MT": "Nicaragua - Matagalpa", + "NI:NI-NS": "Nicaragua - Nueva Segovia", + "NI:NI-RI": "Nicaragua - Rivas", + "NI:NI-SJ": "Nicaragua - Río San Juan", + "NE": "Niger", + "NG:AB": "Nigeria - Abia", + "NG:FC": "Nigeria - Abuja", + "NG:AD": "Nigeria - Adamawa", + "NG:AK": "Nigeria - Akwa Ibom", + "NG:AN": "Nigeria - Anambra", + "NG:BA": "Nigeria - Bauchi", + "NG:BY": "Nigeria - Bayelsa", + "NG:BE": "Nigeria - Benue", + "NG:BO": "Nigeria - Borno", + "NG:CR": "Nigeria - Cross River", + "NG:DE": "Nigeria - Delta", + "NG:EB": "Nigeria - Ebonyi", + "NG:ED": "Nigeria - Edo", + "NG:EK": "Nigeria - Ekiti", + "NG:EN": "Nigeria - Enugu", + "NG:GO": "Nigeria - Gombe", + "NG:IM": "Nigeria - Imo", + "NG:JI": "Nigeria - Jigawa", + "NG:KD": "Nigeria - Kaduna", + "NG:KN": "Nigeria - Kano", + "NG:KT": "Nigeria - Katsina", + "NG:KE": "Nigeria - Kebbi", + "NG:KO": "Nigeria - Kogi", + "NG:KW": "Nigeria - Kwara", + "NG:LA": "Nigeria - Lagos", + "NG:NA": "Nigeria - Nasarawa", + "NG:NI": "Nigeria - Niger", + "NG:OG": "Nigeria - Ogun", + "NG:ON": "Nigeria - Ondo", + "NG:OS": "Nigeria - Osun", + "NG:OY": "Nigeria - Oyo", + "NG:PL": "Nigeria - Plateau", + "NG:RI": "Nigeria - Rivers", + "NG:SO": "Nigeria - Sokoto", + "NG:TA": "Nigeria - Taraba", + "NG:YO": "Nigeria - Yobe", + "NG:ZA": "Nigeria - Zamfara", + "NU": "Niue", + "NF": "Norfolk Island", + "KP": "North Korea", + "MK": "North Macedonia", + "MP": "Northern Mariana Islands", + "NO": "Norway", + "OM": "Oman", + "PK:JK": "Pakistan - Azad Kashmir", + "PK:BA": "Pakistan - Balochistan", + "PK:TA": "Pakistan - FATA", + "PK:GB": "Pakistan - Gilgit Baltistan", + "PK:IS": "Pakistan - Islamabad Capital Territory", + "PK:KP": "Pakistan - Khyber Pakhtunkhwa", + "PK:PB": "Pakistan - Punjab", + "PK:SD": "Pakistan - Sindh", + "PS": "Palestinian Territory", + "PA:PA-1": "Panama - Bocas del Toro", + "PA:PA-2": "Panama - Coclé", + "PA:PA-3": "Panama - Colón", + "PA:PA-4": "Panama - Chiriquí", + "PA:PA-5": "Panama - Darién", + "PA:PA-6": "Panama - Herrera", + "PA:PA-7": "Panama - Los Santos", + "PA:PA-8": "Panama - Panamá", + "PA:PA-9": "Panama - Veraguas", + "PA:PA-10": "Panama - West Panamá", + "PA:PA-EM": "Panama - Emberá", + "PA:PA-KY": "Panama - Guna Yala", + "PA:PA-NB": "Panama - Ngöbe-Buglé", + "PG": "Papua New Guinea", + "PY:PY-ASU": "Paraguay - Asunción", + "PY:PY-1": "Paraguay - Concepción", + "PY:PY-2": "Paraguay - San Pedro", + "PY:PY-3": "Paraguay - Cordillera", + "PY:PY-4": "Paraguay - Guairá", + "PY:PY-5": "Paraguay - Caaguazú", + "PY:PY-6": "Paraguay - Caazapá", + "PY:PY-7": "Paraguay - Itapúa", + "PY:PY-8": "Paraguay - Misiones", + "PY:PY-9": "Paraguay - Paraguarí", + "PY:PY-10": "Paraguay - Alto Paraná", + "PY:PY-11": "Paraguay - Central", + "PY:PY-12": "Paraguay - Ñeembucú", + "PY:PY-13": "Paraguay - Amambay", + "PY:PY-14": "Paraguay - Canindeyú", + "PY:PY-15": "Paraguay - Presidente Hayes", + "PY:PY-16": "Paraguay - Alto Paraguay", + "PY:PY-17": "Paraguay - Boquerón", + "PE:CAL": "Peru - El Callao", + "PE:LMA": "Peru - Municipalidad Metropolitana de Lima", + "PE:AMA": "Peru - Amazonas", + "PE:ANC": "Peru - Ancash", + "PE:APU": "Peru - Apurímac", + "PE:ARE": "Peru - Arequipa", + "PE:AYA": "Peru - Ayacucho", + "PE:CAJ": "Peru - Cajamarca", + "PE:CUS": "Peru - Cusco", + "PE:HUV": "Peru - Huancavelica", + "PE:HUC": "Peru - Huánuco", + "PE:ICA": "Peru - Ica", + "PE:JUN": "Peru - Junín", + "PE:LAL": "Peru - La Libertad", + "PE:LAM": "Peru - Lambayeque", + "PE:LIM": "Peru - Lima", + "PE:LOR": "Peru - Loreto", + "PE:MDD": "Peru - Madre de Dios", + "PE:MOQ": "Peru - Moquegua", + "PE:PAS": "Peru - Pasco", + "PE:PIU": "Peru - Piura", + "PE:PUN": "Peru - Puno", + "PE:SAM": "Peru - San Martín", + "PE:TAC": "Peru - Tacna", + "PE:TUM": "Peru - Tumbes", + "PE:UCA": "Peru - Ucayali", + "PH:ABR": "Philippines - Abra", + "PH:AGN": "Philippines - Agusan del Norte", + "PH:AGS": "Philippines - Agusan del Sur", + "PH:AKL": "Philippines - Aklan", + "PH:ALB": "Philippines - Albay", + "PH:ANT": "Philippines - Antique", + "PH:APA": "Philippines - Apayao", + "PH:AUR": "Philippines - Aurora", + "PH:BAS": "Philippines - Basilan", + "PH:BAN": "Philippines - Bataan", + "PH:BTN": "Philippines - Batanes", + "PH:BTG": "Philippines - Batangas", + "PH:BEN": "Philippines - Benguet", + "PH:BIL": "Philippines - Biliran", + "PH:BOH": "Philippines - Bohol", + "PH:BUK": "Philippines - Bukidnon", + "PH:BUL": "Philippines - Bulacan", + "PH:CAG": "Philippines - Cagayan", + "PH:CAN": "Philippines - Camarines Norte", + "PH:CAS": "Philippines - Camarines Sur", + "PH:CAM": "Philippines - Camiguin", + "PH:CAP": "Philippines - Capiz", + "PH:CAT": "Philippines - Catanduanes", + "PH:CAV": "Philippines - Cavite", + "PH:CEB": "Philippines - Cebu", + "PH:COM": "Philippines - Compostela Valley", + "PH:NCO": "Philippines - Cotabato", + "PH:DAV": "Philippines - Davao del Norte", + "PH:DAS": "Philippines - Davao del Sur", + "PH:DAC": "Philippines - Davao Occidental", + "PH:DAO": "Philippines - Davao Oriental", + "PH:DIN": "Philippines - Dinagat Islands", + "PH:EAS": "Philippines - Eastern Samar", + "PH:GUI": "Philippines - Guimaras", + "PH:IFU": "Philippines - Ifugao", + "PH:ILN": "Philippines - Ilocos Norte", + "PH:ILS": "Philippines - Ilocos Sur", + "PH:ILI": "Philippines - Iloilo", + "PH:ISA": "Philippines - Isabela", + "PH:KAL": "Philippines - Kalinga", + "PH:LUN": "Philippines - La Union", + "PH:LAG": "Philippines - Laguna", + "PH:LAN": "Philippines - Lanao del Norte", + "PH:LAS": "Philippines - Lanao del Sur", + "PH:LEY": "Philippines - Leyte", + "PH:MAG": "Philippines - Maguindanao", + "PH:MAD": "Philippines - Marinduque", + "PH:MAS": "Philippines - Masbate", + "PH:MSC": "Philippines - Misamis Occidental", + "PH:MSR": "Philippines - Misamis Oriental", + "PH:MOU": "Philippines - Mountain Province", + "PH:NEC": "Philippines - Negros Occidental", + "PH:NER": "Philippines - Negros Oriental", + "PH:NSA": "Philippines - Northern Samar", + "PH:NUE": "Philippines - Nueva Ecija", + "PH:NUV": "Philippines - Nueva Vizcaya", + "PH:MDC": "Philippines - Occidental Mindoro", + "PH:MDR": "Philippines - Oriental Mindoro", + "PH:PLW": "Philippines - Palawan", + "PH:PAM": "Philippines - Pampanga", + "PH:PAN": "Philippines - Pangasinan", + "PH:QUE": "Philippines - Quezon", + "PH:QUI": "Philippines - Quirino", + "PH:RIZ": "Philippines - Rizal", + "PH:ROM": "Philippines - Romblon", + "PH:WSA": "Philippines - Samar", + "PH:SAR": "Philippines - Sarangani", + "PH:SIQ": "Philippines - Siquijor", + "PH:SOR": "Philippines - Sorsogon", + "PH:SCO": "Philippines - South Cotabato", + "PH:SLE": "Philippines - Southern Leyte", + "PH:SUK": "Philippines - Sultan Kudarat", + "PH:SLU": "Philippines - Sulu", + "PH:SUN": "Philippines - Surigao del Norte", + "PH:SUR": "Philippines - Surigao del Sur", + "PH:TAR": "Philippines - Tarlac", + "PH:TAW": "Philippines - Tawi-Tawi", + "PH:ZMB": "Philippines - Zambales", + "PH:ZAN": "Philippines - Zamboanga del Norte", + "PH:ZAS": "Philippines - Zamboanga del Sur", + "PH:ZSI": "Philippines - Zamboanga Sibugay", + "PH:00": "Philippines - Metro Manila", + "PN": "Pitcairn", + "PL": "Poland", + "PT": "Portugal", + "PR": "Puerto Rico", + "QA": "Qatar", + "RE": "Reunion", + "RO:AB": "Romania - Alba", + "RO:AR": "Romania - Arad", + "RO:AG": "Romania - Argeș", + "RO:BC": "Romania - Bacău", + "RO:BH": "Romania - Bihor", + "RO:BN": "Romania - Bistrița-Năsăud", + "RO:BT": "Romania - Botoșani", + "RO:BR": "Romania - Brăila", + "RO:BV": "Romania - Brașov", + "RO:B": "Romania - București", + "RO:BZ": "Romania - Buzău", + "RO:CL": "Romania - Călărași", + "RO:CS": "Romania - Caraș-Severin", + "RO:CJ": "Romania - Cluj", + "RO:CT": "Romania - Constanța", + "RO:CV": "Romania - Covasna", + "RO:DB": "Romania - Dâmbovița", + "RO:DJ": "Romania - Dolj", + "RO:GL": "Romania - Galați", + "RO:GR": "Romania - Giurgiu", + "RO:GJ": "Romania - Gorj", + "RO:HR": "Romania - Harghita", + "RO:HD": "Romania - Hunedoara", + "RO:IL": "Romania - Ialomița", + "RO:IS": "Romania - Iași", + "RO:IF": "Romania - Ilfov", + "RO:MM": "Romania - Maramureș", + "RO:MH": "Romania - Mehedinți", + "RO:MS": "Romania - Mureș", + "RO:NT": "Romania - Neamț", + "RO:OT": "Romania - Olt", + "RO:PH": "Romania - Prahova", + "RO:SJ": "Romania - Sălaj", + "RO:SM": "Romania - Satu Mare", + "RO:SB": "Romania - Sibiu", + "RO:SV": "Romania - Suceava", + "RO:TR": "Romania - Teleorman", + "RO:TM": "Romania - Timiș", + "RO:TL": "Romania - Tulcea", + "RO:VL": "Romania - Vâlcea", + "RO:VS": "Romania - Vaslui", + "RO:VN": "Romania - Vrancea", + "RU": "Russia", + "RW": "Rwanda", + "ST": "São Tomé and Príncipe", + "BL": "Saint Barthélemy", + "SH": "Saint Helena", + "KN": "Saint Kitts and Nevis", + "LC": "Saint Lucia", + "SX": "Saint Martin (Dutch part)", + "MF": "Saint Martin (French part)", + "PM": "Saint Pierre and Miquelon", + "VC": "Saint Vincent and the Grenadines", + "WS": "Samoa", + "SM": "San Marino", + "SA": "Saudi Arabia", + "SN": "Senegal", + "RS:RS00": "Serbia - Belgrade", + "RS:RS14": "Serbia - Bor", + "RS:RS11": "Serbia - Braničevo", + "RS:RS02": "Serbia - Central Banat", + "RS:RS10": "Serbia - Danube", + "RS:RS23": "Serbia - Jablanica", + "RS:RS09": "Serbia - Kolubara", + "RS:RS08": "Serbia - Mačva", + "RS:RS17": "Serbia - Morava", + "RS:RS20": "Serbia - Nišava", + "RS:RS01": "Serbia - North Bačka", + "RS:RS03": "Serbia - North Banat", + "RS:RS24": "Serbia - Pčinja", + "RS:RS22": "Serbia - Pirot", + "RS:RS13": "Serbia - Pomoravlje", + "RS:RS19": "Serbia - Rasina", + "RS:RS18": "Serbia - Raška", + "RS:RS06": "Serbia - South Bačka", + "RS:RS04": "Serbia - South Banat", + "RS:RS07": "Serbia - Srem", + "RS:RS12": "Serbia - Šumadija", + "RS:RS21": "Serbia - Toplica", + "RS:RS05": "Serbia - West Bačka", + "RS:RS15": "Serbia - Zaječar", + "RS:RS16": "Serbia - Zlatibor", + "RS:RS25": "Serbia - Kosovo", + "RS:RS26": "Serbia - Peć", + "RS:RS27": "Serbia - Prizren", + "RS:RS28": "Serbia - Kosovska Mitrovica", + "RS:RS29": "Serbia - Kosovo-Pomoravlje", + "RS:RSKM": "Serbia - Kosovo-Metohija", + "RS:RSVO": "Serbia - Vojvodina", + "SC": "Seychelles", + "SL": "Sierra Leone", + "SG": "Singapore", + "SK": "Slovakia", + "SI": "Slovenia", + "SB": "Solomon Islands", + "SO": "Somalia", + "ZA:EC": "South Africa - Eastern Cape", + "ZA:FS": "South Africa - Free State", + "ZA:GP": "South Africa - Gauteng", + "ZA:KZN": "South Africa - KwaZulu-Natal", + "ZA:LP": "South Africa - Limpopo", + "ZA:MP": "South Africa - Mpumalanga", + "ZA:NC": "South Africa - Northern Cape", + "ZA:NW": "South Africa - North West", + "ZA:WC": "South Africa - Western Cape", + "GS": "South Georgia/Sandwich Islands", + "KR": "South Korea", + "SS": "South Sudan", + "ES:C": "Spain - A Coruña", + "ES:VI": "Spain - Araba/Álava", + "ES:AB": "Spain - Albacete", + "ES:A": "Spain - Alicante", + "ES:AL": "Spain - Almería", + "ES:O": "Spain - Asturias", + "ES:AV": "Spain - Ávila", + "ES:BA": "Spain - Badajoz", + "ES:PM": "Spain - Baleares", + "ES:B": "Spain - Barcelona", + "ES:BU": "Spain - Burgos", + "ES:CC": "Spain - Cáceres", + "ES:CA": "Spain - Cádiz", + "ES:S": "Spain - Cantabria", + "ES:CS": "Spain - Castellón", + "ES:CE": "Spain - Ceuta", + "ES:CR": "Spain - Ciudad Real", + "ES:CO": "Spain - Córdoba", + "ES:CU": "Spain - Cuenca", + "ES:GI": "Spain - Girona", + "ES:GR": "Spain - Granada", + "ES:GU": "Spain - Guadalajara", + "ES:SS": "Spain - Gipuzkoa", + "ES:H": "Spain - Huelva", + "ES:HU": "Spain - Huesca", + "ES:J": "Spain - Jaén", + "ES:LO": "Spain - La Rioja", + "ES:GC": "Spain - Las Palmas", + "ES:LE": "Spain - León", + "ES:L": "Spain - Lleida", + "ES:LU": "Spain - Lugo", + "ES:M": "Spain - Madrid", + "ES:MA": "Spain - Málaga", + "ES:ML": "Spain - Melilla", + "ES:MU": "Spain - Murcia", + "ES:NA": "Spain - Navarra", + "ES:OR": "Spain - Ourense", + "ES:P": "Spain - Palencia", + "ES:PO": "Spain - Pontevedra", + "ES:SA": "Spain - Salamanca", + "ES:TF": "Spain - Santa Cruz de Tenerife", + "ES:SG": "Spain - Segovia", + "ES:SE": "Spain - Sevilla", + "ES:SO": "Spain - Soria", + "ES:T": "Spain - Tarragona", + "ES:TE": "Spain - Teruel", + "ES:TO": "Spain - Toledo", + "ES:V": "Spain - Valencia", + "ES:VA": "Spain - Valladolid", + "ES:BI": "Spain - Biscay", + "ES:ZA": "Spain - Zamora", + "ES:Z": "Spain - Zaragoza", + "LK": "Sri Lanka", + "SD": "Sudan", + "SR": "Suriname", + "SJ": "Svalbard and Jan Mayen", + "SE": "Sweden", + "CH:AG": "Switzerland - Aargau", + "CH:AR": "Switzerland - Appenzell Ausserrhoden", + "CH:AI": "Switzerland - Appenzell Innerrhoden", + "CH:BL": "Switzerland - Basel-Landschaft", + "CH:BS": "Switzerland - Basel-Stadt", + "CH:BE": "Switzerland - Bern", + "CH:FR": "Switzerland - Fribourg", + "CH:GE": "Switzerland - Geneva", + "CH:GL": "Switzerland - Glarus", + "CH:GR": "Switzerland - Graubünden", + "CH:JU": "Switzerland - Jura", + "CH:LU": "Switzerland - Luzern", + "CH:NE": "Switzerland - Neuchâtel", + "CH:NW": "Switzerland - Nidwalden", + "CH:OW": "Switzerland - Obwalden", + "CH:SH": "Switzerland - Schaffhausen", + "CH:SZ": "Switzerland - Schwyz", + "CH:SO": "Switzerland - Solothurn", + "CH:SG": "Switzerland - St. Gallen", + "CH:TG": "Switzerland - Thurgau", + "CH:TI": "Switzerland - Ticino", + "CH:UR": "Switzerland - Uri", + "CH:VS": "Switzerland - Valais", + "CH:VD": "Switzerland - Vaud", + "CH:ZG": "Switzerland - Zug", + "CH:ZH": "Switzerland - Zürich", + "SY": "Syria", + "TW": "Taiwan", + "TJ": "Tajikistan", + "TZ:TZ01": "Tanzania - Arusha", + "TZ:TZ02": "Tanzania - Dar es Salaam", + "TZ:TZ03": "Tanzania - Dodoma", + "TZ:TZ04": "Tanzania - Iringa", + "TZ:TZ05": "Tanzania - Kagera", + "TZ:TZ06": "Tanzania - Pemba North", + "TZ:TZ07": "Tanzania - Zanzibar North", + "TZ:TZ08": "Tanzania - Kigoma", + "TZ:TZ09": "Tanzania - Kilimanjaro", + "TZ:TZ10": "Tanzania - Pemba South", + "TZ:TZ11": "Tanzania - Zanzibar South", + "TZ:TZ12": "Tanzania - Lindi", + "TZ:TZ13": "Tanzania - Mara", + "TZ:TZ14": "Tanzania - Mbeya", + "TZ:TZ15": "Tanzania - Zanzibar West", + "TZ:TZ16": "Tanzania - Morogoro", + "TZ:TZ17": "Tanzania - Mtwara", + "TZ:TZ18": "Tanzania - Mwanza", + "TZ:TZ19": "Tanzania - Coast", + "TZ:TZ20": "Tanzania - Rukwa", + "TZ:TZ21": "Tanzania - Ruvuma", + "TZ:TZ22": "Tanzania - Shinyanga", + "TZ:TZ23": "Tanzania - Singida", + "TZ:TZ24": "Tanzania - Tabora", + "TZ:TZ25": "Tanzania - Tanga", + "TZ:TZ26": "Tanzania - Manyara", + "TZ:TZ27": "Tanzania - Geita", + "TZ:TZ28": "Tanzania - Katavi", + "TZ:TZ29": "Tanzania - Njombe", + "TZ:TZ30": "Tanzania - Simiyu", + "TH:TH-37": "Thailand - Amnat Charoen", + "TH:TH-15": "Thailand - Ang Thong", + "TH:TH-14": "Thailand - Ayutthaya", + "TH:TH-10": "Thailand - Bangkok", + "TH:TH-38": "Thailand - Bueng Kan", + "TH:TH-31": "Thailand - Buri Ram", + "TH:TH-24": "Thailand - Chachoengsao", + "TH:TH-18": "Thailand - Chai Nat", + "TH:TH-36": "Thailand - Chaiyaphum", + "TH:TH-22": "Thailand - Chanthaburi", + "TH:TH-50": "Thailand - Chiang Mai", + "TH:TH-57": "Thailand - Chiang Rai", + "TH:TH-20": "Thailand - Chonburi", + "TH:TH-86": "Thailand - Chumphon", + "TH:TH-46": "Thailand - Kalasin", + "TH:TH-62": "Thailand - Kamphaeng Phet", + "TH:TH-71": "Thailand - Kanchanaburi", + "TH:TH-40": "Thailand - Khon Kaen", + "TH:TH-81": "Thailand - Krabi", + "TH:TH-52": "Thailand - Lampang", + "TH:TH-51": "Thailand - Lamphun", + "TH:TH-42": "Thailand - Loei", + "TH:TH-16": "Thailand - Lopburi", + "TH:TH-58": "Thailand - Mae Hong Son", + "TH:TH-44": "Thailand - Maha Sarakham", + "TH:TH-49": "Thailand - Mukdahan", + "TH:TH-26": "Thailand - Nakhon Nayok", + "TH:TH-73": "Thailand - Nakhon Pathom", + "TH:TH-48": "Thailand - Nakhon Phanom", + "TH:TH-30": "Thailand - Nakhon Ratchasima", + "TH:TH-60": "Thailand - Nakhon Sawan", + "TH:TH-80": "Thailand - Nakhon Si Thammarat", + "TH:TH-55": "Thailand - Nan", + "TH:TH-96": "Thailand - Narathiwat", + "TH:TH-39": "Thailand - Nong Bua Lam Phu", + "TH:TH-43": "Thailand - Nong Khai", + "TH:TH-12": "Thailand - Nonthaburi", + "TH:TH-13": "Thailand - Pathum Thani", + "TH:TH-94": "Thailand - Pattani", + "TH:TH-82": "Thailand - Phang Nga", + "TH:TH-93": "Thailand - Phatthalung", + "TH:TH-56": "Thailand - Phayao", + "TH:TH-67": "Thailand - Phetchabun", + "TH:TH-76": "Thailand - Phetchaburi", + "TH:TH-66": "Thailand - Phichit", + "TH:TH-65": "Thailand - Phitsanulok", + "TH:TH-54": "Thailand - Phrae", + "TH:TH-83": "Thailand - Phuket", + "TH:TH-25": "Thailand - Prachin Buri", + "TH:TH-77": "Thailand - Prachuap Khiri Khan", + "TH:TH-85": "Thailand - Ranong", + "TH:TH-70": "Thailand - Ratchaburi", + "TH:TH-21": "Thailand - Rayong", + "TH:TH-45": "Thailand - Roi Et", + "TH:TH-27": "Thailand - Sa Kaeo", + "TH:TH-47": "Thailand - Sakon Nakhon", + "TH:TH-11": "Thailand - Samut Prakan", + "TH:TH-74": "Thailand - Samut Sakhon", + "TH:TH-75": "Thailand - Samut Songkhram", + "TH:TH-19": "Thailand - Saraburi", + "TH:TH-91": "Thailand - Satun", + "TH:TH-17": "Thailand - Sing Buri", + "TH:TH-33": "Thailand - Sisaket", + "TH:TH-90": "Thailand - Songkhla", + "TH:TH-64": "Thailand - Sukhothai", + "TH:TH-72": "Thailand - Suphan Buri", + "TH:TH-84": "Thailand - Surat Thani", + "TH:TH-32": "Thailand - Surin", + "TH:TH-63": "Thailand - Tak", + "TH:TH-92": "Thailand - Trang", + "TH:TH-23": "Thailand - Trat", + "TH:TH-34": "Thailand - Ubon Ratchathani", + "TH:TH-41": "Thailand - Udon Thani", + "TH:TH-61": "Thailand - Uthai Thani", + "TH:TH-53": "Thailand - Uttaradit", + "TH:TH-95": "Thailand - Yala", + "TH:TH-35": "Thailand - Yasothon", + "TL": "Timor-Leste", + "TG": "Togo", + "TK": "Tokelau", + "TO": "Tonga", + "TT": "Trinidad and Tobago", + "TN": "Tunisia", + "TR:TR01": "Turkey - Adana", + "TR:TR02": "Turkey - Adıyaman", + "TR:TR03": "Turkey - Afyon", + "TR:TR04": "Turkey - Ağrı", + "TR:TR05": "Turkey - Amasya", + "TR:TR06": "Turkey - Ankara", + "TR:TR07": "Turkey - Antalya", + "TR:TR08": "Turkey - Artvin", + "TR:TR09": "Turkey - Aydın", + "TR:TR10": "Turkey - Balıkesir", + "TR:TR11": "Turkey - Bilecik", + "TR:TR12": "Turkey - Bingöl", + "TR:TR13": "Turkey - Bitlis", + "TR:TR14": "Turkey - Bolu", + "TR:TR15": "Turkey - Burdur", + "TR:TR16": "Turkey - Bursa", + "TR:TR17": "Turkey - Çanakkale", + "TR:TR18": "Turkey - Çankırı", + "TR:TR19": "Turkey - Çorum", + "TR:TR20": "Turkey - Denizli", + "TR:TR21": "Turkey - Diyarbakır", + "TR:TR22": "Turkey - Edirne", + "TR:TR23": "Turkey - Elazığ", + "TR:TR24": "Turkey - Erzincan", + "TR:TR25": "Turkey - Erzurum", + "TR:TR26": "Turkey - Eskişehir", + "TR:TR27": "Turkey - Gaziantep", + "TR:TR28": "Turkey - Giresun", + "TR:TR29": "Turkey - Gümüşhane", + "TR:TR30": "Turkey - Hakkari", + "TR:TR31": "Turkey - Hatay", + "TR:TR32": "Turkey - Isparta", + "TR:TR33": "Turkey - İçel", + "TR:TR34": "Turkey - İstanbul", + "TR:TR35": "Turkey - İzmir", + "TR:TR36": "Turkey - Kars", + "TR:TR37": "Turkey - Kastamonu", + "TR:TR38": "Turkey - Kayseri", + "TR:TR39": "Turkey - Kırklareli", + "TR:TR40": "Turkey - Kırşehir", + "TR:TR41": "Turkey - Kocaeli", + "TR:TR42": "Turkey - Konya", + "TR:TR43": "Turkey - Kütahya", + "TR:TR44": "Turkey - Malatya", + "TR:TR45": "Turkey - Manisa", + "TR:TR46": "Turkey - Kahramanmaraş", + "TR:TR47": "Turkey - Mardin", + "TR:TR48": "Turkey - Muğla", + "TR:TR49": "Turkey - Muş", + "TR:TR50": "Turkey - Nevşehir", + "TR:TR51": "Turkey - Niğde", + "TR:TR52": "Turkey - Ordu", + "TR:TR53": "Turkey - Rize", + "TR:TR54": "Turkey - Sakarya", + "TR:TR55": "Turkey - Samsun", + "TR:TR56": "Turkey - Siirt", + "TR:TR57": "Turkey - Sinop", + "TR:TR58": "Turkey - Sivas", + "TR:TR59": "Turkey - Tekirdağ", + "TR:TR60": "Turkey - Tokat", + "TR:TR61": "Turkey - Trabzon", + "TR:TR62": "Turkey - Tunceli", + "TR:TR63": "Turkey - Şanlıurfa", + "TR:TR64": "Turkey - Uşak", + "TR:TR65": "Turkey - Van", + "TR:TR66": "Turkey - Yozgat", + "TR:TR67": "Turkey - Zonguldak", + "TR:TR68": "Turkey - Aksaray", + "TR:TR69": "Turkey - Bayburt", + "TR:TR70": "Turkey - Karaman", + "TR:TR71": "Turkey - Kırıkkale", + "TR:TR72": "Turkey - Batman", + "TR:TR73": "Turkey - Şırnak", + "TR:TR74": "Turkey - Bartın", + "TR:TR75": "Turkey - Ardahan", + "TR:TR76": "Turkey - Iğdır", + "TR:TR77": "Turkey - Yalova", + "TR:TR78": "Turkey - Karabük", + "TR:TR79": "Turkey - Kilis", + "TR:TR80": "Turkey - Osmaniye", + "TR:TR81": "Turkey - Düzce", + "TM": "Turkmenistan", + "TC": "Turks and Caicos Islands", + "TV": "Tuvalu", + "UG:UG314": "Uganda - Abim", + "UG:UG301": "Uganda - Adjumani", + "UG:UG322": "Uganda - Agago", + "UG:UG323": "Uganda - Alebtong", + "UG:UG315": "Uganda - Amolatar", + "UG:UG324": "Uganda - Amudat", + "UG:UG216": "Uganda - Amuria", + "UG:UG316": "Uganda - Amuru", + "UG:UG302": "Uganda - Apac", + "UG:UG303": "Uganda - Arua", + "UG:UG217": "Uganda - Budaka", + "UG:UG218": "Uganda - Bududa", + "UG:UG201": "Uganda - Bugiri", + "UG:UG235": "Uganda - Bugweri", + "UG:UG420": "Uganda - Buhweju", + "UG:UG117": "Uganda - Buikwe", + "UG:UG219": "Uganda - Bukedea", + "UG:UG118": "Uganda - Bukomansimbi", + "UG:UG220": "Uganda - Bukwa", + "UG:UG225": "Uganda - Bulambuli", + "UG:UG416": "Uganda - Buliisa", + "UG:UG401": "Uganda - Bundibugyo", + "UG:UG430": "Uganda - Bunyangabu", + "UG:UG402": "Uganda - Bushenyi", + "UG:UG202": "Uganda - Busia", + "UG:UG221": "Uganda - Butaleja", + "UG:UG119": "Uganda - Butambala", + "UG:UG233": "Uganda - Butebo", + "UG:UG120": "Uganda - Buvuma", + "UG:UG226": "Uganda - Buyende", + "UG:UG317": "Uganda - Dokolo", + "UG:UG121": "Uganda - Gomba", + "UG:UG304": "Uganda - Gulu", + "UG:UG403": "Uganda - Hoima", + "UG:UG417": "Uganda - Ibanda", + "UG:UG203": "Uganda - Iganga", + "UG:UG418": "Uganda - Isingiro", + "UG:UG204": "Uganda - Jinja", + "UG:UG318": "Uganda - Kaabong", + "UG:UG404": "Uganda - Kabale", + "UG:UG405": "Uganda - Kabarole", + "UG:UG213": "Uganda - Kaberamaido", + "UG:UG427": "Uganda - Kagadi", + "UG:UG428": "Uganda - Kakumiro", + "UG:UG101": "Uganda - Kalangala", + "UG:UG222": "Uganda - Kaliro", + "UG:UG122": "Uganda - Kalungu", + "UG:UG102": "Uganda - Kampala", + "UG:UG205": "Uganda - Kamuli", + "UG:UG413": "Uganda - Kamwenge", + "UG:UG414": "Uganda - Kanungu", + "UG:UG206": "Uganda - Kapchorwa", + "UG:UG236": "Uganda - Kapelebyong", + "UG:UG126": "Uganda - Kasanda", + "UG:UG406": "Uganda - Kasese", + "UG:UG207": "Uganda - Katakwi", + "UG:UG112": "Uganda - Kayunga", + "UG:UG407": "Uganda - Kibaale", + "UG:UG103": "Uganda - Kiboga", + "UG:UG227": "Uganda - Kibuku", + "UG:UG432": "Uganda - Kikuube", + "UG:UG419": "Uganda - Kiruhura", + "UG:UG421": "Uganda - Kiryandongo", + "UG:UG408": "Uganda - Kisoro", + "UG:UG305": "Uganda - Kitgum", + "UG:UG319": "Uganda - Koboko", + "UG:UG325": "Uganda - Kole", + "UG:UG306": "Uganda - Kotido", + "UG:UG208": "Uganda - Kumi", + "UG:UG333": "Uganda - Kwania", + "UG:UG228": "Uganda - Kween", + "UG:UG123": "Uganda - Kyankwanzi", + "UG:UG422": "Uganda - Kyegegwa", + "UG:UG415": "Uganda - Kyenjojo", + "UG:UG125": "Uganda - Kyotera", + "UG:UG326": "Uganda - Lamwo", + "UG:UG307": "Uganda - Lira", + "UG:UG229": "Uganda - Luuka", + "UG:UG104": "Uganda - Luwero", + "UG:UG124": "Uganda - Lwengo", + "UG:UG114": "Uganda - Lyantonde", + "UG:UG223": "Uganda - Manafwa", + "UG:UG320": "Uganda - Maracha", + "UG:UG105": "Uganda - Masaka", + "UG:UG409": "Uganda - Masindi", + "UG:UG214": "Uganda - Mayuge", + "UG:UG209": "Uganda - Mbale", + "UG:UG410": "Uganda - Mbarara", + "UG:UG423": "Uganda - Mitooma", + "UG:UG115": "Uganda - Mityana", + "UG:UG308": "Uganda - Moroto", + "UG:UG309": "Uganda - Moyo", + "UG:UG106": "Uganda - Mpigi", + "UG:UG107": "Uganda - Mubende", + "UG:UG108": "Uganda - Mukono", + "UG:UG334": "Uganda - Nabilatuk", + "UG:UG311": "Uganda - Nakapiripirit", + "UG:UG116": "Uganda - Nakaseke", + "UG:UG109": "Uganda - Nakasongola", + "UG:UG230": "Uganda - Namayingo", + "UG:UG234": "Uganda - Namisindwa", + "UG:UG224": "Uganda - Namutumba", + "UG:UG327": "Uganda - Napak", + "UG:UG310": "Uganda - Nebbi", + "UG:UG231": "Uganda - Ngora", + "UG:UG424": "Uganda - Ntoroko", + "UG:UG411": "Uganda - Ntungamo", + "UG:UG328": "Uganda - Nwoya", + "UG:UG331": "Uganda - Omoro", + "UG:UG329": "Uganda - Otuke", + "UG:UG321": "Uganda - Oyam", + "UG:UG312": "Uganda - Pader", + "UG:UG332": "Uganda - Pakwach", + "UG:UG210": "Uganda - Pallisa", + "UG:UG110": "Uganda - Rakai", + "UG:UG429": "Uganda - Rubanda", + "UG:UG425": "Uganda - Rubirizi", + "UG:UG431": "Uganda - Rukiga", + "UG:UG412": "Uganda - Rukungiri", + "UG:UG111": "Uganda - Sembabule", + "UG:UG232": "Uganda - Serere", + "UG:UG426": "Uganda - Sheema", + "UG:UG215": "Uganda - Sironko", + "UG:UG211": "Uganda - Soroti", + "UG:UG212": "Uganda - Tororo", + "UG:UG113": "Uganda - Wakiso", + "UG:UG313": "Uganda - Yumbe", + "UG:UG330": "Uganda - Zombo", + "UA:VN": "Ukraine - Vinnytsia Oblast", + "UA:VL": "Ukraine - Volyn Oblast", + "UA:DP": "Ukraine - Dnipropetrovsk Oblast", + "UA:DT": "Ukraine - Donetsk Oblast", + "UA:ZT": "Ukraine - Zhytomyr Oblast", + "UA:ZK": "Ukraine - Zakarpattia Oblast", + "UA:ZP": "Ukraine - Zaporizhzhia Oblast", + "UA:IF": "Ukraine - Ivano-Frankivsk Oblast", + "UA:KV": "Ukraine - Kyiv Oblast", + "UA:KH": "Ukraine - Kirovohrad Oblast", + "UA:LH": "Ukraine - Luhansk Oblast", + "UA:LV": "Ukraine - Lviv Oblast", + "UA:MY": "Ukraine - Mykolaiv Oblast", + "UA:OD": "Ukraine - Odessa Oblast", + "UA:PL": "Ukraine - Poltava Oblast", + "UA:RV": "Ukraine - Rivne Oblast", + "UA:SM": "Ukraine - Sumy Oblast", + "UA:TP": "Ukraine - Ternopil Oblast", + "UA:KK": "Ukraine - Kharkiv Oblast", + "UA:KS": "Ukraine - Kherson Oblast", + "UA:KM": "Ukraine - Khmelnytskyi Oblast", + "UA:CK": "Ukraine - Cherkasy Oblast", + "UA:CH": "Ukraine - Chernihiv Oblast", + "UA:CV": "Ukraine - Chernivtsi Oblast", + "AE": "United Arab Emirates", + "GB": "United Kingdom (UK)", + "US:AL": "United States (US) - Alabama", + "US:AK": "United States (US) - Alaska", + "US:AZ": "United States (US) - Arizona", + "US:AR": "United States (US) - Arkansas", + "US:CA": "United States (US) - California", + "US:CO": "United States (US) - Colorado", + "US:CT": "United States (US) - Connecticut", + "US:DE": "United States (US) - Delaware", + "US:DC": "United States (US) - District Of Columbia", + "US:FL": "United States (US) - Florida", + "US:GA": "United States (US) - Georgia", + "US:HI": "United States (US) - Hawaii", + "US:ID": "United States (US) - Idaho", + "US:IL": "United States (US) - Illinois", + "US:IN": "United States (US) - Indiana", + "US:IA": "United States (US) - Iowa", + "US:KS": "United States (US) - Kansas", + "US:KY": "United States (US) - Kentucky", + "US:LA": "United States (US) - Louisiana", + "US:ME": "United States (US) - Maine", + "US:MD": "United States (US) - Maryland", + "US:MA": "United States (US) - Massachusetts", + "US:MI": "United States (US) - Michigan", + "US:MN": "United States (US) - Minnesota", + "US:MS": "United States (US) - Mississippi", + "US:MO": "United States (US) - Missouri", + "US:MT": "United States (US) - Montana", + "US:NE": "United States (US) - Nebraska", + "US:NV": "United States (US) - Nevada", + "US:NH": "United States (US) - New Hampshire", + "US:NJ": "United States (US) - New Jersey", + "US:NM": "United States (US) - New Mexico", + "US:NY": "United States (US) - New York", + "US:NC": "United States (US) - North Carolina", + "US:ND": "United States (US) - North Dakota", + "US:OH": "United States (US) - Ohio", + "US:OK": "United States (US) - Oklahoma", + "US:OR": "United States (US) - Oregon", + "US:PA": "United States (US) - Pennsylvania", + "US:RI": "United States (US) - Rhode Island", + "US:SC": "United States (US) - South Carolina", + "US:SD": "United States (US) - South Dakota", + "US:TN": "United States (US) - Tennessee", + "US:TX": "United States (US) - Texas", + "US:UT": "United States (US) - Utah", + "US:VT": "United States (US) - Vermont", + "US:VA": "United States (US) - Virginia", + "US:WA": "United States (US) - Washington", + "US:WV": "United States (US) - West Virginia", + "US:WI": "United States (US) - Wisconsin", + "US:WY": "United States (US) - Wyoming", + "US:AA": "United States (US) - Armed Forces (AA)", + "US:AE": "United States (US) - Armed Forces (AE)", + "US:AP": "United States (US) - Armed Forces (AP)", + "UM:81": "United States (US) Minor Outlying Islands - Baker Island", + "UM:84": "United States (US) Minor Outlying Islands - Howland Island", + "UM:86": "United States (US) Minor Outlying Islands - Jarvis Island", + "UM:67": "United States (US) Minor Outlying Islands - Johnston Atoll", + "UM:89": "United States (US) Minor Outlying Islands - Kingman Reef", + "UM:71": "United States (US) Minor Outlying Islands - Midway Atoll", + "UM:76": "United States (US) Minor Outlying Islands - Navassa Island", + "UM:95": "United States (US) Minor Outlying Islands - Palmyra Atoll", + "UM:79": "United States (US) Minor Outlying Islands - Wake Island", + "UY:UY-AR": "Uruguay - Artigas", + "UY:UY-CA": "Uruguay - Canelones", + "UY:UY-CL": "Uruguay - Cerro Largo", + "UY:UY-CO": "Uruguay - Colonia", + "UY:UY-DU": "Uruguay - Durazno", + "UY:UY-FS": "Uruguay - Flores", + "UY:UY-FD": "Uruguay - Florida", + "UY:UY-LA": "Uruguay - Lavalleja", + "UY:UY-MA": "Uruguay - Maldonado", + "UY:UY-MO": "Uruguay - Montevideo", + "UY:UY-PA": "Uruguay - Paysandú", + "UY:UY-RN": "Uruguay - Río Negro", + "UY:UY-RV": "Uruguay - Rivera", + "UY:UY-RO": "Uruguay - Rocha", + "UY:UY-SA": "Uruguay - Salto", + "UY:UY-SJ": "Uruguay - San José", + "UY:UY-SO": "Uruguay - Soriano", + "UY:UY-TA": "Uruguay - Tacuarembó", + "UY:UY-TT": "Uruguay - Treinta y Tres", + "UZ": "Uzbekistan", + "VU": "Vanuatu", + "VA": "Vatican", + "VE:VE-A": "Venezuela - Capital", + "VE:VE-B": "Venezuela - Anzoátegui", + "VE:VE-C": "Venezuela - Apure", + "VE:VE-D": "Venezuela - Aragua", + "VE:VE-E": "Venezuela - Barinas", + "VE:VE-F": "Venezuela - Bolívar", + "VE:VE-G": "Venezuela - Carabobo", + "VE:VE-H": "Venezuela - Cojedes", + "VE:VE-I": "Venezuela - Falcón", + "VE:VE-J": "Venezuela - Guárico", + "VE:VE-K": "Venezuela - Lara", + "VE:VE-L": "Venezuela - Mérida", + "VE:VE-M": "Venezuela - Miranda", + "VE:VE-N": "Venezuela - Monagas", + "VE:VE-O": "Venezuela - Nueva Esparta", + "VE:VE-P": "Venezuela - Portuguesa", + "VE:VE-R": "Venezuela - Sucre", + "VE:VE-S": "Venezuela - Táchira", + "VE:VE-T": "Venezuela - Trujillo", + "VE:VE-U": "Venezuela - Yaracuy", + "VE:VE-V": "Venezuela - Zulia", + "VE:VE-W": "Venezuela - Federal Dependencies", + "VE:VE-X": "Venezuela - La Guaira (Vargas)", + "VE:VE-Y": "Venezuela - Delta Amacuro", + "VE:VE-Z": "Venezuela - Amazonas", + "VN": "Vietnam", + "VG": "Virgin Islands (British)", + "VI": "Virgin Islands (US)", + "WF": "Wallis and Futuna", + "EH": "Western Sahara", + "YE": "Yemen", + "ZM:ZM-01": "Zambia - Western", + "ZM:ZM-02": "Zambia - Central", + "ZM:ZM-03": "Zambia - Eastern", + "ZM:ZM-04": "Zambia - Luapula", + "ZM:ZM-05": "Zambia - Northern", + "ZM:ZM-06": "Zambia - North-Western", + "ZM:ZM-07": "Zambia - Southern", + "ZM:ZM-08": "Zambia - Copperbelt", + "ZM:ZM-09": "Zambia - Lusaka", + "ZM:ZM-10": "Zambia - Muchinga", + "ZW": "Zimbabwe" +}; + +const countries = { + "AF": "Afghanistan", + "AX": "Åland Islands", + "AL": "Albania", + "DZ": "Algeria", + "AS": "American Samoa", + "AD": "Andorra", + "AO": "Angola", + "AI": "Anguilla", + "AQ": "Antarctica", + "AG": "Antigua and Barbuda", + "AR": "Argentina", + "AM": "Armenia", + "AW": "Aruba", + "AU": "Australia", + "AT": "Austria", + "AZ": "Azerbaijan", + "BS": "Bahamas", + "BH": "Bahrain", + "BD": "Bangladesh", + "BB": "Barbados", + "BY": "Belarus", + "PW": "Belau", + "BE": "Belgium", + "BZ": "Belize", + "BJ": "Benin", + "BM": "Bermuda", + "BT": "Bhutan", + "BO": "Bolivia", + "BQ": "Bonaire, Saint Eustatius and Saba", + "BA": "Bosnia and Herzegovina", + "BW": "Botswana", + "BV": "Bouvet Island", + "BR": "Brazil", + "IO": "British Indian Ocean Territory", + "BN": "Brunei", + "BG": "Bulgaria", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Cambodia", + "CM": "Cameroon", + "CA": "Canada", + "CV": "Cape Verde", + "KY": "Cayman Islands", + "CF": "Central African Republic", + "TD": "Chad", + "CL": "Chile", + "CN": "China", + "CX": "Christmas Island", + "CC": "Cocos (Keeling) Islands", + "CO": "Colombia", + "KM": "Comoros", + "CG": "Congo (Brazzaville)", + "CD": "Congo (Kinshasa)", + "CK": "Cook Islands", + "CR": "Costa Rica", + "HR": "Croatia", + "CU": "Cuba", + "CW": "Curaçao", + "CY": "Cyprus", + "CZ": "Czech Republic", + "DK": "Denmark", + "DJ": "Djibouti", + "DM": "Dominica", + "DO": "Dominican Republic", + "EC": "Ecuador", + "EG": "Egypt", + "SV": "El Salvador", + "GQ": "Equatorial Guinea", + "ER": "Eritrea", + "EE": "Estonia", + "SZ": "Eswatini", + "ET": "Ethiopia", + "FK": "Falkland Islands", + "FO": "Faroe Islands", + "FJ": "Fiji", + "FI": "Finland", + "FR": "France", + "GF": "French Guiana", + "PF": "French Polynesia", + "TF": "French Southern Territories", + "GA": "Gabon", + "GM": "Gambia", + "GE": "Georgia", + "DE": "Germany", + "GH": "Ghana", + "GI": "Gibraltar", + "GR": "Greece", + "GL": "Greenland", + "GD": "Grenada", + "GP": "Guadeloupe", + "GU": "Guam", + "GT": "Guatemala", + "GG": "Guernsey", + "GN": "Guinea", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HT": "Haiti", + "HM": "Heard Island and McDonald Islands", + "HN": "Honduras", + "HK": "Hong Kong", + "HU": "Hungary", + "IS": "Iceland", + "IN": "India", + "ID": "Indonesia", + "IR": "Iran", + "IQ": "Iraq", + "IE": "Ireland", + "IM": "Isle of Man", + "IL": "Israel", + "IT": "Italy", + "CI": "Ivory Coast", + "JM": "Jamaica", + "JP": "Japan", + "JE": "Jersey", + "JO": "Jordan", + "KZ": "Kazakhstan", + "KE": "Kenya", + "KI": "Kiribati", + "KW": "Kuwait", + "KG": "Kyrgyzstan", + "LA": "Laos", + "LV": "Latvia", + "LB": "Lebanon", + "LS": "Lesotho", + "LR": "Liberia", + "LY": "Libya", + "LI": "Liechtenstein", + "LT": "Lithuania", + "LU": "Luxembourg", + "MO": "Macao", + "MG": "Madagascar", + "MW": "Malawi", + "MY": "Malaysia", + "MV": "Maldives", + "ML": "Mali", + "MT": "Malta", + "MH": "Marshall Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MU": "Mauritius", + "YT": "Mayotte", + "MX": "Mexico", + "FM": "Micronesia", + "MD": "Moldova", + "MC": "Monaco", + "MN": "Mongolia", + "ME": "Montenegro", + "MS": "Montserrat", + "MA": "Morocco", + "MZ": "Mozambique", + "MM": "Myanmar", + "NA": "Namibia", + "NR": "Nauru", + "NP": "Nepal", + "NL": "Netherlands", + "NC": "New Caledonia", + "NZ": "New Zealand", + "NI": "Nicaragua", + "NE": "Niger", + "NG": "Nigeria", + "NU": "Niue", + "NF": "Norfolk Island", + "KP": "North Korea", + "MK": "North Macedonia", + "MP": "Northern Mariana Islands", + "NO": "Norway", + "OM": "Oman", + "PK": "Pakistan", + "PS": "Palestinian Territory", + "PA": "Panama", + "PG": "Papua New Guinea", + "PY": "Paraguay", + "PE": "Peru", + "PH": "Philippines", + "PN": "Pitcairn", + "PL": "Poland", + "PT": "Portugal", + "PR": "Puerto Rico", + "QA": "Qatar", + "RE": "Reunion", + "RO": "Romania", + "RU": "Russia", + "RW": "Rwanda", + "ST": "São Tomé and Príncipe", + "BL": "Saint Barthélemy", + "SH": "Saint Helena", + "KN": "Saint Kitts and Nevis", + "LC": "Saint Lucia", + "SX": "Saint Martin (Dutch part)", + "MF": "Saint Martin (French part)", + "PM": "Saint Pierre and Miquelon", + "VC": "Saint Vincent and the Grenadines", + "WS": "Samoa", + "SM": "San Marino", + "SA": "Saudi Arabia", + "SN": "Senegal", + "RS": "Serbia", + "SC": "Seychelles", + "SL": "Sierra Leone", + "SG": "Singapore", + "SK": "Slovakia", + "SI": "Slovenia", + "SB": "Solomon Islands", + "SO": "Somalia", + "ZA": "South Africa", + "GS": "South Georgia/Sandwich Islands", + "KR": "South Korea", + "SS": "South Sudan", + "ES": "Spain", + "LK": "Sri Lanka", + "SD": "Sudan", + "SR": "Suriname", + "SJ": "Svalbard and Jan Mayen", + "SE": "Sweden", + "CH": "Switzerland", + "SY": "Syria", + "TW": "Taiwan", + "TJ": "Tajikistan", + "TZ": "Tanzania", + "TH": "Thailand", + "TL": "Timor-Leste", + "TG": "Togo", + "TK": "Tokelau", + "TO": "Tonga", + "TT": "Trinidad and Tobago", + "TN": "Tunisia", + "TR": "Turkey", + "TM": "Turkmenistan", + "TC": "Turks and Caicos Islands", + "TV": "Tuvalu", + "UG": "Uganda", + "UA": "Ukraine", + "AE": "United Arab Emirates", + "GB": "United Kingdom (UK)", + "US": "United States (US)", + "UM": "United States (US) Minor Outlying Islands", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VU": "Vanuatu", + "VA": "Vatican", + "VE": "Venezuela", + "VN": "Vietnam", + "VG": "Virgin Islands (British)", + "VI": "Virgin Islands (US)", + "WF": "Wallis and Futuna", + "EH": "Western Sahara", + "YE": "Yemen", + "ZM": "Zambia", + "ZW": "Zimbabwe" +}; + +module.exports = { + countries, + currencies, + stateOptions, +}; diff --git a/plugins/woocommerce/tests/api-core-tests/playwright.config.js b/plugins/woocommerce/tests/api-core-tests/playwright.config.js index 460a6681668..ed1ecd645b6 100644 --- a/plugins/woocommerce/tests/api-core-tests/playwright.config.js +++ b/plugins/woocommerce/tests/api-core-tests/playwright.config.js @@ -1,30 +1,27 @@ const { devices } = require( '@playwright/test' ); +const { + BASE_URL, + CI, + DEFAULT_TIMEOUT_OVERRIDE, + USER_KEY, + USER_SECRET, +} = process.env; require( 'dotenv' ).config(); -let baseURL = 'http://localhost:8086'; -let userKey = 'admin'; -let userSecret = 'password'; - -if ( process.env.BASE_URL ) { - baseURL = process.env.BASE_URL; -} - -if ( process.env.USER_KEY ) { - userKey = process.env.USER_KEY; -} - -if ( process.env.USER_SECRET ) { - userSecret = process.env.USER_SECRET; -} +const baseURL = BASE_URL ?? 'http://localhost:8086'; +const userKey = USER_KEY ?? 'admin'; +const userSecret = USER_SECRET ?? 'password'; const base64auth = btoa( `${ userKey }:${ userSecret }` ); const config = { - timeout: 90 * 1000, + timeout: DEFAULT_TIMEOUT_OVERRIDE + ? Number( DEFAULT_TIMEOUT_OVERRIDE ) + : 90 * 1000, expect: { timeout: 20 * 1000 }, outputDir: './report', testDir: 'tests', - retries: process.env.CI ? 4 : 2, + retries: CI ? 4 : 2, workers: 4, reporter: [ [ 'list' ], @@ -32,14 +29,24 @@ const config = { 'html', { outputFolder: 'output', - open: process.env.CI ? 'never' : 'always', + open: CI ? 'never' : 'always', }, ], [ 'allure-playwright', - { outputFolder: 'api-test-report/allure-results' }, + { + outputFolder: + process.env.ALLURE_RESULTS_DIR ?? + 'tests/api-core-tests/api-test-report/allure-results', + }, + ], + [ + 'json', + { + outputFile: + 'tests/api-core-tests/api-test-report/test-results.json', + }, ], - [ 'json', { outputFile: 'api-test-report/test-results.json' } ], ], use: { screenshot: 'only-on-failure', diff --git a/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js index b241e223240..6234bf7e155 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js @@ -67,7 +67,7 @@ test.describe('Customers API tests: CRUD', () => { const response = await request.get('/wp-json/wc/v3/customers'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toEqual(0); }); @@ -85,7 +85,7 @@ test.describe('Customers API tests: CRUD', () => { }); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toBeGreaterThanOrEqual(2); }); }); @@ -132,7 +132,7 @@ test.describe('Customers API tests: CRUD', () => { const response = await request.get('/wp-json/wc/v3/customers'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toBeGreaterThan(0); }); }); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js new file mode 100644 index 00000000000..fd75cca644b --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js @@ -0,0 +1,27401 @@ +const { + test, + expect +} = require('@playwright/test'); +const exp = require('constants'); +const { + refund +} = require('../../data'); + +/** + * Tests for the WooCommerce Refunds API. + * + * @group api + * @group data + * + */ +test.describe('Data API tests', () => { + + test('can list all data', async ({ + request + }) => { + // call API to retrieve data values + const response = await request.get('/wp-json/wc/v3/data'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "continents", + "description": "List of supported continents, countries, and states.", + }), + + expect.objectContaining({ + "slug": "countries", + "description": "List of supported states in a given country.", + }), + + expect.objectContaining({ + "slug": "currencies", + "description": "List of supported currencies.", + }), + + ]) + ); + }); + + test('can view all continents', async ({ + request + }) => { + // call API to retrieve all continents + const response = await request.get('/wp-json/wc/v3/data/continents'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "AF", + "name": "Africa", + "countries": [{ + "code": "AO", + "name": "Angolan kwanza", + "currency_code": "AOA", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "BGO", + "name": "Bengo" + }, + { + "code": "BLU", + "name": "Benguela" + }, + { + "code": "BIE", + "name": "Bié" + }, + { + "code": "CAB", + "name": "Cabinda" + }, + { + "code": "CNN", + "name": "Cunene" + }, + { + "code": "HUA", + "name": "Huambo" + }, + { + "code": "HUI", + "name": "Huíla" + }, + { + "code": "CCU", + "name": "Kuando Kubango" + }, + { + "code": "CNO", + "name": "Kwanza-Norte" + }, + { + "code": "CUS", + "name": "Kwanza-Sul" + }, + { + "code": "LUA", + "name": "Luanda" + }, + { + "code": "LNO", + "name": "Lunda-Norte" + }, + { + "code": "LSU", + "name": "Lunda-Sul" + }, + { + "code": "MAL", + "name": "Malanje" + }, + { + "code": "MOX", + "name": "Moxico" + }, + { + "code": "NAM", + "name": "Namibe" + }, + { + "code": "UIG", + "name": "Uíge" + }, + { + "code": "ZAI", + "name": "Zaire" + } + ] + }, + { + "code": "BF", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BI", + "name": "Burundian franc", + "currency_code": "BIF", + "currency_pos": "right", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BJ", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "AL", + "name": "Alibori" + }, + { + "code": "AK", + "name": "Atakora" + }, + { + "code": "AQ", + "name": "Atlantique" + }, + { + "code": "BO", + "name": "Borgou" + }, + { + "code": "CO", + "name": "Collines" + }, + { + "code": "KO", + "name": "Kouffo" + }, + { + "code": "DO", + "name": "Donga" + }, + { + "code": "LI", + "name": "Littoral" + }, + { + "code": "MO", + "name": "Mono" + }, + { + "code": "OU", + "name": "Ouémé" + }, + { + "code": "PL", + "name": "Plateau" + }, + { + "code": "ZO", + "name": "Zou" + } + ] + }, + { + "code": "BW", + "name": "Botswana pula", + "currency_code": "BWP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CD", + "name": "Congolese franc", + "currency_code": "CDF", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CF", + "name": "Central African CFA franc", + "currency_code": "XAF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CG", + "name": "Central African CFA franc", + "currency_code": "XAF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CI", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CM", + "name": "Central African CFA franc", + "currency_code": "XAF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CV", + "name": "Cape Verdean escudo", + "currency_code": "CVE", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "DJ", + "name": "Djiboutian franc", + "currency_code": "DJF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "DZ", + "name": "Algerian dinar", + "currency_code": "DZD", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "DZ-01", + "name": "Adrar" + }, + { + "code": "DZ-02", + "name": "Chlef" + }, + { + "code": "DZ-03", + "name": "Laghouat" + }, + { + "code": "DZ-04", + "name": "Oum El Bouaghi" + }, + { + "code": "DZ-05", + "name": "Batna" + }, + { + "code": "DZ-06", + "name": "Béjaïa" + }, + { + "code": "DZ-07", + "name": "Biskra" + }, + { + "code": "DZ-08", + "name": "Béchar" + }, + { + "code": "DZ-09", + "name": "Blida" + }, + { + "code": "DZ-10", + "name": "Bouira" + }, + { + "code": "DZ-11", + "name": "Tamanghasset" + }, + { + "code": "DZ-12", + "name": "Tébessa" + }, + { + "code": "DZ-13", + "name": "Tlemcen" + }, + { + "code": "DZ-14", + "name": "Tiaret" + }, + { + "code": "DZ-15", + "name": "Tizi Ouzou" + }, + { + "code": "DZ-16", + "name": "Algiers" + }, + { + "code": "DZ-17", + "name": "Djelfa" + }, + { + "code": "DZ-18", + "name": "Jijel" + }, + { + "code": "DZ-19", + "name": "Sétif" + }, + { + "code": "DZ-20", + "name": "Saïda" + }, + { + "code": "DZ-21", + "name": "Skikda" + }, + { + "code": "DZ-22", + "name": "Sidi Bel Abbès" + }, + { + "code": "DZ-23", + "name": "Annaba" + }, + { + "code": "DZ-24", + "name": "Guelma" + }, + { + "code": "DZ-25", + "name": "Constantine" + }, + { + "code": "DZ-26", + "name": "Médéa" + }, + { + "code": "DZ-27", + "name": "Mostaganem" + }, + { + "code": "DZ-28", + "name": "M’Sila" + }, + { + "code": "DZ-29", + "name": "Mascara" + }, + { + "code": "DZ-30", + "name": "Ouargla" + }, + { + "code": "DZ-31", + "name": "Oran" + }, + { + "code": "DZ-32", + "name": "El Bayadh" + }, + { + "code": "DZ-33", + "name": "Illizi" + }, + { + "code": "DZ-34", + "name": "Bordj Bou Arréridj" + }, + { + "code": "DZ-35", + "name": "Boumerdès" + }, + { + "code": "DZ-36", + "name": "El Tarf" + }, + { + "code": "DZ-37", + "name": "Tindouf" + }, + { + "code": "DZ-38", + "name": "Tissemsilt" + }, + { + "code": "DZ-39", + "name": "El Oued" + }, + { + "code": "DZ-40", + "name": "Khenchela" + }, + { + "code": "DZ-41", + "name": "Souk Ahras" + }, + { + "code": "DZ-42", + "name": "Tipasa" + }, + { + "code": "DZ-43", + "name": "Mila" + }, + { + "code": "DZ-44", + "name": "Aïn Defla" + }, + { + "code": "DZ-45", + "name": "Naama" + }, + { + "code": "DZ-46", + "name": "Aïn Témouchent" + }, + { + "code": "DZ-47", + "name": "Ghardaïa" + }, + { + "code": "DZ-48", + "name": "Relizane" + } + ] + }, + { + "code": "EG", + "name": "Egyptian pound", + "currency_code": "EGP", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "EGALX", + "name": "Alexandria" + }, + { + "code": "EGASN", + "name": "Aswan" + }, + { + "code": "EGAST", + "name": "Asyut" + }, + { + "code": "EGBA", + "name": "Red Sea" + }, + { + "code": "EGBH", + "name": "Beheira" + }, + { + "code": "EGBNS", + "name": "Beni Suef" + }, + { + "code": "EGC", + "name": "Cairo" + }, + { + "code": "EGDK", + "name": "Dakahlia" + }, + { + "code": "EGDT", + "name": "Damietta" + }, + { + "code": "EGFYM", + "name": "Faiyum" + }, + { + "code": "EGGH", + "name": "Gharbia" + }, + { + "code": "EGGZ", + "name": "Giza" + }, + { + "code": "EGIS", + "name": "Ismailia" + }, + { + "code": "EGJS", + "name": "South Sinai" + }, + { + "code": "EGKB", + "name": "Qalyubia" + }, + { + "code": "EGKFS", + "name": "Kafr el-Sheikh" + }, + { + "code": "EGKN", + "name": "Qena" + }, + { + "code": "EGLX", + "name": "Luxor" + }, + { + "code": "EGMN", + "name": "Minya" + }, + { + "code": "EGMNF", + "name": "Monufia" + }, + { + "code": "EGMT", + "name": "Matrouh" + }, + { + "code": "EGPTS", + "name": "Port Said" + }, + { + "code": "EGSHG", + "name": "Sohag" + }, + { + "code": "EGSHR", + "name": "Al Sharqia" + }, + { + "code": "EGSIN", + "name": "North Sinai" + }, + { + "code": "EGSUZ", + "name": "Suez" + }, + { + "code": "EGWAD", + "name": "New Valley" + } + ] + }, + { + "code": "EH", + "name": "Moroccan dirham", + "currency_code": "MAD", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ER", + "name": "Eritrean nakfa", + "currency_code": "ERN", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ET", + "name": "Ethiopian birr", + "currency_code": "ETB", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GA", + "name": "Central African CFA franc", + "currency_code": "XAF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GH", + "name": "Ghana cedi", + "currency_code": "GHS", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "AF", + "name": "Ahafo" + }, + { + "code": "AH", + "name": "Ashanti" + }, + { + "code": "BA", + "name": "Brong-Ahafo" + }, + { + "code": "BO", + "name": "Bono" + }, + { + "code": "BE", + "name": "Bono East" + }, + { + "code": "CP", + "name": "Central" + }, + { + "code": "EP", + "name": "Eastern" + }, + { + "code": "AA", + "name": "Greater Accra" + }, + { + "code": "NE", + "name": "North East" + }, + { + "code": "NP", + "name": "Northern" + }, + { + "code": "OT", + "name": "Oti" + }, + { + "code": "SV", + "name": "Savannah" + }, + { + "code": "UE", + "name": "Upper East" + }, + { + "code": "UW", + "name": "Upper West" + }, + { + "code": "TV", + "name": "Volta" + }, + { + "code": "WP", + "name": "Western" + }, + { + "code": "WN", + "name": "Western North" + } + ] + }, + { + "code": "GM", + "name": "Gambian dalasi", + "currency_code": "GMD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GN", + "name": "Guinean franc", + "currency_code": "GNF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GQ", + "name": "Central African CFA franc", + "currency_code": "XAF", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GW", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KE", + "name": "Kenyan shilling", + "currency_code": "KES", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "KE01", + "name": "Baringo" + }, + { + "code": "KE02", + "name": "Bomet" + }, + { + "code": "KE03", + "name": "Bungoma" + }, + { + "code": "KE04", + "name": "Busia" + }, + { + "code": "KE05", + "name": "Elgeyo-Marakwet" + }, + { + "code": "KE06", + "name": "Embu" + }, + { + "code": "KE07", + "name": "Garissa" + }, + { + "code": "KE08", + "name": "Homa Bay" + }, + { + "code": "KE09", + "name": "Isiolo" + }, + { + "code": "KE10", + "name": "Kajiado" + }, + { + "code": "KE11", + "name": "Kakamega" + }, + { + "code": "KE12", + "name": "Kericho" + }, + { + "code": "KE13", + "name": "Kiambu" + }, + { + "code": "KE14", + "name": "Kilifi" + }, + { + "code": "KE15", + "name": "Kirinyaga" + }, + { + "code": "KE16", + "name": "Kisii" + }, + { + "code": "KE17", + "name": "Kisumu" + }, + { + "code": "KE18", + "name": "Kitui" + }, + { + "code": "KE19", + "name": "Kwale" + }, + { + "code": "KE20", + "name": "Laikipia" + }, + { + "code": "KE21", + "name": "Lamu" + }, + { + "code": "KE22", + "name": "Machakos" + }, + { + "code": "KE23", + "name": "Makueni" + }, + { + "code": "KE24", + "name": "Mandera" + }, + { + "code": "KE25", + "name": "Marsabit" + }, + { + "code": "KE26", + "name": "Meru" + }, + { + "code": "KE27", + "name": "Migori" + }, + { + "code": "KE28", + "name": "Mombasa" + }, + { + "code": "KE29", + "name": "Murang’a" + }, + { + "code": "KE30", + "name": "Nairobi County" + }, + { + "code": "KE31", + "name": "Nakuru" + }, + { + "code": "KE32", + "name": "Nandi" + }, + { + "code": "KE33", + "name": "Narok" + }, + { + "code": "KE34", + "name": "Nyamira" + }, + { + "code": "KE35", + "name": "Nyandarua" + }, + { + "code": "KE36", + "name": "Nyeri" + }, + { + "code": "KE37", + "name": "Samburu" + }, + { + "code": "KE38", + "name": "Siaya" + }, + { + "code": "KE39", + "name": "Taita-Taveta" + }, + { + "code": "KE40", + "name": "Tana River" + }, + { + "code": "KE41", + "name": "Tharaka-Nithi" + }, + { + "code": "KE42", + "name": "Trans Nzoia" + }, + { + "code": "KE43", + "name": "Turkana" + }, + { + "code": "KE44", + "name": "Uasin Gishu" + }, + { + "code": "KE45", + "name": "Vihiga" + }, + { + "code": "KE46", + "name": "Wajir" + }, + { + "code": "KE47", + "name": "West Pokot" + } + ] + }, + { + "code": "KM", + "name": "Comorian franc", + "currency_code": "KMF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LR", + "name": "Liberian dollar", + "currency_code": "LRD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "BM", + "name": "Bomi" + }, + { + "code": "BN", + "name": "Bong" + }, + { + "code": "GA", + "name": "Gbarpolu" + }, + { + "code": "GB", + "name": "Grand Bassa" + }, + { + "code": "GC", + "name": "Grand Cape Mount" + }, + { + "code": "GG", + "name": "Grand Gedeh" + }, + { + "code": "GK", + "name": "Grand Kru" + }, + { + "code": "LO", + "name": "Lofa" + }, + { + "code": "MA", + "name": "Margibi" + }, + { + "code": "MY", + "name": "Maryland" + }, + { + "code": "MO", + "name": "Montserrado" + }, + { + "code": "NM", + "name": "Nimba" + }, + { + "code": "RV", + "name": "Rivercess" + }, + { + "code": "RG", + "name": "River Gee" + }, + { + "code": "SN", + "name": "Sinoe" + } + ] + }, + { + "code": "LS", + "name": "Lesotho loti", + "currency_code": "LSL", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LY", + "name": "Libyan dinar", + "currency_code": "LYD", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MA", + "name": "Moroccan dirham", + "currency_code": "MAD", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MG", + "name": "Malagasy ariary", + "currency_code": "MGA", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ML", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MR", + "name": "Mauritanian ouguiya", + "currency_code": "MRU", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MU", + "name": "Mauritian rupee", + "currency_code": "MUR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MW", + "name": "Malawian kwacha", + "currency_code": "MWK", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MZ", + "name": "Mozambican metical", + "currency_code": "MZN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "MZP", + "name": "Cabo Delgado" + }, + { + "code": "MZG", + "name": "Gaza" + }, + { + "code": "MZI", + "name": "Inhambane" + }, + { + "code": "MZB", + "name": "Manica" + }, + { + "code": "MZL", + "name": "Maputo Province" + }, + { + "code": "MZMPM", + "name": "Maputo" + }, + { + "code": "MZN", + "name": "Nampula" + }, + { + "code": "MZA", + "name": "Niassa" + }, + { + "code": "MZS", + "name": "Sofala" + }, + { + "code": "MZT", + "name": "Tete" + }, + { + "code": "MZQ", + "name": "Zambézia" + } + ] + }, + { + "code": "NA", + "name": "Namibian dollar", + "currency_code": "NAD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "ER", + "name": "Erongo" + }, + { + "code": "HA", + "name": "Hardap" + }, + { + "code": "KA", + "name": "Karas" + }, + { + "code": "KE", + "name": "Kavango East" + }, + { + "code": "KW", + "name": "Kavango West" + }, + { + "code": "KH", + "name": "Khomas" + }, + { + "code": "KU", + "name": "Kunene" + }, + { + "code": "OW", + "name": "Ohangwena" + }, + { + "code": "OH", + "name": "Omaheke" + }, + { + "code": "OS", + "name": "Omusati" + }, + { + "code": "ON", + "name": "Oshana" + }, + { + "code": "OT", + "name": "Oshikoto" + }, + { + "code": "OD", + "name": "Otjozondjupa" + }, + { + "code": "CA", + "name": "Zambezi" + } + ] + }, + { + "code": "NE", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NG", + "name": "Nigerian naira", + "currency_code": "NGN", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "AB", + "name": "Abia" + }, + { + "code": "FC", + "name": "Abuja" + }, + { + "code": "AD", + "name": "Adamawa" + }, + { + "code": "AK", + "name": "Akwa Ibom" + }, + { + "code": "AN", + "name": "Anambra" + }, + { + "code": "BA", + "name": "Bauchi" + }, + { + "code": "BY", + "name": "Bayelsa" + }, + { + "code": "BE", + "name": "Benue" + }, + { + "code": "BO", + "name": "Borno" + }, + { + "code": "CR", + "name": "Cross River" + }, + { + "code": "DE", + "name": "Delta" + }, + { + "code": "EB", + "name": "Ebonyi" + }, + { + "code": "ED", + "name": "Edo" + }, + { + "code": "EK", + "name": "Ekiti" + }, + { + "code": "EN", + "name": "Enugu" + }, + { + "code": "GO", + "name": "Gombe" + }, + { + "code": "IM", + "name": "Imo" + }, + { + "code": "JI", + "name": "Jigawa" + }, + { + "code": "KD", + "name": "Kaduna" + }, + { + "code": "KN", + "name": "Kano" + }, + { + "code": "KT", + "name": "Katsina" + }, + { + "code": "KE", + "name": "Kebbi" + }, + { + "code": "KO", + "name": "Kogi" + }, + { + "code": "KW", + "name": "Kwara" + }, + { + "code": "LA", + "name": "Lagos" + }, + { + "code": "NA", + "name": "Nasarawa" + }, + { + "code": "NI", + "name": "Niger" + }, + { + "code": "OG", + "name": "Ogun" + }, + { + "code": "ON", + "name": "Ondo" + }, + { + "code": "OS", + "name": "Osun" + }, + { + "code": "OY", + "name": "Oyo" + }, + { + "code": "PL", + "name": "Plateau" + }, + { + "code": "RI", + "name": "Rivers" + }, + { + "code": "SO", + "name": "Sokoto" + }, + { + "code": "TA", + "name": "Taraba" + }, + { + "code": "YO", + "name": "Yobe" + }, + { + "code": "ZA", + "name": "Zamfara" + } + ] + }, + { + "code": "RE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "RW", + "name": "Rwandan franc", + "currency_code": "RWF", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SC", + "name": "Seychellois rupee", + "currency_code": "SCR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SD", + "name": "Sudanese pound", + "currency_code": "SDG", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SH", + "name": "Saint Helena pound", + "currency_code": "SHP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SL", + "name": "Sierra Leonean leone", + "currency_code": "SLL", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SN", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "SNDB", + "name": "Diourbel" + }, + { + "code": "SNDK", + "name": "Dakar" + }, + { + "code": "SNFK", + "name": "Fatick" + }, + { + "code": "SNKA", + "name": "Kaffrine" + }, + { + "code": "SNKD", + "name": "Kolda" + }, + { + "code": "SNKE", + "name": "Kédougou" + }, + { + "code": "SNKL", + "name": "Kaolack" + }, + { + "code": "SNLG", + "name": "Louga" + }, + { + "code": "SNMT", + "name": "Matam" + }, + { + "code": "SNSE", + "name": "Sédhiou" + }, + { + "code": "SNSL", + "name": "Saint-Louis" + }, + { + "code": "SNTC", + "name": "Tambacounda" + }, + { + "code": "SNTH", + "name": "Thiès" + }, + { + "code": "SNZG", + "name": "Ziguinchor" + } + ] + }, + { + "code": "SO", + "name": "Somali shilling", + "currency_code": "SOS", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SS", + "name": "South Sudanese pound", + "currency_code": "SSP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ST", + "name": "São Tomé and Príncipe dobra", + "currency_code": "STN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SZ", + "name": "Swazi lilangeni", + "currency_code": "SZL", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TD", + "name": "Central African CFA franc", + "currency_code": "XAF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TG", + "name": "West African CFA franc", + "currency_code": "XOF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TN", + "name": "Tunisian dinar", + "currency_code": "TND", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TZ", + "name": "Tanzanian shilling", + "currency_code": "TZS", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "TZ01", + "name": "Arusha" + }, + { + "code": "TZ02", + "name": "Dar es Salaam" + }, + { + "code": "TZ03", + "name": "Dodoma" + }, + { + "code": "TZ04", + "name": "Iringa" + }, + { + "code": "TZ05", + "name": "Kagera" + }, + { + "code": "TZ06", + "name": "Pemba North" + }, + { + "code": "TZ07", + "name": "Zanzibar North" + }, + { + "code": "TZ08", + "name": "Kigoma" + }, + { + "code": "TZ09", + "name": "Kilimanjaro" + }, + { + "code": "TZ10", + "name": "Pemba South" + }, + { + "code": "TZ11", + "name": "Zanzibar South" + }, + { + "code": "TZ12", + "name": "Lindi" + }, + { + "code": "TZ13", + "name": "Mara" + }, + { + "code": "TZ14", + "name": "Mbeya" + }, + { + "code": "TZ15", + "name": "Zanzibar West" + }, + { + "code": "TZ16", + "name": "Morogoro" + }, + { + "code": "TZ17", + "name": "Mtwara" + }, + { + "code": "TZ18", + "name": "Mwanza" + }, + { + "code": "TZ19", + "name": "Coast" + }, + { + "code": "TZ20", + "name": "Rukwa" + }, + { + "code": "TZ21", + "name": "Ruvuma" + }, + { + "code": "TZ22", + "name": "Shinyanga" + }, + { + "code": "TZ23", + "name": "Singida" + }, + { + "code": "TZ24", + "name": "Tabora" + }, + { + "code": "TZ25", + "name": "Tanga" + }, + { + "code": "TZ26", + "name": "Manyara" + }, + { + "code": "TZ27", + "name": "Geita" + }, + { + "code": "TZ28", + "name": "Katavi" + }, + { + "code": "TZ29", + "name": "Njombe" + }, + { + "code": "TZ30", + "name": "Simiyu" + } + ] + }, + { + "code": "UG", + "name": "Ugandan shilling", + "currency_code": "UGX", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "UG314", + "name": "Abim" + }, + { + "code": "UG301", + "name": "Adjumani" + }, + { + "code": "UG322", + "name": "Agago" + }, + { + "code": "UG323", + "name": "Alebtong" + }, + { + "code": "UG315", + "name": "Amolatar" + }, + { + "code": "UG324", + "name": "Amudat" + }, + { + "code": "UG216", + "name": "Amuria" + }, + { + "code": "UG316", + "name": "Amuru" + }, + { + "code": "UG302", + "name": "Apac" + }, + { + "code": "UG303", + "name": "Arua" + }, + { + "code": "UG217", + "name": "Budaka" + }, + { + "code": "UG218", + "name": "Bududa" + }, + { + "code": "UG201", + "name": "Bugiri" + }, + { + "code": "UG235", + "name": "Bugweri" + }, + { + "code": "UG420", + "name": "Buhweju" + }, + { + "code": "UG117", + "name": "Buikwe" + }, + { + "code": "UG219", + "name": "Bukedea" + }, + { + "code": "UG118", + "name": "Bukomansimbi" + }, + { + "code": "UG220", + "name": "Bukwa" + }, + { + "code": "UG225", + "name": "Bulambuli" + }, + { + "code": "UG416", + "name": "Buliisa" + }, + { + "code": "UG401", + "name": "Bundibugyo" + }, + { + "code": "UG430", + "name": "Bunyangabu" + }, + { + "code": "UG402", + "name": "Bushenyi" + }, + { + "code": "UG202", + "name": "Busia" + }, + { + "code": "UG221", + "name": "Butaleja" + }, + { + "code": "UG119", + "name": "Butambala" + }, + { + "code": "UG233", + "name": "Butebo" + }, + { + "code": "UG120", + "name": "Buvuma" + }, + { + "code": "UG226", + "name": "Buyende" + }, + { + "code": "UG317", + "name": "Dokolo" + }, + { + "code": "UG121", + "name": "Gomba" + }, + { + "code": "UG304", + "name": "Gulu" + }, + { + "code": "UG403", + "name": "Hoima" + }, + { + "code": "UG417", + "name": "Ibanda" + }, + { + "code": "UG203", + "name": "Iganga" + }, + { + "code": "UG418", + "name": "Isingiro" + }, + { + "code": "UG204", + "name": "Jinja" + }, + { + "code": "UG318", + "name": "Kaabong" + }, + { + "code": "UG404", + "name": "Kabale" + }, + { + "code": "UG405", + "name": "Kabarole" + }, + { + "code": "UG213", + "name": "Kaberamaido" + }, + { + "code": "UG427", + "name": "Kagadi" + }, + { + "code": "UG428", + "name": "Kakumiro" + }, + { + "code": "UG101", + "name": "Kalangala" + }, + { + "code": "UG222", + "name": "Kaliro" + }, + { + "code": "UG122", + "name": "Kalungu" + }, + { + "code": "UG102", + "name": "Kampala" + }, + { + "code": "UG205", + "name": "Kamuli" + }, + { + "code": "UG413", + "name": "Kamwenge" + }, + { + "code": "UG414", + "name": "Kanungu" + }, + { + "code": "UG206", + "name": "Kapchorwa" + }, + { + "code": "UG236", + "name": "Kapelebyong" + }, + { + "code": "UG126", + "name": "Kasanda" + }, + { + "code": "UG406", + "name": "Kasese" + }, + { + "code": "UG207", + "name": "Katakwi" + }, + { + "code": "UG112", + "name": "Kayunga" + }, + { + "code": "UG407", + "name": "Kibaale" + }, + { + "code": "UG103", + "name": "Kiboga" + }, + { + "code": "UG227", + "name": "Kibuku" + }, + { + "code": "UG432", + "name": "Kikuube" + }, + { + "code": "UG419", + "name": "Kiruhura" + }, + { + "code": "UG421", + "name": "Kiryandongo" + }, + { + "code": "UG408", + "name": "Kisoro" + }, + { + "code": "UG305", + "name": "Kitgum" + }, + { + "code": "UG319", + "name": "Koboko" + }, + { + "code": "UG325", + "name": "Kole" + }, + { + "code": "UG306", + "name": "Kotido" + }, + { + "code": "UG208", + "name": "Kumi" + }, + { + "code": "UG333", + "name": "Kwania" + }, + { + "code": "UG228", + "name": "Kween" + }, + { + "code": "UG123", + "name": "Kyankwanzi" + }, + { + "code": "UG422", + "name": "Kyegegwa" + }, + { + "code": "UG415", + "name": "Kyenjojo" + }, + { + "code": "UG125", + "name": "Kyotera" + }, + { + "code": "UG326", + "name": "Lamwo" + }, + { + "code": "UG307", + "name": "Lira" + }, + { + "code": "UG229", + "name": "Luuka" + }, + { + "code": "UG104", + "name": "Luwero" + }, + { + "code": "UG124", + "name": "Lwengo" + }, + { + "code": "UG114", + "name": "Lyantonde" + }, + { + "code": "UG223", + "name": "Manafwa" + }, + { + "code": "UG320", + "name": "Maracha" + }, + { + "code": "UG105", + "name": "Masaka" + }, + { + "code": "UG409", + "name": "Masindi" + }, + { + "code": "UG214", + "name": "Mayuge" + }, + { + "code": "UG209", + "name": "Mbale" + }, + { + "code": "UG410", + "name": "Mbarara" + }, + { + "code": "UG423", + "name": "Mitooma" + }, + { + "code": "UG115", + "name": "Mityana" + }, + { + "code": "UG308", + "name": "Moroto" + }, + { + "code": "UG309", + "name": "Moyo" + }, + { + "code": "UG106", + "name": "Mpigi" + }, + { + "code": "UG107", + "name": "Mubende" + }, + { + "code": "UG108", + "name": "Mukono" + }, + { + "code": "UG334", + "name": "Nabilatuk" + }, + { + "code": "UG311", + "name": "Nakapiripirit" + }, + { + "code": "UG116", + "name": "Nakaseke" + }, + { + "code": "UG109", + "name": "Nakasongola" + }, + { + "code": "UG230", + "name": "Namayingo" + }, + { + "code": "UG234", + "name": "Namisindwa" + }, + { + "code": "UG224", + "name": "Namutumba" + }, + { + "code": "UG327", + "name": "Napak" + }, + { + "code": "UG310", + "name": "Nebbi" + }, + { + "code": "UG231", + "name": "Ngora" + }, + { + "code": "UG424", + "name": "Ntoroko" + }, + { + "code": "UG411", + "name": "Ntungamo" + }, + { + "code": "UG328", + "name": "Nwoya" + }, + { + "code": "UG331", + "name": "Omoro" + }, + { + "code": "UG329", + "name": "Otuke" + }, + { + "code": "UG321", + "name": "Oyam" + }, + { + "code": "UG312", + "name": "Pader" + }, + { + "code": "UG332", + "name": "Pakwach" + }, + { + "code": "UG210", + "name": "Pallisa" + }, + { + "code": "UG110", + "name": "Rakai" + }, + { + "code": "UG429", + "name": "Rubanda" + }, + { + "code": "UG425", + "name": "Rubirizi" + }, + { + "code": "UG431", + "name": "Rukiga" + }, + { + "code": "UG412", + "name": "Rukungiri" + }, + { + "code": "UG111", + "name": "Sembabule" + }, + { + "code": "UG232", + "name": "Serere" + }, + { + "code": "UG426", + "name": "Sheema" + }, + { + "code": "UG215", + "name": "Sironko" + }, + { + "code": "UG211", + "name": "Soroti" + }, + { + "code": "UG212", + "name": "Tororo" + }, + { + "code": "UG113", + "name": "Wakiso" + }, + { + "code": "UG313", + "name": "Yumbe" + }, + { + "code": "UG330", + "name": "Zombo" + } + ] + }, + { + "code": "YT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ZA", + "name": "South African rand", + "currency_code": "ZAR", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "EC", + "name": "Eastern Cape" + }, + { + "code": "FS", + "name": "Free State" + }, + { + "code": "GP", + "name": "Gauteng" + }, + { + "code": "KZN", + "name": "KwaZulu-Natal" + }, + { + "code": "LP", + "name": "Limpopo" + }, + { + "code": "MP", + "name": "Mpumalanga" + }, + { + "code": "NC", + "name": "Northern Cape" + }, + { + "code": "NW", + "name": "North West" + }, + { + "code": "WC", + "name": "Western Cape" + } + ] + }, + { + "code": "ZM", + "name": "Zambian kwacha", + "currency_code": "ZMW", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "ZM-01", + "name": "Western" + }, + { + "code": "ZM-02", + "name": "Central" + }, + { + "code": "ZM-03", + "name": "Eastern" + }, + { + "code": "ZM-04", + "name": "Luapula" + }, + { + "code": "ZM-05", + "name": "Northern" + }, + { + "code": "ZM-06", + "name": "North-Western" + }, + { + "code": "ZM-07", + "name": "Southern" + }, + { + "code": "ZM-08", + "name": "Copperbelt" + }, + { + "code": "ZM-09", + "name": "Lusaka" + }, + { + "code": "ZM-10", + "name": "Muchinga" + } + ] + }, + { + "code": "ZW", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + } + ], + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "AN", + "name": "Antarctica", + "countries": [{ + "code": "AQ", + "name": "Antarctica", + "states": [] + }, + { + "code": "BV", + "name": "Bouvet Island", + "states": [] + }, + { + "code": "GS", + "name": "South Georgia/Sandwich Islands", + "states": [] + }, + { + "code": "HM", + "name": "Heard Island and McDonald Islands", + "states": [] + }, + { + "code": "TF", + "name": "French Southern Territories", + "states": [] + } + ], + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "AS", + "name": "Asia", + "countries": [{ + "code": "AE", + "name": "United Arab Emirates dirham", + "currency_code": "AED", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AF", + "name": "Afghan afghani", + "currency_code": "AFN", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AM", + "name": "Armenian dram", + "currency_code": "AMD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AZ", + "name": "Azerbaijani manat", + "currency_code": "AZN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BD", + "name": "Bangladeshi taka", + "currency_code": "BDT", + "currency_pos": "right", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "BD-05", + "name": "Bagerhat" + }, + { + "code": "BD-01", + "name": "Bandarban" + }, + { + "code": "BD-02", + "name": "Barguna" + }, + { + "code": "BD-06", + "name": "Barishal" + }, + { + "code": "BD-07", + "name": "Bhola" + }, + { + "code": "BD-03", + "name": "Bogura" + }, + { + "code": "BD-04", + "name": "Brahmanbaria" + }, + { + "code": "BD-09", + "name": "Chandpur" + }, + { + "code": "BD-10", + "name": "Chattogram" + }, + { + "code": "BD-12", + "name": "Chuadanga" + }, + { + "code": "BD-11", + "name": "Cox's Bazar" + }, + { + "code": "BD-08", + "name": "Cumilla" + }, + { + "code": "BD-13", + "name": "Dhaka" + }, + { + "code": "BD-14", + "name": "Dinajpur" + }, + { + "code": "BD-15", + "name": "Faridpur " + }, + { + "code": "BD-16", + "name": "Feni" + }, + { + "code": "BD-19", + "name": "Gaibandha" + }, + { + "code": "BD-18", + "name": "Gazipur" + }, + { + "code": "BD-17", + "name": "Gopalganj" + }, + { + "code": "BD-20", + "name": "Habiganj" + }, + { + "code": "BD-21", + "name": "Jamalpur" + }, + { + "code": "BD-22", + "name": "Jashore" + }, + { + "code": "BD-25", + "name": "Jhalokati" + }, + { + "code": "BD-23", + "name": "Jhenaidah" + }, + { + "code": "BD-24", + "name": "Joypurhat" + }, + { + "code": "BD-29", + "name": "Khagrachhari" + }, + { + "code": "BD-27", + "name": "Khulna" + }, + { + "code": "BD-26", + "name": "Kishoreganj" + }, + { + "code": "BD-28", + "name": "Kurigram" + }, + { + "code": "BD-30", + "name": "Kushtia" + }, + { + "code": "BD-31", + "name": "Lakshmipur" + }, + { + "code": "BD-32", + "name": "Lalmonirhat" + }, + { + "code": "BD-36", + "name": "Madaripur" + }, + { + "code": "BD-37", + "name": "Magura" + }, + { + "code": "BD-33", + "name": "Manikganj " + }, + { + "code": "BD-39", + "name": "Meherpur" + }, + { + "code": "BD-38", + "name": "Moulvibazar" + }, + { + "code": "BD-35", + "name": "Munshiganj" + }, + { + "code": "BD-34", + "name": "Mymensingh" + }, + { + "code": "BD-48", + "name": "Naogaon" + }, + { + "code": "BD-43", + "name": "Narail" + }, + { + "code": "BD-40", + "name": "Narayanganj" + }, + { + "code": "BD-42", + "name": "Narsingdi" + }, + { + "code": "BD-44", + "name": "Natore" + }, + { + "code": "BD-45", + "name": "Nawabganj" + }, + { + "code": "BD-41", + "name": "Netrakona" + }, + { + "code": "BD-46", + "name": "Nilphamari" + }, + { + "code": "BD-47", + "name": "Noakhali" + }, + { + "code": "BD-49", + "name": "Pabna" + }, + { + "code": "BD-52", + "name": "Panchagarh" + }, + { + "code": "BD-51", + "name": "Patuakhali" + }, + { + "code": "BD-50", + "name": "Pirojpur" + }, + { + "code": "BD-53", + "name": "Rajbari" + }, + { + "code": "BD-54", + "name": "Rajshahi" + }, + { + "code": "BD-56", + "name": "Rangamati" + }, + { + "code": "BD-55", + "name": "Rangpur" + }, + { + "code": "BD-58", + "name": "Satkhira" + }, + { + "code": "BD-62", + "name": "Shariatpur" + }, + { + "code": "BD-57", + "name": "Sherpur" + }, + { + "code": "BD-59", + "name": "Sirajganj" + }, + { + "code": "BD-61", + "name": "Sunamganj" + }, + { + "code": "BD-60", + "name": "Sylhet" + }, + { + "code": "BD-63", + "name": "Tangail" + }, + { + "code": "BD-64", + "name": "Thakurgaon" + } + ] + }, + { + "code": "BH", + "name": "Bahraini dinar", + "currency_code": "BHD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BN", + "name": "Brunei dollar", + "currency_code": "BND", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BT", + "name": "Bhutanese ngultrum", + "currency_code": "BTN", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CC", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CN", + "name": "Chinese yuan", + "currency_code": "CNY", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "CN1", + "name": "Yunnan / 云南" + }, + { + "code": "CN2", + "name": "Beijing / 北京" + }, + { + "code": "CN3", + "name": "Tianjin / 天津" + }, + { + "code": "CN4", + "name": "Hebei / 河北" + }, + { + "code": "CN5", + "name": "Shanxi / 山西" + }, + { + "code": "CN6", + "name": "Inner Mongolia / 內蒙古" + }, + { + "code": "CN7", + "name": "Liaoning / 辽宁" + }, + { + "code": "CN8", + "name": "Jilin / 吉林" + }, + { + "code": "CN9", + "name": "Heilongjiang / 黑龙江" + }, + { + "code": "CN10", + "name": "Shanghai / 上海" + }, + { + "code": "CN11", + "name": "Jiangsu / 江苏" + }, + { + "code": "CN12", + "name": "Zhejiang / 浙江" + }, + { + "code": "CN13", + "name": "Anhui / 安徽" + }, + { + "code": "CN14", + "name": "Fujian / 福建" + }, + { + "code": "CN15", + "name": "Jiangxi / 江西" + }, + { + "code": "CN16", + "name": "Shandong / 山东" + }, + { + "code": "CN17", + "name": "Henan / 河南" + }, + { + "code": "CN18", + "name": "Hubei / 湖北" + }, + { + "code": "CN19", + "name": "Hunan / 湖南" + }, + { + "code": "CN20", + "name": "Guangdong / 广东" + }, + { + "code": "CN21", + "name": "Guangxi Zhuang / 广西壮族" + }, + { + "code": "CN22", + "name": "Hainan / 海南" + }, + { + "code": "CN23", + "name": "Chongqing / 重庆" + }, + { + "code": "CN24", + "name": "Sichuan / 四川" + }, + { + "code": "CN25", + "name": "Guizhou / 贵州" + }, + { + "code": "CN26", + "name": "Shaanxi / 陕西" + }, + { + "code": "CN27", + "name": "Gansu / 甘肃" + }, + { + "code": "CN28", + "name": "Qinghai / 青海" + }, + { + "code": "CN29", + "name": "Ningxia Hui / 宁夏" + }, + { + "code": "CN30", + "name": "Macao / 澳门" + }, + { + "code": "CN31", + "name": "Tibet / 西藏" + }, + { + "code": "CN32", + "name": "Xinjiang / 新疆" + } + ] + }, + { + "code": "CX", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CY", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GE", + "name": "Georgian lari", + "currency_code": "GEL", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "HK", + "name": "Hong Kong dollar", + "currency_code": "HKD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "HONG KONG", + "name": "Hong Kong Island" + }, + { + "code": "KOWLOON", + "name": "Kowloon" + }, + { + "code": "NEW TERRITORIES", + "name": "New Territories" + } + ] + }, + { + "code": "ID", + "name": "Indonesian rupiah", + "currency_code": "IDR", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AC", + "name": "Daerah Istimewa Aceh" + }, + { + "code": "SU", + "name": "Sumatera Utara" + }, + { + "code": "SB", + "name": "Sumatera Barat" + }, + { + "code": "RI", + "name": "Riau" + }, + { + "code": "KR", + "name": "Kepulauan Riau" + }, + { + "code": "JA", + "name": "Jambi" + }, + { + "code": "SS", + "name": "Sumatera Selatan" + }, + { + "code": "BB", + "name": "Bangka Belitung" + }, + { + "code": "BE", + "name": "Bengkulu" + }, + { + "code": "LA", + "name": "Lampung" + }, + { + "code": "JK", + "name": "DKI Jakarta" + }, + { + "code": "JB", + "name": "Jawa Barat" + }, + { + "code": "BT", + "name": "Banten" + }, + { + "code": "JT", + "name": "Jawa Tengah" + }, + { + "code": "JI", + "name": "Jawa Timur" + }, + { + "code": "YO", + "name": "Daerah Istimewa Yogyakarta" + }, + { + "code": "BA", + "name": "Bali" + }, + { + "code": "NB", + "name": "Nusa Tenggara Barat" + }, + { + "code": "NT", + "name": "Nusa Tenggara Timur" + }, + { + "code": "KB", + "name": "Kalimantan Barat" + }, + { + "code": "KT", + "name": "Kalimantan Tengah" + }, + { + "code": "KI", + "name": "Kalimantan Timur" + }, + { + "code": "KS", + "name": "Kalimantan Selatan" + }, + { + "code": "KU", + "name": "Kalimantan Utara" + }, + { + "code": "SA", + "name": "Sulawesi Utara" + }, + { + "code": "ST", + "name": "Sulawesi Tengah" + }, + { + "code": "SG", + "name": "Sulawesi Tenggara" + }, + { + "code": "SR", + "name": "Sulawesi Barat" + }, + { + "code": "SN", + "name": "Sulawesi Selatan" + }, + { + "code": "GO", + "name": "Gorontalo" + }, + { + "code": "MA", + "name": "Maluku" + }, + { + "code": "MU", + "name": "Maluku Utara" + }, + { + "code": "PA", + "name": "Papua" + }, + { + "code": "PB", + "name": "Papua Barat" + } + ] + }, + { + "code": "IL", + "name": "Israeli new shekel", + "currency_code": "ILS", + "currency_pos": "right_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IN", + "name": "Indian rupee", + "currency_code": "INR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "AP", + "name": "Andhra Pradesh" + }, + { + "code": "AR", + "name": "Arunachal Pradesh" + }, + { + "code": "AS", + "name": "Assam" + }, + { + "code": "BR", + "name": "Bihar" + }, + { + "code": "CT", + "name": "Chhattisgarh" + }, + { + "code": "GA", + "name": "Goa" + }, + { + "code": "GJ", + "name": "Gujarat" + }, + { + "code": "HR", + "name": "Haryana" + }, + { + "code": "HP", + "name": "Himachal Pradesh" + }, + { + "code": "JK", + "name": "Jammu and Kashmir" + }, + { + "code": "JH", + "name": "Jharkhand" + }, + { + "code": "KA", + "name": "Karnataka" + }, + { + "code": "KL", + "name": "Kerala" + }, + { + "code": "LA", + "name": "Ladakh" + }, + { + "code": "MP", + "name": "Madhya Pradesh" + }, + { + "code": "MH", + "name": "Maharashtra" + }, + { + "code": "MN", + "name": "Manipur" + }, + { + "code": "ML", + "name": "Meghalaya" + }, + { + "code": "MZ", + "name": "Mizoram" + }, + { + "code": "NL", + "name": "Nagaland" + }, + { + "code": "OR", + "name": "Odisha" + }, + { + "code": "PB", + "name": "Punjab" + }, + { + "code": "RJ", + "name": "Rajasthan" + }, + { + "code": "SK", + "name": "Sikkim" + }, + { + "code": "TN", + "name": "Tamil Nadu" + }, + { + "code": "TS", + "name": "Telangana" + }, + { + "code": "TR", + "name": "Tripura" + }, + { + "code": "UK", + "name": "Uttarakhand" + }, + { + "code": "UP", + "name": "Uttar Pradesh" + }, + { + "code": "WB", + "name": "West Bengal" + }, + { + "code": "AN", + "name": "Andaman and Nicobar Islands" + }, + { + "code": "CH", + "name": "Chandigarh" + }, + { + "code": "DN", + "name": "Dadra and Nagar Haveli" + }, + { + "code": "DD", + "name": "Daman and Diu" + }, + { + "code": "DL", + "name": "Delhi" + }, + { + "code": "LD", + "name": "Lakshadeep" + }, + { + "code": "PY", + "name": "Pondicherry (Puducherry)" + } + ] + }, + { + "code": "IO", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IQ", + "name": "Iraqi dinar", + "currency_code": "IQD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IR", + "name": "Iranian rial", + "currency_code": "IRR", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "KHZ", + "name": "Khuzestan (خوزستان)" + }, + { + "code": "THR", + "name": "Tehran (تهران)" + }, + { + "code": "ILM", + "name": "Ilaam (ایلام)" + }, + { + "code": "BHR", + "name": "Bushehr (بوشهر)" + }, + { + "code": "ADL", + "name": "Ardabil (اردبیل)" + }, + { + "code": "ESF", + "name": "Isfahan (اصفهان)" + }, + { + "code": "YZD", + "name": "Yazd (یزد)" + }, + { + "code": "KRH", + "name": "Kermanshah (کرمانشاه)" + }, + { + "code": "KRN", + "name": "Kerman (کرمان)" + }, + { + "code": "HDN", + "name": "Hamadan (همدان)" + }, + { + "code": "GZN", + "name": "Ghazvin (قزوین)" + }, + { + "code": "ZJN", + "name": "Zanjan (زنجان)" + }, + { + "code": "LRS", + "name": "Luristan (لرستان)" + }, + { + "code": "ABZ", + "name": "Alborz (البرز)" + }, + { + "code": "EAZ", + "name": "East Azarbaijan (آذربایجان شرقی)" + }, + { + "code": "WAZ", + "name": "West Azarbaijan (آذربایجان غربی)" + }, + { + "code": "CHB", + "name": "Chaharmahal and Bakhtiari (چهارمحال و بختیاری)" + }, + { + "code": "SKH", + "name": "South Khorasan (خراسان جنوبی)" + }, + { + "code": "RKH", + "name": "Razavi Khorasan (خراسان رضوی)" + }, + { + "code": "NKH", + "name": "North Khorasan (خراسان شمالی)" + }, + { + "code": "SMN", + "name": "Semnan (سمنان)" + }, + { + "code": "FRS", + "name": "Fars (فارس)" + }, + { + "code": "QHM", + "name": "Qom (قم)" + }, + { + "code": "KRD", + "name": "Kurdistan / کردستان)" + }, + { + "code": "KBD", + "name": "Kohgiluyeh and BoyerAhmad (کهگیلوییه و بویراحمد)" + }, + { + "code": "GLS", + "name": "Golestan (گلستان)" + }, + { + "code": "GIL", + "name": "Gilan (گیلان)" + }, + { + "code": "MZN", + "name": "Mazandaran (مازندران)" + }, + { + "code": "MKZ", + "name": "Markazi (مرکزی)" + }, + { + "code": "HRZ", + "name": "Hormozgan (هرمزگان)" + }, + { + "code": "SBN", + "name": "Sistan and Baluchestan (سیستان و بلوچستان)" + } + ] + }, + { + "code": "JO", + "name": "Jordanian dinar", + "currency_code": "JOD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "JP", + "name": "Japanese yen", + "currency_code": "JPY", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "JP01", + "name": "Hokkaido" + }, + { + "code": "JP02", + "name": "Aomori" + }, + { + "code": "JP03", + "name": "Iwate" + }, + { + "code": "JP04", + "name": "Miyagi" + }, + { + "code": "JP05", + "name": "Akita" + }, + { + "code": "JP06", + "name": "Yamagata" + }, + { + "code": "JP07", + "name": "Fukushima" + }, + { + "code": "JP08", + "name": "Ibaraki" + }, + { + "code": "JP09", + "name": "Tochigi" + }, + { + "code": "JP10", + "name": "Gunma" + }, + { + "code": "JP11", + "name": "Saitama" + }, + { + "code": "JP12", + "name": "Chiba" + }, + { + "code": "JP13", + "name": "Tokyo" + }, + { + "code": "JP14", + "name": "Kanagawa" + }, + { + "code": "JP15", + "name": "Niigata" + }, + { + "code": "JP16", + "name": "Toyama" + }, + { + "code": "JP17", + "name": "Ishikawa" + }, + { + "code": "JP18", + "name": "Fukui" + }, + { + "code": "JP19", + "name": "Yamanashi" + }, + { + "code": "JP20", + "name": "Nagano" + }, + { + "code": "JP21", + "name": "Gifu" + }, + { + "code": "JP22", + "name": "Shizuoka" + }, + { + "code": "JP23", + "name": "Aichi" + }, + { + "code": "JP24", + "name": "Mie" + }, + { + "code": "JP25", + "name": "Shiga" + }, + { + "code": "JP26", + "name": "Kyoto" + }, + { + "code": "JP27", + "name": "Osaka" + }, + { + "code": "JP28", + "name": "Hyogo" + }, + { + "code": "JP29", + "name": "Nara" + }, + { + "code": "JP30", + "name": "Wakayama" + }, + { + "code": "JP31", + "name": "Tottori" + }, + { + "code": "JP32", + "name": "Shimane" + }, + { + "code": "JP33", + "name": "Okayama" + }, + { + "code": "JP34", + "name": "Hiroshima" + }, + { + "code": "JP35", + "name": "Yamaguchi" + }, + { + "code": "JP36", + "name": "Tokushima" + }, + { + "code": "JP37", + "name": "Kagawa" + }, + { + "code": "JP38", + "name": "Ehime" + }, + { + "code": "JP39", + "name": "Kochi" + }, + { + "code": "JP40", + "name": "Fukuoka" + }, + { + "code": "JP41", + "name": "Saga" + }, + { + "code": "JP42", + "name": "Nagasaki" + }, + { + "code": "JP43", + "name": "Kumamoto" + }, + { + "code": "JP44", + "name": "Oita" + }, + { + "code": "JP45", + "name": "Miyazaki" + }, + { + "code": "JP46", + "name": "Kagoshima" + }, + { + "code": "JP47", + "name": "Okinawa" + } + ] + }, + { + "code": "KG", + "name": "Kyrgyzstani som", + "currency_code": "KGS", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KH", + "name": "Cambodian riel", + "currency_code": "KHR", + "currency_pos": "right", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KP", + "name": "North Korean won", + "currency_code": "KPW", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KR", + "name": "South Korean won", + "currency_code": "KRW", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KW", + "name": "Kuwaiti dinar", + "currency_code": "KWD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KZ", + "name": "Kazakhstani tenge", + "currency_code": "KZT", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LA", + "name": "Lao kip", + "currency_code": "LAK", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AT", + "name": "Attapeu" + }, + { + "code": "BK", + "name": "Bokeo" + }, + { + "code": "BL", + "name": "Bolikhamsai" + }, + { + "code": "CH", + "name": "Champasak" + }, + { + "code": "HO", + "name": "Houaphanh" + }, + { + "code": "KH", + "name": "Khammouane" + }, + { + "code": "LM", + "name": "Luang Namtha" + }, + { + "code": "LP", + "name": "Luang Prabang" + }, + { + "code": "OU", + "name": "Oudomxay" + }, + { + "code": "PH", + "name": "Phongsaly" + }, + { + "code": "SL", + "name": "Salavan" + }, + { + "code": "SV", + "name": "Savannakhet" + }, + { + "code": "VI", + "name": "Vientiane Province" + }, + { + "code": "VT", + "name": "Vientiane" + }, + { + "code": "XA", + "name": "Sainyabuli" + }, + { + "code": "XE", + "name": "Sekong" + }, + { + "code": "XI", + "name": "Xiangkhouang" + }, + { + "code": "XS", + "name": "Xaisomboun" + } + ] + }, + { + "code": "LB", + "name": "Lebanese pound", + "currency_code": "LBP", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LK", + "name": "Sri Lankan rupee", + "currency_code": "LKR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MM", + "name": "Burmese kyat", + "currency_code": "MMK", + "currency_pos": "right_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MN", + "name": "Mongolian tögrög", + "currency_code": "MNT", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MO", + "name": "Macanese pataca", + "currency_code": "MOP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MV", + "name": "Maldivian rufiyaa", + "currency_code": "MVR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MY", + "name": "Malaysian ringgit", + "currency_code": "MYR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "JHR", + "name": "Johor" + }, + { + "code": "KDH", + "name": "Kedah" + }, + { + "code": "KTN", + "name": "Kelantan" + }, + { + "code": "LBN", + "name": "Labuan" + }, + { + "code": "MLK", + "name": "Malacca (Melaka)" + }, + { + "code": "NSN", + "name": "Negeri Sembilan" + }, + { + "code": "PHG", + "name": "Pahang" + }, + { + "code": "PNG", + "name": "Penang (Pulau Pinang)" + }, + { + "code": "PRK", + "name": "Perak" + }, + { + "code": "PLS", + "name": "Perlis" + }, + { + "code": "SBH", + "name": "Sabah" + }, + { + "code": "SWK", + "name": "Sarawak" + }, + { + "code": "SGR", + "name": "Selangor" + }, + { + "code": "TRG", + "name": "Terengganu" + }, + { + "code": "PJY", + "name": "Putrajaya" + }, + { + "code": "KUL", + "name": "Kuala Lumpur" + } + ] + }, + { + "code": "NP", + "name": "Nepalese rupee", + "currency_code": "NPR", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "BAG", + "name": "Bagmati" + }, + { + "code": "BHE", + "name": "Bheri" + }, + { + "code": "DHA", + "name": "Dhaulagiri" + }, + { + "code": "GAN", + "name": "Gandaki" + }, + { + "code": "JAN", + "name": "Janakpur" + }, + { + "code": "KAR", + "name": "Karnali" + }, + { + "code": "KOS", + "name": "Koshi" + }, + { + "code": "LUM", + "name": "Lumbini" + }, + { + "code": "MAH", + "name": "Mahakali" + }, + { + "code": "MEC", + "name": "Mechi" + }, + { + "code": "NAR", + "name": "Narayani" + }, + { + "code": "RAP", + "name": "Rapti" + }, + { + "code": "SAG", + "name": "Sagarmatha" + }, + { + "code": "SET", + "name": "Seti" + } + ] + }, + { + "code": "OM", + "name": "Omani rial", + "currency_code": "OMR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PH", + "name": "Philippine peso", + "currency_code": "PHP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "ABR", + "name": "Abra" + }, + { + "code": "AGN", + "name": "Agusan del Norte" + }, + { + "code": "AGS", + "name": "Agusan del Sur" + }, + { + "code": "AKL", + "name": "Aklan" + }, + { + "code": "ALB", + "name": "Albay" + }, + { + "code": "ANT", + "name": "Antique" + }, + { + "code": "APA", + "name": "Apayao" + }, + { + "code": "AUR", + "name": "Aurora" + }, + { + "code": "BAS", + "name": "Basilan" + }, + { + "code": "BAN", + "name": "Bataan" + }, + { + "code": "BTN", + "name": "Batanes" + }, + { + "code": "BTG", + "name": "Batangas" + }, + { + "code": "BEN", + "name": "Benguet" + }, + { + "code": "BIL", + "name": "Biliran" + }, + { + "code": "BOH", + "name": "Bohol" + }, + { + "code": "BUK", + "name": "Bukidnon" + }, + { + "code": "BUL", + "name": "Bulacan" + }, + { + "code": "CAG", + "name": "Cagayan" + }, + { + "code": "CAN", + "name": "Camarines Norte" + }, + { + "code": "CAS", + "name": "Camarines Sur" + }, + { + "code": "CAM", + "name": "Camiguin" + }, + { + "code": "CAP", + "name": "Capiz" + }, + { + "code": "CAT", + "name": "Catanduanes" + }, + { + "code": "CAV", + "name": "Cavite" + }, + { + "code": "CEB", + "name": "Cebu" + }, + { + "code": "COM", + "name": "Compostela Valley" + }, + { + "code": "NCO", + "name": "Cotabato" + }, + { + "code": "DAV", + "name": "Davao del Norte" + }, + { + "code": "DAS", + "name": "Davao del Sur" + }, + { + "code": "DAC", + "name": "Davao Occidental" + }, + { + "code": "DAO", + "name": "Davao Oriental" + }, + { + "code": "DIN", + "name": "Dinagat Islands" + }, + { + "code": "EAS", + "name": "Eastern Samar" + }, + { + "code": "GUI", + "name": "Guimaras" + }, + { + "code": "IFU", + "name": "Ifugao" + }, + { + "code": "ILN", + "name": "Ilocos Norte" + }, + { + "code": "ILS", + "name": "Ilocos Sur" + }, + { + "code": "ILI", + "name": "Iloilo" + }, + { + "code": "ISA", + "name": "Isabela" + }, + { + "code": "KAL", + "name": "Kalinga" + }, + { + "code": "LUN", + "name": "La Union" + }, + { + "code": "LAG", + "name": "Laguna" + }, + { + "code": "LAN", + "name": "Lanao del Norte" + }, + { + "code": "LAS", + "name": "Lanao del Sur" + }, + { + "code": "LEY", + "name": "Leyte" + }, + { + "code": "MAG", + "name": "Maguindanao" + }, + { + "code": "MAD", + "name": "Marinduque" + }, + { + "code": "MAS", + "name": "Masbate" + }, + { + "code": "MSC", + "name": "Misamis Occidental" + }, + { + "code": "MSR", + "name": "Misamis Oriental" + }, + { + "code": "MOU", + "name": "Mountain Province" + }, + { + "code": "NEC", + "name": "Negros Occidental" + }, + { + "code": "NER", + "name": "Negros Oriental" + }, + { + "code": "NSA", + "name": "Northern Samar" + }, + { + "code": "NUE", + "name": "Nueva Ecija" + }, + { + "code": "NUV", + "name": "Nueva Vizcaya" + }, + { + "code": "MDC", + "name": "Occidental Mindoro" + }, + { + "code": "MDR", + "name": "Oriental Mindoro" + }, + { + "code": "PLW", + "name": "Palawan" + }, + { + "code": "PAM", + "name": "Pampanga" + }, + { + "code": "PAN", + "name": "Pangasinan" + }, + { + "code": "QUE", + "name": "Quezon" + }, + { + "code": "QUI", + "name": "Quirino" + }, + { + "code": "RIZ", + "name": "Rizal" + }, + { + "code": "ROM", + "name": "Romblon" + }, + { + "code": "WSA", + "name": "Samar" + }, + { + "code": "SAR", + "name": "Sarangani" + }, + { + "code": "SIQ", + "name": "Siquijor" + }, + { + "code": "SOR", + "name": "Sorsogon" + }, + { + "code": "SCO", + "name": "South Cotabato" + }, + { + "code": "SLE", + "name": "Southern Leyte" + }, + { + "code": "SUK", + "name": "Sultan Kudarat" + }, + { + "code": "SLU", + "name": "Sulu" + }, + { + "code": "SUN", + "name": "Surigao del Norte" + }, + { + "code": "SUR", + "name": "Surigao del Sur" + }, + { + "code": "TAR", + "name": "Tarlac" + }, + { + "code": "TAW", + "name": "Tawi-Tawi" + }, + { + "code": "ZMB", + "name": "Zambales" + }, + { + "code": "ZAN", + "name": "Zamboanga del Norte" + }, + { + "code": "ZAS", + "name": "Zamboanga del Sur" + }, + { + "code": "ZSI", + "name": "Zamboanga Sibugay" + }, + { + "code": "00", + "name": "Metro Manila" + } + ] + }, + { + "code": "PK", + "name": "Pakistani rupee", + "currency_code": "PKR", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "JK", + "name": "Azad Kashmir" + }, + { + "code": "BA", + "name": "Balochistan" + }, + { + "code": "TA", + "name": "FATA" + }, + { + "code": "GB", + "name": "Gilgit Baltistan" + }, + { + "code": "IS", + "name": "Islamabad Capital Territory" + }, + { + "code": "KP", + "name": "Khyber Pakhtunkhwa" + }, + { + "code": "PB", + "name": "Punjab" + }, + { + "code": "SD", + "name": "Sindh" + } + ] + }, + { + "code": "PS", + "name": "Jordanian dinar", + "currency_code": "JOD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 3, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "QA", + "name": "Qatari riyal", + "currency_code": "QAR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SA", + "name": "Saudi riyal", + "currency_code": "SAR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SG", + "name": "Singapore dollar", + "currency_code": "SGD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SY", + "name": "Syrian pound", + "currency_code": "SYP", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TH", + "name": "Thai baht", + "currency_code": "THB", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "TH-37", + "name": "Amnat Charoen" + }, + { + "code": "TH-15", + "name": "Ang Thong" + }, + { + "code": "TH-14", + "name": "Ayutthaya" + }, + { + "code": "TH-10", + "name": "Bangkok" + }, + { + "code": "TH-38", + "name": "Bueng Kan" + }, + { + "code": "TH-31", + "name": "Buri Ram" + }, + { + "code": "TH-24", + "name": "Chachoengsao" + }, + { + "code": "TH-18", + "name": "Chai Nat" + }, + { + "code": "TH-36", + "name": "Chaiyaphum" + }, + { + "code": "TH-22", + "name": "Chanthaburi" + }, + { + "code": "TH-50", + "name": "Chiang Mai" + }, + { + "code": "TH-57", + "name": "Chiang Rai" + }, + { + "code": "TH-20", + "name": "Chonburi" + }, + { + "code": "TH-86", + "name": "Chumphon" + }, + { + "code": "TH-46", + "name": "Kalasin" + }, + { + "code": "TH-62", + "name": "Kamphaeng Phet" + }, + { + "code": "TH-71", + "name": "Kanchanaburi" + }, + { + "code": "TH-40", + "name": "Khon Kaen" + }, + { + "code": "TH-81", + "name": "Krabi" + }, + { + "code": "TH-52", + "name": "Lampang" + }, + { + "code": "TH-51", + "name": "Lamphun" + }, + { + "code": "TH-42", + "name": "Loei" + }, + { + "code": "TH-16", + "name": "Lopburi" + }, + { + "code": "TH-58", + "name": "Mae Hong Son" + }, + { + "code": "TH-44", + "name": "Maha Sarakham" + }, + { + "code": "TH-49", + "name": "Mukdahan" + }, + { + "code": "TH-26", + "name": "Nakhon Nayok" + }, + { + "code": "TH-73", + "name": "Nakhon Pathom" + }, + { + "code": "TH-48", + "name": "Nakhon Phanom" + }, + { + "code": "TH-30", + "name": "Nakhon Ratchasima" + }, + { + "code": "TH-60", + "name": "Nakhon Sawan" + }, + { + "code": "TH-80", + "name": "Nakhon Si Thammarat" + }, + { + "code": "TH-55", + "name": "Nan" + }, + { + "code": "TH-96", + "name": "Narathiwat" + }, + { + "code": "TH-39", + "name": "Nong Bua Lam Phu" + }, + { + "code": "TH-43", + "name": "Nong Khai" + }, + { + "code": "TH-12", + "name": "Nonthaburi" + }, + { + "code": "TH-13", + "name": "Pathum Thani" + }, + { + "code": "TH-94", + "name": "Pattani" + }, + { + "code": "TH-82", + "name": "Phang Nga" + }, + { + "code": "TH-93", + "name": "Phatthalung" + }, + { + "code": "TH-56", + "name": "Phayao" + }, + { + "code": "TH-67", + "name": "Phetchabun" + }, + { + "code": "TH-76", + "name": "Phetchaburi" + }, + { + "code": "TH-66", + "name": "Phichit" + }, + { + "code": "TH-65", + "name": "Phitsanulok" + }, + { + "code": "TH-54", + "name": "Phrae" + }, + { + "code": "TH-83", + "name": "Phuket" + }, + { + "code": "TH-25", + "name": "Prachin Buri" + }, + { + "code": "TH-77", + "name": "Prachuap Khiri Khan" + }, + { + "code": "TH-85", + "name": "Ranong" + }, + { + "code": "TH-70", + "name": "Ratchaburi" + }, + { + "code": "TH-21", + "name": "Rayong" + }, + { + "code": "TH-45", + "name": "Roi Et" + }, + { + "code": "TH-27", + "name": "Sa Kaeo" + }, + { + "code": "TH-47", + "name": "Sakon Nakhon" + }, + { + "code": "TH-11", + "name": "Samut Prakan" + }, + { + "code": "TH-74", + "name": "Samut Sakhon" + }, + { + "code": "TH-75", + "name": "Samut Songkhram" + }, + { + "code": "TH-19", + "name": "Saraburi" + }, + { + "code": "TH-91", + "name": "Satun" + }, + { + "code": "TH-17", + "name": "Sing Buri" + }, + { + "code": "TH-33", + "name": "Sisaket" + }, + { + "code": "TH-90", + "name": "Songkhla" + }, + { + "code": "TH-64", + "name": "Sukhothai" + }, + { + "code": "TH-72", + "name": "Suphan Buri" + }, + { + "code": "TH-84", + "name": "Surat Thani" + }, + { + "code": "TH-32", + "name": "Surin" + }, + { + "code": "TH-63", + "name": "Tak" + }, + { + "code": "TH-92", + "name": "Trang" + }, + { + "code": "TH-23", + "name": "Trat" + }, + { + "code": "TH-34", + "name": "Ubon Ratchathani" + }, + { + "code": "TH-41", + "name": "Udon Thani" + }, + { + "code": "TH-61", + "name": "Uthai Thani" + }, + { + "code": "TH-53", + "name": "Uttaradit" + }, + { + "code": "TH-95", + "name": "Yala" + }, + { + "code": "TH-35", + "name": "Yasothon" + } + ] + }, + { + "code": "TJ", + "name": "Tajikistani somoni", + "currency_code": "TJS", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TL", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TM", + "name": "Turkmenistan manat", + "currency_code": "TMT", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TW", + "name": "New Taiwan dollar", + "currency_code": "TWD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "UZ", + "name": "Uzbekistani som", + "currency_code": "UZS", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "VN", + "name": "Vietnamese đồng", + "currency_code": "VND", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "YE", + "name": "Yemeni rial", + "currency_code": "YER", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + } + ], + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "EU", + "name": "Europe", + "countries": [{ + "code": "AD", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AL", + "name": "Albanian lek", + "currency_code": "ALL", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "AL-01", + "name": "Berat" + }, + { + "code": "AL-09", + "name": "Dibër" + }, + { + "code": "AL-02", + "name": "Durrës" + }, + { + "code": "AL-03", + "name": "Elbasan" + }, + { + "code": "AL-04", + "name": "Fier" + }, + { + "code": "AL-05", + "name": "Gjirokastër" + }, + { + "code": "AL-06", + "name": "Korçë" + }, + { + "code": "AL-07", + "name": "Kukës" + }, + { + "code": "AL-08", + "name": "Lezhë" + }, + { + "code": "AL-10", + "name": "Shkodër" + }, + { + "code": "AL-11", + "name": "Tirana" + }, + { + "code": "AL-12", + "name": "Vlorë" + } + ] + }, + { + "code": "AT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AX", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BA", + "name": "Bosnia and Herzegovina convertible mark", + "currency_code": "BAM", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BG", + "name": "Bulgarian lev", + "currency_code": "BGN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "BG-01", + "name": "Blagoevgrad" + }, + { + "code": "BG-02", + "name": "Burgas" + }, + { + "code": "BG-08", + "name": "Dobrich" + }, + { + "code": "BG-07", + "name": "Gabrovo" + }, + { + "code": "BG-26", + "name": "Haskovo" + }, + { + "code": "BG-09", + "name": "Kardzhali" + }, + { + "code": "BG-10", + "name": "Kyustendil" + }, + { + "code": "BG-11", + "name": "Lovech" + }, + { + "code": "BG-12", + "name": "Montana" + }, + { + "code": "BG-13", + "name": "Pazardzhik" + }, + { + "code": "BG-14", + "name": "Pernik" + }, + { + "code": "BG-15", + "name": "Pleven" + }, + { + "code": "BG-16", + "name": "Plovdiv" + }, + { + "code": "BG-17", + "name": "Razgrad" + }, + { + "code": "BG-18", + "name": "Ruse" + }, + { + "code": "BG-27", + "name": "Shumen" + }, + { + "code": "BG-19", + "name": "Silistra" + }, + { + "code": "BG-20", + "name": "Sliven" + }, + { + "code": "BG-21", + "name": "Smolyan" + }, + { + "code": "BG-23", + "name": "Sofia District" + }, + { + "code": "BG-22", + "name": "Sofia" + }, + { + "code": "BG-24", + "name": "Stara Zagora" + }, + { + "code": "BG-25", + "name": "Targovishte" + }, + { + "code": "BG-03", + "name": "Varna" + }, + { + "code": "BG-04", + "name": "Veliko Tarnovo" + }, + { + "code": "BG-05", + "name": "Vidin" + }, + { + "code": "BG-06", + "name": "Vratsa" + }, + { + "code": "BG-28", + "name": "Yambol" + } + ] + }, + { + "code": "BY", + "name": "Belarusian ruble", + "currency_code": "BYN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CH", + "name": "Swiss franc", + "currency_code": "CHF", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": "'", + "weight_unit": "kg", + "states": [{ + "code": "AG", + "name": "Aargau" + }, + { + "code": "AR", + "name": "Appenzell Ausserrhoden" + }, + { + "code": "AI", + "name": "Appenzell Innerrhoden" + }, + { + "code": "BL", + "name": "Basel-Landschaft" + }, + { + "code": "BS", + "name": "Basel-Stadt" + }, + { + "code": "BE", + "name": "Bern" + }, + { + "code": "FR", + "name": "Fribourg" + }, + { + "code": "GE", + "name": "Geneva" + }, + { + "code": "GL", + "name": "Glarus" + }, + { + "code": "GR", + "name": "Graubünden" + }, + { + "code": "JU", + "name": "Jura" + }, + { + "code": "LU", + "name": "Luzern" + }, + { + "code": "NE", + "name": "Neuchâtel" + }, + { + "code": "NW", + "name": "Nidwalden" + }, + { + "code": "OW", + "name": "Obwalden" + }, + { + "code": "SH", + "name": "Schaffhausen" + }, + { + "code": "SZ", + "name": "Schwyz" + }, + { + "code": "SO", + "name": "Solothurn" + }, + { + "code": "SG", + "name": "St. Gallen" + }, + { + "code": "TG", + "name": "Thurgau" + }, + { + "code": "TI", + "name": "Ticino" + }, + { + "code": "UR", + "name": "Uri" + }, + { + "code": "VS", + "name": "Valais" + }, + { + "code": "VD", + "name": "Vaud" + }, + { + "code": "ZG", + "name": "Zug" + }, + { + "code": "ZH", + "name": "Zürich" + } + ] + }, + { + "code": "CZ", + "name": "Czech koruna", + "currency_code": "CZK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "DE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "DE-BW", + "name": "Baden-Württemberg" + }, + { + "code": "DE-BY", + "name": "Bavaria" + }, + { + "code": "DE-BE", + "name": "Berlin" + }, + { + "code": "DE-BB", + "name": "Brandenburg" + }, + { + "code": "DE-HB", + "name": "Bremen" + }, + { + "code": "DE-HH", + "name": "Hamburg" + }, + { + "code": "DE-HE", + "name": "Hesse" + }, + { + "code": "DE-MV", + "name": "Mecklenburg-Vorpommern" + }, + { + "code": "DE-NI", + "name": "Lower Saxony" + }, + { + "code": "DE-NW", + "name": "North Rhine-Westphalia" + }, + { + "code": "DE-RP", + "name": "Rhineland-Palatinate" + }, + { + "code": "DE-SL", + "name": "Saarland" + }, + { + "code": "DE-SN", + "name": "Saxony" + }, + { + "code": "DE-ST", + "name": "Saxony-Anhalt" + }, + { + "code": "DE-SH", + "name": "Schleswig-Holstein" + }, + { + "code": "DE-TH", + "name": "Thuringia" + } + ] + }, + { + "code": "DK", + "name": "Danish krone", + "currency_code": "DKK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "EE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ES", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "C", + "name": "A Coruña" + }, + { + "code": "VI", + "name": "Araba/Álava" + }, + { + "code": "AB", + "name": "Albacete" + }, + { + "code": "A", + "name": "Alicante" + }, + { + "code": "AL", + "name": "Almería" + }, + { + "code": "O", + "name": "Asturias" + }, + { + "code": "AV", + "name": "Ávila" + }, + { + "code": "BA", + "name": "Badajoz" + }, + { + "code": "PM", + "name": "Baleares" + }, + { + "code": "B", + "name": "Barcelona" + }, + { + "code": "BU", + "name": "Burgos" + }, + { + "code": "CC", + "name": "Cáceres" + }, + { + "code": "CA", + "name": "Cádiz" + }, + { + "code": "S", + "name": "Cantabria" + }, + { + "code": "CS", + "name": "Castellón" + }, + { + "code": "CE", + "name": "Ceuta" + }, + { + "code": "CR", + "name": "Ciudad Real" + }, + { + "code": "CO", + "name": "Córdoba" + }, + { + "code": "CU", + "name": "Cuenca" + }, + { + "code": "GI", + "name": "Girona" + }, + { + "code": "GR", + "name": "Granada" + }, + { + "code": "GU", + "name": "Guadalajara" + }, + { + "code": "SS", + "name": "Gipuzkoa" + }, + { + "code": "H", + "name": "Huelva" + }, + { + "code": "HU", + "name": "Huesca" + }, + { + "code": "J", + "name": "Jaén" + }, + { + "code": "LO", + "name": "La Rioja" + }, + { + "code": "GC", + "name": "Las Palmas" + }, + { + "code": "LE", + "name": "León" + }, + { + "code": "L", + "name": "Lleida" + }, + { + "code": "LU", + "name": "Lugo" + }, + { + "code": "M", + "name": "Madrid" + }, + { + "code": "MA", + "name": "Málaga" + }, + { + "code": "ML", + "name": "Melilla" + }, + { + "code": "MU", + "name": "Murcia" + }, + { + "code": "NA", + "name": "Navarra" + }, + { + "code": "OR", + "name": "Ourense" + }, + { + "code": "P", + "name": "Palencia" + }, + { + "code": "PO", + "name": "Pontevedra" + }, + { + "code": "SA", + "name": "Salamanca" + }, + { + "code": "TF", + "name": "Santa Cruz de Tenerife" + }, + { + "code": "SG", + "name": "Segovia" + }, + { + "code": "SE", + "name": "Sevilla" + }, + { + "code": "SO", + "name": "Soria" + }, + { + "code": "T", + "name": "Tarragona" + }, + { + "code": "TE", + "name": "Teruel" + }, + { + "code": "TO", + "name": "Toledo" + }, + { + "code": "V", + "name": "Valencia" + }, + { + "code": "VA", + "name": "Valladolid" + }, + { + "code": "BI", + "name": "Biscay" + }, + { + "code": "ZA", + "name": "Zamora" + }, + { + "code": "Z", + "name": "Zaragoza" + } + ] + }, + { + "code": "FI", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "FO", + "name": "Danish krone", + "currency_code": "DKK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "FR", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GB", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "foot", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "oz", + "states": [] + }, + { + "code": "GG", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GI", + "name": "Gibraltar pound", + "currency_code": "GIP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GR", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "I", + "name": "Attica" + }, + { + "code": "A", + "name": "East Macedonia and Thrace" + }, + { + "code": "B", + "name": "Central Macedonia" + }, + { + "code": "C", + "name": "West Macedonia" + }, + { + "code": "D", + "name": "Epirus" + }, + { + "code": "E", + "name": "Thessaly" + }, + { + "code": "F", + "name": "Ionian Islands" + }, + { + "code": "G", + "name": "West Greece" + }, + { + "code": "H", + "name": "Central Greece" + }, + { + "code": "J", + "name": "Peloponnese" + }, + { + "code": "K", + "name": "North Aegean" + }, + { + "code": "L", + "name": "South Aegean" + }, + { + "code": "M", + "name": "Crete" + } + ] + }, + { + "code": "HR", + "name": "Croatian kuna", + "currency_code": "HRK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "HU", + "name": "Hungarian forint", + "currency_code": "HUF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "BK", + "name": "Bács-Kiskun" + }, + { + "code": "BE", + "name": "Békés" + }, + { + "code": "BA", + "name": "Baranya" + }, + { + "code": "BZ", + "name": "Borsod-Abaúj-Zemplén" + }, + { + "code": "BU", + "name": "Budapest" + }, + { + "code": "CS", + "name": "Csongrád-Csanád" + }, + { + "code": "FE", + "name": "Fejér" + }, + { + "code": "GS", + "name": "Győr-Moson-Sopron" + }, + { + "code": "HB", + "name": "Hajdú-Bihar" + }, + { + "code": "HE", + "name": "Heves" + }, + { + "code": "JN", + "name": "Jász-Nagykun-Szolnok" + }, + { + "code": "KE", + "name": "Komárom-Esztergom" + }, + { + "code": "NO", + "name": "Nógrád" + }, + { + "code": "PE", + "name": "Pest" + }, + { + "code": "SO", + "name": "Somogy" + }, + { + "code": "SZ", + "name": "Szabolcs-Szatmár-Bereg" + }, + { + "code": "TO", + "name": "Tolna" + }, + { + "code": "VA", + "name": "Vas" + }, + { + "code": "VE", + "name": "Veszprém" + }, + { + "code": "ZA", + "name": "Zala" + } + ] + }, + { + "code": "IE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "CW", + "name": "Carlow" + }, + { + "code": "CN", + "name": "Cavan" + }, + { + "code": "CE", + "name": "Clare" + }, + { + "code": "CO", + "name": "Cork" + }, + { + "code": "DL", + "name": "Donegal" + }, + { + "code": "D", + "name": "Dublin" + }, + { + "code": "G", + "name": "Galway" + }, + { + "code": "KY", + "name": "Kerry" + }, + { + "code": "KE", + "name": "Kildare" + }, + { + "code": "KK", + "name": "Kilkenny" + }, + { + "code": "LS", + "name": "Laois" + }, + { + "code": "LM", + "name": "Leitrim" + }, + { + "code": "LK", + "name": "Limerick" + }, + { + "code": "LD", + "name": "Longford" + }, + { + "code": "LH", + "name": "Louth" + }, + { + "code": "MO", + "name": "Mayo" + }, + { + "code": "MH", + "name": "Meath" + }, + { + "code": "MN", + "name": "Monaghan" + }, + { + "code": "OY", + "name": "Offaly" + }, + { + "code": "RN", + "name": "Roscommon" + }, + { + "code": "SO", + "name": "Sligo" + }, + { + "code": "TA", + "name": "Tipperary" + }, + { + "code": "WD", + "name": "Waterford" + }, + { + "code": "WH", + "name": "Westmeath" + }, + { + "code": "WX", + "name": "Wexford" + }, + { + "code": "WW", + "name": "Wicklow" + } + ] + }, + { + "code": "IM", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IS", + "name": "Icelandic króna", + "currency_code": "ISK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AG", + "name": "Agrigento" + }, + { + "code": "AL", + "name": "Alessandria" + }, + { + "code": "AN", + "name": "Ancona" + }, + { + "code": "AO", + "name": "Aosta" + }, + { + "code": "AR", + "name": "Arezzo" + }, + { + "code": "AP", + "name": "Ascoli Piceno" + }, + { + "code": "AT", + "name": "Asti" + }, + { + "code": "AV", + "name": "Avellino" + }, + { + "code": "BA", + "name": "Bari" + }, + { + "code": "BT", + "name": "Barletta-Andria-Trani" + }, + { + "code": "BL", + "name": "Belluno" + }, + { + "code": "BN", + "name": "Benevento" + }, + { + "code": "BG", + "name": "Bergamo" + }, + { + "code": "BI", + "name": "Biella" + }, + { + "code": "BO", + "name": "Bologna" + }, + { + "code": "BZ", + "name": "Bolzano" + }, + { + "code": "BS", + "name": "Brescia" + }, + { + "code": "BR", + "name": "Brindisi" + }, + { + "code": "CA", + "name": "Cagliari" + }, + { + "code": "CL", + "name": "Caltanissetta" + }, + { + "code": "CB", + "name": "Campobasso" + }, + { + "code": "CE", + "name": "Caserta" + }, + { + "code": "CT", + "name": "Catania" + }, + { + "code": "CZ", + "name": "Catanzaro" + }, + { + "code": "CH", + "name": "Chieti" + }, + { + "code": "CO", + "name": "Como" + }, + { + "code": "CS", + "name": "Cosenza" + }, + { + "code": "CR", + "name": "Cremona" + }, + { + "code": "KR", + "name": "Crotone" + }, + { + "code": "CN", + "name": "Cuneo" + }, + { + "code": "EN", + "name": "Enna" + }, + { + "code": "FM", + "name": "Fermo" + }, + { + "code": "FE", + "name": "Ferrara" + }, + { + "code": "FI", + "name": "Firenze" + }, + { + "code": "FG", + "name": "Foggia" + }, + { + "code": "FC", + "name": "Forlì-Cesena" + }, + { + "code": "FR", + "name": "Frosinone" + }, + { + "code": "GE", + "name": "Genova" + }, + { + "code": "GO", + "name": "Gorizia" + }, + { + "code": "GR", + "name": "Grosseto" + }, + { + "code": "IM", + "name": "Imperia" + }, + { + "code": "IS", + "name": "Isernia" + }, + { + "code": "SP", + "name": "La Spezia" + }, + { + "code": "AQ", + "name": "L'Aquila" + }, + { + "code": "LT", + "name": "Latina" + }, + { + "code": "LE", + "name": "Lecce" + }, + { + "code": "LC", + "name": "Lecco" + }, + { + "code": "LI", + "name": "Livorno" + }, + { + "code": "LO", + "name": "Lodi" + }, + { + "code": "LU", + "name": "Lucca" + }, + { + "code": "MC", + "name": "Macerata" + }, + { + "code": "MN", + "name": "Mantova" + }, + { + "code": "MS", + "name": "Massa-Carrara" + }, + { + "code": "MT", + "name": "Matera" + }, + { + "code": "ME", + "name": "Messina" + }, + { + "code": "MI", + "name": "Milano" + }, + { + "code": "MO", + "name": "Modena" + }, + { + "code": "MB", + "name": "Monza e della Brianza" + }, + { + "code": "NA", + "name": "Napoli" + }, + { + "code": "NO", + "name": "Novara" + }, + { + "code": "NU", + "name": "Nuoro" + }, + { + "code": "OR", + "name": "Oristano" + }, + { + "code": "PD", + "name": "Padova" + }, + { + "code": "PA", + "name": "Palermo" + }, + { + "code": "PR", + "name": "Parma" + }, + { + "code": "PV", + "name": "Pavia" + }, + { + "code": "PG", + "name": "Perugia" + }, + { + "code": "PU", + "name": "Pesaro e Urbino" + }, + { + "code": "PE", + "name": "Pescara" + }, + { + "code": "PC", + "name": "Piacenza" + }, + { + "code": "PI", + "name": "Pisa" + }, + { + "code": "PT", + "name": "Pistoia" + }, + { + "code": "PN", + "name": "Pordenone" + }, + { + "code": "PZ", + "name": "Potenza" + }, + { + "code": "PO", + "name": "Prato" + }, + { + "code": "RG", + "name": "Ragusa" + }, + { + "code": "RA", + "name": "Ravenna" + }, + { + "code": "RC", + "name": "Reggio Calabria" + }, + { + "code": "RE", + "name": "Reggio Emilia" + }, + { + "code": "RI", + "name": "Rieti" + }, + { + "code": "RN", + "name": "Rimini" + }, + { + "code": "RM", + "name": "Roma" + }, + { + "code": "RO", + "name": "Rovigo" + }, + { + "code": "SA", + "name": "Salerno" + }, + { + "code": "SS", + "name": "Sassari" + }, + { + "code": "SV", + "name": "Savona" + }, + { + "code": "SI", + "name": "Siena" + }, + { + "code": "SR", + "name": "Siracusa" + }, + { + "code": "SO", + "name": "Sondrio" + }, + { + "code": "SU", + "name": "Sud Sardegna" + }, + { + "code": "TA", + "name": "Taranto" + }, + { + "code": "TE", + "name": "Teramo" + }, + { + "code": "TR", + "name": "Terni" + }, + { + "code": "TO", + "name": "Torino" + }, + { + "code": "TP", + "name": "Trapani" + }, + { + "code": "TN", + "name": "Trento" + }, + { + "code": "TV", + "name": "Treviso" + }, + { + "code": "TS", + "name": "Trieste" + }, + { + "code": "UD", + "name": "Udine" + }, + { + "code": "VA", + "name": "Varese" + }, + { + "code": "VE", + "name": "Venezia" + }, + { + "code": "VB", + "name": "Verbano-Cusio-Ossola" + }, + { + "code": "VC", + "name": "Vercelli" + }, + { + "code": "VR", + "name": "Verona" + }, + { + "code": "VV", + "name": "Vibo Valentia" + }, + { + "code": "VI", + "name": "Vicenza" + }, + { + "code": "VT", + "name": "Viterbo" + } + ] + }, + { + "code": "JE", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LI", + "name": "Swiss franc", + "currency_code": "CHF", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": "'", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LU", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LV", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MC", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MD", + "name": "Moldovan leu", + "currency_code": "MDL", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "C", + "name": "Chișinău" + }, + { + "code": "BL", + "name": "Bălți" + }, + { + "code": "AN", + "name": "Anenii Noi" + }, + { + "code": "BS", + "name": "Basarabeasca" + }, + { + "code": "BR", + "name": "Briceni" + }, + { + "code": "CH", + "name": "Cahul" + }, + { + "code": "CT", + "name": "Cantemir" + }, + { + "code": "CL", + "name": "Călărași" + }, + { + "code": "CS", + "name": "Căușeni" + }, + { + "code": "CM", + "name": "Cimișlia" + }, + { + "code": "CR", + "name": "Criuleni" + }, + { + "code": "DN", + "name": "Dondușeni" + }, + { + "code": "DR", + "name": "Drochia" + }, + { + "code": "DB", + "name": "Dubăsari" + }, + { + "code": "ED", + "name": "Edineț" + }, + { + "code": "FL", + "name": "Fălești" + }, + { + "code": "FR", + "name": "Florești" + }, + { + "code": "GE", + "name": "UTA Găgăuzia" + }, + { + "code": "GL", + "name": "Glodeni" + }, + { + "code": "HN", + "name": "Hîncești" + }, + { + "code": "IL", + "name": "Ialoveni" + }, + { + "code": "LV", + "name": "Leova" + }, + { + "code": "NS", + "name": "Nisporeni" + }, + { + "code": "OC", + "name": "Ocnița" + }, + { + "code": "OR", + "name": "Orhei" + }, + { + "code": "RZ", + "name": "Rezina" + }, + { + "code": "RS", + "name": "Rîșcani" + }, + { + "code": "SG", + "name": "Sîngerei" + }, + { + "code": "SR", + "name": "Soroca" + }, + { + "code": "ST", + "name": "Strășeni" + }, + { + "code": "SD", + "name": "Șoldănești" + }, + { + "code": "SV", + "name": "Ștefan Vodă" + }, + { + "code": "TR", + "name": "Taraclia" + }, + { + "code": "TL", + "name": "Telenești" + }, + { + "code": "UN", + "name": "Ungheni" + } + ] + }, + { + "code": "ME", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MK", + "name": "Macedonian denar", + "currency_code": "MKD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NL", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NO", + "name": "Norwegian krone", + "currency_code": "NOK", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PL", + "name": "Polish złoty", + "currency_code": "PLN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "RO", + "name": "Romanian leu", + "currency_code": "RON", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AB", + "name": "Alba" + }, + { + "code": "AR", + "name": "Arad" + }, + { + "code": "AG", + "name": "Argeș" + }, + { + "code": "BC", + "name": "Bacău" + }, + { + "code": "BH", + "name": "Bihor" + }, + { + "code": "BN", + "name": "Bistrița-Năsăud" + }, + { + "code": "BT", + "name": "Botoșani" + }, + { + "code": "BR", + "name": "Brăila" + }, + { + "code": "BV", + "name": "Brașov" + }, + { + "code": "B", + "name": "București" + }, + { + "code": "BZ", + "name": "Buzău" + }, + { + "code": "CL", + "name": "Călărași" + }, + { + "code": "CS", + "name": "Caraș-Severin" + }, + { + "code": "CJ", + "name": "Cluj" + }, + { + "code": "CT", + "name": "Constanța" + }, + { + "code": "CV", + "name": "Covasna" + }, + { + "code": "DB", + "name": "Dâmbovița" + }, + { + "code": "DJ", + "name": "Dolj" + }, + { + "code": "GL", + "name": "Galați" + }, + { + "code": "GR", + "name": "Giurgiu" + }, + { + "code": "GJ", + "name": "Gorj" + }, + { + "code": "HR", + "name": "Harghita" + }, + { + "code": "HD", + "name": "Hunedoara" + }, + { + "code": "IL", + "name": "Ialomița" + }, + { + "code": "IS", + "name": "Iași" + }, + { + "code": "IF", + "name": "Ilfov" + }, + { + "code": "MM", + "name": "Maramureș" + }, + { + "code": "MH", + "name": "Mehedinți" + }, + { + "code": "MS", + "name": "Mureș" + }, + { + "code": "NT", + "name": "Neamț" + }, + { + "code": "OT", + "name": "Olt" + }, + { + "code": "PH", + "name": "Prahova" + }, + { + "code": "SJ", + "name": "Sălaj" + }, + { + "code": "SM", + "name": "Satu Mare" + }, + { + "code": "SB", + "name": "Sibiu" + }, + { + "code": "SV", + "name": "Suceava" + }, + { + "code": "TR", + "name": "Teleorman" + }, + { + "code": "TM", + "name": "Timiș" + }, + { + "code": "TL", + "name": "Tulcea" + }, + { + "code": "VL", + "name": "Vâlcea" + }, + { + "code": "VS", + "name": "Vaslui" + }, + { + "code": "VN", + "name": "Vrancea" + } + ] + }, + { + "code": "RS", + "name": "Serbian dinar", + "currency_code": "RSD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "RS00", + "name": "Belgrade" + }, + { + "code": "RS14", + "name": "Bor" + }, + { + "code": "RS11", + "name": "Braničevo" + }, + { + "code": "RS02", + "name": "Central Banat" + }, + { + "code": "RS10", + "name": "Danube" + }, + { + "code": "RS23", + "name": "Jablanica" + }, + { + "code": "RS09", + "name": "Kolubara" + }, + { + "code": "RS08", + "name": "Mačva" + }, + { + "code": "RS17", + "name": "Morava" + }, + { + "code": "RS20", + "name": "Nišava" + }, + { + "code": "RS01", + "name": "North Bačka" + }, + { + "code": "RS03", + "name": "North Banat" + }, + { + "code": "RS24", + "name": "Pčinja" + }, + { + "code": "RS22", + "name": "Pirot" + }, + { + "code": "RS13", + "name": "Pomoravlje" + }, + { + "code": "RS19", + "name": "Rasina" + }, + { + "code": "RS18", + "name": "Raška" + }, + { + "code": "RS06", + "name": "South Bačka" + }, + { + "code": "RS04", + "name": "South Banat" + }, + { + "code": "RS07", + "name": "Srem" + }, + { + "code": "RS12", + "name": "Šumadija" + }, + { + "code": "RS21", + "name": "Toplica" + }, + { + "code": "RS05", + "name": "West Bačka" + }, + { + "code": "RS15", + "name": "Zaječar" + }, + { + "code": "RS16", + "name": "Zlatibor" + }, + { + "code": "RS25", + "name": "Kosovo" + }, + { + "code": "RS26", + "name": "Peć" + }, + { + "code": "RS27", + "name": "Prizren" + }, + { + "code": "RS28", + "name": "Kosovska Mitrovica" + }, + { + "code": "RS29", + "name": "Kosovo-Pomoravlje" + }, + { + "code": "RSKM", + "name": "Kosovo-Metohija" + }, + { + "code": "RSVO", + "name": "Vojvodina" + } + ] + }, + { + "code": "RU", + "name": "Russian ruble", + "currency_code": "RUB", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SE", + "name": "Swedish krona", + "currency_code": "SEK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SI", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SJ", + "name": "Norwegian krone", + "currency_code": "NOK", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SK", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SM", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TR", + "name": "Turkish lira", + "currency_code": "TRY", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "TR01", + "name": "Adana" + }, + { + "code": "TR02", + "name": "Adıyaman" + }, + { + "code": "TR03", + "name": "Afyon" + }, + { + "code": "TR04", + "name": "Ağrı" + }, + { + "code": "TR05", + "name": "Amasya" + }, + { + "code": "TR06", + "name": "Ankara" + }, + { + "code": "TR07", + "name": "Antalya" + }, + { + "code": "TR08", + "name": "Artvin" + }, + { + "code": "TR09", + "name": "Aydın" + }, + { + "code": "TR10", + "name": "Balıkesir" + }, + { + "code": "TR11", + "name": "Bilecik" + }, + { + "code": "TR12", + "name": "Bingöl" + }, + { + "code": "TR13", + "name": "Bitlis" + }, + { + "code": "TR14", + "name": "Bolu" + }, + { + "code": "TR15", + "name": "Burdur" + }, + { + "code": "TR16", + "name": "Bursa" + }, + { + "code": "TR17", + "name": "Çanakkale" + }, + { + "code": "TR18", + "name": "Çankırı" + }, + { + "code": "TR19", + "name": "Çorum" + }, + { + "code": "TR20", + "name": "Denizli" + }, + { + "code": "TR21", + "name": "Diyarbakır" + }, + { + "code": "TR22", + "name": "Edirne" + }, + { + "code": "TR23", + "name": "Elazığ" + }, + { + "code": "TR24", + "name": "Erzincan" + }, + { + "code": "TR25", + "name": "Erzurum" + }, + { + "code": "TR26", + "name": "Eskişehir" + }, + { + "code": "TR27", + "name": "Gaziantep" + }, + { + "code": "TR28", + "name": "Giresun" + }, + { + "code": "TR29", + "name": "Gümüşhane" + }, + { + "code": "TR30", + "name": "Hakkari" + }, + { + "code": "TR31", + "name": "Hatay" + }, + { + "code": "TR32", + "name": "Isparta" + }, + { + "code": "TR33", + "name": "İçel" + }, + { + "code": "TR34", + "name": "İstanbul" + }, + { + "code": "TR35", + "name": "İzmir" + }, + { + "code": "TR36", + "name": "Kars" + }, + { + "code": "TR37", + "name": "Kastamonu" + }, + { + "code": "TR38", + "name": "Kayseri" + }, + { + "code": "TR39", + "name": "Kırklareli" + }, + { + "code": "TR40", + "name": "Kırşehir" + }, + { + "code": "TR41", + "name": "Kocaeli" + }, + { + "code": "TR42", + "name": "Konya" + }, + { + "code": "TR43", + "name": "Kütahya" + }, + { + "code": "TR44", + "name": "Malatya" + }, + { + "code": "TR45", + "name": "Manisa" + }, + { + "code": "TR46", + "name": "Kahramanmaraş" + }, + { + "code": "TR47", + "name": "Mardin" + }, + { + "code": "TR48", + "name": "Muğla" + }, + { + "code": "TR49", + "name": "Muş" + }, + { + "code": "TR50", + "name": "Nevşehir" + }, + { + "code": "TR51", + "name": "Niğde" + }, + { + "code": "TR52", + "name": "Ordu" + }, + { + "code": "TR53", + "name": "Rize" + }, + { + "code": "TR54", + "name": "Sakarya" + }, + { + "code": "TR55", + "name": "Samsun" + }, + { + "code": "TR56", + "name": "Siirt" + }, + { + "code": "TR57", + "name": "Sinop" + }, + { + "code": "TR58", + "name": "Sivas" + }, + { + "code": "TR59", + "name": "Tekirdağ" + }, + { + "code": "TR60", + "name": "Tokat" + }, + { + "code": "TR61", + "name": "Trabzon" + }, + { + "code": "TR62", + "name": "Tunceli" + }, + { + "code": "TR63", + "name": "Şanlıurfa" + }, + { + "code": "TR64", + "name": "Uşak" + }, + { + "code": "TR65", + "name": "Van" + }, + { + "code": "TR66", + "name": "Yozgat" + }, + { + "code": "TR67", + "name": "Zonguldak" + }, + { + "code": "TR68", + "name": "Aksaray" + }, + { + "code": "TR69", + "name": "Bayburt" + }, + { + "code": "TR70", + "name": "Karaman" + }, + { + "code": "TR71", + "name": "Kırıkkale" + }, + { + "code": "TR72", + "name": "Batman" + }, + { + "code": "TR73", + "name": "Şırnak" + }, + { + "code": "TR74", + "name": "Bartın" + }, + { + "code": "TR75", + "name": "Ardahan" + }, + { + "code": "TR76", + "name": "Iğdır" + }, + { + "code": "TR77", + "name": "Yalova" + }, + { + "code": "TR78", + "name": "Karabük" + }, + { + "code": "TR79", + "name": "Kilis" + }, + { + "code": "TR80", + "name": "Osmaniye" + }, + { + "code": "TR81", + "name": "Düzce" + } + ] + }, + { + "code": "UA", + "name": "Ukrainian hryvnia", + "currency_code": "UAH", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "UA05", + "name": "Vinnychchyna" + }, + { + "code": "UA07", + "name": "Volyn" + }, + { + "code": "UA09", + "name": "Luhanshchyna" + }, + { + "code": "UA12", + "name": "Dnipropetrovshchyna" + }, + { + "code": "UA14", + "name": "Donechchyna" + }, + { + "code": "UA18", + "name": "Zhytomyrshchyna" + }, + { + "code": "UA21", + "name": "Zakarpattia" + }, + { + "code": "UA23", + "name": "Zaporizhzhya" + }, + { + "code": "UA26", + "name": "Prykarpattia" + }, + { + "code": "UA30", + "name": "Kyiv" + }, + { + "code": "UA32", + "name": "Kyivshchyna" + }, + { + "code": "UA35", + "name": "Kirovohradschyna" + }, + { + "code": "UA40", + "name": "Sevastopol" + }, + { + "code": "UA43", + "name": "Crimea" + }, + { + "code": "UA46", + "name": "Lvivshchyna" + }, + { + "code": "UA48", + "name": "Mykolayivschyna" + }, + { + "code": "UA51", + "name": "Odeshchyna" + }, + { + "code": "UA53", + "name": "Poltavshchyna" + }, + { + "code": "UA56", + "name": "Rivnenshchyna" + }, + { + "code": "UA59", + "name": "Sumshchyna" + }, + { + "code": "UA61", + "name": "Ternopilshchyna" + }, + { + "code": "UA63", + "name": "Kharkivshchyna" + }, + { + "code": "UA65", + "name": "Khersonshchyna" + }, + { + "code": "UA68", + "name": "Khmelnychchyna" + }, + { + "code": "UA71", + "name": "Cherkashchyna" + }, + { + "code": "UA74", + "name": "Chernihivshchyna" + }, + { + "code": "UA77", + "name": "Chernivtsi Oblast" + } + ] + }, + { + "code": "VA", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + } + ], + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "NA", + "name": "North America", + "countries": [{ + "code": "AG", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AI", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AW", + "name": "Aruban florin", + "currency_code": "AWG", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BB", + "name": "Barbadian dollar", + "currency_code": "BBD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BL", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BM", + "name": "Bermudian dollar", + "currency_code": "BMD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BQ", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BS", + "name": "Bahamian dollar", + "currency_code": "BSD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BZ", + "name": "Belize dollar", + "currency_code": "BZD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CA", + "name": "Canadian dollar", + "currency_code": "CAD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "AB", + "name": "Alberta" + }, + { + "code": "BC", + "name": "British Columbia" + }, + { + "code": "MB", + "name": "Manitoba" + }, + { + "code": "NB", + "name": "New Brunswick" + }, + { + "code": "NL", + "name": "Newfoundland and Labrador" + }, + { + "code": "NT", + "name": "Northwest Territories" + }, + { + "code": "NS", + "name": "Nova Scotia" + }, + { + "code": "NU", + "name": "Nunavut" + }, + { + "code": "ON", + "name": "Ontario" + }, + { + "code": "PE", + "name": "Prince Edward Island" + }, + { + "code": "QC", + "name": "Quebec" + }, + { + "code": "SK", + "name": "Saskatchewan" + }, + { + "code": "YT", + "name": "Yukon Territory" + } + ] + }, + { + "code": "CR", + "name": "Costa Rican colón", + "currency_code": "CRC", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "CR-A", + "name": "Alajuela" + }, + { + "code": "CR-C", + "name": "Cartago" + }, + { + "code": "CR-G", + "name": "Guanacaste" + }, + { + "code": "CR-H", + "name": "Heredia" + }, + { + "code": "CR-L", + "name": "Limón" + }, + { + "code": "CR-P", + "name": "Puntarenas" + }, + { + "code": "CR-SJ", + "name": "San José" + } + ] + }, + { + "code": "CU", + "name": "Cuban convertible peso", + "currency_code": "CUC", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CW", + "name": "Netherlands Antillean guilder", + "currency_code": "ANG", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "DM", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "DO", + "name": "Dominican peso", + "currency_code": "DOP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "DO-01", + "name": "Distrito Nacional" + }, + { + "code": "DO-02", + "name": "Azua" + }, + { + "code": "DO-03", + "name": "Baoruco" + }, + { + "code": "DO-04", + "name": "Barahona" + }, + { + "code": "DO-33", + "name": "Cibao Nordeste" + }, + { + "code": "DO-34", + "name": "Cibao Noroeste" + }, + { + "code": "DO-35", + "name": "Cibao Norte" + }, + { + "code": "DO-36", + "name": "Cibao Sur" + }, + { + "code": "DO-05", + "name": "Dajabón" + }, + { + "code": "DO-06", + "name": "Duarte" + }, + { + "code": "DO-08", + "name": "El Seibo" + }, + { + "code": "DO-37", + "name": "El Valle" + }, + { + "code": "DO-07", + "name": "Elías Piña" + }, + { + "code": "DO-38", + "name": "Enriquillo" + }, + { + "code": "DO-09", + "name": "Espaillat" + }, + { + "code": "DO-30", + "name": "Hato Mayor" + }, + { + "code": "DO-19", + "name": "Hermanas Mirabal" + }, + { + "code": "DO-39", + "name": "Higüamo" + }, + { + "code": "DO-10", + "name": "Independencia" + }, + { + "code": "DO-11", + "name": "La Altagracia" + }, + { + "code": "DO-12", + "name": "La Romana" + }, + { + "code": "DO-13", + "name": "La Vega" + }, + { + "code": "DO-14", + "name": "María Trinidad Sánchez" + }, + { + "code": "DO-28", + "name": "Monseñor Nouel" + }, + { + "code": "DO-15", + "name": "Monte Cristi" + }, + { + "code": "DO-29", + "name": "Monte Plata" + }, + { + "code": "DO-40", + "name": "Ozama" + }, + { + "code": "DO-16", + "name": "Pedernales" + }, + { + "code": "DO-17", + "name": "Peravia" + }, + { + "code": "DO-18", + "name": "Puerto Plata" + }, + { + "code": "DO-20", + "name": "Samaná" + }, + { + "code": "DO-21", + "name": "San Cristóbal" + }, + { + "code": "DO-31", + "name": "San José de Ocoa" + }, + { + "code": "DO-22", + "name": "San Juan" + }, + { + "code": "DO-23", + "name": "San Pedro de Macorís" + }, + { + "code": "DO-24", + "name": "Sánchez Ramírez" + }, + { + "code": "DO-25", + "name": "Santiago" + }, + { + "code": "DO-26", + "name": "Santiago Rodríguez" + }, + { + "code": "DO-32", + "name": "Santo Domingo" + }, + { + "code": "DO-41", + "name": "Valdesia" + }, + { + "code": "DO-27", + "name": "Valverde" + }, + { + "code": "DO-42", + "name": "Yuma" + } + ] + }, + { + "code": "GD", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GL", + "name": "Danish krone", + "currency_code": "DKK", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GP", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GT", + "name": "Guatemalan quetzal", + "currency_code": "GTQ", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "GT-AV", + "name": "Alta Verapaz" + }, + { + "code": "GT-BV", + "name": "Baja Verapaz" + }, + { + "code": "GT-CM", + "name": "Chimaltenango" + }, + { + "code": "GT-CQ", + "name": "Chiquimula" + }, + { + "code": "GT-PR", + "name": "El Progreso" + }, + { + "code": "GT-ES", + "name": "Escuintla" + }, + { + "code": "GT-GU", + "name": "Guatemala" + }, + { + "code": "GT-HU", + "name": "Huehuetenango" + }, + { + "code": "GT-IZ", + "name": "Izabal" + }, + { + "code": "GT-JA", + "name": "Jalapa" + }, + { + "code": "GT-JU", + "name": "Jutiapa" + }, + { + "code": "GT-PE", + "name": "Petén" + }, + { + "code": "GT-QZ", + "name": "Quetzaltenango" + }, + { + "code": "GT-QC", + "name": "Quiché" + }, + { + "code": "GT-RE", + "name": "Retalhuleu" + }, + { + "code": "GT-SA", + "name": "Sacatepéquez" + }, + { + "code": "GT-SM", + "name": "San Marcos" + }, + { + "code": "GT-SR", + "name": "Santa Rosa" + }, + { + "code": "GT-SO", + "name": "Sololá" + }, + { + "code": "GT-SU", + "name": "Suchitepéquez" + }, + { + "code": "GT-TO", + "name": "Totonicapán" + }, + { + "code": "GT-ZA", + "name": "Zacapa" + } + ] + }, + { + "code": "HN", + "name": "Honduran lempira", + "currency_code": "HNL", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "HN-AT", + "name": "Atlántida" + }, + { + "code": "HN-IB", + "name": "Bay Islands" + }, + { + "code": "HN-CH", + "name": "Choluteca" + }, + { + "code": "HN-CL", + "name": "Colón" + }, + { + "code": "HN-CM", + "name": "Comayagua" + }, + { + "code": "HN-CP", + "name": "Copán" + }, + { + "code": "HN-CR", + "name": "Cortés" + }, + { + "code": "HN-EP", + "name": "El Paraíso" + }, + { + "code": "HN-FM", + "name": "Francisco Morazán" + }, + { + "code": "HN-GD", + "name": "Gracias a Dios" + }, + { + "code": "HN-IN", + "name": "Intibucá" + }, + { + "code": "HN-LE", + "name": "Lempira" + }, + { + "code": "HN-LP", + "name": "La Paz" + }, + { + "code": "HN-OC", + "name": "Ocotepeque" + }, + { + "code": "HN-OL", + "name": "Olancho" + }, + { + "code": "HN-SB", + "name": "Santa Bárbara" + }, + { + "code": "HN-VA", + "name": "Valle" + }, + { + "code": "HN-YO", + "name": "Yoro" + } + ] + }, + { + "code": "HT", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "JM", + "name": "Jamaican dollar", + "currency_code": "JMD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "JM-01", + "name": "Kingston" + }, + { + "code": "JM-02", + "name": "Saint Andrew" + }, + { + "code": "JM-03", + "name": "Saint Thomas" + }, + { + "code": "JM-04", + "name": "Portland" + }, + { + "code": "JM-05", + "name": "Saint Mary" + }, + { + "code": "JM-06", + "name": "Saint Ann" + }, + { + "code": "JM-07", + "name": "Trelawny" + }, + { + "code": "JM-08", + "name": "Saint James" + }, + { + "code": "JM-09", + "name": "Hanover" + }, + { + "code": "JM-10", + "name": "Westmoreland" + }, + { + "code": "JM-11", + "name": "Saint Elizabeth" + }, + { + "code": "JM-12", + "name": "Manchester" + }, + { + "code": "JM-13", + "name": "Clarendon" + }, + { + "code": "JM-14", + "name": "Saint Catherine" + } + ] + }, + { + "code": "KN", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KY", + "name": "Cayman Islands dollar", + "currency_code": "KYD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LC", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MF", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MQ", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MS", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MX", + "name": "Mexican peso", + "currency_code": "MXN", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "DF", + "name": "Ciudad de México" + }, + { + "code": "JA", + "name": "Jalisco" + }, + { + "code": "NL", + "name": "Nuevo León" + }, + { + "code": "AG", + "name": "Aguascalientes" + }, + { + "code": "BC", + "name": "Baja California" + }, + { + "code": "BS", + "name": "Baja California Sur" + }, + { + "code": "CM", + "name": "Campeche" + }, + { + "code": "CS", + "name": "Chiapas" + }, + { + "code": "CH", + "name": "Chihuahua" + }, + { + "code": "CO", + "name": "Coahuila" + }, + { + "code": "CL", + "name": "Colima" + }, + { + "code": "DG", + "name": "Durango" + }, + { + "code": "GT", + "name": "Guanajuato" + }, + { + "code": "GR", + "name": "Guerrero" + }, + { + "code": "HG", + "name": "Hidalgo" + }, + { + "code": "MX", + "name": "Estado de México" + }, + { + "code": "MI", + "name": "Michoacán" + }, + { + "code": "MO", + "name": "Morelos" + }, + { + "code": "NA", + "name": "Nayarit" + }, + { + "code": "OA", + "name": "Oaxaca" + }, + { + "code": "PU", + "name": "Puebla" + }, + { + "code": "QT", + "name": "Querétaro" + }, + { + "code": "QR", + "name": "Quintana Roo" + }, + { + "code": "SL", + "name": "San Luis Potosí" + }, + { + "code": "SI", + "name": "Sinaloa" + }, + { + "code": "SO", + "name": "Sonora" + }, + { + "code": "TB", + "name": "Tabasco" + }, + { + "code": "TM", + "name": "Tamaulipas" + }, + { + "code": "TL", + "name": "Tlaxcala" + }, + { + "code": "VE", + "name": "Veracruz" + }, + { + "code": "YU", + "name": "Yucatán" + }, + { + "code": "ZA", + "name": "Zacatecas" + } + ] + }, + { + "code": "NI", + "name": "Nicaraguan córdoba", + "currency_code": "NIO", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "NI-AN", + "name": "Atlántico Norte" + }, + { + "code": "NI-AS", + "name": "Atlántico Sur" + }, + { + "code": "NI-BO", + "name": "Boaco" + }, + { + "code": "NI-CA", + "name": "Carazo" + }, + { + "code": "NI-CI", + "name": "Chinandega" + }, + { + "code": "NI-CO", + "name": "Chontales" + }, + { + "code": "NI-ES", + "name": "Estelí" + }, + { + "code": "NI-GR", + "name": "Granada" + }, + { + "code": "NI-JI", + "name": "Jinotega" + }, + { + "code": "NI-LE", + "name": "León" + }, + { + "code": "NI-MD", + "name": "Madriz" + }, + { + "code": "NI-MN", + "name": "Managua" + }, + { + "code": "NI-MS", + "name": "Masaya" + }, + { + "code": "NI-MT", + "name": "Matagalpa" + }, + { + "code": "NI-NS", + "name": "Nueva Segovia" + }, + { + "code": "NI-RI", + "name": "Rivas" + }, + { + "code": "NI-SJ", + "name": "Río San Juan" + } + ] + }, + { + "code": "PA", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "PA-1", + "name": "Bocas del Toro" + }, + { + "code": "PA-2", + "name": "Coclé" + }, + { + "code": "PA-3", + "name": "Colón" + }, + { + "code": "PA-4", + "name": "Chiriquí" + }, + { + "code": "PA-5", + "name": "Darién" + }, + { + "code": "PA-6", + "name": "Herrera" + }, + { + "code": "PA-7", + "name": "Los Santos" + }, + { + "code": "PA-8", + "name": "Panamá" + }, + { + "code": "PA-9", + "name": "Veraguas" + }, + { + "code": "PA-10", + "name": "West Panamá" + }, + { + "code": "PA-EM", + "name": "Emberá" + }, + { + "code": "PA-KY", + "name": "Guna Yala" + }, + { + "code": "PA-NB", + "name": "Ngöbe-Buglé" + } + ] + }, + { + "code": "PM", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PR", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SV", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "SV-AH", + "name": "Ahuachapán" + }, + { + "code": "SV-CA", + "name": "Cabañas" + }, + { + "code": "SV-CH", + "name": "Chalatenango" + }, + { + "code": "SV-CU", + "name": "Cuscatlán" + }, + { + "code": "SV-LI", + "name": "La Libertad" + }, + { + "code": "SV-MO", + "name": "Morazán" + }, + { + "code": "SV-PA", + "name": "La Paz" + }, + { + "code": "SV-SA", + "name": "Santa Ana" + }, + { + "code": "SV-SM", + "name": "San Miguel" + }, + { + "code": "SV-SO", + "name": "Sonsonate" + }, + { + "code": "SV-SS", + "name": "San Salvador" + }, + { + "code": "SV-SV", + "name": "San Vicente" + }, + { + "code": "SV-UN", + "name": "La Unión" + }, + { + "code": "SV-US", + "name": "Usulután" + } + ] + }, + { + "code": "SX", + "name": "Netherlands Antillean guilder", + "currency_code": "ANG", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TC", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TT", + "name": "Trinidad and Tobago dollar", + "currency_code": "TTD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "US", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "foot", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "oz", + "states": [{ + "code": "AL", + "name": "Alabama" + }, + { + "code": "AK", + "name": "Alaska" + }, + { + "code": "AZ", + "name": "Arizona" + }, + { + "code": "AR", + "name": "Arkansas" + }, + { + "code": "CA", + "name": "California" + }, + { + "code": "CO", + "name": "Colorado" + }, + { + "code": "CT", + "name": "Connecticut" + }, + { + "code": "DE", + "name": "Delaware" + }, + { + "code": "DC", + "name": "District Of Columbia" + }, + { + "code": "FL", + "name": "Florida" + }, + { + "code": "GA", + "name": "Georgia" + }, + { + "code": "HI", + "name": "Hawaii" + }, + { + "code": "ID", + "name": "Idaho" + }, + { + "code": "IL", + "name": "Illinois" + }, + { + "code": "IN", + "name": "Indiana" + }, + { + "code": "IA", + "name": "Iowa" + }, + { + "code": "KS", + "name": "Kansas" + }, + { + "code": "KY", + "name": "Kentucky" + }, + { + "code": "LA", + "name": "Louisiana" + }, + { + "code": "ME", + "name": "Maine" + }, + { + "code": "MD", + "name": "Maryland" + }, + { + "code": "MA", + "name": "Massachusetts" + }, + { + "code": "MI", + "name": "Michigan" + }, + { + "code": "MN", + "name": "Minnesota" + }, + { + "code": "MS", + "name": "Mississippi" + }, + { + "code": "MO", + "name": "Missouri" + }, + { + "code": "MT", + "name": "Montana" + }, + { + "code": "NE", + "name": "Nebraska" + }, + { + "code": "NV", + "name": "Nevada" + }, + { + "code": "NH", + "name": "New Hampshire" + }, + { + "code": "NJ", + "name": "New Jersey" + }, + { + "code": "NM", + "name": "New Mexico" + }, + { + "code": "NY", + "name": "New York" + }, + { + "code": "NC", + "name": "North Carolina" + }, + { + "code": "ND", + "name": "North Dakota" + }, + { + "code": "OH", + "name": "Ohio" + }, + { + "code": "OK", + "name": "Oklahoma" + }, + { + "code": "OR", + "name": "Oregon" + }, + { + "code": "PA", + "name": "Pennsylvania" + }, + { + "code": "RI", + "name": "Rhode Island" + }, + { + "code": "SC", + "name": "South Carolina" + }, + { + "code": "SD", + "name": "South Dakota" + }, + { + "code": "TN", + "name": "Tennessee" + }, + { + "code": "TX", + "name": "Texas" + }, + { + "code": "UT", + "name": "Utah" + }, + { + "code": "VT", + "name": "Vermont" + }, + { + "code": "VA", + "name": "Virginia" + }, + { + "code": "WA", + "name": "Washington" + }, + { + "code": "WV", + "name": "West Virginia" + }, + { + "code": "WI", + "name": "Wisconsin" + }, + { + "code": "WY", + "name": "Wyoming" + }, + { + "code": "AA", + "name": "Armed Forces (AA)" + }, + { + "code": "AE", + "name": "Armed Forces (AE)" + }, + { + "code": "AP", + "name": "Armed Forces (AP)" + } + ] + }, + { + "code": "VC", + "name": "East Caribbean dollar", + "currency_code": "XCD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "VG", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "VI", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + } + ], + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "OC", + "name": "Oceania", + "countries": [{ + "code": "AS", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AU", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "ACT", + "name": "Australian Capital Territory" + }, + { + "code": "NSW", + "name": "New South Wales" + }, + { + "code": "NT", + "name": "Northern Territory" + }, + { + "code": "QLD", + "name": "Queensland" + }, + { + "code": "SA", + "name": "South Australia" + }, + { + "code": "TAS", + "name": "Tasmania" + }, + { + "code": "VIC", + "name": "Victoria" + }, + { + "code": "WA", + "name": "Western Australia" + } + ] + }, + { + "code": "CK", + "name": "New Zealand dollar", + "currency_code": "NZD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "FJ", + "name": "Fijian dollar", + "currency_code": "FJD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "FM", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GU", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "KI", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MH", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MP", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NC", + "name": "CFP franc", + "currency_code": "XPF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NF", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NR", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NU", + "name": "New Zealand dollar", + "currency_code": "NZD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NZ", + "name": "New Zealand dollar", + "currency_code": "NZD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "NTL", + "name": "Northland" + }, + { + "code": "AUK", + "name": "Auckland" + }, + { + "code": "WKO", + "name": "Waikato" + }, + { + "code": "BOP", + "name": "Bay of Plenty" + }, + { + "code": "TKI", + "name": "Taranaki" + }, + { + "code": "GIS", + "name": "Gisborne" + }, + { + "code": "HKB", + "name": "Hawke’s Bay" + }, + { + "code": "MWT", + "name": "Manawatu-Wanganui" + }, + { + "code": "WGN", + "name": "Wellington" + }, + { + "code": "NSN", + "name": "Nelson" + }, + { + "code": "MBH", + "name": "Marlborough" + }, + { + "code": "TAS", + "name": "Tasman" + }, + { + "code": "WTC", + "name": "West Coast" + }, + { + "code": "CAN", + "name": "Canterbury" + }, + { + "code": "OTA", + "name": "Otago" + }, + { + "code": "STL", + "name": "Southland" + } + ] + }, + { + "code": "PF", + "name": "CFP franc", + "currency_code": "XPF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PG", + "name": "Papua New Guinean kina", + "currency_code": "PGK", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PN", + "name": "New Zealand dollar", + "currency_code": "NZD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PW", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SB", + "name": "Solomon Islands dollar", + "currency_code": "SBD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TK", + "name": "New Zealand dollar", + "currency_code": "NZD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TO", + "name": "Tongan paʻanga", + "currency_code": "TOP", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TV", + "name": "Australian dollar", + "currency_code": "AUD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "UM", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": 81, + "name": "Baker Island" + }, + { + "code": 84, + "name": "Howland Island" + }, + { + "code": 86, + "name": "Jarvis Island" + }, + { + "code": 67, + "name": "Johnston Atoll" + }, + { + "code": 89, + "name": "Kingman Reef" + }, + { + "code": 71, + "name": "Midway Atoll" + }, + { + "code": 76, + "name": "Navassa Island" + }, + { + "code": 95, + "name": "Palmyra Atoll" + }, + { + "code": 79, + "name": "Wake Island" + } + ] + }, + { + "code": "VU", + "name": "Vanuatu vatu", + "currency_code": "VUV", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "WF", + "name": "CFP franc", + "currency_code": "XPF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "WS", + "name": "Samoan tālā", + "currency_code": "WST", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + } + ], + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "code": "SA", + "name": "South America", + "countries": [{ + "code": "AR", + "name": "Argentine peso", + "currency_code": "ARS", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "C", + "name": "Ciudad Autónoma de Buenos Aires" + }, + { + "code": "B", + "name": "Buenos Aires" + }, + { + "code": "K", + "name": "Catamarca" + }, + { + "code": "H", + "name": "Chaco" + }, + { + "code": "U", + "name": "Chubut" + }, + { + "code": "X", + "name": "Córdoba" + }, + { + "code": "W", + "name": "Corrientes" + }, + { + "code": "E", + "name": "Entre Ríos" + }, + { + "code": "P", + "name": "Formosa" + }, + { + "code": "Y", + "name": "Jujuy" + }, + { + "code": "L", + "name": "La Pampa" + }, + { + "code": "F", + "name": "La Rioja" + }, + { + "code": "M", + "name": "Mendoza" + }, + { + "code": "N", + "name": "Misiones" + }, + { + "code": "Q", + "name": "Neuquén" + }, + { + "code": "R", + "name": "Río Negro" + }, + { + "code": "A", + "name": "Salta" + }, + { + "code": "J", + "name": "San Juan" + }, + { + "code": "D", + "name": "San Luis" + }, + { + "code": "Z", + "name": "Santa Cruz" + }, + { + "code": "S", + "name": "Santa Fe" + }, + { + "code": "G", + "name": "Santiago del Estero" + }, + { + "code": "V", + "name": "Tierra del Fuego" + }, + { + "code": "T", + "name": "Tucumán" + } + ] + }, + { + "code": "BO", + "name": "Bolivian boliviano", + "currency_code": "BOB", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "BO-B", + "name": "Beni" + }, + { + "code": "BO-H", + "name": "Chuquisaca" + }, + { + "code": "BO-C", + "name": "Cochabamba" + }, + { + "code": "BO-L", + "name": "La Paz" + }, + { + "code": "BO-O", + "name": "Oruro" + }, + { + "code": "BO-N", + "name": "Pando" + }, + { + "code": "BO-P", + "name": "Potosí" + }, + { + "code": "BO-S", + "name": "Santa Cruz" + }, + { + "code": "BO-T", + "name": "Tarija" + } + ] + }, + { + "code": "BR", + "name": "Brazilian real", + "currency_code": "BRL", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AC", + "name": "Acre" + }, + { + "code": "AL", + "name": "Alagoas" + }, + { + "code": "AP", + "name": "Amapá" + }, + { + "code": "AM", + "name": "Amazonas" + }, + { + "code": "BA", + "name": "Bahia" + }, + { + "code": "CE", + "name": "Ceará" + }, + { + "code": "DF", + "name": "Distrito Federal" + }, + { + "code": "ES", + "name": "Espírito Santo" + }, + { + "code": "GO", + "name": "Goiás" + }, + { + "code": "MA", + "name": "Maranhão" + }, + { + "code": "MT", + "name": "Mato Grosso" + }, + { + "code": "MS", + "name": "Mato Grosso do Sul" + }, + { + "code": "MG", + "name": "Minas Gerais" + }, + { + "code": "PA", + "name": "Pará" + }, + { + "code": "PB", + "name": "Paraíba" + }, + { + "code": "PR", + "name": "Paraná" + }, + { + "code": "PE", + "name": "Pernambuco" + }, + { + "code": "PI", + "name": "Piauí" + }, + { + "code": "RJ", + "name": "Rio de Janeiro" + }, + { + "code": "RN", + "name": "Rio Grande do Norte" + }, + { + "code": "RS", + "name": "Rio Grande do Sul" + }, + { + "code": "RO", + "name": "Rondônia" + }, + { + "code": "RR", + "name": "Roraima" + }, + { + "code": "SC", + "name": "Santa Catarina" + }, + { + "code": "SP", + "name": "São Paulo" + }, + { + "code": "SE", + "name": "Sergipe" + }, + { + "code": "TO", + "name": "Tocantins" + } + ] + }, + { + "code": "CL", + "name": "Chilean peso", + "currency_code": "CLP", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "CL-AI", + "name": "Aisén del General Carlos Ibañez del Campo" + }, + { + "code": "CL-AN", + "name": "Antofagasta" + }, + { + "code": "CL-AP", + "name": "Arica y Parinacota" + }, + { + "code": "CL-AR", + "name": "La Araucanía" + }, + { + "code": "CL-AT", + "name": "Atacama" + }, + { + "code": "CL-BI", + "name": "Biobío" + }, + { + "code": "CL-CO", + "name": "Coquimbo" + }, + { + "code": "CL-LI", + "name": "Libertador General Bernardo O'Higgins" + }, + { + "code": "CL-LL", + "name": "Los Lagos" + }, + { + "code": "CL-LR", + "name": "Los Ríos" + }, + { + "code": "CL-MA", + "name": "Magallanes" + }, + { + "code": "CL-ML", + "name": "Maule" + }, + { + "code": "CL-NB", + "name": "Ñuble" + }, + { + "code": "CL-RM", + "name": "Región Metropolitana de Santiago" + }, + { + "code": "CL-TA", + "name": "Tarapacá" + }, + { + "code": "CL-VS", + "name": "Valparaíso" + } + ] + }, + { + "code": "CO", + "name": "Colombian peso", + "currency_code": "COP", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "CO-AMA", + "name": "Amazonas" + }, + { + "code": "CO-ANT", + "name": "Antioquia" + }, + { + "code": "CO-ARA", + "name": "Arauca" + }, + { + "code": "CO-ATL", + "name": "Atlántico" + }, + { + "code": "CO-BOL", + "name": "Bolívar" + }, + { + "code": "CO-BOY", + "name": "Boyacá" + }, + { + "code": "CO-CAL", + "name": "Caldas" + }, + { + "code": "CO-CAQ", + "name": "Caquetá" + }, + { + "code": "CO-CAS", + "name": "Casanare" + }, + { + "code": "CO-CAU", + "name": "Cauca" + }, + { + "code": "CO-CES", + "name": "Cesar" + }, + { + "code": "CO-CHO", + "name": "Chocó" + }, + { + "code": "CO-COR", + "name": "Córdoba" + }, + { + "code": "CO-CUN", + "name": "Cundinamarca" + }, + { + "code": "CO-DC", + "name": "Capital District" + }, + { + "code": "CO-GUA", + "name": "Guainía" + }, + { + "code": "CO-GUV", + "name": "Guaviare" + }, + { + "code": "CO-HUI", + "name": "Huila" + }, + { + "code": "CO-LAG", + "name": "La Guajira" + }, + { + "code": "CO-MAG", + "name": "Magdalena" + }, + { + "code": "CO-MET", + "name": "Meta" + }, + { + "code": "CO-NAR", + "name": "Nariño" + }, + { + "code": "CO-NSA", + "name": "Norte de Santander" + }, + { + "code": "CO-PUT", + "name": "Putumayo" + }, + { + "code": "CO-QUI", + "name": "Quindío" + }, + { + "code": "CO-RIS", + "name": "Risaralda" + }, + { + "code": "CO-SAN", + "name": "Santander" + }, + { + "code": "CO-SAP", + "name": "San Andrés & Providencia" + }, + { + "code": "CO-SUC", + "name": "Sucre" + }, + { + "code": "CO-TOL", + "name": "Tolima" + }, + { + "code": "CO-VAC", + "name": "Valle del Cauca" + }, + { + "code": "CO-VAU", + "name": "Vaupés" + }, + { + "code": "CO-VID", + "name": "Vichada" + } + ] + }, + { + "code": "EC", + "name": "United States (US) dollar", + "currency_code": "USD", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "EC-A", + "name": "Azuay" + }, + { + "code": "EC-B", + "name": "Bolívar" + }, + { + "code": "EC-F", + "name": "Cañar" + }, + { + "code": "EC-C", + "name": "Carchi" + }, + { + "code": "EC-H", + "name": "Chimborazo" + }, + { + "code": "EC-X", + "name": "Cotopaxi" + }, + { + "code": "EC-O", + "name": "El Oro" + }, + { + "code": "EC-E", + "name": "Esmeraldas" + }, + { + "code": "EC-W", + "name": "Galápagos" + }, + { + "code": "EC-G", + "name": "Guayas" + }, + { + "code": "EC-I", + "name": "Imbabura" + }, + { + "code": "EC-L", + "name": "Loja" + }, + { + "code": "EC-R", + "name": "Los Ríos" + }, + { + "code": "EC-M", + "name": "Manabí" + }, + { + "code": "EC-S", + "name": "Morona-Santiago" + }, + { + "code": "EC-N", + "name": "Napo" + }, + { + "code": "EC-D", + "name": "Orellana" + }, + { + "code": "EC-Y", + "name": "Pastaza" + }, + { + "code": "EC-P", + "name": "Pichincha" + }, + { + "code": "EC-SE", + "name": "Santa Elena" + }, + { + "code": "EC-SD", + "name": "Santo Domingo de los Tsáchilas" + }, + { + "code": "EC-U", + "name": "Sucumbíos" + }, + { + "code": "EC-T", + "name": "Tungurahua" + }, + { + "code": "EC-Z", + "name": "Zamora-Chinchipe" + } + ] + }, + { + "code": "FK", + "name": "Falkland Islands pound", + "currency_code": "FKP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GF", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GY", + "name": "Guyanese dollar", + "currency_code": "GYD", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PE", + "name": "Sol", + "currency_code": "PEN", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "CAL", + "name": "El Callao" + }, + { + "code": "LMA", + "name": "Municipalidad Metropolitana de Lima" + }, + { + "code": "AMA", + "name": "Amazonas" + }, + { + "code": "ANC", + "name": "Ancash" + }, + { + "code": "APU", + "name": "Apurímac" + }, + { + "code": "ARE", + "name": "Arequipa" + }, + { + "code": "AYA", + "name": "Ayacucho" + }, + { + "code": "CAJ", + "name": "Cajamarca" + }, + { + "code": "CUS", + "name": "Cusco" + }, + { + "code": "HUV", + "name": "Huancavelica" + }, + { + "code": "HUC", + "name": "Huánuco" + }, + { + "code": "ICA", + "name": "Ica" + }, + { + "code": "JUN", + "name": "Junín" + }, + { + "code": "LAL", + "name": "La Libertad" + }, + { + "code": "LAM", + "name": "Lambayeque" + }, + { + "code": "LIM", + "name": "Lima" + }, + { + "code": "LOR", + "name": "Loreto" + }, + { + "code": "MDD", + "name": "Madre de Dios" + }, + { + "code": "MOQ", + "name": "Moquegua" + }, + { + "code": "PAS", + "name": "Pasco" + }, + { + "code": "PIU", + "name": "Piura" + }, + { + "code": "PUN", + "name": "Puno" + }, + { + "code": "SAM", + "name": "San Martín" + }, + { + "code": "TAC", + "name": "Tacna" + }, + { + "code": "TUM", + "name": "Tumbes" + }, + { + "code": "UCA", + "name": "Ucayali" + } + ] + }, + { + "code": "PY", + "name": "Paraguayan guaraní", + "currency_code": "PYG", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "PY-ASU", + "name": "Asunción" + }, + { + "code": "PY-1", + "name": "Concepción" + }, + { + "code": "PY-2", + "name": "San Pedro" + }, + { + "code": "PY-3", + "name": "Cordillera" + }, + { + "code": "PY-4", + "name": "Guairá" + }, + { + "code": "PY-5", + "name": "Caaguazú" + }, + { + "code": "PY-6", + "name": "Caazapá" + }, + { + "code": "PY-7", + "name": "Itapúa" + }, + { + "code": "PY-8", + "name": "Misiones" + }, + { + "code": "PY-9", + "name": "Paraguarí" + }, + { + "code": "PY-10", + "name": "Alto Paraná" + }, + { + "code": "PY-11", + "name": "Central" + }, + { + "code": "PY-12", + "name": "Ñeembucú" + }, + { + "code": "PY-13", + "name": "Amambay" + }, + { + "code": "PY-14", + "name": "Canindeyú" + }, + { + "code": "PY-15", + "name": "Presidente Hayes" + }, + { + "code": "PY-16", + "name": "Alto Paraguay" + }, + { + "code": "PY-17", + "name": "Boquerón" + } + ] + }, + { + "code": "SR", + "name": "Surinamese dollar", + "currency_code": "SRD", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "UY", + "name": "Uruguayan peso", + "currency_code": "UYU", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "UY-AR", + "name": "Artigas" + }, + { + "code": "UY-CA", + "name": "Canelones" + }, + { + "code": "UY-CL", + "name": "Cerro Largo" + }, + { + "code": "UY-CO", + "name": "Colonia" + }, + { + "code": "UY-DU", + "name": "Durazno" + }, + { + "code": "UY-FS", + "name": "Flores" + }, + { + "code": "UY-FD", + "name": "Florida" + }, + { + "code": "UY-LA", + "name": "Lavalleja" + }, + { + "code": "UY-MA", + "name": "Maldonado" + }, + { + "code": "UY-MO", + "name": "Montevideo" + }, + { + "code": "UY-PA", + "name": "Paysandú" + }, + { + "code": "UY-RN", + "name": "Río Negro" + }, + { + "code": "UY-RV", + "name": "Rivera" + }, + { + "code": "UY-RO", + "name": "Rocha" + }, + { + "code": "UY-SA", + "name": "Salto" + }, + { + "code": "UY-SJ", + "name": "San José" + }, + { + "code": "UY-SO", + "name": "Soriano" + }, + { + "code": "UY-TA", + "name": "Tacuarembó" + }, + { + "code": "UY-TT", + "name": "Treinta y Tres" + } + ] + }, + { + "code": "VE", + "name": "Bolívar soberano", + "currency_code": "VES", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "VE-A", + "name": "Capital" + }, + { + "code": "VE-B", + "name": "Anzoátegui" + }, + { + "code": "VE-C", + "name": "Apure" + }, + { + "code": "VE-D", + "name": "Aragua" + }, + { + "code": "VE-E", + "name": "Barinas" + }, + { + "code": "VE-F", + "name": "Bolívar" + }, + { + "code": "VE-G", + "name": "Carabobo" + }, + { + "code": "VE-H", + "name": "Cojedes" + }, + { + "code": "VE-I", + "name": "Falcón" + }, + { + "code": "VE-J", + "name": "Guárico" + }, + { + "code": "VE-K", + "name": "Lara" + }, + { + "code": "VE-L", + "name": "Mérida" + }, + { + "code": "VE-M", + "name": "Miranda" + }, + { + "code": "VE-N", + "name": "Monagas" + }, + { + "code": "VE-O", + "name": "Nueva Esparta" + }, + { + "code": "VE-P", + "name": "Portuguesa" + }, + { + "code": "VE-R", + "name": "Sucre" + }, + { + "code": "VE-S", + "name": "Táchira" + }, + { + "code": "VE-T", + "name": "Trujillo" + }, + { + "code": "VE-U", + "name": "Yaracuy" + }, + { + "code": "VE-V", + "name": "Zulia" + }, + { + "code": "VE-W", + "name": "Federal Dependencies" + }, + { + "code": "VE-X", + "name": "La Guaira (Vargas)" + }, + { + "code": "VE-Y", + "name": "Delta Amacuro" + }, + { + "code": "VE-Z", + "name": "Amazonas" + } + ] + } + ], + }) + ]) + ); + }); + + test('can view continent data', async ({ + request + }) => { + // call API to retrieve a specific continent data + const response = await request.get('/wp-json/wc/v3/data/continents/eu'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + + expect(responseJSON).toEqual( + expect.objectContaining({ + "code": "EU", + "name": "Europe", + "countries": [{ + "code": "AD", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AL", + "name": "Albanian lek", + "currency_code": "ALL", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "AL-01", + "name": "Berat" + }, + { + "code": "AL-09", + "name": "Dibër" + }, + { + "code": "AL-02", + "name": "Durrës" + }, + { + "code": "AL-03", + "name": "Elbasan" + }, + { + "code": "AL-04", + "name": "Fier" + }, + { + "code": "AL-05", + "name": "Gjirokastër" + }, + { + "code": "AL-06", + "name": "Korçë" + }, + { + "code": "AL-07", + "name": "Kukës" + }, + { + "code": "AL-08", + "name": "Lezhë" + }, + { + "code": "AL-10", + "name": "Shkodër" + }, + { + "code": "AL-11", + "name": "Tirana" + }, + { + "code": "AL-12", + "name": "Vlorë" + } + ] + }, + { + "code": "AT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "AX", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BA", + "name": "Bosnia and Herzegovina convertible mark", + "currency_code": "BAM", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "BG", + "name": "Bulgarian lev", + "currency_code": "BGN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "BG-01", + "name": "Blagoevgrad" + }, + { + "code": "BG-02", + "name": "Burgas" + }, + { + "code": "BG-08", + "name": "Dobrich" + }, + { + "code": "BG-07", + "name": "Gabrovo" + }, + { + "code": "BG-26", + "name": "Haskovo" + }, + { + "code": "BG-09", + "name": "Kardzhali" + }, + { + "code": "BG-10", + "name": "Kyustendil" + }, + { + "code": "BG-11", + "name": "Lovech" + }, + { + "code": "BG-12", + "name": "Montana" + }, + { + "code": "BG-13", + "name": "Pazardzhik" + }, + { + "code": "BG-14", + "name": "Pernik" + }, + { + "code": "BG-15", + "name": "Pleven" + }, + { + "code": "BG-16", + "name": "Plovdiv" + }, + { + "code": "BG-17", + "name": "Razgrad" + }, + { + "code": "BG-18", + "name": "Ruse" + }, + { + "code": "BG-27", + "name": "Shumen" + }, + { + "code": "BG-19", + "name": "Silistra" + }, + { + "code": "BG-20", + "name": "Sliven" + }, + { + "code": "BG-21", + "name": "Smolyan" + }, + { + "code": "BG-23", + "name": "Sofia District" + }, + { + "code": "BG-22", + "name": "Sofia" + }, + { + "code": "BG-24", + "name": "Stara Zagora" + }, + { + "code": "BG-25", + "name": "Targovishte" + }, + { + "code": "BG-03", + "name": "Varna" + }, + { + "code": "BG-04", + "name": "Veliko Tarnovo" + }, + { + "code": "BG-05", + "name": "Vidin" + }, + { + "code": "BG-06", + "name": "Vratsa" + }, + { + "code": "BG-28", + "name": "Yambol" + } + ] + }, + { + "code": "BY", + "name": "Belarusian ruble", + "currency_code": "BYN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "CH", + "name": "Swiss franc", + "currency_code": "CHF", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": "'", + "weight_unit": "kg", + "states": [{ + "code": "AG", + "name": "Aargau" + }, + { + "code": "AR", + "name": "Appenzell Ausserrhoden" + }, + { + "code": "AI", + "name": "Appenzell Innerrhoden" + }, + { + "code": "BL", + "name": "Basel-Landschaft" + }, + { + "code": "BS", + "name": "Basel-Stadt" + }, + { + "code": "BE", + "name": "Bern" + }, + { + "code": "FR", + "name": "Fribourg" + }, + { + "code": "GE", + "name": "Geneva" + }, + { + "code": "GL", + "name": "Glarus" + }, + { + "code": "GR", + "name": "Graubünden" + }, + { + "code": "JU", + "name": "Jura" + }, + { + "code": "LU", + "name": "Luzern" + }, + { + "code": "NE", + "name": "Neuchâtel" + }, + { + "code": "NW", + "name": "Nidwalden" + }, + { + "code": "OW", + "name": "Obwalden" + }, + { + "code": "SH", + "name": "Schaffhausen" + }, + { + "code": "SZ", + "name": "Schwyz" + }, + { + "code": "SO", + "name": "Solothurn" + }, + { + "code": "SG", + "name": "St. Gallen" + }, + { + "code": "TG", + "name": "Thurgau" + }, + { + "code": "TI", + "name": "Ticino" + }, + { + "code": "UR", + "name": "Uri" + }, + { + "code": "VS", + "name": "Valais" + }, + { + "code": "VD", + "name": "Vaud" + }, + { + "code": "ZG", + "name": "Zug" + }, + { + "code": "ZH", + "name": "Zürich" + } + ] + }, + { + "code": "CZ", + "name": "Czech koruna", + "currency_code": "CZK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "DE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "DE-BW", + "name": "Baden-Württemberg" + }, + { + "code": "DE-BY", + "name": "Bavaria" + }, + { + "code": "DE-BE", + "name": "Berlin" + }, + { + "code": "DE-BB", + "name": "Brandenburg" + }, + { + "code": "DE-HB", + "name": "Bremen" + }, + { + "code": "DE-HH", + "name": "Hamburg" + }, + { + "code": "DE-HE", + "name": "Hesse" + }, + { + "code": "DE-MV", + "name": "Mecklenburg-Vorpommern" + }, + { + "code": "DE-NI", + "name": "Lower Saxony" + }, + { + "code": "DE-NW", + "name": "North Rhine-Westphalia" + }, + { + "code": "DE-RP", + "name": "Rhineland-Palatinate" + }, + { + "code": "DE-SL", + "name": "Saarland" + }, + { + "code": "DE-SN", + "name": "Saxony" + }, + { + "code": "DE-ST", + "name": "Saxony-Anhalt" + }, + { + "code": "DE-SH", + "name": "Schleswig-Holstein" + }, + { + "code": "DE-TH", + "name": "Thuringia" + } + ] + }, + { + "code": "DK", + "name": "Danish krone", + "currency_code": "DKK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "EE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "ES", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "C", + "name": "A Coruña" + }, + { + "code": "VI", + "name": "Araba/Álava" + }, + { + "code": "AB", + "name": "Albacete" + }, + { + "code": "A", + "name": "Alicante" + }, + { + "code": "AL", + "name": "Almería" + }, + { + "code": "O", + "name": "Asturias" + }, + { + "code": "AV", + "name": "Ávila" + }, + { + "code": "BA", + "name": "Badajoz" + }, + { + "code": "PM", + "name": "Baleares" + }, + { + "code": "B", + "name": "Barcelona" + }, + { + "code": "BU", + "name": "Burgos" + }, + { + "code": "CC", + "name": "Cáceres" + }, + { + "code": "CA", + "name": "Cádiz" + }, + { + "code": "S", + "name": "Cantabria" + }, + { + "code": "CS", + "name": "Castellón" + }, + { + "code": "CE", + "name": "Ceuta" + }, + { + "code": "CR", + "name": "Ciudad Real" + }, + { + "code": "CO", + "name": "Córdoba" + }, + { + "code": "CU", + "name": "Cuenca" + }, + { + "code": "GI", + "name": "Girona" + }, + { + "code": "GR", + "name": "Granada" + }, + { + "code": "GU", + "name": "Guadalajara" + }, + { + "code": "SS", + "name": "Gipuzkoa" + }, + { + "code": "H", + "name": "Huelva" + }, + { + "code": "HU", + "name": "Huesca" + }, + { + "code": "J", + "name": "Jaén" + }, + { + "code": "LO", + "name": "La Rioja" + }, + { + "code": "GC", + "name": "Las Palmas" + }, + { + "code": "LE", + "name": "León" + }, + { + "code": "L", + "name": "Lleida" + }, + { + "code": "LU", + "name": "Lugo" + }, + { + "code": "M", + "name": "Madrid" + }, + { + "code": "MA", + "name": "Málaga" + }, + { + "code": "ML", + "name": "Melilla" + }, + { + "code": "MU", + "name": "Murcia" + }, + { + "code": "NA", + "name": "Navarra" + }, + { + "code": "OR", + "name": "Ourense" + }, + { + "code": "P", + "name": "Palencia" + }, + { + "code": "PO", + "name": "Pontevedra" + }, + { + "code": "SA", + "name": "Salamanca" + }, + { + "code": "TF", + "name": "Santa Cruz de Tenerife" + }, + { + "code": "SG", + "name": "Segovia" + }, + { + "code": "SE", + "name": "Sevilla" + }, + { + "code": "SO", + "name": "Soria" + }, + { + "code": "T", + "name": "Tarragona" + }, + { + "code": "TE", + "name": "Teruel" + }, + { + "code": "TO", + "name": "Toledo" + }, + { + "code": "V", + "name": "Valencia" + }, + { + "code": "VA", + "name": "Valladolid" + }, + { + "code": "BI", + "name": "Biscay" + }, + { + "code": "ZA", + "name": "Zamora" + }, + { + "code": "Z", + "name": "Zaragoza" + } + ] + }, + { + "code": "FI", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "FO", + "name": "Danish krone", + "currency_code": "DKK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "FR", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GB", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "foot", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "oz", + "states": [] + }, + { + "code": "GG", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GI", + "name": "Gibraltar pound", + "currency_code": "GIP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "GR", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "I", + "name": "Attica" + }, + { + "code": "A", + "name": "East Macedonia and Thrace" + }, + { + "code": "B", + "name": "Central Macedonia" + }, + { + "code": "C", + "name": "West Macedonia" + }, + { + "code": "D", + "name": "Epirus" + }, + { + "code": "E", + "name": "Thessaly" + }, + { + "code": "F", + "name": "Ionian Islands" + }, + { + "code": "G", + "name": "West Greece" + }, + { + "code": "H", + "name": "Central Greece" + }, + { + "code": "J", + "name": "Peloponnese" + }, + { + "code": "K", + "name": "North Aegean" + }, + { + "code": "L", + "name": "South Aegean" + }, + { + "code": "M", + "name": "Crete" + } + ] + }, + { + "code": "HR", + "name": "Croatian kuna", + "currency_code": "HRK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "HU", + "name": "Hungarian forint", + "currency_code": "HUF", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "BK", + "name": "Bács-Kiskun" + }, + { + "code": "BE", + "name": "Békés" + }, + { + "code": "BA", + "name": "Baranya" + }, + { + "code": "BZ", + "name": "Borsod-Abaúj-Zemplén" + }, + { + "code": "BU", + "name": "Budapest" + }, + { + "code": "CS", + "name": "Csongrád-Csanád" + }, + { + "code": "FE", + "name": "Fejér" + }, + { + "code": "GS", + "name": "Győr-Moson-Sopron" + }, + { + "code": "HB", + "name": "Hajdú-Bihar" + }, + { + "code": "HE", + "name": "Heves" + }, + { + "code": "JN", + "name": "Jász-Nagykun-Szolnok" + }, + { + "code": "KE", + "name": "Komárom-Esztergom" + }, + { + "code": "NO", + "name": "Nógrád" + }, + { + "code": "PE", + "name": "Pest" + }, + { + "code": "SO", + "name": "Somogy" + }, + { + "code": "SZ", + "name": "Szabolcs-Szatmár-Bereg" + }, + { + "code": "TO", + "name": "Tolna" + }, + { + "code": "VA", + "name": "Vas" + }, + { + "code": "VE", + "name": "Veszprém" + }, + { + "code": "ZA", + "name": "Zala" + } + ] + }, + { + "code": "IE", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [{ + "code": "CW", + "name": "Carlow" + }, + { + "code": "CN", + "name": "Cavan" + }, + { + "code": "CE", + "name": "Clare" + }, + { + "code": "CO", + "name": "Cork" + }, + { + "code": "DL", + "name": "Donegal" + }, + { + "code": "D", + "name": "Dublin" + }, + { + "code": "G", + "name": "Galway" + }, + { + "code": "KY", + "name": "Kerry" + }, + { + "code": "KE", + "name": "Kildare" + }, + { + "code": "KK", + "name": "Kilkenny" + }, + { + "code": "LS", + "name": "Laois" + }, + { + "code": "LM", + "name": "Leitrim" + }, + { + "code": "LK", + "name": "Limerick" + }, + { + "code": "LD", + "name": "Longford" + }, + { + "code": "LH", + "name": "Louth" + }, + { + "code": "MO", + "name": "Mayo" + }, + { + "code": "MH", + "name": "Meath" + }, + { + "code": "MN", + "name": "Monaghan" + }, + { + "code": "OY", + "name": "Offaly" + }, + { + "code": "RN", + "name": "Roscommon" + }, + { + "code": "SO", + "name": "Sligo" + }, + { + "code": "TA", + "name": "Tipperary" + }, + { + "code": "WD", + "name": "Waterford" + }, + { + "code": "WH", + "name": "Westmeath" + }, + { + "code": "WX", + "name": "Wexford" + }, + { + "code": "WW", + "name": "Wicklow" + } + ] + }, + { + "code": "IM", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IS", + "name": "Icelandic króna", + "currency_code": "ISK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "IT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AG", + "name": "Agrigento" + }, + { + "code": "AL", + "name": "Alessandria" + }, + { + "code": "AN", + "name": "Ancona" + }, + { + "code": "AO", + "name": "Aosta" + }, + { + "code": "AR", + "name": "Arezzo" + }, + { + "code": "AP", + "name": "Ascoli Piceno" + }, + { + "code": "AT", + "name": "Asti" + }, + { + "code": "AV", + "name": "Avellino" + }, + { + "code": "BA", + "name": "Bari" + }, + { + "code": "BT", + "name": "Barletta-Andria-Trani" + }, + { + "code": "BL", + "name": "Belluno" + }, + { + "code": "BN", + "name": "Benevento" + }, + { + "code": "BG", + "name": "Bergamo" + }, + { + "code": "BI", + "name": "Biella" + }, + { + "code": "BO", + "name": "Bologna" + }, + { + "code": "BZ", + "name": "Bolzano" + }, + { + "code": "BS", + "name": "Brescia" + }, + { + "code": "BR", + "name": "Brindisi" + }, + { + "code": "CA", + "name": "Cagliari" + }, + { + "code": "CL", + "name": "Caltanissetta" + }, + { + "code": "CB", + "name": "Campobasso" + }, + { + "code": "CE", + "name": "Caserta" + }, + { + "code": "CT", + "name": "Catania" + }, + { + "code": "CZ", + "name": "Catanzaro" + }, + { + "code": "CH", + "name": "Chieti" + }, + { + "code": "CO", + "name": "Como" + }, + { + "code": "CS", + "name": "Cosenza" + }, + { + "code": "CR", + "name": "Cremona" + }, + { + "code": "KR", + "name": "Crotone" + }, + { + "code": "CN", + "name": "Cuneo" + }, + { + "code": "EN", + "name": "Enna" + }, + { + "code": "FM", + "name": "Fermo" + }, + { + "code": "FE", + "name": "Ferrara" + }, + { + "code": "FI", + "name": "Firenze" + }, + { + "code": "FG", + "name": "Foggia" + }, + { + "code": "FC", + "name": "Forlì-Cesena" + }, + { + "code": "FR", + "name": "Frosinone" + }, + { + "code": "GE", + "name": "Genova" + }, + { + "code": "GO", + "name": "Gorizia" + }, + { + "code": "GR", + "name": "Grosseto" + }, + { + "code": "IM", + "name": "Imperia" + }, + { + "code": "IS", + "name": "Isernia" + }, + { + "code": "SP", + "name": "La Spezia" + }, + { + "code": "AQ", + "name": "L'Aquila" + }, + { + "code": "LT", + "name": "Latina" + }, + { + "code": "LE", + "name": "Lecce" + }, + { + "code": "LC", + "name": "Lecco" + }, + { + "code": "LI", + "name": "Livorno" + }, + { + "code": "LO", + "name": "Lodi" + }, + { + "code": "LU", + "name": "Lucca" + }, + { + "code": "MC", + "name": "Macerata" + }, + { + "code": "MN", + "name": "Mantova" + }, + { + "code": "MS", + "name": "Massa-Carrara" + }, + { + "code": "MT", + "name": "Matera" + }, + { + "code": "ME", + "name": "Messina" + }, + { + "code": "MI", + "name": "Milano" + }, + { + "code": "MO", + "name": "Modena" + }, + { + "code": "MB", + "name": "Monza e della Brianza" + }, + { + "code": "NA", + "name": "Napoli" + }, + { + "code": "NO", + "name": "Novara" + }, + { + "code": "NU", + "name": "Nuoro" + }, + { + "code": "OR", + "name": "Oristano" + }, + { + "code": "PD", + "name": "Padova" + }, + { + "code": "PA", + "name": "Palermo" + }, + { + "code": "PR", + "name": "Parma" + }, + { + "code": "PV", + "name": "Pavia" + }, + { + "code": "PG", + "name": "Perugia" + }, + { + "code": "PU", + "name": "Pesaro e Urbino" + }, + { + "code": "PE", + "name": "Pescara" + }, + { + "code": "PC", + "name": "Piacenza" + }, + { + "code": "PI", + "name": "Pisa" + }, + { + "code": "PT", + "name": "Pistoia" + }, + { + "code": "PN", + "name": "Pordenone" + }, + { + "code": "PZ", + "name": "Potenza" + }, + { + "code": "PO", + "name": "Prato" + }, + { + "code": "RG", + "name": "Ragusa" + }, + { + "code": "RA", + "name": "Ravenna" + }, + { + "code": "RC", + "name": "Reggio Calabria" + }, + { + "code": "RE", + "name": "Reggio Emilia" + }, + { + "code": "RI", + "name": "Rieti" + }, + { + "code": "RN", + "name": "Rimini" + }, + { + "code": "RM", + "name": "Roma" + }, + { + "code": "RO", + "name": "Rovigo" + }, + { + "code": "SA", + "name": "Salerno" + }, + { + "code": "SS", + "name": "Sassari" + }, + { + "code": "SV", + "name": "Savona" + }, + { + "code": "SI", + "name": "Siena" + }, + { + "code": "SR", + "name": "Siracusa" + }, + { + "code": "SO", + "name": "Sondrio" + }, + { + "code": "SU", + "name": "Sud Sardegna" + }, + { + "code": "TA", + "name": "Taranto" + }, + { + "code": "TE", + "name": "Teramo" + }, + { + "code": "TR", + "name": "Terni" + }, + { + "code": "TO", + "name": "Torino" + }, + { + "code": "TP", + "name": "Trapani" + }, + { + "code": "TN", + "name": "Trento" + }, + { + "code": "TV", + "name": "Treviso" + }, + { + "code": "TS", + "name": "Trieste" + }, + { + "code": "UD", + "name": "Udine" + }, + { + "code": "VA", + "name": "Varese" + }, + { + "code": "VE", + "name": "Venezia" + }, + { + "code": "VB", + "name": "Verbano-Cusio-Ossola" + }, + { + "code": "VC", + "name": "Vercelli" + }, + { + "code": "VR", + "name": "Verona" + }, + { + "code": "VV", + "name": "Vibo Valentia" + }, + { + "code": "VI", + "name": "Vicenza" + }, + { + "code": "VT", + "name": "Viterbo" + } + ] + }, + { + "code": "JE", + "name": "Pound sterling", + "currency_code": "GBP", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LI", + "name": "Swiss franc", + "currency_code": "CHF", + "currency_pos": "left_space", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": "'", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LU", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "LV", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MC", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MD", + "name": "Moldovan leu", + "currency_code": "MDL", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "C", + "name": "Chișinău" + }, + { + "code": "BL", + "name": "Bălți" + }, + { + "code": "AN", + "name": "Anenii Noi" + }, + { + "code": "BS", + "name": "Basarabeasca" + }, + { + "code": "BR", + "name": "Briceni" + }, + { + "code": "CH", + "name": "Cahul" + }, + { + "code": "CT", + "name": "Cantemir" + }, + { + "code": "CL", + "name": "Călărași" + }, + { + "code": "CS", + "name": "Căușeni" + }, + { + "code": "CM", + "name": "Cimișlia" + }, + { + "code": "CR", + "name": "Criuleni" + }, + { + "code": "DN", + "name": "Dondușeni" + }, + { + "code": "DR", + "name": "Drochia" + }, + { + "code": "DB", + "name": "Dubăsari" + }, + { + "code": "ED", + "name": "Edineț" + }, + { + "code": "FL", + "name": "Fălești" + }, + { + "code": "FR", + "name": "Florești" + }, + { + "code": "GE", + "name": "UTA Găgăuzia" + }, + { + "code": "GL", + "name": "Glodeni" + }, + { + "code": "HN", + "name": "Hîncești" + }, + { + "code": "IL", + "name": "Ialoveni" + }, + { + "code": "LV", + "name": "Leova" + }, + { + "code": "NS", + "name": "Nisporeni" + }, + { + "code": "OC", + "name": "Ocnița" + }, + { + "code": "OR", + "name": "Orhei" + }, + { + "code": "RZ", + "name": "Rezina" + }, + { + "code": "RS", + "name": "Rîșcani" + }, + { + "code": "SG", + "name": "Sîngerei" + }, + { + "code": "SR", + "name": "Soroca" + }, + { + "code": "ST", + "name": "Strășeni" + }, + { + "code": "SD", + "name": "Șoldănești" + }, + { + "code": "SV", + "name": "Ștefan Vodă" + }, + { + "code": "TR", + "name": "Taraclia" + }, + { + "code": "TL", + "name": "Telenești" + }, + { + "code": "UN", + "name": "Ungheni" + } + ] + }, + { + "code": "ME", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MK", + "name": "Macedonian denar", + "currency_code": "MKD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "MT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left", + "decimal_sep": ".", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ",", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NL", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "NO", + "name": "Norwegian krone", + "currency_code": "NOK", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PL", + "name": "Polish złoty", + "currency_code": "PLN", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "PT", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "RO", + "name": "Romanian leu", + "currency_code": "RON", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "AB", + "name": "Alba" + }, + { + "code": "AR", + "name": "Arad" + }, + { + "code": "AG", + "name": "Argeș" + }, + { + "code": "BC", + "name": "Bacău" + }, + { + "code": "BH", + "name": "Bihor" + }, + { + "code": "BN", + "name": "Bistrița-Năsăud" + }, + { + "code": "BT", + "name": "Botoșani" + }, + { + "code": "BR", + "name": "Brăila" + }, + { + "code": "BV", + "name": "Brașov" + }, + { + "code": "B", + "name": "București" + }, + { + "code": "BZ", + "name": "Buzău" + }, + { + "code": "CL", + "name": "Călărași" + }, + { + "code": "CS", + "name": "Caraș-Severin" + }, + { + "code": "CJ", + "name": "Cluj" + }, + { + "code": "CT", + "name": "Constanța" + }, + { + "code": "CV", + "name": "Covasna" + }, + { + "code": "DB", + "name": "Dâmbovița" + }, + { + "code": "DJ", + "name": "Dolj" + }, + { + "code": "GL", + "name": "Galați" + }, + { + "code": "GR", + "name": "Giurgiu" + }, + { + "code": "GJ", + "name": "Gorj" + }, + { + "code": "HR", + "name": "Harghita" + }, + { + "code": "HD", + "name": "Hunedoara" + }, + { + "code": "IL", + "name": "Ialomița" + }, + { + "code": "IS", + "name": "Iași" + }, + { + "code": "IF", + "name": "Ilfov" + }, + { + "code": "MM", + "name": "Maramureș" + }, + { + "code": "MH", + "name": "Mehedinți" + }, + { + "code": "MS", + "name": "Mureș" + }, + { + "code": "NT", + "name": "Neamț" + }, + { + "code": "OT", + "name": "Olt" + }, + { + "code": "PH", + "name": "Prahova" + }, + { + "code": "SJ", + "name": "Sălaj" + }, + { + "code": "SM", + "name": "Satu Mare" + }, + { + "code": "SB", + "name": "Sibiu" + }, + { + "code": "SV", + "name": "Suceava" + }, + { + "code": "TR", + "name": "Teleorman" + }, + { + "code": "TM", + "name": "Timiș" + }, + { + "code": "TL", + "name": "Tulcea" + }, + { + "code": "VL", + "name": "Vâlcea" + }, + { + "code": "VS", + "name": "Vaslui" + }, + { + "code": "VN", + "name": "Vrancea" + } + ] + }, + { + "code": "RS", + "name": "Serbian dinar", + "currency_code": "RSD", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "RS00", + "name": "Belgrade" + }, + { + "code": "RS14", + "name": "Bor" + }, + { + "code": "RS11", + "name": "Braničevo" + }, + { + "code": "RS02", + "name": "Central Banat" + }, + { + "code": "RS10", + "name": "Danube" + }, + { + "code": "RS23", + "name": "Jablanica" + }, + { + "code": "RS09", + "name": "Kolubara" + }, + { + "code": "RS08", + "name": "Mačva" + }, + { + "code": "RS17", + "name": "Morava" + }, + { + "code": "RS20", + "name": "Nišava" + }, + { + "code": "RS01", + "name": "North Bačka" + }, + { + "code": "RS03", + "name": "North Banat" + }, + { + "code": "RS24", + "name": "Pčinja" + }, + { + "code": "RS22", + "name": "Pirot" + }, + { + "code": "RS13", + "name": "Pomoravlje" + }, + { + "code": "RS19", + "name": "Rasina" + }, + { + "code": "RS18", + "name": "Raška" + }, + { + "code": "RS06", + "name": "South Bačka" + }, + { + "code": "RS04", + "name": "South Banat" + }, + { + "code": "RS07", + "name": "Srem" + }, + { + "code": "RS12", + "name": "Šumadija" + }, + { + "code": "RS21", + "name": "Toplica" + }, + { + "code": "RS05", + "name": "West Bačka" + }, + { + "code": "RS15", + "name": "Zaječar" + }, + { + "code": "RS16", + "name": "Zlatibor" + }, + { + "code": "RS25", + "name": "Kosovo" + }, + { + "code": "RS26", + "name": "Peć" + }, + { + "code": "RS27", + "name": "Prizren" + }, + { + "code": "RS28", + "name": "Kosovska Mitrovica" + }, + { + "code": "RS29", + "name": "Kosovo-Pomoravlje" + }, + { + "code": "RSKM", + "name": "Kosovo-Metohija" + }, + { + "code": "RSVO", + "name": "Vojvodina" + } + ] + }, + { + "code": "RU", + "name": "Russian ruble", + "currency_code": "RUB", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SE", + "name": "Swedish krona", + "currency_code": "SEK", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SI", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SJ", + "name": "Norwegian krone", + "currency_code": "NOK", + "currency_pos": "left_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 0, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SK", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [] + }, + { + "code": "SM", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + }, + { + "code": "TR", + "name": "Turkish lira", + "currency_code": "TRY", + "currency_pos": "left", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [{ + "code": "TR01", + "name": "Adana" + }, + { + "code": "TR02", + "name": "Adıyaman" + }, + { + "code": "TR03", + "name": "Afyon" + }, + { + "code": "TR04", + "name": "Ağrı" + }, + { + "code": "TR05", + "name": "Amasya" + }, + { + "code": "TR06", + "name": "Ankara" + }, + { + "code": "TR07", + "name": "Antalya" + }, + { + "code": "TR08", + "name": "Artvin" + }, + { + "code": "TR09", + "name": "Aydın" + }, + { + "code": "TR10", + "name": "Balıkesir" + }, + { + "code": "TR11", + "name": "Bilecik" + }, + { + "code": "TR12", + "name": "Bingöl" + }, + { + "code": "TR13", + "name": "Bitlis" + }, + { + "code": "TR14", + "name": "Bolu" + }, + { + "code": "TR15", + "name": "Burdur" + }, + { + "code": "TR16", + "name": "Bursa" + }, + { + "code": "TR17", + "name": "Çanakkale" + }, + { + "code": "TR18", + "name": "Çankırı" + }, + { + "code": "TR19", + "name": "Çorum" + }, + { + "code": "TR20", + "name": "Denizli" + }, + { + "code": "TR21", + "name": "Diyarbakır" + }, + { + "code": "TR22", + "name": "Edirne" + }, + { + "code": "TR23", + "name": "Elazığ" + }, + { + "code": "TR24", + "name": "Erzincan" + }, + { + "code": "TR25", + "name": "Erzurum" + }, + { + "code": "TR26", + "name": "Eskişehir" + }, + { + "code": "TR27", + "name": "Gaziantep" + }, + { + "code": "TR28", + "name": "Giresun" + }, + { + "code": "TR29", + "name": "Gümüşhane" + }, + { + "code": "TR30", + "name": "Hakkari" + }, + { + "code": "TR31", + "name": "Hatay" + }, + { + "code": "TR32", + "name": "Isparta" + }, + { + "code": "TR33", + "name": "İçel" + }, + { + "code": "TR34", + "name": "İstanbul" + }, + { + "code": "TR35", + "name": "İzmir" + }, + { + "code": "TR36", + "name": "Kars" + }, + { + "code": "TR37", + "name": "Kastamonu" + }, + { + "code": "TR38", + "name": "Kayseri" + }, + { + "code": "TR39", + "name": "Kırklareli" + }, + { + "code": "TR40", + "name": "Kırşehir" + }, + { + "code": "TR41", + "name": "Kocaeli" + }, + { + "code": "TR42", + "name": "Konya" + }, + { + "code": "TR43", + "name": "Kütahya" + }, + { + "code": "TR44", + "name": "Malatya" + }, + { + "code": "TR45", + "name": "Manisa" + }, + { + "code": "TR46", + "name": "Kahramanmaraş" + }, + { + "code": "TR47", + "name": "Mardin" + }, + { + "code": "TR48", + "name": "Muğla" + }, + { + "code": "TR49", + "name": "Muş" + }, + { + "code": "TR50", + "name": "Nevşehir" + }, + { + "code": "TR51", + "name": "Niğde" + }, + { + "code": "TR52", + "name": "Ordu" + }, + { + "code": "TR53", + "name": "Rize" + }, + { + "code": "TR54", + "name": "Sakarya" + }, + { + "code": "TR55", + "name": "Samsun" + }, + { + "code": "TR56", + "name": "Siirt" + }, + { + "code": "TR57", + "name": "Sinop" + }, + { + "code": "TR58", + "name": "Sivas" + }, + { + "code": "TR59", + "name": "Tekirdağ" + }, + { + "code": "TR60", + "name": "Tokat" + }, + { + "code": "TR61", + "name": "Trabzon" + }, + { + "code": "TR62", + "name": "Tunceli" + }, + { + "code": "TR63", + "name": "Şanlıurfa" + }, + { + "code": "TR64", + "name": "Uşak" + }, + { + "code": "TR65", + "name": "Van" + }, + { + "code": "TR66", + "name": "Yozgat" + }, + { + "code": "TR67", + "name": "Zonguldak" + }, + { + "code": "TR68", + "name": "Aksaray" + }, + { + "code": "TR69", + "name": "Bayburt" + }, + { + "code": "TR70", + "name": "Karaman" + }, + { + "code": "TR71", + "name": "Kırıkkale" + }, + { + "code": "TR72", + "name": "Batman" + }, + { + "code": "TR73", + "name": "Şırnak" + }, + { + "code": "TR74", + "name": "Bartın" + }, + { + "code": "TR75", + "name": "Ardahan" + }, + { + "code": "TR76", + "name": "Iğdır" + }, + { + "code": "TR77", + "name": "Yalova" + }, + { + "code": "TR78", + "name": "Karabük" + }, + { + "code": "TR79", + "name": "Kilis" + }, + { + "code": "TR80", + "name": "Osmaniye" + }, + { + "code": "TR81", + "name": "Düzce" + } + ] + }, + { + "code": "UA", + "name": "Ukrainian hryvnia", + "currency_code": "UAH", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": " ", + "weight_unit": "kg", + "states": [{ + "code": "UA05", + "name": "Vinnychchyna" + }, + { + "code": "UA07", + "name": "Volyn" + }, + { + "code": "UA09", + "name": "Luhanshchyna" + }, + { + "code": "UA12", + "name": "Dnipropetrovshchyna" + }, + { + "code": "UA14", + "name": "Donechchyna" + }, + { + "code": "UA18", + "name": "Zhytomyrshchyna" + }, + { + "code": "UA21", + "name": "Zakarpattia" + }, + { + "code": "UA23", + "name": "Zaporizhzhya" + }, + { + "code": "UA26", + "name": "Prykarpattia" + }, + { + "code": "UA30", + "name": "Kyiv" + }, + { + "code": "UA32", + "name": "Kyivshchyna" + }, + { + "code": "UA35", + "name": "Kirovohradschyna" + }, + { + "code": "UA40", + "name": "Sevastopol" + }, + { + "code": "UA43", + "name": "Crimea" + }, + { + "code": "UA46", + "name": "Lvivshchyna" + }, + { + "code": "UA48", + "name": "Mykolayivschyna" + }, + { + "code": "UA51", + "name": "Odeshchyna" + }, + { + "code": "UA53", + "name": "Poltavshchyna" + }, + { + "code": "UA56", + "name": "Rivnenshchyna" + }, + { + "code": "UA59", + "name": "Sumshchyna" + }, + { + "code": "UA61", + "name": "Ternopilshchyna" + }, + { + "code": "UA63", + "name": "Kharkivshchyna" + }, + { + "code": "UA65", + "name": "Khersonshchyna" + }, + { + "code": "UA68", + "name": "Khmelnychchyna" + }, + { + "code": "UA71", + "name": "Cherkashchyna" + }, + { + "code": "UA74", + "name": "Chernihivshchyna" + }, + { + "code": "UA77", + "name": "Chernivtsi Oblast" + } + ] + }, + { + "code": "VA", + "name": "Euro", + "currency_code": "EUR", + "currency_pos": "right_space", + "decimal_sep": ",", + "dimension_unit": "cm", + "num_decimals": 2, + "thousand_sep": ".", + "weight_unit": "kg", + "states": [] + } + ], + }) + ); + }); + + test('can view all countries', async ({ + request + }) => { + // call API to retrieve all countries + const response = await request.get('/wp-json/wc/v3/data/countries'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AF", + "name": "Afghanistan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AX", + "name": "Åland Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AL", + "name": "Albania", + "states": [{ + "code": "AL-01", + "name": "Berat" + }, + { + "code": "AL-09", + "name": "Dibër" + }, + { + "code": "AL-02", + "name": "Durrës" + }, + { + "code": "AL-03", + "name": "Elbasan" + }, + { + "code": "AL-04", + "name": "Fier" + }, + { + "code": "AL-05", + "name": "Gjirokastër" + }, + { + "code": "AL-06", + "name": "Korçë" + }, + { + "code": "AL-07", + "name": "Kukës" + }, + { + "code": "AL-08", + "name": "Lezhë" + }, + { + "code": "AL-10", + "name": "Shkodër" + }, + { + "code": "AL-11", + "name": "Tirana" + }, + { + "code": "AL-12", + "name": "Vlorë" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DZ", + "name": "Algeria", + "states": [{ + "code": "DZ-01", + "name": "Adrar" + }, + { + "code": "DZ-02", + "name": "Chlef" + }, + { + "code": "DZ-03", + "name": "Laghouat" + }, + { + "code": "DZ-04", + "name": "Oum El Bouaghi" + }, + { + "code": "DZ-05", + "name": "Batna" + }, + { + "code": "DZ-06", + "name": "Béjaïa" + }, + { + "code": "DZ-07", + "name": "Biskra" + }, + { + "code": "DZ-08", + "name": "Béchar" + }, + { + "code": "DZ-09", + "name": "Blida" + }, + { + "code": "DZ-10", + "name": "Bouira" + }, + { + "code": "DZ-11", + "name": "Tamanghasset" + }, + { + "code": "DZ-12", + "name": "Tébessa" + }, + { + "code": "DZ-13", + "name": "Tlemcen" + }, + { + "code": "DZ-14", + "name": "Tiaret" + }, + { + "code": "DZ-15", + "name": "Tizi Ouzou" + }, + { + "code": "DZ-16", + "name": "Algiers" + }, + { + "code": "DZ-17", + "name": "Djelfa" + }, + { + "code": "DZ-18", + "name": "Jijel" + }, + { + "code": "DZ-19", + "name": "Sétif" + }, + { + "code": "DZ-20", + "name": "Saïda" + }, + { + "code": "DZ-21", + "name": "Skikda" + }, + { + "code": "DZ-22", + "name": "Sidi Bel Abbès" + }, + { + "code": "DZ-23", + "name": "Annaba" + }, + { + "code": "DZ-24", + "name": "Guelma" + }, + { + "code": "DZ-25", + "name": "Constantine" + }, + { + "code": "DZ-26", + "name": "Médéa" + }, + { + "code": "DZ-27", + "name": "Mostaganem" + }, + { + "code": "DZ-28", + "name": "M’Sila" + }, + { + "code": "DZ-29", + "name": "Mascara" + }, + { + "code": "DZ-30", + "name": "Ouargla" + }, + { + "code": "DZ-31", + "name": "Oran" + }, + { + "code": "DZ-32", + "name": "El Bayadh" + }, + { + "code": "DZ-33", + "name": "Illizi" + }, + { + "code": "DZ-34", + "name": "Bordj Bou Arréridj" + }, + { + "code": "DZ-35", + "name": "Boumerdès" + }, + { + "code": "DZ-36", + "name": "El Tarf" + }, + { + "code": "DZ-37", + "name": "Tindouf" + }, + { + "code": "DZ-38", + "name": "Tissemsilt" + }, + { + "code": "DZ-39", + "name": "El Oued" + }, + { + "code": "DZ-40", + "name": "Khenchela" + }, + { + "code": "DZ-41", + "name": "Souk Ahras" + }, + { + "code": "DZ-42", + "name": "Tipasa" + }, + { + "code": "DZ-43", + "name": "Mila" + }, + { + "code": "DZ-44", + "name": "Aïn Defla" + }, + { + "code": "DZ-45", + "name": "Naama" + }, + { + "code": "DZ-46", + "name": "Aïn Témouchent" + }, + { + "code": "DZ-47", + "name": "Ghardaïa" + }, + { + "code": "DZ-48", + "name": "Relizane" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AS", + "name": "American Samoa", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AD", + "name": "Andorra", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AO", + "name": "Angola", + "states": [{ + "code": "BGO", + "name": "Bengo" + }, + { + "code": "BLU", + "name": "Benguela" + }, + { + "code": "BIE", + "name": "Bié" + }, + { + "code": "CAB", + "name": "Cabinda" + }, + { + "code": "CNN", + "name": "Cunene" + }, + { + "code": "HUA", + "name": "Huambo" + }, + { + "code": "HUI", + "name": "Huíla" + }, + { + "code": "CCU", + "name": "Kuando Kubango" + }, + { + "code": "CNO", + "name": "Kwanza-Norte" + }, + { + "code": "CUS", + "name": "Kwanza-Sul" + }, + { + "code": "LUA", + "name": "Luanda" + }, + { + "code": "LNO", + "name": "Lunda-Norte" + }, + { + "code": "LSU", + "name": "Lunda-Sul" + }, + { + "code": "MAL", + "name": "Malanje" + }, + { + "code": "MOX", + "name": "Moxico" + }, + { + "code": "NAM", + "name": "Namibe" + }, + { + "code": "UIG", + "name": "Uíge" + }, + { + "code": "ZAI", + "name": "Zaire" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AI", + "name": "Anguilla", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AQ", + "name": "Antarctica", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AG", + "name": "Antigua and Barbuda", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AR", + "name": "Argentina", + "states": [{ + "code": "C", + "name": "Ciudad Autónoma de Buenos Aires" + }, + { + "code": "B", + "name": "Buenos Aires" + }, + { + "code": "K", + "name": "Catamarca" + }, + { + "code": "H", + "name": "Chaco" + }, + { + "code": "U", + "name": "Chubut" + }, + { + "code": "X", + "name": "Córdoba" + }, + { + "code": "W", + "name": "Corrientes" + }, + { + "code": "E", + "name": "Entre Ríos" + }, + { + "code": "P", + "name": "Formosa" + }, + { + "code": "Y", + "name": "Jujuy" + }, + { + "code": "L", + "name": "La Pampa" + }, + { + "code": "F", + "name": "La Rioja" + }, + { + "code": "M", + "name": "Mendoza" + }, + { + "code": "N", + "name": "Misiones" + }, + { + "code": "Q", + "name": "Neuquén" + }, + { + "code": "R", + "name": "Río Negro" + }, + { + "code": "A", + "name": "Salta" + }, + { + "code": "J", + "name": "San Juan" + }, + { + "code": "D", + "name": "San Luis" + }, + { + "code": "Z", + "name": "Santa Cruz" + }, + { + "code": "S", + "name": "Santa Fe" + }, + { + "code": "G", + "name": "Santiago del Estero" + }, + { + "code": "V", + "name": "Tierra del Fuego" + }, + { + "code": "T", + "name": "Tucumán" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AM", + "name": "Armenia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AW", + "name": "Aruba", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AU", + "name": "Australia", + "states": [{ + "code": "ACT", + "name": "Australian Capital Territory" + }, + { + "code": "NSW", + "name": "New South Wales" + }, + { + "code": "NT", + "name": "Northern Territory" + }, + { + "code": "QLD", + "name": "Queensland" + }, + { + "code": "SA", + "name": "South Australia" + }, + { + "code": "TAS", + "name": "Tasmania" + }, + { + "code": "VIC", + "name": "Victoria" + }, + { + "code": "WA", + "name": "Western Australia" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AT", + "name": "Austria", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AZ", + "name": "Azerbaijan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BS", + "name": "Bahamas", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BH", + "name": "Bahrain", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BD", + "name": "Bangladesh", + "states": [{ + "code": "BD-05", + "name": "Bagerhat" + }, + { + "code": "BD-01", + "name": "Bandarban" + }, + { + "code": "BD-02", + "name": "Barguna" + }, + { + "code": "BD-06", + "name": "Barishal" + }, + { + "code": "BD-07", + "name": "Bhola" + }, + { + "code": "BD-03", + "name": "Bogura" + }, + { + "code": "BD-04", + "name": "Brahmanbaria" + }, + { + "code": "BD-09", + "name": "Chandpur" + }, + { + "code": "BD-10", + "name": "Chattogram" + }, + { + "code": "BD-12", + "name": "Chuadanga" + }, + { + "code": "BD-11", + "name": "Cox's Bazar" + }, + { + "code": "BD-08", + "name": "Cumilla" + }, + { + "code": "BD-13", + "name": "Dhaka" + }, + { + "code": "BD-14", + "name": "Dinajpur" + }, + { + "code": "BD-15", + "name": "Faridpur " + }, + { + "code": "BD-16", + "name": "Feni" + }, + { + "code": "BD-19", + "name": "Gaibandha" + }, + { + "code": "BD-18", + "name": "Gazipur" + }, + { + "code": "BD-17", + "name": "Gopalganj" + }, + { + "code": "BD-20", + "name": "Habiganj" + }, + { + "code": "BD-21", + "name": "Jamalpur" + }, + { + "code": "BD-22", + "name": "Jashore" + }, + { + "code": "BD-25", + "name": "Jhalokati" + }, + { + "code": "BD-23", + "name": "Jhenaidah" + }, + { + "code": "BD-24", + "name": "Joypurhat" + }, + { + "code": "BD-29", + "name": "Khagrachhari" + }, + { + "code": "BD-27", + "name": "Khulna" + }, + { + "code": "BD-26", + "name": "Kishoreganj" + }, + { + "code": "BD-28", + "name": "Kurigram" + }, + { + "code": "BD-30", + "name": "Kushtia" + }, + { + "code": "BD-31", + "name": "Lakshmipur" + }, + { + "code": "BD-32", + "name": "Lalmonirhat" + }, + { + "code": "BD-36", + "name": "Madaripur" + }, + { + "code": "BD-37", + "name": "Magura" + }, + { + "code": "BD-33", + "name": "Manikganj " + }, + { + "code": "BD-39", + "name": "Meherpur" + }, + { + "code": "BD-38", + "name": "Moulvibazar" + }, + { + "code": "BD-35", + "name": "Munshiganj" + }, + { + "code": "BD-34", + "name": "Mymensingh" + }, + { + "code": "BD-48", + "name": "Naogaon" + }, + { + "code": "BD-43", + "name": "Narail" + }, + { + "code": "BD-40", + "name": "Narayanganj" + }, + { + "code": "BD-42", + "name": "Narsingdi" + }, + { + "code": "BD-44", + "name": "Natore" + }, + { + "code": "BD-45", + "name": "Nawabganj" + }, + { + "code": "BD-41", + "name": "Netrakona" + }, + { + "code": "BD-46", + "name": "Nilphamari" + }, + { + "code": "BD-47", + "name": "Noakhali" + }, + { + "code": "BD-49", + "name": "Pabna" + }, + { + "code": "BD-52", + "name": "Panchagarh" + }, + { + "code": "BD-51", + "name": "Patuakhali" + }, + { + "code": "BD-50", + "name": "Pirojpur" + }, + { + "code": "BD-53", + "name": "Rajbari" + }, + { + "code": "BD-54", + "name": "Rajshahi" + }, + { + "code": "BD-56", + "name": "Rangamati" + }, + { + "code": "BD-55", + "name": "Rangpur" + }, + { + "code": "BD-58", + "name": "Satkhira" + }, + { + "code": "BD-62", + "name": "Shariatpur" + }, + { + "code": "BD-57", + "name": "Sherpur" + }, + { + "code": "BD-59", + "name": "Sirajganj" + }, + { + "code": "BD-61", + "name": "Sunamganj" + }, + { + "code": "BD-60", + "name": "Sylhet" + }, + { + "code": "BD-63", + "name": "Tangail" + }, + { + "code": "BD-64", + "name": "Thakurgaon" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BB", + "name": "Barbados", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BY", + "name": "Belarus", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PW", + "name": "Belau", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BE", + "name": "Belgium", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BZ", + "name": "Belize", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BJ", + "name": "Benin", + "states": [{ + "code": "AL", + "name": "Alibori" + }, + { + "code": "AK", + "name": "Atakora" + }, + { + "code": "AQ", + "name": "Atlantique" + }, + { + "code": "BO", + "name": "Borgou" + }, + { + "code": "CO", + "name": "Collines" + }, + { + "code": "KO", + "name": "Kouffo" + }, + { + "code": "DO", + "name": "Donga" + }, + { + "code": "LI", + "name": "Littoral" + }, + { + "code": "MO", + "name": "Mono" + }, + { + "code": "OU", + "name": "Ouémé" + }, + { + "code": "PL", + "name": "Plateau" + }, + { + "code": "ZO", + "name": "Zou" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BM", + "name": "Bermuda", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BT", + "name": "Bhutan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BO", + "name": "Bolivia", + "states": [{ + "code": "BO-B", + "name": "Beni" + }, + { + "code": "BO-H", + "name": "Chuquisaca" + }, + { + "code": "BO-C", + "name": "Cochabamba" + }, + { + "code": "BO-L", + "name": "La Paz" + }, + { + "code": "BO-O", + "name": "Oruro" + }, + { + "code": "BO-N", + "name": "Pando" + }, + { + "code": "BO-P", + "name": "Potosí" + }, + { + "code": "BO-S", + "name": "Santa Cruz" + }, + { + "code": "BO-T", + "name": "Tarija" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BQ", + "name": "Bonaire, Saint Eustatius and Saba", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BA", + "name": "Bosnia and Herzegovina", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BW", + "name": "Botswana", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BV", + "name": "Bouvet Island", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BR", + "name": "Brazil", + "states": [{ + "code": "AC", + "name": "Acre" + }, + { + "code": "AL", + "name": "Alagoas" + }, + { + "code": "AP", + "name": "Amapá" + }, + { + "code": "AM", + "name": "Amazonas" + }, + { + "code": "BA", + "name": "Bahia" + }, + { + "code": "CE", + "name": "Ceará" + }, + { + "code": "DF", + "name": "Distrito Federal" + }, + { + "code": "ES", + "name": "Espírito Santo" + }, + { + "code": "GO", + "name": "Goiás" + }, + { + "code": "MA", + "name": "Maranhão" + }, + { + "code": "MT", + "name": "Mato Grosso" + }, + { + "code": "MS", + "name": "Mato Grosso do Sul" + }, + { + "code": "MG", + "name": "Minas Gerais" + }, + { + "code": "PA", + "name": "Pará" + }, + { + "code": "PB", + "name": "Paraíba" + }, + { + "code": "PR", + "name": "Paraná" + }, + { + "code": "PE", + "name": "Pernambuco" + }, + { + "code": "PI", + "name": "Piauí" + }, + { + "code": "RJ", + "name": "Rio de Janeiro" + }, + { + "code": "RN", + "name": "Rio Grande do Norte" + }, + { + "code": "RS", + "name": "Rio Grande do Sul" + }, + { + "code": "RO", + "name": "Rondônia" + }, + { + "code": "RR", + "name": "Roraima" + }, + { + "code": "SC", + "name": "Santa Catarina" + }, + { + "code": "SP", + "name": "São Paulo" + }, + { + "code": "SE", + "name": "Sergipe" + }, + { + "code": "TO", + "name": "Tocantins" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IO", + "name": "British Indian Ocean Territory", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BN", + "name": "Brunei", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BG", + "name": "Bulgaria", + "states": [{ + "code": "BG-01", + "name": "Blagoevgrad" + }, + { + "code": "BG-02", + "name": "Burgas" + }, + { + "code": "BG-08", + "name": "Dobrich" + }, + { + "code": "BG-07", + "name": "Gabrovo" + }, + { + "code": "BG-26", + "name": "Haskovo" + }, + { + "code": "BG-09", + "name": "Kardzhali" + }, + { + "code": "BG-10", + "name": "Kyustendil" + }, + { + "code": "BG-11", + "name": "Lovech" + }, + { + "code": "BG-12", + "name": "Montana" + }, + { + "code": "BG-13", + "name": "Pazardzhik" + }, + { + "code": "BG-14", + "name": "Pernik" + }, + { + "code": "BG-15", + "name": "Pleven" + }, + { + "code": "BG-16", + "name": "Plovdiv" + }, + { + "code": "BG-17", + "name": "Razgrad" + }, + { + "code": "BG-18", + "name": "Ruse" + }, + { + "code": "BG-27", + "name": "Shumen" + }, + { + "code": "BG-19", + "name": "Silistra" + }, + { + "code": "BG-20", + "name": "Sliven" + }, + { + "code": "BG-21", + "name": "Smolyan" + }, + { + "code": "BG-23", + "name": "Sofia District" + }, + { + "code": "BG-22", + "name": "Sofia" + }, + { + "code": "BG-24", + "name": "Stara Zagora" + }, + { + "code": "BG-25", + "name": "Targovishte" + }, + { + "code": "BG-03", + "name": "Varna" + }, + { + "code": "BG-04", + "name": "Veliko Tarnovo" + }, + { + "code": "BG-05", + "name": "Vidin" + }, + { + "code": "BG-06", + "name": "Vratsa" + }, + { + "code": "BG-28", + "name": "Yambol" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BF", + "name": "Burkina Faso", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BI", + "name": "Burundi", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KH", + "name": "Cambodia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CM", + "name": "Cameroon", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CA", + "name": "Canada", + "states": [{ + "code": "AB", + "name": "Alberta" + }, + { + "code": "BC", + "name": "British Columbia" + }, + { + "code": "MB", + "name": "Manitoba" + }, + { + "code": "NB", + "name": "New Brunswick" + }, + { + "code": "NL", + "name": "Newfoundland and Labrador" + }, + { + "code": "NT", + "name": "Northwest Territories" + }, + { + "code": "NS", + "name": "Nova Scotia" + }, + { + "code": "NU", + "name": "Nunavut" + }, + { + "code": "ON", + "name": "Ontario" + }, + { + "code": "PE", + "name": "Prince Edward Island" + }, + { + "code": "QC", + "name": "Quebec" + }, + { + "code": "SK", + "name": "Saskatchewan" + }, + { + "code": "YT", + "name": "Yukon Territory" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CV", + "name": "Cape Verde", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KY", + "name": "Cayman Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CF", + "name": "Central African Republic", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TD", + "name": "Chad", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CL", + "name": "Chile", + "states": [{ + "code": "CL-AI", + "name": "Aisén del General Carlos Ibañez del Campo" + }, + { + "code": "CL-AN", + "name": "Antofagasta" + }, + { + "code": "CL-AP", + "name": "Arica y Parinacota" + }, + { + "code": "CL-AR", + "name": "La Araucanía" + }, + { + "code": "CL-AT", + "name": "Atacama" + }, + { + "code": "CL-BI", + "name": "Biobío" + }, + { + "code": "CL-CO", + "name": "Coquimbo" + }, + { + "code": "CL-LI", + "name": "Libertador General Bernardo O'Higgins" + }, + { + "code": "CL-LL", + "name": "Los Lagos" + }, + { + "code": "CL-LR", + "name": "Los Ríos" + }, + { + "code": "CL-MA", + "name": "Magallanes" + }, + { + "code": "CL-ML", + "name": "Maule" + }, + { + "code": "CL-NB", + "name": "Ñuble" + }, + { + "code": "CL-RM", + "name": "Región Metropolitana de Santiago" + }, + { + "code": "CL-TA", + "name": "Tarapacá" + }, + { + "code": "CL-VS", + "name": "Valparaíso" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CN", + "name": "China", + "states": [{ + "code": "CN1", + "name": "Yunnan / 云南" + }, + { + "code": "CN2", + "name": "Beijing / 北京" + }, + { + "code": "CN3", + "name": "Tianjin / 天津" + }, + { + "code": "CN4", + "name": "Hebei / 河北" + }, + { + "code": "CN5", + "name": "Shanxi / 山西" + }, + { + "code": "CN6", + "name": "Inner Mongolia / 內蒙古" + }, + { + "code": "CN7", + "name": "Liaoning / 辽宁" + }, + { + "code": "CN8", + "name": "Jilin / 吉林" + }, + { + "code": "CN9", + "name": "Heilongjiang / 黑龙江" + }, + { + "code": "CN10", + "name": "Shanghai / 上海" + }, + { + "code": "CN11", + "name": "Jiangsu / 江苏" + }, + { + "code": "CN12", + "name": "Zhejiang / 浙江" + }, + { + "code": "CN13", + "name": "Anhui / 安徽" + }, + { + "code": "CN14", + "name": "Fujian / 福建" + }, + { + "code": "CN15", + "name": "Jiangxi / 江西" + }, + { + "code": "CN16", + "name": "Shandong / 山东" + }, + { + "code": "CN17", + "name": "Henan / 河南" + }, + { + "code": "CN18", + "name": "Hubei / 湖北" + }, + { + "code": "CN19", + "name": "Hunan / 湖南" + }, + { + "code": "CN20", + "name": "Guangdong / 广东" + }, + { + "code": "CN21", + "name": "Guangxi Zhuang / 广西壮族" + }, + { + "code": "CN22", + "name": "Hainan / 海南" + }, + { + "code": "CN23", + "name": "Chongqing / 重庆" + }, + { + "code": "CN24", + "name": "Sichuan / 四川" + }, + { + "code": "CN25", + "name": "Guizhou / 贵州" + }, + { + "code": "CN26", + "name": "Shaanxi / 陕西" + }, + { + "code": "CN27", + "name": "Gansu / 甘肃" + }, + { + "code": "CN28", + "name": "Qinghai / 青海" + }, + { + "code": "CN29", + "name": "Ningxia Hui / 宁夏" + }, + { + "code": "CN30", + "name": "Macao / 澳门" + }, + { + "code": "CN31", + "name": "Tibet / 西藏" + }, + { + "code": "CN32", + "name": "Xinjiang / 新疆" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CX", + "name": "Christmas Island", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CC", + "name": "Cocos (Keeling) Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CO", + "name": "Colombia", + "states": [{ + "code": "CO-AMA", + "name": "Amazonas" + }, + { + "code": "CO-ANT", + "name": "Antioquia" + }, + { + "code": "CO-ARA", + "name": "Arauca" + }, + { + "code": "CO-ATL", + "name": "Atlántico" + }, + { + "code": "CO-BOL", + "name": "Bolívar" + }, + { + "code": "CO-BOY", + "name": "Boyacá" + }, + { + "code": "CO-CAL", + "name": "Caldas" + }, + { + "code": "CO-CAQ", + "name": "Caquetá" + }, + { + "code": "CO-CAS", + "name": "Casanare" + }, + { + "code": "CO-CAU", + "name": "Cauca" + }, + { + "code": "CO-CES", + "name": "Cesar" + }, + { + "code": "CO-CHO", + "name": "Chocó" + }, + { + "code": "CO-COR", + "name": "Córdoba" + }, + { + "code": "CO-CUN", + "name": "Cundinamarca" + }, + { + "code": "CO-DC", + "name": "Capital District" + }, + { + "code": "CO-GUA", + "name": "Guainía" + }, + { + "code": "CO-GUV", + "name": "Guaviare" + }, + { + "code": "CO-HUI", + "name": "Huila" + }, + { + "code": "CO-LAG", + "name": "La Guajira" + }, + { + "code": "CO-MAG", + "name": "Magdalena" + }, + { + "code": "CO-MET", + "name": "Meta" + }, + { + "code": "CO-NAR", + "name": "Nariño" + }, + { + "code": "CO-NSA", + "name": "Norte de Santander" + }, + { + "code": "CO-PUT", + "name": "Putumayo" + }, + { + "code": "CO-QUI", + "name": "Quindío" + }, + { + "code": "CO-RIS", + "name": "Risaralda" + }, + { + "code": "CO-SAN", + "name": "Santander" + }, + { + "code": "CO-SAP", + "name": "San Andrés & Providencia" + }, + { + "code": "CO-SUC", + "name": "Sucre" + }, + { + "code": "CO-TOL", + "name": "Tolima" + }, + { + "code": "CO-VAC", + "name": "Valle del Cauca" + }, + { + "code": "CO-VAU", + "name": "Vaupés" + }, + { + "code": "CO-VID", + "name": "Vichada" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KM", + "name": "Comoros", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CG", + "name": "Congo (Brazzaville)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CD", + "name": "Congo (Kinshasa)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CK", + "name": "Cook Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CR", + "name": "Costa Rica", + "states": [{ + "code": "CR-A", + "name": "Alajuela" + }, + { + "code": "CR-C", + "name": "Cartago" + }, + { + "code": "CR-G", + "name": "Guanacaste" + }, + { + "code": "CR-H", + "name": "Heredia" + }, + { + "code": "CR-L", + "name": "Limón" + }, + { + "code": "CR-P", + "name": "Puntarenas" + }, + { + "code": "CR-SJ", + "name": "San José" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HR", + "name": "Croatia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CU", + "name": "Cuba", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CW", + "name": "Curaçao", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CY", + "name": "Cyprus", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CZ", + "name": "Czech Republic", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DK", + "name": "Denmark", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DJ", + "name": "Djibouti", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DM", + "name": "Dominica", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DO", + "name": "Dominican Republic", + "states": [{ + "code": "DO-01", + "name": "Distrito Nacional" + }, + { + "code": "DO-02", + "name": "Azua" + }, + { + "code": "DO-03", + "name": "Baoruco" + }, + { + "code": "DO-04", + "name": "Barahona" + }, + { + "code": "DO-33", + "name": "Cibao Nordeste" + }, + { + "code": "DO-34", + "name": "Cibao Noroeste" + }, + { + "code": "DO-35", + "name": "Cibao Norte" + }, + { + "code": "DO-36", + "name": "Cibao Sur" + }, + { + "code": "DO-05", + "name": "Dajabón" + }, + { + "code": "DO-06", + "name": "Duarte" + }, + { + "code": "DO-08", + "name": "El Seibo" + }, + { + "code": "DO-37", + "name": "El Valle" + }, + { + "code": "DO-07", + "name": "Elías Piña" + }, + { + "code": "DO-38", + "name": "Enriquillo" + }, + { + "code": "DO-09", + "name": "Espaillat" + }, + { + "code": "DO-30", + "name": "Hato Mayor" + }, + { + "code": "DO-19", + "name": "Hermanas Mirabal" + }, + { + "code": "DO-39", + "name": "Higüamo" + }, + { + "code": "DO-10", + "name": "Independencia" + }, + { + "code": "DO-11", + "name": "La Altagracia" + }, + { + "code": "DO-12", + "name": "La Romana" + }, + { + "code": "DO-13", + "name": "La Vega" + }, + { + "code": "DO-14", + "name": "María Trinidad Sánchez" + }, + { + "code": "DO-28", + "name": "Monseñor Nouel" + }, + { + "code": "DO-15", + "name": "Monte Cristi" + }, + { + "code": "DO-29", + "name": "Monte Plata" + }, + { + "code": "DO-40", + "name": "Ozama" + }, + { + "code": "DO-16", + "name": "Pedernales" + }, + { + "code": "DO-17", + "name": "Peravia" + }, + { + "code": "DO-18", + "name": "Puerto Plata" + }, + { + "code": "DO-20", + "name": "Samaná" + }, + { + "code": "DO-21", + "name": "San Cristóbal" + }, + { + "code": "DO-31", + "name": "San José de Ocoa" + }, + { + "code": "DO-22", + "name": "San Juan" + }, + { + "code": "DO-23", + "name": "San Pedro de Macorís" + }, + { + "code": "DO-24", + "name": "Sánchez Ramírez" + }, + { + "code": "DO-25", + "name": "Santiago" + }, + { + "code": "DO-26", + "name": "Santiago Rodríguez" + }, + { + "code": "DO-32", + "name": "Santo Domingo" + }, + { + "code": "DO-41", + "name": "Valdesia" + }, + { + "code": "DO-27", + "name": "Valverde" + }, + { + "code": "DO-42", + "name": "Yuma" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "EC", + "name": "Ecuador", + "states": [{ + "code": "EC-A", + "name": "Azuay" + }, + { + "code": "EC-B", + "name": "Bolívar" + }, + { + "code": "EC-F", + "name": "Cañar" + }, + { + "code": "EC-C", + "name": "Carchi" + }, + { + "code": "EC-H", + "name": "Chimborazo" + }, + { + "code": "EC-X", + "name": "Cotopaxi" + }, + { + "code": "EC-O", + "name": "El Oro" + }, + { + "code": "EC-E", + "name": "Esmeraldas" + }, + { + "code": "EC-W", + "name": "Galápagos" + }, + { + "code": "EC-G", + "name": "Guayas" + }, + { + "code": "EC-I", + "name": "Imbabura" + }, + { + "code": "EC-L", + "name": "Loja" + }, + { + "code": "EC-R", + "name": "Los Ríos" + }, + { + "code": "EC-M", + "name": "Manabí" + }, + { + "code": "EC-S", + "name": "Morona-Santiago" + }, + { + "code": "EC-N", + "name": "Napo" + }, + { + "code": "EC-D", + "name": "Orellana" + }, + { + "code": "EC-Y", + "name": "Pastaza" + }, + { + "code": "EC-P", + "name": "Pichincha" + }, + { + "code": "EC-SE", + "name": "Santa Elena" + }, + { + "code": "EC-SD", + "name": "Santo Domingo de los Tsáchilas" + }, + { + "code": "EC-U", + "name": "Sucumbíos" + }, + { + "code": "EC-T", + "name": "Tungurahua" + }, + { + "code": "EC-Z", + "name": "Zamora-Chinchipe" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "EG", + "name": "Egypt", + "states": [{ + "code": "EGALX", + "name": "Alexandria" + }, + { + "code": "EGASN", + "name": "Aswan" + }, + { + "code": "EGAST", + "name": "Asyut" + }, + { + "code": "EGBA", + "name": "Red Sea" + }, + { + "code": "EGBH", + "name": "Beheira" + }, + { + "code": "EGBNS", + "name": "Beni Suef" + }, + { + "code": "EGC", + "name": "Cairo" + }, + { + "code": "EGDK", + "name": "Dakahlia" + }, + { + "code": "EGDT", + "name": "Damietta" + }, + { + "code": "EGFYM", + "name": "Faiyum" + }, + { + "code": "EGGH", + "name": "Gharbia" + }, + { + "code": "EGGZ", + "name": "Giza" + }, + { + "code": "EGIS", + "name": "Ismailia" + }, + { + "code": "EGJS", + "name": "South Sinai" + }, + { + "code": "EGKB", + "name": "Qalyubia" + }, + { + "code": "EGKFS", + "name": "Kafr el-Sheikh" + }, + { + "code": "EGKN", + "name": "Qena" + }, + { + "code": "EGLX", + "name": "Luxor" + }, + { + "code": "EGMN", + "name": "Minya" + }, + { + "code": "EGMNF", + "name": "Monufia" + }, + { + "code": "EGMT", + "name": "Matrouh" + }, + { + "code": "EGPTS", + "name": "Port Said" + }, + { + "code": "EGSHG", + "name": "Sohag" + }, + { + "code": "EGSHR", + "name": "Al Sharqia" + }, + { + "code": "EGSIN", + "name": "North Sinai" + }, + { + "code": "EGSUZ", + "name": "Suez" + }, + { + "code": "EGWAD", + "name": "New Valley" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SV", + "name": "El Salvador", + "states": [{ + "code": "SV-AH", + "name": "Ahuachapán" + }, + { + "code": "SV-CA", + "name": "Cabañas" + }, + { + "code": "SV-CH", + "name": "Chalatenango" + }, + { + "code": "SV-CU", + "name": "Cuscatlán" + }, + { + "code": "SV-LI", + "name": "La Libertad" + }, + { + "code": "SV-MO", + "name": "Morazán" + }, + { + "code": "SV-PA", + "name": "La Paz" + }, + { + "code": "SV-SA", + "name": "Santa Ana" + }, + { + "code": "SV-SM", + "name": "San Miguel" + }, + { + "code": "SV-SO", + "name": "Sonsonate" + }, + { + "code": "SV-SS", + "name": "San Salvador" + }, + { + "code": "SV-SV", + "name": "San Vicente" + }, + { + "code": "SV-UN", + "name": "La Unión" + }, + { + "code": "SV-US", + "name": "Usulután" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GQ", + "name": "Equatorial Guinea", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ER", + "name": "Eritrea", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "EE", + "name": "Estonia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SZ", + "name": "Eswatini", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ET", + "name": "Ethiopia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FK", + "name": "Falkland Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FO", + "name": "Faroe Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FJ", + "name": "Fiji", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FI", + "name": "Finland", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FR", + "name": "France", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GF", + "name": "French Guiana", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PF", + "name": "French Polynesia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TF", + "name": "French Southern Territories", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GA", + "name": "Gabon", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GM", + "name": "Gambia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GE", + "name": "Georgia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DE", + "name": "Germany", + "states": [{ + "code": "DE-BW", + "name": "Baden-Württemberg" + }, + { + "code": "DE-BY", + "name": "Bavaria" + }, + { + "code": "DE-BE", + "name": "Berlin" + }, + { + "code": "DE-BB", + "name": "Brandenburg" + }, + { + "code": "DE-HB", + "name": "Bremen" + }, + { + "code": "DE-HH", + "name": "Hamburg" + }, + { + "code": "DE-HE", + "name": "Hesse" + }, + { + "code": "DE-MV", + "name": "Mecklenburg-Vorpommern" + }, + { + "code": "DE-NI", + "name": "Lower Saxony" + }, + { + "code": "DE-NW", + "name": "North Rhine-Westphalia" + }, + { + "code": "DE-RP", + "name": "Rhineland-Palatinate" + }, + { + "code": "DE-SL", + "name": "Saarland" + }, + { + "code": "DE-SN", + "name": "Saxony" + }, + { + "code": "DE-ST", + "name": "Saxony-Anhalt" + }, + { + "code": "DE-SH", + "name": "Schleswig-Holstein" + }, + { + "code": "DE-TH", + "name": "Thuringia" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GH", + "name": "Ghana", + "states": [{ + "code": "AF", + "name": "Ahafo" + }, + { + "code": "AH", + "name": "Ashanti" + }, + { + "code": "BA", + "name": "Brong-Ahafo" + }, + { + "code": "BO", + "name": "Bono" + }, + { + "code": "BE", + "name": "Bono East" + }, + { + "code": "CP", + "name": "Central" + }, + { + "code": "EP", + "name": "Eastern" + }, + { + "code": "AA", + "name": "Greater Accra" + }, + { + "code": "NE", + "name": "North East" + }, + { + "code": "NP", + "name": "Northern" + }, + { + "code": "OT", + "name": "Oti" + }, + { + "code": "SV", + "name": "Savannah" + }, + { + "code": "UE", + "name": "Upper East" + }, + { + "code": "UW", + "name": "Upper West" + }, + { + "code": "TV", + "name": "Volta" + }, + { + "code": "WP", + "name": "Western" + }, + { + "code": "WN", + "name": "Western North" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GI", + "name": "Gibraltar", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GR", + "name": "Greece", + "states": [{ + "code": "I", + "name": "Attica" + }, + { + "code": "A", + "name": "East Macedonia and Thrace" + }, + { + "code": "B", + "name": "Central Macedonia" + }, + { + "code": "C", + "name": "West Macedonia" + }, + { + "code": "D", + "name": "Epirus" + }, + { + "code": "E", + "name": "Thessaly" + }, + { + "code": "F", + "name": "Ionian Islands" + }, + { + "code": "G", + "name": "West Greece" + }, + { + "code": "H", + "name": "Central Greece" + }, + { + "code": "J", + "name": "Peloponnese" + }, + { + "code": "K", + "name": "North Aegean" + }, + { + "code": "L", + "name": "South Aegean" + }, + { + "code": "M", + "name": "Crete" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GL", + "name": "Greenland", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GD", + "name": "Grenada", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GP", + "name": "Guadeloupe", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GU", + "name": "Guam", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GT", + "name": "Guatemala", + "states": [{ + "code": "GT-AV", + "name": "Alta Verapaz" + }, + { + "code": "GT-BV", + "name": "Baja Verapaz" + }, + { + "code": "GT-CM", + "name": "Chimaltenango" + }, + { + "code": "GT-CQ", + "name": "Chiquimula" + }, + { + "code": "GT-PR", + "name": "El Progreso" + }, + { + "code": "GT-ES", + "name": "Escuintla" + }, + { + "code": "GT-GU", + "name": "Guatemala" + }, + { + "code": "GT-HU", + "name": "Huehuetenango" + }, + { + "code": "GT-IZ", + "name": "Izabal" + }, + { + "code": "GT-JA", + "name": "Jalapa" + }, + { + "code": "GT-JU", + "name": "Jutiapa" + }, + { + "code": "GT-PE", + "name": "Petén" + }, + { + "code": "GT-QZ", + "name": "Quetzaltenango" + }, + { + "code": "GT-QC", + "name": "Quiché" + }, + { + "code": "GT-RE", + "name": "Retalhuleu" + }, + { + "code": "GT-SA", + "name": "Sacatepéquez" + }, + { + "code": "GT-SM", + "name": "San Marcos" + }, + { + "code": "GT-SR", + "name": "Santa Rosa" + }, + { + "code": "GT-SO", + "name": "Sololá" + }, + { + "code": "GT-SU", + "name": "Suchitepéquez" + }, + { + "code": "GT-TO", + "name": "Totonicapán" + }, + { + "code": "GT-ZA", + "name": "Zacapa" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GG", + "name": "Guernsey", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GN", + "name": "Guinea", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GW", + "name": "Guinea-Bissau", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GY", + "name": "Guyana", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HT", + "name": "Haiti", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HM", + "name": "Heard Island and McDonald Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HN", + "name": "Honduras", + "states": [{ + "code": "HN-AT", + "name": "Atlántida" + }, + { + "code": "HN-IB", + "name": "Bay Islands" + }, + { + "code": "HN-CH", + "name": "Choluteca" + }, + { + "code": "HN-CL", + "name": "Colón" + }, + { + "code": "HN-CM", + "name": "Comayagua" + }, + { + "code": "HN-CP", + "name": "Copán" + }, + { + "code": "HN-CR", + "name": "Cortés" + }, + { + "code": "HN-EP", + "name": "El Paraíso" + }, + { + "code": "HN-FM", + "name": "Francisco Morazán" + }, + { + "code": "HN-GD", + "name": "Gracias a Dios" + }, + { + "code": "HN-IN", + "name": "Intibucá" + }, + { + "code": "HN-LE", + "name": "Lempira" + }, + { + "code": "HN-LP", + "name": "La Paz" + }, + { + "code": "HN-OC", + "name": "Ocotepeque" + }, + { + "code": "HN-OL", + "name": "Olancho" + }, + { + "code": "HN-SB", + "name": "Santa Bárbara" + }, + { + "code": "HN-VA", + "name": "Valle" + }, + { + "code": "HN-YO", + "name": "Yoro" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HK", + "name": "Hong Kong", + "states": [{ + "code": "HONG KONG", + "name": "Hong Kong Island" + }, + { + "code": "KOWLOON", + "name": "Kowloon" + }, + { + "code": "NEW TERRITORIES", + "name": "New Territories" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HU", + "name": "Hungary", + "states": [{ + "code": "BK", + "name": "Bács-Kiskun" + }, + { + "code": "BE", + "name": "Békés" + }, + { + "code": "BA", + "name": "Baranya" + }, + { + "code": "BZ", + "name": "Borsod-Abaúj-Zemplén" + }, + { + "code": "BU", + "name": "Budapest" + }, + { + "code": "CS", + "name": "Csongrád-Csanád" + }, + { + "code": "FE", + "name": "Fejér" + }, + { + "code": "GS", + "name": "Győr-Moson-Sopron" + }, + { + "code": "HB", + "name": "Hajdú-Bihar" + }, + { + "code": "HE", + "name": "Heves" + }, + { + "code": "JN", + "name": "Jász-Nagykun-Szolnok" + }, + { + "code": "KE", + "name": "Komárom-Esztergom" + }, + { + "code": "NO", + "name": "Nógrád" + }, + { + "code": "PE", + "name": "Pest" + }, + { + "code": "SO", + "name": "Somogy" + }, + { + "code": "SZ", + "name": "Szabolcs-Szatmár-Bereg" + }, + { + "code": "TO", + "name": "Tolna" + }, + { + "code": "VA", + "name": "Vas" + }, + { + "code": "VE", + "name": "Veszprém" + }, + { + "code": "ZA", + "name": "Zala" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IS", + "name": "Iceland", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IN", + "name": "India", + "states": [{ + "code": "AP", + "name": "Andhra Pradesh" + }, + { + "code": "AR", + "name": "Arunachal Pradesh" + }, + { + "code": "AS", + "name": "Assam" + }, + { + "code": "BR", + "name": "Bihar" + }, + { + "code": "CT", + "name": "Chhattisgarh" + }, + { + "code": "GA", + "name": "Goa" + }, + { + "code": "GJ", + "name": "Gujarat" + }, + { + "code": "HR", + "name": "Haryana" + }, + { + "code": "HP", + "name": "Himachal Pradesh" + }, + { + "code": "JK", + "name": "Jammu and Kashmir" + }, + { + "code": "JH", + "name": "Jharkhand" + }, + { + "code": "KA", + "name": "Karnataka" + }, + { + "code": "KL", + "name": "Kerala" + }, + { + "code": "LA", + "name": "Ladakh" + }, + { + "code": "MP", + "name": "Madhya Pradesh" + }, + { + "code": "MH", + "name": "Maharashtra" + }, + { + "code": "MN", + "name": "Manipur" + }, + { + "code": "ML", + "name": "Meghalaya" + }, + { + "code": "MZ", + "name": "Mizoram" + }, + { + "code": "NL", + "name": "Nagaland" + }, + { + "code": "OR", + "name": "Odisha" + }, + { + "code": "PB", + "name": "Punjab" + }, + { + "code": "RJ", + "name": "Rajasthan" + }, + { + "code": "SK", + "name": "Sikkim" + }, + { + "code": "TN", + "name": "Tamil Nadu" + }, + { + "code": "TS", + "name": "Telangana" + }, + { + "code": "TR", + "name": "Tripura" + }, + { + "code": "UK", + "name": "Uttarakhand" + }, + { + "code": "UP", + "name": "Uttar Pradesh" + }, + { + "code": "WB", + "name": "West Bengal" + }, + { + "code": "AN", + "name": "Andaman and Nicobar Islands" + }, + { + "code": "CH", + "name": "Chandigarh" + }, + { + "code": "DN", + "name": "Dadra and Nagar Haveli" + }, + { + "code": "DD", + "name": "Daman and Diu" + }, + { + "code": "DL", + "name": "Delhi" + }, + { + "code": "LD", + "name": "Lakshadeep" + }, + { + "code": "PY", + "name": "Pondicherry (Puducherry)" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ID", + "name": "Indonesia", + "states": [{ + "code": "AC", + "name": "Daerah Istimewa Aceh" + }, + { + "code": "SU", + "name": "Sumatera Utara" + }, + { + "code": "SB", + "name": "Sumatera Barat" + }, + { + "code": "RI", + "name": "Riau" + }, + { + "code": "KR", + "name": "Kepulauan Riau" + }, + { + "code": "JA", + "name": "Jambi" + }, + { + "code": "SS", + "name": "Sumatera Selatan" + }, + { + "code": "BB", + "name": "Bangka Belitung" + }, + { + "code": "BE", + "name": "Bengkulu" + }, + { + "code": "LA", + "name": "Lampung" + }, + { + "code": "JK", + "name": "DKI Jakarta" + }, + { + "code": "JB", + "name": "Jawa Barat" + }, + { + "code": "BT", + "name": "Banten" + }, + { + "code": "JT", + "name": "Jawa Tengah" + }, + { + "code": "JI", + "name": "Jawa Timur" + }, + { + "code": "YO", + "name": "Daerah Istimewa Yogyakarta" + }, + { + "code": "BA", + "name": "Bali" + }, + { + "code": "NB", + "name": "Nusa Tenggara Barat" + }, + { + "code": "NT", + "name": "Nusa Tenggara Timur" + }, + { + "code": "KB", + "name": "Kalimantan Barat" + }, + { + "code": "KT", + "name": "Kalimantan Tengah" + }, + { + "code": "KI", + "name": "Kalimantan Timur" + }, + { + "code": "KS", + "name": "Kalimantan Selatan" + }, + { + "code": "KU", + "name": "Kalimantan Utara" + }, + { + "code": "SA", + "name": "Sulawesi Utara" + }, + { + "code": "ST", + "name": "Sulawesi Tengah" + }, + { + "code": "SG", + "name": "Sulawesi Tenggara" + }, + { + "code": "SR", + "name": "Sulawesi Barat" + }, + { + "code": "SN", + "name": "Sulawesi Selatan" + }, + { + "code": "GO", + "name": "Gorontalo" + }, + { + "code": "MA", + "name": "Maluku" + }, + { + "code": "MU", + "name": "Maluku Utara" + }, + { + "code": "PA", + "name": "Papua" + }, + { + "code": "PB", + "name": "Papua Barat" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IR", + "name": "Iran", + "states": [{ + "code": "KHZ", + "name": "Khuzestan (خوزستان)" + }, + { + "code": "THR", + "name": "Tehran (تهران)" + }, + { + "code": "ILM", + "name": "Ilaam (ایلام)" + }, + { + "code": "BHR", + "name": "Bushehr (بوشهر)" + }, + { + "code": "ADL", + "name": "Ardabil (اردبیل)" + }, + { + "code": "ESF", + "name": "Isfahan (اصفهان)" + }, + { + "code": "YZD", + "name": "Yazd (یزد)" + }, + { + "code": "KRH", + "name": "Kermanshah (کرمانشاه)" + }, + { + "code": "KRN", + "name": "Kerman (کرمان)" + }, + { + "code": "HDN", + "name": "Hamadan (همدان)" + }, + { + "code": "GZN", + "name": "Ghazvin (قزوین)" + }, + { + "code": "ZJN", + "name": "Zanjan (زنجان)" + }, + { + "code": "LRS", + "name": "Luristan (لرستان)" + }, + { + "code": "ABZ", + "name": "Alborz (البرز)" + }, + { + "code": "EAZ", + "name": "East Azarbaijan (آذربایجان شرقی)" + }, + { + "code": "WAZ", + "name": "West Azarbaijan (آذربایجان غربی)" + }, + { + "code": "CHB", + "name": "Chaharmahal and Bakhtiari (چهارمحال و بختیاری)" + }, + { + "code": "SKH", + "name": "South Khorasan (خراسان جنوبی)" + }, + { + "code": "RKH", + "name": "Razavi Khorasan (خراسان رضوی)" + }, + { + "code": "NKH", + "name": "North Khorasan (خراسان شمالی)" + }, + { + "code": "SMN", + "name": "Semnan (سمنان)" + }, + { + "code": "FRS", + "name": "Fars (فارس)" + }, + { + "code": "QHM", + "name": "Qom (قم)" + }, + { + "code": "KRD", + "name": "Kurdistan / کردستان)" + }, + { + "code": "KBD", + "name": "Kohgiluyeh and BoyerAhmad (کهگیلوییه و بویراحمد)" + }, + { + "code": "GLS", + "name": "Golestan (گلستان)" + }, + { + "code": "GIL", + "name": "Gilan (گیلان)" + }, + { + "code": "MZN", + "name": "Mazandaran (مازندران)" + }, + { + "code": "MKZ", + "name": "Markazi (مرکزی)" + }, + { + "code": "HRZ", + "name": "Hormozgan (هرمزگان)" + }, + { + "code": "SBN", + "name": "Sistan and Baluchestan (سیستان و بلوچستان)" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IQ", + "name": "Iraq", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IE", + "name": "Ireland", + "states": [{ + "code": "CW", + "name": "Carlow" + }, + { + "code": "CN", + "name": "Cavan" + }, + { + "code": "CE", + "name": "Clare" + }, + { + "code": "CO", + "name": "Cork" + }, + { + "code": "DL", + "name": "Donegal" + }, + { + "code": "D", + "name": "Dublin" + }, + { + "code": "G", + "name": "Galway" + }, + { + "code": "KY", + "name": "Kerry" + }, + { + "code": "KE", + "name": "Kildare" + }, + { + "code": "KK", + "name": "Kilkenny" + }, + { + "code": "LS", + "name": "Laois" + }, + { + "code": "LM", + "name": "Leitrim" + }, + { + "code": "LK", + "name": "Limerick" + }, + { + "code": "LD", + "name": "Longford" + }, + { + "code": "LH", + "name": "Louth" + }, + { + "code": "MO", + "name": "Mayo" + }, + { + "code": "MH", + "name": "Meath" + }, + { + "code": "MN", + "name": "Monaghan" + }, + { + "code": "OY", + "name": "Offaly" + }, + { + "code": "RN", + "name": "Roscommon" + }, + { + "code": "SO", + "name": "Sligo" + }, + { + "code": "TA", + "name": "Tipperary" + }, + { + "code": "WD", + "name": "Waterford" + }, + { + "code": "WH", + "name": "Westmeath" + }, + { + "code": "WX", + "name": "Wexford" + }, + { + "code": "WW", + "name": "Wicklow" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IM", + "name": "Isle of Man", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IL", + "name": "Israel", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IT", + "name": "Italy", + "states": [{ + "code": "AG", + "name": "Agrigento" + }, + { + "code": "AL", + "name": "Alessandria" + }, + { + "code": "AN", + "name": "Ancona" + }, + { + "code": "AO", + "name": "Aosta" + }, + { + "code": "AR", + "name": "Arezzo" + }, + { + "code": "AP", + "name": "Ascoli Piceno" + }, + { + "code": "AT", + "name": "Asti" + }, + { + "code": "AV", + "name": "Avellino" + }, + { + "code": "BA", + "name": "Bari" + }, + { + "code": "BT", + "name": "Barletta-Andria-Trani" + }, + { + "code": "BL", + "name": "Belluno" + }, + { + "code": "BN", + "name": "Benevento" + }, + { + "code": "BG", + "name": "Bergamo" + }, + { + "code": "BI", + "name": "Biella" + }, + { + "code": "BO", + "name": "Bologna" + }, + { + "code": "BZ", + "name": "Bolzano" + }, + { + "code": "BS", + "name": "Brescia" + }, + { + "code": "BR", + "name": "Brindisi" + }, + { + "code": "CA", + "name": "Cagliari" + }, + { + "code": "CL", + "name": "Caltanissetta" + }, + { + "code": "CB", + "name": "Campobasso" + }, + { + "code": "CE", + "name": "Caserta" + }, + { + "code": "CT", + "name": "Catania" + }, + { + "code": "CZ", + "name": "Catanzaro" + }, + { + "code": "CH", + "name": "Chieti" + }, + { + "code": "CO", + "name": "Como" + }, + { + "code": "CS", + "name": "Cosenza" + }, + { + "code": "CR", + "name": "Cremona" + }, + { + "code": "KR", + "name": "Crotone" + }, + { + "code": "CN", + "name": "Cuneo" + }, + { + "code": "EN", + "name": "Enna" + }, + { + "code": "FM", + "name": "Fermo" + }, + { + "code": "FE", + "name": "Ferrara" + }, + { + "code": "FI", + "name": "Firenze" + }, + { + "code": "FG", + "name": "Foggia" + }, + { + "code": "FC", + "name": "Forlì-Cesena" + }, + { + "code": "FR", + "name": "Frosinone" + }, + { + "code": "GE", + "name": "Genova" + }, + { + "code": "GO", + "name": "Gorizia" + }, + { + "code": "GR", + "name": "Grosseto" + }, + { + "code": "IM", + "name": "Imperia" + }, + { + "code": "IS", + "name": "Isernia" + }, + { + "code": "SP", + "name": "La Spezia" + }, + { + "code": "AQ", + "name": "L'Aquila" + }, + { + "code": "LT", + "name": "Latina" + }, + { + "code": "LE", + "name": "Lecce" + }, + { + "code": "LC", + "name": "Lecco" + }, + { + "code": "LI", + "name": "Livorno" + }, + { + "code": "LO", + "name": "Lodi" + }, + { + "code": "LU", + "name": "Lucca" + }, + { + "code": "MC", + "name": "Macerata" + }, + { + "code": "MN", + "name": "Mantova" + }, + { + "code": "MS", + "name": "Massa-Carrara" + }, + { + "code": "MT", + "name": "Matera" + }, + { + "code": "ME", + "name": "Messina" + }, + { + "code": "MI", + "name": "Milano" + }, + { + "code": "MO", + "name": "Modena" + }, + { + "code": "MB", + "name": "Monza e della Brianza" + }, + { + "code": "NA", + "name": "Napoli" + }, + { + "code": "NO", + "name": "Novara" + }, + { + "code": "NU", + "name": "Nuoro" + }, + { + "code": "OR", + "name": "Oristano" + }, + { + "code": "PD", + "name": "Padova" + }, + { + "code": "PA", + "name": "Palermo" + }, + { + "code": "PR", + "name": "Parma" + }, + { + "code": "PV", + "name": "Pavia" + }, + { + "code": "PG", + "name": "Perugia" + }, + { + "code": "PU", + "name": "Pesaro e Urbino" + }, + { + "code": "PE", + "name": "Pescara" + }, + { + "code": "PC", + "name": "Piacenza" + }, + { + "code": "PI", + "name": "Pisa" + }, + { + "code": "PT", + "name": "Pistoia" + }, + { + "code": "PN", + "name": "Pordenone" + }, + { + "code": "PZ", + "name": "Potenza" + }, + { + "code": "PO", + "name": "Prato" + }, + { + "code": "RG", + "name": "Ragusa" + }, + { + "code": "RA", + "name": "Ravenna" + }, + { + "code": "RC", + "name": "Reggio Calabria" + }, + { + "code": "RE", + "name": "Reggio Emilia" + }, + { + "code": "RI", + "name": "Rieti" + }, + { + "code": "RN", + "name": "Rimini" + }, + { + "code": "RM", + "name": "Roma" + }, + { + "code": "RO", + "name": "Rovigo" + }, + { + "code": "SA", + "name": "Salerno" + }, + { + "code": "SS", + "name": "Sassari" + }, + { + "code": "SV", + "name": "Savona" + }, + { + "code": "SI", + "name": "Siena" + }, + { + "code": "SR", + "name": "Siracusa" + }, + { + "code": "SO", + "name": "Sondrio" + }, + { + "code": "SU", + "name": "Sud Sardegna" + }, + { + "code": "TA", + "name": "Taranto" + }, + { + "code": "TE", + "name": "Teramo" + }, + { + "code": "TR", + "name": "Terni" + }, + { + "code": "TO", + "name": "Torino" + }, + { + "code": "TP", + "name": "Trapani" + }, + { + "code": "TN", + "name": "Trento" + }, + { + "code": "TV", + "name": "Treviso" + }, + { + "code": "TS", + "name": "Trieste" + }, + { + "code": "UD", + "name": "Udine" + }, + { + "code": "VA", + "name": "Varese" + }, + { + "code": "VE", + "name": "Venezia" + }, + { + "code": "VB", + "name": "Verbano-Cusio-Ossola" + }, + { + "code": "VC", + "name": "Vercelli" + }, + { + "code": "VR", + "name": "Verona" + }, + { + "code": "VV", + "name": "Vibo Valentia" + }, + { + "code": "VI", + "name": "Vicenza" + }, + { + "code": "VT", + "name": "Viterbo" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CI", + "name": "Ivory Coast", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JM", + "name": "Jamaica", + "states": [{ + "code": "JM-01", + "name": "Kingston" + }, + { + "code": "JM-02", + "name": "Saint Andrew" + }, + { + "code": "JM-03", + "name": "Saint Thomas" + }, + { + "code": "JM-04", + "name": "Portland" + }, + { + "code": "JM-05", + "name": "Saint Mary" + }, + { + "code": "JM-06", + "name": "Saint Ann" + }, + { + "code": "JM-07", + "name": "Trelawny" + }, + { + "code": "JM-08", + "name": "Saint James" + }, + { + "code": "JM-09", + "name": "Hanover" + }, + { + "code": "JM-10", + "name": "Westmoreland" + }, + { + "code": "JM-11", + "name": "Saint Elizabeth" + }, + { + "code": "JM-12", + "name": "Manchester" + }, + { + "code": "JM-13", + "name": "Clarendon" + }, + { + "code": "JM-14", + "name": "Saint Catherine" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JP", + "name": "Japan", + "states": [{ + "code": "JP01", + "name": "Hokkaido" + }, + { + "code": "JP02", + "name": "Aomori" + }, + { + "code": "JP03", + "name": "Iwate" + }, + { + "code": "JP04", + "name": "Miyagi" + }, + { + "code": "JP05", + "name": "Akita" + }, + { + "code": "JP06", + "name": "Yamagata" + }, + { + "code": "JP07", + "name": "Fukushima" + }, + { + "code": "JP08", + "name": "Ibaraki" + }, + { + "code": "JP09", + "name": "Tochigi" + }, + { + "code": "JP10", + "name": "Gunma" + }, + { + "code": "JP11", + "name": "Saitama" + }, + { + "code": "JP12", + "name": "Chiba" + }, + { + "code": "JP13", + "name": "Tokyo" + }, + { + "code": "JP14", + "name": "Kanagawa" + }, + { + "code": "JP15", + "name": "Niigata" + }, + { + "code": "JP16", + "name": "Toyama" + }, + { + "code": "JP17", + "name": "Ishikawa" + }, + { + "code": "JP18", + "name": "Fukui" + }, + { + "code": "JP19", + "name": "Yamanashi" + }, + { + "code": "JP20", + "name": "Nagano" + }, + { + "code": "JP21", + "name": "Gifu" + }, + { + "code": "JP22", + "name": "Shizuoka" + }, + { + "code": "JP23", + "name": "Aichi" + }, + { + "code": "JP24", + "name": "Mie" + }, + { + "code": "JP25", + "name": "Shiga" + }, + { + "code": "JP26", + "name": "Kyoto" + }, + { + "code": "JP27", + "name": "Osaka" + }, + { + "code": "JP28", + "name": "Hyogo" + }, + { + "code": "JP29", + "name": "Nara" + }, + { + "code": "JP30", + "name": "Wakayama" + }, + { + "code": "JP31", + "name": "Tottori" + }, + { + "code": "JP32", + "name": "Shimane" + }, + { + "code": "JP33", + "name": "Okayama" + }, + { + "code": "JP34", + "name": "Hiroshima" + }, + { + "code": "JP35", + "name": "Yamaguchi" + }, + { + "code": "JP36", + "name": "Tokushima" + }, + { + "code": "JP37", + "name": "Kagawa" + }, + { + "code": "JP38", + "name": "Ehime" + }, + { + "code": "JP39", + "name": "Kochi" + }, + { + "code": "JP40", + "name": "Fukuoka" + }, + { + "code": "JP41", + "name": "Saga" + }, + { + "code": "JP42", + "name": "Nagasaki" + }, + { + "code": "JP43", + "name": "Kumamoto" + }, + { + "code": "JP44", + "name": "Oita" + }, + { + "code": "JP45", + "name": "Miyazaki" + }, + { + "code": "JP46", + "name": "Kagoshima" + }, + { + "code": "JP47", + "name": "Okinawa" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JE", + "name": "Jersey", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JO", + "name": "Jordan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KZ", + "name": "Kazakhstan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KE", + "name": "Kenya", + "states": [{ + "code": "KE01", + "name": "Baringo" + }, + { + "code": "KE02", + "name": "Bomet" + }, + { + "code": "KE03", + "name": "Bungoma" + }, + { + "code": "KE04", + "name": "Busia" + }, + { + "code": "KE05", + "name": "Elgeyo-Marakwet" + }, + { + "code": "KE06", + "name": "Embu" + }, + { + "code": "KE07", + "name": "Garissa" + }, + { + "code": "KE08", + "name": "Homa Bay" + }, + { + "code": "KE09", + "name": "Isiolo" + }, + { + "code": "KE10", + "name": "Kajiado" + }, + { + "code": "KE11", + "name": "Kakamega" + }, + { + "code": "KE12", + "name": "Kericho" + }, + { + "code": "KE13", + "name": "Kiambu" + }, + { + "code": "KE14", + "name": "Kilifi" + }, + { + "code": "KE15", + "name": "Kirinyaga" + }, + { + "code": "KE16", + "name": "Kisii" + }, + { + "code": "KE17", + "name": "Kisumu" + }, + { + "code": "KE18", + "name": "Kitui" + }, + { + "code": "KE19", + "name": "Kwale" + }, + { + "code": "KE20", + "name": "Laikipia" + }, + { + "code": "KE21", + "name": "Lamu" + }, + { + "code": "KE22", + "name": "Machakos" + }, + { + "code": "KE23", + "name": "Makueni" + }, + { + "code": "KE24", + "name": "Mandera" + }, + { + "code": "KE25", + "name": "Marsabit" + }, + { + "code": "KE26", + "name": "Meru" + }, + { + "code": "KE27", + "name": "Migori" + }, + { + "code": "KE28", + "name": "Mombasa" + }, + { + "code": "KE29", + "name": "Murang’a" + }, + { + "code": "KE30", + "name": "Nairobi County" + }, + { + "code": "KE31", + "name": "Nakuru" + }, + { + "code": "KE32", + "name": "Nandi" + }, + { + "code": "KE33", + "name": "Narok" + }, + { + "code": "KE34", + "name": "Nyamira" + }, + { + "code": "KE35", + "name": "Nyandarua" + }, + { + "code": "KE36", + "name": "Nyeri" + }, + { + "code": "KE37", + "name": "Samburu" + }, + { + "code": "KE38", + "name": "Siaya" + }, + { + "code": "KE39", + "name": "Taita-Taveta" + }, + { + "code": "KE40", + "name": "Tana River" + }, + { + "code": "KE41", + "name": "Tharaka-Nithi" + }, + { + "code": "KE42", + "name": "Trans Nzoia" + }, + { + "code": "KE43", + "name": "Turkana" + }, + { + "code": "KE44", + "name": "Uasin Gishu" + }, + { + "code": "KE45", + "name": "Vihiga" + }, + { + "code": "KE46", + "name": "Wajir" + }, + { + "code": "KE47", + "name": "West Pokot" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KI", + "name": "Kiribati", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KW", + "name": "Kuwait", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KG", + "name": "Kyrgyzstan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LA", + "name": "Laos", + "states": [{ + "code": "AT", + "name": "Attapeu" + }, + { + "code": "BK", + "name": "Bokeo" + }, + { + "code": "BL", + "name": "Bolikhamsai" + }, + { + "code": "CH", + "name": "Champasak" + }, + { + "code": "HO", + "name": "Houaphanh" + }, + { + "code": "KH", + "name": "Khammouane" + }, + { + "code": "LM", + "name": "Luang Namtha" + }, + { + "code": "LP", + "name": "Luang Prabang" + }, + { + "code": "OU", + "name": "Oudomxay" + }, + { + "code": "PH", + "name": "Phongsaly" + }, + { + "code": "SL", + "name": "Salavan" + }, + { + "code": "SV", + "name": "Savannakhet" + }, + { + "code": "VI", + "name": "Vientiane Province" + }, + { + "code": "VT", + "name": "Vientiane" + }, + { + "code": "XA", + "name": "Sainyabuli" + }, + { + "code": "XE", + "name": "Sekong" + }, + { + "code": "XI", + "name": "Xiangkhouang" + }, + { + "code": "XS", + "name": "Xaisomboun" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LV", + "name": "Latvia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LB", + "name": "Lebanon", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LS", + "name": "Lesotho", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LR", + "name": "Liberia", + "states": [{ + "code": "BM", + "name": "Bomi" + }, + { + "code": "BN", + "name": "Bong" + }, + { + "code": "GA", + "name": "Gbarpolu" + }, + { + "code": "GB", + "name": "Grand Bassa" + }, + { + "code": "GC", + "name": "Grand Cape Mount" + }, + { + "code": "GG", + "name": "Grand Gedeh" + }, + { + "code": "GK", + "name": "Grand Kru" + }, + { + "code": "LO", + "name": "Lofa" + }, + { + "code": "MA", + "name": "Margibi" + }, + { + "code": "MY", + "name": "Maryland" + }, + { + "code": "MO", + "name": "Montserrado" + }, + { + "code": "NM", + "name": "Nimba" + }, + { + "code": "RV", + "name": "Rivercess" + }, + { + "code": "RG", + "name": "River Gee" + }, + { + "code": "SN", + "name": "Sinoe" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LY", + "name": "Libya", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LI", + "name": "Liechtenstein", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LT", + "name": "Lithuania", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LU", + "name": "Luxembourg", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MO", + "name": "Macao", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MG", + "name": "Madagascar", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MW", + "name": "Malawi", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MY", + "name": "Malaysia", + "states": [{ + "code": "JHR", + "name": "Johor" + }, + { + "code": "KDH", + "name": "Kedah" + }, + { + "code": "KTN", + "name": "Kelantan" + }, + { + "code": "LBN", + "name": "Labuan" + }, + { + "code": "MLK", + "name": "Malacca (Melaka)" + }, + { + "code": "NSN", + "name": "Negeri Sembilan" + }, + { + "code": "PHG", + "name": "Pahang" + }, + { + "code": "PNG", + "name": "Penang (Pulau Pinang)" + }, + { + "code": "PRK", + "name": "Perak" + }, + { + "code": "PLS", + "name": "Perlis" + }, + { + "code": "SBH", + "name": "Sabah" + }, + { + "code": "SWK", + "name": "Sarawak" + }, + { + "code": "SGR", + "name": "Selangor" + }, + { + "code": "TRG", + "name": "Terengganu" + }, + { + "code": "PJY", + "name": "Putrajaya" + }, + { + "code": "KUL", + "name": "Kuala Lumpur" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MV", + "name": "Maldives", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ML", + "name": "Mali", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MT", + "name": "Malta", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MH", + "name": "Marshall Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MQ", + "name": "Martinique", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MR", + "name": "Mauritania", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MU", + "name": "Mauritius", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "YT", + "name": "Mayotte", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MX", + "name": "Mexico", + "states": [{ + "code": "DF", + "name": "Ciudad de México" + }, + { + "code": "JA", + "name": "Jalisco" + }, + { + "code": "NL", + "name": "Nuevo León" + }, + { + "code": "AG", + "name": "Aguascalientes" + }, + { + "code": "BC", + "name": "Baja California" + }, + { + "code": "BS", + "name": "Baja California Sur" + }, + { + "code": "CM", + "name": "Campeche" + }, + { + "code": "CS", + "name": "Chiapas" + }, + { + "code": "CH", + "name": "Chihuahua" + }, + { + "code": "CO", + "name": "Coahuila" + }, + { + "code": "CL", + "name": "Colima" + }, + { + "code": "DG", + "name": "Durango" + }, + { + "code": "GT", + "name": "Guanajuato" + }, + { + "code": "GR", + "name": "Guerrero" + }, + { + "code": "HG", + "name": "Hidalgo" + }, + { + "code": "MX", + "name": "Estado de México" + }, + { + "code": "MI", + "name": "Michoacán" + }, + { + "code": "MO", + "name": "Morelos" + }, + { + "code": "NA", + "name": "Nayarit" + }, + { + "code": "OA", + "name": "Oaxaca" + }, + { + "code": "PU", + "name": "Puebla" + }, + { + "code": "QT", + "name": "Querétaro" + }, + { + "code": "QR", + "name": "Quintana Roo" + }, + { + "code": "SL", + "name": "San Luis Potosí" + }, + { + "code": "SI", + "name": "Sinaloa" + }, + { + "code": "SO", + "name": "Sonora" + }, + { + "code": "TB", + "name": "Tabasco" + }, + { + "code": "TM", + "name": "Tamaulipas" + }, + { + "code": "TL", + "name": "Tlaxcala" + }, + { + "code": "VE", + "name": "Veracruz" + }, + { + "code": "YU", + "name": "Yucatán" + }, + { + "code": "ZA", + "name": "Zacatecas" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FM", + "name": "Micronesia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MD", + "name": "Moldova", + "states": [{ + "code": "C", + "name": "Chișinău" + }, + { + "code": "BL", + "name": "Bălți" + }, + { + "code": "AN", + "name": "Anenii Noi" + }, + { + "code": "BS", + "name": "Basarabeasca" + }, + { + "code": "BR", + "name": "Briceni" + }, + { + "code": "CH", + "name": "Cahul" + }, + { + "code": "CT", + "name": "Cantemir" + }, + { + "code": "CL", + "name": "Călărași" + }, + { + "code": "CS", + "name": "Căușeni" + }, + { + "code": "CM", + "name": "Cimișlia" + }, + { + "code": "CR", + "name": "Criuleni" + }, + { + "code": "DN", + "name": "Dondușeni" + }, + { + "code": "DR", + "name": "Drochia" + }, + { + "code": "DB", + "name": "Dubăsari" + }, + { + "code": "ED", + "name": "Edineț" + }, + { + "code": "FL", + "name": "Fălești" + }, + { + "code": "FR", + "name": "Florești" + }, + { + "code": "GE", + "name": "UTA Găgăuzia" + }, + { + "code": "GL", + "name": "Glodeni" + }, + { + "code": "HN", + "name": "Hîncești" + }, + { + "code": "IL", + "name": "Ialoveni" + }, + { + "code": "LV", + "name": "Leova" + }, + { + "code": "NS", + "name": "Nisporeni" + }, + { + "code": "OC", + "name": "Ocnița" + }, + { + "code": "OR", + "name": "Orhei" + }, + { + "code": "RZ", + "name": "Rezina" + }, + { + "code": "RS", + "name": "Rîșcani" + }, + { + "code": "SG", + "name": "Sîngerei" + }, + { + "code": "SR", + "name": "Soroca" + }, + { + "code": "ST", + "name": "Strășeni" + }, + { + "code": "SD", + "name": "Șoldănești" + }, + { + "code": "SV", + "name": "Ștefan Vodă" + }, + { + "code": "TR", + "name": "Taraclia" + }, + { + "code": "TL", + "name": "Telenești" + }, + { + "code": "UN", + "name": "Ungheni" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MC", + "name": "Monaco", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MN", + "name": "Mongolia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ME", + "name": "Montenegro", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MS", + "name": "Montserrat", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MA", + "name": "Morocco", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MZ", + "name": "Mozambique", + "states": [{ + "code": "MZP", + "name": "Cabo Delgado" + }, + { + "code": "MZG", + "name": "Gaza" + }, + { + "code": "MZI", + "name": "Inhambane" + }, + { + "code": "MZB", + "name": "Manica" + }, + { + "code": "MZL", + "name": "Maputo Province" + }, + { + "code": "MZMPM", + "name": "Maputo" + }, + { + "code": "MZN", + "name": "Nampula" + }, + { + "code": "MZA", + "name": "Niassa" + }, + { + "code": "MZS", + "name": "Sofala" + }, + { + "code": "MZT", + "name": "Tete" + }, + { + "code": "MZQ", + "name": "Zambézia" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MM", + "name": "Myanmar", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NA", + "name": "Namibia", + "states": [{ + "code": "ER", + "name": "Erongo" + }, + { + "code": "HA", + "name": "Hardap" + }, + { + "code": "KA", + "name": "Karas" + }, + { + "code": "KE", + "name": "Kavango East" + }, + { + "code": "KW", + "name": "Kavango West" + }, + { + "code": "KH", + "name": "Khomas" + }, + { + "code": "KU", + "name": "Kunene" + }, + { + "code": "OW", + "name": "Ohangwena" + }, + { + "code": "OH", + "name": "Omaheke" + }, + { + "code": "OS", + "name": "Omusati" + }, + { + "code": "ON", + "name": "Oshana" + }, + { + "code": "OT", + "name": "Oshikoto" + }, + { + "code": "OD", + "name": "Otjozondjupa" + }, + { + "code": "CA", + "name": "Zambezi" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NR", + "name": "Nauru", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NP", + "name": "Nepal", + "states": [{ + "code": "BAG", + "name": "Bagmati" + }, + { + "code": "BHE", + "name": "Bheri" + }, + { + "code": "DHA", + "name": "Dhaulagiri" + }, + { + "code": "GAN", + "name": "Gandaki" + }, + { + "code": "JAN", + "name": "Janakpur" + }, + { + "code": "KAR", + "name": "Karnali" + }, + { + "code": "KOS", + "name": "Koshi" + }, + { + "code": "LUM", + "name": "Lumbini" + }, + { + "code": "MAH", + "name": "Mahakali" + }, + { + "code": "MEC", + "name": "Mechi" + }, + { + "code": "NAR", + "name": "Narayani" + }, + { + "code": "RAP", + "name": "Rapti" + }, + { + "code": "SAG", + "name": "Sagarmatha" + }, + { + "code": "SET", + "name": "Seti" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NL", + "name": "Netherlands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NC", + "name": "New Caledonia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NZ", + "name": "New Zealand", + "states": [{ + "code": "NTL", + "name": "Northland" + }, + { + "code": "AUK", + "name": "Auckland" + }, + { + "code": "WKO", + "name": "Waikato" + }, + { + "code": "BOP", + "name": "Bay of Plenty" + }, + { + "code": "TKI", + "name": "Taranaki" + }, + { + "code": "GIS", + "name": "Gisborne" + }, + { + "code": "HKB", + "name": "Hawke’s Bay" + }, + { + "code": "MWT", + "name": "Manawatu-Wanganui" + }, + { + "code": "WGN", + "name": "Wellington" + }, + { + "code": "NSN", + "name": "Nelson" + }, + { + "code": "MBH", + "name": "Marlborough" + }, + { + "code": "TAS", + "name": "Tasman" + }, + { + "code": "WTC", + "name": "West Coast" + }, + { + "code": "CAN", + "name": "Canterbury" + }, + { + "code": "OTA", + "name": "Otago" + }, + { + "code": "STL", + "name": "Southland" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NI", + "name": "Nicaragua", + "states": [{ + "code": "NI-AN", + "name": "Atlántico Norte" + }, + { + "code": "NI-AS", + "name": "Atlántico Sur" + }, + { + "code": "NI-BO", + "name": "Boaco" + }, + { + "code": "NI-CA", + "name": "Carazo" + }, + { + "code": "NI-CI", + "name": "Chinandega" + }, + { + "code": "NI-CO", + "name": "Chontales" + }, + { + "code": "NI-ES", + "name": "Estelí" + }, + { + "code": "NI-GR", + "name": "Granada" + }, + { + "code": "NI-JI", + "name": "Jinotega" + }, + { + "code": "NI-LE", + "name": "León" + }, + { + "code": "NI-MD", + "name": "Madriz" + }, + { + "code": "NI-MN", + "name": "Managua" + }, + { + "code": "NI-MS", + "name": "Masaya" + }, + { + "code": "NI-MT", + "name": "Matagalpa" + }, + { + "code": "NI-NS", + "name": "Nueva Segovia" + }, + { + "code": "NI-RI", + "name": "Rivas" + }, + { + "code": "NI-SJ", + "name": "Río San Juan" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NE", + "name": "Niger", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NG", + "name": "Nigeria", + "states": [{ + "code": "AB", + "name": "Abia" + }, + { + "code": "FC", + "name": "Abuja" + }, + { + "code": "AD", + "name": "Adamawa" + }, + { + "code": "AK", + "name": "Akwa Ibom" + }, + { + "code": "AN", + "name": "Anambra" + }, + { + "code": "BA", + "name": "Bauchi" + }, + { + "code": "BY", + "name": "Bayelsa" + }, + { + "code": "BE", + "name": "Benue" + }, + { + "code": "BO", + "name": "Borno" + }, + { + "code": "CR", + "name": "Cross River" + }, + { + "code": "DE", + "name": "Delta" + }, + { + "code": "EB", + "name": "Ebonyi" + }, + { + "code": "ED", + "name": "Edo" + }, + { + "code": "EK", + "name": "Ekiti" + }, + { + "code": "EN", + "name": "Enugu" + }, + { + "code": "GO", + "name": "Gombe" + }, + { + "code": "IM", + "name": "Imo" + }, + { + "code": "JI", + "name": "Jigawa" + }, + { + "code": "KD", + "name": "Kaduna" + }, + { + "code": "KN", + "name": "Kano" + }, + { + "code": "KT", + "name": "Katsina" + }, + { + "code": "KE", + "name": "Kebbi" + }, + { + "code": "KO", + "name": "Kogi" + }, + { + "code": "KW", + "name": "Kwara" + }, + { + "code": "LA", + "name": "Lagos" + }, + { + "code": "NA", + "name": "Nasarawa" + }, + { + "code": "NI", + "name": "Niger" + }, + { + "code": "OG", + "name": "Ogun" + }, + { + "code": "ON", + "name": "Ondo" + }, + { + "code": "OS", + "name": "Osun" + }, + { + "code": "OY", + "name": "Oyo" + }, + { + "code": "PL", + "name": "Plateau" + }, + { + "code": "RI", + "name": "Rivers" + }, + { + "code": "SO", + "name": "Sokoto" + }, + { + "code": "TA", + "name": "Taraba" + }, + { + "code": "YO", + "name": "Yobe" + }, + { + "code": "ZA", + "name": "Zamfara" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NU", + "name": "Niue", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NF", + "name": "Norfolk Island", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KP", + "name": "North Korea", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MK", + "name": "North Macedonia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MP", + "name": "Northern Mariana Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NO", + "name": "Norway", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "OM", + "name": "Oman", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PK", + "name": "Pakistan", + "states": [{ + "code": "JK", + "name": "Azad Kashmir" + }, + { + "code": "BA", + "name": "Balochistan" + }, + { + "code": "TA", + "name": "FATA" + }, + { + "code": "GB", + "name": "Gilgit Baltistan" + }, + { + "code": "IS", + "name": "Islamabad Capital Territory" + }, + { + "code": "KP", + "name": "Khyber Pakhtunkhwa" + }, + { + "code": "PB", + "name": "Punjab" + }, + { + "code": "SD", + "name": "Sindh" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PS", + "name": "Palestinian Territory", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PA", + "name": "Panama", + "states": [{ + "code": "PA-1", + "name": "Bocas del Toro" + }, + { + "code": "PA-2", + "name": "Coclé" + }, + { + "code": "PA-3", + "name": "Colón" + }, + { + "code": "PA-4", + "name": "Chiriquí" + }, + { + "code": "PA-5", + "name": "Darién" + }, + { + "code": "PA-6", + "name": "Herrera" + }, + { + "code": "PA-7", + "name": "Los Santos" + }, + { + "code": "PA-8", + "name": "Panamá" + }, + { + "code": "PA-9", + "name": "Veraguas" + }, + { + "code": "PA-10", + "name": "West Panamá" + }, + { + "code": "PA-EM", + "name": "Emberá" + }, + { + "code": "PA-KY", + "name": "Guna Yala" + }, + { + "code": "PA-NB", + "name": "Ngöbe-Buglé" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PG", + "name": "Papua New Guinea", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PY", + "name": "Paraguay", + "states": [{ + "code": "PY-ASU", + "name": "Asunción" + }, + { + "code": "PY-1", + "name": "Concepción" + }, + { + "code": "PY-2", + "name": "San Pedro" + }, + { + "code": "PY-3", + "name": "Cordillera" + }, + { + "code": "PY-4", + "name": "Guairá" + }, + { + "code": "PY-5", + "name": "Caaguazú" + }, + { + "code": "PY-6", + "name": "Caazapá" + }, + { + "code": "PY-7", + "name": "Itapúa" + }, + { + "code": "PY-8", + "name": "Misiones" + }, + { + "code": "PY-9", + "name": "Paraguarí" + }, + { + "code": "PY-10", + "name": "Alto Paraná" + }, + { + "code": "PY-11", + "name": "Central" + }, + { + "code": "PY-12", + "name": "Ñeembucú" + }, + { + "code": "PY-13", + "name": "Amambay" + }, + { + "code": "PY-14", + "name": "Canindeyú" + }, + { + "code": "PY-15", + "name": "Presidente Hayes" + }, + { + "code": "PY-16", + "name": "Alto Paraguay" + }, + { + "code": "PY-17", + "name": "Boquerón" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PE", + "name": "Peru", + "states": [{ + "code": "CAL", + "name": "El Callao" + }, + { + "code": "LMA", + "name": "Municipalidad Metropolitana de Lima" + }, + { + "code": "AMA", + "name": "Amazonas" + }, + { + "code": "ANC", + "name": "Ancash" + }, + { + "code": "APU", + "name": "Apurímac" + }, + { + "code": "ARE", + "name": "Arequipa" + }, + { + "code": "AYA", + "name": "Ayacucho" + }, + { + "code": "CAJ", + "name": "Cajamarca" + }, + { + "code": "CUS", + "name": "Cusco" + }, + { + "code": "HUV", + "name": "Huancavelica" + }, + { + "code": "HUC", + "name": "Huánuco" + }, + { + "code": "ICA", + "name": "Ica" + }, + { + "code": "JUN", + "name": "Junín" + }, + { + "code": "LAL", + "name": "La Libertad" + }, + { + "code": "LAM", + "name": "Lambayeque" + }, + { + "code": "LIM", + "name": "Lima" + }, + { + "code": "LOR", + "name": "Loreto" + }, + { + "code": "MDD", + "name": "Madre de Dios" + }, + { + "code": "MOQ", + "name": "Moquegua" + }, + { + "code": "PAS", + "name": "Pasco" + }, + { + "code": "PIU", + "name": "Piura" + }, + { + "code": "PUN", + "name": "Puno" + }, + { + "code": "SAM", + "name": "San Martín" + }, + { + "code": "TAC", + "name": "Tacna" + }, + { + "code": "TUM", + "name": "Tumbes" + }, + { + "code": "UCA", + "name": "Ucayali" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PH", + "name": "Philippines", + "states": [{ + "code": "ABR", + "name": "Abra" + }, + { + "code": "AGN", + "name": "Agusan del Norte" + }, + { + "code": "AGS", + "name": "Agusan del Sur" + }, + { + "code": "AKL", + "name": "Aklan" + }, + { + "code": "ALB", + "name": "Albay" + }, + { + "code": "ANT", + "name": "Antique" + }, + { + "code": "APA", + "name": "Apayao" + }, + { + "code": "AUR", + "name": "Aurora" + }, + { + "code": "BAS", + "name": "Basilan" + }, + { + "code": "BAN", + "name": "Bataan" + }, + { + "code": "BTN", + "name": "Batanes" + }, + { + "code": "BTG", + "name": "Batangas" + }, + { + "code": "BEN", + "name": "Benguet" + }, + { + "code": "BIL", + "name": "Biliran" + }, + { + "code": "BOH", + "name": "Bohol" + }, + { + "code": "BUK", + "name": "Bukidnon" + }, + { + "code": "BUL", + "name": "Bulacan" + }, + { + "code": "CAG", + "name": "Cagayan" + }, + { + "code": "CAN", + "name": "Camarines Norte" + }, + { + "code": "CAS", + "name": "Camarines Sur" + }, + { + "code": "CAM", + "name": "Camiguin" + }, + { + "code": "CAP", + "name": "Capiz" + }, + { + "code": "CAT", + "name": "Catanduanes" + }, + { + "code": "CAV", + "name": "Cavite" + }, + { + "code": "CEB", + "name": "Cebu" + }, + { + "code": "COM", + "name": "Compostela Valley" + }, + { + "code": "NCO", + "name": "Cotabato" + }, + { + "code": "DAV", + "name": "Davao del Norte" + }, + { + "code": "DAS", + "name": "Davao del Sur" + }, + { + "code": "DAC", + "name": "Davao Occidental" + }, + { + "code": "DAO", + "name": "Davao Oriental" + }, + { + "code": "DIN", + "name": "Dinagat Islands" + }, + { + "code": "EAS", + "name": "Eastern Samar" + }, + { + "code": "GUI", + "name": "Guimaras" + }, + { + "code": "IFU", + "name": "Ifugao" + }, + { + "code": "ILN", + "name": "Ilocos Norte" + }, + { + "code": "ILS", + "name": "Ilocos Sur" + }, + { + "code": "ILI", + "name": "Iloilo" + }, + { + "code": "ISA", + "name": "Isabela" + }, + { + "code": "KAL", + "name": "Kalinga" + }, + { + "code": "LUN", + "name": "La Union" + }, + { + "code": "LAG", + "name": "Laguna" + }, + { + "code": "LAN", + "name": "Lanao del Norte" + }, + { + "code": "LAS", + "name": "Lanao del Sur" + }, + { + "code": "LEY", + "name": "Leyte" + }, + { + "code": "MAG", + "name": "Maguindanao" + }, + { + "code": "MAD", + "name": "Marinduque" + }, + { + "code": "MAS", + "name": "Masbate" + }, + { + "code": "MSC", + "name": "Misamis Occidental" + }, + { + "code": "MSR", + "name": "Misamis Oriental" + }, + { + "code": "MOU", + "name": "Mountain Province" + }, + { + "code": "NEC", + "name": "Negros Occidental" + }, + { + "code": "NER", + "name": "Negros Oriental" + }, + { + "code": "NSA", + "name": "Northern Samar" + }, + { + "code": "NUE", + "name": "Nueva Ecija" + }, + { + "code": "NUV", + "name": "Nueva Vizcaya" + }, + { + "code": "MDC", + "name": "Occidental Mindoro" + }, + { + "code": "MDR", + "name": "Oriental Mindoro" + }, + { + "code": "PLW", + "name": "Palawan" + }, + { + "code": "PAM", + "name": "Pampanga" + }, + { + "code": "PAN", + "name": "Pangasinan" + }, + { + "code": "QUE", + "name": "Quezon" + }, + { + "code": "QUI", + "name": "Quirino" + }, + { + "code": "RIZ", + "name": "Rizal" + }, + { + "code": "ROM", + "name": "Romblon" + }, + { + "code": "WSA", + "name": "Samar" + }, + { + "code": "SAR", + "name": "Sarangani" + }, + { + "code": "SIQ", + "name": "Siquijor" + }, + { + "code": "SOR", + "name": "Sorsogon" + }, + { + "code": "SCO", + "name": "South Cotabato" + }, + { + "code": "SLE", + "name": "Southern Leyte" + }, + { + "code": "SUK", + "name": "Sultan Kudarat" + }, + { + "code": "SLU", + "name": "Sulu" + }, + { + "code": "SUN", + "name": "Surigao del Norte" + }, + { + "code": "SUR", + "name": "Surigao del Sur" + }, + { + "code": "TAR", + "name": "Tarlac" + }, + { + "code": "TAW", + "name": "Tawi-Tawi" + }, + { + "code": "ZMB", + "name": "Zambales" + }, + { + "code": "ZAN", + "name": "Zamboanga del Norte" + }, + { + "code": "ZAS", + "name": "Zamboanga del Sur" + }, + { + "code": "ZSI", + "name": "Zamboanga Sibugay" + }, + { + "code": "00", + "name": "Metro Manila" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PN", + "name": "Pitcairn", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PL", + "name": "Poland", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PT", + "name": "Portugal", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PR", + "name": "Puerto Rico", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "QA", + "name": "Qatar", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RE", + "name": "Reunion", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RO", + "name": "Romania", + "states": [{ + "code": "AB", + "name": "Alba" + }, + { + "code": "AR", + "name": "Arad" + }, + { + "code": "AG", + "name": "Argeș" + }, + { + "code": "BC", + "name": "Bacău" + }, + { + "code": "BH", + "name": "Bihor" + }, + { + "code": "BN", + "name": "Bistrița-Năsăud" + }, + { + "code": "BT", + "name": "Botoșani" + }, + { + "code": "BR", + "name": "Brăila" + }, + { + "code": "BV", + "name": "Brașov" + }, + { + "code": "B", + "name": "București" + }, + { + "code": "BZ", + "name": "Buzău" + }, + { + "code": "CL", + "name": "Călărași" + }, + { + "code": "CS", + "name": "Caraș-Severin" + }, + { + "code": "CJ", + "name": "Cluj" + }, + { + "code": "CT", + "name": "Constanța" + }, + { + "code": "CV", + "name": "Covasna" + }, + { + "code": "DB", + "name": "Dâmbovița" + }, + { + "code": "DJ", + "name": "Dolj" + }, + { + "code": "GL", + "name": "Galați" + }, + { + "code": "GR", + "name": "Giurgiu" + }, + { + "code": "GJ", + "name": "Gorj" + }, + { + "code": "HR", + "name": "Harghita" + }, + { + "code": "HD", + "name": "Hunedoara" + }, + { + "code": "IL", + "name": "Ialomița" + }, + { + "code": "IS", + "name": "Iași" + }, + { + "code": "IF", + "name": "Ilfov" + }, + { + "code": "MM", + "name": "Maramureș" + }, + { + "code": "MH", + "name": "Mehedinți" + }, + { + "code": "MS", + "name": "Mureș" + }, + { + "code": "NT", + "name": "Neamț" + }, + { + "code": "OT", + "name": "Olt" + }, + { + "code": "PH", + "name": "Prahova" + }, + { + "code": "SJ", + "name": "Sălaj" + }, + { + "code": "SM", + "name": "Satu Mare" + }, + { + "code": "SB", + "name": "Sibiu" + }, + { + "code": "SV", + "name": "Suceava" + }, + { + "code": "TR", + "name": "Teleorman" + }, + { + "code": "TM", + "name": "Timiș" + }, + { + "code": "TL", + "name": "Tulcea" + }, + { + "code": "VL", + "name": "Vâlcea" + }, + { + "code": "VS", + "name": "Vaslui" + }, + { + "code": "VN", + "name": "Vrancea" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RU", + "name": "Russia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RW", + "name": "Rwanda", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ST", + "name": "São Tomé and Príncipe", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BL", + "name": "Saint Barthélemy", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SH", + "name": "Saint Helena", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KN", + "name": "Saint Kitts and Nevis", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LC", + "name": "Saint Lucia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SX", + "name": "Saint Martin (Dutch part)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MF", + "name": "Saint Martin (French part)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PM", + "name": "Saint Pierre and Miquelon", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VC", + "name": "Saint Vincent and the Grenadines", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "WS", + "name": "Samoa", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SM", + "name": "San Marino", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SA", + "name": "Saudi Arabia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SN", + "name": "Senegal", + "states": [{ + "code": "SNDB", + "name": "Diourbel" + }, + { + "code": "SNDK", + "name": "Dakar" + }, + { + "code": "SNFK", + "name": "Fatick" + }, + { + "code": "SNKA", + "name": "Kaffrine" + }, + { + "code": "SNKD", + "name": "Kolda" + }, + { + "code": "SNKE", + "name": "Kédougou" + }, + { + "code": "SNKL", + "name": "Kaolack" + }, + { + "code": "SNLG", + "name": "Louga" + }, + { + "code": "SNMT", + "name": "Matam" + }, + { + "code": "SNSE", + "name": "Sédhiou" + }, + { + "code": "SNSL", + "name": "Saint-Louis" + }, + { + "code": "SNTC", + "name": "Tambacounda" + }, + { + "code": "SNTH", + "name": "Thiès" + }, + { + "code": "SNZG", + "name": "Ziguinchor" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RS", + "name": "Serbia", + "states": [{ + "code": "RS00", + "name": "Belgrade" + }, + { + "code": "RS14", + "name": "Bor" + }, + { + "code": "RS11", + "name": "Braničevo" + }, + { + "code": "RS02", + "name": "Central Banat" + }, + { + "code": "RS10", + "name": "Danube" + }, + { + "code": "RS23", + "name": "Jablanica" + }, + { + "code": "RS09", + "name": "Kolubara" + }, + { + "code": "RS08", + "name": "Mačva" + }, + { + "code": "RS17", + "name": "Morava" + }, + { + "code": "RS20", + "name": "Nišava" + }, + { + "code": "RS01", + "name": "North Bačka" + }, + { + "code": "RS03", + "name": "North Banat" + }, + { + "code": "RS24", + "name": "Pčinja" + }, + { + "code": "RS22", + "name": "Pirot" + }, + { + "code": "RS13", + "name": "Pomoravlje" + }, + { + "code": "RS19", + "name": "Rasina" + }, + { + "code": "RS18", + "name": "Raška" + }, + { + "code": "RS06", + "name": "South Bačka" + }, + { + "code": "RS04", + "name": "South Banat" + }, + { + "code": "RS07", + "name": "Srem" + }, + { + "code": "RS12", + "name": "Šumadija" + }, + { + "code": "RS21", + "name": "Toplica" + }, + { + "code": "RS05", + "name": "West Bačka" + }, + { + "code": "RS15", + "name": "Zaječar" + }, + { + "code": "RS16", + "name": "Zlatibor" + }, + { + "code": "RS25", + "name": "Kosovo" + }, + { + "code": "RS26", + "name": "Peć" + }, + { + "code": "RS27", + "name": "Prizren" + }, + { + "code": "RS28", + "name": "Kosovska Mitrovica" + }, + { + "code": "RS29", + "name": "Kosovo-Pomoravlje" + }, + { + "code": "RSKM", + "name": "Kosovo-Metohija" + }, + { + "code": "RSVO", + "name": "Vojvodina" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SC", + "name": "Seychelles", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SL", + "name": "Sierra Leone", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SG", + "name": "Singapore", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SK", + "name": "Slovakia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SI", + "name": "Slovenia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SB", + "name": "Solomon Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SO", + "name": "Somalia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ZA", + "name": "South Africa", + "states": [{ + "code": "EC", + "name": "Eastern Cape" + }, + { + "code": "FS", + "name": "Free State" + }, + { + "code": "GP", + "name": "Gauteng" + }, + { + "code": "KZN", + "name": "KwaZulu-Natal" + }, + { + "code": "LP", + "name": "Limpopo" + }, + { + "code": "MP", + "name": "Mpumalanga" + }, + { + "code": "NC", + "name": "Northern Cape" + }, + { + "code": "NW", + "name": "North West" + }, + { + "code": "WC", + "name": "Western Cape" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GS", + "name": "South Georgia/Sandwich Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KR", + "name": "South Korea", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SS", + "name": "South Sudan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ES", + "name": "Spain", + "states": [{ + "code": "C", + "name": "A Coruña" + }, + { + "code": "VI", + "name": "Araba/Álava" + }, + { + "code": "AB", + "name": "Albacete" + }, + { + "code": "A", + "name": "Alicante" + }, + { + "code": "AL", + "name": "Almería" + }, + { + "code": "O", + "name": "Asturias" + }, + { + "code": "AV", + "name": "Ávila" + }, + { + "code": "BA", + "name": "Badajoz" + }, + { + "code": "PM", + "name": "Baleares" + }, + { + "code": "B", + "name": "Barcelona" + }, + { + "code": "BU", + "name": "Burgos" + }, + { + "code": "CC", + "name": "Cáceres" + }, + { + "code": "CA", + "name": "Cádiz" + }, + { + "code": "S", + "name": "Cantabria" + }, + { + "code": "CS", + "name": "Castellón" + }, + { + "code": "CE", + "name": "Ceuta" + }, + { + "code": "CR", + "name": "Ciudad Real" + }, + { + "code": "CO", + "name": "Córdoba" + }, + { + "code": "CU", + "name": "Cuenca" + }, + { + "code": "GI", + "name": "Girona" + }, + { + "code": "GR", + "name": "Granada" + }, + { + "code": "GU", + "name": "Guadalajara" + }, + { + "code": "SS", + "name": "Gipuzkoa" + }, + { + "code": "H", + "name": "Huelva" + }, + { + "code": "HU", + "name": "Huesca" + }, + { + "code": "J", + "name": "Jaén" + }, + { + "code": "LO", + "name": "La Rioja" + }, + { + "code": "GC", + "name": "Las Palmas" + }, + { + "code": "LE", + "name": "León" + }, + { + "code": "L", + "name": "Lleida" + }, + { + "code": "LU", + "name": "Lugo" + }, + { + "code": "M", + "name": "Madrid" + }, + { + "code": "MA", + "name": "Málaga" + }, + { + "code": "ML", + "name": "Melilla" + }, + { + "code": "MU", + "name": "Murcia" + }, + { + "code": "NA", + "name": "Navarra" + }, + { + "code": "OR", + "name": "Ourense" + }, + { + "code": "P", + "name": "Palencia" + }, + { + "code": "PO", + "name": "Pontevedra" + }, + { + "code": "SA", + "name": "Salamanca" + }, + { + "code": "TF", + "name": "Santa Cruz de Tenerife" + }, + { + "code": "SG", + "name": "Segovia" + }, + { + "code": "SE", + "name": "Sevilla" + }, + { + "code": "SO", + "name": "Soria" + }, + { + "code": "T", + "name": "Tarragona" + }, + { + "code": "TE", + "name": "Teruel" + }, + { + "code": "TO", + "name": "Toledo" + }, + { + "code": "V", + "name": "Valencia" + }, + { + "code": "VA", + "name": "Valladolid" + }, + { + "code": "BI", + "name": "Biscay" + }, + { + "code": "ZA", + "name": "Zamora" + }, + { + "code": "Z", + "name": "Zaragoza" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LK", + "name": "Sri Lanka", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SD", + "name": "Sudan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SR", + "name": "Suriname", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SJ", + "name": "Svalbard and Jan Mayen", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SE", + "name": "Sweden", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CH", + "name": "Switzerland", + "states": [{ + "code": "AG", + "name": "Aargau" + }, + { + "code": "AR", + "name": "Appenzell Ausserrhoden" + }, + { + "code": "AI", + "name": "Appenzell Innerrhoden" + }, + { + "code": "BL", + "name": "Basel-Landschaft" + }, + { + "code": "BS", + "name": "Basel-Stadt" + }, + { + "code": "BE", + "name": "Bern" + }, + { + "code": "FR", + "name": "Fribourg" + }, + { + "code": "GE", + "name": "Geneva" + }, + { + "code": "GL", + "name": "Glarus" + }, + { + "code": "GR", + "name": "Graubünden" + }, + { + "code": "JU", + "name": "Jura" + }, + { + "code": "LU", + "name": "Luzern" + }, + { + "code": "NE", + "name": "Neuchâtel" + }, + { + "code": "NW", + "name": "Nidwalden" + }, + { + "code": "OW", + "name": "Obwalden" + }, + { + "code": "SH", + "name": "Schaffhausen" + }, + { + "code": "SZ", + "name": "Schwyz" + }, + { + "code": "SO", + "name": "Solothurn" + }, + { + "code": "SG", + "name": "St. Gallen" + }, + { + "code": "TG", + "name": "Thurgau" + }, + { + "code": "TI", + "name": "Ticino" + }, + { + "code": "UR", + "name": "Uri" + }, + { + "code": "VS", + "name": "Valais" + }, + { + "code": "VD", + "name": "Vaud" + }, + { + "code": "ZG", + "name": "Zug" + }, + { + "code": "ZH", + "name": "Zürich" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SY", + "name": "Syria", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TW", + "name": "Taiwan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TJ", + "name": "Tajikistan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TZ", + "name": "Tanzania", + "states": [{ + "code": "TZ01", + "name": "Arusha" + }, + { + "code": "TZ02", + "name": "Dar es Salaam" + }, + { + "code": "TZ03", + "name": "Dodoma" + }, + { + "code": "TZ04", + "name": "Iringa" + }, + { + "code": "TZ05", + "name": "Kagera" + }, + { + "code": "TZ06", + "name": "Pemba North" + }, + { + "code": "TZ07", + "name": "Zanzibar North" + }, + { + "code": "TZ08", + "name": "Kigoma" + }, + { + "code": "TZ09", + "name": "Kilimanjaro" + }, + { + "code": "TZ10", + "name": "Pemba South" + }, + { + "code": "TZ11", + "name": "Zanzibar South" + }, + { + "code": "TZ12", + "name": "Lindi" + }, + { + "code": "TZ13", + "name": "Mara" + }, + { + "code": "TZ14", + "name": "Mbeya" + }, + { + "code": "TZ15", + "name": "Zanzibar West" + }, + { + "code": "TZ16", + "name": "Morogoro" + }, + { + "code": "TZ17", + "name": "Mtwara" + }, + { + "code": "TZ18", + "name": "Mwanza" + }, + { + "code": "TZ19", + "name": "Coast" + }, + { + "code": "TZ20", + "name": "Rukwa" + }, + { + "code": "TZ21", + "name": "Ruvuma" + }, + { + "code": "TZ22", + "name": "Shinyanga" + }, + { + "code": "TZ23", + "name": "Singida" + }, + { + "code": "TZ24", + "name": "Tabora" + }, + { + "code": "TZ25", + "name": "Tanga" + }, + { + "code": "TZ26", + "name": "Manyara" + }, + { + "code": "TZ27", + "name": "Geita" + }, + { + "code": "TZ28", + "name": "Katavi" + }, + { + "code": "TZ29", + "name": "Njombe" + }, + { + "code": "TZ30", + "name": "Simiyu" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TH", + "name": "Thailand", + "states": [{ + "code": "TH-37", + "name": "Amnat Charoen" + }, + { + "code": "TH-15", + "name": "Ang Thong" + }, + { + "code": "TH-14", + "name": "Ayutthaya" + }, + { + "code": "TH-10", + "name": "Bangkok" + }, + { + "code": "TH-38", + "name": "Bueng Kan" + }, + { + "code": "TH-31", + "name": "Buri Ram" + }, + { + "code": "TH-24", + "name": "Chachoengsao" + }, + { + "code": "TH-18", + "name": "Chai Nat" + }, + { + "code": "TH-36", + "name": "Chaiyaphum" + }, + { + "code": "TH-22", + "name": "Chanthaburi" + }, + { + "code": "TH-50", + "name": "Chiang Mai" + }, + { + "code": "TH-57", + "name": "Chiang Rai" + }, + { + "code": "TH-20", + "name": "Chonburi" + }, + { + "code": "TH-86", + "name": "Chumphon" + }, + { + "code": "TH-46", + "name": "Kalasin" + }, + { + "code": "TH-62", + "name": "Kamphaeng Phet" + }, + { + "code": "TH-71", + "name": "Kanchanaburi" + }, + { + "code": "TH-40", + "name": "Khon Kaen" + }, + { + "code": "TH-81", + "name": "Krabi" + }, + { + "code": "TH-52", + "name": "Lampang" + }, + { + "code": "TH-51", + "name": "Lamphun" + }, + { + "code": "TH-42", + "name": "Loei" + }, + { + "code": "TH-16", + "name": "Lopburi" + }, + { + "code": "TH-58", + "name": "Mae Hong Son" + }, + { + "code": "TH-44", + "name": "Maha Sarakham" + }, + { + "code": "TH-49", + "name": "Mukdahan" + }, + { + "code": "TH-26", + "name": "Nakhon Nayok" + }, + { + "code": "TH-73", + "name": "Nakhon Pathom" + }, + { + "code": "TH-48", + "name": "Nakhon Phanom" + }, + { + "code": "TH-30", + "name": "Nakhon Ratchasima" + }, + { + "code": "TH-60", + "name": "Nakhon Sawan" + }, + { + "code": "TH-80", + "name": "Nakhon Si Thammarat" + }, + { + "code": "TH-55", + "name": "Nan" + }, + { + "code": "TH-96", + "name": "Narathiwat" + }, + { + "code": "TH-39", + "name": "Nong Bua Lam Phu" + }, + { + "code": "TH-43", + "name": "Nong Khai" + }, + { + "code": "TH-12", + "name": "Nonthaburi" + }, + { + "code": "TH-13", + "name": "Pathum Thani" + }, + { + "code": "TH-94", + "name": "Pattani" + }, + { + "code": "TH-82", + "name": "Phang Nga" + }, + { + "code": "TH-93", + "name": "Phatthalung" + }, + { + "code": "TH-56", + "name": "Phayao" + }, + { + "code": "TH-67", + "name": "Phetchabun" + }, + { + "code": "TH-76", + "name": "Phetchaburi" + }, + { + "code": "TH-66", + "name": "Phichit" + }, + { + "code": "TH-65", + "name": "Phitsanulok" + }, + { + "code": "TH-54", + "name": "Phrae" + }, + { + "code": "TH-83", + "name": "Phuket" + }, + { + "code": "TH-25", + "name": "Prachin Buri" + }, + { + "code": "TH-77", + "name": "Prachuap Khiri Khan" + }, + { + "code": "TH-85", + "name": "Ranong" + }, + { + "code": "TH-70", + "name": "Ratchaburi" + }, + { + "code": "TH-21", + "name": "Rayong" + }, + { + "code": "TH-45", + "name": "Roi Et" + }, + { + "code": "TH-27", + "name": "Sa Kaeo" + }, + { + "code": "TH-47", + "name": "Sakon Nakhon" + }, + { + "code": "TH-11", + "name": "Samut Prakan" + }, + { + "code": "TH-74", + "name": "Samut Sakhon" + }, + { + "code": "TH-75", + "name": "Samut Songkhram" + }, + { + "code": "TH-19", + "name": "Saraburi" + }, + { + "code": "TH-91", + "name": "Satun" + }, + { + "code": "TH-17", + "name": "Sing Buri" + }, + { + "code": "TH-33", + "name": "Sisaket" + }, + { + "code": "TH-90", + "name": "Songkhla" + }, + { + "code": "TH-64", + "name": "Sukhothai" + }, + { + "code": "TH-72", + "name": "Suphan Buri" + }, + { + "code": "TH-84", + "name": "Surat Thani" + }, + { + "code": "TH-32", + "name": "Surin" + }, + { + "code": "TH-63", + "name": "Tak" + }, + { + "code": "TH-92", + "name": "Trang" + }, + { + "code": "TH-23", + "name": "Trat" + }, + { + "code": "TH-34", + "name": "Ubon Ratchathani" + }, + { + "code": "TH-41", + "name": "Udon Thani" + }, + { + "code": "TH-61", + "name": "Uthai Thani" + }, + { + "code": "TH-53", + "name": "Uttaradit" + }, + { + "code": "TH-95", + "name": "Yala" + }, + { + "code": "TH-35", + "name": "Yasothon" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TL", + "name": "Timor-Leste", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TG", + "name": "Togo", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TK", + "name": "Tokelau", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TO", + "name": "Tonga", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TT", + "name": "Trinidad and Tobago", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TN", + "name": "Tunisia", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TR", + "name": "Turkey", + "states": [{ + "code": "TR01", + "name": "Adana" + }, + { + "code": "TR02", + "name": "Adıyaman" + }, + { + "code": "TR03", + "name": "Afyon" + }, + { + "code": "TR04", + "name": "Ağrı" + }, + { + "code": "TR05", + "name": "Amasya" + }, + { + "code": "TR06", + "name": "Ankara" + }, + { + "code": "TR07", + "name": "Antalya" + }, + { + "code": "TR08", + "name": "Artvin" + }, + { + "code": "TR09", + "name": "Aydın" + }, + { + "code": "TR10", + "name": "Balıkesir" + }, + { + "code": "TR11", + "name": "Bilecik" + }, + { + "code": "TR12", + "name": "Bingöl" + }, + { + "code": "TR13", + "name": "Bitlis" + }, + { + "code": "TR14", + "name": "Bolu" + }, + { + "code": "TR15", + "name": "Burdur" + }, + { + "code": "TR16", + "name": "Bursa" + }, + { + "code": "TR17", + "name": "Çanakkale" + }, + { + "code": "TR18", + "name": "Çankırı" + }, + { + "code": "TR19", + "name": "Çorum" + }, + { + "code": "TR20", + "name": "Denizli" + }, + { + "code": "TR21", + "name": "Diyarbakır" + }, + { + "code": "TR22", + "name": "Edirne" + }, + { + "code": "TR23", + "name": "Elazığ" + }, + { + "code": "TR24", + "name": "Erzincan" + }, + { + "code": "TR25", + "name": "Erzurum" + }, + { + "code": "TR26", + "name": "Eskişehir" + }, + { + "code": "TR27", + "name": "Gaziantep" + }, + { + "code": "TR28", + "name": "Giresun" + }, + { + "code": "TR29", + "name": "Gümüşhane" + }, + { + "code": "TR30", + "name": "Hakkari" + }, + { + "code": "TR31", + "name": "Hatay" + }, + { + "code": "TR32", + "name": "Isparta" + }, + { + "code": "TR33", + "name": "İçel" + }, + { + "code": "TR34", + "name": "İstanbul" + }, + { + "code": "TR35", + "name": "İzmir" + }, + { + "code": "TR36", + "name": "Kars" + }, + { + "code": "TR37", + "name": "Kastamonu" + }, + { + "code": "TR38", + "name": "Kayseri" + }, + { + "code": "TR39", + "name": "Kırklareli" + }, + { + "code": "TR40", + "name": "Kırşehir" + }, + { + "code": "TR41", + "name": "Kocaeli" + }, + { + "code": "TR42", + "name": "Konya" + }, + { + "code": "TR43", + "name": "Kütahya" + }, + { + "code": "TR44", + "name": "Malatya" + }, + { + "code": "TR45", + "name": "Manisa" + }, + { + "code": "TR46", + "name": "Kahramanmaraş" + }, + { + "code": "TR47", + "name": "Mardin" + }, + { + "code": "TR48", + "name": "Muğla" + }, + { + "code": "TR49", + "name": "Muş" + }, + { + "code": "TR50", + "name": "Nevşehir" + }, + { + "code": "TR51", + "name": "Niğde" + }, + { + "code": "TR52", + "name": "Ordu" + }, + { + "code": "TR53", + "name": "Rize" + }, + { + "code": "TR54", + "name": "Sakarya" + }, + { + "code": "TR55", + "name": "Samsun" + }, + { + "code": "TR56", + "name": "Siirt" + }, + { + "code": "TR57", + "name": "Sinop" + }, + { + "code": "TR58", + "name": "Sivas" + }, + { + "code": "TR59", + "name": "Tekirdağ" + }, + { + "code": "TR60", + "name": "Tokat" + }, + { + "code": "TR61", + "name": "Trabzon" + }, + { + "code": "TR62", + "name": "Tunceli" + }, + { + "code": "TR63", + "name": "Şanlıurfa" + }, + { + "code": "TR64", + "name": "Uşak" + }, + { + "code": "TR65", + "name": "Van" + }, + { + "code": "TR66", + "name": "Yozgat" + }, + { + "code": "TR67", + "name": "Zonguldak" + }, + { + "code": "TR68", + "name": "Aksaray" + }, + { + "code": "TR69", + "name": "Bayburt" + }, + { + "code": "TR70", + "name": "Karaman" + }, + { + "code": "TR71", + "name": "Kırıkkale" + }, + { + "code": "TR72", + "name": "Batman" + }, + { + "code": "TR73", + "name": "Şırnak" + }, + { + "code": "TR74", + "name": "Bartın" + }, + { + "code": "TR75", + "name": "Ardahan" + }, + { + "code": "TR76", + "name": "Iğdır" + }, + { + "code": "TR77", + "name": "Yalova" + }, + { + "code": "TR78", + "name": "Karabük" + }, + { + "code": "TR79", + "name": "Kilis" + }, + { + "code": "TR80", + "name": "Osmaniye" + }, + { + "code": "TR81", + "name": "Düzce" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TM", + "name": "Turkmenistan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TC", + "name": "Turks and Caicos Islands", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TV", + "name": "Tuvalu", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UG", + "name": "Uganda", + "states": [{ + "code": "UG314", + "name": "Abim" + }, + { + "code": "UG301", + "name": "Adjumani" + }, + { + "code": "UG322", + "name": "Agago" + }, + { + "code": "UG323", + "name": "Alebtong" + }, + { + "code": "UG315", + "name": "Amolatar" + }, + { + "code": "UG324", + "name": "Amudat" + }, + { + "code": "UG216", + "name": "Amuria" + }, + { + "code": "UG316", + "name": "Amuru" + }, + { + "code": "UG302", + "name": "Apac" + }, + { + "code": "UG303", + "name": "Arua" + }, + { + "code": "UG217", + "name": "Budaka" + }, + { + "code": "UG218", + "name": "Bududa" + }, + { + "code": "UG201", + "name": "Bugiri" + }, + { + "code": "UG235", + "name": "Bugweri" + }, + { + "code": "UG420", + "name": "Buhweju" + }, + { + "code": "UG117", + "name": "Buikwe" + }, + { + "code": "UG219", + "name": "Bukedea" + }, + { + "code": "UG118", + "name": "Bukomansimbi" + }, + { + "code": "UG220", + "name": "Bukwa" + }, + { + "code": "UG225", + "name": "Bulambuli" + }, + { + "code": "UG416", + "name": "Buliisa" + }, + { + "code": "UG401", + "name": "Bundibugyo" + }, + { + "code": "UG430", + "name": "Bunyangabu" + }, + { + "code": "UG402", + "name": "Bushenyi" + }, + { + "code": "UG202", + "name": "Busia" + }, + { + "code": "UG221", + "name": "Butaleja" + }, + { + "code": "UG119", + "name": "Butambala" + }, + { + "code": "UG233", + "name": "Butebo" + }, + { + "code": "UG120", + "name": "Buvuma" + }, + { + "code": "UG226", + "name": "Buyende" + }, + { + "code": "UG317", + "name": "Dokolo" + }, + { + "code": "UG121", + "name": "Gomba" + }, + { + "code": "UG304", + "name": "Gulu" + }, + { + "code": "UG403", + "name": "Hoima" + }, + { + "code": "UG417", + "name": "Ibanda" + }, + { + "code": "UG203", + "name": "Iganga" + }, + { + "code": "UG418", + "name": "Isingiro" + }, + { + "code": "UG204", + "name": "Jinja" + }, + { + "code": "UG318", + "name": "Kaabong" + }, + { + "code": "UG404", + "name": "Kabale" + }, + { + "code": "UG405", + "name": "Kabarole" + }, + { + "code": "UG213", + "name": "Kaberamaido" + }, + { + "code": "UG427", + "name": "Kagadi" + }, + { + "code": "UG428", + "name": "Kakumiro" + }, + { + "code": "UG101", + "name": "Kalangala" + }, + { + "code": "UG222", + "name": "Kaliro" + }, + { + "code": "UG122", + "name": "Kalungu" + }, + { + "code": "UG102", + "name": "Kampala" + }, + { + "code": "UG205", + "name": "Kamuli" + }, + { + "code": "UG413", + "name": "Kamwenge" + }, + { + "code": "UG414", + "name": "Kanungu" + }, + { + "code": "UG206", + "name": "Kapchorwa" + }, + { + "code": "UG236", + "name": "Kapelebyong" + }, + { + "code": "UG126", + "name": "Kasanda" + }, + { + "code": "UG406", + "name": "Kasese" + }, + { + "code": "UG207", + "name": "Katakwi" + }, + { + "code": "UG112", + "name": "Kayunga" + }, + { + "code": "UG407", + "name": "Kibaale" + }, + { + "code": "UG103", + "name": "Kiboga" + }, + { + "code": "UG227", + "name": "Kibuku" + }, + { + "code": "UG432", + "name": "Kikuube" + }, + { + "code": "UG419", + "name": "Kiruhura" + }, + { + "code": "UG421", + "name": "Kiryandongo" + }, + { + "code": "UG408", + "name": "Kisoro" + }, + { + "code": "UG305", + "name": "Kitgum" + }, + { + "code": "UG319", + "name": "Koboko" + }, + { + "code": "UG325", + "name": "Kole" + }, + { + "code": "UG306", + "name": "Kotido" + }, + { + "code": "UG208", + "name": "Kumi" + }, + { + "code": "UG333", + "name": "Kwania" + }, + { + "code": "UG228", + "name": "Kween" + }, + { + "code": "UG123", + "name": "Kyankwanzi" + }, + { + "code": "UG422", + "name": "Kyegegwa" + }, + { + "code": "UG415", + "name": "Kyenjojo" + }, + { + "code": "UG125", + "name": "Kyotera" + }, + { + "code": "UG326", + "name": "Lamwo" + }, + { + "code": "UG307", + "name": "Lira" + }, + { + "code": "UG229", + "name": "Luuka" + }, + { + "code": "UG104", + "name": "Luwero" + }, + { + "code": "UG124", + "name": "Lwengo" + }, + { + "code": "UG114", + "name": "Lyantonde" + }, + { + "code": "UG223", + "name": "Manafwa" + }, + { + "code": "UG320", + "name": "Maracha" + }, + { + "code": "UG105", + "name": "Masaka" + }, + { + "code": "UG409", + "name": "Masindi" + }, + { + "code": "UG214", + "name": "Mayuge" + }, + { + "code": "UG209", + "name": "Mbale" + }, + { + "code": "UG410", + "name": "Mbarara" + }, + { + "code": "UG423", + "name": "Mitooma" + }, + { + "code": "UG115", + "name": "Mityana" + }, + { + "code": "UG308", + "name": "Moroto" + }, + { + "code": "UG309", + "name": "Moyo" + }, + { + "code": "UG106", + "name": "Mpigi" + }, + { + "code": "UG107", + "name": "Mubende" + }, + { + "code": "UG108", + "name": "Mukono" + }, + { + "code": "UG334", + "name": "Nabilatuk" + }, + { + "code": "UG311", + "name": "Nakapiripirit" + }, + { + "code": "UG116", + "name": "Nakaseke" + }, + { + "code": "UG109", + "name": "Nakasongola" + }, + { + "code": "UG230", + "name": "Namayingo" + }, + { + "code": "UG234", + "name": "Namisindwa" + }, + { + "code": "UG224", + "name": "Namutumba" + }, + { + "code": "UG327", + "name": "Napak" + }, + { + "code": "UG310", + "name": "Nebbi" + }, + { + "code": "UG231", + "name": "Ngora" + }, + { + "code": "UG424", + "name": "Ntoroko" + }, + { + "code": "UG411", + "name": "Ntungamo" + }, + { + "code": "UG328", + "name": "Nwoya" + }, + { + "code": "UG331", + "name": "Omoro" + }, + { + "code": "UG329", + "name": "Otuke" + }, + { + "code": "UG321", + "name": "Oyam" + }, + { + "code": "UG312", + "name": "Pader" + }, + { + "code": "UG332", + "name": "Pakwach" + }, + { + "code": "UG210", + "name": "Pallisa" + }, + { + "code": "UG110", + "name": "Rakai" + }, + { + "code": "UG429", + "name": "Rubanda" + }, + { + "code": "UG425", + "name": "Rubirizi" + }, + { + "code": "UG431", + "name": "Rukiga" + }, + { + "code": "UG412", + "name": "Rukungiri" + }, + { + "code": "UG111", + "name": "Sembabule" + }, + { + "code": "UG232", + "name": "Serere" + }, + { + "code": "UG426", + "name": "Sheema" + }, + { + "code": "UG215", + "name": "Sironko" + }, + { + "code": "UG211", + "name": "Soroti" + }, + { + "code": "UG212", + "name": "Tororo" + }, + { + "code": "UG113", + "name": "Wakiso" + }, + { + "code": "UG313", + "name": "Yumbe" + }, + { + "code": "UG330", + "name": "Zombo" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UA", + "name": "Ukraine", + "states": [{ + "code": "UA05", + "name": "Vinnychchyna" + }, + { + "code": "UA07", + "name": "Volyn" + }, + { + "code": "UA09", + "name": "Luhanshchyna" + }, + { + "code": "UA12", + "name": "Dnipropetrovshchyna" + }, + { + "code": "UA14", + "name": "Donechchyna" + }, + { + "code": "UA18", + "name": "Zhytomyrshchyna" + }, + { + "code": "UA21", + "name": "Zakarpattia" + }, + { + "code": "UA23", + "name": "Zaporizhzhya" + }, + { + "code": "UA26", + "name": "Prykarpattia" + }, + { + "code": "UA30", + "name": "Kyiv" + }, + { + "code": "UA32", + "name": "Kyivshchyna" + }, + { + "code": "UA35", + "name": "Kirovohradschyna" + }, + { + "code": "UA40", + "name": "Sevastopol" + }, + { + "code": "UA43", + "name": "Crimea" + }, + { + "code": "UA46", + "name": "Lvivshchyna" + }, + { + "code": "UA48", + "name": "Mykolayivschyna" + }, + { + "code": "UA51", + "name": "Odeshchyna" + }, + { + "code": "UA53", + "name": "Poltavshchyna" + }, + { + "code": "UA56", + "name": "Rivnenshchyna" + }, + { + "code": "UA59", + "name": "Sumshchyna" + }, + { + "code": "UA61", + "name": "Ternopilshchyna" + }, + { + "code": "UA63", + "name": "Kharkivshchyna" + }, + { + "code": "UA65", + "name": "Khersonshchyna" + }, + { + "code": "UA68", + "name": "Khmelnychchyna" + }, + { + "code": "UA71", + "name": "Cherkashchyna" + }, + { + "code": "UA74", + "name": "Chernihivshchyna" + }, + { + "code": "UA77", + "name": "Chernivtsi Oblast" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AE", + "name": "United Arab Emirates", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GB", + "name": "United Kingdom (UK)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "US", + "name": "United States (US)", + "states": [{ + "code": "AL", + "name": "Alabama" + }, + { + "code": "AK", + "name": "Alaska" + }, + { + "code": "AZ", + "name": "Arizona" + }, + { + "code": "AR", + "name": "Arkansas" + }, + { + "code": "CA", + "name": "California" + }, + { + "code": "CO", + "name": "Colorado" + }, + { + "code": "CT", + "name": "Connecticut" + }, + { + "code": "DE", + "name": "Delaware" + }, + { + "code": "DC", + "name": "District Of Columbia" + }, + { + "code": "FL", + "name": "Florida" + }, + { + "code": "GA", + "name": "Georgia" + }, + { + "code": "HI", + "name": "Hawaii" + }, + { + "code": "ID", + "name": "Idaho" + }, + { + "code": "IL", + "name": "Illinois" + }, + { + "code": "IN", + "name": "Indiana" + }, + { + "code": "IA", + "name": "Iowa" + }, + { + "code": "KS", + "name": "Kansas" + }, + { + "code": "KY", + "name": "Kentucky" + }, + { + "code": "LA", + "name": "Louisiana" + }, + { + "code": "ME", + "name": "Maine" + }, + { + "code": "MD", + "name": "Maryland" + }, + { + "code": "MA", + "name": "Massachusetts" + }, + { + "code": "MI", + "name": "Michigan" + }, + { + "code": "MN", + "name": "Minnesota" + }, + { + "code": "MS", + "name": "Mississippi" + }, + { + "code": "MO", + "name": "Missouri" + }, + { + "code": "MT", + "name": "Montana" + }, + { + "code": "NE", + "name": "Nebraska" + }, + { + "code": "NV", + "name": "Nevada" + }, + { + "code": "NH", + "name": "New Hampshire" + }, + { + "code": "NJ", + "name": "New Jersey" + }, + { + "code": "NM", + "name": "New Mexico" + }, + { + "code": "NY", + "name": "New York" + }, + { + "code": "NC", + "name": "North Carolina" + }, + { + "code": "ND", + "name": "North Dakota" + }, + { + "code": "OH", + "name": "Ohio" + }, + { + "code": "OK", + "name": "Oklahoma" + }, + { + "code": "OR", + "name": "Oregon" + }, + { + "code": "PA", + "name": "Pennsylvania" + }, + { + "code": "RI", + "name": "Rhode Island" + }, + { + "code": "SC", + "name": "South Carolina" + }, + { + "code": "SD", + "name": "South Dakota" + }, + { + "code": "TN", + "name": "Tennessee" + }, + { + "code": "TX", + "name": "Texas" + }, + { + "code": "UT", + "name": "Utah" + }, + { + "code": "VT", + "name": "Vermont" + }, + { + "code": "VA", + "name": "Virginia" + }, + { + "code": "WA", + "name": "Washington" + }, + { + "code": "WV", + "name": "West Virginia" + }, + { + "code": "WI", + "name": "Wisconsin" + }, + { + "code": "WY", + "name": "Wyoming" + }, + { + "code": "AA", + "name": "Armed Forces (AA)" + }, + { + "code": "AE", + "name": "Armed Forces (AE)" + }, + { + "code": "AP", + "name": "Armed Forces (AP)" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UM", + "name": "United States (US) Minor Outlying Islands", + "states": [{ + "code": 81, + "name": "Baker Island" + }, + { + "code": 84, + "name": "Howland Island" + }, + { + "code": 86, + "name": "Jarvis Island" + }, + { + "code": 67, + "name": "Johnston Atoll" + }, + { + "code": 89, + "name": "Kingman Reef" + }, + { + "code": 71, + "name": "Midway Atoll" + }, + { + "code": 76, + "name": "Navassa Island" + }, + { + "code": 95, + "name": "Palmyra Atoll" + }, + { + "code": 79, + "name": "Wake Island" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UY", + "name": "Uruguay", + "states": [{ + "code": "UY-AR", + "name": "Artigas" + }, + { + "code": "UY-CA", + "name": "Canelones" + }, + { + "code": "UY-CL", + "name": "Cerro Largo" + }, + { + "code": "UY-CO", + "name": "Colonia" + }, + { + "code": "UY-DU", + "name": "Durazno" + }, + { + "code": "UY-FS", + "name": "Flores" + }, + { + "code": "UY-FD", + "name": "Florida" + }, + { + "code": "UY-LA", + "name": "Lavalleja" + }, + { + "code": "UY-MA", + "name": "Maldonado" + }, + { + "code": "UY-MO", + "name": "Montevideo" + }, + { + "code": "UY-PA", + "name": "Paysandú" + }, + { + "code": "UY-RN", + "name": "Río Negro" + }, + { + "code": "UY-RV", + "name": "Rivera" + }, + { + "code": "UY-RO", + "name": "Rocha" + }, + { + "code": "UY-SA", + "name": "Salto" + }, + { + "code": "UY-SJ", + "name": "San José" + }, + { + "code": "UY-SO", + "name": "Soriano" + }, + { + "code": "UY-TA", + "name": "Tacuarembó" + }, + { + "code": "UY-TT", + "name": "Treinta y Tres" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UZ", + "name": "Uzbekistan", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VU", + "name": "Vanuatu", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VA", + "name": "Vatican", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VE", + "name": "Venezuela", + "states": [{ + "code": "VE-A", + "name": "Capital" + }, + { + "code": "VE-B", + "name": "Anzoátegui" + }, + { + "code": "VE-C", + "name": "Apure" + }, + { + "code": "VE-D", + "name": "Aragua" + }, + { + "code": "VE-E", + "name": "Barinas" + }, + { + "code": "VE-F", + "name": "Bolívar" + }, + { + "code": "VE-G", + "name": "Carabobo" + }, + { + "code": "VE-H", + "name": "Cojedes" + }, + { + "code": "VE-I", + "name": "Falcón" + }, + { + "code": "VE-J", + "name": "Guárico" + }, + { + "code": "VE-K", + "name": "Lara" + }, + { + "code": "VE-L", + "name": "Mérida" + }, + { + "code": "VE-M", + "name": "Miranda" + }, + { + "code": "VE-N", + "name": "Monagas" + }, + { + "code": "VE-O", + "name": "Nueva Esparta" + }, + { + "code": "VE-P", + "name": "Portuguesa" + }, + { + "code": "VE-R", + "name": "Sucre" + }, + { + "code": "VE-S", + "name": "Táchira" + }, + { + "code": "VE-T", + "name": "Trujillo" + }, + { + "code": "VE-U", + "name": "Yaracuy" + }, + { + "code": "VE-V", + "name": "Zulia" + }, + { + "code": "VE-W", + "name": "Federal Dependencies" + }, + { + "code": "VE-X", + "name": "La Guaira (Vargas)" + }, + { + "code": "VE-Y", + "name": "Delta Amacuro" + }, + { + "code": "VE-Z", + "name": "Amazonas" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VN", + "name": "Vietnam", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VG", + "name": "Virgin Islands (British)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VI", + "name": "Virgin Islands (US)", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "WF", + "name": "Wallis and Futuna", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "EH", + "name": "Western Sahara", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "YE", + "name": "Yemen", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ZM", + "name": "Zambia", + "states": [{ + "code": "ZM-01", + "name": "Western" + }, + { + "code": "ZM-02", + "name": "Central" + }, + { + "code": "ZM-03", + "name": "Eastern" + }, + { + "code": "ZM-04", + "name": "Luapula" + }, + { + "code": "ZM-05", + "name": "Northern" + }, + { + "code": "ZM-06", + "name": "North-Western" + }, + { + "code": "ZM-07", + "name": "Southern" + }, + { + "code": "ZM-08", + "name": "Copperbelt" + }, + { + "code": "ZM-09", + "name": "Lusaka" + }, + { + "code": "ZM-10", + "name": "Muchinga" + } + ], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ZW", + "name": "Zimbabwe", + "states": [], + "_links": { + "self": [{ + "href": expect.any(String) + }], + "collection": [{ + "href": expect.any(String) + }] + } + }) + ])); + }); + + test('can view country data', async ({ + request + }) => { + // call API to retrieve a specific country data + const response = await request.get('/wp-json/wc/v3/data/countries/au'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + + expect(responseJSON).toEqual( + expect.objectContaining({ + "code": "AU", + "name": "Australia", + "states": [{ + "code": "ACT", + "name": "Australian Capital Territory" + }, + { + "code": "NSW", + "name": "New South Wales" + }, + { + "code": "NT", + "name": "Northern Territory" + }, + { + "code": "QLD", + "name": "Queensland" + }, + { + "code": "SA", + "name": "South Australia" + }, + { + "code": "TAS", + "name": "Tasmania" + }, + { + "code": "VIC", + "name": "Victoria" + }, + { + "code": "WA", + "name": "Western Australia" + } + ], + }) + ); + }); + + test('can view all currencies', async ({ + request + }) => { + // call API to retrieve all currencies + const response = await request.get('/wp-json/wc/v3/data/currencies'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AED", + "name": "United Arab Emirates dirham", + "symbol": "د.إ", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AED") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AFN", + "name": "Afghan afghani", + "symbol": "؋", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AFN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ALL", + "name": "Albanian lek", + "symbol": "L", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ALL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AMD", + "name": "Armenian dram", + "symbol": "AMD", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AMD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ANG", + "name": "Netherlands Antillean guilder", + "symbol": "ƒ", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ANG") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AOA", + "name": "Angolan kwanza", + "symbol": "Kz", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AOA") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ARS", + "name": "Argentine peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ARS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AUD", + "name": "Australian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AUD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AWG", + "name": "Aruban florin", + "symbol": "Afl.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AWG") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "AZN", + "name": "Azerbaijani manat", + "symbol": "AZN", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/AZN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BAM", + "name": "Bosnia and Herzegovina convertible mark", + "symbol": "KM", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BAM") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BBD", + "name": "Barbadian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BBD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BDT", + "name": "Bangladeshi taka", + "symbol": "৳ ", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BDT") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BGN", + "name": "Bulgarian lev", + "symbol": "лв.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BGN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BHD", + "name": "Bahraini dinar", + "symbol": ".د.ب", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BHD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BIF", + "name": "Burundian franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BIF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BMD", + "name": "Bermudian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BMD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BND", + "name": "Brunei dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BND") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BOB", + "name": "Bolivian boliviano", + "symbol": "Bs.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BOB") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BRL", + "name": "Brazilian real", + "symbol": "R$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BRL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BSD", + "name": "Bahamian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BSD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BTC", + "name": "Bitcoin", + "symbol": "฿", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BTC") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BTN", + "name": "Bhutanese ngultrum", + "symbol": "Nu.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BTN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BWP", + "name": "Botswana pula", + "symbol": "P", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BWP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BYR", + "name": "Belarusian ruble (old)", + "symbol": "Br", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BYR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BYN", + "name": "Belarusian ruble", + "symbol": "Br", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BYN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "BZD", + "name": "Belize dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/BZD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CAD", + "name": "Canadian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CAD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CDF", + "name": "Congolese franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CDF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CHF", + "name": "Swiss franc", + "symbol": "CHF", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CHF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CLP", + "name": "Chilean peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CLP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CNY", + "name": "Chinese yuan", + "symbol": "¥", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CNY") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "COP", + "name": "Colombian peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/COP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CRC", + "name": "Costa Rican colón", + "symbol": "₡", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CRC") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CUC", + "name": "Cuban convertible peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CUC") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CUP", + "name": "Cuban peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CUP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CVE", + "name": "Cape Verdean escudo", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CVE") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "CZK", + "name": "Czech koruna", + "symbol": "Kč", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/CZK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DJF", + "name": "Djiboutian franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/DJF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DKK", + "name": "Danish krone", + "symbol": "kr.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/DKK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DOP", + "name": "Dominican peso", + "symbol": "RD$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/DOP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "DZD", + "name": "Algerian dinar", + "symbol": "د.ج", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/DZD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "EGP", + "name": "Egyptian pound", + "symbol": "EGP", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/EGP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ERN", + "name": "Eritrean nakfa", + "symbol": "Nfk", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ERN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ETB", + "name": "Ethiopian birr", + "symbol": "Br", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ETB") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "EUR", + "name": "Euro", + "symbol": "€", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/EUR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FJD", + "name": "Fijian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/FJD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "FKP", + "name": "Falkland Islands pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/FKP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GBP", + "name": "Pound sterling", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GBP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GEL", + "name": "Georgian lari", + "symbol": "₾", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GEL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GGP", + "name": "Guernsey pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GGP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GHS", + "name": "Ghana cedi", + "symbol": "₵", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GHS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GIP", + "name": "Gibraltar pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GIP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GMD", + "name": "Gambian dalasi", + "symbol": "D", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GMD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GNF", + "name": "Guinean franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GNF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GTQ", + "name": "Guatemalan quetzal", + "symbol": "Q", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GTQ") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "GYD", + "name": "Guyanese dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/GYD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HKD", + "name": "Hong Kong dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/HKD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HNL", + "name": "Honduran lempira", + "symbol": "L", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/HNL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HRK", + "name": "Croatian kuna", + "symbol": "kn", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/HRK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HTG", + "name": "Haitian gourde", + "symbol": "G", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/HTG") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "HUF", + "name": "Hungarian forint", + "symbol": "Ft", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/HUF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IDR", + "name": "Indonesian rupiah", + "symbol": "Rp", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/IDR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ILS", + "name": "Israeli new shekel", + "symbol": "₪", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ILS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IMP", + "name": "Manx pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/IMP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "INR", + "name": "Indian rupee", + "symbol": "₹", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/INR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IQD", + "name": "Iraqi dinar", + "symbol": "د.ع", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/IQD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IRR", + "name": "Iranian rial", + "symbol": "﷼", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/IRR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "IRT", + "name": "Iranian toman", + "symbol": "تومان", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/IRT") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ISK", + "name": "Icelandic króna", + "symbol": "kr.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ISK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JEP", + "name": "Jersey pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/JEP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JMD", + "name": "Jamaican dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/JMD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JOD", + "name": "Jordanian dinar", + "symbol": "د.ا", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/JOD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "JPY", + "name": "Japanese yen", + "symbol": "¥", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/JPY") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KES", + "name": "Kenyan shilling", + "symbol": "KSh", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KES") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KGS", + "name": "Kyrgyzstani som", + "symbol": "сом", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KGS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KHR", + "name": "Cambodian riel", + "symbol": "៛", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KHR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KMF", + "name": "Comorian franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KMF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KPW", + "name": "North Korean won", + "symbol": "₩", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KPW") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KRW", + "name": "South Korean won", + "symbol": "₩", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KRW") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KWD", + "name": "Kuwaiti dinar", + "symbol": "د.ك", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KWD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KYD", + "name": "Cayman Islands dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KYD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "KZT", + "name": "Kazakhstani tenge", + "symbol": "₸", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/KZT") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LAK", + "name": "Lao kip", + "symbol": "₭", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/LAK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LBP", + "name": "Lebanese pound", + "symbol": "ل.ل", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/LBP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LKR", + "name": "Sri Lankan rupee", + "symbol": "රු", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/LKR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LRD", + "name": "Liberian dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/LRD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LSL", + "name": "Lesotho loti", + "symbol": "L", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/LSL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "LYD", + "name": "Libyan dinar", + "symbol": "د.ل", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/LYD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MAD", + "name": "Moroccan dirham", + "symbol": "د.م.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MAD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MDL", + "name": "Moldovan leu", + "symbol": "MDL", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MDL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MGA", + "name": "Malagasy ariary", + "symbol": "Ar", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MGA") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MKD", + "name": "Macedonian denar", + "symbol": "ден", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MKD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MMK", + "name": "Burmese kyat", + "symbol": "Ks", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MMK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MNT", + "name": "Mongolian tögrög", + "symbol": "₮", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MNT") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MOP", + "name": "Macanese pataca", + "symbol": "P", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MOP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MRU", + "name": "Mauritanian ouguiya", + "symbol": "UM", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MRU") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MUR", + "name": "Mauritian rupee", + "symbol": "₨", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MUR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MVR", + "name": "Maldivian rufiyaa", + "symbol": ".ރ", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MVR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MWK", + "name": "Malawian kwacha", + "symbol": "MK", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MWK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MXN", + "name": "Mexican peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MXN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MYR", + "name": "Malaysian ringgit", + "symbol": "RM", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MYR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "MZN", + "name": "Mozambican metical", + "symbol": "MT", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/MZN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NAD", + "name": "Namibian dollar", + "symbol": "N$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/NAD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NGN", + "name": "Nigerian naira", + "symbol": "₦", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/NGN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NIO", + "name": "Nicaraguan córdoba", + "symbol": "C$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/NIO") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NOK", + "name": "Norwegian krone", + "symbol": "kr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/NOK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NPR", + "name": "Nepalese rupee", + "symbol": "₨", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/NPR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "NZD", + "name": "New Zealand dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/NZD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "OMR", + "name": "Omani rial", + "symbol": "ر.ع.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/OMR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PAB", + "name": "Panamanian balboa", + "symbol": "B/.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PAB") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PEN", + "name": "Sol", + "symbol": "S/", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PEN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PGK", + "name": "Papua New Guinean kina", + "symbol": "K", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PGK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PHP", + "name": "Philippine peso", + "symbol": "₱", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PHP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PKR", + "name": "Pakistani rupee", + "symbol": "₨", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PKR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PLN", + "name": "Polish złoty", + "symbol": "zł", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PLN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PRB", + "name": "Transnistrian ruble", + "symbol": "р.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PRB") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "PYG", + "name": "Paraguayan guaraní", + "symbol": "₲", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/PYG") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "QAR", + "name": "Qatari riyal", + "symbol": "ر.ق", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/QAR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RON", + "name": "Romanian leu", + "symbol": "lei", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/RON") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RSD", + "name": "Serbian dinar", + "symbol": "рсд", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/RSD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RUB", + "name": "Russian ruble", + "symbol": "₽", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/RUB") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "RWF", + "name": "Rwandan franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/RWF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SAR", + "name": "Saudi riyal", + "symbol": "ر.س", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SAR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SBD", + "name": "Solomon Islands dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SBD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SCR", + "name": "Seychellois rupee", + "symbol": "₨", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SCR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SDG", + "name": "Sudanese pound", + "symbol": "ج.س.", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SDG") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SEK", + "name": "Swedish krona", + "symbol": "kr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SEK") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SGD", + "name": "Singapore dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SGD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SHP", + "name": "Saint Helena pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SHP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SLL", + "name": "Sierra Leonean leone", + "symbol": "Le", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SLL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SOS", + "name": "Somali shilling", + "symbol": "Sh", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SOS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SRD", + "name": "Surinamese dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SRD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SSP", + "name": "South Sudanese pound", + "symbol": "£", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SSP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "STN", + "name": "São Tomé and Príncipe dobra", + "symbol": "Db", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/STN") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SYP", + "name": "Syrian pound", + "symbol": "ل.س", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SYP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "SZL", + "name": "Swazi lilangeni", + "symbol": "E", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/SZL") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "THB", + "name": "Thai baht", + "symbol": "฿", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/THB") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TJS", + "name": "Tajikistani somoni", + "symbol": "ЅМ", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TJS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TMT", + "name": "Turkmenistan manat", + "symbol": "m", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TMT") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TND", + "name": "Tunisian dinar", + "symbol": "د.ت", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TND") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TOP", + "name": "Tongan paʻanga", + "symbol": "T$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TOP") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TRY", + "name": "Turkish lira", + "symbol": "₺", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TRY") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TTD", + "name": "Trinidad and Tobago dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TTD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TWD", + "name": "New Taiwan dollar", + "symbol": "NT$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TWD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "TZS", + "name": "Tanzanian shilling", + "symbol": "Sh", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/TZS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UAH", + "name": "Ukrainian hryvnia", + "symbol": "₴", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/UAH") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UGX", + "name": "Ugandan shilling", + "symbol": "UGX", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/UGX") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "USD", + "name": "United States (US) dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/USD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UYU", + "name": "Uruguayan peso", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/UYU") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "UZS", + "name": "Uzbekistani som", + "symbol": "UZS", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/UZS") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VEF", + "name": "Venezuelan bolívar", + "symbol": "Bs F", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/VEF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VES", + "name": "Bolívar soberano", + "symbol": "Bs.S", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/VES") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VND", + "name": "Vietnamese đồng", + "symbol": "₫", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/VND") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "VUV", + "name": "Vanuatu vatu", + "symbol": "Vt", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/VUV") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "WST", + "name": "Samoan tālā", + "symbol": "T", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/WST") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "XAF", + "name": "Central African CFA franc", + "symbol": "CFA", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/XAF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "XCD", + "name": "East Caribbean dollar", + "symbol": "$", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/XCD") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "XOF", + "name": "West African CFA franc", + "symbol": "CFA", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/XOF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "XPF", + "name": "CFP franc", + "symbol": "Fr", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/XPF") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "YER", + "name": "Yemeni rial", + "symbol": "﷼", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/YER") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ZAR", + "name": "South African rand", + "symbol": "R", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ZAR") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + expect(responseJSON).toEqual(expect.arrayContaining([ + expect.objectContaining({ + "code": "ZMW", + "name": "Zambian kwacha", + "symbol": "ZK", + "_links": { + "self": [{ + "href": expect.stringContaining("data/currencies/ZMW") + }], + "collection": [{ + "href": expect.stringContaining("data/currencies") + }] + } + }) + ])); + }); + + test('can view currency data', async ({ + request + }) => { + // call API to retrieve a specific currency data + const response = await request.get('/wp-json/wc/v3/data/currencies/fkp'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + + expect(responseJSON).toEqual( + expect.objectContaining({ + "code": "FKP", + "name": "Falkland Islands pound", + "symbol": "£" + })); + }); + + test('can view current currency', async ({ + request + }) => { + // call API to retrieve current currency data + const response = await request.get('/wp-json/wc/v3/data/currencies/current'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + + expect(responseJSON).toEqual( + expect.objectContaining({ + "code": "USD", + "name": "United States (US) dollar", + "symbol": "$", + })); + }); +}); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/orders/orders-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/orders/orders-crud.test.js index 5b50e8b5ee1..61a8f681f06 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/orders/orders-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/orders/orders-crud.test.js @@ -175,7 +175,7 @@ test.describe('Orders API tests: CRUD', () => { const response = await request.get(`/wp-json/wc/v3/orders/${orderId}/notes`); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toBeGreaterThan(0); }); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js new file mode 100644 index 00000000000..f1e83fe7dba --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js @@ -0,0 +1,238 @@ +const { + test, + expect +} = require('@playwright/test'); + +/** + * Tests for the WooCommerce Refunds API. + * + * @group api + * @group payment gateways + * + */ +test.describe('Payment Gateways API tests', () => { + + test('can view all payment gatways', 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); + + + 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": "" + } + }, + }), + + 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": "" + } + }, + }), + + 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": { + "Flat rate": { + "flat_rate": "Any "Flat rate" method" + }, + "Free shipping": { + "free_shipping": "Any "Free shipping" method" + }, + "Local pickup": { + "local_pickup": "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": "" + } + }, + }) + + ]) + ); + }); + + test('can view a payment gateway', async ({ + request + }) => { + // call API to retrieve a single payment gatway + const response = await request.get('/wp-json/wc/v3/payment_gateways/bacs'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + + expect(responseJSON).toEqual( + 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": "" + } + }, + })); + }); + + test('can update a payment gateway', async ({ + request + }) => { + // call API to update a payment gatway + const response = await request.put('/wp-json/wc/v3/payment_gateways/bacs', { + data: { + enabled: true + } + }); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + expect(responseJSON).toEqual( + expect.objectContaining({ + enabled: true + }), + ); + + // reset payment gateway setting + await request.put('/wp-json/wc/v3/payment_gateways/bacs', { + data: { + enabled: false + } + }); + }); +}); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js index 017fdc10285..098f686712a 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js @@ -125,7 +125,7 @@ test.describe('Products API tests: CRUD', () => { // call API to update a product attribute term const response = await request.put(`wp-json/wc/v3/products/attributes/${productAttributeId}/terms/${productAttributeTermId}`, { data: { - name:'Square' + name: 'Square' } }); const responseJSON = await response.json(); @@ -398,14 +398,14 @@ test.describe('Products API tests: CRUD', () => { expect(responseJSON.image).toEqual(null); expect(responseJSON.menu_order).toEqual(0); expect(responseJSON.count).toEqual(0); - + }); - + test('can retrieve all product categories', async ({ request }) => { - // call API to retrieve all product tags + // call API to retrieve all product categories const response = await request.get('/wp-json/wc/v3/products/categories'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); @@ -417,7 +417,7 @@ test.describe('Products API tests: CRUD', () => { test('can update a product category', async ({ request }) => { - // call API to retrieve all product tags + // call API to retrieve all product categories const response = await request.put(`wp-json/wc/v3/products/categories/${productCategoryId}`, { data: { description: 'Games played on a video games console or computer.' @@ -485,13 +485,12 @@ test.describe('Products API tests: CRUD', () => { `wp-json/wc/v3/products/categories/batch`, { data: { create: [{ - name: "" + name: "Another Category Name" }, ], update: [{ - id: category1Id, - description: "Put them on your head." - } - ], + id: category1Id, + description: "Put them on your head." + }], delete: [ category2Id ] @@ -523,6 +522,245 @@ test.describe('Products API tests: CRUD', () => { }); }); + test.describe('Product review tests: CRUD', () => { + let productReviewId; + + test('can add a product review', async ({ + request + }) => { + const response = await request.post('wp-json/wc/v3/products/reviews', { + data: { + product_id: productId, + 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 existant 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: productId, + 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(productId); + 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(productId); + 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}` + ); + /** + * currently returns a 403 (forbidden) rather than a 404 (not found) + * an issue has been raised to track this + * See: https://github.com/woocommerce/woocommerce/issues/35162 + */ + expect(getDeletedProductReviewResponse.status()).toEqual(403); + }); + + 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: productId, + review: "Nice product!", + reviewer: "John Doe", + reviewer_email: "john.doe@example.com", + rating: 4 + }, + { + product_id: productId, + 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(productId); + 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(productId); + 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: productId, + 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}` + ); + /** + * currently returns a 403 (forbidden) rather than a 404 (not found) + * an issue has been raised to track this + * See: https://github.com/woocommerce/woocommerce/issues/35162 + */ + expect(getDeletedProductReviewResponse.status()).toEqual(403); + + // 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; @@ -556,14 +794,14 @@ test.describe('Products API tests: CRUD', () => { expect(responseJSON.slug).toEqual('priority'); expect(responseJSON.description).toEqual(''); expect(responseJSON.count).toEqual(0); - + }); - + test('can retrieve all product shipping classes', async ({ request }) => { - // call API to retrieve all product tags + // call API to retrieve all product shipping classes const response = await request.get('/wp-json/wc/v3/products/shipping_classes'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); @@ -574,7 +812,7 @@ test.describe('Products API tests: CRUD', () => { test('can update a product shipping class', async ({ request }) => { - // call API to retrieve all product tags + // call API to retrieve a product shipping class const response = await request.put(`wp-json/wc/v3/products/shipping_classes/${productShippingClassId}`, { data: { description: 'This is a description for the Priority shipping class.' @@ -641,10 +879,9 @@ test.describe('Products API tests: CRUD', () => { name: "Express" }, ], update: [{ - id: shippingClass1Id, - description: "Priority shipping." - } - ], + id: shippingClass1Id, + description: "Priority shipping." + }], delete: [ shippingClass2Id ] @@ -676,7 +913,7 @@ test.describe('Products API tests: CRUD', () => { }); }); - + test.describe('Product tags tests: CRUD', () => { let productTagId; @@ -722,7 +959,7 @@ test.describe('Products API tests: CRUD', () => { test('can update a product tag', async ({ request }) => { - // call API to retrieve all product tags + // call API to update a product tag const response = await request.put(`wp-json/wc/v3/products/tags/${productTagId}`, { data: { description: 'Genuine leather.' @@ -861,25 +1098,183 @@ test.describe('Products API tests: CRUD', () => { }); }); - test('can add a variable product', async ({ - request - }) => { - const response = await request.post('wp-json/wc/v3/products', { - data: variableProduct, + test.describe('Product variation tests: CRUD', () => { + let variableProductId; + let productVariationId; + + test('can add a variable product', async ({ + request + }) => { + const response = await request.post('wp-json/wc/v3/products', { + data: variableProduct, + }); + const responseJSON = await response.json(); + variableProductId = responseJSON.id; + expect(response.status()).toEqual(201); + expect(typeof variableProductId).toEqual('number'); + expect(responseJSON).toMatchObject(variableProduct); + expect(responseJSON.status).toEqual('publish'); }); - const responseJSON = await response.json(); - const variableProductId = responseJSON.id; - expect(response.status()).toEqual(201); - expect(typeof variableProductId).toEqual('number'); - expect(responseJSON).toMatchObject(variableProduct); - expect(responseJSON.status).toEqual('publish'); + test('can add a product variation', async ({ + request + }) => { + const response = await request.post(`wp-json/wc/v3/products/${variableProductId}/variations`, { + data: { + "regular_price": "29.00", + "attributes": [{ + "name": "Colour", + "option": "Green" + }] + }, + }); + const responseJSON = await response.json(); + productVariationId = responseJSON.id; + expect(response.status()).toEqual(201); + expect(typeof productVariationId).toEqual('number'); + expect(responseJSON.regular_price).toEqual("29.00"); + }); - // Cleanup: Delete the variable product - await request.delete(`wp-json/wc/v3/products/${ variableProductId }`, { - data: { - force: true, - }, + test('can retrieve a product variation', async ({ + request + }) => { + const response = await request.get(`wp-json/wc/v3/products/${variableProductId}/variations/${productVariationId}`); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON.id).toEqual(productVariationId); + expect(responseJSON.regular_price).toEqual('29.00'); + }); + + test('can retrieve all product variations', async ({ + request + }) => { + // call API to retrieve all product variations + const response = await request.get(`wp-json/wc/v3/products/${variableProductId}/variations`); + 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 variation', async ({ + request + }) => { + // call API to update the product variation + const response = await request.put(`wp-json/wc/v3/products/${variableProductId}/variations/${productVariationId}`, { + data: { + "regular_price": "30.00", + } + }); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON.id).toEqual(productVariationId); + expect(responseJSON.regular_price).toEqual('30.00'); + }); + + test('can permanently delete a product variation', async ({ + request + }) => { + // Delete the product variation. + const response = await request.delete( + `wp-json/wc/v3/products/${variableProductId}/variations/${productVariationId}`, { + data: { + force: true, + }, + } + ); + expect(response.status()).toEqual(200); + + // Verify that the product variation can no longer be retrieved. + const getDeletedProductVariationResponse = await request.get( + `wp-json/wc/v3/products/${variableProductId}/variations/${productVariationId}` + ); + expect(getDeletedProductVariationResponse.status()).toEqual(404); + }); + + test('can batch update product variations', async ({ + request + }) => { + // Batch create 2 product variations + const response = await request.post( + `wp-json/wc/v3/products/${variableProductId}/variations/batch`, { + data: { + create: [{ + "regular_price": "30.00", + "attributes": [{ + "name": "Colour", + "option": "Green" + }] + }, + { + "regular_price": "35.00", + "attributes": [{ + "name": "Colour", + "option": "Red" + }] + } + ] + } + } + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + const variation1Id = responseJSON.create[0].id; + const variation2Id = responseJSON.create[1].id; + expect(typeof variation1Id).toEqual('number'); + expect(typeof variation2Id).toEqual('number'); + expect(responseJSON.create[0].price).toEqual('30.00'); + expect(responseJSON.create[1].price).toEqual('35.00'); + + // Batch create a new variation, update a variation and delete another. + const responseBatchUpdate = await request.post( + `wp-json/wc/v3/products/${variableProductId}/variations/batch`, { + data: { + create: [{ + "regular_price": "25.99", + "attributes": [{ + "name": "Colour", + "option": "Blue" + }] + }], + update: [{ + id: variation2Id, + "regular_price": "35.99", + }], + delete: [ + variation1Id + ] + } + } + ); + + expect(response.status()).toEqual(200); + const responseBatchUpdateJSON = await responseBatchUpdate.json(); + const variation3Id = responseBatchUpdateJSON.create[0].id; + const responseUpdatedVariation = await request.get(`wp-json/wc/v3/products/${variableProductId}/variations/${variation2Id}`); + const responseUpdatedVariationJSON = await responseUpdatedVariation.json(); + expect(responseUpdatedVariationJSON.regular_price).toEqual('35.99'); + + // Verify that the deleted product variation can no longer be retrieved. + const getDeletedProductVariationResponse = await request.get( + `wp-json/wc/v3/products/${variableProductId}/variations/${variation1Id}` + ); + expect(getDeletedProductVariationResponse.status()).toEqual(404); + + // Batch delete the created product variations + await request.post( + `wp-json/wc/v3/products/${variableProductId}/variations/batch`, { + data: { + delete: [variation2Id, variation3Id] + } + } + ); + + // Cleanup: Delete the variable product + await request.delete(`wp-json/wc/v3/products/${ variableProductId }`, { + data: { + force: true, + }, + }); }); }); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js new file mode 100644 index 00000000000..2766df5af22 --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js @@ -0,0 +1,407 @@ +const { + test, + expect +} = require('@playwright/test'); + +/** + * Tests for the WooCommerce Refunds API. + * + * @group api + * @group reports + * + */ +test.describe('Reports API tests', () => { + + test('can view all reports', async ({ + request + }) => { + // call API to retrieve the reports + const response = await request.get('/wp-json/wc/v3/reports'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "sales", + "description": "List of sales reports.", + }) + ])); + }); + + test('can view sales reports', async ({ + request + }) => { + // call API to retrieve the sales reports + const response = await request.get('/wp-json/wc/v3/reports/sales'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + const today = new Date(); + const dd = String(today.getDate()).padStart(2, '0'); + const mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0! + const yyyy = today.getFullYear(); + const dateString = yyyy + '-' + mm + '-' + dd; + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "total_sales": expect.any(String), + "net_sales": expect.any(String), + "average_sales": expect.any(String), + "total_orders": expect.any(Number), + "total_items": expect.any(Number), + "total_tax": expect.any(String), + "total_shipping": expect.any(String), + "total_refunds": expect.any(Number), + "total_discount": expect.any(String), + "totals_grouped_by": "day", + "totals": expect.objectContaining({ + [dateString]: { + "sales": expect.any(String), + "orders": expect.any(Number), + "items": expect.any(Number), + "tax": expect.any(String), + "shipping": expect.any(String), + "discount": expect.any(String), + "customers": expect.any(Number) + } + }), + "total_customers": expect.any(Number), + }) + ])); + }); + + test('can view top sellers reports', async ({ + request + }) => { + // call API to retrieve the top sellers + const response = await request.get('/wp-json/wc/v3/reports/top_sellers'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([])); + }); + + test('can view coupons totals', async ({ + request + }) => { + // call API to retrieve the coupons totals + const response = await request.get('/wp-json/wc/v3/reports/coupons/totals'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "percent", + "name": "Percentage discount", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "fixed_cart", + "name": "Fixed cart discount", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "fixed_product", + "name": "Fixed product discount", + "total": expect.any(Number) + }) + ])); + }); + + test('can view customers totals', async ({ + request + }) => { + // call API to retrieve the customers totals + const response = await request.get('/wp-json/wc/v3/reports/customers/totals'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "paying", + "name": "Paying customer", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "non_paying", + "name": "Non-paying customer", + "total": expect.any(Number) + }) + ])); + }); + + test('can view orders totals', async ({ + request + }) => { + // call API to retrieve the orders totals + const response = await request.get('/wp-json/wc/v3/reports/orders/totals'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "pending", + "name": "Pending payment", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "processing", + "name": "Processing", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "on-hold", + "name": "On hold", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "completed", + "name": "Completed", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "cancelled", + "name": "Cancelled", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "refunded", + "name": "Refunded", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "failed", + "name": "Failed", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "checkout-draft", + "name": "Draft", + "total": expect.any(Number) + }) + ])); + }); + + test('can view products totals', async ({ + request + }) => { + // call API to retrieve the products totals + const response = await request.get('/wp-json/wc/v3/reports/products/totals'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "external", + "name": "External/Affiliate product", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "grouped", + "name": "Grouped product", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "simple", + "name": "Simple product", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "variable", + "name": "Variable product", + "total": expect.any(Number) + }) + ])); + }); + + test('can view reviews totals', async ({ + request + }) => { + // call API to retrieve the reviews totals + const response = await request.get('/wp-json/wc/v3/reports/reviews/totals'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "rated_1_out_of_5", + "name": "Rated 1 out of 5", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "rated_2_out_of_5", + "name": "Rated 2 out of 5", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "rated_3_out_of_5", + "name": "Rated 3 out of 5", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "rated_4_out_of_5", + "name": "Rated 4 out of 5", + "total": expect.any(Number) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "slug": "rated_5_out_of_5", + "name": "Rated 5 out of 5", + "total": expect.any(Number) + }) + ])); + }); +}); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js new file mode 100644 index 00000000000..78853b7e380 --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js @@ -0,0 +1,2574 @@ +const { + test, + expect +} = require('@playwright/test'); +const { + countries, + currencies, + stateOptions +} = require('../../data/settings'); + +/** + * Tests for the WooCommerce API. + * + * @group api + * @group settings + * + */ +test.describe('Settings API tests: CRUD', () => { + + test.describe('List all settings groups', () => { + + test('can retrieve all settings groups', async ({ + request + }) => { + // call API to retrieve all settings groups + const response = await request.get('/wp-json/wc/v3/settings'); + 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: "wc_admin", + label: "WooCommerce Admin", + description: "Settings for WooCommerce admin reporting.", + parent_id: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "general", + label: "General", + description: "", + parent_id: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "products", + label: "Products", + description: "", + parent_id: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "tax", + label: "Tax", + description: "", + parent_id: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "shipping", + label: "Shipping", + description: "", + parent_id: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "checkout", + label: "Payments", + description: "", + parent_id: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "account", + label: "Accounts & Privacy", + description: "", + parent_id: "", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email", + label: "Emails", + description: "", + parent_id: "", + "sub_groups": expect.arrayContaining(["email_new_order"]), + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "integration", + label: "Integration", + description: "", + parent_id: "", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "advanced", + label: "Advanced", + description: "", + parent_id: "", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_new_order", + label: "New order", + description: "New order emails are sent to chosen recipient(s) when a new order is received.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_cancelled_order", + label: "Cancelled order", + description: "Cancelled order emails are sent to chosen recipient(s) when orders have been marked cancelled (if they were previously processing or on-hold).", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_failed_order", + label: "Failed order", + description: "Failed order emails are sent to chosen recipient(s) when orders have been marked failed (if they were previously pending or on-hold).", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_on_hold_order", + label: "Order on-hold", + description: "This is an order notification sent to customers containing order details after an order is placed on-hold from Pending, Cancelled or Failed order status.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_processing_order", + label: "Processing order", + description: "This is an order notification sent to customers containing order details after payment.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_completed_order", + label: "Completed order", + description: "Order complete emails are sent to customers when their orders are marked completed and usually indicate that their orders have been shipped.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_refunded_order", + label: "Refunded order", + description: "Order refunded emails are sent to customers when their orders are refunded.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_invoice", + label: "Customer invoice / Order details", + description: "Customer invoice emails can be sent to customers containing their order information and payment links.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_note", + label: "Customer note", + description: "Customer note emails are sent when you add a note to an order.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_reset_password", + label: "Reset password", + description: "Customer \"reset password\" emails are sent when customers reset their passwords.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "email_customer_new_account", + label: "New account", + description: "Customer \"new account\" emails are sent to the customer when a customer signs up via checkout or account pages.", + parent_id: "email", + "sub_groups": expect.arrayContaining([]), + }) + ])); + }); + }); + + + test.describe('List all settings options', () => { + + test.fixme('can retrieve all general settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/general'); + 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_store_address", + label: "Address line 1", + description: "The street address for your business location.", + type: "text", + default: "", + tip: "The street address for your business location.", + value: "" + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_store_address_2", + label: "Address line 2", + description: "An additional, optional address line for your business location.", + type: "text", + default: "", + tip: "An additional, optional address line for your business location.", + value: "" + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_store_city", + label: "City", + description: "The city in which your business is located.", + type: "text", + default: "", + tip: "The city in which your business is located.", + value: "" + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_default_country", + label: "Country / State", + description: "The country and state or province, if any, in which your business is located.", + type: "select", + default: "US:CA", + tip: "The country and state or province, if any, in which your business is located.", + value: "US:CA", + options: expect.objectContaining(stateOptions) + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_store_postcode", + label: "Postcode / ZIP", + description: "The postal code, if any, in which your business is located.", + type: "text", + default: "", + tip: "The postal code, if any, in which your business is located.", + value: "", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_allowed_countries", + label: "Selling location(s)", + description: "This option lets you limit which countries you are willing to sell to.", + type: "select", + default: "all", + tip: "This option lets you limit which countries you are willing to sell to.", + value: "all", + options: { + "all": "Sell to all countries", + "all_except": "Sell to all countries, except for…", + "specific": "Sell to specific countries" + }, + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_all_except_countries", + label: "Sell to all countries, except for…", + description: "", + type: "multiselect", + default: "", + value: "", + options: expect.objectContaining(countries), + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_specific_allowed_countries", + label: "Sell to specific countries", + description: "", + type: "multiselect", + default: "", + value: "", + options: expect.objectContaining(countries) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_ship_to_countries", + label: "Shipping location(s)", + description: "Choose which countries you want to ship to, or choose to ship to all locations you sell to.", + type: "select", + default: "", + tip: "Choose which countries you want to ship to, or choose to ship to all locations you sell to.", + value: "", + options: expect.objectContaining({ + "": "Ship to all countries you sell to", + "all": "Ship to all countries", + "specific": "Ship to specific countries only", + "disabled": "Disable shipping & shipping calculations" + }) + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_specific_ship_to_countries", + label: "Ship to specific countries", + description: "", + type: "multiselect", + default: "", + value: "", + options: expect.objectContaining(countries) + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_default_customer_address", + label: "Default customer location", + description: "", + type: "select", + default: "base", + tip: "This option determines a customers default location. The MaxMind GeoLite Database will be periodically downloaded to your wp-content directory if using geolocation.", + value: "base", + options: expect.objectContaining({ + "": "No location by default", + "base": "Shop country/region", + "geolocation": "Geolocate", + "geolocation_ajax": "Geolocate (with page caching support)" + }) + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_calc_taxes", + label: "Enable taxes", + description: "Enable tax rates and calculations", + type: "checkbox", + default: "no", + tip: "Rates will be configurable and taxes will be calculated during checkout.", + value: expect.any(String), + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_enable_coupons", + label: "Enable coupons", + description: "Enable the use of coupon codes", + type: "checkbox", + default: "yes", + tip: "Coupons can be applied from the cart and checkout pages.", + value: "yes", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_calc_discounts_sequentially", + label: "", + description: "Calculate coupon discounts sequentially", + type: "checkbox", + default: "no", + tip: "When applying multiple coupons, apply the first coupon to the full price and the second coupon to the discounted price and so on.", + value: "no", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_currency", + label: "Currency", + description: "This controls what currency prices are listed at in the catalog and which currency gateways will take payments in.", + type: "select", + default: "USD", + options: expect.objectContaining(currencies), + tip: "This controls what currency prices are listed at in the catalog and which currency gateways will take payments in.", + value: "USD", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_currency_pos", + label: "Currency position", + description: "This controls the position of the currency symbol.", + type: "select", + default: "left", + options: { + "left": "Left", + "right": "Right", + "left_space": "Left with space", + "right_space": "Right with space" + }, + tip: "This controls the position of the currency symbol.", + value: "left", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_price_thousand_sep", + label: "Thousand separator", + description: "This sets the thousand separator of displayed prices.", + type: "text", + default: ",", + tip: "This sets the thousand separator of displayed prices.", + value: ",", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "woocommerce_price_decimal_sep", + label: "Decimal separator", + description: "This sets the decimal separator of displayed prices.", + type: "text", + default: ".", + tip: "This sets the decimal separator of displayed prices.", + value: ".", + }) + ])); + }); + }); + + test.describe('Retrieve a settings option', () => { + + test('can retrieve a settings option', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/general/woocommerce_allowed_countries'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON).toEqual( + expect.objectContaining({ + "id": "woocommerce_allowed_countries", + "label": "Selling location(s)", + "description": "This option lets you limit which countries you are willing to sell to.", + "type": "select", + "default": "all", + "options": { + "all": "Sell to all countries", + "all_except": "Sell to all countries, except for…", + "specific": "Sell to specific countries" + }, + "tip": "This option lets you limit which countries you are willing to sell to.", + "value": "all", + "group_id": "general", + }) + ); + + }); + + }); + + test.describe('Update a settings option', () => { + + test('can update a settings option', async ({ + request + }) => { + // call API to update settings options + const response = await request.put('/wp-json/wc/v3/settings/general/woocommerce_allowed_countries', { + data: { + value: "all_except" + } + }); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON).toEqual( + expect.objectContaining({ + "id": "woocommerce_allowed_countries", + "label": "Selling location(s)", + "description": "This option lets you limit which countries you are willing to sell to.", + "type": "select", + "default": "all", + "options": { + "all": "Sell to all countries", + "all_except": "Sell to all countries, except for…", + "specific": "Sell to specific countries" + }, + "tip": "This option lets you limit which countries you are willing to sell to.", + "value": "all_except", + "group_id": "general", + }) + ); + + }); + + }); + + test.describe('Batch Update a settings option', () => { + + test('can batch update settings options', async ({ + request + }) => { + + // call API to update settings options + const response = await request.post('/wp-json/wc/v3/settings/general/batch', { + data: { + update: [{ + id: "woocommerce_allowed_countries", + value: "all_except" + }, + { + id: "woocommerce_currency", + value: "GBP" + } + ] + } + }); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + // retrieve the updated settings values + const countriesUpdatedResponse = await request.get('/wp-json/wc/v3/settings/general/woocommerce_allowed_countries'); + const countriesUpdatedResponseJSON = await countriesUpdatedResponse.json(); + expect(countriesUpdatedResponseJSON.value).toEqual('all_except'); + + const currencyUpdatedResponse = await request.get('/wp-json/wc/v3/settings/general/woocommerce_currency'); + const currencyUpdatedResponseJSON = await currencyUpdatedResponse.json(); + expect(currencyUpdatedResponseJSON.value).toEqual('GBP'); + + // call API to restore the settings options + await request.put('/wp-json/wc/v3/settings/general/batch', { + data: { + update: [{ + id: "woocommerce_allowed_countries", + value: "all" + }, + { + id: "woocommerce_currency", + value: "USD" + } + ] + } + }); + }); + + }); + + test.describe('List all Products settings options', () => { + + test('can retrieve all products settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/products'); + 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_shop_page_id", + "label": "Shop page", + "type": "select", + "default": "", + "tip": "This sets the base page of your shop - this is where your product archive will be.", + "value": "5", + "options": { + "2": "Sample Page", + "5": "Shop", + "6": "Cart", + "7": "Checkout", + "8": "My account" + }, + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_cart_redirect_after_add", + "label": "Add to cart behaviour", + "description": "Redirect to the cart page after successful addition", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_ajax_add_to_cart", + "label": "", + "description": "Enable AJAX add to cart buttons on archives", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_placeholder_image", + "label": "Placeholder image", + "description": "", + "type": "text", + "default": "", + "tip": "This is the attachment ID, or image URL, used for placeholder images in the product catalog. Products with no image will use this.", + "value": "4", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_weight_unit", + "label": "Weight unit", + "description": "This controls what unit you will define weights in.", + "type": "select", + "default": "kg", + "options": { + "kg": "kg", + "g": "g", + "lbs": "lbs", + "oz": "oz" + }, + "tip": "This controls what unit you will define weights in.", + "value": "kg", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_dimension_unit", + "label": "Dimensions unit", + "description": "This controls what unit you will define lengths in.", + "type": "select", + "default": "cm", + "options": { + "m": "m", + "cm": "cm", + "mm": "mm", + "in": "in", + "yd": "yd" + }, + "tip": "This controls what unit you will define lengths in.", + "value": "cm", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_reviews", + "label": "Enable reviews", + "description": "Enable product reviews", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_review_rating_verification_label", + "label": "", + "description": "Show \"verified owner\" label on customer reviews", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_review_rating_verification_required", + "label": "", + "description": "Reviews can only be left by \"verified owners\"", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_review_rating", + "label": "Product ratings", + "description": "Enable star rating on reviews", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_review_rating_required", + "label": "", + "description": "Star ratings should be required, not optional", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_manage_stock", + "label": "Manage stock", + "description": "Enable stock management", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_hold_stock_minutes", + "label": "Hold stock (minutes)", + "description": "Hold stock (for unpaid orders) for x minutes. When this limit is reached, the pending order will be cancelled. Leave blank to disable.", + "type": "number", + "default": "60", + "value": "60", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_notify_low_stock", + "label": "Notifications", + "description": "Enable low stock notifications", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_notify_no_stock", + "label": "", + "description": "Enable out of stock notifications", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_stock_email_recipient", + "label": "Notification recipient(s)", + "description": "Enter recipients (comma separated) that will receive this notification.", + "type": "text", + "default": "wordpress@example.com", + "tip": "Enter recipients (comma separated) that will receive this notification.", + "value": "wordpress@example.com", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_notify_low_stock_amount", + "label": "Low stock threshold", + "description": "When product stock reaches this amount you will be notified via email.", + "type": "number", + "default": "2", + "tip": "When product stock reaches this amount you will be notified via email.", + "value": "2", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_notify_no_stock_amount", + "label": "Out of stock threshold", + "description": "When product stock reaches this amount the stock status will change to \"out of stock\" and you will be notified via email. This setting does not affect existing \"in stock\" products.", + "type": "number", + "default": "0", + "tip": "When product stock reaches this amount the stock status will change to \"out of stock\" and you will be notified via email. This setting does not affect existing \"in stock\" products.", + "value": "0", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_hide_out_of_stock_items", + "label": "Out of stock visibility", + "description": "Hide out of stock items from the catalog", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_stock_format", + "label": "Stock display format", + "description": "This controls how stock quantities are displayed on the frontend.", + "type": "select", + "default": "", + "options": { + "": "Always show quantity remaining in stock e.g. \"12 in stock\"", + "low_amount": "Only show quantity remaining in stock when low e.g. \"Only 2 left in stock\"", + "no_amount": "Never show quantity remaining in stock" + }, + "tip": "This controls how stock quantities are displayed on the frontend.", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_file_download_method", + "label": "File download method", + "description": "If you are using X-Accel-Redirect download method along with NGINX server, make sure that you have applied settings as described in Digital/Downloadable Product Handling guide.", + "type": "select", + "default": "force", + "options": { + "force": "Force downloads", + "xsendfile": "X-Accel-Redirect/X-Sendfile", + "redirect": "Redirect only (Insecure)" + }, + "tip": "Forcing downloads will keep URLs hidden, but some servers may serve large files unreliably. If supported, X-Accel-Redirect / X-Sendfile can be used to serve downloads instead (server requires mod_xsendfile).", + "value": "force", + }) + ])); + + + }); + }); + + 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", + }) + ])); + + 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', () => { + + test('can retrieve all shipping settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/shipping'); + 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_enable_shipping_calc", + "label": "Calculations", + "description": "Enable the shipping calculator on the cart page", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_shipping_cost_requires_address", + "label": "", + "description": "Hide shipping costs until an address is entered", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_ship_to_destination", + "label": "Shipping destination", + "description": "This controls which shipping address is used by default.", + "type": "radio", + "default": "billing", + "options": { + "shipping": "Default to customer shipping address", + "billing": "Default to customer billing address", + "billing_only": "Force shipping to the customer billing address" + }, + "tip": "This controls which shipping address is used by default.", + "value": "billing", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_shipping_debug_mode", + "label": "Debug mode", + "description": "Enable debug mode", + "type": "checkbox", + "default": "no", + "tip": "Enable shipping debug mode to show matching shipping zones and to bypass shipping rate cache.", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_shipping_calc", + "label": "Calculations", + "description": "Enable the shipping calculator on the cart page", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_shipping_cost_requires_address", + "label": "", + "description": "Hide shipping costs until an address is entered", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_ship_to_destination", + "label": "Shipping destination", + "description": "This controls which shipping address is used by default.", + "type": "radio", + "default": "billing", + "options": { + "shipping": "Default to customer shipping address", + "billing": "Default to customer billing address", + "billing_only": "Force shipping to the customer billing address" + }, + "tip": "This controls which shipping address is used by default.", + "value": "billing", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_shipping_debug_mode", + "label": "Debug mode", + "description": "Enable debug mode", + "type": "checkbox", + "default": "no", + "tip": "Enable shipping debug mode to show matching shipping zones and to bypass shipping rate cache.", + "value": "no", + }) + ])); + + }); + }); + + test.describe('List all Checkout settings options', () => { + + test('can retrieve all checkout settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/checkout'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([])); + }); + }); + + test.describe('List all Account settings options', () => { + + test('can retrieve all account settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/account'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_guest_checkout", + "label": "Guest checkout", + "description": "Allow customers to place orders without an account", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_checkout_login_reminder", + "label": "Login", + "description": "Allow customers to log into an existing account during checkout", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_signup_and_login_from_checkout", + "label": "Account creation", + "description": "Allow customers to create an account during checkout", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_enable_myaccount_registration", + "label": "", + "description": "Allow customers to create an account on the \"My account\" page", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_registration_generate_username", + "label": "", + "description": "When creating an account, automatically generate an account username for the customer based on their name, surname or email", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_registration_generate_password", + "label": "", + "description": "When creating an account, send the new user a link to set their password", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_erasure_request_removes_order_data", + "label": "Account erasure requests", + "description": "Remove personal data from orders on request", + "type": "checkbox", + "default": "no", + "tip": expect.stringContaining('When handling an account erasure request, should personal data within orders be retained or removed?'), + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_erasure_request_removes_download_data", + "label": "", + "description": "Remove access to downloads on request", + "type": "checkbox", + "default": "no", + "tip": expect.stringContaining('When handling an account erasure request, should access to downloadable files be revoked and download logs cleared?'), + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_allow_bulk_remove_personal_data", + "label": "Personal data removal", + "description": "Allow personal data to be removed in bulk from orders", + "type": "checkbox", + "default": "no", + "tip": "Adds an option to the orders screen for removing personal data in bulk. Note that removing personal data cannot be undone.", + "value": "no", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_registration_privacy_policy_text", + "label": "Registration privacy policy", + "description": "", + "type": "textarea", + "default": "Your personal data will be used to support your experience throughout this website, to manage access to your account, and for other purposes described in our [privacy_policy].", + "tip": "Optionally add some text about your store privacy policy to show on account registration forms.", + "value": "Your personal data will be used to support your experience throughout this website, to manage access to your account, and for other purposes described in our [privacy_policy].", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_checkout_privacy_policy_text", + "label": "Checkout privacy policy", + "description": "", + "type": "textarea", + "default": "Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our [privacy_policy].", + "tip": "Optionally add some text about your store privacy policy to show during checkout.", + "value": "Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our [privacy_policy].", + }) + ])); + + }); + }); + + test.describe('List all Email settings options', () => { + + test('can retrieve all email settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_from_name", + "label": "\"From\" name", + "description": "How the sender name appears in outgoing WooCommerce emails.", + "type": "text", + "default": "WooCommerce Core E2E Test Suite", + "tip": "How the sender name appears in outgoing WooCommerce emails.", + "value": "woocommerce", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_from_address", + "label": "\"From\" address", + "description": "How the sender email appears in outgoing WooCommerce emails.", + "type": "email", + "default": "wordpress@example.com", + "tip": "How the sender email appears in outgoing WooCommerce emails.", + "value": "wordpress@example.com", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_header_image", + "label": "Header image", + "description": "URL to an image you want to show in the email header. Upload images using the media uploader (Admin > Media).", + "type": "text", + "default": "", + "tip": "URL to an image you want to show in the email header. Upload images using the media uploader (Admin > Media).", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_footer_text", + "label": "Footer text", + "description": "The text to appear in the footer of all WooCommerce emails. Available placeholders: {site_title} {site_url}", + "type": "textarea", + "default": "{site_title} — Built with {WooCommerce}", + "tip": "The text to appear in the footer of all WooCommerce emails. Available placeholders: {site_title} {site_url}", + "value": "{site_title} — Built with {WooCommerce}", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_base_color", + "label": "Base color", + "description": "The base color for WooCommerce email templates. Default #7f54b3.", + "type": "color", + "default": "#7f54b3", + "tip": "The base color for WooCommerce email templates. Default #7f54b3.", + "value": "#7f54b3", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_background_color", + "label": "Background color", + "description": "The background color for WooCommerce email templates. Default #f7f7f7.", + "type": "color", + "default": "#f7f7f7", + "tip": "The background color for WooCommerce email templates. Default #f7f7f7.", + "value": "#f7f7f7", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_body_background_color", + "label": "Body background color", + "description": "The main body background color. Default #ffffff.", + "type": "color", + "default": "#ffffff", + "tip": "The main body background color. Default #ffffff.", + "value": "#ffffff", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_email_text_color", + "label": "Body text color", + "description": "The main body text color. Default #3c3c3c.", + "type": "color", + "default": "#3c3c3c", + "tip": "The main body text color. Default #3c3c3c.", + "value": "#3c3c3c", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_merchant_email_notifications", + "label": "Enable email insights", + "description": "Receive email notifications with additional guidance to complete the basic store setup and helpful insights", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + + + }); + }); + + 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); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_cart_page_id", + "label": "Cart page", + "description": "Page contents: [woocommerce_cart]", + "type": "select", + "default": "", + "tip": "Page contents: [woocommerce_cart]", + "value": "6", + "options": { + "2": "Sample Page", + "5": "Shop", + "6": "Cart", + "7": "Checkout", + "8": "My account" + }, + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_checkout_page_id", + "label": "Checkout page", + "description": "Page contents: [woocommerce_checkout]", + "type": "select", + "default": 7, + "tip": "Page contents: [woocommerce_checkout]", + "value": "7", + "options": { + "2": "Sample Page", + "5": "Shop", + "6": "Cart", + "7": "Checkout", + "8": "My account" + } + }) + ])); + 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": "8", + "options": { + "2": "Sample Page", + "5": "Shop", + "6": "Cart", + "7": "Checkout", + "8": "My account" + }, + }) + ])); + 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", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_api_enabled", + "label": "Legacy API", + "description": "Enable the legacy REST API", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + 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", + }) + ])); + 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 see suggested extensions.", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_analytics_enabled", + "label": "Analytics", + "description": "Enables WooCommerce Analytics", + "type": "checkbox", + "default": "yes", + "value": "yes", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_multichannel_marketing_enabled", + "label": "Marketing", + "description": "Enables the new WooCommerce Multichannel Marketing experience in the Marketing page", + "type": "checkbox", + }) + ])); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "woocommerce_navigation_enabled", + "label": "Navigation", + "description": "Adds the new WooCommerce navigation experience to the dashboard", + "type": "checkbox", + "default": "no", + "value": "no", + }) + ])); + + }); + }); + + + test.describe('List all Email New Order settings', () => { + + test('can retrieve all email new order settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_new_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": "Enter recipients (comma separated) for this email. Defaults to wordpress@example.com.", + "type": "text", + "default": "", + "tip": "Enter recipients (comma separated) for this email. Defaults to wordpress@example.com.", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "subject", + "label": "Subject", + "description": "Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}", + "value": "", + }) + ])); + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "heading", + "label": "Email heading", + "description": "Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {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}, {site_address}, {site_url}, {order_date}, {order_number}", + "type": "textarea", + "default": "Congratulations on the sale.", + "tip": "Text to appear below the main email content. Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}", + "value": "Congratulations on the sale.", + }) + ])); + 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 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": "Enter recipients (comma separated) for this email. Defaults to wordpress@example.com.", + "type": "text", + "default": "", + "tip": "Enter recipients (comma separated) for this email. Defaults to wordpress@example.com.", + "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', () => { + + test('can retrieve all email customer on hold order settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_on_hold_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": "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": "We look forward to fulfilling your order soon.", + "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": "We look forward to fulfilling your order soon.", + }) + ])); + 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 Processing Order settings', () => { + + test('can retrieve all email customer processsing order settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_processing_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": "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": "Thanks for using {site_url}!", + "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": "Thanks for using {site_url}!", + }) + ])); + 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 Completed Order settings', () => { + + test('can retrieve all email customer completed order settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_completed_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": "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": "Thanks for shopping with us.", + "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": "Thanks for shopping with us.", + }) + ])); + 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 Refunded Order settings', () => { + + test('can retrieve all email customer refunded order settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_refunded_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": "subject_full", + "label": "Full refund 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": "subject_partial", + "label": "Partial refund 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_full", + "label": "Full refund 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": "heading_partial", + "label": "Partial refund 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": "We hope to see you again soon.", + "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": "We hope to see you again soon.", + }) + ])); + + 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 Invoice settings', () => { + + test('can retrieve all email customer invoice settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_invoice'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + 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": "subject_paid", + "label": "Subject (paid)", + "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_paid", + "label": "Email heading (paid)", + "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": "Thanks for using {site_url}!", + "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": "Thanks for using {site_url}!", + }) + ])); + 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 Note settings', () => { + + test('can retrieve all email customer note settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_note'); + 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": "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": "Thanks for reading.", + "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": "Thanks for reading.", + }) + ])); + 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 Reset Password settings', () => { + + test('can retrieve all email customer reset password settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_reset_password'); + 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": "subject", + "label": "Subject", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "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}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "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}", + "type": "textarea", + "default": "Thanks for reading.", + "tip": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "value": "Thanks for reading.", + }) + ])); + 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 New Account settings', () => { + + test('can retrieve all email customer new account settings', async ({ + request + }) => { + // call API to retrieve all settings options + const response = await request.get('/wp-json/wc/v3/settings/email_customer_new_account'); + 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": "subject", + "label": "Subject", + "description": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "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}", + "type": "text", + "default": "", + "tip": "Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "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}", + "type": "textarea", + "default": "We look forward to seeing you soon.", + "tip": "Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}", + "value": "We look forward to seeing you soon.", + }) + ])); + 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", + }) + ])); + + }); + }); + +}); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js index d3c038b9ca8..51f52e70144 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js @@ -1,7 +1,12 @@ /* eslint-disable */ -const { test, expect } = require('@playwright/test'); +const { + test, + expect +} = require('@playwright/test'); -const { getShippingMethodExample } = require('../../data'); +const { + getShippingMethodExample +} = require('../../data'); /** * Shipping zone id for "Locations not covered by your other zones". @@ -30,11 +35,107 @@ const methodCostIndex = 2; */ test.describe('Shipping methods API tests', () => { + test('cannot create a shipping method', async ({ + request, + }) => { + /** + * call API to attempt to create a shipping method + * This call will not work as we have no ability to create new shipping methods, + * only retrieve the existing shipping methods + * i.e. Flat rate, Free shipping and Local pickup + */ + const response = await request.post( + '/wp-json/wc/v3/shipping_methods', { + data: { + title: "flat_rate", + description: "Lets you charge a fixed rate for shipping.", + }, + } + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(404); + expect(responseJSON.code).toEqual('rest_no_route'); + expect(responseJSON.message).toEqual('No route was found matching the URL and request method.'); + }); + + test('can retrieve all shipping methods', async ({ + request + }) => { + // call API to retrieve all shipping methods + const response = await request.get('/wp-json/wc/v3/shipping_methods'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(true); + expect(responseJSON.length).toEqual(3); + expect(responseJSON[0].id).toEqual("flat_rate"); + expect(responseJSON[1].id).toEqual("free_shipping"); + expect(responseJSON[2].id).toEqual("local_pickup"); + }); + + test('can retrieve a shipping method', async ({ + request + }) => { + // call API to retrieve a shipping method + const response = await request.get( + `/wp-json/wc/v3/shipping_methods/local_pickup` + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + expect(typeof responseJSON.id).toEqual('string'); + }); + + test(`cannot update a shipping method`, async ({ + request, + }) => { + /** + * call API to attempt to update a shipping method + * This call will not work as we have no ability to update new shipping methods, + * only retrieve the existing shipping methods + * i.e. Flat rate, Free shipping and Local pickup + */ + const response = await request.put( + '/wp-json/wc/v3/shipping_methods/local_pickup', { + data: { + description: "update local pickup description" + } + } + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(404); + expect(responseJSON.code).toEqual('rest_no_route'); + expect(responseJSON.message).toEqual('No route was found matching the URL and request method.'); + }); + + + test('cannot delete a shipping method', async ({ + request + }) => { + /** + * call API to attempt to delete a shipping method + * This call will not work as we have no ability to delete shipping methods, + * only retrieve the existing shipping methods + * i.e. Flat rate, Free shipping and Local pickup + */ + const response = await request.delete('/wp-json/wc/v3/shipping_methods', { + data: { + force: true + } + }); + const responseJSON = await response.json(); + expect(response.status()).toEqual(404); + expect(responseJSON.code).toEqual('rest_no_route'); + expect(responseJSON.message).toEqual('No route was found matching the URL and request method.'); + }); + + //loop through each row from the shippingMethods test data table above for (const shippingMethodRow of shippingMethods) { test(`can add a ${shippingMethodRow[methodTitleIndex]} shipping method`, - async ({ request }) => { + async ({ + request + }) => { //create the shipping method const shippingMethod = getShippingMethodExample(shippingMethodRow[methodIdIndex], shippingMethodRow[methodCostIndex]); @@ -54,12 +155,12 @@ test.describe('Shipping methods API tests', () => { const shippingMethodInstanceId = responseJSON.id; - //if the shipping method is flat_rate OR local_pickup then based on the data, it should have a cost value + // if the shipping method is flat_rate OR local_pickup then based on the data, it should have a cost value if (['flat_rate', 'local_pickup'].includes(shippingMethodRow[methodIdIndex])) { expect(responseJSON.settings.cost.value).toEqual(shippingMethodRow[methodCostIndex]); } - // Cleanup: Delete the shipping method + // Cleanup: Remove the shipping method from the shipping zone const deleteResponse = await request.delete(`/wp-json/wc/v3/shipping/zones/${ shippingZoneId }/methods/${ shippingMethodInstanceId }`, { data: { force: true diff --git a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js index 1316257c6f1..ef948dea1c4 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js @@ -121,7 +121,6 @@ test.describe( 'Shipping zones API tests', () => { //call API to retrive the locations of the last created shipping zone const response = await request.get( `/wp-json/wc/v3/shipping/zones/${shippingZone.id}/locations`); - const responseJSON = await response.json(); expect( response.status() ).toEqual( 200 ); //no locations exist initially @@ -142,11 +141,6 @@ test.describe( 'Shipping zones API tests', () => { test( 'can update a shipping region on a shipping zone', async ({request}) => { - //call API to retrive the locations of the last created shipping zone - const response = await request.get( `/wp-json/wc/v3/shipping/zones/${shippingZone.id}/locations`); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - //GB and US locations exist initially //update the locations of the shipping zone regions to contain an individual state const putResponseStateOnly = await request.put( `/wp-json/wc/v3/shipping/zones/${shippingZone.id}/locations`,{ @@ -166,11 +160,6 @@ test.describe( 'Shipping zones API tests', () => { test( 'can clear/delete a shipping region on a shipping zone', async ({request}) => { - //call API to retrive the locations of the last created shipping zone - const response = await request.get( `/wp-json/wc/v3/shipping/zones/${shippingZone.id}/locations`); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - //GB and US locations exist initially //update the locations of the shipping zone regions to contain an individual state const putResponseStateOnly = await request.put( `/wp-json/wc/v3/shipping/zones/${shippingZone.id}/locations`,{ diff --git a/plugins/woocommerce/tests/api-core-tests/tests/system-status/system-status-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/system-status/system-status-crud.test.js new file mode 100644 index 00000000000..3d28851df59 --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/tests/system-status/system-status-crud.test.js @@ -0,0 +1,623 @@ +const { + test, + expect +} = require('@playwright/test'); +const { + refund +} = require('../../data'); + +/** + * Tests for the WooCommerce Refunds API. + * + * @group api + * @group system status + * + */ +test.describe('System Status API tests', () => { + + test('can view all system status items', async ({ + request + }) => { + // call API to create a refund + const response = await request.get('/wp-json/wc/v3/system_status'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + 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), + }) + }) + ); + 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": { + "woocommerce": { + "wp_woocommerce_sessions": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_api_keys": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_attribute_taxonomies": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_downloadable_product_permissions": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_order_items": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_order_itemmeta": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_tax_rates": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_tax_rate_locations": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_shipping_zones": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_shipping_zone_locations": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_shipping_zone_methods": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_payment_tokens": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_payment_tokenmeta": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + }, + "wp_woocommerce_log": { + "data": expect.any(String), + "index": expect.any(String), + "engine": expect.any(String), + } + }, + "other": { + "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), + }, + "wp_wpml_mails": { + "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) + } + ]) + }) + ); + expect(responseJSON).toEqual( + expect.objectContaining({ + inactive_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) + } + ]) + }) + ); + expect(responseJSON).toEqual( + expect.objectContaining({ + dropins_mu_plugins: expect.objectContaining({ + "dropins": [], + "mu_plugins": [] + }) + }) + ); + 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": [], + "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 create a refund + const response = await request.get('/wp-json/wc/v3/system_status/tools'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + expect(responseJSON).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + "id": "clear_transients", + "name": "WooCommerce transients", + "action": "Clear transients", + "description": "This tool will clear the product/shop transients cache.", + }), + expect.objectContaining({ + "id": "clear_expired_transients", + "name": "Expired transients", + "action": "Clear transients", + "description": "This tool will clear ALL expired transients from WordPress.", + }), + expect.objectContaining({ + "id": "clear_expired_download_permissions", + "name": "Used-up download permissions", + "action": "Clean up download permissions", + "description": "This tool will delete expired download permissions and permissions with 0 remaining downloads.", + }), + expect.objectContaining({ + "id": "regenerate_product_lookup_tables", + "name": "Product lookup tables", + "action": "Regenerate", + "description": "This tool will regenerate product lookup table data. This process may take a while.", + }), + ]) + ); + + }); + + test('can retrieve a system status tool', async ({ + request + }) => { + // call API to create a refund + const response = await request.get('/wp-json/wc/v3/system_status/tools/clear_transients'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + expect(responseJSON).toEqual( + expect.objectContaining({ + "id": "clear_transients", + "name": "WooCommerce transients", + "action": "Clear transients", + "description": "This tool will clear the product/shop transients cache.", + }), + ); + }); + + test('can run a tool from system status', async ({ + request + }) => { + // call API to create a refund + const response = await request.put('/wp-json/wc/v3/system_status/tools/clear_transients'); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + expect(responseJSON).toEqual( + expect.objectContaining({ + "id": "clear_transients", + "name": "WooCommerce transients", + "action": "Clear transients", + "description": "This tool will clear the product/shop transients cache.", + "success": true, + "message": "Product transients cleared", + }), + ); + }); + + +}); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-classes-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-classes-crud.test.js index 4396bf9a631..353a3dada79 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-classes-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-classes-crud.test.js @@ -74,7 +74,7 @@ test.describe('Tax Classes API tests: CRUD', () => { const response = await request.get('/wp-json/wc/v3/taxes/classes'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toBeGreaterThan(0); }); }); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-rates-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-rates-crud.test.js index 1d0a636a242..fcc339ba3e2 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-rates-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-rates-crud.test.js @@ -89,7 +89,7 @@ test.describe('Tax Rates API tests: CRUD', () => { const response = await request.get('/wp-json/wc/v3/taxes'); const responseJSON = await response.json(); expect(response.status()).toEqual(200); - expect(Array.isArray(responseJSON)); + expect(Array.isArray(responseJSON)).toBe(true); expect(responseJSON.length).toBeGreaterThan(0); }); }); diff --git a/plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js new file mode 100644 index 00000000000..12993f5d539 --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js @@ -0,0 +1,254 @@ +const { + test, + expect +} = require('@playwright/test'); +const exp = require('constants'); +const { + refund +} = require('../../data'); + +/** + * Tests for the WooCommerce Refunds API. + * + * @group api + * @group webhooks + * + */ +test.describe('Webhooks API tests', () => { + let webhookId; + + test.describe('Create a webhook', () => { + + test('can create a webhook', async ({ + request, + }) => { + // call API to create a webhook + const response = await request.post( + '/wp-json/wc/v3/webhooks', { + data: { + name: "Order updated", + topic: "order.updated", + delivery_url: "http://requestb.in/1g0sxmo1" + }, + } + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(201); + expect(typeof responseJSON.id).toEqual('number'); + expect(responseJSON.name).toEqual("Order updated"); + expect(responseJSON.status).toEqual("active"); + expect(responseJSON.topic).toEqual("order.updated"); + expect(responseJSON.delivery_url).toEqual("http://requestb.in/1g0sxmo1"); + expect(responseJSON.hooks).toEqual( + expect.arrayContaining([ + "woocommerce_update_order", + "woocommerce_order_refunded" + ]) + ); + + webhookId = responseJSON.id; + }); + + }); + + test.describe('Retrieve after create', () => { + test('can retrieve a webhook', async ({ + request + }) => { + // call API to retrieve the previously saved webhook + const response = await request.get( + `/wp-json/wc/v3/webhooks/${webhookId}` + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(Array.isArray(responseJSON)).toBe(false); + expect(typeof responseJSON.id).toEqual('number'); + expect(responseJSON.name).toEqual("Order updated"); + expect(responseJSON.status).toEqual("active"); + expect(responseJSON.topic).toEqual("order.updated"); + expect(responseJSON.delivery_url).toEqual("http://requestb.in/1g0sxmo1"); + expect(responseJSON.hooks).toEqual( + expect.arrayContaining([ + "woocommerce_update_order", + "woocommerce_order_refunded" + ]) + ); + }); + + test('can retrieve all webhooks', async ({ + request + }) => { + // call API to retrieve all webhooks + const response = await request.get('/wp-json/wc/v3/webhooks'); + 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 webhook', () => { + test(`can update a web hook`, async ({ + request, + }) => { + // update webhook + const response = await request.put( + `/wp-json/wc/v3/webhooks/${ webhookId }`, { + data: { + status: "paused" + } + } + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON.status).toEqual("paused"); + }); + }); + + test.describe('Delete a webhook', () => { + test('can permanently delete a webhook', async ({ + request + }) => { + + // Delete the webhook + const response = await request.delete(`/wp-json/wc/v3/webhooks/${ webhookId }`, { + data: { + force: true + } + }); + expect(response.status()).toEqual(200); + + // Verify that the webhook can no longer be retrieved + const getDeletedWebhookResponse = await request.get( + `/wp-json/wc/v3/webhooks/${ webhookId }` + ); + + /** + * Issue raised as we would expect this to return a 400 to be + * consistent with the other API calls + * Issue: https://github.com/woocommerce/woocommerce/issues/35290 + */ + expect(getDeletedWebhookResponse.status()).toEqual(400); + }); + }); + + + test.describe('Batch webhook operations', () => { + let webhookId1; + let webhookId2; + let webhookId3; + test('can batch create webhooks', async ({ + request + }) => { + // Batch create webhooks + // call API to batch create a webhook + const response = await request.post('wp-json/wc/v3/webhooks/batch', { + data: { + create: [{ + name: "Round toe", + topic: "coupon.created", + delivery_url: "http://requestb.in/1g0sxmo1" + }, + { + name: "Customer deleted", + topic: "customer.deleted", + delivery_url: "http://requestb.in/1g0sxmo1" + } + ] + }, + }); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + + // Verify that the new webhooks were created + const webhooks = responseJSON.create; + expect(webhooks).toHaveLength(2); + webhookId1 = webhooks[0].id; + webhookId2 = webhooks[1].id; + expect(webhookId1).toBeDefined(); + expect(webhookId2).toBeDefined(); + expect(webhooks[0].name).toEqual('Round toe'); + expect(webhooks[1].name).toEqual('Customer deleted'); + }); + + test('can batch update webhooks', async ({ + request + }) => { + // set payload to create, update and delete webhooks + const batchUpdatePayload = { + create: [{ + name: "Order Created", + topic: "order.created", + delivery_url: "http://requestb.in/1g0sxmo1" + }, ], + update: [{ + id: webhookId1, + name: 'Square toe', + }, ], + delete: [webhookId2] + }; + + // Call API to batch update the webhooks + const response = await request.post( + 'wp-json/wc/v3/webhooks/batch', { + data: batchUpdatePayload, + } + ); + const responseJSON = await response.json(); + expect(response.status()).toEqual(200); + expect(responseJSON.create).toHaveLength(1); + + webhookId3 = responseJSON.create[0].id; + expect(webhookId3).toBeDefined(); + expect(responseJSON.create[0].name).toEqual('Order Created'); + expect(responseJSON.create[0].topic).toEqual('order.created'); + expect(responseJSON.create[0].delivery_url).toEqual('http://requestb.in/1g0sxmo1'); + + expect(responseJSON.update).toHaveLength(1); + expect(responseJSON.update[0].id).toEqual(webhookId1); + expect(responseJSON.update[0].name).toEqual('Square toe'); + + // Verify that the deleted webhook can no longer be retrieved + const getDeletedWebhookResponse = await request.get( + `/wp-json/wc/v3/webhooks/${ webhookId2 }` + ); + /** + * Issue raised as we would expect this to return a 400 to be + * consistent with the other API calls + * Issue: https://github.com/woocommerce/woocommerce/issues/35290 + */ + expect(getDeletedWebhookResponse.status()).toEqual(400); + + }); + + test('can batch delete webhooks', async ({ + request + }) => { + // Batch delete the created webhooks + const response = await request.post( + 'wp-json/wc/v3/webhooks/batch', { + data: { + delete: [webhookId1, webhookId3] + }, + } + ); + const responseJSON = await response.json(); + + //Call the API to attempte to retrieve the deleted webhooks + const deletedResponse1 = await request.get( + `wp-json/wc/v3/webhooks/${ webhookId1 }` + ); + const deletedResponse3 = await request.get( + `wp-json/wc/v3/webhooks/${ webhookId3 }` + ); + /** + * Issue raised as we would expect this to return a 400 to be + * consistent with the other API calls + * Issue: https://github.com/woocommerce/woocommerce/issues/35290 + */ + expect(deletedResponse1.status()).toEqual(400); + expect(deletedResponse3.status()).toEqual(400); + + }); + }); +}); diff --git a/plugins/woocommerce/tests/bin/install.sh b/plugins/woocommerce/tests/bin/install.sh index 7bcc72190ea..05f7e9065ad 100755 --- a/plugins/woocommerce/tests/bin/install.sh +++ b/plugins/woocommerce/tests/bin/install.sh @@ -131,7 +131,11 @@ install_test_suite() { sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + if [[ "$DB_HOST" == *.sock ]]; then + sed $ioption "s|localhost|:${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + else + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi fi } @@ -145,7 +149,7 @@ install_db() { # If we're trying to connect to a socket we want to handle it differently. if [[ "$DB_HOST" == *.sock ]]; then # create database using the socket - mysqladmin create $DB_NAME --socket="$DB_HOST" + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS" --socket="$DB_HOST" else # Decide whether or not there is a port. local PARTS=(${DB_HOST//\:/ }) diff --git a/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh b/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh index fb02768209d..ebb2cac3099 100755 --- a/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh +++ b/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +ENABLE_HPOS="${ENABLE_HPOS:-0}" + wp-env run tests-cli "wp theme install twentynineteen --activate" wp-env run tests-cli "wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate" @@ -22,23 +24,7 @@ wp-env run tests-cli "wp user create customer customer@woocommercecoree2etestsui echo -e 'Update Blog Name \n' wp-env run tests-cli 'wp option update blogname "WooCommerce Core E2E Test Suite"' -# Enable additional WooCommerce features based on command options -while :; do - case $1 in - -c|--cot) # Enable the COT feature - echo 'Enable the COT feature' - wp-env run tests-cli "wp plugin install https://gist.github.com/vedanshujain/564afec8f5e9235a1257994ed39b1449/archive/9d5f174ebf8eec8e0ce5417d00728524c7f3b6b3.zip --activate" - ;; - --) # End of all options - shift - break - ;; - -?*) - echo "WARN: Unknown option (ignored):" $1 >&2 - ;; - *) # No more options, so break out of the loop - break - esac - - shift -done +if [ $ENABLE_HPOS == 1 ]; then + echo 'Enable the COT feature' + wp-env run tests-cli "wp plugin install https://gist.github.com/vedanshujain/564afec8f5e9235a1257994ed39b1449/archive/b031465052fc3e04b17624acbeeb2569ef4d5301.zip --activate" +fi diff --git a/plugins/woocommerce/tests/e2e-pw/global-setup.js b/plugins/woocommerce/tests/e2e-pw/global-setup.js index 5051c4fd28b..988aabc36a5 100644 --- a/plugins/woocommerce/tests/e2e-pw/global-setup.js +++ b/plugins/woocommerce/tests/e2e-pw/global-setup.js @@ -1,9 +1,18 @@ const { chromium, expect } = require( '@playwright/test' ); const fs = require( 'fs' ); +const { + ADMIN_USER, + ADMIN_PASSWORD, + CUSTOMER_USER, + CUSTOMER_PASSWORD, +} = process.env; +const adminUsername = ADMIN_USER ?? 'admin'; +const adminPassword = ADMIN_PASSWORD ?? 'password'; +const customerUsername = CUSTOMER_USER ?? 'customer'; +const customerPassword = CUSTOMER_PASSWORD ?? 'password'; module.exports = async ( config ) => { - const { stateDir } = config.projects[ 0 ].use; - const { baseURL } = config.projects[ 0 ].use; + const { stateDir, baseURL, userAgent } = config.projects[ 0 ].use; console.log( `State Dir: ${ stateDir }` ); console.log( `Base URL: ${ baseURL }` ); @@ -39,20 +48,27 @@ module.exports = async ( config ) => { let customerLoggedIn = false; let customerKeyConfigured = false; + // Specify user agent when running against an external test site to avoid getting HTTP 406 NOT ACCEPTABLE errors. + const contextOptions = { baseURL, userAgent }; + + // Create browser, browserContext, and page for customer and admin users const browser = await chromium.launch(); - const adminPage = await browser.newPage(); - const customerPage = await browser.newPage(); + const adminContext = await browser.newContext( contextOptions ); + const customerContext = await browser.newContext( contextOptions ); + const adminPage = await adminContext.newPage(); + const customerPage = await customerContext.newPage(); // Sign in as admin user and save state const adminRetries = 5; for ( let i = 0; i < adminRetries; i++ ) { try { console.log( 'Trying to log-in as admin...' ); - await adminPage.goto( `${ baseURL }/wp-admin` ); - await adminPage.fill( 'input[name="log"]', 'admin' ); - await adminPage.fill( 'input[name="pwd"]', 'password' ); + await adminPage.goto( `/wp-admin` ); + await adminPage.fill( 'input[name="log"]', adminUsername ); + await adminPage.fill( 'input[name="pwd"]', adminPassword ); await adminPage.click( 'text=Log In' ); - await adminPage.goto( `${ baseURL }/wp-admin` ); + await adminPage.waitForLoadState( 'networkidle' ); + await expect( adminPage.locator( 'div.wrap > h1' ) ).toHaveText( 'Dashboard' ); @@ -84,7 +100,7 @@ module.exports = async ( config ) => { try { console.log( 'Trying to add consumer token...' ); await adminPage.goto( - `${ baseURL }/wp-admin/admin.php?page=wc-settings&tab=advanced§ion=keys&create-key=1` + `/wp-admin/admin.php?page=wc-settings&tab=advanced§ion=keys&create-key=1` ); await adminPage.fill( '#key_description', 'Key for API access' ); await adminPage.selectOption( '#key_permissions', 'read_write' ); @@ -118,20 +134,22 @@ module.exports = async ( config ) => { for ( let i = 0; i < customerRetries; i++ ) { try { console.log( 'Trying to log-in as customer...' ); - await customerPage.goto( `${ baseURL }/wp-admin` ); - await customerPage.fill( 'input[name="log"]', 'customer' ); - await customerPage.fill( 'input[name="pwd"]', 'password' ); + await customerPage.goto( `/wp-admin` ); + await customerPage.fill( 'input[name="log"]', customerUsername ); + await customerPage.fill( 'input[name="pwd"]', customerPassword ); await customerPage.click( 'text=Log In' ); - await customerPage.goto( `${ baseURL }/my-account/` ); + await customerPage.goto( `/my-account` ); await expect( - customerPage.locator( 'h1.entry-title' ) - ).toContainText( 'My account' ); + customerPage.locator( + '.woocommerce-MyAccount-navigation-link--customer-logout' + ) + ).toBeVisible(); await expect( customerPage.locator( 'div.woocommerce-MyAccount-content > p >> nth=0' ) - ).toContainText( 'Jane Smith' ); + ).toContainText( 'Hello' ); await customerPage .context() @@ -154,5 +172,7 @@ module.exports = async ( config ) => { process.exit( 1 ); } + await adminContext.close(); + await customerContext.close(); await browser.close(); }; diff --git a/plugins/woocommerce/tests/e2e-pw/global-teardown.js b/plugins/woocommerce/tests/e2e-pw/global-teardown.js index 8469f034b93..f657db57ff9 100644 --- a/plugins/woocommerce/tests/e2e-pw/global-teardown.js +++ b/plugins/woocommerce/tests/e2e-pw/global-teardown.js @@ -1,10 +1,17 @@ const { chromium } = require( '@playwright/test' ); +const { ADMIN_USER, ADMIN_PASSWORD } = process.env; +const adminUsername = ADMIN_USER ?? 'admin'; +const adminPassword = ADMIN_PASSWORD ?? 'password'; module.exports = async ( config ) => { - const { baseURL } = config.projects[ 0 ].use; + const { baseURL, userAgent } = config.projects[ 0 ].use; + + // Specify user agent when running against an external test site to avoid getting HTTP 406 NOT ACCEPTABLE errors. + const contextOptions = { baseURL, userAgent }; const browser = await chromium.launch(); - const adminPage = await browser.newPage(); + const context = await browser.newContext( contextOptions ); + const adminPage = await context.newPage(); let consumerTokenCleared = false; @@ -13,12 +20,12 @@ module.exports = async ( config ) => { for ( let i = 0; i < keysRetries; i++ ) { try { console.log( 'Trying to clear consumer token... Try:' + i ); - await adminPage.goto( `${ baseURL }/wp-admin` ); - await adminPage.fill( 'input[name="log"]', 'admin' ); - await adminPage.fill( 'input[name="pwd"]', 'password' ); + await adminPage.goto( `/wp-admin` ); + await adminPage.fill( 'input[name="log"]', adminUsername ); + await adminPage.fill( 'input[name="pwd"]', adminPassword ); await adminPage.click( 'text=Log In' ); await adminPage.goto( - `${ baseURL }/wp-admin/admin.php?page=wc-settings&tab=advanced§ion=keys` + `/wp-admin/admin.php?page=wc-settings&tab=advanced§ion=keys` ); await adminPage.dispatchEvent( 'a.submitdelete', 'click' ); console.log( 'Cleared up consumer token successfully.' ); diff --git a/plugins/woocommerce/tests/e2e-pw/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/playwright.config.js index 849d550bc75..f218c67baf8 100644 --- a/plugins/woocommerce/tests/e2e-pw/playwright.config.js +++ b/plugins/woocommerce/tests/e2e-pw/playwright.config.js @@ -1,8 +1,16 @@ const { devices } = require( '@playwright/test' ); -const { CI, E2E_MAX_FAILURES } = process.env; +const { + ALLURE_RESULTS_DIR, + BASE_URL, + CI, + DEFAULT_TIMEOUT_OVERRIDE, + E2E_MAX_FAILURES, +} = process.env; const config = { - timeout: 90 * 1000, + timeout: DEFAULT_TIMEOUT_OVERRIDE + ? Number( DEFAULT_TIMEOUT_OVERRIDE ) + : 90 * 1000, expect: { timeout: 20 * 1000 }, outputDir: './report', globalSetup: require.resolve( './global-setup' ), @@ -19,17 +27,23 @@ const config = { open: CI ? 'never' : 'always', }, ], - [ 'allure-playwright', { outputFolder: 'e2e/allure-results' } ], - [ 'json', { outputFile: 'e2e/test-results.json' } ], + [ + 'allure-playwright', + { + outputFolder: + ALLURE_RESULTS_DIR ?? 'tests/e2e-pw/allure-results', + }, + ], + [ 'json', { outputFile: 'tests/e2e-pw/test-results.json' } ], ], maxFailures: E2E_MAX_FAILURES ? Number( E2E_MAX_FAILURES ) : 0, use: { + baseURL: BASE_URL ?? 'http://localhost:8086', screenshot: 'only-on-failure', - video: 'on-first-retry', + stateDir: 'tests/e2e-pw/storage/', trace: 'retain-on-failure', + video: 'on-first-retry', viewport: { width: 1280, height: 720 }, - baseURL: 'http://localhost:8086', - stateDir: 'e2e/storage/', }, projects: [ { diff --git a/plugins/woocommerce/tests/e2e-pw/storage/adminState.json b/plugins/woocommerce/tests/e2e-pw/storage/adminState.json deleted file mode 100644 index 8098ee78c77..00000000000 --- a/plugins/woocommerce/tests/e2e-pw/storage/adminState.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "cookies": [ - { - "sameSite": "Lax", - "name": "wordpress_dc5025de8b60c0a511df7c07d81ead97", - "value": "admin%7C1660405922%7CjsNIRwmbjFh7KIGlSHy50rSs6X0L0zISppmBqgh0u0u%7C8ac4ff905331544a3cbcf0c58840ebfc3f492d4511dd17f48dbed18f5662a2c6", - "domain": "localhost", - "path": "/wp-content/plugins", - "expires": -1, - "httpOnly": true, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wordpress_dc5025de8b60c0a511df7c07d81ead97", - "value": "admin%7C1660405922%7CjsNIRwmbjFh7KIGlSHy50rSs6X0L0zISppmBqgh0u0u%7C8ac4ff905331544a3cbcf0c58840ebfc3f492d4511dd17f48dbed18f5662a2c6", - "domain": "localhost", - "path": "/wp-admin", - "expires": -1, - "httpOnly": true, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wordpress_test_cookie", - "value": "WP%20Cookie%20check", - "domain": "localhost", - "path": "/", - "expires": -1, - "httpOnly": false, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wordpress_logged_in_dc5025de8b60c0a511df7c07d81ead97", - "value": "admin%7C1660405922%7CjsNIRwmbjFh7KIGlSHy50rSs6X0L0zISppmBqgh0u0u%7C6de27e0d8393de7715c07e144424d90733d53e65b8665c8a7db1ad94798c369c", - "domain": "localhost", - "path": "/", - "expires": -1, - "httpOnly": true, - "secure": false - }, - { - "sameSite": "Lax", - "name": "tk_ai", - "value": "woo%3AT9QFbJmpuSGWcjApaONaBtSB", - "domain": "localhost", - "path": "/", - "expires": -1, - "httpOnly": false, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wp-settings-time-1", - "value": "1660233126", - "domain": "localhost", - "path": "/", - "expires": 1691769126.937059, - "httpOnly": false, - "secure": false - } - ], - "origins": [] -} \ No newline at end of file diff --git a/plugins/woocommerce/tests/e2e-pw/storage/customerState.json b/plugins/woocommerce/tests/e2e-pw/storage/customerState.json deleted file mode 100644 index 6fa41f487e0..00000000000 --- a/plugins/woocommerce/tests/e2e-pw/storage/customerState.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "cookies": [ - { - "sameSite": "Lax", - "name": "wordpress_dc5025de8b60c0a511df7c07d81ead97", - "value": "customer%7C1660405929%7CL0OCLRBBMubq8iKqOeQU1NqOzTWFd7ppECd1n100GKG%7Cccb6b6b9e1190e94ef751360fded6ec026c89350f9f4695e9bf900ca09d72a84", - "domain": "localhost", - "path": "/wp-content/plugins", - "expires": -1, - "httpOnly": true, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wordpress_dc5025de8b60c0a511df7c07d81ead97", - "value": "customer%7C1660405929%7CL0OCLRBBMubq8iKqOeQU1NqOzTWFd7ppECd1n100GKG%7Cccb6b6b9e1190e94ef751360fded6ec026c89350f9f4695e9bf900ca09d72a84", - "domain": "localhost", - "path": "/wp-admin", - "expires": -1, - "httpOnly": true, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wordpress_test_cookie", - "value": "WP%20Cookie%20check", - "domain": "localhost", - "path": "/", - "expires": -1, - "httpOnly": false, - "secure": false - }, - { - "sameSite": "Lax", - "name": "wordpress_logged_in_dc5025de8b60c0a511df7c07d81ead97", - "value": "customer%7C1660405929%7CL0OCLRBBMubq8iKqOeQU1NqOzTWFd7ppECd1n100GKG%7C869a28fa8ef17799b6d097d84c8155c7d0dd18cd9967f7a18e3f49f41dd71226", - "domain": "localhost", - "path": "/", - "expires": -1, - "httpOnly": true, - "secure": false - } - ], - "origins": [] -} \ No newline at end of file diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-emails.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-emails.spec.js index 3f3e653d904..e4dba4d13c3 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-emails.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-emails.spec.js @@ -1,4 +1,5 @@ const { test, expect } = require( '@playwright/test' ); +const { ADMIN_USER_EMAIL } = process.env; const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; test.describe( 'Merchant > Order Action emails received', () => { @@ -11,7 +12,7 @@ test.describe( 'Merchant > Order Action emails received', () => { const adminEmail = process.env.USE_WP_ENV === '1' ? 'wordpress@example.com' - : 'admin@woocommercecoree2etestsuite.com'; + : ADMIN_USER_EMAIL ?? 'admin@woocommercecoree2etestsuite.com'; const storeName = 'WooCommerce Core E2E Test Suite'; let orderId, newOrderId; diff --git a/plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/update-woocommerce.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/update-woocommerce.spec.js new file mode 100644 index 00000000000..ba7d9d78a1e --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/update-woocommerce.spec.js @@ -0,0 +1,121 @@ +const { ADMINSTATE, UPDATE_WC, ADMIN_USER, ADMIN_PASSWORD } = process.env; +const { test, expect } = require( '@playwright/test' ); +const path = require( 'path' ); +const { + deletePlugin, + downloadZip, + deleteZip, +} = require( '../../utils/plugin-utils' ); + +const pluginZipPath = path.resolve( __dirname, '../../tmp/woocommerce.zip' ); + +test.describe( 'WooCommerce plugin can be uploaded and activated', () => { + // Skip test if UPDATE_WC is falsy. + test.skip( + ! Boolean( UPDATE_WC ), + `Skipping this test because UPDATE_WC is falsy: ${ UPDATE_WC }` + ); + + test.use( { storageState: ADMINSTATE } ); + + test.beforeAll( async () => { + // Download WooCommerce ZIP + await downloadZip( { + url: + 'https://github.com/woocommerce/woocommerce/releases/download/nightly/woocommerce-trunk-nightly.zip', + downloadPath: pluginZipPath, + } ); + } ); + + test.afterAll( async () => { + // Clean up downloaded zip + await deleteZip( pluginZipPath ); + } ); + + test( 'can upload and activate the WooCommerce plugin', async ( { + page, + playwright, + baseURL, + } ) => { + // Delete WooCommerce if it's installed. + await deletePlugin( { + request: playwright.request, + baseURL, + slug: 'woocommerce', + username: ADMIN_USER, + password: ADMIN_PASSWORD, + } ); + + // Open the plugin install page + await page.goto( 'wp-admin/plugin-install.php', { + waitUntil: 'networkidle', + } ); + + // Upload the plugin zip + await page.click( 'a.upload-view-toggle' ); + await expect( page.locator( 'p.install-help' ) ).toBeVisible(); + await expect( page.locator( 'p.install-help' ) ).toContainText( + 'If you have a plugin in a .zip format, you may install or update it by uploading it here.' + ); + const [ fileChooser ] = await Promise.all( [ + page.waitForEvent( 'filechooser' ), + page.click( '#pluginzip' ), + ] ); + await fileChooser.setFiles( pluginZipPath ); + await page.click( '#install-plugin-submit' ); + await page.waitForLoadState( 'networkidle' ); + + // Activate the plugin + await page.click( '.button-primary' ); + await page.waitForLoadState( 'networkidle' ); + + // Go to 'Installed plugins' page + await page.goto( 'wp-admin/plugins.php', { + waitUntil: 'networkidle', + } ); + + // Assert that 'WooCommerce' is listed and active + await expect( + page.locator( '.plugin-title strong', { hasText: /^WooCommerce$/ } ) + ).toBeVisible(); + await expect( page.locator( '#deactivate-woocommerce' ) ).toBeVisible(); + } ); + + test( 'can run the database update', async ( { page } ) => { + const updateButton = page.locator( 'text=Update WooCommerce Database' ); + const updateCompleteMessage = page.locator( + 'text=WooCommerce database update complete.' + ); + + // Navigate to 'Installed Plugins' page + await page.goto( 'wp-admin/plugins.php', { + waitUntil: 'networkidle', + } ); + + // Skip this test if the "Update WooCommerce Database" button didn't appear. + test.skip( + ! ( await updateButton.isVisible() ), + 'The "Update WooCommerce Database" button did not appear after updating WooCommerce. Verify with the team if the WooCommerce version being tested does not really trigger a database update.' + ); + + // If the notice appears, start DB update + await updateButton.click(); + await page.waitForLoadState( 'networkidle' ); + + // Repeatedly reload the Plugins page up to 10 times until the message "WooCommerce database update complete." appears. + for ( + let reloads = 0; + reloads < 10 && ! ( await updateCompleteMessage.isVisible() ); + reloads++ + ) { + await page.goto( 'wp-admin/plugins.php', { + waitUntil: 'networkidle', + } ); + + // Wait 10s before the next reload. + await page.waitForTimeout( 10000 ); + } + + await expect( updateCompleteMessage ).toBeVisible(); + } ); +} ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/upload-plugin.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/upload-plugin.spec.js new file mode 100644 index 00000000000..a92195e7255 --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/smoke-tests/upload-plugin.spec.js @@ -0,0 +1,104 @@ +const { + ADMINSTATE, + ADMIN_USER, + ADMIN_PASSWORD, + GITHUB_TOKEN, + PLUGIN_NAME, + PLUGIN_REPOSITORY, +} = process.env; +const { test, expect } = require( '@playwright/test' ); +const path = require( 'path' ); +const { + createPlugin, + deletePlugin, + downloadZip, + deleteZip, + getLatestReleaseZipUrl, +} = require( '../../utils/plugin-utils' ); + +const adminUsername = ADMIN_USER ?? 'admin'; +const adminPassword = ADMIN_PASSWORD ?? 'password'; + +let pluginZipPath; +let pluginSlug; + +test.describe( `${ PLUGIN_NAME } plugin can be uploaded and activated`, () => { + // Skip test if PLUGIN_REPOSITORY is falsy. + test.skip( + ! PLUGIN_REPOSITORY, + `Skipping this test because value of PLUGIN_REPOSITORY was falsy: ${ PLUGIN_REPOSITORY }` + ); + + test.use( { storageState: ADMINSTATE } ); + + test.beforeAll( async () => { + pluginSlug = PLUGIN_REPOSITORY.split( '/' ).pop(); + + // Get the download URL and filename of the plugin + const pluginDownloadURL = await getLatestReleaseZipUrl( { + repository: PLUGIN_REPOSITORY, + authorizationToken: GITHUB_TOKEN, + } ); + const zipFilename = pluginDownloadURL.split( '/' ).pop(); + pluginZipPath = path.resolve( __dirname, `../../tmp/${ zipFilename }` ); + + // Download the needed plugin. + await downloadZip( { + url: pluginDownloadURL, + downloadPath: pluginZipPath, + authToken: GITHUB_TOKEN, + } ); + } ); + + test.afterAll( async ( { baseURL, playwright } ) => { + // Delete the downloaded zip. + await deleteZip( pluginZipPath ); + + // Delete the plugin from the test site. + await deletePlugin( { + request: playwright.request, + baseURL, + slug: pluginSlug, + username: adminUsername, + password: adminPassword, + } ); + } ); + + test( `can upload and activate ${ PLUGIN_NAME }`, async ( { + page, + playwright, + baseURL, + } ) => { + // Delete the plugin if it's installed. + await deletePlugin( { + request: playwright.request, + baseURL, + slug: pluginSlug, + username: adminUsername, + password: adminPassword, + } ); + + // Install and activate plugin + await createPlugin( { + request: playwright.request, + baseURL, + slug: pluginSlug.split( '/' ).pop(), + username: adminUsername, + password: adminPassword, + } ); + + // Go to 'Installed plugins' page. + // Repeat in case the newly installed plugin redirects to their own onboarding screen upon first install, like what Yoast SEO does. + let reload = 2; + do { + await page.goto( 'wp-admin/plugins.php', { + waitUntil: 'networkidle', + } ); + } while ( ! page.url().includes( '/plugins.php' ) && --reload ); + + // Assert that the plugin is listed and active + await expect( + page.locator( `#deactivate-${ pluginSlug }` ) + ).toBeVisible(); + } ); +} ); diff --git a/plugins/woocommerce/tests/e2e-pw/utils/plugin-utils.js b/plugins/woocommerce/tests/e2e-pw/utils/plugin-utils.js new file mode 100644 index 00000000000..69e84a7a6ac --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/utils/plugin-utils.js @@ -0,0 +1,196 @@ +const { APIRequest } = require( '@playwright/test' ); +const axios = require( 'axios' ).default; +const fs = require( 'fs' ); +const path = require( 'path' ); + +/** + * Encode basic auth username and password to be used in HTTP Authorization header. + * + * @param {string} username + * @param {string} password + * @returns Base64-encoded string + */ +const encodeCredentials = ( username, password ) => { + return Buffer.from( `${ username }:${ password }` ).toString( 'base64' ); +}; + +/** + * Deactivate and delete a plugin specified by the given `slug` using the WordPress API. + * + * @param {object} params + * @param {APIRequest} params.request + * @param {string} params.baseURL + * @param {string} params.slug + * @param {string} params.username + * @param {string} params.password + */ +export const deletePlugin = async ( { + request, + baseURL, + slug, + username, + password, +} ) => { + // Check if plugin is installed by getting the list of installed plugins, and then finding the one whose `textdomain` property equals `slug`. + const apiContext = await request.newContext( { + baseURL, + extraHTTPHeaders: { + Authorization: `Basic ${ encodeCredentials( username, password ) }`, + }, + } ); + const listPluginsResponse = await apiContext.get( + `/wp-json/wp/v2/plugins`, + { + failOnStatusCode: true, + } + ); + const pluginsList = await listPluginsResponse.json(); + const pluginToDelete = pluginsList.find( + ( { textdomain } ) => textdomain === slug + ); + + // If installed, get its `plugin` value and use it to deactivate and delete it. + if ( pluginToDelete ) { + const { plugin } = pluginToDelete; + + await apiContext.put( `/wp-json/wp/v2/plugins/${ plugin }`, { + data: { status: 'inactive' }, + failOnStatusCode: true, + } ); + + await apiContext.delete( `/wp-json/wp/v2/plugins/${ plugin }`, { + failOnStatusCode: true, + } ); + } +}; + +/** + * Download the zip file from a remote location. + * + * @param {object} param + * @param {string} param.url The URL where the zip file is located. + * @param {string} param.downloadPath The location where to download the zip to. + * @param {string} param.authToken Authorization token used to authenticate with the GitHub API if required. + */ +export const downloadZip = async ( { url, downloadPath, authToken } ) => { + // Create destination folder. + const dir = path.dirname( downloadPath ); + fs.mkdirSync( dir, { recursive: true } ); + + // Download the zip. + const options = { + url, + responseType: 'stream', + headers: { + 'user-agent': 'node.js', + }, + }; + + // If provided with a token, use it for authorization + if ( authToken ) { + options.headers.Authorization = `token ${ authToken }`; + } + + const response = await axios( options ); + response.data.pipe( fs.createWriteStream( downloadPath ) ); +}; + +/** + * Delete a zip file. Useful when cleaning up downloaded plugin zips. + * + * @param {string} zipFilePath Local file path to the ZIP. + */ +export const deleteZip = async ( zipFilePath ) => { + console.log( `Deleting file located in ${ zipFilePath }...` ); + await fs.unlink( zipFilePath, ( err ) => { + if ( err ) throw err; + console.log( `Successfully deleted!` ); + } ); +}; + +/** + * Get the download URL of the latest release zip for a plugin using GitHub's {@link https://docs.github.com/en/rest/releases/releases Releases API}. + * + * @param {{repository: string, authorizationToken: string, prerelease: boolean, perPage: number}} param + * @param {string} repository The repository owner and name. For example: `woocommerce/woocommerce`. + * @param {string} authorizationToken Authorization token used to authenticate with the GitHub API if required. + * @param {boolean} prerelease Flag on whether to get a prelease or not. + * @param {number} perPage Limit of entries returned from the latest releases list, defaults to 3. + * @return {string} Download URL for the release zip file. + */ +export const getLatestReleaseZipUrl = async ( { + repository, + authorizationToken, + prerelease = false, + perPage = 3, +} ) => { + const requesturl = prerelease + ? `https://api.github.com/repos/${ repository }/releases?per_page=${ perPage }` + : `https://api.github.com/repos/${ repository }/releases/latest`; + + const options = { + url: requesturl, + headers: { 'user-agent': 'node.js' }, + }; + + // If provided with a token, use it for authorization + if ( authorizationToken ) { + options.headers.Authorization = `token ${ authorizationToken }`; + } + + // Call the List releases API endpoint in GitHub + const response = await axios( options ); + const body = response.data; + + // If it's a prerelease, find the first one and return its download URL. + if ( prerelease ) { + const latestPrerelease = body.find( ( { prerelease } ) => prerelease ); + + return latestPrerelease.assets[ 0 ].browser_download_url; + } else if ( authorizationToken ) { + // If it's a private repo, we need to download the archive this way. + // Use uploaded assets over downloading the zip archive. + if ( + body.assets && + body.assets.length > 0 && + body.assets[ 0 ].browser_download_url + ) { + return body.assets[ 0 ].browser_download_url; + } else { + const tagName = body.tag_name; + return `https://github.com/${ repository }/archive/${ tagName }.zip`; + } + } else { + return body.assets[ 0 ].browser_download_url; + } +}; + +/** + * Use the {@link https://developer.wordpress.org/rest-api/reference/plugins/#create-a-plugin Create plugin endpoint} to install and activate a plugin. + * + * @param {object} params + * @param {APIRequest} params.request + * @param {string} params.baseURL + * @param {string} params.slug + * @param {string} params.username + * @param {string} params.password + */ +export const createPlugin = async ( { + request, + baseURL, + slug, + username, + password, +} ) => { + const apiContext = await request.newContext( { + baseURL, + extraHTTPHeaders: { + Authorization: `Basic ${ encodeCredentials( username, password ) }`, + }, + } ); + + await apiContext.post( '/wp-json/wp/v2/plugins', { + data: { slug, status: 'active' }, + failOnStatusCode: true, + } ); +}; diff --git a/plugins/woocommerce/tests/legacy/unit-tests/cart/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/cart/functions.php index fead6b21622..2edb95f6335 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/cart/functions.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/cart/functions.php @@ -15,23 +15,23 @@ class WC_Tests_Cart_Functions extends WC_Unit_Test_Case { */ private function get_checkout_url() { - // Get the checkout URL + // Get the checkout URL. $checkout_page_id = wc_get_page_id( 'checkout' ); $checkout_url = ''; - // Check if there is a checkout page + // Check if there is a checkout page. if ( $checkout_page_id ) { - // Get the permalink + // Get the permalink. $checkout_url = get_permalink( $checkout_page_id ); - // Force SSL if needed + // Force SSL if needed. if ( is_ssl() || 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ) { $checkout_url = str_replace( 'http:', 'https:', $checkout_url ); } - // Allow filtering of checkout URL + // Allow filtering of checkout URL. $checkout_url = apply_filters( 'woocommerce_get_checkout_url', $checkout_url ); } @@ -44,10 +44,10 @@ class WC_Tests_Cart_Functions extends WC_Unit_Test_Case { * @since 2.5.0 */ public function test_get_checkout_url_regular() { - // Make sure pages exist + // Make sure pages exist. WC_Install::create_pages(); - // Force SSL checkout + // Force SSL checkout. update_option( 'woocommerce_force_ssl_checkout', 'no' ); $this->assertEquals( $this->get_checkout_url(), wc_get_checkout_url() ); @@ -59,10 +59,10 @@ class WC_Tests_Cart_Functions extends WC_Unit_Test_Case { * @since 2.5.0 */ public function test_get_checkout_url_ssl() { - // Make sure pages exist + // Make sure pages exist. WC_Install::create_pages(); - // Force SSL checkout + // Force SSL checkout. update_option( 'woocommerce_force_ssl_checkout', 'yes' ); $this->assertEquals( $this->get_checkout_url(), wc_get_checkout_url() ); @@ -74,16 +74,16 @@ class WC_Tests_Cart_Functions extends WC_Unit_Test_Case { * @since 2.3.0 */ public function test_wc_empty_cart() { - // Create dummy product + // Create dummy product. $product = WC_Helper_Product::create_simple_product(); - // Add the product to the cart + // Add the product to the cart. WC()->cart->add_to_cart( $product->get_id(), 1 ); - // Empty the cart + // Empty the cart. wc_empty_cart(); - // Check if the cart is empty + // Check if the cart is empty. $this->assertEquals( 0, WC()->cart->get_cart_contents_count() ); } @@ -137,24 +137,25 @@ class WC_Tests_Cart_Functions extends WC_Unit_Test_Case { * Test wc_add_to_cart_message */ public function test_wc_add_to_cart_message() { - $product = WC_Helper_Product::create_simple_product(); + $product = WC_Helper_Product::create_simple_product(); + $wp_button_class = esc_attr( wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '' ); $message = wc_add_to_cart_message( array( $product->get_id() => 1 ), false, true ); - $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); + $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); $message = wc_add_to_cart_message( array( $product->get_id() => 3 ), false, true ); - $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); + $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); $message = wc_add_to_cart_message( array( $product->get_id() => 1 ), true, true ); - $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); + $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); $message = wc_add_to_cart_message( array( $product->get_id() => 3 ), true, true ); - $this->assertEquals( 'View cart 3 × “Dummy Product” have been added to your cart.', $message ); + $this->assertEquals( 'View cart 3 × “Dummy Product” have been added to your cart.', $message ); $message = wc_add_to_cart_message( $product->get_id(), false, true ); - $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); + $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); $message = wc_add_to_cart_message( $product->get_id(), true, true ); - $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); + $this->assertEquals( 'View cart “Dummy Product” has been added to your cart.', $message ); } } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/admin-notes.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/admin-notes.php index d53cc86b611..d582747369f 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/admin-notes.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/admin-notes.php @@ -218,79 +218,6 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case { $this->assertEquals( 4, count( $notes ) ); } - /** - * Test getting notes when the user is in tasklist experiment returns notes of size `per_page` without any filters. - * - * @since 3.5.0 - */ - public function test_getting_notes_when_user_is_in_tasklist_experiment_returns_unfiltered_notes() { - // Given. - wp_set_current_user( $this->user ); - WC_Helper_Admin_Notes::reset_notes_dbs(); - // Notes of the following two names are hidden when the user is not in the task list experiment. - WC_Helper_Admin_Notes::add_note_for_test( 'wc-admin-complete-store-details' ); - WC_Helper_Admin_Notes::add_note_for_test( 'wc-admin-update-store-details' ); - // Other notes. - WC_Helper_Admin_Notes::add_note_for_test( 'winter-sales' ); - WC_Helper_Admin_Notes::add_note_for_test( '2022-promo' ); - - $this->set_user_in_tasklist_experiment(); - - // When. - $request = new WP_REST_Request( 'GET', $this->endpoint ); - $request->set_query_params( - array( - 'page' => '1', - 'per_page' => '3', - ) - ); - $response = $this->server->dispatch( $request ); - $notes = $response->get_data(); - - // Then. - $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( 3, count( $notes ) ); - $this->assertEquals( $notes[0]['name'], 'wc-admin-complete-store-details' ); - $this->assertEquals( $notes[1]['name'], 'wc-admin-update-store-details' ); - $this->assertEquals( $notes[2]['name'], 'winter-sales' ); - } - - /** - * Test getting notes when the user is not in tasklist experiment excludes two notes. - * @since 3.5.0 - */ - public function test_getting_notes_when_user_is_not_in_tasklist_experiment_excludes_two_notes() { - $this->markTestSkipped( 'We are disabling the experiments for now.' ); - // Given. - wp_set_current_user( $this->user ); - WC_Helper_Admin_Notes::reset_notes_dbs(); - // Notes of the following two names are hidden when the user is not in the task list experiment. - WC_Helper_Admin_Notes::add_note_for_test( 'wc-admin-complete-store-details' ); - WC_Helper_Admin_Notes::add_note_for_test( 'wc-admin-update-store-details' ); - // Other notes. - WC_Helper_Admin_Notes::add_note_for_test( 'summer-sales' ); - WC_Helper_Admin_Notes::add_note_for_test( '2022-promo' ); - - $this->set_user_out_of_tasklist_experiment(); - - // When. - $request = new WP_REST_Request( 'GET', $this->endpoint ); - $request->set_query_params( - array( - 'page' => '1', - 'per_page' => '3', - ) - ); - $response = $this->server->dispatch( $request ); - $notes = $response->get_data(); - - // Then. - $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( 2, count( $notes ) ); - $this->assertEquals( $notes[0]['name'], 'summer-sales' ); - $this->assertEquals( $notes[1]['name'], '2022-promo' ); - } - /** * Test getting notes of a certain type. * @@ -621,30 +548,4 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( 2, count( $notes ) ); } - - /** - * Simulates when the user is in tasklist experiment similar to `API/Notes` `is_tasklist_experiment_assigned_treatment` function. - */ - private function set_user_in_tasklist_experiment() { - // When the user is participating in either `wc-admin-complete-store-details` or `wc-admin-update-store-details` AB tests, - // the user is in tasklist experiment. - update_option( 'woocommerce_allow_tracking', 'yes' ); - $date = new \DateTime(); - $date->setTimeZone( new \DateTimeZone( 'UTC' ) ); - - $experiment_name = sprintf( - 'woocommerce_tasklist_progression_headercard_%s_%s', - $date->format( 'Y' ), - $date->format( 'm' ) - ); - set_transient( 'abtest_variation_' . $experiment_name, 'treatment' ); - } - - /** - * Simulates when the user is not in tasklist experiment similar to `API/Notes` `is_tasklist_experiment_assigned_treatment` function. - */ - private function set_user_out_of_tasklist_experiment() { - // Any experiment is off when `woocommerce_allow_tracking` option is false. - update_option( 'woocommerce_allow_tracking', false ); - } } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-tasks.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-tasks.php index ad35e8b3103..495ae081525 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-tasks.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-tasks.php @@ -51,7 +51,6 @@ class WC_Admin_Tests_API_Onboarding_Tasks extends WC_REST_Unit_Test_Case { // Resetting task list options and lists. update_option( Task::DISMISSED_OPTION, array() ); - update_option( Task::SNOOZED_OPTION, array() ); TaskLists::clear_lists(); } @@ -185,185 +184,6 @@ class WC_Admin_Tests_API_Onboarding_Tasks extends WC_REST_Unit_Test_Case { $this->assertSame( 'Custom post content', get_the_content( null, null, $data['post_id'] ) ); } - - /** - * Test that a task can be snoozed. - * @group tasklist - */ - public function test_task_can_be_snoozed() { - wp_set_current_user( $this->user ); - - TaskLists::add_list( - array( - 'id' => 'test-list', - ) - ); - - TaskLists::add_task( - 'test-list', - new TestTask( - TaskLists::get_list( 'test-list' ), - array( - 'id' => 'test-task', - 'title' => 'Test Task', - 'is_snoozeable' => true, - ) - ) - ); - - $request = new WP_REST_Request( 'POST', $this->endpoint . '/test-task/snooze' ); - $request->set_headers( array( 'content-type' => 'application/json' ) ); - $response = $this->server->dispatch( $request ); - $data = $response->get_data(); - - $task = TaskLists::get_task( 'test-task' ); - - $this->assertEquals( $data['isSnoozed'], true ); - $this->assertEquals( isset( $data['snoozedUntil'] ), true ); - $this->assertEquals( $task->is_snoozed(), true ); - $this->assertNotNull( $task->get_snoozed_until() ); - - } - - /** - * Test that a task can be snoozed with determined list ID. - * @group tasklist - */ - public function test_task_can_be_snoozed_with_list_id() { - wp_set_current_user( $this->user ); - - TaskLists::add_list( - array( - 'id' => 'test-list', - ) - ); - - TaskLists::add_task( - 'test-list', - new TestTask( - TaskLists::get_list( 'test-list' ), - array( - 'id' => 'test-task', - 'title' => 'Test Task', - 'is_snoozeable' => true, - ) - ) - ); - - $request = new WP_REST_Request( 'POST', $this->endpoint . '/test-task/snooze' ); - $request->set_headers( array( 'content-type' => 'application/json' ) ); - $request->set_body( wp_json_encode( array( 'task_list_id' => 'test-list' ) ) ); - $response = $this->server->dispatch( $request ); - $data = $response->get_data(); - - $task = TaskLists::get_task( 'test-task' ); - - $this->assertEquals( $data['isSnoozed'], true ); - $this->assertEquals( isset( $data['snoozedUntil'] ), true ); - $this->assertEquals( $task->is_snoozed(), true ); - $this->assertNotNull( $task->get_snoozed_until() ); - } - - /** - * Test that a task can be snoozed with determined duration. - * @group tasklist - */ - public function test_task_can_be_snoozed_with_duration() { - wp_set_current_user( $this->user ); - - TaskLists::add_list( - array( - 'id' => 'test-list', - ) - ); - - TaskLists::add_task( - 'test-list', - new TestTask( - TaskLists::get_list( 'test-list' ), - array( - 'id' => 'test-task', - 'title' => 'Test Task', - 'is_snoozeable' => true, - ) - ) - ); - - $request = new WP_REST_Request( 'POST', $this->endpoint . '/test-task/snooze' ); - $request->set_headers( array( 'content-type' => 'application/json' ) ); - $request->set_body( wp_json_encode( array( 'duration' => 'week' ) ) ); - $response = $this->server->dispatch( $request ); - $data = $response->get_data(); - - $task = TaskLists::get_task( 'test-task' ); - - $week_in_ms = WEEK_IN_SECONDS * 1000; - // Taking off 1 minute as matching a week is very precise and we might run into some race conditions otherwise. - $week_in_ms -= MINUTE_IN_SECONDS * 1000; - - $this->assertEquals( $data['snoozedUntil'] >= ( ( time() * 1000 ) + $week_in_ms ), true ); - - } - - /** - * Test that a snoozed task can be undone. - * @group tasklist - */ - public function test_snoozed_task_can_be_undone() { - wp_set_current_user( $this->user ); - - TaskLists::add_list( - array( - 'id' => 'test-list', - ) - ); - - TaskLists::add_task( - 'test-list', - new TestTask( - TaskLists::get_list( 'test-list' ), - array( - 'id' => 'test-task', - 'title' => 'Test Task', - 'is_snoozeable' => true, - ) - ) - ); - - $task = TaskLists::get_task( 'test-task' ); - - $task->snooze(); - - $this->assertEquals( $task->is_snoozed(), true ); - - $request = new WP_REST_Request( 'POST', $this->endpoint . '/test-task/undo_snooze' ); - $request->set_headers( array( 'content-type' => 'application/json' ) ); - $response = $this->server->dispatch( $request ); - $data = $response->get_data(); - - $task_after_request = TaskLists::get_task( 'test-task' ); - - $this->assertEquals( $task_after_request->is_snoozed(), false ); - - } - - /** - * Test that snooze endpoint returns error for invalid task. - * @group tasklist - */ - public function test_snoozed_task_invalid() { - $this->markTestSkipped( 'Skipped temporarily due to change in endpoint behavior.' ); - wp_set_current_user( $this->user ); - - $request = new WP_REST_Request( 'POST', $this->endpoint . '/test-task/snooze' ); - $request->set_headers( array( 'content-type' => 'application/json' ) ); - $response = $this->server->dispatch( $request ); - $response_data = $response->get_data(); - - $this->assertEquals( $response_data['data']['status'], 404 ); - $this->assertEquals( $response_data['code'], 'woocommerce_rest_invalid_task' ); - } - /** * Test that a task can be dismissed. * @group tasklist diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php index 3ea07b24fc8..75d7d0ca8dc 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php @@ -6,6 +6,8 @@ * @since 3.5.0 */ +// phpcs:disable Squiz.Classes.ClassFileName.NoMatch, Squiz.Classes.ValidClassName.NotCamelCaps + use \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore; /** @@ -503,7 +505,8 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { WC_Helper_Queue::run_all_pending(); - $this->assertTrue( $result ); + $this->assertNotEquals( -1, $result ); + $request = new WP_REST_Request( 'GET', $this->endpoint ); $response = $this->server->dispatch( $request ); $reports = $response->get_data(); @@ -599,7 +602,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { WC_Helper_Queue::run_all_pending(); // Didn't update anything. - $this->assertTrue( $result ); + $this->assertNotEquals( -1, $result ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $response = $this->server->dispatch( $request ); $reports = $response->get_data(); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php index ba2ee818c71..bebd752a96f 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php @@ -99,100 +99,6 @@ class WC_Admin_Tests_OnboardingTasks_Task extends WC_Unit_Test_Case { } - /** - * Tests that a task can be snoozed. - */ - public function test_snooze() { - $task = new TestTask( - new TaskList( array( 'id' => 'setup' ) ), - array( - 'id' => 'wc-unit-test-snoozeable-task', - 'is_snoozeable' => true, - ) - ); - - $update = $task->snooze(); - $snoozed = get_option( Task::SNOOZED_OPTION, array() ); - $this->assertEquals( true, $update ); - $this->assertArrayHasKey( $task->get_id(), $snoozed ); - } - - /** - * Tests that a task can be unsnoozed. - */ - public function test_undo_snooze() { - $task = new TestTask( - new TaskList( array( 'id' => 'setup' ) ), - array( - 'id' => 'wc-unit-test-snoozeable-task', - 'is_snoozeable' => true, - ) - ); - - $task->snooze(); - $task->undo_snooze(); - $snoozed = get_option( Task::SNOOZED_OPTION, array() ); - $this->assertArrayNotHasKey( $task->get_id(), $snoozed ); - } - - /** - * Tests that a task's snooze time is automatically added. - */ - public function test_snoozed_until() { - $time = time() * 1000; - $snoozed = get_option( Task::SNOOZED_OPTION, array() ); - $snoozed['wc-unit-test-task'] = $time; - update_option( Task::SNOOZED_OPTION, $snoozed ); - - $task = new TestTask( - new TaskList( array( 'id' => 'setup' ) ), - array( - 'id' => 'wc-unit-test-task', - 'is_snoozeable' => true, - ) - ); - - $this->assertEquals( $time, $task->get_snoozed_until() ); - - } - - /** - * Tests that a non snoozeable task cannot be snoozed. - */ - public function test_not_snoozeable() { - $task = new TestTask( - new TaskList( array( 'id' => 'setup' ) ), - array( - 'id' => 'wc-unit-test-snoozeable-task', - 'is_snoozeable' => false, - ) - ); - - $task->snooze(); - $this->assertEquals( false, $task->is_snoozed() ); - } - - /** - * Tests that a task is no longer consider snoozed after the time has passed. - */ - public function test_snooze_time() { - $task = new TestTask( - new TaskList( array( 'id' => 'setup' ) ), - array( - 'id' => 'wc-unit-test-snoozeable-task', - 'is_snoozeable' => true, - ) - ); - - $time = time() * 1000 - 1; - $snoozed = get_option( Task::SNOOZED_OPTION, array() ); - $snoozed['wc-unit-test-snoozeable-task'] = $time; - update_option( Task::SNOOZED_OPTION, $snoozed ); - - $this->assertEquals( false, $task->is_snoozed() ); - } - - /** * Tests that a task's properties are returned as JSON. */ diff --git a/plugins/woocommerce/tests/performance/bin/init-sample-products.sh b/plugins/woocommerce/tests/performance/bin/init-sample-products.sh index 8ff1244b972..e5a941876f4 100755 --- a/plugins/woocommerce/tests/performance/bin/init-sample-products.sh +++ b/plugins/woocommerce/tests/performance/bin/init-sample-products.sh @@ -1,5 +1,7 @@ #!/bin/bash +ENABLE_HPOS="${ENABLE_HPOS:-0}" + echo "Initializing WooCommerce E2E" wp-env run tests-cli "wp plugin activate woocommerce" @@ -40,3 +42,9 @@ wp-env run tests-cli "wp import wp-content/plugins/woocommerce/sample-data/sampl wp-env run tests-cli "wp theme install storefront --activate" echo "Success! Your E2E Test Environment is now ready." + + +if [ $ENABLE_HPOS == 1 ]; then + echo 'Enable the COT feature' + wp-env run tests-cli "wp plugin install https://gist.github.com/vedanshujain/564afec8f5e9235a1257994ed39b1449/archive/b031465052fc3e04b17624acbeeb2569ef4d5301.zip --activate" +fi diff --git a/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js b/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js new file mode 100644 index 00000000000..3ec046b709e --- /dev/null +++ b/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js @@ -0,0 +1,107 @@ +/* eslint-disable no-shadow */ +/* eslint-disable import/no-unresolved */ +/** + * External dependencies + */ +import { sleep, check, group } from 'k6'; +import http from 'k6/http'; +import { + randomIntBetween, + findBetween, +} from 'https://jslib.k6.io/k6-utils/1.1.0/index.js'; + +/** + * Internal dependencies + */ +import { base_url, think_time_min, think_time_max } from '../../config.js'; +import { + htmlRequestHeader, + commonRequestHeaders, + commonGetRequestHeaders, + commonNonStandardHeaders, +} from '../../headers.js'; + +export function myAccountOrders() { + let response; + let my_account_order_id; + + group( 'My Account', function () { + const requestHeaders = Object.assign( + {}, + htmlRequestHeader, + commonRequestHeaders, + commonGetRequestHeaders, + commonNonStandardHeaders + ); + + response = http.get( `${ base_url }/my-account`, { + headers: requestHeaders, + tags: { name: 'Shopper - My Account' }, + } ); + check( response, { + 'is status 200': ( r ) => r.status === 200, + 'body contains: my account welcome message': ( response ) => + response.body.includes( + 'From your account dashboard you can view' + ), + } ); + } ); + + sleep( randomIntBetween( `${ think_time_min }`, `${ think_time_max }` ) ); + + group( 'My Account Orders', function () { + const requestHeaders = Object.assign( + {}, + htmlRequestHeader, + commonRequestHeaders, + commonGetRequestHeaders, + commonNonStandardHeaders + ); + + response = http.get( `${ base_url }/my-account/orders/`, { + headers: requestHeaders, + tags: { name: 'Shopper - My Account Orders' }, + } ); + check( response, { + 'is status 200': ( r ) => r.status === 200, + "body contains: 'Orders' title": ( response ) => + response.body.includes( '>Orders' ), + } ); + my_account_order_id = findBetween( + response.body, + 'my-account/view-order/', + '/">' + ); + } ); + + sleep( randomIntBetween( `${ think_time_min }`, `${ think_time_max }` ) ); + + group( 'My Account Open Order', function () { + const requestHeaders = Object.assign( + {}, + htmlRequestHeader, + commonRequestHeaders, + commonGetRequestHeaders, + commonNonStandardHeaders + ); + + response = http.get( + `${ base_url }/my-account/view-order/${ my_account_order_id }`, + { + headers: requestHeaders, + tags: { name: 'Shopper - My Account Open Order' }, + } + ); + check( response, { + 'is status 200': ( r ) => r.status === 200, + "body contains: 'Order number' title": ( response ) => + response.body.includes( `${ my_account_order_id }` ), + } ); + } ); + + sleep( randomIntBetween( `${ think_time_min }`, `${ think_time_max }` ) ); +} + +export default function () { + myAccountOrders(); +} diff --git a/plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js b/plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js index d6b56104371..01e670e7bb8 100644 --- a/plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js +++ b/plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js @@ -10,6 +10,7 @@ import { cartRemoveItem } from '../requests/shopper/cart-remove-item.js'; import { checkoutGuest } from '../requests/shopper/checkout-guest.js'; import { checkoutCustomerLogin } from '../requests/shopper/checkout-customer-login.js'; import { myAccount } from '../requests/shopper/my-account.js'; +import { myAccountOrders } from '../requests/shopper/my-account-orders.js'; import { categoryPage } from '../requests/shopper/category-page.js'; import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant.js'; import { products } from '../requests/merchant/products.js'; @@ -82,156 +83,165 @@ export const options = { }, }, thresholds: { - checks: [ 'rate==1' ], + checks: ['rate==1'], 'http_req_duration{name:Shopper - Site Root}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Shop Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Search Products}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Category Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Product Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=add_to_cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - View Cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Remove Item From Cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=apply_coupon}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Update Cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - View Checkout}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=update_order_review}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=checkout}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Order Received}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=get_refreshed_fragments}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Login to Checkout}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - My Account Login Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Login to My Account}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Orders}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Open Order}': [ + `${shopper_request_threshold}`, ], 'http_req_duration{name:Merchant - WP Login Page}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Login to WP Admin}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - WC-Admin}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/orders?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/products/reviews?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/products/low-in-stock?}': - [ `${ merchant_request_threshold }` ], + [`${merchant_request_threshold}`], 'http_req_duration{name:Merchant - All Orders}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Completed Orders}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - New Order Page}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Create New Order}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Open Order}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Update Existing Order Status}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Search Orders By Product}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Search Orders By Customer Email}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Search Orders By Customer Address}': - [ `${ merchant_request_threshold }` ], + [`${merchant_request_threshold}`], 'http_req_duration{name:Merchant - Filter Orders By Month}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Filter Orders By Customer}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - All Products}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Add New Product}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - action=sample-permalink}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - action=heartbeat autosave}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Update New Product}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Coupons}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-admin/onboarding/tasks?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/admin/notes?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-admin/options?options=woocommerce_ces_tracks_queue}': - [ `${ merchant_request_threshold }` ], + [`${merchant_request_threshold}`], 'http_req_duration{name:Merchant - action=heartbeat}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:API - Create Order}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Retrieve Order}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Update Order (Status)}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Delete Order}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Batch Create Orders}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Batch Update (Status) Orders}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], }, }; @@ -253,6 +263,7 @@ export function checkoutCustomerLoginFlow() { } export function myAccountFlow() { myAccount(); + myAccountOrders(); } export function cartFlow() { cartRemoveItem(); diff --git a/plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js b/plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js index b8049ec1152..ac98b0330f6 100644 --- a/plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js +++ b/plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js @@ -10,6 +10,7 @@ import { cartRemoveItem } from '../requests/shopper/cart-remove-item.js'; import { checkoutGuest } from '../requests/shopper/checkout-guest.js'; import { checkoutCustomerLogin } from '../requests/shopper/checkout-customer-login.js'; import { myAccount } from '../requests/shopper/my-account.js'; +import { myAccountOrders } from '../requests/shopper/my-account-orders.js'; import { categoryPage } from '../requests/shopper/category-page.js'; import { wpLogin } from '../requests/merchant/wp-login.js'; import { products } from '../requests/merchant/products.js'; @@ -138,6 +139,15 @@ export const options = { 'http_req_duration{name:Shopper - Login to My Account}': [ `${ shopper_request_threshold }`, ], + 'http_req_duration{name:Shopper - My Account}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Orders}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Open Order}': [ + `${shopper_request_threshold}`, + ], 'http_req_duration{name:Merchant - WP Login Page}': [ `${ merchant_request_threshold }`, ], @@ -254,6 +264,7 @@ export function checkoutCustomerLoginFlow() { } export function myAccountFlow() { myAccount(); + myAccountOrders(); } export function cartFlow() { cartRemoveItem(); diff --git a/plugins/woocommerce/tests/performance/tests/simple-all-requests.js b/plugins/woocommerce/tests/performance/tests/simple-all-requests.js index a2a9c5762f3..6eb654c15a0 100644 --- a/plugins/woocommerce/tests/performance/tests/simple-all-requests.js +++ b/plugins/woocommerce/tests/performance/tests/simple-all-requests.js @@ -11,6 +11,7 @@ import { cartApplyCoupon } from '../requests/shopper/cart-apply-coupon.js'; import { checkoutGuest } from '../requests/shopper/checkout-guest.js'; import { checkoutCustomerLogin } from '../requests/shopper/checkout-customer-login.js'; import { myAccount } from '../requests/shopper/my-account.js'; +import { myAccountOrders } from '../requests/shopper/my-account-orders.js'; import { categoryPage } from '../requests/shopper/category-page.js'; import { products } from '../requests/merchant/products.js'; import { addProduct } from '../requests/merchant/add-product.js'; @@ -73,7 +74,7 @@ export const options = { allMerchantSmoke: { executor: 'per-vu-iterations', vus: 1, - iterations: 1, + iterations: 2, maxDuration: '360s', exec: 'allMerchantFlow', }, @@ -86,156 +87,165 @@ export const options = { }, }, thresholds: { - checks: [ 'rate==1' ], + checks: ['rate==1'], 'http_req_duration{name:Shopper - Site Root}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Shop Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Search Products}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Category Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Product Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=add_to_cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - View Cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Remove Item From Cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=apply_coupon}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Update Cart}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - View Checkout}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=update_order_review}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=checkout}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Order Received}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - wc-ajax=get_refreshed_fragments}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Login to Checkout}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - My Account Login Page}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, ], 'http_req_duration{name:Shopper - Login to My Account}': [ - `${ shopper_request_threshold }`, + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Orders}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Open Order}': [ + `${shopper_request_threshold}`, ], 'http_req_duration{name:Merchant - WP Login Page}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Login to WP Admin}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - WC-Admin}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/orders?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/products/reviews?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/products/low-in-stock?}': - [ `${ merchant_request_threshold }` ], + [`${merchant_request_threshold}`], 'http_req_duration{name:Merchant - All Orders}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Completed Orders}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - New Order Page}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Create New Order}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Open Order}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Update Existing Order Status}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Search Orders By Product}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Search Orders By Customer Email}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Search Orders By Customer Address}': - [ `${ merchant_request_threshold }` ], + [`${merchant_request_threshold}`], 'http_req_duration{name:Merchant - Filter Orders By Month}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Filter Orders By Customer}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - All Products}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Add New Product}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - action=sample-permalink}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - action=heartbeat autosave}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Update New Product}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - Coupons}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-admin/onboarding/tasks?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-analytics/admin/notes?}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:Merchant - wc-admin/options?options=woocommerce_ces_tracks_queue}': - [ `${ merchant_request_threshold }` ], + [`${merchant_request_threshold}`], 'http_req_duration{name:Merchant - action=heartbeat}': [ - `${ merchant_request_threshold }`, + `${merchant_request_threshold}`, ], 'http_req_duration{name:API - Create Order}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Retrieve Order}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Update Order (Status)}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Delete Order}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Batch Create Orders}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], 'http_req_duration{name:API - Batch Update (Status) Orders}': [ - `${ api_request_threshold }`, + `${api_request_threshold}`, ], }, }; @@ -257,6 +267,7 @@ export function checkoutCustomerLoginFlow() { } export function myAccountFlow() { myAccount(); + myAccountOrders(); } export function cartFlow() { cartRemoveItem(); @@ -264,7 +275,7 @@ export function cartFlow() { } export function allMerchantFlow() { - if ( admin_acc_login === true ) { + if (admin_acc_login === true) { myAccountMerchantLogin(); } else { wpLogin(); diff --git a/plugins/woocommerce/tests/performance/tests/wc-baseline-load.js b/plugins/woocommerce/tests/performance/tests/wc-baseline-load.js new file mode 100644 index 00000000000..d2ae164f612 --- /dev/null +++ b/plugins/woocommerce/tests/performance/tests/wc-baseline-load.js @@ -0,0 +1,328 @@ +/* eslint-disable eqeqeq */ +/* eslint-disable no-undef */ +/** + * Internal dependencies + */ +import { homePage } from '../requests/shopper/home.js'; +import { shopPage } from '../requests/shopper/shop-page.js'; +import { searchProduct } from '../requests/shopper/search-product.js'; +import { singleProduct } from '../requests/shopper/single-product.js'; +import { cart } from '../requests/shopper/cart.js'; +import { cartRemoveItem } from '../requests/shopper/cart-remove-item.js'; +import { cartApplyCoupon } from '../requests/shopper/cart-apply-coupon.js'; +import { checkoutGuest } from '../requests/shopper/checkout-guest.js'; +import { checkoutCustomerLogin } from '../requests/shopper/checkout-customer-login.js'; +import { myAccountOrders } from '../requests/shopper/my-account-orders.js'; +import { categoryPage } from '../requests/shopper/category-page.js'; +import { products } from '../requests/merchant/products.js'; +import { addProduct } from '../requests/merchant/add-product.js'; +import { coupons } from '../requests/merchant/coupons.js'; +import { orders } from '../requests/merchant/orders.js'; +import { ordersSearch } from '../requests/merchant/orders-search.js'; +import { ordersFilter } from '../requests/merchant/orders-filter.js'; +import { addOrder } from '../requests/merchant/add-order.js'; +import { homeWCAdmin } from '../requests/merchant/home-wc-admin.js'; +import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant.js'; +import { ordersAPI } from '../requests/api/orders.js'; +import { admin_acc_login } from '../config.js'; + +const shopper_request_threshold = 'p(95)<100000'; +const merchant_request_threshold = 'p(95)<100000'; +const api_request_threshold = 'p(95)<100000'; + +export const options = { + scenarios: { + merchantOrders: { + executor: 'ramping-arrival-rate', + startRate: 2, // starting iterations per timeUnit + timeUnit: '10s', + preAllocatedVUs: 5, + maxVUs: 9, + stages: [ + // target value is iterations per timeUnit + { target: 2, duration: '60s' }, + { target: 5, duration: '500s' }, + { target: 1, duration: '60' }, + ], + exec: 'merchantOrderFlows', + }, + merchantOther: { + executor: 'ramping-arrival-rate', + startRate: 2, // starting iterations per timeUnit + timeUnit: '10s', + preAllocatedVUs: 5, + maxVUs: 9, + stages: [ + // target value is iterations per timeUnit + { target: 2, duration: '60s' }, + { target: 5, duration: '500s' }, + { target: 1, duration: '60' }, + ], + exec: 'merchantOtherFlows', + }, + shopperBrowsing: { + executor: 'ramping-arrival-rate', + startRate: 2, // starting iterations per timeUnit + timeUnit: '10s', + preAllocatedVUs: 5, + maxVUs: 9, + stages: [ + // target value is iterations per timeUnit + { target: 2, duration: '60s' }, + { target: 10, duration: '500s' }, + { target: 1, duration: '60' }, + ], + exec: 'shopperBrowsingFlows', + }, + shopperGuestCheckouts: { + executor: 'ramping-arrival-rate', + startRate: 2, // starting iterations per timeUnit + timeUnit: '10s', + preAllocatedVUs: 5, + maxVUs: 9, + stages: [ + // target value is iterations per timeUnit + { target: 2, duration: '60s' }, + { target: 5, duration: '500s' }, + { target: 1, duration: '60' }, + ], + exec: 'checkoutGuestFlow', + }, + shopperCustomerCheckouts: { + executor: 'ramping-arrival-rate', + startRate: 2, // starting iterations per timeUnit + timeUnit: '10s', + preAllocatedVUs: 5, + maxVUs: 9, + stages: [ + // target value is iterations per timeUnit + { target: 2, duration: '60s' }, + { target: 5, duration: '500s' }, + { target: 1, duration: '60' }, + ], + exec: 'checkoutCustomerLoginFlow', + }, + apiBackground: { + executor: 'ramping-arrival-rate', + startRate: 1, // starting iterations per timeUnit + timeUnit: '10s', + preAllocatedVUs: 5, + maxVUs: 5, + stages: [ + // target value is iterations per timeUnit + { target: 1, duration: '60s' }, + { target: 5, duration: '500s' }, + { target: 1, duration: '60' }, + ], + exec: 'allAPIFlow', + }, + }, + thresholds: { + 'http_req_duration{name:Shopper - Site Root}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Shop Page}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Search Products}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Category Page}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Product Page}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - wc-ajax=add_to_cart}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - View Cart}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Remove Item From Cart}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - wc-ajax=apply_coupon}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Update Cart}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - View Checkout}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - wc-ajax=update_order_review}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - wc-ajax=checkout}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Order Received}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - wc-ajax=get_refreshed_fragments}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Login to Checkout}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Login Page}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - Login to My Account}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Orders}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Shopper - My Account Open Order}': [ + `${shopper_request_threshold}`, + ], + 'http_req_duration{name:Merchant - WP Login Page}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Login to WP Admin}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - WC-Admin}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - wc-analytics/orders?}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - wc-analytics/products/reviews?}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - wc-analytics/products/low-in-stock?}': + [`${merchant_request_threshold}`], + 'http_req_duration{name:Merchant - All Orders}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Completed Orders}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - New Order Page}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Create New Order}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Open Order}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Update Existing Order Status}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Search Orders By Product}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Search Orders By Customer Email}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Search Orders By Customer Address}': + [`${merchant_request_threshold}`], + 'http_req_duration{name:Merchant - Filter Orders By Month}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Filter Orders By Customer}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - All Products}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Add New Product}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - action=sample-permalink}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - action=heartbeat autosave}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Update New Product}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - Coupons}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - wc-admin/onboarding/tasks?}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - wc-analytics/admin/notes?}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:Merchant - wc-admin/options?options=woocommerce_ces_tracks_queue}': + [`${merchant_request_threshold}`], + 'http_req_duration{name:Merchant - action=heartbeat}': [ + `${merchant_request_threshold}`, + ], + 'http_req_duration{name:API - Create Order}': [ + `${api_request_threshold}`, + ], + 'http_req_duration{name:API - Retrieve Order}': [ + `${api_request_threshold}`, + ], + 'http_req_duration{name:API - Update Order (Status)}': [ + `${api_request_threshold}`, + ], + 'http_req_duration{name:API - Delete Order}': [ + `${api_request_threshold}`, + ], + 'http_req_duration{name:API - Batch Create Orders}': [ + `${api_request_threshold}`, + ], + 'http_req_duration{name:API - Batch Update (Status) Orders}': [ + `${api_request_threshold}`, + ], + }, +}; + +// Use myAccountMerchantLogin() instead of wpLogin() if having issues with login. +export function merchantOrderFlows() { + if (admin_acc_login === true) { + myAccountMerchantLogin(); + } else { + wpLogin(); + } + addOrder(); + orders(); + ordersSearch(); + ordersFilter(); +} + +// Use myAccountMerchantLogin() instead of wpLogin() if having issues with login. +export function merchantOtherFlows() { + if (admin_acc_login === true) { + myAccountMerchantLogin(); + } else { + wpLogin(); + } + homeWCAdmin(); + addProduct(); + products(); + coupons(); +} +export function shopperBrowsingFlows() { + homePage(); + shopPage(); + searchProduct(); + singleProduct(); + cartRemoveItem(); + cartApplyCoupon(); + categoryPage(); +} +export function checkoutGuestFlow() { + cart(); + checkoutGuest(); +} +export function checkoutCustomerLoginFlow() { + cart(); + checkoutCustomerLogin(); + myAccountOrders(); +} +export function allAPIFlow() { + ordersAPI(); +} diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php index 5a271fda33e..1ef4c8c4ae6 100644 --- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php +++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php @@ -19,7 +19,7 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case { // Set default country to non-US so that 'payments' task gets added but 'woocommerce-payments' doesn't, // by default it won't be considered completed but we can manually change that as needed. update_option( 'woocommerce_default_country', 'JP' ); - $password = wp_generate_password( 8, false, false ); + $password = wp_generate_password( 8, false, false ); $this->admin = wp_insert_user( array( 'user_login' => "test_admin$password", @@ -88,11 +88,16 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case { * Tests widget does not display when task list is complete. */ public function test_widget_does_not_display_when_task_list_complete() { - $task_list = new class { + // phpcs:disable Squiz.Commenting + $task_list = new class() { public function is_complete() { return true; } + public function is_hidden() { + return false; + } }; + // phpcs:enable Squiz.Commenting $widget = $this->get_widget(); $widget->set_task_list( $task_list ); diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/COTRedirectionControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/COTRedirectionControllerTest.php new file mode 100644 index 00000000000..f15a085ff47 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/COTRedirectionControllerTest.php @@ -0,0 +1,181 @@ +sut = new COTRedirectionController(); + $this->sut->setup(); + $this->redirected_to = ''; + + add_filter( 'wp_redirect', array( $this, 'watch_and_anull_redirects' ) ); + } + + /** + * Remove our redirect listener. + * + * @return void + */ + public function tearDown(): void { + parent::tearDown(); + remove_filter( 'wp_redirect', array( $this, 'watch_and_anull_redirects' ) ); + } + + /** + * Captures the attempted redirect location, and stops the redirect from taking place. + * + * @param string $url Redirect location. + * + * @return null + */ + public function watch_and_anull_redirects( string $url ) { + $this->redirected_to = $url; + return null; + } + + /** + * Supplies the URL of the last attempted redirect, then resets ready for the next test. + * + * @return string + */ + private function get_redirect_attempt(): string { + $return = $this->redirected_to; + $this->redirected_to = ''; + return $return; + } + + /** + * Test that redirects only occur in relation to HPOS admin screen requests. + * + * @return void + */ + public function test_redirects_only_impact_hpos_admin_requests() { + $this->sut->handle_hpos_admin_requests( array( 'page' => 'wc-orders' ) ); + $this->assertNotEmpty( $this->get_redirect_attempt(), 'A redirect was attempted in relation to an HPOS admin request.' ); + + $this->sut->handle_hpos_admin_requests( array( 'page' => 'foo' ) ); + $this->assertEmpty( $this->get_redirect_attempt(), 'A redirect was not attempted in relation to a non-HPOS admin request.' ); + } + + /** + * Test order editor redirects work (in relation to creating new orders). + * + * @return void + */ + public function test_redirects_to_the_new_order_screen(): void { + $this->sut->handle_hpos_admin_requests( + array( + 'action' => 'new', + 'page' => 'wc-orders', + ) + ); + + $this->assertStringContainsString( + '/wp-admin/post-new.php?post_type=shop_order', + $this->get_redirect_attempt(), + 'Attempts to access the new order page (HPOS) are successfully redirected to the new order page (CPT).' + ); + } + + /** + * Test order editor redirects work (in relation to existing orders). + * + * @return void + */ + public function test_redirects_to_the_order_editor_screen(): void { + $this->sut->handle_hpos_admin_requests( + array( + 'action' => 'edit', + 'id' => 12345, + 'page' => 'wc-orders', + ) + ); + + $redirect_url = $this->get_redirect_attempt(); + $redirect_base = wp_parse_url( $redirect_url, PHP_URL_PATH ); + parse_str( wp_parse_url( $redirect_url, PHP_URL_QUERY ), $redirect_query ); + + $this->assertStringContainsString( + '/post.php', + $redirect_base, + 'Confirm order editor redirects go to the expected WordPress admin controller.' + ); + + $this->assertEquals( + '12345', + $redirect_query['post'], + 'Confirm order editor redirects maintain the correct order ID.' + ); + } + + /** + * Tests order list table redirects work. + * + * @return void + */ + public function test_redirects_to_the_order_admin_list_screen(): void { + $this->sut->handle_hpos_admin_requests( + array( + 'arbitrary' => '3pd-integration', + 'order' => array( + 123, + 456, + ), + 'page' => 'wc-orders', + ) + ); + + $redirect_url = $this->get_redirect_attempt(); + $redirect_base = wp_parse_url( $redirect_url, PHP_URL_PATH ); + parse_str( wp_parse_url( $redirect_url, PHP_URL_QUERY ), $redirect_query ); + + $this->assertStringContainsString( + '/edit.php', + $redirect_base, + 'Confirm order list table redirects go to the expected WordPress admin controller.' + ); + + $this->assertEquals( + array( + '123', + '456', + ), + $redirect_query['post'], + 'Confirm order list table redirects maintain a list of order IDs for bulk action requests (if one was passed).' + ); + + $this->assertEquals( + 'shop_order', + $redirect_query['post_type'], + 'Confirm order list table redirects reference the correct custom post type.' + ); + + $this->assertEquals( + '3pd-integration', + $redirect_query['arbitrary'], + 'Confirm that arbitrary query parameters are also passed across via order list table redirects.' + ); + } +} diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/DataSynchronizerTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/DataSynchronizerTests.php index f073b7c8076..e64999ec1f9 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/DataSynchronizerTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/DataSynchronizerTests.php @@ -146,4 +146,50 @@ class DataSynchronizerTests extends WC_Unit_Test_Case { 'The order was successfully copied to the COT table, outside of a dedicated synchronization batch.' ); } + + /** + * When sync is enbabled and the posts store is authoritative, creating an order and updating the status from + * draft to some non-draft status (as happens when an order is manually created in the admin environment) should + * result in the same status change being made in the duplicate COT record. + */ + public function test_status_syncs_correctly_after_order_creation() { + global $wpdb; + $orders_table = OrdersTableDataStore::get_orders_table_name(); + + // Enable sync, make the posts table authoritative. + update_option( $this->sut::ORDERS_DATA_SYNC_ENABLED_OPTION, 'yes' ); + update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' ); + + // When a new order is manually created in the admin environment, WordPress automatically creates an empty + // draft post for us. + $order_id = (int) wp_insert_post( + array( + 'post_type' => 'shop_order', + 'post_status' => 'draft', + ) + ); + + // Once the admin user decides to go ahead and save the order (ie, they click 'Create'), we start performing + // various updates to the order record. + wc_get_order( $order_id )->save(); + + // As soon as the order is saved via our own internal API, the DataSynchronizer should create a copy of the + // record in the COT table. + $this->assertEquals( + 'draft', + $wpdb->get_var( "SELECT status FROM $orders_table WHERE id = $order_id" ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + 'When HPOS is enabled but the posts data store is authoritative, saving an order will result in a duplicate with the same status being saved in the COT table.' + ); + + // In a separate operation, the status will be updated to an actual non-draft order status. This should also be + // observed by the DataSynchronizer and a further update made to the COT table. + $order = wc_get_order( $order_id ); + $order->set_status( 'pending' ); + $order->save(); + $this->assertEquals( + 'wc-pending', + $wpdb->get_var( "SELECT status FROM $orders_table WHERE id = $order_id" ), //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + 'When the order status is updated, the change should be observed by the DataSynhronizer and a matching update will take place in the COT table.' + ); + } } diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index e0b2840e719..3b512c14328 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -187,10 +187,12 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { 'status' => 'on-hold', 'cart_hash' => 'YET-ANOTHER-CART-HASH', ); + static $datastore_updates = array( 'email_sent' => true, 'order_stock_reduced' => true, ); + static $meta_to_update = array( 'my_meta_key' => array( 'my', 'custom', 'meta' ), ); @@ -478,6 +480,38 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { } } + /** + * @testdox Test the trash-untrash cycle with sync enabled. + */ + public function test_cot_datastore_untrash() { + global $wpdb; + + $this->enable_cot_sync(); + + // Tests trashing of orders. + $order = $this->create_complex_cot_order(); + $order->set_status( 'on-hold' ); + $order->save(); + $order_id = $order->get_id(); + + $this->sut->trash_order( $order ); + + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders + $orders_table = $this->sut::get_orders_table_name(); + $this->assertEquals( 'trash', $wpdb->get_var( $wpdb->prepare( "SELECT status FROM {$orders_table} WHERE id = %d", $order_id ) ) ); + $this->assertEquals( 'trash', $wpdb->get_var( $wpdb->prepare( "SELECT post_status FROM {$wpdb->posts} WHERE id = %d", $order_id ) ) ); + + $this->sut->read( $order ); + $this->sut->untrash_order( $order ); + + $this->assertEquals( 'on-hold', $order->get_status() ); + $this->assertEquals( 'wc-on-hold', get_post_status( $order_id ) ); + + $this->assertEmpty( $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$this->sut->get_meta_table_name()} WHERE order_id = %d AND meta_key LIKE '_wp_trash_meta_%'", $order_id ) ) ); + $this->assertEmpty( $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key LIKE '_wp_trash_meta_%'", $order_id ) ) ); + //phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders + } + /** * @testDox Tests the `delete()` method on the COT datastore -- full deletes. * @@ -1003,10 +1037,14 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { * @testDox Test `get_unpaid_orders()`. */ public function test_get_unpaid_orders(): void { + // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested -- Intentional usage since timezone is changed for this file. $now = current_time( 'timestamp' ); // Create a few orders. - $orders_by_status = array( 'wc-completed' => 3, 'wc-pending' => 2 ); + $orders_by_status = array( + 'wc-completed' => 3, + 'wc-pending' => 2, + ); $unpaid_ids = array(); foreach ( $orders_by_status as $order_status => $order_count ) { foreach ( range( 1, $order_count ) as $_ ) { @@ -1114,7 +1152,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $this->enable_cot_sync(); $order = $this->create_complex_cot_order(); $post_order_comparison_closure = function ( $order ) { - $post_order = $this->get_post_orders_for_ids( array( $order->get_id() ) )[ $order->get_id() ]; + $post_order = $this->get_post_orders_for_ids( array( $order->get_id() => $order ) )[ $order->get_id() ]; return $this->is_post_different_from_order( $order, $post_order ); }; @@ -1129,6 +1167,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $r_order = new WC_Order(); $r_order->set_id( $order->get_id() ); + $this->switch_data_store( $r_order, $this->sut ); // Reading again will make a call to migrate_post_record. $this->sut->read( $r_order ); $this->assertFalse( $post_order_comparison_closure->call( $this->sut, $r_order ) ); @@ -1206,7 +1245,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { /** * Helper method to allow switching data stores. * - * @param WC_Order $order Order object. + * @param WC_Order $order Order object. * @param WC_Data_Store $data_store Data store object to switch order to. */ private function switch_data_store( $order, $data_store ) { @@ -1228,6 +1267,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $order_1 = new WC_Order(); $order_1->set_billing_city( 'Fort Quality' ); $this->switch_data_store( $order_1, $this->sut ); + $this->disable_cot_sync(); $order_1->save(); $product = new WC_Product_Simple(); @@ -1257,14 +1297,43 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { ); // Order 1's billing address references "Quality" and so does one of Order 2's order items. - $query = new OrdersTableQuery( array( 's' => 'Quality' ) ); + $query = new OrdersTableQuery( array( 's' => 'Quality' ) ); + $orders_array = $query->orders; + sort( $orders_array ); $this->assertEquals( array( $order_1->get_id(), $order_2->get_id() ), - $query->orders, + $orders_array, 'Search terms match against address data as well as order item names.' ); } + /** + * @testDox Ensure search works as expected on updated orders. + */ + public function test_cot_query_search_update() { + $order_1 = new WC_Order(); + $this->switch_data_store( $order_1, $this->sut ); + $this->disable_cot_sync(); + $order_1->save(); + + $order_1->set_billing_city( 'New Cybertron' ); + $order_1->save(); + + $order_2 = new WC_Order(); + $this->switch_data_store( $order_2, $this->sut ); + $order_2->save(); + + $order_2->set_billing_city( 'Gigantian City' ); + $order_2->save(); + + $query = new OrdersTableQuery( array( 's' => 'Cybertron' ) ); + $this->assertEquals( + array( $order_1->get_id() ), + $query->orders, + 'Search terms match against updated address data.' + ); + } + /** * Test methods get_total_tax_refunded and get_total_shipping_refunded. */ @@ -1374,7 +1443,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { array( 'field' => 'order_key', 'value' => 'planck_1', - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertEqualsCanonicalizing( array( $order_ids[0], $order_ids[1] ), $query->orders ); @@ -1408,8 +1477,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { array( 'field' => 'order_key', 'value' => '[0-9]$', - 'compare' => 'RLIKE' - ) + 'compare' => 'RLIKE', + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertEqualsCanonicalizing( $order_ids, $query->orders ); @@ -1419,8 +1488,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { array( 'field' => 'order_key', 'value' => '[^0-9]$', - 'compare' => 'NOT RLIKE' - ) + 'compare' => 'NOT RLIKE', + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertCount( 0, $query->posts ); @@ -1432,7 +1501,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { 'value' => '10.0', 'compare' => '<=', 'type' => 'NUMERIC', - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertEqualsCanonicalizing( array( $order_ids[2] ), $query->orders ); @@ -1442,7 +1511,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { array( 'field' => 'non_existing_field', 'value' => 'any-value', - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertCount( 0, $query->posts ); @@ -1453,7 +1522,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { 'field' => 'wc_orders.total_amount', 'value' => 5.5, 'compare' => 'IN', - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertCount( 0, $query->posts ); @@ -1464,7 +1533,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { 'field' => 'wc_orders.total_amount', 'value' => 10.0, 'compare' => 'EXOSTS', - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertCount( 0, $query->posts ); @@ -1475,7 +1544,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { 'field' => 'total', 'compare' => 'BETWEEN', 'value' => 10.0, - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertCount( 0, $query->posts ); @@ -1486,12 +1555,12 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { 'field' => 'total', 'compare' => 'NOT BETWEEN', 'value' => array( 1.0 ), - ) + ), ); $query = new OrdersTableQuery( array( 'field_query' => $field_query ) ); $this->assertCount( 0, $query->posts ); - // Test combinations of field_query with regular query args: + // Test combinations of field_query with regular query args. $args = array( 'id' => array( $order_ids[0], $order_ids[1] ), ); @@ -1506,7 +1575,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { array( 'field' => 'id', 'value' => $order_ids[1], - ) + ), ); $query = new OrdersTableQuery( $args ); $this->assertEqualsCanonicalizing( array( $order_ids[1] ), $query->orders ); @@ -1520,7 +1589,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $query = new OrdersTableQuery( $args ); $this->assertCount( 0, $query->orders ); - // Now a more complex query with meta_query and date_query: + // Now a more complex query with meta_query and date_query. $args = array( 'shipping_address' => 'The Universe', 'field_query' => array( @@ -1537,12 +1606,13 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $this->assertEqualsCanonicalizing( array( $order_ids[0], $order_ids[1] ), $query->orders ); // ... but only Planck is more than 80 years old. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Intentional usage for test. $args['meta_query'] = array( array( 'key' => 'customer_age', 'value' => 80, - 'compare' => '>=' - ) + 'compare' => '>=', + ), ); $query = new OrdersTableQuery( $args ); $this->assertEqualsCanonicalizing( array( $order_ids[1] ), $query->orders ); @@ -1604,10 +1674,10 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { /** * Helper method to assert props are set. * - * @param array $props List of props to test. + * @param array $props List of props to test. * @param WC_Order $order Order object. - * @param mixed $value Value to assert. - * @param array $ds_getter_setter_names List of props with custom getter/setter names. + * @param mixed $value Value to assert. + * @param array $ds_getter_setter_names List of props with custom getter/setter names. */ private function assert_get_prop_via_ds_object_and_metadata( array $props, WC_Order $order, $value, array $ds_getter_setter_names ) { wp_cache_flush(); @@ -1680,8 +1750,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { * Helper function to set prop via data store. * * @param WC_Order $order Order object. - * @param array $props List of props and their setter names. - * @param mixed $value value to set. + * @param array $props List of props and their setter names. + * @param mixed $value value to set. */ private function set_props_via_data_store( $order, $props, $value ) { foreach ( $props as $meta_key_name => $prop_name ) { @@ -1693,8 +1763,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { * Helper function to set prop value via object. * * @param WC_Order $order Order object. - * @param array $props List of props and their setter names. - * @param mixed $value value to set. + * @param array $props List of props and their setter names. + * @param mixed $value value to set. */ private function set_props_via_order_object( $order, $props, $value ) { foreach ( $props as $meta_key_name => $prop_name ) { @@ -1707,8 +1777,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { * Helper function to assert prop value via data store. * * @param WC_Order $order Order object. - * @param array $props List of props and their getter names. - * @param mixed $value value to assert. + * @param array $props List of props and their getter names. + * @param mixed $value value to assert. */ private function assert_props_value_via_data_store( $order, $props, $value ) { foreach ( $props as $meta_key_name => $prop_name ) { @@ -1720,8 +1790,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { * Helper function to assert prop value via order object. * * @param WC_Order $order Order object. - * @param array $props List of props and their getter names. - * @param mixed $value value to assert. + * @param array $props List of props and their getter names. + * @param mixed $value value to assert. */ private function assert_props_value_via_order_object( $order, $props, $value ) { foreach ( $props as $meta_key_name => $prop_name ) { @@ -1769,6 +1839,8 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { * @testDox Test that multiple calls to read don't try to sync again. */ public function test_read_multiple_dont_sync_again_for_same_order() { + $this->toggle_cot( true ); + $this->enable_cot_sync(); $order = $this->create_complex_cot_order(); $order_id = $order->get_id(); @@ -1777,7 +1849,6 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { return $this->should_sync_order( $order ); }; - $this->enable_cot_sync(); $order = new WC_Order(); $order->set_id( $order_id ); $orders = array( $order_id => $order ); @@ -1785,4 +1856,60 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $this->sut->read_multiple( $orders ); $this->assertFalse( $should_sync_callable->call( $this->sut, $order ) ); } + + /** + * @testDox Make sure get_order return false when checking an order of different order types without warning. + */ + public function test_get_order_with_id_for_different_type() { + $this->toggle_cot( true ); + $this->disable_cot_sync(); + $product = new \WC_Product(); + $product->save(); + $this->assertFalse( wc_get_order( $product->get_id() ) ); + } + + /** + * @testDox Make sure that getting order type for non order return without warning. + */ + public function test_get_order_type_for_non_order() { + $product = WC_Helper_Product::create_simple_product(); + $product->save(); + $this->assertEquals( '', $this->sut->get_order_type( $product->get_id() ) ); + } + + /** + * @testDox Test get order type working as expected. + */ + public function test_get_order_type_for_order() { + $order = $this->create_complex_cot_order(); + $this->assertEquals( 'shop_order', $this->sut->get_order_type( $order->get_id() ) ); + } + + /** + * @testDox Test that we are not duplicating address indexing when updating. + */ + public function test_address_index_saved_on_update() { + global $wpdb; + $this->toggle_cot( true ); + $this->disable_cot_sync(); + $order = new WC_Order(); + $order->set_billing_address_1( '123 Main St' ); + $order->save(); + + $this->assertTrue( false !== strpos( $order->get_meta( '_billing_address_index', true ), '123 Main St' ) ); + $order = wc_get_order( $order->get_id() ); + $order->set_billing_address_2( 'Apt 1' ); + $order->save(); + + $order_meta_table = $this->sut::get_meta_table_name(); + // Assert that we are not duplicating address indexes. + $result = $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM {$order_meta_table} WHERE order_id = %d AND meta_key = '_billing_address_index'", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $order->get_id() + ) + ); + + $this->assertEquals( 1, $result ); + } } diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableRefundDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableRefundDataStoreTests.php index d720011f887..21bb0ddd481 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableRefundDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableRefundDataStoreTests.php @@ -12,6 +12,7 @@ use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper; * Class OrdersTableRefundDataStoreTests. */ class OrdersTableRefundDataStoreTests extends WC_Unit_Test_Case { + use \Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait; /** * @var PostsToOrdersMigrationController @@ -73,4 +74,31 @@ class OrdersTableRefundDataStoreTests extends WC_Unit_Test_Case { $this->assertEquals( 'Test', $refreshed_refund->get_reason() ); } + /** + * @testDox Test that refunds can be backfilled correctly. + */ + public function test_refunds_backfill() { + $this->enable_cot_sync(); + $this->toggle_cot( true ); + $order = OrderHelper::create_complex_data_store_order( $this->order_data_store ); + $refund = wc_create_refund( + array( + 'order_id' => $order->get_id(), + 'amount' => 10, + 'reason' => 'Test', + ) + ); + $refund->save(); + $this->assertTrue( $refund->get_id() > 0 ); + + // Check that data was saved. + $refreshed_refund = new WC_Order_Refund(); + $cpt_store = $this->sut->get_cpt_data_store_instance(); + $refreshed_refund->set_id( $refund->get_id() ); + $cpt_store->read( $refreshed_refund ); + $this->assertEquals( $refund->get_id(), $refreshed_refund->get_id() ); + $this->assertEquals( 10, $refreshed_refund->get_amount() ); + $this->assertEquals( 'Test', $refreshed_refund->get_reason() ); + } + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b890df03d66..178406855ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -257,7 +257,7 @@ importers: d3-shape: ^1.3.7 d3-time-format: ^2.3.0 dompurify: ^2.3.6 - downshift: ^6.1.9 + downshift: ^6.1.12 emoji-flags: ^1.3.0 eslint: ^8.12.0 gridicons: ^3.4.0 @@ -325,7 +325,7 @@ importers: d3-shape: 1.3.7 d3-time-format: 2.3.0 dompurify: 2.3.6 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 emoji-flags: 1.3.0 gridicons: 3.4.0_react@17.0.2 lodash: 4.17.21 @@ -1307,7 +1307,7 @@ importers: '@babel/core': 7.12.9 '@babel/preset-env': 7.12.7 '@babel/register': 7.12.1 - '@playwright/test': ^1.26.1 + '@playwright/test': ^1.27.1 '@typescript-eslint/eslint-plugin': 3.10.1 '@typescript-eslint/experimental-utils': 3.10.1 '@typescript-eslint/parser': 3.10.1 @@ -1326,19 +1326,20 @@ importers: allure-commandline: ^2.17.2 allure-playwright: ^2.0.0-beta.16 autoprefixer: 9.8.6 + axios: ^0.24.0 babel-eslint: 10.1.0 chai: 4.2.0 chai-as-promised: 7.1.1 config: 3.3.3 cross-env: 6.0.3 deasync: 0.1.26 + dotenv: ^10.0.0 eslint: ^8.12.0 eslint-config-wpcalypso: 5.0.0 eslint-plugin-jest: 23.20.0 istanbul: 1.0.0-alpha.2 jest: ^27.5.1 mocha: 7.2.0 - playwright: ^1.26.1 prettier: npm:wp-prettier@2.0.5 stylelint: ^13.8.0 typescript: ^4.8.3 @@ -1351,7 +1352,7 @@ importers: '@babel/core': 7.12.9 '@babel/preset-env': 7.12.7_@babel+core@7.12.9 '@babel/register': 7.12.1_@babel+core@7.12.9 - '@playwright/test': 1.26.1 + '@playwright/test': 1.27.1 '@typescript-eslint/eslint-plugin': 3.10.1_s5hr7yeqqy6e4q6twdgyz7l2pu '@typescript-eslint/experimental-utils': 3.10.1_z4bbprzjrhnsfa24uvmcbu7f5q '@typescript-eslint/parser': 3.10.1_z4bbprzjrhnsfa24uvmcbu7f5q @@ -1370,19 +1371,20 @@ importers: allure-commandline: 2.18.1 allure-playwright: 2.0.0-beta.19 autoprefixer: 9.8.6 + axios: 0.24.0 babel-eslint: 10.1.0_eslint@8.25.0 chai: 4.2.0 chai-as-promised: 7.1.1_chai@4.2.0 config: 3.3.3 cross-env: 6.0.3 deasync: 0.1.26 + dotenv: 10.0.0 eslint: 8.25.0 eslint-config-wpcalypso: 5.0.0_44ie4thsizlsiqkjwba6wctao4 eslint-plugin-jest: 23.20.0_z4bbprzjrhnsfa24uvmcbu7f5q istanbul: 1.0.0-alpha.2 jest: 27.5.1 mocha: 7.2.0 - playwright: 1.26.1 prettier: /wp-prettier/2.0.5 stylelint: 13.13.1 typescript: 4.8.4 @@ -1425,6 +1427,7 @@ importers: '@types/react-router-dom': ^5.3.3 '@types/react-transition-group': ^4.4.4 '@types/testing-library__jest-dom': ^5.14.3 + '@types/wordpress__blocks': ^11.0.7 '@types/wordpress__components': ^19.10.1 '@types/wordpress__compose': ^4.0.1 '@types/wordpress__data': ^6.0.0 @@ -1459,6 +1462,8 @@ importers: '@wordpress/api-fetch': ^6.0.1 '@wordpress/babel-preset-default': ^6.5.1 '@wordpress/base-styles': ^4.3.0 + '@wordpress/block-editor': ^9.8.0 + '@wordpress/blocks': ^11.17.0 '@wordpress/browserslist-config': ^4.1.1 '@wordpress/components': ^19.5.0 '@wordpress/compose': ^5.1.2 @@ -1565,12 +1570,14 @@ importers: '@automattic/explat-client-react-helpers': 0.0.4 '@automattic/interpolate-components': 1.2.1_adlholpkqbiq5amp2fy4vkqcli '@react-spring/web': 9.4.4_sfoxds7t5ydpegc3knd667wn6m + '@types/wordpress__blocks': 11.0.7_sfoxds7t5ydpegc3knd667wn6m '@woocommerce/api': link:../../packages/js/api '@woocommerce/e2e-environment': link:../../packages/js/e2e-environment '@woocommerce/e2e-utils': link:../../packages/js/e2e-utils '@wordpress/a11y': 3.5.0 '@wordpress/api-fetch': 6.1.1 '@wordpress/base-styles': 4.3.0 + '@wordpress/blocks': 11.18.0_react@17.0.2 '@wordpress/components': 19.6.1_lxraipvdfcmyzw3sdzk3k7kygu '@wordpress/compose': 5.2.1_react@17.0.2 '@wordpress/core-data': 4.2.1_react@17.0.2 @@ -1666,6 +1673,7 @@ importers: '@woocommerce/onboarding': link:../../packages/js/onboarding '@woocommerce/tracks': link:../../packages/js/tracks '@wordpress/babel-preset-default': 6.6.1 + '@wordpress/block-editor': 9.8.0_67fiyx7k2wr2ple2yfldahug5u '@wordpress/browserslist-config': 4.1.2 '@wordpress/custom-templated-path-webpack-plugin': 2.1.2_webpack@5.70.0 '@wordpress/jest-preset-default': 8.1.1_lensd4roph2efz67jc3dgzevhq @@ -2563,6 +2571,7 @@ packages: dependencies: '@babel/helper-explode-assignable-expression': 7.18.6 '@babel/types': 7.19.3 + dev: true /@babel/helper-builder-binary-assignment-operator-visitor/7.18.9: resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==} @@ -2577,39 +2586,13 @@ packages: peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/compat-data': 7.17.7 + '@babel/compat-data': 7.19.3 '@babel/core': 7.12.9 - '@babel/helper-validator-option': 7.16.7 + '@babel/helper-validator-option': 7.18.6 browserslist: 4.20.4 semver: 6.3.0 dev: true - /@babel/helper-compilation-targets/7.17.7_@babel+core@7.12.9: - resolution: {integrity: sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.17.7 - '@babel/core': 7.12.9 - '@babel/helper-validator-option': 7.16.7 - browserslist: 4.20.4 - semver: 6.3.0 - dev: true - - /@babel/helper-compilation-targets/7.17.7_@babel+core@7.16.12: - resolution: {integrity: sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.17.7 - '@babel/core': 7.16.12 - '@babel/helper-validator-option': 7.16.7 - browserslist: 4.20.4 - semver: 6.3.0 - dev: false - /@babel/helper-compilation-targets/7.17.7_@babel+core@7.17.8: resolution: {integrity: sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==} engines: {node: '>=6.9.0'} @@ -2826,6 +2809,29 @@ packages: '@babel/core': 7.17.8 '@babel/helper-annotate-as-pure': 7.18.6 regexpu-core: 5.2.1 + dev: true + + /@babel/helper-create-regexp-features-plugin/7.19.0_@babel+core@7.12.9: + resolution: {integrity: sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-annotate-as-pure': 7.18.6 + regexpu-core: 5.2.1 + dev: true + + /@babel/helper-create-regexp-features-plugin/7.19.0_@babel+core@7.16.12: + resolution: {integrity: sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-annotate-as-pure': 7.18.6 + regexpu-core: 5.2.1 + dev: false /@babel/helper-create-regexp-features-plugin/7.19.0_@babel+core@7.17.8: resolution: {integrity: sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==} @@ -3384,7 +3390,7 @@ packages: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.16.7_@babel+core@7.16.12: @@ -3394,7 +3400,7 @@ packages: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.16.7_@babel+core@7.17.8: @@ -3541,7 +3547,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-class-features-plugin': 7.16.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 transitivePeerDependencies: - supports-color dev: true @@ -3672,7 +3678,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.12.9 dev: true @@ -3683,7 +3689,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.12.9 dev: true @@ -3694,7 +3700,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.16.12 dev: false @@ -3736,7 +3742,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.12.9 dev: true @@ -3747,7 +3753,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.12.9 dev: true @@ -3758,7 +3764,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.16.12 dev: false @@ -3790,7 +3796,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.12.9 dev: true @@ -3801,7 +3807,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.12.9 dev: true @@ -3812,7 +3818,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.16.12 dev: false @@ -3844,7 +3850,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.12.9 dev: true @@ -3855,7 +3861,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.12.9 dev: true @@ -3866,7 +3872,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.16.12 dev: false @@ -3898,7 +3904,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.12.9 dev: true @@ -3951,7 +3957,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.12.9 dev: true @@ -3962,7 +3968,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.12.9 dev: true @@ -3973,7 +3979,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.16.12 dev: false @@ -4029,12 +4035,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.17.7 + '@babel/compat-data': 7.19.3 '@babel/core': 7.12.9 '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.12.9 - '@babel/plugin-transform-parameters': 7.16.7_@babel+core@7.12.9 + '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.12.9 dev: true /@babel/plugin-proposal-object-rest-spread/7.17.3_@babel+core@7.16.12: @@ -4043,12 +4049,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.17.7 + '@babel/compat-data': 7.19.3 '@babel/core': 7.16.12 '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.16.12 - '@babel/plugin-transform-parameters': 7.16.7_@babel+core@7.16.12 + '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.16.12 dev: false /@babel/plugin-proposal-object-rest-spread/7.17.3_@babel+core@7.17.8: @@ -4085,7 +4091,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.12.9 dev: true @@ -4096,7 +4102,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.12.9 dev: true @@ -4107,7 +4113,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.16.12 dev: false @@ -4139,7 +4145,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.12.9 dev: true @@ -4222,7 +4228,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-class-features-plugin': 7.16.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 transitivePeerDependencies: - supports-color dev: true @@ -4287,7 +4293,7 @@ packages: '@babel/core': 7.12.9 '@babel/helper-annotate-as-pure': 7.16.7 '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.12.9 transitivePeerDependencies: - supports-color @@ -4302,7 +4308,7 @@ packages: '@babel/core': 7.16.12 '@babel/helper-annotate-as-pure': 7.16.7 '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.16.12 transitivePeerDependencies: - supports-color @@ -4345,7 +4351,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-regexp-features-plugin': 7.16.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-proposal-unicode-property-regex/7.16.7_@babel+core@7.12.9: @@ -4379,6 +4385,29 @@ packages: '@babel/core': 7.17.8 '@babel/helper-create-regexp-features-plugin': 7.17.0_@babel+core@7.17.8 '@babel/helper-plugin-utils': 7.18.9 + dev: true + + /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} + engines: {node: '>=4'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} + engines: {node: '>=4'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} @@ -4464,7 +4493,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.16.12: @@ -4474,7 +4503,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.17.8: @@ -4484,7 +4513,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-decorators/7.16.0_@babel+core@7.17.8: resolution: {integrity: sha512-nxnnngZClvlY13nHJAIDow0S7Qzhq64fQ/NlqS+VER3kjW/4F0jLhXjeL8jcwSwz6Ca3rotT5NJD2T9I7lcv7g==} @@ -4502,7 +4531,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.16.12: @@ -4511,7 +4540,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.17.8: @@ -4520,7 +4549,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-export-default-from/7.16.7_@babel+core@7.17.8: resolution: {integrity: sha512-4C3E4NsrLOgftKaTYTULhHsuQrGv3FHrBzOMDiS7UYKIpgGBkAdawg4h+EI8zPeK9M0fiIIh72hIwsI24K7MbA==} @@ -4598,7 +4627,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.16.12: resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} @@ -4606,7 +4635,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.17.8: @@ -4615,7 +4644,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-jsx/7.12.1_@babel+core@7.12.9: resolution: {integrity: sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==} @@ -4633,7 +4662,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-syntax-jsx/7.16.0_@babel+core@7.16.12: @@ -4643,7 +4672,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-jsx/7.16.7_@babel+core@7.17.8: @@ -4721,7 +4750,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.16.12: resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} @@ -4729,7 +4758,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.17.8: @@ -4738,7 +4767,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.12.9: resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} @@ -4746,7 +4775,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.16.12: resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} @@ -4754,7 +4783,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.17.8: @@ -4763,7 +4792,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.12.9: resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} @@ -4771,7 +4800,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.16.12: resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} @@ -4779,7 +4808,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.17.8: @@ -4788,7 +4817,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.12.9: resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} @@ -4851,7 +4880,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.16.12: resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} @@ -4860,7 +4889,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.17.8: @@ -4870,7 +4899,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 /@babel/plugin-syntax-typescript/7.16.7_@babel+core@7.16.12: resolution: {integrity: sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==} @@ -4907,7 +4936,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-arrow-functions/7.16.7_@babel+core@7.12.9: @@ -4917,7 +4946,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-arrow-functions/7.16.7_@babel+core@7.16.12: @@ -4927,7 +4956,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-arrow-functions/7.16.7_@babel+core@7.17.8: @@ -4957,7 +4986,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-module-imports': 7.16.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-remap-async-to-generator': 7.16.4 transitivePeerDependencies: - supports-color @@ -5025,7 +5054,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-block-scoped-functions/7.16.7_@babel+core@7.12.9: @@ -5035,7 +5064,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-block-scoped-functions/7.16.7_@babel+core@7.16.12: @@ -5045,7 +5074,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-block-scoped-functions/7.16.7_@babel+core@7.17.8: @@ -5074,7 +5103,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-block-scoping/7.16.7_@babel+core@7.12.9: @@ -5084,7 +5113,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-block-scoping/7.16.7_@babel+core@7.16.12: @@ -5094,7 +5123,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-block-scoping/7.16.7_@babel+core@7.17.8: @@ -5126,7 +5155,7 @@ packages: '@babel/helper-annotate-as-pure': 7.16.0 '@babel/helper-function-name': 7.16.0 '@babel/helper-optimise-call-expression': 7.16.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-replace-supers': 7.16.0 '@babel/helper-split-export-declaration': 7.16.0 globals: 11.12.0 @@ -5217,7 +5246,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-computed-properties/7.16.7_@babel+core@7.12.9: @@ -5227,7 +5256,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-computed-properties/7.16.7_@babel+core@7.16.12: @@ -5237,7 +5266,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-computed-properties/7.16.7_@babel+core@7.17.8: @@ -5266,7 +5295,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-destructuring/7.17.7_@babel+core@7.12.9: @@ -5276,7 +5305,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-destructuring/7.17.7_@babel+core@7.16.12: @@ -5286,7 +5315,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-destructuring/7.17.7_@babel+core@7.17.8: @@ -5316,7 +5345,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-regexp-features-plugin': 7.16.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-dotall-regex/7.16.7_@babel+core@7.12.9: @@ -5350,6 +5379,29 @@ packages: '@babel/core': 7.17.8 '@babel/helper-create-regexp-features-plugin': 7.17.0_@babel+core@7.17.8 '@babel/helper-plugin-utils': 7.18.9 + dev: true + + /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.12.9: + resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.12.9 + '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + dev: true + + /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.16.12: + resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.17.8: resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} @@ -5368,7 +5420,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-duplicate-keys/7.16.7_@babel+core@7.12.9: @@ -5378,7 +5430,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-duplicate-keys/7.16.7_@babel+core@7.16.12: @@ -5388,7 +5440,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-duplicate-keys/7.16.7_@babel+core@7.17.8: @@ -5418,7 +5470,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-builder-binary-assignment-operator-visitor': 7.16.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-exponentiation-operator/7.16.7_@babel+core@7.12.9: @@ -5428,8 +5480,8 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.16.7 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-exponentiation-operator/7.16.7_@babel+core@7.16.12: @@ -5439,8 +5491,8 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.16.7 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-exponentiation-operator/7.16.7_@babel+core@7.17.8: @@ -5481,7 +5533,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-for-of/7.16.7_@babel+core@7.12.9: @@ -5491,7 +5543,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-for-of/7.16.7_@babel+core@7.16.12: @@ -5501,7 +5553,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-for-of/7.16.7_@babel+core@7.17.8: @@ -5531,7 +5583,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-function-name': 7.16.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-function-name/7.16.7_@babel+core@7.12.9: @@ -5588,7 +5640,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-literals/7.16.7_@babel+core@7.12.9: @@ -5598,7 +5650,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-literals/7.16.7_@babel+core@7.16.12: @@ -5608,7 +5660,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-literals/7.16.7_@babel+core@7.17.8: @@ -5637,7 +5689,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-member-expression-literals/7.16.7_@babel+core@7.12.9: @@ -5647,7 +5699,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-member-expression-literals/7.16.7_@babel+core@7.16.12: @@ -5657,7 +5709,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-member-expression-literals/7.16.7_@babel+core@7.17.8: @@ -5687,7 +5739,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: - supports-color @@ -5756,7 +5808,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-simple-access': 7.16.0 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -5830,7 +5882,7 @@ packages: '@babel/core': 7.12.9 '@babel/helper-hoist-variables': 7.16.0 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-validator-identifier': 7.15.7 babel-plugin-dynamic-import-node: 2.3.3 transitivePeerDependencies: @@ -5908,7 +5960,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 transitivePeerDependencies: - supports-color dev: true @@ -6021,7 +6073,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-new-target/7.16.7_@babel+core@7.12.9: @@ -6031,7 +6083,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-new-target/7.16.7_@babel+core@7.16.12: @@ -6041,7 +6093,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-new-target/7.16.7_@babel+core@7.17.8: @@ -6070,7 +6122,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-replace-supers': 7.16.0 transitivePeerDependencies: - supports-color @@ -6134,7 +6186,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-parameters/7.16.7_@babel+core@7.12.9: @@ -6144,7 +6196,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-parameters/7.16.7_@babel+core@7.16.12: @@ -6154,7 +6206,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-parameters/7.16.7_@babel+core@7.17.8: @@ -6164,7 +6216,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-parameters/7.18.8_@babel+core@7.12.9: @@ -6177,6 +6229,16 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true + /@babel/plugin-transform-parameters/7.18.8_@babel+core@7.16.12: + resolution: {integrity: sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + dev: false + /@babel/plugin-transform-parameters/7.18.8_@babel+core@7.17.8: resolution: {integrity: sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==} engines: {node: '>=6.9.0'} @@ -6193,7 +6255,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-property-literals/7.16.7_@babel+core@7.12.9: @@ -6203,7 +6265,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-property-literals/7.16.7_@babel+core@7.16.12: @@ -6213,7 +6275,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-property-literals/7.16.7_@babel+core@7.17.8: @@ -6405,7 +6467,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-reserved-words/7.16.7_@babel+core@7.12.9: @@ -6415,7 +6477,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-reserved-words/7.16.7_@babel+core@7.16.12: @@ -6425,7 +6487,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-reserved-words/7.16.7_@babel+core@7.17.8: @@ -6471,9 +6533,9 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-module-imports': 7.16.0 - '@babel/helper-plugin-utils': 7.14.5 - babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.16.12 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.19.0 + babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.16.12 babel-plugin-polyfill-corejs3: 0.4.0_@babel+core@7.16.12 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.16.12 semver: 6.3.0 @@ -6488,9 +6550,9 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-module-imports': 7.16.0 - '@babel/helper-plugin-utils': 7.14.5 - babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.17.8 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.19.0 + babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.17.8 babel-plugin-polyfill-corejs3: 0.4.0_@babel+core@7.17.8 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.17.8 semver: 6.3.0 @@ -6521,7 +6583,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-shorthand-properties/7.16.7_@babel+core@7.12.9: @@ -6531,7 +6593,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-shorthand-properties/7.16.7_@babel+core@7.16.12: @@ -6541,7 +6603,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-shorthand-properties/7.16.7_@babel+core@7.17.8: @@ -6570,7 +6632,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/helper-skip-transparent-expression-wrappers': 7.16.0 dev: true @@ -6624,7 +6686,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-sticky-regex/7.16.7_@babel+core@7.12.9: @@ -6634,7 +6696,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-sticky-regex/7.16.7_@babel+core@7.16.12: @@ -6644,7 +6706,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-sticky-regex/7.16.7_@babel+core@7.17.8: @@ -6673,7 +6735,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-template-literals/7.16.7_@babel+core@7.12.9: @@ -6683,7 +6745,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-template-literals/7.16.7_@babel+core@7.16.12: @@ -6693,7 +6755,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-template-literals/7.16.7_@babel+core@7.17.8: @@ -6722,7 +6784,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-typeof-symbol/7.16.7_@babel+core@7.12.9: @@ -6732,7 +6794,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-typeof-symbol/7.16.7_@babel+core@7.16.12: @@ -6742,7 +6804,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-typeof-symbol/7.16.7_@babel+core@7.17.8: @@ -6786,7 +6848,7 @@ packages: dependencies: '@babel/core': 7.17.8 '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.17.8 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 '@babel/plugin-syntax-typescript': 7.16.7_@babel+core@7.17.8 transitivePeerDependencies: - supports-color @@ -6811,7 +6873,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-unicode-escapes/7.16.7_@babel+core@7.12.9: @@ -6821,7 +6883,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-unicode-escapes/7.16.7_@babel+core@7.16.12: @@ -6831,7 +6893,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-unicode-escapes/7.16.7_@babel+core@7.17.8: @@ -6861,7 +6923,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-regexp-features-plugin': 7.16.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-unicode-regex/7.16.7_@babel+core@7.12.9: @@ -6872,7 +6934,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-regexp-features-plugin': 7.17.0_@babel+core@7.12.9 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: true /@babel/plugin-transform-unicode-regex/7.16.7_@babel+core@7.16.12: @@ -6883,7 +6945,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-create-regexp-features-plugin': 7.17.0_@babel+core@7.16.12 - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.19.0 dev: false /@babel/plugin-transform-unicode-regex/7.16.7_@babel+core@7.17.8: @@ -6999,7 +7061,7 @@ packages: dependencies: '@babel/compat-data': 7.17.7 '@babel/core': 7.12.9 - '@babel/helper-compilation-targets': 7.17.7_@babel+core@7.12.9 + '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.12.9 '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-validator-option': 7.16.7 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.16.7_@babel+core@7.12.9 @@ -7066,7 +7128,7 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.16.7_@babel+core@7.12.9 '@babel/plugin-transform-unicode-regex': 7.16.7_@babel+core@7.12.9 '@babel/preset-modules': 0.1.5_@babel+core@7.12.9 - '@babel/types': 7.17.0 + '@babel/types': 7.19.3 babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.12.9 babel-plugin-polyfill-corejs3: 0.5.2_@babel+core@7.12.9 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.12.9 @@ -7084,7 +7146,7 @@ packages: dependencies: '@babel/compat-data': 7.17.7 '@babel/core': 7.16.12 - '@babel/helper-compilation-targets': 7.17.7_@babel+core@7.16.12 + '@babel/helper-compilation-targets': 7.19.3_@babel+core@7.16.12 '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-validator-option': 7.16.7 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.16.7_@babel+core@7.16.12 @@ -7151,7 +7213,7 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.16.7_@babel+core@7.16.12 '@babel/plugin-transform-unicode-regex': 7.16.7_@babel+core@7.16.12 '@babel/preset-modules': 0.1.5_@babel+core@7.16.12 - '@babel/types': 7.17.0 + '@babel/types': 7.19.3 babel-plugin-polyfill-corejs2: 0.3.0_@babel+core@7.16.12 babel-plugin-polyfill-corejs3: 0.5.2_@babel+core@7.16.12 babel-plugin-polyfill-regenerator: 0.3.0_@babel+core@7.16.12 @@ -7348,9 +7410,9 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/plugin-proposal-unicode-property-regex': 7.16.7_@babel+core@7.12.9 - '@babel/plugin-transform-dotall-regex': 7.16.7_@babel+core@7.12.9 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.12.9 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.12.9 '@babel/types': 7.19.3 esutils: 2.0.3 dev: true @@ -7361,9 +7423,9 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/plugin-proposal-unicode-property-regex': 7.16.7_@babel+core@7.16.12 - '@babel/plugin-transform-dotall-regex': 7.16.7_@babel+core@7.16.12 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.16.12 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.16.12 '@babel/types': 7.19.3 esutils: 2.0.3 dev: false @@ -7374,9 +7436,9 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/plugin-proposal-unicode-property-regex': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-dotall-regex': 7.16.7_@babel+core@7.17.8 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.17.8 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.17.8 '@babel/types': 7.19.3 esutils: 2.0.3 @@ -7402,8 +7464,8 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/helper-validator-option': 7.16.7 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-validator-option': 7.18.6 '@babel/plugin-transform-typescript': 7.16.8_@babel+core@7.16.12 transitivePeerDependencies: - supports-color @@ -7590,7 +7652,6 @@ packages: /@discoveryjs/json-ext/0.5.7: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} - dev: true /@emotion/babel-plugin/11.7.2_@babel+core@7.17.8: resolution: {integrity: sha512-6mGSCWi9UzXut/ZAN6lGFu33wGR3SJisNl3c0tvlmb8XChH1b2SUvxvnOh7hvLpqyRdHHU9AiazV3Cwbk5SXKQ==} @@ -7610,7 +7671,6 @@ packages: find-root: 1.1.0 source-map: 0.5.7 stylis: 4.0.13 - dev: false /@emotion/cache/10.0.29: resolution: {integrity: sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==} @@ -7628,7 +7688,6 @@ packages: '@emotion/utils': 1.1.0 '@emotion/weak-memoize': 0.2.5 stylis: 4.0.13 - dev: false /@emotion/core/10.3.1_react@16.14.0: resolution: {integrity: sha512-447aUEjPIm0MnE6QYIaFz9VQOHSXf4Iu6EWOIqq11EAPqinkSZmfymPTmlOE3QjLv846lH4JVZBUOtwGbuQoww==} @@ -7679,13 +7738,13 @@ packages: '@emotion/serialize': 1.0.2 '@emotion/sheet': 1.1.0 '@emotion/utils': 1.1.0 - dev: false /@emotion/hash/0.8.0: resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} /@emotion/is-prop-valid/0.8.8: resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} + requiresBuild: true dependencies: '@emotion/memoize': 0.7.4 @@ -7693,14 +7752,12 @@ packages: resolution: {integrity: sha512-3QnhqeL+WW88YjYbQL5gUIkthuMw7a0NGbZ7wfFVk2kg/CK5w8w5FFa0RzWjyY1+sujN0NWbtSHH6OJmWHtJpQ==} dependencies: '@emotion/memoize': 0.7.5 - dev: false /@emotion/memoize/0.7.4: resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} /@emotion/memoize/0.7.5: resolution: {integrity: sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==} - dev: false /@emotion/native/10.0.27_peeplpcor766cv2dor4ihhuuki: resolution: {integrity: sha512-3qxR2XFizGfABKKbX9kAYc0PHhKuCEuyxshoq3TaMEbi9asWHdQVChg32ULpblm4XAf9oxaitAU7J9SfdwFxtw==} @@ -7770,7 +7827,6 @@ packages: '@types/react': 17.0.40 hoist-non-react-statics: 3.3.2 react: 17.0.2 - dev: false /@emotion/serialize/0.11.16: resolution: {integrity: sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==} @@ -7789,14 +7845,12 @@ packages: '@emotion/unitless': 0.7.5 '@emotion/utils': 1.1.0 csstype: 3.0.10 - dev: false /@emotion/sheet/0.9.4: resolution: {integrity: sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==} /@emotion/sheet/1.1.0: resolution: {integrity: sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==} - dev: false /@emotion/styled-base/10.3.0_gfrer23gq2rp2t523t6qbxrx6m: resolution: {integrity: sha512-PBRqsVKR7QRNkmfH78hTSSwHWcwDpecH9W6heujWAcyp2wdz/64PP73s7fWS1dIPm8/Exc8JAzYS8dEWXjv60w==} @@ -7872,7 +7926,6 @@ packages: '@emotion/utils': 1.1.0 '@types/react': 17.0.40 react: 17.0.2 - dev: false /@emotion/styled/11.8.1_lddnk6nv2rrayprsm6yu5n7lz4: resolution: {integrity: sha512-OghEVAYBZMpEquHZwuelXcRjRJQOVayvbmNR0zr174NHdmMgrNkLC6TljKC5h9lZLkN5WGrdUcrKlOJ4phhoTQ==} @@ -7912,7 +7965,6 @@ packages: /@emotion/utils/1.1.0: resolution: {integrity: sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ==} - dev: false /@emotion/weak-memoize/0.2.5: resolution: {integrity: sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==} @@ -8015,7 +8067,6 @@ packages: /@floating-ui/core/1.0.1: resolution: {integrity: sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA==} - dev: false /@floating-ui/dom/0.4.5: resolution: {integrity: sha512-b+prvQgJt8pieaKYMSJBXHxX/DYwdLsAWxKYqnO5dO2V4oo/TYBZJAUQCVNjTWWsrs6o4VDrNcP9+E70HAhJdw==} @@ -8027,7 +8078,6 @@ packages: resolution: {integrity: sha512-5X9WSvZ8/fjy3gDu8yx9HAA4KG1lazUN2P4/VnaXLxTO9Dz53HI1oYoh1OlhqFNlHgGDiwFX5WhFCc2ljbW3yA==} dependencies: '@floating-ui/core': 1.0.1 - dev: false /@floating-ui/react-dom/0.6.3_6rln7q2jvtuewdvbdwpg4txtvm: resolution: {integrity: sha512-hC+pS5D6AgS2wWjbmSQ6UR6Kpy+drvWGJIri6e1EDGADTPsCaa4KzCgmCczHrQeInx9tqs81EyDmbKJYY2swKg==} @@ -8052,7 +8102,6 @@ packages: '@floating-ui/dom': 1.0.2 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 - dev: false /@gar/promisify/1.1.3: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} @@ -9626,13 +9675,13 @@ packages: dependencies: '@octokit/openapi-types': 13.10.0 - /@playwright/test/1.26.1: - resolution: {integrity: sha512-bNxyZASVt2adSZ9gbD7NCydzcb5JaI0OR9hc7s+nmPeH604gwp0zp17NNpwXY4c8nvuBGQQ9oGDx72LE+cUWvw==} + /@playwright/test/1.27.1: + resolution: {integrity: sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==} engines: {node: '>=14'} hasBin: true dependencies: '@types/node': 17.0.21 - playwright-core: 1.26.1 + playwright-core: 1.27.1 dev: true /@pmmmwh/react-refresh-webpack-plugin/0.5.1_a3gyllrqvxpec3fpybsrposvju: @@ -9880,7 +9929,6 @@ packages: '@react-spring/shared': 9.5.5_react@17.0.2 '@react-spring/types': 9.5.5 react: 17.0.2 - dev: false /@react-spring/core/9.4.4_react@17.0.2: resolution: {integrity: sha512-llgb0ljFyjMB0JhWsaFHOi9XFT8n1jBMVs1IFY2ipIBerWIRWrgUmIpakLPHTa4c4jwqTaDSwX90s2a0iN7dxQ==} @@ -9904,7 +9952,6 @@ packages: '@react-spring/shared': 9.5.5_react@17.0.2 '@react-spring/types': 9.5.5 react: 17.0.2 - dev: false /@react-spring/rafz/9.4.4: resolution: {integrity: sha512-5ki/sQ06Mdf8AuFstSt5zbNNicRT4LZogiJttDAww1ozhuvemafNWEHxhzcULgCPCDu2s7HsroaISV7+GQWrhw==} @@ -9912,7 +9959,6 @@ packages: /@react-spring/rafz/9.5.5: resolution: {integrity: sha512-F/CLwB0d10jL6My5vgzRQxCNY2RNyDJZedRBK7FsngdCmzoq3V4OqqNc/9voJb9qRC2wd55oGXUeXv2eIaFmsw==} - dev: false /@react-spring/shared/9.4.4_react@17.0.2: resolution: {integrity: sha512-ySVgScDZlhm/+Iy2smY9i/DDrShArY0j6zjTS/Re1lasKnhq8qigoGiAxe8xMPJNlCaj3uczCqHy3TY9bKRtfQ==} @@ -9932,7 +9978,6 @@ packages: '@react-spring/rafz': 9.5.5 '@react-spring/types': 9.5.5 react: 17.0.2 - dev: false /@react-spring/types/9.4.4: resolution: {integrity: sha512-KpxKt/D//q/t/6FBcde/RE36LKp8PpWu7kFEMLwpzMGl9RpcexunmYOQJWwmJWtkQjgE1YRr7DzBMryz6La1cQ==} @@ -9940,7 +9985,6 @@ packages: /@react-spring/types/9.5.5: resolution: {integrity: sha512-7I/qY8H7Enwasxr4jU6WmtNK+RZ4Z/XvSlDvjXFVe7ii1x0MoSlkw6pD7xuac8qrHQRm9BTcbZNyeeKApYsvCg==} - dev: false /@react-spring/web/9.4.4_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-iJmOLdhcuizriUlu/xqBc5y8KaFts+UI+iC+GxyTwBtzxA9czKiSAZW2ESuhG8stafa3jncwjfTQQp84KN36cw==} @@ -9968,7 +10012,6 @@ packages: '@react-spring/types': 9.5.5 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 - dev: false /@romainberger/css-diff/1.0.3: resolution: {integrity: sha512-zR2EvxtJvQXRxFtTnqazMsJADngyVIulzYQ+wVYWRC1Hw3e4gfEIbigX46wTsPUyjAI+lRXFrBSoCWcgZ6ZSlQ==} @@ -10664,26 +10707,26 @@ packages: optional: true dependencies: '@babel/core': 7.17.8 - '@babel/plugin-proposal-class-properties': 7.16.7_@babel+core@7.17.8 + '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.17.8 '@babel/plugin-proposal-decorators': 7.16.4_@babel+core@7.17.8 '@babel/plugin-proposal-export-default-from': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-proposal-nullish-coalescing-operator': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-proposal-object-rest-spread': 7.17.3_@babel+core@7.17.8 - '@babel/plugin-proposal-optional-chaining': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-proposal-private-methods': 7.16.11_@babel+core@7.17.8 + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.17.8 + '@babel/plugin-proposal-object-rest-spread': 7.18.9_@babel+core@7.17.8 + '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.17.8 + '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.17.8 '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.17.8 - '@babel/plugin-transform-arrow-functions': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-block-scoping': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-classes': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-destructuring': 7.17.7_@babel+core@7.17.8 - '@babel/plugin-transform-for-of': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-parameters': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-shorthand-properties': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-spread': 7.16.7_@babel+core@7.17.8 - '@babel/plugin-transform-template-literals': 7.16.7_@babel+core@7.17.8 + '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.17.8 + '@babel/plugin-transform-block-scoping': 7.18.9_@babel+core@7.17.8 + '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.17.8 + '@babel/plugin-transform-destructuring': 7.18.13_@babel+core@7.17.8 + '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.17.8 + '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.17.8 + '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.17.8 + '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.17.8 + '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.17.8 '@babel/preset-env': 7.19.3_@babel+core@7.17.8 '@babel/preset-react': 7.16.7_@babel+core@7.17.8 - '@babel/preset-typescript': 7.16.7_@babel+core@7.17.8 + '@babel/preset-typescript': 7.18.6_@babel+core@7.17.8 '@storybook/addons': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/api': 6.4.19_sfoxds7t5ydpegc3knd667wn6m '@storybook/channel-postmessage': 6.4.19 @@ -10707,7 +10750,7 @@ packages: babel-plugin-macros: 2.8.0 babel-plugin-polyfill-corejs3: 0.1.7_@babel+core@7.17.8 case-sensitive-paths-webpack-plugin: 2.4.0 - core-js: 3.21.1 + core-js: 3.25.5 css-loader: 3.6.0_webpack@4.46.0 file-loader: 6.2.0_webpack@4.46.0 find-up: 5.0.0 @@ -12965,7 +13008,7 @@ packages: copy-to-clipboard: 3.3.1 core-js: 3.25.5 core-js-pure: 3.19.1 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 emotion-theming: 10.3.0_gfrer23gq2rp2t523t6qbxrx6m fuse.js: 3.6.1 global: 4.4.0 @@ -13005,7 +13048,7 @@ packages: copy-to-clipboard: 3.3.1 core-js: 3.25.5 core-js-pure: 3.19.1 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 emotion-theming: 10.3.0_gfrer23gq2rp2t523t6qbxrx6m fuse.js: 3.6.1 global: 4.4.0 @@ -13851,7 +13894,7 @@ packages: '@types/wordpress__notices': 3.5.0 '@types/wordpress__rich-text': 3.4.6 '@wordpress/element': 4.8.0 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 re-resizable: 6.9.5_prpqlkd37azqwypxturxi7uyci transitivePeerDependencies: - react @@ -13866,7 +13909,7 @@ packages: '@types/wordpress__notices': 3.5.0 '@types/wordpress__rich-text': 3.4.6 '@wordpress/element': 4.8.0 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 re-resizable: 6.9.5_sfoxds7t5ydpegc3knd667wn6m transitivePeerDependencies: - react @@ -14038,12 +14081,12 @@ packages: '@typescript-eslint/scope-manager': 5.15.0 '@typescript-eslint/type-utils': 5.15.0_z4bbprzjrhnsfa24uvmcbu7f5q '@typescript-eslint/utils': 5.15.0_z4bbprzjrhnsfa24uvmcbu7f5q - debug: 4.3.4 + debug: 4.3.3 eslint: 8.25.0 functional-red-black-tree: 1.0.1 ignore: 5.2.0 regexpp: 3.2.0 - semver: 7.3.7 + semver: 7.3.5 tsutils: 3.21.0_typescript@4.8.4 typescript: 4.8.4 transitivePeerDependencies: @@ -14064,12 +14107,12 @@ packages: '@typescript-eslint/scope-manager': 5.15.0 '@typescript-eslint/type-utils': 5.15.0_himlt4eddny2rsb5zkuydvuf7u '@typescript-eslint/utils': 5.15.0_himlt4eddny2rsb5zkuydvuf7u - debug: 4.3.4 + debug: 4.3.3 eslint: 8.11.0 functional-red-black-tree: 1.0.1 ignore: 5.2.0 regexpp: 3.2.0 - semver: 7.3.7 + semver: 7.3.5 tsutils: 3.21.0_typescript@4.8.4 typescript: 4.8.4 transitivePeerDependencies: @@ -14757,7 +14800,6 @@ packages: /@use-gesture/core/10.2.10: resolution: {integrity: sha512-7WFIDfeTB+7RBui8YOrB2xbgmvMsvaCDjyzrdvECKkgOpIynNSdhlLXjiFuqQMtnK71IL/9WNZNU0P8xuaLuUQ==} - dev: false /@use-gesture/react/10.2.10_react@17.0.2: resolution: {integrity: sha512-znChnKVAMMGXD9J7fCKN686BJNBlUJaRtCu92IQXVWdcxg4MqS0SgsBslGnTWXTlsHVkg5zcGjKYf7qYkOf0Rg==} @@ -14766,7 +14808,6 @@ packages: dependencies: '@use-gesture/core': 10.2.10 react: 17.0.2 - dev: false /@webassemblyjs/ast/1.11.1: resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} @@ -14994,7 +15035,6 @@ packages: dependencies: webpack: 5.70.0_webpack-cli@4.9.2 webpack-cli: 4.9.2_wbg6qaiqcwsayvtung7xs6mhka - dev: true /@webpack-cli/info/1.4.1_webpack-cli@4.9.2: resolution: {integrity: sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA==} @@ -15003,7 +15043,6 @@ packages: dependencies: envinfo: 7.8.1 webpack-cli: 4.9.2_wbg6qaiqcwsayvtung7xs6mhka - dev: true /@webpack-cli/serve/1.6.1_webpack-cli@4.9.2: resolution: {integrity: sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==} @@ -15015,7 +15054,6 @@ packages: optional: true dependencies: webpack-cli: 4.9.2_wbg6qaiqcwsayvtung7xs6mhka - dev: true /@wojtekmaj/enzyme-adapter-react-17/0.6.6_7ltvq4e2railvf5uya4ffxpe2a: resolution: {integrity: sha512-gSfhg8CiL0Vwc2UgUblGVZIy7M0KyXaZsd8+QwzV8TSVRLkGyzdLtYEcs9wRWyQTsdmOd+oRGqbVgUX7AVJxug==} @@ -15120,7 +15158,6 @@ packages: '@babel/runtime': 7.19.0 '@wordpress/dom-ready': 3.19.0 '@wordpress/i18n': 4.19.0 - dev: false /@wordpress/a11y/3.4.1: resolution: {integrity: sha512-SjeLO8x/Y/QAcKBrvyJiu8KVAPckRLNwuFfgX7zCGM8vBfg+Depj94Hp55ARLjq0oXHg7EWKxSdzNkvmTz8AIA==} @@ -15192,14 +15229,12 @@ packages: '@babel/runtime': 7.19.0 '@wordpress/i18n': 4.19.0 '@wordpress/url': 3.20.0 - dev: false /@wordpress/autop/3.19.0: resolution: {integrity: sha512-Vl164Ilwmkx3M0LEyXkFdgksHjs3/FnHtw76tvdjjnLXtErUUIZ2y+hdCe+Esh8BhAUYXW420JU5KKvbidmykg==} engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wordpress/babel-plugin-import-jsx-pragma/1.1.3_@babel+core@7.12.9: resolution: {integrity: sha512-WkVeFZpM5yuHigWe8llZDeMRa4bhMQoHu9dzs1s3cmB1do2mhk341Iw34FidWto14Dzd+383K71vxJejqjKOwQ==} @@ -15408,7 +15443,6 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wordpress/block-editor/10.2.0_67fiyx7k2wr2ple2yfldahug5u: resolution: {integrity: sha512-9Bxq9hY3WEqodn/K/WSE+PoIwv6jKkKBP0pxXFJTWV1yc8/Np9QHV/7wG7qjztxxgu00FrYF7u8OZyvjPrSNYw==} @@ -15482,19 +15516,19 @@ packages: '@wordpress/blob': 3.19.0 '@wordpress/blocks': 11.18.0_react@17.0.2 '@wordpress/components': 20.0.0_67fiyx7k2wr2ple2yfldahug5u - '@wordpress/compose': 5.14.0_react@17.0.2 + '@wordpress/compose': 5.17.0_react@17.0.2 '@wordpress/data': 7.3.0_react@17.0.2 '@wordpress/date': 4.19.0 - '@wordpress/deprecated': 3.16.0 - '@wordpress/dom': 3.16.0 + '@wordpress/deprecated': 3.19.0 + '@wordpress/dom': 3.19.0 '@wordpress/element': 4.17.0 - '@wordpress/hooks': 3.16.0 + '@wordpress/hooks': 3.19.0 '@wordpress/html-entities': 3.19.0 - '@wordpress/i18n': 4.16.0 + '@wordpress/i18n': 4.19.0 '@wordpress/icons': 9.10.0 - '@wordpress/is-shallow-equal': 4.16.0 + '@wordpress/is-shallow-equal': 4.19.0 '@wordpress/keyboard-shortcuts': 3.17.0_react@17.0.2 - '@wordpress/keycodes': 3.16.0 + '@wordpress/keycodes': 3.19.0 '@wordpress/notices': 3.19.0_react@17.0.2 '@wordpress/rich-text': 5.17.0_react@17.0.2 '@wordpress/shortcode': 3.19.0 @@ -15520,7 +15554,6 @@ packages: transitivePeerDependencies: - '@babel/core' - '@types/react' - dev: false /@wordpress/block-library/7.16.0_67fiyx7k2wr2ple2yfldahug5u: resolution: {integrity: sha512-iuFqo2Ms08z0s1t1MM4mI7Gt+oBmj7KW6hRPEdQst+8jaG6hpQX6TgOzBt2Nw+0P0w8QRdyJjoQsB1cipGcNgQ==} @@ -15581,7 +15614,6 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wordpress/blocks/11.18.0_react@17.0.2: resolution: {integrity: sha512-6YHyDQNa6UrAzF3oKFOyu/1F32u7h5q/gpsE1439KDGVLsrc8rSxx3rE6G6TXbJ5YC8MqDrOItMwbw14TGKPAQ==} @@ -15615,7 +15647,6 @@ packages: showdown: 1.9.1 simple-html-tokenizer: 0.5.11 uuid: 8.3.2 - dev: false /@wordpress/blocks/11.3.1_react@17.0.2: resolution: {integrity: sha512-0T/qD1/hxJpNrUrJ2suZY0MP6Gw83mXfkaOupZ7rwjcWEi8c6AmzXaU/amAMNobM6oiNr4Sa6FctnnTGCEC1mQ==} @@ -15758,7 +15789,7 @@ packages: classnames: 2.3.1 colord: 2.9.2 dom-scroll-into-view: 1.2.1 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 framer-motion: 6.2.8_sfoxds7t5ydpegc3knd667wn6m gradient-parser: 0.1.5 highlight-words-core: 1.2.2 @@ -15806,12 +15837,12 @@ packages: '@wordpress/is-shallow-equal': 4.4.1 '@wordpress/keycodes': 3.4.1 '@wordpress/primitives': 3.2.1 - '@wordpress/rich-text': 5.17.0_react@17.0.2 + '@wordpress/rich-text': 5.2.1_react@17.0.2 '@wordpress/warning': 2.4.1 classnames: 2.3.1 colord: 2.9.2 dom-scroll-into-view: 1.2.1 - downshift: 6.1.7_react@17.0.2 + downshift: 6.1.12_react@17.0.2 framer-motion: 6.2.8_sfoxds7t5ydpegc3knd667wn6m gradient-parser: 0.1.5 highlight-words-core: 1.2.2 @@ -15860,12 +15891,12 @@ packages: '@wordpress/is-shallow-equal': 4.4.1 '@wordpress/keycodes': 3.4.1 '@wordpress/primitives': 3.2.1 - '@wordpress/rich-text': 5.17.0_react@17.0.2 + '@wordpress/rich-text': 5.2.1_react@17.0.2 '@wordpress/warning': 2.4.1 classnames: 2.3.1 colord: 2.9.2 dom-scroll-into-view: 1.2.1 - downshift: 6.1.7_react@17.0.2 + downshift: 6.1.12_react@17.0.2 framer-motion: 6.2.8_sfoxds7t5ydpegc3knd667wn6m gradient-parser: 0.1.5 highlight-words-core: 1.2.2 @@ -15974,9 +16005,9 @@ packages: change-case: 4.1.2 classnames: 2.3.1 colord: 2.9.2 - date-fns: 2.28.0 + date-fns: 2.29.3 dom-scroll-into-view: 1.2.1 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 framer-motion: 6.2.8_sfoxds7t5ydpegc3knd667wn6m gradient-parser: 0.1.5 highlight-words-core: 1.2.2 @@ -15993,7 +16024,6 @@ packages: transitivePeerDependencies: - '@babel/core' - '@types/react' - dev: false /@wordpress/components/21.2.0_67fiyx7k2wr2ple2yfldahug5u: resolution: {integrity: sha512-pYz+EY+Tv/O2JuDBXpaFH/zv9Evty/e6NOGjOzddSeaShZ/mCq2DpUSWPuTFBEAjtv6h9HnpkakbNnEeio5yNA==} @@ -16031,7 +16061,7 @@ packages: colord: 2.9.2 date-fns: 2.28.0 dom-scroll-into-view: 1.2.1 - downshift: 6.1.9_react@17.0.2 + downshift: 6.1.12_react@17.0.2 framer-motion: 6.2.8_sfoxds7t5ydpegc3knd667wn6m gradient-parser: 0.1.5 highlight-words-core: 1.2.2 @@ -16135,7 +16165,6 @@ packages: mousetrap: 1.6.5 react: 17.0.2 use-memo-one: 1.1.2_react@17.0.2 - dev: false /@wordpress/compose/5.2.1_react@17.0.2: resolution: {integrity: sha512-0l5UOiq5tDFeuIsdSVsWzNETHZagTnSBSTdGsxDmKIi5NC7vf1pXs4rlrEA45vUdFm/SbpIA9gp+NFzfpVKIXw==} @@ -16377,7 +16406,6 @@ packages: redux: 4.2.0 turbo-combine-reducers: 1.0.2 use-memo-one: 1.1.2_react@17.0.2 - dev: false /@wordpress/date/3.15.1: resolution: {integrity: sha512-SuHiObvjbegL8RpaSQ6JqFnG+QyGP+oUhx1FZDMdt1nOQA9HE7D5ssVlZFlMEAdo6iS8xMuW+4SgJN3Eo1fb4w==} @@ -16395,7 +16423,6 @@ packages: '@wordpress/deprecated': 3.19.0 moment: 2.29.1 moment-timezone: 0.5.34 - dev: false /@wordpress/date/4.4.1: resolution: {integrity: sha512-G2qcMB+EekBLIMO0YTEvhSfhTDAGM94WGe696DG4EevlBmMmgTSCATR8IvlD2+rta5Ut8qPJ1w2i0cy4AwslJA==} @@ -16480,7 +16507,6 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wordpress/dom-ready/3.4.1: resolution: {integrity: sha512-w6DVKKpNwX0XUp0Cuh1OyFyGXLabr47k/ecRHKmQkQh9LdjRew7QvxUHYDN1rejRvq5GqcDb7Gnkz4E6hWIo4Q==} @@ -16504,14 +16530,6 @@ packages: lodash: 4.17.21 dev: false - /@wordpress/dom/3.16.0: - resolution: {integrity: sha512-WOwEYXQWaZ4ZkQgL//jyB/FN33vPuFUHcr1Tc0o1T5zScNJrWVTiILokkFVv2AxqPZkrq4WhxKN9ZGRyo6VlOA==} - engines: {node: '>=12'} - dependencies: - '@babel/runtime': 7.19.0 - '@wordpress/deprecated': 3.19.0 - dev: false - /@wordpress/dom/3.19.0: resolution: {integrity: sha512-re4o53E5w0c5j9IW5vw8daTTmx6JQ9xgwxy2uWddAgSau7jq4LvvlWORCmgFEiCcFGgRBxhqJTndquKuDJ34PQ==} engines: {node: '>=12'} @@ -16572,8 +16590,8 @@ packages: puppeteer: '>=1.19.0' dependencies: '@babel/runtime': 7.19.0 - '@wordpress/keycodes': 3.16.0 - '@wordpress/url': 3.16.0 + '@wordpress/keycodes': 3.19.0 + '@wordpress/url': 3.20.0 jest: 27.5.1 lodash: 4.17.21 node-fetch: 2.6.7 @@ -16585,7 +16603,7 @@ packages: /@wordpress/element/2.20.3: resolution: {integrity: sha512-f4ZPTDf9CxiiOXiMxc4v1K7jcBMT4dsiehVOpkKzCDKboNXp4qVf8oe5PE23VGZNEjcOj5Mkg9hB57R0nqvMTw==} dependencies: - '@babel/runtime': 7.17.7 + '@babel/runtime': 7.19.0 '@types/react': 16.14.31 '@types/react-dom': 16.9.16 '@wordpress/escape-html': 1.12.2 @@ -16836,13 +16854,6 @@ packages: dependencies: '@babel/runtime': 7.19.0 - /@wordpress/hooks/3.16.0: - resolution: {integrity: sha512-KpY8KFp2/3TX6lKmffNmdkeaH9c4CN1iJ8SiCufjGgRCnVWmWe/HcEJ5OjhUvBnRkhsLMY7pvlXMU8Mh7nLxyA==} - engines: {node: '>=12'} - dependencies: - '@babel/runtime': 7.19.0 - dev: false - /@wordpress/hooks/3.19.0: resolution: {integrity: sha512-iJNZnQ08ZFFlXpVBbSA2NuVMiKxGpNLQsDiwIuIzTmwWl8ZECYikuGC3vMCiG3xUz47JR5eGA5wN63kjkPm5bA==} engines: {node: '>=12'} @@ -16875,7 +16886,6 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wordpress/html-entities/3.4.1: resolution: {integrity: sha512-wSuwgONTefnhCB9B7mKS+e8islHuCkprfDc+FhqVAa6r5RbVBGvaHUJs8embgdtww7MwBRMnskNf/buQ8Jr02A==} @@ -16924,20 +16934,6 @@ packages: tannin: 1.2.0 dev: false - /@wordpress/i18n/4.16.0: - resolution: {integrity: sha512-N7BChVVaQpt63e2Wgc0ST+ahUuhSjd6bqHqgIBnxZ4LU3c8tzd/etYjBqSM8RPcI9gSOM32ddlTnJgAxgntKaA==} - engines: {node: '>=12'} - hasBin: true - dependencies: - '@babel/runtime': 7.19.0 - '@wordpress/hooks': 3.19.0 - gettext-parser: 1.4.0 - lodash: 4.17.21 - memize: 1.1.0 - sprintf-js: 1.1.2 - tannin: 1.2.0 - dev: false - /@wordpress/i18n/4.19.0: resolution: {integrity: sha512-FL2+NghSYLqd9iib8otQLYF/G7EHh+/9zKbW6K3ok+FNwpF7/8UaMq/52CED2zWqD36Uw0vNX9AP7gRYTot0cA==} engines: {node: '>=12'} @@ -17020,7 +17016,6 @@ packages: '@babel/runtime': 7.19.0 '@wordpress/element': 4.17.0 '@wordpress/primitives': 3.17.0 - dev: false /@wordpress/is-shallow-equal/3.1.3: resolution: {integrity: sha512-eDLhfC4aaSgklzqwc6F/F4zmJVpTVTAvhqX+q0SP/8LPcP2HuKErPHVrEc75PMWqIutja2wJg98YSNPdewrj1w==} @@ -17185,7 +17180,6 @@ packages: '@wordpress/keycodes': 3.19.0 react: 17.0.2 rememo: 4.0.0 - dev: false /@wordpress/keycodes/2.19.3: resolution: {integrity: sha512-8rNdmP5M1ifTgLIL0dt/N1uTGsq/Rx1ydCXy+gg24WdxBRhyu5sudNVCtascVXo26aIfOH9OJRdqRZZTEORhog==} @@ -17194,16 +17188,6 @@ packages: '@wordpress/i18n': 3.20.0 lodash: 4.17.21 - /@wordpress/keycodes/3.16.0: - resolution: {integrity: sha512-Vs/t3GBMaJ3dBAPZfhuZBuxdwagJdXhpSpvnkX3/MJrn6sRrLKijxkWK8x26PfkDePQ+3kiupP2pEoIwSCTUXg==} - engines: {node: '>=12'} - dependencies: - '@babel/runtime': 7.19.0 - '@wordpress/i18n': 4.19.0 - change-case: 4.1.2 - lodash: 4.17.21 - dev: false - /@wordpress/keycodes/3.19.0: resolution: {integrity: sha512-uEITYKlknuZPP9tSF0y8s/GECsgJMceUkfFJH3JplKpPvw5RYJB49hTg40P5CPVoRLJgvJqoeR1Bdo3o312wjA==} engines: {node: '>=12'} @@ -17243,7 +17227,6 @@ packages: '@wordpress/a11y': 3.19.0 '@wordpress/data': 7.3.0_react@17.0.2 react: 17.0.2 - dev: false /@wordpress/notices/3.4.1_react@17.0.2: resolution: {integrity: sha512-Y7e2GLlB5wjLOtxsXzJd3jg/p6LV2GeeUnk+reURqUbb/4rlVlXQuMPOboTxLRB/0eTMNwWFI/MIr+NKbuY7MQ==} @@ -17368,7 +17351,6 @@ packages: '@babel/runtime': 7.19.0 '@wordpress/element': 4.17.0 classnames: 2.3.1 - dev: false /@wordpress/primitives/3.2.1: resolution: {integrity: sha512-dOrQQudydRw4szT60t+5b9jwMwxB4LMxNRlkbyGqqNwjv11Vq52FT9rVeLs0CvlqklluCyZu5KnUp/dELxIYJw==} @@ -17414,7 +17396,6 @@ packages: dependencies: '@babel/runtime': 7.19.0 requestidlecallback: 0.3.0 - dev: false /@wordpress/priority-queue/2.4.1: resolution: {integrity: sha512-5+pyUvQCQTTkoiccnO5G6AUDxzCKdAiDh3oLbl+qLz3j56iGuLoKWR6L5ySj+knaYIZb4g8expFsbvf2+RcVtw==} @@ -17465,7 +17446,6 @@ packages: is-promise: 4.0.0 redux: 4.2.0 rungen: 0.3.2 - dev: false /@wordpress/redux-routine/4.4.1_redux@4.2.0: resolution: {integrity: sha512-AqSEWN0PNxp00g1da+laL2rr0SP0AAfGpoqfzd55wIjWMQnHEf2pDsLvo6gQ9jyauuY5Wn2GUsYmGjQ+WjSf4w==} @@ -17546,7 +17526,6 @@ packages: memize: 1.1.0 react: 17.0.2 rememo: 4.0.0 - dev: false /@wordpress/rich-text/5.2.1_react@17.0.2: resolution: {integrity: sha512-PBoDPQjihEOteHlDvVRtAjmDTx3T3NRr/GAX8MKVajECWFhiS6tKY2R/llg7fnJAinCIhEAfpNwQDpx2UCp3bA==} @@ -17754,7 +17733,6 @@ packages: dependencies: '@babel/runtime': 7.19.0 memize: 1.1.0 - dev: false /@wordpress/style-engine/0.15.0: resolution: {integrity: sha512-F6wt4g8xnli6bOR0Syd4iz4r5jFha7DZLzi2krmgH3cSTK4DDPj2g1YOJrRIEqXX4aPmdZDurTqQZoJvt9qaqQ==} @@ -17762,7 +17740,6 @@ packages: dependencies: '@babel/runtime': 7.19.0 lodash: 4.17.21 - dev: false /@wordpress/style-engine/1.2.0: resolution: {integrity: sha512-RoyTFpxDS7uOJuNG31J/153JLKCNftU1/wMMkf0qXDpP+1k4h9em1+iIPPAGPRW5pSq/ky95fAaQAnl+FgI6Wg==} @@ -17814,7 +17791,6 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wordpress/url/2.22.2_react-native@0.70.0: resolution: {integrity: sha512-aqpYKQXzyzkCOm+GzZRYlLb+wh58g0cwR1PaKAl0UXaBS4mdS+X6biMriylb4P8CVC/RR7CSw5XI20JC24KDwQ==} @@ -17825,14 +17801,6 @@ packages: transitivePeerDependencies: - react-native - /@wordpress/url/3.16.0: - resolution: {integrity: sha512-5hlT8KfioKrmfqQAHihj2pWqc8oMUFNae3n5/Wlu8H60Btf5h+cBfxr6eiOXPEVX9Ko9NskLjmAqCxxoiNviqg==} - engines: {node: '>=12'} - dependencies: - '@babel/runtime': 7.19.0 - remove-accents: 0.4.2 - dev: false - /@wordpress/url/3.20.0: resolution: {integrity: sha512-geLgg7AWh/pIFPQEI43hQYR6MvEUi3QIawaPPoYMqw3Fne1UvbnXW9ETsCLzYyegmV8DXzcYSMPeSBQf+HL/ig==} engines: {node: '>=12'} @@ -17895,7 +17863,6 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.19.0 - dev: false /@wp-g2/components/0.0.140_uk23rajygp47dvnd4kbkngbaoi: resolution: {integrity: sha512-bychuhZ3wPSB457CHYcogoPQPlP/eUA9GoTo0Fv0rj7f44Gr9XlPoqVT+GQa3CmPnvSCAl1sjoe75Vkaoo/O1w==} @@ -17912,7 +17879,7 @@ packages: '@wp-g2/styles': 0.0.140_lnjyjqhbidocvrkn4aqhnph4yi '@wp-g2/utils': 0.0.140_wdcame2n4eqmtj7c7r7wzweise csstype: 3.0.10 - downshift: 6.1.9_react@16.14.0 + downshift: 6.1.12_react@16.14.0 framer-motion: 2.9.5_wdcame2n4eqmtj7c7r7wzweise highlight-words-core: 1.2.2 history: 4.10.1 @@ -19243,6 +19210,19 @@ packages: - supports-color dev: true + /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.16.12: + resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.19.3 + '@babel/core': 7.16.12 + '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.16.12 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: false + /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.17.8: resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} peerDependencies: @@ -19274,7 +19254,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-define-polyfill-provider': 0.3.0_@babel+core@7.12.9 - core-js-compat: 3.21.1 + core-js-compat: 3.25.5 transitivePeerDependencies: - supports-color dev: true @@ -19286,7 +19266,7 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-define-polyfill-provider': 0.3.0_@babel+core@7.16.12 - core-js-compat: 3.21.1 + core-js-compat: 3.25.5 transitivePeerDependencies: - supports-color dev: false @@ -19298,7 +19278,7 @@ packages: dependencies: '@babel/core': 7.17.8 '@babel/helper-define-polyfill-provider': 0.3.0_@babel+core@7.17.8 - core-js-compat: 3.21.1 + core-js-compat: 3.25.5 transitivePeerDependencies: - supports-color dev: true @@ -19704,7 +19684,6 @@ packages: check-types: 8.0.3 hoopy: 0.1.4 tryer: 1.0.1 - dev: true /big-integer/1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} @@ -19789,7 +19768,6 @@ packages: /body-scroll-lock/3.1.5: resolution: {integrity: sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==} - dev: false /body/5.1.0: resolution: {integrity: sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ==} @@ -20534,7 +20512,6 @@ packages: /check-types/8.0.3: resolution: {integrity: sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==} - dev: true /cheerio-select/1.5.0: resolution: {integrity: sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==} @@ -20671,7 +20648,6 @@ packages: /classnames/2.3.1: resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==} - dev: false /clean-css/4.2.4: resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} @@ -20984,7 +20960,6 @@ packages: /colorette/2.0.16: resolution: {integrity: sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==} - dev: true /colors/0.6.2: resolution: {integrity: sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw==} @@ -21067,7 +21042,6 @@ packages: /commander/7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} - dev: true /commander/8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} @@ -21150,7 +21124,7 @@ packages: resolution: {integrity: sha512-WpAmaKbMNmS3OProfHIdJiNleNJdgUrJfbKArXua28QF7+0CoZjlLn0lp6vlc+dl5r2/X9GQiQRQQU4BzSa69w==} /concat-map/0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} /concat-stream/1.6.2: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} @@ -21616,7 +21590,7 @@ packages: postcss-value-parser: 4.2.0 schema-utils: 2.7.1 semver: 6.3.0 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@3.3.12 /css-loader/5.2.7_webpack@5.70.0: resolution: {integrity: sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==} @@ -21986,7 +21960,6 @@ packages: /date-fns/2.29.3: resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==} engines: {node: '>=0.11'} - dev: false /dateformat/3.0.3: resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} @@ -22479,11 +22452,10 @@ packages: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: '@babel/runtime': 7.19.0 - csstype: 3.0.10 + csstype: 3.1.1 /dom-scroll-into-view/1.2.1: resolution: {integrity: sha512-LwNVg3GJOprWDO+QhLL1Z9MMgWe/KAFLxVWKzjRTxNSPn8/LLDIfmuG71YHznXCqaqTjvHJDYO1MEAgX6XCNbQ==} - dev: false /dom-serializer/0.2.2: resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} @@ -22585,13 +22557,37 @@ packages: /dotenv/10.0.0: resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} engines: {node: '>=10'} - dev: false /dotenv/8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} dev: true + /downshift/6.1.12_react@16.14.0: + resolution: {integrity: sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA==} + peerDependencies: + react: '>=16.12.0' + dependencies: + '@babel/runtime': 7.19.0 + compute-scroll-into-view: 1.0.17 + prop-types: 15.8.1 + react: 16.14.0 + react-is: 17.0.2 + tslib: 2.3.1 + dev: false + + /downshift/6.1.12_react@17.0.2: + resolution: {integrity: sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA==} + peerDependencies: + react: '>=16.12.0' + dependencies: + '@babel/runtime': 7.19.0 + compute-scroll-into-view: 1.0.17 + prop-types: 15.8.1 + react: 17.0.2 + react-is: 17.0.2 + tslib: 2.3.1 + /downshift/6.1.7_react@17.0.2: resolution: {integrity: sha512-cVprZg/9Lvj/uhYRxELzlu1aezRcgPWBjTvspiGTVEU64gF5pRdSRKFVLcxqsZC637cLAGMbL40JavEfWnqgNg==} peerDependencies: @@ -22618,21 +22614,8 @@ packages: tslib: 2.3.1 dev: false - /downshift/6.1.9_react@17.0.2: - resolution: {integrity: sha512-mzvk61WOX4MEsYHMKCXEVwuz/zM84x/WrCbaCQw71hyNN0fmWXvV673uOQy2idgIA+yqDsjtkV5KPfAFWuQylg==} - peerDependencies: - react: '>=16.12.0' - dependencies: - '@babel/runtime': 7.19.0 - compute-scroll-into-view: 1.0.17 - prop-types: 15.8.1 - react: 17.0.2 - react-is: 17.0.2 - tslib: 2.3.1 - /duplexer/0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - dev: true /duplexer3/0.1.4: resolution: {integrity: sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA==} @@ -22664,7 +22647,6 @@ packages: resolution: {integrity: sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==} engines: {node: '>=0.10.0'} requiresBuild: true - dev: true /ejs/3.1.8: resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} @@ -23295,11 +23277,11 @@ packages: eslint-import-resolver-node: 0.3.6 eslint-module-utils: 2.7.3_lkzaig2qiyp6elizstfbgvzhie has: 1.0.3 - is-core-module: 2.10.0 + is-core-module: 2.8.0 is-glob: 4.0.3 - minimatch: 3.1.2 + minimatch: 3.0.4 object.values: 1.1.5 - resolve: 1.22.1 + resolve: 1.20.0 tsconfig-paths: 3.14.0 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -23357,11 +23339,11 @@ packages: eslint-import-resolver-node: 0.3.6 eslint-module-utils: 2.7.3_54d5qjwnmqnp5634aqlesxatge has: 1.0.3 - is-core-module: 2.10.0 + is-core-module: 2.8.0 is-glob: 4.0.3 - minimatch: 3.1.2 + minimatch: 3.0.4 object.values: 1.1.5 - resolve: 1.22.1 + resolve: 1.20.0 tsconfig-paths: 3.14.0 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -24774,7 +24756,6 @@ packages: /fastest-levenshtein/1.0.12: resolution: {integrity: sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==} - dev: true /fastq/1.13.0: resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} @@ -24918,7 +24899,6 @@ packages: /filesize/3.6.1: resolution: {integrity: sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==} engines: {node: '>= 0.4.0'} - dev: true /fill-range/4.0.0: resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} @@ -25523,7 +25503,6 @@ packages: tslib: 2.3.1 optionalDependencies: '@emotion/is-prop-valid': 0.8.8 - dev: false /framesync/4.1.0: resolution: {integrity: sha512-MmgZ4wCoeVxNbx2xp5hN/zPDCbLSKiDt4BbbslK7j/pM2lg5S0vhTNv1v8BCVb99JPIo6hXBFdwzU7Q4qcAaoQ==} @@ -25535,7 +25514,6 @@ packages: resolution: {integrity: sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==} dependencies: tslib: 2.3.1 - dev: false /fresh/0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} @@ -26204,7 +26182,6 @@ packages: /gradient-parser/0.1.5: resolution: {integrity: sha1-DH4heVWeXOfY1x9EI6+TcQCyJIw=} engines: {node: '>=0.10.0'} - dev: false /grapheme-splitter/1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} @@ -26454,7 +26431,6 @@ packages: dependencies: duplexer: 0.1.2 pify: 4.0.1 - dev: true /gzip-size/6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} @@ -26694,11 +26670,9 @@ packages: /hey-listen/1.0.8: resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} - dev: false /highlight-words-core/1.2.2: resolution: {integrity: sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==} - dev: false /highlight.js/10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -26757,7 +26731,6 @@ packages: /hoopy/0.1.4: resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==} engines: {node: '>= 6.0.0'} - dev: true /hosted-git-info/2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -26771,7 +26744,6 @@ packages: /hpq/1.3.0: resolution: {integrity: sha512-fvYTvdCFOWQupGxqkahrkA+ERBuMdzkxwtUdKrxR6rmMd4Pfl+iZ1QiQYoaZ0B/v0y59MOMnz3XFUWbT50/NWA==} - dev: false /hsl-regex/1.0.0: resolution: {integrity: sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=} @@ -27341,7 +27313,6 @@ packages: /interpret/2.2.0: resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} engines: {node: '>= 0.10'} - dev: true /invariant/2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -32212,7 +32183,7 @@ packages: webpack: ^5.0.0 dependencies: schema-utils: 4.0.0 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@4.9.2 /minimalistic-assert/1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -32225,7 +32196,6 @@ packages: resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} dependencies: brace-expansion: 1.1.11 - dev: true /minimatch/3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -32841,7 +32811,6 @@ packages: /normalize-wheel/1.0.1: resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==} - dev: false /npm-bundled/1.1.2: resolution: {integrity: sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==} @@ -33252,7 +33221,6 @@ packages: /opener/1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true - dev: true /opn/5.5.0: resolution: {integrity: sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==} @@ -33907,21 +33875,12 @@ packages: find-up: 5.0.0 dev: true - /playwright-core/1.26.1: - resolution: {integrity: sha512-hzFchhhxnEiPc4qVPs9q2ZR+5eKNifY2hQDHtg1HnTTUuphYCBP8ZRb2si+B1TR7BHirgXaPi48LIye5SgrLAA==} + /playwright-core/1.27.1: + resolution: {integrity: sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==} engines: {node: '>=14'} hasBin: true dev: true - /playwright/1.26.1: - resolution: {integrity: sha512-WQmEdCgYYe8jOEkhkW9QLcK0PB+w1RZztBLYIT10MEEsENYg251cU0IzebDINreQsUt+HCwwRhtdz4weH9ICcQ==} - engines: {node: '>=14'} - hasBin: true - requiresBuild: true - dependencies: - playwright-core: 1.26.1 - dev: true - /plur/4.0.0: resolution: {integrity: sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==} engines: {node: '>=10'} @@ -33955,7 +33914,6 @@ packages: hey-listen: 1.0.8 style-value-types: 5.0.0 tslib: 2.3.1 - dev: false /popmotion/9.0.0-rc.20: resolution: {integrity: sha512-f98sny03WuA+c8ckBjNNXotJD4G2utG/I3Q23NU69OEafrXtxxSukAaJBxzbtxwDvz3vtZK69pu9ojdkMoBNTg==} @@ -35695,7 +35653,6 @@ packages: react: 17.0.2 react-dom: 17.0.2_react@17.0.2 tslib: 2.0.1 - dev: false /react-easy-crop/4.5.1_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-MVzCWmKXTwZTK0iYqlF/gPLdLqvUGrLGX7SQ4g+DO3b/lCiVAwxZKLeZ1wjDfG+r/yEWUoL7At5a0kkDJeU+rQ==} @@ -36596,7 +36553,6 @@ packages: react: 17.0.2 react-dom: 17.0.2_react@17.0.2 reakit-utils: 0.15.2_sfoxds7t5ydpegc3knd667wn6m - dev: false /reakit-system/0.15.2_wdcame2n4eqmtj7c7r7wzweise: resolution: {integrity: sha512-TvRthEz0DmD0rcJkGamMYx+bATwnGNWJpe/lc8UV2Js8nnPvkaxrHk5fX9cVASFrWbaIyegZHCWUBfxr30bmmA==} @@ -36647,7 +36603,6 @@ packages: dependencies: react: 17.0.2 react-dom: 17.0.2_react@17.0.2 - dev: false /reakit-utils/0.15.2_wdcame2n4eqmtj7c7r7wzweise: resolution: {integrity: sha512-i/RYkq+W6hvfFmXw5QW7zvfJJT/K8a4qZ0hjA79T61JAFPGt23DsfxwyBbyK91GZrJ9HMrXFVXWMovsKBc1qEQ==} @@ -36701,7 +36656,6 @@ packages: reakit-utils: 0.15.2_sfoxds7t5ydpegc3knd667wn6m transitivePeerDependencies: - react-dom - dev: false /reakit-warning/0.6.2_wdcame2n4eqmtj7c7r7wzweise: resolution: {integrity: sha512-z/3fvuc46DJyD3nJAUOto6inz2EbSQTjvI/KBQDqxwB0y02HDyeP8IWOJxvkuAUGkWpeSx+H3QWQFSNiPcHtmw==} @@ -36757,7 +36711,6 @@ packages: reakit-system: 0.15.2_sfoxds7t5ydpegc3knd667wn6m reakit-utils: 0.15.2_sfoxds7t5ydpegc3knd667wn6m reakit-warning: 0.6.2_sfoxds7t5ydpegc3knd667wn6m - dev: false /reakit/1.3.11_wdcame2n4eqmtj7c7r7wzweise: resolution: {integrity: sha512-mYxw2z0fsJNOQKAEn5FJCPTU3rcrY33YZ/HzoWqZX0G7FwySp1wkCYW79WhuYMNIUFQ8s3Baob1RtsEywmZSig==} @@ -36807,7 +36760,6 @@ packages: engines: {node: '>= 0.10'} dependencies: resolve: 1.22.1 - dev: true /redent/3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} @@ -37083,7 +37035,6 @@ packages: /rememo/4.0.0: resolution: {integrity: sha512-6BAfg1Dqg6UteZBEH9k6EHHersM86/EcBOMtJV+h+xEn1GC3H+gAgJWpexWYAamAxD0qXNmIt50iS/zuZKnQag==} - dev: false /remove-accents/0.4.2: resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} @@ -37190,7 +37141,6 @@ packages: /requestidlecallback/0.3.0: resolution: {integrity: sha512-TWHFkT7S9p7IxLC5A1hYmAYQx2Eb9w1skrXmQ+dS1URyvR8tenMLl4lHbqEOUnpEYxNKpkVMXUgknVpBZWXXfQ==} - dev: false /require-directory/2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} @@ -37529,7 +37479,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.1.1 semver: 7.3.5 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@3.3.12 /sass-loader/12.6.0_sass@1.49.9+webpack@5.70.0: resolution: {integrity: sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==} @@ -37889,7 +37839,6 @@ packages: hasBin: true dependencies: yargs: 14.2.3 - dev: false /shx/0.3.4: resolution: {integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==} @@ -37921,7 +37870,6 @@ packages: /simple-html-tokenizer/0.5.11: resolution: {integrity: sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==} - dev: false /simple-swizzle/0.2.2: resolution: {integrity: sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=} @@ -38668,7 +38616,6 @@ packages: dependencies: hey-listen: 1.0.8 tslib: 2.3.1 - dev: false /styled-griddie/0.1.3: resolution: {integrity: sha512-RjsiiADJrRpdPTF8NR26nlZutnvkrX78tiM5/za/E+ftVdpjD8ZBb2iOzrIzfix80uDcHYQbg3iIR0lOGaYmEQ==} @@ -38993,7 +38940,6 @@ packages: /stylis/4.0.13: resolution: {integrity: sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==} - dev: false /sudo-prompt/9.2.1: resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==} @@ -39762,7 +39708,6 @@ packages: /traverse/0.6.6: resolution: {integrity: sha512-kdf4JKs8lbARxWdp7RKdNzoJBhGUcIalSYibuGyHJbmk40pOysQ0+QPvlkCOICOivDWU2IJo2rkrxyTK2AH4fw==} - dev: false /tree-kill/1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} @@ -39803,7 +39748,6 @@ packages: /tryer/1.0.1: resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} - dev: true /ts-dedent/2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} @@ -40010,7 +39954,6 @@ packages: /tslib/2.0.1: resolution: {integrity: sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==} - dev: false /tslib/2.3.1: resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} @@ -40831,7 +40774,6 @@ packages: date-fns: 2.29.3 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 - dev: false /use-memo-one/1.1.2_react@16.14.0: resolution: {integrity: sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==} @@ -41132,6 +41074,7 @@ packages: /w3c-hr-time/1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + deprecated: Use your platform's native performance.now() and performance.timeOrigin. dependencies: browser-process-hrtime: 1.0.0 @@ -41273,7 +41216,6 @@ packages: - bufferutil - supports-color - utf-8-validate - dev: true /webpack-bundle-analyzer/4.6.1: resolution: {integrity: sha512-oKz9Oz9j3rUciLNfpGFjOb49/jEpXNmWdVH8Ls//zNcnLlQdTGXQQMsBbb/gR7Zl8WNLxVCq+0Hqbx3zv6twBw==} @@ -41369,7 +41311,6 @@ packages: webpack: 5.70.0_webpack-cli@4.9.2 webpack-bundle-analyzer: 3.9.0 webpack-merge: 5.8.0 - dev: true /webpack-cli/4.9.2_webpack@5.70.0: resolution: {integrity: sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==} @@ -41496,7 +41437,6 @@ packages: dependencies: clone-deep: 4.0.1 wildcard: 2.0.0 - dev: true /webpack-remove-empty-scripts/0.7.3_webpack@5.70.0: resolution: {integrity: sha512-yipqb25A0qtH7X9vKt6yihwyYkTtSlRiDdBb2QsyrkqGM3hpfAcfOO1lYDef9HQUNm3s8ojmorbNg32XXX6FYg==} @@ -41644,7 +41584,7 @@ packages: '@webassemblyjs/wasm-parser': 1.11.1 acorn: 8.8.0 acorn-import-assertions: 1.8.0_acorn@8.8.0 - browserslist: 4.20.4 + browserslist: 4.21.4 chrome-trace-event: 1.0.3 enhanced-resolve: 5.9.2 es-module-lexer: 0.9.3 @@ -41683,7 +41623,7 @@ packages: '@webassemblyjs/wasm-parser': 1.11.1 acorn: 8.8.0 acorn-import-assertions: 1.8.0_acorn@8.8.0 - browserslist: 4.20.4 + browserslist: 4.21.4 chrome-trace-event: 1.0.3 enhanced-resolve: 5.9.2 es-module-lexer: 0.9.3 @@ -41724,7 +41664,7 @@ packages: '@webassemblyjs/wasm-parser': 1.11.1 acorn: 8.8.0 acorn-import-assertions: 1.8.0_acorn@8.8.0 - browserslist: 4.20.4 + browserslist: 4.21.4 chrome-trace-event: 1.0.3 enhanced-resolve: 5.9.2 es-module-lexer: 0.9.3 @@ -41764,7 +41704,7 @@ packages: '@webassemblyjs/wasm-parser': 1.11.1 acorn: 8.8.0 acorn-import-assertions: 1.8.0_acorn@8.8.0 - browserslist: 4.20.4 + browserslist: 4.21.4 chrome-trace-event: 1.0.3 enhanced-resolve: 5.9.2 es-module-lexer: 0.9.3 @@ -41786,7 +41726,6 @@ packages: - '@swc/core' - esbuild - uglify-js - dev: true /websocket-driver/0.7.4: resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} @@ -41926,7 +41865,6 @@ packages: /wildcard/2.0.0: resolution: {integrity: sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==} - dev: true /window-size/0.2.0: resolution: {integrity: sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=} @@ -42242,7 +42180,6 @@ packages: dependencies: camelcase: 5.3.1 decamelize: 1.2.0 - dev: false /yargs-parser/18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} @@ -42304,7 +42241,6 @@ packages: which-module: 2.0.0 y18n: 4.0.3 yargs-parser: 15.0.3 - dev: false /yargs/15.4.1: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} @@ -42511,4 +42447,4 @@ packages: - react-native - supports-color - utf-8-validate - dev: false \ No newline at end of file + dev: false diff --git a/tools/code-analyzer/src/commands/analyzer/analyzer-lint.ts b/tools/code-analyzer/src/commands/analyzer/analyzer-lint.ts index 043d2ad3984..16da3295a0a 100644 --- a/tools/code-analyzer/src/commands/analyzer/analyzer-lint.ts +++ b/tools/code-analyzer/src/commands/analyzer/analyzer-lint.ts @@ -53,7 +53,8 @@ const program = new Command() sinceVersion, skipSchemaCheck, source, - base + base, + outputStyle ); if ( changes.templates.size ) { diff --git a/tools/code-analyzer/src/lib/hook-changes.ts b/tools/code-analyzer/src/lib/hook-changes.ts index 9ce4abb3f26..0bf4f3da5c3 100644 --- a/tools/code-analyzer/src/lib/hook-changes.ts +++ b/tools/code-analyzer/src/lib/hook-changes.ts @@ -2,6 +2,7 @@ * External dependencies */ import { getFilename, getPatches } from 'cli-core/src/util'; +import fs from 'node:fs'; /** * Internal dependencies @@ -20,9 +21,14 @@ export type HookChangeDescription = { hookType: string; changeType: 'new' | 'updated'; version: string; + ghLink: string; }; -export const scanForHookChanges = ( content: string, version: string ) => { +export const scanForHookChanges = ( + content: string, + version: string, + tmpRepoPath: string +) => { const changes: Map< string, HookChangeDescription > = new Map(); if ( ! content.match( /diff --git a\/(.+).php/g ) ) { @@ -73,14 +79,39 @@ export const scanForHookChanges = ( content: string, version: string ) => { const changeType = getHookChangeType( raw ); if ( ! hookName[ 2 ].startsWith( '-' ) ) { - changes.set( filePath, { - filePath, - name, - hookType, - description, - changeType, - version, - } ); + let ghLink = ''; + + fs.readFile( + tmpRepoPath + filePath, + 'utf-8', + function ( err, data ) { + if ( err ) { + console.error( err ); + } + + const reg = new RegExp( name ); + data.split( '\n' ).forEach( ( line, index ) => { + if ( line.match( reg ) ) { + const lineNum = index + 1; + + ghLink = `https://github.com/woocommerce/woocommerce/blob/${ version }/${ filePath.replace( + /(^\/)/, + '' + ) }#L${ lineNum }`; + } + } ); + + changes.set( filePath, { + filePath, + name, + hookType, + description, + changeType, + version, + ghLink, + } ); + } + ); } } } diff --git a/tools/code-analyzer/src/lib/scan-changes.ts b/tools/code-analyzer/src/lib/scan-changes.ts index 9e5dc0cd6a1..4ed137f97d3 100644 --- a/tools/code-analyzer/src/lib/scan-changes.ts +++ b/tools/code-analyzer/src/lib/scan-changes.ts @@ -5,6 +5,7 @@ import { Logger } from 'cli-core/src/logger'; import { join } from 'path'; import { cloneRepo, generateDiff } from 'cli-core/src/git'; import { readFile } from 'fs/promises'; +import { execSync } from 'child_process'; /** * Internal dependencies @@ -20,7 +21,8 @@ export const scanForChanges = async ( sinceVersion: string, skipSchemaCheck: boolean, source: string, - base: string + base: string, + outputStyle: string ) => { Logger.startTask( `Making temporary clone of ${ source }...` ); const tmpRepoPath = await cloneRepo( source ); @@ -37,10 +39,17 @@ export const scanForChanges = async ( Logger.error ); + // Only checkout the compare version if we're in CLI mode. + if ( outputStyle === 'cli' ) { + execSync( `cd ${ tmpRepoPath } && git checkout ${ compareVersion }`, { + stdio: 'pipe', + } ); + } + const pluginPath = join( tmpRepoPath, 'plugins/woocommerce' ); Logger.startTask( 'Detecting hook changes...' ); - const hookChanges = scanForHookChanges( diff, sinceVersion ); + const hookChanges = scanForHookChanges( diff, sinceVersion, tmpRepoPath ); Logger.endTask(); Logger.startTask( 'Detecting template changes...' ); @@ -61,6 +70,9 @@ export const scanForChanges = async ( ); const packageJSON = JSON.parse( fileStr ); + // Temporarily save the current PNPM version. + await execAsync( `tmpgPNPM="$(pnpm --version)"` ); + if ( packageJSON.engines && packageJSON.engines.pnpm ) { await execAsync( `npm i -g pnpm@${ packageJSON.engines.pnpm }`, @@ -92,6 +104,9 @@ export const scanForChanges = async ( schemaChanges = schemaDiff || []; + // Restore the previously saved PNPM version + await execAsync( `npm i -g pnpm@"$tmpgPNPM"` ); + Logger.endTask(); } diff --git a/tools/code-analyzer/src/print.ts b/tools/code-analyzer/src/print.ts index a522df7676e..1c36f69578a 100644 --- a/tools/code-analyzer/src/print.ts +++ b/tools/code-analyzer/src/print.ts @@ -19,11 +19,10 @@ export const printTemplateResults = ( title: string, log: ( s: string ) => void ): void => { - //[code,title,message] if ( output === 'github' ) { let opt = '\\n\\n### Template changes:'; for ( const { filePath, code, message } of data ) { - opt += `\\n* **file:** ${ filePath }`; + opt += `\\n* **File:** ${ filePath }`; opt += `\\n * ${ code.toUpperCase() }: ${ message }`; log( `::${ code } file=${ filePath },line=1,title=${ title }::${ message }` @@ -56,12 +55,6 @@ export const printHookResults = ( sectionTitle: string, log: ( s: string ) => void ) => { - // [ - // 'NOTICE', - // title, - // message, - // description, - // ] if ( output === 'github' ) { let opt = '\\n\\n### New hooks:'; for ( const { @@ -70,9 +63,9 @@ export const printHookResults = ( version, description, hookType, - changeType, + changeType } of data ) { - opt += `\\n* **file:** ${ filePath }`; + opt += `\\n* **File:** ${ filePath }`; const cliMessage = `**${ name }** introduced in ${ version }`; const ghMessage = `\\'${ name }\\' introduced in ${ version }`; @@ -96,6 +89,7 @@ export const printHookResults = ( description, hookType, changeType, + ghLink, } of data ) { const cliMessage = `**${ name }** introduced in ${ version }`; const ghMessage = `\\'${ name }\\' introduced in ${ version }`; @@ -106,8 +100,10 @@ export const printHookResults = ( log( '---------------------------------------------------' ); log( `HOOK: ${ name }: ${ description }` ); log( '---------------------------------------------------' ); - log( `NOTICE | ${ title } | ${ message }` ); + log( `GITHUB: ${ ghLink }` ); log( '---------------------------------------------------' ); + log( `NOTICE | ${ title } | ${ message }` ); + log( '---------------------------------------------------\n' ); } } }; diff --git a/tools/release-posts/commands/release-post/release-post-release.ts b/tools/release-posts/commands/release-post/release-post-release.ts index 6a765cf36d1..7dda60cf4fa 100644 --- a/tools/release-posts/commands/release-post/release-post-release.ts +++ b/tools/release-posts/commands/release-post/release-post-release.ts @@ -51,7 +51,14 @@ const program = new Command() if ( ! options.previousVersion && previousVersion ) { // e.g 6.8.0 -> 6.7.0 - previousVersion.minor -= 1; + previousVersion.major = + previousVersion.minor === 0 + ? previousVersion.major - 1 + : previousVersion.major; + + previousVersion.minor = + previousVersion.minor === 0 ? 9 : previousVersion.minor - 1; + previousVersion.format(); } @@ -74,7 +81,8 @@ const program = new Command() currentVersion, false, 'https://github.com/woocommerce/woocommerce.git', - previousVersion.toString() + previousVersion.toString(), + 'cli' ); const schemaChanges = changes.schema.filter( diff --git a/tools/release-posts/templates/hooks.ejs b/tools/release-posts/templates/hooks.ejs index 898e394c197..6133b5cae76 100644 --- a/tools/release-posts/templates/hooks.ejs +++ b/tools/release-posts/templates/hooks.ejs @@ -10,11 +10,13 @@ + - <% changes.hooks.forEach(({ name, description }) => { %> + <% changes.hooks.forEach(({ name, description, ghLink }) => { %> + <% }) %> diff --git a/tools/release-posts/templates/templates.ejs b/tools/release-posts/templates/templates.ejs index 0ecc08b816c..ec7d93ecf95 100644 --- a/tools/release-posts/templates/templates.ejs +++ b/tools/release-posts/templates/templates.ejs @@ -12,11 +12,11 @@ - + <% changes.templates.forEach((change) => { %> - + <% }) %> diff --git a/tools/version-bump/README.md b/tools/version-bump/README.md index b588a3fd586..3bb50735fd3 100644 --- a/tools/version-bump/README.md +++ b/tools/version-bump/README.md @@ -9,7 +9,7 @@ Bump WooCommerce to version 7.1.0: ``` -pnpm run version --filter version-bump -- bump woocommerce -v 7.1.0 +pnpm --filter version-bump run version bump woocommerce -v 7.1.0 ``` **Arguments**: diff --git a/tools/version-bump/lib/update.ts b/tools/version-bump/lib/update.ts index 527b466016b..e94235f4ed6 100644 --- a/tools/version-bump/lib/update.ts +++ b/tools/version-bump/lib/update.ts @@ -25,7 +25,7 @@ export const updateReadmeStableTag = async ( const readmeContents = await readFile( filePath, 'utf8' ); const updatedReadmeContents = readmeContents.replace( - /Stable tag: \d.\d.\d\n/m, + /Stable tag: \d+\.\d+\.\d+\n/m, `Stable tag: ${ nextVersion }\n` ); @@ -50,7 +50,7 @@ export const updateReadmeChangelog = async ( const readmeContents = await readFile( filePath, 'utf8' ); const updatedReadmeContents = readmeContents.replace( - /= \d.\d.\d \d\d\d\d-XX-XX =\n/m, + /= \d+\.\d+\.\d+ \d\d\d\d-XX-XX =\n/m, `= ${ nextVersion } ${ new Date().getFullYear() }-XX-XX =\n` ); @@ -86,7 +86,7 @@ export const updateClassPluginFile = async ( const classPluginFileContents = await readFile( filePath, 'utf8' ); const updatedClassPluginFileContents = classPluginFileContents.replace( - /public \$version = '\d.\d.\d';\n/m, + /public \$version = '\d+\.\d+\.\d+';\n/m, `public $version = '${ nextVersion }';\n` ); @@ -142,7 +142,7 @@ export const updatePluginFile = async ( const pluginFileContents = await readFile( filePath, 'utf8' ); const updatedPluginFileContents = pluginFileContents.replace( - /Version: \d.\d.\d.*\n/m, + /Version: \d+\.\d+\.\d+.*\n/m, `Version: ${ nextVersion }\n` ); await writeFile( filePath, updatedPluginFileContents );
Filter DescriptionGitHub Link
<%= name %> <%= description %>Link
<%= change %><%= change.filePath %>