Merge branch 'trunk' into add-multiple-items-and-apply-coupon

This commit is contained in:
Paul Kang 2024-08-22 12:23:25 -07:00 committed by GitHub
commit 993bdc6b90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
111 changed files with 1425 additions and 960 deletions

View File

@ -12,7 +12,7 @@
"dependencies": [
"pnpm"
],
"pinVersion": "^9.1.0",
"pinVersion": "9.1.3",
"packages": [
"**"
]

View File

@ -3,7 +3,7 @@
"MD003": { "style": "atx" },
"MD007": { "indent": 4 },
"MD013": { "line_length": 9999 },
"MD024": { "allow_different_nesting": true },
"MD024": { "siblings_only": true },
"MD033": { "allowed_elements": ["video"] },
"MD035": false,
"MD041": false,

View File

@ -761,7 +761,7 @@
"menu_title": "Data storage",
"tags": "reference",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/data-storage.md",
"hash": "ea827670b3a888f69cb50bc78dc10827c802abb135a5846f31cfa55e882f7faa",
"hash": "1ea2fabf927f9ced2fee89f930e9195c7df4d98ceb3d847436337376578802a3",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/data-storage.md",
"id": "b3e0b17ca74596e858c26887c1e4c8ee6c8f6102"
},
@ -1059,7 +1059,7 @@
"categories": []
},
{
"content": "\nDiscover how to customize the WooCommerce product editor, from extending product data to adding unique functionalities.\n\nThis handbook is a guide for extension developers looking to add support for the new product editor in their extensions. The product editor uses [Gutenberg's Block Editor](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-editor), which is going to help WooCommerce evolve alongside the WordPress ecosystem.",
"content": "\nDiscover how to customize the WooCommerce product editor, from extending product data to adding unique functionalities.\n\nThis handbook is a guide for extension developers looking to add support for the new product editor in their extensions. The product editor uses [Gutenberg's Block Editor](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-editor), which is going to help WooCommerce evolve alongside the WordPress ecosystem.\n",
"category_slug": "product-editor",
"category_title": "Product Editor",
"posts": [
@ -1378,7 +1378,7 @@
{
"post_title": "Template structure & Overriding templates via a theme",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/theme-development/template-structure.md",
"hash": "c0e346e7e21682bd645998f0607efe18f9f63601f53f4a53ab43248eefcab2d1",
"hash": "ff781eff7998ea93723f644bddd4f6da6f73c635bcfc3cd46950f03a8b83b26c",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/theme-development/template-structure.md",
"id": "34bfebec9fc45e680976814928a7b8a1778af14e"
},
@ -1731,5 +1731,5 @@
"categories": []
}
],
"hash": "95e835afe855d5a898bfc31337d2a42b582794d5e1aaacb9b4a98cfc97748e05"
"hash": "b6fab4eae1266824ee3e876c8fa5fd0342f59b4a0e5978f1460afc67d82e6d94"
}

View File

@ -7,21 +7,27 @@ tags: reference
When developing for WordPress and WooCommerce, it's important to consider the nature and permanence of your data. This will help you decide the best way to store it. Here's a quick primer:
## Transients
If the data may not always be present (i.e., it expires), use a [transient](https://developer.wordpress.org/apis/handbook/transients/). Transients are a simple and standardized way of storing cached data in the database temporarily by giving it a custom name and a timeframe after which it will expire and be deleted.
## WP Cache
If the data is persistent but not always present, consider using the [WP Cache](https://developer.wordpress.org/reference/classes/wp_object_cache/). The WP Cache functions allow you to cache data that is computationally expensive to regenerate, such as complex query results.
## wp_options Table
If the data is persistent and always present, consider the [wp_options table](https://developer.wordpress.org/apis/handbook/options/). The Options API is a simple and standardized way of storing data in the wp_options table in the WordPress database.
## Post Types
If the data type is an entity with n units, consider a [post type](https://developer.wordpress.org/post_type/). Post types are "types" of content that are stored in the same way, but are easy to distinguish in the code and UI.
## Taxonomies
If the data is a means of sorting/categorizing an entity, consider a [taxonomy](https://developer.wordpress.org/taxonomy/). Taxonomies are a way of grouping things together.
## Logging
Logs should be written to a file using the [WC_Logger](https://woocommerce.com/wc-apidocs/class-WC_Logger.html) class. This is a simple and standardized way of recording events and errors for debugging purposes.
Remember, the best method of data storage depends on the nature of the data and how it will be used in your application.

View File

@ -6,4 +6,4 @@ post_title: Product Editor
Discover how to customize the WooCommerce product editor, from extending product data to adding unique functionalities.
This handbook is a guide for extension developers looking to add support for the new product editor in their extensions. The product editor uses [Gutenberg's Block Editor](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-editor), which is going to help WooCommerce evolve alongside the WordPress ecosystem.
This handbook is a guide for extension developers looking to add support for the new product editor in their extensions. The product editor uses [Gutenberg's Block Editor](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-editor), which is going to help WooCommerce evolve alongside the WordPress ecosystem.

View File

@ -6,7 +6,7 @@
"packageManager": "pnpm@9.1.3",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"private": true,
"repository": {

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/admin-e2e-tests/README.md",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"repository": {
"type": "git",

View File

@ -5,7 +5,7 @@
"main": "index.js",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"scripts": {
"e2e": "jest",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/api/README.md",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"repository": {
"type": "git",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"license": "GPL-3.0-or-later",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"keywords": [
"wordpress",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -7,7 +7,7 @@
"main": "index.js",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"scripts": {
"changelog": "composer install && composer exec -- changelogger"

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -5,7 +5,7 @@
"main": "index.js",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"repository": {
"type": "git",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"license": "GPL-3.0-or-later",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"keywords": [
"wordpress",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -11,7 +11,7 @@
],
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/currency/README.md",
"repository": {

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -10,7 +10,7 @@
],
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/customer-effort-score/README.md",
"repository": {

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -11,7 +11,7 @@
],
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/data/README.md",
"repository": {

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -11,7 +11,7 @@
],
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/date/README.md",
"repository": {

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -10,7 +10,7 @@
],
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/dependency-extraction-webpack-plugin/README.md",
"repository": {

View File

@ -10,7 +10,7 @@
"license": "GPL-3.0+",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"main": "build/index.js",
"module": "build-module/index.js",

View File

@ -12,7 +12,7 @@
],
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/e2e-environment/README.md",
"bugs": {

View File

@ -10,7 +10,7 @@
"license": "GPL-3.0+",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"main": "build/index.js",
"module": "build-module/index.js",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"license": "GPL-2.0-or-later",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"keywords": [
"wordpress",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"license": "GPL-3.0-or-later",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"keywords": [
"wordpress",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"license": "GPL-2.0-or-later",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"keywords": [
"wordpress",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -12,7 +12,7 @@
],
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/expression-evaluation/README.md",
"repository": {

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -5,7 +5,7 @@
"main": "index.js",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"repository": {
"type": "git",

View File

@ -10,7 +10,7 @@
],
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"bin": {
"e2e-builds": "./build.js"

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"license": "GPL-2.0-or-later",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/internal-js-tests/README.md",
"repository": {

View File

@ -6,7 +6,7 @@
"license": "GPL-2.0-or-later",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"keywords": [
"wordpress",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"license": "GPL-3.0-or-later",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"keywords": [
"wordpress",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"license": "GPL-2.0-or-later",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"keywords": [
"wordpress",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"license": "GPL-3.0-or-later",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"keywords": [
"wordpress",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"license": "GPL-3.0-or-later",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"keywords": [
"wordpress",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"license": "GPL-2.0-or-later",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"keywords": [
"wordpress",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -6,7 +6,7 @@
"license": "GPL-3.0-or-later",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"keywords": [
"wordpress",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -94,7 +94,7 @@
},
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"lint-staged": {
"*.php": [

View File

@ -220,7 +220,7 @@ const resetPatternsAndProducts = () => async () => {
method: 'DELETE',
} ),
apiFetch( {
path: '/wc/private/ai/products',
path: '/wc-admin/ai/products',
method: 'DELETE',
} ),
] );
@ -278,7 +278,7 @@ export const updateStorePatterns = async (
additional_errors?: unknown[];
} >( [
apiFetch( {
path: '/wc/private/ai/products',
path: '/wc-admin/ai/products',
method: 'POST',
data: {
business_description:

View File

@ -242,7 +242,7 @@
},
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"config": {
"ci": {

View File

@ -55,6 +55,7 @@ require 'tools/trigger-update-callbacks.php';
require 'tools/reset-cys.php';
require 'tools/set-block-template-logging-threshold.php';
require 'tools/set-coming-soon-mode.php';
require 'tools/fake-woo-payments-gateway.php';
require 'tracks/class-tracks-debug-log.php';
require 'features/features.php';
require 'rest-api-filters/class-wca-test-helper-rest-api-filters.php';

View File

@ -0,0 +1,62 @@
<?php
/**
* Fake WooPayments class.
*
* @package WC_Beta_Tester
*/
class Fake_WCPayments extends WC_Payment_Gateway_WCPay {
/**
* Constructor.
*/
public function __construct() {
$this->id = 'woocommerce_payments';
$this->has_fields = true;
$this->method_title = 'WooPayments';
$this->method_description = $this->get_method_description();
$this->description = '';
$this->supports = array(
'products',
'refunds',
);
}
/**
* Returns true if the gateway needs additional configuration, false if it's ready to use.
*
* @see WC_Payment_Gateway::needs_setup
* @return bool
*/
public function needs_setup() {
return false;
}
/**
* Check if the payment gateway is connected. This method is also used by
* external plugins to check if a connection has been established.
*/
public function is_connected() {
return true;
}
/**
* Get the connection URL.
* Called directly by WooCommerce Core.
*
* @return string Connection URL.
*/
public function get_connection_url() {
return '';
}
/**
* Checks if the gateway is enabled, and also if it's configured enough to accept payments from customers.
*
* Use parent method value alongside other business rules to make the decision.
*
* @return bool Whether the gateway is enabled and ready to accept payments.
*/
public function is_available() {
return true;
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* Fake WooCommercePayments account data for testing purposes.
*
* @package WC_Beta_Tester
* */
defined( 'ABSPATH' ) || exit();
register_woocommerce_admin_test_helper_rest_route(
'/tools/fake-wcpay-completion/v1',
'tools_get_fake_wcpay_completion_status',
array(
'methods' => 'GET',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/tools/fake-wcpay-completion/v1',
'tools_set_fake_wcpay_completion',
array(
'methods' => 'POST',
'args' => array(
'enabled' => array(
'type' => 'enum',
'enum' => array( 'yes', 'no' ),
'required' => true,
'description' => 'Whether to enable or disable fake WooPayments completion',
),
),
)
);
/**
* Get the current status of fake WooPayments completion.
*/
function tools_get_fake_wcpay_completion_status() {
return new WP_REST_Response( array( 'enabled' => get_option( 'wc_beta_tester_fake_wcpay_completion', 'no' ) ), 200 );
}
/**
* A tool to enable/disable fake WooPayments completion.
*
* @param WP_REST_Request $request Request object.
*/
function tools_set_fake_wcpay_completion( $request ) {
$enabled = $request->get_param( 'enabled' );
update_option( 'wc_beta_tester_fake_wcpay_completion', $enabled );
return new WP_REST_Response( array( 'enabled' => $enabled ), 200 );
}
if (
'yes' === get_option( 'wc_beta_tester_fake_wcpay_completion', 'no' ) &&
class_exists( 'WC_Payment_Gateway_WCPay' )
) {
add_filter( 'woocommerce_payment_gateways', 'tools_fake_wcpay' );
add_filter( 'woocommerce_available_payment_gateways', 'tools_fake_wcpay' );
require_once dirname( __FILE__ ) . '/class-fake-wcpayments.php';
/**
* Fake WooPayments completion.
*
* @param array $gateways List of available payment gateways.
*/
function tools_fake_wcpay( $gateways ) {
$gateways['woocommerce_payments'] = new Fake_WCPayments();
return $gateways;
}
}

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add fake WooPayments completion tool

View File

@ -91,7 +91,7 @@
},
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"lint-staged": {
"*.php": [

View File

@ -0,0 +1,31 @@
/**
* External dependencies
*/
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { STORE_KEY } from '../data/constants';
export const FAKE_WOO_PAYMENTS_ACTION_NAME = 'fakeWooPayments';
export const FakeWooPayments = () => {
const isEnabled = useSelect( ( select ) =>
select( STORE_KEY ).getIsFakeWooPaymentsEnabled()
);
const getDescription = () => {
switch ( isEnabled ) {
case 'yes':
return 'Enabled 🟢';
case 'no':
return 'Disabled 🔴';
case 'error':
return 'Error 🙁';
default:
return 'Loading ...';
}
};
return <div>{ getDescription() }</div>;
};

View File

@ -15,6 +15,10 @@ import {
SetComingSoonMode,
UPDATE_COMING_SOON_MODE_ACTION_NAME,
} from './set-coming-soon-mode';
import {
FakeWooPayments,
FAKE_WOO_PAYMENTS_ACTION_NAME,
} from './fake-woo-payments';
import {
UPDATE_WCCOM_REQUEST_ERRORS_MODE,
@ -97,4 +101,9 @@ export default [
description: <SetWccomRequestErrros />,
action: UPDATE_WCCOM_REQUEST_ERRORS_MODE,
},
{
command: 'Toggle Fake WooPayments Completion Status',
description: <FakeWooPayments />,
action: FAKE_WOO_PAYMENTS_ACTION_NAME,
},
];

View File

@ -282,3 +282,28 @@ export function* updateWccomRequestErrorsMode( params ) {
} );
} );
}
export function* fakeWooPayments( params ) {
yield runCommand( 'Toggle Fake WooPayments Completion', function* () {
const newStatus = params.enabled === 'yes' ? 'no' : 'yes';
yield apiFetch( {
path: API_NAMESPACE + '/tools/fake-wcpay-completion/v1',
method: 'POST',
data: {
enabled: newStatus,
},
} );
yield updateCommandParams( 'fakeWooPayments', {
enabled: newStatus,
} );
yield updateMessage(
'Toggle Fake WooPayments Completion',
`Fake WooPayments completion ${
newStatus === 'yes' ? 'disabled' : 'enabled'
}`
);
} );
}

View File

@ -14,6 +14,7 @@ const DEFAULT_STATE = {
updateBlockTemplateLoggingThreshold: {},
runSelectedUpdateCallbacks: {},
updateWccomRequestErrorsMode: {},
fakeWooPayments: {},
},
status: '',
dbUpdateVersions: [],

View File

@ -18,6 +18,7 @@ import { UPDATE_BLOCK_TEMPLATE_LOGGING_THRESHOLD_ACTION_NAME } from '../commands
import { UPDATE_COMING_SOON_MODE_ACTION_NAME } from '../commands/set-coming-soon-mode';
import { TRIGGER_UPDATE_CALLBACKS_ACTION_NAME } from '../commands/trigger-update-callbacks';
import { UPDATE_WCCOM_REQUEST_ERRORS_MODE } from '../commands/set-wccom-request-errors';
import { FAKE_WOO_PAYMENTS_ACTION_NAME } from '../commands/fake-woo-payments';
export function* getCronJobs() {
const path = `${ API_NAMESPACE }/tools/get-cron-list/v1`;
@ -135,3 +136,17 @@ export function* getWccomRequestErrorsMode() {
throw new Error( error );
}
}
export function* getIsFakeWooPaymentsEnabled() {
try {
const response = yield apiFetch( {
path: API_NAMESPACE + '/tools/fake-wcpay-completion/v1',
method: 'GET',
} );
yield updateCommandParams( FAKE_WOO_PAYMENTS_ACTION_NAME, {
enabled: response.enabled || 'no',
} );
} catch ( error ) {
throw new Error( error );
}
}

View File

@ -41,3 +41,7 @@ export function getComingSoonMode( state ) {
export function getWccomRequestErrorsMode( state ) {
return state.params.updateWccomRequestErrorsMode.mode;
}
export function getIsFakeWooPaymentsEnabled( state ) {
return state.params.fakeWooPayments.enabled;
}

View File

@ -43,6 +43,33 @@ const scaleHelp: Record< string, string > = {
),
};
const sizeUnits: { value: string; label: string }[] = [
{
value: 'px',
label: 'px',
},
{
value: 'em',
label: 'em',
},
{
value: 'rem',
label: 'rem',
},
{
value: '%',
label: '%',
},
{
value: 'vw',
label: 'vw',
},
{
value: 'vh',
label: 'vh',
},
];
export const ImageSizeSettings = ( {
scale,
width,
@ -60,12 +87,7 @@ export const ImageSizeSettings = ( {
setAttributes( { height: value } );
} }
value={ height }
units={ [
{
value: 'px',
label: 'px',
},
] }
units={ sizeUnits }
/>
<UnitControl
label={ __( 'Width', 'woocommerce' ) }
@ -73,12 +95,7 @@ export const ImageSizeSettings = ( {
setAttributes( { width: value } );
} }
value={ width }
units={ [
{
value: 'px',
label: 'px',
},
] }
units={ sizeUnits }
/>
{ height && (
<ToolsPanelItem

View File

@ -253,7 +253,7 @@
},
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"dependencies": {
"@ariakit/react": "^0.4.4",

View File

@ -0,0 +1,639 @@
/**
* External dependencies
*/
import { test as base, expect } from '@woocommerce/e2e-utils';
/**
* Internal dependencies
*/
import ProductCollectionPage, { SELECTORS } from '../product-collection.page';
const test = base.extend< { pageObject: ProductCollectionPage } >( {
pageObject: async ( { page, admin, editor }, use ) => {
const pageObject = new ProductCollectionPage( {
page,
admin,
editor,
} );
await use( pageObject );
},
} );
test.describe( 'Product Collection', () => {
test.describe( 'Inspector Controls', () => {
test( 'Reflects the correct number of columns according to sidebar settings', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.setNumberOfColumns( 2 );
await expect( pageObject.productTemplate ).toHaveClass(
/columns-2/
);
await pageObject.setNumberOfColumns( 4 );
await expect( pageObject.productTemplate ).toHaveClass(
/columns-4/
);
await pageObject.publishAndGoToFrontend();
await expect( pageObject.productTemplate ).toHaveClass(
/columns-4/
);
} );
test( 'Order By - sort products by title in descending order correctly', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
const sortedTitles = [
'WordPress Pennant',
'V-Neck T-Shirt',
'T-Shirt with Logo',
'T-Shirt',
/Sunglasses/, // In the frontend it's "Protected: Sunglasses"
'Single',
'Polo',
'Long Sleeve Tee',
'Logo Collection',
];
await pageObject.setOrderBy( 'title/desc' );
await expect( pageObject.productTitles ).toHaveText( sortedTitles );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.productTitles ).toHaveText( sortedTitles );
} );
// Products can be filtered based on 'on sale' status.
test( 'Products can be filtered based on "on sale" status', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
let allProducts = pageObject.products;
let saleProducts = pageObject.products.filter( {
hasText: 'Product on sale',
} );
await expect( allProducts ).toHaveCount( 9 );
await expect( saleProducts ).toHaveCount( 6 );
await pageObject.setShowOnlyProductsOnSale( {
onSale: true,
} );
await expect( allProducts ).toHaveCount( 6 );
await expect( saleProducts ).toHaveCount( 6 );
await pageObject.publishAndGoToFrontend();
await pageObject.refreshLocators( 'frontend' );
allProducts = pageObject.products;
saleProducts = pageObject.products.filter( {
hasText: 'Product on sale',
} );
await expect( allProducts ).toHaveCount( 6 );
await expect( saleProducts ).toHaveCount( 6 );
} );
test( 'Products can be filtered based on selection in handpicked products option', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.addFilter( 'Show Hand-picked Products' );
const filterName = 'Hand-picked Products';
await pageObject.setFilterComboboxValue( filterName, [ 'Album' ] );
await expect( pageObject.products ).toHaveCount( 1 );
const productNames = [ 'Album', 'Cap' ];
await pageObject.setFilterComboboxValue( filterName, productNames );
await expect( pageObject.products ).toHaveCount( 2 );
await expect( pageObject.productTitles ).toHaveText( productNames );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 2 );
await expect( pageObject.productTitles ).toHaveText( productNames );
} );
test( 'Products can be filtered based on keyword.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.addFilter( 'Keyword' );
await pageObject.setKeyword( 'Album' );
await expect( pageObject.productTitles ).toHaveText( [ 'Album' ] );
await pageObject.setKeyword( 'Cap' );
await expect( pageObject.productTitles ).toHaveText( [ 'Cap' ] );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.productTitles ).toHaveText( [ 'Cap' ] );
} );
test( 'Products can be filtered based on category.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
const filterName = 'Product categories';
await pageObject.addFilter( 'Show product categories' );
await pageObject.setFilterComboboxValue( filterName, [
'Clothing',
] );
await expect( pageObject.productTitles ).toHaveText( [
'Logo Collection',
] );
await pageObject.setFilterComboboxValue( filterName, [
'Accessories',
] );
const accessoriesProductNames = [
'Beanie',
'Beanie with Logo',
'Belt',
'Cap',
'Sunglasses',
];
await expect( pageObject.productTitles ).toHaveText(
accessoriesProductNames
);
await pageObject.publishAndGoToFrontend();
const frontendAccessoriesProductNames = [
'Beanie',
'Beanie with Logo',
'Belt',
'Cap',
'Protected: Sunglasses',
];
await expect( pageObject.productTitles ).toHaveText(
frontendAccessoriesProductNames
);
} );
test( 'Products can be filtered based on tags.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
const filterName = 'Product tags';
await pageObject.addFilter( 'Show product tags' );
await pageObject.setFilterComboboxValue( filterName, [
'Recommended',
] );
await expect( pageObject.productTitles ).toHaveText( [
'Beanie',
'Hoodie',
] );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.productTitles ).toHaveText( [
'Beanie',
'Hoodie',
] );
} );
test( 'Products can be filtered based on product attributes like color, size etc.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.addFilter( 'Show Product Attributes' );
await pageObject.setProductAttribute( 'Color', 'Green' );
await expect( pageObject.products ).toHaveCount( 3 );
await pageObject.setProductAttribute( 'Size', 'Large' );
await expect( pageObject.products ).toHaveCount( 1 );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 1 );
} );
test( 'Products can be filtered based on stock status (in stock, out of stock, or backorder).', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.setFilterComboboxValue( 'Stock status', [
'Out of stock',
] );
await expect( pageObject.productTitles ).toHaveText( [
'T-Shirt with Logo',
] );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.productTitles ).toHaveText( [
'T-Shirt with Logo',
] );
} );
test( 'Products can be filtered based on featured status.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Featured' );
await pageObject.setShowOnlyFeaturedProducts( {
featured: true,
} );
// In test data we have only 4 featured products.
await expect( pageObject.products ).toHaveCount( 4 );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 4 );
} );
test( 'Products can be filtered based on created date.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Created' );
await pageObject.setCreatedFilter( {
operator: 'within',
range: 'last3months',
} );
// Products are created with the fixed publish date back in 2019
// so there's no products published in last 3 months.
await expect( pageObject.products ).toHaveCount( 0 );
await pageObject.setCreatedFilter( {
operator: 'before',
range: 'last3months',
} );
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 9 );
} );
test( 'Products can be filtered based on price range.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Price Range' );
await pageObject.setPriceRange( {
min: '18.33',
} );
await expect( pageObject.products ).toHaveCount( 7 );
await pageObject.setPriceRange( {
min: '15.28',
max: '17.21',
} );
await expect( pageObject.products ).toHaveCount( 1 );
await pageObject.setPriceRange( {
max: '17.29',
} );
await expect( pageObject.products ).toHaveCount( 4 );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 4 );
} );
// See https://github.com/woocommerce/woocommerce/pull/49917
test( 'Price range is inclusive in both editor and frontend.', async ( {
page,
pageObject,
editor,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Price Range' );
await pageObject.setPriceRange( {
min: '45',
max: '55',
} );
// Wait for the products to be filtered.
await expect( pageObject.products ).not.toHaveCount( 9 );
await expect(
pageObject.products.filter( { hasText: '$45.00' } )
).not.toHaveCount( 0 );
await expect(
pageObject.products.filter( { hasText: '$55.00' } )
).not.toHaveCount( 0 );
// Reset the price range.
await pageObject.setPriceRange( {
min: '0',
max: '0',
} );
await expect( pageObject.products ).toHaveCount( 9 );
await editor.insertBlock( {
name: 'woocommerce/filter-wrapper',
attributes: { filterType: 'price-filter' },
} );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 9 );
await page
.getByRole( 'textbox', {
name: 'Filter products by minimum',
} )
.dblclick();
await page.keyboard.type( '45' );
await page
.getByRole( 'textbox', {
name: 'Filter products by maximum',
} )
.dblclick();
await page.keyboard.type( '55' );
await page.keyboard.press( 'Tab' );
// Wait for the products to be filtered.
await expect( pageObject.products ).not.toHaveCount( 9 );
await expect(
pageObject.products.filter( { hasText: '$45.00' } )
).not.toHaveCount( 0 );
await expect(
pageObject.products.filter( { hasText: '$55.00' } )
).not.toHaveCount( 0 );
} );
test.describe( '"Use page context" control', () => {
test( 'should be visible on posts', async ( { pageObject } ) => {
await pageObject.createNewPostAndInsertBlock();
await expect(
pageObject
.locateSidebarSettings()
.locator( SELECTORS.usePageContextControl )
).toBeVisible();
} );
[
'woocommerce/woocommerce//archive-product',
'woocommerce/woocommerce//taxonomy-product_cat',
'woocommerce/woocommerce//taxonomy-product_tag',
'woocommerce/woocommerce//taxonomy-product_attribute',
'woocommerce/woocommerce//product-search-results',
].forEach( ( slug ) => {
test( `should be visible in archive template: ${ slug }`, async ( {
pageObject,
editor,
} ) => {
await pageObject.goToEditorTemplate( slug );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate();
await pageObject.focusProductCollection();
await editor.openDocumentSettingsSidebar();
await expect(
pageObject
.locateSidebarSettings()
.locator( SELECTORS.usePageContextControl )
).toBeVisible();
} );
} );
[
'woocommerce/woocommerce//single-product',
'twentytwentyfour//home',
'twentytwentyfour//index',
].forEach( ( slug ) => {
test( `should be visible in non-archive template: ${ slug }`, async ( {
pageObject,
editor,
} ) => {
await pageObject.goToEditorTemplate( slug );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate();
await pageObject.focusProductCollection();
await editor.openDocumentSettingsSidebar();
await expect(
pageObject
.locateSidebarSettings()
.locator( SELECTORS.usePageContextControl )
).toBeVisible();
} );
} );
test( 'should work as expected in Product Catalog template', async ( {
pageObject,
editor,
} ) => {
await pageObject.goToEditorTemplate();
await pageObject.focusProductCollection();
await editor.openDocumentSettingsSidebar();
const sidebarSettings = pageObject.locateSidebarSettings();
// Inherit query from template should be visible & enabled by default
await expect(
sidebarSettings.locator( SELECTORS.usePageContextControl )
).toBeVisible();
await expect(
sidebarSettings.locator(
`${ SELECTORS.usePageContextControl } input`
)
).toBeChecked();
// "On sale control" should be hidden when inherit query from template is enabled
await expect(
sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
).toBeHidden();
// "On sale control" should be visible when inherit query from template is disabled
await pageObject.setInheritQueryFromTemplate( false );
await expect(
sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
).toBeVisible();
// "On sale control" should retain its state when inherit query from template is enabled again
await pageObject.setShowOnlyProductsOnSale( {
onSale: true,
isLocatorsRefreshNeeded: false,
} );
await expect(
sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
).toBeChecked();
await pageObject.setInheritQueryFromTemplate( true );
await expect(
sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
).toBeHidden();
await pageObject.setInheritQueryFromTemplate( false );
await expect(
sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
).toBeVisible();
await expect(
sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
).toBeChecked();
} );
test( 'is enabled by default unless already enabled elsewhere', async ( {
pageObject,
editor,
} ) => {
const productCollection = editor.canvas.getByLabel(
'Block: Product Collection',
{ exact: true }
);
const usePageContextToggle = pageObject
.locateSidebarSettings()
.locator( SELECTORS.usePageContextControl )
.locator( 'input' );
// First Product Catalog
// Option should be visible & ENABLED by default
await pageObject.goToEditorTemplate();
await editor.selectBlocks( productCollection.first() );
await editor.openDocumentSettingsSidebar();
await expect( usePageContextToggle ).toBeChecked();
// Second Product Catalog
// Option should be visible & DISABLED by default
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate( 'productCatalog' );
await editor.selectBlocks( productCollection.last() );
await expect( usePageContextToggle ).not.toBeChecked();
// Disable the option in the first Product Catalog
await editor.selectBlocks( productCollection.first() );
await usePageContextToggle.click();
// Third Product Catalog
// Option should be visible & ENABLED by default
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate( 'productCatalog' );
await editor.selectBlocks( productCollection.last() );
await expect( usePageContextToggle ).toBeChecked();
} );
test( 'allows filtering in non-archive context', async ( {
pageObject,
editor,
page,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost( 'productCatalog' );
await expect( pageObject.products ).toHaveCount( 18 );
await page.getByLabel( 'Toggle block inserter' ).click();
await page.getByRole( 'tab', { name: 'Patterns' } ).click();
await page
.getByPlaceholder( 'Search' )
.fill( 'product filters' );
await page.getByLabel( 'Product Filters' ).click();
const postId = await editor.publishPost();
await page.goto( `/?p=${ postId }` );
const productCollection = page.locator(
'.wp-block-woocommerce-product-collection'
);
await expect(
productCollection.first().locator( SELECTORS.product )
).toHaveCount( 9 );
await expect(
productCollection.last().locator( SELECTORS.product )
).toHaveCount( 9 );
await page
.getByRole( 'textbox', {
name: 'Filter products by maximum',
} )
.dblclick();
await page.keyboard.type( '10' );
await page.keyboard.press( 'Tab' );
await expect(
productCollection.first().locator( SELECTORS.product )
).toHaveCount( 1 );
await expect(
productCollection.last().locator( SELECTORS.product )
).toHaveCount( 9 );
} );
test( 'correctly combines editor and front-end filters', async ( {
pageObject,
editor,
page,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Show product categories' );
await pageObject.setFilterComboboxValue( 'Product categories', [
'Music',
] );
await page.getByLabel( 'Toggle block inserter' ).click();
await page.getByRole( 'tab', { name: 'Patterns' } ).click();
await page
.getByPlaceholder( 'Search' )
.fill( 'product filters' );
await page.getByLabel( 'Product Filters' ).click();
await expect( pageObject.products ).toHaveCount( 2 );
const postId = await editor.publishPost();
await page.goto( `/?p=${ postId }` );
await pageObject.refreshLocators( 'frontend' );
await expect( pageObject.products ).toHaveCount( 2 );
await page
.getByRole( 'textbox', {
name: 'Filter products by maximum',
} )
.dblclick();
await page.keyboard.type( '5' );
await page.keyboard.press( 'Tab' );
await expect( pageObject.products ).toHaveCount( 1 );
} );
} );
} );
} );

View File

@ -165,623 +165,6 @@ test.describe( 'Product Collection', () => {
} );
} );
test.describe( 'Inspector Controls', () => {
test( 'Reflects the correct number of columns according to sidebar settings', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.setNumberOfColumns( 2 );
await expect( pageObject.productTemplate ).toHaveClass(
/columns-2/
);
await pageObject.setNumberOfColumns( 4 );
await expect( pageObject.productTemplate ).toHaveClass(
/columns-4/
);
await pageObject.publishAndGoToFrontend();
await expect( pageObject.productTemplate ).toHaveClass(
/columns-4/
);
} );
test( 'Order By - sort products by title in descending order correctly', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
const sortedTitles = [
'WordPress Pennant',
'V-Neck T-Shirt',
'T-Shirt with Logo',
'T-Shirt',
/Sunglasses/, // In the frontend it's "Protected: Sunglasses"
'Single',
'Polo',
'Long Sleeve Tee',
'Logo Collection',
];
await pageObject.setOrderBy( 'title/desc' );
await expect( pageObject.productTitles ).toHaveText( sortedTitles );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.productTitles ).toHaveText( sortedTitles );
} );
// Products can be filtered based on 'on sale' status.
test( 'Products can be filtered based on "on sale" status', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
let allProducts = pageObject.products;
let saleProducts = pageObject.products.filter( {
hasText: 'Product on sale',
} );
await expect( allProducts ).toHaveCount( 9 );
await expect( saleProducts ).toHaveCount( 6 );
await pageObject.setShowOnlyProductsOnSale( {
onSale: true,
} );
await expect( allProducts ).toHaveCount( 6 );
await expect( saleProducts ).toHaveCount( 6 );
await pageObject.publishAndGoToFrontend();
await pageObject.refreshLocators( 'frontend' );
allProducts = pageObject.products;
saleProducts = pageObject.products.filter( {
hasText: 'Product on sale',
} );
await expect( allProducts ).toHaveCount( 6 );
await expect( saleProducts ).toHaveCount( 6 );
} );
test( 'Products can be filtered based on selection in handpicked products option', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.addFilter( 'Show Hand-picked Products' );
const filterName = 'Hand-picked Products';
await pageObject.setFilterComboboxValue( filterName, [ 'Album' ] );
await expect( pageObject.products ).toHaveCount( 1 );
const productNames = [ 'Album', 'Cap' ];
await pageObject.setFilterComboboxValue( filterName, productNames );
await expect( pageObject.products ).toHaveCount( 2 );
await expect( pageObject.productTitles ).toHaveText( productNames );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 2 );
await expect( pageObject.productTitles ).toHaveText( productNames );
} );
test( 'Products can be filtered based on keyword.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.addFilter( 'Keyword' );
await pageObject.setKeyword( 'Album' );
await expect( pageObject.productTitles ).toHaveText( [ 'Album' ] );
await pageObject.setKeyword( 'Cap' );
await expect( pageObject.productTitles ).toHaveText( [ 'Cap' ] );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.productTitles ).toHaveText( [ 'Cap' ] );
} );
test( 'Products can be filtered based on category.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
const filterName = 'Product categories';
await pageObject.addFilter( 'Show product categories' );
await pageObject.setFilterComboboxValue( filterName, [
'Clothing',
] );
await expect( pageObject.productTitles ).toHaveText( [
'Logo Collection',
] );
await pageObject.setFilterComboboxValue( filterName, [
'Accessories',
] );
const accessoriesProductNames = [
'Beanie',
'Beanie with Logo',
'Belt',
'Cap',
'Sunglasses',
];
await expect( pageObject.productTitles ).toHaveText(
accessoriesProductNames
);
await pageObject.publishAndGoToFrontend();
const frontendAccessoriesProductNames = [
'Beanie',
'Beanie with Logo',
'Belt',
'Cap',
'Protected: Sunglasses',
];
await expect( pageObject.productTitles ).toHaveText(
frontendAccessoriesProductNames
);
} );
test( 'Products can be filtered based on tags.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
const filterName = 'Product tags';
await pageObject.addFilter( 'Show product tags' );
await pageObject.setFilterComboboxValue( filterName, [
'Recommended',
] );
await expect( pageObject.productTitles ).toHaveText( [
'Beanie',
'Hoodie',
] );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.productTitles ).toHaveText( [
'Beanie',
'Hoodie',
] );
} );
test( 'Products can be filtered based on product attributes like color, size etc.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.addFilter( 'Show Product Attributes' );
await pageObject.setProductAttribute( 'Color', 'Green' );
await expect( pageObject.products ).toHaveCount( 3 );
await pageObject.setProductAttribute( 'Size', 'Large' );
await expect( pageObject.products ).toHaveCount( 1 );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 1 );
} );
test( 'Products can be filtered based on stock status (in stock, out of stock, or backorder).', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await pageObject.setFilterComboboxValue( 'Stock status', [
'Out of stock',
] );
await expect( pageObject.productTitles ).toHaveText( [
'T-Shirt with Logo',
] );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.productTitles ).toHaveText( [
'T-Shirt with Logo',
] );
} );
test( 'Products can be filtered based on featured status.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Featured' );
await pageObject.setShowOnlyFeaturedProducts( {
featured: true,
} );
// In test data we have only 4 featured products.
await expect( pageObject.products ).toHaveCount( 4 );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 4 );
} );
test( 'Products can be filtered based on created date.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Created' );
await pageObject.setCreatedFilter( {
operator: 'within',
range: 'last3months',
} );
// Products are created with the fixed publish date back in 2019
// so there's no products published in last 3 months.
await expect( pageObject.products ).toHaveCount( 0 );
await pageObject.setCreatedFilter( {
operator: 'before',
range: 'last3months',
} );
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 9 );
} );
test( 'Products can be filtered based on price range.', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Price Range' );
await pageObject.setPriceRange( {
min: '18.33',
} );
await expect( pageObject.products ).toHaveCount( 7 );
await pageObject.setPriceRange( {
min: '15.28',
max: '17.21',
} );
await expect( pageObject.products ).toHaveCount( 1 );
await pageObject.setPriceRange( {
max: '17.29',
} );
await expect( pageObject.products ).toHaveCount( 4 );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 4 );
} );
// See https://github.com/woocommerce/woocommerce/pull/49917
test( 'Price range is inclusive in both editor and frontend.', async ( {
page,
pageObject,
editor,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Price Range' );
await pageObject.setPriceRange( {
min: '45',
max: '55',
} );
// Wait for the products to be filtered.
await expect( pageObject.products ).not.toHaveCount( 9 );
await expect(
pageObject.products.filter( { hasText: '$45.00' } )
).not.toHaveCount( 0 );
await expect(
pageObject.products.filter( { hasText: '$55.00' } )
).not.toHaveCount( 0 );
// Reset the price range.
await pageObject.setPriceRange( {
min: '0',
max: '0',
} );
await expect( pageObject.products ).toHaveCount( 9 );
await editor.insertBlock( {
name: 'woocommerce/filter-wrapper',
attributes: { filterType: 'price-filter' },
} );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 9 );
await page
.getByRole( 'textbox', {
name: 'Filter products by minimum',
} )
.dblclick();
await page.keyboard.type( '45' );
await page
.getByRole( 'textbox', {
name: 'Filter products by maximum',
} )
.dblclick();
await page.keyboard.type( '55' );
await page.keyboard.press( 'Tab' );
// Wait for the products to be filtered.
await expect( pageObject.products ).not.toHaveCount( 9 );
await expect(
pageObject.products.filter( { hasText: '$45.00' } )
).not.toHaveCount( 0 );
await expect(
pageObject.products.filter( { hasText: '$55.00' } )
).not.toHaveCount( 0 );
} );
test.describe( '"Use page context" control', () => {
test( 'should be visible on posts', async ( { pageObject } ) => {
await pageObject.createNewPostAndInsertBlock();
await expect(
pageObject
.locateSidebarSettings()
.locator( SELECTORS.usePageContextControl )
).toBeVisible();
} );
[
'woocommerce/woocommerce//archive-product',
'woocommerce/woocommerce//taxonomy-product_cat',
'woocommerce/woocommerce//taxonomy-product_tag',
'woocommerce/woocommerce//taxonomy-product_attribute',
'woocommerce/woocommerce//product-search-results',
].forEach( ( slug ) => {
test( `should be visible in archive template: ${ slug }`, async ( {
pageObject,
editor,
} ) => {
await pageObject.goToEditorTemplate( slug );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate();
await pageObject.focusProductCollection();
await editor.openDocumentSettingsSidebar();
await expect(
pageObject
.locateSidebarSettings()
.locator( SELECTORS.usePageContextControl )
).toBeVisible();
} );
} );
[
'woocommerce/woocommerce//single-product',
'twentytwentyfour//home',
'twentytwentyfour//index',
].forEach( ( slug ) => {
test( `should be visible in non-archive template: ${ slug }`, async ( {
pageObject,
editor,
} ) => {
await pageObject.goToEditorTemplate( slug );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate();
await pageObject.focusProductCollection();
await editor.openDocumentSettingsSidebar();
await expect(
pageObject
.locateSidebarSettings()
.locator( SELECTORS.usePageContextControl )
).toBeVisible();
} );
} );
test( 'should work as expected in Product Catalog template', async ( {
pageObject,
editor,
} ) => {
await pageObject.goToEditorTemplate();
await pageObject.focusProductCollection();
await editor.openDocumentSettingsSidebar();
const sidebarSettings = pageObject.locateSidebarSettings();
// Inherit query from template should be visible & enabled by default
await expect(
sidebarSettings.locator( SELECTORS.usePageContextControl )
).toBeVisible();
await expect(
sidebarSettings.locator(
`${ SELECTORS.usePageContextControl } input`
)
).toBeChecked();
// "On sale control" should be hidden when inherit query from template is enabled
await expect(
sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
).toBeHidden();
// "On sale control" should be visible when inherit query from template is disabled
await pageObject.setInheritQueryFromTemplate( false );
await expect(
sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
).toBeVisible();
// "On sale control" should retain its state when inherit query from template is enabled again
await pageObject.setShowOnlyProductsOnSale( {
onSale: true,
isLocatorsRefreshNeeded: false,
} );
await expect(
sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
).toBeChecked();
await pageObject.setInheritQueryFromTemplate( true );
await expect(
sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
).toBeHidden();
await pageObject.setInheritQueryFromTemplate( false );
await expect(
sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
).toBeVisible();
await expect(
sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
).toBeChecked();
} );
test( 'is enabled by default unless already enabled elsewhere', async ( {
pageObject,
editor,
} ) => {
const productCollection = editor.canvas.getByLabel(
'Block: Product Collection',
{ exact: true }
);
const usePageContextToggle = pageObject
.locateSidebarSettings()
.locator( SELECTORS.usePageContextControl )
.locator( 'input' );
// First Product Catalog
// Option should be visible & ENABLED by default
await pageObject.goToEditorTemplate();
await editor.selectBlocks( productCollection.first() );
await editor.openDocumentSettingsSidebar();
await expect( usePageContextToggle ).toBeChecked();
// Second Product Catalog
// Option should be visible & DISABLED by default
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate( 'productCatalog' );
await editor.selectBlocks( productCollection.last() );
await expect( usePageContextToggle ).not.toBeChecked();
// Disable the option in the first Product Catalog
await editor.selectBlocks( productCollection.first() );
await usePageContextToggle.click();
// Third Product Catalog
// Option should be visible & ENABLED by default
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate( 'productCatalog' );
await editor.selectBlocks( productCollection.last() );
await expect( usePageContextToggle ).toBeChecked();
} );
test( 'allows filtering in non-archive context', async ( {
pageObject,
editor,
page,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost( 'productCatalog' );
await expect( pageObject.products ).toHaveCount( 18 );
await page.getByLabel( 'Toggle block inserter' ).click();
await page.getByRole( 'tab', { name: 'Patterns' } ).click();
await page
.getByPlaceholder( 'Search' )
.fill( 'product filters' );
await page.getByLabel( 'Product Filters' ).click();
const postId = await editor.publishPost();
await page.goto( `/?p=${ postId }` );
const productCollection = page.locator(
'.wp-block-woocommerce-product-collection'
);
await expect(
productCollection.first().locator( SELECTORS.product )
).toHaveCount( 9 );
await expect(
productCollection.last().locator( SELECTORS.product )
).toHaveCount( 9 );
await page
.getByRole( 'textbox', {
name: 'Filter products by maximum',
} )
.dblclick();
await page.keyboard.type( '10' );
await page.keyboard.press( 'Tab' );
await expect(
productCollection.first().locator( SELECTORS.product )
).toHaveCount( 1 );
await expect(
productCollection.last().locator( SELECTORS.product )
).toHaveCount( 9 );
} );
test( 'correctly combines editor and front-end filters', async ( {
pageObject,
editor,
page,
} ) => {
await pageObject.createNewPostAndInsertBlock();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Show product categories' );
await pageObject.setFilterComboboxValue( 'Product categories', [
'Music',
] );
await page.getByLabel( 'Toggle block inserter' ).click();
await page.getByRole( 'tab', { name: 'Patterns' } ).click();
await page
.getByPlaceholder( 'Search' )
.fill( 'product filters' );
await page.getByLabel( 'Product Filters' ).click();
await expect( pageObject.products ).toHaveCount( 2 );
const postId = await editor.publishPost();
await page.goto( `/?p=${ postId }` );
await pageObject.refreshLocators( 'frontend' );
await expect( pageObject.products ).toHaveCount( 2 );
await page
.getByRole( 'textbox', {
name: 'Filter products by maximum',
} )
.dblclick();
await page.keyboard.type( '5' );
await page.keyboard.press( 'Tab' );
await expect( pageObject.products ).toHaveCount( 1 );
} );
} );
} );
test.describe( 'Toolbar settings', () => {
test.beforeEach( async ( { pageObject } ) => {
await pageObject.createNewPostAndInsertBlock();

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
CYS - Move the "ai/products" endpoint to woocommerce admin API.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: enhancement
Add additional sizing units for product image block

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Add e2e tests to confirm that the store is in coming soon mode after completing the core profiler

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix pnpm version to 9.1.3 to avoid dependency installation issues.

View File

@ -0,0 +1,5 @@
Significance: patch
Type: dev
Comment: Store API: Add test coverage for Product Reviews endpoint

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Conditionally set new order email sent meta field

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Move the inspector controls e2e tests from product collection file to its own file

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Update reset password e2e to use or locator to check reset status

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
fix small lint errors and fix lint rule to make doc contribution easier

View File

@ -2,7 +2,7 @@
"name": "woocommerce/woocommerce",
"description": "An eCommerce toolkit that helps you sell anything. Beautifully.",
"homepage": "https://woocommerce.com/",
"version": "9.3.0",
"version": "9.4.0",
"type": "wordpress-plugin",
"license": "GPL-3.0-or-later",
"prefer-stable": true,

View File

@ -46,7 +46,7 @@ final class WooCommerce {
*
* @var string
*/
public $version = '9.3.0';
public $version = '9.4.0';
/**
* WooCommerce Schema version.

View File

@ -109,10 +109,11 @@ if ( ! class_exists( 'WC_Email_New_Order' ) ) :
}
if ( $this->is_enabled() && $this->get_recipient() ) {
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
$order->update_meta_data( '_new_order_email_sent', 'true' );
$order->save();
$email_sent_successfully = $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
if ( $email_sent_successfully ) {
$order->update_meta_data( '_new_order_email_sent', 'true' );
$order->save();
}
}
$this->restore_locale();

View File

@ -2,7 +2,7 @@
"name": "@woocommerce/plugin-woocommerce",
"private": true,
"title": "WooCommerce",
"version": "9.3.0",
"version": "9.4.0",
"homepage": "https://woocommerce.com/",
"repository": {
"type": "git",
@ -639,7 +639,7 @@
},
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"
"pnpm": "9.1.3"
},
"browserslist": [
"> 0.1%",

View File

@ -169,6 +169,6 @@ WooCommerce comes with some sample data you can use to see how products look; im
== Changelog ==
= 9.3.0 2024-XX-XX =
= 9.4.0 2024-XX-XX =
[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/changelog.txt).

View File

@ -0,0 +1,128 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Admin\API\AI;
use Automattic\WooCommerce\Blocks\AI\Connection;
use Automattic\WooCommerce\Blocks\AIContent\UpdateProducts;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
defined( 'ABSPATH' ) || exit;
/**
* Product controller
*
* @internal
*/
class Products extends AIEndpoint {
/**
* Endpoint.
*
* @var string
*/
protected $endpoint = 'products';
/**
* Register routes.
*/
public function register_routes() {
$this->register(
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'generate_products_content' ),
'permission_callback' => array( Middleware::class, 'is_authorized' ),
'args' => array(
'business_description' => array(
'description' => __( 'The business description for a given store.', 'woocommerce' ),
'type' => 'string',
),
'images' => array(
'description' => __( 'The images for a given store.', 'woocommerce' ),
'type' => 'object',
),
),
),
array(
'methods' => \WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_products' ),
'permission_callback' => array( Middleware::class, 'is_authorized' ),
),
)
);
}
/**
* Generate the content for the products.
*
* @param WP_REST_Request $request Request object.
*
* @return WP_Error|WP_REST_Response
*/
public function generate_products_content( WP_REST_Request $request ) {
$allow_ai_connection = get_option( 'woocommerce_blocks_allow_ai_connection' );
if ( ! $allow_ai_connection ) {
return rest_ensure_response(
new WP_Error(
'ai_connection_not_allowed',
__( 'AI content generation is not allowed on this store. Update your store settings if you wish to enable this feature.', 'woocommerce' )
)
);
}
$business_description = sanitize_text_field( wp_unslash( $request['business_description'] ) );
if ( empty( $business_description ) ) {
$business_description = get_option( 'woo_ai_describe_store_description' );
}
$ai_connection = new Connection();
$site_id = $ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return $site_id;
}
$token = $ai_connection->get_jwt_token( $site_id );
if ( is_wp_error( $token ) ) {
return $token;
}
$images = $request['images'];
$populate_products = ( new UpdateProducts() )->generate_content( $ai_connection, $token, $images, $business_description );
if ( is_wp_error( $populate_products ) ) {
return $populate_products;
}
if ( ! isset( $populate_products['product_content'] ) ) {
return new WP_Error( 'product_content_not_found', __( 'Product content not found.', 'woocommerce' ) );
}
$product_content = $populate_products['product_content'];
$item = array(
'ai_content_generated' => true,
'product_content' => $product_content,
);
return rest_ensure_response( $item );
}
/**
* Remove products generated by AI.
*
* @return WP_Error|WP_REST_Response
*/
public function delete_products() {
( new UpdateProducts() )->reset_products_content();
return rest_ensure_response( array( 'removed' => true ) );
}
}

View File

@ -92,6 +92,7 @@ class Init {
'Automattic\WooCommerce\Admin\API\AI\Images',
'Automattic\WooCommerce\Admin\API\AI\Patterns',
'Automattic\WooCommerce\Admin\API\AI\Product',
'Automattic\WooCommerce\Admin\API\AI\Products',
);
}

View File

@ -1,50 +0,0 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1\AI;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
/**
* Middleware class.
*
* @internal
*/
class Middleware {
/**
* Ensure that the user is allowed to make this request.
*
* @throws RouteException If the user is not allowed to make this request.
* @return boolean
*/
public static function is_authorized() {
try {
if ( ! current_user_can( 'manage_options' ) ) {
throw new RouteException( 'woocommerce_rest_invalid_user', __( 'You are not allowed to make this request. Please make sure you are logged in.', 'woocommerce' ), 403 );
}
} catch ( RouteException $error ) {
return new \WP_Error(
$error->getErrorCode(),
$error->getMessage(),
array( 'status' => $error->getCode() )
);
}
$allow_ai_connection = get_option( 'woocommerce_blocks_allow_ai_connection' );
if ( ! $allow_ai_connection ) {
try {
throw new RouteException( 'ai_connection_not_allowed', __( 'AI content generation is not allowed on this store. Update your store settings if you wish to enable this feature.', 'woocommerce' ), 403 );
} catch ( RouteException $error ) {
return new \WP_Error(
$error->getErrorCode(),
$error->getMessage(),
array( 'status' => $error->getCode() )
);
}
}
return true;
}
}

View File

@ -1,153 +0,0 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1\AI;
use Automattic\WooCommerce\Blocks\AI\Connection;
use Automattic\WooCommerce\Blocks\AIContent\UpdateProducts;
use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractRoute;
/**
* Products class.
*
* @internal
*/
class Products extends AbstractRoute {
/**
* The route identifier.
*
* @var string
*/
const IDENTIFIER = 'ai/products';
/**
* The schema item identifier.
*
* @var string
*/
const SCHEMA_TYPE = 'ai/products';
/**
* Get the path of this REST route.
*
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/ai/products';
}
/**
* Get method arguments for this REST route.
*
* @return array An array of endpoints.
*/
public function get_args() {
return [
[
'methods' => \WP_REST_Server::CREATABLE,
'callback' => [ $this, 'get_response' ],
'permission_callback' => [ Middleware::class, 'is_authorized' ],
'args' => [
'business_description' => [
'description' => __( 'The business description for a given store.', 'woocommerce' ),
'type' => 'string',
],
'images' => [
'description' => __( 'The images for a given store.', 'woocommerce' ),
'type' => 'object',
],
],
],
[
'methods' => \WP_REST_Server::DELETABLE,
'callback' => [ $this, 'get_response' ],
'permission_callback' => [ Middleware::class, 'is_authorized' ],
],
'schema' => [ $this->schema, 'get_public_item_schema' ],
'allow_batch' => [ 'v1' => true ],
];
}
/**
* Generate the content for the products.
*
* @param \WP_REST_Request $request Request object.
*
* @return bool|string|\WP_Error|\WP_REST_Response
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
$allow_ai_connection = get_option( 'woocommerce_blocks_allow_ai_connection' );
if ( ! $allow_ai_connection ) {
return rest_ensure_response(
$this->error_to_response(
new \WP_Error(
'ai_connection_not_allowed',
__( 'AI content generation is not allowed on this store. Update your store settings if you wish to enable this feature.', 'woocommerce' )
)
)
);
}
$business_description = sanitize_text_field( wp_unslash( $request['business_description'] ) );
if ( empty( $business_description ) ) {
$business_description = get_option( 'woo_ai_describe_store_description' );
}
$ai_connection = new Connection();
$site_id = $ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return $this->error_to_response( $site_id );
}
$token = $ai_connection->get_jwt_token( $site_id );
if ( is_wp_error( $token ) ) {
return $this->error_to_response( $token );
}
$images = $request['images'];
$populate_products = ( new UpdateProducts() )->generate_content( $ai_connection, $token, $images, $business_description );
if ( is_wp_error( $populate_products ) ) {
return $this->error_to_response( $populate_products );
}
if ( ! isset( $populate_products['product_content'] ) ) {
return $this->error_to_response( new \WP_Error( 'product_content_not_found', __( 'Product content not found.', 'woocommerce' ) ) );
}
$product_content = $populate_products['product_content'];
$item = array(
'ai_content_generated' => true,
'product_content' => $product_content,
);
return rest_ensure_response( $item );
}
/**
* Remove products generated by AI.
*
* @param \WP_REST_Request $request Request object.
*
* @return bool|string|\WP_Error|\WP_REST_Response
*/
protected function get_route_delete_response( \WP_REST_Request $request ) {
( new UpdateProducts() )->reset_products_content();
return rest_ensure_response( array( 'removed' => true ) );
}
}

View File

@ -66,10 +66,8 @@ class RoutesController {
Routes\V1\ProductsById::IDENTIFIER => Routes\V1\ProductsById::class,
Routes\V1\ProductsBySlug::IDENTIFIER => Routes\V1\ProductsBySlug::class,
],
// @todo Migrate internal AI routes to WooCommerce Core codebase.
'private' => [
Routes\V1\AI\Products::IDENTIFIER => Routes\V1\AI\Products::class,
Routes\V1\Patterns::IDENTIFIER => Routes\V1\Patterns::class,
Routes\V1\Patterns::IDENTIFIER => Routes\V1\Patterns::class,
],
];
}

View File

@ -54,7 +54,6 @@ class SchemaController {
Schemas\V1\ProductCategorySchema::IDENTIFIER => Schemas\V1\ProductCategorySchema::class,
Schemas\V1\ProductCollectionDataSchema::IDENTIFIER => Schemas\V1\ProductCollectionDataSchema::class,
Schemas\V1\ProductReviewSchema::IDENTIFIER => Schemas\V1\ProductReviewSchema::class,
Schemas\V1\AI\ProductsSchema::IDENTIFIER => Schemas\V1\AI\ProductsSchema::class,
Schemas\V1\PatternsSchema::IDENTIFIER => Schemas\V1\PatternsSchema::class,
],
];

View File

@ -1,48 +0,0 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1\AI;
use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema;
/**
* ProductsSchema class.
*
* @internal
*/
class ProductsSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'ai/products';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'ai/products';
/**
* Products schema properties.
*
* @return array
*/
public function get_properties() {
return [];
}
/**
* Get the Products response.
*
* @param array $item Item to get response for.
*
* @return array
*/
public function get_item_response( $item ) {
return [
'ai_content_generated' => $item['ai_content_generated'],
'product_content' => $item['product_content'],
];
}
}

View File

@ -1,8 +1,22 @@
const { test, expect } = require( '@playwright/test' );
const { test, expect, request } = require( '@playwright/test' );
const { setOption } = require( '../../utils/options' );
test.describe( 'Store owner can complete the core profiler', () => {
test.use( { storageState: process.env.ADMINSTATE } );
test.beforeAll( async ( { baseURL } ) => {
try {
await setOption(
request,
baseURL,
'woocommerce_coming_soon',
'no'
);
} catch ( error ) {
console.log( error );
}
} );
test( 'Can complete the core profiler skipping extension install', async ( {
page,
} ) => {
@ -393,12 +407,32 @@ test.describe( 'Store owner can complete the core profiler', () => {
page.getByLabel( 'Delete Pinterest for' )
).toBeHidden();
} );
await test.step( 'Confirm that the store is in coming soon mode after completing the core profiler', async () => {
await page.goto( 'wp-admin/admin.php?page=wc-admin' );
await expect(
page.getByRole( 'menuitem', { name: 'Store coming soon' } )
).toBeVisible();
} );
} );
} );
test.describe( 'Store owner can skip the core profiler', () => {
test.use( { storageState: process.env.ADMINSTATE } );
test.beforeAll( async ( { baseURL } ) => {
try {
await setOption(
request,
baseURL,
'woocommerce_coming_soon',
'no'
);
} catch ( error ) {
console.log( error );
}
} );
test( 'Can click skip guided setup', async ( { page } ) => {
await page.goto(
'wp-admin/admin.php?page=wc-admin&path=%2Fsetup-wizard'
@ -426,6 +460,13 @@ test.describe( 'Store owner can skip the core profiler', () => {
name: 'Welcome to WooCommerce Core E2E Test Suite',
} )
).toBeVisible();
await test.step( 'Confirm that the store is in coming soon mode after skipping the core profiler', async () => {
await page.goto( 'wp-admin/admin.php?page=wc-admin' );
await expect(
page.getByRole( 'menuitem', { name: 'Store coming soon' } )
).toBeVisible();
} );
} );
test( 'Can connect to WooCommerce.com', async ( { page } ) => {

Some files were not shown because too many files have changed in this diff Show More