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 01/36] 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 02/36] 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 03/36] 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 04/36] 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 05/36] 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 06/36] [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 07/36] 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 08/36] 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 09/36] 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 10/36] 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 11/36] 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 12/36] 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 13/36] [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 14/36] 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 35/36] 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 36/36] 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