Merge branch 'trunk' into packages/api/json-transform-tests

This commit is contained in:
Lucas Bustamante 2022-02-02 18:14:31 -03:00
commit 23b1c10518
55 changed files with 805 additions and 453 deletions

View File

@ -3,7 +3,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
ref: ref:
description: 'By default the zip file is generated from the branch the workflow runs from, but you can specify an explicit reference to use instead here (e.g. refs/tags/tag_name). The resulting file will be available as an artifact on the workflow run.' description: 'By default the zip file is generated from the branch the workflow runs from, but you can specify an explicit reference to use instead here (e.g. refs/tags/tag_name or refs/heads/release/x.x). The resulting file will be available as an artifact on the workflow run.'
required: false required: false
default: '' default: ''
jobs: jobs:

View File

@ -96,7 +96,9 @@ jobs:
await script({github, context}) await script({github, context})
- name: Remove label from pull request. - name: Remove label from pull request.
if: always() && ${{ contains( github.event.pull_request.labels.*.name, format('run{0} smoke tests', ':')) }} if: |
always()
&& contains( github.event.pull_request.labels.*.name, format('run{0} smoke tests', ':'))
uses: actions-ecosystem/action-remove-labels@v1 uses: actions-ecosystem/action-remove-labels@v1
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -88,7 +88,7 @@ jobs:
- plugin: 'WooCommerce PayPal Payments' - plugin: 'WooCommerce PayPal Payments'
repo: 'woocommerce/woocommerce-paypal-payments' repo: 'woocommerce/woocommerce-paypal-payments'
- plugin: 'WooCommerce Shipping & Tax' - plugin: 'WooCommerce Shipping & Tax'
repo: 'woocommerce/woocommerce-services' repo: 'automattic/woocommerce-services'
- plugin: 'WooCommerce Subscriptions' - plugin: 'WooCommerce Subscriptions'
repo: WC_SUBSCRIPTIONS_REPO repo: WC_SUBSCRIPTIONS_REPO
private: true private: true
@ -146,9 +146,9 @@ jobs:
WC_E2E_SCREENSHOTS: 1 WC_E2E_SCREENSHOTS: 1
E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }}
E2E_SLACK_CHANNEL: ${{ secrets.RELEASE_TEST_SLACK_CHANNEL }} E2E_SLACK_CHANNEL: ${{ secrets.RELEASE_TEST_SLACK_CHANNEL }}
GITHUB_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }} PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }}
PLUGIN_NAME: ${{ matrix.plugin }} PLUGIN_NAME: ${{ matrix.plugin }}
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }} GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: | run: |
pnpx wc-e2e test:e2e tests/e2e/specs/smoke-tests/upload-plugin.js pnpx wc-e2e test:e2e tests/e2e/specs/smoke-tests/upload-plugin.js
pnpm nx test-e2e woocommerce pnpx wc-e2e test:e2e

View File

@ -118,7 +118,7 @@ jobs:
- plugin: 'WooCommerce PayPal Payments' - plugin: 'WooCommerce PayPal Payments'
repo: 'woocommerce/woocommerce-paypal-payments' repo: 'woocommerce/woocommerce-paypal-payments'
- plugin: 'WooCommerce Shipping & Tax' - plugin: 'WooCommerce Shipping & Tax'
repo: 'woocommerce/woocommerce-services' repo: 'automattic/woocommerce-services'
- plugin: 'WooCommerce Subscriptions' - plugin: 'WooCommerce Subscriptions'
repo: WC_SUBSCRIPTIONS_REPO repo: WC_SUBSCRIPTIONS_REPO
private: true private: true
@ -170,9 +170,9 @@ jobs:
WC_E2E_SCREENSHOTS: 1 WC_E2E_SCREENSHOTS: 1
E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }}
E2E_SLACK_CHANNEL: ${{ secrets.RELEASE_TEST_SLACK_CHANNEL }} E2E_SLACK_CHANNEL: ${{ secrets.RELEASE_TEST_SLACK_CHANNEL }}
GITHUB_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }} PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }}
PLUGIN_NAME: ${{ matrix.plugin }} PLUGIN_NAME: ${{ matrix.plugin }}
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }} GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: | run: |
pnpx wc-e2e test:e2e tests/e2e/specs/smoke-tests/upload-plugin.js pnpx wc-e2e test:e2e tests/e2e/specs/smoke-tests/upload-plugin.js
pnpm nx test-e2e woocommerce pnpx wc-e2e test:e2e

120
.github/workflows/triage-replies.yml vendored Normal file
View File

@ -0,0 +1,120 @@
name: Add issue triage comments.
on:
issues:
types:
- labeled
jobs:
add-dev-comment:
if: "github.event.label.name == 'needs developer feedback'"
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Add developer feedback comment
uses: actions/github-script@v5
with:
github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Hi @${{ github.event.issue.user.login }},\n\n\
Thank you for opening the issue! It requires further feedback from the WooCommerce Core team.\n\n\
We are adding the `needs developer feedback` label to this issue so that the Core team could take a look.\n\n\
Please note it may take a few days for them to get to this issue. Thank you for your patience.'
})
add-reproduction-comment:
if: "github.event.label.name == 'status: needs reproduction'"
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Add needs reproduction comment
uses: actions/github-script@v5
with:
github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'We are adding the `status: needs reproduction` label to this issue to try reproduce it on the \
current released version of WooCommerce.\n\n\
Thank you for your patience.'
})
add-support-comment:
if: "github.event.label.name == 'type: support request'"
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Add support request comment
uses: actions/github-script@v5
with:
github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Hi @${{ github.event.issue.user.login }},\n\n\
While our goal is to address all the issues reported in this repository, \
GitHub should be treated as a place to report confirmed bugs only.\n\n\
The type of issue you submitted looks like a support request which may or may not reveal a bug once proper \
troubleshooting is done. In order to confirm the bug, please follow one of the steps below:\n\n\
- Review [WooCommerce Self-Service Guide](https://docs.woocommerce.com/document/woocommerce-self-service-guide/) \
to see if the solutions listed there apply to your case;\n\
- If you are a paying customer of WooCommerce, contact WooCommerce support by \
[opening a ticket or starting a live chat](https://woocommerce.com/contact-us/);\n\
- Make a post on [WooCommerce community forum](https://wordpress.org/support/plugin/woocommerce/)\n\n\
If you confirm the bug, please provide us with clear steps to reproduce it.\n\n\
We are closing this issue for now as it seems to be a support request and not a bug. \
If we missed something, please leave a comment and we will take a second look.'
})
- name: Close support request issue
uses: actions/github-script@v5
with:
github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }}
script: |
github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: 'closed'
})
add-votes-comment:
if: "github.event.label.name == 'votes needed'"
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Add votes needed comment
uses: actions/github-script@v5
with:
github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Thanks for the suggestion @${{ github.event.issue.user.login }},\n\n\
While we appreciate you sharing your ideas with us, it doesnt fit in with our current priorities for the project.\n\
At some point, we may revisit our priorities and look through the list of suggestions like this one to see if it \
warrants a second look.\n\n\
In the meantime, we are going to close this issue with the `votes needed` label and evaluate over time if this \
issue collects more feedback.\n\n\
Dont be alarmed if you dont see any activity on this issue for a while. \
We'll keep an eye on the popularity of this request.'
})
- name: Close votes needed issue
uses: actions/github-script@v5
with:
github-token: ${{ secrets.WC_BOT_TRIAGE_TOKEN }}
script: |
github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: 'closed'
})

View File

@ -1,11 +1,14 @@
# Unreleased # Unreleased
## Changed
- Updated top level menu css selectors
## Fixed ## Fixed
- Moved `merchant.login()` out of `beforeAll()` block and into test body for retried runs. - Moved `merchant.login()` out of `beforeAll()` block and into test body for retried runs.
## Added ## Added
- This package is now transpiled with Babel, which allows the usage of modern, yet compatible JS code.
- A `specs/data` folder to store page element data. - A `specs/data` folder to store page element data.
- Tests to verify that different top-level menu and their associated sub-menus load successfully. - Tests to verify that different top-level menu and their associated sub-menus load successfully.
- Test scaffolding via `npx wc-e2e install @woocommerce/e2e-core-tests` - Test scaffolding via `npx wc-e2e install @woocommerce/e2e-core-tests`

View File

@ -5,13 +5,9 @@
export const MENUS = [ export const MENUS = [
[ [
'WooCommerce', 'WooCommerce',
'#adminmenu > li:nth-child(8) > a', '.menu-top > a[href*=wc-admin].menu-top-first',
[ [
[ [ 'Home', '', 'Home' ],
'Home',
'',
'Home',
],
[ [
'Orders', 'Orders',
'#toplevel_page_woocommerce > ul > li:nth-child(3) > a', '#toplevel_page_woocommerce > ul > li:nth-child(3) > a',
@ -37,7 +33,7 @@ export const MENUS = [
], ],
[ [
'Products', 'Products',
'#adminmenu > li:nth-child(9) > a', '.menu-top > a[href*=product].menu-top',
[ [
[ [
'All Products', 'All Products',
@ -68,7 +64,7 @@ export const MENUS = [
], ],
[ [
'Marketing', 'Marketing',
'#adminmenu > li:nth-child(11) > a', '.menu-top > a[href*=marketing].menu-top',
[ [
[ [
'Overview', 'Overview',

View File

@ -9,8 +9,8 @@ const {
uiUnblocked, uiUnblocked,
evalAndClick, evalAndClick,
clickUpdateOrder, clickUpdateOrder,
waitForSelector,
} = require( '@woocommerce/e2e-utils' ); } = require( '@woocommerce/e2e-utils' );
const { waitForSelector } = require( '@woocommerce/e2e-environment' );
/** /**
* Evaluate and click a button selector then wait for a result selector. * Evaluate and click a button selector then wait for a result selector.

View File

@ -44,7 +44,7 @@ const runCheckoutCreateAccountTest = () => {
await shopper.addToCartFromShopPage( productId ); await shopper.addToCartFromShopPage( productId );
await uiUnblocked(); await uiUnblocked();
await shopper.goToCheckout(); await shopper.goToCheckout();
}); }, 45000 );
it('can create an account during checkout', async () => { it('can create an account during checkout', async () => {
// Fill all the details for a new customer // Fill all the details for a new customer

View File

@ -1,42 +1,35 @@
# Unreleased # Unreleased
## Fixed ## Changes
- Updated `deleteDownloadedPluginFiles()` to also be able to delete directories.
- Added the `resolveSingleE2EPath` method which builds a path to a specific E2E test
- It still supports the use of `plugins/woocommerce` and/or `tests/e2e` in file paths to avoid any breaking changes
## Fixed
- Added the ability to take screenshots from multiple test failures (when retried) in `utils/take-screenshot.js`.
## Added ## Added
- Added `post-results-to-github-pr.js` to post smoke test results to a GitHub PR. - Added `post-results-to-github-pr.js` to post smoke test results to a GitHub PR.
- Added jest flags to generate a json test report - Added jest flags to generate a json test report
- Added more entries to `default.json` - Added more entries to `default.json`
- Save `test-results.json` from test runs to the `tests/e2e` folder.
## Added
- Added `await` for every call to `shopper.logout` - Added `await` for every call to `shopper.logout`
- Updated `getLatestReleaseZipUrl()` to allow passing in an authorization token and simplified arguments to just the repository name - Updated `getLatestReleaseZipUrl()` to allow passing in an authorization token and simplified arguments to just the repository name
- Added `upload.ini` which increases the limits for uploading files (such as for plugins) in the Docker environment - Added `upload.ini` which increases the limits for uploading files (such as for plugins) in the Docker environment
- Test setup, scaffolding, and removal via `wc-e2e install` and `wc-e2e uninstall` - Test setup, scaffolding, and removal via `wc-e2e install` and `wc-e2e uninstall`
## Fixed
- Updated the browserViewport in `jest.setup.js` to match the `defaultViewport` dimensions defined in `jest-puppeteer.config.js`
## Added
- Added quotes around `WORDPRESS_TITLE` value in .env file to address issue with docker compose 2 "key cannot contain a space" error. - Added quotes around `WORDPRESS_TITLE` value in .env file to address issue with docker compose 2 "key cannot contain a space" error.
- Added `LATEST_WP_VERSION_MINUS` that allows setting a number to subtract from the current WordPress version for the WordPress Docker image. - Added `LATEST_WP_VERSION_MINUS` that allows setting a number to subtract from the current WordPress version for the WordPress Docker image.
- Support for PHP_VERSION, MARIADB_VERSION environment variables for built in container initialization - Support for PHP_VERSION, MARIADB_VERSION environment variables for built in container initialization
- `resolveLocalE2ePath` to resolve path to local E2E file - `resolveLocalE2ePath` to resolve path to local E2E file
- `WC_E2E_FOLDER` for mapping plugin root to path within repo - `WC_E2E_FOLDER` for mapping plugin root to path within repo
- Added the `resolveSingleE2EPath` method which builds a path to a specific E2E test
- Added the ability to take screenshots from multiple test failures (when retried) in `utils/take-screenshot.js`.
## Changed
- Updated `getLatestReleaseZipUrl()` to allow passing in an authorization token and simplified arguments to just the repository name
## Fixed ## Fixed
- Updated the browserViewport in `jest.setup.js` to match the `defaultViewport` dimensions defined in `jest-puppeteer.config.js`
- Use consistent `defaultViewport` in both headless and non-headless context - Use consistent `defaultViewport` in both headless and non-headless context
- Add missing `config` dependency
# 0.2.3 # 0.2.3

View File

@ -114,6 +114,10 @@ await takeScreenshotFor( 'name of current step' );
Screenshots will be saved to `tests/e2e/screenshots`. This folder is cleared at the beginning of each test run. Screenshots will be saved to `tests/e2e/screenshots`. This folder is cleared at the beginning of each test run.
#### Test results
The test results are saved in `json` format in `tests/e2e/test-results.json`.
### Override default test timeout ### Override default test timeout
To override the default timeout for the tests, you can use the `DEFAULT_TIMEOUT_OVERRIDE` flag and pass in a maximum timeout in milliseconds. For example, you can pass it in when running the tests from the command line: To override the default timeout for the tests, you can use the `DEFAULT_TIMEOUT_OVERRIDE` flag and pass in a maximum timeout in milliseconds. For example, you can pass it in when running the tests from the command line:

View File

@ -76,12 +76,13 @@ if ( program.args.length == 1 ) {
} }
let jestCommand = 'jest'; let jestCommand = 'jest';
let outputFile = 'test-results.json';
const jestArgs = [ const jestArgs = [
'--maxWorkers=1', '--maxWorkers=1',
'--rootDir=./', '--rootDir=./',
'--verbose', '--verbose',
'--json', '--json',
'--outputFile=test-results.json', '--outputFile=' + outputFile,
...program.args, ...program.args,
]; ];
@ -110,6 +111,11 @@ const jestProcess = spawnSync( jestCommand, jestArgs, {
env: envVars, env: envVars,
} ); } );
let results = resolvePackagePath( outputFile );
if ( fs.existsSync( results ) ) {
let localResults = resolveLocalE2ePath( outputFile );
fs.copyFileSync( results, localResults );
}
console.log( 'Jest exit code: ' + jestProcess.status ); console.log( 'Jest exit code: ' + jestProcess.status );
// Pass Jest exit code to npm // Pass Jest exit code to npm

View File

@ -5,14 +5,22 @@ const resultsFile = path.resolve( __dirname, '../test-results.json' );
const buildOutput = ( results ) => { const buildOutput = ( results ) => {
const { TITLE, SMOKE_TEST_URL } = process.env; const { TITLE, SMOKE_TEST_URL } = process.env;
const resultKeys = Object.keys( results );
let output = `## ${ TITLE }:\n\n`; let output = `## ${ TITLE }:\n\n`;
output += `**Test URL:** ${ SMOKE_TEST_URL }\n`; output += `**Test URL:** ${ SMOKE_TEST_URL }\n`;
output += `**Total Number of Passed Tests:** ${ results.numTotalTests }\n`;
output += `**Total Number of Failed Tests:** ${ results.numFailedTests }\n`; resultKeys.forEach( ( key ) => {
output += `**Total Number of Test Suites:** ${ results.numTotalTestSuites }\n`; // The keys that we care about all start with 'num'
output += `**Total Number of Passed Test Suites:** ${ results.numPassedTestSuites }\n`; if ( key.includes( 'num' ) ) {
output += `**Total Number of Failed Test Suites:** ${ results.numFailedTestSuites }\n`; // match only capitalized words
const words = key.match( /[A-Z][a-z]+/g );
output += `**Total Number of ${ words.join( ' ' ) }:** ${
results[ key ]
}\n`;
}
} );
return output; return output;
}; };
@ -26,7 +34,8 @@ module.exports = async ( { github, context } ) => {
output = buildOutput( results ); output = buildOutput( results );
} else { } else {
output = `## Test Results Not Found!`; output = `## Test Results Not Found! \n\n`;
output += 'The path to the `test-results.json` file may need to be updated.';
} }
await github.rest.issues.createComment( { await github.rest.issues.createComment( {

View File

@ -29,6 +29,7 @@
"@wordpress/jest-preset-default": "^7.1.3", "@wordpress/jest-preset-default": "^7.1.3",
"app-root-path": "^3.0.0", "app-root-path": "^3.0.0",
"commander": "4.1.1", "commander": "4.1.1",
"config": "3.3.3",
"jest": "^25.1.0", "jest": "^25.1.0",
"jest-circus": "25.1.0", "jest-circus": "25.1.0",
"jest-each": "25.5.0", "jest-each": "25.5.0",

View File

@ -152,13 +152,20 @@ const downloadZip = async ( fileUrl, downloadPath, authorizationToken ) => {
const deleteDownloadedPluginFiles = async () => { const deleteDownloadedPluginFiles = async () => {
const pluginSavePath = resolveLocalE2ePath( 'plugins' ); const pluginSavePath = resolveLocalE2ePath( 'plugins' );
fs.readdir( pluginSavePath, ( err, files ) => { fs.readdir( pluginSavePath, ( err, contents ) => {
if ( err ) throw err; if ( err ) throw err;
for ( const file of files ) { for ( const content of contents ) {
fs.unlink( path.join( pluginSavePath, file ), ( error ) => { const contentPath = path.join( pluginSavePath, content );
if ( error ) throw error; const stats = fs.lstatSync( contentPath );
} );
if ( stats.isDirectory() ) {
fs.rmSync( contentPath, { recursive: true, force: true } );
} else {
fs.unlink( contentPath, ( error ) => {
if ( error ) throw error;
} );
}
} }
} ); } );
}; };

View File

@ -1,6 +1,6 @@
{ {
"require-dev": { "require-dev": {
"woocommerce/woocommerce-sniffs": "^0.1.0" "woocommerce/woocommerce-sniffs": "^0.1.2"
}, },
"config": { "config": {
"platform": { "platform": {

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c50f65dd9f9a26d397f7bb30228d7a88", "content-hash": "51112a9a1fd6cd39c29579a93a59cf9c",
"packages": [], "packages": [],
"packages-dev": [ "packages-dev": [
{ {
@ -286,16 +286,16 @@
}, },
{ {
"name": "woocommerce/woocommerce-sniffs", "name": "woocommerce/woocommerce-sniffs",
"version": "0.1.1", "version": "0.1.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/woocommerce/woocommerce-sniffs.git", "url": "https://github.com/woocommerce/woocommerce-sniffs.git",
"reference": "eb604d751b61c42f31ff1aa24113c7c0de438553" "reference": "5566270d280a300bc24bd0cb055a8b9325afdd6b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/eb604d751b61c42f31ff1aa24113c7c0de438553", "url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/5566270d280a300bc24bd0cb055a8b9325afdd6b",
"reference": "eb604d751b61c42f31ff1aa24113c7c0de438553", "reference": "5566270d280a300bc24bd0cb055a8b9325afdd6b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -322,7 +322,11 @@
"woocommerce", "woocommerce",
"wordpress" "wordpress"
], ],
"time": "2021-07-29T17:25:16+00:00" "support": {
"issues": "https://github.com/woocommerce/woocommerce-sniffs/issues",
"source": "https://github.com/woocommerce/woocommerce-sniffs/tree/0.1.2"
},
"time": "2022-01-21T20:13:23+00:00"
}, },
{ {
"name": "wp-coding-standards/wpcs", "name": "wp-coding-standards/wpcs",
@ -381,5 +385,5 @@
"platform-overrides": { "platform-overrides": {
"php": "7.0" "php": "7.0"
}, },
"plugin-api-version": "1.1.0" "plugin-api-version": "2.2.0"
} }

View File

@ -21,8 +21,8 @@
"pelago/emogrifier": "3.1.0", "pelago/emogrifier": "3.1.0",
"psr/container": "1.0.0", "psr/container": "1.0.0",
"woocommerce/action-scheduler": "3.4.0", "woocommerce/action-scheduler": "3.4.0",
"woocommerce/woocommerce-admin": "3.1.0-rc.1", "woocommerce/woocommerce-admin": "3.1.0",
"woocommerce/woocommerce-blocks": "6.7.1" "woocommerce/woocommerce-blocks": "6.7.3"
}, },
"require-dev": { "require-dev": {
"bamarni/composer-bin-plugin": "^1.4", "bamarni/composer-bin-plugin": "^1.4",

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "edd58bb30d9ec1e9ba74f499fb4a919b", "content-hash": "d9b680722733ec1fae9414023f943830",
"packages": [ "packages": [
{ {
"name": "automattic/jetpack-autoloader", "name": "automattic/jetpack-autoloader",
@ -543,16 +543,16 @@
}, },
{ {
"name": "woocommerce/woocommerce-admin", "name": "woocommerce/woocommerce-admin",
"version": "3.1.0-rc.1", "version": "3.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git", "url": "https://github.com/woocommerce/woocommerce-admin.git",
"reference": "0c7a42f0c934f7f2e44c0534e2cec6fcb8362180" "reference": "59dfff627d9b7b1d5396686e69b709a820f5e7e5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/0c7a42f0c934f7f2e44c0534e2cec6fcb8362180", "url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/59dfff627d9b7b1d5396686e69b709a820f5e7e5",
"reference": "0c7a42f0c934f7f2e44c0534e2cec6fcb8362180", "reference": "59dfff627d9b7b1d5396686e69b709a820f5e7e5",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -608,22 +608,22 @@
"homepage": "https://github.com/woocommerce/woocommerce-admin", "homepage": "https://github.com/woocommerce/woocommerce-admin",
"support": { "support": {
"issues": "https://github.com/woocommerce/woocommerce-admin/issues", "issues": "https://github.com/woocommerce/woocommerce-admin/issues",
"source": "https://github.com/woocommerce/woocommerce-admin/tree/v3.1.0-rc.1" "source": "https://github.com/woocommerce/woocommerce-admin/tree/v3.1.0"
}, },
"time": "2022-01-13T02:42:30+00:00" "time": "2022-01-25T02:36:40+00:00"
}, },
{ {
"name": "woocommerce/woocommerce-blocks", "name": "woocommerce/woocommerce-blocks",
"version": "v6.7.1", "version": "v6.7.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/woocommerce/woocommerce-gutenberg-products-block.git", "url": "https://github.com/woocommerce/woocommerce-gutenberg-products-block.git",
"reference": "d215803d7310046050bde26420c3082edcdbf866" "reference": "742439da222272235190cbf14cc5ea0d4f178fa5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/d215803d7310046050bde26420c3082edcdbf866", "url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/742439da222272235190cbf14cc5ea0d4f178fa5",
"reference": "d215803d7310046050bde26420c3082edcdbf866", "reference": "742439da222272235190cbf14cc5ea0d4f178fa5",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -662,9 +662,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues", "issues": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues",
"source": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/v6.7.1" "source": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/v6.7.3"
}, },
"time": "2022-01-07T16:15:03+00:00" "time": "2022-01-24T10:38:10+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [

View File

@ -545,7 +545,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
// If setting the status, ensure it's set to a valid status. // If setting the status, ensure it's set to a valid status.
if ( true === $this->object_read ) { if ( true === $this->object_read ) {
// Only allow valid new status. // Only allow valid new status.
if ( ! in_array( 'wc-' . $new_status, $this->get_valid_statuses(), true ) && 'trash' !== $new_status ) { if ( ! in_array( 'wc-' . $new_status, $this->get_valid_statuses(), true ) && 'trash' !== $new_status && 'auto-draft' !== $new_status ) {
$new_status = 'pending'; $new_status = 'pending';
} }

View File

@ -7,6 +7,7 @@
*/ */
use Automattic\Jetpack\Constants; use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
if ( ! defined( 'ABSPATH' ) ) { if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly. exit; // Exit if accessed directly.
@ -20,40 +21,14 @@ if ( ! class_exists( 'WC_Admin_Dashboard_Setup', false ) ) :
class WC_Admin_Dashboard_Setup { class WC_Admin_Dashboard_Setup {
/** /**
* List of tasks. * The task list.
*
* @var array
*/ */
private $tasks = array( private $task_list = null;
'store_details' => array(
'completed' => false, /**
'button_link' => 'admin.php?page=wc-admin&path=%2Fsetup-wizard', * The tasks.
), */
'products' => array( private $tasks = null;
'completed' => false,
'button_link' => 'admin.php?page=wc-admin&task=products',
),
'woocommerce-payments' => array(
'completed' => false,
'button_link' => 'admin.php?page=wc-admin&path=%2Fpayments%2Fconnect',
),
'payments' => array(
'completed' => false,
'button_link' => 'admin.php?page=wc-admin&task=payments',
),
'tax' => array(
'completed' => false,
'button_link' => 'admin.php?page=wc-admin&task=tax',
),
'shipping' => array(
'completed' => false,
'button_link' => 'admin.php?page=wc-admin&task=shipping',
),
'appearance' => array(
'completed' => false,
'button_link' => 'admin.php?page=wc-admin&task=appearance',
),
);
/** /**
* # of completed tasks. * # of completed tasks.
@ -67,9 +42,6 @@ if ( ! class_exists( 'WC_Admin_Dashboard_Setup', false ) ) :
*/ */
public function __construct() { public function __construct() {
if ( $this->should_display_widget() ) { if ( $this->should_display_widget() ) {
$this->populate_general_tasks();
$this->populate_payment_tasks();
$this->completed_tasks_count = $this->get_completed_tasks_count();
add_meta_box( add_meta_box(
'wc_admin_dashboard_setup', 'wc_admin_dashboard_setup',
__( 'WooCommerce Setup', 'woocommerce' ), __( 'WooCommerce Setup', 'woocommerce' ),
@ -93,9 +65,10 @@ if ( ! class_exists( 'WC_Admin_Dashboard_Setup', false ) ) :
return; return;
} }
$button_link = $task['button_link']; $button_link = $this->get_button_link( $task );
$completed_tasks_count = $this->completed_tasks_count; $completed_tasks_count = $this->get_completed_tasks_count();
$tasks_count = count( $this->tasks ); $step_number = $this->get_completed_tasks_count() + 1;
$tasks_count = count( $this->get_tasks() );
// Given 'r' (circle element's r attr), dashoffset = ((100-$desired_percentage)/100) * PI * (r*2). // Given 'r' (circle element's r attr), dashoffset = ((100-$desired_percentage)/100) * PI * (r*2).
$progress_percentage = ( $completed_tasks_count / $tasks_count ) * 100; $progress_percentage = ( $completed_tasks_count / $tasks_count ) * 100;
@ -106,35 +79,68 @@ if ( ! class_exists( 'WC_Admin_Dashboard_Setup', false ) ) :
} }
/** /**
* Populate tasks from the database. * Get the button link for a given task.
*
* @param Task $task Task.
* @return string
*/ */
private function populate_general_tasks() { public function get_button_link( $task ) {
$tasks = get_option( 'woocommerce_task_list_tracked_completed_tasks', array() ); $url = $task->get_json()['actionUrl'];
foreach ( $tasks as $task ) {
if ( isset( $this->tasks[ $task ] ) ) { if ( substr( $url, 0, 4 ) === 'http' ) {
$this->tasks[ $task ]['completed'] = true; return $url;
$this->tasks[ $task ]['button_link'] = wc_admin_url( $this->tasks[ $task ]['button_link'] ); } elseif ( $url ) {
} return wc_admin_url( '&path=' . $url );
} }
return admin_url( 'admin.php?page=wc-admin&task=' . $task->get_id() );
} }
/** /**
* Getter for $tasks * Get the task list.
*
* @return array
*/
public function get_task_list() {
if ( $this->task_list ) {
return $this->task_list;
}
$this->set_task_list( TaskLists::get_list( 'setup' ) );
return $this->task_list;
}
/**
* Set the task list.
*/
public function set_task_list( $task_list ) {
return $this->task_list = $task_list;
}
/**
* Get the tasks.
* *
* @return array * @return array
*/ */
public function get_tasks() { public function get_tasks() {
if ( $this->tasks ) {
return $this->tasks;
}
$this->tasks = $this->get_task_list()->get_viewable_tasks();
return $this->tasks; return $this->tasks;
} }
/** /**
* Return # of completed tasks * Return # of completed tasks
*
* @return integer
*/ */
public function get_completed_tasks_count() { public function get_completed_tasks_count() {
$completed_tasks = array_filter( $completed_tasks = array_filter(
$this->tasks, $this->get_tasks(),
function( $task ) { function( $task ) {
return $task['completed']; return $task->is_complete();
} }
); );
@ -148,7 +154,7 @@ if ( ! class_exists( 'WC_Admin_Dashboard_Setup', false ) ) :
*/ */
private function get_next_task() { private function get_next_task() {
foreach ( $this->get_tasks() as $task ) { foreach ( $this->get_tasks() as $task ) {
if ( false === $task['completed'] ) { if ( false === $task->is_complete() ) {
return $task; return $task;
} }
} }
@ -161,51 +167,13 @@ if ( ! class_exists( 'WC_Admin_Dashboard_Setup', false ) ) :
* *
* @return bool * @return bool
*/ */
private function should_display_widget() { public function should_display_widget() {
return WC()->is_wc_admin_active() && return current_user_can( 'manage_woocommerce' ) &&
'yes' !== get_option( 'woocommerce_task_list_complete' ) && WC()->is_wc_admin_active() &&
'yes' !== get_option( 'woocommerce_task_list_hidden' ); ! $this->get_task_list()->is_complete() &&
! $this->get_task_list()->is_hidden();
} }
/**
* Populate payment tasks's visibility and completion
*/
private function populate_payment_tasks() {
$is_woo_payment_installed = is_plugin_active( 'woocommerce-payments/woocommerce-payments.php' );
$country = explode( ':', get_option( 'woocommerce_default_country', 'US:CA' ) )[0];
// woocommerce-payments requires its plugin activated and country must be US.
if ( ! $is_woo_payment_installed || 'US' !== $country ) {
unset( $this->tasks['woocommerce-payments'] );
}
// payments can't be used when woocommerce-payments exists and country is US.
if ( $is_woo_payment_installed && 'US' === $country ) {
unset( $this->tasks['payments'] );
}
if ( isset( $this->tasks['payments'] ) ) {
$gateways = WC()->payment_gateways->get_available_payment_gateways();
$enabled_gateways = array_filter(
$gateways,
function ( $gateway ) {
return 'yes' === $gateway->enabled;
}
);
$this->tasks['payments']['completed'] = ! empty( $enabled_gateways );
}
if ( isset( $this->tasks['woocommerce-payments'] ) ) {
$wc_pay_is_connected = false;
if ( class_exists( '\WC_Payments' ) ) {
$wc_payments_gateway = \WC_Payments::get_gateway();
$wc_pay_is_connected = method_exists( $wc_payments_gateway, 'is_connected' )
? $wc_payments_gateway->is_connected()
: false;
}
$this->tasks['woocommerce-payments']['completed'] = $wc_pay_is_connected;
}
}
} }
endif; endif;

View File

@ -57,7 +57,7 @@ class WC_Admin_Menus {
$woocommerce_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiPjxwYXRoIGZpbGw9IiNhMmFhYjIiIGQ9Ik02MTIuMTkyIDQyNi4zMzZjMC02Ljg5Ni0zLjEzNi01MS42LTI4LTUxLjYtMzcuMzYgMC00Ni43MDQgNzIuMjU2LTQ2LjcwNCA4Mi42MjQgMCAzLjQwOCAzLjE1MiA1OC40OTYgMjguMDMyIDU4LjQ5NiAzNC4xOTItLjAzMiA0Ni42NzItNzIuMjg4IDQ2LjY3Mi04OS41MnptMjAyLjE5MiAwYzAtNi44OTYtMy4xNTItNTEuNi0yOC4wMzItNTEuNi0zNy4yOCAwLTQ2LjYwOCA3Mi4yNTYtNDYuNjA4IDgyLjYyNCAwIDMuNDA4IDMuMDcyIDU4LjQ5NiAyNy45NTIgNTguNDk2IDM0LjE5Mi0uMDMyIDQ2LjY4OC03Mi4yODggNDYuNjg4LTg5LjUyek0xNDEuMjk2Ljc2OGMtNjguMjI0IDAtMTIzLjUwNCA1NS40ODgtMTIzLjUwNCAxMjMuOTJ2NjUwLjcyYzAgNjguNDMyIDU1LjI5NiAxMjMuOTIgMTIzLjUwNCAxMjMuOTJoMzM5LjgwOGwxMjMuNTA0IDEyMy45MzZWODk5LjMyOGgyNzguMDQ4YzY4LjIyNCAwIDEyMy41Mi01NS40NzIgMTIzLjUyLTEyMy45MnYtNjUwLjcyYzAtNjguNDMyLTU1LjI5Ni0xMjMuOTItMTIzLjUyLTEyMy45MmgtNzQxLjM2em01MjYuODY0IDQyMi4xNmMwIDU1LjA4OC0zMS4wODggMTU0Ljg4LTEwMi42NCAxNTQuODgtNi4yMDggMC0xOC40OTYtMy42MTYtMjUuNDI0LTYuMDE2LTMyLjUxMi0xMS4xNjgtNTAuMTkyLTQ5LjY5Ni01Mi4zNTItNjYuMjU2IDAgMC0zLjA3Mi0xNy43OTItMy4wNzItNDAuNzUyIDAtMjIuOTkyIDMuMDcyLTQ1LjMyOCAzLjA3Mi00NS4zMjggMTUuNTUyLTc1LjcyOCA0My41NTItMTA2LjczNiA5Ni40NDgtMTA2LjczNiA1OS4wNzItLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4ek00ODYuNDk2IDMwMi40YzAgMy4zOTItNDMuNTUyIDE0MS4xNjgtNDMuNTUyIDIxMy40MjR2NzUuNzEyYy0yLjU5MiAxMi4wOC00LjE2IDI0LjE0NC0yMS44MjQgMjQuMTQ0LTQ2LjYwOCAwLTg4Ljg4LTE1MS40NzItOTIuMDE2LTE2MS44NC02LjIwOCA2Ljg5Ni02Mi4yNCAxNjEuODQtOTYuNDQ4IDE2MS44NC0yNC44NjQgMC00My41NTItMTEzLjY0OC00Ni42MDgtMTIzLjkzNkMxNzYuNzA0IDQzNi42NzIgMTYwIDMzNC4yMjQgMTYwIDMyNy4zMjhjMC0yMC42NzIgMS4xNTItMzguNzM2IDI2LjA0OC0zOC43MzYgNi4yMDggMCAyMS42IDYuMDY0IDIzLjcxMiAxNy4xNjggMTEuNjQ4IDYyLjAzMiAxNi42ODggMTIwLjUxMiAyOS4xNjggMTg1Ljk2OCAxLjg1NiAyLjkyOCAxLjUwNCA3LjAwOCA0LjU2IDEwLjQzMiAzLjE1Mi0xMC4yODggNjYuOTI4LTE2OC43ODQgOTQuOTYtMTY4Ljc4NCAyMi41NDQgMCAzMC40IDQ0LjU5MiAzMy41MzYgNjEuODI0IDYuMjA4IDIwLjY1NiAxMy4wODggNTUuMjE2IDIyLjQxNiA4Mi43NTIgMC0xMy43NzYgMTIuNDgtMjAzLjEyIDY1LjM5Mi0yMDMuMTIgMTguNTkyLjAzMiAyNi43MDQgNi45MjggMjYuNzA0IDI3LjU2OHpNODcwLjMyIDQyMi45MjhjMCA1NS4wODgtMzEuMDg4IDE1NC44OC0xMDIuNjQgMTU0Ljg4LTYuMTkyIDAtMTguNDQ4LTMuNjE2LTI1LjQyNC02LjAxNi0zMi40MzItMTEuMTY4LTUwLjE3Ni00OS42OTYtNTIuMjg4LTY2LjI1NiAwIDAtMy44ODgtMTcuOTItMy44ODgtNDAuODk2czMuODg4LTQ1LjE4NCAzLjg4OC00NS4xODRjMTUuNTUyLTc1LjcyOCA0My40ODgtMTA2LjczNiA5Ni4zODQtMTA2LjczNiA1OS4xMDQtLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4eiIvPjwvc3ZnPg=='; $woocommerce_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiPjxwYXRoIGZpbGw9IiNhMmFhYjIiIGQ9Ik02MTIuMTkyIDQyNi4zMzZjMC02Ljg5Ni0zLjEzNi01MS42LTI4LTUxLjYtMzcuMzYgMC00Ni43MDQgNzIuMjU2LTQ2LjcwNCA4Mi42MjQgMCAzLjQwOCAzLjE1MiA1OC40OTYgMjguMDMyIDU4LjQ5NiAzNC4xOTItLjAzMiA0Ni42NzItNzIuMjg4IDQ2LjY3Mi04OS41MnptMjAyLjE5MiAwYzAtNi44OTYtMy4xNTItNTEuNi0yOC4wMzItNTEuNi0zNy4yOCAwLTQ2LjYwOCA3Mi4yNTYtNDYuNjA4IDgyLjYyNCAwIDMuNDA4IDMuMDcyIDU4LjQ5NiAyNy45NTIgNTguNDk2IDM0LjE5Mi0uMDMyIDQ2LjY4OC03Mi4yODggNDYuNjg4LTg5LjUyek0xNDEuMjk2Ljc2OGMtNjguMjI0IDAtMTIzLjUwNCA1NS40ODgtMTIzLjUwNCAxMjMuOTJ2NjUwLjcyYzAgNjguNDMyIDU1LjI5NiAxMjMuOTIgMTIzLjUwNCAxMjMuOTJoMzM5LjgwOGwxMjMuNTA0IDEyMy45MzZWODk5LjMyOGgyNzguMDQ4YzY4LjIyNCAwIDEyMy41Mi01NS40NzIgMTIzLjUyLTEyMy45MnYtNjUwLjcyYzAtNjguNDMyLTU1LjI5Ni0xMjMuOTItMTIzLjUyLTEyMy45MmgtNzQxLjM2em01MjYuODY0IDQyMi4xNmMwIDU1LjA4OC0zMS4wODggMTU0Ljg4LTEwMi42NCAxNTQuODgtNi4yMDggMC0xOC40OTYtMy42MTYtMjUuNDI0LTYuMDE2LTMyLjUxMi0xMS4xNjgtNTAuMTkyLTQ5LjY5Ni01Mi4zNTItNjYuMjU2IDAgMC0zLjA3Mi0xNy43OTItMy4wNzItNDAuNzUyIDAtMjIuOTkyIDMuMDcyLTQ1LjMyOCAzLjA3Mi00NS4zMjggMTUuNTUyLTc1LjcyOCA0My41NTItMTA2LjczNiA5Ni40NDgtMTA2LjczNiA1OS4wNzItLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4ek00ODYuNDk2IDMwMi40YzAgMy4zOTItNDMuNTUyIDE0MS4xNjgtNDMuNTUyIDIxMy40MjR2NzUuNzEyYy0yLjU5MiAxMi4wOC00LjE2IDI0LjE0NC0yMS44MjQgMjQuMTQ0LTQ2LjYwOCAwLTg4Ljg4LTE1MS40NzItOTIuMDE2LTE2MS44NC02LjIwOCA2Ljg5Ni02Mi4yNCAxNjEuODQtOTYuNDQ4IDE2MS44NC0yNC44NjQgMC00My41NTItMTEzLjY0OC00Ni42MDgtMTIzLjkzNkMxNzYuNzA0IDQzNi42NzIgMTYwIDMzNC4yMjQgMTYwIDMyNy4zMjhjMC0yMC42NzIgMS4xNTItMzguNzM2IDI2LjA0OC0zOC43MzYgNi4yMDggMCAyMS42IDYuMDY0IDIzLjcxMiAxNy4xNjggMTEuNjQ4IDYyLjAzMiAxNi42ODggMTIwLjUxMiAyOS4xNjggMTg1Ljk2OCAxLjg1NiAyLjkyOCAxLjUwNCA3LjAwOCA0LjU2IDEwLjQzMiAzLjE1Mi0xMC4yODggNjYuOTI4LTE2OC43ODQgOTQuOTYtMTY4Ljc4NCAyMi41NDQgMCAzMC40IDQ0LjU5MiAzMy41MzYgNjEuODI0IDYuMjA4IDIwLjY1NiAxMy4wODggNTUuMjE2IDIyLjQxNiA4Mi43NTIgMC0xMy43NzYgMTIuNDgtMjAzLjEyIDY1LjM5Mi0yMDMuMTIgMTguNTkyLjAzMiAyNi43MDQgNi45MjggMjYuNzA0IDI3LjU2OHpNODcwLjMyIDQyMi45MjhjMCA1NS4wODgtMzEuMDg4IDE1NC44OC0xMDIuNjQgMTU0Ljg4LTYuMTkyIDAtMTguNDQ4LTMuNjE2LTI1LjQyNC02LjAxNi0zMi40MzItMTEuMTY4LTUwLjE3Ni00OS42OTYtNTIuMjg4LTY2LjI1NiAwIDAtMy44ODgtMTcuOTItMy44ODgtNDAuODk2czMuODg4LTQ1LjE4NCAzLjg4OC00NS4xODRjMTUuNTUyLTc1LjcyOCA0My40ODgtMTA2LjczNiA5Ni4zODQtMTA2LjczNiA1OS4xMDQtLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4eiIvPjwvc3ZnPg==';
if ( current_user_can( 'edit_others_shop_orders' ) ) { if ( self::can_view_woocommerce_menu_item() ) {
$menu[] = array( '', 'read', 'separator-woocommerce', '', 'wp-menu-separator woocommerce' ); // WPCS: override ok. $menu[] = array( '', 'read', 'separator-woocommerce', '', 'wp-menu-separator woocommerce' ); // WPCS: override ok.
} }
@ -70,7 +70,7 @@ class WC_Admin_Menus {
* Add menu item. * Add menu item.
*/ */
public function reports_menu() { public function reports_menu() {
if ( current_user_can( 'edit_others_shop_orders' ) ) { if ( self::can_view_woocommerce_menu_item() ) {
add_submenu_page( 'woocommerce', __( 'Reports', 'woocommerce' ), __( 'Reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ) ); add_submenu_page( 'woocommerce', __( 'Reports', 'woocommerce' ), __( 'Reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ) );
} else { } else {
add_menu_page( __( 'Sales reports', 'woocommerce' ), __( 'Sales reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ), 'dashicons-chart-bar', '55.6' ); add_menu_page( __( 'Sales reports', 'woocommerce' ), __( 'Sales reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ), 'dashicons-chart-bar', '55.6' );
@ -86,6 +86,13 @@ class WC_Admin_Menus {
add_action( 'load-' . $settings_page, array( $this, 'settings_page_init' ) ); add_action( 'load-' . $settings_page, array( $this, 'settings_page_init' ) );
} }
/**
* Check if the user can access the top-level WooCommerce item.
*/
public static function can_view_woocommerce_menu_item() {
return current_user_can( 'edit_others_shop_orders' );
}
/** /**
* Loads gateways and shipping methods into memory for use within settings. * Loads gateways and shipping methods into memory for use within settings.
*/ */
@ -241,7 +248,7 @@ class WC_Admin_Menus {
* @return bool * @return bool
*/ */
public function custom_menu_order( $enabled ) { public function custom_menu_order( $enabled ) {
return $enabled || current_user_can( 'edit_others_shop_orders' ); return $enabled || self::can_view_woocommerce_menu_item();
} }
/** /**

View File

@ -138,6 +138,11 @@ class WC_Admin_Notices {
'php72_required_in_woo_65', 'php72_required_in_woo_65',
__( '<h4>PHP version requirements will change soon</h4><p>WooCommerce 6.5, scheduled for <b>May 2022</b>, will require PHP 7.2 or newer to work. Your server is currently running an older version of PHP, so this change will impact your store. Upgrading to at least PHP 7.4 is recommended. <b><a href="https://developer.woocommerce.com/2022/01/05/new-requirement-for-woocommerce-6-5-php-7-2/">Learn more about this change.</a></b></p>', 'woocommerce' ) __( '<h4>PHP version requirements will change soon</h4><p>WooCommerce 6.5, scheduled for <b>May 2022</b>, will require PHP 7.2 or newer to work. Your server is currently running an older version of PHP, so this change will impact your store. Upgrading to at least PHP 7.4 is recommended. <b><a href="https://developer.woocommerce.com/2022/01/05/new-requirement-for-woocommerce-6-5-php-7-2/">Learn more about this change.</a></b></p>', 'woocommerce' )
); );
$wp_version_is_ok = version_compare( get_bloginfo( 'version' ), WC_NOTICE_MIN_WP_VERSION, '>=' );
if ( $wp_version_is_ok ) {
self::hide_notice( WC_PHP_MIN_REQUIREMENTS_NOTICE );
}
} }
/** /**
@ -209,14 +214,23 @@ class WC_Admin_Notices {
$hide_notice = sanitize_text_field( wp_unslash( $_GET['wc-hide-notice'] ) ); // WPCS: input var ok, CSRF ok. $hide_notice = sanitize_text_field( wp_unslash( $_GET['wc-hide-notice'] ) ); // WPCS: input var ok, CSRF ok.
self::remove_notice( $hide_notice ); self::hide_notice( $hide_notice );
update_user_meta( get_current_user_id(), 'dismissed_' . $hide_notice . '_notice', true );
do_action( 'woocommerce_hide_' . $hide_notice . '_notice' );
} }
} }
/**
* Hide a single notice.
*
* @param $name Notice name.
*/
private static function hide_notice( $name ) {
self::remove_notice( $name );
update_user_meta( get_current_user_id(), 'dismissed_' . $name . '_notice', true );
do_action( 'woocommerce_hide_' . $name . '_notice' );
}
/** /**
* Add notices + styles if needed. * Add notices + styles if needed.
*/ */

View File

@ -92,7 +92,13 @@ class WC_Product_CSV_Importer_Controller {
* @return bool * @return bool
*/ */
public static function is_file_valid_csv( $file, $check_path = true ) { public static function is_file_valid_csv( $file, $check_path = true ) {
if ( $check_path && apply_filters( 'woocommerce_product_csv_importer_check_import_file_path', true ) && false !== stripos( $file, '://' ) ) { /**
* Can be used to override the decision to check the import file path.
*
* @param bool $check_import_file_path If the import file path should be checked.
* @param string $file Path of the file to be checked.
*/
if ( $check_path && apply_filters( 'woocommerce_product_csv_importer_check_import_file_path', true, $file ) && false !== stripos( $file, '://' ) ) {
return false; return false;
} }

View File

@ -57,8 +57,9 @@ defined( 'ABSPATH' ) || exit;
* @since 3.6.0 * @since 3.6.0
* *
* @param WP_Post $variation Post data. * @param WP_Post $variation Post data.
* @param int $loop Position in the loop.
*/ */
do_action( 'woocommerce_variation_header', $variation ); do_action( 'woocommerce_variation_header', $variation, $loop );
?> ?>
</h3> </h3>
<div class="woocommerce_variable_attributes wc-metabox-content" style="display: none;"> <div class="woocommerce_variable_attributes wc-metabox-content" style="display: none;">

View File

@ -15,7 +15,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<circle r="6.5" cx="10" cy="10" fill="transparent" stroke-dasharray="40.859" stroke-dashoffset="0"></circle> <circle r="6.5" cx="10" cy="10" fill="transparent" stroke-dasharray="40.859" stroke-dashoffset="0"></circle>
<circle class="bar" r="6.5" cx="190" cy="10" fill="transparent" stroke-dasharray="40.859" stroke-dashoffset="<?php echo esc_attr( $circle_dashoffset ); ?>" transform='rotate(-90 100 100)'></circle> <circle class="bar" r="6.5" cx="190" cy="10" fill="transparent" stroke-dasharray="40.859" stroke-dashoffset="<?php echo esc_attr( $circle_dashoffset ); ?>" transform='rotate(-90 100 100)'></circle>
</svg> </svg>
<span><?php echo esc_html_e( 'Step', 'woocommerce' ); ?> <?php echo esc_html( $completed_tasks_count ); ?> <?php echo esc_html_e( 'of', 'woocommerce' ); ?> <?php echo esc_html( $tasks_count ); ?></span> <span><?php echo esc_html_e( 'Step', 'woocommerce' ); ?> <?php echo esc_html( $step_number ); ?> <?php echo esc_html_e( 'of', 'woocommerce' ); ?> <?php echo esc_html( $tasks_count ); ?></span>
</span> </span>
<div class="description"> <div class="description">

View File

@ -47,7 +47,7 @@ final class WC_Cart_Session {
public function init() { public function init() {
add_action( 'wp_loaded', array( $this, 'get_cart_from_session' ) ); add_action( 'wp_loaded', array( $this, 'get_cart_from_session' ) );
add_action( 'woocommerce_cart_emptied', array( $this, 'destroy_cart_session' ) ); add_action( 'woocommerce_cart_emptied', array( $this, 'destroy_cart_session' ) );
add_action( 'woocommerce_after_calculate_totals', array( $this, 'set_session' ) ); add_action( 'woocommerce_after_calculate_totals', array( $this, 'set_session' ), 1000 );
add_action( 'woocommerce_cart_loaded_from_session', array( $this, 'set_session' ) ); add_action( 'woocommerce_cart_loaded_from_session', array( $this, 'set_session' ) );
add_action( 'woocommerce_removed_coupon', array( $this, 'set_session' ) ); add_action( 'woocommerce_removed_coupon', array( $this, 'set_session' ) );

View File

@ -247,19 +247,21 @@ class WC_Checkout {
if ( 'no' === get_option( 'woocommerce_registration_generate_username' ) ) { if ( 'no' === get_option( 'woocommerce_registration_generate_username' ) ) {
$this->fields['account']['account_username'] = array( $this->fields['account']['account_username'] = array(
'type' => 'text', 'type' => 'text',
'label' => __( 'Account username', 'woocommerce' ), 'label' => __( 'Account username', 'woocommerce' ),
'required' => true, 'required' => true,
'placeholder' => esc_attr__( 'Username', 'woocommerce' ), 'placeholder' => esc_attr__( 'Username', 'woocommerce' ),
'autocomplete' => 'username',
); );
} }
if ( 'no' === get_option( 'woocommerce_registration_generate_password' ) ) { if ( 'no' === get_option( 'woocommerce_registration_generate_password' ) ) {
$this->fields['account']['account_password'] = array( $this->fields['account']['account_password'] = array(
'type' => 'password', 'type' => 'password',
'label' => __( 'Create account password', 'woocommerce' ), 'label' => __( 'Create account password', 'woocommerce' ),
'required' => true, 'required' => true,
'placeholder' => esc_attr__( 'Password', 'woocommerce' ), 'placeholder' => esc_attr__( 'Password', 'woocommerce' ),
'autocomplete' => 'new-password',
); );
} }
$this->fields = apply_filters( 'woocommerce_checkout_fields', $this->fields ); $this->fields = apply_filters( 'woocommerce_checkout_fields', $this->fields );

View File

@ -1056,7 +1056,6 @@ class WC_Countries {
'GT' => array( 'GT' => array(
'postcode' => array( 'postcode' => array(
'required' => false, 'required' => false,
'hidden' => true,
), ),
'state' => array( 'state' => array(
'label' => __( 'Department', 'woocommerce' ), 'label' => __( 'Department', 'woocommerce' ),

View File

@ -169,6 +169,10 @@ class WC_Install {
'wc_update_600_migrate_rate_limit_options', 'wc_update_600_migrate_rate_limit_options',
'wc_update_600_db_version', 'wc_update_600_db_version',
), ),
'6.3.0' => array(
'wc_update_630_create_product_attributes_lookup_table',
'wc_update_630_db_version',
),
); );
/** /**
@ -586,22 +590,22 @@ class WC_Install {
$pages = apply_filters( $pages = apply_filters(
'woocommerce_create_pages', 'woocommerce_create_pages',
array( array(
'shop' => array( 'shop' => array(
'name' => _x( 'shop', 'Page slug', 'woocommerce' ), 'name' => _x( 'shop', 'Page slug', 'woocommerce' ),
'title' => _x( 'Shop', 'Page title', 'woocommerce' ), 'title' => _x( 'Shop', 'Page title', 'woocommerce' ),
'content' => '', 'content' => '',
), ),
'cart' => array( 'cart' => array(
'name' => _x( 'cart', 'Page slug', 'woocommerce' ), 'name' => _x( 'cart', 'Page slug', 'woocommerce' ),
'title' => _x( 'Cart', 'Page title', 'woocommerce' ), 'title' => _x( 'Cart', 'Page title', 'woocommerce' ),
'content' => '<!-- wp:shortcode -->[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']<!-- /wp:shortcode -->', 'content' => '<!-- wp:shortcode -->[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']<!-- /wp:shortcode -->',
), ),
'checkout' => array( 'checkout' => array(
'name' => _x( 'checkout', 'Page slug', 'woocommerce' ), 'name' => _x( 'checkout', 'Page slug', 'woocommerce' ),
'title' => _x( 'Checkout', 'Page title', 'woocommerce' ), 'title' => _x( 'Checkout', 'Page title', 'woocommerce' ),
'content' => '<!-- wp:shortcode -->[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']<!-- /wp:shortcode -->', 'content' => '<!-- wp:shortcode -->[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']<!-- /wp:shortcode -->',
), ),
'myaccount' => array( 'myaccount' => array(
'name' => _x( 'my-account', 'Page slug', 'woocommerce' ), 'name' => _x( 'my-account', 'Page slug', 'woocommerce' ),
'title' => _x( 'My account', 'Page title', 'woocommerce' ), 'title' => _x( 'My account', 'Page title', 'woocommerce' ),
'content' => '<!-- wp:shortcode -->[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']<!-- /wp:shortcode -->', 'content' => '<!-- wp:shortcode -->[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']<!-- /wp:shortcode -->',

View File

@ -651,7 +651,7 @@ class WC_Email extends WC_Settings_API {
$message = apply_filters( 'woocommerce_mail_content', $this->style_inline( $message ) ); $message = apply_filters( 'woocommerce_mail_content', $this->style_inline( $message ) );
$mail_callback = apply_filters( 'woocommerce_mail_callback', 'wp_mail', $this ); $mail_callback = apply_filters( 'woocommerce_mail_callback', 'wp_mail', $this );
$mail_callback_params = apply_filters( 'woocommerce_mail_callback_params', array( $to, $subject, $message, $headers, $attachments ), $this ); $mail_callback_params = apply_filters( 'woocommerce_mail_callback_params', array( $to, wp_specialchars_decode( $subject ), $message, $headers, $attachments ), $this );
$return = $mail_callback( ...$mail_callback_params ); $return = $mail_callback( ...$mail_callback_params );
remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) ); remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );

View File

@ -184,6 +184,6 @@ abstract class WC_CSV_Batch_Exporter extends WC_CSV_Exporter {
* @return int * @return int
*/ */
public function get_percent_complete() { public function get_percent_complete() {
return $this->total_rows ? floor( ( $this->get_total_exported() / $this->total_rows ) * 100 ) : 100; return $this->total_rows ? (int) floor( ( $this->get_total_exported() / $this->total_rows ) * 100 ) : 100;
} }
} }

View File

@ -244,7 +244,7 @@ class WC_REST_Customers_V1_Controller extends WC_REST_Controller {
$prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
$prepared_args['search'] = $request['search']; $prepared_args['search'] = $request['search'];
if ( '' !== $prepared_args['search'] ) { if ( ! empty( $prepared_args['search'] ) ) {
$prepared_args['search'] = '*' . $prepared_args['search'] . '*'; $prepared_args['search'] = '*' . $prepared_args['search'] . '*';
} }

View File

@ -924,7 +924,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
* @return array * @return array
*/ */
protected function get_order_statuses() { protected function get_order_statuses() {
$order_statuses = array(); $order_statuses = array( 'auto-draft' );
foreach ( array_keys( wc_get_order_statuses() ) as $status ) { foreach ( array_keys( wc_get_order_statuses() ) as $status ) {
$order_statuses[] = str_replace( 'wc-', '', $status ); $order_statuses[] = str_replace( 'wc-', '', $status );

View File

@ -20,7 +20,7 @@ class WC_Products_Tracking {
add_action( 'load-edit.php', array( $this, 'track_products_view' ), 10 ); add_action( 'load-edit.php', array( $this, 'track_products_view' ), 10 );
add_action( 'load-edit-tags.php', array( $this, 'track_categories_and_tags_view' ), 10, 2 ); add_action( 'load-edit-tags.php', array( $this, 'track_categories_and_tags_view' ), 10, 2 );
add_action( 'edit_post', array( $this, 'track_product_updated' ), 10, 2 ); add_action( 'edit_post', array( $this, 'track_product_updated' ), 10, 2 );
add_action( 'transition_post_status', array( $this, 'track_product_published' ), 10, 3 ); add_action( 'wp_after_insert_post', array( $this, 'track_product_published' ), 10, 4 );
add_action( 'created_product_cat', array( $this, 'track_product_category_created' ) ); add_action( 'created_product_cat', array( $this, 'track_product_category_created' ) );
add_action( 'add_meta_boxes_product', array( $this, 'track_product_updated_client_side' ), 10 ); add_action( 'add_meta_boxes_product', array( $this, 'track_product_updated_client_side' ), 10 );
} }
@ -156,21 +156,29 @@ class WC_Products_Tracking {
/** /**
* Send a Tracks event when a product is published. * Send a Tracks event when a product is published.
* *
* @param string $new_status New post_status. * @param int $post_id Post ID.
* @param string $old_status Previous post_status. * @param WP_Post $post Post object.
* @param object $post WordPress post. * @param bool $update Whether this is an existing post being updated.
* @param null|WP_Post $post_before Null for new posts, the WP_Post object prior
* to the update for updated posts.
*/ */
public function track_product_published( $new_status, $old_status, $post ) { public function track_product_published( $post_id, $post, $update, $post_before ) {
if ( if (
'product' !== $post->post_type || 'product' !== $post->post_type ||
'publish' !== $new_status || 'publish' !== $post->post_status ||
'publish' === $old_status ( $post_before && 'publish' === $post_before->post_status )
) { ) {
return; return;
} }
$product = wc_get_product( $post_id );
$properties = array( $properties = array(
'product_id' => $post->ID, 'product_id' => $post_id,
'product_type' => $product->get_type(),
'is_downloadable' => $product->is_downloadable() ? 'yes' : 'no',
'is_virtual' => $product->is_virtual() ? 'yes' : 'no',
'manage_stock' => $product->get_manage_stock() ? 'yes' : 'no',
); );
WC_Tracks::record_event( 'product_add_publish', $properties ); WC_Tracks::record_event( 'product_add_publish', $properties );

View File

@ -465,9 +465,10 @@ function wc_is_file_valid_csv( $file, $check_path = true ) {
* Filter check for CSV file path. * Filter check for CSV file path.
* *
* @since 3.6.4 * @since 3.6.4
* @param bool $check_import_file_path If requires file path check. Defaults to true. * @param bool $check_import_file_path If requires file path check. Defaults to true.
* @param string $file Path of the file to be checked.
*/ */
$check_import_file_path = apply_filters( 'woocommerce_csv_importer_check_import_file_path', true ); $check_import_file_path = apply_filters( 'woocommerce_csv_importer_check_import_file_path', true, $file );
if ( $check_path && $check_import_file_path && false !== stripos( $file, '://' ) ) { if ( $check_path && $check_import_file_path && false !== stripos( $file, '://' ) ) {
return false; return false;

View File

@ -11,6 +11,8 @@
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Internal\AssignDefaultCategory; use Automattic\WooCommerce\Internal\AssignDefaultCategory;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
/** /**
* Update file paths for 2.0 * Update file paths for 2.0
@ -2306,7 +2308,7 @@ function wc_update_560_db_version() {
function wc_update_600_migrate_rate_limit_options() { function wc_update_600_migrate_rate_limit_options() {
global $wpdb; global $wpdb;
$rate_limits = $wpdb->get_results( $rate_limits = $wpdb->get_results(
" "
SELECT option_name, option_value SELECT option_name, option_value
FROM $wpdb->options FROM $wpdb->options
@ -2335,3 +2337,28 @@ function wc_update_600_migrate_rate_limit_options() {
function wc_update_600_db_version() { function wc_update_600_db_version() {
WC_Install::update_db_version( '6.0.0' ); WC_Install::update_db_version( '6.0.0' );
} }
/**
* Create the product attributes lookup table and initiate its filling,
* unless the table had been already created manually (via the tools page).
*
* @return false Always false, since the LookupDataStore class handles all the data filling process.
*/
function wc_update_630_create_product_attributes_lookup_table() {
$data_store = wc_get_container()->get( LookupDataStore::class );
if ( $data_store->check_lookup_table_exists() ) {
return false;
}
$data_regenerator = wc_get_container()->get( DataRegenerator::class );
$data_regenerator->initiate_regeneration();
return false;
}
/**
*
* Update DB version to 6.3.0.
*/
function wc_update_630_db_version() {
WC_Install::update_db_version( '6.3.0' );
}

View File

@ -601,7 +601,7 @@ dl.variation,
margin-right: 0.5rem; margin-right: 0.5rem;
} }
input { input[type="number"] {
width: 5em; width: 5em;
} }
} }

View File

@ -185,7 +185,7 @@ $tt2-gray: #f7f7f7;
button[name='add-to-cart'], button[name='add-to-cart'],
input[name='submit'], input[name='submit'],
button.single_add_to_cart_button, button.single_add_to_cart_button,
button[type='submit'] { button[type='submit']:not(.wp-block-search__button) {
display: inline-block; display: inline-block;
text-align: center; text-align: center;
word-break: break-word; word-break: break-word;
@ -294,10 +294,20 @@ $tt2-gray: #f7f7f7;
border-radius: 1rem; border-radius: 1rem;
border-style: solid; border-style: solid;
line-height: 1.5rem; line-height: 1.5rem;
padding: 0 0.5rem 0 0.5rem; padding: 0;
font-size: 0.6rem; font-size: 0.6rem;
background: var( --wp--preset--color--white );; background: var( --wp--preset--color--white );
border-color: 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,<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" focusable="false" viewBox="0 0 24 24" width="24" height="24"><path d="M13.5 6C10.5 6 8 8.5 8 11.5c0 1.1.3 2.1.9 3l-3.4 3 1 1.1 3.4-2.9c1 .9 2.2 1.4 3.6 1.4 3 0 5.5-2.5 5.5-5.5C19 8.5 16.5 6 13.5 6zm0 9.5c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z" /></svg>');
display: block;
transform: rotateY(180deg);
margin-left: 1.55rem;
}
} }
figure.woocommerce-product-gallery__wrapper { figure.woocommerce-product-gallery__wrapper {
@ -747,6 +757,12 @@ $tt2-gray: #f7f7f7;
color: var(--wp--preset--color--black); color: var(--wp--preset--color--black);
} }
} }
&.woocommerce-orders-table__cell-order-actions {
a.button {
display: block;
}
}
} }
} }
} }
@ -884,7 +900,8 @@ $tt2-gray: #f7f7f7;
} }
} }
.woocommerce-checkout { .woocommerce-checkout,
&.woocommerce-order-pay {
display: table; display: table;
h3 { h3 {
@ -923,11 +940,13 @@ $tt2-gray: #f7f7f7;
.woocommerce-billing-fields__field-wrapper, .woocommerce-billing-fields__field-wrapper,
.woocommerce-checkout-review-order-table, .woocommerce-checkout-review-order-table,
.woocommerce-checkout-payment { .woocommerce-checkout-payment,
#payment {
margin-top: 4rem; margin-top: 4rem;
} }
.woocommerce-checkout-review-order-table { .woocommerce-checkout-review-order-table,
#order_review .shop_table {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
@ -1204,3 +1223,63 @@ $tt2-gray: #f7f7f7;
} }
} }
} }
.wp-block-search {
.wp-block-search__label {
font-weight: normal;
}
.wp-block-search__input {
padding: .9rem 1.1rem;
border: 1px solid var(--wp--preset--color--black);
}
.wp-block-search__button {
padding: 1rem 1.2rem;
}
}
.wc-block-product-search {
form {
.wc-block-product-search__fields {
display: flex;
flex: auto;
flex-wrap: nowrap;
max-width: 100%;
.wc-block-product-search__field {
padding: .9rem 1.1rem;
flex-grow: 1;
border: 1px solid var(--wp--preset--color--black);
font-size: inherit;
font-family: inherit;
}
.wc-block-product-search__button {
display: flex;
background-color: var( --wp--preset--color--primary );
color: #fff;
border: 1px solid var(--wp--preset--color--black);
padding: 1rem 1.2rem;
margin: 0 0 0 .7rem;
}
}
}
}
.woocommerce-store-notice {
color: var(--wp--preset--color--black);
border-top: 2px solid var( --wp--preset--color--primary );
background: $tt2-gray;
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;
}
}

View File

@ -548,7 +548,7 @@ dl.variation,
margin-right: 0.5rem; margin-right: 0.5rem;
} }
input { input[type="number"] {
width: 5em; width: 5em;
} }
} }

View File

@ -21,7 +21,9 @@ defined( 'ABSPATH' ) || exit;
* the data store classes) whenever a product is created/updated. * the data store classes) whenever a product is created/updated.
* *
* Additionally, after the regeneration is completed a 'woocommerce_attribute_lookup_enabled' option * Additionally, after the regeneration is completed a 'woocommerce_attribute_lookup_enabled' option
* with a value of 'no' will have been created. * with a value of 'yes' will have been created, thus effectively enabling the table usage
* (with an exception: if the regeneration was manually aborted via deleting the
* 'woocommerce_attribute_lookup_regeneration_in_progress' option) the option will be set to 'no'.
* *
* This class also adds two entries to the Status - Tools menu: one for manually regenerating the table contents, * This class also adds two entries to the Status - Tools menu: one for manually regenerating the table contents,
* and another one for enabling or disabling the actual lookup table usage. * and another one for enabling or disabling the actual lookup table usage.
@ -84,16 +86,11 @@ class DataRegenerator {
* deletes the lookup table and related options if they exist, * deletes the lookup table and related options if they exist,
* then it creates the table and runs the first step of the regeneration process. * then it creates the table and runs the first step of the regeneration process.
* *
* This is the method that should be used as a callback for a data regeneration in wc-update-functions, e.g.: * This method is intended ONLY to be used as a callback for a db update in wc-update-functions,
* * regeneration triggered from the tools page will use initiate_regeneration_from_tools_page instead.
* function wc_update_XX_regenerate_product_attributes_lookup_table() {
* wc_get_container()->get(DataRegenerator::class)->initiate_regeneration();
* return false;
* }
*
* (Note how we are returning "false" since the class handles the step scheduling by itself).
*/ */
public function initiate_regeneration() { public function initiate_regeneration() {
$this->data_store->unset_regeneration_aborted_flag();
$this->enable_or_disable_lookup_table_usage( false ); $this->enable_or_disable_lookup_table_usage( false );
$this->delete_all_attributes_lookup_data(); $this->delete_all_attributes_lookup_data();
@ -101,23 +98,19 @@ class DataRegenerator {
if ( $products_exist ) { if ( $products_exist ) {
$this->enqueue_regeneration_step_run(); $this->enqueue_regeneration_step_run();
} else { } else {
$this->finalize_regeneration(); $this->finalize_regeneration( true );
} }
} }
/** /**
* Delete all the existing data related to the lookup table, including the table itself. * Delete all the existing data related to the lookup table, including the table itself.
*
* Shortcut to run this method in case the debug tools UI isn't available or for quick debugging:
*
* wp eval "wc_get_container()->get(Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator::class)->delete_all_attributes_lookup_data();"
*/ */
public function delete_all_attributes_lookup_data() { private function delete_all_attributes_lookup_data() {
global $wpdb; global $wpdb;
delete_option( 'woocommerce_attribute_lookup_enabled' ); delete_option( 'woocommerce_attribute_lookup_enabled' );
delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ); delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' );
delete_option( 'woocommerce_attribute_lookup_last_products_page_processed' ); delete_option( 'woocommerce_attribute_lookup_processed_count' );
$this->data_store->unset_regeneration_in_progress_flag(); $this->data_store->unset_regeneration_in_progress_flag();
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
@ -167,7 +160,7 @@ CREATE TABLE ' . $this->lookup_table_name . '(
$this->data_store->set_regeneration_in_progress_flag(); $this->data_store->set_regeneration_in_progress_flag();
update_option( 'woocommerce_attribute_lookup_last_product_id_to_process', current( $last_existing_product_id ) ); update_option( 'woocommerce_attribute_lookup_last_product_id_to_process', current( $last_existing_product_id ) );
update_option( 'woocommerce_attribute_lookup_last_products_page_processed', 0 ); update_option( 'woocommerce_attribute_lookup_processed_count', 0 );
return true; return true;
} }
@ -178,6 +171,10 @@ CREATE TABLE ' . $this->lookup_table_name . '(
*/ */
private function run_regeneration_step_callback() { private function run_regeneration_step_callback() {
if ( ! $this->data_store->regeneration_is_in_progress() ) { if ( ! $this->data_store->regeneration_is_in_progress() ) {
// No regeneration in progress at this point means that the regeneration process
// was manually aborted via deleting the 'woocommerce_attribute_lookup_regeneration_in_progress' option.
$this->data_store->set_regeneration_aborted_flag();
$this->finalize_regeneration( false );
return; return;
} }
@ -185,7 +182,7 @@ CREATE TABLE ' . $this->lookup_table_name . '(
if ( $result ) { if ( $result ) {
$this->enqueue_regeneration_step_run(); $this->enqueue_regeneration_step_run();
} else { } else {
$this->finalize_regeneration(); $this->finalize_regeneration( true );
} }
} }
@ -209,14 +206,21 @@ CREATE TABLE ' . $this->lookup_table_name . '(
* @return bool True if more steps need to be run, false otherwise. * @return bool True if more steps need to be run, false otherwise.
*/ */
private function do_regeneration_step() { private function do_regeneration_step() {
$last_products_page_processed = get_option( 'woocommerce_attribute_lookup_last_products_page_processed' ); /**
$current_products_page = (int) $last_products_page_processed + 1; * Filter to alter the count of products that will be processed in each step of the product attributes lookup table regeneration process.
*
* @since 6.3
* @param int $count Default processing step size.
*/
$products_per_generation_step = apply_filters( 'woocommerce_attribute_lookup_regeneration_step_size', self::PRODUCTS_PER_GENERATION_STEP );
$products_already_processed = get_option( 'woocommerce_attribute_lookup_processed_count', 0 );
$product_ids = WC()->call_function( $product_ids = WC()->call_function(
'wc_get_products', 'wc_get_products',
array( array(
'limit' => self::PRODUCTS_PER_GENERATION_STEP, 'limit' => $products_per_generation_step,
'page' => $current_products_page, 'offset' => $products_already_processed,
'orderby' => array( 'orderby' => array(
'ID' => 'ASC', 'ID' => 'ASC',
), ),
@ -232,7 +236,8 @@ CREATE TABLE ' . $this->lookup_table_name . '(
$this->data_store->create_data_for_product( $id ); $this->data_store->create_data_for_product( $id );
} }
update_option( 'woocommerce_attribute_lookup_last_products_page_processed', $current_products_page ); $products_already_processed += count( $product_ids );
update_option( 'woocommerce_attribute_lookup_processed_count', $products_already_processed );
$last_product_id_to_process = get_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ); $last_product_id_to_process = get_option( 'woocommerce_attribute_lookup_last_product_id_to_process' );
return end( $product_ids ) < $last_product_id_to_process; return end( $product_ids ) < $last_product_id_to_process;
@ -240,11 +245,14 @@ CREATE TABLE ' . $this->lookup_table_name . '(
/** /**
* Cleanup/final option setup after the regeneration has been completed. * Cleanup/final option setup after the regeneration has been completed.
*
* @param bool $enable_usage Whether the table usage should be enabled or not.
*
*/ */
private function finalize_regeneration() { private function finalize_regeneration( bool $enable_usage ) {
delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ); delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' );
delete_option( 'woocommerce_attribute_lookup_last_products_page_processed' ); delete_option( 'woocommerce_attribute_lookup_processed_count' );
update_option( 'woocommerce_attribute_lookup_enabled', 'no' ); update_option( 'woocommerce_attribute_lookup_enabled', $enable_usage ? 'yes' : 'no' );
$this->data_store->unset_regeneration_in_progress_flag(); $this->data_store->unset_regeneration_in_progress_flag();
} }
@ -255,76 +263,44 @@ CREATE TABLE ' . $this->lookup_table_name . '(
* @return array The tools array with the entry added. * @return array The tools array with the entry added.
*/ */
private function add_initiate_regeneration_entry_to_tools_array( array $tools_array ) { private function add_initiate_regeneration_entry_to_tools_array( array $tools_array ) {
$lookup_table_exists = $this->data_store->check_lookup_table_exists(); if ( ! $this->data_store->check_lookup_table_exists() ) {
$generation_is_in_progress = $this->data_store->regeneration_is_in_progress(); return $tools_array;
// Regenerate table.
if ( $lookup_table_exists ) {
$generate_item_name = __( 'Regenerate the product attributes lookup table', 'woocommerce' );
$generate_item_desc = __( 'This tool will regenerate the product attributes lookup table data from existing product(s) data. This process may take a while.', 'woocommerce' );
$generate_item_return = __( 'Product attributes lookup table data is regenerating', 'woocommerce' );
$generate_item_button = __( 'Regenerate', 'woocommerce' );
} else {
$generate_item_name = __( 'Create and fill product attributes lookup table', 'woocommerce' );
$generate_item_desc = __( 'This tool will create the product attributes lookup table data and fill it with existing products data. This process may take a while.', 'woocommerce' );
$generate_item_return = __( 'Product attributes lookup table is being filled', 'woocommerce' );
$generate_item_button = __( 'Create', 'woocommerce' );
} }
$entry = array( $generation_is_in_progress = $this->data_store->regeneration_is_in_progress();
'name' => $generate_item_name,
'desc' => $generate_item_desc,
'requires_refresh' => true,
'callback' => function() use ( $generate_item_return ) {
$this->initiate_regeneration_from_tools_page();
return $generate_item_return;
},
);
if ( $lookup_table_exists ) { $entry = array(
$entry['selector'] = array( 'name' => __( 'Regenerate the product attributes lookup table', 'woocommerce' ),
'desc' => __( 'This tool will regenerate the product attributes lookup table data from existing product(s) data. This process may take a while.', 'woocommerce' ),
'requires_refresh' => true,
'callback' => function() {
$this->initiate_regeneration_from_tools_page();
return __( 'Product attributes lookup table data is regenerating', 'woocommerce' );
},
'selector' => array(
'description' => __( 'Select a product to regenerate the data for, or leave empty for a full table regeneration:', 'woocommerce' ), 'description' => __( 'Select a product to regenerate the data for, or leave empty for a full table regeneration:', 'woocommerce' ),
'class' => 'wc-product-search', 'class' => 'wc-product-search',
'search_action' => 'woocommerce_json_search_products', 'search_action' => 'woocommerce_json_search_products',
'name' => 'regenerate_product_attribute_lookup_data_product_id', 'name' => 'regenerate_product_attribute_lookup_data_product_id',
'placeholder' => esc_attr__( 'Search for a product&hellip;', 'woocommerce' ), 'placeholder' => esc_attr__( 'Search for a product&hellip;', 'woocommerce' ),
); ),
} );
if ( $generation_is_in_progress ) { if ( $generation_is_in_progress ) {
$entry['button'] = sprintf( $entry['button'] = sprintf(
/* translators: %d: How many products have been processed so far. */ /* translators: %d: How many products have been processed so far. */
__( 'Filling in progress (%d)', 'woocommerce' ), __( 'Filling in progress (%d)', 'woocommerce' ),
get_option( 'woocommerce_attribute_lookup_last_products_page_processed', 0 ) * self::PRODUCTS_PER_GENERATION_STEP get_option( 'woocommerce_attribute_lookup_processed_count', 0 )
); );
$entry['disabled'] = true; $entry['disabled'] = true;
} else { } else {
$entry['button'] = $generate_item_button; $entry['button'] = __( 'Regenerate', 'woocommerce' );
} }
$tools_array['regenerate_product_attributes_lookup_table'] = $entry; $tools_array['regenerate_product_attributes_lookup_table'] = $entry;
if ( $lookup_table_exists ) {
// Delete the table.
$tools_array['delete_product_attributes_lookup_table'] = array(
'name' => __( 'Delete the product attributes lookup table', 'woocommerce' ),
'desc' => sprintf(
'<strong class="red">%1$s</strong> %2$s',
__( 'Note:', 'woocommerce' ),
__( 'This will delete the product attributes lookup table. You can create it again with the "Create and fill product attributes lookup table" tool.', 'woocommerce' )
),
'button' => __( 'Delete', 'woocommerce' ),
'requires_refresh' => true,
'callback' => function () {
$this->delete_all_attributes_lookup_data();
return __( 'Product attributes lookup table has been deleted.', 'woocommerce' );
},
);
}
return $tools_array; return $tools_array;
} }

View File

@ -226,17 +226,15 @@ class Filterer {
if ( ! empty( $attributes_to_filter_by ) ) { if ( ! empty( $attributes_to_filter_by ) ) {
$and_term_ids = array(); $and_term_ids = array();
$or_term_ids = array();
foreach ( $attributes_to_filter_by as $taxonomy => $data ) { foreach ( $attributes_to_filter_by as $taxonomy => $data ) {
if ( 'and' !== $data['query_type'] ) {
continue;
}
$all_terms = get_terms( $taxonomy, array( 'hide_empty' => false ) ); $all_terms = get_terms( $taxonomy, array( 'hide_empty' => false ) );
$term_ids_by_slug = wp_list_pluck( $all_terms, 'term_id', 'slug' ); $term_ids_by_slug = wp_list_pluck( $all_terms, 'term_id', 'slug' );
$term_ids_to_filter_by = array_values( array_intersect_key( $term_ids_by_slug, array_flip( $data['terms'] ) ) ); $term_ids_to_filter_by = array_values( array_intersect_key( $term_ids_by_slug, array_flip( $data['terms'] ) ) );
if ( 'and' === $data['query_type'] ) { $and_term_ids = array_merge( $and_term_ids, $term_ids_to_filter_by );
$and_term_ids = array_merge( $and_term_ids, $term_ids_to_filter_by );
} else {
$or_term_ids = array_merge( $or_term_ids, $term_ids_to_filter_by );
}
} }
if ( ! empty( $and_term_ids ) ) { if ( ! empty( $and_term_ids ) ) {
@ -261,17 +259,6 @@ class Filterer {
AND term_id in {$term_ids_list} AND term_id in {$term_ids_list}
) temp )"; ) temp )";
} }
if ( ! empty( $or_term_ids ) ) {
$term_ids_list = '(' . join( ',', $or_term_ids ) . ')';
$query['where'] .= "
AND product_or_parent_id IN ( SELECT product_or_parent_id FROM (
SELECT product_or_parent_id FROM {$this->lookup_table_name}
WHERE term_id in {$term_ids_list}
{$in_stock_clause}
) temp )";
}
} else { } else {
$query['where'] .= $in_stock_clause; $query['where'] .= $in_stock_clause;
} }

View File

@ -36,7 +36,7 @@ class LookupDataStore {
public function __construct() { public function __construct() {
global $wpdb; global $wpdb;
$this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup'; $this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
$this->init_hooks(); $this->init_hooks();
} }
@ -84,9 +84,18 @@ class LookupDataStore {
$settings[] = $title_item; $settings[] = $title_item;
if ( ! $regeneration_is_in_progress ) { if ( ! $regeneration_is_in_progress ) {
$regeneration_aborted_warning =
$this->regeneration_was_aborted() ?
sprintf(
"<p><strong style='color: #E00000'>%s</strong></p><p>%s</p>",
__( 'WARNING: The product attributes lookup table regeneration process was aborted.', 'woocommerce' ),
__( 'This means that the table is probably in an inconsistent state. It\'s recommended to run a new regeneration process (Status - Tools - Regenerate the product attributes lookup table) before enabling the table usage.', 'woocommerce' )
) : null;
$settings[] = array( $settings[] = array(
'title' => __( 'Enable table usage', 'woocommerce' ), 'title' => __( 'Enable table usage', 'woocommerce' ),
'desc' => __( 'Use the product attributes lookup table for catalog filtering.', 'woocommerce' ), 'desc' => __( 'Use the product attributes lookup table for catalog filtering.', 'woocommerce' ),
'desc_tip' => $regeneration_aborted_warning,
'id' => 'woocommerce_attribute_lookup_enabled', 'id' => 'woocommerce_attribute_lookup_enabled',
'default' => 'no', 'default' => 'no',
'type' => 'checkbox', 'type' => 'checkbox',
@ -648,4 +657,28 @@ class LookupDataStore {
public function unset_regeneration_in_progress_flag() { public function unset_regeneration_in_progress_flag() {
delete_option( 'woocommerce_attribute_lookup_regeneration_in_progress' ); delete_option( 'woocommerce_attribute_lookup_regeneration_in_progress' );
} }
/**
* Set a flag indicating that the last lookup table regeneration process started was aborted.
*/
public function set_regeneration_aborted_flag() {
update_option( 'woocommerce_attribute_lookup_regeneration_aborted', 'yes' );
}
/**
* Remove the flag indicating that the last lookup table regeneration process started was aborted.
*/
public function unset_regeneration_aborted_flag() {
delete_option( 'woocommerce_attribute_lookup_regeneration_aborted' );
}
/**
* Tells if the last lookup table regeneration process started was aborted
* (via deleting the 'woocommerce_attribute_lookup_regeneration_in_progress' option).
*
* @return bool True if the last lookup table regeneration process was aborted.
*/
public function regeneration_was_aborted(): bool {
return 'yes' === get_option( 'woocommerce_attribute_lookup_regeneration_aborted' );
}
} }

View File

@ -28,7 +28,7 @@ if ( apply_filters( 'woocommerce_checkout_show_terms', true ) && function_exists
<p class="form-row validate-required"> <p class="form-row validate-required">
<label class="woocommerce-form__label woocommerce-form__label-for-checkbox checkbox"> <label class="woocommerce-form__label woocommerce-form__label-for-checkbox checkbox">
<input type="checkbox" class="woocommerce-form__input woocommerce-form__input-checkbox input-checkbox" name="terms" <?php checked( apply_filters( 'woocommerce_terms_is_checked_default', isset( $_POST['terms'] ) ), true ); // WPCS: input var ok, csrf ok. ?> id="terms" /> <input type="checkbox" class="woocommerce-form__input woocommerce-form__input-checkbox input-checkbox" name="terms" <?php checked( apply_filters( 'woocommerce_terms_is_checked_default', isset( $_POST['terms'] ) ), true ); // WPCS: input var ok, csrf ok. ?> id="terms" />
<span class="woocommerce-terms-and-conditions-checkbox-text"><?php wc_terms_and_conditions_checkbox_text(); ?></span>&nbsp;<span class="required">*</span> <span class="woocommerce-terms-and-conditions-checkbox-text"><?php wc_terms_and_conditions_checkbox_text(); ?></span>&nbsp;<abbr class="required" title="<?php esc_attr_e( 'required', 'woocommerce' ); ?>">*</abbr>
</label> </label>
<input type="hidden" name="terms-field" value="1" /> <input type="hidden" name="terms-field" value="1" />
</p> </p>

View File

@ -13,30 +13,36 @@ const {
beforeAll, beforeAll,
} = require( '@jest/globals' ); } = require( '@jest/globals' );
const { GITHUB_REPOSITORY, PLUGIN_NAME, GITHUB_TOKEN } = process.env; const { GITHUB_REPOSITORY, PLUGIN_NAME, GITHUB_TOKEN, PLUGIN_REPOSITORY } = process.env;
// allows us to upload plugins from different repositories.
const pluginName = PLUGIN_NAME ? PLUGIN_NAME : 'WooCommerce';
const repository = PLUGIN_REPOSITORY ? PLUGIN_REPOSITORY : GITHUB_REPOSITORY;
let zipUrl; let zipUrl;
let pluginPath; let pluginPath;
utils.describeIf( GITHUB_REPOSITORY )( 'Upload and activate plugin', () => { utils.describeIf( repository )(
beforeAll( async () => { `Upload and activate ${ pluginName } from ${ repository }`,
zipUrl = await getLatestReleaseZipUrl( GITHUB_REPOSITORY, GITHUB_TOKEN ); () => {
beforeAll( async () => {
zipUrl = await getLatestReleaseZipUrl( repository, GITHUB_TOKEN );
pluginPath = await getRemotePluginZip( zipUrl, GITHUB_TOKEN ); pluginPath = await getRemotePluginZip( zipUrl, GITHUB_TOKEN );
await merchant.login(); await merchant.login();
}); } );
afterAll( async () => { afterAll( async () => {
await merchant.logout(); await merchant.logout();
}); } );
it( 'can upload and activate the provided plugin', async () => { it( 'can upload and activate the provided plugin', async () => {
await merchant.uploadAndActivatePlugin( pluginPath, PLUGIN_NAME ); await merchant.uploadAndActivatePlugin( pluginPath, PLUGIN_NAME );
}); } );
it( 'can remove downloaded plugin zip', async () => { it( 'can remove downloaded plugin zip', async () => {
await deleteDownloadedPluginFiles(); await deleteDownloadedPluginFiles();
} ); } );
}
}); );

View File

@ -0,0 +1,53 @@
import { sleep, check, group } from "k6";
import http from "k6/http";
import { Trend } from "k6/metrics";
import { randomIntBetween } from "https://jslib.k6.io/k6-utils/1.1.0/index.js";
import {
base_url,
think_time_min,
think_time_max,
product_search_term,
} from "../../config.js";
import {
htmlRequestHeader,
commonRequestHeaders,
commonGetRequestHeaders,
commonNonStandardHeaders,
} from "../../headers.js";
// Custom metrics to add to standard results output.
let postTypeOrderSearchTrend = new Trend("wc_get_post_type_order_search");
export function ordersSearch() {
let response;
group("Orders Search", function () {
var requestHeaders = Object.assign({},
htmlRequestHeader,
commonRequestHeaders,
commonGetRequestHeaders,
commonNonStandardHeaders
);
response = http.get(
`${base_url}/wp-admin/edit.php?s=${product_search_term}&` +
`post_status=all&post_type=shop_order&action=-1&m=0&_customer_user&` +
`paged=1&action2=-1`,
{
headers: requestHeaders,
}
);
postTypeOrderSearchTrend.add(response.timings.duration);
check(response, {
"is status 200": (r) => r.status === 200,
"body contains: 'Search results' subtitle": (response) =>
response.body.includes("Search results for:"),
});
});
sleep(randomIntBetween(`${think_time_min}`, `${think_time_max}`));
}
export default function () {
ordersSearch();
}

View File

@ -13,6 +13,7 @@ import { products } from '../requests/merchant/products.js';
import { addProduct } from '../requests/merchant/add-product.js'; import { addProduct } from '../requests/merchant/add-product.js';
import { orders } from '../requests/merchant/orders.js'; import { orders } from '../requests/merchant/orders.js';
import { ordersHeartbeat } from '../requests/merchant/orders-heartbeat.js'; import { ordersHeartbeat } from '../requests/merchant/orders-heartbeat.js';
import { ordersSearch } from '../requests/merchant/orders-search.js';
import { homeWCAdmin } from '../requests/merchant/home-wc-admin.js'; import { homeWCAdmin } from '../requests/merchant/home-wc-admin.js';
import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant.js'; import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant.js';
import { categoryPage } from '../requests/shopper/category-page.js'; import { categoryPage } from '../requests/shopper/category-page.js';
@ -107,6 +108,7 @@ export function merchantAllFlows() {
myAccountMerchantLogin(); myAccountMerchantLogin();
homeWCAdmin(); homeWCAdmin();
orders(); orders();
ordersSearch();
products(); products();
addProduct(); addProduct();
} }

View File

@ -13,6 +13,7 @@ import { products } from '../requests/merchant/products.js';
import { addProduct } from '../requests/merchant/add-product.js'; import { addProduct } from '../requests/merchant/add-product.js';
import { orders } from '../requests/merchant/orders.js'; import { orders } from '../requests/merchant/orders.js';
import { ordersHeartbeat } from '../requests/merchant/orders-heartbeat.js'; import { ordersHeartbeat } from '../requests/merchant/orders-heartbeat.js';
import { ordersSearch } from '../requests/merchant/orders-search.js';
import { homeWCAdmin } from '../requests/merchant/home-wc-admin.js'; import { homeWCAdmin } from '../requests/merchant/home-wc-admin.js';
import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant.js'; import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant.js';
import { categoryPage } from '../requests/shopper/category-page.js'; import { categoryPage } from '../requests/shopper/category-page.js';
@ -97,6 +98,7 @@ export function merchantAllFlows() {
myAccountMerchantLogin(); myAccountMerchantLogin();
homeWCAdmin(); homeWCAdmin();
orders(); orders();
ordersSearch();
products(); products();
addProduct(); addProduct();
} }

View File

@ -2,6 +2,7 @@ import { wpLogin } from '../requests/merchant/wp-login.js';
import { products } from '../requests/merchant/products.js'; import { products } from '../requests/merchant/products.js';
import { addProduct } from '../requests/merchant/add-product.js'; import { addProduct } from '../requests/merchant/add-product.js';
import { orders } from '../requests/merchant/orders.js'; import { orders } from '../requests/merchant/orders.js';
import { ordersSearch } from '../requests/merchant/orders-search.js';
import { homeWCAdmin } from '../requests/merchant/home-wc-admin.js'; import { homeWCAdmin } from '../requests/merchant/home-wc-admin.js';
import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant.js'; import { myAccountMerchantLogin } from '../requests/merchant/my-account-merchant.js';
@ -22,6 +23,7 @@ export function allMerchantFlow() {
wpLogin(); wpLogin();
homeWCAdmin(); homeWCAdmin();
orders(); orders();
ordersSearch();
products(); products();
addProduct(); addProduct();
} }

View File

@ -5,6 +5,8 @@
* @package WooCommerce\Tests\Admin * @package WooCommerce\Tests\Admin
*/ */
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskList;
/** /**
* Class WC_Admin_Dashboard_Setup_Test * Class WC_Admin_Dashboard_Setup_Test
*/ */
@ -17,6 +19,16 @@ 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, // 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. // by default it won't be considered completed but we can manually change that as needed.
update_option( 'woocommerce_default_country', 'JP' ); update_option( 'woocommerce_default_country', 'JP' );
$password = wp_generate_password( 8, false, false );
$this->admin = wp_insert_user(
array(
'user_login' => "test_admin$password",
'user_pass' => $password,
'user_email' => "admin$password@example.com",
'role' => 'administrator',
)
);
wp_set_current_user( $this->admin );
parent::setUp(); parent::setUp();
} }
@ -52,38 +64,71 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
return ob_get_clean(); return ob_get_clean();
} }
/**
* Tests widget does not get rendered when woocommerce_task_list_hidden or woocommerce_task_list_hidden
* is true.
*
* @dataProvider should_display_widget_data_provider
*
* @param array $options a set of options.
*/
public function test_widget_does_not_get_rendered( array $options ) {
global $wp_meta_boxes;
foreach ( $options as $name => $value ) {
update_option( $name, $value );
}
$this->get_widget();
$this->assertNull( $wp_meta_boxes );
}
/** /**
* Given both woocommerce_task_list_hidden and woocommerce_task_list_complete are false * Given the task list is not hidden and is not complete, make sure the widget is rendered.
* Then the widget should be added to the $wp_meta_boxes
*/ */
public function test_widget_gets_rendered_when_both_options_are_false() { public function test_widget_render() {
// Force the "payments" task to be considered incomplete.
add_filter(
'woocommerce_available_payment_gateways',
function() {
return array();
}
);
global $wp_meta_boxes; global $wp_meta_boxes;
update_option( 'woocommerce_task_list_complete', false ); $task_list = $this->get_widget()->get_task_list();
update_option( 'woocommerce_task_list_hidden', false ); $task_list->unhide();
$this->get_widget(); $this->get_widget();
$this->assertArrayHasKey( 'wc_admin_dashboard_setup', $wp_meta_boxes['dashboard']['normal']['high'] ); $this->assertArrayHasKey( 'wc_admin_dashboard_setup', $wp_meta_boxes['dashboard']['normal']['high'] );
} }
/**
* 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 {
public function is_complete() {
return true;
}
};
$widget = $this->get_widget();
$widget->set_task_list( $task_list );
$this->assertFalse( $widget->should_display_widget() );
}
/**
* Tests widget does not display when task list is hidden.
*/
public function test_widget_does_not_display_when_task_list_hidden() {
$widget = $this->get_widget();
$widget->get_task_list()->hide();
$this->assertFalse( $widget->should_display_widget() );
}
/**
* Tests widget does not display when user cannot manage woocommerce.
*/
public function test_widget_does_not_display_when_missing_capabilities() {
$password = wp_generate_password( 8, false, false );
$author = wp_insert_user(
array(
'user_login' => "test_author$password",
'user_pass' => $password,
'user_email' => "author$password@example.com",
'role' => 'author',
)
);
wp_set_current_user( $author );
$widget = $this->get_widget();
$this->assertFalse( $widget->should_display_widget() );
}
/** /**
* Tests the widget output when 1 task has been completed. * Tests the widget output when 1 task has been completed.
*/ */
@ -99,10 +144,9 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
$html = $this->get_widget_output(); $html = $this->get_widget_output();
$required_strings = array( $required_strings = array(
'Step 0 of 6', 'Step \d+ of \d+',
'You&#039;re almost there! Once you complete store setup you can start receiving orders.', 'You&#039;re almost there! Once you complete store setup you can start receiving orders.',
'Start selling', 'Start selling',
'admin.php\?page=wc-admin&amp;path=%2Fsetup-wizard',
); );
foreach ( $required_strings as $required_string ) { foreach ( $required_strings as $required_string ) {
@ -126,42 +170,13 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
} }
); );
$completed_tasks = array( 'payments' ); $completed_tasks_count = $this->get_widget()->get_completed_tasks_count();
$tasks = $this->get_widget()->get_tasks(); $tasks_count = count( $this->get_widget()->get_tasks() );
$tasks_count = count( $tasks ); $step_number = $completed_tasks_count + 1;
unset( $tasks['payments'] ); // That one is completed already. if ( $completed_tasks_count === $tasks_count ) {
foreach ( $tasks as $key => $task ) { $this->assertEmpty( $this->get_widget_output() );
array_push( $completed_tasks, $key ); } else {
update_option( 'woocommerce_task_list_tracked_completed_tasks', $completed_tasks ); $this->assertRegexp( "/Step ${step_number} of 6/", $this->get_widget_output() );
$completed_tasks_count = count( $completed_tasks );
// When all tasks are completed, assert that the widget output is empty.
// As widget won't be rendered when tasks are completed.
if ( $completed_tasks_count === $tasks_count ) {
$this->assertEmpty( $this->get_widget_output() );
} else {
$this->assertRegexp( "/Step ${completed_tasks_count} of 6/", $this->get_widget_output() );
}
} }
} }
/**
* Provides dataset that controls output of `should_display_widget`
*/
public function should_display_widget_data_provider() {
return array(
array(
array(
'woocommerce_task_list_complete' => 'yes',
'woocommerce_task_list_hidden' => 'no',
),
),
array(
array(
'woocommerce_task_list_complete' => 'no',
'woocommerce_task_list_hidden' => 'yes',
),
),
);
}
} }

View File

@ -7,6 +7,8 @@
// phpcs:ignore Squiz.Commenting.FileComment.Missing // phpcs:ignore Squiz.Commenting.FileComment.Missing
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
require_once __DIR__ . '/class-wc-settings-unit-test-case.php'; require_once __DIR__ . '/class-wc-settings-unit-test-case.php';
/** /**
@ -28,6 +30,11 @@ class WC_Settings_Products_Test extends WC_Settings_Unit_Test_Case {
'downloadable', 'downloadable',
); );
// TODO: Once the lookup table is created in a migration, remove the check and just include 'advanced' in $expected.
if ( wc_get_container()->get( LookupDataStore::class )->check_lookup_table_exists() ) {
array_push( $expected, 'advanced' );
}
$this->assertEquals( $expected, $section_names ); $this->assertEquals( $expected, $section_names );
} }
@ -134,12 +141,12 @@ class WC_Settings_Products_Test extends WC_Settings_Unit_Test_Case {
$settings_ids_and_types = $this->get_ids_and_types( $settings ); $settings_ids_and_types = $this->get_ids_and_types( $settings );
$expected = array( $expected = array(
'digital_download_options' => array( 'title', 'sectionend' ), 'digital_download_options' => array( 'title', 'sectionend' ),
'woocommerce_file_download_method' => 'select', 'woocommerce_file_download_method' => 'select',
'woocommerce_downloads_redirect_fallback_allowed' => 'checkbox', 'woocommerce_downloads_redirect_fallback_allowed' => 'checkbox',
'woocommerce_downloads_require_login' => 'checkbox', 'woocommerce_downloads_require_login' => 'checkbox',
'woocommerce_downloads_grant_access_after_payment' => 'checkbox', 'woocommerce_downloads_grant_access_after_payment' => 'checkbox',
'woocommerce_downloads_add_hash_to_filename' => 'checkbox', 'woocommerce_downloads_add_hash_to_filename' => 'checkbox',
); );
$this->assertEquals( $expected, $settings_ids_and_types ); $this->assertEquals( $expected, $settings_ids_and_types );

View File

@ -121,7 +121,7 @@ class DataRegeneratorTest extends \WC_Unit_Test_Case {
$this->sut->initiate_regeneration(); $this->sut->initiate_regeneration();
$this->assertEquals( 100, get_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ) ); $this->assertEquals( 100, get_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ) );
$this->assertEquals( 0, get_option( 'woocommerce_attribute_lookup_last_products_page_processed' ) ); $this->assertEquals( 0, get_option( 'woocommerce_attribute_lookup_processed_count' ) );
$this->assertFalse( get_option( 'woocommerce_attribute_lookup_enabled' ) ); $this->assertFalse( get_option( 'woocommerce_attribute_lookup_enabled' ) );
$expected_enqueued = array( $expected_enqueued = array(
@ -156,8 +156,8 @@ class DataRegeneratorTest extends \WC_Unit_Test_Case {
$this->sut->initiate_regeneration(); $this->sut->initiate_regeneration();
$this->assertFalse( get_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ) ); $this->assertFalse( get_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ) );
$this->assertFalse( get_option( 'woocommerce_attribute_lookup_last_products_page_processed' ) ); $this->assertFalse( get_option( 'woocommerce_attribute_lookup_processed_count' ) );
$this->assertEquals( 'no', get_option( 'woocommerce_attribute_lookup_enabled' ) ); $this->assertEquals( 'yes', get_option( 'woocommerce_attribute_lookup_enabled' ) );
$this->assertEmpty( $this->queue->get_methods_called() ); $this->assertEmpty( $this->queue->get_methods_called() );
} }
@ -165,15 +165,15 @@ class DataRegeneratorTest extends \WC_Unit_Test_Case {
* @testdox `initiate_regeneration` processes one chunk of products IDs and enqueues next step if there are more products available. * @testdox `initiate_regeneration` processes one chunk of products IDs and enqueues next step if there are more products available.
*/ */
public function test_initiate_regeneration_correctly_processes_ids_and_enqueues_next_step() { public function test_initiate_regeneration_correctly_processes_ids_and_enqueues_next_step() {
$requested_products_pages = array(); $requested_products_offsets = array();
$this->register_legacy_proxy_function_mocks( $this->register_legacy_proxy_function_mocks(
array( array(
'wc_get_products' => function( $args ) use ( &$requested_products_pages ) { 'wc_get_products' => function( $args ) use ( &$requested_products_offsets ) {
if ( 'DESC' === current( $args['orderby'] ) ) { if ( 'DESC' === current( $args['orderby'] ) ) {
return array( 100 ); return array( 100 );
} else { } else {
$requested_products_pages[] = $args['page']; $requested_products_offsets[] = $args['offset'];
return array( 1, 2, 3 ); return array( 1, 2, 3 );
} }
}, },
@ -186,13 +186,13 @@ class DataRegeneratorTest extends \WC_Unit_Test_Case {
$this->sut->initiate_regeneration(); $this->sut->initiate_regeneration();
$this->queue->clear_methods_called(); $this->queue->clear_methods_called();
update_option( 'woocommerce_attribute_lookup_last_products_page_processed', 7 ); update_option( 'woocommerce_attribute_lookup_processed_count', 7 );
do_action( 'woocommerce_run_product_attribute_lookup_regeneration_callback' ); do_action( 'woocommerce_run_product_attribute_lookup_regeneration_callback' );
$this->assertEquals( array( 1, 2, 3 ), $this->lookup_data_store->passed_products ); $this->assertEquals( array( 1, 2, 3 ), $this->lookup_data_store->passed_products );
$this->assertEquals( array( 8 ), $requested_products_pages ); $this->assertEquals( array( 7 ), $requested_products_offsets );
$this->assertEquals( 8, get_option( 'woocommerce_attribute_lookup_last_products_page_processed' ) ); $this->assertEquals( 7 + count( $this->lookup_data_store->passed_products ), get_option( 'woocommerce_attribute_lookup_processed_count' ) );
$expected_enqueued = array( $expected_enqueued = array(
'method' => 'schedule_single', 'method' => 'schedule_single',
@ -205,6 +205,50 @@ class DataRegeneratorTest extends \WC_Unit_Test_Case {
$this->assertEquals( sort( $expected_enqueued ), sort( $actual_enqueued ) ); $this->assertEquals( sort( $expected_enqueued ), sort( $actual_enqueued ) );
} }
/**
* @testDox Products are processed in groups of whatever the 'woocommerce_attribute_lookup_regeneration_step_size' filter returns, defaulting to PRODUCTS_PER_GENERATION_STEP
*
* @testWith [true]
* [false]
*
* @param bool $set_filter Whether to use the filter to change the processing group size or not.
*/
public function test_regeneration_uses_the_woocommerce_attribute_lookup_regeneration_step_size_filter( bool $set_filter ) {
$requested_step_sizes = array();
if ( $set_filter ) {
\add_filter(
'woocommerce_attribute_lookup_regeneration_step_size',
function( $default_filter_size ) {
return 100;
}
);
}
$this->register_legacy_proxy_function_mocks(
array(
'wc_get_products' => function( $args ) use ( &$requested_step_sizes ) {
if ( 'DESC' === current( $args['orderby'] ) ) {
return array( 100 );
} else {
$requested_step_sizes[] = $args['limit'];
return array( 1, 2, 3 );
}
},
)
);
$this->sut->initiate_regeneration();
$this->queue->clear_methods_called();
do_action( 'woocommerce_run_product_attribute_lookup_regeneration_callback' );
remove_all_filters( ' woocommerce_attribute_lookup_regeneration_step_size' );
$expected_limit_value = $set_filter ? 100 : DataRegenerator::PRODUCTS_PER_GENERATION_STEP;
$this->assertEquals( array( $expected_limit_value ), $requested_step_sizes );
}
/** /**
* @testdox `initiate_regeneration` finishes regeneration when the max product id is reached or no more products are returned. * @testdox `initiate_regeneration` finishes regeneration when the max product id is reached or no more products are returned.
* *
@ -215,15 +259,12 @@ class DataRegeneratorTest extends \WC_Unit_Test_Case {
* @param array $product_ids The products ids that wc_get_products will return. * @param array $product_ids The products ids that wc_get_products will return.
*/ */
public function test_initiate_regeneration_finishes_when_no_more_products_available( $product_ids ) { public function test_initiate_regeneration_finishes_when_no_more_products_available( $product_ids ) {
$requested_products_pages = array();
$this->register_legacy_proxy_function_mocks( $this->register_legacy_proxy_function_mocks(
array( array(
'wc_get_products' => function( $args ) use ( &$requested_products_pages, $product_ids ) { 'wc_get_products' => function( $args ) use ( &$requested_products_offsets, $product_ids ) {
if ( 'DESC' === current( $args['orderby'] ) ) { if ( 'DESC' === current( $args['orderby'] ) ) {
return array( 100 ); return array( 100 );
} else { } else {
$requested_products_pages[] = $args['page'];
return $product_ids; return $product_ids;
} }
}, },
@ -237,8 +278,8 @@ class DataRegeneratorTest extends \WC_Unit_Test_Case {
$this->assertEquals( $product_ids, $this->lookup_data_store->passed_products ); $this->assertEquals( $product_ids, $this->lookup_data_store->passed_products );
$this->assertFalse( get_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ) ); $this->assertFalse( get_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ) );
$this->assertFalse( get_option( 'woocommerce_attribute_lookup_last_products_page_processed' ) ); $this->assertFalse( get_option( 'woocommerce_attribute_lookup_processed_count' ) );
$this->assertEquals( 'no', get_option( 'woocommerce_attribute_lookup_enabled' ) ); $this->assertEquals( 'yes', get_option( 'woocommerce_attribute_lookup_enabled' ) );
$this->assertEmpty( $this->queue->get_methods_called() ); $this->assertEmpty( $this->queue->get_methods_called() );
} }
} }

View File

@ -545,16 +545,7 @@ class FiltererTest extends \WC_Unit_Test_Case {
$this->assertEmpty( $filtered_product_ids ); $this->assertEmpty( $filtered_product_ids );
} }
/* $expected_to_be_included_in_count = 'or' === $filter_type || $expected_to_be_visible;
* If a variable product defines an attribute value that isn't used by any variation:
* When using the lookup table: that value is not included in the count.
* When not using the lookup table: the value is included in the count since it is part of the parent product.
*/
if ( $using_lookup_table && 'or' === $filter_type && array( 'Green' ) === $attributes ) {
$expected_to_be_included_in_count = false;
} else {
$expected_to_be_included_in_count = 'or' === $filter_type || $expected_to_be_visible;
}
$this->assert_counters( 'Color', $expected_to_be_included_in_count ? array( 'Blue', 'Red' ) : array(), $filter_type ); $this->assert_counters( 'Color', $expected_to_be_included_in_count ? array( 'Blue', 'Red' ) : array(), $filter_type );
} }
@ -822,16 +813,7 @@ class FiltererTest extends \WC_Unit_Test_Case {
$this->assertEmpty( $filtered_product_ids ); $this->assertEmpty( $filtered_product_ids );
} }
/* $expected_to_be_included_in_count = 'or' === $filter_type || $expected_to_be_visible;
* If a variable product defines an attribute value that isn't used by any variation:
* When using the lookup table: that value is not included in the count.
* When not using the lookup table: the value is included in the count since it is part of the parent product.
*/
if ( $using_lookup_table && 'or' === $filter_type && array( 'Elastic' ) === $attributes ) {
$expected_to_be_included_in_count = false;
} else {
$expected_to_be_included_in_count = 'or' === $filter_type || $expected_to_be_visible;
}
$this->assert_counters( 'Features', $expected_to_be_included_in_count ? array( 'Washable', 'Ironable' ) : array(), $filter_type ); $this->assert_counters( 'Features', $expected_to_be_included_in_count ? array( 'Washable', 'Ironable' ) : array(), $filter_type );
} }
@ -1075,16 +1057,7 @@ class FiltererTest extends \WC_Unit_Test_Case {
$this->assertEmpty( $filtered_product_ids ); $this->assertEmpty( $filtered_product_ids );
} }
/* $expected_counted_attributes = 'or' === $filter_type || $expected_to_be_visible ? array( 'Blue', 'Red' ) : array();
* If a variable product defines an attribute value that isn't used by any variation:
* When using the lookup table: that value is not included in the count.
* When not using the lookup table: the value is included in the count since it is part of the parent product.
*/
if ( $using_lookup_table && 'or' === $filter_type && array( 'Green' ) === $attributes ) {
$expected_counted_attributes = array();
} else {
$expected_counted_attributes = 'or' === $filter_type || $expected_to_be_visible ? array( 'Blue', 'Red' ) : array();
}
$this->assert_counters( 'Color', $expected_counted_attributes, $filter_type ); $this->assert_counters( 'Color', $expected_counted_attributes, $filter_type );
} }
@ -1208,16 +1181,7 @@ class FiltererTest extends \WC_Unit_Test_Case {
$this->assertEmpty( $filtered_product_ids ); $this->assertEmpty( $filtered_product_ids );
} }
/* $expected_to_be_included_in_count = 'or' === $filter_type || $expected_to_be_visible;
* If a variable product defines an attribute value that isn't used by any variation:
* When using the lookup table: that value is not included in the count.
* When not using the lookup table: the value is included in the count since it is part of the parent product.
*/
if ( $using_lookup_table && 'or' === $filter_type && array( 'White' ) === $attributes ) {
$expected_to_be_included_in_count = false;
} else {
$expected_to_be_included_in_count = 'or' === $filter_type || $expected_to_be_visible;
}
$this->assert_counters( 'Color', $expected_to_be_included_in_count ? array( 'Blue', 'Red', 'Green' ) : array(), $filter_type ); $this->assert_counters( 'Color', $expected_to_be_included_in_count ? array( 'Blue', 'Red', 'Green' ) : array(), $filter_type );
} }
@ -1356,9 +1320,12 @@ class FiltererTest extends \WC_Unit_Test_Case {
wp_set_object_terms( $product_simple_1->get_id(), $terms, 'product_visibility' ); wp_set_object_terms( $product_simple_1->get_id(), $terms, 'product_visibility' );
wp_set_object_terms( $product_variable_1['id'], $terms, 'product_visibility' ); wp_set_object_terms( $product_variable_1['id'], $terms, 'product_visibility' );
$filtered_product_ids = $this->do_product_request( array() ); $actual_filtered_product_ids = $this->do_product_request( array() );
$expected_filtered_product_ids = array( $product_simple_2->get_id(), $product_variable_2['id'] );
sort( $actual_filtered_product_ids );
sort( $expected_filtered_product_ids );
$this->assertEquals( array( $product_simple_2->get_id(), $product_variable_2['id'] ), $filtered_product_ids ); $this->assertEquals( $expected_filtered_product_ids, $actual_filtered_product_ids );
$this->assert_counters( 'Color', $expected_colors_included_in_counters ); $this->assert_counters( 'Color', $expected_colors_included_in_counters );
$this->assert_counters( 'Features', array( 'Ironable' ) ); $this->assert_counters( 'Features', array( 'Ironable' ) );

View File

@ -9,7 +9,6 @@ use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore; use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
use Automattic\WooCommerce\Testing\Tools\FakeQueue; use Automattic\WooCommerce\Testing\Tools\FakeQueue;
use Automattic\WooCommerce\Utilities\ArrayUtil; use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks\FunctionsMockerHack;
/** /**
* Tests for the LookupDataStore class. * Tests for the LookupDataStore class.
@ -36,7 +35,6 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
*/ */
public static function tearDownAfterClass() { public static function tearDownAfterClass() {
parent::tearDownAfterClass(); parent::tearDownAfterClass();
wc_get_container()->get( DataRegenerator::class )->delete_all_attributes_lookup_data();
} }
/** /**

View File

@ -155,6 +155,7 @@ importers:
'@wordpress/jest-preset-default': ^7.1.3 '@wordpress/jest-preset-default': ^7.1.3
app-root-path: ^3.0.0 app-root-path: ^3.0.0
commander: 4.1.1 commander: 4.1.1
config: 3.3.3
eslint: ^8.1.0 eslint: ^8.1.0
jest: ^25.1.0 jest: ^25.1.0
jest-circus: 25.1.0 jest-circus: 25.1.0
@ -175,6 +176,7 @@ importers:
'@wordpress/jest-preset-default': 7.1.3_@babel+core@7.12.9+jest@25.5.4 '@wordpress/jest-preset-default': 7.1.3_@babel+core@7.12.9+jest@25.5.4
app-root-path: 3.0.0 app-root-path: 3.0.0
commander: 4.1.1 commander: 4.1.1
config: 3.3.3
jest: 25.5.4 jest: 25.5.4
jest-circus: 25.1.0 jest-circus: 25.1.0
jest-each: 25.5.0 jest-each: 25.5.0