From 007bd21a35a5745f9ea75dc9c6b224cce37cf168 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 11 Sep 2024 15:39:28 +0800 Subject: [PATCH 01/59] Clean up purchase task (#51274) * Remove Purchase task * Add changelog --- .../changelog/update-remove-purchase-task | 4 + .../OnboardingTasks/Tasks/Purchase.php | 203 ------------------ .../onboarding-tasks/tasks/purchase.php | 186 ---------------- 3 files changed, 4 insertions(+), 389 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-remove-purchase-task delete mode 100644 plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Purchase.php delete mode 100644 plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/tasks/purchase.php diff --git a/plugins/woocommerce/changelog/update-remove-purchase-task b/plugins/woocommerce/changelog/update-remove-purchase-task new file mode 100644 index 00000000000..20806d693a2 --- /dev/null +++ b/plugins/woocommerce/changelog/update-remove-purchase-task @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Clean up Purchase task diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Purchase.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Purchase.php deleted file mode 100644 index 2c9383f4a60..00000000000 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Purchase.php +++ /dev/null @@ -1,203 +0,0 @@ -undo_dismiss(); - } - - /** - * Get the task arguments. - * ID. - * - * @return string - */ - public function get_id() { - return 'purchase'; - } - - /** - * Title. - * - * @return string - */ - public function get_title() { - $products = $this->get_paid_products_and_themes(); - $first_product = count( $products['purchaseable'] ) >= 1 ? $products['purchaseable'][0] : false; - - if ( ! $first_product ) { - return null; - } - - $product_label = isset( $first_product['label'] ) ? $first_product['label'] : $first_product['title']; - $additional_count = count( $products['purchaseable'] ) - 1; - - if ( $this->get_parent_option( 'use_completed_title' ) && $this->is_complete() ) { - return count( $products['purchaseable'] ) === 1 - ? sprintf( - /* translators: %1$s: a purchased product name */ - __( - 'You added %1$s', - 'woocommerce' - ), - $product_label - ) - : sprintf( - /* translators: %1$s: a purchased product name, %2$d the number of other products purchased */ - _n( - 'You added %1$s and %2$d other product', - 'You added %1$s and %2$d other products', - $additional_count, - 'woocommerce' - ), - $product_label, - $additional_count - ); - } - - return count( $products['purchaseable'] ) === 1 - ? sprintf( - /* translators: %1$s: a purchaseable product name */ - __( - 'Add %s to my store', - 'woocommerce' - ), - $product_label - ) - : sprintf( - /* translators: %1$s: a purchaseable product name, %2$d the number of other products to purchase */ - _n( - 'Add %1$s and %2$d more product to my store', - 'Add %1$s and %2$d more products to my store', - $additional_count, - 'woocommerce' - ), - $product_label, - $additional_count - ); - } - - /** - * Content. - * - * @return string - */ - public function get_content() { - $products = $this->get_paid_products_and_themes(); - - if ( count( $products['remaining'] ) === 1 ) { - return isset( $products['purchaseable'][0]['description'] ) ? $products['purchaseable'][0]['description'] : $products['purchaseable'][0]['excerpt']; - } - return sprintf( - /* translators: %1$s: list of product names comma separated, %2%s the last product name */ - __( - 'Good choice! You chose to add %1$s and %2$s to your store.', - 'woocommerce' - ), - implode( ', ', array_slice( $products['remaining'], 0, -1 ) ) . ( count( $products['remaining'] ) > 2 ? ',' : '' ), - end( $products['remaining'] ) - ); - } - - /** - * Action label. - * - * @return string - */ - public function get_action_label() { - return __( 'Purchase & install now', 'woocommerce' ); - } - - - /** - * Time. - * - * @return string - */ - public function get_time() { - return __( '2 minutes', 'woocommerce' ); - } - - /** - * Task completion. - * - * @return bool - */ - public function is_complete() { - $products = $this->get_paid_products_and_themes(); - return count( $products['remaining'] ) === 0; - } - - /** - * Dismissable. - * - * @return bool - */ - public function is_dismissable() { - return true; - } - - /** - * Task visibility. - * - * @return bool - */ - public function can_view() { - $products = $this->get_paid_products_and_themes(); - return count( $products['purchaseable'] ) > 0; - } - - /** - * Get purchaseable and remaining products. - * - * @return array purchaseable and remaining products and themes. - */ - public static function get_paid_products_and_themes() { - $relevant_products = OnboardingProducts::get_relevant_products(); - - $profiler_data = get_option( OnboardingProfile::DATA_OPTION, array() ); - $theme = isset( $profiler_data['theme'] ) ? $profiler_data['theme'] : null; - $paid_theme = $theme ? OnboardingThemes::get_paid_theme_by_slug( $theme ) : null; - if ( $paid_theme ) { - - $relevant_products['purchaseable'][] = $paid_theme; - - if ( isset( $paid_theme['is_installed'] ) && false === $paid_theme['is_installed'] ) { - $relevant_products['remaining'][] = $paid_theme['title']; - } - } - return $relevant_products; - } -} diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/tasks/purchase.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/tasks/purchase.php deleted file mode 100644 index 043052a0371..00000000000 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/tasks/purchase.php +++ /dev/null @@ -1,186 +0,0 @@ -task = new Purchase( new TaskList() ); - set_transient( - OnboardingThemes::THEMES_TRANSIENT, - array( - 'free' => array( - 'slug' => 'free', - 'is_installed' => false, - ), - 'paid' => array( - 'slug' => 'paid', - 'id' => 12312, - 'price' => '$79.00', - 'title' => 'theme title', - 'is_installed' => false, - ), - 'paid_installed' => array( - 'slug' => 'paid_installed', - 'id' => 12312, - 'price' => '$79.00', - 'title' => 'theme title', - 'is_installed' => true, - ), - 'free_with_price' => array( - 'slug' => 'free_with_price', - 'id' => 12312, - 'price' => '$0.00', - 'title' => 'theme title', - 'is_installed' => false, - ), - ) - ); - } - - /** - * Tear down. - */ - public function tearDown(): void { - parent::tearDown(); - delete_transient( OnboardingThemes::THEMES_TRANSIENT ); - delete_option( OnboardingProfile::DATA_OPTION ); - } - - /** - * Test is_complete function of Purchase task. - */ - public function test_is_complete_if_no_remaining_products() { - update_option( OnboardingProfile::DATA_OPTION, array( 'product_types' => array( 'physical' ) ) ); - $this->assertEquals( true, $this->task->is_complete() ); - } - - /** - * Test is_complete function of Purchase task. - */ - public function test_is_not_complete_if_remaining_paid_products() { - update_option( OnboardingProfile::DATA_OPTION, array( 'product_types' => array( 'memberships' ) ) ); - $this->assertEquals( false, $this->task->is_complete() ); - } - - /** - * Test is_complete function of Purchase task. - */ - public function test_is_complete_if_no_paid_themes() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array(), - 'theme' => 'free', - ) - ); - $this->assertEquals( true, $this->task->is_complete() ); - } - - /** - * Test is_complete function of Purchase task. - */ - public function test_is_not_complete_if_paid_theme_that_is_not_installed() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array(), - 'theme' => 'paid', - ) - ); - $this->assertEquals( false, $this->task->is_complete() ); - } - - /** - * Test is_complete function of Purchase task. - */ - public function test_is_complete_if_paid_theme_that_is_installed() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array(), - 'theme' => 'paid_installed', - ) - ); - $this->assertEquals( true, $this->task->is_complete() ); - } - - /** - * Test is_complete function of Purchase task. - */ - public function test_is_complete_if_free_theme_with_set_price() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array(), - 'theme' => 'free_with_price', - ) - ); - $this->assertEquals( true, $this->task->is_complete() ); - } - - /** - * Test the task title for a single paid item. - */ - public function test_get_title_if_single_paid_item() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array(), - 'theme' => 'paid', - ) - ); - $this->assertEquals( 'Add theme title to my store', $this->task->get_title() ); - } - - /** - * Test the task title if 2 paid items exist. - */ - public function test_get_title_if_multiple_paid_themes() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array( 'memberships' ), - 'theme' => 'paid', - ) - ); - $this->assertEquals( 'Add Memberships and 1 more product to my store', $this->task->get_title() ); - } - - /** - * Test the task title if multiple additional paid items exist. - */ - public function test_get_title_if_multiple_paid_products() { - update_option( - OnboardingProfile::DATA_OPTION, - array( - 'product_types' => array( 'memberships', 'bookings' ), - 'theme' => 'paid', - ) - ); - $this->assertEquals( 'Add Memberships and 2 more products to my store', $this->task->get_title() ); - } -} From b728f53f2902d56214d05ac34ac0266fafde85ad Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 11 Sep 2024 16:13:48 +0800 Subject: [PATCH 02/59] Fix duplicate spec evaluation in `evaluate_specs()` (#51166) * feat: Add memoization to EvaluateSuggestion class This commit adds memoization to the `EvaluateSuggestion` class in order to improve performance by caching the results of the `evaluate_specs` method. The memoization is implemented using an associative array called `$memo`, which stores the results of previous evaluations based on the input specifications and logger arguments. The memoization logic checks if the results for a given set of specifications and logger arguments already exist in the `$memo` array, and if so, returns the cached results instead of re-evaluating the specs. This helps to avoid redundant computations and improves the overall efficiency of the `evaluate_specs` method. * Add changelog * Only use specs as key * Move shipping test to correct folder * Fix tests * Fix tests * Fix lint * Address PR feedback --- .../changelog/fix-duplicate-spec-evaluation | 4 ++ .../EvaluateSuggestion.php | 49 ++++++++++++++++++- .../evaluate-suggestion.php | 41 ++++++++++++++++ .../payment-gateway-suggestions.php | 9 ++-- .../DefaultShippingPartnersTest.php | 4 +- .../ShippingPartnerSuggestionsTest.php | 7 ++- .../WCPayPromotion/DefaultPromotionsTest.php | 2 + .../Admin/WCPayPromotion/InitTest.php | 6 ++- 8 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-duplicate-spec-evaluation rename plugins/woocommerce/tests/php/src/Admin/{ => Features}/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php (97%) diff --git a/plugins/woocommerce/changelog/fix-duplicate-spec-evaluation b/plugins/woocommerce/changelog/fix-duplicate-spec-evaluation new file mode 100644 index 00000000000..299eaf7c9d6 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-duplicate-spec-evaluation @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix duplicate spec evaluation in evaluate_specs() diff --git a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php index 45d609da940..4b614367199 100644 --- a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php +++ b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php @@ -13,6 +13,13 @@ use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\RuleEvaluator; * Evaluates the spec and returns the evaluated suggestion. */ class EvaluateSuggestion { + /** + * Stores memoized results of evaluate_specs. + * + * @var array + */ + protected static $memo = array(); + /** * Evaluates the spec and returns the suggestion. * @@ -58,6 +65,12 @@ class EvaluateSuggestion { * @return array The visible suggestions and errors. */ public static function evaluate_specs( $specs, $logger_args = array() ) { + $specs_key = self::get_memo_key( $specs ); + + if ( isset( self::$memo[ $specs_key ] ) ) { + return self::$memo[ $specs_key ]; + } + $suggestions = array(); $errors = array(); @@ -72,9 +85,43 @@ class EvaluateSuggestion { } } - return array( + $result = array( 'suggestions' => $suggestions, 'errors' => $errors, ); + + // Memoize results, with a fail safe to prevent unbounded memory growth. + // This limit is unlikely to be reached under normal circumstances. + if ( count( self::$memo ) > 50 ) { + self::reset_memo(); + } + self::$memo[ $specs_key ] = $result; + + return $result; + } + + /** + * Resets the memoized results. Useful for testing. + */ + public static function reset_memo() { + self::$memo = array(); + } + + /** + * Returns a memoization key for the given specs. + * + * @param array $specs The specs to generate a key for. + * + * @return string The memoization key. + */ + private static function get_memo_key( $specs ) { + $data = wp_json_encode( $specs ); + + if ( function_exists( 'hash' ) && in_array( 'xxh3', hash_algos(), true ) ) { + // Use xxHash (xxh3) if available. + return hash( 'xxh3', $data ); + } + // Fall back to CRC32. + return (string) crc32( $data ); } } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/evaluate-suggestion.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/evaluate-suggestion.php index 15fee01caea..3b0a70952c6 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/evaluate-suggestion.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/evaluate-suggestion.php @@ -323,6 +323,31 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_EvaluateSuggestion extends WC_Uni remove_filter( 'woocommerce_admin_remote_specs_evaluator_should_log', '__return_true' ); } + /** + * Test that the memo is set correctly. + */ + public function test_memo_set_correctly() { + $specs = array( + array( + 'id' => 'test-gateway-1', + 'is_visible' => true, + ), + array( + 'id' => 'test-gateway-2', + 'is_visible' => false, + ), + ); + + $result = TestableEvaluateSuggestion::evaluate_specs( $specs ); + $memo = TestableEvaluateSuggestion::get_memo_for_tests(); + + $this->assertCount( 1, $memo ); + $memo_key = array_keys( $memo )[0]; + $this->assertEquals( $result, $memo[ $memo_key ] ); + $this->assertCount( 1, $result['suggestions'] ); + $this->assertEquals( 'test-gateway-1', $result['suggestions'][0]->id ); + } + /** * Overrides the WC logger. * @@ -359,3 +384,19 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_EvaluateSuggestion extends WC_Uni ); } } + +//phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound, Squiz.Classes.ClassFileName.NoMatch, Suin.Classes.PSR4.IncorrectClassName +/** + * TestableEvaluateSuggestion class. + */ +class TestableEvaluateSuggestion extends EvaluateSuggestion { + /** + * Get the memo for testing. + * + * @return array + */ + public static function get_memo_for_tests() { + return self::$memo; + } +} +//phpcs:enable Generic.Files.OneObjectStructurePerFile.MultipleFound, Squiz.Classes.ClassFileName.NoMatch, Suin.Classes.PSR4.IncorrectClassName diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/payment-gateway-suggestions.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/payment-gateway-suggestions.php index 728c78ce286..bb2ef0f93f1 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/payment-gateway-suggestions.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/payment-gateway-suggestions.php @@ -25,7 +25,7 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_Init extends WC_Unit_Test_Case { delete_option( 'woocommerce_show_marketplace_suggestions' ); add_filter( 'transient_woocommerce_admin_' . PaymentGatewaySuggestionsDataSourcePoller::ID . '_specs', - function( $value ) { + function ( $value ) { if ( $value ) { return $value; } @@ -37,6 +37,8 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_Init extends WC_Unit_Test_Case { ); } ); + + EvaluateSuggestion::reset_memo(); } /** @@ -57,7 +59,7 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_Init extends WC_Unit_Test_Case { remove_all_filters( 'transient_woocommerce_admin_' . PaymentGatewaySuggestionsDataSourcePoller::ID . '_specs' ); add_filter( DataSourcePoller::FILTER_NAME, - function() { + function () { return array(); } ); @@ -242,7 +244,7 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_Init extends WC_Unit_Test_Case { add_filter( 'locale', - function( $_locale ) { + function () { return 'zh_TW'; } ); @@ -364,5 +366,4 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_Init extends WC_Unit_Test_Case { // Clean up. delete_option( PaymentGatewaySuggestions::RECOMMENDED_PAYMENT_PLUGINS_DISMISS_OPTION ); } - } diff --git a/plugins/woocommerce/tests/php/src/Admin/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php similarity index 97% rename from plugins/woocommerce/tests/php/src/Admin/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php rename to plugins/woocommerce/tests/php/src/Admin/Features/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php index ecea313caa7..44b15c965f6 100644 --- a/plugins/woocommerce/tests/php/src/Admin/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/Features/ShippingPartnerSuggestions/DefaultShippingPartnersTest.php @@ -1,7 +1,7 @@ mock_logger = $this->getMockBuilder( 'WC_Logger_Interface' )->getMock(); add_filter( 'woocommerce_logging_class', array( $this, 'override_wc_logger' ) ); + + EvaluateSuggestion::reset_memo(); } /** @@ -91,7 +94,7 @@ class ShippingPartnerSuggestionsTest extends WC_Unit_Test_Case { remove_all_filters( 'transient_woocommerce_admin_' . ShippingPartnerSuggestionsDataSourcePoller::ID . '_specs' ); add_filter( DataSourcePoller::FILTER_NAME, - function() { + function () { return array(); } ); diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/DefaultPromotionsTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/DefaultPromotionsTest.php index 893cc7a9753..29351fce779 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/DefaultPromotionsTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/DefaultPromotionsTest.php @@ -29,6 +29,8 @@ class DefaultPromotionsTest extends WC_Unit_Test_Case { update_option( 'woocommerce_store_address', 'foo' ); update_option( 'active_plugins', array( 'foo/foo.php' ) ); + + EvaluateSuggestion::reset_memo(); } /** diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/InitTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/InitTest.php index 3a51a919fa3..cfe7b606f69 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/InitTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Admin/WCPayPromotion/InitTest.php @@ -26,7 +26,7 @@ class InitTest extends WC_Unit_Test_Case { delete_option( 'woocommerce_show_marketplace_suggestions' ); add_filter( 'transient_woocommerce_admin_' . WCPayPromotionDataSourcePoller::ID . '_specs', - function( $value ) { + function ( $value ) { if ( $value ) { return $value; } @@ -38,6 +38,8 @@ class InitTest extends WC_Unit_Test_Case { ); } ); + + EvaluateSuggestion::reset_memo(); } /** @@ -59,7 +61,7 @@ class InitTest extends WC_Unit_Test_Case { remove_all_filters( 'transient_woocommerce_admin_' . WCPayPromotionDataSourcePoller::ID . '_specs' ); add_filter( DataSourcePoller::FILTER_NAME, - function() { + function () { return array(); } ); From e4f03589abdfa794454766b8eff1bded4498a546 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Wed, 11 Sep 2024 09:54:43 -0300 Subject: [PATCH 03/59] Deprecate single product block save (#51153) * Add deprecated save * Fix button styles * Add changelog * Add attributes to deprecated prop * Add supports prop to deprecated obj * Add code comment --- .../js/blocks/single-product/deprecated.tsx | 28 +++++++++++++++++++ .../assets/js/blocks/single-product/index.tsx | 2 ++ .../assets/js/blocks/single-product/save.tsx | 1 + .../fix-deprectated_single_product_block_save | 4 +++ .../client/legacy/css/woocommerce.scss | 5 +++- 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/single-product/deprecated.tsx create mode 100644 plugins/woocommerce/changelog/fix-deprectated_single_product_block_save diff --git a/plugins/woocommerce-blocks/assets/js/blocks/single-product/deprecated.tsx b/plugins/woocommerce-blocks/assets/js/blocks/single-product/deprecated.tsx new file mode 100644 index 00000000000..6b34e7e0532 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/single-product/deprecated.tsx @@ -0,0 +1,28 @@ +/** + * External dependencies + */ +import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; + +const v1 = { + attributes: metadata.attributes, + supports: metadata.supports, + save: () => { + const blockProps = useBlockProps.save(); + + return ( +
+ { /* @ts-expect-error: `InnerBlocks.Content` is a component that is typed in WordPress core*/ } + +
+ ); + }, +}; + +const deprecated = [ v1 ]; + +export default deprecated; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/single-product/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/single-product/index.tsx index 2aa3556604d..a2f9363ef2b 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/single-product/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/single-product/index.tsx @@ -10,10 +10,12 @@ import { BLOCK_ICON } from './constants'; import metadata from './block.json'; import edit from './edit'; import save from './save'; +import deprecated from './deprecated'; // @ts-expect-error: `registerBlockType` is a function that is typed in WordPress core. registerBlockType( metadata, { icon: BLOCK_ICON, edit, save, + deprecated, } ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/single-product/save.tsx b/plugins/woocommerce-blocks/assets/js/blocks/single-product/save.tsx index 4c8f4ef2973..d856b438b9a 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/single-product/save.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/single-product/save.tsx @@ -4,6 +4,7 @@ import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; const Save = () => { + // We add the `woocommerce` class to the wrapper to apply WooCommerce styles to the block. const blockProps = useBlockProps.save( { className: 'woocommerce', } ); diff --git a/plugins/woocommerce/changelog/fix-deprectated_single_product_block_save b/plugins/woocommerce/changelog/fix-deprectated_single_product_block_save new file mode 100644 index 00000000000..f6ce0f16e03 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-deprectated_single_product_block_save @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Deprecate single product block save #51153 diff --git a/plugins/woocommerce/client/legacy/css/woocommerce.scss b/plugins/woocommerce/client/legacy/css/woocommerce.scss index 8c97759bf97..46c0d0405e4 100644 --- a/plugins/woocommerce/client/legacy/css/woocommerce.scss +++ b/plugins/woocommerce/client/legacy/css/woocommerce.scss @@ -1699,7 +1699,10 @@ p.demo_store, * Buttons */ .woocommerce:where(body:not(.woocommerce-block-theme-has-button-styles)), -:where(body:not(.woocommerce-block-theme-has-button-styles)) .woocommerce { +:where(body:not(.woocommerce-block-theme-has-button-styles)):where( + :not(.edit-post-visual-editor *) + ) + .woocommerce { a.button, button.button, input.button, From 96497814e47adf7b623f4fc83a4a2ea2b66b8a7f Mon Sep 17 00:00:00 2001 From: Seghir Nadir Date: Wed, 11 Sep 2024 15:44:09 +0200 Subject: [PATCH 04/59] Revert update to createRoot in Checkout block. (#51289) * Revert "[React 18] Use `createRoot().render()` instead of `render()` (#48843)" This reverts commit 752273e6ce40db6e738800a815e99581d2be2736. * Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce --------- Co-authored-by: github-actions --- .../js/atomic/utils/render-parent-block.tsx | 5 +- .../cart-checkout/form/test/index.js | 13 +- .../assets/js/base/utils/render-frontend.tsx | 80 +--- .../js/blocks/attribute-filter/test/block.tsx | 29 +- .../checkout-pickup-options-block/block.tsx | 23 +- .../checkout-shipping-methods-block/block.tsx | 30 +- .../assets/js/blocks/mini-cart/block.tsx | 28 +- .../assets/js/blocks/mini-cart/test/block.js | 23 +- .../js/blocks/rating-filter/test/block.tsx | 404 ++++++++-------- .../js/blocks/stock-filter/test/block.tsx | 447 +++++++++--------- .../51289-revert-react-18-in-checkout-block | 4 + .../shopper/checkout-block-coupons.spec.js | 20 +- 12 files changed, 525 insertions(+), 581 deletions(-) create mode 100644 plugins/woocommerce/changelog/51289-revert-react-18-in-checkout-block diff --git a/plugins/woocommerce-blocks/assets/js/atomic/utils/render-parent-block.tsx b/plugins/woocommerce-blocks/assets/js/atomic/utils/render-parent-block.tsx index 24b6c1bd296..913371d0983 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/utils/render-parent-block.tsx +++ b/plugins/woocommerce-blocks/assets/js/atomic/utils/render-parent-block.tsx @@ -15,7 +15,6 @@ import { hasInnerBlocks, } from '@woocommerce/blocks-checkout'; import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary'; -import type { ReactRootWithContainer } from '@woocommerce/base-utils'; /** * This file contains logic used on the frontend to convert DOM elements (saved by the block editor) to React @@ -295,7 +294,7 @@ export const renderParentBlock = ( { selector: string; // Function to generate the props object for the block. getProps: ( el: Element, i: number ) => Record< string, unknown >; -} ): ReactRootWithContainer[] => { +} ): void => { /** * In addition to getProps, we need to render and return the children. This adds children to props. */ @@ -311,7 +310,7 @@ export const renderParentBlock = ( { /** * The only difference between using renderParentBlock and renderFrontend is that here we provide children. */ - return renderFrontend( { + renderFrontend( { Block, selector, getProps: getPropsWithChildren, diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/test/index.js b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/test/index.js index 9151c6e39e7..3b802cdb8dd 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/test/index.js +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/test/index.js @@ -21,11 +21,16 @@ jest.mock( '@wordpress/element', () => { }; } ); -const renderInCheckoutProvider = ( ui, options = {} ) => { +const renderInCheckoutProvider = ( ui, options = { legacyRoot: true } ) => { const Wrapper = ( { children } ) => { return { children }; }; const result = render( ui, { wrapper: Wrapper, ...options } ); + // We need to switch to React 17 rendering to allow these tests to keep passing, but as a result the React + // rendering error will be shown. + expect( console ).toHaveErroredWith( + `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot` + ); return result; }; @@ -124,7 +129,7 @@ describe( 'Form Component', () => { ); }; - test( 'updates context value when interacting with form elements', async () => { + it( 'updates context value when interacting with form elements', async () => { renderInCheckoutProvider( <> @@ -150,7 +155,7 @@ describe( 'Form Component', () => { ); } ); - test( 'input fields update when changing the country', async () => { + it( 'input fields update when changing the country', async () => { renderInCheckoutProvider( ); await act( async () => { @@ -177,7 +182,7 @@ describe( 'Form Component', () => { expect( screen.getByLabelText( /Postal code/ ) ).toBeInTheDocument(); } ); - test( 'input values are reset after changing the country', async () => { + it( 'input values are reset after changing the country', async () => { renderInCheckoutProvider( ); // First enter an address with no state, but fill the city. diff --git a/plugins/woocommerce-blocks/assets/js/base/utils/render-frontend.tsx b/plugins/woocommerce-blocks/assets/js/base/utils/render-frontend.tsx index 0a1739a5407..4867fd9815f 100644 --- a/plugins/woocommerce-blocks/assets/js/base/utils/render-frontend.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/utils/render-frontend.tsx @@ -1,9 +1,8 @@ /** * External dependencies */ -import { createRoot, useEffect, Suspense } from '@wordpress/element'; +import { render, Suspense } from '@wordpress/element'; import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary'; -import type { Root } from 'react-dom/client'; // Some blocks take care of rendering their inner blocks automatically. For // example, the empty cart. In those cases, we don't want to trigger the render @@ -28,11 +27,6 @@ export type GetPropsFn< TAttributes extends Record< string, unknown > > = ( el: HTMLElement, i: number ) => BlockProps< TProps, TAttributes >; -export type ReactRootWithContainer = { - container: HTMLElement; - root: Root; -}; - interface RenderBlockParams< TProps extends Record< string, unknown >, TAttributes extends Record< string, unknown > @@ -61,32 +55,20 @@ export const renderBlock = < attributes = {} as TAttributes, props = {} as BlockProps< TProps, TAttributes >, errorBoundaryProps = {}, -}: RenderBlockParams< TProps, TAttributes > ): Root => { - const BlockWrapper = () => { - useEffect( () => { +}: RenderBlockParams< TProps, TAttributes > ): void => { + render( + + }> + { Block && } + + , + container, + () => { if ( container.classList ) { container.classList.remove( 'is-loading' ); } - }, [] ); - - return ( - - Loading... - } - > - { Block && ( - - ) } - - - ); - }; - - const root = createRoot( container ); - root.render( ); - return root; + } + ); }; interface RenderBlockInContainersParams< @@ -117,14 +99,10 @@ const renderBlockInContainers = < containers, getProps = () => ( {} as BlockProps< TProps, TAttributes > ), getErrorBoundaryProps = () => ( {} ), -}: RenderBlockInContainersParams< - TProps, - TAttributes -> ): ReactRootWithContainer[] => { +}: RenderBlockInContainersParams< TProps, TAttributes > ): void => { if ( containers.length === 0 ) { - return []; + return; } - const roots: ReactRootWithContainer[] = []; // Use Array.forEach for IE11 compatibility. Array.prototype.forEach.call( containers, ( el, i ) => { @@ -136,19 +114,14 @@ const renderBlockInContainers = < ...( props.attributes || {} ), }; - roots.push( { + renderBlock( { + Block, container: el, - root: renderBlock( { - Block, - container: el, - props, - attributes, - errorBoundaryProps, - } ), + props, + attributes, + errorBoundaryProps, } ); } ); - - return roots; }; // Given an element and a list of wrappers, check if the element is inside at @@ -184,10 +157,7 @@ const renderBlockOutsideWrappers = < getErrorBoundaryProps, selector, wrappers, -}: RenderBlockOutsideWrappersParams< - TProps, - TAttributes -> ): ReactRootWithContainer[] => { +}: RenderBlockOutsideWrappersParams< TProps, TAttributes > ): void => { const containers = document.body.querySelectorAll( selector ); // Filter out blocks inside the wrappers. if ( wrappers && wrappers.length > 0 ) { @@ -195,8 +165,7 @@ const renderBlockOutsideWrappers = < return ! isElementInsideWrappers( el, wrappers ); } ); } - - return renderBlockInContainers( { + renderBlockInContainers( { Block, containers, getProps, @@ -265,21 +234,20 @@ export const renderFrontend = < props: | RenderBlockOutsideWrappersParams< TProps, TAttributes > | RenderBlockInsideWrapperParams< TProps, TAttributes > -): ReactRootWithContainer[] => { +): void => { const wrappersToSkipOnLoad = document.body.querySelectorAll( selectorsToSkipOnLoad.join( ',' ) ); const { Block, getProps, getErrorBoundaryProps, selector } = props; - const roots = renderBlockOutsideWrappers( { + renderBlockOutsideWrappers( { Block, getProps, getErrorBoundaryProps, selector, wrappers: wrappersToSkipOnLoad, } ); - // For each wrapper, add an event listener to render the inner blocks when // `wc-blocks_render_blocks_frontend` event is triggered. Array.prototype.forEach.call( wrappersToSkipOnLoad, ( wrapper ) => { @@ -287,8 +255,6 @@ export const renderFrontend = < renderBlockInsideWrapper( { ...props, wrapper } ); } ); } ); - - return roots; }; export default renderFrontend; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/attribute-filter/test/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/attribute-filter/test/block.tsx index 3b1b191e7ff..ecdc9b111ae 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/attribute-filter/test/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/attribute-filter/test/block.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { act, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import * as hooks from '@woocommerce/base-context/hooks'; import userEvent from '@testing-library/user-event'; @@ -106,8 +106,14 @@ const setup = ( params: SetupParams ) => { results: stubCollectionData(), isLoading: false, } ); - const utils = render( ); - + const utils = render( , { + legacyRoot: true, + } ); + // We need to switch to React 17 rendering to allow these tests to keep passing, but as a result the React + // rendering error will be shown. + expect( console ).toHaveErroredWith( + `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot` + ); const applyButton = screen.getByRole( 'button', { name: /apply/i } ); const smallAttributeCheckbox = screen.getByRole( 'checkbox', { name: /small/i, @@ -158,10 +164,8 @@ describe( 'Filter by Attribute block', () => { test( 'should enable Apply button when filter attributes are changed', async () => { const { applyButton, smallAttributeCheckbox } = setupWithoutSelectedFilterAttributes(); + await userEvent.click( smallAttributeCheckbox ); - await act( async () => { - await userEvent.click( smallAttributeCheckbox ); - } ); expect( applyButton ).not.toBeDisabled(); } ); } ); @@ -176,25 +180,18 @@ describe( 'Filter by Attribute block', () => { test( 'should enable Apply button when filter attributes are changed', async () => { const { applyButton, smallAttributeCheckbox } = setupWithSelectedFilterAttributes(); + await userEvent.click( smallAttributeCheckbox ); - await act( async () => { - await userEvent.click( smallAttributeCheckbox ); - } ); expect( applyButton ).not.toBeDisabled(); } ); test( 'should disable Apply button when deselecting the same previously selected attribute', async () => { const { applyButton, smallAttributeCheckbox } = setupWithSelectedFilterAttributes( { filterSize: 'small' } ); - - await act( async () => { - await userEvent.click( smallAttributeCheckbox ); - } ); + await userEvent.click( smallAttributeCheckbox ); expect( applyButton ).not.toBeDisabled(); - await act( async () => { - await userEvent.click( smallAttributeCheckbox ); - } ); + await userEvent.click( smallAttributeCheckbox ); expect( applyButton ).toBeDisabled(); } ); } ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-pickup-options-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-pickup-options-block/block.tsx index 9d6984b6984..916662798cf 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-pickup-options-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-pickup-options-block/block.tsx @@ -6,7 +6,6 @@ import { useState, useEffect, useCallback, - useMemo, createInterpolateElement, } from '@wordpress/element'; import { useShippingData, useStoreCart } from '@woocommerce/base-context/hooks'; @@ -139,12 +138,10 @@ const renderPickupLocation = ( const Block = (): JSX.Element | null => { const { shippingRates, selectShippingRate } = useShippingData(); - // Memoize pickup locations to prevent re-rendering when the shipping rates change. - const pickupLocations = useMemo( () => { - return ( shippingRates[ 0 ]?.shipping_rates || [] ).filter( - isPackageRateCollectable - ); - }, [ shippingRates ] ); + // Get pickup locations from the first shipping package. + const pickupLocations = ( shippingRates[ 0 ]?.shipping_rates || [] ).filter( + isPackageRateCollectable + ); const [ selectedOption, setSelectedOption ] = useState< string >( () => pickupLocations.find( ( rate ) => rate.selected )?.rate_id || '' @@ -171,19 +168,13 @@ const Block = (): JSX.Element | null => { renderPickupLocation, }; + // Update the selected option if there is no rate selected on mount. useEffect( () => { - if ( - ! selectedOption && - pickupLocations[ 0 ] && - selectedOption !== pickupLocations[ 0 ].rate_id - ) { + if ( ! selectedOption && pickupLocations[ 0 ] ) { setSelectedOption( pickupLocations[ 0 ].rate_id ); onSelectRate( pickupLocations[ 0 ].rate_id ); } - // Removing onSelectRate as it lead to an infinite loop when only one pickup location is available. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ pickupLocations, selectedOption ] ); - + }, [ onSelectRate, pickupLocations, selectedOption ] ); const packageCount = getShippingRatesPackageCount( shippingRates ); return ( <> diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx index 80362a4b009..13633478b0c 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx @@ -26,7 +26,6 @@ import type { } from '@woocommerce/types'; import NoticeBanner from '@woocommerce/base-components/notice-banner'; import type { ReactElement } from 'react'; -import { useMemo } from '@wordpress/element'; /** * Renders a shipping rate control option. @@ -74,22 +73,19 @@ const Block = ( { noShippingPlaceholder = null } ): ReactElement | null => { const { shippingAddress } = useCustomerData(); - const filteredShippingRates = useMemo( () => { - return isCollectable - ? shippingRates.map( ( shippingRatesPackage ) => { - return { - ...shippingRatesPackage, - shipping_rates: - shippingRatesPackage.shipping_rates.filter( - ( shippingRatesPackageRate ) => - ! hasCollectableRate( - shippingRatesPackageRate.method_id - ) - ), - }; - } ) - : shippingRates; - }, [ shippingRates, isCollectable ] ); + const filteredShippingRates = isCollectable + ? shippingRates.map( ( shippingRatesPackage ) => { + return { + ...shippingRatesPackage, + shipping_rates: shippingRatesPackage.shipping_rates.filter( + ( shippingRatesPackageRate ) => + ! hasCollectableRate( + shippingRatesPackageRate.method_id + ) + ), + }; + } ) + : shippingRates; if ( ! needsShipping ) { return null; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/block.tsx index 77b4097c39c..59e33ddcd74 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/block.tsx @@ -20,10 +20,15 @@ import { isCartResponseTotals, isNumber, } from '@woocommerce/types'; -import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; +import { + unmountComponentAtNode, + useCallback, + useEffect, + useRef, + useState, +} from '@wordpress/element'; import { sprintf, _n } from '@wordpress/i18n'; import clsx from 'clsx'; -import type { ReactRootWithContainer } from '@woocommerce/base-utils'; /** * Internal dependencies @@ -105,8 +110,6 @@ const MiniCartBlock = ( attributes: Props ): JSX.Element => { setContentsNode( node ); }, [] ); - const rootRef = useRef< ReactRootWithContainer[] | null >( null ); - useEffect( () => { const body = document.querySelector( 'body' ); if ( body ) { @@ -131,7 +134,7 @@ const MiniCartBlock = ( attributes: Props ): JSX.Element => { return; } if ( isOpen ) { - const renderedBlock = renderParentBlock( { + renderParentBlock( { Block: MiniCartContentsBlock, blockName, getProps: ( el: Element ) => { @@ -148,25 +151,16 @@ const MiniCartBlock = ( attributes: Props ): JSX.Element => { selector: '.wp-block-woocommerce-mini-cart-contents', blockMap: getRegisteredBlockComponents( blockName ), } ); - rootRef.current = renderedBlock; } } return () => { if ( contentsNode instanceof Element && isOpen ) { - const unmountingContainer = contentsNode.querySelector( + const container = contentsNode.querySelector( '.wp-block-woocommerce-mini-cart-contents' ); - - if ( unmountingContainer ) { - const foundRoot = rootRef?.current?.find( - ( { container } ) => unmountingContainer === container - ); - if ( typeof foundRoot?.root?.unmount === 'function' ) { - setTimeout( () => { - foundRoot.root.unmount(); - } ); - } + if ( container ) { + unmountComponentAtNode( container ); } } }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/test/block.js b/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/test/block.js index 94a9644b75b..313a7c3f3af 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/test/block.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/test/block.js @@ -111,6 +111,13 @@ describe( 'Testing Mini-Cart', () => { await waitFor( () => expect( screen.getByText( /your cart/i ) ).toBeInTheDocument() ); + + // The opening of the drawer uses deprecated ReactDOM.render. + expect( console ).toHaveErroredWith( + `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot%s`, + // The stack trace + expect.any( String ) + ); } ); it( 'closes the drawer when clicking on the close button', async () => { @@ -125,11 +132,9 @@ describe( 'Testing Mini-Cart', () => { // Close drawer. let closeButton = null; - await waitFor( () => { closeButton = screen.getByLabelText( /close/i ); } ); - if ( closeButton ) { await act( async () => { await user.click( closeButton ); @@ -141,6 +146,13 @@ describe( 'Testing Mini-Cart', () => { screen.queryByText( /your cart/i ) ).not.toBeInTheDocument(); } ); + + // The opening of the drawer uses deprecated ReactDOM.render. + expect( console ).toHaveErroredWith( + `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot%s`, + // The stack trace + expect.any( String ) + ); } ); it( 'renders empty cart if there are no items in the cart', async () => { @@ -155,6 +167,13 @@ describe( 'Testing Mini-Cart', () => { } ); expect( fetchMock ).toHaveBeenCalledTimes( 1 ); + + // The opening of the drawer uses deprecated ReactDOM.render. + expect( console ).toHaveErroredWith( + `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot%s`, + // The stack trace + expect.any( String ) + ); } ); it( 'updates contents when removed from cart event is triggered', async () => { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/rating-filter/test/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/rating-filter/test/block.tsx index 2b1483dd00f..d442d9bc001 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/rating-filter/test/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/rating-filter/test/block.tsx @@ -2,14 +2,7 @@ * External dependencies */ import React from '@wordpress/element'; -import { - act, - cleanup, - render, - screen, - waitFor, - within, -} from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; import * as hooks from '@woocommerce/base-context/hooks'; import userEvent from '@testing-library/user-event'; @@ -66,7 +59,6 @@ const selectors = { }; const setup = ( params: SetupParams ) => { - cleanup(); const url = `http://woo.local/${ params.filterRating ? '?rating_filter=' + params.filterRating : '' }`; @@ -86,7 +78,14 @@ const setup = ( params: SetupParams ) => { } ); const { container, ...utils } = render( - + , + { legacyRoot: true } + ); + + // We need to switch to React 17 rendering to allow these tests to keep passing, but as a result the React + // rendering error will be shown. + expect( console ).toHaveErroredWith( + `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot` ); const getList = () => container.querySelector( selectors.list ); @@ -204,81 +203,74 @@ describe( 'Filter by Rating block', () => { describe( 'Single choice Dropdown', () => { test( 'renders dropdown', () => { const { getDropdown, getList } = setupSingleChoiceDropdown(); - expect( getDropdown() ).toBeInTheDocument(); expect( getList() ).toBeNull(); } ); - test( 'renders chips based on URL params', async () => { - await waitFor( async () => { - const ratingParam = '2'; - const { getRating2Chips, getRating4Chips, getRating5Chips } = - setupSingleChoiceDropdown( ratingParam ); + test( 'renders chips based on URL params', () => { + const ratingParam = '2'; + const { getRating2Chips, getRating4Chips, getRating5Chips } = + setupSingleChoiceDropdown( ratingParam ); - expect( getRating2Chips() ).toBeInTheDocument(); - expect( getRating4Chips() ).toBeNull(); - expect( getRating5Chips() ).toBeNull(); - } ); + expect( getRating2Chips() ).toBeInTheDocument(); + expect( getRating4Chips() ).toBeNull(); + expect( getRating5Chips() ).toBeNull(); } ); test( 'replaces chosen option when another one is clicked', async () => { - await waitFor( async () => { - const ratingParam = '2'; - const { - getDropdown, - getRating2Chips, - getRating4Chips, - getRating4Suggestion, - } = setupSingleChoiceDropdown( ratingParam ); + const ratingParam = '2'; + const { + getDropdown, + getRating2Chips, + getRating4Chips, + getRating4Suggestion, + } = setupSingleChoiceDropdown( ratingParam ); - expect( getRating2Chips() ).toBeInTheDocument(); - expect( getRating4Chips() ).toBeNull(); + expect( getRating2Chips() ).toBeInTheDocument(); + expect( getRating4Chips() ).toBeNull(); - const dropdown = getDropdown(); + const dropdown = getDropdown(); - if ( dropdown ) { - await userEvent.click( dropdown ); - acceptErrorWithDuplicatedKeys(); - } + if ( dropdown ) { + await userEvent.click( dropdown ); + acceptErrorWithDuplicatedKeys(); + } - const rating4Suggestion = getRating4Suggestion(); + const rating4Suggestion = getRating4Suggestion(); - if ( rating4Suggestion ) { - await userEvent.click( rating4Suggestion ); - } + if ( rating4Suggestion ) { + await userEvent.click( rating4Suggestion ); + } - expect( getRating2Chips() ).toBeNull(); - expect( getRating4Chips() ).toBeInTheDocument(); - } ); + expect( getRating2Chips() ).toBeNull(); + expect( getRating4Chips() ).toBeInTheDocument(); } ); test( 'removes the option when the X button is clicked', async () => { - await waitFor( async () => { - const ratingParam = '4'; - const { - getRating2Chips, - getRating4Chips, - getRating5Chips, - getRemoveButtonFromChips, - } = setupMultipleChoiceDropdown( ratingParam ); + const ratingParam = '4'; + const { + getRating2Chips, + getRating4Chips, + getRating5Chips, + getRemoveButtonFromChips, + } = setupMultipleChoiceDropdown( ratingParam ); - expect( getRating2Chips() ).toBeNull(); - expect( getRating4Chips() ).toBeInTheDocument(); - expect( getRating5Chips() ).toBeNull(); + expect( getRating2Chips() ).toBeNull(); + expect( getRating4Chips() ).toBeInTheDocument(); + expect( getRating5Chips() ).toBeNull(); - const removeRating4Button = getRemoveButtonFromChips( - getRating4Chips() - ); + const removeRating4Button = getRemoveButtonFromChips( + getRating4Chips() + ); - if ( removeRating4Button ) { - await userEvent.click( removeRating4Button ); - acceptErrorWithDuplicatedKeys(); - } + if ( removeRating4Button ) { + await userEvent.click( removeRating4Button ); + acceptErrorWithDuplicatedKeys(); + } - expect( getRating2Chips() ).toBeNull(); - expect( getRating4Chips() ).toBeNull(); - expect( getRating5Chips() ).toBeNull(); - } ); + expect( getRating2Chips() ).toBeNull(); + expect( getRating4Chips() ).toBeNull(); + expect( getRating5Chips() ).toBeNull(); } ); } ); @@ -289,89 +281,83 @@ describe( 'Filter by Rating block', () => { expect( getList() ).toBeNull(); } ); - test( 'renders chips based on URL params', async () => { - await waitFor( async () => { - const ratingParam = '2,4'; - const { getRating2Chips, getRating4Chips, getRating5Chips } = - setupMultipleChoiceDropdown( ratingParam ); + test( 'renders chips based on URL params', () => { + const ratingParam = '2,4'; + const { getRating2Chips, getRating4Chips, getRating5Chips } = + setupMultipleChoiceDropdown( ratingParam ); - expect( getRating2Chips() ).toBeInTheDocument(); - expect( getRating4Chips() ).toBeInTheDocument(); - expect( getRating5Chips() ).toBeNull(); - } ); + expect( getRating2Chips() ).toBeInTheDocument(); + expect( getRating4Chips() ).toBeInTheDocument(); + expect( getRating5Chips() ).toBeNull(); } ); test( 'adds chosen option to another one that is clicked', async () => { - await waitFor( async () => { - const ratingParam = '2'; - const { - getDropdown, - getRating2Chips, - getRating4Chips, - getRating5Chips, - getRating4Suggestion, - getRating5Suggestion, - } = setupMultipleChoiceDropdown( ratingParam ); + const ratingParam = '2'; + const { + getDropdown, + getRating2Chips, + getRating4Chips, + getRating5Chips, + getRating4Suggestion, + getRating5Suggestion, + } = setupMultipleChoiceDropdown( ratingParam ); - expect( getRating2Chips() ).toBeInTheDocument(); - expect( getRating4Chips() ).toBeNull(); - expect( getRating5Chips() ).toBeNull(); + expect( getRating2Chips() ).toBeInTheDocument(); + expect( getRating4Chips() ).toBeNull(); + expect( getRating5Chips() ).toBeNull(); - const dropdown = getDropdown(); + const dropdown = getDropdown(); - if ( dropdown ) { - await userEvent.click( dropdown ); - acceptErrorWithDuplicatedKeys(); - } + if ( dropdown ) { + await userEvent.click( dropdown ); + acceptErrorWithDuplicatedKeys(); + } - const rating4Suggestion = getRating4Suggestion(); + const rating4Suggestion = getRating4Suggestion(); - if ( rating4Suggestion ) { - await userEvent.click( rating4Suggestion ); - } + if ( rating4Suggestion ) { + await userEvent.click( rating4Suggestion ); + } - expect( getRating2Chips() ).toBeInTheDocument(); - expect( getRating4Chips() ).toBeInTheDocument(); - expect( getRating5Chips() ).toBeNull(); + expect( getRating2Chips() ).toBeInTheDocument(); + expect( getRating4Chips() ).toBeInTheDocument(); + expect( getRating5Chips() ).toBeNull(); - const rating5Suggestion = getRating5Suggestion(); + const rating5Suggestion = getRating5Suggestion(); - if ( rating5Suggestion ) { - await userEvent.click( rating5Suggestion ); - } + if ( rating5Suggestion ) { + await userEvent.click( rating5Suggestion ); + } - expect( getRating2Chips() ).toBeInTheDocument(); - expect( getRating4Chips() ).toBeInTheDocument(); - expect( getRating5Chips() ).toBeInTheDocument(); - } ); + expect( getRating2Chips() ).toBeInTheDocument(); + expect( getRating4Chips() ).toBeInTheDocument(); + expect( getRating5Chips() ).toBeInTheDocument(); } ); test( 'removes the option when the X button is clicked', async () => { - await waitFor( async () => { - const ratingParam = '2,4,5'; - const { - getRating2Chips, - getRating4Chips, - getRating5Chips, - getRemoveButtonFromChips, - } = setupMultipleChoiceDropdown( ratingParam ); + const ratingParam = '2,4,5'; + const { + getRating2Chips, + getRating4Chips, + getRating5Chips, + getRemoveButtonFromChips, + } = setupMultipleChoiceDropdown( ratingParam ); - expect( getRating2Chips() ).toBeInTheDocument(); - expect( getRating4Chips() ).toBeInTheDocument(); - expect( getRating5Chips() ).toBeInTheDocument(); + expect( getRating2Chips() ).toBeInTheDocument(); + expect( getRating4Chips() ).toBeInTheDocument(); + expect( getRating5Chips() ).toBeInTheDocument(); - const removeRating4Button = getRemoveButtonFromChips( - getRating4Chips() - ); + const removeRating4Button = getRemoveButtonFromChips( + getRating4Chips() + ); - if ( removeRating4Button ) { - await userEvent.click( removeRating4Button ); - } + if ( removeRating4Button ) { + await userEvent.click( removeRating4Button ); + } - expect( getRating2Chips() ).toBeInTheDocument(); - expect( getRating4Chips() ).toBeNull(); - expect( getRating5Chips() ).toBeInTheDocument(); - } ); + expect( getRating2Chips() ).toBeInTheDocument(); + expect( getRating4Chips() ).toBeNull(); + expect( getRating5Chips() ).toBeInTheDocument(); } ); } ); @@ -382,67 +368,61 @@ describe( 'Filter by Rating block', () => { expect( getList() ).toBeInTheDocument(); } ); - test( 'renders checked options based on URL params', async () => { - await waitFor( async () => { - const ratingParam = '4'; - const { - getRating2Checkbox, - getRating4Checkbox, - getRating5Checkbox, - } = setupSingleChoiceList( ratingParam ); + test( 'renders checked options based on URL params', () => { + const ratingParam = '4'; + const { + getRating2Checkbox, + getRating4Checkbox, + getRating5Checkbox, + } = setupSingleChoiceList( ratingParam ); - expect( getRating2Checkbox()?.checked ).toBeFalsy(); - expect( getRating4Checkbox()?.checked ).toBeTruthy(); - expect( getRating5Checkbox()?.checked ).toBeFalsy(); - } ); + expect( getRating2Checkbox()?.checked ).toBeFalsy(); + expect( getRating4Checkbox()?.checked ).toBeTruthy(); + expect( getRating5Checkbox()?.checked ).toBeFalsy(); } ); test( 'replaces chosen option when another one is clicked', async () => { - await waitFor( async () => { - const ratingParam = '2'; - const { - getRating2Checkbox, - getRating4Checkbox, - getRating5Checkbox, - } = setupSingleChoiceList( ratingParam ); + const ratingParam = '2'; + const { + getRating2Checkbox, + getRating4Checkbox, + getRating5Checkbox, + } = setupSingleChoiceList( ratingParam ); - expect( getRating2Checkbox()?.checked ).toBeTruthy(); - expect( getRating4Checkbox()?.checked ).toBeFalsy(); - expect( getRating5Checkbox()?.checked ).toBeFalsy(); + expect( getRating2Checkbox()?.checked ).toBeTruthy(); + expect( getRating4Checkbox()?.checked ).toBeFalsy(); + expect( getRating5Checkbox()?.checked ).toBeFalsy(); - const rating4checkbox = getRating4Checkbox(); + const rating4checkbox = getRating4Checkbox(); - if ( rating4checkbox ) { - await act( async () => { - await userEvent.click( rating4checkbox ); - } ); - } + if ( rating4checkbox ) { + await userEvent.click( rating4checkbox ); + } - expect( getRating2Checkbox()?.checked ).toBeFalsy(); - expect( getRating4Checkbox()?.checked ).toBeTruthy(); - expect( getRating5Checkbox()?.checked ).toBeFalsy(); - } ); + expect( getRating2Checkbox()?.checked ).toBeFalsy(); + expect( getRating4Checkbox()?.checked ).toBeTruthy(); + expect( getRating5Checkbox()?.checked ).toBeFalsy(); } ); test( 'removes the option when it is clicked again', async () => { - await waitFor( async () => { - const ratingParam = '4'; - const { - getRating2Checkbox, - getRating4Checkbox, - getRating5Checkbox, - } = setupMultipleChoiceList( ratingParam ); + const ratingParam = '4'; + const { + getRating2Checkbox, + getRating4Checkbox, + getRating5Checkbox, + } = setupMultipleChoiceList( ratingParam ); - expect( getRating2Checkbox()?.checked ).toBeFalsy(); - expect( getRating4Checkbox()?.checked ).toBeTruthy(); - expect( getRating5Checkbox()?.checked ).toBeFalsy(); + expect( getRating2Checkbox()?.checked ).toBeFalsy(); + expect( getRating4Checkbox()?.checked ).toBeTruthy(); + expect( getRating5Checkbox()?.checked ).toBeFalsy(); - const rating4checkbox = getRating4Checkbox(); + const rating4checkbox = getRating4Checkbox(); - if ( rating4checkbox ) { - await userEvent.click( rating4checkbox ); - } + if ( rating4checkbox ) { + await userEvent.click( rating4checkbox ); + } + await waitFor( () => { expect( getRating2Checkbox()?.checked ).toBeFalsy(); expect( getRating4Checkbox()?.checked ).toBeFalsy(); expect( getRating5Checkbox()?.checked ).toBeFalsy(); @@ -457,40 +437,38 @@ describe( 'Filter by Rating block', () => { expect( getList() ).toBeInTheDocument(); } ); - test( 'renders chips based on URL params', async () => { - await waitFor( async () => { - const ratingParam = '4,5'; - const { - getRating2Checkbox, - getRating4Checkbox, - getRating5Checkbox, - } = setupMultipleChoiceList( ratingParam ); + test( 'renders chips based on URL params', () => { + const ratingParam = '4,5'; + const { + getRating2Checkbox, + getRating4Checkbox, + getRating5Checkbox, + } = setupMultipleChoiceList( ratingParam ); - expect( getRating2Checkbox()?.checked ).toBeFalsy(); - expect( getRating4Checkbox()?.checked ).toBeTruthy(); - expect( getRating5Checkbox()?.checked ).toBeTruthy(); - } ); + expect( getRating2Checkbox()?.checked ).toBeFalsy(); + expect( getRating4Checkbox()?.checked ).toBeTruthy(); + expect( getRating5Checkbox()?.checked ).toBeTruthy(); } ); test( 'adds chosen option to another one that is clicked', async () => { - await waitFor( async () => { - const ratingParam = '2,4'; - const { - getRating2Checkbox, - getRating4Checkbox, - getRating5Checkbox, - } = setupMultipleChoiceList( ratingParam ); + const ratingParam = '2,4'; + const { + getRating2Checkbox, + getRating4Checkbox, + getRating5Checkbox, + } = setupMultipleChoiceList( ratingParam ); - expect( getRating2Checkbox()?.checked ).toBeTruthy(); - expect( getRating4Checkbox()?.checked ).toBeTruthy(); - expect( getRating5Checkbox()?.checked ).toBeFalsy(); + expect( getRating2Checkbox()?.checked ).toBeTruthy(); + expect( getRating4Checkbox()?.checked ).toBeTruthy(); + expect( getRating5Checkbox()?.checked ).toBeFalsy(); - const rating5checkbox = getRating5Checkbox(); + const rating5checkbox = getRating5Checkbox(); - if ( rating5checkbox ) { - await userEvent.click( rating5checkbox ); - } + if ( rating5checkbox ) { + await userEvent.click( rating5checkbox ); + } + await waitFor( () => { expect( getRating2Checkbox()?.checked ).toBeTruthy(); expect( getRating4Checkbox()?.checked ).toBeTruthy(); expect( getRating5Checkbox()?.checked ).toBeTruthy(); @@ -498,24 +476,24 @@ describe( 'Filter by Rating block', () => { } ); test( 'removes the option when it is clicked again', async () => { - await waitFor( async () => { - const ratingParam = '2,4'; - const { - getRating2Checkbox, - getRating4Checkbox, - getRating5Checkbox, - } = setupMultipleChoiceList( ratingParam ); + const ratingParam = '2,4'; + const { + getRating2Checkbox, + getRating4Checkbox, + getRating5Checkbox, + } = setupMultipleChoiceList( ratingParam ); - expect( getRating2Checkbox()?.checked ).toBeTruthy(); - expect( getRating4Checkbox()?.checked ).toBeTruthy(); - expect( getRating5Checkbox()?.checked ).toBeFalsy(); + expect( getRating2Checkbox()?.checked ).toBeTruthy(); + expect( getRating4Checkbox()?.checked ).toBeTruthy(); + expect( getRating5Checkbox()?.checked ).toBeFalsy(); - const rating2checkbox = getRating2Checkbox(); + const rating2checkbox = getRating2Checkbox(); - if ( rating2checkbox ) { - await userEvent.click( rating2checkbox ); - } + if ( rating2checkbox ) { + await userEvent.click( rating2checkbox ); + } + await waitFor( () => { expect( getRating2Checkbox()?.checked ).toBeFalsy(); expect( getRating4Checkbox()?.checked ).toBeTruthy(); expect( getRating5Checkbox()?.checked ).toBeFalsy(); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/stock-filter/test/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/stock-filter/test/block.tsx index e5f52059a5a..bc1b1c59c58 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/stock-filter/test/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/stock-filter/test/block.tsx @@ -2,14 +2,7 @@ * External dependencies */ import React from '@wordpress/element'; -import { - act, - cleanup, - render, - screen, - within, - waitFor, -} from '@testing-library/react'; +import { act, render, screen, within, waitFor } from '@testing-library/react'; import { default as fetchMock } from 'jest-fetch-mock'; import userEvent from '@testing-library/user-event'; @@ -75,7 +68,6 @@ const selectors = { }; const setup = ( params: SetupParams = {} ) => { - cleanup(); const url = `http://woo.local/${ params.filterStock ? '?filter_stock_status=' + params.filterStock : '' }`; @@ -95,7 +87,14 @@ const setup = ( params: SetupParams = {} ) => { }; const { container, ...utils } = render( - + , + { legacyRoot: true } + ); + + // We need to switch to React 17 rendering to allow these tests to keep passing, but as a result the React + // rendering error will be shown. + expect( console ).toHaveErroredWith( + `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot` ); const getList = () => container.querySelector( selectors.list ); @@ -228,7 +227,7 @@ describe( 'Filter by Stock block', () => { fetchMock.resetMocks(); } ); - test( 'renders the stock filter block', async () => { + it( 'renders the stock filter block', async () => { const { container } = setup( { showFilterButton: false, showCounts: false, @@ -236,7 +235,7 @@ describe( 'Filter by Stock block', () => { expect( container ).toMatchSnapshot(); } ); - test( 'renders the stock filter block with the filter button', async () => { + it( 'renders the stock filter block with the filter button', async () => { const { container } = setup( { showFilterButton: true, showCounts: false, @@ -244,7 +243,7 @@ describe( 'Filter by Stock block', () => { expect( container ).toMatchSnapshot(); } ); - test( 'renders the stock filter block with the product counts', async () => { + it( 'renders the stock filter block with the product counts', async () => { const { container } = setup( { showFilterButton: false, showCounts: true, @@ -255,86 +254,80 @@ describe( 'Filter by Stock block', () => { describe( 'Single choice Dropdown', () => { test( 'renders dropdown', () => { const { getDropdown, getList } = setupSingleChoiceDropdown(); - expect( getDropdown() ).toBeInTheDocument(); expect( getList() ).toBeNull(); } ); - test( 'renders chips based on URL params', async () => { - await waitFor( async () => { - const ratingParam = 'instock'; - const { - getInStockChips, - getOutOfStockChips, - getOnBackorderChips, - } = setupSingleChoiceDropdown( ratingParam ); + test( 'renders chips based on URL params', () => { + const ratingParam = 'instock'; + const { getInStockChips, getOutOfStockChips, getOnBackorderChips } = + setupSingleChoiceDropdown( ratingParam ); - expect( getInStockChips() ).toBeInTheDocument(); - expect( getOutOfStockChips() ).toBeNull(); - expect( getOnBackorderChips() ).toBeNull(); - } ); + expect( getInStockChips() ).toBeInTheDocument(); + expect( getOutOfStockChips() ).toBeNull(); + expect( getOnBackorderChips() ).toBeNull(); } ); test( 'replaces chosen option when another one is clicked', async () => { - await waitFor( async () => { - const user = userEvent.setup(); - const ratingParam = 'instock'; - const { - getDropdown, - getInStockChips, - getOutOfStockChips, - getOutOfStockSuggestion, - } = setupSingleChoiceDropdown( ratingParam ); + const user = userEvent.setup(); + const ratingParam = 'instock'; + const { + getDropdown, + getInStockChips, + getOutOfStockChips, + getOutOfStockSuggestion, + } = setupSingleChoiceDropdown( ratingParam ); - expect( getInStockChips() ).toBeInTheDocument(); - expect( getOutOfStockChips() ).toBeNull(); + expect( getInStockChips() ).toBeInTheDocument(); + expect( getOutOfStockChips() ).toBeNull(); - const dropdown = getDropdown(); + const dropdown = getDropdown(); - if ( dropdown ) { - await act( async () => { - await user.click( dropdown ); - } ); - } + if ( dropdown ) { + await act( async () => { + await user.click( dropdown ); + } ); + } - const outOfStockSuggestion = getOutOfStockSuggestion(); + const outOfStockSuggestion = getOutOfStockSuggestion(); - if ( outOfStockSuggestion ) { - await act( async () => { - await user.click( outOfStockSuggestion ); - } ); - } + if ( outOfStockSuggestion ) { + await act( async () => { + await user.click( outOfStockSuggestion ); + } ); + } - expect( getInStockChips() ).toBeNull(); - expect( getOutOfStockChips() ).toBeInTheDocument(); - } ); + expect( getInStockChips() ).toBeNull(); + expect( getOutOfStockChips() ).toBeInTheDocument(); } ); test( 'removes the option when the X button is clicked', async () => { - await waitFor( async () => { - const user = userEvent.setup(); - const ratingParam = 'outofstock'; - const { - getInStockChips, - getOutOfStockChips, - getOnBackorderChips, - getRemoveButtonFromChips, - } = setupMultipleChoiceDropdown( ratingParam ); + const user = userEvent.setup(); + const ratingParam = 'outofstock'; + const { + getInStockChips, + getOutOfStockChips, + getOnBackorderChips, + getRemoveButtonFromChips, + } = setupMultipleChoiceDropdown( ratingParam ); + await waitFor( () => { expect( getInStockChips() ).toBeNull(); expect( getOutOfStockChips() ).toBeInTheDocument(); expect( getOnBackorderChips() ).toBeNull(); + } ); - const removeOutOfStockButton = getRemoveButtonFromChips( - getOutOfStockChips() - ); + const removeOutOfStockButton = getRemoveButtonFromChips( + getOutOfStockChips() + ); - if ( removeOutOfStockButton ) { - await act( async () => { - await user.click( removeOutOfStockButton ); - } ); - } + if ( removeOutOfStockButton ) { + act( async () => { + await user.click( removeOutOfStockButton ); + } ); + } + await waitFor( () => { expect( getInStockChips() ).toBeNull(); expect( getOutOfStockChips() ).toBeNull(); expect( getOnBackorderChips() ).toBeNull(); @@ -349,65 +342,67 @@ describe( 'Filter by Stock block', () => { expect( getList() ).toBeNull(); } ); - test( 'renders chips based on URL params', async () => { - await waitFor( async () => { - const ratingParam = 'instock,onbackorder'; - const { - getInStockChips, - getOutOfStockChips, - getOnBackorderChips, - } = setupMultipleChoiceDropdown( ratingParam ); + test( 'renders chips based on URL params', () => { + const ratingParam = 'instock,onbackorder'; + const { getInStockChips, getOutOfStockChips, getOnBackorderChips } = + setupMultipleChoiceDropdown( ratingParam ); - expect( getInStockChips() ).toBeInTheDocument(); - expect( getOutOfStockChips() ).toBeNull(); - expect( getOnBackorderChips() ).toBeInTheDocument(); - } ); + expect( getInStockChips() ).toBeInTheDocument(); + expect( getOutOfStockChips() ).toBeNull(); + expect( getOnBackorderChips() ).toBeInTheDocument(); } ); test( 'adds chosen option to another one that is clicked', async () => { - await waitFor( async () => { - const user = userEvent.setup(); - const ratingParam = 'onbackorder'; - const { - getDropdown, - getInStockChips, - getOutOfStockChips, - getOnBackorderChips, - getInStockSuggestion, - getOutOfStockSuggestion, - } = setupMultipleChoiceDropdown( ratingParam ); + const user = userEvent.setup(); + const ratingParam = 'onbackorder'; + const { + getDropdown, + getInStockChips, + getOutOfStockChips, + getOnBackorderChips, + getInStockSuggestion, + getOutOfStockSuggestion, + } = setupMultipleChoiceDropdown( ratingParam ); + await waitFor( () => { expect( getInStockChips() ).toBeNull(); expect( getOutOfStockChips() ).toBeNull(); expect( getOnBackorderChips() ).toBeInTheDocument(); + } ); + const dropdown = getDropdown(); - const dropdown = getDropdown(); - - if ( dropdown ) { + if ( dropdown ) { + await act( async () => { await user.click( dropdown ); - } + } ); + } - const inStockSuggestion = getInStockSuggestion(); + const inStockSuggestion = getInStockSuggestion(); - if ( inStockSuggestion ) { + if ( inStockSuggestion ) { + await act( async () => { await user.click( inStockSuggestion ); - } + } ); + } - expect( getInStockChips() ).toBeInTheDocument(); - expect( getOutOfStockChips() ).toBeNull(); - expect( getOnBackorderChips() ).toBeInTheDocument(); + expect( getInStockChips() ).toBeInTheDocument(); + expect( getOutOfStockChips() ).toBeNull(); + expect( getOnBackorderChips() ).toBeInTheDocument(); - const freshDropdown = getDropdown(); - if ( freshDropdown ) { + const freshDropdown = getDropdown(); + if ( freshDropdown ) { + await act( async () => { await user.click( freshDropdown ); - } + } ); + } - const outOfStockSuggestion = getOutOfStockSuggestion(); + const outOfStockSuggestion = getOutOfStockSuggestion(); - if ( outOfStockSuggestion ) { - await userEvent.click( outOfStockSuggestion ); - } + if ( outOfStockSuggestion ) { + userEvent.click( outOfStockSuggestion ); + } + await waitFor( () => { expect( getInStockChips() ).toBeInTheDocument(); expect( getOutOfStockChips() ).toBeInTheDocument(); expect( getOnBackorderChips() ).toBeInTheDocument(); @@ -415,30 +410,32 @@ describe( 'Filter by Stock block', () => { } ); test( 'removes the option when the X button is clicked', async () => { - await waitFor( async () => { - const user = userEvent.setup(); - const ratingParam = 'instock,outofstock,onbackorder'; - const { - getInStockChips, - getOutOfStockChips, - getOnBackorderChips, - getRemoveButtonFromChips, - } = setupMultipleChoiceDropdown( ratingParam ); + const user = userEvent.setup(); + const ratingParam = 'instock,outofstock,onbackorder'; + const { + getInStockChips, + getOutOfStockChips, + getOnBackorderChips, + getRemoveButtonFromChips, + } = setupMultipleChoiceDropdown( ratingParam ); + await waitFor( () => { expect( getInStockChips() ).toBeInTheDocument(); expect( getOutOfStockChips() ).toBeInTheDocument(); expect( getOnBackorderChips() ).toBeInTheDocument(); + } ); - const removeOutOfStockButton = getRemoveButtonFromChips( - getOutOfStockChips() - ); + const removeOutOfStockButton = getRemoveButtonFromChips( + getOutOfStockChips() + ); - if ( removeOutOfStockButton ) { - await act( async () => { - await user.click( removeOutOfStockButton ); - } ); - } + if ( removeOutOfStockButton ) { + act( async () => { + await user.click( removeOutOfStockButton ); + } ); + } + await waitFor( () => { expect( getInStockChips() ).toBeInTheDocument(); expect( getOutOfStockChips() ).toBeNull(); expect( getOnBackorderChips() ).toBeInTheDocument(); @@ -453,73 +450,67 @@ describe( 'Filter by Stock block', () => { expect( getList() ).toBeInTheDocument(); } ); - test( 'renders checked options based on URL params', async () => { - await waitFor( async () => { - const ratingParam = 'instock'; - const { - getInStockCheckbox, - getOutOfStockCheckbox, - getOnBackorderCheckbox, - } = setupSingleChoiceList( ratingParam ); + test( 'renders checked options based on URL params', () => { + const ratingParam = 'instock'; + const { + getInStockCheckbox, + getOutOfStockCheckbox, + getOnBackorderCheckbox, + } = setupSingleChoiceList( ratingParam ); - expect( getInStockCheckbox()?.checked ).toBeTruthy(); - expect( getOutOfStockCheckbox()?.checked ).toBeFalsy(); - expect( getOnBackorderCheckbox()?.checked ).toBeFalsy(); - } ); + expect( getInStockCheckbox()?.checked ).toBeTruthy(); + expect( getOutOfStockCheckbox()?.checked ).toBeFalsy(); + expect( getOnBackorderCheckbox()?.checked ).toBeFalsy(); } ); test( 'replaces chosen option when another one is clicked', async () => { - await waitFor( async () => { - const user = userEvent.setup(); - const ratingParam = 'outofstock'; - const { - getInStockCheckbox, - getOutOfStockCheckbox, - getOnBackorderCheckbox, - } = setupSingleChoiceList( ratingParam ); + const user = userEvent.setup(); + const ratingParam = 'outofstock'; + const { + getInStockCheckbox, + getOutOfStockCheckbox, + getOnBackorderCheckbox, + } = setupSingleChoiceList( ratingParam ); - expect( getInStockCheckbox()?.checked ).toBeFalsy(); - expect( getOutOfStockCheckbox()?.checked ).toBeTruthy(); - expect( getOnBackorderCheckbox()?.checked ).toBeFalsy(); + expect( getInStockCheckbox()?.checked ).toBeFalsy(); + expect( getOutOfStockCheckbox()?.checked ).toBeTruthy(); + expect( getOnBackorderCheckbox()?.checked ).toBeFalsy(); - const onBackorderCheckbox = getOnBackorderCheckbox(); + const onBackorderCheckbox = getOnBackorderCheckbox(); - if ( onBackorderCheckbox ) { - await act( async () => { - await user.click( onBackorderCheckbox ); - } ); - } + if ( onBackorderCheckbox ) { + await act( async () => { + await user.click( onBackorderCheckbox ); + } ); + } - expect( getInStockCheckbox()?.checked ).toBeFalsy(); - expect( getOutOfStockCheckbox()?.checked ).toBeFalsy(); - expect( getOnBackorderCheckbox()?.checked ).toBeTruthy(); - } ); + expect( getInStockCheckbox()?.checked ).toBeFalsy(); + expect( getOutOfStockCheckbox()?.checked ).toBeFalsy(); + expect( getOnBackorderCheckbox()?.checked ).toBeTruthy(); } ); test( 'removes the option when it is clicked again', async () => { - await waitFor( async () => { - const ratingParam = 'onbackorder'; - const { - getInStockCheckbox, - getOutOfStockCheckbox, - getOnBackorderCheckbox, - } = setupMultipleChoiceList( ratingParam ); + const ratingParam = 'onbackorder'; + const { + getInStockCheckbox, + getOutOfStockCheckbox, + getOnBackorderCheckbox, + } = setupMultipleChoiceList( ratingParam ); + expect( getInStockCheckbox()?.checked ).toBeFalsy(); + expect( getOutOfStockCheckbox()?.checked ).toBeFalsy(); + expect( getOnBackorderCheckbox()?.checked ).toBeTruthy(); + + const onBackorderCheckbox = getOnBackorderCheckbox(); + + if ( onBackorderCheckbox ) { + userEvent.click( onBackorderCheckbox ); + } + + await waitFor( () => { expect( getInStockCheckbox()?.checked ).toBeFalsy(); expect( getOutOfStockCheckbox()?.checked ).toBeFalsy(); - expect( getOnBackorderCheckbox()?.checked ).toBeTruthy(); - - const onBackorderCheckbox = getOnBackorderCheckbox(); - - if ( onBackorderCheckbox ) { - userEvent.click( onBackorderCheckbox ); - } - - await waitFor( () => { - expect( getInStockCheckbox()?.checked ).toBeFalsy(); - expect( getOutOfStockCheckbox()?.checked ).toBeFalsy(); - expect( getOnBackorderCheckbox()?.checked ).toBeFalsy(); - } ); + expect( getOnBackorderCheckbox()?.checked ).toBeFalsy(); } ); } ); } ); @@ -531,72 +522,66 @@ describe( 'Filter by Stock block', () => { expect( getList() ).toBeInTheDocument(); } ); - test( 'renders chips based on URL params', async () => { - await waitFor( async () => { - const ratingParam = 'instock,onbackorder'; - const { - getInStockCheckbox, - getOutOfStockCheckbox, - getOnBackorderCheckbox, - } = setupMultipleChoiceList( ratingParam ); + test( 'renders chips based on URL params', () => { + const ratingParam = 'instock,onbackorder'; + const { + getInStockCheckbox, + getOutOfStockCheckbox, + getOnBackorderCheckbox, + } = setupMultipleChoiceList( ratingParam ); - expect( getInStockCheckbox()?.checked ).toBeTruthy(); - expect( getOutOfStockCheckbox()?.checked ).toBeFalsy(); - expect( getOnBackorderCheckbox()?.checked ).toBeTruthy(); - } ); + expect( getInStockCheckbox()?.checked ).toBeTruthy(); + expect( getOutOfStockCheckbox()?.checked ).toBeFalsy(); + expect( getOnBackorderCheckbox()?.checked ).toBeTruthy(); } ); test( 'adds chosen option to another one that is clicked', async () => { - await waitFor( async () => { - const ratingParam = 'outofstock,onbackorder'; - const { - getInStockCheckbox, - getOutOfStockCheckbox, - getOnBackorderCheckbox, - } = setupMultipleChoiceList( ratingParam ); + const ratingParam = 'outofstock,onbackorder'; + const { + getInStockCheckbox, + getOutOfStockCheckbox, + getOnBackorderCheckbox, + } = setupMultipleChoiceList( ratingParam ); - expect( getInStockCheckbox()?.checked ).toBeFalsy(); + expect( getInStockCheckbox()?.checked ).toBeFalsy(); + expect( getOutOfStockCheckbox()?.checked ).toBeTruthy(); + expect( getOnBackorderCheckbox()?.checked ).toBeTruthy(); + + const inStockCheckbox = getInStockCheckbox(); + + if ( inStockCheckbox ) { + userEvent.click( inStockCheckbox ); + } + + await waitFor( () => { + expect( getInStockCheckbox()?.checked ).toBeTruthy(); expect( getOutOfStockCheckbox()?.checked ).toBeTruthy(); expect( getOnBackorderCheckbox()?.checked ).toBeTruthy(); - - const inStockCheckbox = getInStockCheckbox(); - - if ( inStockCheckbox ) { - userEvent.click( inStockCheckbox ); - } - - await waitFor( () => { - expect( getInStockCheckbox()?.checked ).toBeTruthy(); - expect( getOutOfStockCheckbox()?.checked ).toBeTruthy(); - expect( getOnBackorderCheckbox()?.checked ).toBeTruthy(); - } ); } ); } ); test( 'removes the option when it is clicked again', async () => { - await waitFor( async () => { - const ratingParam = 'instock,outofstock'; - const { - getInStockCheckbox, - getOutOfStockCheckbox, - getOnBackorderCheckbox, - } = setupMultipleChoiceList( ratingParam ); + const ratingParam = 'instock,outofstock'; + const { + getInStockCheckbox, + getOutOfStockCheckbox, + getOnBackorderCheckbox, + } = setupMultipleChoiceList( ratingParam ); - expect( getInStockCheckbox()?.checked ).toBeTruthy(); + expect( getInStockCheckbox()?.checked ).toBeTruthy(); + expect( getOutOfStockCheckbox()?.checked ).toBeTruthy(); + expect( getOnBackorderCheckbox()?.checked ).toBeFalsy(); + + const inStockCheckbox = getInStockCheckbox(); + + if ( inStockCheckbox ) { + userEvent.click( inStockCheckbox ); + } + + await waitFor( () => { + expect( getInStockCheckbox()?.checked ).toBeFalsy(); expect( getOutOfStockCheckbox()?.checked ).toBeTruthy(); expect( getOnBackorderCheckbox()?.checked ).toBeFalsy(); - - const inStockCheckbox = getInStockCheckbox(); - - if ( inStockCheckbox ) { - userEvent.click( inStockCheckbox ); - } - - await waitFor( () => { - expect( getInStockCheckbox()?.checked ).toBeFalsy(); - expect( getOutOfStockCheckbox()?.checked ).toBeTruthy(); - expect( getOnBackorderCheckbox()?.checked ).toBeFalsy(); - } ); } ); } ); } ); diff --git a/plugins/woocommerce/changelog/51289-revert-react-18-in-checkout-block b/plugins/woocommerce/changelog/51289-revert-react-18-in-checkout-block new file mode 100644 index 00000000000..f61f7fda144 --- /dev/null +++ b/plugins/woocommerce/changelog/51289-revert-react-18-in-checkout-block @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Revert update to React 18 in Checkout block. \ No newline at end of file diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block-coupons.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block-coupons.spec.js index 6db1b13ff81..95bdb3f28f6 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block-coupons.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block-coupons.spec.js @@ -138,7 +138,9 @@ test.describe( await page .getByRole( 'button', { name: 'Add a coupon' } ) .click(); - await page.getByLabel( 'Enter code' ).fill( coupons[ i ].code ); + await page + .locator( '#wc-block-components-totals-coupon__input-0' ) + .fill( coupons[ i ].code ); await page.getByText( 'Apply', { exact: true } ).click(); await expect( page @@ -181,7 +183,9 @@ test.describe( await page .getByRole( 'button', { name: 'Add a coupon' } ) .click(); - await page.getByLabel( 'Enter code' ).fill( coupons[ i ].code ); + await page + .locator( '#wc-block-components-totals-coupon__input-0' ) + .fill( coupons[ i ].code ); await page.getByText( 'Apply', { exact: true } ).click(); await expect( page @@ -221,7 +225,9 @@ test.describe( } ) => { // try to add two same coupons and verify the error message await page.getByRole( 'button', { name: 'Add a coupon' } ).click(); - await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code ); + await page + .locator( '#wc-block-components-totals-coupon__input-0' ) + .fill( coupons[ 0 ].code ); await page.getByText( 'Apply', { exact: true } ).click(); await expect( page @@ -231,7 +237,9 @@ test.describe( ) ).toBeVisible(); await page.getByRole( 'button', { name: 'Add a coupon' } ).click(); - await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code ); + await page + .locator( '#wc-block-components-totals-coupon__input-0' ) + .fill( coupons[ 0 ].code ); await page.getByText( 'Apply', { exact: true } ).click(); await expect( page @@ -247,7 +255,9 @@ test.describe( } ) => { // add coupon with usage limit await page.getByRole( 'button', { name: 'Add a coupon' } ).click(); - await page.getByLabel( 'Enter code' ).fill( couponLimitedCode ); + await page + .locator( '#wc-block-components-totals-coupon__input-0' ) + .fill( couponLimitedCode ); await page.getByText( 'Apply', { exact: true } ).click(); await expect( page From 0b16cfa06a014cc8060d9174b71f42c00f67a9aa Mon Sep 17 00:00:00 2001 From: Alexandre Lara Date: Wed, 11 Sep 2024 11:30:35 -0300 Subject: [PATCH 05/59] [Experimental] Product Filters Redesign > Overlay: Add Fullscreen view (#50505) * Add variation to Product Filters Overlay Navigation * Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce * Move Product Filters Overlay Navigation to correct position * Hide block when it is outside the Product Filters template part * Display Navigation block in the frontend * Show the Product Filters Overlay Navigation on the frontend * Add logic to hide Product Filters Overlay Navigation block on the frontend * Hide block on the Overlay template part * Fix eslint errors * Update the block variation title * Remove the `isActive` property from the block variations * Use Product Filters block context * Replace enum with const * Remove unnecessary `StyleAttributesUtils` * Rename context key * Move BlockOverlayAttribute to the constants.ts file * fix BlockOverlayAttribute import * Fix import error * Improve code for the shouldHideBlock method * Remove unnecessary attributes property * Fix error in ProductFiltersOverlay block * Add dialog to the Product Filters block * Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce * Fix interactivity api error * Prevent block from being hidden on Product Filters template part * Fix inspector controls when block is hidden * Add clickable action to the Product Filters Overlay Navigation block * Fix interactivity directives that were not working for the Overlay * Fix issue with dialog styles not being correctly applied * Add the `closeDialog` functionality * Parse and render blocks for the Product Filters overlay * Fix padding * Fix style for Product Filters Overlay navigation block * Add e2e test * Add e2e test to Product Filters Overlay template part * Fix e2e test * Fix issue causing the trigger button to show even though the overlay mode is set to 'Never" * Fix issue causing close button to not be displayed in the dialog * Add e2e tests * Fix issue that was preventing users from scrolling down the dialog content * Remove text duplication in e2e tests * Remove unnecessary imports * Fix php cs errors * Fix php cs error * Revert changes on Product Gallery modal styles * Fix lint errors * fix php cs lint errors * fix php cs error --------- Co-authored-by: github-actions --- .../woocommerce-blocks/assets/css/style.scss | 4 + .../js/blocks/product-filters/frontend.tsx | 33 ++- .../js/blocks/product-filters/index.tsx | 1 + .../overlay-navigation/block.json | 1 + .../overlay-navigation/frontend.tsx | 19 ++ .../overlay-navigation/inspector-controls.tsx | 1 - .../overlay-navigation/style.scss | 11 +- .../js/blocks/product-filters/style.scss | 22 ++ ...-overlay-template-part.block_theme.spec.ts | 276 +++++++++++++++++- .../product-filters.block_theme.spec.ts | 1 + .../product-filters/product-filters.page.ts | 36 +++ ...9977-add-fullscreen-filters-overlay-dialog | 4 + .../src/Blocks/BlockTypes/ProductFilters.php | 111 ++++++- .../ProductFiltersOverlayNavigation.php | 32 +- 14 files changed, 521 insertions(+), 31 deletions(-) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/frontend.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/style.scss create mode 100644 plugins/woocommerce/changelog/50505-feat-49977-add-fullscreen-filters-overlay-dialog diff --git a/plugins/woocommerce-blocks/assets/css/style.scss b/plugins/woocommerce-blocks/assets/css/style.scss index b3003f7bdcf..5fdf23c1f0e 100644 --- a/plugins/woocommerce-blocks/assets/css/style.scss +++ b/plugins/woocommerce-blocks/assets/css/style.scss @@ -1,3 +1,7 @@ +body.wc-modal--open { + overflow: hidden; +} + body.wc-block-product-gallery-modal-open { overflow: hidden; } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx index 0182c842c02..dab9ef6c6c9 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx @@ -1,15 +1,40 @@ /** * External dependencies */ -import { store } from '@woocommerce/interactivity'; +import { getContext as getContextFn, store } from '@woocommerce/interactivity'; export interface ProductFiltersContext { - productId: string; + isDialogOpen: boolean; + hasPageWithWordPressAdminBar: boolean; } +const getContext = ( ns?: string ) => + getContextFn< ProductFiltersContext >( ns ); + const productFilters = { - state: {}, - actions: {}, + state: { + isDialogOpen: () => { + const context = getContext(); + return context.isDialogOpen; + }, + }, + actions: { + openDialog: () => { + const context = getContext(); + document.body.classList.add( 'wc-modal--open' ); + context.hasPageWithWordPressAdminBar = Boolean( + document.getElementById( 'wpadminbar' ) + ); + + context.isDialogOpen = true; + }, + closeDialog: () => { + const context = getContext(); + document.body.classList.remove( 'wc-modal--open' ); + + context.isDialogOpen = false; + }, + }, callbacks: {}, }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/index.tsx index b0ef8827509..b003dabf907 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/index.tsx @@ -9,6 +9,7 @@ import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; */ import metadata from './block.json'; import { ProductFiltersBlockSettings } from './settings'; +import './style.scss'; if ( isExperimentalBlocksEnabled() ) { registerBlockType( metadata, ProductFiltersBlockSettings ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/block.json index a7330ff4e6d..f876968991c 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/block.json +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/block.json @@ -40,6 +40,7 @@ } }, "supports": { + "interactivity": true, "align": [ "left", "right", "center"], "inserter": false, "color": { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/frontend.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/frontend.tsx new file mode 100644 index 00000000000..77411ae98ea --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/frontend.tsx @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import { store } from '@woocommerce/interactivity'; + +export interface ProductFiltersContext { + isDialogOpen: boolean; +} + +const productFiltersOverlayNavigation = { + state: {}, + actions: {}, + callbacks: {}, +}; + +store( 'woocommerce/product-filters', productFiltersOverlayNavigation ); + +export type ProductFiltersOverlayNavigation = + typeof productFiltersOverlayNavigation; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/inspector-controls.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/inspector-controls.tsx index 7c439cd1ac3..16c7352cfdf 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/inspector-controls.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/inspector-controls.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @wordpress/no-unsafe-wp-apis */ /** * External dependencies */ diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/style.scss index dfc9b703aab..da985d86584 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/style.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/style.scss @@ -8,15 +8,18 @@ cursor: pointer; &.alignright { - margin-left: auto; + justify-content: right; } &.alignleft { - margin-left: unset; + justify-content: unset; } &.aligncenter { - margin-left: auto; - margin-right: auto; + justify-content: center; + } + + &.hidden { + display: none; } } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/style.scss new file mode 100644 index 00000000000..5b836c1e93c --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/style.scss @@ -0,0 +1,22 @@ +.wc-block-product-filters { + dialog { + flex-direction: column; + position: fixed; + border: none; + top: 0; + z-index: 9999; + height: 100vh; + width: 100vw; + + &.wc-block-product-filters--dialog-open { + display: flex; + padding-left: 0; + overflow-y: auto; + } + + &.wc-block-product-filters--with-admin-bar { + margin-top: $gap; + height: calc(100vh - 2 * $gap); + } + } +} diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts index 9601e004dea..a5e02ea94fa 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts @@ -3,6 +3,35 @@ */ import { test, expect } from '@woocommerce/e2e-utils'; +const templatePartData = { + selectors: { + frontend: {}, + editor: { + blocks: { + activeFilters: { + title: 'Active (Experimental)', + blockLabel: 'Block: Active (Experimental)', + }, + productFilters: { + title: 'Product Filters (Experimental)', + blockLabel: 'Block: Product Filters (Experimental)', + }, + filterOptions: { + title: 'Filter Options', + blockLabel: 'Block: Filter Options', + }, + productFiltersOverlayNavigation: { + title: 'Overlay Navigation (Experimental)', + name: 'woocommerce/product-filters-overlay-navigation', + blockLabel: 'Block: Overlay Navigation (Experimental)', + }, + }, + }, + }, + slug: 'product-filters', + productPage: '/product/hoodie/', +}; + test.describe( 'Filters Overlay Template Part', () => { test.beforeEach( async ( { admin, requestUtils } ) => { await requestUtils.activatePlugin( @@ -34,10 +63,255 @@ test.describe( 'Filters Overlay Template Part', () => { .locator( '[data-type="core/template-part"]' ) .filter( { has: editor.canvas.getByLabel( - 'Block: Product Filters (Experimental)' + templatePartData.selectors.editor.blocks.productFilters + .blockLabel ), } ); await expect( productFiltersTemplatePart ).toBeVisible(); } ); + + test.describe( 'frontend', () => { + test.beforeEach( async ( { admin } ) => { + await admin.visitSiteEditor( { + postId: `woocommerce/woocommerce//archive-product`, + postType: 'wp_template', + canvas: 'edit', + } ); + } ); + + test( 'should open and close the dialog when clicking on the Product Filters Overlay Navigation block', async ( { + editor, + page, + frontendUtils, + } ) => { + await editor.setContent( '' ); + await editor.openGlobalBlockInserter(); + await page + .getByText( + templatePartData.selectors.editor.blocks.productFilters + .title + ) + .click(); + const block = editor.canvas.getByLabel( + templatePartData.selectors.editor.blocks.productFilters + .blockLabel + ); + await expect( block ).toBeVisible(); + + // This forces the list view to show the inner blocks of the Product Filters template part. + await editor.canvas + .getByLabel( + templatePartData.selectors.editor.blocks.activeFilters + .blockLabel + ) + .getByLabel( + templatePartData.selectors.editor.blocks.filterOptions + .blockLabel + ) + .click(); + + await editor.openDocumentSettingsSidebar(); + await page.getByLabel( 'Document Overview' ).click(); + await page + .getByRole( 'link', { + name: templatePartData.selectors.editor.blocks + .productFilters.title, + } ) + .nth( 1 ) + .click(); + + const layoutSettings = editor.page.getByText( + 'OverlayNeverMobileAlways' + ); + await layoutSettings.getByLabel( 'Always' ).click(); + await editor.page + .getByRole( 'link', { + name: templatePartData.selectors.editor.blocks + .productFiltersOverlayNavigation.title, + } ) + .click(); + + await editor.saveSiteEditorEntities( { + isOnlyCurrentEntityDirty: false, + } ); + + await page.goto( '/shop/' ); + + const productFiltersOverlayNavigation = ( + await frontendUtils.getBlockByName( + templatePartData.selectors.editor.blocks + .productFiltersOverlayNavigation.name + ) + ).filter( { + has: page.locator( ':visible' ), + } ); + + await expect( productFiltersOverlayNavigation ).toBeVisible(); + + await page + .locator( '.wc-block-product-filters-overlay-navigation' ) + .first() + .click(); + + const productFiltersDialog = page.locator( + '.wc-block-product-filters--dialog-open' + ); + + await expect( productFiltersDialog ).toBeVisible(); + + const productFiltersDialogCloseButton = ( + await frontendUtils.getBlockByName( + templatePartData.selectors.editor.blocks + .productFiltersOverlayNavigation.name + ) + ).filter( { hasText: 'Close' } ); + + await expect( productFiltersDialogCloseButton ).toBeVisible(); + + await productFiltersDialogCloseButton.click(); + + await expect( productFiltersDialog ).toBeHidden(); + } ); + + test( 'should hide Product Filters Overlay Navigation block when the Overlay mode is set to `Never`', async ( { + editor, + page, + frontendUtils, + } ) => { + await editor.setContent( '' ); + await editor.openGlobalBlockInserter(); + await page + .getByText( + templatePartData.selectors.editor.blocks.productFilters + .title + ) + .click(); + const block = editor.canvas.getByLabel( + templatePartData.selectors.editor.blocks.productFilters + .blockLabel + ); + await expect( block ).toBeVisible(); + + // This forces the list view to show the inner blocks of the Product Filters template part. + await editor.canvas + .getByLabel( + templatePartData.selectors.editor.blocks.activeFilters + .blockLabel + ) + .getByLabel( + templatePartData.selectors.editor.blocks.filterOptions + .blockLabel + ) + .click(); + + await editor.openDocumentSettingsSidebar(); + await page.getByLabel( 'Document Overview' ).click(); + await page + .getByRole( 'link', { + name: templatePartData.selectors.editor.blocks + .productFilters.title, + } ) + .nth( 1 ) + .click(); + + const layoutSettings = editor.page.getByText( + 'OverlayNeverMobileAlways' + ); + await layoutSettings.getByLabel( 'Never' ).click(); + await editor.page + .getByRole( 'link', { + name: templatePartData.selectors.editor.blocks + .productFiltersOverlayNavigation.title, + } ) + .click(); + + await editor.saveSiteEditorEntities( { + isOnlyCurrentEntityDirty: true, + } ); + + await page.goto( '/shop/' ); + + const productFiltersOverlayNavigation = ( + await frontendUtils.getBlockByName( + templatePartData.selectors.editor.blocks + .productFiltersOverlayNavigation.name + ) + ).filter( { + has: page.locator( ':visible' ), + } ); + + await expect( productFiltersOverlayNavigation ).toBeHidden(); + } ); + + test( 'should hide Product Filters Overlay Navigation block when the Overlay mode is set to `Mobile` and user is on desktop', async ( { + editor, + page, + frontendUtils, + } ) => { + await editor.setContent( '' ); + await editor.openGlobalBlockInserter(); + await page + .getByText( + templatePartData.selectors.editor.blocks.productFilters + .title + ) + .click(); + const block = editor.canvas.getByLabel( + templatePartData.selectors.editor.blocks.productFilters + .blockLabel + ); + await expect( block ).toBeVisible(); + + // This forces the list view to show the inner blocks of the Product Filters template part. + await editor.canvas + .getByLabel( + templatePartData.selectors.editor.blocks.activeFilters + .blockLabel + ) + .getByLabel( + templatePartData.selectors.editor.blocks.filterOptions + .blockLabel + ) + .click(); + + await editor.openDocumentSettingsSidebar(); + await page.getByLabel( 'Document Overview' ).click(); + await page + .getByRole( 'link', { + name: templatePartData.selectors.editor.blocks + .productFilters.title, + } ) + .nth( 1 ) + .click(); + + const layoutSettings = editor.page.getByText( + 'OverlayNeverMobileAlways' + ); + await layoutSettings.getByLabel( 'Mobile' ).click(); + await editor.page + .getByRole( 'link', { + name: templatePartData.selectors.editor.blocks + .productFiltersOverlayNavigation.title, + } ) + .click(); + + await editor.saveSiteEditorEntities( { + isOnlyCurrentEntityDirty: false, + } ); + + await page.goto( '/shop/' ); + + const productFiltersOverlayNavigation = ( + await frontendUtils.getBlockByName( + templatePartData.selectors.editor.blocks + .productFiltersOverlayNavigation.name + ) + ).filter( { + has: page.locator( ':visible' ), + } ); + + await expect( productFiltersOverlayNavigation ).toBeHidden(); + } ); + } ); } ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts index 8f4461180b6..aa019e69ed7 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts @@ -21,6 +21,7 @@ const blockData = { }, slug: 'archive-product', productPage: '/product/hoodie/', + shopPage: '/shop/', }; const test = base.extend< { pageObject: ProductFiltersPage } >( { diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.page.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.page.ts index e688c1b737d..bd7903fdb5c 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.page.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.page.ts @@ -43,4 +43,40 @@ export class ProductFiltersPage { } return this.editor.getBlockByName( blockName ); } + + async getProductFiltersOverlayNavigationBlock( { + page, + }: { + page: 'frontend' | 'editor'; + } ) { + const blockName = 'woocommerce/product-filters-overlay-navigation'; + if ( page === 'frontend' ) { + return ( + await this.frontendUtils.getBlockByName( blockName ) + ).filter( { + has: this.page.locator( ':visible' ), + } ); + } + return this.editor.canvas.getByLabel( + 'Block: Overlay Navigation (Experimental)' + ); + } + + async selectOverlayMode( { + mode, + }: { + mode: 'mobile' | 'always' | 'never'; + } ) { + switch ( mode ) { + case 'always': + await this.page.getByLabel( 'Always' ).click(); + break; + case 'mobile': + await this.page.getByLabel( 'Mobile' ).click(); + break; + case 'never': + await this.page.getByLabel( 'Never' ).click(); + break; + } + } } diff --git a/plugins/woocommerce/changelog/50505-feat-49977-add-fullscreen-filters-overlay-dialog b/plugins/woocommerce/changelog/50505-feat-49977-add-fullscreen-filters-overlay-dialog new file mode 100644 index 00000000000..efab38908ab --- /dev/null +++ b/plugins/woocommerce/changelog/50505-feat-49977-add-fullscreen-filters-overlay-dialog @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak +Comment: Add the Fullscreen view to the Product Filters + diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php index 024fb4f7960..9c149ef6b3c 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php @@ -1,6 +1,8 @@ render_template_part( $template_part ); + + $html = strtr( + '', + array( + '{{html}}' => $html, + ) + ); + + $p = new \WP_HTML_Tag_Processor( $html ); + if ( $p->next_tag() ) { + $p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-filters' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); + $p->set_attribute( 'data-wc-bind--hidden', '!state.isDialogOpen' ); + $p->set_attribute( 'data-wc-class--wc-block-product-filters--dialog-open', 'state.isDialogOpen' ); + $p->set_attribute( 'data-wc-class--wc-block-product-filters--with-admin-bar', 'context.hasPageWithWordPressAdminBar' ); + $html = $p->get_updated_html(); + } + + return $html; + } + + /** + * This method is used to render the template part. For each template part, we parse the blocks and render them. + * + * @param string $template_part The template part to render. + * @return string The rendered template part. + */ + protected function render_template_part( $template_part ) { + $parsed_blocks = parse_blocks( $template_part ); + $wrapper_template_part_block = $parsed_blocks[0]; + $html = $wrapper_template_part_block['innerHTML']; + $target_div = ''; + + $template_part_content_html = array_reduce( + $wrapper_template_part_block['innerBlocks'], + function ( $carry, $item ) { + if ( 'core/template-part' === $item['blockName'] ) { + $inner_template_part = BlockTemplateUtils::get_template_part( $item['attrs']['slug'] ); + $inner_template_part_content_html = $this->render_template_part( $inner_template_part ); + + return $carry . $inner_template_part_content_html; + } + return $carry . render_block( $item ); + }, + '' + ); + + $html = str_replace( $target_div, $template_part_content_html . $target_div, $html ); + + return $html; + } + + /** + * Inject dialog into the product filters HTML. + * + * @param string $product_filters_html The Product Filters HTML. + * @param string $dialog_html The dialog HTML. + * + * @return string + */ + protected function inject_dialog( $product_filters_html, $dialog_html ) { + // Find the position of the last . + $pos = strrpos( $product_filters_html, '' ); + + if ( $pos ) { + // Inject the dialog_html at the correct position. + $html = substr_replace( $product_filters_html, $dialog_html, $pos, 0 ); + + return $html; + } + + return $product_filters_html; } /** @@ -39,6 +116,28 @@ class ProductFilters extends AbstractBlock { * @return string Rendered block type output. */ protected function render( $attributes, $content, $block ) { - return $content; + $html = $content; + $p = new \WP_HTML_Tag_Processor( $html ); + + if ( $p->next_tag() ) { + $p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-filters' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); + $p->set_attribute( + 'data-wc-context', + wp_json_encode( + array( + 'isDialogOpen' => false, + 'hasPageWithWordPressAdminBar' => false, + ), + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP + ) + ); + $html = $p->get_updated_html(); + } + + $dialog_html = $this->render_dialog(); + + $html = $this->inject_dialog( $html, $dialog_html ); + + return $html; } } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlayNavigation.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlayNavigation.php index 0eb6cfe11d6..ebfdaba12d5 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlayNavigation.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlayNavigation.php @@ -21,17 +21,6 @@ class ProductFiltersOverlayNavigation extends AbstractBlock { return [ 'woocommerce/product-filters/overlay' ]; } - /** - * Get the frontend script handle for this block type. - * - * @see $this->register_block_type() - * @param string $key Data to get, or default to everything. - * @return array|string|null - */ - protected function get_block_type_script( $key = null ) { - return null; - } - /** * Include and render the block. * @@ -46,13 +35,13 @@ class ProductFiltersOverlayNavigation extends AbstractBlock { 'class' => 'wc-block-product-filters-overlay-navigation', ) ); - $overlay_mode = $block->context['woocommerce/product-filters/overlay']; + $overlay_mode = isset( $block->context['woocommerce/product-filters/overlay'] ) ? $block->context['woocommerce/product-filters/overlay'] : 'never'; - if ( 'never' === $overlay_mode || ( ! wp_is_mobile() && 'mobile' === $overlay_mode ) ) { + if ( 'open-overlay' === $attributes['triggerType'] && ( 'never' === $overlay_mode || ( ! wp_is_mobile() && 'mobile' === $overlay_mode ) ) ) { return null; } - $html_content = strtr( + $html = strtr( '
{{primary_content}} {{secondary_content}} @@ -63,7 +52,20 @@ class ProductFiltersOverlayNavigation extends AbstractBlock { '{{secondary_content}}' => 'open-overlay' === $attributes['triggerType'] ? $this->render_label( $attributes ) : $this->render_icon( $attributes ), ) ); - return $html_content; + + $p = new \WP_HTML_Tag_Processor( $html ); + + if ( $p->next_tag() ) { + $p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-filters' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); + $p->set_attribute( + 'data-wc-on--click', + 'open-overlay' === $attributes['triggerType'] ? 'actions.openDialog' : 'actions.closeDialog' + ); + $p->set_attribute( 'data-wc-class--hidden', 'open-overlay' === $attributes['triggerType'] ? 'state.isDialogOpen' : '!state.isDialogOpen' ); + $html = $p->get_updated_html(); + } + + return $html; } /** From d4cc6578c77ac550957586abd6f00576d10fee75 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 21:48:29 +0700 Subject: [PATCH 06/59] Delete changelog files based on PR 51289 (#51294) Delete changelog files for 51289 Co-authored-by: WooCommerce Bot --- .../changelog/51289-revert-react-18-in-checkout-block | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/51289-revert-react-18-in-checkout-block diff --git a/plugins/woocommerce/changelog/51289-revert-react-18-in-checkout-block b/plugins/woocommerce/changelog/51289-revert-react-18-in-checkout-block deleted file mode 100644 index f61f7fda144..00000000000 --- a/plugins/woocommerce/changelog/51289-revert-react-18-in-checkout-block +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Revert update to React 18 in Checkout block. \ No newline at end of file From d87c3d38d72c12469f1bf9c35364d77076a93e66 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 11 Sep 2024 16:25:20 +0100 Subject: [PATCH 07/59] Performance: Move collectableMethodIds to cart/checkout block assets rather than loading it sitewide (#51174) * Move collectableMethodIds to cart/checkout block assets * changelog --- .../changelog/fix-improve-local-pickup-id-settings | 4 ++++ plugins/woocommerce/src/Blocks/BlockTypes/Cart.php | 4 ++++ plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php | 7 +++++-- .../woocommerce/src/Blocks/Shipping/ShippingController.php | 2 -- 4 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-improve-local-pickup-id-settings diff --git a/plugins/woocommerce/changelog/fix-improve-local-pickup-id-settings b/plugins/woocommerce/changelog/fix-improve-local-pickup-id-settings new file mode 100644 index 00000000000..4315888d20c --- /dev/null +++ b/plugins/woocommerce/changelog/fix-improve-local-pickup-id-settings @@ -0,0 +1,4 @@ +Significance: minor +Type: performance + +Only load local pickup methods on cart/checkout pages diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php b/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php index 11f8bdb2d26..ffc73228bc1 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php @@ -245,8 +245,12 @@ class Cart extends AbstractBlock { $this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ) ); $this->asset_data_registry->register_page_id( isset( $attributes['checkoutPageId'] ) ? $attributes['checkoutPageId'] : 0 ); $this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() ); + $pickup_location_settings = LocalPickupUtils::get_local_pickup_settings(); + $local_pickup_method_ids = LocalPickupUtils::get_local_pickup_method_ids(); + $this->asset_data_registry->add( 'localPickupEnabled', $pickup_location_settings['enabled'] ); + $this->asset_data_registry->add( 'collectableMethodIds', $local_pickup_method_ids ); // Hydrate the following data depending on admin or frontend context. if ( ! is_admin() && ! WC()->is_rest_api_request() ) { diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php index 60737382791..211b57c63eb 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php @@ -370,8 +370,11 @@ class Checkout extends AbstractBlock { $this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() ); $pickup_location_settings = LocalPickupUtils::get_local_pickup_settings(); + $local_pickup_method_ids = LocalPickupUtils::get_local_pickup_method_ids(); + $this->asset_data_registry->add( 'localPickupEnabled', $pickup_location_settings['enabled'] ); $this->asset_data_registry->add( 'localPickupText', $pickup_location_settings['title'] ); + $this->asset_data_registry->add( 'collectableMethodIds', $local_pickup_method_ids ); $is_block_editor = $this->is_block_editor(); @@ -385,8 +388,8 @@ class Checkout extends AbstractBlock { $shipping_methods = WC()->shipping()->get_shipping_methods(); $formatted_shipping_methods = array_reduce( $shipping_methods, - function ( $acc, $method ) { - if ( in_array( $method->id, LocalPickupUtils::get_local_pickup_method_ids(), true ) ) { + function ( $acc, $method ) use ( $local_pickup_method_ids ) { + if ( in_array( $method->id, $local_pickup_method_ids, true ) ) { return $acc; } if ( $method->supports( 'settings' ) ) { diff --git a/plugins/woocommerce/src/Blocks/Shipping/ShippingController.php b/plugins/woocommerce/src/Blocks/Shipping/ShippingController.php index 219ca8abe4c..418fedab766 100644 --- a/plugins/woocommerce/src/Blocks/Shipping/ShippingController.php +++ b/plugins/woocommerce/src/Blocks/Shipping/ShippingController.php @@ -60,8 +60,6 @@ class ShippingController { } ); } - - $this->asset_data_registry->add( 'collectableMethodIds', array( 'Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils', 'get_local_pickup_method_ids' ) ); $this->asset_data_registry->add( 'shippingCostRequiresAddress', get_option( 'woocommerce_shipping_cost_requires_address', false ) === 'yes' ); add_action( 'rest_api_init', array( $this, 'register_settings' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) ); From 0e2258b57dd5a16a5ab7653247cdb034981f631a Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:32:18 +0100 Subject: [PATCH 08/59] [e2e tests] Fix flaky test and refactor admin-tasks payment spec (#51269) --- .../e2e-fix-flaky-and-refactor-payment-spec | 4 + .../e2e-pw/tests/admin-tasks/payment.spec.js | 142 +++++++----------- 2 files changed, 59 insertions(+), 87 deletions(-) create mode 100644 plugins/woocommerce/changelog/e2e-fix-flaky-and-refactor-payment-spec diff --git a/plugins/woocommerce/changelog/e2e-fix-flaky-and-refactor-payment-spec b/plugins/woocommerce/changelog/e2e-fix-flaky-and-refactor-payment-spec new file mode 100644 index 00000000000..4f73f5cc6c2 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-fix-flaky-and-refactor-payment-spec @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + + diff --git a/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js index d5cffb9a16d..eec70d721a8 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js @@ -1,45 +1,12 @@ -const { test, expect } = require( '@playwright/test' ); -const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; +const { test: baseTest, expect } = require( '../../fixtures/fixtures' ); -test.describe( 'Payment setup task', () => { - test.use( { storageState: process.env.ADMINSTATE } ); - - test.beforeEach( async ( { baseURL } ) => { - await new wcApi( { - url: baseURL, - consumerKey: process.env.CONSUMER_KEY, - consumerSecret: process.env.CONSUMER_SECRET, - version: 'wc-admin', - } ).post( 'onboarding/profile', { +const test = baseTest.extend( { + storageState: process.env.ADMINSTATE, + page: async ( { api, page, wpApi, wcAdminApi }, use ) => { + await wcAdminApi.post( 'onboarding/profile', { skipped: true, } ); - } ); - test.afterAll( async ( { baseURL } ) => { - const api = new wcApi( { - url: baseURL, - consumerKey: process.env.CONSUMER_KEY, - consumerSecret: process.env.CONSUMER_SECRET, - version: 'wc/v3', - } ); - await api.put( 'payment_gateways/bacs', { - enabled: false, - } ); - await api.put( 'payment_gateways/cod', { - enabled: false, - } ); - } ); - - test( 'Saving valid bank account transfer details enables the payment method', async ( { - baseURL, - page, - } ) => { - const api = new wcApi( { - url: baseURL, - consumerKey: process.env.CONSUMER_KEY, - consumerSecret: process.env.CONSUMER_SECRET, - version: 'wc/v3', - } ); // Ensure store's base country location is a WooPayments non-supported country (AF). // Otherwise, the WooPayments task page logic or WooPayments redirects will kick in. await api.post( 'settings/general/batch', { @@ -50,14 +17,44 @@ test.describe( 'Payment setup task', () => { }, ], } ); + + const bacsInitialState = await api.get( 'payment_gateways/bacs' ); + const codInitialState = await api.get( 'payment_gateways/cod' ); + + // Disable the help popover. + await wpApi.post( '/wp-json/wp/v2/users/1?_locale=user', { + data: { + woocommerce_meta: { + help_panel_highlight_shown: '"yes"', + }, + }, + } ); + + await use( page ); + + // Reset the payment gateways to their initial state. + await api.put( 'payment_gateways/bacs', { + enabled: bacsInitialState.data.enabled, + } ); + await api.put( 'payment_gateways/cod', { + enabled: codInitialState.data.enabled, + } ); + }, +} ); + +test.describe( 'Payment setup task', () => { + test( 'Saving valid bank account transfer details enables the payment method', async ( { + page, + api, + } ) => { + await api.put( 'payment_gateways/bacs', { + enabled: false, + } ); + // Load the bank transfer page. await page.goto( 'wp-admin/admin.php?page=wc-admin&task=payments&id=bacs' ); - // purposely no await -- close the help dialog if/when it appears - page.locator( '.components-button.is-small.has-icon' ) - .click() - .catch( () => {} ); // Fill in bank transfer form. await page @@ -93,25 +90,8 @@ test.describe( 'Payment setup task', () => { } ); test( 'Can visit the payment setup task from the homescreen if the setup wizard has been skipped', async ( { - baseURL, page, } ) => { - const api = new wcApi( { - url: baseURL, - consumerKey: process.env.CONSUMER_KEY, - consumerSecret: process.env.CONSUMER_SECRET, - version: 'wc/v3', - } ); - // Ensure store's base country location is a WooPayments non-supported country (AF). - // Otherwise, the WooPayments task page logic or WooPayments redirects will kick in. - await api.post( 'settings/general/batch', { - update: [ - { - id: 'woocommerce_default_country', - value: 'AF', - }, - ], - } ); await page.goto( 'wp-admin/admin.php?page=wc-admin' ); await page.locator( 'text=Get paid' ).click(); await expect( @@ -121,43 +101,31 @@ test.describe( 'Payment setup task', () => { test( 'Enabling cash on delivery enables the payment method', async ( { page, - baseURL, + api, } ) => { - const api = new wcApi( { - url: baseURL, - consumerKey: process.env.CONSUMER_KEY, - consumerSecret: process.env.CONSUMER_SECRET, - version: 'wc/v3', + await api.put( 'payment_gateways/cod', { + enabled: false, } ); - // Ensure store's base country location is a WooPayments non-supported country (AF). - // Otherwise, the WooPayments task page logic or WooPayments redirects will kick in. - await api.post( 'settings/general/batch', { - update: [ - { - id: 'woocommerce_default_country', - value: 'AF', - }, - ], - } ); - await page.goto( 'wp-admin/admin.php?page=wc-admin&task=payments' ); - // purposely no await -- close the help dialog if/when it appears - page.locator( '.components-button.is-small.has-icon' ) - .click() - .catch( () => {} ); + const paymentGatewaysResponse = page.waitForResponse( + ( response ) => + response.url().includes( 'wp-json/wc/v3/payment_gateways' ) && + response.ok() + ); + await page.goto( 'wp-admin/admin.php?page=wc-admin&task=payments' ); + await paymentGatewaysResponse; // Enable COD payment option. await page - .locator( - 'div.woocommerce-task-payment-cod > div.woocommerce-task-payment__footer > .woocommerce-task-payment__action' - ) + .locator( 'div.woocommerce-task-payment-cod' ) + .getByRole( 'button', { name: 'Enable' } ) .click(); // Check that COD was set up. await expect( - page.locator( - 'div.woocommerce-task-payment-cod > div.woocommerce-task-payment__footer > .woocommerce-task-payment__action' - ) - ).toContainText( 'Manage' ); + page + .locator( 'div.woocommerce-task-payment-cod' ) + .getByRole( 'button', { name: 'Manage' } ) + ).toBeVisible(); await page.goto( 'wp-admin/admin.php?page=wc-settings&tab=checkout' ); From 0c8a8bd6cf4b139cb1a25bac01dd84f29667364b Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Wed, 11 Sep 2024 19:10:00 +0100 Subject: [PATCH 09/59] [e2e tests] Fix flakiness and refactor create-order spec (#51292) --- ...e-fix-flakiness-refactor-create-order-spec | 4 + .../tests/merchant/create-order.spec.js | 565 +++++++++--------- 2 files changed, 275 insertions(+), 294 deletions(-) create mode 100644 plugins/woocommerce/changelog/e2e-fix-flakiness-refactor-create-order-spec diff --git a/plugins/woocommerce/changelog/e2e-fix-flakiness-refactor-create-order-spec b/plugins/woocommerce/changelog/e2e-fix-flakiness-refactor-create-order-spec new file mode 100644 index 00000000000..4f73f5cc6c2 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-fix-flakiness-refactor-create-order-spec @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + + diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-order.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-order.spec.js index 087e95556b7..16efdae3d39 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-order.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-order.spec.js @@ -1,10 +1,6 @@ -const { test, expect } = require( '@playwright/test' ); -const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; +const { test: baseTest, expect } = require( '../../fixtures/fixtures' ); +const { random } = require( '../../utils/helpers' ); -const simpleProductName = 'Add new order simple product'; -const variableProductName = 'Add new order variable product'; -const externalProductName = 'Add new order external product'; -const groupedProductName = 'Add new order grouped product'; const taxClasses = [ { name: 'Tax Class Simple', @@ -37,28 +33,230 @@ const taxRates = [ }, ]; const taxTotals = [ '10.00', '20.00', '240.00' ]; -let simpleProductId, - variableProductId, - externalProductId, - subProductAId, - subProductBId, - groupedProductId, - customerId, - orderId; + +async function getOrderIdFromPage( page ) { + // get order ID from the page + const orderText = await page + .locator( 'h2.woocommerce-order-data__heading' ) + .textContent(); + const parts = orderText.match( /([0-9])\w+/ ); + return parts[ 0 ]; +} + +async function addProductToOrder( page, product, quantity ) { + await page.getByRole( 'button', { name: 'Add item(s)' } ).click(); + await page.getByRole( 'button', { name: 'Add product(s)' } ).click(); + await page.getByText( 'Search for a product…' ).click(); + await page.locator( 'span > .select2-search__field' ).fill( product.name ); + await page.getByRole( 'option', { name: product.name } ).first().click(); + await page + .locator( 'tr' ) + .filter( { hasText: product.name } ) + .getByPlaceholder( '1' ) + .fill( quantity.toString() ); + await page.locator( '#btn-ok' ).click(); +} + +const test = baseTest.extend( { + storageState: process.env.ADMINSTATE, + order: async ( { api }, use ) => { + const order = {}; + + await use( order ); + + if ( order.id ) { + await api.delete( `orders/${ order.id }`, { force: true } ); + } + }, + + customer: async ( { api }, use ) => { + let customer = {}; + const username = `sideshowbob_${ random() }`; + + await api + .post( 'customers', { + email: `${ username }@example.com`, + first_name: 'Sideshow', + last_name: 'Bob', + username, + billing: { + first_name: 'Sideshow', + last_name: 'Bob', + company: 'Die Bart Die', + address_1: '123 Fake St', + address_2: '', + city: 'Springfield', + state: 'FL', + postcode: '12345', + country: 'US', + email: `${ username }@example.com`, + phone: '555-555-5556', + }, + shipping: { + first_name: 'Sideshow', + last_name: 'Bob', + company: 'Die Bart Die', + address_1: '321 Fake St', + address_2: '', + city: 'Springfield', + state: 'FL', + postcode: '12345', + country: 'US', + }, + } ) + .then( ( response ) => { + customer = response.data; + } ); + + await use( customer ); + + // Cleanup + await api.delete( `customers/${ customer.id }`, { force: true } ); + }, + + simpleProduct: async ( { api }, use ) => { + let product = {}; + + await api + .post( 'products', { + name: `Product simple ${ random() }`, + type: 'simple', + regular_price: '100', + tax_class: 'Tax Class Simple', + } ) + .then( ( response ) => { + product = response.data; + } ); + + await use( product ); + + // Cleanup + await api.delete( `products/${ product.id }`, { force: true } ); + }, + + variableProduct: async ( { api }, use ) => { + let product = {}; + + const variations = [ + { + regular_price: '100', + attributes: [ + { + name: 'Size', + option: 'Small', + }, + { + name: 'Colour', + option: 'Yellow', + }, + ], + tax_class: 'Tax Class Variable', + }, + { + regular_price: '100', + attributes: [ + { + name: 'Size', + option: 'Medium', + }, + { + name: 'Colour', + option: 'Magenta', + }, + ], + tax_class: 'Tax Class Variable', + }, + ]; + + await api + .post( 'products', { + name: `Product variable ${ random() }`, + type: 'variable', + tax_class: 'Tax Class Variable', + } ) + .then( ( response ) => { + product = response.data; + } ); + + for ( const key in variations ) { + api.post( + `products/${ product.id }/variations`, + variations[ key ] + ); + } + + await use( product ); + + // Cleanup + await api.delete( `products/${ product.id }`, { force: true } ); + }, + + externalProduct: async ( { api }, use ) => { + let product = {}; + + await api + .post( 'products', { + name: `Product external ${ random() }`, + regular_price: '800', + tax_class: 'Tax Class External', + external_url: 'https://wordpress.org/plugins/woocommerce', + type: 'external', + button_text: 'Buy now', + } ) + .then( ( response ) => { + product = response.data; + } ); + + await use( product ); + + // Cleanup + await api.delete( `products/${ product.id }`, { force: true } ); + }, + + groupedProduct: async ( { api }, use ) => { + let product = {}; + let subProductAId; + let subProductBId; + + await api + .post( 'products', { + name: 'Add-on A', + regular_price: '11.95', + } ) + .then( ( response ) => { + subProductAId = response.data.id; + } ); + await api + .post( 'products', { + name: 'Add-on B', + regular_price: '18.97', + } ) + .then( ( response ) => { + subProductBId = response.data.id; + } ); + await api + .post( 'products', { + name: `Product grouped ${ random() }`, + regular_price: '29.99', + grouped_products: [ subProductAId, subProductBId ], + type: 'grouped', + } ) + .then( ( response ) => { + product = response.data; + } ); + + await use( product ); + + // Cleanup + await api.delete( `products/${ product.id }`, { force: true } ); + }, +} ); test.describe( 'WooCommerce Orders > Add new order', { tag: [ '@services', '@hpos' ] }, () => { - test.use( { storageState: process.env.ADMINSTATE } ); - - test.beforeAll( async ( { baseURL } ) => { - const api = new wcApi( { - url: baseURL, - consumerKey: process.env.CONSUMER_KEY, - consumerSecret: process.env.CONSUMER_SECRET, - version: 'wc/v3', - } ); + test.beforeAll( async ( { api } ) => { // enable taxes on the account await api.put( 'settings/general/woocommerce_calc_taxes', { value: 'yes', @@ -71,171 +269,9 @@ test.describe( for ( let i = 0; i < taxRates.length; i++ ) { await api.post( 'taxes', taxRates[ i ] ); } - // create simple product - await api - .post( 'products', { - name: simpleProductName, - type: 'simple', - regular_price: '100', - tax_class: 'Tax Class Simple', - } ) - .then( ( resp ) => { - simpleProductId = resp.data.id; - } ); - // create variable product - const variations = [ - { - regular_price: '100', - attributes: [ - { - name: 'Size', - option: 'Small', - }, - { - name: 'Colour', - option: 'Yellow', - }, - ], - tax_class: 'Tax Class Variable', - }, - { - regular_price: '100', - attributes: [ - { - name: 'Size', - option: 'Medium', - }, - { - name: 'Colour', - option: 'Magenta', - }, - ], - tax_class: 'Tax Class Variable', - }, - ]; - await api - .post( 'products', { - name: variableProductName, - type: 'variable', - tax_class: 'Tax Class Variable', - } ) - .then( ( response ) => { - variableProductId = response.data.id; - for ( const key in variations ) { - api.post( - `products/${ variableProductId }/variations`, - variations[ key ] - ); - } - } ); - // create external product - await api - .post( 'products', { - name: externalProductName, - regular_price: '800', - tax_class: 'Tax Class External', - external_url: 'https://wordpress.org/plugins/woocommerce', - type: 'external', - button_text: 'Buy now', - } ) - .then( ( response ) => { - externalProductId = response.data.id; - } ); - // create grouped product - await api - .post( 'products', { - name: 'Add-on A', - regular_price: '11.95', - } ) - .then( ( response ) => { - subProductAId = response.data.id; - } ); - await api - .post( 'products', { - name: 'Add-on B', - regular_price: '18.97', - } ) - .then( ( response ) => { - subProductBId = response.data.id; - } ); - await api - .post( 'products', { - name: groupedProductName, - regular_price: '29.99', - grouped_products: [ subProductAId, subProductBId ], - type: 'grouped', - } ) - .then( ( response ) => { - groupedProductId = response.data.id; - } ); - // create a customer - await api - .post( 'customers', { - email: 'sideshowbob@example.com', - first_name: 'Sideshow', - last_name: 'Bob', - username: 'sideshowbob', - billing: { - first_name: 'Sideshow', - last_name: 'Bob', - company: 'Die Bart Die', - address_1: '123 Fake St', - address_2: '', - city: 'Springfield', - state: 'FL', - postcode: '12345', - country: 'US', - email: 'sideshowbob@example.com', - phone: '555-555-5556', - }, - shipping: { - first_name: 'Sideshow', - last_name: 'Bob', - company: 'Die Bart Die', - address_1: '321 Fake St', - address_2: '', - city: 'Springfield', - state: 'FL', - postcode: '12345', - country: 'US', - }, - } ) - .then( ( response ) => { - customerId = response.data.id; - } ); } ); - test.afterEach( async ( { baseURL } ) => { - const api = new wcApi( { - url: baseURL, - consumerKey: process.env.CONSUMER_KEY, - consumerSecret: process.env.CONSUMER_SECRET, - version: 'wc/v3', - } ); - // clean up order after each test - if ( orderId && orderId !== '' ) { - await api.delete( `orders/${ orderId }`, { force: true } ); - } - } ); - - test.afterAll( async ( { baseURL } ) => { - const api = new wcApi( { - url: baseURL, - consumerKey: process.env.CONSUMER_KEY, - consumerSecret: process.env.CONSUMER_SECRET, - version: 'wc/v3', - } ); - // cleans up all products after run - await api.post( 'products/batch', { - delete: [ - simpleProductId, - variableProductId, - externalProductId, - subProductAId, - subProductBId, - groupedProductId, - ], - } ); + test.afterAll( async ( { api } ) => { // clean up tax classes and rates for ( const { slug } of taxClasses ) { await api @@ -258,19 +294,15 @@ test.describe( await api.put( 'settings/general/woocommerce_calc_taxes', { value: 'no', } ); - // clean up customer - await api.delete( `customers/${ customerId }`, { force: true } ); } ); - test( 'can create a simple guest order', async ( { page } ) => { + test( 'can create a simple guest order', async ( { + page, + simpleProduct, + order, + } ) => { await page.goto( 'wp-admin/admin.php?page=wc-orders&action=new' ); - - // get order ID from the page - const orderText = await page - .locator( 'h2.woocommerce-order-data__heading' ) - .textContent(); - orderId = orderText.match( /([0-9])\w+/ ); - orderId = orderId[ 0 ].toString(); + order.id = await getOrderIdFromPage( page ); await page .locator( '#order_status' ) @@ -334,22 +366,7 @@ test.describe( .fill( 'Only asked for a slushie' ); // Add a product - await page.getByRole( 'button', { name: 'Add item(s)' } ).click(); - await page - .getByRole( 'button', { name: 'Add product(s)' } ) - .click(); - await page.getByText( 'Search for a product…' ).click(); - await page - .locator( 'span > .select2-search__field' ) - .fill( 'Simple' ); - await page - .getByRole( 'option', { name: simpleProductName } ) - .click(); - await page - .getByRole( 'row', { name: '×Add new order simple product' } ) - .getByPlaceholder( '1' ) - .fill( '2' ); - await page.locator( '#btn-ok' ).click(); + await addProductToOrder( page, simpleProduct, 2 ); // Create the order await page.getByRole( 'button', { name: 'Create' } ).click(); @@ -375,40 +392,26 @@ test.describe( test( 'can create an order for an existing customer', async ( { page, + simpleProduct, + customer, + order, } ) => { await page.goto( 'wp-admin/admin.php?page=wc-orders&action=new' ); - - // get order ID from the page - const orderText = await page - .locator( 'h2.woocommerce-order-data__heading' ) - .textContent(); - orderId = orderText.match( /([0-9])\w+/ ); - orderId = orderId[ 0 ].toString(); + order.id = await getOrderIdFromPage( page ); // Select customer await page.getByText( 'Guest' ).click(); await page .locator( 'input[aria-owns="select2-customer_user-results"]' ) - .fill( 'sideshowbob@' ); - await page.getByRole( 'option', { name: 'Sideshow Bob' } ).click(); + .fill( customer.username ); + await page + .getByRole( 'option', { + name: `${ customer.first_name } ${ customer.last_name }`, + } ) + .click(); // Add a product - await page.getByRole( 'button', { name: 'Add item(s)' } ).click(); - await page - .getByRole( 'button', { name: 'Add product(s)' } ) - .click(); - await page.getByText( 'Search for a product…' ).click(); - await page - .locator( 'span > .select2-search__field' ) - .fill( 'Simple' ); - await page - .getByRole( 'option', { name: simpleProductName } ) - .click(); - await page - .getByRole( 'row', { name: '×Add new order simple product' } ) - .getByPlaceholder( '1' ) - .fill( '2' ); - await page.locator( '#btn-ok' ).click(); + await addProductToOrder( page, simpleProduct, 2 ); // Create the order await page.getByRole( 'button', { name: 'Create' } ).click(); @@ -431,12 +434,14 @@ test.describe( // View customer profile await page.getByRole( 'link', { name: 'Profile →' } ).click(); await expect( - page.getByRole( 'heading', { name: 'Edit User sideshowbob' } ) + page.getByRole( 'heading', { + name: `Edit User ${ customer.username }`, + } ) ).toBeVisible(); // Go back to the order await page.goto( - `wp-admin/admin.php?page=wc-orders&action=edit&id=${ orderId }` + `wp-admin/admin.php?page=wc-orders&action=edit&id=${ order.id }` ); await page .getByRole( 'link', { @@ -449,17 +454,12 @@ test.describe( await expect( page.getByRole( 'row' ) ).toHaveCount( 3 ); // 1 order and header and footer rows } ); - test( 'can create new order', async ( { page } ) => { + test( 'can create new order', async ( { page, order } ) => { await page.goto( 'wp-admin/admin.php?page=wc-orders&action=new' ); await expect( page.locator( 'h1.wp-heading-inline' ) ).toContainText( 'Add new order' ); - // get order ID from the page - const orderText = await page - .locator( 'h2.woocommerce-order-data__heading' ) - .textContent(); - orderId = orderText.match( /([0-9])\w+/ ); - orderId = orderId[ 0 ].toString(); + order.id = await getOrderIdFromPage( page ); await page .locator( '#order_status' ) @@ -488,74 +488,51 @@ test.describe( test( 'can create new complex order with multiple product types & tax classes', async ( { page, + simpleProduct, + variableProduct, + externalProduct, + groupedProduct, + order, } ) => { - orderId = ''; await page.goto( 'wp-admin/admin.php?page=wc-orders&action=new' ); + order.id = await getOrderIdFromPage( page ); // open modal for adding line items await page.locator( 'button.add-line-item' ).click(); await page.locator( 'button.add-order-item' ).click(); // search for each product to add - await page.locator( 'text=Search for a product…' ).click(); - await page - .locator( '.select2-search--dropdown' ) - .getByRole( 'combobox' ) - .pressSequentially( simpleProductName ); - await page - .locator( - 'li.select2-results__option.select2-results__option--highlighted' - ) - .click(); - - await page.locator( 'text=Search for a product…' ).click(); - await page - .locator( '.select2-search--dropdown' ) - .getByRole( 'combobox' ) - .pressSequentially( variableProductName ); - await page - .locator( - 'li.select2-results__option.select2-results__option--highlighted' - ) - .click(); - - await page.locator( 'text=Search for a product…' ).click(); - await page - .locator( '.select2-search--dropdown' ) - .getByRole( 'combobox' ) - .type( groupedProductName ); - await page - .locator( - 'li.select2-results__option.select2-results__option--highlighted' - ) - .click(); - - await page.locator( 'text=Search for a product…' ).click(); - await page - .locator( '.select2-search--dropdown' ) - .getByRole( 'combobox' ) - .type( externalProductName ); - await page - .locator( - 'li.select2-results__option.select2-results__option--highlighted' - ) - .click(); + for ( const product of [ + simpleProduct, + variableProduct, + groupedProduct, + externalProduct, + ] ) { + await page.getByText( 'Search for a product…' ).click(); + await page + .locator( 'span > .select2-search__field' ) + .fill( product.name ); + await page + .getByRole( 'option', { name: product.name } ) + .first() + .click(); + } await page.locator( 'button#btn-ok' ).click(); // assert that products added await expect( page.locator( 'td.name > a >> nth=0' ) - ).toContainText( simpleProductName ); + ).toContainText( simpleProduct.name ); await expect( page.locator( 'td.name > a >> nth=1' ) - ).toContainText( variableProductName ); + ).toContainText( variableProduct.name ); await expect( page.locator( 'td.name > a >> nth=2' ) - ).toContainText( groupedProductName ); + ).toContainText( groupedProduct.name ); await expect( page.locator( 'td.name > a >> nth=3' ) - ).toContainText( externalProductName ); + ).toContainText( externalProduct.name ); // Recalculate taxes page.on( 'dialog', ( dialog ) => dialog.accept() ); From 41cf2b285e8ee690b1ea804b6a774345d7144225 Mon Sep 17 00:00:00 2001 From: Roy Ho Date: Wed, 11 Sep 2024 12:11:51 -0700 Subject: [PATCH 10/59] [Experimental] Product filters/overlay nav block redesign (#51211) * Remove overlay icon settings from parent block * Change overlay block icon to generic button icon * Add logic to remove overlay nav when overlay mode is set to never * Add logic to add overlay nav when overlay mode is not never * Add default attributes for overlay nav button * Add changefile(s) from automation for the following project(s): woocommerce-blocks * Use unique icons for each overlay navigation close and open * Add icon picker for open overlay navigation * Add trigger type for overlay close navigation * Add missing settings label * Fix e2e tests * Skip a test for overlay-navigation block * Add e2e tests for overlay button behavior * Fix linting error * Skip overlay nav tests * Fix icon size value not saving * Revise logic to target explicitly the innerblock of product-filters * Ensure overlay navigation is of type open-overlay * Prevent possible race conditions --------- Co-authored-by: github-actions --- .../js/blocks/product-filters/block.json | 12 - .../assets/js/blocks/product-filters/edit.tsx | 213 ++++++++---------- .../overlay-navigation/block-variations.tsx | 3 + .../overlay-navigation/block.json | 6 +- .../inner-blocks/overlay-navigation/edit.tsx | 37 ++- .../inner-blocks/overlay-navigation/index.tsx | 2 +- .../overlay-navigation/inspector-controls.tsx | 60 ++++- .../inner-blocks/overlay-navigation/types.ts | 1 + ...ers-overlay-navigation.block_theme.spec.ts | 10 +- .../product-filters.block_theme.spec.ts | 80 +++---- .../51211-product-filters-overlay-nav-block | 4 + .../parts/product-filters-overlay.html | 2 +- 12 files changed, 237 insertions(+), 193 deletions(-) create mode 100644 plugins/woocommerce/changelog/51211-product-filters-overlay-nav-block diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/block.json index ec5bd9b5d3c..b8d3b6457fb 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/block.json +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/block.json @@ -53,18 +53,6 @@ "overlay": { "type": "string", "default": "never" - }, - "overlayIcon": { - "type": "string", - "default": "filter-icon-1" - }, - "overlayButtonStyle": { - "type": "string", - "default": "label-icon" - }, - "overlayIconSize": { - "type": "number", - "default": "12" } }, "viewScript": "wc-product-filters-frontend", diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx index de6fbd958cb..09d6521991e 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx @@ -1,7 +1,6 @@ /** * External dependencies */ -import { filter, filterThreeLines } from '@woocommerce/icons'; import { getSetting } from '@woocommerce/settings'; import { AttributeSetting } from '@woocommerce/types'; import { @@ -10,14 +9,19 @@ import { useBlockProps, useInnerBlocksProps, } from '@wordpress/block-editor'; -import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks'; +import { + BlockEditProps, + BlockInstance, + InnerBlockTemplate, + createBlock, +} from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; -import { Icon, menu, settings } from '@wordpress/icons'; +import { useEffect } from '@wordpress/element'; +import { select, dispatch } from '@wordpress/data'; +import { useLocalStorageState } from '@woocommerce/base-hooks'; import { ExternalLink, PanelBody, - RadioControl, - RangeControl, // @ts-expect-error - no types. // eslint-disable-next-line @wordpress/no-unsafe-wp-apis __experimentalToggleGroupControl as ToggleGroupControl, @@ -111,6 +115,7 @@ const TEMPLATE: InnerBlockTemplate[] = [ export const Edit = ( { setAttributes, attributes, + clientId, }: BlockEditProps< BlockAttributes > ) => { const blockProps = useBlockProps(); @@ -119,6 +124,84 @@ export const Edit = ( { '' ); + const [ + productFiltersOverlayNavigationAttributes, + setProductFiltersOverlayNavigationAttributes, + ] = useLocalStorageState< Record< string, unknown > >( + 'product-filters-overlay-navigation-attributes', + {} + ); + + useEffect( () => { + const filtersClientIds = select( 'core/block-editor' ).getBlocksByName( + 'woocommerce/product-filters' + ); + + let overlayBlock: + | BlockInstance< { [ k: string ]: unknown } > + | undefined; + + for ( const filterClientId of filtersClientIds ) { + const filterBlock = + select( 'core/block-editor' ).getBlock( filterClientId ); + + if ( filterBlock ) { + for ( const innerBlock of filterBlock.innerBlocks ) { + if ( + innerBlock.name === + 'woocommerce/product-filters-overlay-navigation' && + innerBlock.attributes.triggerType === 'open-overlay' + ) { + overlayBlock = innerBlock; + } + } + } + } + + if ( attributes.overlay === 'never' && overlayBlock ) { + setProductFiltersOverlayNavigationAttributes( + overlayBlock.attributes + ); + + dispatch( 'core/block-editor' ).updateBlockAttributes( + overlayBlock.clientId, + { + lock: {}, + } + ); + + dispatch( 'core/block-editor' ).removeBlock( + overlayBlock.clientId + ); + } else if ( attributes.overlay !== 'never' && ! overlayBlock ) { + if ( productFiltersOverlayNavigationAttributes ) { + productFiltersOverlayNavigationAttributes.triggerType = + 'open-overlay'; + } + + dispatch( 'core/block-editor' ).insertBlock( + createBlock( + 'woocommerce/product-filters-overlay-navigation', + productFiltersOverlayNavigationAttributes + ? productFiltersOverlayNavigationAttributes + : { + align: 'left', + triggerType: 'open-overlay', + lock: { move: true, remove: true }, + } + ), + 0, + clientId, + false + ); + } + }, [ + attributes.overlay, + clientId, + productFiltersOverlayNavigationAttributes, + setProductFiltersOverlayNavigationAttributes, + ] ); + return (
@@ -144,126 +227,6 @@ export const Edit = ( { label={ __( 'Always', 'woocommerce' ) } /> - { attributes.overlay === 'mobile' && ( - <> - { - setAttributes( { - overlayButtonStyle: value, - } ); - } } - options={ [ - { - value: 'label-icon', - label: __( - 'Label and icon', - 'woocommerce' - ), - }, - { - value: 'label', - label: __( - 'Label only', - 'woocommerce' - ), - }, - { - value: 'icon', - label: __( 'Icon only', 'woocommerce' ), - }, - ] } - /> - { attributes.overlayButtonStyle !== 'label' && ( - <> - { - setAttributes( { - overlayIcon: value, - } ); - } } - > - - } - /> - - } - /> - - } - /> - - } - /> - - - setAttributes( { - overlayIconSize: value, - } ) - } - min={ 20 } - max={ 80 } - /> - - ) } - - ) } { attributes.overlay !== 'never' && ( , + isActive: [ 'triggerType' ], }, ]; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/block.json index f876968991c..2670f83d872 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/block.json +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/block.json @@ -20,12 +20,16 @@ "default": "link" }, "iconSize": { - "type": "string" + "type": "number" }, "overlayMode": { "type": "string", "default": "never" }, + "overlayIcon": { + "type": "string", + "default": "filter-icon-1" + }, "style": { "type": "object", "default": { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/edit.tsx index 43e026f95cf..8c9a6bee4b5 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/edit.tsx @@ -6,7 +6,8 @@ import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; import { BlockEditProps, store as blocksStore } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; import clsx from 'clsx'; -import { Icon, close } from '@wordpress/icons'; +import { Icon, close, menu, settings } from '@wordpress/icons'; +import { filter, filterThreeLines } from '@woocommerce/icons'; /** * Internal dependencies @@ -16,7 +17,6 @@ import type { BlockContext, BlockVariationTriggerType, } from './types'; -import { default as productFiltersIcon } from '../../icon'; import { BlockOverlayAttribute as ProductFiltersBlockOverlayAttribute } from '../../constants'; import './editor.scss'; import { Inspector } from './inspector-controls'; @@ -37,16 +37,33 @@ const OverlayNavigationLabel = ( { const OverlayNavigationIcon = ( { variation, iconSize, + overlayIcon, style, }: { variation: BlockVariationTriggerType; iconSize: number | undefined; + overlayIcon: string; style: BlockAttributes[ 'style' ]; } ) => { let icon = close; if ( variation === 'open-overlay' ) { - icon = productFiltersIcon(); + switch ( overlayIcon ) { + case 'filter-icon-4': + icon = settings; + break; + case 'filter-icon-3': + icon = menu; + break; + case 'filter-icon-2': + icon = filterThreeLines; + break; + case 'filter-icon-1': + icon = filter; + break; + default: + icon = filter; + } } return ( @@ -65,11 +82,13 @@ const OverlayNavigationContent = ( { variation, iconSize, style, + overlayIcon, navigationStyle, }: { variation: BlockVariationTriggerType; iconSize: BlockAttributes[ 'iconSize' ]; style: BlockAttributes[ 'style' ]; + overlayIcon: BlockAttributes[ 'overlayIcon' ]; navigationStyle: BlockAttributes[ 'navigationStyle' ]; } ) => { const overlayNavigationLabel = ( @@ -79,6 +98,7 @@ const OverlayNavigationContent = ( { ); @@ -111,8 +131,14 @@ const OverlayNavigationContent = ( { type BlockProps = BlockEditProps< BlockAttributes > & { context: BlockContext }; export const Edit = ( { attributes, setAttributes, context }: BlockProps ) => { - const { navigationStyle, buttonStyle, iconSize, style, triggerType } = - attributes; + const { + navigationStyle, + buttonStyle, + iconSize, + overlayIcon, + style, + triggerType, + } = attributes; const { 'woocommerce/product-filters/overlay': productFiltersOverlayMode } = context; const blockProps = useBlockProps( { @@ -214,6 +240,7 @@ export const Edit = ( { attributes, setAttributes, context }: BlockProps ) => { variation={ triggerType } iconSize={ iconSize } navigationStyle={ navigationStyle } + overlayIcon={ overlayIcon } style={ style } />
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/index.tsx index b5f9b97a83b..000ea4807fe 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/index.tsx @@ -2,8 +2,8 @@ * External dependencies */ import { registerBlockType } from '@wordpress/blocks'; -import { Icon } from '@wordpress/icons'; import { closeSquareShadow } from '@woocommerce/icons'; +import { Icon } from '@wordpress/icons'; import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; /** diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/inspector-controls.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/inspector-controls.tsx index 16c7352cfdf..847d7369082 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/inspector-controls.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/inspector-controls.tsx @@ -4,6 +4,8 @@ import { InspectorControls } from '@wordpress/block-editor'; import { BlockEditProps } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; +import { filter, filterThreeLines } from '@woocommerce/icons'; +import { Icon, menu, settings } from '@wordpress/icons'; import { PanelBody, RadioControl, @@ -34,7 +36,8 @@ export const Inspector = ( { setAttributes, buttonStyles, }: InspectorProps ) => { - const { navigationStyle, buttonStyle, iconSize } = attributes; + const { navigationStyle, buttonStyle, iconSize, overlayIcon, triggerType } = + attributes; return ( @@ -100,6 +103,61 @@ export const Inspector = ( { /> ) } + { triggerType === 'open-overlay' && + navigationStyle !== 'label-only' && ( + { + setAttributes( { + overlayIcon: value, + } ); + } } + > + } + /> + + } + /> + } + /> + } + /> + + ) } + { navigationStyle !== 'label-only' && ( { } ); } ); - test( 'should be included in the Filters Overlay template part', async ( { + // Since we need to overhaul the overlay area, we can skip this test for now. + // eslint-disable-next-line playwright/no-skipped-test + test.skip( 'should be included in the Filters Overlay template part', async ( { editor, } ) => { const block = editor.canvas.getByLabel( `Block: ${ blockData.title }` ); await expect( block ).toBeVisible(); } ); - test( 'should have settings and styles controls', async ( { editor } ) => { + // Since we need to overhaul the overlay area, we can skip this test for now. + // eslint-disable-next-line playwright/no-skipped-test + test.skip( 'should have settings and styles controls', async ( { + editor, + } ) => { const block = editor.canvas.getByLabel( `Block: ${ blockData.title }` ); await block.click(); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts index aa019e69ed7..887dcf60e20 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts @@ -17,6 +17,16 @@ const blockData = { settings: {}, layoutWrapper: '.wp-block-woocommerce-product-filters-is-layout-flex', + blocks: { + filters: { + title: 'Product Filters (Experimental)', + label: 'Block: Product Filters (Experimental)', + }, + overlay: { + title: 'Overlay Navigation (Experimental)', + label: 'Block: Overlay Navigation (Experimental)', + }, + }, }, }, slug: 'archive-product', @@ -54,7 +64,7 @@ test.describe( `${ blockData.name }`, () => { await pageObject.addProductFiltersBlock( { cleanContent: true } ); const block = editor.canvas.getByLabel( - 'Block: Product Filters (Experimental)' + blockData.selectors.editor.blocks.filters.label ); await expect( block ).toBeVisible(); @@ -142,7 +152,7 @@ test.describe( `${ blockData.name }`, () => { await pageObject.addProductFiltersBlock( { cleanContent: true } ); const block = editor.canvas.getByLabel( - 'Block: Product Filters (Experimental)' + blockData.selectors.editor.blocks.filters.label ); await expect( block ).toBeVisible(); @@ -152,7 +162,7 @@ test.describe( `${ blockData.name }`, () => { await expect( listView ).toBeVisible(); const productFiltersBlockListItem = listView.getByRole( 'link', { - name: 'Product Filters (Experimental)', + name: blockData.selectors.editor.blocks.filters.title, } ); await expect( productFiltersBlockListItem ).toBeVisible(); const listViewExpander = @@ -199,7 +209,7 @@ test.describe( `${ blockData.name }`, () => { await pageObject.addProductFiltersBlock( { cleanContent: true } ); const block = editor.canvas.getByLabel( - 'Block: Product Filters (Experimental)' + blockData.selectors.editor.blocks.filters.label ); await expect( block ).toBeVisible(); @@ -246,10 +256,17 @@ test.describe( `${ blockData.name }`, () => { } ) => { await pageObject.addProductFiltersBlock( { cleanContent: true } ); - const block = editor.canvas.getByLabel( - 'Block: Product Filters (Experimental)' + const filtersBlock = editor.canvas.getByLabel( + blockData.selectors.editor.blocks.filters.label ); - await expect( block ).toBeVisible(); + await expect( filtersBlock ).toBeVisible(); + + const overlayBlock = editor.canvas.getByLabel( + blockData.selectors.editor.blocks.overlay.label + ); + + // Overlay mode is set to 'Never' by default so the block should be hidden + await expect( overlayBlock ).toBeHidden(); await editor.openDocumentSettingsSidebar(); @@ -260,17 +277,6 @@ test.describe( `${ blockData.name }`, () => { // Overlay settings const overlayModeSettings = [ 'Never', 'Mobile', 'Always' ]; - const overlayButtonSettings = [ - 'Label and icon', - 'Label only', - 'Icon only', - ]; - const overlayIconsSettings = [ - 'Filter icon 1', - 'Filter icon 2', - 'Filter icon 3', - 'Filter icon 4', - ]; await expect( editor.page.getByText( 'Overlay' ) ).toBeVisible(); @@ -278,43 +284,27 @@ test.describe( `${ blockData.name }`, () => { await expect( editor.page.getByText( mode ) ).toBeVisible(); } + await editor.page.getByLabel( 'Never' ).click(); + + await expect( editor.page.getByText( 'Edit overlay' ) ).toBeHidden(); + + await expect( overlayBlock ).toBeHidden(); + await editor.page.getByLabel( 'Mobile' ).click(); - await expect( editor.page.getByText( 'BUTTON' ) ).toBeVisible(); - for ( const mode of overlayButtonSettings ) { - await expect( editor.page.getByText( mode ) ).toBeVisible(); - } - - for ( const mode of overlayIconsSettings ) { - await expect( editor.page.getByLabel( mode ) ).toBeVisible(); - } - - await expect( editor.page.getByText( 'ICON SIZE' ) ).toBeVisible(); await expect( editor.page.getByText( 'Edit overlay' ) ).toBeVisible(); + await expect( overlayBlock ).toBeVisible(); + await editor.page.getByLabel( 'Always' ).click(); - await expect( editor.page.getByText( 'BUTTON' ) ).toBeHidden(); - - for ( const mode of overlayButtonSettings ) { - await expect( editor.page.getByText( mode ) ).toBeHidden(); - } - - for ( const mode of overlayIconsSettings ) { - await expect( editor.page.getByLabel( mode ) ).toBeHidden(); - } - await expect( editor.page.getByText( 'Edit overlay' ) ).toBeVisible(); - await editor.page.getByLabel( 'Mobile' ).click(); + await expect( overlayBlock ).toBeVisible(); - await editor.page.locator( 'input[value="label"]' ).click(); + await editor.page.getByLabel( 'Never' ).click(); - for ( const mode of overlayIconsSettings ) { - await expect( editor.page.getByLabel( mode ) ).toBeHidden(); - } - - await expect( editor.page.getByText( 'Edit overlay' ) ).toBeVisible(); + await expect( overlayBlock ).toBeHidden(); } ); test( 'Layout > default to vertical stretch', async ( { diff --git a/plugins/woocommerce/changelog/51211-product-filters-overlay-nav-block b/plugins/woocommerce/changelog/51211-product-filters-overlay-nav-block new file mode 100644 index 00000000000..ba8c4d767e8 --- /dev/null +++ b/plugins/woocommerce/changelog/51211-product-filters-overlay-nav-block @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak +Comment: Product Filters: update overlay navigation UX + diff --git a/plugins/woocommerce/templates/parts/product-filters-overlay.html b/plugins/woocommerce/templates/parts/product-filters-overlay.html index c50b5c3c386..0e39b331cf8 100644 --- a/plugins/woocommerce/templates/parts/product-filters-overlay.html +++ b/plugins/woocommerce/templates/parts/product-filters-overlay.html @@ -1,6 +1,6 @@
- +
From ed9742f18b6a4573b93809f52d877fa8b4eda984 Mon Sep 17 00:00:00 2001 From: Karol Manijak <20098064+kmanijak@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:37:51 +0200 Subject: [PATCH 11/59] Skip failing tests from experimental Product filters feature (#51305) * Skip failing tests from experimental feature * Add changelog --- ...product-filters-overlay-template-part.block_theme.spec.ts | 4 +++- plugins/woocommerce/changelog/test-skip-failing-tests | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/test-skip-failing-tests diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts index a5e02ea94fa..4dae7c49c84 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts @@ -174,7 +174,9 @@ test.describe( 'Filters Overlay Template Part', () => { await expect( productFiltersDialog ).toBeHidden(); } ); - test( 'should hide Product Filters Overlay Navigation block when the Overlay mode is set to `Never`', async ( { + // Since we need to overhaul the overlay area, we can skip this test for now. + // eslint-disable-next-line playwright/no-skipped-test + test.skip( 'should hide Product Filters Overlay Navigation block when the Overlay mode is set to `Never`', async ( { editor, page, frontendUtils, diff --git a/plugins/woocommerce/changelog/test-skip-failing-tests b/plugins/woocommerce/changelog/test-skip-failing-tests new file mode 100644 index 00000000000..723af34edc0 --- /dev/null +++ b/plugins/woocommerce/changelog/test-skip-failing-tests @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: It's a test skip for experimental feature, not worth explaining + + From c96f9f38bdcd1c4dead8d9d18c336375702cb166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20P=C3=A9rez=20Pellicer?= <5908855+puntope@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:07:28 +0400 Subject: [PATCH 12/59] Reduce slow reports tests (#51279) * Fix slow tests * Lint * Changelog * Lint * Lint --- plugins/woocommerce/changelog/dev-slow-tests | 4 ++ .../helpers/class-wc-helper-queue.php | 32 ++++++++------ .../api/products-lowinstock.php | 2 +- .../api/reports-categories.php | 8 ++-- .../api/reports-coupons-stats.php | 2 +- .../woocommerce-admin/api/reports-coupons.php | 4 +- .../api/reports-customers-stats.php | 2 +- .../api/reports-customers.php | 44 +++++++++---------- .../api/reports-downloads-stats.php | 2 +- .../api/reports-downloads.php | 2 +- .../woocommerce-admin/api/reports-export.php | 4 +- .../woocommerce-admin/api/reports-import.php | 10 ++--- .../api/reports-orders-stats.php | 5 ++- .../woocommerce-admin/api/reports-orders.php | 13 +++--- .../api/reports-performance-indicators.php | 2 +- .../api/reports-products-stats.php | 2 +- .../api/reports-products.php | 4 +- .../api/reports-taxes-stats.php | 2 +- .../woocommerce-admin/api/reports-taxes.php | 4 +- .../api/reports-variations.php | 6 +-- .../class-wc-tests-reports-coupons-stats.php | 4 +- .../class-wc-tests-reports-coupons.php | 4 +- .../class-wc-tests-reports-customers.php | 8 ++-- .../class-wc-tests-reports-orders-stats.php | 30 ++++++------- .../reports/class-wc-tests-reports-orders.php | 8 ++-- .../class-wc-tests-reports-products.php | 12 ++--- .../class-wc-tests-reports-revenue-stats.php | 2 +- .../class-wc-tests-reports-variations.php | 6 +-- 28 files changed, 120 insertions(+), 108 deletions(-) create mode 100644 plugins/woocommerce/changelog/dev-slow-tests diff --git a/plugins/woocommerce/changelog/dev-slow-tests b/plugins/woocommerce/changelog/dev-slow-tests new file mode 100644 index 00000000000..d60c7d44a53 --- /dev/null +++ b/plugins/woocommerce/changelog/dev-slow-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix slow test for Reports diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-queue.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-queue.php index a2f4e0acfb8..50f3c8766a4 100644 --- a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-queue.php +++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-queue.php @@ -13,36 +13,42 @@ class WC_Helper_Queue { /** * Get all pending queued actions. - * + * @param string|null $group Optionally. Filter the actions by group. * @return array Pending jobs. */ - public static function get_all_pending() { - $jobs = WC()->queue()->search( - array( - 'per_page' => -1, - 'status' => 'pending', - 'claimed' => false, - ) + public static function get_all_pending( $group = null ) { + $args = array( + 'per_page' => -1, + 'status' => 'pending', + 'claimed' => false, ); - return $jobs; + if ( $group ) { + $args['group'] = $group; + } + + return WC()->queue()->search( $args ); } + + /** * Run all pending queued actions. - * + * @param string|null $group Optionally. Filter the actions by group. * @return void */ - public static function run_all_pending() { + public static function run_all_pending( $group = null ) { $queue_runner = new ActionScheduler_QueueRunner(); - - while ( $jobs = self::get_all_pending() ) { + $jobs = self::get_all_pending( $group ); + while ( $jobs ) { foreach ( $jobs as $job_id => $job ) { $queue_runner->process_action( $job_id ); } + $jobs = self::get_all_pending( $group ); } } + /** * Cancel all pending actions. * diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/products-lowinstock.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/products-lowinstock.php index b85177b3221..489a4d66fc9 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/products-lowinstock.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/products-lowinstock.php @@ -51,7 +51,7 @@ class WC_Admin_Tests_API_ProductsLowInStock extends WC_REST_Unit_Test_Case { $order->save(); // Sync analytics data (used for last order date). - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', '/wc-analytics/products/low-in-stock' ); $request->set_param( 'low_in_stock', true ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-categories.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-categories.php index f5f37b9404d..7b704fc7b4a 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-categories.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-categories.php @@ -66,7 +66,7 @@ class WC_Admin_Tests_API_Reports_Categories extends WC_REST_Unit_Test_Case { $order->set_total( 100 ); // $25 x 4. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $uncategorized_term = get_term_by( 'slug', 'uncategorized', 'product_cat' ); @@ -114,7 +114,7 @@ class WC_Admin_Tests_API_Reports_Categories extends WC_REST_Unit_Test_Case { $product->set_category_ids( array( $second_category_id ) ); $product->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $uncategorized_term = get_term_by( 'slug', 'uncategorized', 'product_cat' ); @@ -170,7 +170,7 @@ class WC_Admin_Tests_API_Reports_Categories extends WC_REST_Unit_Test_Case { $order->set_total( 100 ); // $25 x 4. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Populate all of the data. $product = new WC_Product_Simple(); @@ -186,7 +186,7 @@ class WC_Admin_Tests_API_Reports_Categories extends WC_REST_Unit_Test_Case { $order->set_total( 400 ); // $100 x 4. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $uncategorized_term = get_term_by( 'slug', 'uncategorized', 'product_cat' ); $params = array( diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-coupons-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-coupons-stats.php index 9ebdcb6db25..86de2a966a4 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-coupons-stats.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-coupons-stats.php @@ -87,7 +87,7 @@ class WC_Admin_Tests_API_Reports_Coupons_Stats extends WC_REST_Unit_Test_Case { $order_2c->set_date_created( $time ); $order_2c->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $request->set_query_params( diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-coupons.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-coupons.php index ee8d5271552..b085e3ee690 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-coupons.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-coupons.php @@ -82,7 +82,7 @@ class WC_Admin_Tests_API_Reports_Coupons extends WC_REST_Unit_Test_Case { $order_2c->calculate_totals(); $order_2c->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); $coupon_reports = $response->get_data(); @@ -132,7 +132,7 @@ class WC_Admin_Tests_API_Reports_Coupons extends WC_REST_Unit_Test_Case { $order_1c->calculate_totals(); $order_1c->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $request->set_query_params( diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers-stats.php index 6b2c479bf93..0cc1ccacdc7 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers-stats.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers-stats.php @@ -127,7 +127,7 @@ class WC_Admin_Tests_API_Reports_Customers_Stats extends WC_REST_Unit_Test_Case $order->set_total( 9.12 ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $response = $this->server->dispatch( $request ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php index 24a3af46ec5..166bf344918 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php @@ -149,7 +149,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order->set_total( 100 ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $request->set_query_params( array( 'per_page' => 10 ) ); @@ -164,7 +164,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { // Creating a customer should show up regardless of orders. $customer = WC_Helper_Customer::create_customer( 'customer', 'password', 'customer@example.com' ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $request->set_query_params( @@ -221,7 +221,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order->set_total( 100 ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $request->set_query_params( @@ -317,7 +317,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order->save(); // Ensure order customer data is synced to lookup table. - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $query_params = array( 'force_cache_refresh' => true, @@ -400,7 +400,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $customer->set_billing_city( '' ); $customer->set_first_name( 'customer_andrei_1' ); $customer->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request->set_query_params( array( @@ -433,7 +433,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { // Test filter_empty param by state and postcode non empty. $customer = WC_Helper_Customer::create_customer( 'customer_2', 'password', 'customer_2@example.com' ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request->set_query_params( array( @@ -498,7 +498,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order->set_total( 100 ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $response = $this->server->dispatch( $request ); @@ -527,7 +527,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order->set_total( 100 ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $response = $this->server->dispatch( $request ); @@ -555,7 +555,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order->set_total( 100 ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $response = $this->server->dispatch( $request ); @@ -584,7 +584,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { // Creating a customer should show up regardless of orders. $customer = WC_Helper_Customer::create_customer( 'deleteme', 'password', 'deleteme@example.com' ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $request->set_query_params( @@ -602,7 +602,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { // Delete the user associated with the customer. wp_delete_user( $customer->get_id() ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Verify they are gone. $request = new WP_REST_Request( 'GET', $this->endpoint ); @@ -630,7 +630,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order->set_total( 100 ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // update order info. $order->set_billing_city( 'Random' ); @@ -638,11 +638,11 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order->set_billing_postcode( '54321' ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $result = CustomersDataStore::sync_order_customer( $order->get_id() ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $this->assertNotEquals( -1, $result ); @@ -671,7 +671,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order2->set_total( 100 ); $order2->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $customer_id = CustomersDataStore::get_existing_customer_id_from_order( $order ); $customer2_id = CustomersDataStore::get_existing_customer_id_from_order( $order2 ); @@ -682,11 +682,11 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order->set_billing_postcode( '54321' ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $result = CustomersDataStore::sync_order_customer( $order->get_id() ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Didn't update anything. $this->assertTrue( -1 === $result ); @@ -721,7 +721,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order3->set_billing_email( 'different@example.org' ); $order3->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $customer_id = CustomersDataStore::get_existing_customer_id_from_order( $order ); $customer2_id = CustomersDataStore::get_existing_customer_id_from_order( $order2 ); @@ -734,11 +734,11 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order3->set_billing_postcode( '54321' ); $order3->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $result = CustomersDataStore::sync_order_customer( $order3->get_id() ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Didn't update anything. $this->assertNotEquals( -1, $result ); @@ -780,7 +780,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order3->set_total( 100 ); $order3->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $customer_id = CustomersDataStore::get_existing_customer_id_from_order( $order ); $customer2_id = CustomersDataStore::get_existing_customer_id_from_order( $order2 ); @@ -794,7 +794,7 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $order->set_date_created( time() + 60 ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $latest_order = CustomersDataStore::get_last_order( $customer_id ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-downloads-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-downloads-stats.php index 0e098e40b72..8a58da14e7b 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-downloads-stats.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-downloads-stats.php @@ -152,7 +152,7 @@ class WC_Admin_Tests_API_Reports_Downloads_Stats extends WC_REST_Unit_Test_Case $order->save(); $order_1 = $order->get_id(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $download = new WC_Customer_Download(); $download->set_user_id( 1 ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-downloads.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-downloads.php index 3750eaba48c..98b10737888 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-downloads.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-downloads.php @@ -169,7 +169,7 @@ class WC_Admin_Tests_API_Reports_Downloads extends WC_REST_Unit_Test_Case { $object->set_timestamp( gmdate( 'Y-m-d H:00:00', $time - ( 2 * DAY_IN_SECONDS ) ) ); $id = $object->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); return array( 'time' => $time, diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-export.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-export.php index 48494f8fcf0..faae1f452eb 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-export.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-export.php @@ -119,7 +119,7 @@ class WC_Admin_Tests_API_Reports_Export extends WC_REST_Unit_Test_Case { $order->calculate_totals(); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Initiate an export of the taxes report. $response = $this->server->dispatch( new WP_REST_Request( 'POST', '/wc-analytics/reports/taxes/export' ) ); @@ -143,7 +143,7 @@ class WC_Admin_Tests_API_Reports_Export extends WC_REST_Unit_Test_Case { $this->assertStringMatchesFormat( '%s/wc-analytics/reports/taxes/export/%d/status', $status['_links']['self'][0]['href'] ); // Run the pending export jobs. - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Check that the status shows 100% and includes a download url. $response = $this->server->dispatch( new WP_REST_Request( 'GET', $status_route ) ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-import.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-import.php index b40e02390ea..e9cd6edb762 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-import.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-import.php @@ -119,7 +119,7 @@ class WC_Admin_Tests_API_Reports_Import extends WC_REST_Unit_Test_Case { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( 'success', $report['status'] ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', '/wc-analytics/reports/customers' ); $response = $this->server->dispatch( $request ); @@ -161,7 +161,7 @@ class WC_Admin_Tests_API_Reports_Import extends WC_REST_Unit_Test_Case { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( 'success', $report['status'] ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', '/wc-analytics/reports/customers' ); $response = $this->server->dispatch( $request ); @@ -247,7 +247,7 @@ class WC_Admin_Tests_API_Reports_Import extends WC_REST_Unit_Test_Case { } // Check that stats exist before deleting. - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', '/wc-analytics/reports/orders' ); $request->set_query_params( array( 'per_page' => 25 ) ); @@ -273,7 +273,7 @@ class WC_Admin_Tests_API_Reports_Import extends WC_REST_Unit_Test_Case { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( 'success', $report['status'] ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Check that stats have been deleted. $request = new WP_REST_Request( 'GET', '/wc-analytics/reports/orders' ); @@ -375,7 +375,7 @@ class WC_Admin_Tests_API_Reports_Import extends WC_REST_Unit_Test_Case { $this->assertEquals( 0, $report['orders']['imported'] ); $this->assertEquals( 4, $report['orders']['total'] ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Test import status after processing. $request = new WP_REST_Request( 'GET', $this->endpoint . '/status' ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders-stats.php index 18c8b856222..08b2a41cf74 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders-stats.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders-stats.php @@ -154,7 +154,7 @@ class WC_Admin_Tests_API_Reports_Orders_Stats extends WC_REST_Unit_Test_Case { $global_attribute = new WC_Product_Attribute(); $global_attribute->set_id( $size_attr_id ); $global_attribute->set_name( 'pa_size' ); - $global_attribute->set_options( array( $large_term->term_id ) ); // Set to small. + $global_attribute->set_options( array( $large_term->term_id ) ); // Set to large. $global_attribute->set_position( 1 ); $global_attribute->set_visible( true ); $global_attribute->set_variation( false ); @@ -187,7 +187,8 @@ class WC_Admin_Tests_API_Reports_Orders_Stats extends WC_REST_Unit_Test_Case { $order->save(); } - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); + WC_Helper_Queue::run_all_pending( 'woocommerce-db-updates' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $request->set_query_params( array( 'per_page' => 15 ) ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders.php index 38aeb49eed6..2a695b5d9ff 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders.php @@ -69,7 +69,7 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case { $order->set_total( 100 ); // $25 x 4. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $expected_customer_id = CustomersDataStore::get_customer_id_by_user_id( 1 ); @@ -126,7 +126,7 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case { ) ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); $reports = $response->get_data(); @@ -238,7 +238,8 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case { $order->save(); } - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); + WC_Helper_Queue::run_all_pending( 'woocommerce-db-updates' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $request->set_query_params( array( 'per_page' => 15 ) ); @@ -369,7 +370,7 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case { $order->save(); } - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $request->set_query_params( array( 'per_page' => 15 ) ); @@ -438,7 +439,7 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case { $order_to_be_included->set_status( 'completed' ); $order_to_be_included->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Test product exclusion. $request = new WP_REST_Request( 'GET', $this->endpoint ); @@ -515,7 +516,7 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case { $second_order->set_status( 'on-hold' ); $second_order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Get the created orders from REST API. $request = new WP_REST_Request( 'GET', $this->endpoint ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-performance-indicators.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-performance-indicators.php index 8cbab9032a1..058a40370a6 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-performance-indicators.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-performance-indicators.php @@ -90,7 +90,7 @@ class WC_Admin_Tests_API_Reports_Performance_Indicators extends WC_REST_Unit_Tes $object->set_user_ip_address( '1.2.3.4' ); $object->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $time = time(); $request = new WP_REST_Request( 'GET', $this->endpoint ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-products-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-products-stats.php index f203a65b84e..49862190354 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-products-stats.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-products-stats.php @@ -71,7 +71,7 @@ class WC_Admin_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case { $order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $request->set_query_params( diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-products.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-products.php index 9e6aade8a16..b332a5047ad 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-products.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-products.php @@ -67,7 +67,7 @@ class WC_Admin_Tests_API_Reports_Products extends WC_REST_Unit_Test_Case { $order->set_total( 100 ); // $25 x 4. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); $reports = $response->get_data(); @@ -109,7 +109,7 @@ class WC_Admin_Tests_API_Reports_Products extends WC_REST_Unit_Test_Case { $order->set_total( 100 ); // $25 x 4. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $request->set_query_params( diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-taxes-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-taxes-stats.php index 1920b20b029..9ea322b25eb 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-taxes-stats.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-taxes-stats.php @@ -99,7 +99,7 @@ class WC_Admin_Tests_API_Reports_Taxes_Stats extends WC_REST_Unit_Test_Case { $wc_refund = wc_create_refund( $refund ); } - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); $reports = $response->get_data(); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-taxes.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-taxes.php index 3f896cc5b78..cfadb1787a8 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-taxes.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-taxes.php @@ -115,7 +115,7 @@ class WC_Admin_Tests_API_Reports_Taxes extends WC_REST_Unit_Test_Case { ) ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); $reports = $response->get_data(); @@ -416,6 +416,6 @@ class WC_Admin_Tests_API_Reports_Taxes extends WC_REST_Unit_Test_Case { ) ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); } } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-variations.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-variations.php index d8906331c79..2c4a974ad1b 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-variations.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-variations.php @@ -69,7 +69,7 @@ class WC_Admin_Tests_API_Reports_Variations extends WC_REST_Unit_Test_Case { $order->set_total( 100 ); // $25 x 4. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); $reports = $response->get_data(); @@ -102,7 +102,7 @@ class WC_Admin_Tests_API_Reports_Variations extends WC_REST_Unit_Test_Case { $order->set_total( 15 ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $request->set_query_params( @@ -146,7 +146,7 @@ class WC_Admin_Tests_API_Reports_Variations extends WC_REST_Unit_Test_Case { $order->set_total( 100 ); // $25 x 4. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $request = new WP_REST_Request( 'GET', $this->endpoint ); $request->set_query_params( diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-coupons-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-coupons-stats.php index 04e1c98698d..4abbd68e1cf 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-coupons-stats.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-coupons-stats.php @@ -57,7 +57,7 @@ class WC_Admin_Tests_Reports_Coupons_Stats extends WC_Unit_Test_Case { $order_2c->calculate_totals(); $order_2c->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new CouponsStatsDataStore(); $start_time = gmdate( 'Y-m-d 00:00:00', $order->get_date_created()->getOffsetTimestamp() ); @@ -133,7 +133,7 @@ class WC_Admin_Tests_Reports_Coupons_Stats extends WC_Unit_Test_Case { // Delete the coupon. $coupon_1->delete( true ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $start_time = gmdate( 'Y-m-d 00:00:00', $order->get_date_created()->getOffsetTimestamp() ); $end_time = gmdate( 'Y-m-d 23:59:59', $order->get_date_created()->getOffsetTimestamp() ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-coupons.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-coupons.php index 867de990e28..f7d96278eeb 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-coupons.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-coupons.php @@ -59,7 +59,7 @@ class WC_Admin_Tests_Reports_Coupons extends WC_Unit_Test_Case { $order_2c->calculate_totals(); $order_2c->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new CouponsDataStore(); $start_time = gmdate( 'Y-m-d 00:00:00', $order->get_date_created()->getOffsetTimestamp() ); @@ -375,7 +375,7 @@ class WC_Admin_Tests_Reports_Coupons extends WC_Unit_Test_Case { // Delete the coupons. $coupon_2->delete( true ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new CouponsDataStore(); $start_time = gmdate( 'Y-m-d 00:00:00', $order->get_date_created()->getOffsetTimestamp() ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-customers.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-customers.php index c25c56b1376..bb7283220fe 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-customers.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-customers.php @@ -29,7 +29,7 @@ class WC_Admin_Tests_Reports_Customer extends WC_Unit_Test_Case { $product->set_regular_price( 25 ); $product->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $customer_id = DataStore::get_customer_id_by_user_id( $customer->get_id() ); // This is the customer ID from lookup table. @@ -39,7 +39,7 @@ class WC_Admin_Tests_Reports_Customer extends WC_Unit_Test_Case { $order->save(); } - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Customer should have 3 orders. $this->assertSame( 3, DataStore::get_order_count( $customer_id ) ); @@ -75,7 +75,7 @@ class WC_Admin_Tests_Reports_Customer extends WC_Unit_Test_Case { $product2->set_regular_price( 2 ); $product2->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Create the first order. $order1 = WC_Helper_Order::create_order( $customer->get_id(), $product1 ); @@ -85,7 +85,7 @@ class WC_Admin_Tests_Reports_Customer extends WC_Unit_Test_Case { $order2 = WC_Helper_Order::create_order( $customer->get_id(), $product2 ); $order2->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $customer_id = DataStore::get_customer_id_by_user_id( $customer->get_id() ); // This is the customer ID from lookup table. diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php index f19ee7b5dc0..09a1628daad 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php @@ -63,7 +63,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { ) ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new OrdersStatsDataStore(); @@ -208,7 +208,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { $order->save(); } - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new OrdersStatsDataStore(); @@ -377,7 +377,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { ) ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new OrdersStatsDataStore(); @@ -666,7 +666,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { $orders[] = $order; } - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new OrdersStatsDataStore(); @@ -917,7 +917,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { } } - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new OrdersStatsDataStore(); @@ -3865,7 +3865,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { $order->apply_coupon( $coupon ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Check if lookup tables are populated. foreach ( $tables as $table ) { @@ -3996,7 +3996,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { $order_2->calculate_totals(); $order_2->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new OrdersStatsDataStore(); @@ -4544,7 +4544,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { $orders[] = $order; } - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new OrdersStatsDataStore(); @@ -5328,7 +5328,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { $orders[] = $order; } - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); global $wpdb; $res = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}wc_order_stats" ); @@ -6094,7 +6094,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { $order_0->set_total( 100 ); $order_0->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $start_time = gmdate( 'Y-m-d H:00:00', $order_0->get_date_created()->getOffsetTimestamp() ); $end_time = gmdate( 'Y-m-d H:59:59', $order_0->get_date_created()->getOffsetTimestamp() ); @@ -6114,7 +6114,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { $order_1->set_total( 100 ); $order_1->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Time frame includes both orders -> customer is a new customer. $start_time = gmdate( 'Y-m-d H:00:00', $order_0->get_date_created()->getOffsetTimestamp() ); @@ -6153,7 +6153,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { $order_2->set_total( 100 ); $order_2->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Time frame includes second and third order -> there is one returning customer. $start_time = gmdate( 'Y-m-d H:i:s', $order_0_time + 1 ); @@ -6204,7 +6204,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { $order_0->set_total( 100 ); $order_0->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $start_time = gmdate( 'Y-m-d H:00:00', $order_0->get_date_created()->getOffsetTimestamp() ); $end_time = gmdate( 'Y-m-d H:59:59', $order_0->get_date_created()->getOffsetTimestamp() ); @@ -6224,7 +6224,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { $order_1->set_total( 100 ); $order_1->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Time frame includes both orders -> customer is a new customer. $start_time = gmdate( 'Y-m-d H:00:00', $order_0->get_date_created()->getOffsetTimestamp() ); @@ -6263,7 +6263,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { $order_2->set_total( 100 ); $order_2->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // Time frame includes second and third order -> there is one returning customer. $start_time = gmdate( 'Y-m-d H:i:s', $order_0_time + 1 ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders.php index 078da5f087b..40b50f9a248 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders.php @@ -64,7 +64,7 @@ class WC_Admin_Tests_Reports_Orders extends WC_Unit_Test_Case { $order->set_status( 'completed' ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new OrdersDataStore(); $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); @@ -143,7 +143,7 @@ class WC_Admin_Tests_Reports_Orders extends WC_Unit_Test_Case { ) ); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new OrdersDataStore(); $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); @@ -255,7 +255,7 @@ class WC_Admin_Tests_Reports_Orders extends WC_Unit_Test_Case { $order2->set_status( 'completed' ); $order2->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new OrdersDataStore(); $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); @@ -310,7 +310,7 @@ class WC_Admin_Tests_Reports_Orders extends WC_Unit_Test_Case { $order_2->set_status( 'completed' ); $order_2->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); $end_time = gmdate( 'Y-m-d H:59:59', $order->get_date_created()->getOffsetTimestamp() ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-products.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-products.php index e088c2f47e4..94e74ab59a3 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-products.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-products.php @@ -42,7 +42,7 @@ class WC_Admin_Tests_Reports_Products extends WC_Unit_Test_Case { $order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new ProductsDataStore(); $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); @@ -119,7 +119,7 @@ class WC_Admin_Tests_Reports_Products extends WC_Unit_Test_Case { $order_2->set_date_created( $date_created_2 ); $order_2->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new ProductsDataStore(); $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); @@ -220,7 +220,7 @@ class WC_Admin_Tests_Reports_Products extends WC_Unit_Test_Case { $order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new ProductsDataStore(); $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); @@ -301,7 +301,7 @@ class WC_Admin_Tests_Reports_Products extends WC_Unit_Test_Case { $order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new ProductsDataStore(); $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); @@ -383,7 +383,7 @@ class WC_Admin_Tests_Reports_Products extends WC_Unit_Test_Case { break; } - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new ProductsDataStore(); $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); @@ -441,7 +441,7 @@ class WC_Admin_Tests_Reports_Products extends WC_Unit_Test_Case { $order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $term = wp_insert_term( 'Unused Category', 'product_cat' ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-revenue-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-revenue-stats.php index 30a9b0832de..7722921486f 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-revenue-stats.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-revenue-stats.php @@ -43,7 +43,7 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case { $order->set_total( 97 ); // $25x4 products + $10 shipping - $20 discount + $7 tax. $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); // /reports/revenue/stats is mapped to Orders_Data_Store. $data_store = new OrdersStatsDataStore(); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-variations.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-variations.php index 66e55d262d1..435600978ca 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-variations.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-variations.php @@ -42,7 +42,7 @@ class WC_Admin_Tests_Reports_Variations extends WC_Unit_Test_Case { $order->set_status( 'completed' ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new VariationsDataStore(); $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); @@ -111,7 +111,7 @@ class WC_Admin_Tests_Reports_Variations extends WC_Unit_Test_Case { $order->set_status( 'completed' ); $order->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new VariationsDataStore(); $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); @@ -228,7 +228,7 @@ class WC_Admin_Tests_Reports_Variations extends WC_Unit_Test_Case { $order_3->set_status( 'completed' ); $order_3->save(); - WC_Helper_Queue::run_all_pending(); + WC_Helper_Queue::run_all_pending( 'wc-admin-data' ); $data_store = new VariationsDataStore(); From 56c04f2bfb1c72da6e9be09e68d9da1cb306ecd9 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 12 Sep 2024 17:14:25 +0800 Subject: [PATCH 13/59] Fix unsnooze notes not working (#51254) * Fix saving note timestamp * Fix missing wc_admin_unsnooze_admin_notes action * Add changelog * Update tests --- .../woocommerce/changelog/fix-unsnooze-notes | 4 +++ plugins/woocommerce/src/Admin/Notes/Note.php | 4 +-- plugins/woocommerce/src/Admin/Notes/Notes.php | 2 +- .../notes/class-wc-tests-notes-note.php | 27 +++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-unsnooze-notes diff --git a/plugins/woocommerce/changelog/fix-unsnooze-notes b/plugins/woocommerce/changelog/fix-unsnooze-notes new file mode 100644 index 00000000000..a2f5b082011 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-unsnooze-notes @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix wc_admin_unsnooze_admin_notes events are being needlessly created diff --git a/plugins/woocommerce/src/Admin/Notes/Note.php b/plugins/woocommerce/src/Admin/Notes/Note.php index de8cde59dc7..af986572efa 100644 --- a/plugins/woocommerce/src/Admin/Notes/Note.php +++ b/plugins/woocommerce/src/Admin/Notes/Note.php @@ -534,7 +534,7 @@ class Note extends \WC_Data { $this->error( 'admin_note_invalid_data', __( 'The admin note date prop cannot be empty.', 'woocommerce' ) ); } - if ( is_string( $date ) ) { + if ( is_string( $date ) && ! is_numeric( $date ) ) { $date = wc_string_to_timestamp( $date ); } $this->set_date_prop( 'date_created', $date ); @@ -546,7 +546,7 @@ class Note extends \WC_Data { * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. */ public function set_date_reminder( $date ) { - if ( is_string( $date ) ) { + if ( is_string( $date ) && ! is_numeric( $date ) ) { $date = wc_string_to_timestamp( $date ); } $this->set_date_prop( 'date_reminder', $date ); diff --git a/plugins/woocommerce/src/Admin/Notes/Notes.php b/plugins/woocommerce/src/Admin/Notes/Notes.php index 138a49cafd6..8a7c6ccd2c2 100644 --- a/plugins/woocommerce/src/Admin/Notes/Notes.php +++ b/plugins/woocommerce/src/Admin/Notes/Notes.php @@ -24,6 +24,7 @@ class Notes { add_action( 'admin_init', array( __CLASS__, 'schedule_unsnooze_notes' ) ); add_action( 'admin_init', array( __CLASS__, 'possibly_delete_survey_notes' ) ); add_action( 'update_option_woocommerce_show_marketplace_suggestions', array( __CLASS__, 'possibly_delete_marketing_notes' ), 10, 2 ); + add_action( self::UNSNOOZE_HOOK, array( __CLASS__, 'unsnooze_notes' ) ); } /** @@ -406,7 +407,6 @@ class Notes { wp_set_current_user( $user_id ); self::record_tracks_event_without_cookies( $event_name, $params ); wp_set_current_user( $current_user_id ); - } /** diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/notes/class-wc-tests-notes-note.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/notes/class-wc-tests-notes-note.php index 700fb702d0e..d4f7d0f8455 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/notes/class-wc-tests-notes-note.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/notes/class-wc-tests-notes-note.php @@ -53,4 +53,31 @@ class WC_Admin_Tests_Notes_Note extends WC_Unit_Test_Case { $this->assertEquals( $date_created_from_first_save, $date_created_from_second_save ); } + /** + * Tests setting date_reminder with various input types. + * + * @dataProvider date_reminder_provider + * @param mixed $input Input date value. + * @param int $expected_timestamp Expected timestamp. + */ + public function test_set_date_reminder_with_various_inputs( $input, $expected_timestamp ) { + $note = new Note(); + $note->set_date_reminder( $input ); + $date_reminder = $note->get_date_reminder(); + $this->assertEquals( $expected_timestamp, $date_reminder ); + } + + /** + * Data provider for test_set_date_reminder_with_various_inputs. + * + * @return array + */ + public function date_reminder_provider() { + return array( + 'timestamp' => array( 1609459200, '2021-01-01T00:00:00+00:00' ), + 'timestamp string' => array( '1609459200', '2021-01-01T00:00:00+00:00' ), + 'date string' => array( '2021-01-01', '2021-01-01T00:00:00+00:00' ), + 'WC_DateTime object' => array( new WC_DateTime( '2021-01-01' ), '2021-01-01T00:00:00+00:00' ), + ); + } } From 7eb31840e72b4b97c8237cad0e15b611b64ef812 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Thu, 12 Sep 2024 18:12:29 +0800 Subject: [PATCH 14/59] Disable remote logging feature by default (#51312) * Disable remote logging feature by default * Changelog --- .../changelog/tweak-disable-remote-logging-by-default | 4 ++++ .../woocommerce/src/Internal/Features/FeaturesController.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/tweak-disable-remote-logging-by-default diff --git a/plugins/woocommerce/changelog/tweak-disable-remote-logging-by-default b/plugins/woocommerce/changelog/tweak-disable-remote-logging-by-default new file mode 100644 index 00000000000..cd22a7c5643 --- /dev/null +++ b/plugins/woocommerce/changelog/tweak-disable-remote-logging-by-default @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Disable remote logging feature by default diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php index e777fc63fb8..1b9b4515c33 100644 --- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php +++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php @@ -241,7 +241,7 @@ class FeaturesController { 'Enable this feature to log errors and related data to Automattic servers for debugging purposes and to improve WooCommerce', 'woocommerce' ), - 'enabled_by_default' => true, + 'enabled_by_default' => false, 'disable_ui' => true, 'is_legacy' => false, 'is_experimental' => true, From d1f80608b234990073c23ec99cd487472ec75e2c Mon Sep 17 00:00:00 2001 From: Brent MacKinnon Date: Thu, 12 Sep 2024 07:13:18 -0300 Subject: [PATCH 15/59] Update development-environment.md (#51259) * Update development-environment.md fixing a character encoding issue * add manifest --------- Co-authored-by: piinthecloud --- docs/docs-manifest.json | 4 ++-- docs/getting-started/development-environment.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs-manifest.json b/docs/docs-manifest.json index 9896e658e12..f2a9ecc4fc2 100644 --- a/docs/docs-manifest.json +++ b/docs/docs-manifest.json @@ -874,7 +874,7 @@ "menu_title": "Development environment setup", "tags": "tutorial, setup", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/development-environment.md", - "hash": "b9f56987247aee67eaa2c74d1059e1cadfd335fa81c77b76c0717648d5631c0f", + "hash": "9e471d3f44a882fe61dcad9e5207d51b280a7220aae1bf6e4ae1fbdd68b7e3d4", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/development-environment.md", "id": "9080572a3904349c44c565ca7e1bef1212c58757" }, @@ -1804,5 +1804,5 @@ "categories": [] } ], - "hash": "8199f0d3c854474839300ed606f03f9f286ace35f65d7c47ffc6477762eaf51e" + "hash": "ffae56d5a4993b151a54ec2117be1acf6a02c9fcab5b5662a2a583ea0e743f1d" } \ No newline at end of file diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index 3cde937ffdb..0701be60fc1 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -14,7 +14,7 @@ If you would like to contribute to the WooCommerce core platform; please read ou ## Prerequisites -WooCommerce does adhere to WordPress code standards and guidelines, so it’s best to familiarize yourself with [WordPress Development](https://learn.wordpress.org/tutorial/introduction-to-wordpress/) as well as [PHP](https://www.php.net/). +WooCommerce does adhere to WordPress code standards and guidelines, so it's best to familiarize yourself with [WordPress Development](https://learn.wordpress.org/tutorial/introduction-to-wordpress/) as well as [PHP](https://www.php.net/). Knowledge and understanding of [WooCommerce Hooks and Filters](https://woocommerce.com/document/introduction-to-hooks-actions-and-filters/) will allow you to add and change code without editing core files. You can learn more about WordPress hooks and filters in the [WordPress Plugin Development Handbook](https://developer.wordpress.org/plugins/hooks/). From b0401ef25d9956cf617b2f5c0fa210a87a3faa94 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Thu, 12 Sep 2024 11:18:13 +0100 Subject: [PATCH 16/59] [Experimental] Delayed Account Creation Block (#50934) * Add block to templates * Register block type with php * Create block type class * Update webpack * Move password strength meter component * Add button styles when disabled * Move password strength component * Block WIP * CSRF token handling * Put new block behind feature flag * Add experimental flag docs * Update icon + description * Changelog * Lint errors * Style controls * Adjust icon markup * subsctring match * More specific import * Fix test fail caused by layout shift * Wording changes from Figma * Check if logged in, not just if the current email is registered * Use opacity for disabled button text * Sync order data with customer after account creation * Add id/fragment to form --- .../js/base/components/button/style.scss | 6 + .../js/base/components/cart-checkout/index.js | 1 + .../password-strength-meter/index.tsx | 0 .../password-strength-meter/style.scss | 2 +- .../create-password.tsx | 6 +- .../create-account/block.json | 56 ++++++ .../create-account/edit.tsx | 125 +++++++++++++ .../create-account/form.tsx | 143 ++++++++++++++ .../create-account/frontend.tsx | 24 +++ .../create-account/index.tsx | 45 +++++ .../create-account/style.scss | 82 +++++++++ .../create-account/utils.ts | 6 + .../woocommerce-blocks/bin/webpack-entries.js | 4 + ...ature-flags-and-experimental-interfaces.md | 4 + .../validation-input-error/index.tsx | 6 +- .../validation-input-error/style.scss | 12 +- ...checkout-block.shopper.block_theme.spec.ts | 2 + .../add-delayed-account-creation-block-50632 | 4 + .../AbstractOrderConfirmationBlock.php | 8 +- .../OrderConfirmation/CreateAccount.php | 174 ++++++++++++++++++ .../BlockTypes/OrderConfirmation/Status.php | 2 +- .../src/Blocks/BlockTypesController.php | 1 + 22 files changed, 703 insertions(+), 10 deletions(-) rename plugins/woocommerce-blocks/assets/js/{blocks/checkout => base/components/cart-checkout}/password-strength-meter/index.tsx (100%) rename plugins/woocommerce-blocks/assets/js/{blocks/checkout => base/components/cart-checkout}/password-strength-meter/style.scss (98%) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/block.json create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/edit.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/form.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/frontend.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/index.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/style.scss create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/utils.ts create mode 100644 plugins/woocommerce/changelog/add-delayed-account-creation-block-50632 create mode 100644 plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/CreateAccount.php diff --git a/plugins/woocommerce-blocks/assets/js/base/components/button/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/button/style.scss index fb6b57a0f1f..7fd81991e3d 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/button/style.scss +++ b/plugins/woocommerce-blocks/assets/js/base/components/button/style.scss @@ -32,6 +32,12 @@ } } + &:disabled { + .wc-block-components-button__text { + opacity: 0.5; + } + } + &.outlined { background: transparent; color: currentColor; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/index.js b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/index.js index a8849e5df8f..3a068a8718f 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/index.js +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/index.js @@ -18,4 +18,5 @@ export { default as ShippingRatesControlPackage } from './shipping-rates-control export { default as PaymentMethodIcons } from './payment-method-icons'; export { default as PaymentMethodLabel } from './payment-method-label'; export { default as AdditionalFieldsPlaceholder } from './additional-fields-placeholder'; +export { default as PasswordStrengthMeter } from './password-strength-meter'; export * from './totals'; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/password-strength-meter/index.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/password-strength-meter/index.tsx similarity index 100% rename from plugins/woocommerce-blocks/assets/js/blocks/checkout/password-strength-meter/index.tsx rename to plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/password-strength-meter/index.tsx diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/password-strength-meter/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/password-strength-meter/style.scss similarity index 98% rename from plugins/woocommerce-blocks/assets/js/blocks/checkout/password-strength-meter/style.scss rename to plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/password-strength-meter/style.scss index ffa5f7652d7..10bdc64f939 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/password-strength-meter/style.scss +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/password-strength-meter/style.scss @@ -60,7 +60,7 @@ } .wc-block-components-password-strength__meter[value="2"], .wc-block-components-password-strength__meter[value="2"] + .wc-block-components-password-strength__result { - color: #ff6f00; + color: $alert-red; } .wc-block-components-password-strength__meter[value="3"], .wc-block-components-password-strength__meter[value="3"] + .wc-block-components-password-strength__result { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/create-password.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/create-password.tsx index 68e4b2b85df..ba874e14e13 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/create-password.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/create-password.tsx @@ -6,11 +6,7 @@ import { useState } from '@wordpress/element'; import { ValidatedTextInput } from '@woocommerce/blocks-components'; import { useDispatch, useSelect } from '@wordpress/data'; import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; - -/** - * Internal dependencies - */ -import PasswordStrengthMeter from '../../password-strength-meter'; +import PasswordStrengthMeter from '@woocommerce/base-components/cart-checkout/password-strength-meter'; const CreatePassword = () => { const [ passwordStrength, setPasswordStrength ] = useState( 0 ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/block.json b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/block.json new file mode 100644 index 00000000000..9f2fb19cd51 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/block.json @@ -0,0 +1,56 @@ +{ + "name": "woocommerce/order-confirmation-create-account", + "version": "1.0.0", + "title": "Account Creation", + "description": "Allow customers to create an account after their purchase. Configure this feature in your store settings.", + "category": "woocommerce", + "keywords": [ + "WooCommerce" + ], + "attributes": { + "customerEmail": { + "type": "string", + "default": "" + }, + "nonceToken": { + "type": "string", + "default": "" + }, + "align": { + "type": "string", + "default": "wide" + }, + "className": { + "type": "string", + "default": "" + }, + "hasDarkControls": { + "type": "boolean", + "default": false + } + }, + "supports": { + "color": { + "background": true, + "text": true, + "button": true + }, + "multiple": false, + "align": [ + "wide", + "full" + ], + "html": false, + "spacing": { + "padding": true, + "margin": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + } + }, + "textdomain": "woocommerce", + "apiVersion": 3, + "$schema": "https://schemas.wp.org/trunk/block.json" +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/edit.tsx new file mode 100644 index 00000000000..5b061b89ea6 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/edit.tsx @@ -0,0 +1,125 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import clsx from 'clsx'; +import type { TemplateArray, BlockAttributes } from '@wordpress/blocks'; +import { Disabled, PanelBody, ToggleControl } from '@wordpress/components'; +import { + InnerBlocks, + useBlockProps, + InspectorControls, +} from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import './style.scss'; +import { SITE_TITLE } from '../../../settings/shared/default-constants'; +import Form from './form'; + +const defaultTemplate = [ + [ + 'core/heading', + { + level: 3, + content: sprintf( + /* translators: %s: site name */ + __( 'Create an account with %s', 'woocommerce' ), + SITE_TITLE + ), + }, + ], + [ + 'core/list', + {}, + [ + [ + 'core/list-item', + { + content: __( 'Faster future purchases', 'woocommerce' ), + }, + ], + [ + 'core/list-item', + { + content: __( 'Securely save payment info', 'woocommerce' ), + }, + ], + [ + 'core/list-item', + { + content: __( + 'Track orders & view shopping history', + 'woocommerce' + ), + }, + ], + ], + ], +] as TemplateArray; + +type EditProps = { + attributes: { + hasDarkControls: boolean; + }; + setAttributes: ( attrs: BlockAttributes ) => void; +}; + +export const Edit = ( { + attributes, + setAttributes, +}: EditProps ): JSX.Element => { + const className = clsx( 'wc-block-order-confirmation-create-account', { + 'has-dark-controls': attributes.hasDarkControls, + } ); + const blockProps = useBlockProps( { + className, + } ); + + return ( +
+ + +
+ + + + + setAttributes( { + hasDarkControls: ! attributes.hasDarkControls, + } ) + } + /> + + +
+ ); +}; + +export const Save = (): JSX.Element => { + return ( +
+ +
+ ); +}; + +export default Edit; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/form.tsx b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/form.tsx new file mode 100644 index 00000000000..4d7446443f5 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/form.tsx @@ -0,0 +1,143 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState, createInterpolateElement } from '@wordpress/element'; +import Button from '@woocommerce/base-components/button'; +import PasswordStrengthMeter from '@woocommerce/base-components/cart-checkout/password-strength-meter'; +import { PRIVACY_URL, TERMS_URL } from '@woocommerce/block-settings'; +import { ValidatedTextInput } from '@woocommerce/blocks-components'; +import { useSelect } from '@wordpress/data'; +import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; + +const termsPageLink = TERMS_URL ? ( + + { __( 'Terms', 'woocommerce' ) } + +) : ( + { __( 'Terms', 'woocommerce' ) } +); + +const privacyPageLink = PRIVACY_URL ? ( + + { __( 'Privacy Policy', 'woocommerce' ) } + +) : ( + { __( 'Privacy Policy', 'woocommerce' ) } +); + +const Form = ( { + attributes: blockAttributes, + isEditor, +}: { + attributes?: { customerEmail?: string; nonceToken?: string }; + isEditor: boolean; +} ) => { + const [ isLoading, setIsLoading ] = useState( false ); + const [ password, setPassword ] = useState( '' ); + const [ passwordStrength, setPasswordStrength ] = useState( 0 ); + const hasValidationError = useSelect( ( select ) => + select( VALIDATION_STORE_KEY ).getValidationError( 'account-password' ) + ); + const customerEmail = + blockAttributes?.customerEmail || + ( isEditor ? 'customer@email.com' : '' ); + const nonceToken = blockAttributes?.nonceToken || ''; + + return ( + { + if ( hasValidationError ) { + event.preventDefault(); + return; + } + setIsLoading( true ); + } } + > +

+ { createInterpolateElement( + __( 'Set a password for ', 'woocommerce' ), + { + email: { customerEmail }, + } + ) } +

+
+ { + if ( + validity.valueMissing || + validity.badInput || + validity.typeMismatch + ) { + return __( + 'Please enter a valid password', + 'woocommerce' + ); + } + } } + customValidation={ ( inputObject ) => { + if ( passwordStrength < 2 ) { + inputObject.setCustomValidity( + __( + 'Please create a stronger password', + 'woocommerce' + ) + ); + return false; + } + return true; + } } + onChange={ ( value: string ) => setPassword( value ) } + feedback={ + + setPasswordStrength( strength ) + } + /> + } + /> +
+ + + + + +

+ { createInterpolateElement( + /* translators: %1$s terms page link, %2$s privacy page link. */ + __( + 'By creating an account you agree to our and .', + 'woocommerce' + ), + { terms: termsPageLink, privacy: privacyPageLink } + ) } +

+ + ); +}; + +export default Form; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/frontend.tsx b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/frontend.tsx new file mode 100644 index 00000000000..8af1238fb5e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/frontend.tsx @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { renderFrontend } from '@woocommerce/base-utils'; + +/** + * Internal dependencies + */ +import Block from './form'; +import { parseAttributes } from './utils'; + +const getProps = ( el: HTMLElement ) => { + return { + attributes: parseAttributes( el.dataset ), + isEditor: false, + }; +}; + +// This does not replace the entire block markup, just the form part. +renderFrontend( { + selector: '.woocommerce-order-confirmation-create-account-form', + Block, + getProps, +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/index.tsx new file mode 100644 index 00000000000..d12ff49337c --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/index.tsx @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { registerBlockType } from '@wordpress/blocks'; +import { Icon, people } from '@wordpress/icons'; +import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; +import { ExternalLink } from '@wordpress/components'; +import { ADMIN_URL } from '@woocommerce/settings'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { Save, Edit } from './edit'; + +if ( isExperimentalBlocksEnabled() ) { + registerBlockType( metadata, { + apiVersion: 3, + description: ( + <> + { metadata.description } +
+ + { __( 'Manage account settings', 'woocommerce' ) } + + + ), + icon: { + src: ( + + ), + }, + attributes: { + ...metadata.attributes, + }, + edit: Edit, + save: Save, + } ); +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/style.scss new file mode 100644 index 00000000000..8da053ef4e1 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/style.scss @@ -0,0 +1,82 @@ +.wc-block-order-confirmation-create-account { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: $gap; + padding: $gap-larger; + margin-top: $gap-larger !important; + margin-bottom: $gap-larger !important; + background: rgba(0, 0, 0, 0.04); + box-sizing: border-box; + + > div { + flex: 1; + } + + p { + margin-top: 0; + margin-bottom: 0; + } + + .woocommerce-order-confirmation-create-account-content, + .block-editor-block-list__layout { + > :first-child { + margin-top: 0 !important; + } + > :last-child { + margin-bottom: 0 !important; + } + ul { + li { + margin-bottom: $gap; + } + } + * { + color: inherit; + } + } + + form { + display: flex; + flex-direction: column; + gap: $gap; + + p, + .wc-block-components-text-input { + margin-top: 0; + margin-bottom: 0; + } + + .wc-block-components-button { + width: 100%; + padding: 1em; + } + + .wc-block-order-confirmation-create-account-terms { + @include font-size(small); + text-align: center; + + a, + span { + white-space: nowrap; + } + } + + .wc-block-components-password-strength.hidden { + display: none; + } + } + + .woocommerce-order-confirmation-create-account-success { + text-align: center; + padding: $gap-larger 0; + + > :first-child { + margin-top: 0 !important; + } + > :last-child { + margin-bottom: 0 !important; + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/utils.ts new file mode 100644 index 00000000000..51040951d07 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/utils.ts @@ -0,0 +1,6 @@ +export const parseAttributes = ( data: Record< string, unknown > ) => { + return { + customerEmail: data?.customerEmail || '', + nonceToken: data?.nonceToken || '', + }; +}; diff --git a/plugins/woocommerce-blocks/bin/webpack-entries.js b/plugins/woocommerce-blocks/bin/webpack-entries.js index a3adb0662fc..11a84832ef5 100644 --- a/plugins/woocommerce-blocks/bin/webpack-entries.js +++ b/plugins/woocommerce-blocks/bin/webpack-entries.js @@ -163,6 +163,10 @@ const blocks = { 'order-confirmation-additional-fields': { customDir: 'order-confirmation/additional-fields', }, + 'order-confirmation-create-account': { + customDir: 'order-confirmation/create-account', + isExperimental: true, + }, }; // Intentional separation of cart and checkout entry points to allow for better code splitting. diff --git a/plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md b/plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md index 2254ddb6d82..f35c9a00d2d 100644 --- a/plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md +++ b/plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md @@ -58,6 +58,10 @@ The majority of our feature flagging is blocks, this is a list of them: - [PHP flag](https://github.com/woocommerce/woocommerce/blob/a0f9d159e5196983d93064762fd20a510de57d55/plugins/woocommerce/src/Blocks/BlockTypesController.php#L303) - [Webpack flag](https://github.com/woocommerce/woocommerce/blob/a0f9d159e5196983d93064762fd20a510de57d55/plugins/woocommerce-blocks/bin/webpack-entries.js#L101) - [JS flag](https://github.com/woocommerce/woocommerce/blob/a0f9d159e5196983d93064762fd20a510de57d55/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/stock-filter/index.tsx#L15) +- Delayed Account Creation (Experimental) + - [PHP flag](https://github.com/woocommerce/woocommerce/blob/9897737880dcbef9831ee41799684dab1960d94f/plugins/woocommerce/src/Blocks/BlockTypesController.php#L417) + - [Webpack flag](https://github.com/woocommerce/woocommerce/blob/9897737880dcbef9831ee41799684dab1960d94f/plugins/woocommerce-blocks/bin/webpack-entries.js#L168) + - [JS flag](https://github.com/woocommerce/woocommerce/blob/9897737880dcbef9831ee41799684dab1960d94f/plugins/woocommerce-blocks/assets/js/blocks/order-confirmation/create-account/index.tsx#L14) ## Features behind flags diff --git a/plugins/woocommerce-blocks/packages/components/validation-input-error/index.tsx b/plugins/woocommerce-blocks/packages/components/validation-input-error/index.tsx index 759468951df..7959d855b9e 100644 --- a/plugins/woocommerce-blocks/packages/components/validation-input-error/index.tsx +++ b/plugins/woocommerce-blocks/packages/components/validation-input-error/index.tsx @@ -3,6 +3,7 @@ */ import { useSelect } from '@wordpress/data'; import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; +import { Icon, warning } from '@wordpress/icons'; /** * Internal dependencies @@ -38,7 +39,10 @@ export const ValidationInputError = ( { return (
-

{ errorMessage }

+

+ + { errorMessage } +

); }; diff --git a/plugins/woocommerce-blocks/packages/components/validation-input-error/style.scss b/plugins/woocommerce-blocks/packages/components/validation-input-error/style.scss index ff5f571d745..3819f68a499 100644 --- a/plugins/woocommerce-blocks/packages/components/validation-input-error/style.scss +++ b/plugins/woocommerce-blocks/packages/components/validation-input-error/style.scss @@ -6,7 +6,17 @@ > p { margin: 0; - padding: $gap-smallest 0 0 0; + padding: $gap-smaller 0 0 0; + display: flex; + align-items: center; + gap: 2px; + } + + svg { + fill: currentColor; + width: 1.5em; + height: 1.5em; + margin-top: -1px; } } diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout-block.shopper.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout-block.shopper.block_theme.spec.ts index de86eff5685..d3b78538f47 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout-block.shopper.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout-block.shopper.block_theme.spec.ts @@ -482,6 +482,8 @@ test.describe( 'Shopper → Checkout Form Errors (guest user)', () => { await frontendUtils.goToCheckout(); await page.getByLabel( 'Email address' ).clear(); + // Notices on the email field will move content when the field loses focus. This can cause the click to "miss". + await page.getByRole( 'button', { name: 'Place order' } ).focus(); await page.getByRole( 'button', { name: 'Place order' } ).click(); // Verify that all required fields show the correct warning. diff --git a/plugins/woocommerce/changelog/add-delayed-account-creation-block-50632 b/plugins/woocommerce/changelog/add-delayed-account-creation-block-50632 new file mode 100644 index 00000000000..ffd1d6f1c9b --- /dev/null +++ b/plugins/woocommerce/changelog/add-delayed-account-creation-block-50632 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Added experimental delayed order creation block. diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlock.php index 09c9881c8b6..4887dd04e30 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlock.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlock.php @@ -186,7 +186,13 @@ abstract class AbstractOrderConfirmationBlock extends AbstractBlock { */ protected function is_email_verified( $order ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash - if ( empty( $_POST ) || ! isset( $_POST['email'] ) || ! wp_verify_nonce( $_POST['check_submission'] ?? '', 'wc_verify_email' ) ) { + if ( empty( $_POST ) || ! isset( $_POST['email'], $_POST['_wpnonce'] ) ) { + return false; + } + + $nonce_value = sanitize_key( wp_unslash( $_POST['_wpnonce'] ?? '' ) ); + + if ( ! wp_verify_nonce( $nonce_value, 'wc_verify_email' ) && ! wp_verify_nonce( $nonce_value, 'wc_create_account' ) ) { return false; } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/CreateAccount.php b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/CreateAccount.php new file mode 100644 index 00000000000..6f355cc02ba --- /dev/null +++ b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/CreateAccount.php @@ -0,0 +1,174 @@ +register_block_type() + * @param string $key Data to get, or default to everything. + * @return array|string + */ + protected function get_block_type_script( $key = null ) { + $script = [ + 'handle' => 'wc-order-confirmation-create-account-block-frontend', + 'path' => $this->asset_api->get_block_asset_build_path( 'order-confirmation-create-account-frontend' ), + 'dependencies' => [], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Process posted account form. + * + * @param \WC_Order $order Order object. + * @return \WP_Error|int + */ + protected function process_form_post( $order ) { + if ( ! isset( $_POST['create-account'], $_POST['email'], $_POST['password'], $_POST['_wpnonce'] ) ) { + return 0; + } + + if ( ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'wc_create_account' ) ) { + return new \WP_Error( 'invalid_nonce', __( 'Unable to create account. Please try again.', 'woocommerce' ) ); + } + + $user_email = sanitize_email( wp_unslash( $_POST['email'] ) ); + $password = wp_unslash( $_POST['password'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + // Does order already have user? + if ( $order->get_customer_id() ) { + return new \WP_Error( 'order_already_has_user', __( 'This order is already linked to a user account.', 'woocommerce' ) ); + } + + // Check given details match the current viewed order. + if ( $order->get_billing_email() !== $user_email ) { + return new \WP_Error( 'email_mismatch', __( 'The email address provided does not match the email address on this order.', 'woocommerce' ) ); + } + + if ( empty( $password ) || strlen( $password ) < 8 ) { + return new \WP_Error( 'password_too_short', __( 'Password must be at least 8 characters.', 'woocommerce' ) ); + } + + $customer_id = wc_create_new_customer( + $user_email, + '', + $password, + [ + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'source' => 'delayed-account-creation', + ] + ); + + if ( is_wp_error( $customer_id ) ) { + return $customer_id; + } + + // Associate customer with the order. + $order->set_customer_id( $customer_id ); + $order->save(); + + // Associate addresses from the order with the customer. + $order_controller = new OrderController(); + $order_controller->sync_customer_data_with_order( $order ); + + // Set the customer auth cookie. + wc_set_customer_auth_cookie( $customer_id ); + + return $customer_id; + } + + /** + * This renders the content of the block within the wrapper. + * + * @param \WC_Order $order Order object. + * @param string|false $permission If the current user can view the order details or not. + * @param array $attributes Block attributes. + * @param string $content Original block content. + * @return string + */ + protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) { + if ( ! $permission ) { + return ''; + } + + // Check registration is possible for this order/customer, and if not, return early. + if ( is_user_logged_in() || email_exists( $order->get_billing_email() ) ) { + return ''; + } + + $result = $this->process_form_post( $order ); + + if ( is_wp_error( $result ) ) { + $notice = wc_print_notice( $result->get_error_message(), 'error', [], true ); + } elseif ( $result ) { + return $this->render_confirmation(); + } + + $processor = new \WP_HTML_Tag_Processor( + $content . + '' + ); + + if ( ! $processor->next_tag( array( 'class_name' => 'wp-block-woocommerce-order-confirmation-create-account' ) ) ) { + return $content; + } + + $processor->set_attribute( 'class', '' ); + $processor->set_attribute( 'style', '' ); + $processor->add_class( 'woocommerce-order-confirmation-create-account-content' ); + + if ( ! $processor->next_tag( array( 'class_name' => 'woocommerce-order-confirmation-create-account-form' ) ) ) { + return $content; + } + + $processor->set_attribute( 'data-customer-email', $order->get_billing_email() ); + $processor->set_attribute( 'data-nonce-token', wp_create_nonce( 'wc_create_account' ) ); + + if ( ! empty( $attributes['hasDarkControls'] ) ) { + $processor->add_class( 'has-dark-controls' ); + } + + return $processor->get_updated_html(); + } + + /** + * Render the block when an account has been registered. + * + * @return string + */ + protected function render_confirmation() { + $content = ''; + + return $content; + } +} diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php index 6cc50f68d3a..9c38143c870 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php @@ -265,7 +265,7 @@ class Status extends AbstractOrderConfirmationBlock {

', esc_attr( 'verify-email-submit' ), esc_html__( 'Confirm email and view order', 'woocommerce' ), - wp_nonce_field( 'wc_verify_email', 'check_submission', true, false ), + wp_nonce_field( 'wc_verify_email', '_wpnonce', true, false ), esc_attr( wc_wp_theme_get_element_class_name( 'button' ) ) ) . ''; diff --git a/plugins/woocommerce/src/Blocks/BlockTypesController.php b/plugins/woocommerce/src/Blocks/BlockTypesController.php index f6d84ecc051..059152fd7fc 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypesController.php +++ b/plugins/woocommerce/src/Blocks/BlockTypesController.php @@ -414,6 +414,7 @@ final class BlockTypesController { $block_types[] = 'ProductFilterRating'; $block_types[] = 'ProductFilterActive'; $block_types[] = 'ProductFilterClearButton'; + $block_types[] = 'OrderConfirmation\CreateAccount'; } /** From c7c054d8f8423146f5a2ed14e309bb43d60bf12d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 19:03:04 +0700 Subject: [PATCH 17/59] Delete changelog files based on PR 51312 (#51319) Delete changelog files for 51312 Co-authored-by: WooCommerce Bot --- .../changelog/tweak-disable-remote-logging-by-default | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/tweak-disable-remote-logging-by-default diff --git a/plugins/woocommerce/changelog/tweak-disable-remote-logging-by-default b/plugins/woocommerce/changelog/tweak-disable-remote-logging-by-default deleted file mode 100644 index cd22a7c5643..00000000000 --- a/plugins/woocommerce/changelog/tweak-disable-remote-logging-by-default +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: tweak - -Disable remote logging feature by default From 358bc1fee2e13f626903727e43f593fd75aee1b4 Mon Sep 17 00:00:00 2001 From: Raja sekar Date: Thu, 12 Sep 2024 17:44:25 +0530 Subject: [PATCH 18/59] Make padding consistent and remove bottom margin for for all types of product cards (#51288) * Make padding consistent for all type of card contents as 24px * Add changefile(s) from automation for the following project(s): woocommerce * override margin bottom added by woo payments plugin for product cards * Add changefile(s) from automation for the following project(s): woocommerce --------- Co-authored-by: github-actions --- .../components/product-card/product-card.scss | 9 +++++---- ...51288-update-wccom-21570-in-app-product-cards-padding | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 plugins/woocommerce/changelog/51288-update-wccom-21570-in-app-product-cards-padding diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.scss b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.scss index 524c47155d3..9f3d404d053 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card.scss @@ -4,6 +4,7 @@ &__product-card { padding: $large-gap; border-radius: $grid-unit-05 !important; + margin-bottom: 0; /* When product card is loading, contents will be empty and we render skeleton loader wireframes: */ &.is-loading { @@ -243,7 +244,7 @@ } &.woocommerce-marketplace__product-card--theme { - padding: 0 0 $medium-gap; + padding: 0 0 $large-gap; overflow: hidden; .woocommerce-marketplace__product-card__content { @@ -265,10 +266,10 @@ right: 0; } .woocommerce-marketplace__product-card__header { - padding-left: $medium-gap; + padding: 0 $large-gap; } .woocommerce-marketplace__product-card__footer { - padding: 0 $medium-gap; + padding: 0 $large-gap; } .woocommerce-marketplace__product-card__price { margin-right: $medium-gap; @@ -313,7 +314,7 @@ display: flex; flex-direction: column; gap: $small-gap; - padding: 16px 24px; + padding: $large-gap; height: 100%; } diff --git a/plugins/woocommerce/changelog/51288-update-wccom-21570-in-app-product-cards-padding b/plugins/woocommerce/changelog/51288-update-wccom-21570-in-app-product-cards-padding new file mode 100644 index 00000000000..1f91bb228e1 --- /dev/null +++ b/plugins/woocommerce/changelog/51288-update-wccom-21570-in-app-product-cards-padding @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Update product card content padding to 24px and add 0 margin bottom to product cards \ No newline at end of file From 97182ef9ca7770faf6e36f6292084e5ce96d5882 Mon Sep 17 00:00:00 2001 From: Raja sekar Date: Thu, 12 Sep 2024 18:28:48 +0530 Subject: [PATCH 19/59] Changes to IAM footer (#51315) * add max-width and border raidus to footer container - to make it look similar to design your own theme box * remove marketplace copy from footer * add another tab to footer * Add changefile(s) from automation for the following project(s): woocommerce * Add changefile(s) from automation for the following project(s): woocommerce * add feedback on code --------- Co-authored-by: github-actions --- .../marketplace/components/footer/footer.scss | 13 ++----------- .../marketplace/components/footer/footer.tsx | 15 +++++++++------ .../components/icon-with-text/icon-with-text.tsx | 2 +- ...5-update-wccom-21574-in-app-marketplace-footer | 4 ++++ 4 files changed, 16 insertions(+), 18 deletions(-) create mode 100644 plugins/woocommerce/changelog/51315-update-wccom-21574-in-app-marketplace-footer diff --git a/plugins/woocommerce-admin/client/marketplace/components/footer/footer.scss b/plugins/woocommerce-admin/client/marketplace/components/footer/footer.scss index 7ca6592b5e2..f659836081a 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/footer/footer.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/footer/footer.scss @@ -2,7 +2,8 @@ .woocommerce-marketplace__footer { background: $gray-0; - border-top: 1px solid $gray-200; + border-radius: 4px; + max-width: 1600px; margin: auto; padding: $content-spacing-xlarge $content-spacing-small; @@ -34,16 +35,6 @@ gap: $large-gap; } -.woocommerce-marketplace__footer-logo { - color: $wp-gray-50; - display: flex; - font-size: 14px; - font-weight: 600; - line-height: 20px; - gap: $small-gap; - margin: 48px 0 0; -} - @media screen and (min-width: $breakpoint-medium) { .woocommerce-marketplace__footer { padding: $content-spacing-xlarge $content-spacing-large; diff --git a/plugins/woocommerce-admin/client/marketplace/components/footer/footer.tsx b/plugins/woocommerce-admin/client/marketplace/components/footer/footer.tsx index 53accc69dd7..d68e01a27a9 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/footer/footer.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/footer/footer.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { check, commentContent, shield } from '@wordpress/icons'; +import { check, commentContent, shield, people } from '@wordpress/icons'; import { createInterpolateElement } from '@wordpress/element'; /** @@ -10,7 +10,6 @@ import { createInterpolateElement } from '@wordpress/element'; */ import './footer.scss'; import IconWithText from '../icon-with-text/icon-with-text'; -import WooIcon from '../../assets/images/woo-icon.svg'; import { MARKETPLACE_HOST } from '../constants'; const refundPolicyTitle = createInterpolateElement( @@ -71,10 +70,14 @@ function FooterContent(): JSX.Element { 'woocommerce' ) } /> -
-
- - { __( 'Woo Marketplace', 'woocommerce' ) } +
); diff --git a/plugins/woocommerce-admin/client/marketplace/components/icon-with-text/icon-with-text.tsx b/plugins/woocommerce-admin/client/marketplace/components/icon-with-text/icon-with-text.tsx index 1eec0b67a9d..7f5867a9ff5 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/icon-with-text/icon-with-text.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/icon-with-text/icon-with-text.tsx @@ -11,7 +11,7 @@ import './icon-with-text.scss'; export interface IconWithTextProps { icon: JSX.Element; - title: ReactElement; + title: ReactElement | string; description: string; } diff --git a/plugins/woocommerce/changelog/51315-update-wccom-21574-in-app-marketplace-footer b/plugins/woocommerce/changelog/51315-update-wccom-21574-in-app-marketplace-footer new file mode 100644 index 00000000000..7f77d1fe125 --- /dev/null +++ b/plugins/woocommerce/changelog/51315-update-wccom-21574-in-app-marketplace-footer @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Update footer design, add one more element to footer content and remove `woo-marketplace` copy at footer bottom. \ No newline at end of file From 3dc0d69091274fac02a603aae5eec192b35676f7 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Thu, 12 Sep 2024 21:36:08 +0800 Subject: [PATCH 20/59] Re-enable remote logging feature by default (#51320) * Reenable remote logging feature by default * Changelog --- .../changelog/tweak-reenable-remote-logging-by-default | 4 ++++ .../woocommerce/src/Internal/Features/FeaturesController.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/tweak-reenable-remote-logging-by-default diff --git a/plugins/woocommerce/changelog/tweak-reenable-remote-logging-by-default b/plugins/woocommerce/changelog/tweak-reenable-remote-logging-by-default new file mode 100644 index 00000000000..efcc6da22eb --- /dev/null +++ b/plugins/woocommerce/changelog/tweak-reenable-remote-logging-by-default @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Reenable remote logging feature by default diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php index 1b9b4515c33..e777fc63fb8 100644 --- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php +++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php @@ -241,7 +241,7 @@ class FeaturesController { 'Enable this feature to log errors and related data to Automattic servers for debugging purposes and to improve WooCommerce', 'woocommerce' ), - 'enabled_by_default' => false, + 'enabled_by_default' => true, 'disable_ui' => true, 'is_legacy' => false, 'is_experimental' => true, From 5e4a6fc5e65026b7de2d53a243e7b99ea0d87cbf Mon Sep 17 00:00:00 2001 From: Brent MacKinnon Date: Thu, 12 Sep 2024 10:51:32 -0300 Subject: [PATCH 21/59] Update totals-footer-item.md (#51261) * Update totals-footer-item.md replacing `<` & `>` with `<` & `>` for the purposes of displaying html within code snippets on woocommerce docs site. * update < > with < > * more small fixes * update manifiest * update erroneous tag swap --------- Co-authored-by: piinthecloud Co-authored-by: Jacklyn Biggin --- .../additional-checkout-fields.md | 4 +- .../available-filters/README.md | 4 +- .../available-filters/order-summary-items.md | 28 +++++------ .../available-filters/totals-footer-item.md | 6 +-- .../available-slot-fills.md | 30 ++++++------ .../payment-method-integration.md | 4 +- docs/cart-and-checkout-blocks/slot-fills.md | 8 ++-- .../customising-checkout-fields.md | 8 ++-- ...ing_custom_fields_in_your_theme_or_site.md | 2 +- docs/code-snippets/link-to-logged-data.md | 2 +- docs/docs-manifest.json | 46 +++++++++--------- ...roducts-to-add-products-onboarding-list.md | 4 +- .../implementing-settings.md | 22 ++++----- .../integrating-coming-soon-mode.md | 2 +- docs/extension-development/settings-api.md | 6 +-- .../customizing-endpoint-urls.md | 38 +++++++-------- .../troubleshooting-endpoints.md | 38 +++++++-------- .../cli-tools.md | 2 +- .../register-product-collection.md | 2 +- .../how-to-guides/custom-field-tutorial.md | 10 ++-- .../extending-woocommerce-admin-reports.md | 2 +- .../classic-theme-developer-handbook.md | 4 +- docs/wc-cli/wc-cli-commands.md | 48 +++++++++---------- 23 files changed, 160 insertions(+), 160 deletions(-) diff --git a/docs/cart-and-checkout-blocks/additional-checkout-fields.md b/docs/cart-and-checkout-blocks/additional-checkout-fields.md index 29981df9bdc..89af92968c2 100644 --- a/docs/cart-and-checkout-blocks/additional-checkout-fields.md +++ b/docs/cart-and-checkout-blocks/additional-checkout-fields.md @@ -339,11 +339,11 @@ This results in the following address form (the billing form will be the same): The rendered markup looks like this: ```html - + data-custom="custom data" value="" > ``` ### Rendering a checkbox field diff --git a/docs/cart-and-checkout-blocks/available-filters/README.md b/docs/cart-and-checkout-blocks/available-filters/README.md index 6185107f41b..80eeab35f71 100644 --- a/docs/cart-and-checkout-blocks/available-filters/README.md +++ b/docs/cart-and-checkout-blocks/available-filters/README.md @@ -81,7 +81,7 @@ const modifyCartItemClass = ( defaultValue, extensions, args ) => { const modifyCartItemPrice = ( defaultValue, extensions, args ) => { if ( isOrderSummaryContext( args ) ) { - return ' for all items'; + return '<price/> for all items'; } return defaultValue; }; @@ -95,7 +95,7 @@ const modifyItemName = ( defaultValue, extensions, args ) => { const modifySubtotalPriceFormat = ( defaultValue, extensions, args ) => { if ( isOrderSummaryContext( args ) ) { - return ' per item'; + return '<price/> per item'; } return defaultValue; }; diff --git a/docs/cart-and-checkout-blocks/available-filters/order-summary-items.md b/docs/cart-and-checkout-blocks/available-filters/order-summary-items.md index ce4134df03c..c210fa39fcf 100644 --- a/docs/cart-and-checkout-blocks/available-filters/order-summary-items.md +++ b/docs/cart-and-checkout-blocks/available-filters/order-summary-items.md @@ -106,17 +106,17 @@ The `cartItemPrice` filter allows to format the order summary item price. ### Parameters -- _defaultValue_ `string` (default: ``) - The default order summary item price. +- _defaultValue_ `string` (default: `<price/>`) - The default order summary item price. - _extensions_ `object` (default: `{}`) - The extensions object. - _args_ `object` - The arguments object with the following keys: - _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](#cart-object). - _cartItem_ `object` - The order summary item object from `wc/store/cart`, see [order summary item object](#cart-item-object). - _context_ `string` (allowed values: `cart` or `summary`) - The context of the item. -- _validation_ `boolean` - Checks if the return value contains the substring ``. +- _validation_ `boolean` - Checks if the return value contains the substring `<price/>`. ### Returns -- `string` - The modified format of the order summary item price, which must contain the substring ``, or the original price format. +- `string` - The modified format of the order summary item price, which must contain the substring `<price/>`, or the original price format. ### Code examples @@ -132,7 +132,7 @@ const modifyCartItemPrice = ( defaultValue, extensions, args, validation ) => { return defaultValue; } - return ' for all items'; + return '<price/> for all items'; }; registerCheckoutFilters( 'example-extension', { @@ -153,14 +153,14 @@ const modifyCartItemPrice = ( defaultValue, extensions, args, validation ) => { } if ( args?.cartItem?.name === 'Beanie with Logo' ) { - return ' to keep you ☀️'; + return '<price/> to keep you ☀️'; } if ( args?.cartItem?.name === 'Sunglasses' ) { - return ' to keep you ❄️'; + return '<price/> to keep you ❄️'; } - return ' for all items'; + return '<price/> for all items'; }; registerCheckoutFilters( 'example-extension', { @@ -261,17 +261,17 @@ The `subtotalPriceFormat` filter allows to format the order summary item subtota ### Parameters -- _defaultValue_ `string` (default: ``) - The default order summary item subtotal price. +- _defaultValue_ `string` (default: `<price/>`) - The default order summary item subtotal price. - _extensions_ `object` (default: `{}`) - The extensions object. - _args_ `object` - The arguments object with the following keys: - _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](#cart-object). - _cartItem_ `object` - The order summary item object from `wc/store/cart`, see [order summary item object](#cart-item-object). - _context_ `string` (allowed values: `cart` or `summary`) - The context of the item. -- _validation_ `boolean` - Checks if the return value contains the substring ``. +- _validation_ `boolean` - Checks if the return value contains the substring `<price/>`. ### Returns -- `string` - The modified format of the order summary item subtotal price, which must contain the substring ``, or the original price format. +- `string` - The modified format of the order summary item subtotal price, which must contain the substring `<price/>`, or the original price format. ### Code examples @@ -292,7 +292,7 @@ const modifySubtotalPriceFormat = ( return defaultValue; } - return ' per item'; + return '<price/> per item'; }; registerCheckoutFilters( 'example-extension', { @@ -318,14 +318,14 @@ const modifySubtotalPriceFormat = ( } if ( args?.cartItem?.name === 'Beanie with Logo' ) { - return ' per warm beanie'; + return '<price/> per warm beanie'; } if ( args?.cartItem?.name === 'Sunglasses' ) { - return ' per cool sunglasses'; + return '<price/> per cool sunglasses'; } - return ' per item'; + return '<price/> per item'; }; registerCheckoutFilters( 'example-extension', { diff --git a/docs/cart-and-checkout-blocks/available-filters/totals-footer-item.md b/docs/cart-and-checkout-blocks/available-filters/totals-footer-item.md index 360c7627b80..c3a72631330 100644 --- a/docs/cart-and-checkout-blocks/available-filters/totals-footer-item.md +++ b/docs/cart-and-checkout-blocks/available-filters/totals-footer-item.md @@ -71,11 +71,11 @@ The `totalValue` filter allows to format the total price in the footer of the Ca - _extensions_ `object` (default: `{}`) - The extensions object. - _args_ `object` - The arguments object with the following keys: - _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](#cart-object). -- _validation_ `boolean` - Checks if the return value contains the substring ``. +- _validation_ `boolean` - Checks if the return value contains the substring `<price/>`. ### Returns -- `string` - The modified format of the total price, which must contain the substring ``, or the original price format. +- `string` - The modified format of the total price, which must contain the substring `<price/>`, or the original price format. ### Code example @@ -83,7 +83,7 @@ The `totalValue` filter allows to format the total price in the footer of the Ca const { registerCheckoutFilters } = window.wc.blocksCheckout; const modifyTotalsPrice = ( defaultValue, extensions, args, validation ) => { - return 'Pay now'; + return 'Pay <price/> now'; }; registerCheckoutFilters( 'my-extension', { diff --git a/docs/cart-and-checkout-blocks/available-slot-fills.md b/docs/cart-and-checkout-blocks/available-slot-fills.md index 80effcba522..efc47abcfa2 100644 --- a/docs/cart-and-checkout-blocks/available-slot-fills.md +++ b/docs/cart-and-checkout-blocks/available-slot-fills.md @@ -22,11 +22,11 @@ const { ExperimentalOrderMeta } = window.wc.blocksCheckout; const render = () => { return ( - -
+ <ExperimentalOrderMeta> + <div class="wc-block-components-totals-wrapper"> { __( 'Yearly recurring total ...', 'YOUR-TEXTDOMAIN' ) } -
-
+ </div> + </ExperimentalOrderMeta> ); }; @@ -61,9 +61,9 @@ const { ExperimentalOrderShippingPackages } = window.wc.blocksCheckout; const render = () => { return ( - -
{ __( 'Express Shipping', 'YOUR-TEXTDOMAIN' ) }
-
+ <ExperimentalOrderShippingPackages> + <div>{ __( 'Express Shipping', 'YOUR-TEXTDOMAIN' ) }</div> + </ExperimentalOrderShippingPackages> ); }; @@ -104,14 +104,14 @@ const { ExperimentalOrderLocalPickupPackages } = window.wc.blocksCheckout; const render = () => { return ( - -
+ <ExperimentalOrderLocalPickupPackages> + <div> { __( 'By using our convenient local pickup option, you can come to our store and pick up your order. We will send you and email when your order is ready for pickup.', 'YOUR-TEXTDOMAIN' ) } -
-
+ </div> + </ExperimentalOrderLocalPickupPackages> ); }; @@ -143,11 +143,11 @@ const { ExperimentalDiscountsMeta } = window.wc.blocksCheckout; const render = () => { return ( - -
+ <ExperimentalDiscountsMeta> + <div class="wc-block-components-totals-wrapper"> { __( 'You have 98683 coins to spend ...', 'YOUR-TEXTDOMAIN' ) } -
-
+ </div> + </ExperimentalDiscountsMeta> ); }; diff --git a/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md b/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md index 602bcb7deaa..d3457214654 100644 --- a/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md +++ b/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md @@ -43,8 +43,8 @@ The options you feed the configuration instance should be an object in this shap ```js const options = { name: 'my_payment_method', - content:
A React node
, - edit:
A React node
, + content: <div>A React node</div>, + edit: <div>A React node</div>, canMakePayment: () => true, paymentMethodId: 'new_payment_method', supports: { diff --git a/docs/cart-and-checkout-blocks/slot-fills.md b/docs/cart-and-checkout-blocks/slot-fills.md index d694d03bb1e..38003d4921f 100644 --- a/docs/cart-and-checkout-blocks/slot-fills.md +++ b/docs/cart-and-checkout-blocks/slot-fills.md @@ -32,14 +32,14 @@ const { registerPlugin } = wp.plugins; const { ExperimentalOrderMeta } = wc.blocksCheckout; const MyCustomComponent = ( { cart, extensions } ) => { - return
Hello WooCommerce
; + return <div className="my-component">Hello WooCommerce</div>; }; const render = () => { return ( - - - + <ExperimentalOrderMeta> + <MyCustomComponent /> + </ExperimentalOrderMeta> ); }; diff --git a/docs/code-snippets/customising-checkout-fields.md b/docs/code-snippets/customising-checkout-fields.md index 4b8aa96aee8..ab90d600be1 100644 --- a/docs/code-snippets/customising-checkout-fields.md +++ b/docs/code-snippets/customising-checkout-fields.md @@ -295,7 +295,7 @@ function custom_override_checkout_fields( $fields ) { add_action( 'woocommerce_admin_order_data_after_shipping_address', 'my_custom_checkout_field_display_admin_order_meta', 10, 1 ); function my_custom_checkout_field_display_admin_order_meta($order){ - echo '

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

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

' . esc_html__( 'My Field' ) . '

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

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

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

- + <h2></h2> + <table class="form-table"> generate_settings_html(); ?> -
+ </table> - - - - - - - - - - - - - - - - - +<<>?xml version="1.0" encoding="UTF-8"?> +<configuration> + <system.webServer> + <handlers accessPolicy="Read, Execute, Script" /> + <rewrite> + <rules> + <rule name="wordpress" patternSyntax="Wildcard"> + <match url="*" /> + <conditions> + <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> + <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> + </conditions> + <action type="Rewrite" url="index.php" /> + </rule> + </rules> + </rewrite> + </system.webServer> +</configuration> ``` ### Pages direct to wrong place @@ -74,6 +74,6 @@ Landing on the wrong page when clicking an endpoint URL is typically caused by i ### How to Remove "Downloads" from My Account -Sometimes the "Downloads" endpoint on the "My account" page does not need to be displayed. This can be removed by going to **WooCommerce → Settings → Advanced → Account endpoints** and clearing the Downloads endpoint field. +Sometimes the "Downloads" endpoint on the "My account" page does not need to be displayed. This can be removed by going to **WooCommerce > Settings > Advanced > Account endpoints** and clearing the Downloads endpoint field. ![Account endpoints](https://developer.woocommerce.com/wp-content/uploads/2023/12/Screenshot-2023-04-09-at-11.45.58-PM.png) diff --git a/docs/getting-started/troubleshooting-endpoints.md b/docs/getting-started/troubleshooting-endpoints.md index 467d9dba1cb..11de5172810 100644 --- a/docs/getting-started/troubleshooting-endpoints.md +++ b/docs/getting-started/troubleshooting-endpoints.md @@ -18,24 +18,24 @@ For more information, learn how to [Customize Endpoints](./customizing-endpoint- On Windows servers, the **web.config** file may not be set correctly to allow for the endpoints to work correctly. In this case, clicking on endpoint links (e.g. /edit-account/ or /customer-logout/) may appear to do nothing except refresh the page. In order to resolve this, try simplifying the **web.config** file on your Windows server. Here's a sample file configuration: ```xml - - - - - - - - - - - - - - - - - - +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <system.webServer> + <handlers accessPolicy="Read, Execute, Script" /> + <rewrite> + <rules> + <rule name="wordpress" patternSyntax="Wildcard"> + <match url="*" /> + <conditions> + <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> + <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> + </conditions> + <action type="Rewrite" url="index.php" /> + </rule> + </rules> + </rewrite> + </system.webServer> +</configuration> ``` ## Pages direct to wrong place @@ -44,6 +44,6 @@ Landing on the wrong page when clicking an endpoint URL is typically caused by i ## How to Remove "Downloads" from My Account -Sometimes the "Downloads" endpoint on the "My account" page does not need to be displayed. This can be removed by going to **WooCommerce → Settings → Advanced → Account endpoints** and clearing the Downloads endpoint field. +Sometimes the "Downloads" endpoint on the "My account" page does not need to be displayed. This can be removed by going to **WooCommerce > Settings > Advanced > Account endpoints** and clearing the Downloads endpoint field. ![Account endpoints](https://developer.woocommerce.com/wp-content/uploads/2023/12/Screenshot-2023-04-09-at-11.45.58-PM.png) diff --git a/docs/high-performance-order-storage/cli-tools.md b/docs/high-performance-order-storage/cli-tools.md index e61035d60cd..c4b1877b9e5 100644 --- a/docs/high-performance-order-storage/cli-tools.md +++ b/docs/high-performance-order-storage/cli-tools.md @@ -229,7 +229,7 @@ The backfill command can be used to selectively migrate order data (or whole ord The exact syntax for this command is as follows: ```plaintext -wp wc hpos backfill --from= --to= [--meta_keys=] [--props=] +wp wc hpos backfill <order_id> --from=<datastore> --to=<datastore> [--meta_keys=<meta_keys>] [--props=<props>] ``` You have to specify which datastore to use as source (either `posts` or `hpos`) and which one to use as destination. The `--meta_keys` and `--props` arguments receive a comma separated list of meta keys and order properties, which can be used to move only certain data from one datastore to the other, instead of the whole order. diff --git a/docs/product-collection-block/register-product-collection.md b/docs/product-collection-block/register-product-collection.md index 677668ba71e..b754746a679 100644 --- a/docs/product-collection-block/register-product-collection.md +++ b/docs/product-collection-block/register-product-collection.md @@ -36,7 +36,7 @@ We will explain important arguments that can be passed to `__experimentalRegiste A Collection is defined by an object that can contain the following fields: -- `name` (type `string`): A unique and machine-readable collection name. We recommend using the format `/product-collection/`. Both `` and `` should consist only of alphanumeric characters and hyphens (e.g., `my-plugin/product-collection/my-collection`). +- `name` (type `string`): A unique and machine-readable collection name. We recommend using the format `<plugin-name>/product-collection/<collection-name>`. Both `<plugin-name>` and `<collection-name>` should consist only of alphanumeric characters and hyphens (e.g., `my-plugin/product-collection/my-collection`). - `title` (type `string`): The title of the collection, which will be displayed in various places including the block inserter and collection chooser. - `description` (optional, type `string`): A human-readable description of the collection. - `innerBlocks` (optional, type `Array[]`): An array of inner blocks that will be added to the collection. If not provided, the default inner blocks will be used. diff --git a/docs/product-editor-development/how-to-guides/custom-field-tutorial.md b/docs/product-editor-development/how-to-guides/custom-field-tutorial.md index 64fb3203025..ae243d3c9dc 100644 --- a/docs/product-editor-development/how-to-guides/custom-field-tutorial.md +++ b/docs/product-editor-development/how-to-guides/custom-field-tutorial.md @@ -62,7 +62,7 @@ In React: import { registerBlockType } from '@wordpress/blocks'; function Edit() { - return

Hello World (from the editor).

; + return <p>Hello World (from the editor).</p>; } registerBlockType( 'tutorial/new-product-form-field', { @@ -217,8 +217,8 @@ function Edit( { attributes } ) { const blockProps = useWooBlockProps( attributes ); return ( -
- -
+ /> + </div> ); } ``` diff --git a/docs/reporting/extending-woocommerce-admin-reports.md b/docs/reporting/extending-woocommerce-admin-reports.md index 2abe7862fe3..fe328313aee 100644 --- a/docs/reporting/extending-woocommerce-admin-reports.md +++ b/docs/reporting/extending-woocommerce-admin-reports.md @@ -32,7 +32,7 @@ npm run create-wc-extension After choosing a name, move into that folder and start webpack to watch and build files. ```sh -cd ../ +cd ../<my-plugin-name> npm install npm start ``` diff --git a/docs/theme-development/classic-theme-developer-handbook.md b/docs/theme-development/classic-theme-developer-handbook.md index dcdd64c0b6f..3f34881afe9 100644 --- a/docs/theme-development/classic-theme-developer-handbook.md +++ b/docs/theme-development/classic-theme-developer-handbook.md @@ -73,11 +73,11 @@ add_action('woocommerce_before_main_content', 'my_theme_wrapper_start', 10); add_action('woocommerce_after_main_content', 'my_theme_wrapper_end', 10); function my_theme_wrapper_start() { - echo '
'; + echo '<section id="main">'; } function my_theme_wrapper_end() { - echo '
'; + echo '</section>'; } ``` diff --git a/docs/wc-cli/wc-cli-commands.md b/docs/wc-cli/wc-cli-commands.md index 06606bdb500..aa7d3e3a646 100644 --- a/docs/wc-cli/wc-cli-commands.md +++ b/docs/wc-cli/wc-cli-commands.md @@ -117,7 +117,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope ### wc customer_download -#### wc customer_download list +#### wc customer_download list <customer_id> - `--customer_id` - Unique identifier for the resource. - `--context` - Scope under which the request is made; determines fields present in response. @@ -198,7 +198,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope ### wc order_note -#### wc order_note list +#### wc order_note list <order_id> - `--order_id` - The order ID. - `--context` - Scope under which the request is made; determines fields present in response. @@ -211,14 +211,14 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc order_note create +#### wc order_note create <order_id> - `--order_id` - The order ID. - `--note` - Order note content. (*Required*) - `--customer_note` - If true, the note will be shown to customers and they will be notified. If false, the note will be for admin reference only. - `--porcelain` - Output just the id when the operation is successful. -#### wc order_note get [id] +#### wc order_note get <order_id> [id] - `--id` - Unique identifier for the resource. - `--order_id` - The order ID. @@ -231,7 +231,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc order_note delete [id] +#### wc order_note delete <order_id> [id] - `--id` - Unique identifier for the resource. - `--order_id` - The order ID. @@ -240,7 +240,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope ### wc shop_order_refund -#### wc shop_order_refund list +#### wc shop_order_refund list <order_id> - `--order_id` - The order ID. - `--context` - Scope under which the request is made; determines fields present in response. @@ -265,7 +265,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc shop_order_refund create +#### wc shop_order_refund create <order_id> - `--order_id` - The order ID. - `--amount` - Refund amount. @@ -276,7 +276,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--api_refund` - When true, the payment gateway API is used to generate the refund. - `--porcelain` - Output just the id when the operation is successful. -#### wc shop_order_refund get [id] +#### wc shop_order_refund get <order_id> [id] - `--order_id` - The order ID. - `--id` - Unique identifier for the resource. @@ -289,7 +289,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc shop_order_refund delete [id] +#### wc shop_order_refund delete <order_id> [id] - `--order_id` - The order ID. - `--id` - Unique identifier for the resource. @@ -386,7 +386,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope ### wc product_attribute_term -#### wc product_attribute_term list +#### wc product_attribute_term list <attribute_id> - `--attribute_id` - Unique identifier for the attribute of the terms. - `--context` - Scope under which the request is made; determines fields present in response. @@ -409,7 +409,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc product_attribute_term create +#### wc product_attribute_term create <attribute_id> - `--attribute_id` - Unique identifier for the attribute of the terms. - `--name` - Name for the resource. (*Required*) @@ -418,7 +418,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--menu_order` - Menu order, used to custom sort the resource. - `--porcelain` - Output just the id when the operation is successful. -#### wc product_attribute_term get [id] +#### wc product_attribute_term get <attribute_id> [id] - `--id` - Unique identifier for the resource. - `--attribute_id` - Unique identifier for the attribute of the terms. @@ -431,7 +431,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc product_attribute_term update [id] +#### wc product_attribute_term update <attribute_id> [id] - `--id` - Unique identifier for the resource. - `--attribute_id` - Unique identifier for the attribute of the terms. @@ -441,7 +441,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--menu_order` - Menu order, used to custom sort the resource. - `--porcelain` - Output just the id when the operation is successful. -#### wc product_attribute_term delete [id] +#### wc product_attribute_term delete <attribute_id> [id] - `--id` - Unique identifier for the resource. - `--attribute_id` - Unique identifier for the attribute of the terms. @@ -565,7 +565,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope ### wc product_review -#### wc product_review list +#### wc product_review list <product_id> - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the variation. @@ -578,7 +578,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc product_review create +#### wc product_review create <product_id> - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the variation. @@ -590,7 +590,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--email` - Email of the reviewer. (*Required*) - `--porcelain` - Output just the id when the operation is successful. -#### wc product_review get [id] +#### wc product_review get <product_id> [id] - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the resource. @@ -603,7 +603,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc product_review update [id] +#### wc product_review update <product_id> [id] - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the resource. @@ -615,7 +615,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--email` - Reviewer email. - `--porcelain` - Output just the id when the operation is successful. -#### wc product_review delete [id] +#### wc product_review delete <product_id> [id] - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the resource. @@ -893,7 +893,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope ### wc product_variation -#### wc product_variation list +#### wc product_variation list <product_id> - `--product_id` - Unique identifier for the variable product. - `--context` - Scope under which the request is made; determines fields present in response. @@ -932,7 +932,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc product_variation create +#### wc product_variation create <product_id> - `--product_id` - Unique identifier for the variable product. - `--description` - Variation description. @@ -964,7 +964,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--meta_data` - Meta data. - `--porcelain` - Output just the id when the operation is successful. -#### wc product_variation get [id] +#### wc product_variation get <product_id> [id] - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the variation. @@ -977,7 +977,7 @@ Default: table Options: table, json, csv, ids, yaml, count, headers, body, envelope -#### wc product_variation update [id] +#### wc product_variation update <product_id> [id] - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the variation. @@ -1010,7 +1010,7 @@ Options: table, json, csv, ids, yaml, count, headers, body, envelope - `--meta_data` - Meta data. - `--porcelain` - Output just the id when the operation is successful. -#### wc product_variation delete [id] +#### wc product_variation delete <product_id> [id] - `--product_id` - Unique identifier for the variable product. - `--id` - Unique identifier for the variation. From 2433f9f1c495466bb9053689a64ae69dbaf3a6f2 Mon Sep 17 00:00:00 2001 From: Naman Malhotra Date: Thu, 12 Sep 2024 16:55:47 +0300 Subject: [PATCH 22/59] Update changelog.txt - trunk (#51324) --- changelog.txt | 235 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) diff --git a/changelog.txt b/changelog.txt index e34b03260d4..9f5b5545157 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,240 @@ == Changelog == += 9.3.0 2024-09-10 = + +**WooCommerce** + +* Enhancement - Add query params masking to remote logger [#51108](https://github.com/woocommerce/woocommerce/pull/51108) +* Update - Added more paths to remote logger query param whitelist [#51108](https://github.com/woocommerce/woocommerce/pull/51108) +* Fix - Revert update to React 18 in Checkout block. [#51289](https://github.com/woocommerce/woocommerce/pull/51289) +* Fix - Add check to ensure themes API is safe [#51081](https://github.com/woocommerce/woocommerce/pull/51081) +* Fix - CYS - Remove usage of `prepare_item_for_response` function in `Images` endpoint. [#50923](https://github.com/woocommerce/woocommerce/pull/50923) +* Fix - Add ability for a screen reader to announce the current tab on a single product page. [#50373](https://github.com/woocommerce/woocommerce/pull/50373) +* Fix - Add a label to the product pagination for the woocommerce pagination [#49924](https://github.com/woocommerce/woocommerce/pull/49924) +* Fix - Add aria-current to the current link in My Account side nav [#49800](https://github.com/woocommerce/woocommerce/pull/49800) +* Fix - Add aria-label on View order button to aid in accessibility for screen readers [#49424](https://github.com/woocommerce/woocommerce/pull/49424) +* Fix - Add CSS outline for site visibility badge keyboard accessibility [#50794](https://github.com/woocommerce/woocommerce/pull/50794) +* Fix - Add scope attribute and aria-label to the product attributes table [#49768](https://github.com/woocommerce/woocommerce/pull/49768) +* Fix - Add to Cart with Options - Fix translation when used inside the Single Product block. [#50628](https://github.com/woocommerce/woocommerce/pull/50628) +* Fix - Allow verified parameter to be set by REST API request [#50525](https://github.com/woocommerce/woocommerce/pull/50525) +* Fix - Avoid PHP warnings if `add-to-cart.php` template does not pass `aria-describedby_text` [#48969](https://github.com/woocommerce/woocommerce/pull/48969) +* Fix - Cart block: Strip HTML tags and decode HTML entities in quantity change notifications. [#50541](https://github.com/woocommerce/woocommerce/pull/50541) +* Fix - Changed from using React.render to React.createRoot for marketing coupons as it has been deprecated since React 18 [#48832](https://github.com/woocommerce/woocommerce/pull/48832) +* Fix - Changed from using React.render to React.createRoot for payment methods promotion, shipping settings region zone as it has been deprecated since React 18 [#48835](https://github.com/woocommerce/woocommerce/pull/48835) +* Fix - Changed from using React.render to React.createRoot for print shipping banner as it has been deprecated since React 18 [#48831](https://github.com/woocommerce/woocommerce/pull/48831) +* Fix - Changed from using React.render to React.createRoot for product-usage-notice-modal as it has been deprecated since React 18 [#50765](https://github.com/woocommerce/woocommerce/pull/50765) +* Fix - Changed from using React.render to React.createRoot for wc addon tour as it has been deprecated since React 18 [#48833](https://github.com/woocommerce/woocommerce/pull/48833) +* Fix - Changed from using React.render to React.createRoot for WCAdmin uses as it has been deprecated since React 18 [#48785](https://github.com/woocommerce/woocommerce/pull/48785) +* Fix - Changed instances of prime marks inappropriately used when apostrophes are supposed to be used for some parts of WC Admin JS/TS/TSX files [#50776](https://github.com/woocommerce/woocommerce/pull/50776) +* Fix - Clear product unique ID (`global_unique_id`) when duplicating products. [#50629](https://github.com/woocommerce/woocommerce/pull/50629) +* Fix - Compatibility Layer: fix 'woocommerce_before_single_product_summary' hook position. [#50392](https://github.com/woocommerce/woocommerce/pull/50392) +* Fix - CYS - Improve the error when a request fails due to permissions [#50211](https://github.com/woocommerce/woocommerce/pull/50211) +* Fix - CYS - Update the "show_on_front" setting to "posts" to avoid overriding the "page" template. [#50083](https://github.com/woocommerce/woocommerce/pull/50083) +* Fix - CYS: disable zoom out on fonts/color pairs iframe [#50498](https://github.com/woocommerce/woocommerce/pull/50498) +* Fix - CYS: Fix auto scroll when a new block is added. [#50431](https://github.com/woocommerce/woocommerce/pull/50431) +* Fix - CYS: Improve opt in flow [#50529](https://github.com/woocommerce/woocommerce/pull/50529) +* Fix - Display address card for virtual products if shopper's address is known [#50127](https://github.com/woocommerce/woocommerce/pull/50127) +* Fix - Enable skipped E2E tests for attributes #50143 [#50143](https://github.com/woocommerce/woocommerce/pull/50143) +* Fix - Ensure coupon errors are visible on block checkout when invalid coupons are removed. [#50412](https://github.com/woocommerce/woocommerce/pull/50412) +* Fix - Ensure low and no stock email notification routine is triggered whenever product stock changes [#49583](https://github.com/woocommerce/woocommerce/pull/49583) +* Fix - Ensure session object is initialized before attempting to get chosen shipping methods [#50774](https://github.com/woocommerce/woocommerce/pull/50774) +* Fix - Ensure that the orders REST endpoint behaves the same as the UI when updating an order to remove a line item. [#50606](https://github.com/woocommerce/woocommerce/pull/50606) +* Fix - Featured Product: Fix variable product Selection dropdown #50633 [#50633](https://github.com/woocommerce/woocommerce/pull/50633) +* Fix - Fix "Product Meta" translations - Register the block server side. [#50625](https://github.com/woocommerce/woocommerce/pull/50625) +* Fix - Fix: ensure the global product object is always ready for compatibility layer by disabling default render routine of Product Templates inner blocks. [#49971](https://github.com/woocommerce/woocommerce/pull/49971) +* Fix - Fix activating the installed subscription when the user has multiple active licenses for the same product. [#49803](https://github.com/woocommerce/woocommerce/pull/49803) +* Fix - Fix address heading level on My Account page. [#49764](https://github.com/woocommerce/woocommerce/pull/49764) +* Fix - Fix an admin bar CSS positioning bug in WordPress.com on mobile [#50709](https://github.com/woocommerce/woocommerce/pull/50709) +* Fix - Fix cart shortcode updates when not used on the main cart page. [#50524](https://github.com/woocommerce/woocommerce/pull/50524) +* Fix - Fix core profiler checkbox vertical alignment and border color [#50151](https://github.com/woocommerce/woocommerce/pull/50151) +* Fix - Fix core profiler set up my store button and TOS are too close to each other [#50579](https://github.com/woocommerce/woocommerce/pull/50579) +* Fix - Fix e2e Google for WooCommerce strict mode violation error [#50189](https://github.com/woocommerce/woocommerce/pull/50189) +* Fix - Fixed Core Profiler's sticky footer button problem [#50727](https://github.com/woocommerce/woocommerce/pull/50727) +* Fix - Fixed placeholders in the classic cart shipping calculator to update with country selection. [#49684](https://github.com/woocommerce/woocommerce/pull/49684) +* Fix - Fixes a bug where some express payment buttons weren't being rendered correctly [#49304](https://github.com/woocommerce/woocommerce/pull/49304) +* Fix - Fix extensionCartUpdates to surface generic error messages, and include documentation for the error handling. [#49762](https://github.com/woocommerce/woocommerce/pull/49762) +* Fix - Fix focus order on checkout block page. [#49649](https://github.com/woocommerce/woocommerce/pull/49649) +* Fix - Fix navigation badge decreases when installing extension in "Grow your business task" [#50584](https://github.com/woocommerce/woocommerce/pull/50584) +* Fix - Fix page titles of the cart and checkout page when using blocks and FSE themes. [#49986](https://github.com/woocommerce/woocommerce/pull/49986) +* Fix - Fix rescheduling of actions that are blocked by other delayed actions [#50082](https://github.com/woocommerce/woocommerce/pull/50082) +* Fix - Fix the "Add payment methods" link in LYS congrat screen redirects to a blank page [#50609](https://github.com/woocommerce/woocommerce/pull/50609) +* Fix - Fix translation - Avoid registering blocks in the wrong context. [#50615](https://github.com/woocommerce/woocommerce/pull/50615) +* Fix - Fix `Product meta` console error. [#50680](https://github.com/woocommerce/woocommerce/pull/50680) +* Fix - Fix `store-title` endpoint - Pass default value to `get_option`. [#50673](https://github.com/woocommerce/woocommerce/pull/50673) +* Fix - Hide save changes button in main payments screen [#50064](https://github.com/woocommerce/woocommerce/pull/50064) +* Fix - In Remote Specs, treat empty arrays as valid cached values so individual engines can return default values. [#50521](https://github.com/woocommerce/woocommerce/pull/50521) +* Fix - Keep focus on shipping option input once selected [#49360](https://github.com/woocommerce/woocommerce/pull/49360) +* Fix - Make the matching variations alert a live region [#50132](https://github.com/woocommerce/woocommerce/pull/50132) +* Fix - Only count published products in productCount [#50503](https://github.com/woocommerce/woocommerce/pull/50503) +* Fix - Prevent fatal error if NULL is provided in array_search under Jetpack Stats [#50696](https://github.com/woocommerce/woocommerce/pull/50696) +* Fix - Prevent Store API orders being placed with empty state [#50028](https://github.com/woocommerce/woocommerce/pull/50028) +* Fix - Prevent sync-on-read from affecting results of HPOS diff CLI tool. [#49726](https://github.com/woocommerce/woocommerce/pull/49726) +* Fix - Product Collection: Fix max price query to include prices less or equal to the given max value. [#49917](https://github.com/woocommerce/woocommerce/pull/49917) +* Fix - Product Collection: fix the preview if used in Products by specific Category or Tag [#49889](https://github.com/woocommerce/woocommerce/pull/49889) +* Fix - Product Price block: prevent price amounts from breaking into multiple lines [#50660](https://github.com/woocommerce/woocommerce/pull/50660) +* Fix - Properly detect active plugins in multisite WP installations. [#50417](https://github.com/woocommerce/woocommerce/pull/50417) +* Fix - Reduce error noise in the user profile screen, by removing the requirement for custom fields to have a class attribute. [#48079](https://github.com/woocommerce/woocommerce/pull/48079) +* Fix - Remove Active Shipping Zones check for displaying shipping calculator on the Cart Page. [#49214](https://github.com/woocommerce/woocommerce/pull/49214) +* Fix - Single product block - Fix translation for title and description in edit mode. [#50599](https://github.com/woocommerce/woocommerce/pull/50599) +* Fix - Store API: Do not resume pending orders--create a new order instead [#50531](https://github.com/woocommerce/woocommerce/pull/50531) +* Fix - Transform labels in shipping zone region selector to decode html entities [#50694](https://github.com/woocommerce/woocommerce/pull/50694) +* Fix - Treat post_type=product as a shop page. [#50567](https://github.com/woocommerce/woocommerce/pull/50567) +* Fix - Update product order status colors to ensure accessible color contrasts [#49934](https://github.com/woocommerce/woocommerce/pull/49934) +* Add - Add an additional field for the email settings that sets the footer text color [#49648](https://github.com/woocommerce/woocommerce/pull/49648) +* Add - Add blueprint behind a feature flag for testing purposes. [#49763](https://github.com/woocommerce/woocommerce/pull/49763) +* Add - Add field for the email footer text color [#49648](https://github.com/woocommerce/woocommerce/pull/49648) +* Add - Add function to clear system status theme info cache [#50803](https://github.com/woocommerce/woocommerce/pull/50803) +* Add - Add methods required by extensions to control product feature usage based on subscription status. [#50218](https://github.com/woocommerce/woocommerce/pull/50218) +* Add - Add parameter to avoid attempting to create the logs directory if it doesn't exist [#49766](https://github.com/woocommerce/woocommerce/pull/49766) +* Add - Add Pattern button to no blocks view on the CYS assembler [#49981](https://github.com/woocommerce/woocommerce/pull/49981) +* Add - Add reactified main payments screen [#49972](https://github.com/woocommerce/woocommerce/pull/49972) +* Add - Add reactify-classic-payments-settings feature flag [#49966](https://github.com/woocommerce/woocommerce/pull/49966) +* Add - Add tracks for WordPress Importer/Export pages. [#50769](https://github.com/woocommerce/woocommerce/pull/50769) +* Add - Add `FilteredGetDataTrait`, `OrderAwareControllerTrait`, and `StatsDataStoreTrait` for extension developers to reuse while creating custom Analytics [#49425](https://github.com/woocommerce/woocommerce/pull/49425) +* Add - Implement server-side remote error logging [#49599](https://github.com/woocommerce/woocommerce/pull/49599) +* Add - Inform screen reader users when mini cart updates [#48295](https://github.com/woocommerce/woocommerce/pull/48295) +* Add - Integrate JS remote logging package in WooCommerce Admin [#50134](https://github.com/woocommerce/woocommerce/pull/50134) +* Add - Product Collection: emit the JS event when PC block is rendered [#50166](https://github.com/woocommerce/woocommerce/pull/50166) +* Add - Product Collection: Enable Context-Aware Previews by Adding `usesReference` to `registerProductCollection` [#49796](https://github.com/woocommerce/woocommerce/pull/49796) +* Add - Track frequency of unhandled JS errors with MC Stats [#50155](https://github.com/woocommerce/woocommerce/pull/50155) +* Add - Use MC Stats for PHP fatal error counting [#49658](https://github.com/woocommerce/woocommerce/pull/49658) +* Add - [E2E tests]: Add product description using the block editor #50232 [#50232](https://github.com/woocommerce/woocommerce/pull/50232) +* Update - Update WooCommerce Shipping Promo Banner to install the latest version of WooCommerce Shipping instead of WCS&T. [#50970](https://github.com/woocommerce/woocommerce/pull/50970) +* Update - Add abbreviations for fields GTIN, UPC, EAN, OR ISBN [#50042](https://github.com/woocommerce/woocommerce/pull/50042) +* Update - Add additional fields to new product editor e2e tests. [#50241](https://github.com/woocommerce/woocommerce/pull/50241) +* Update - Add confirmation prompt for site visibility settings when changing from live to coming soon mode [#50759](https://github.com/woocommerce/woocommerce/pull/50759) +* Update - Add pattern validation for global_unique_id [#50501](https://github.com/woocommerce/woocommerce/pull/50501) +* Update - Add remote logger as a log handler to wc logger [#50430](https://github.com/woocommerce/woocommerce/pull/50430) +* Update - Add request_uri prop to remote logging data [#50671](https://github.com/woocommerce/woocommerce/pull/50671) +* Update - Add woocommerce_coming_soon option for all sites [#50581](https://github.com/woocommerce/woocommerce/pull/50581) +* Update - Comment: Fix typos in documentation. [#50282](https://github.com/woocommerce/woocommerce/pull/50282) +* Update - CYS - Add tests for the Full Composability feature. [#49748](https://github.com/woocommerce/woocommerce/pull/49748) +* Update - CYS - Run appropriate tests depending on the WordPress version. [#50016](https://github.com/woocommerce/woocommerce/pull/50016) +* Update - CYS - Update icon and text colors in the assembler. [#50478](https://github.com/woocommerce/woocommerce/pull/50478) +* Update - CYS: Improve opt-in flow fonts. [#50086](https://github.com/woocommerce/woocommerce/pull/50086) +* Update - CYS: Improve opt-in flow patterns. [#50080](https://github.com/woocommerce/woocommerce/pull/50080) +* Update - CYS: Improve tracking survey [#50196](https://github.com/woocommerce/woocommerce/pull/50196) +* Update - CYS: Improve tracking survey [#50354](https://github.com/woocommerce/woocommerce/pull/50354) +* Update - CYS: Update the tracking URL to the external Fiverr link in sidebar of the **Add your logo** screen. [#50753](https://github.com/woocommerce/woocommerce/pull/50753) +* Update - Enable remote logging feature flag [#50351](https://github.com/woocommerce/woocommerce/pull/50351) +* Update - feat: add `aria-required` attributes to WC form fields [#48371](https://github.com/woocommerce/woocommerce/pull/48371) +* Update - Fixed log-out link behavior so that redirects work, and so that security nonces are automatically added to link in navigation menus. [#49605](https://github.com/woocommerce/woocommerce/pull/49605) +* Update - Migrate LYS user meta [#50664](https://github.com/woocommerce/woocommerce/pull/50664) +* Update - Move marketing task to things to do next task list [#50487](https://github.com/woocommerce/woocommerce/pull/50487) +* Update - Move site visibility badge to admin bar. [#50775](https://github.com/woocommerce/woocommerce/pull/50775) +* Update - Remove "Need help?" modal from onboarding [#47812](https://github.com/woocommerce/woocommerce/pull/47812) +* Update - Remove all links from the CYS sidebars [#50414](https://github.com/woocommerce/woocommerce/pull/50414) +* Update - Remove remote API call from marketing task [#50479](https://github.com/woocommerce/woocommerce/pull/50479) +* Update - Remove WooCommerce Navigation client side feature and deprecate PHP classes. [#50190](https://github.com/woocommerce/woocommerce/pull/50190) +* Update - Renamed columns inside In-App Marketplace > My subscriptions and added action to turn auto-renewal on for a subscription [#49985](https://github.com/woocommerce/woocommerce/pull/49985) +* Update - Rename woocommerce_is_store_page to woocommerce_is_extension_store_page [#50771](https://github.com/woocommerce/woocommerce/pull/50771) +* Update - Reverting the new `buttonAttributes` API. This will be included in a later release [#50763](https://github.com/woocommerce/woocommerce/pull/50763) +* Update - Revert the Zoom Out feature for the CYS experience [#50535](https://github.com/woocommerce/woocommerce/pull/50535) +* Update - Show expiring and expired notices to active and unconnected subscriptions [#50383](https://github.com/woocommerce/woocommerce/pull/50383) +* Update - Store API: Remove the need for nonces when using cart tokens. Remove deprecated X-WC-Store-API-Nonce header. [#50025](https://github.com/woocommerce/woocommerce/pull/50025) +* Update - Strip HTML tags from aria-label in wc_help_tip function [#50103](https://github.com/woocommerce/woocommerce/pull/50103) +* Update - Text adjustments on shipping zones settings page [#50136](https://github.com/woocommerce/woocommerce/pull/50136) +* Update - Update AdditionalPayments task to use default payment gateways [#50674](https://github.com/woocommerce/woocommerce/pull/50674) +* Update - Update add product task button section UI [#50580](https://github.com/woocommerce/woocommerce/pull/50580) +* Update - Update all blocks to use API Version 3. [#48720](https://github.com/woocommerce/woocommerce/pull/48720) +* Update - Update Blueprint settings layout. [#50724](https://github.com/woocommerce/woocommerce/pull/50724) +* Update - Update core profiler continue button container on extension screen [#50582](https://github.com/woocommerce/woocommerce/pull/50582) +* Update - Update Store Alert actions to have unique keys. [#50424](https://github.com/woocommerce/woocommerce/pull/50424) +* Update - Update WooCommercePayments task is_supported to use default suggestions [#50585](https://github.com/woocommerce/woocommerce/pull/50585) +* Dev - Execute test env setup on host instead of wp-env container [#51021](https://github.com/woocommerce/woocommerce/pull/51021) +* Dev - Added code docs with examples to the Analytics classes [#49425](https://github.com/woocommerce/woocommerce/pull/49425) +* Dev - Add lost password e2e tests [#50611](https://github.com/woocommerce/woocommerce/pull/50611) +* Dev - Add unit tests for the product_add_publish track. [#49916](https://github.com/woocommerce/woocommerce/pull/49916) +* Dev - CI: introduce PHPUnit tests sharding. [#50084](https://github.com/woocommerce/woocommerce/pull/50084) +* Dev - CI: minor speed boost of wp-env startup. [#50445](https://github.com/woocommerce/woocommerce/pull/50445) +* Dev - CI: speedup assets size verification job execution time. [#50178](https://github.com/woocommerce/woocommerce/pull/50178) +* Dev - CI: Use a single shard when re-running failed tests in CI [#50492](https://github.com/woocommerce/woocommerce/pull/50492) +* Dev - CI config: update changes list to include more paths [#50399](https://github.com/woocommerce/woocommerce/pull/50399) +* Dev - Clean up unused images [#50516](https://github.com/woocommerce/woocommerce/pull/50516) +* Dev - CYS - Document possible Intro pages [#50171](https://github.com/woocommerce/woocommerce/pull/50171) +* Dev - CYS - Move the "ai/patterns" endpoint to woocommerce admin API. [#50372](https://github.com/woocommerce/woocommerce/pull/50372) +* Dev - CYS - Move the "ai/store-info" endpoint to woocommerce admin API [#50363](https://github.com/woocommerce/woocommerce/pull/50363) +* Dev - CYS - Move the ai/business-description endpoint to woocommerce admin API [#50359](https://github.com/woocommerce/woocommerce/pull/50359) +* Dev - CYS - Move the ai/store-title endpoint to woocommerce admin API [#50352](https://github.com/woocommerce/woocommerce/pull/50352) +* Dev - CYS - Move the `ai/images` endpoint to woocommerce admin API [#50365](https://github.com/woocommerce/woocommerce/pull/50365) +* Dev - CYS - Move the `ai/product` endpoint to woocommerce admin API. [#50393](https://github.com/woocommerce/woocommerce/pull/50393) +* Dev - CYS: add E2E tests for fonts installation. [#50210](https://github.com/woocommerce/woocommerce/pull/50210) +* Dev - E2E tests: add a flaky test reporter for Core e2e tests [#50259](https://github.com/woocommerce/woocommerce/pull/50259) +* Dev - E2E tests: add an option to skip the env setup script running before test execution [#50620](https://github.com/woocommerce/woocommerce/pull/50620) +* Dev - E2E tests: add buildkite-test-collector for blocks e2e tests [#50642](https://github.com/woocommerce/woocommerce/pull/50642) +* Dev - E2E tests: add environment reporter [#49988](https://github.com/woocommerce/woocommerce/pull/49988) +* Dev - E2E tests: add hpos-disabled env and tagged tests with hpos tag [#50448](https://github.com/woocommerce/woocommerce/pull/50448) +* Dev - E2E tests: fixed broken logo picker tests [#50473](https://github.com/woocommerce/woocommerce/pull/50473) +* Dev - E2E tests: fix flakiness in page-loads customer page test [#50559](https://github.com/woocommerce/woocommerce/pull/50559) +* Dev - E2E tests: fix flakiness in product attributes test [#50485](https://github.com/woocommerce/woocommerce/pull/50485) +* Dev - E2E tests: removed Github reporter [#50256](https://github.com/woocommerce/woocommerce/pull/50256) +* Dev - E2E tests: Removed unnecessary pause in the test [#50043](https://github.com/woocommerce/woocommerce/pull/50043) +* Dev - E2E tests for verifying approve, spam and reply to product reviews. [#50060](https://github.com/woocommerce/woocommerce/pull/50060) +* Dev - Fix E2E tests SKU field id #49729 [#49729](https://github.com/woocommerce/woocommerce/pull/49729) +* Dev - Fixes a flaky product variations e2e test [#50807](https://github.com/woocommerce/woocommerce/pull/50807) +* Dev - Fix Metrics CI job [#50214](https://github.com/woocommerce/woocommerce/pull/50214) +* Dev - Fix optional param in PHPdoc for `WC_Admin_Marketplace_Promotions` to generate code-reference w/o warnings [#50732](https://github.com/woocommerce/woocommerce/pull/50732) +* Dev - Fix the Metrics job by adding a missing NVM install step [#50482](https://github.com/woocommerce/woocommerce/pull/50482) +* Dev - Make the Metrics tests use utilities provided by the updated @wordpress/e2e-test-utils-playwright package. [#50626](https://github.com/woocommerce/woocommerce/pull/50626) +* Dev - Mark ReportTable tableData prop as not required [#50816](https://github.com/woocommerce/woocommerce/pull/50816) +* Dev - Monorepo: enable new linting rules for PHP (PSR-4 naming, Strict types declaration). [#49438](https://github.com/woocommerce/woocommerce/pull/49438) +* Dev - Monorepo: tweak Webpack loaders paths filtering for better build perfromance. [#49714](https://github.com/woocommerce/woocommerce/pull/49714) +* Dev - move block theme docs to docs site folder [#50638](https://github.com/woocommerce/woocommerce/pull/50638) +* Dev - move part of checkout docs to main docs folder [#49984](https://github.com/woocommerce/woocommerce/pull/49984) +* Dev - Move `ReportError` to `@woocommerce/components` as `AnalyticsError` [#50108](https://github.com/woocommerce/woocommerce/pull/50108) +* Dev - moving product collection docs to main docs folder [#50368](https://github.com/woocommerce/woocommerce/pull/50368) +* Dev - Reduce duplicated code in Analytics controllers, unify their behavior and API. [#49425](https://github.com/woocommerce/woocommerce/pull/49425) +* Dev - Reduce the amount of duplicated code in Analytics `DataStore`s. [#49425](https://github.com/woocommerce/woocommerce/pull/49425) +* Dev - Removed defaultProps from React functional components since they will be deprecated for React 19 [#50266](https://github.com/woocommerce/woocommerce/pull/50266) +* Dev - Removed directive to disable woocommerce_coming_soon in e2e tests so that we get better test coverage [#50344](https://github.com/woocommerce/woocommerce/pull/50344) +* Dev - Render a React placeholder for offline and WooCommerce Payments settings sections [#50008](https://github.com/woocommerce/woocommerce/pull/50008) +* Dev - Replace `Automattic\WooCommerce\Admin\API\Reports\*\Query` classes with a single `GenericQuery` class. [#49425](https://github.com/woocommerce/woocommerce/pull/49425) +* Dev - Switch `render()` to `createRoot().render()` to use React 18 features. [#48843](https://github.com/woocommerce/woocommerce/pull/48843) +* Dev - Tests: moved api core tests as a suite in e2e-pw [#50024](https://github.com/woocommerce/woocommerce/pull/50024) +* Dev - Tweak the lost password e2e logic [#50666](https://github.com/woocommerce/woocommerce/pull/50666) +* Dev - Update @wordpress/e2e-test-utils-playwright core dependency to wp-6.6 [#50274](https://github.com/woocommerce/woocommerce/pull/50274) +* Dev - Updated e2e tests docs to clarify the use of environments [#50530](https://github.com/woocommerce/woocommerce/pull/50530) +* Dev - Updated the workflow prompting for testing instructions to only run once (preventing double comments) [#50034](https://github.com/woocommerce/woocommerce/pull/50034) +* Dev - Update E2E tests for linked list and variation creation with new component changes. [#50128](https://github.com/woocommerce/woocommerce/pull/50128) +* Dev - Update lys e2e tests to test with both classic and block themes [#50657](https://github.com/woocommerce/woocommerce/pull/50657) +* Dev - Update Playwright to 1.46.1 from 1.45.1 [#50772](https://github.com/woocommerce/woocommerce/pull/50772) +* Dev - Update WP version to 6.6 in Blocks wp-env config. [#49704](https://github.com/woocommerce/woocommerce/pull/49704) +* Dev - Use stricter text selector on test [#50848](https://github.com/woocommerce/woocommerce/pull/50848) +* Dev - [Filter Products by Price]: Update view when changing the min/max value #50651 [#50651](https://github.com/woocommerce/woocommerce/pull/50651) +* Tweak - Disable remote logging feature by default [#51312](https://github.com/woocommerce/woocommerce/pull/51312) +* Tweak - Add GTIN in structured data [#50087](https://github.com/woocommerce/woocommerce/pull/50087) +* Tweak - Add link to title, remove link from a description, minor copy changes to site visibility settings page [#50781](https://github.com/woocommerce/woocommerce/pull/50781) +* Tweak - Add the `woocommerce_should_clear_cart_after_payment` filter to influence whether the cart should be cleared after payment. [#44515](https://github.com/woocommerce/woocommerce/pull/44515) +* Tweak - allows the quantity selector on block cart page to render as readonly when editable is false [#49450](https://github.com/woocommerce/woocommerce/pull/49450) +* Tweak - Bump Jetpack COnnection, Jetpack Constants and a8c MC Stats [#50471](https://github.com/woocommerce/woocommerce/pull/50471) +* Tweak - Extract the checkbox list option logic into its own component [#50566](https://github.com/woocommerce/woocommerce/pull/50566) +* Tweak - Make `geolocation_ajax_get_location_hash` case-insensitive, to reduce the number of cache misses. [#45439](https://github.com/woocommerce/woocommerce/pull/45439) +* Tweak - Optimize large image files [#50517](https://github.com/woocommerce/woocommerce/pull/50517) +* Tweak - Product Collection: fix the PHP deprecated warning [#50661](https://github.com/woocommerce/woocommerce/pull/50661) +* Tweak - Reduce core profiler sticky footer height [#50788](https://github.com/woocommerce/woocommerce/pull/50788) +* Tweak - Remove colon from product data meta box checkboxes [#50619](https://github.com/woocommerce/woocommerce/pull/50619) +* Tweak - Remove the code related to the automatic Products (Beta) -> Product Collection upgrade. [#50440](https://github.com/woocommerce/woocommerce/pull/50440) +* Tweak - Set timeout to 2 seconds for helper product-usage-notice-rules endpoint request [#50821](https://github.com/woocommerce/woocommerce/pull/50821) +* Tweak - Update size of site visibility badge. [#50792](https://github.com/woocommerce/woocommerce/pull/50792) +* Tweak - Vertically center product meta elements [#50826](https://github.com/woocommerce/woocommerce/pull/50826) +* Performance - Cache order dates in options for performance. [#50066](https://github.com/woocommerce/woocommerce/pull/50066) +* Performance - Compress pattern placeholder image assets [#50405](https://github.com/woocommerce/woocommerce/pull/50405) +* Performance - Improve performance of maybe_assign_default_product_cat by only dropping cache and term recounting if changes were made in the database [#50006](https://github.com/woocommerce/woocommerce/pull/50006) +* Performance - Improve setup_tasks_remaining performance [#50655](https://github.com/woocommerce/woocommerce/pull/50655) +* Enhancement - Add a filter to override the SKU database lock. [#49755](https://github.com/woocommerce/woocommerce/pull/49755) +* Enhancement - Add email type to Checkout block email field. [#48611](https://github.com/woocommerce/woocommerce/pull/48611) +* Enhancement - Add filter `woocommerce_is_store_page` to modify whether Coming Soon mode considers a URL a store page or not. [#50174](https://github.com/woocommerce/woocommerce/pull/50174) +* Enhancement - Add username in email reset-password link [#49737](https://github.com/woocommerce/woocommerce/pull/49737) +* Enhancement - CYS: improve CTA [#50278](https://github.com/woocommerce/woocommerce/pull/50278) +* Enhancement - Ensure `wccomHelper` data is only loaded on the Extensions page where it's needed. [#49758](https://github.com/woocommerce/woocommerce/pull/49758) +* Enhancement - Fixed minor issues in the developer documentation recently added by public resources team [#50845](https://github.com/woocommerce/woocommerce/pull/50845) +* Enhancement - Hide zoomed product images for screen readers. [#50003](https://github.com/woocommerce/woocommerce/pull/50003) +* Enhancement - Improve hover style on product tabs when using the Minimal style in the Product Details block [#50605](https://github.com/woocommerce/woocommerce/pull/50605) +* Enhancement - Make screen readers announce notice messages once page loads. [#50061](https://github.com/woocommerce/woocommerce/pull/50061) +* Enhancement - Refactor: Migrate the All Products block to API version 3 [#50203](https://github.com/woocommerce/woocommerce/pull/50203) +* Enhancement - Remove opacity from the hover style of the mini cart button [#50240](https://github.com/woocommerce/woocommerce/pull/50240) +* Enhancement - Use standard link color in legal disclaimers on core profiler [#50830](https://github.com/woocommerce/woocommerce/pull/50830) + = 9.2.3 2024-08-26 = **WooCommerce** From a06c6ba496c0826e77ccfab0a2dfa4a8c929765a Mon Sep 17 00:00:00 2001 From: Ivan Stojadinov Date: Thu, 12 Sep 2024 16:01:40 +0200 Subject: [PATCH 23/59] [e2e] External sites - Include API tests for Pressable and WPCOM (#51284) * Pressable - run only /api-tests * Skip on Pressable * Fix lint errors * WPCOM - run API only * Revert "WPCOM - run API only" This reverts commit 5e59be1663c5ee434be57bc38286ef85402439ef. * WPCOM - run API only * Update settings-crud.test.js so they are passing against Pressable * Skip "can retrieve all tax settings" * Skip "can view all system status items" on WPCOM * Several skips in settings-crud.test.js * Skip "Product review tests: CRUD" on WPCOM * Skip "List all products > categories" on WPCOM * Skip "can view all payment gateways" on WPCOM * Skip "Orders API tests" on WPCOM * Skip "Customers API tests: CRUD" on WPCOM * Revert `playwright.config.js` files and include '**/api-tests/**/*.test.js', * Add changefile(s) from automation for the following project(s): woocommerce * Skip three more API tests for WPCOM * Skip two coupons tests * Update report name for Pressable and WPCOM to ``*-core-e2e-and-api` --------- Co-authored-by: github-actions --- ...4-e2e-external-pressable-include-api-tests | 4 + plugins/woocommerce/package.json | 5 +- .../default-pressable/playwright.config.js | 1 + .../envs/default-wpcom/playwright.config.js | 6 +- .../tests/api-tests/coupons/coupons.test.js | 100 +- .../customers/customers-crud.test.js | 963 +-- .../api-tests/orders/order-complex.test.js | 88 +- .../tests/api-tests/orders/orders.test.js | 6200 +++++++++-------- .../payment-gateways-crud.test.js | 318 +- .../api-tests/products/product-list.test.js | 2171 +++--- .../api-tests/products/products-crud.test.js | 717 +- .../api-tests/settings/settings-crud.test.js | 1034 +-- .../system-status/system-status-crud.test.js | 1114 +-- 13 files changed, 6488 insertions(+), 6233 deletions(-) create mode 100644 plugins/woocommerce/changelog/51284-e2e-external-pressable-include-api-tests diff --git a/plugins/woocommerce/changelog/51284-e2e-external-pressable-include-api-tests b/plugins/woocommerce/changelog/51284-e2e-external-pressable-include-api-tests new file mode 100644 index 00000000000..0351b91c1d8 --- /dev/null +++ b/plugins/woocommerce/changelog/51284-e2e-external-pressable-include-api-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Include API tests into test suites for Pressable and WPCOM. \ No newline at end of file diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index a01cd250ffc..8ec41d6cfda 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -556,7 +556,7 @@ "on-demand" ], "report": { - "resultsBlobName": "default-pressable-core-e2e", + "resultsBlobName": "default-pressable-core-e2e-and-api", "resultsPath": "tests/e2e-pw/test-results", "allure": true } @@ -572,7 +572,7 @@ "on-demand" ], "report": { - "resultsBlobName": "default-wpcom-core-e2e", + "resultsBlobName": "default-wpcom-core-e2e-and-api", "resultsPath": "tests/e2e-pw/test-results", "allure": true } @@ -681,7 +681,6 @@ "node_modules/@woocommerce/e2e-core-tests/CHANGELOG.md", "node_modules/@woocommerce/api/dist/", "node_modules/@woocommerce/admin-e2e-tests/build", - "node_modules/@woocommerce/classic-assets/build", "node_modules/@woocommerce/block-library/build", "node_modules/@woocommerce/block-library/blocks.ini", "node_modules/@woocommerce/admin-library/build", diff --git a/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/playwright.config.js index 259c06b7fc6..4de1709668c 100644 --- a/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/playwright.config.js +++ b/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/playwright.config.js @@ -16,6 +16,7 @@ config = { '**/customize-store/**/*.spec.js', '**/merchant/**/*.spec.js', '**/shopper/**/*.spec.js', + '**/api-tests/**/*.test.js', ], grepInvert: /@skip-on-default-pressable/, }, diff --git a/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js index 62169c1d0dc..08977a04e71 100644 --- a/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js +++ b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js @@ -7,7 +7,11 @@ config = { { name: 'default wpcom', use: { ...devices[ 'Desktop Chrome' ] }, - testMatch: [ '**/basic.spec.js', '**/shopper/**/*.spec.js' ], + testMatch: [ + '**/basic.spec.js', + '**/shopper/**/*.spec.js', + '**/api-tests/**/*.test.js', + ], grepInvert: /@skip-on-default-wpcom/, }, ], diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/coupons/coupons.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/coupons/coupons.test.js index 7c9a8a06565..a4a7cc9abe9 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/coupons/coupons.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/coupons/coupons.test.js @@ -69,26 +69,30 @@ test.describe( 'Coupons API tests', () => { ); } ); - test( 'can permanently delete a coupon', async ( { request } ) => { - //call API to delete previously created coupon - const response = await request.delete( - `/wp-json/wc/v3/coupons/${ couponId }`, - { - data: { force: true }, - } - ); + test( + 'can permanently delete a coupon', + { tag: '@skip-on-default-wpcom' }, + async ( { request } ) => { + //call API to delete previously created coupon + const response = await request.delete( + `/wp-json/wc/v3/coupons/${ couponId }`, + { + data: { force: true }, + } + ); - //validate response - expect( response.status() ).toEqual( 200 ); + //validate response + expect( response.status() ).toEqual( 200 ); - //call API to retrieve previously deleted coupon - const getCouponResponse = await request.get( - `/wp-json/wc/v3/coupons/${ couponId }` - ); + //call API to retrieve previously deleted coupon + const getCouponResponse = await request.get( + `/wp-json/wc/v3/coupons/${ couponId }` + ); - //validate response - expect( getCouponResponse.status() ).toEqual( 404 ); - } ); + //validate response + expect( getCouponResponse.status() ).toEqual( 404 ); + } + ); } ); test.describe( 'Batch update coupons', () => { @@ -180,38 +184,42 @@ test.describe( 'Batch update coupons', () => { expect( updatedCoupons[ 1 ].amount ).toEqual( '25.00' ); } ); - test( 'can batch delete coupons', async ( { request } ) => { - // Batch delete the 2 coupons. - const couponIdsToDelete = expectedCoupons.map( ( { id } ) => id ); - const batchDeletePayload = { - delete: couponIdsToDelete, - }; + test( + 'can batch delete coupons', + { tag: '@skip-on-default-wpcom' }, + async ( { request } ) => { + // Batch delete the 2 coupons. + const couponIdsToDelete = expectedCoupons.map( ( { id } ) => id ); + const batchDeletePayload = { + delete: couponIdsToDelete, + }; - //Call API to batch delete the coupons - const batchDeleteResponse = await request.post( - 'wp-json/wc/v3/coupons/batch', - { - data: batchDeletePayload, - } - ); - const batchDeletePayloadJSON = await batchDeleteResponse.json(); - - // Verify that the response shows the 2 coupons. - const deletedCouponIds = batchDeletePayloadJSON.delete.map( - ( { id } ) => id - ); - expect( batchDeleteResponse.status() ).toEqual( 200 ); - expect( deletedCouponIds ).toEqual( couponIdsToDelete ); - - // Verify that the 2 deleted coupons cannot be retrieved. - for ( const couponId of couponIdsToDelete ) { - //Call the API to attempte to retrieve the coupons - const response = await request.get( - `wp-json/wc/v3/coupons/${ couponId }` + //Call API to batch delete the coupons + const batchDeleteResponse = await request.post( + 'wp-json/wc/v3/coupons/batch', + { + data: batchDeletePayload, + } ); - expect( response.status() ).toEqual( 404 ); + const batchDeletePayloadJSON = await batchDeleteResponse.json(); + + // Verify that the response shows the 2 coupons. + const deletedCouponIds = batchDeletePayloadJSON.delete.map( + ( { id } ) => id + ); + expect( batchDeleteResponse.status() ).toEqual( 200 ); + expect( deletedCouponIds ).toEqual( couponIdsToDelete ); + + // Verify that the 2 deleted coupons cannot be retrieved. + for ( const couponId of couponIdsToDelete ) { + //Call the API to attempte to retrieve the coupons + const response = await request.get( + `wp-json/wc/v3/coupons/${ couponId }` + ); + expect( response.status() ).toEqual( 404 ); + } } - } ); + ); } ); test.describe( 'List coupons', () => { diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/customers/customers-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/customers/customers-crud.test.js index 17be7e0a3bc..a93fef4198a 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/customers/customers-crud.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/customers/customers-crud.test.js @@ -2,500 +2,537 @@ const { test, expect } = require( '../../../fixtures/api-tests-fixtures' ); const { admin } = require( '../../../test-data/data' ); const { customer } = require( '../../../data' ); -test.describe( 'Customers API tests: CRUD', () => { - let customerId; - let subscriberUserId; - let subscriberUserCreatedDuringTests = false; +test.describe( + 'Customers API tests: CRUD', + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, + () => { + let customerId; + let subscriberUserId; + let subscriberUserCreatedDuringTests = false; - test.beforeAll( async ( { request } ) => { - // Call the API to return all users and determine if a - // subscriber user has been created - const customersResponse = await request.get( - '/wp-json/wc/v3/customers', - { - params: { - role: 'all', - }, - } - ); - const customersResponseJSON = await customersResponse.json(); - - for ( const element of customersResponseJSON ) { - if ( element.role === 'subscriber' ) { - subscriberUserId = element.id; - break; - } - } - - // If a subscriber user has not been created then create one - if ( ! subscriberUserId ) { - const now = Date.now(); - const userResponse = await request.post( '/wp-json/wp/v2/users', { - data: { - username: `customer_${ now }`, - email: `customer_${ now }@woocommercecoretestsuite.com`, - first_name: 'Jane', - last_name: 'Smith', - roles: [ 'subscriber' ], - password: 'password', - name: 'Jane', - }, - } ); - const userResponseJSON = await userResponse.json(); - // set subscriber user id to newly created user - subscriberUserId = userResponseJSON.id; - subscriberUserCreatedDuringTests = true; - } - - // Verify the subscriber user has been created - const response = await request.get( - `/wp-json/wc/v3/customers/${ subscriberUserId }` - ); - const responseJSON = await response.json(); - // eslint-disable-next-line jest/no-standalone-expect - expect( response.status() ).toEqual( 200 ); - // eslint-disable-next-line jest/no-standalone-expect - expect( responseJSON.role ).toEqual( 'subscriber' ); - } ); - - test.afterAll( async ( { request } ) => { - // delete subscriber user if one was created during the execution of these tests - if ( subscriberUserCreatedDuringTests ) { - await request.delete( - `/wp-json/wc/v3/customers/${ subscriberUserId }`, + test.beforeAll( async ( { request } ) => { + // Call the API to return all users and determine if a + // subscriber user has been created + const customersResponse = await request.get( + '/wp-json/wc/v3/customers', { - data: { - force: true, + params: { + role: 'all', }, } ); - } - } ); + const customersResponseJSON = await customersResponse.json(); - test.describe( 'Retrieve after env setup', () => { - /** - * when the environment is created, - * (https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/e2e-pw#woocommerce-playwright-end-to-end-tests), - * we have an admin user and a subscriber user that can both be - * accessed through their ids - * admin user will have id 1 and subscriber user will have id 2 - * neither of these are returned as part of the get all customers call - * unless the role 'all' is passed as a search param - * but they can be accessed by specific id reference - */ - test( 'can retrieve admin user', async ( { request } ) => { - // call API to retrieve the previously saved customer - const response = await request.get( '/wp-json/wc/v3/customers/1' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.is_paying_customer ).toEqual( false ); - expect( responseJSON.role ).toEqual( 'administrator' ); - // this test was updated to allow for local test setup and other test sites. - expect( responseJSON.username ).toEqual( admin.username ); - } ); + for ( const element of customersResponseJSON ) { + if ( element.role === 'subscriber' ) { + subscriberUserId = element.id; + break; + } + } - test( 'can retrieve subscriber user', async ( { request } ) => { - // if environment was created with subscriber user - // call API to retrieve the customer with id 2 + // If a subscriber user has not been created then create one + if ( ! subscriberUserId ) { + const now = Date.now(); + const userResponse = await request.post( + '/wp-json/wp/v2/users', + { + data: { + username: `customer_${ now }`, + email: `customer_${ now }@woocommercecoretestsuite.com`, + first_name: 'Jane', + last_name: 'Smith', + roles: [ 'subscriber' ], + password: 'password', + name: 'Jane', + }, + } + ); + const userResponseJSON = await userResponse.json(); + // set subscriber user id to newly created user + subscriberUserId = userResponseJSON.id; + subscriberUserCreatedDuringTests = true; + } + + // Verify the subscriber user has been created const response = await request.get( `/wp-json/wc/v3/customers/${ subscriberUserId }` ); const responseJSON = await response.json(); + // eslint-disable-next-line jest/no-standalone-expect expect( response.status() ).toEqual( 200 ); - expect( responseJSON.is_paying_customer ).toEqual( false ); + // eslint-disable-next-line jest/no-standalone-expect expect( responseJSON.role ).toEqual( 'subscriber' ); } ); - test( 'retrieve user with id 0 is invalid', async ( { request } ) => { - // call API to retrieve the previously saved customer - const response = await request.get( '/wp-json/wc/v3/customers/0' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 404 ); - expect( responseJSON.code ).toEqual( - 'woocommerce_rest_invalid_id' - ); - expect( responseJSON.message ).toEqual( 'Invalid resource ID.' ); + test.afterAll( async ( { request } ) => { + // delete subscriber user if one was created during the execution of these tests + if ( subscriberUserCreatedDuringTests ) { + await request.delete( + `/wp-json/wc/v3/customers/${ subscriberUserId }`, + { + data: { + force: true, + }, + } + ); + } } ); - test( 'can retrieve customers', async ( { request } ) => { - // call API to retrieve all customers should initially return empty array - const response = await request.get( '/wp-json/wc/v3/customers' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); - expect( responseJSON.length ).toEqual( 1 ); - } ); - - // however, if we pass in the search string for role 'all' then all users are returned - test( 'can retrieve all customers', async ( { request } ) => { - // call API to retrieve all customers should initially return empty array - // unless the role 'all' is passed as a search string, in which case the admin - // and subscriber users will be returned - const response = await request.get( '/wp-json/wc/v3/customers', { - params: { - role: 'all', - }, - } ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); - expect( responseJSON.length ).toBeGreaterThanOrEqual( 3 ); - } ); - } ); - - test.describe( 'Create a customer', () => { - test( 'can create a customer', async ( { request } ) => { - // call API to create a customer - const response = await request.post( '/wp-json/wc/v3/customers', { - data: customer, - } ); - const responseJSON = await response.json(); - - // Save the customer ID. It will be used by the retrieve, update, and delete tests. - customerId = responseJSON.id; - - expect( response.status() ).toEqual( 201 ); - expect( typeof responseJSON.id ).toEqual( 'number' ); - // Verify that the customer role is 'customer' - expect( responseJSON.role ).toEqual( 'customer' ); - } ); - } ); - - test.describe( 'Retrieve after create', () => { - test( 'can retrieve a customer', async ( { request } ) => { - // call API to retrieve the previously saved customer - const response = await request.get( - `/wp-json/wc/v3/customers/${ customerId }` - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.id ).toEqual( customerId ); - expect( responseJSON.is_paying_customer ).toEqual( false ); - expect( responseJSON.role ).toEqual( 'customer' ); - } ); - - test( 'can retrieve all customers after create', async ( { - request, - } ) => { - // call API to retrieve all customers - const response = await request.get( '/wp-json/wc/v3/customers' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); - expect( responseJSON.length ).toBeGreaterThan( 0 ); - } ); - } ); - - test.describe( 'Update a customer', () => { - test( `can update the admin user/customer`, async ( { request } ) => { + test.describe( 'Retrieve after env setup', () => { /** - * update customer names (regular, billing and shipping) to admin - * (these were initialised blank when the environment is created, - * (https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/e2e-pw#woocommerce-playwright-end-to-end-tests + * when the environment is created, + * (https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/e2e-pw#woocommerce-playwright-end-to-end-tests), + * we have an admin user and a subscriber user that can both be + * accessed through their ids + * admin user will have id 1 and subscriber user will have id 2 + * neither of these are returned as part of the get all customers call + * unless the role 'all' is passed as a search param + * but they can be accessed by specific id reference */ - const response = await request.put( `/wp-json/wc/v3/customers/1`, { - data: { - first_name: 'admin', + test( 'can retrieve admin user', async ( { request } ) => { + // call API to retrieve the previously saved customer + const response = await request.get( + '/wp-json/wc/v3/customers/1' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.is_paying_customer ).toEqual( false ); + expect( responseJSON.role ).toEqual( 'administrator' ); + // this test was updated to allow for local test setup and other test sites. + expect( responseJSON.username ).toEqual( admin.username ); + } ); + + test( 'can retrieve subscriber user', async ( { request } ) => { + // if environment was created with subscriber user + // call API to retrieve the customer with id 2 + const response = await request.get( + `/wp-json/wc/v3/customers/${ subscriberUserId }` + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.is_paying_customer ).toEqual( false ); + expect( responseJSON.role ).toEqual( 'subscriber' ); + } ); + + test( 'retrieve user with id 0 is invalid', async ( { + request, + } ) => { + // call API to retrieve the previously saved customer + const response = await request.get( + '/wp-json/wc/v3/customers/0' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 404 ); + expect( responseJSON.code ).toEqual( + 'woocommerce_rest_invalid_id' + ); + expect( responseJSON.message ).toEqual( + 'Invalid resource ID.' + ); + } ); + + test( 'can retrieve customers', async ( { request } ) => { + // call API to retrieve all customers should initially return empty array + const response = await request.get( + '/wp-json/wc/v3/customers' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); + expect( responseJSON.length ).toEqual( 1 ); + } ); + + // however, if we pass in the search string for role 'all' then all users are returned + test( 'can retrieve all customers', async ( { request } ) => { + // call API to retrieve all customers should initially return empty array + // unless the role 'all' is passed as a search string, in which case the admin + // and subscriber users will be returned + const response = await request.get( + '/wp-json/wc/v3/customers', + { + params: { + role: 'all', + }, + } + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); + expect( responseJSON.length ).toBeGreaterThanOrEqual( 3 ); + } ); + } ); + + test.describe( 'Create a customer', () => { + test( 'can create a customer', async ( { request } ) => { + // call API to create a customer + const response = await request.post( + '/wp-json/wc/v3/customers', + { + data: customer, + } + ); + const responseJSON = await response.json(); + + // Save the customer ID. It will be used by the retrieve, update, and delete tests. + customerId = responseJSON.id; + + expect( response.status() ).toEqual( 201 ); + expect( typeof responseJSON.id ).toEqual( 'number' ); + // Verify that the customer role is 'customer' + expect( responseJSON.role ).toEqual( 'customer' ); + } ); + } ); + + test.describe( 'Retrieve after create', () => { + test( 'can retrieve a customer', async ( { request } ) => { + // call API to retrieve the previously saved customer + const response = await request.get( + `/wp-json/wc/v3/customers/${ customerId }` + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.id ).toEqual( customerId ); + expect( responseJSON.is_paying_customer ).toEqual( false ); + expect( responseJSON.role ).toEqual( 'customer' ); + } ); + + test( 'can retrieve all customers after create', async ( { + request, + } ) => { + // call API to retrieve all customers + const response = await request.get( + '/wp-json/wc/v3/customers' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); + expect( responseJSON.length ).toBeGreaterThan( 0 ); + } ); + } ); + + test.describe( 'Update a customer', () => { + test( `can update the admin user/customer`, async ( { + request, + } ) => { + /** + * update customer names (regular, billing and shipping) to admin + * (these were initialised blank when the environment is created, + * (https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/e2e-pw#woocommerce-playwright-end-to-end-tests + */ + const response = await request.put( + `/wp-json/wc/v3/customers/1`, + { + data: { + first_name: 'admin', + billing: { + first_name: 'admin', + }, + shipping: { + first_name: 'admin', + }, + }, + } + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.first_name ).toEqual( 'admin' ); + expect( responseJSON.billing.first_name ).toEqual( 'admin' ); + expect( responseJSON.shipping.first_name ).toEqual( 'admin' ); + } ); + + test( 'retrieve after update admin', async ( { request } ) => { + // call API to retrieve the admin customer we updated above + const response = await request.get( + '/wp-json/wc/v3/customers/1' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.first_name ).toEqual( 'admin' ); + expect( responseJSON.billing.first_name ).toEqual( 'admin' ); + expect( responseJSON.shipping.first_name ).toEqual( 'admin' ); + } ); + + test( `can update the subscriber user/customer`, async ( { + request, + } ) => { + // update customer names (billing and shipping) to Jane + // (these were initialised blank, only regular first_name was populated) + const response = await request.put( + `/wp-json/wc/v3/customers/${ subscriberUserId }`, + { + data: { + billing: { + first_name: 'Jane', + }, + shipping: { + first_name: 'Jane', + }, + }, + } + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.first_name ).toEqual( 'Jane' ); + expect( responseJSON.billing.first_name ).toEqual( 'Jane' ); + expect( responseJSON.shipping.first_name ).toEqual( 'Jane' ); + } ); + + test( 'retrieve after update subscriber', async ( { request } ) => { + // call API to retrieve the subscriber customer we updated above + const response = await request.get( + `/wp-json/wc/v3/customers/${ subscriberUserId }` + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.first_name ).toEqual( 'Jane' ); + expect( responseJSON.billing.first_name ).toEqual( 'Jane' ); + expect( responseJSON.shipping.first_name ).toEqual( 'Jane' ); + } ); + + test( `can update a customer`, async ( { request } ) => { + // update customer names (regular, billing and shipping) from John to Jack + const response = await request.put( + `/wp-json/wc/v3/customers/${ customerId }`, + { + data: { + first_name: 'Jack', + billing: { + first_name: 'Jack', + }, + shipping: { + first_name: 'Jack', + }, + }, + } + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.first_name ).toEqual( 'Jack' ); + expect( responseJSON.billing.first_name ).toEqual( 'Jack' ); + expect( responseJSON.shipping.first_name ).toEqual( 'Jack' ); + } ); + + test( 'retrieve after update customer', async ( { request } ) => { + // call API to retrieve the updated customer we created above + const response = await request.get( + `/wp-json/wc/v3/customers/${ customerId }` + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.first_name ).toEqual( 'Jack' ); + expect( responseJSON.billing.first_name ).toEqual( 'Jack' ); + expect( responseJSON.shipping.first_name ).toEqual( 'Jack' ); + } ); + } ); + + test.describe( 'Delete a customer', () => { + test( 'can permanently delete an customer', async ( { + request, + } ) => { + // Delete the customer. + const response = await request.delete( + `/wp-json/wc/v3/customers/${ customerId }`, + { + data: { + force: true, + }, + } + ); + expect( response.status() ).toEqual( 200 ); + + // Verify that the customer can no longer be retrieved. + const getDeletedCustomerResponse = await request.get( + `/wp-json/wc/v3/customers/${ customer }` + ); + expect( getDeletedCustomerResponse.status() ).toEqual( 404 ); + } ); + } ); + + test.describe( 'Batch update customers', () => { + /** + * 2 Customers to be created in one batch. + */ + const expectedCustomers = [ + { + email: 'john.doe2@example.com', + first_name: 'John', + last_name: 'Doe', + username: 'john.doe2', billing: { - first_name: 'admin', + first_name: 'John', + last_name: 'Doe', + company: '', + address_1: '969 Market', + address_2: '', + city: 'San Francisco', + state: 'CA', + postcode: '94103', + country: 'US', + email: 'john.doe2@example.com', + phone: '(555) 555-5555', }, shipping: { - first_name: 'admin', + first_name: 'John', + last_name: 'Doe', + company: '', + address_1: '969 Market', + address_2: '', + city: 'San Francisco', + state: 'CA', + postcode: '94103', + country: 'US', }, }, - } ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.first_name ).toEqual( 'admin' ); - expect( responseJSON.billing.first_name ).toEqual( 'admin' ); - expect( responseJSON.shipping.first_name ).toEqual( 'admin' ); - } ); - - test( 'retrieve after update admin', async ( { request } ) => { - // call API to retrieve the admin customer we updated above - const response = await request.get( '/wp-json/wc/v3/customers/1' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.first_name ).toEqual( 'admin' ); - expect( responseJSON.billing.first_name ).toEqual( 'admin' ); - expect( responseJSON.shipping.first_name ).toEqual( 'admin' ); - } ); - - test( `can update the subscriber user/customer`, async ( { - request, - } ) => { - // update customer names (billing and shipping) to Jane - // (these were initialised blank, only regular first_name was populated) - const response = await request.put( - `/wp-json/wc/v3/customers/${ subscriberUserId }`, { - data: { - billing: { - first_name: 'Jane', - }, - shipping: { - first_name: 'Jane', - }, - }, - } - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.first_name ).toEqual( 'Jane' ); - expect( responseJSON.billing.first_name ).toEqual( 'Jane' ); - expect( responseJSON.shipping.first_name ).toEqual( 'Jane' ); - } ); - - test( 'retrieve after update subscriber', async ( { request } ) => { - // call API to retrieve the subscriber customer we updated above - const response = await request.get( - `/wp-json/wc/v3/customers/${ subscriberUserId }` - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.first_name ).toEqual( 'Jane' ); - expect( responseJSON.billing.first_name ).toEqual( 'Jane' ); - expect( responseJSON.shipping.first_name ).toEqual( 'Jane' ); - } ); - - test( `can update a customer`, async ( { request } ) => { - // update customer names (regular, billing and shipping) from John to Jack - const response = await request.put( - `/wp-json/wc/v3/customers/${ customerId }`, - { - data: { - first_name: 'Jack', - billing: { - first_name: 'Jack', - }, - shipping: { - first_name: 'Jack', - }, - }, - } - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.first_name ).toEqual( 'Jack' ); - expect( responseJSON.billing.first_name ).toEqual( 'Jack' ); - expect( responseJSON.shipping.first_name ).toEqual( 'Jack' ); - } ); - - test( 'retrieve after update customer', async ( { request } ) => { - // call API to retrieve the updated customer we created above - const response = await request.get( - `/wp-json/wc/v3/customers/${ customerId }` - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.first_name ).toEqual( 'Jack' ); - expect( responseJSON.billing.first_name ).toEqual( 'Jack' ); - expect( responseJSON.shipping.first_name ).toEqual( 'Jack' ); - } ); - } ); - - test.describe( 'Delete a customer', () => { - test( 'can permanently delete an customer', async ( { request } ) => { - // Delete the customer. - const response = await request.delete( - `/wp-json/wc/v3/customers/${ customerId }`, - { - data: { - force: true, - }, - } - ); - expect( response.status() ).toEqual( 200 ); - - // Verify that the customer can no longer be retrieved. - const getDeletedCustomerResponse = await request.get( - `/wp-json/wc/v3/customers/${ customer }` - ); - expect( getDeletedCustomerResponse.status() ).toEqual( 404 ); - } ); - } ); - - test.describe( 'Batch update customers', () => { - /** - * 2 Customers to be created in one batch. - */ - const expectedCustomers = [ - { - email: 'john.doe2@example.com', - first_name: 'John', - last_name: 'Doe', - username: 'john.doe2', - billing: { - first_name: 'John', - last_name: 'Doe', - company: '', - address_1: '969 Market', - address_2: '', - city: 'San Francisco', - state: 'CA', - postcode: '94103', - country: 'US', - email: 'john.doe2@example.com', - phone: '(555) 555-5555', - }, - shipping: { - first_name: 'John', - last_name: 'Doe', - company: '', - address_1: '969 Market', - address_2: '', - city: 'San Francisco', - state: 'CA', - postcode: '94103', - country: 'US', - }, - }, - { - email: 'joao.silva2@example.com', - first_name: 'João', - last_name: 'Silva', - username: 'joao.silva2', - billing: { - first_name: 'João', - last_name: 'Silva', - company: '', - address_1: 'Av. Brasil, 432', - address_2: '', - city: 'Rio de Janeiro', - state: 'RJ', - postcode: '12345-000', - country: 'BR', email: 'joao.silva2@example.com', - phone: '(55) 5555-5555', - }, - shipping: { first_name: 'João', last_name: 'Silva', - company: '', - address_1: 'Av. Brasil, 432', - address_2: '', - city: 'Rio de Janeiro', - state: 'RJ', - postcode: '12345-000', - country: 'BR', + username: 'joao.silva2', + billing: { + first_name: 'João', + last_name: 'Silva', + company: '', + address_1: 'Av. Brasil, 432', + address_2: '', + city: 'Rio de Janeiro', + state: 'RJ', + postcode: '12345-000', + country: 'BR', + email: 'joao.silva2@example.com', + phone: '(55) 5555-5555', + }, + shipping: { + first_name: 'João', + last_name: 'Silva', + company: '', + address_1: 'Av. Brasil, 432', + address_2: '', + city: 'Rio de Janeiro', + state: 'RJ', + postcode: '12345-000', + country: 'BR', + }, }, - }, - ]; + ]; - // set payload to use batch create: action - const batchCreate2CustomersPayload = { - create: expectedCustomers, - }; - - test( 'can batch create customers', async ( { request } ) => { - // Batch create 2 new customers. - // call API to batch create customers - const response = await request.post( - 'wp-json/wc/v3/customers/batch', - { - data: batchCreate2CustomersPayload, - } - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - - // Verify that the 2 new customers were created - const actualCustomers = responseJSON.create; - expect( actualCustomers ).toHaveLength( expectedCustomers.length ); - - for ( let i = 0; i < actualCustomers.length; i++ ) { - const { id, first_name } = actualCustomers[ i ]; - const expectedCustomerName = expectedCustomers[ i ].first_name; - - expect( id ).toBeDefined(); - expect( first_name ).toEqual( expectedCustomerName ); - - // Save the customer id - expectedCustomers[ i ].id = id; - } - } ); - - test( 'can batch update customers', async ( { request } ) => { - // set payload to use batch update: action - const batchUpdatePayload = { - update: [ - { - id: expectedCustomers[ 0 ].id, - email: 'emailupdated@example.com', - }, - { - id: expectedCustomers[ 1 ].id, - billing: { - address_1: '123 Addressupdate Street', - }, - }, - ], + // set payload to use batch create: action + const batchCreate2CustomersPayload = { + create: expectedCustomers, }; - // Call API to update the customers - const response = await request.post( - 'wp-json/wc/v3/customers/batch', - { - data: batchUpdatePayload, - } - ); - const responseJSON = await response.json(); - - // Verify the response code and the number of customers that were updated. - const updatedCustomers = responseJSON.update; - expect( response.status() ).toEqual( 200 ); - expect( updatedCustomers ).toHaveLength( 2 ); - - // Verify that the 1st customer was updated to have a new email address. - expect( updatedCustomers[ 0 ].id ).toEqual( - expectedCustomers[ 0 ].id - ); - expect( updatedCustomers[ 0 ].email ).toEqual( - 'emailupdated@example.com' - ); - - // Verify that the amount of the 2nd customer was updated to have a new billing address. - expect( updatedCustomers[ 1 ].id ).toEqual( - expectedCustomers[ 1 ].id - ); - expect( updatedCustomers[ 1 ].billing.address_1 ).toEqual( - '123 Addressupdate Street' - ); - } ); - - test( 'can batch delete customers', async ( { request } ) => { - // Batch delete the 2 customers. - const customerIdsToDelete = expectedCustomers.map( - ( { id } ) => id - ); - const batchDeletePayload = { - delete: customerIdsToDelete, - }; - - //Call API to batch delete the customers - const response = await request.post( - 'wp-json/wc/v3/customers/batch', - { - data: batchDeletePayload, - } - ); - const responseJSON = await response.json(); - - // Verify that the response shows the 2 customers. - const deletedCustomerIds = responseJSON.delete.map( - ( { id } ) => id - ); - expect( response.status() ).toEqual( 200 ); - expect( deletedCustomerIds ).toEqual( customerIdsToDelete ); - - // Verify that the 2 deleted customers cannot be retrieved. - for ( const id of customerIdsToDelete ) { - //Call the API to attempte to retrieve the customers - const r = await request.get( - `wp-json/wc/v3/customers/${ id }` + test( 'can batch create customers', async ( { request } ) => { + // Batch create 2 new customers. + // call API to batch create customers + const response = await request.post( + 'wp-json/wc/v3/customers/batch', + { + data: batchCreate2CustomersPayload, + } ); - expect( r.status() ).toEqual( 404 ); - } + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + + // Verify that the 2 new customers were created + const actualCustomers = responseJSON.create; + expect( actualCustomers ).toHaveLength( + expectedCustomers.length + ); + + for ( let i = 0; i < actualCustomers.length; i++ ) { + const { id, first_name } = actualCustomers[ i ]; + const expectedCustomerName = + expectedCustomers[ i ].first_name; + + expect( id ).toBeDefined(); + expect( first_name ).toEqual( expectedCustomerName ); + + // Save the customer id + expectedCustomers[ i ].id = id; + } + } ); + + test( 'can batch update customers', async ( { request } ) => { + // set payload to use batch update: action + const batchUpdatePayload = { + update: [ + { + id: expectedCustomers[ 0 ].id, + email: 'emailupdated@example.com', + }, + { + id: expectedCustomers[ 1 ].id, + billing: { + address_1: '123 Addressupdate Street', + }, + }, + ], + }; + + // Call API to update the customers + const response = await request.post( + 'wp-json/wc/v3/customers/batch', + { + data: batchUpdatePayload, + } + ); + const responseJSON = await response.json(); + + // Verify the response code and the number of customers that were updated. + const updatedCustomers = responseJSON.update; + expect( response.status() ).toEqual( 200 ); + expect( updatedCustomers ).toHaveLength( 2 ); + + // Verify that the 1st customer was updated to have a new email address. + expect( updatedCustomers[ 0 ].id ).toEqual( + expectedCustomers[ 0 ].id + ); + expect( updatedCustomers[ 0 ].email ).toEqual( + 'emailupdated@example.com' + ); + + // Verify that the amount of the 2nd customer was updated to have a new billing address. + expect( updatedCustomers[ 1 ].id ).toEqual( + expectedCustomers[ 1 ].id + ); + expect( updatedCustomers[ 1 ].billing.address_1 ).toEqual( + '123 Addressupdate Street' + ); + } ); + + test( 'can batch delete customers', async ( { request } ) => { + // Batch delete the 2 customers. + const customerIdsToDelete = expectedCustomers.map( + ( { id } ) => id + ); + const batchDeletePayload = { + delete: customerIdsToDelete, + }; + + //Call API to batch delete the customers + const response = await request.post( + 'wp-json/wc/v3/customers/batch', + { + data: batchDeletePayload, + } + ); + const responseJSON = await response.json(); + + // Verify that the response shows the 2 customers. + const deletedCustomerIds = responseJSON.delete.map( + ( { id } ) => id + ); + expect( response.status() ).toEqual( 200 ); + expect( deletedCustomerIds ).toEqual( customerIdsToDelete ); + + // Verify that the 2 deleted customers cannot be retrieved. + for ( const id of customerIdsToDelete ) { + //Call the API to attempte to retrieve the customers + const r = await request.get( + `wp-json/wc/v3/customers/${ id }` + ); + expect( r.status() ).toEqual( 404 ); + } + } ); } ); - } ); -} ); + } +); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-complex.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-complex.test.js index 5308ecdcfa1..90df59a1b9c 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-complex.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-complex.test.js @@ -208,48 +208,52 @@ test.describe( 'Orders API test', () => { } ); } ); - test( 'can add complex order', async ( { request } ) => { - //ensure tax calculations are enabled - await request.put( - '/wp-json/wc/v3/settings/general/woocommerce_calc_taxes', - { - data: { - value: 'yes', - }, + test( + 'can add complex order', + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, + async ( { request } ) => { + //ensure tax calculations are enabled + await request.put( + '/wp-json/wc/v3/settings/general/woocommerce_calc_taxes', + { + data: { + value: 'yes', + }, + } + ); + + // Create the complex order and save its ID. + const response = await request.post( '/wp-json/wc/v3/orders', { + data: order, + } ); + const responseJSON = await response.json(); + + order.id = responseJSON.id; + + expect( response.status() ).toEqual( 201 ); + + // Verify order and tax totals + expect( responseJSON.total ).toEqual( expectedOrderTotal ); + expect( responseJSON.total_tax ).toEqual( expectedTaxTotal ); + + // Verify total tax of each product line item + const expectedTaxTotalsPerLineItem = [ + [ simpleProduct, expectedSimpleProductTaxTotal ], + [ variableProduct, expectedVariableProductTaxTotal ], + [ groupedProduct, expectedSimpleProductTaxTotal ], + [ externalProduct, expectedExternalProductTaxTotal ], + ]; + for ( const [ + product, + expectedLineTaxTotal, + ] of expectedTaxTotalsPerLineItem ) { + const { total_tax: actualLineTaxTotal } = + responseJSON.line_items.find( + ( { product_id } ) => product_id === product.id + ); + + expect( actualLineTaxTotal ).toEqual( expectedLineTaxTotal ); } - ); - - // Create the complex order and save its ID. - const response = await request.post( '/wp-json/wc/v3/orders', { - data: order, - } ); - const responseJSON = await response.json(); - - order.id = responseJSON.id; - - expect( response.status() ).toEqual( 201 ); - - // Verify order and tax totals - expect( responseJSON.total ).toEqual( expectedOrderTotal ); - expect( responseJSON.total_tax ).toEqual( expectedTaxTotal ); - - // Verify total tax of each product line item - const expectedTaxTotalsPerLineItem = [ - [ simpleProduct, expectedSimpleProductTaxTotal ], - [ variableProduct, expectedVariableProductTaxTotal ], - [ groupedProduct, expectedSimpleProductTaxTotal ], - [ externalProduct, expectedExternalProductTaxTotal ], - ]; - for ( const [ - product, - expectedLineTaxTotal, - ] of expectedTaxTotalsPerLineItem ) { - const { total_tax: actualLineTaxTotal } = - responseJSON.line_items.find( - ( { product_id } ) => product_id === product.id - ); - - expect( actualLineTaxTotal ).toEqual( expectedLineTaxTotal ); } - } ); + ); } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders.test.js index f0164b40982..b6a556ce2de 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders.test.js @@ -34,3232 +34,3274 @@ const updatedCustomerShipping = { phone: '123456789', }; -test.describe.serial( 'Orders API tests', () => { - let orderId, sampleData; +test.describe.serial( + 'Orders API tests', + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, + () => { + let orderId, sampleData; - test.beforeAll( async ( { request } ) => { - const createSampleCategories = async () => { - const clothing = await request.post( - '/wp-json/wc/v3/products/categories', - { - data: { - name: 'Clothing', - }, - } - ); - const clothingJSON = await clothing.json(); - - const accessories = await request.post( - '/wp-json/wc/v3/products/categories', - { - data: { - name: 'Accessories', - parent: clothingJSON.id, - }, - } - ); - const accessoriesJSON = await accessories.json(); - - const hoodies = await request.post( - '/wp-json/wc/v3/products/categories', - { - data: { - name: 'Hoodies', - parent: clothingJSON.id, - }, - } - ); - const hoodiesJSON = await hoodies.json(); - - const tshirts = await request.post( - '/wp-json/wc/v3/products/categories', - { - data: { - name: 'Tshirts', - parent: clothingJSON.id, - }, - } - ); - const tshirtsJSON = await tshirts.json(); - - const decor = await request.post( - '/wp-json/wc/v3/products/categories', - { - data: { - name: 'Decor', - }, - } - ); - const decorJSON = await decor.json(); - - const music = await request.post( - '/wp-json/wc/v3/products/categories', - { - data: { - name: 'Music', - }, - } - ); - const musicJSON = await music.json(); - - return { - clothingJSON, - accessoriesJSON, - hoodiesJSON, - tshirtsJSON, - decorJSON, - musicJSON, - }; - }; - - const createSampleAttributes = async () => { - const color = await request.post( - '/wp-json/wc/v3/products/attributes', - { - data: { - name: 'Color', - }, - } - ); - const colorJSON = await color.json(); - - const size = await request.post( - '/wp-json/wc/v3/products/attributes', - { - data: { - name: 'Size', - }, - } - ); - const sizeJSON = await size.json(); - - const colorNames = [ 'Blue', 'Gray', 'Green', 'Red', 'Yellow' ]; - - const colorNamesObjectArray = colorNames.map( ( name ) => ( { - name, - } ) ); - - const colors = await request.post( - `/wp-json/wc/v3/products/attributes/${ colorJSON.id }/terms/batch`, - { - data: { - create: colorNamesObjectArray, - }, - } - ); - - const colorsJSON = await colors.json(); - - const sizeNames = [ 'Large', 'Medium', 'Small' ]; - - const sizeNamesObjectArray = sizeNames.map( ( name ) => ( { - name, - } ) ); - - const sizes = await request.post( - `/wp-json/wc/v3/products/attributes/${ sizeJSON.id }/terms/batch`, - { - data: { - create: sizeNamesObjectArray, - }, - } - ); - const sizesJSON = await sizes.json(); - - return { - colorJSON, - colors: colorsJSON.create, - sizeJSON, - sizes: sizesJSON.create, - }; - }; - - const createSampleTags = async () => { - const cool = await request.post( '/wp-json/wc/v3/products/tags', { - data: { - name: 'Cool', - }, - } ); - const coolJSON = await cool.json(); - - return { - coolJSON, - }; - }; - - const createSampleShippingClasses = async () => { - const freight = await request.post( - '/wp-json/wc/v3/products/shipping_classes', - { - data: { - name: 'Freight', - }, - } - ); - const freightJSON = await freight.json(); - - return { - freightJSON, - }; - }; - - const createSampleTaxClasses = async () => { - //check to see if Reduced Rate tax class exists - if not, create it - let reducedRate = await request.get( - '/wp-json/wc/v3/taxes/classes/reduced-rate' - ); - let reducedRateJSON = await reducedRate.json(); - expect( Array.isArray( reducedRateJSON ) ).toBe( true ); - - //if tax class does not exist then create it - if ( reducedRateJSON.length < 1 ) { - reducedRate = await request.post( - '/wp-json/wc/v3/taxes/classes', + test.beforeAll( async ( { request } ) => { + const createSampleCategories = async () => { + const clothing = await request.post( + '/wp-json/wc/v3/products/categories', { data: { - name: 'Reduced Rate', + name: 'Clothing', }, } ); - reducedRateJSON = await reducedRate.json(); - return { reducedRateJSON }; - } + const clothingJSON = await clothing.json(); - // return an empty object as nothing new was created so nothing will - // need deleted during cleanup - return {}; - }; - - const createSampleSimpleProducts = async ( - categories, - attributes, - tags - ) => { - const description = - '

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

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

This is a simple product.

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

This is a simple product.

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

This is a simple, virtual product.

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

This is a simple, virtual product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

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

\n', - short_description: - '

This is an external product.

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

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

\n', - short_description: - '

This is a grouped product.

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

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

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

This is a variable product.

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

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

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

This is a variable product.

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

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

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

This is a simple product.

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

This is a simple product.

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

This is a simple, virtual product.

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

This is a simple, virtual product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

This is a simple product.

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

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

\n', + short_description: + '

This is an external product.

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

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

\n', + short_description: + '

This is a grouped product.

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

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

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

This is a variable product.

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

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

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

This is a variable product.

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

Nice simple product!

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

On reflection, I hate this thing!

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

Nice simple product!

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

On reflection, I hate this thing!

\n' + ); + expect( responseUpdatedReviewJSON.rating ).toEqual( 1 ); + + // Verify that the deleted review can no longer be retrieved. + const getDeletedProductReviewResponse = await request.get( + `wp-json/wc/v3/products/reviews/${ review2Id }` + ); + expect( getDeletedProductReviewResponse.status() ).toEqual( + 404 + ); + + // Batch delete the created tags + await request.post( `wp-json/wc/v3/products/reviews/batch`, { + data: { + delete: [ review1Id, review3Id ], + }, + } ); + } ); + } + ); test.describe( 'Product shipping classes tests: CRUD', () => { let productShippingClassId; @@ -958,83 +981,91 @@ test.describe( 'Products API tests: CRUD', () => { ); } ); - test( 'can batch update product shipping classes', async ( { - request, - } ) => { - // Batch create product shipping classes. - const response = await request.post( - `wp-json/wc/v3/products/shipping_classes/batch`, - { - data: { - create: [ - { - name: 'Small Items', - }, - { - name: 'Large Items', - }, - ], - }, - } - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( responseJSON.create[ 0 ].name ).toEqual( 'Small Items' ); - expect( responseJSON.create[ 1 ].name ).toEqual( 'Large Items' ); - const shippingClass1Id = responseJSON.create[ 0 ].id; - const shippingClass2Id = responseJSON.create[ 1 ].id; + test( + 'can batch update product shipping classes', + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, + async ( { request } ) => { + // Batch create product shipping classes. + const response = await request.post( + `wp-json/wc/v3/products/shipping_classes/batch`, + { + data: { + create: [ + { + name: 'Small Items', + }, + { + name: 'Large Items', + }, + ], + }, + } + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( responseJSON.create[ 0 ].name ).toEqual( + 'Small Items' + ); + expect( responseJSON.create[ 1 ].name ).toEqual( + 'Large Items' + ); + const shippingClass1Id = responseJSON.create[ 0 ].id; + const shippingClass2Id = responseJSON.create[ 1 ].id; - // Batch create a new shipping class, update a shipping class and delete another. - const responseBatchUpdate = await request.post( - `wp-json/wc/v3/products/shipping_classes/batch`, - { - data: { - create: [ - { - name: 'Express', - }, - ], - update: [ - { - id: shippingClass1Id, - description: 'Priority shipping.', - }, - ], - delete: [ shippingClass2Id ], - }, - } - ); - const responseBatchUpdateJSON = await responseBatchUpdate.json(); - const shippingClass3Id = responseBatchUpdateJSON.create[ 0 ].id; - expect( response.status() ).toEqual( 200 ); + // Batch create a new shipping class, update a shipping class and delete another. + const responseBatchUpdate = await request.post( + `wp-json/wc/v3/products/shipping_classes/batch`, + { + data: { + create: [ + { + name: 'Express', + }, + ], + update: [ + { + id: shippingClass1Id, + description: 'Priority shipping.', + }, + ], + delete: [ shippingClass2Id ], + }, + } + ); + const responseBatchUpdateJSON = + await responseBatchUpdate.json(); + const shippingClass3Id = responseBatchUpdateJSON.create[ 0 ].id; + expect( response.status() ).toEqual( 200 ); - const responseUpdatedShippingClass = await request.get( - `wp-json/wc/v3/products/shipping_classes/${ shippingClass1Id }` - ); - const responseUpdatedShippingClassJSON = - await responseUpdatedShippingClass.json(); - expect( responseUpdatedShippingClassJSON.description ).toEqual( - 'Priority shipping.' - ); + const responseUpdatedShippingClass = await request.get( + `wp-json/wc/v3/products/shipping_classes/${ shippingClass1Id }` + ); + const responseUpdatedShippingClassJSON = + await responseUpdatedShippingClass.json(); + expect( responseUpdatedShippingClassJSON.description ).toEqual( + 'Priority shipping.' + ); - // Verify that the product tag can no longer be retrieved. - const getDeletedProductShippingClassResponse = await request.get( - `wp-json/wc/v3/products/shipping_classes/${ shippingClass2Id }` - ); - expect( getDeletedProductShippingClassResponse.status() ).toEqual( - 404 - ); + // Verify that the product tag can no longer be retrieved. + const getDeletedProductShippingClassResponse = + await request.get( + `wp-json/wc/v3/products/shipping_classes/${ shippingClass2Id }` + ); + expect( + getDeletedProductShippingClassResponse.status() + ).toEqual( 404 ); - // Batch delete the created tags - await request.post( - `wp-json/wc/v3/products/shipping_classes/batch`, - { - data: { - delete: [ shippingClass1Id, shippingClass3Id ], - }, - } - ); - } ); + // Batch delete the created tags + await request.post( + `wp-json/wc/v3/products/shipping_classes/batch`, + { + data: { + delete: [ shippingClass1Id, shippingClass3Id ], + }, + } + ); + } + ); } ); test.describe( 'Product tags tests: CRUD', () => { diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/settings/settings-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/settings/settings-crud.test.js index 376d606c745..e7caee7e0b4 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/settings/settings-crud.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/settings/settings-crud.test.js @@ -280,7 +280,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => { type: 'text', default: '', tip: 'The street address for your business location.', - value: '', + value: expect.any( String ), } ), ] ) ); @@ -310,7 +310,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => { type: 'text', default: '', tip: 'The city in which your business is located.', - value: '', + value: expect.any( String ), } ), ] ) ); @@ -341,7 +341,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => { type: 'text', default: '', tip: 'The postal code, if any, in which your business is located.', - value: '', + value: expect.any( String ), } ), ] ) ); @@ -1019,157 +1019,164 @@ test.describe.serial( 'Settings API tests: CRUD', () => { } ); test.describe( 'List all Tax settings options', () => { - test( 'can retrieve all tax settings', async ( { request } ) => { - // call API to retrieve all settings options - const response = await request.get( '/wp-json/wc/v3/settings/tax' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); - expect( responseJSON.length ).toBeGreaterThan( 0 ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_prices_include_tax', - label: 'Prices entered with tax', - description: '', - type: 'radio', - default: 'no', - options: { - yes: 'Yes, I will enter prices inclusive of tax', - no: 'No, I will enter prices exclusive of tax', - }, - tip: 'This option is important as it will affect how you input prices. Changing it will not update existing products.', - value: 'no', - } ), - ] ) - ); + test( + 'can retrieve all tax settings', + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, + async ( { request } ) => { + // call API to retrieve all settings options + const response = await request.get( + '/wp-json/wc/v3/settings/tax' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); + expect( responseJSON.length ).toBeGreaterThan( 0 ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_prices_include_tax', + label: 'Prices entered with tax', + description: '', + type: 'radio', + default: 'no', + options: { + yes: 'Yes, I will enter prices inclusive of tax', + no: 'No, I will enter prices exclusive of tax', + }, + tip: 'This option is important as it will affect how you input prices. Changing it will not update existing products.', + value: 'no', + } ), + ] ) + ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_tax_based_on', - label: 'Calculate tax based on', - description: '', - type: 'select', - default: 'shipping', - options: { - shipping: 'Customer shipping address', - billing: 'Customer billing address', - base: 'Shop base address', - }, - tip: 'This option determines which address is used to calculate tax.', - value: 'shipping', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_shipping_tax_class', - label: 'Shipping tax class', - description: - 'Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.', - type: 'select', - default: 'inherit', - options: { - inherit: 'Shipping tax class based on cart items', - '': 'Standard', - 'reduced-rate': 'Reduced rate', - 'zero-rate': 'Zero rate', - }, - tip: 'Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.', - value: 'inherit', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_tax_round_at_subtotal', - label: 'Rounding', - description: - 'Round tax at subtotal level, instead of rounding per line', - type: 'checkbox', - default: 'no', - value: 'no', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_tax_classes', - label: 'Additional tax classes', - description: '', - type: 'textarea', - default: '', - tip: 'List additional tax classes you need below (1 per line, e.g. Reduced Rates). These are in addition to "Standard rate" which exists by default.', - value: '', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_tax_display_shop', - label: 'Display prices in the shop', - description: '', - type: 'select', - default: 'excl', - options: { - incl: 'Including tax', - excl: 'Excluding tax', - }, - value: 'excl', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_tax_display_cart', - label: 'Display prices during cart and checkout', - description: '', - type: 'select', - default: 'excl', - options: { - incl: 'Including tax', - excl: 'Excluding tax', - }, - value: 'excl', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_price_display_suffix', - label: 'Price display suffix', - description: '', - type: 'text', - default: '', - tip: 'Define text to show after your product prices. This could be, for example, "inc. Vat" to explain your pricing. You can also have prices substituted here using one of the following: {price_including_tax}, {price_excluding_tax}.', - value: '', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_tax_total_display', - label: 'Display tax totals', - description: '', - type: 'select', - default: 'itemized', - options: { - single: 'As a single total', - itemized: 'Itemized', - }, - value: 'itemized', - } ), - ] ) - ); - } ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_tax_based_on', + label: 'Calculate tax based on', + description: '', + type: 'select', + default: 'shipping', + options: { + shipping: 'Customer shipping address', + billing: 'Customer billing address', + base: 'Shop base address', + }, + tip: 'This option determines which address is used to calculate tax.', + value: 'shipping', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_shipping_tax_class', + label: 'Shipping tax class', + description: + 'Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.', + type: 'select', + default: 'inherit', + options: { + inherit: + 'Shipping tax class based on cart items', + '': 'Standard', + 'reduced-rate': 'Reduced rate', + 'zero-rate': 'Zero rate', + }, + tip: 'Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.', + value: 'inherit', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_tax_round_at_subtotal', + label: 'Rounding', + description: + 'Round tax at subtotal level, instead of rounding per line', + type: 'checkbox', + default: 'no', + value: 'no', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_tax_classes', + label: 'Additional tax classes', + description: '', + type: 'textarea', + default: '', + tip: 'List additional tax classes you need below (1 per line, e.g. Reduced Rates). These are in addition to "Standard rate" which exists by default.', + value: '', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_tax_display_shop', + label: 'Display prices in the shop', + description: '', + type: 'select', + default: 'excl', + options: { + incl: 'Including tax', + excl: 'Excluding tax', + }, + value: 'excl', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_tax_display_cart', + label: 'Display prices during cart and checkout', + description: '', + type: 'select', + default: 'excl', + options: { + incl: 'Including tax', + excl: 'Excluding tax', + }, + value: 'excl', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_price_display_suffix', + label: 'Price display suffix', + description: '', + type: 'text', + default: '', + tip: 'Define text to show after your product prices. This could be, for example, "inc. Vat" to explain your pricing. You can also have prices substituted here using one of the following: {price_including_tax}, {price_excluding_tax}.', + value: '', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_tax_total_display', + label: 'Display tax totals', + description: '', + type: 'select', + default: 'itemized', + options: { + single: 'As a single total', + itemized: 'Itemized', + }, + value: 'itemized', + } ), + ] ) + ); + } + ); } ); test.describe( 'List all Shipping settings options', () => { @@ -1613,286 +1620,294 @@ test.describe.serial( 'Settings API tests: CRUD', () => { } ); } ); - test.describe( 'List all Advanced settings options', () => { - test( 'can retrieve all advanced settings', async ( { request } ) => { - // call API to retrieve all settings options - const response = await request.get( - '/wp-json/wc/v3/settings/advanced' - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); + test.describe( + 'List all Advanced settings options', + { tag: '@skip-on-default-wpcom' }, + () => { + test( 'can retrieve all advanced settings', async ( { + request, + } ) => { + // call API to retrieve all settings options + const response = await request.get( + '/wp-json/wc/v3/settings/advanced' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); + + // not present in external host + // eslint-disable-next-line playwright/no-conditional-in-test + if ( ! shouldSkip ) { + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_cart_page_id', + label: 'Cart page', + description: + 'Page where shoppers review their shopping cart', + type: 'select', + default: '', + tip: 'Page where shoppers review their shopping cart', + value: expect.any( String ), + options: expect.any( Object ), + } ), + ] ) + ); + } + + // not present in external host + // eslint-disable-next-line playwright/no-conditional-in-test + if ( ! shouldSkip ) { + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_checkout_page_id', + label: 'Checkout page', + description: + 'Page where shoppers go to finalize their purchase', + type: 'select', + default: expect.any( Number ), + tip: 'Page where shoppers go to finalize their purchase', + value: expect.any( String ), + options: expect.any( Object ), + } ), + ] ) + ); + } - // not present in external host - // eslint-disable-next-line playwright/no-conditional-in-test - if ( ! shouldSkip ) { expect( responseJSON ).toEqual( expect.arrayContaining( [ expect.objectContaining( { - id: 'woocommerce_cart_page_id', - label: 'Cart page', + id: 'woocommerce_myaccount_page_id', + label: 'My account page', description: - 'Page where shoppers review their shopping cart', + 'Page contents: [woocommerce_my_account]', type: 'select', default: '', - tip: 'Page where shoppers review their shopping cart', + tip: 'Page contents: [woocommerce_my_account]', value: expect.any( String ), options: expect.any( Object ), } ), ] ) ); - } - - // not present in external host - // eslint-disable-next-line playwright/no-conditional-in-test - if ( ! shouldSkip ) { expect( responseJSON ).toEqual( expect.arrayContaining( [ expect.objectContaining( { - id: 'woocommerce_checkout_page_id', - label: 'Checkout page', + id: 'woocommerce_checkout_pay_endpoint', + label: 'Pay', description: - 'Page where shoppers go to finalize their purchase', - type: 'select', - default: expect.any( Number ), - tip: 'Page where shoppers go to finalize their purchase', - value: expect.any( String ), - options: expect.any( Object ), + 'Endpoint for the "Checkout → Pay" page.', + type: 'text', + default: 'order-pay', + tip: 'Endpoint for the "Checkout → Pay" page.', + value: 'order-pay', } ), ] ) ); - } - - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_page_id', - label: 'My account page', - description: 'Page contents: [woocommerce_my_account]', - type: 'select', - default: '', - tip: 'Page contents: [woocommerce_my_account]', - value: expect.any( String ), - options: expect.any( Object ), - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_checkout_pay_endpoint', - label: 'Pay', - description: - 'Endpoint for the "Checkout → Pay" page.', - type: 'text', - default: 'order-pay', - tip: 'Endpoint for the "Checkout → Pay" page.', - value: 'order-pay', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_checkout_order_received_endpoint', - label: 'Order received', - description: - 'Endpoint for the "Checkout → Order received" page.', - type: 'text', - default: 'order-received', - tip: 'Endpoint for the "Checkout → Order received" page.', - value: 'order-received', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_add_payment_method_endpoint', - label: 'Add payment method', - description: - 'Endpoint for the "Checkout → Add payment method" page.', - type: 'text', - default: 'add-payment-method', - tip: 'Endpoint for the "Checkout → Add payment method" page.', - value: 'add-payment-method', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_delete_payment_method_endpoint', - label: 'Delete payment method', - description: - 'Endpoint for the delete payment method page.', - type: 'text', - default: 'delete-payment-method', - tip: 'Endpoint for the delete payment method page.', - value: 'delete-payment-method', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_orders_endpoint', - label: 'Orders', - description: - 'Endpoint for the "My account → Orders" page.', - type: 'text', - default: 'orders', - tip: 'Endpoint for the "My account → Orders" page.', - value: 'orders', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_view_order_endpoint', - label: 'View order', - description: - 'Endpoint for the "My account → View order" page.', - type: 'text', - default: 'view-order', - tip: 'Endpoint for the "My account → View order" page.', - value: 'view-order', - } ), - ] ) - ); - - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_downloads_endpoint', - label: 'Downloads', - description: - 'Endpoint for the "My account → Downloads" page.', - type: 'text', - default: 'downloads', - tip: 'Endpoint for the "My account → Downloads" page.', - value: 'downloads', - } ), - ] ) - ); - - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_edit_account_endpoint', - label: 'Edit account', - description: - 'Endpoint for the "My account → Edit account" page.', - type: 'text', - default: 'edit-account', - tip: 'Endpoint for the "My account → Edit account" page.', - value: 'edit-account', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_edit_address_endpoint', - label: 'Addresses', - description: - 'Endpoint for the "My account → Addresses" page.', - type: 'text', - default: 'edit-address', - tip: 'Endpoint for the "My account → Addresses" page.', - value: 'edit-address', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_payment_methods_endpoint', - label: 'Payment methods', - description: - 'Endpoint for the "My account → Payment methods" page.', - type: 'text', - default: 'payment-methods', - tip: 'Endpoint for the "My account → Payment methods" page.', - value: 'payment-methods', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_myaccount_lost_password_endpoint', - label: 'Lost password', - description: - 'Endpoint for the "My account → Lost password" page.', - type: 'text', - default: 'lost-password', - tip: 'Endpoint for the "My account → Lost password" page.', - value: 'lost-password', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_logout_endpoint', - label: 'Logout', - description: - 'Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true', - type: 'text', - default: 'customer-logout', - tip: 'Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true', - value: 'customer-logout', - } ), - ] ) - ); - // eslint-disable-next-line playwright/no-conditional-in-test - if ( ! shouldSkip ) { expect( responseJSON ).toEqual( expect.arrayContaining( [ expect.objectContaining( { - id: 'woocommerce_allow_tracking', - label: 'Enable tracking', + id: 'woocommerce_checkout_order_received_endpoint', + label: 'Order received', description: - 'Allow usage of WooCommerce to be tracked', + 'Endpoint for the "Checkout → Order received" page.', + type: 'text', + default: 'order-received', + tip: 'Endpoint for the "Checkout → Order received" page.', + value: 'order-received', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_add_payment_method_endpoint', + label: 'Add payment method', + description: + 'Endpoint for the "Checkout → Add payment method" page.', + type: 'text', + default: 'add-payment-method', + tip: 'Endpoint for the "Checkout → Add payment method" page.', + value: 'add-payment-method', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_delete_payment_method_endpoint', + label: 'Delete payment method', + description: + 'Endpoint for the delete payment method page.', + type: 'text', + default: 'delete-payment-method', + tip: 'Endpoint for the delete payment method page.', + value: 'delete-payment-method', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_orders_endpoint', + label: 'Orders', + description: + 'Endpoint for the "My account → Orders" page.', + type: 'text', + default: 'orders', + tip: 'Endpoint for the "My account → Orders" page.', + value: 'orders', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_view_order_endpoint', + label: 'View order', + description: + 'Endpoint for the "My account → View order" page.', + type: 'text', + default: 'view-order', + tip: 'Endpoint for the "My account → View order" page.', + value: 'view-order', + } ), + ] ) + ); + + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_downloads_endpoint', + label: 'Downloads', + description: + 'Endpoint for the "My account → Downloads" page.', + type: 'text', + default: 'downloads', + tip: 'Endpoint for the "My account → Downloads" page.', + value: 'downloads', + } ), + ] ) + ); + + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_edit_account_endpoint', + label: 'Edit account', + description: + 'Endpoint for the "My account → Edit account" page.', + type: 'text', + default: 'edit-account', + tip: 'Endpoint for the "My account → Edit account" page.', + value: 'edit-account', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_edit_address_endpoint', + label: 'Addresses', + description: + 'Endpoint for the "My account → Addresses" page.', + type: 'text', + default: 'edit-address', + tip: 'Endpoint for the "My account → Addresses" page.', + value: 'edit-address', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_payment_methods_endpoint', + label: 'Payment methods', + description: + 'Endpoint for the "My account → Payment methods" page.', + type: 'text', + default: 'payment-methods', + tip: 'Endpoint for the "My account → Payment methods" page.', + value: 'payment-methods', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_myaccount_lost_password_endpoint', + label: 'Lost password', + description: + 'Endpoint for the "My account → Lost password" page.', + type: 'text', + default: 'lost-password', + tip: 'Endpoint for the "My account → Lost password" page.', + value: 'lost-password', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_logout_endpoint', + label: 'Logout', + description: + 'Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true', + type: 'text', + default: 'customer-logout', + tip: 'Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true', + value: 'customer-logout', + } ), + ] ) + ); + // eslint-disable-next-line playwright/no-conditional-in-test + if ( ! shouldSkip ) { + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_allow_tracking', + label: 'Enable tracking', + description: + 'Allow usage of WooCommerce to be tracked', + type: 'checkbox', + default: 'no', + tip: 'To opt out, leave this box unticked. Your store remains untracked, and no data will be collected. Read about what usage data is tracked at:
WooCommerce.com Usage Tracking Documentation.', + value: 'no', + } ), + ] ) + ); + } else { + // Test is failing on external hosts + } + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_show_marketplace_suggestions', + label: 'Show Suggestions', + description: + 'Display suggestions within WooCommerce', type: 'checkbox', - default: 'no', - tip: 'To opt out, leave this box unticked. Your store remains untracked, and no data will be collected. Read about what usage data is tracked at: WooCommerce.com Usage Tracking Documentation.', - value: 'no', + default: 'yes', + tip: 'Leave this box unchecked if you do not want to pull suggested extensions from WooCommerce.com. You will see a static list of extensions instead.', + value: 'yes', } ), ] ) ); - } else { - // Test is failing on external hosts - } - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_show_marketplace_suggestions', - label: 'Show Suggestions', - description: 'Display suggestions within WooCommerce', - type: 'checkbox', - default: 'yes', - tip: 'Leave this box unchecked if you do not want to pull suggested extensions from WooCommerce.com. You will see a static list of extensions instead.', - value: 'yes', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'woocommerce_analytics_enabled', - label: 'Analytics', - description: 'Enable WooCommerce Analytics', - type: 'checkbox', - default: 'yes', - value: 'yes', - } ), - ] ) - ); - } ); - } ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'woocommerce_analytics_enabled', + label: 'Analytics', + description: 'Enable WooCommerce Analytics', + type: 'checkbox', + default: 'yes', + value: 'yes', + } ), + ] ) + ); + } ); + } + ); test.describe( 'List all Email New Order settings', () => { test( 'can retrieve all email new order settings', async ( { @@ -1930,7 +1945,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => { tip: expect.stringContaining( 'Enter recipients (comma separated) for this email. Defaults to' ), - value: '', + value: expect.any( String ), } ), ] ) ); @@ -1998,107 +2013,110 @@ test.describe.serial( 'Settings API tests: CRUD', () => { } ); test.describe( 'List all Email Failed Order settings', () => { - test( 'can retrieve all email failed order settings', async ( { - request, - } ) => { - // call API to retrieve all settings options - const response = await request.get( - '/wp-json/wc/v3/settings/email_failed_order' - ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); - expect( Array.isArray( responseJSON ) ).toBe( true ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'enabled', - label: 'Enable/Disable', - description: '', - type: 'checkbox', - default: 'yes', - value: 'yes', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'recipient', - label: 'Recipient(s)', - description: expect.stringContaining( - 'Enter recipients (comma separated) for this email. Defaults to' - ), - type: 'text', - default: '', - tip: expect.stringContaining( - 'Enter recipients (comma separated) for this email. Defaults to' - ), - value: '', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'subject', - label: 'Subject', - description: - 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', - type: 'text', - default: '', - tip: 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', - value: '', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'heading', - label: 'Email heading', - description: - 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', - type: 'text', - default: '', - tip: 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', - value: '', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'additional_content', - label: 'Additional content', - description: - 'Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', - type: 'textarea', - default: - 'Hopefully they’ll be back. Read more about troubleshooting failed payments.', - tip: 'Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', - value: 'Hopefully they’ll be back. Read more about troubleshooting failed payments.', - } ), - ] ) - ); - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - id: 'email_type', - label: 'Email type', - description: 'Choose which format of email to send.', - type: 'select', - default: 'html', - options: { - plain: 'Plain text', - html: 'HTML', - multipart: 'Multipart', - }, - tip: 'Choose which format of email to send.', - value: 'html', - } ), - ] ) - ); - } ); + test( + 'can retrieve all email failed order settings', + { tag: '@skip-on-default-pressable' }, + async ( { request } ) => { + // call API to retrieve all settings options + const response = await request.get( + '/wp-json/wc/v3/settings/email_failed_order' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + expect( Array.isArray( responseJSON ) ).toBe( true ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'enabled', + label: 'Enable/Disable', + description: '', + type: 'checkbox', + default: 'yes', + value: 'yes', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'recipient', + label: 'Recipient(s)', + description: expect.stringContaining( + 'Enter recipients (comma separated) for this email. Defaults to' + ), + type: 'text', + default: '', + tip: expect.stringContaining( + 'Enter recipients (comma separated) for this email. Defaults to' + ), + value: '', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'subject', + label: 'Subject', + description: + 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', + type: 'text', + default: '', + tip: 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', + value: '', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'heading', + label: 'Email heading', + description: + 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', + type: 'text', + default: '', + tip: 'Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', + value: '', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'additional_content', + label: 'Additional content', + description: + 'Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', + type: 'textarea', + default: + 'Hopefully they’ll be back. Read more about troubleshooting failed payments.', + tip: 'Text to appear below the main email content. Available placeholders: {site_title}</code>, <code>{site_address}</code>, <code>{site_url}</code>, <code>{order_date}</code>, <code>{order_number}', + value: 'Hopefully they’ll be back. Read more about troubleshooting failed payments.', + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + id: 'email_type', + label: 'Email type', + description: + 'Choose which format of email to send.', + type: 'select', + default: 'html', + options: { + plain: 'Plain text', + html: 'HTML', + multipart: 'Multipart', + }, + tip: 'Choose which format of email to send.', + value: 'html', + } ), + ] ) + ); + } + ); } ); test.describe( 'List all Email Customer On Hold Order settings', () => { diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/system-status/system-status-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/system-status/system-status-crud.test.js index 65d56a07196..005c48a99cd 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/system-status/system-status-crud.test.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/system-status/system-status-crud.test.js @@ -3,582 +3,590 @@ const { BASE_URL } = process.env; const shouldSkip = BASE_URL !== undefined && ! BASE_URL.includes( 'localhost' ); test.describe( 'System Status API tests', () => { - test( 'can view all system status items', async ( { request } ) => { - // call API to view all system status items - const response = await request.get( '/wp-json/wc/v3/system_status' ); - const responseJSON = await response.json(); - expect( response.status() ).toEqual( 200 ); + test( + 'can view all system status items', + { tag: '@skip-on-default-wpcom' }, + async ( { request } ) => { + // call API to view all system status items + const response = await request.get( + '/wp-json/wc/v3/system_status' + ); + const responseJSON = await response.json(); + expect( response.status() ).toEqual( 200 ); + + // local environment differs from external hosts. Local listed first. + // eslint-disable-next-line playwright/no-conditional-in-test + if ( ! shouldSkip ) { + expect( responseJSON ).toEqual( + expect.objectContaining( { + environment: expect.objectContaining( { + home_url: expect.any( String ), + site_url: expect.any( String ), + version: expect.any( String ), + log_directory: expect.any( String ), + log_directory_writable: expect.any( Boolean ), + wp_version: expect.any( String ), + wp_multisite: expect.any( Boolean ), + wp_memory_limit: expect.any( Number ), + wp_debug_mode: expect.any( Boolean ), + wp_cron: expect.any( Boolean ), + language: expect.any( String ), + external_object_cache: null, + server_info: expect.any( String ), + php_version: expect.any( String ), + php_post_max_size: expect.any( Number ), + php_max_execution_time: expect.any( Number ), + php_max_input_vars: expect.any( Number ), + curl_version: expect.any( String ), + suhosin_installed: expect.any( Boolean ), + max_upload_size: expect.any( Number ), + mysql_version: expect.any( String ), + mysql_version_string: expect.any( String ), + default_timezone: expect.any( String ), + fsockopen_or_curl_enabled: expect.any( Boolean ), + soapclient_enabled: expect.any( Boolean ), + domdocument_enabled: expect.any( Boolean ), + gzip_enabled: expect.any( Boolean ), + mbstring_enabled: expect.any( Boolean ), + remote_post_successful: expect.any( Boolean ), + remote_post_response: expect.any( String ), + remote_get_successful: expect.any( Boolean ), + remote_get_response: expect.any( String ), + } ), + } ) + ); + } else { + expect( responseJSON ).toEqual( + expect.objectContaining( { + environment: expect.objectContaining( { + home_url: expect.any( String ), + site_url: expect.any( String ), + version: expect.any( String ), + log_directory: expect.any( String ), + log_directory_writable: expect.any( Boolean ), + wp_version: expect.any( String ), + wp_multisite: expect.any( Boolean ), + wp_memory_limit: expect.any( Number ), + wp_debug_mode: expect.any( Boolean ), + wp_cron: expect.any( Boolean ), + language: expect.any( String ), + external_object_cache: expect.any( Boolean ), + server_info: expect.any( String ), + php_version: expect.any( String ), + php_post_max_size: expect.any( Number ), + php_max_execution_time: expect.any( Number ), + php_max_input_vars: expect.any( Number ), + curl_version: expect.any( String ), + suhosin_installed: expect.any( Boolean ), + max_upload_size: expect.any( Number ), + mysql_version: expect.any( String ), + mysql_version_string: expect.any( String ), + default_timezone: expect.any( String ), + fsockopen_or_curl_enabled: expect.any( Boolean ), + soapclient_enabled: expect.any( Boolean ), + domdocument_enabled: expect.any( Boolean ), + gzip_enabled: expect.any( Boolean ), + mbstring_enabled: expect.any( Boolean ), + remote_post_successful: expect.any( Boolean ), + remote_post_response: expect.any( Number ), + remote_get_successful: expect.any( Boolean ), + remote_get_response: expect.any( Number ), + } ), + } ) + ); + } - // local environment differs from external hosts. Local listed first. - // eslint-disable-next-line playwright/no-conditional-in-test - if ( ! shouldSkip ) { expect( responseJSON ).toEqual( expect.objectContaining( { - environment: expect.objectContaining( { - home_url: expect.any( String ), - site_url: expect.any( String ), - version: expect.any( String ), - log_directory: expect.any( String ), - log_directory_writable: expect.any( Boolean ), - wp_version: expect.any( String ), - wp_multisite: expect.any( Boolean ), - wp_memory_limit: expect.any( Number ), - wp_debug_mode: expect.any( Boolean ), - wp_cron: expect.any( Boolean ), - language: expect.any( String ), - external_object_cache: null, - server_info: expect.any( String ), - php_version: expect.any( String ), - php_post_max_size: expect.any( Number ), - php_max_execution_time: expect.any( Number ), - php_max_input_vars: expect.any( Number ), - curl_version: expect.any( String ), - suhosin_installed: expect.any( Boolean ), - max_upload_size: expect.any( Number ), - mysql_version: expect.any( String ), - mysql_version_string: expect.any( String ), - default_timezone: expect.any( String ), - fsockopen_or_curl_enabled: expect.any( Boolean ), - soapclient_enabled: expect.any( Boolean ), - domdocument_enabled: expect.any( Boolean ), - gzip_enabled: expect.any( Boolean ), - mbstring_enabled: expect.any( Boolean ), - remote_post_successful: expect.any( Boolean ), - remote_post_response: expect.any( String ), - remote_get_successful: expect.any( Boolean ), - remote_get_response: expect.any( String ), - } ), - } ) - ); - } else { - expect( responseJSON ).toEqual( - expect.objectContaining( { - environment: expect.objectContaining( { - home_url: expect.any( String ), - site_url: expect.any( String ), - version: expect.any( String ), - log_directory: expect.any( String ), - log_directory_writable: expect.any( Boolean ), - wp_version: expect.any( String ), - wp_multisite: expect.any( Boolean ), - wp_memory_limit: expect.any( Number ), - wp_debug_mode: expect.any( Boolean ), - wp_cron: expect.any( Boolean ), - language: expect.any( String ), - external_object_cache: expect.any( Boolean ), - server_info: expect.any( String ), - php_version: expect.any( String ), - php_post_max_size: expect.any( Number ), - php_max_execution_time: expect.any( Number ), - php_max_input_vars: expect.any( Number ), - curl_version: expect.any( String ), - suhosin_installed: expect.any( Boolean ), - max_upload_size: expect.any( Number ), - mysql_version: expect.any( String ), - mysql_version_string: expect.any( String ), - default_timezone: expect.any( String ), - fsockopen_or_curl_enabled: expect.any( Boolean ), - soapclient_enabled: expect.any( Boolean ), - domdocument_enabled: expect.any( Boolean ), - gzip_enabled: expect.any( Boolean ), - mbstring_enabled: expect.any( Boolean ), - remote_post_successful: expect.any( Boolean ), - remote_post_response: expect.any( Number ), - remote_get_successful: expect.any( Boolean ), - remote_get_response: expect.any( Number ), - } ), - } ) - ); - } - - expect( responseJSON ).toEqual( - expect.objectContaining( { - database: expect.objectContaining( { - wc_database_version: expect.any( String ), - database_prefix: expect.any( String ), - maxmind_geoip_database: expect.any( String ), - database_tables: expect.objectContaining( { - woocommerce: expect.objectContaining( { - wp_woocommerce_sessions: expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_api_keys: expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_attribute_taxonomies: - expect.objectContaining( { + database: expect.objectContaining( { + wc_database_version: expect.any( String ), + database_prefix: expect.any( String ), + maxmind_geoip_database: expect.any( String ), + database_tables: expect.objectContaining( { + woocommerce: expect.objectContaining( { + wp_woocommerce_sessions: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_api_keys: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_attribute_taxonomies: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_downloadable_product_permissions: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_order_items: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_order_itemmeta: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_tax_rates: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_tax_rate_locations: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_shipping_zones: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_shipping_zone_locations: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_shipping_zone_methods: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_payment_tokens: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_payment_tokenmeta: + expect.objectContaining( { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + } ), + wp_woocommerce_log: expect.objectContaining( { data: expect.any( String ), index: expect.any( String ), engine: expect.any( String ), } ), - wp_woocommerce_downloadable_product_permissions: - expect.objectContaining( { + } ), + other: expect.objectContaining( { + wp_actionscheduler_actions: { data: expect.any( String ), index: expect.any( String ), engine: expect.any( String ), - } ), - wp_woocommerce_order_items: expect.objectContaining( + }, + wp_actionscheduler_claims: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_actionscheduler_groups: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_actionscheduler_logs: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_commentmeta: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_comments: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_links: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_options: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_postmeta: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_posts: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_termmeta: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_terms: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_term_relationships: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_term_taxonomy: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_usermeta: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_users: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_admin_notes: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_admin_note_actions: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_category_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_customer_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_download_log: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_order_coupon_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_order_product_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_order_stats: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_order_tax_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_product_attributes_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_product_download_directories: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_product_meta_lookup: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_rate_limits: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_reserved_stock: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_tax_rate_classes: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + wp_wc_webhooks: { + data: expect.any( String ), + index: expect.any( String ), + engine: expect.any( String ), + }, + } ), + } ), + database_size: { + data: expect.any( Number ), + index: expect.any( Number ), + }, + } ), + } ) + ); + + expect( responseJSON ).toEqual( + expect.objectContaining( { + active_plugins: expect.arrayContaining( [ + { + plugin: expect.any( String ), + name: expect.any( String ), + version: expect.any( String ), + version_latest: expect.any( String ), + url: expect.any( String ), + author_name: expect.any( String ), + author_url: expect.any( String ), + network_activated: expect.any( Boolean ), + }, + { + plugin: expect.any( String ), + name: expect.any( String ), + version: expect.any( String ), + version_latest: expect.any( String ), + url: expect.any( String ), + author_name: expect.any( String ), + author_url: expect.any( String ), + network_activated: expect.any( Boolean ), + }, + { + plugin: expect.any( String ), + name: expect.any( String ), + version: expect.any( String ), + version_latest: expect.any( String ), + url: expect.any( String ), + author_name: expect.any( String ), + author_url: expect.any( String ), + network_activated: expect.any( Boolean ), + }, + { + plugin: expect.any( String ), + name: expect.any( String ), + version: expect.any( String ), + version_latest: expect.any( String ), + url: expect.any( String ), + author_name: expect.any( String ), + author_url: expect.any( String ), + network_activated: expect.any( Boolean ), + }, + ] ), + } ) + ); + + // local environment differs from external hosts. Local listed first. + // eslint-disable-next-line playwright/no-conditional-in-test + if ( ! shouldSkip ) { + expect( responseJSON ).toEqual( + expect.objectContaining( { + dropins_mu_plugins: expect.objectContaining( { + dropins: expect.arrayContaining( [] ), + mu_plugins: expect.arrayContaining( [] ), + } ), + } ) + ); + } else { + expect( responseJSON ).toEqual( + expect.objectContaining( { + dropins_mu_plugins: expect.objectContaining( { + dropins: expect.arrayContaining( [ { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } - ), - wp_woocommerce_order_itemmeta: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_tax_rates: expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_tax_rate_locations: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_shipping_zones: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_shipping_zone_locations: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_shipping_zone_methods: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_payment_tokens: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_payment_tokenmeta: - expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), - wp_woocommerce_log: expect.objectContaining( { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - } ), + name: expect.any( String ), + plugin: expect.any( String ), + }, + { + name: expect.any( String ), + plugin: expect.any( String ), + }, + ] ), + mu_plugins: [], } ), - other: expect.objectContaining( { - wp_actionscheduler_actions: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_actionscheduler_claims: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_actionscheduler_groups: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_actionscheduler_logs: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_commentmeta: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_comments: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_links: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_options: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_postmeta: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_posts: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_termmeta: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_terms: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_term_relationships: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_term_taxonomy: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_usermeta: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_users: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_admin_notes: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_admin_note_actions: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_category_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_customer_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_download_log: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_order_coupon_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_order_product_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_order_stats: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_order_tax_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_product_attributes_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_product_download_directories: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_product_meta_lookup: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_rate_limits: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_reserved_stock: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_tax_rate_classes: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - wp_wc_webhooks: { - data: expect.any( String ), - index: expect.any( String ), - engine: expect.any( String ), - }, - } ), - } ), - database_size: { - data: expect.any( Number ), - index: expect.any( Number ), - }, - } ), - } ) - ); - - expect( responseJSON ).toEqual( - expect.objectContaining( { - active_plugins: expect.arrayContaining( [ - { - plugin: expect.any( String ), - name: expect.any( String ), - version: expect.any( String ), - version_latest: expect.any( String ), - url: expect.any( String ), - author_name: expect.any( String ), - author_url: expect.any( String ), - network_activated: expect.any( Boolean ), - }, - { - plugin: expect.any( String ), - name: expect.any( String ), - version: expect.any( String ), - version_latest: expect.any( String ), - url: expect.any( String ), - author_name: expect.any( String ), - author_url: expect.any( String ), - network_activated: expect.any( Boolean ), - }, - { - plugin: expect.any( String ), - name: expect.any( String ), - version: expect.any( String ), - version_latest: expect.any( String ), - url: expect.any( String ), - author_name: expect.any( String ), - author_url: expect.any( String ), - network_activated: expect.any( Boolean ), - }, - { - plugin: expect.any( String ), - name: expect.any( String ), - version: expect.any( String ), - version_latest: expect.any( String ), - url: expect.any( String ), - author_name: expect.any( String ), - author_url: expect.any( String ), - network_activated: expect.any( Boolean ), - }, - ] ), - } ) - ); - - // local environment differs from external hosts. Local listed first. - // eslint-disable-next-line playwright/no-conditional-in-test - if ( ! shouldSkip ) { + } ) + ); + } expect( responseJSON ).toEqual( expect.objectContaining( { - dropins_mu_plugins: expect.objectContaining( { - dropins: expect.arrayContaining( [] ), - mu_plugins: expect.arrayContaining( [] ), + theme: expect.objectContaining( { + name: expect.any( String ), + version: expect.any( String ), + version_latest: expect.any( String ), + author_url: expect.any( String ), + is_child_theme: expect.any( Boolean ), + has_woocommerce_support: expect.any( Boolean ), + has_woocommerce_file: expect.any( Boolean ), + has_outdated_templates: expect.any( Boolean ), + overrides: expect.any( Array ), + parent_name: expect.any( String ), + parent_version: expect.any( String ), + parent_version_latest: expect.any( String ), + parent_author_url: expect.any( String ), } ), } ) ); - } else { expect( responseJSON ).toEqual( expect.objectContaining( { - dropins_mu_plugins: expect.objectContaining( { - dropins: expect.arrayContaining( [ - { - name: expect.any( String ), - plugin: expect.any( String ), - }, - { - name: expect.any( String ), - plugin: expect.any( String ), - }, - ] ), - mu_plugins: [], + settings: expect.objectContaining( { + api_enabled: expect.any( Boolean ), + force_ssl: expect.any( Boolean ), + currency: expect.any( String ), + currency_symbol: expect.any( String ), + currency_position: expect.any( String ), + thousand_separator: expect.any( String ), + decimal_separator: expect.any( String ), + number_of_decimals: expect.any( Number ), + geolocation_enabled: expect.any( Boolean ), + taxonomies: { + external: expect.any( String ), + grouped: expect.any( String ), + simple: expect.any( String ), + variable: expect.any( String ), + }, + product_visibility_terms: { + 'exclude-from-catalog': expect.any( String ), + 'exclude-from-search': expect.any( String ), + featured: expect.any( String ), + outofstock: expect.any( String ), + 'rated-1': expect.any( String ), + 'rated-2': expect.any( String ), + 'rated-3': expect.any( String ), + 'rated-4': expect.any( String ), + 'rated-5': expect.any( String ), + }, + woocommerce_com_connected: expect.any( String ), } ), } ) ); + expect( responseJSON ).toEqual( + expect.objectContaining( { + security: expect.objectContaining( { + secure_connection: expect.any( Boolean ), + hide_errors: expect.any( Boolean ), + } ), + } ) + ); + expect( responseJSON ).toEqual( + expect.objectContaining( { + pages: expect.arrayContaining( [ + { + page_name: expect.any( String ), + page_id: expect.any( String ), + page_set: expect.any( Boolean ), + page_exists: expect.any( Boolean ), + page_visible: expect.any( Boolean ), + shortcode: expect.any( String ), + block: expect.any( String ), + shortcode_required: expect.any( Boolean ), + shortcode_present: expect.any( Boolean ), + block_present: expect.any( Boolean ), + block_required: expect.any( Boolean ), + }, + { + page_name: expect.any( String ), + page_id: expect.any( String ), + page_set: expect.any( Boolean ), + page_exists: expect.any( Boolean ), + page_visible: expect.any( Boolean ), + shortcode: expect.any( String ), + block: expect.any( String ), + shortcode_required: expect.any( Boolean ), + shortcode_present: expect.any( Boolean ), + block_present: expect.any( Boolean ), + block_required: expect.any( Boolean ), + }, + { + page_name: expect.any( String ), + page_id: expect.any( String ), + page_set: expect.any( Boolean ), + page_exists: expect.any( Boolean ), + page_visible: expect.any( Boolean ), + shortcode: expect.any( String ), + block: expect.any( String ), + shortcode_required: expect.any( Boolean ), + shortcode_present: expect.any( Boolean ), + block_present: expect.any( Boolean ), + block_required: expect.any( Boolean ), + }, + { + page_name: expect.any( String ), + page_id: expect.any( String ), + page_set: expect.any( Boolean ), + page_exists: expect.any( Boolean ), + page_visible: expect.any( Boolean ), + shortcode: expect.any( String ), + block: expect.any( String ), + shortcode_required: expect.any( Boolean ), + shortcode_present: expect.any( Boolean ), + block_present: expect.any( Boolean ), + block_required: expect.any( Boolean ), + }, + { + page_name: expect.any( String ), + page_id: expect.any( String ), + page_set: expect.any( Boolean ), + page_exists: expect.any( Boolean ), + page_visible: expect.any( Boolean ), + shortcode: expect.any( String ), + block: expect.any( String ), + shortcode_required: expect.any( Boolean ), + shortcode_present: expect.any( Boolean ), + block_present: expect.any( Boolean ), + block_required: expect.any( Boolean ), + }, + ] ), + } ) + ); + expect( responseJSON ).toEqual( + expect.objectContaining( { + post_type_counts: expect.arrayContaining( [ + { + type: expect.any( String ), + count: expect.any( String ), + }, + { + type: expect.any( String ), + count: expect.any( String ), + }, + { + type: expect.any( String ), + count: expect.any( String ), + }, + ] ), + } ) + ); } - expect( responseJSON ).toEqual( - expect.objectContaining( { - theme: expect.objectContaining( { - name: expect.any( String ), - version: expect.any( String ), - version_latest: expect.any( String ), - author_url: expect.any( String ), - is_child_theme: expect.any( Boolean ), - has_woocommerce_support: expect.any( Boolean ), - has_woocommerce_file: expect.any( Boolean ), - has_outdated_templates: expect.any( Boolean ), - overrides: expect.any( Array ), - parent_name: expect.any( String ), - parent_version: expect.any( String ), - parent_version_latest: expect.any( String ), - parent_author_url: expect.any( String ), - } ), - } ) - ); - expect( responseJSON ).toEqual( - expect.objectContaining( { - settings: expect.objectContaining( { - api_enabled: expect.any( Boolean ), - force_ssl: expect.any( Boolean ), - currency: expect.any( String ), - currency_symbol: expect.any( String ), - currency_position: expect.any( String ), - thousand_separator: expect.any( String ), - decimal_separator: expect.any( String ), - number_of_decimals: expect.any( Number ), - geolocation_enabled: expect.any( Boolean ), - taxonomies: { - external: expect.any( String ), - grouped: expect.any( String ), - simple: expect.any( String ), - variable: expect.any( String ), - }, - product_visibility_terms: { - 'exclude-from-catalog': expect.any( String ), - 'exclude-from-search': expect.any( String ), - featured: expect.any( String ), - outofstock: expect.any( String ), - 'rated-1': expect.any( String ), - 'rated-2': expect.any( String ), - 'rated-3': expect.any( String ), - 'rated-4': expect.any( String ), - 'rated-5': expect.any( String ), - }, - woocommerce_com_connected: expect.any( String ), - } ), - } ) - ); - expect( responseJSON ).toEqual( - expect.objectContaining( { - security: expect.objectContaining( { - secure_connection: expect.any( Boolean ), - hide_errors: expect.any( Boolean ), - } ), - } ) - ); - expect( responseJSON ).toEqual( - expect.objectContaining( { - pages: expect.arrayContaining( [ - { - page_name: expect.any( String ), - page_id: expect.any( String ), - page_set: expect.any( Boolean ), - page_exists: expect.any( Boolean ), - page_visible: expect.any( Boolean ), - shortcode: expect.any( String ), - block: expect.any( String ), - shortcode_required: expect.any( Boolean ), - shortcode_present: expect.any( Boolean ), - block_present: expect.any( Boolean ), - block_required: expect.any( Boolean ), - }, - { - page_name: expect.any( String ), - page_id: expect.any( String ), - page_set: expect.any( Boolean ), - page_exists: expect.any( Boolean ), - page_visible: expect.any( Boolean ), - shortcode: expect.any( String ), - block: expect.any( String ), - shortcode_required: expect.any( Boolean ), - shortcode_present: expect.any( Boolean ), - block_present: expect.any( Boolean ), - block_required: expect.any( Boolean ), - }, - { - page_name: expect.any( String ), - page_id: expect.any( String ), - page_set: expect.any( Boolean ), - page_exists: expect.any( Boolean ), - page_visible: expect.any( Boolean ), - shortcode: expect.any( String ), - block: expect.any( String ), - shortcode_required: expect.any( Boolean ), - shortcode_present: expect.any( Boolean ), - block_present: expect.any( Boolean ), - block_required: expect.any( Boolean ), - }, - { - page_name: expect.any( String ), - page_id: expect.any( String ), - page_set: expect.any( Boolean ), - page_exists: expect.any( Boolean ), - page_visible: expect.any( Boolean ), - shortcode: expect.any( String ), - block: expect.any( String ), - shortcode_required: expect.any( Boolean ), - shortcode_present: expect.any( Boolean ), - block_present: expect.any( Boolean ), - block_required: expect.any( Boolean ), - }, - { - page_name: expect.any( String ), - page_id: expect.any( String ), - page_set: expect.any( Boolean ), - page_exists: expect.any( Boolean ), - page_visible: expect.any( Boolean ), - shortcode: expect.any( String ), - block: expect.any( String ), - shortcode_required: expect.any( Boolean ), - shortcode_present: expect.any( Boolean ), - block_present: expect.any( Boolean ), - block_required: expect.any( Boolean ), - }, - ] ), - } ) - ); - expect( responseJSON ).toEqual( - expect.objectContaining( { - post_type_counts: expect.arrayContaining( [ - { - type: expect.any( String ), - count: expect.any( String ), - }, - { - type: expect.any( String ), - count: expect.any( String ), - }, - { - type: expect.any( String ), - count: expect.any( String ), - }, - ] ), - } ) - ); - } ); + ); test( 'can view all system status tools', async ( { request } ) => { // call API to view system status tools From f39e1d72c01ed158ffda8d7bc605420b09f4b2dc Mon Sep 17 00:00:00 2001 From: Maikel Perez Date: Thu, 12 Sep 2024 12:47:23 -0300 Subject: [PATCH 24/59] CYS: Fix Looker dashboard data (#51206) * Track data for font and color pairings including 'Create your own' option in CYS * Add changelog file * Track data for logo flow * Send the open param as part of the tracking event customize_your_store_assembler_hub_color_palette_create_toggle --- .../global-styles/variation-container.jsx | 23 +++++++++++++ ...idebar-navigation-screen-color-palette.tsx | 10 ++++++ .../sidebar-navigation-screen-logo.tsx | 34 ++++++++++++++++--- plugins/woocommerce/changelog/add-50832 | 4 +++ 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-50832 diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx index ab05865014a..60313651ee7 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx @@ -14,6 +14,11 @@ import { mergeBaseAndUserConfigs } from '@wordpress/edit-site/build-module/compo import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; import { isEqual, noop } from 'lodash'; +/** + * Internal dependencies + */ +import { trackEvent } from '~/customize-store/tracking'; + const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); // Removes the typography settings from the styles when the user is changing @@ -100,6 +105,24 @@ export const VariationContainer = ( { variation, children } ) => { ), }; } ); + + if ( variation.settings.color?.palette ) { + trackEvent( + 'customize_your_store_assembler_hub_color_palette_item_click', + { + item: variation.title, + } + ); + } + + if ( variation.settings.typography ) { + trackEvent( + 'customize_your_store_assembler_hub_typography_item_click', + { + item: variation.title, + } + ); + } }; const selectOnEnter = ( event ) => { diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx index b1b10bab2e6..c851175894a 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx @@ -17,6 +17,7 @@ import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; import { CustomizeStoreContext } from '../'; import { SidebarNavigationScreen } from './sidebar-navigation-screen'; import { ColorPalette, ColorPanel } from './global-styles'; +import { trackEvent } from '~/customize-store/tracking'; import { FlowType } from '~/customize-store/types'; const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); @@ -27,6 +28,14 @@ const SidebarNavigationScreenColorPaletteContent = () => { const hasCreatedOwnColors = !! ( user.settings.color && user.settings.color.palette.hasCreatedOwnColors ); + + function handlePanelBodyToggle( open?: boolean ) { + trackEvent( + 'customize_your_store_assembler_hub_color_palette_create_toggle', + { open } + ); + } + // Wrap in a BlockEditorProvider to ensure that the Iframe's dependencies are // loaded. This is necessary because the Iframe component waits until // the block editor store's `__internalIsInitialized` is true before @@ -45,6 +54,7 @@ const SidebarNavigationScreenColorPaletteContent = () => { className="woocommerce-customize-store__color-panel-container" title={ __( 'or create your own', 'woocommerce' ) } initialOpen={ hasCreatedOwnColors } + onToggle={ handlePanelBodyToggle } > diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-logo.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-logo.tsx index 0a55417170d..10f4c6a5f02 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-logo.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-logo.tsx @@ -297,16 +297,26 @@ const LogoEdit = ( { ); } + function handleMediaUploadSelect( media: { id: string; url: string } ) { + onInitialSelectLogo( media ); + trackEvent( 'customize_your_store_assembler_hub_logo_select' ); + } + if ( ! logoUrl ) { return ( void } ) => ( - - -); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/constants.ts index 8c47d8c30e4..e535dfebc16 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/constants.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/constants.ts @@ -5,49 +5,71 @@ import { __ } from '@wordpress/i18n'; export const attributeOptionsPreview = [ { - id: 23, - name: __( 'Blue', 'woocommerce' ), - slug: 'blue', - attr_slug: 'blue', - description: '', - parent: 0, - count: 4, + label: __( 'Blue', 'woocommerce' ), + value: 'blue', + rawData: { + id: 23, + name: __( 'Blue', 'woocommerce' ), + slug: 'blue', + attr_slug: 'blue', + description: '', + parent: 0, + count: 4, + }, }, { - id: 29, - name: __( 'Gray', 'woocommerce' ), - slug: 'gray', - attr_slug: 'gray', - description: '', - parent: 0, - count: 3, + label: __( 'Gray', 'woocommerce' ), + value: 'gray', + selected: true, + rawData: { + id: 29, + name: __( 'Gray', 'woocommerce' ), + slug: 'gray', + attr_slug: 'gray', + description: '', + parent: 0, + count: 3, + }, }, { - id: 24, - name: __( 'Green', 'woocommerce' ), - slug: 'green', - attr_slug: 'green', - description: '', - parent: 0, - count: 3, + label: __( 'Green', 'woocommerce' ), + value: 'green', + rawData: { + id: 24, + name: __( 'Green', 'woocommerce' ), + slug: 'green', + attr_slug: 'green', + description: '', + parent: 0, + count: 3, + }, }, { - id: 25, - name: __( 'Red', 'woocommerce' ), - slug: 'red', - attr_slug: 'red', - description: '', - parent: 0, - count: 4, + label: __( 'Red', 'woocommerce' ), + value: 'red', + selected: true, + rawData: { + id: 25, + name: __( 'Red', 'woocommerce' ), + slug: 'red', + attr_slug: 'red', + description: '', + parent: 0, + count: 4, + }, }, { - id: 30, - name: __( 'Yellow', 'woocommerce' ), - slug: 'yellow', - attr_slug: 'yellow', - description: '', - parent: 0, - count: 1, + label: __( 'Yellow', 'woocommerce' ), + value: 'yellow', + rawData: { + id: 30, + name: __( 'Yellow', 'woocommerce' ), + slug: 'yellow', + attr_slug: 'yellow', + description: '', + parent: 0, + count: 1, + }, }, ]; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx index 9fb37b004b1..5f709d0089d 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx @@ -5,29 +5,34 @@ import { useCollection, useCollectionData, } from '@woocommerce/base-context/hooks'; -import { getSetting } from '@woocommerce/settings'; import { AttributeSetting, AttributeTerm, objectHasProp, } from '@woocommerce/types'; -import { useBlockProps } from '@wordpress/block-editor'; -import { Disabled, Notice, withSpokenMessages } from '@wordpress/components'; -import { useEffect, useState, useMemo } from '@wordpress/element'; +import { + useBlockProps, + useInnerBlocksProps, + BlockContextProvider, +} from '@wordpress/block-editor'; +import { withSpokenMessages } from '@wordpress/components'; +import { useEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { getSetting } from '@woocommerce/settings'; /** * Internal dependencies */ -import { AttributeDropdown } from './components/attribute-dropdown'; -import { Preview as CheckboxListPreview } from './components/checkbox-list-editor'; -import { Inspector } from './components/inspector'; -import { NoAttributesPlaceholder } from './components/placeholder'; +import { Inspector } from './inspector'; import { attributeOptionsPreview } from './constants'; import './style.scss'; import { EditProps, isAttributeCounts } from './types'; import { getAttributeFromId } from './utils'; -import './editor.scss'; +import { getAllowedBlocks } from '../../utils'; +import { EXCLUDED_BLOCKS } from '../../constants'; +import { FilterOptionItem } from '../../types'; +import { InitialDisabled } from '../../components/initial-disabled'; +import { Notice } from '../../components/notice'; const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] ); @@ -47,8 +52,10 @@ const Edit = ( props: EditProps ) => { const attributeObject = getAttributeFromId( attributeId ); const [ attributeOptions, setAttributeOptions ] = useState< - AttributeTerm[] + FilterOptionItem[] >( [] ); + const [ isOptionsLoading, setIsOptionsLoading ] = + useState< boolean >( true ); const { results: attributeTerms, isLoading: isTermsLoading } = useCollection< AttributeTerm >( { @@ -59,7 +66,7 @@ const Edit = ( props: EditProps ) => { query: { orderby: 'menu_order', hide_empty: hideEmpty }, } ); - const { results: filteredCounts, isLoading: isCountsLoading } = + const { results: filteredCounts, isLoading: isFilterCountsLoading } = useCollectionData( { queryAttribute: { taxonomy: attributeObject?.taxonomy || '', @@ -69,90 +76,137 @@ const Edit = ( props: EditProps ) => { isEditor: true, } ); - const isLoading = isTermsLoading || isCountsLoading; - useEffect( () => { + if ( isTermsLoading || isFilterCountsLoading ) return; + const termIdHasProducts = objectHasProp( filteredCounts, 'attribute_counts' ) && isAttributeCounts( filteredCounts.attribute_counts ) ? filteredCounts.attribute_counts.map( ( term ) => term.term ) : []; - if ( termIdHasProducts.length === 0 && hideEmpty ) - return setAttributeOptions( [] ); + if ( termIdHasProducts.length === 0 && hideEmpty ) { + setAttributeOptions( [] ); + } else { + setAttributeOptions( + attributeTerms + .filter( ( term ) => { + if ( hideEmpty ) + return termIdHasProducts.includes( term.id ); + return true; + } ) + .sort( ( a, b ) => { + switch ( sortOrder ) { + case 'name-asc': + return a.name > b.name ? 1 : -1; + case 'name-desc': + return a.name < b.name ? 1 : -1; + case 'count-asc': + return a.count > b.count ? 1 : -1; + case 'count-desc': + default: + return a.count < b.count ? 1 : -1; + } + } ) + .map( ( term, index ) => ( { + label: showCounts + ? `${ term.name } (${ term.count })` + : term.name, + value: term.id.toString(), + selected: index === 1, + rawData: term, + } ) ) + ); + } - setAttributeOptions( - attributeTerms - .filter( ( term ) => { - if ( hideEmpty ) - return termIdHasProducts.includes( term.id ); - return true; - } ) - .sort( ( a, b ) => { - switch ( sortOrder ) { - case 'name-asc': - return a.name > b.name ? 1 : -1; - case 'name-desc': - return a.name < b.name ? 1 : -1; - case 'count-asc': - return a.count > b.count ? 1 : -1; - case 'count-desc': - default: - return a.count < b.count ? 1 : -1; - } - } ) - ); - }, [ attributeTerms, filteredCounts, sortOrder, hideEmpty ] ); + setIsOptionsLoading( false ); + }, [ + showCounts, + attributeTerms, + filteredCounts, + sortOrder, + hideEmpty, + isTermsLoading, + isFilterCountsLoading, + ] ); - const Wrapper = ( { children }: { children: React.ReactNode } ) => ( -
- - { children } -
+ const { children, ...innerBlocksProps } = useInnerBlocksProps( + useBlockProps(), + { + allowedBlocks: getAllowedBlocks( EXCLUDED_BLOCKS ), + template: [ + [ + 'core/group', + { + layout: { + type: 'flex', + flexWrap: 'nowrap', + }, + metadata: { + name: __( 'Header', 'woocommerce' ), + }, + style: { + spacing: { + blockGap: '0', + }, + }, + }, + [ + [ + 'core/heading', + { + level: 3, + content: + attributeObject?.label || + __( 'Attribute', 'woocommerce' ), + }, + ], + [ + 'woocommerce/product-filter-clear-button', + { + lock: { + remove: true, + move: false, + }, + }, + ], + ], + ], + [ + displayStyle, + { + lock: { + remove: true, + }, + }, + ], + ], + } ); - const loadingState = useMemo( () => { - return [ ...Array( 5 ) ].map( ( x, i ) => ( -
  • -   -
  • - ) ); - }, [] ); + const isLoading = + isTermsLoading || isFilterCountsLoading || isOptionsLoading; - if ( isPreview ) { - return ( - - - { - if ( showCounts ) - return `${ term.name } (${ term.count })`; - return term.name; - } ) } - /> - - - ); - } - - // Block rendering starts. if ( Object.keys( ATTRIBUTES ).length === 0 ) return ( - - - +
    + + +

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

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

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

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

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

    - +
    ); return ( - - - { displayStyle === 'dropdown' ? ( - - ) : ( - { - if ( showCounts ) - return `${ term.name } (${ term.count })`; - return term.name; - } ) } - /> - ) } - - +
    + + + + { children } + + +
    ); }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/editor.scss deleted file mode 100644 index 0580af4584f..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/editor.scss +++ /dev/null @@ -1,8 +0,0 @@ -.wp-block-woocommerce-product-filter-attribute__loading { - padding: 0; - - li { - @include placeholder(); - margin: 5px 0; - } -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/frontend.ts index 0f9eb935586..6170ececffb 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/frontend.ts @@ -8,7 +8,7 @@ import { HTMLElementEvent } from '@woocommerce/types'; /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; type AttributeFilterContext = { attributeSlug: string; @@ -106,5 +106,12 @@ store( 'woocommerce/product-filter-attribute', { navigate( getUrl( selectedTerms, attributeSlug, queryType ) ); }, + + clearFilters: () => { + const { attributeSlug, queryType } = + getContext< ActiveAttributeFilterContext >(); + + navigate( getUrl( [], attributeSlug, queryType ) ); + }, }, } ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/index.tsx index 6c1c49b3fb7..66a1c806d0b 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/index.tsx @@ -2,18 +2,21 @@ * External dependencies */ import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; -import { productFilterOptions } from '@woocommerce/icons'; +import { productFilterAttribute } from '@woocommerce/icons'; import { getSetting } from '@woocommerce/settings'; -import { registerBlockType } from '@wordpress/blocks'; import { AttributeSetting } from '@woocommerce/types'; +import { registerBlockType } from '@wordpress/blocks'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ import metadata from './block.json'; import Edit from './edit'; +import Save from './save'; import './style.scss'; +const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] ); if ( isExperimentalBlocksEnabled() ) { const defaultAttribute = getSetting< AttributeSetting >( 'defaultProductFilterAttribute' @@ -21,7 +24,7 @@ if ( isExperimentalBlocksEnabled() ) { registerBlockType( metadata, { edit: Edit, - icon: productFilterOptions, + icon: productFilterAttribute, attributes: { ...metadata.attributes, attributeId: { @@ -29,5 +32,25 @@ if ( isExperimentalBlocksEnabled() ) { default: parseInt( defaultAttribute.attribute_id, 10 ), }, }, + save: Save, + variations: ATTRIBUTES.map( ( attribute, index ) => { + return { + name: `product-filter-attribute-${ attribute.attribute_name }`, + title: `${ attribute.attribute_label } (Experimental)`, + description: sprintf( + // translators: %s is the attribute label. + __( + `Enable customers to filter the product collection by selecting one or more %s attributes.`, + 'woocommerce' + ), + attribute.attribute_label + ), + attributes: { + attributeId: parseInt( attribute.attribute_id, 10 ), + }, + isActive: [ 'attributeId' ], + isDefault: index === 0, + }; + } ), } ); } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/inspector.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/inspector.tsx similarity index 68% rename from plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/inspector.tsx rename to plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/inspector.tsx index 21b62ac0857..d2efdccbc07 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/inspector.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/inspector.tsx @@ -5,8 +5,9 @@ import { getSetting } from '@woocommerce/settings'; import { AttributeSetting } from '@woocommerce/types'; import { InspectorControls } from '@wordpress/block-editor'; import { dispatch, useSelect } from '@wordpress/data'; -import { createInterpolateElement } from '@wordpress/element'; +import { createInterpolateElement, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { Block, getBlockTypes, createBlock } from '@wordpress/blocks'; import { ComboboxControl, PanelBody, @@ -23,12 +24,15 @@ import { /** * Internal dependencies */ -import { sortOrderOptions } from '../constants'; -import { BlockAttributes, EditProps } from '../types'; -import { getAttributeFromId } from '../utils'; +import { sortOrderOptions } from './constants'; +import { BlockAttributes, EditProps } from './types'; +import { getAttributeFromId } from './utils'; +import { getInnerBlockByName } from '../../utils'; const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] ); +let displayStyleOptions: Block[] = []; + export const Inspector = ( { clientId, attributes, @@ -43,47 +47,29 @@ export const Inspector = ( { hideEmpty, clearButton, } = attributes; - const { updateBlockAttributes } = dispatch( 'core/block-editor' ); - const { productFilterWrapperBlockId, productFilterWrapperHeadingBlockId } = - useSelect( - ( select ) => { - if ( ! clientId ) - return { - productFilterWrapperBlockId: undefined, - productFilterWrapperHeadingBlockId: undefined, - }; + const { updateBlockAttributes, insertBlock, replaceBlock } = + dispatch( 'core/block-editor' ); + const filterBlock = useSelect( + ( select ) => { + return select( 'core/block-editor' ).getBlock( clientId ); + }, + [ clientId ] + ); + const [ displayStyleBlocksAttributes, setDisplayStyleBlocksAttributes ] = + useState< Record< string, unknown > >( {} ); - const { getBlockParentsByBlockName, getBlock } = - select( 'core/block-editor' ); + const filterHeadingBlock = getInnerBlockByName( + filterBlock, + 'core/heading' + ); - const parentBlocksByBlockName = getBlockParentsByBlockName( - clientId, - 'woocommerce/product-filter' - ); - - if ( parentBlocksByBlockName.length === 0 ) - return { - productFilterWrapperBlockId: undefined, - productFilterWrapperHeadingBlockId: undefined, - }; - - const parentBlockId = parentBlocksByBlockName[ 0 ]; - - const parentBlock = getBlock( parentBlockId ); - const headerGroupBlock = parentBlock?.innerBlocks.find( - ( block ) => block.name === 'core/group' - ); - const headingBlock = headerGroupBlock?.innerBlocks.find( - ( block ) => block.name === 'core/heading' - ); - - return { - productFilterWrapperBlockId: parentBlockId, - productFilterWrapperHeadingBlockId: headingBlock?.clientId, - }; - }, - [ clientId ] + if ( displayStyleOptions.length === 0 ) { + displayStyleOptions = getBlockTypes().filter( ( blockType ) => + blockType.ancestor?.includes( + 'woocommerce/product-filter-attribute' + ) ); + } return ( <> @@ -102,17 +88,9 @@ export const Inspector = ( { } ); const attributeObject = getAttributeFromId( numericId ); - if ( productFilterWrapperBlockId ) { + if ( filterHeadingBlock ) { updateBlockAttributes( - productFilterWrapperBlockId, - { - attributeId: numericId, - } - ); - } - if ( productFilterWrapperHeadingBlockId ) { - updateBlockAttributes( - productFilterWrapperHeadingBlockId, + filterHeadingBlock.clientId, { content: attributeObject?.label ?? @@ -188,17 +166,46 @@ export const Inspector = ( { value={ displayStyle } onChange={ ( value: BlockAttributes[ 'displayStyle' ] - ) => setAttributes( { displayStyle: value } ) } + ) => { + if ( ! filterBlock ) return; + const currentStyleBlock = getInnerBlockByName( + filterBlock, + displayStyle + ); + + if ( currentStyleBlock ) { + setDisplayStyleBlocksAttributes( { + ...displayStyleBlocksAttributes, + [ displayStyle ]: + currentStyleBlock.attributes, + } ); + replaceBlock( + currentStyleBlock.clientId, + createBlock( + value, + displayStyleBlocksAttributes[ value ] || + {} + ) + ); + } else { + insertBlock( + createBlock( value ), + filterBlock.innerBlocks.length, + filterBlock.clientId, + false + ); + } + setAttributes( { displayStyle: value } ); + } } style={ { width: '100%' } } > - - + { displayStyleOptions.map( ( blockType ) => ( + + ) ) } { + const blockProps = useBlockProps.save(); + const innerBlocksProps = useInnerBlocksProps.save( blockProps ); + return
    ; +}; + +export default Save; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json new file mode 100644 index 00000000000..75be211eb62 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "name": "woocommerce/product-filter-checkbox-list", + "version": "1.0.0", + "title": "List", + "description": "Display a list of filter options.", + "category": "woocommerce", + "keywords": [ + "WooCommerce" + ], + "textdomain": "woocommerce", + "apiVersion": 3, + "ancestor": [ + "woocommerce/product-filter-attribute" + ], + "supports": { + "color": { + "enableContrastChecker": false + } + }, + "usesContext": [ + "filterData" + ], + "attributes": { + "optionElementBorder": { + "type": "string", + "default": "" + }, + "customOptionElementBorder": { + "type": "string", + "default": "" + }, + "optionElementSelected": { + "type": "string", + "default": "" + }, + "customOptionElementSelected": { + "type": "string", + "default": "" + }, + "optionElement": { + "type": "string", + "default": "" + }, + "customOptionElement": { + "type": "string", + "default": "" + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx new file mode 100644 index 00000000000..3e4ea3b73c4 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx @@ -0,0 +1,223 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; +import { __ } from '@wordpress/i18n'; +import { Icon } from '@wordpress/components'; +import { checkMark } from '@woocommerce/icons'; +import { useMemo } from '@wordpress/element'; +import { + useBlockProps, + withColors, + InspectorControls, + // @ts-expect-error - no types. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, + // @ts-expect-error - no types. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, +} from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import './style.scss'; +import './editor.scss'; +import { EditProps } from './types'; + +const Edit = ( props: EditProps ): JSX.Element => { + const { + clientId, + context, + attributes, + setAttributes, + optionElementBorder, + setOptionElementBorder, + optionElementSelected, + setOptionElementSelected, + optionElement, + setOptionElement, + } = props; + + const { + customOptionElementBorder, + customOptionElementSelected, + customOptionElement, + } = attributes; + const { filterData } = context; + const { isLoading, items } = filterData; + + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + const blockProps = useBlockProps( { + className: clsx( 'wc-block-product-filter-checkbox-list', { + 'is-loading': isLoading, + 'has-option-element-border-color': + optionElementBorder.color || customOptionElementBorder, + 'has-option-element-selected-color': + optionElementSelected.color || customOptionElementSelected, + 'has-option-element-color': + optionElement.color || customOptionElement, + } ), + style: { + '--wc-product-filter-checkbox-list-option-element-border': + optionElementBorder.color || customOptionElementBorder, + '--wc-product-filter-checkbox-list-option-element-selected': + optionElementSelected.color || customOptionElementSelected, + '--wc-product-filter-checkbox-list-option-element': + optionElement.color || customOptionElement, + }, + } ); + + const loadingState = useMemo( () => { + return [ ...Array( 5 ) ].map( ( x, i ) => ( +
  • +   +
  • + ) ); + }, [] ); + + if ( ! items ) { + return <>; + } + + const threshold = 15; + const isLongList = items.length > threshold; + + return ( + <> +
    +
      + { isLoading && loadingState } + { ! isLoading && + ( isLongList + ? items.slice( 0, threshold ) + : items + ).map( ( item, index ) => ( +
    • + +
    • + ) ) } +
    + { ! isLoading && isLongList && ( + + { __( 'Show more…', 'woocommerce' ) } + + ) } +
    + + { colorGradientSettings.hasColorsOrGradients && ( + { + setOptionElementBorder( colorValue ); + setAttributes( { + customOptionElementBorder: colorValue, + } ); + }, + resetAllFilter: () => { + setOptionElementBorder( '' ); + setAttributes( { + customOptionElementBorder: '', + } ); + }, + }, + { + label: __( + 'Option Element (Selected)', + 'woocommerce' + ), + colorValue: + optionElementSelected.color || + customOptionElementSelected, + isShownByDefault: true, + enableAlpha: true, + onColorChange: ( colorValue: string ) => { + setOptionElementSelected( colorValue ); + setAttributes( { + customOptionElementSelected: colorValue, + } ); + }, + resetAllFilter: () => { + setOptionElementSelected( '' ); + setAttributes( { + customOptionElementSelected: '', + } ); + }, + }, + { + label: __( 'Option Element', 'woocommerce' ), + colorValue: + optionElement.color || customOptionElement, + isShownByDefault: true, + enableAlpha: true, + onColorChange: ( colorValue: string ) => { + setOptionElement( colorValue ); + setAttributes( { + customOptionElement: colorValue, + } ); + }, + resetAllFilter: () => { + setOptionElement( '' ); + setAttributes( { + customOptionElement: '', + } ); + }, + }, + ] } + panelId={ clientId } + { ...colorGradientSettings } + /> + ) } + + + ); +}; + +export default withColors( { + optionElementBorder: 'option-element-border', + optionElementSelected: 'option-element-border', + optionElement: 'option-element', +} )( Edit ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/editor.scss new file mode 100644 index 00000000000..dbca49525b7 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/editor.scss @@ -0,0 +1,10 @@ +.wc-block-product-filter-checkbox-list.is-loading { + .wc-block-product-filter-checkbox-list__list { + padding: 0; + + li { + @include placeholder(); + margin: 5px 0; + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts new file mode 100644 index 00000000000..f88d26cf9e2 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import { getContext, store } from '@woocommerce/interactivity'; +import { HTMLElementEvent } from '@woocommerce/types'; + +/** + * Internal dependencies + */ + +export type CheckboxListContext = { + items: { + id: string; + label: string; + value: string; + checked: boolean; + }[]; + showAll: boolean; +}; + +store( 'woocommerce/product-filter-checkbox-list', { + actions: { + showAllItems: () => { + const context = getContext< CheckboxListContext >(); + context.showAll = true; + }, + + selectCheckboxItem: ( event: HTMLElementEvent< HTMLInputElement > ) => { + const context = getContext< CheckboxListContext >(); + const value = event.target.value; + + context.items = context.items.map( ( item ) => { + if ( item.value.toString() === value ) { + return { + ...item, + checked: ! item.checked, + }; + } + return item; + } ); + }, + }, +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/index.tsx new file mode 100644 index 00000000000..d87769fa657 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/index.tsx @@ -0,0 +1,20 @@ +/** + * External dependencies + */ +import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; +import { productFilterOptions } from '@woocommerce/icons'; +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import Edit from './edit'; +import './style.scss'; + +if ( isExperimentalBlocksEnabled() ) { + registerBlockType( metadata, { + edit: Edit, + icon: productFilterOptions, + } ); +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/style.scss new file mode 100644 index 00000000000..ece2e52bc7f --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/style.scss @@ -0,0 +1,86 @@ +:where(.wc-block-product-filter-checkbox-list__list) { + list-style: none outside; + margin: 0; + padding: 0; +} + +.wc-block-product-filter-checkbox-list__item.hidden { + display: none; +} + + +:where(.wc-block-product-filter-checkbox-list__label) { + align-items: center; + display: flex; + gap: 0.625em; +} + +.wc-block-product-filter-checkbox-list__item .wc-block-product-filter-checkbox-list__label { + margin-bottom: 0; +} + +:where(.wc-block-product-filter-checkbox-list__input-wrapper) { + display: block; + position: relative; +} + +.wc-block-product-filter-checkbox-list__input-wrapper::before { + content: ""; + left: 0; + position: absolute; + top: 0; + background: currentColor; + opacity: 0.1; + width: 1em; + height: 1em; + border-radius: 2px; + + .has-option-element-color & { + display: none; + } +} + +:where(.wc-block-product-filter-checkbox-list__input) { + appearance: none; + border-radius: 2px; + border: 1px solid var(--wc-product-filter-checkbox-list-option-element-border, transparent); + color: inherit; + display: block; + font-size: inherit; + height: 1em; + margin: 0; + width: 1em; + background: var(--wc-product-filter-checkbox-list-option-element, transparent); +} + +.wc-block-product-filter-checkbox-list__input:checked + .wc-block-product-filter-checkbox-list__mark { + display: block; + pointer-events: none; +} + +.wc-block-product-filter-checkbox-list__input:focus { + outline-width: 1px; + outline-color: var(--wc-product-filter-checkbox-list-option-element-border, currentColor); +} + +:where(.wc-block-product-filter-checkbox-list__mark) { + box-sizing: border-box; + display: none; + height: 1em; + left: 0; + padding: 0.2em; + position: absolute; + top: 0; + width: 1em; + color: var(--wc-product-filter-checkbox-list-option-element-selected, currentColor); +} + +:where(.wc-block-product-filter-checkbox-list__show-more) { + cursor: pointer; + text-decoration: underline; +} + +.wc-block-product-filter-checkbox-list__show-more.hidden { + display: none; +} + diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/types.ts new file mode 100644 index 00000000000..ef10e7f6bd4 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/types.ts @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import { BlockEditProps } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { FilterBlockContext } from '../../types'; + +export type Color = { + slug: string; + class: string; + name: string; + color: string; +}; + +export type BlockAttributes = { + className: string; + optionElementBorder: string; + customOptionElementBorder: string; + optionElementSelected: string; + customOptionElementSelected: string; + optionElement: string; + customOptionElement: string; +}; + +export type EditProps = BlockEditProps< BlockAttributes > & { + context: FilterBlockContext; + optionElementBorder: Color; + setOptionElementBorder: ( value: string ) => void; + optionElementSelected: Color; + setOptionElementSelected: ( value: string ) => void; + optionElement: Color; + setOptionElement: ( value: string ) => void; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json new file mode 100644 index 00000000000..44e26c25279 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "name": "woocommerce/product-filter-chips", + "version": "1.0.0", + "title": "Chips", + "description": "Display filter options as chips.", + "category": "woocommerce", + "keywords": [ + "WooCommerce" + ], + "textdomain": "woocommerce", + "apiVersion": 3, + "ancestor": [ + "woocommerce/product-filter-attribute" + ], + "supports": {}, + "usesContext": [ + "filterData", + "isParentSelected" + ], + "attributes": {} +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx new file mode 100644 index 00000000000..fcefe04c0ab --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx @@ -0,0 +1,15 @@ +/** + * External dependencies + */ +import { useBlockProps } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import './style.scss'; + +const Edit = () => { + return
    These are chips.
    ; +}; + +export default Edit; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/index.tsx new file mode 100644 index 00000000000..d87769fa657 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/index.tsx @@ -0,0 +1,20 @@ +/** + * External dependencies + */ +import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; +import { productFilterOptions } from '@woocommerce/icons'; +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import Edit from './edit'; +import './style.scss'; + +if ( isExperimentalBlocksEnabled() ) { + registerBlockType( metadata, { + edit: Edit, + icon: productFilterOptions, + } ); +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss new file mode 100644 index 00000000000..a8af7fda118 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss @@ -0,0 +1,3 @@ +:where(.wc-block-product-filter-chips) { + // WIP +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/types.ts similarity index 59% rename from plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/types.ts rename to plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/types.ts index 726784edfd3..0a68e80edca 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/types.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/types.ts @@ -6,13 +6,9 @@ import { BlockEditProps } from '@wordpress/blocks'; /** * Internal dependencies */ -import { BLOCK_NAME_MAP } from './constants'; - -export type FilterType = keyof typeof BLOCK_NAME_MAP; export type BlockAttributes = { - filterType: FilterType; - heading: string; + className: string; }; export type EditProps = BlockEditProps< BlockAttributes >; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/frontend.ts index 0bfc299b660..7fccd55836c 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/frontend.ts @@ -1,3 +1,8 @@ +/** + * Logic in this file is unused and should be moved to product-fitlers block. + * + * @see https://github.com/woocommerce/woocommerce/issues/50868 + */ /** * External dependencies */ @@ -6,7 +11,7 @@ import { store, getContext } from '@woocommerce/interactivity'; /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; const getQueryParams = ( e: Event ) => { const filterNavContainer = ( e.target as HTMLElement )?.closest( diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts index 844e3c364f6..482e3e51117 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts @@ -9,7 +9,7 @@ import { debounce } from '@woocommerce/base-utils'; /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; import type { PriceFilterContext, PriceFilterStore } from './types'; const getUrl = ( context: PriceFilterContext ) => { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block-variations.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block-variations.tsx deleted file mode 100644 index 7d33338bea6..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block-variations.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/** - * External dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { BlockVariation } from '@wordpress/blocks'; -import { - productFilterActive, - productFilterAttribute, - productFilterPrice, - productFilterRating, - productFilterStockStatus, -} from '@woocommerce/icons'; -import { getSetting } from '@woocommerce/settings'; -import { AttributeSetting, objectHasProp } from '@woocommerce/types'; - -const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] ); - -const variations: BlockVariation[] = [ - { - name: 'product-filter-active', - title: __( 'Active (Experimental)', 'woocommerce' ), - description: __( - 'Display the currently active filters.', - 'woocommerce' - ), - attributes: { - heading: __( 'Active filters', 'woocommerce' ), - filterType: 'active-filters', - }, - icon: productFilterActive, - isDefault: true, - }, - { - name: 'product-filter-price', - title: __( 'Price (Experimental)', 'woocommerce' ), - description: __( - 'Enable customers to filter the product collection by choosing a price range.', - 'woocommerce' - ), - attributes: { - filterType: 'price-filter', - heading: __( 'Price', 'woocommerce' ), - }, - icon: productFilterPrice, - }, - { - name: 'product-filter-stock-status', - title: __( 'Status (Experimental)', 'woocommerce' ), - description: __( - 'Enable customers to filter the product collection by stock status.', - 'woocommerce' - ), - attributes: { - filterType: 'stock-filter', - heading: __( 'Status', 'woocommerce' ), - }, - icon: productFilterStockStatus, - }, - { - name: 'product-filter-rating', - title: __( 'Rating (Experimental)', 'woocommerce' ), - description: __( - 'Enable customers to filter the product collection by rating.', - 'woocommerce' - ), - attributes: { - filterType: 'rating-filter', - heading: __( 'Rating', 'woocommerce' ), - }, - icon: productFilterRating, - }, -]; - -ATTRIBUTES.forEach( ( attribute ) => { - variations.push( { - name: `product-filter-attribute-${ attribute.attribute_name }`, - title: `${ attribute.attribute_label } (Experimental)`, - description: sprintf( - // translators: %s is the attribute label. - __( - `Enable customers to filter the product collection by selecting one or more %s attributes.`, - 'woocommerce' - ), - attribute.attribute_label - ), - attributes: { - filterType: 'attribute-filter', - heading: attribute.attribute_label, - attributeId: parseInt( attribute.attribute_id, 10 ), - }, - icon: productFilterAttribute, - // Can be `isActive: [ 'filterType', 'attributeId' ]`, but the API is available from 6.6. - isActive: ( blockAttributes, variationAttributes ) => { - return ( - blockAttributes.filterType === variationAttributes.filterType && - blockAttributes.attributeId === variationAttributes.attributeId - ); - }, - } ); -} ); - -variations.push( { - name: 'product-filter-attribute', - title: __( 'Attribute (Experimental)', 'woocommerce' ), - description: __( - 'Enable customers to filter the product collection by selecting one or more attributes, such as color.', - 'woocommerce' - ), - attributes: { - filterType: 'attribute-filter', - heading: __( 'Attribute', 'woocommerce' ), - attributeId: 0, - }, - icon: productFilterAttribute, -} ); - -/** - * Add `isActive` function to all Product Filter block variations. - * `isActive` function is used to find a variation match from a created - * Block by providing its attributes. - */ -variations.forEach( ( variation ) => { - if ( ! objectHasProp( variation, 'isActive' ) ) { - // @ts-expect-error: `isActive` is currently typed wrong in `@wordpress/blocks`. - variation.isActive = [ 'filterType' ]; - } -} ); - -export const blockVariations = variations; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block.json deleted file mode 100644 index 673300a288c..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "woocommerce/product-filter", - "version": "1.0.0", - "title": "Product Filter (Experimental)", - "description": "A block that adds product filters to the product collection.", - "category": "woocommerce", - "keywords": [ - "WooCommerce", - "Filters" - ], - "textdomain": "woocommerce", - "supports": { - "html": false, - "reusable": false, - "inserter": true - }, - "ancestor": [ - "woocommerce/product-filters" - ], - "usesContext": [ - "query", - "queryId" - ], - "attributes": { - "filterType": { - "type": "string" - }, - "heading": { - "type": "string" - }, - "isPreview": { - "type": "boolean", - "default": false - }, - "attributeId": { - "type": "number", - "default": 0 - } - }, - "example": { - "attributes": { - "isPreview": true - } - }, - "apiVersion": 3, - "$schema": "https://schemas.wp.org/trunk/block.json" -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/components/warning.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/components/warning.tsx deleted file mode 100644 index 33b16eaf5cf..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/components/warning.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * External dependencies - */ -import { getSetting } from '@woocommerce/settings'; -import { Notice } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; - -const Warning = () => { - const isWidgetEditor = getSetting< boolean >( 'isWidgetEditor' ); - if ( isWidgetEditor ) { - return ( - - { __( - 'The widget area containing Collection Filters block needs to be placed on a product archive page for filters to function properly.', - 'woocommerce' - ) } - - ); - } - - const isSiteEditor = getSetting< boolean >( 'isSiteEditor' ); - if ( ! isSiteEditor ) { - return ( - - { __( - 'When added to a post or page, Collection Filters block needs to be nested inside a Product Collection block to function properly.', - 'woocommerce' - ) } - - ); - } - - return null; -}; - -export default Warning; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/constants.ts deleted file mode 100644 index ba212b495f2..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const BLOCK_NAME_MAP = { - 'active-filters': 'woocommerce/product-filter-active', - 'price-filter': 'woocommerce/product-filter-price', - 'stock-filter': 'woocommerce/product-filter-stock-status', - 'rating-filter': 'woocommerce/product-filter-rating', - 'attribute-filter': 'woocommerce/product-filter-attribute', - 'clear-button': 'woocommerce/product-filter-clear-button', -}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/edit.tsx deleted file mode 100644 index 9706f037f9f..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/edit.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/** - * External dependencies - */ -import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; -import { BlockEditProps } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import Warning from './components/warning'; -import './editor.scss'; -import { getAllowedBlocks } from './utils'; -import { BLOCK_NAME_MAP } from './constants'; -import type { FilterType } from './types'; - -const Edit = ( { - attributes, - clientId, -}: BlockEditProps< { - heading: string; - filterType: FilterType; - isPreview: boolean; - attributeId: number | undefined; -} > ) => { - const blockProps = useBlockProps(); - - const isNested = useSelect( ( select ) => { - const { getBlockParentsByBlockName } = select( 'core/block-editor' ); - return !! getBlockParentsByBlockName( - clientId, - 'woocommerce/product-collection' - ).length; - } ); - - return ( - - ); -}; - -export default Edit; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/editor.scss deleted file mode 100644 index b739f84f377..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/editor.scss +++ /dev/null @@ -1,5 +0,0 @@ -.wp-block-woocommerce-collection-filters { - .components-notice { - margin: 0; - } -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/frontend.ts deleted file mode 100644 index 13a489f89c0..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/frontend.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * External dependencies - */ -import { navigate as navigateFn } from '@woocommerce/interactivity'; -import { getSetting } from '@woocommerce/settings'; - -const isBlockTheme = getSetting< boolean >( 'isBlockTheme' ); -const isProductArchive = getSetting< boolean >( 'isProductArchive' ); -const needsRefresh = getSetting< boolean >( - 'needsRefreshForInteractivityAPI', - false -); - -export function navigate( href: string, options = {} ) { - /** - * We may need to reset the current page when changing filters. - * This is because the current page may not exist for this set - * of filters and will 404 when the user navigates to it. - * - * There are different pagination formats to consider, as documented here: - * https://github.com/WordPress/gutenberg/blob/317eb8f14c8e1b81bf56972cca2694be250580e3/packages/block-library/src/query-pagination-numbers/index.php#L22-L85 - */ - const url = new URL( href ); - // When pretty permalinks are enabled, the page number may be in the path name. - url.pathname = url.pathname.replace( /\/page\/[0-9]+/i, '' ); - // When plain permalinks are enabled, the page number may be in the "paged" query parameter. - url.searchParams.delete( 'paged' ); - // On posts and pages the page number will be in a query parameter that - // identifies which block we are paginating. - url.searchParams.forEach( ( _, key ) => { - if ( key.match( /^query(?:-[0-9]+)?-page$/ ) ) { - url.searchParams.delete( key ); - } - } ); - // Make sure to update the href with the changes. - href = url.href; - - if ( needsRefresh || ( ! isBlockTheme && isProductArchive ) ) { - return ( window.location.href = href ); - } - return navigateFn( href, options ); -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/index.tsx deleted file mode 100644 index 850ac787f5d..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/** - * External dependencies - */ -import { - BlockInstance, - createBlock, - registerBlockType, -} from '@wordpress/blocks'; -import { Icon, more } from '@wordpress/icons'; -import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; -/** - * Internal dependencies - */ -import metadata from './block.json'; -import edit from './edit'; -import save from './save'; -import { BLOCK_NAME_MAP } from './constants'; -import { BlockAttributes } from './types'; -import { blockVariations } from './block-variations'; - -if ( isExperimentalBlocksEnabled() ) { - registerBlockType( metadata, { - icon: { - src: ( - - ), - }, - edit, - save, - variations: blockVariations, - transforms: { - from: [ - { - type: 'block', - blocks: [ 'woocommerce/filter-wrapper' ], - transform: ( - attributes: BlockAttributes, - innerBlocks: BlockInstance[] - ) => { - const newInnerBlocks: BlockInstance[] = []; - // Loop through inner blocks to preserve the block order. - innerBlocks.forEach( ( block ) => { - if ( - block.name === - `woocommerce/${ attributes.filterType }` - ) { - newInnerBlocks.push( - createBlock( - BLOCK_NAME_MAP[ attributes.filterType ], - block.attributes - ) - ); - } - - if ( block.name === 'core/heading' ) { - newInnerBlocks.push( block ); - } - } ); - - return createBlock( - 'woocommerce/product-filter', - attributes, - newInnerBlocks - ); - }, - }, - ], - }, - } ); -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/save.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/save.tsx deleted file mode 100644 index 992874cb932..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/save.tsx +++ /dev/null @@ -1,8 +0,0 @@ -/** - * External dependencies - */ -import { InnerBlocks } from '@wordpress/block-editor'; - -export default function save() { - return ; -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/utils.ts deleted file mode 100644 index 10a27961150..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * External dependencies - */ -import { getBlockTypes } from '@wordpress/blocks'; - -/** - * Returns an array of allowed block names excluding the disallowedBlocks array. - * - * @param disallowedBlocks Array of block names to disallow. - * @return Array of allowed block names. - */ -export const getAllowedBlocks = ( disallowedBlocks: string[] ) => { - const allBlocks = getBlockTypes(); - - return allBlocks - .map( ( block ) => block.name ) - .filter( ( name ) => ! disallowedBlocks.includes( name ) ); -}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/frontend.ts index 3bd9f961704..8d865131b33 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/frontend.ts @@ -8,7 +8,7 @@ import { DropdownContext } from '@woocommerce/interactivity-components/dropdown' /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; function getUrl( filters: Array< string | null > ) { filters = filters.filter( Boolean ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/stock-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/stock-filter/frontend.ts index 9833ff753c1..f0037014671 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/stock-filter/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/stock-filter/frontend.ts @@ -9,7 +9,7 @@ import { CheckboxListContext } from '@woocommerce/interactivity-components/check /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; const getUrl = ( activeFilters: string ) => { const url = new URL( window.location.href ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts index f7ad8b247e9..0709a1490b8 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts @@ -7,8 +7,8 @@ export type BlockOverlayAttributeOptions = ( typeof BlockOverlayAttribute )[ keyof typeof BlockOverlayAttribute ]; export interface BlockAttributes { - productId?: string; setAttributes: ( attributes: ProductFiltersBlockAttributes ) => void; + productId?: string; overlay: BlockOverlayAttributeOptions; overlayIcon: | 'filter-icon-1' @@ -18,3 +18,23 @@ export interface BlockAttributes { overlayButtonStyle: 'label-icon' | 'label' | 'icon'; overlayIconSize?: number; } + +export type FilterOptionItem = { + label: string; + value: string; + selected?: boolean; + rawData?: Record< string, unknown >; +}; + +export type FilterBlockContext = { + filterData: { + isLoading: boolean; + items?: FilterOptionItem[]; + range?: { + min: number; + max: number; + step: number; + }; + }; + isParentSelected: boolean; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/utils.ts new file mode 100644 index 00000000000..d21e622fc3e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/utils.ts @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { BlockInstance, getBlockTypes } from '@wordpress/blocks'; + +/** + * Returns an array of allowed block names excluding the disallowedBlocks array. + * + * @param disallowedBlocks Array of block names to disallow. + * @return Array of allowed block names. + */ +export const getAllowedBlocks = ( disallowedBlocks: string[] ) => { + const allBlocks = getBlockTypes(); + + return allBlocks + .map( ( block ) => block.name ) + .filter( ( name ) => ! disallowedBlocks.includes( name ) ); +}; + +export const getInnerBlockByName = ( + block: BlockInstance | null, + name: string +): BlockInstance | null => { + if ( ! block ) return null; + + if ( block.innerBlocks.length === 0 ) return null; + + for ( const innerBlock of block.innerBlocks ) { + if ( innerBlock.name === name ) return innerBlock; + const innerInnerBlock = getInnerBlockByName( innerBlock, name ); + if ( innerInnerBlock ) return innerInnerBlock; + } + + return null; +}; diff --git a/plugins/woocommerce-blocks/assets/js/icons/index.js b/plugins/woocommerce-blocks/assets/js/icons/index.js index 446bbadcebb..6cfa7e5f667 100644 --- a/plugins/woocommerce-blocks/assets/js/icons/index.js +++ b/plugins/woocommerce-blocks/assets/js/icons/index.js @@ -4,6 +4,7 @@ export { default as bagAlt } from './library/bag-alt'; export { default as barcode } from './library/barcode'; export { default as cart } from './library/cart'; export { default as cartOutline } from './library/cart-outline'; +export { default as checkMark } from './library/check-mark'; export { default as checkPayment } from './library/check-payment'; export { default as closeSquareShadow } from './library/close-square-shadow'; export { default as customerAccount } from './library/customer-account'; diff --git a/plugins/woocommerce-blocks/assets/js/icons/library/check-mark.tsx b/plugins/woocommerce-blocks/assets/js/icons/library/check-mark.tsx new file mode 100644 index 00000000000..cd7e6b1bffa --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/icons/library/check-mark.tsx @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { IconProps } from '@wordpress/icons/build-types/icon'; +import { Path, SVG } from '@wordpress/primitives'; + +const CheckMark = ( props: IconProps ) => ( + + + +); + +export default CheckMark; diff --git a/plugins/woocommerce-blocks/bin/webpack-entries.js b/plugins/woocommerce-blocks/bin/webpack-entries.js index 11a84832ef5..aeeb511c7eb 100644 --- a/plugins/woocommerce-blocks/bin/webpack-entries.js +++ b/plugins/woocommerce-blocks/bin/webpack-entries.js @@ -88,10 +88,6 @@ const blocks = { 'product-filters': { isExperimental: true, }, - 'product-filter': { - isExperimental: true, - customDir: 'product-filters/inner-blocks/product-filter', - }, 'product-filters-overlay': { isExperimental: true, customDir: 'product-filters/inner-blocks/overlay', @@ -124,6 +120,14 @@ const blocks = { customDir: 'product-filters/inner-blocks/clear-button', isExperimental: true, }, + 'product-filter-checkbox-list': { + customDir: 'product-filters/inner-blocks/checkbox-list', + isExperimental: true, + }, + 'product-filter-chips': { + customDir: 'product-filters/inner-blocks/chips', + isExperimental: true, + }, 'order-confirmation-summary': { customDir: 'order-confirmation/summary', }, diff --git a/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars b/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars index f99258d7b35..1434a83d574 100644 --- a/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars +++ b/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars @@ -1,11 +1,12 @@ - - -

    Active filters

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

    Filter by Attribute

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

    Attribute

    + + + -
    + - - +
    + -{{#> wp-block blockName='woocommerce/product-filter-attribute' attributes=attributes }} +
    {{/ wp-block }} +
    + +
    diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/active-filters.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/active-filters.block_theme.spec.ts deleted file mode 100644 index b4c78fd0ab1..00000000000 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/active-filters.block_theme.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * External dependencies - */ -import { TemplateCompiler, test as base, expect } from '@woocommerce/e2e-utils'; - -const test = base.extend< { templateCompiler: TemplateCompiler } >( { - templateCompiler: async ( { requestUtils }, use ) => { - const compiler = await requestUtils.createTemplateFromFile( - 'archive-product_active-filters' - ); - await use( compiler ); - }, -} ); - -test.describe( 'Product Filter: Active Filters Block', () => { - test.describe( 'frontend', () => { - test.beforeEach( async ( { requestUtils } ) => { - await requestUtils.activatePlugin( - 'woocommerce-blocks-test-enable-experimental-features' - ); - } ); - - test( 'Without any filters selected, only a wrapper block is rendered', async ( { - page, - templateCompiler, - } ) => { - await templateCompiler.compile(); - - await page.goto( '/shop' ); - - const locator = page.locator( - '.wp-block-woocommerce-product-filter' - ); - - await expect( locator ).toHaveCount( 1 ); - - const html = await locator.innerHTML(); - expect( html.trim() ).toBe( '' ); - } ); - - test( 'With rating filters applied it shows the correct active filters', async ( { - page, - templateCompiler, - } ) => { - await templateCompiler.compile(); - - await page.goto( `${ '/shop' }?rating_filter=1,2,5` ); - - await expect( page.getByText( 'Rating:' ) ).toBeVisible(); - await expect( page.getByText( 'Rated 1 out of 5' ) ).toBeVisible(); - await expect( page.getByText( 'Rated 2 out of 5' ) ).toBeVisible(); - await expect( page.getByText( 'Rated 5 out of 5' ) ).toBeVisible(); - } ); - - test( 'With stock filters applied it shows the correct active filters', async ( { - page, - templateCompiler, - } ) => { - await templateCompiler.compile(); - - await page.goto( - `${ '/shop' }?filter_stock_status=instock,onbackorder` - ); - - await expect( page.getByText( 'Stock Status:' ) ).toBeVisible(); - await expect( page.getByText( 'In stock' ) ).toBeVisible(); - await expect( page.getByText( 'On backorder' ) ).toBeVisible(); - } ); - - test( 'With attribute filters applied it shows the correct active filters', async ( { - page, - templateCompiler, - } ) => { - await templateCompiler.compile(); - - await page.goto( - `${ '/shop' }?filter_color=blue,gray&query_type_color=or` - ); - - await expect( page.getByText( 'Color:' ) ).toBeVisible(); - await expect( page.getByText( 'Blue' ) ).toBeVisible(); - await expect( page.getByText( 'Gray' ) ).toBeVisible(); - } ); - - test( 'With price filters applied it shows the correct active filters', async ( { - page, - templateCompiler, - } ) => { - await templateCompiler.compile(); - - await page.goto( `${ '/shop' }?min_price=17&max_price=71` ); - - await expect( page.getByText( 'Price:' ) ).toBeVisible(); - await expect( - page.getByText( 'Between $17 and $71' ) - ).toBeVisible(); - } ); - } ); -} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/attribute-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/attribute-filter.block_theme.spec.ts deleted file mode 100644 index d39c14ff018..00000000000 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/attribute-filter.block_theme.spec.ts +++ /dev/null @@ -1,316 +0,0 @@ -/** - * External dependencies - */ -import { TemplateCompiler, test as base, expect } from '@woocommerce/e2e-utils'; - -const COLOR_ATTRIBUTE_VALUES = [ 'Blue', 'Gray', 'Green', 'Red', 'Yellow' ]; -const COLOR_ATTRIBUTES_WITH_COUNTS = [ - 'Blue (4)', - 'Gray (2)', - 'Green (3)', - 'Red (4)', - 'Yellow (1)', -]; - -const test = base.extend< { templateCompiler: TemplateCompiler } >( { - templateCompiler: async ( { requestUtils }, use ) => { - const compiler = await requestUtils.createTemplateFromFile( - 'archive-product_attribute-filter' - ); - await use( compiler ); - }, -} ); - -test.describe( 'Product Filter: Attribute Block', () => { - test.describe( 'With default display style', () => { - test.beforeEach( async ( { requestUtils, templateCompiler } ) => { - await requestUtils.activatePlugin( - 'woocommerce-blocks-test-enable-experimental-features' - ); - await templateCompiler.compile( { - attributes: { - attributeId: 1, - }, - } ); - } ); - - test( 'clear button is not shown on initial page load', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await expect( button ).toBeHidden(); - } ); - - test( 'renders a checkbox list with the available attribute filters', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const attributes = page.locator( - '.wc-block-interactivity-components-checkbox-list__label' - ); - - await expect( attributes ).toHaveCount( 5 ); - - for ( let i = 0; i < COLOR_ATTRIBUTE_VALUES.length; i++ ) { - await expect( attributes.nth( i ) ).toHaveText( - COLOR_ATTRIBUTE_VALUES[ i ] - ); - } - } ); - - test( 'filters the list of products by selecting an attribute', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const grayCheckbox = page.getByText( 'Gray' ); - await grayCheckbox.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=gray.*/ ); - - const products = page.locator( '.wc-block-product' ); - - await expect( products ).toHaveCount( 2 ); - } ); - - test( 'clear button appears after a filter is applied', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const grayCheckbox = page.getByText( 'Gray' ); - await grayCheckbox.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=gray.*/ ); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await expect( button ).toBeVisible(); - } ); - - test( 'clear button hides after deselecting all filters', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const grayCheckbox = page.getByText( 'Gray' ); - await grayCheckbox.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=gray.*/ ); - - await grayCheckbox.click(); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await expect( button ).toBeHidden(); - } ); - - test( 'filters are cleared after clear button is clicked', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const grayCheckbox = page.getByText( 'Gray' ); - await grayCheckbox.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=gray.*/ ); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await button.click(); - - COLOR_ATTRIBUTE_VALUES.map( async ( color ) => { - const element = page.locator( - `input[value="${ color.toLowerCase() }"]` - ); - - await expect( element ).not.toBeChecked(); - } ); - } ); - } ); - - test.describe( 'With show counts enabled', () => { - test.beforeEach( async ( { requestUtils, templateCompiler } ) => { - await requestUtils.activatePlugin( - 'woocommerce-blocks-test-enable-experimental-features' - ); - await templateCompiler.compile( { - attributes: { - attributeId: 1, - showCounts: true, - }, - } ); - } ); - - test( 'Renders checkboxes with associated product counts', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const attributes = page.locator( - '.wc-block-interactivity-components-checkbox-list__label' - ); - - await expect( attributes ).toHaveCount( 5 ); - - for ( let i = 0; i < COLOR_ATTRIBUTES_WITH_COUNTS.length; i++ ) { - await expect( attributes.nth( i ) ).toHaveText( - COLOR_ATTRIBUTES_WITH_COUNTS[ i ] - ); - } - } ); - } ); - - test.describe( "With display style 'dropdown'", () => { - test.beforeEach( async ( { requestUtils, templateCompiler } ) => { - await requestUtils.activatePlugin( - 'woocommerce-blocks-test-enable-experimental-features' - ); - await templateCompiler.compile( { - attributes: { - attributeId: 1, - displayStyle: 'dropdown', - }, - } ); - } ); - - test( 'clear button is not shown on initial page load', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await expect( button ).toBeHidden(); - } ); - - test( 'renders a dropdown list with the available attribute filters', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const dropdownLocator = page.locator( - '.wc-interactivity-dropdown' - ); - - await expect( dropdownLocator ).toBeVisible(); - await dropdownLocator.click(); - - for ( let i = 0; i < COLOR_ATTRIBUTE_VALUES.length; i++ ) { - await expect( - dropdownLocator.getByText( COLOR_ATTRIBUTE_VALUES[ i ] ) - ).toBeVisible(); - } - } ); - - test( 'Clicking a dropdown option should filter the displayed products', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const dropdownLocator = page.locator( - '.wc-interactivity-dropdown' - ); - - await expect( dropdownLocator ).toBeVisible(); - await dropdownLocator.click(); - - const yellowOption = page.getByText( 'Yellow' ); - await yellowOption.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=yellow.*/ ); - - const products = page.locator( '.wc-block-product' ); - - await expect( products ).toHaveCount( 1 ); - } ); - - test( 'clear button appears after a filter is applied', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const dropdownLocator = page.locator( - '.wc-interactivity-dropdown' - ); - - await expect( dropdownLocator ).toBeVisible(); - await dropdownLocator.click(); - - const yellowOption = page.getByText( 'Yellow' ); - await yellowOption.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=yellow.*/ ); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await expect( button ).toBeVisible(); - } ); - - test( 'clear button hides after deselecting all filters', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const dropdownLocator = page.locator( - '.wc-interactivity-dropdown' - ); - - await dropdownLocator.click(); - - const yellowOption = page.getByText( 'Yellow' ); - await yellowOption.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=yellow.*/ ); - - await dropdownLocator.click(); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - const removeFilter = page.locator( - '.wc-interactivity-dropdown__badge-remove' - ); - - await removeFilter.click(); - - await expect( button ).toBeHidden(); - } ); - - test( 'filters are cleared after clear button is clicked', async ( { - page, - } ) => { - await page.goto( '/shop' ); - - const dropdownLocator = page.locator( - '.wc-interactivity-dropdown' - ); - - await dropdownLocator.click(); - - const yellowOption = page.getByText( 'Yellow' ); - await yellowOption.click(); - - // wait for navigation - await page.waitForURL( /.*filter_color=yellow.*/ ); - - const button = page.getByRole( 'button', { name: 'Clear' } ); - - await button.click(); - - const placeholder = page.getByText( 'Select Color' ); - - await expect( placeholder ).toBeVisible(); - } ); - } ); -} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/basic.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/basic.block_theme.spec.ts deleted file mode 100644 index db9f8dd5b10..00000000000 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/basic.block_theme.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * External dependencies - */ -import { test, expect } from '@woocommerce/e2e-utils'; - -const filterBlocks = [ - { - name: 'woocommerce/product-filter-price', - title: 'Product Filter: Price (Experimental)', - heading: 'Filter by Price', - }, - { - name: 'woocommerce/product-filter-stock-status', - title: 'Product Filter: Stock Status (Experimental)', - heading: 'Filter by Stock Status', - }, - { - name: 'woocommerce/product-filter-rating', - title: 'Product Filter: Rating (Experimental)', - heading: 'Filter by Rating', - }, - { - name: 'woocommerce/product-filter-attribute', - title: 'Product Filter: Attribute (Experimental)', - heading: 'Filter by Attribute', - }, - { - name: 'woocommerce/product-filter-active', - title: 'Product Filter: Active Filters (Experimental)', - heading: 'Active Filters', - }, -]; - -test.describe( 'Filter blocks registration', () => { - test.beforeEach( async ( { admin } ) => { - await admin.createNewPost(); - } ); - - test( 'Variations cannot be inserted through the inserter.', async ( { - page, - editor, - } ) => { - for ( const block of filterBlocks ) { - await editor.openGlobalBlockInserter(); - await page.getByPlaceholder( 'Search' ).fill( block.title ); - const filterBlock = page.getByRole( 'option', { - name: block.title, - exact: true, - } ); - - await expect( filterBlock ).toBeHidden(); - } - } ); -} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts index 4dae7c49c84..97ae2631985 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts @@ -105,10 +105,6 @@ test.describe( 'Filters Overlay Template Part', () => { templatePartData.selectors.editor.blocks.activeFilters .blockLabel ) - .getByLabel( - templatePartData.selectors.editor.blocks.filterOptions - .blockLabel - ) .click(); await editor.openDocumentSettingsSidebar(); @@ -201,10 +197,6 @@ test.describe( 'Filters Overlay Template Part', () => { templatePartData.selectors.editor.blocks.activeFilters .blockLabel ) - .getByLabel( - templatePartData.selectors.editor.blocks.filterOptions - .blockLabel - ) .click(); await editor.openDocumentSettingsSidebar(); @@ -221,12 +213,6 @@ test.describe( 'Filters Overlay Template Part', () => { 'OverlayNeverMobileAlways' ); await layoutSettings.getByLabel( 'Never' ).click(); - await editor.page - .getByRole( 'link', { - name: templatePartData.selectors.editor.blocks - .productFiltersOverlayNavigation.title, - } ) - .click(); await editor.saveSiteEditorEntities( { isOnlyCurrentEntityDirty: true, @@ -271,10 +257,6 @@ test.describe( 'Filters Overlay Template Part', () => { templatePartData.selectors.editor.blocks.activeFilters .blockLabel ) - .getByLabel( - templatePartData.selectors.editor.blocks.filterOptions - .blockLabel - ) .click(); await editor.openDocumentSettingsSidebar(); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/active-filter-frontend.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/active-filter-frontend.block_theme.spec.ts new file mode 100644 index 00000000000..341a57ee21e --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/active-filter-frontend.block_theme.spec.ts @@ -0,0 +1,115 @@ +/** + * External dependencies + */ +import { TemplateCompiler, test as base, expect } from '@woocommerce/e2e-utils'; + +const test = base.extend< { templateCompiler: TemplateCompiler } >( { + templateCompiler: async ( { requestUtils }, use ) => { + const compiler = await requestUtils.createTemplateFromFile( + 'archive-product_active-filters' + ); + await use( compiler ); + }, +} ); + +test.describe( 'woocommerce/product-filter-active - Frontend', () => { + test.beforeEach( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( + 'woocommerce-blocks-test-enable-experimental-features' + ); + } ); + + test( 'Without any filters selected, only a wrapper block is rendered', async ( { + page, + templateCompiler, + } ) => { + await templateCompiler.compile( { + attributes: { + displayStyle: 'list', + }, + } ); + + await page.goto( '/shop' ); + + const locator = page.locator( + '.wp-block-woocommerce-product-filter-active' + ); + + await expect( locator ).toHaveCount( 1 ); + + const html = await locator.innerHTML(); + expect( html.trim() ).toBe( '' ); + } ); + + test( 'With rating filters applied it shows the correct active filters', async ( { + page, + templateCompiler, + } ) => { + await templateCompiler.compile( { + attributes: { + displayStyle: 'list', + }, + } ); + + await page.goto( `${ '/shop' }?rating_filter=1,2,5` ); + + await expect( page.getByText( 'Rating:' ) ).toBeVisible(); + await expect( page.getByText( 'Rated 1 out of 5' ) ).toBeVisible(); + await expect( page.getByText( 'Rated 2 out of 5' ) ).toBeVisible(); + await expect( page.getByText( 'Rated 5 out of 5' ) ).toBeVisible(); + } ); + + test( 'With stock filters applied it shows the correct active filters', async ( { + page, + templateCompiler, + } ) => { + await templateCompiler.compile( { + attributes: { + displayStyle: 'list', + }, + } ); + + await page.goto( + `${ '/shop' }?filter_stock_status=instock,onbackorder` + ); + + await expect( page.getByText( 'Stock Status:' ) ).toBeVisible(); + await expect( page.getByText( 'In stock' ) ).toBeVisible(); + await expect( page.getByText( 'On backorder' ) ).toBeVisible(); + } ); + + test( 'With attribute filters applied it shows the correct active filters', async ( { + page, + templateCompiler, + } ) => { + await templateCompiler.compile( { + attributes: { + displayStyle: 'list', + }, + } ); + + await page.goto( + `${ '/shop' }?filter_color=blue,gray&query_type_color=or` + ); + + await expect( page.getByText( 'Color:' ) ).toBeVisible(); + await expect( page.getByText( 'Blue' ) ).toBeVisible(); + await expect( page.getByText( 'Gray' ) ).toBeVisible(); + } ); + + test( 'With price filters applied it shows the correct active filters', async ( { + page, + templateCompiler, + } ) => { + await templateCompiler.compile( { + attributes: { + displayStyle: 'list', + }, + } ); + + await page.goto( `${ '/shop' }?min_price=17&max_price=71` ); + + await expect( page.getByText( 'Price:' ) ).toBeVisible(); + await expect( page.getByText( 'Between $17 and $71' ) ).toBeVisible(); + } ); +} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-editor.block_theme.spec.ts similarity index 91% rename from plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts rename to plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-editor.block_theme.spec.ts index 54220f04b92..77f9b13f090 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-editor.block_theme.spec.ts @@ -48,9 +48,7 @@ test.describe( `${ blockData.name }`, () => { } ) => { await pageObject.addProductFiltersBlock( { cleanContent: true } ); - const block = editor.canvas - .getByLabel( 'Block: Color (Experimental)' ) - .getByLabel( 'Block: Filter Options' ); + const block = editor.canvas.getByLabel( 'Block: Color (Experimental)' ); await expect( block ).toBeVisible(); @@ -82,9 +80,7 @@ test.describe( `${ blockData.name }`, () => { } ) => { await pageObject.addProductFiltersBlock( { cleanContent: true } ); - const block = editor.canvas - .getByLabel( 'Block: Color (Experimental)' ) - .getByLabel( 'Block: Filter Options' ); + const block = editor.canvas.getByLabel( 'Block: Color (Experimental)' ); await expect( block ).toBeVisible(); @@ -112,9 +108,7 @@ test.describe( `${ blockData.name }`, () => { } ) => { await pageObject.addProductFiltersBlock( { cleanContent: true } ); - const block = editor.canvas - .getByLabel( 'Block: Color (Experimental)' ) - .getByLabel( 'Block: Filter Options' ); + const block = editor.canvas.getByLabel( 'Block: Color (Experimental)' ); await editor.openDocumentSettingsSidebar(); await block.click(); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-frontend.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-frontend.block_theme.spec.ts new file mode 100644 index 00000000000..8f79924b291 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-frontend.block_theme.spec.ts @@ -0,0 +1,171 @@ +/** + * External dependencies + */ +import { TemplateCompiler, test as base, expect } from '@woocommerce/e2e-utils'; + +const COLOR_ATTRIBUTE_VALUES = [ 'Blue', 'Gray', 'Green', 'Red', 'Yellow' ]; +const COLOR_ATTRIBUTES_WITH_COUNTS = [ + 'Blue (4)', + 'Gray (2)', + 'Green (3)', + 'Red (4)', + 'Yellow (1)', +]; + +const test = base.extend< { templateCompiler: TemplateCompiler } >( { + templateCompiler: async ( { requestUtils }, use ) => { + const compiler = await requestUtils.createTemplateFromFile( + 'archive-product_attribute-filter' + ); + await use( compiler ); + }, +} ); + +test.describe( 'woocommerce/product-filter-attribute - Frontend', () => { + test.describe( 'With default display style', () => { + test.beforeEach( async ( { requestUtils, templateCompiler } ) => { + await requestUtils.activatePlugin( + 'woocommerce-blocks-test-enable-experimental-features' + ); + await templateCompiler.compile( { + attributes: { + attributeId: 1, + }, + } ); + } ); + + test( 'clear button is not shown on initial page load', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeHidden(); + } ); + + test( 'renders a checkbox list with the available attribute filters', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const listItems = page + .getByLabel( 'Filter Options' ) + .getByRole( 'listitem' ); + + await expect( listItems ).toHaveCount( 5 ); + + for ( let i = 0; i < COLOR_ATTRIBUTE_VALUES.length; i++ ) { + await expect( listItems.nth( i ) ).toHaveText( + COLOR_ATTRIBUTE_VALUES[ i ] + ); + } + } ); + + test( 'filters the list of products by selecting an attribute', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const grayCheckbox = page.getByText( 'Gray' ); + await grayCheckbox.click(); + + // wait for navigation + await page.waitForURL( /.*filter_color=gray.*/ ); + + const products = page.locator( '.wc-block-product' ); + + await expect( products ).toHaveCount( 2 ); + } ); + + test( 'clear button appears after a filter is applied', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const grayCheckbox = page.getByText( 'Gray' ); + await grayCheckbox.click(); + + // wait for navigation + await page.waitForURL( /.*filter_color=gray.*/ ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeVisible(); + } ); + + test( 'clear button hides after deselecting all filters', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const grayCheckbox = page.getByText( 'Gray' ); + await grayCheckbox.click(); + + // wait for navigation + await page.waitForURL( /.*filter_color=gray.*/ ); + + await grayCheckbox.click(); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeHidden(); + } ); + + test( 'filters are cleared after clear button is clicked', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const grayCheckbox = page.getByText( 'Gray' ); + await grayCheckbox.click(); + + // wait for navigation + await page.waitForURL( /.*filter_color=gray.*/ ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await button.click(); + + COLOR_ATTRIBUTE_VALUES.map( async ( color ) => { + const element = page.locator( + `input[value="${ color.toLowerCase() }"]` + ); + + await expect( element ).not.toBeChecked(); + } ); + } ); + } ); + + test.describe( 'With show counts enabled', () => { + test.beforeEach( async ( { requestUtils, templateCompiler } ) => { + await requestUtils.activatePlugin( + 'woocommerce-blocks-test-enable-experimental-features' + ); + await templateCompiler.compile( { + attributes: { + attributeId: 1, + showCounts: true, + }, + } ); + } ); + + test( 'Renders checkboxes with associated product counts', async ( { + page, + } ) => { + await page.goto( '/shop' ); + + const listItems = page + .getByLabel( 'Filter Options' ) + .getByRole( 'listitem' ); + + await expect( listItems ).toHaveCount( 5 ); + + for ( let i = 0; i < COLOR_ATTRIBUTES_WITH_COUNTS.length; i++ ) { + await expect( listItems.nth( i ) ).toHaveText( + COLOR_ATTRIBUTES_WITH_COUNTS[ i ] + ); + } + } ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/price-filter-frontend.block_theme.spec.ts similarity index 99% rename from plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.block_theme.spec.ts rename to plugins/woocommerce-blocks/tests/e2e/tests/product-filters/price-filter-frontend.block_theme.spec.ts index 796fd4af488..191c16f53f4 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/price-filter-frontend.block_theme.spec.ts @@ -12,7 +12,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( { }, } ); -test.describe( 'Product Filter: Price Filter Block', () => { +test.describe.skip( 'Product Filter: Price Filter Block', () => { test.describe( 'frontend', () => { test.beforeEach( async ( { requestUtils, templateCompiler } ) => { await requestUtils.activatePlugin( diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts index d065baf73dc..3d16f0c9e89 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts @@ -52,13 +52,7 @@ test.describe( 'Product Filters Template Part', () => { const block = editor.canvas.getByLabel( `Block: ${ blockData.name }` ); await expect( block ).toBeVisible(); - const searchTerms = [ - 'Status (Experimental)', - 'Price (Experimental)', - 'Rating (Experimental)', - 'Attribute (Experimental)', - 'Active (Experimental)', - ]; + const searchTerms = [ 'Color (Experimental)', 'Active (Experimental)' ]; for ( const filter of searchTerms ) { await editor.selectBlocks( blockData.selectors.editor.block ); @@ -78,13 +72,7 @@ test.describe( 'Product Filters Template Part', () => { await searchResult.click(); - let _locator = `[aria-label="Block: ${ filter }"]`; - - // We need to treat the attributes filter different because - // the variation of the block label depends on the product attribute. - if ( filter === 'Attribute (Experimental)' ) { - _locator = '.wp-block-woocommerce-product-filter-attribute'; - } + const _locator = `[aria-label="Block: ${ filter }"]`; await expect( editor.canvas.locator( _locator ) ).toHaveCount( 2 ); } diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts index 887dcf60e20..e6838d7c37d 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts @@ -68,53 +68,17 @@ test.describe( `${ blockData.name }`, () => { ); await expect( block ).toBeVisible(); - const activeHeading = block.getByText( 'Active', { exact: true } ); - const activeFilterBlock = block - .getByLabel( 'Block: Filter Options' ) - .and( - editor.canvas.locator( - '[data-type="woocommerce/product-filter-active"]' - ) - ); - await expect( activeHeading ).toBeVisible(); + const activeFilterBlock = block.getByLabel( + 'Block: Active (Experimental)' + ); await expect( activeFilterBlock ).toBeVisible(); - const priceHeading = block.getByText( 'Price', { - exact: true, - } ); - const priceFilterBlock = block - .getByLabel( 'Block: Filter Options' ) - .and( - editor.canvas.locator( - '[data-type="woocommerce/product-filter-price"]' - ) - ); - await expect( priceHeading ).toBeVisible(); - await expect( priceFilterBlock ).toBeVisible(); - - const statusHeading = block.getByText( 'Status', { - exact: true, - } ); - const statusFilterBlock = block - .getByLabel( 'Block: Filter Options' ) - .and( - editor.canvas.locator( - '[data-type="woocommerce/product-filter-stock-status"]' - ) - ); - await expect( statusHeading ).toBeVisible(); - await expect( statusFilterBlock ).toBeVisible(); - const colorHeading = block.getByText( 'Color', { exact: true, } ); - const colorFilterBlock = block - .getByLabel( 'Block: Filter Options' ) - .and( - editor.canvas.locator( - '[data-type="woocommerce/product-filter-attribute"]' - ) - ); + const colorFilterBlock = block.getByLabel( + 'Block: Color (Experimental)' + ); const expectedColorFilterOptions = [ 'Blue', 'Green', @@ -122,27 +86,11 @@ test.describe( `${ blockData.name }`, () => { 'Red', 'Yellow', ]; - const colorFilterOptions = ( - await colorFilterBlock.allInnerTexts() - )[ 0 ].split( '\n' ); await expect( colorHeading ).toBeVisible(); await expect( colorFilterBlock ).toBeVisible(); - expect( colorFilterOptions ).toEqual( - expect.arrayContaining( expectedColorFilterOptions ) - ); - - const ratingHeading = block.getByText( 'Rating', { - exact: true, - } ); - const ratingFilterBlock = block - .getByLabel( 'Block: Filter Options' ) - .and( - editor.canvas.locator( - '[data-type="woocommerce/product-filter-rating"]' - ) - ); - await expect( ratingHeading ).toBeVisible(); - await expect( ratingFilterBlock ).toBeVisible(); + for ( const option of expectedColorFilterOptions ) { + await expect( colorFilterBlock ).toContainText( option ); + } } ); test( 'should contain the correct inner block names in the list view', async ( { @@ -181,25 +129,10 @@ test.describe( `${ blockData.name }`, () => { ); await expect( productFilterActiveBlocksListItem ).toBeVisible(); - const productFilterPriceBlockListItem = listView.getByText( - 'Price (Experimental)' - ); - await expect( productFilterPriceBlockListItem ).toBeVisible(); - - const productFilterStatusBlockListItem = listView.getByText( - 'Status (Experimental)' - ); - await expect( productFilterStatusBlockListItem ).toBeVisible(); - const productFilterAttributeBlockListItem = listView.getByText( 'Color (Experimental)' // it must select the attribute with the highest product count ); await expect( productFilterAttributeBlockListItem ).toBeVisible(); - - const productFilterRatingBlockListItem = listView.getByText( - 'Rating (Experimental)' - ); - await expect( productFilterRatingBlockListItem ).toBeVisible(); } ); test( 'should display the correct inspector style controls', async ( { @@ -370,7 +303,7 @@ test.describe( `${ blockData.name }`, () => { ).toHaveCSS( 'align-items', 'center' ); } ); - test( 'Layout > Orientation: changing option should update the preview', async ( { + test.skip( 'Layout > Orientation: changing option should update the preview', async ( { editor, pageObject, } ) => { diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/rating-filter-frontend.block_theme.spec.ts similarity index 97% rename from plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.block_theme.spec.ts rename to plugins/woocommerce-blocks/tests/e2e/tests/product-filters/rating-filter-frontend.block_theme.spec.ts index 279fd391567..272aeb4966f 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/rating-filter-frontend.block_theme.spec.ts @@ -12,7 +12,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( { }, } ); -test.describe( 'Product Filter: Rating Filter Block', () => { +test.describe.skip( 'Product Filter: Rating Filter Block', () => { test.describe( 'frontend', () => { test.beforeEach( async ( { requestUtils, templateCompiler } ) => { await requestUtils.activatePlugin( diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/stock-status-frontend.block_theme.spec.ts similarity index 98% rename from plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.block_theme.spec.ts rename to plugins/woocommerce-blocks/tests/e2e/tests/product-filters/stock-status-frontend.block_theme.spec.ts index c465257eea2..4012ea324af 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/stock-status-frontend.block_theme.spec.ts @@ -12,7 +12,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( { }, } ); -test.describe( 'Product Filter: Stock Status Block', () => { +test.describe.skip( 'Product Filter: Stock Status Block', () => { test.describe( 'With default display style', () => { test.beforeEach( async ( { requestUtils, templateCompiler } ) => { await requestUtils.activatePlugin( diff --git a/plugins/woocommerce/changelog/try-new-improved-filter-blocks-structure b/plugins/woocommerce/changelog/try-new-improved-filter-blocks-structure new file mode 100644 index 00000000000..61232016454 --- /dev/null +++ b/plugins/woocommerce/changelog/try-new-improved-filter-blocks-structure @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: Filter blocks: new and improve filter blocks structure. + + diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php deleted file mode 100644 index 6cfc9a1465a..00000000000 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php +++ /dev/null @@ -1,157 +0,0 @@ -register_block_type() - * @param string $key Data to get, or default to everything. - * @return array|string|null - */ - protected function get_block_type_script( $key = null ) { - return null; - } - - /** - * Extra data passed through from server to client for block. - * - * @param array $attributes Any attributes that currently are available from the block. - * Note, this will be empty in the editor context when the block is - * not in the post content on editor load. - */ - protected function enqueue_data( array $attributes = [] ) { - global $pagenow; - parent::enqueue_data( $attributes ); - - $this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() ); - $this->asset_data_registry->add( 'isProductArchive', is_shop() || is_product_taxonomy() ); - $this->asset_data_registry->add( 'isSiteEditor', 'site-editor.php' === $pagenow ); - $this->asset_data_registry->add( 'isWidgetEditor', 'widgets.php' === $pagenow || 'customize.php' === $pagenow ); - } - - /** - * Check array for checked item. - * - * @param array $items Items to check. - */ - private function hasSelectedFilter( $items ) { - foreach ( $items as $key => $value ) { - if ( 'checked' === $key && true === $value ) { - return true; - } - - if ( is_array( $value ) && $this->hasSelectedFilter( $value ) ) { - return true; - } - } - - return false; - } - - /** - * Render the block. - * - * @param array $attributes Block attributes. - * @param string $content Block content. - * @param WP_Block $block Block instance. - * @return string Rendered block type output. - */ - protected function render( $attributes, $content, $block ) { - if ( is_admin() ) { - return $content; - } - - $tags = new WP_HTML_Tag_Processor( $content ); - $has_selected_filter = false; - - while ( $tags->next_tag( 'div' ) ) { - $items = $tags->get_attribute( 'data-wc-context' ) ? json_decode( $tags->get_attribute( 'data-wc-context' ), true ) : null; - - // For checked box filters. - if ( $items && array_key_exists( 'items', $items ) ) { - $has_selected_filter = $this->hasSelectedFilter( $items['items'] ); - break; - } - - // For price range filter. - if ( $items && array_key_exists( 'minPrice', $items ) ) { - if ( $items['minPrice'] > $items['minRange'] || $items['maxPrice'] < $items['maxRange'] ) { - $has_selected_filter = true; - break; - } - } - - // For dropdown filters. - if ( $items && array_key_exists( 'selectedItems', $items ) ) { - if ( count( $items['selectedItems'] ) > 0 ) { - $has_selected_filter = true; - break; - } - } - } - - $attributes_data = array( - 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), - 'data-wc-context' => wp_json_encode( array( 'hasSelectedFilter' => $has_selected_filter ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), - 'class' => 'wc-block-product-filters', - ); - - if ( ! isset( $block->context['queryId'] ) ) { - $attributes_data['data-wc-navigation-id'] = $this->generate_navigation_id( $block ); - } - - $tags = new WP_HTML_Tag_Processor( $content ); - - while ( $tags->next_tag( 'div' ) ) { - if ( 'yes' === $tags->get_attribute( 'data-has-filter' ) ) { - return sprintf( - '', - get_block_wrapper_attributes( $attributes_data ), - $content - ); - } - } - - return sprintf( - '', - get_block_wrapper_attributes( $attributes_data ), - ); - } - - /** - * Generate a unique navigation ID for the block. - * - * @param mixed $block - Block instance. - * @return string - Unique navigation ID. - */ - private function generate_navigation_id( $block ) { - return sprintf( - 'wc-product-filter-%s', - md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) ) - ); - } -} diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php index 679227f3a09..11469663107 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php @@ -57,7 +57,6 @@ final class ProductFilterActive extends AbstractBlock { array( 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), 'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), - 'data-has-filter' => empty( $active_filters ) ? 'no' : 'yes', ) ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php index d997799c772..0a5327f9ce0 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php @@ -1,10 +1,10 @@ get_default_product_attribute(); - $attributes['attributeId'] = $default_product_attribute->attribute_id; + protected function render( $block_attributes, $content, $block ) { + if ( empty( $block_attributes['attributeId'] ) ) { + $default_product_attribute = $this->get_default_product_attribute(); + $block_attributes['attributeId'] = $default_product_attribute->attribute_id; } // don't render if its admin, or ajax in progress. - if ( is_admin() || wp_doing_ajax() || empty( $attributes['attributeId'] ) ) { + if ( is_admin() || wp_doing_ajax() || empty( $block_attributes['attributeId'] ) ) { return ''; } - $product_attribute = wc_get_attribute( $attributes['attributeId'] ); - $attribute_counts = $this->get_attribute_counts( $block, $product_attribute->slug, $attributes['queryType'] ); + $product_attribute = wc_get_attribute( $block_attributes['attributeId'] ); + $attribute_counts = $this->get_attribute_counts( $block, $product_attribute->slug, $block_attributes['queryType'] ); if ( empty( $attribute_counts ) ) { return sprintf( @@ -181,7 +182,6 @@ final class ProductFilterAttribute extends AbstractBlock { get_block_wrapper_attributes( array( 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), - 'data-has-filter' => 'no', ) ), ); @@ -202,118 +202,56 @@ final class ProductFilterAttribute extends AbstractBlock { ); $attribute_options = array_map( - function ( $term ) use ( $attribute_counts, $selected_terms ) { + function ( $term ) use ( $block_attributes, $attribute_counts, $selected_terms ) { $term = (array) $term; $term['count'] = $attribute_counts[ $term['term_id'] ]; $term['selected'] = in_array( $term['slug'], $selected_terms, true ); - return $term; + return array( + 'label' => $block_attributes['showCounts'] ? sprintf( '%1$s (%2$d)', $term['name'], $term['count'] ) : $term['name'], + 'value' => $term['slug'], + 'selected' => $term['selected'], + 'rawData' => $term, + ); }, $attribute_terms ); $filtered_options = array_filter( $attribute_options, - function ( $option ) { - return $option['count'] > 0; + function ( $option ) use ( $block_attributes ) { + $hide_empty = $block_attributes['hideEmpty'] ?? true; + if ( $hide_empty ) { + return $option['rawData']['count'] > 0; + } + return true; } ); - $filter_content = 'dropdown' === $attributes['displayStyle'] ? - $this->render_attribute_dropdown( $filtered_options, $attributes ) : - $this->render_attribute_checkbox_list( $filtered_options, $attributes ); + $filter_context = array( + 'on_change' => "{$this->get_full_block_name()}::actions.updateProducts", + 'items' => $filtered_options, + ); + + foreach ( $block->parsed_block['innerBlocks'] as $inner_block ) { + $content .= ( new \WP_Block( $inner_block, array( 'filterData' => $filter_context ) ) )->render(); + } $context = array( - 'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ), - 'queryType' => $attributes['queryType'], - 'selectType' => 'multiple', + 'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ), + 'queryType' => $block_attributes['queryType'], + 'selectType' => 'multiple', + 'hasSelectedFilters' => count( $selected_terms ) > 0, ); return sprintf( - '
    %2$s%3$s
    ', + '
    %2$s
    ', get_block_wrapper_attributes( array( 'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), - 'data-has-filter' => 'yes', ) ), - $content, - $filter_content - ); - } - - /** - * Render the dropdown. - * - * @param array $options Data to render the dropdown. - * @param bool $attributes Block attributes. - */ - private function render_attribute_dropdown( $options, $attributes ) { - if ( empty( $options ) ) { - return ''; - } - - $list_items = array(); - $selected_items = array(); - - $product_attribute = wc_get_attribute( $attributes['attributeId'] ); - - foreach ( $options as $option ) { - $item = array( - 'label' => $attributes['showCounts'] ? sprintf( '%1$s (%2$d)', $option['name'], $option['count'] ) : $option['name'], - 'value' => $option['slug'], - ); - - $list_items[] = $item; - - if ( $option['selected'] ) { - $selected_items[] = $item; - } - } - - return Dropdown::render( - array( - 'items' => $list_items, - 'action' => "{$this->get_full_block_name()}::actions.navigate", - 'selected_items' => $selected_items, - 'select_type' => 'multiple', - // translators: %s is a product attribute name. - 'placeholder' => sprintf( __( 'Select %s', 'woocommerce' ), $product_attribute->name ), - ) - ); - } - - /** - * Render the attribute filter checkbox list. - * - * @param mixed $options Attribute filter options to render in the checkbox list. - * @param mixed $attributes Block attributes. - * @return string - */ - private function render_attribute_checkbox_list( $options, $attributes ) { - if ( empty( $options ) ) { - return ''; - } - - $show_counts = $attributes['showCounts'] ?? false; - - $list_options = array_map( - function ( $option ) use ( $show_counts ) { - return array( - 'id' => $option['slug'] . '-' . $option['term_id'], - 'checked' => $option['selected'], - 'label' => $show_counts ? sprintf( '%1$s (%2$d)', $option['name'], $option['count'] ) : $option['name'], - 'value' => $option['slug'], - ); - }, - $options - ); - - return CheckboxList::render( - array( - 'items' => $list_options, - 'on_change' => "{$this->get_full_block_name()}::actions.updateProducts", - ) + $content ); } @@ -380,7 +318,16 @@ final class ProductFilterAttribute extends AbstractBlock { $cached = get_transient( 'wc_block_product_filter_attribute_default_attribute' ); - if ( $cached ) { + if ( + $cached && + isset( $cached->attribute_id ) && + isset( $cached->attribute_name ) && + isset( $cached->attribute_label ) && + isset( $cached->attribute_type ) && + isset( $cached->attribute_orderby ) && + isset( $cached->attribute_public ) && + '0' !== $cached->attribute_id + ) { return $cached; } @@ -428,10 +375,9 @@ final class ProductFilterAttribute extends AbstractBlock { if ( $attribute_id ) { $default_attribute = $attributes[ $attribute_id ]; + set_transient( 'wc_block_product_filter_attribute_default_attribute', $default_attribute, DAY_IN_SECONDS ); } - set_transient( 'wc_block_product_filter_attribute_default_attribute', $default_attribute ); - return $default_attribute; } @@ -447,32 +393,26 @@ final class ProductFilterAttribute extends AbstractBlock { 'inserter' => false, 'content' => strtr( ' - - -
    - -

    {{attribute_label}}

    - + +
    + +
    + +

    {{attribute_label}}

    + - - -
    - -
    - Clear -
    - -
    - - + + +
    + +
    + +
    + + +
    - - - - + ', array( '{{attribute_id}}' => intval( $default_attribute->attribute_id ), @@ -482,4 +422,18 @@ final class ProductFilterAttribute extends AbstractBlock { ) ); } + + /** + * Skip default rendering routine for inner blocks. + * + * @param array $settings Array of determined settings for registering a block type. + * @param array $metadata Metadata provided for registering a block type. + * @return array + */ + public function add_block_type_metadata_settings( $settings, $metadata ) { + if ( ! empty( $metadata['name'] ) && "woocommerce/{$this->block_name}" === $metadata['name'] ) { + $settings['skip_inner_blocks'] = true; + } + return $settings; + } } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php new file mode 100644 index 00000000000..a4a299d9ff8 --- /dev/null +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php @@ -0,0 +1,155 @@ +context['filterData']; + $items = $context['items'] ?? array(); + $checkbox_list_context = array( 'items' => $items ); + $on_change = $context['on_change'] ?? ''; + $namespace = wp_json_encode( array( 'namespace' => 'woocommerce/product-filter-checkbox-list' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ); + + $classes = array( + 'has-option-element-border-color' => $this->get_color_attribute_value( 'optionElementBorder', $attributes ), + 'has-option-element-selected-color' => $this->get_color_attribute_value( 'optionElementSelected', $attributes ), + 'has-option-element-color' => $this->get_color_attribute_value( 'optionElement', $attributes ), + ); + $classes = array_filter( $classes ); + + $styles = array( + '--wc-product-filter-checkbox-list-option-element-border' => $this->get_color_attribute_value( 'optionElementBorder', $attributes ), + '--wc-product-filter-checkbox-list-option-element-selected' => $this->get_color_attribute_value( 'optionElementSelected', $attributes ), + '--wc-product-filter-checkbox-list-option-element' => $this->get_color_attribute_value( 'optionElement', $attributes ), + ); + $style = array_reduce( + array_keys( $styles ), + function ( $acc, $key ) use ( $styles ) { + if ( $styles[ $key ] ) { + return $acc . "{$key}: var( --wp--preset--color--{$styles[$key]} );"; + } + } + ); + + $checked_items = array_filter( + $items, + function ( $item ) { + return $item['selected']; + } + ); + $show_initially = $context['show_initially'] ?? 15; + $remaining_initial_unchecked = count( $checked_items ) > $show_initially ? count( $checked_items ) : $show_initially - count( $checked_items ); + $count = 0; + + $wrapper_attributes = array( + 'data-wc-interactive' => esc_attr( $namespace ), + 'data-wc-context' => wp_json_encode( $checkbox_list_context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), + 'class' => implode( ' ', array_keys( $classes ) ), + 'style' => esc_attr( $style ), + ); + + ob_start(); + ?> +
    > +
      + + +
    • = $remaining_initial_unchecked ) : + ?> + class="wc-block-product-filter-checkbox-list__item hidden" + data-wc-class--hidden="!context.showAll" + + + + + class="wc-block-product-filter-checkbox-list__item" + > + +
    • + +
    + $show_initially ) : ?> + + + + +
    + '!context.hasSelectedFilter', + 'data-wc-bind--hidden' => '!context.hasSelectedFilters', ) ); $p = new \WP_HTML_Tag_Processor( $content ); if ( $p->next_tag( array( 'class_name' => 'wp-block-button__link' ) ) ) { - $p->set_attribute( 'data-wc-on--click', 'actions.clear' ); + $p->set_attribute( 'data-wc-on--click', 'actions.clearFilters' ); $style = $p->get_attribute( 'style' ); $p->set_attribute( 'style', 'outline:none;' . $style ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php index 9c149ef6b3c..0137f32abf4 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php @@ -23,6 +23,23 @@ class ProductFilters extends AbstractBlock { return array( 'postId' ); } + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = array() ) { + global $pagenow; + parent::enqueue_data( $attributes ); + + $this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() ); + $this->asset_data_registry->add( 'isProductArchive', is_shop() || is_product_taxonomy() ); + $this->asset_data_registry->add( 'isSiteEditor', 'site-editor.php' === $pagenow ); + $this->asset_data_registry->add( 'isWidgetEditor', 'widgets.php' === $pagenow || 'customize.php' === $pagenow ); + } + /** * Return the dialog content. * @@ -116,12 +133,10 @@ class ProductFilters extends AbstractBlock { * @return string Rendered block type output. */ protected function render( $attributes, $content, $block ) { - $html = $content; - $p = new \WP_HTML_Tag_Processor( $html ); - - if ( $p->next_tag() ) { - $p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-filters' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); - $p->set_attribute( + $tags = new \WP_HTML_Tag_Processor( $content ); + if ( $tags->next_tag() ) { + $tags->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/' . $this->block_name ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); + $tags->set_attribute( 'data-wc-context', wp_json_encode( array( @@ -131,13 +146,29 @@ class ProductFilters extends AbstractBlock { JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); - $html = $p->get_updated_html(); + $tags->set_attribute( 'data-wc-navigation-id', $this->generate_navigation_id( $block ) ); + + if ( + 'always' === $attributes['overlay'] || + ( 'mobile' === $attributes['overlay'] && wp_is_mobile() ) + ) { + return $this->inject_dialog( $tags->get_updated_html(), $this->render_dialog() ); + } + + return $tags->get_updated_html(); } + } - $dialog_html = $this->render_dialog(); - - $html = $this->inject_dialog( $html, $dialog_html ); - - return $html; + /** + * Generate a unique navigation ID for the block. + * + * @param mixed $block - Block instance. + * @return string - Unique navigation ID. + */ + private function generate_navigation_id( $block ) { + return sprintf( + 'wc-product-filters-%s', + md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) ) + ); } } diff --git a/plugins/woocommerce/src/Blocks/BlockTypesController.php b/plugins/woocommerce/src/Blocks/BlockTypesController.php index 059152fd7fc..03e87ecbbad 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypesController.php +++ b/plugins/woocommerce/src/Blocks/BlockTypesController.php @@ -404,7 +404,6 @@ final class BlockTypesController { // Update plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md // when modifying this list. if ( Features::is_enabled( 'experimental-blocks' ) ) { - $block_types[] = 'ProductFilter'; $block_types[] = 'ProductFilters'; $block_types[] = 'ProductFiltersOverlay'; $block_types[] = 'ProductFiltersOverlayNavigation'; @@ -414,6 +413,8 @@ final class BlockTypesController { $block_types[] = 'ProductFilterRating'; $block_types[] = 'ProductFilterActive'; $block_types[] = 'ProductFilterClearButton'; + $block_types[] = 'ProductFilterCheckboxList'; + $block_types[] = 'ProductFilterChips'; $block_types[] = 'OrderConfirmation\CreateAccount'; } diff --git a/plugins/woocommerce/templates/parts/product-filters.html b/plugins/woocommerce/templates/parts/product-filters.html index 9e699ccd71d..a4e82272829 100644 --- a/plugins/woocommerce/templates/parts/product-filters.html +++ b/plugins/woocommerce/templates/parts/product-filters.html @@ -1,79 +1,28 @@ - -
    - +
    + +

    Filters

    + -
    -

    Filters

    - + - - -

    Active

    - + - - - - - -
    -

    Price

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

    Status

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

    Rating

    - - - - -
    - -
    - -
    - - - - - - -
    - -
    -
    + +
    + +
    + Apply +
    + +
    + +
    From 9b41815229b09ec0fa452bf632afc0b89bcfcfaa Mon Sep 17 00:00:00 2001 From: Barry Hughes <3594411+barryhughes@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:39:37 -0700 Subject: [PATCH 45/59] Produce CSV Import enhancements (cherry-pick PR#51344) (#51348) * Move AJAX callback to produt importer controller * Add better validation for file paths in CSV product import * Add utility function to create a no-index directory * Add CSV upload helper * Simplify product CSV import logic using helper * Update tests * Add changelog * Changelog. * Remove changelog file; entry added directly to root changelog.txt. --------- Co-authored-by: Jorge Torres --- changelog.txt | 1 + .../admin/class-wc-admin-importers.php | 106 +------ ...ass-wc-product-csv-importer-controller.php | 269 +++++++++++++----- plugins/woocommerce/src/Container.php | 2 + .../Admin/ImportExport/CSVUploadHelper.php | 175 ++++++++++++ .../ImportExportServiceProvider.php | 35 +++ .../src/Internal/Utilities/FilesystemUtil.php | 30 ++ .../tests/merchant/product-import-csv.spec.js | 3 +- 8 files changed, 448 insertions(+), 173 deletions(-) create mode 100644 plugins/woocommerce/src/Internal/Admin/ImportExport/CSVUploadHelper.php create mode 100644 plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/ImportExportServiceProvider.php diff --git a/changelog.txt b/changelog.txt index 19f15145abc..2ba165e2bdc 100644 --- a/changelog.txt +++ b/changelog.txt @@ -145,6 +145,7 @@ * Update - Update core profiler continue button container on extension screen [#50582](https://github.com/woocommerce/woocommerce/pull/50582) * Update - Update Store Alert actions to have unique keys. [#50424](https://github.com/woocommerce/woocommerce/pull/50424) * Update - Update WooCommercePayments task is_supported to use default suggestions [#50585](https://github.com/woocommerce/woocommerce/pull/50585) +* Update - Enhance CSV path and upload handling in product import [#51344](https://github.com/woocommerce/woocommerce/pull/51344) * Dev - Execute test env setup on host instead of wp-env container [#51021](https://github.com/woocommerce/woocommerce/pull/51021) * Dev - Added code docs with examples to the Analytics classes [#49425](https://github.com/woocommerce/woocommerce/pull/49425) * Dev - Add lost password e2e tests [#50611](https://github.com/woocommerce/woocommerce/pull/50611) diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-importers.php b/plugins/woocommerce/includes/admin/class-wc-admin-importers.php index 6dfec075b89..3c16a0b07d9 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-importers.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-importers.php @@ -215,114 +215,12 @@ class WC_Admin_Importers { * Ajax callback for importing one batch of products from a CSV. */ public function do_ajax_product_import() { - global $wpdb; - - check_ajax_referer( 'wc-product-import', 'security' ); - - if ( ! $this->import_allowed() || ! isset( $_POST['file'] ) ) { // PHPCS: input var ok. + if ( ! $this->import_allowed() ) { wp_send_json_error( array( 'message' => __( 'Insufficient privileges to import products.', 'woocommerce' ) ) ); } include_once WC_ABSPATH . 'includes/admin/importers/class-wc-product-csv-importer-controller.php'; - include_once WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php'; - - $file = wc_clean( wp_unslash( $_POST['file'] ) ); // PHPCS: input var ok. - $params = array( - 'delimiter' => ! empty( $_POST['delimiter'] ) ? wc_clean( wp_unslash( $_POST['delimiter'] ) ) : ',', // PHPCS: input var ok. - 'start_pos' => isset( $_POST['position'] ) ? absint( $_POST['position'] ) : 0, // PHPCS: input var ok. - 'mapping' => isset( $_POST['mapping'] ) ? (array) wc_clean( wp_unslash( $_POST['mapping'] ) ) : array(), // PHPCS: input var ok. - 'update_existing' => isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false, // PHPCS: input var ok. - 'character_encoding' => isset( $_POST['character_encoding'] ) ? wc_clean( wp_unslash( $_POST['character_encoding'] ) ) : '', - - /** - * Batch size for the product import process. - * - * @param int $size Batch size. - * - * @since 3.1.0 - */ - 'lines' => apply_filters( 'woocommerce_product_import_batch_size', 30 ), - 'parse' => true, - ); - - // Log failures. - if ( 0 !== $params['start_pos'] ) { - $error_log = array_filter( (array) get_user_option( 'product_import_error_log' ) ); - } else { - $error_log = array(); - } - - $importer = WC_Product_CSV_Importer_Controller::get_importer( $file, $params ); - $results = $importer->import(); - $percent_complete = $importer->get_percent_complete(); - $error_log = array_merge( $error_log, $results['failed'], $results['skipped'] ); - - update_user_option( get_current_user_id(), 'product_import_error_log', $error_log ); - - if ( 100 === $percent_complete ) { - // @codingStandardsIgnoreStart. - $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_original_id' ) ); - $wpdb->delete( $wpdb->posts, array( - 'post_type' => 'product', - 'post_status' => 'importing', - ) ); - $wpdb->delete( $wpdb->posts, array( - 'post_type' => 'product_variation', - 'post_status' => 'importing', - ) ); - // @codingStandardsIgnoreEnd. - - // Clean up orphaned data. - $wpdb->query( - " - DELETE {$wpdb->posts}.* FROM {$wpdb->posts} - LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->posts}.post_parent - WHERE wp.ID IS NULL AND {$wpdb->posts}.post_type = 'product_variation' - " - ); - $wpdb->query( - " - DELETE {$wpdb->postmeta}.* FROM {$wpdb->postmeta} - LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->postmeta}.post_id - WHERE wp.ID IS NULL - " - ); - // @codingStandardsIgnoreStart. - $wpdb->query( " - DELETE tr.* FROM {$wpdb->term_relationships} tr - LEFT JOIN {$wpdb->posts} wp ON wp.ID = tr.object_id - LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id - WHERE wp.ID IS NULL - AND tt.taxonomy IN ( '" . implode( "','", array_map( 'esc_sql', get_object_taxonomies( 'product' ) ) ) . "' ) - " ); - // @codingStandardsIgnoreEnd. - - // Send success. - wp_send_json_success( - array( - 'position' => 'done', - 'percentage' => 100, - 'url' => add_query_arg( array( '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ) ), admin_url( 'edit.php?post_type=product&page=product_importer&step=done' ) ), - 'imported' => is_countable( $results['imported'] ) ? count( $results['imported'] ) : 0, - 'imported_variations' => is_countable( $results['imported_variations'] ) ? count( $results['imported_variations'] ) : 0, - 'failed' => is_countable( $results['failed'] ) ? count( $results['failed'] ) : 0, - 'updated' => is_countable( $results['updated'] ) ? count( $results['updated'] ) : 0, - 'skipped' => is_countable( $results['skipped'] ) ? count( $results['skipped'] ) : 0, - ) - ); - } else { - wp_send_json_success( - array( - 'position' => $importer->get_file_position(), - 'percentage' => $percent_complete, - 'imported' => is_countable( $results['imported'] ) ? count( $results['imported'] ) : 0, - 'imported_variations' => is_countable( $results['imported_variations'] ) ? count( $results['imported_variations'] ) : 0, - 'failed' => is_countable( $results['failed'] ) ? count( $results['failed'] ) : 0, - 'updated' => is_countable( $results['updated'] ) ? count( $results['updated'] ) : 0, - 'skipped' => is_countable( $results['skipped'] ) ? count( $results['skipped'] ) : 0, - ) - ); - } + WC_Product_CSV_Importer_Controller::dispatch_ajax(); } /** diff --git a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php index e5fac1cecb9..cadf53a7bc5 100644 --- a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php +++ b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php @@ -103,6 +103,56 @@ class WC_Product_CSV_Importer_Controller { return wc_is_file_valid_csv( $file, $check_path ); } + /** + * Runs before controller actions to check that the file used during the import is valid. + * + * @since 9.3.0 + * + * @param string $path Path to test. + * + * @throws \Exception When file validation fails. + */ + protected static function check_file_path( string $path ): void { + $is_valid_file = false; + + if ( ! empty( $path ) ) { + $path = realpath( $path ); + $is_valid_file = false !== $path; + } + + // File must be readable. + $is_valid_file = $is_valid_file && is_readable( $path ); + + // Check that file is within an allowed location. + if ( $is_valid_file ) { + $in_valid_location = false; + $valid_locations = array(); + $valid_locations[] = ABSPATH; + + $upload_dir = wp_get_upload_dir(); + if ( false === $upload_dir['error'] ) { + $valid_locations[] = $upload_dir['basedir']; + } + + foreach ( $valid_locations as $valid_location ) { + if ( 0 === stripos( $path, trailingslashit( realpath( $valid_location ) ) ) ) { + $in_valid_location = true; + break; + } + } + + $is_valid_file = $in_valid_location; + } + + if ( ! $is_valid_file ) { + throw new \Exception( esc_html__( 'File path provided for import is invalid.', 'woocommerce' ) ); + } + + if ( ! self::is_file_valid_csv( $path ) ) { + throw new \Exception( esc_html__( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); + } + } + /** * Get all the valid filetypes for a CSV file. * @@ -263,17 +313,151 @@ class WC_Product_CSV_Importer_Controller { * Dispatch current step and show correct view. */ public function dispatch() { - // phpcs:ignore WordPress.Security.NonceVerification.Missing - if ( ! empty( $_POST['save_step'] ) && ! empty( $this->steps[ $this->step ]['handler'] ) ) { - call_user_func( $this->steps[ $this->step ]['handler'], $this ); + $output = ''; + + try { + // phpcs:ignore WordPress.Security.NonceVerification.Missing + if ( ! empty( $_POST['save_step'] ) && ! empty( $this->steps[ $this->step ]['handler'] ) ) { + if ( is_callable( $this->steps[ $this->step ]['handler'] ) ) { + call_user_func( $this->steps[ $this->step ]['handler'], $this ); + } + } + + ob_start(); + + if ( is_callable( $this->steps[ $this->step ]['view'] ) ) { + call_user_func( $this->steps[ $this->step ]['view'], $this ); + } + + $output = ob_get_clean(); + } catch ( \Exception $e ) { + $this->add_error( $e->getMessage() ); } + $this->output_header(); $this->output_steps(); $this->output_errors(); - call_user_func( $this->steps[ $this->step ]['view'], $this ); + echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output is HTML we've generated ourselves. $this->output_footer(); } + /** + * Processes AJAX requests related to a product CSV import. + * + * @since 9.3.0 + */ + public static function dispatch_ajax() { + global $wpdb; + + check_ajax_referer( 'wc-product-import', 'security' ); + + try { + $file = wc_clean( wp_unslash( $_POST['file'] ?? '' ) ); // PHPCS: input var ok. + self::check_file_path( $file ); + + $params = array( + 'delimiter' => ! empty( $_POST['delimiter'] ) ? wc_clean( wp_unslash( $_POST['delimiter'] ) ) : ',', // PHPCS: input var ok. + 'start_pos' => isset( $_POST['position'] ) ? absint( $_POST['position'] ) : 0, // PHPCS: input var ok. + 'mapping' => isset( $_POST['mapping'] ) ? (array) wc_clean( wp_unslash( $_POST['mapping'] ) ) : array(), // PHPCS: input var ok. + 'update_existing' => isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false, // PHPCS: input var ok. + 'character_encoding' => isset( $_POST['character_encoding'] ) ? wc_clean( wp_unslash( $_POST['character_encoding'] ) ) : '', + + /** + * Batch size for the product import process. + * + * @param int $size Batch size. + * + * @since 3.1.0 + */ + 'lines' => apply_filters( 'woocommerce_product_import_batch_size', 1 ), + 'parse' => true, + ); + + // Log failures. + if ( 0 !== $params['start_pos'] ) { + $error_log = array_filter( (array) get_user_option( 'product_import_error_log' ) ); + } else { + $error_log = array(); + } + + include_once WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php'; + + $importer = self::get_importer( $file, $params ); + $results = $importer->import(); + $percent_complete = $importer->get_percent_complete(); + $error_log = array_merge( $error_log, $results['failed'], $results['skipped'] ); + + update_user_option( get_current_user_id(), 'product_import_error_log', $error_log ); + + if ( 100 === $percent_complete ) { + // @codingStandardsIgnoreStart. + $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_original_id' ) ); + $wpdb->delete( $wpdb->posts, array( + 'post_type' => 'product', + 'post_status' => 'importing', + ) ); + $wpdb->delete( $wpdb->posts, array( + 'post_type' => 'product_variation', + 'post_status' => 'importing', + ) ); + // @codingStandardsIgnoreEnd. + + // Clean up orphaned data. + $wpdb->query( + " + DELETE {$wpdb->posts}.* FROM {$wpdb->posts} + LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->posts}.post_parent + WHERE wp.ID IS NULL AND {$wpdb->posts}.post_type = 'product_variation' + " + ); + $wpdb->query( + " + DELETE {$wpdb->postmeta}.* FROM {$wpdb->postmeta} + LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->postmeta}.post_id + WHERE wp.ID IS NULL + " + ); + // @codingStandardsIgnoreStart. + $wpdb->query( " + DELETE tr.* FROM {$wpdb->term_relationships} tr + LEFT JOIN {$wpdb->posts} wp ON wp.ID = tr.object_id + LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id + WHERE wp.ID IS NULL + AND tt.taxonomy IN ( '" . implode( "','", array_map( 'esc_sql', get_object_taxonomies( 'product' ) ) ) . "' ) + " ); + // @codingStandardsIgnoreEnd. + + // Send success. + wp_send_json_success( + array( + 'position' => 'done', + 'percentage' => 100, + 'url' => add_query_arg( array( '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ) ), admin_url( 'edit.php?post_type=product&page=product_importer&step=done' ) ), + 'imported' => is_countable( $results['imported'] ) ? count( $results['imported'] ) : 0, + 'imported_variations' => is_countable( $results['imported_variations'] ) ? count( $results['imported_variations'] ) : 0, + 'failed' => is_countable( $results['failed'] ) ? count( $results['failed'] ) : 0, + 'updated' => is_countable( $results['updated'] ) ? count( $results['updated'] ) : 0, + 'skipped' => is_countable( $results['skipped'] ) ? count( $results['skipped'] ) : 0, + ) + ); + } else { + wp_send_json_success( + array( + 'position' => $importer->get_file_position(), + 'percentage' => $percent_complete, + 'imported' => is_countable( $results['imported'] ) ? count( $results['imported'] ) : 0, + 'imported_variations' => is_countable( $results['imported_variations'] ) ? count( $results['imported_variations'] ) : 0, + 'failed' => is_countable( $results['failed'] ) ? count( $results['failed'] ) : 0, + 'updated' => is_countable( $results['updated'] ) ? count( $results['updated'] ) : 0, + 'skipped' => is_countable( $results['skipped'] ) ? count( $results['skipped'] ) : 0, + ) + ); + } + } catch ( \Exception $e ) { + wp_send_json_error( array( 'message' => $e->getMessage() ) ); + } + } + /** * Output information about the uploading process. */ @@ -314,60 +498,20 @@ class WC_Product_CSV_Importer_Controller { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce already verified in WC_Product_CSV_Importer_Controller::upload_form_handler() $file_url = isset( $_POST['file_url'] ) ? wc_clean( wp_unslash( $_POST['file_url'] ) ) : ''; - if ( empty( $file_url ) ) { - if ( ! isset( $_FILES['import'] ) ) { - return new WP_Error( 'woocommerce_product_csv_importer_upload_file_empty', __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.', 'woocommerce' ) ); + try { + if ( ! empty( $file_url ) ) { + $path = ABSPATH . $file_url; + self::check_file_path( $path ); + } else { + $csv_import_util = wc_get_container()->get( Automattic\WooCommerce\Internal\Admin\ImportExport\CSVUploadHelper::class ); + $upload = $csv_import_util->handle_csv_upload( 'product', 'import', self::get_valid_csv_filetypes() ); + $path = $upload['file']; } - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated - if ( ! self::is_file_valid_csv( wc_clean( wp_unslash( $_FILES['import']['name'] ) ), false ) ) { - return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); - } - - $overrides = array( - 'test_form' => false, - 'mimes' => self::get_valid_csv_filetypes(), - ); - $import = $_FILES['import']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash - $upload = wp_handle_upload( $import, $overrides ); - - if ( isset( $upload['error'] ) ) { - return new WP_Error( 'woocommerce_product_csv_importer_upload_error', $upload['error'] ); - } - - // Construct the object array. - $object = array( - 'post_title' => basename( $upload['file'] ), - 'post_content' => $upload['url'], - 'post_mime_type' => $upload['type'], - 'guid' => $upload['url'], - 'context' => 'import', - 'post_status' => 'private', - ); - - // Save the data. - $id = wp_insert_attachment( $object, $upload['file'] ); - - /* - * Schedule a cleanup for one day from now in case of failed - * import or missing wp_import_cleanup() call. - */ - wp_schedule_single_event( time() + DAY_IN_SECONDS, 'importer_scheduled_cleanup', array( $id ) ); - - return $upload['file']; - } elseif ( - ( 0 === stripos( realpath( ABSPATH . $file_url ), ABSPATH ) ) && - file_exists( ABSPATH . $file_url ) - ) { - if ( ! self::is_file_valid_csv( ABSPATH . $file_url ) ) { - return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); - } - - return ABSPATH . $file_url; + return $path; + } catch ( \Exception $e ) { + return new \WP_Error( 'woocommerce_product_csv_importer_upload_invalid_file', $e->getMessage() ); } - // phpcs:enable - - return new WP_Error( 'woocommerce_product_csv_importer_upload_invalid_file', __( 'Please upload or provide the link to a valid CSV file.', 'woocommerce' ) ); } /** @@ -375,6 +519,8 @@ class WC_Product_CSV_Importer_Controller { */ protected function mapping_form() { check_admin_referer( 'woocommerce-csv-importer' ); + self::check_file_path( $this->file ); + $args = array( 'lines' => 1, 'delimiter' => $this->delimiter, @@ -412,18 +558,7 @@ class WC_Product_CSV_Importer_Controller { // Displaying this page triggers Ajax action to run the import with a valid nonce, // therefore this page needs to be nonce protected as well. check_admin_referer( 'woocommerce-csv-importer' ); - - if ( ! self::is_file_valid_csv( $this->file ) ) { - $this->add_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); - $this->output_errors(); - return; - } - - if ( ! is_file( $this->file ) ) { - $this->add_error( __( 'The file does not exist, please try again.', 'woocommerce' ) ); - $this->output_errors(); - return; - } + self::check_file_path( $this->file ); if ( ! empty( $_POST['map_from'] ) && ! empty( $_POST['map_to'] ) ) { $mapping_from = wc_clean( wp_unslash( $_POST['map_from'] ) ); diff --git a/plugins/woocommerce/src/Container.php b/plugins/woocommerce/src/Container.php index 56d3b5b1e98..17ceac62783 100644 --- a/plugins/woocommerce/src/Container.php +++ b/plugins/woocommerce/src/Container.php @@ -32,6 +32,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\BatchP use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\LayoutTemplatesServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ComingSoonServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\StatsServiceProvider; +use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ImportExportServiceProvider; /** * PSR11 compliant dependency injection container for WooCommerce. @@ -83,6 +84,7 @@ final class Container { EnginesServiceProvider::class, ComingSoonServiceProvider::class, StatsServiceProvider::class, + ImportExportServiceProvider::class, ); /** diff --git a/plugins/woocommerce/src/Internal/Admin/ImportExport/CSVUploadHelper.php b/plugins/woocommerce/src/Internal/Admin/ImportExport/CSVUploadHelper.php new file mode 100644 index 00000000000..c1759bc5e93 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Admin/ImportExport/CSVUploadHelper.php @@ -0,0 +1,175 @@ +get_import_subdir_name(); + if ( $create ) { + FilesystemUtil::mkdir_p_not_indexable( $upload_dir ); + } + return $upload_dir; + } + + /** + * Handles a CSV file upload. + * + * @param string $import_type Type of upload or context. + * @param string $files_index $_FILES index that contains the file to upload. + * @param array|null $allowed_mime_types List of allowed MIME types. + * @return array { + * Details for the uploaded file. + * + * @type int $id Attachment ID. + * @type string $file Full path to uploaded file. + * } + * + * @throws \Exception In case of error. + */ + public function handle_csv_upload( string $import_type, string $files_index = 'import', ?array $allowed_mime_types = null ): array { + $import_type = sanitize_key( $import_type ); + if ( ! $import_type ) { + throw new \Exception( 'Import type is invalid.' ); + } + + if ( ! $allowed_mime_types ) { + $allowed_mime_types = array( + 'csv' => 'text/csv', + 'txt' => 'text/plain', + ); + } + + $file = $_FILES[ $files_index ] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing + if ( ! isset( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) { + throw new \Exception( esc_html__( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.', 'woocommerce' ) ); + } + + if ( ! function_exists( 'wp_import_handle_upload' ) ) { + require_once ABSPATH . 'wp-admin/includes/import.php'; + } + + // Make sure upload dir exists. + $this->get_import_dir(); + + // Add prefix. + $file['name'] = $import_type . '-' . $file['name']; + + $overrides_callback = function ( $overrides_ ) use ( $allowed_mime_types ) { + $overrides_['test_form'] = false; + $overrides_['test_type'] = true; + $overrides_['mimes'] = $allowed_mime_types; + return $overrides_; + }; + + self::add_filter( 'upload_dir', array( $this, 'override_upload_dir' ) ); + self::add_filter( 'wp_unique_filename', array( $this, 'override_unique_filename' ), 0, 2 ); + self::add_filter( 'wp_handle_upload_overrides', $overrides_callback, 999 ); + self::add_filter( 'wp_handle_upload_prefilter', array( $this, 'remove_txt_from_uploaded_file' ), 0 ); + + $orig_files_import = $_FILES['import'] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing + $_FILES['import'] = $file; // wp_import_handle_upload() expects the file to be in 'import'. + + $upload = wp_import_handle_upload(); + + remove_filter( 'upload_dir', array( $this, 'override_upload_dir' ) ); + remove_filter( 'wp_unique_filename', array( $this, 'override_unique_filename' ), 0 ); + remove_filter( 'wp_handle_upload_overrides', $overrides_callback, 999 ); + remove_filter( 'wp_handle_upload_prefilter', array( $this, 'remove_txt_from_uploaded_file' ), 0 ); + + if ( $orig_files_import ) { + $_FILES['import'] = $orig_files_import; + } else { + unset( $_FILES['import'] ); + } + + if ( ! empty( $upload['error'] ) ) { + throw new \Exception( esc_html( $upload['error'] ) ); + } + + if ( ! wc_is_file_valid_csv( $upload['file'], false ) ) { + wp_delete_attachment( $file['id'], true ); + throw new \Exception( esc_html__( 'Invalid file type for a CSV import.', 'woocommerce' ) ); + } + + return $upload; + } + + /** + * Hooked onto 'upload_dir' to override the default upload directory for a CSV upload. + * + * @param array $uploads WP upload dir details. + * @return array + */ + private function override_upload_dir( $uploads ): array { + $new_subdir = '/' . $this->get_import_subdir_name(); + + $uploads['path'] = $uploads['basedir'] . $new_subdir; + $uploads['url'] = $uploads['baseurl'] . $new_subdir; + $uploads['subdir'] = $new_subdir; + + return $uploads; + } + + /** + * Adds a random string to the name of an uploaded CSV file to make it less discoverable. Hooked onto 'wp_unique_filename'. + * + * @param string $filename File name. + * @param string $ext File extension. + * @return string + */ + private function override_unique_filename( string $filename, string $ext ): string { + $length = min( 10, 255 - strlen( $filename ) - 1 ); + if ( 1 < $length ) { + $suffix = strtolower( wp_generate_password( $length, false, false ) ); + $filename = substr( $filename, 0, strlen( $filename ) - strlen( $ext ) ) . '-' . $suffix . $ext; + } + + return $filename; + } + + /** + * `wp_import_handle_upload()` appends .txt to any file name. This function is hooked onto 'wp_handle_upload_prefilter' + * to remove those extra characters. + * + * @param array $file File details in the form of a $_FILES entry. + * @return array Modified file details. + */ + private function remove_txt_from_uploaded_file( array $file ): array { + $file['name'] = substr( $file['name'], 0, -4 ); + return $file; + } +} diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/ImportExportServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/ImportExportServiceProvider.php new file mode 100644 index 00000000000..58984a41fbf --- /dev/null +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/ImportExportServiceProvider.php @@ -0,0 +1,35 @@ +share( CSVUploadHelper::class ); + } +} diff --git a/plugins/woocommerce/src/Internal/Utilities/FilesystemUtil.php b/plugins/woocommerce/src/Internal/Utilities/FilesystemUtil.php index 4bac73265b3..3b91fcbc721 100644 --- a/plugins/woocommerce/src/Internal/Utilities/FilesystemUtil.php +++ b/plugins/woocommerce/src/Internal/Utilities/FilesystemUtil.php @@ -31,6 +31,36 @@ class FilesystemUtil { return $wp_filesystem; } + /** + * Recursively creates a directory (if it doesn't exist) and adds an empty index.html and a .htaccess to prevent + * directory listing. + * + * @since 9.3.0 + * + * @param string $path Directory to create. + * @throws \Exception In case of error. + */ + public static function mkdir_p_not_indexable( string $path ): void { + $wp_fs = self::get_wp_filesystem(); + + if ( $wp_fs->is_dir( $path ) ) { + return; + } + + if ( ! wp_mkdir_p( $path ) ) { + throw new \Exception( esc_html( sprintf( 'Could not create directory: %s.', wp_basename( $path ) ) ) ); + } + + $files = array( + '.htaccess' => 'deny from all', + 'index.html' => '', + ); + + foreach ( $files as $name => $content ) { + $wp_fs->put_contents( trailingslashit( $path ) . $name, $content ); + } + } + /** * Wrapper to initialize the WP filesystem with defined credentials if they are available. * diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-import-csv.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-import-csv.spec.js index 103d50fd8f6..507568812a0 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-import-csv.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/product-import-csv.spec.js @@ -89,8 +89,7 @@ const productCategories = [ ]; const productAttributes = [ 'Color', 'Size' ]; -const errorMessage = - 'Invalid file type. The importer supports CSV and TXT file formats.'; +const errorMessage = 'File is empty. Please upload something more substantial.'; test.describe.serial( 'Import Products from a CSV file', From 0eb4383918aea3fbd956495c70ac4c349679e89e Mon Sep 17 00:00:00 2001 From: jonathansadowski Date: Fri, 13 Sep 2024 14:20:19 -0500 Subject: [PATCH 46/59] Update nvm instructions in README.md (#51240) * Update nvm instructions in README.md * Update getting started instructions for nvm install * Update docs manifest --- README.md | 4 ++-- docs/docs-manifest.json | 4 ++-- .../getting-started/development-environment.md | 18 ++++++------------ 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a0dc78caa81..4ff249daa69 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ To get up and running within the WooCommerce Monorepo, you will need to make sur Once you've installed all of the prerequisites, you can run the following commands to get everything working. ```bash -# Ensure that you're using the correct version of Node -nvm use +# Ensure that correct version of Node is installed and being used +nvm install # Install the PHP and Composer dependencies for all of the plugins, packages, and tools pnpm install # Build all of the plugins, packages, and tools in the monorepo diff --git a/docs/docs-manifest.json b/docs/docs-manifest.json index f7579ca4cef..45cec2a5a3f 100644 --- a/docs/docs-manifest.json +++ b/docs/docs-manifest.json @@ -874,7 +874,7 @@ "menu_title": "Development environment setup", "tags": "tutorial, setup", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/development-environment.md", - "hash": "9e471d3f44a882fe61dcad9e5207d51b280a7220aae1bf6e4ae1fbdd68b7e3d4", + "hash": "bf5d77349ea64d1b8e19fe6b7472be35ed92406c5aafe677ce92363fb13f94d4", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/development-environment.md", "id": "9080572a3904349c44c565ca7e1bef1212c58757" }, @@ -1804,5 +1804,5 @@ "categories": [] } ], - "hash": "77c102c35a45b0681e7b70def9d639d764e4e5068121c2ef4dd23477c0f8784c" + "hash": "dfe48a2a48d383c1f4e5b5bb4258d369dd4e80ac8582462b16bbfedd879cb0bf" } \ No newline at end of file diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index 0701be60fc1..989587d4153 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -80,22 +80,16 @@ git clone https://github.com/woocommerce/woocommerce.git cd woocommerce ``` -### Activate the required Node version +### Install and Activate the required Node version ```sh -nvm use -Found '/path/to/woocommerce/.nvmrc' with version -Now using node v12.21.0 (npm v6.14.11) +nvm install +Found '/path/to/woocommerce/.nvmrc' with version +v20.17.0 is already installed. +Now using node v20.17.0 (npm v10.8.2) ``` -Note: if you don't have the required version of Node installed, NVM will alert you so you can install it: - -```sh -Found '/path/to/woocommerce/.nvmrc' with version -N/A: version "v12 -> N/A" is not yet installed. - -You need to run "nvm install v12" to install it before using it. -``` +Note: if you don't have the required version of Node installed, NVM will install it. ### Install dependencies From ae207242109f0b5e79fa82189a49500c8ff283d6 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Fri, 13 Sep 2024 18:52:12 -0300 Subject: [PATCH 47/59] Add inspector controls to Product Search block (#51247) * Add inspector controls to product search * Add changelog * Move inspector panel to Styles * Remove not used Fragment * improve code * Rename enum * Rename type ProductSearchBlockProps * Use PositionOptions constant * Change hook namespace --- .../js/blocks/product-search/constants.ts | 10 ++ .../assets/js/blocks/product-search/index.tsx | 38 ++++- .../product-search/inspector-controls.tsx | 156 ++++++++++++++++++ .../assets/js/blocks/product-search/types.ts | 23 +++ .../assets/js/blocks/product-search/utils.ts | 75 +++++++++ ...-47890_inspector_control_to_product_search | 4 + 6 files changed, 303 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-search/constants.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-search/inspector-controls.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-search/types.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-search/utils.ts create mode 100644 plugins/woocommerce/changelog/add-47890_inspector_control_to_product_search diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-search/constants.ts new file mode 100644 index 00000000000..ae42399cf3d --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/constants.ts @@ -0,0 +1,10 @@ +export const SEARCH_BLOCK_NAME = 'core/search'; +export const SEARCH_VARIATION_NAME = 'woocommerce/product-search'; + +export enum PositionOptions { + OUTSIDE = 'button-outside', + INSIDE = 'button-inside', + NO_BUTTON = 'no-button', + BUTTON_ONLY = 'button-only', + INPUT_AND_BUTTON = 'input-and-button', +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx index 9691e2cdf9a..89bd2ac4dac 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/index.tsx @@ -2,6 +2,7 @@ /** * External dependencies */ +import { addFilter } from '@wordpress/hooks'; import { store as blockEditorStore, Warning } from '@wordpress/block-editor'; import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; @@ -9,6 +10,7 @@ import { Icon, search } from '@wordpress/icons'; import { getSettingWithCoercion } from '@woocommerce/settings'; import { isBoolean } from '@woocommerce/types'; import { Button } from '@wordpress/components'; +import type { Block as BlockType } from '@wordpress/blocks'; import { // @ts-ignore waiting for @types/wordpress__blocks update registerBlockVariation, @@ -21,8 +23,10 @@ import { */ import './style.scss'; import './editor.scss'; +import { withProductSearchControls } from './inspector-controls'; import Block from './block'; import Edit from './edit'; +import { SEARCH_BLOCK_NAME, SEARCH_VARIATION_NAME } from './constants'; const isBlockVariationAvailable = getSettingWithCoercion( 'isBlockVariationAvailable', @@ -71,6 +75,7 @@ const PRODUCT_SEARCH_ATTRIBUTES = { query: { post_type: 'product', }, + namespace: SEARCH_VARIATION_NAME, }; const DeprecatedBlockEdit = ( { clientId }: { clientId: string } ) => { @@ -115,7 +120,7 @@ const DeprecatedBlockEdit = ( { clientId }: { clientId: string } ) => { ); }; -registerBlockType( 'woocommerce/product-search', { +registerBlockType( SEARCH_VARIATION_NAME, { title: __( 'Product Search', 'woocommerce' ), apiVersion: 3, icon: { @@ -146,7 +151,7 @@ registerBlockType( 'woocommerce/product-search', { isMatch: ( { idBase, instance } ) => idBase === 'woocommerce_product_search' && !! instance?.raw, transform: ( { instance } ) => - createBlock( 'woocommerce/product-search', { + createBlock( SEARCH_VARIATION_NAME, { label: instance.raw.title || PRODUCT_SEARCH_ATTRIBUTES.label, @@ -172,9 +177,31 @@ registerBlockType( 'woocommerce/product-search', { }, } ); +function registerProductSearchNamespace( props: BlockType, blockName: string ) { + if ( blockName === 'core/search' ) { + // Gracefully handle if settings.attributes is undefined. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore -- We need this because `attributes` is marked as `readonly` + props.attributes = { + ...props.attributes, + namespace: { + type: 'string', + }, + }; + } + + return props; +} + +addFilter( + 'blocks.registerBlockType', + SEARCH_VARIATION_NAME, + registerProductSearchNamespace +); + if ( isBlockVariationAvailable ) { registerBlockVariation( 'core/search', { - name: 'woocommerce/product-search', + name: SEARCH_VARIATION_NAME, title: __( 'Product Search', 'woocommerce' ), icon: { src: ( @@ -199,4 +226,9 @@ if ( isBlockVariationAvailable ) { ), attributes: PRODUCT_SEARCH_ATTRIBUTES, } ); + addFilter( + 'editor.BlockEdit', + SEARCH_BLOCK_NAME, + withProductSearchControls + ); } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/inspector-controls.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-search/inspector-controls.tsx new file mode 100644 index 00000000000..3212dd9e76e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/inspector-controls.tsx @@ -0,0 +1,156 @@ +/** + * External dependencies + */ +import { type ElementType, useEffect, useState } from '@wordpress/element'; +import { EditorBlock } from '@woocommerce/types'; +import { __ } from '@wordpress/i18n'; +import { InspectorControls } from '@wordpress/block-editor'; +import { + PanelBody, + RadioControl, + ToggleControl, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - Ignoring because `__experimentalToggleGroupControl` is not yet in the type definitions. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalToggleGroupControl as ToggleGroupControl, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - Ignoring because `__experimentalToggleGroupControlOption` is not yet in the type definitions. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalToggleGroupControlOption as ToggleGroupControlOption, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { + getInputAndButtonOption, + getSelectedRadioControlOption, + isInputAndButtonOption, + isWooSearchBlockVariation, +} from './utils'; +import { ButtonPositionProps, ProductSearchBlockProps } from './types'; +import { PositionOptions } from './constants'; + +const ProductSearchControls = ( props: ProductSearchBlockProps ) => { + const { attributes, setAttributes } = props; + const { buttonPosition, buttonUseIcon, showLabel } = attributes; + const [ initialPosition, setInitialPosition ] = + useState< ButtonPositionProps >( buttonPosition ); + + useEffect( () => { + if ( + isInputAndButtonOption( buttonPosition ) && + initialPosition !== buttonPosition + ) { + setInitialPosition( buttonPosition ); + } + }, [ buttonPosition ] ); + + return ( + + + & + PositionOptions.INPUT_AND_BUTTON + ) => { + if ( selected !== PositionOptions.INPUT_AND_BUTTON ) { + setAttributes( { + buttonPosition: selected, + } ); + } else { + const newButtonPosition = + getInputAndButtonOption( initialPosition ); + setAttributes( { + buttonPosition: newButtonPosition, + } ); + } + } } + /> + { buttonPosition !== PositionOptions.NO_BUTTON && ( + <> + { buttonPosition !== PositionOptions.BUTTON_ONLY && ( + { + setAttributes( { + buttonPosition: value, + } ); + } } + value={ getInputAndButtonOption( + buttonPosition + ) } + > + + + + ) } + { + setAttributes( { + buttonUseIcon: value, + } ); + } } + value={ buttonUseIcon } + > + + + + + ) } + + setAttributes( { + showLabel: showInputLabel, + } ) + } + /> + + + ); +}; + +export const withProductSearchControls = + < T extends EditorBlock< T > >( BlockEdit: ElementType ) => + ( props: ProductSearchBlockProps ) => { + return isWooSearchBlockVariation( props ) ? ( + <> + + + + ) : ( + + ); + }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-search/types.ts new file mode 100644 index 00000000000..290523ae727 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/types.ts @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import type { EditorBlock } from '@woocommerce/types'; + +export type ButtonPositionProps = + | 'button-outside' + | 'button-inside' + | 'no-button' + | 'button-only'; + +export interface SearchBlockAttributes { + buttonPosition: ButtonPositionProps; + buttonText?: string; + buttonUseIcon: boolean; + isSearchFieldHidden: boolean; + label?: string; + namespace?: string; + placeholder?: string; + showLabel: boolean; +} + +export type ProductSearchBlockProps = EditorBlock< SearchBlockAttributes >; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-search/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-search/utils.ts new file mode 100644 index 00000000000..5b70f82a27a --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-search/utils.ts @@ -0,0 +1,75 @@ +/** + * Internal dependencies + */ +import { + PositionOptions, + SEARCH_BLOCK_NAME, + SEARCH_VARIATION_NAME, +} from './constants'; +import { ButtonPositionProps, ProductSearchBlockProps } from './types'; + +/** + * Identifies if a block is a Search block variation from our conventions + * + * We are extending Gutenberg's core Search block with our variations, and + * also adding extra namespaced attributes. If those namespaced attributes + * are present, we can be fairly sure it is our own registered variation. + * + * @param {ProductSearchBlockProps} block - A WooCommerce block. + */ +export function isWooSearchBlockVariation( block: ProductSearchBlockProps ) { + return ( + block.name === SEARCH_BLOCK_NAME && + block.attributes?.namespace === SEARCH_VARIATION_NAME + ); +} + +/** + * Checks if the given button position is a valid option for input and button placement. + * + * The function verifies if the provided `buttonPosition` matches one of the predefined + * values for placing a button either inside or outside an input field. + * + * @param {string} buttonPosition - The position of the button to check. + */ +export function isInputAndButtonOption( buttonPosition: string ): boolean { + return ( + buttonPosition === 'button-outside' || + buttonPosition === 'button-inside' + ); +} + +/** + * Returns the option for the selected button position + * + * Based on the provided `buttonPosition`, the function returns a predefined option + * if the position is valid for input and button placement. If the position is not + * one of the predefined options, it returns the original `buttonPosition`. + * + * @param {string} buttonPosition - The position of the button to evaluate. + */ +export function getSelectedRadioControlOption( + buttonPosition: string +): string { + if ( isInputAndButtonOption( buttonPosition ) ) { + return PositionOptions.INPUT_AND_BUTTON; + } + return buttonPosition; +} + +/** + * Returns the appropriate option for input and button placement based on the given value + * + * This function checks if the provided `value` is a valid option for placing a button either + * inside or outside an input field. If the `value` is valid, it is returned as is. If the `value` + * is not valid, the function returns a default option. + * + * @param {ButtonPositionProps} value - The position of the button to evaluate. + */ +export function getInputAndButtonOption( value: ButtonPositionProps ) { + if ( isInputAndButtonOption( value ) ) { + return value; + } + // The default value is 'inside' for input and button. + return PositionOptions.OUTSIDE; +} diff --git a/plugins/woocommerce/changelog/add-47890_inspector_control_to_product_search b/plugins/woocommerce/changelog/add-47890_inspector_control_to_product_search new file mode 100644 index 00000000000..351aa293cfd --- /dev/null +++ b/plugins/woocommerce/changelog/add-47890_inspector_control_to_product_search @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add inspector controls to Product Search block #51247 From 63d93d73551a1cc908e5385818796c9d042eca2f Mon Sep 17 00:00:00 2001 From: Narendra Sishodiya <32844880+narenin@users.noreply.github.com> Date: Sun, 15 Sep 2024 20:51:55 +0530 Subject: [PATCH 48/59] Implemented PHPCS Fixes in OrdersTableQuery.php and ProductQuery.php (#51281) * Implemented PHPCS Fixes in OrdersTableQuery.php and ProductQuery.php * Reverted gmdate() to date() * Add changelog * Fixed linting issue in Update OrdersTableQuery.php --------- Co-authored-by: Alex Florisca --- .../woocommerce/changelog/51281-phpcs-fixes | 4 ++ .../DataStores/Orders/OrdersTableQuery.php | 17 ++--- .../src/StoreApi/Utilities/ProductQuery.php | 72 +++++++++---------- 3 files changed, 49 insertions(+), 44 deletions(-) create mode 100644 plugins/woocommerce/changelog/51281-phpcs-fixes diff --git a/plugins/woocommerce/changelog/51281-phpcs-fixes b/plugins/woocommerce/changelog/51281-phpcs-fixes new file mode 100644 index 00000000000..9db48f6d016 --- /dev/null +++ b/plugins/woocommerce/changelog/51281-phpcs-fixes @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Fix PHPCS warnings in OrdersTableQuery.php and ProductQuery.php diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableQuery.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableQuery.php index e20a5aa2287..f448fe83551 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableQuery.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableQuery.php @@ -336,11 +336,11 @@ class OrdersTableQuery { * Generates a `WP_Date_Query` compatible query from a given date. * YYYY-MM-DD queries have 'day' precision for backwards compatibility. * - * @param mixed $date The date. Can be a {@see \WC_DateTime}, a timestamp or a string. + * @param mixed $date The date. Can be a {@see \WC_DateTime}, a timestamp or a string. * @return array An array with keys 'year', 'month', 'day' and possibly 'hour', 'minute' and 'second'. */ private function date_to_date_query_arg( $date ): array { - $result = array( + $result = array( 'year' => '', 'month' => '', 'day' => '', @@ -373,10 +373,12 @@ class OrdersTableQuery { /** * Returns UTC-based date query arguments for a combination of local time dates and a date shorthand operator. * - * @param array $dates_raw Array of dates (in local time) to use in combination with the operator. + * @param array $dates_raw Array of dates (in local time) to use in combination with the operator. * @param string $operator One of the operators supported by date queries (<, <=, =, ..., >, >=). * @return array Partial date query arg with relevant dates now UTC-based. * + * @throws \Exception If an invalid date shorthand operator is specified. + * * @since 8.2.0 */ private function local_time_to_gmt_date_query( $dates_raw, $operator ) { @@ -387,7 +389,7 @@ class OrdersTableQuery { $raw_date = is_numeric( $raw_date ) ? $raw_date : strtotime( get_gmt_from_date( date( 'Y-m-d', strtotime( $raw_date ) ) ) ); } - $date1 = end( $dates_raw ); + $date1 = end( $dates_raw ); switch ( $operator ) { case '>': @@ -410,9 +412,9 @@ class OrdersTableQuery { 'inclusive' => true, ), array( - 'before' => $this->date_to_date_query_arg( $date1 + DAY_IN_SECONDS ), - 'inclusive' => false, - ) + 'before' => $this->date_to_date_query_arg( $date1 + DAY_IN_SECONDS ), + 'inclusive' => false, + ), ); break; case '<=': @@ -474,7 +476,6 @@ class OrdersTableQuery { foreach ( $date_keys as $date_key ) { $is_local = in_array( $date_key, $local_date_keys, true ); $date_value = $this->args[ $date_key ]; - $operator = '='; $dates_raw = array(); $dates = array(); diff --git a/plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php b/plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php index 25f915d2862..3fce3214ab0 100644 --- a/plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php +++ b/plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php @@ -16,7 +16,7 @@ class ProductQuery { * @return array */ public function prepare_objects_query( $request ) { - $args = [ + $args = array( 'offset' => $request['offset'], 'order' => $request['order'], 'orderby' => $request['orderby'], @@ -31,17 +31,17 @@ class ProductQuery { 'fields' => 'ids', 'ignore_sticky_posts' => true, 'post_status' => 'publish', - 'date_query' => [], + 'date_query' => array(), 'post_type' => 'product', - ]; + ); // If searching for a specific SKU or slug, allow any post type. if ( ! empty( $request['sku'] ) || ! empty( $request['slug'] ) ) { - $args['post_type'] = [ 'product', 'product_variation' ]; + $args['post_type'] = array( 'product', 'product_variation' ); } // Taxonomy query to filter products by type, category, tag, shipping class, and attribute. - $tax_query = []; + $tax_query = array(); // Filter product type by slug. if ( ! empty( $request['type'] ) ) { @@ -49,11 +49,11 @@ class ProductQuery { $args['post_type'] = 'product_variation'; } else { $args['post_type'] = 'product'; - $tax_query[] = [ + $tax_query[] = array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $request['type'], - ]; + ); } } @@ -77,12 +77,12 @@ class ProductQuery { } // Set custom args to handle later during clauses. - $custom_keys = [ + $custom_keys = array( 'sku', 'min_price', 'max_price', 'stock_status', - ]; + ); foreach ( $custom_keys as $key ) { if ( ! empty( $request[ $key ] ) ) { @@ -90,11 +90,11 @@ class ProductQuery { } } - $operator_mapping = [ + $operator_mapping = array( 'in' => 'IN', 'not_in' => 'NOT IN', 'and' => 'AND', - ]; + ); // Gets all registered product taxonomies and prefixes them with `tax_`. // This is needed to avoid situations where a user registers a new product taxonomy with the same name as default field. @@ -107,10 +107,10 @@ class ProductQuery { ); // Map between taxonomy name and arg key. - $default_taxonomies = [ + $default_taxonomies = array( 'product_cat' => 'category', 'product_tag' => 'tag', - ]; + ); $taxonomies = array_merge( $all_product_taxonomies, $default_taxonomies ); @@ -118,18 +118,18 @@ class ProductQuery { foreach ( $taxonomies as $taxonomy => $key ) { if ( ! empty( $request[ $key ] ) ) { $operator = $request->get_param( $key . '_operator' ) && isset( $operator_mapping[ $request->get_param( $key . '_operator' ) ] ) ? $operator_mapping[ $request->get_param( $key . '_operator' ) ] : 'IN'; - $tax_query[] = [ + $tax_query[] = array( 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $request[ $key ], 'operator' => $operator, - ]; + ); } } // Filter by attributes. if ( ! empty( $request['attributes'] ) ) { - $att_queries = []; + $att_queries = array(); foreach ( $request['attributes'] as $attribute ) { if ( empty( $attribute['term_id'] ) && empty( $attribute['slug'] ) ) { @@ -137,22 +137,22 @@ class ProductQuery { } if ( in_array( $attribute['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { $operator = isset( $attribute['operator'], $operator_mapping[ $attribute['operator'] ] ) ? $operator_mapping[ $attribute['operator'] ] : 'IN'; - $att_queries[] = [ + $att_queries[] = array( 'taxonomy' => $attribute['attribute'], 'field' => ! empty( $attribute['term_id'] ) ? 'term_id' : 'slug', 'terms' => ! empty( $attribute['term_id'] ) ? $attribute['term_id'] : $attribute['slug'], 'operator' => $operator, - ]; + ); } } if ( 1 < count( $att_queries ) ) { // Add relation arg when using multiple attributes. $relation = $request->get_param( 'attribute_relation' ) && isset( $operator_mapping[ $request->get_param( 'attribute_relation' ) ] ) ? $operator_mapping[ $request->get_param( 'attribute_relation' ) ] : 'IN'; - $tax_query[] = [ + $tax_query[] = array( 'relation' => $relation, $att_queries, - ]; + ); } else { $tax_query = array_merge( $tax_query, $att_queries ); } @@ -176,12 +176,12 @@ class ProductQuery { // Filter featured. if ( is_bool( $request['featured'] ) ) { - $args['tax_query'][] = [ + $args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'name', 'terms' => 'featured', 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', - ]; + ); } // Filter by on sale products. @@ -190,7 +190,7 @@ class ProductQuery { $on_sale_ids = wc_get_product_ids_on_sale(); // Use 0 when there's no on sale products to avoid return all products. - $on_sale_ids = empty( $on_sale_ids ) ? [ 0 ] : $on_sale_ids; + $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; $args[ $on_sale_key ] += $on_sale_ids; } @@ -203,25 +203,25 @@ class ProductQuery { $exclude_from_catalog = 'search' === $catalog_visibility ? '' : 'exclude-from-catalog'; $exclude_from_search = 'catalog' === $catalog_visibility ? '' : 'exclude-from-search'; - $args['tax_query'][] = [ + $args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'name', - 'terms' => [ $exclude_from_catalog, $exclude_from_search ], + 'terms' => array( $exclude_from_catalog, $exclude_from_search ), 'operator' => 'hidden' === $catalog_visibility ? 'AND' : 'NOT IN', 'rating_filter' => true, - ]; + ); } if ( $rating ) { - $rating_terms = []; + $rating_terms = array(); foreach ( $rating as $value ) { $rating_terms[] = 'rated-' . $value; } - $args['tax_query'][] = [ + $args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'name', 'terms' => $rating_terms, - ]; + ); } $orderby = $request->get_param( 'orderby' ); @@ -283,7 +283,7 @@ class ProductQuery { public function get_results( $request ) { $query_args = $this->prepare_objects_query( $request ); - add_filter( 'posts_clauses', [ $this, 'add_query_clauses' ], 10, 2 ); + add_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10, 2 ); $query = new \WP_Query(); $results = $query->query( $query_args ); @@ -297,13 +297,13 @@ class ProductQuery { $total_posts = $count_query->found_posts; } - remove_filter( 'posts_clauses', [ $this, 'add_query_clauses' ], 10 ); + remove_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10 ); - return [ + return array( 'results' => $results, 'total' => (int) $total_posts, 'pages' => $query->query_vars['posts_per_page'] > 0 ? (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ) : 1, - ]; + ); } /** @@ -315,11 +315,11 @@ class ProductQuery { public function get_objects( $request ) { $results = $this->get_results( $request ); - return [ + return array( 'objects' => array_map( 'wc_get_product', $results['results'] ), 'total' => $results['total'], 'pages' => $results['pages'], - ]; + ); } /** @@ -442,7 +442,7 @@ class ProductQuery { return ''; } - $or_queries = []; + $or_queries = array(); // We need to adjust the filter for each possible tax class and combine the queries into one. foreach ( $product_tax_classes as $tax_class ) { From d397e39c751611151d052fcd8698367e18af1a41 Mon Sep 17 00:00:00 2001 From: Vladimir Reznichenko Date: Mon, 16 Sep 2024 09:05:29 +0200 Subject: [PATCH 49/59] [dev] Update code owners files: initial ownership for Flux team (#51260) --- CODEOWNERS | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 614fa2f46be..532335b71de 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1,14 @@ -/.github/ @woocommerce/atlas +# Monorepo CI and package managers manifests. +/.github/ @woocommerce/flux +**/composer.json @woocommerce/flux +**/package.json @woocommerce/flux + +# Monorepo tooling folders. +/bin/ @woocommerce/flux +/tools/ @woocommerce/flux +/packages/js/eslint-plugin/ @woocommerce/flux +/packages/js/dependency-extraction-webpack-plugin/ @woocommerce/flux + +# Files in root of repository +/.* @woocommerce/flux +/*.* @woocommerce/flux From 79b8a4015737ea4c7fe32ca2023216e44ed14849 Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:11:15 +0100 Subject: [PATCH 50/59] Update nighty build workflow (#51328) --- .github/workflows/nightly-builds.yml | 57 ++++++++++++++++------------ 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/.github/workflows/nightly-builds.yml b/.github/workflows/nightly-builds.yml index 6a3e22fa123..71ce84bf288 100644 --- a/.github/workflows/nightly-builds.yml +++ b/.github/workflows/nightly-builds.yml @@ -4,23 +4,25 @@ on: - cron: '0 0 * * *' # Run at 12 AM UTC. workflow_dispatch: -permissions: {} +env: + SOURCE_REF: trunk + TARGET_REF: nightly + RELEASE_ID: 25945111 + +permissions: { } jobs: build: if: github.repository_owner == 'woocommerce' name: Nightly builds - strategy: - fail-fast: false - matrix: - build: [trunk] + runs-on: ubuntu-20.04 permissions: contents: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: - ref: ${{ matrix.build }} + ref: ${{ env.SOURCE_REF }} - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo @@ -31,26 +33,31 @@ jobs: working-directory: plugins/woocommerce run: bash bin/build-zip.sh - - name: Deploy nightly build - uses: WebFreak001/deploy-nightly@v1.1.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload nightly build + uses: WebFreak001/deploy-nightly@46ecbabd7fad70d3e7d2c97fe8cd54e7a52e215b #v3.2.0 with: - upload_url: https://uploads.github.com/repos/${{ github.repository }}/releases/25945111/assets{?name,label} - release_id: 25945111 + token: ${{ secrets.GITHUB_TOKEN }} + upload_url: https://uploads.github.com/repos/${{ github.repository }}/releases/${{ env.RELEASE_ID }}/assets{?name,label} + release_id: ${{ env.RELEASE_ID }} asset_path: plugins/woocommerce/woocommerce.zip - asset_name: woocommerce-${{ matrix.build }}-nightly.zip + asset_name: woocommerce-${{ env.SOURCE_REF }}-nightly.zip asset_content_type: application/zip max_releases: 1 - update: - name: Update nightly tag commit ref - runs-on: ubuntu-20.04 - permissions: - contents: write - steps: - - name: Update nightly tag - uses: richardsimko/github-tag-action@v1.0.5 + + - name: Update nightly tag commit ref + uses: actions/github-script@v7 with: - tag_name: nightly - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const sourceRef = process.env.SOURCE_REF; + const targetRef = process.env.TARGET_REF; + const branchData = await github.rest.repos.getBranch({ + ...context.repo, + branch: sourceRef, + }); + + await github.rest.git.updateRef({ + ...context.repo, + ref: `tags/${ targetRef }`, + sha: branchData.data.commit.sha, + }); From 77a17e48b77dc84193867c570b2db22b8e4340da Mon Sep 17 00:00:00 2001 From: Seghir Nadir Date: Mon, 16 Sep 2024 11:22:38 +0200 Subject: [PATCH 51/59] update Mongolia postcode to be 5 digits instead of 6 in Checkout block (#50279) * update Mongolia postcode to be 5 digits instead of 6 * Add changefile(s) from automation for the following project(s): woocommerce-blocks * update validation package and expand mongolia checks * Add changefile(s) from automation for the following project(s): woocommerce-blocks * update lock * fix lock again --------- Co-authored-by: github-actions --- plugins/woocommerce-blocks/package.json | 2 +- .../packages/checkout/utils/validation/is-postcode.ts | 1 + .../changelog/50279-fix-mongolia-postcode-5 | 4 ++++ pnpm-lock.yaml | 10 +++++----- 4 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 plugins/woocommerce/changelog/50279-fix-mongolia-postcode-5 diff --git a/plugins/woocommerce-blocks/package.json b/plugins/woocommerce-blocks/package.json index cd63f8a5ca4..45f4c6d706b 100644 --- a/plugins/woocommerce-blocks/package.json +++ b/plugins/woocommerce-blocks/package.json @@ -288,7 +288,7 @@ "fast-deep-equal": "^3.1.3", "fast-sort": "^3.4.0", "html-react-parser": "3.0.4", - "postcode-validator": "3.8.15", + "postcode-validator": "3.9.2", "preact": "^10.19.3", "prop-types": "^15.8.1", "react-number-format": "4.9.3", diff --git a/plugins/woocommerce-blocks/packages/checkout/utils/validation/is-postcode.ts b/plugins/woocommerce-blocks/packages/checkout/utils/validation/is-postcode.ts index ec2ca97a1c3..2ab21d238b9 100644 --- a/plugins/woocommerce-blocks/packages/checkout/utils/validation/is-postcode.ts +++ b/plugins/woocommerce-blocks/packages/checkout/utils/validation/is-postcode.ts @@ -13,6 +13,7 @@ const CUSTOM_REGEXES = new Map< string, RegExp >( [ [ 'JP', /^([0-9]{3})([-]?)([0-9]{4})$/ ], [ 'KH', /^[0-9]{6}$/ ], // Cambodia (6-digit postal code). [ 'LI', /^(94[8-9][0-9])$/ ], + [ 'MN', /^[0-9]{5}(-[0-9]{4})?$/ ], // Mongolia (5-digit postal code or 5-digit postal code followed by a hyphen and 4-digit postal code). [ 'NI', /^[1-9]{1}[0-9]{4}$/ ], // Nicaragua (5-digit postal code) [ 'NL', /^([1-9][0-9]{3})(\s?)(?!SA|SD|SS)[A-Z]{2}$/i ], [ 'SI', /^([1-9][0-9]{3})$/ ], diff --git a/plugins/woocommerce/changelog/50279-fix-mongolia-postcode-5 b/plugins/woocommerce/changelog/50279-fix-mongolia-postcode-5 new file mode 100644 index 00000000000..b6f3ac11940 --- /dev/null +++ b/plugins/woocommerce/changelog/50279-fix-mongolia-postcode-5 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Adjust Mongolia postcode validation to be 5 digits or 5 digits followed by 4 digits. \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a972c8f247..296a64d656a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4025,8 +4025,8 @@ importers: specifier: 3.0.4 version: 3.0.4(react@18.3.1) postcode-validator: - specifier: 3.8.15 - version: 3.8.15 + specifier: 3.9.2 + version: 3.9.2 preact: specifier: ^10.19.3 version: 10.19.3 @@ -20623,8 +20623,8 @@ packages: resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} engines: {node: '>=0.10.0'} - postcode-validator@3.8.15: - resolution: {integrity: sha512-B2oeZ4E9D7JBHk0GEo0lv9aqHGd6vv+VsWoTG6Jt0tMtc5RUzSXOSQixBZgAAn4A/Dbajp8GbwzMtUkkyYw2Ig==} + postcode-validator@3.9.2: + resolution: {integrity: sha512-C+oaXif+z+mAN1EWDZG/EM2dnrUxRQR0gpMq8VeLSZwHuT3Bqo4hR3+k7OaEevPl0VK/7W27JwEQb24CbdtzuQ==} postcss-calc@7.0.5: resolution: {integrity: sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==} @@ -56933,7 +56933,7 @@ snapshots: posix-character-classes@0.1.1: {} - postcode-validator@3.8.15: {} + postcode-validator@3.9.2: {} postcss-calc@7.0.5: dependencies: From cebdcc61d0d0cc11a2fe6c51c3ed03516dfe23e7 Mon Sep 17 00:00:00 2001 From: Ivan Stojadinov Date: Mon, 16 Sep 2024 13:56:44 +0200 Subject: [PATCH 52/59] [e2e] External - Expand WPCOM suite, part 1 (#51303) * Expand WPCOM suite * Skip core-profiler.spec.js on WPCOM - no "Coming soon" * Skip `Analytics-related tests` on WPCOM - different sums * Skip `Marketing Overview page have relevant content` - no content on WPCOM * Payment setup task - make Save button more unique * Include more tests in playwright.config.js * Skip `Store owner can skip the core profiler` * Add changefile(s) from automation for the following project(s): woocommerce * Make "Get paid" more unique --------- Co-authored-by: github-actions --- .../51303-e2e-external-expand-wpcom-suite | 4 ++ .../envs/default-wpcom/playwright.config.js | 4 ++ .../activate-and-setup/core-profiler.spec.js | 4 +- .../admin-analytics/analytics-data.spec.js | 9 ++- .../tests/admin-marketing/overview.spec.js | 68 ++++++++++--------- .../e2e-pw/tests/admin-tasks/payment.spec.js | 4 +- 6 files changed, 56 insertions(+), 37 deletions(-) create mode 100644 plugins/woocommerce/changelog/51303-e2e-external-expand-wpcom-suite diff --git a/plugins/woocommerce/changelog/51303-e2e-external-expand-wpcom-suite b/plugins/woocommerce/changelog/51303-e2e-external-expand-wpcom-suite new file mode 100644 index 00000000000..975d531637f --- /dev/null +++ b/plugins/woocommerce/changelog/51303-e2e-external-expand-wpcom-suite @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Expand the e2e suite we're running on WPCOM. \ No newline at end of file diff --git a/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js index 08977a04e71..276458fa570 100644 --- a/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js +++ b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js @@ -9,6 +9,10 @@ config = { use: { ...devices[ 'Desktop Chrome' ] }, testMatch: [ '**/basic.spec.js', + '**/activate-and-setup/**/*.spec.js', + '**/admin-analytics/**/*.spec.js', + '**/admin-marketing/**/*.spec.js', + '**/admin-tasks/**/*.spec.js', '**/shopper/**/*.spec.js', '**/api-tests/**/*.test.js', ], diff --git a/plugins/woocommerce/tests/e2e-pw/tests/activate-and-setup/core-profiler.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/activate-and-setup/core-profiler.spec.js index f0cf8016e40..a4679791f03 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/activate-and-setup/core-profiler.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/activate-and-setup/core-profiler.spec.js @@ -3,7 +3,7 @@ const { setOption } = require( '../../utils/options' ); test.describe( 'Store owner can complete the core profiler', - { tag: '@skip-on-default-pressable' }, + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, () => { test.use( { storageState: process.env.ADMINSTATE } ); @@ -450,7 +450,7 @@ test.describe( test.describe( 'Store owner can skip the core profiler', - { tag: '@skip-on-default-pressable' }, + { tag: [ '@skip-on-default-pressable', '@skip-on-default-wpcom' ] }, () => { test.use( { storageState: process.env.ADMINSTATE } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/admin-analytics/analytics-data.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/admin-analytics/analytics-data.spec.js index 6cfd1d35fd2..9d8ebd604a9 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/admin-analytics/analytics-data.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/admin-analytics/analytics-data.spec.js @@ -25,7 +25,14 @@ const test = baseTest.extend( { test.describe( 'Analytics-related tests', - { tag: [ '@payments', '@services', '@skip-on-default-pressable' ] }, + { + tag: [ + '@payments', + '@services', + '@skip-on-default-pressable', + '@skip-on-default-wpcom', + ], + }, () => { let categoryIds, productIds, orderIds, setupPage; diff --git a/plugins/woocommerce/tests/e2e-pw/tests/admin-marketing/overview.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/admin-marketing/overview.spec.js index 2ababaf97ec..773f160caf2 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/admin-marketing/overview.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/admin-marketing/overview.spec.js @@ -15,40 +15,44 @@ test.describe( 'Marketing page', () => { ).toBeVisible(); } ); - test( 'Marketing Overview page have relevant content', async ( { - page, - } ) => { - // Go to the Marketing page. - await page.goto( 'wp-admin/admin.php?page=wc-admin&path=%2Fmarketing' ); + test( + 'Marketing Overview page have relevant content', + { tag: '@skip-on-default-wpcom' }, + async ( { page } ) => { + // Go to the Marketing page. + await page.goto( + 'wp-admin/admin.php?page=wc-admin&path=%2Fmarketing' + ); - // Heading should be overview - await expect( - page.getByRole( 'heading', { name: 'Overview' } ) - ).toBeVisible(); + // Heading should be overview + await expect( + page.getByRole( 'heading', { name: 'Overview' } ) + ).toBeVisible(); - // Sections present - await expect( - page.getByText( 'Channels', { exact: true } ) - ).toBeVisible(); - await expect( - page.getByText( 'Discover more marketing tools' ) - ).toBeVisible(); - await expect( - page.getByRole( 'tab', { name: 'Email' } ) - ).toBeVisible(); - await expect( - page.getByRole( 'tab', { name: 'Automations' } ) - ).toBeVisible(); - await expect( - page.getByRole( 'tab', { name: 'Conversion' } ) - ).toBeVisible(); - await expect( - page.getByRole( 'tab', { name: 'CRM', exact: true } ) - ).toBeVisible(); - await expect( - page.getByText( 'Learn about marketing a store' ) - ).toBeVisible(); - } ); + // Sections present + await expect( + page.getByText( 'Channels', { exact: true } ) + ).toBeVisible(); + await expect( + page.getByText( 'Discover more marketing tools' ) + ).toBeVisible(); + await expect( + page.getByRole( 'tab', { name: 'Email' } ) + ).toBeVisible(); + await expect( + page.getByRole( 'tab', { name: 'Automations' } ) + ).toBeVisible(); + await expect( + page.getByRole( 'tab', { name: 'Conversion' } ) + ).toBeVisible(); + await expect( + page.getByRole( 'tab', { name: 'CRM', exact: true } ) + ).toBeVisible(); + await expect( + page.getByText( 'Learn about marketing a store' ) + ).toBeVisible(); + } + ); test( 'Introduction can be dismissed', diff --git a/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js index eec70d721a8..035b63be270 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js @@ -73,7 +73,7 @@ test.describe( 'Payment setup task', () => { await page .locator( '//input[@placeholder="BIC / Swift"]' ) .fill( 'ABBA' ); - await page.locator( 'text=Save' ).click(); + await page.getByRole( 'button', { name: 'Save' } ).click(); // Check that bank transfers were set up. await expect( @@ -93,7 +93,7 @@ test.describe( 'Payment setup task', () => { page, } ) => { await page.goto( 'wp-admin/admin.php?page=wc-admin' ); - await page.locator( 'text=Get paid' ).click(); + await page.getByRole( 'button', { name: '3 Get paid' } ).click(); await expect( page.locator( '.woocommerce-layout__header-wrapper > h1' ) ).toHaveText( 'Get paid' ); From 1ef1aaa1f077738770b0697a4c47ec92c34fbffe Mon Sep 17 00:00:00 2001 From: Seghir Nadir Date: Mon, 16 Sep 2024 15:03:20 +0200 Subject: [PATCH 53/59] Hadren styles for interactive elements in Checkout block (#51375) * reset styles for panel button * reset styles for address card edit and address line 2 * Update shipping selector buttons * fix line height * Add changefile(s) from automation for the following project(s): woocommerce-blocks * remove extra styles no longer needed * update styles to balance chevron and change to span --------- Co-authored-by: github-actions Co-authored-by: Alex Florisca --- .../form/address-line-2-field.tsx | 6 +- .../js/blocks/checkout/address-card/index.tsx | 6 +- .../checkout-shipping-method-block/block.tsx | 6 +- .../checkout-shipping-method-block/edit.tsx | 6 +- .../checkout-shipping-method-block/style.scss | 15 +- plugins/woocommerce-blocks/package.json | 2 +- .../packages/components/panel/index.tsx | 42 +- .../packages/components/panel/style.scss | 31 +- ...date-switch-to-ariakit-buttons-in-checkout | 4 + pnpm-lock.yaml | 360 +++++++++++++++--- 10 files changed, 357 insertions(+), 121 deletions(-) create mode 100644 plugins/woocommerce/changelog/51375-update-switch-to-ariakit-buttons-in-checkout diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/address-line-2-field.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/address-line-2-field.tsx index 0c12494ff70..ad7bb145709 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/address-line-2-field.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/address-line-2-field.tsx @@ -5,6 +5,7 @@ import { ValidatedTextInput } from '@woocommerce/blocks-components'; import { AddressFormValues, ContactFormValues } from '@woocommerce/settings'; import { useState, Fragment, useCallback } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; +import { Button } from '@ariakit/react'; /** * Internal dependencies @@ -50,7 +51,8 @@ const AddressLine2Field = < T extends AddressFormValues | ContactFormValues >( { /> ) : ( <> - + { onEdit && ( - + ) }
    ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.tsx index f6b033aa709..a42bdd5a61d 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/block.tsx @@ -10,6 +10,7 @@ import { CART_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data'; import { useDispatch, useSelect } from '@wordpress/data'; import { isPackageRateCollectable } from '@woocommerce/base-utils'; import { getSetting } from '@woocommerce/settings'; +import { Button } from '@ariakit/react'; /** * Internal dependencies @@ -18,7 +19,6 @@ import { RatePrice, getLocalPickupPrices, getShippingPrices } from './shared'; import type { minMaxPrices } from './shared'; import { defaultLocalPickupText, defaultShippingText } from './constants'; import { shippingAddressHasValidationErrors } from '../../../../data/cart/utils'; -import Button from '../../../../base/components/button'; const SHIPPING_RATE_ERROR = { hidden: true, @@ -44,8 +44,8 @@ const LocalPickupSelector = ( { } ) => { return ( - + { isOpen && (
    { children } diff --git a/plugins/woocommerce-blocks/packages/components/panel/style.scss b/plugins/woocommerce-blocks/packages/components/panel/style.scss index ac4f5722bde..8df33917d6a 100644 --- a/plugins/woocommerce-blocks/packages/components/panel/style.scss +++ b/plugins/woocommerce-blocks/packages/components/panel/style.scss @@ -14,31 +14,28 @@ } .wc-block-components-panel__button { - @include reset-box(); + box-sizing: border-box; height: auto; - line-height: 1; + line-height: inherit; margin-top: em(6px); padding-right: #{24px + $gap-smaller}; padding-top: em($gap-small - 6px); + padding-left: 0 !important; position: relative; text-align: left; width: 100%; word-break: break-word; &[aria-expanded="true"] { - padding-bottom: $gap-smaller; - margin-bottom: $gap-smaller; + margin-bottom: $gap-smaller * 2; } &, &:hover, &:focus, &:active { - @include reset-color(); - @include reset-typography(); - background: transparent; - box-shadow: none; cursor: pointer; + padding-left: 0 !important; } > .wc-block-components-panel__button-icon { @@ -58,21 +55,3 @@ display: none; } } - -// Extra classes for specificity. -.theme-twentytwentyone.theme-twentytwentyone.theme-twentytwentyone -.wc-block-components-panel__button { - background-color: inherit; - color: inherit; -} - -.theme-twentytwenty .wc-block-components-panel__button, -.theme-twentyseventeen .wc-block-components-panel__button { - background: none transparent; - color: inherit; - - &.wc-block-components-panel__button:hover, - &.wc-block-components-panel__button:focus { - background: none transparent; - } -} diff --git a/plugins/woocommerce/changelog/51375-update-switch-to-ariakit-buttons-in-checkout b/plugins/woocommerce/changelog/51375-update-switch-to-ariakit-buttons-in-checkout new file mode 100644 index 00000000000..ccedf5ae319 --- /dev/null +++ b/plugins/woocommerce/changelog/51375-update-switch-to-ariakit-buttons-in-checkout @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Harden styles for interactive elements in Checkout block to prevent style leakage. \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 296a64d656a..a2f8a2a6aab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -256,7 +256,7 @@ importers: version: 10.5.0(sass@1.69.5)(webpack@5.89.0(webpack-cli@3.3.12)) ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) + version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) typescript: specifier: ^5.3.3 version: 5.3.3 @@ -292,7 +292,7 @@ importers: version: 2.3.2 debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@9.4.0) + version: 4.3.4(supports-color@8.1.1) dompurify: specifier: ^2.4.7 version: 2.4.7 @@ -453,7 +453,7 @@ importers: version: 27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)) ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) + version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) typescript: specifier: ^5.3.3 version: 5.3.3 @@ -2866,7 +2866,7 @@ importers: dependencies: debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@9.4.0) + version: 4.3.4(supports-color@8.1.1) devDependencies: '@babel/core': specifier: ^7.23.5 @@ -2975,7 +2975,7 @@ importers: version: 1.2.5(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2) debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@9.4.0) + version: 4.3.4(supports-color@8.1.1) prop-types: specifier: ^15.8.1 version: 15.8.1 @@ -2991,10 +2991,10 @@ importers: devDependencies: '@babel/preset-react': specifier: 7.23.3 - version: 7.23.3(@babel/core@7.25.2) + version: 7.23.3(@babel/core@7.24.7) '@babel/preset-typescript': specifier: 7.23.2 - version: 7.23.2(@babel/core@7.25.2) + version: 7.23.2(@babel/core@7.24.7) '@svgr/webpack': specifier: ^8.1.0 version: 8.1.0(typescript@5.3.3) @@ -3045,10 +3045,10 @@ importers: version: 2.17.0(wp-prettier@2.8.5) '@wordpress/scripts': specifier: ^19.2.4 - version: 19.2.4(@babel/core@7.25.2)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4) + version: 19.2.4(@babel/core@7.24.7)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4) babel-jest: specifier: ~27.5.1 - version: 27.5.1(@babel/core@7.25.2) + version: 27.5.1(@babel/core@7.24.7) eslint: specifier: ^8.55.0 version: 8.55.0 @@ -3352,7 +3352,7 @@ importers: version: 3.34.0 debug: specifier: ^4.3.4 - version: 4.3.4(supports-color@9.4.0) + version: 4.3.4(supports-color@8.1.1) dompurify: specifier: ^2.4.7 version: 2.4.7 @@ -3906,7 +3906,7 @@ importers: version: 2.17.0(wp-prettier@2.8.5) '@wordpress/scripts': specifier: ^19.2.4 - version: 19.2.4(@babel/core@7.25.2)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4) + version: 19.2.4(@babel/core@7.25.2)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4) eslint: specifier: ^8.55.0 version: 8.55.0 @@ -3929,7 +3929,7 @@ importers: plugins/woocommerce-blocks: dependencies: '@ariakit/react': - specifier: ^0.4.4 + specifier: ^0.4.5 version: 0.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@dnd-kit/core': specifier: ^6.1.0 @@ -4642,7 +4642,7 @@ importers: version: 27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)) ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) + version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3) @@ -4839,7 +4839,7 @@ importers: version: 1.2.2 ts-jest: specifier: ~29.1.1 - version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) + version: 29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3) ts-loader: specifier: ^9.5.1 version: 9.5.1(typescript@5.3.3)(webpack@5.89.0(webpack-cli@3.3.12)) @@ -21629,7 +21629,7 @@ packages: engines: {node: '>=18'} hasBin: true peerDependencies: - react: ^17.0.2 + react: 18.2.0 react-number-format@4.9.3: resolution: {integrity: sha512-am1A1xYAbENuKJ+zpM7V+B1oRTSeOHYltqVKExznIVFweBzhLmOBmyb1DfIKjHo90E0bo1p3nzVJ2NgS5xh+sQ==} @@ -21819,7 +21819,7 @@ packages: react-with-direction@1.4.0: resolution: {integrity: sha512-ybHNPiAmaJpoWwugwqry9Hd1Irl2hnNXlo/2SXQBwbLn/jGMauMS2y9jw+ydyX5V9ICryCqObNSthNt5R94xpg==} peerDependencies: - react: ^17.0.2 + react: ^0.14 || ^15 || ^16 react-dom: ^0.14 || ^15 || ^16 react-with-styles-interface-css@4.0.3: @@ -25603,7 +25603,7 @@ snapshots: '@wordpress/primitives': 3.55.0 '@wordpress/react-i18n': 3.55.0 classnames: 2.3.2 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) react: 17.0.2 react-dom: 17.0.2(react@17.0.2) react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -25628,7 +25628,7 @@ snapshots: '@wordpress/primitives': 3.55.0 '@wordpress/react-i18n': 3.55.0 classnames: 2.3.2 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) react: 17.0.2 react-dom: 17.0.2(react@17.0.2) react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -25732,7 +25732,7 @@ snapshots: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 lodash: 4.17.21 @@ -25755,7 +25755,7 @@ snapshots: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -25775,7 +25775,7 @@ snapshots: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -25838,6 +25838,14 @@ snapshots: eslint-visitor-keys: 2.1.0 semver: 6.3.1 + '@babel/eslint-parser@7.23.3(@babel/core@7.24.7)(eslint@7.32.0)': + dependencies: + '@babel/core': 7.24.7 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 7.32.0 + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 + '@babel/eslint-parser@7.23.3(@babel/core@7.25.2)(eslint@7.32.0)': dependencies: '@babel/core': 7.25.2 @@ -26002,6 +26010,19 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.7 semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.23.6(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.24.7 + semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.23.6(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -26993,7 +27014,6 @@ snapshots: dependencies: '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.24.8 - optional: true '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.25.2)': dependencies: @@ -27230,6 +27250,11 @@ snapshots: '@babel/core': 7.23.2 '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -28841,6 +28866,11 @@ snapshots: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-react-display-name@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-react-display-name@7.24.7(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -28860,6 +28890,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/plugin-transform-react-jsx': 7.25.2(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -28951,6 +28988,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/types': 7.25.2 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -28974,6 +29022,12 @@ snapshots: '@babel/helper-annotate-as-pure': 7.24.7 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-react-pure-annotations@7.23.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-react-pure-annotations@7.23.3(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -29286,13 +29340,13 @@ snapshots: '@babel/helper-plugin-utils': 7.24.8 '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.2) - '@babel/plugin-transform-typescript@7.23.6(@babel/core@7.25.2)': + '@babel/plugin-transform-typescript@7.23.6(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.24.7 '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-create-class-features-plugin': 7.23.6(@babel/core@7.25.2) + '@babel/helper-create-class-features-plugin': 7.23.6(@babel/core@7.24.7) '@babel/helper-plugin-utils': 7.24.8 - '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.25.2) + '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.24.7) '@babel/plugin-transform-typescript@7.25.2(@babel/core@7.12.9)': dependencies: @@ -30033,8 +30087,8 @@ snapshots: '@babel/preset-flow@7.23.3(@babel/core@7.23.2)': dependencies: '@babel/core': 7.23.2 - '@babel/helper-plugin-utils': 7.24.8 - '@babel/helper-validator-option': 7.24.8 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.23.5 '@babel/plugin-transform-flow-strip-types': 7.23.3(@babel/core@7.23.2) '@babel/preset-flow@7.23.3(@babel/core@7.23.5)': @@ -30119,6 +30173,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-react@7.23.3(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-validator-option': 7.24.8 + '@babel/plugin-transform-react-display-name': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx': 7.25.2(@babel/core@7.24.7) + '@babel/plugin-transform-react-jsx-development': 7.22.5(@babel/core@7.24.7) + '@babel/plugin-transform-react-pure-annotations': 7.23.3(@babel/core@7.24.7) + transitivePeerDependencies: + - supports-color + '@babel/preset-react@7.23.3(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -30142,14 +30208,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.23.2(@babel/core@7.25.2)': + '@babel/preset-typescript@7.23.2(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.25.2) - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.25.2) - '@babel/plugin-transform-typescript': 7.23.6(@babel/core@7.25.2) + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.24.7) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.24.7) + '@babel/plugin-transform-typescript': 7.23.6(@babel/core@7.24.7) transitivePeerDependencies: - supports-color @@ -32107,7 +32173,7 @@ snapshots: '@oclif/color': 1.0.13 '@oclif/core': 2.15.0(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3) chalk: 4.1.2 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) fs-extra: 9.1.0 http-call: 5.3.0 load-json-file: 5.3.0 @@ -32596,7 +32662,7 @@ snapshots: '@puppeteer/browsers@1.4.6(typescript@5.3.2)': dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.0 @@ -32610,7 +32676,7 @@ snapshots: '@puppeteer/browsers@1.4.6(typescript@5.3.3)': dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.0 @@ -32624,7 +32690,7 @@ snapshots: '@puppeteer/browsers@1.9.0': dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.1 @@ -38349,7 +38415,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.56.0 '@typescript-eslint/type-utils': 5.56.0(eslint@8.55.0)(typescript@5.3.2) '@typescript-eslint/utils': 5.56.0(eslint@8.55.0)(typescript@5.3.2) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 grapheme-splitter: 1.0.4 ignore: 5.3.0 @@ -38368,7 +38434,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.55.0)(typescript@5.3.2) '@typescript-eslint/utils': 5.62.0(eslint@8.55.0)(typescript@5.3.2) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -38387,7 +38453,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.55.0)(typescript@5.3.3) '@typescript-eslint/utils': 5.62.0(eslint@8.55.0)(typescript@5.3.3) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -38467,7 +38533,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.56.0 '@typescript-eslint/types': 5.56.0 '@typescript-eslint/typescript-estree': 5.56.0(typescript@5.3.2) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 optionalDependencies: typescript: 5.3.2 @@ -39018,7 +39084,7 @@ snapshots: webpack: 5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@4.10.0) webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack-dev-server@4.15.1)(webpack@5.91.0) - '@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0))': + '@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0(webpack@5.89.0))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0))': dependencies: webpack: 5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0) webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0) @@ -39038,7 +39104,7 @@ snapshots: envinfo: 7.13.0 webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack-dev-server@4.15.1)(webpack@5.91.0) - '@webpack-cli/info@1.5.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0))': + '@webpack-cli/info@1.5.0(webpack-cli@4.10.0(webpack@5.89.0))': dependencies: envinfo: 7.13.0 webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0) @@ -42113,6 +42179,33 @@ snapshots: - supports-color - typescript + '@wordpress/eslint-plugin@9.3.0(@babel/core@7.24.7)(eslint@7.32.0)(typescript@5.3.3)': + dependencies: + '@babel/eslint-parser': 7.23.3(@babel/core@7.24.7)(eslint@7.32.0) + '@typescript-eslint/eslint-plugin': 4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.3.3))(eslint@7.32.0)(typescript@5.3.3) + '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.3.3) + '@wordpress/prettier-config': 1.4.0(wp-prettier@2.2.1-beta-1) + cosmiconfig: 7.1.0 + eslint: 7.32.0 + eslint-config-prettier: 7.2.0(eslint@7.32.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.3.3))(eslint@7.32.0) + eslint-plugin-jest: 24.7.0(@typescript-eslint/eslint-plugin@4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.3.3))(eslint@7.32.0)(typescript@5.3.3))(eslint@7.32.0)(typescript@5.3.3) + eslint-plugin-jsdoc: 36.1.1(eslint@7.32.0) + eslint-plugin-jsx-a11y: 6.8.0(eslint@7.32.0) + eslint-plugin-prettier: 3.4.1(eslint-config-prettier@7.2.0(eslint@7.32.0))(eslint@7.32.0)(wp-prettier@2.2.1-beta-1) + eslint-plugin-react: 7.33.2(eslint@7.32.0) + eslint-plugin-react-hooks: 4.6.0(eslint@7.32.0) + globals: 12.4.0 + prettier: wp-prettier@2.2.1-beta-1 + requireindex: 1.2.0 + optionalDependencies: + typescript: 5.3.3 + transitivePeerDependencies: + - '@babel/core' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + '@wordpress/eslint-plugin@9.3.0(@babel/core@7.25.2)(eslint@7.32.0)(typescript@5.3.3)': dependencies: '@babel/eslint-parser': 7.23.3(@babel/core@7.25.2)(eslint@7.32.0) @@ -42467,6 +42560,20 @@ snapshots: - react-dom - supports-color + '@wordpress/jest-preset-default@7.1.3(@babel/core@7.24.7)(jest@26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + '@wojtekmaj/enzyme-adapter-react-17': 0.6.7(enzyme@3.11.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@wordpress/jest-console': 4.1.1(jest@26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))) + babel-jest: 26.6.3(@babel/core@7.24.7) + enzyme: 3.11.0 + enzyme-to-json: 3.6.2(enzyme@3.11.0) + jest: 26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)) + transitivePeerDependencies: + - '@babel/core' + - react + - react-dom + - supports-color + '@wordpress/jest-preset-default@7.1.3(@babel/core@7.25.2)(jest@26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@wojtekmaj/enzyme-adapter-react-17': 0.6.7(enzyme@3.11.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -43216,7 +43323,86 @@ snapshots: - utf-8-validate - webpack-command - '@wordpress/scripts@19.2.4(@babel/core@7.25.2)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4)': + '@wordpress/scripts@19.2.4(@babel/core@7.24.7)(debug@4.3.4)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4)': + dependencies: + '@svgr/webpack': 5.5.0 + '@wordpress/babel-preset-default': 6.17.0 + '@wordpress/browserslist-config': 4.1.3 + '@wordpress/dependency-extraction-webpack-plugin': 3.7.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + '@wordpress/eslint-plugin': 9.3.0(@babel/core@7.24.7)(eslint@7.32.0)(typescript@5.3.3) + '@wordpress/jest-preset-default': 7.1.3(@babel/core@7.24.7)(jest@26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@wordpress/npm-package-json-lint-config': 4.32.0(npm-package-json-lint@5.4.2) + '@wordpress/postcss-plugins-preset': 3.6.1(postcss@8.4.32) + '@wordpress/prettier-config': 1.4.0(wp-prettier@2.2.1-beta-1) + '@wordpress/stylelint-config': 19.1.0(stylelint@13.13.1) + babel-jest: 26.6.3(@babel/core@7.24.7) + babel-loader: 8.3.0(@babel/core@7.24.7)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + browserslist: 4.19.3 + chalk: 4.1.2 + check-node-version: 4.2.1 + clean-webpack-plugin: 3.0.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + cross-spawn: 5.1.0 + css-loader: 6.8.1(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + cssnano: 5.1.12(postcss@8.4.32) + cwd: 0.10.0 + dir-glob: 3.0.1 + eslint: 7.32.0 + eslint-plugin-markdown: 2.2.1(eslint@7.32.0) + expect-puppeteer: 4.4.0 + filenamify: 4.3.0 + jest: 26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)) + jest-circus: 26.6.3(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)) + jest-dev-server: 5.0.3(debug@4.3.4) + jest-environment-node: 26.6.2 + markdownlint: 0.23.1 + markdownlint-cli: 0.27.1 + merge-deep: 3.0.3 + mini-css-extract-plugin: 2.7.6(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + minimist: 1.2.8 + npm-package-json-lint: 5.4.2 + postcss: 8.4.32 + postcss-loader: 6.2.1(postcss@8.4.32)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + prettier: wp-prettier@2.2.1-beta-1 + puppeteer-core: 10.4.0 + read-pkg-up: 1.0.1 + resolve-bin: 0.4.3 + sass: 1.69.5 + sass-loader: 12.6.0(sass@1.69.5)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + source-map-loader: 3.0.2(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + stylelint: 13.13.1 + terser-webpack-plugin: 5.3.6(uglify-js@3.17.4)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + webpack: 5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0) + webpack-bundle-analyzer: 4.7.0 + webpack-cli: 4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0) + webpack-livereload-plugin: 3.0.2(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + transitivePeerDependencies: + - '@babel/core' + - '@swc/core' + - '@webpack-cli/generators' + - '@webpack-cli/migrate' + - bufferutil + - canvas + - debug + - esbuild + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - fibers + - file-loader + - node-sass + - postcss-jsx + - postcss-markdown + - react + - react-dom + - sass-embedded + - supports-color + - ts-node + - typescript + - uglify-js + - utf-8-validate + - webpack-dev-server + + '@wordpress/scripts@19.2.4(@babel/core@7.25.2)(file-loader@6.2.0(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3))(typescript@5.3.3)(uglify-js@3.17.4)': dependencies: '@svgr/webpack': 5.5.0 '@wordpress/babel-preset-default': 6.17.0 @@ -44568,6 +44754,20 @@ snapshots: transitivePeerDependencies: - supports-color + babel-jest@26.6.3(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@jest/transform': 26.6.2 + '@jest/types': 26.6.2 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 26.6.2(@babel/core@7.24.7) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + babel-jest@26.6.3(@babel/core@7.25.2): dependencies: '@babel/core': 7.25.2 @@ -44596,6 +44796,20 @@ snapshots: transitivePeerDependencies: - supports-color + babel-jest@27.5.1(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@jest/transform': 27.5.1 + '@jest/types': 27.5.1 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 27.5.1(@babel/core@7.24.7) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + babel-jest@27.5.1(@babel/core@7.25.2): dependencies: '@babel/core': 7.25.2 @@ -44700,6 +44914,15 @@ snapshots: schema-utils: 2.7.1 webpack: 5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@4.10.0) + babel-loader@8.3.0(@babel/core@7.24.7)(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)): + dependencies: + '@babel/core': 7.24.7 + find-cache-dir: 3.3.2 + loader-utils: 2.0.4 + make-dir: 3.1.0 + schema-utils: 2.7.1 + webpack: 5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0) + babel-loader@8.3.0(@babel/core@7.25.2)(webpack@4.47.0(webpack-cli@3.3.12(webpack@5.89.0))): dependencies: '@babel/core': 7.25.2 @@ -45143,7 +45366,6 @@ snapshots: '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.7) - optional: true babel-preset-current-node-syntax@1.0.1(@babel/core@7.25.2): dependencies: @@ -45191,6 +45413,12 @@ snapshots: babel-plugin-jest-hoist: 26.6.2 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.5) + babel-preset-jest@26.6.2(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + babel-plugin-jest-hoist: 26.6.2 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.7) + babel-preset-jest@26.6.2(@babel/core@7.25.2): dependencies: '@babel/core': 7.25.2 @@ -45203,6 +45431,12 @@ snapshots: babel-plugin-jest-hoist: 27.5.1 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.5) + babel-preset-jest@27.5.1(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + babel-plugin-jest-hoist: 27.5.1 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.7) + babel-preset-jest@27.5.1(@babel/core@7.25.2): dependencies: '@babel/core': 7.25.2 @@ -48173,7 +48407,7 @@ snapshots: eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.56.0(eslint@8.55.0)(typescript@5.3.2))(eslint-import-resolver-webpack@0.13.2)(eslint-plugin-import@2.28.1)(eslint@8.55.0): dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) enhanced-resolve: 5.15.0 eslint: 8.55.0 eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.56.0(eslint@8.55.0)(typescript@5.3.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.56.0(eslint@8.55.0)(typescript@5.3.2))(eslint-import-resolver-webpack@0.13.2)(eslint-plugin-import@2.28.1)(eslint@8.55.0))(eslint-import-resolver-webpack@0.13.2(eslint-plugin-import@2.28.1)(webpack@5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)))(eslint@8.55.0) @@ -48190,7 +48424,7 @@ snapshots: eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.55.0)(typescript@5.3.3))(eslint-import-resolver-webpack@0.13.8)(eslint-plugin-import@2.29.0)(eslint@8.55.0): dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) enhanced-resolve: 5.15.0 eslint: 8.55.0 eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.55.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@5.62.0(eslint@8.55.0)(typescript@5.3.3))(eslint-import-resolver-webpack@0.13.8)(eslint-plugin-import@2.29.0)(eslint@8.55.0))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.0)(webpack@5.89.0(webpack-cli@4.10.0)))(eslint@8.55.0) @@ -49598,7 +49832,7 @@ snapshots: follow-redirects@1.15.6(debug@4.3.4): optionalDependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) follow-redirects@1.5.10: dependencies: @@ -52791,7 +53025,9 @@ snapshots: pretty-format: 24.9.0 throat: 4.1.0 transitivePeerDependencies: + - bufferutil - supports-color + - utf-8-validate jest-jasmine2@25.5.4: dependencies: @@ -54463,7 +54699,7 @@ snapshots: chalk: 5.2.0 cli-truncate: 3.1.0 commander: 10.0.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) execa: 7.2.0 lilconfig: 2.1.0 listr2: 5.0.8(enquirer@2.4.1) @@ -55760,7 +55996,7 @@ snapshots: dependencies: carlo: 0.9.46 chokidar: 3.5.3 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) isbinaryfile: 3.0.3 mime: 2.6.0 opn: 5.5.0 @@ -56288,7 +56524,7 @@ snapshots: '@oclif/plugin-warn-if-update-available': 2.1.1(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3) aws-sdk: 2.1515.0 concurrently: 7.6.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) find-yarn-workspace-root: 2.0.0 fs-extra: 8.1.0 github-slugger: 1.5.0 @@ -57798,7 +58034,7 @@ snapshots: puppeteer-core@13.7.0(encoding@0.1.13): dependencies: cross-fetch: 3.1.5(encoding@0.1.13) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.981744 extract-zip: 2.0.1 https-proxy-agent: 5.0.1 @@ -57837,7 +58073,7 @@ snapshots: '@puppeteer/browsers': 1.4.6(typescript@5.3.2) chromium-bidi: 0.4.16(devtools-protocol@0.0.1147663) cross-fetch: 4.0.0(encoding@0.1.13) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.1147663 ws: 8.13.0 optionalDependencies: @@ -57853,7 +58089,7 @@ snapshots: '@puppeteer/browsers': 1.4.6(typescript@5.3.3) chromium-bidi: 0.4.16(devtools-protocol@0.0.1147663) cross-fetch: 4.0.0(encoding@0.1.13) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.1147663 ws: 8.13.0 optionalDependencies: @@ -57869,7 +58105,7 @@ snapshots: '@puppeteer/browsers': 1.9.0 chromium-bidi: 0.5.1(devtools-protocol@0.0.1203626) cross-fetch: 4.0.0(encoding@0.1.13) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.1203626 ws: 8.14.2 transitivePeerDependencies: @@ -57905,7 +58141,7 @@ snapshots: puppeteer@17.1.3(encoding@0.1.13): dependencies: cross-fetch: 3.1.5(encoding@0.1.13) - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.1036444 extract-zip: 2.0.1 https-proxy-agent: 5.0.1 @@ -58233,7 +58469,7 @@ snapshots: react-docgen-typescript-plugin@1.0.5(typescript@5.3.2)(webpack@5.91.0(@swc/core@1.3.100)(esbuild@0.18.20)(webpack-cli@5.1.4)): dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -60110,7 +60346,7 @@ snapshots: dependencies: '@kwsites/file-exists': 1.1.1 '@kwsites/promise-deferred': 1.1.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -60859,7 +61095,7 @@ snapshots: colord: 2.9.3 cosmiconfig: 7.1.0 css-functions-list: 3.2.1 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4(supports-color@8.1.1) fast-glob: 3.3.2 fastest-levenshtein: 1.0.16 file-entry-cache: 6.0.1 @@ -61663,7 +61899,7 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.24.7) - ts-jest@29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3): + ts-jest@29.1.1(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@27.5.1(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.3.100)(@types/node@16.18.68)(typescript@5.3.3)))(typescript@5.3.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -62769,8 +63005,8 @@ snapshots: webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) - '@webpack-cli/info': 1.5.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.7.0)(webpack@5.89.0)) + '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0(webpack@5.89.0))(webpack@5.89.0(uglify-js@3.17.4)(webpack-cli@4.10.0)) + '@webpack-cli/info': 1.5.0(webpack-cli@4.10.0(webpack@5.89.0)) '@webpack-cli/serve': 1.7.0(webpack-cli@4.10.0(webpack@5.89.0)) colorette: 2.0.20 commander: 7.2.0 From a89bd1a48510b617b166cb6ce555bdd3eda95084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomek=20Wytr=C4=99bowicz?= Date: Mon, 16 Sep 2024 15:57:58 +0200 Subject: [PATCH 54/59] Use page query param consistently as string for ReportsTable (#51396) --- packages/js/data/changelog/fix-51395-reporttable-page | 4 ++++ packages/js/data/src/reports/utils.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 packages/js/data/changelog/fix-51395-reporttable-page diff --git a/packages/js/data/changelog/fix-51395-reporttable-page b/packages/js/data/changelog/fix-51395-reporttable-page new file mode 100644 index 00000000000..9b02145b01f --- /dev/null +++ b/packages/js/data/changelog/fix-51395-reporttable-page @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Use page query param consistently as string for `getReportTableQuery`. diff --git a/packages/js/data/src/reports/utils.ts b/packages/js/data/src/reports/utils.ts index 065c4847653..39cf4869798 100644 --- a/packages/js/data/src/reports/utils.ts +++ b/packages/js/data/src/reports/utils.ts @@ -561,7 +561,7 @@ export function getReportTableQuery( before: noIntervals ? undefined : appendTimestamp( datesFromQuery.primary.before, 'end' ), - page: query.paged || 1, + page: query.paged || '1', per_page: query.per_page || QUERY_DEFAULTS.pageSize, ...filterQuery, ...tableQuery, From 450a299bae4ae4891db39e2e2c0646d1ac1e9586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomek=20Wytr=C4=99bowicz?= Date: Mon, 16 Sep 2024 15:59:18 +0200 Subject: [PATCH 55/59] Fix tax_code in the report export for removed rates (#51395) --- .../changelog/fix-49759-removed-taxname-export | 4 ++++ .../src/Admin/API/Reports/Taxes/Controller.php | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-49759-removed-taxname-export diff --git a/plugins/woocommerce/changelog/fix-49759-removed-taxname-export b/plugins/woocommerce/changelog/fix-49759-removed-taxname-export new file mode 100644 index 00000000000..2d76f39f150 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-49759-removed-taxname-export @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Improve performance of tax report export generation and fix tax_code for removed rates. diff --git a/plugins/woocommerce/src/Admin/API/Reports/Taxes/Controller.php b/plugins/woocommerce/src/Admin/API/Reports/Taxes/Controller.php index d31db463c0c..a794fee018f 100644 --- a/plugins/woocommerce/src/Admin/API/Reports/Taxes/Controller.php +++ b/plugins/woocommerce/src/Admin/API/Reports/Taxes/Controller.php @@ -244,7 +244,15 @@ class Controller extends GenericController implements ExportableInterface { */ public function prepare_item_for_export( $item ) { return array( - 'tax_code' => \WC_Tax::get_rate_code( $item['tax_rate_id'] ), + 'tax_code' => \WC_Tax::get_rate_code( + (object) array( + 'tax_rate_id' => $item['tax_rate_id'], + 'tax_rate_country' => $item['country'], + 'tax_rate_state' => $item['state'], + 'tax_rate_name' => $item['name'], + 'tax_rate_priority' => $item['priority'], + ) + ), 'rate' => $item['tax_rate'], 'total_tax' => self::csv_number_format( $item['total_tax'] ), 'order_tax' => self::csv_number_format( $item['order_tax'] ), From bf0646c307e7adfa8c040135a9bc664a5ea99dad Mon Sep 17 00:00:00 2001 From: Boro Sitnikovski Date: Mon, 16 Sep 2024 16:57:05 +0200 Subject: [PATCH 56/59] Add error handling when the call to 'install-url' endpoint fails (#51298) * Add error handling when the call to 'install-url' endpoint fails * Changelog * Ignore any explicitly * Move the function below to address lint * Change 'Try again' to a href, suggesting to download and install manually * Lint --- .../table/actions/install.tsx | 115 +++++++++--------- ...fix-wccom-20223-in-app-subs-error-handling | 4 + 2 files changed, 60 insertions(+), 59 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-wccom-20223-in-app-subs-error-handling diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install.tsx index 1691f46c4e3..6b321d07bd9 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/actions/install.tsx @@ -51,6 +51,48 @@ export default function Install( props: InstallProps ) { ); }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleInstallError = ( error: any ) => { + loadSubscriptions( false ).then( () => { + let errorMessage = sprintf( + // translators: %s is the product name. + __( '%s couldn’t be installed.', 'woocommerce' ), + props.subscription.product_name + ); + if ( error?.success === false && error?.data.message ) { + errorMessage += ' ' + error.data.message; + } + addNotice( + props.subscription.product_key, + errorMessage, + NoticeStatus.Error, + { + actions: [ + { + label: __( + 'Download and install manually', + 'woocommerce' + ), + url: 'https://woocommerce.com/my-account/downloads/', + }, + ], + } + ); + stopInstall(); + + if ( props.onError ) { + props.onError(); + } + } ); + + recordEvent( 'marketplace_product_install_failed', { + product_zip_slug: props.subscription.zip_slug, + product_id: props.subscription.product_id, + product_current_version: props.subscription.version, + error_message: error?.data?.message, + } ); + }; + const install = () => { recordEvent( 'marketplace_product_install_button_clicked', { product_zip_slug: props.subscription.zip_slug, @@ -90,71 +132,26 @@ export default function Install( props: InstallProps ) { props.onSuccess(); } } ) - .catch( ( error ) => { - loadSubscriptions( false ).then( () => { - let errorMessage = sprintf( - // translators: %s is the product name. - __( '%s couldn’t be installed.', 'woocommerce' ), - props.subscription.product_name - ); - if ( error?.success === false && error?.data.message ) { - errorMessage += ' ' + error.data.message; - } - addNotice( - props.subscription.product_key, - errorMessage, - NoticeStatus.Error, - { - actions: [ - { - label: __( 'Try again', 'woocommerce' ), - onClick: install, - }, - ], - } - ); - stopInstall(); - - if ( props.onError ) { - props.onError(); - } - } ); - - recordEvent( 'marketplace_product_install_failed', { + .catch( handleInstallError ); + } else { + getInstallUrl( props.subscription ) + .then( ( url: string ) => { + recordEvent( 'marketplace_product_install_url', { product_zip_slug: props.subscription.zip_slug, product_id: props.subscription.product_id, product_current_version: props.subscription.version, - error_message: error?.data?.message, + product_install_url: url, } ); - } ); - } else { - getInstallUrl( props.subscription ).then( ( url: string ) => { - recordEvent( 'marketplace_product_install_url', { - product_zip_slug: props.subscription.zip_slug, - product_id: props.subscription.product_id, - product_current_version: props.subscription.version, - product_install_url: url, - } ); - stopInstall(); + stopInstall(); - if ( url ) { - window.open( url, '_self' ); - } else { - addNotice( - props.subscription.product_key, - sprintf( - // translators: %s is the product name. - __( - '%s couldn’t be installed. Please install the product manually.', - 'woocommerce' - ), - props.subscription.product_name - ), - NoticeStatus.Error - ); - } - } ); + if ( url ) { + window.open( url, '_self' ); + } else { + throw new Error(); + } + } ) + .catch( handleInstallError ); } }; diff --git a/plugins/woocommerce/changelog/fix-wccom-20223-in-app-subs-error-handling b/plugins/woocommerce/changelog/fix-wccom-20223-in-app-subs-error-handling new file mode 100644 index 00000000000..0815a090e50 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-wccom-20223-in-app-subs-error-handling @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Introduce error handling on the in-app my subscriptions page From e4d995938fe18ef9af3cf28bcfc1901338de5a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20P=C3=A9rez=20Pellicer?= <5908855+puntope@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:45:47 +0400 Subject: [PATCH 57/59] Set customer email in reports data if available (#51367) * Set customer email in reports data if available. * Add changelog * Lint --- .../changelog/fix-use-customer-email-if-available | 4 ++++ .../src/Admin/API/Reports/Customers/DataStore.php | 5 +++++ 2 files changed, 9 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-use-customer-email-if-available diff --git a/plugins/woocommerce/changelog/fix-use-customer-email-if-available b/plugins/woocommerce/changelog/fix-use-customer-email-if-available new file mode 100644 index 00000000000..58490385da9 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-use-customer-email-if-available @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Set customer email in reports if customer data is available diff --git a/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php index e13ed41c47c..7f740eff74e 100644 --- a/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php +++ b/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php @@ -615,6 +615,11 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { if ( is_null( $customer_user ) ) { $customer_user = new \WC_Customer( $user_id ); } + + // Set email as customer email instead of Order Billing Email if we have a customer. + $data['email'] = $customer_user->get_email( 'edit' ); + + // Adding other relevant customer data. $data['user_id'] = $user_id; $data['username'] = $customer_user->get_username( 'edit' ); $data['date_registered'] = $customer_user->get_date_created( 'edit' ) ? $customer_user->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ) : null; From 0036b4d293a9ad4e19b47f553c925ff1cd996abf Mon Sep 17 00:00:00 2001 From: RJ <27843274+rjchow@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:33:41 +1000 Subject: [PATCH 58/59] add: tax task completion filter (#51362) * add: tax task completion filter * fix: add phpcs ignore for missing hook comment * lint --- .../woocommerce/changelog/add-tax-task-completion-filter | 4 ++++ .../src/Admin/Features/OnboardingTasks/Tasks/Tax.php | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-tax-task-completion-filter diff --git a/plugins/woocommerce/changelog/add-tax-task-completion-filter b/plugins/woocommerce/changelog/add-tax-task-completion-filter new file mode 100644 index 00000000000..89ab40a7240 --- /dev/null +++ b/plugins/woocommerce/changelog/add-tax-task-completion-filter @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Adds a filter for third party tax plugins to indicate that they have completed the tax task diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php index ba33d9b2362..b0a65b969e6 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php @@ -15,6 +15,7 @@ class Tax extends Task { /** * Used to cache is_complete() method result. + * * @var null */ private $is_complete_result = null; @@ -109,12 +110,16 @@ class Tax extends Task { */ public function is_complete() { if ( $this->is_complete_result === null ) { - $wc_connect_taxes_enabled = get_option( 'wc_connect_taxes_enabled' ); + $wc_connect_taxes_enabled = get_option( 'wc_connect_taxes_enabled' ); $is_wc_connect_taxes_enabled = ( $wc_connect_taxes_enabled === 'yes' ) || ( $wc_connect_taxes_enabled === true ); // seems that in some places boolean is used, and other places 'yes' | 'no' is used + // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- We will replace this with a formal system by WC 9.6 so lets not advertise it yet. + $third_party_complete = apply_filters( 'woocommerce_admin_third_party_tax_setup_complete', false ); + $this->is_complete_result = $is_wc_connect_taxes_enabled || count( TaxDataStore::get_taxes( array() ) ) > 0 || - get_option( 'woocommerce_no_sales_tax' ) !== false; + get_option( 'woocommerce_no_sales_tax' ) !== false || + $third_party_complete; } return $this->is_complete_result; From 160b3e3ca710fc8487ed603d6fc8fc6f9516bf62 Mon Sep 17 00:00:00 2001 From: Moon Date: Mon, 16 Sep 2024 21:02:21 -0700 Subject: [PATCH 59/59] Add use-wp-horizon feature flag (#51421) * Add use-wp-horizon feature flag * Add changefile(s) from automation for the following project(s): woocommerce --------- Co-authored-by: github-actions --- plugins/woocommerce/changelog/51421-update-use-horizon-env | 4 ++++ plugins/woocommerce/client/admin/config/core.json | 3 ++- plugins/woocommerce/client/admin/config/development.json | 3 ++- plugins/woocommerce/src/Admin/API/OnboardingPlugins.php | 5 +++++ 4 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/51421-update-use-horizon-env diff --git a/plugins/woocommerce/changelog/51421-update-use-horizon-env b/plugins/woocommerce/changelog/51421-update-use-horizon-env new file mode 100644 index 00000000000..0258515d62b --- /dev/null +++ b/plugins/woocommerce/changelog/51421-update-use-horizon-env @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add use-wp-horizon feature flag to set calpyso_env to horizon \ No newline at end of file diff --git a/plugins/woocommerce/client/admin/config/core.json b/plugins/woocommerce/client/admin/config/core.json index 3c201f07922..6bfd9954259 100644 --- a/plugins/woocommerce/client/admin/config/core.json +++ b/plugins/woocommerce/client/admin/config/core.json @@ -39,6 +39,7 @@ "launch-your-store": true, "product-editor-template-system": false, "blueprint": false, - "reactify-classic-payments-settings": false + "reactify-classic-payments-settings": false, + "use-wp-horizon": false } } diff --git a/plugins/woocommerce/client/admin/config/development.json b/plugins/woocommerce/client/admin/config/development.json index 802c62fe6a3..da6f07d82dd 100644 --- a/plugins/woocommerce/client/admin/config/development.json +++ b/plugins/woocommerce/client/admin/config/development.json @@ -39,6 +39,7 @@ "launch-your-store": true, "product-editor-template-system": false, "blueprint": true, - "reactify-classic-payments-settings": false + "reactify-classic-payments-settings": false, + "use-wp-horizon": false } } diff --git a/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php b/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php index fb6fa25570d..55f55edfe85 100644 --- a/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php +++ b/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php @@ -11,6 +11,7 @@ defined( 'ABSPATH' ) || exit; use ActionScheduler; use Automattic\Jetpack\Connection\Manager; +use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\PluginsHelper; use Automattic\WooCommerce\Admin\PluginsInstallLoggers\AsynPluginsInstallLogger; use WC_REST_Data_Controller; @@ -238,6 +239,10 @@ class OnboardingPlugins extends WC_REST_Data_Controller { $redirect_url = $request->get_param( 'redirect_url' ); $calypso_env = defined( 'WOOCOMMERCE_CALYPSO_ENVIRONMENT' ) && in_array( WOOCOMMERCE_CALYPSO_ENVIRONMENT, [ 'development', 'wpcalypso', 'horizon', 'stage' ], true ) ? WOOCOMMERCE_CALYPSO_ENVIRONMENT : 'production'; + if ( Features::is_enabled( 'use-wp-horizon' ) ) { + $calypso_env = 'horizon'; + } + return [ 'success' => ! $errors->has_errors(), 'errors' => $errors->get_error_messages(),