Commit 5479dd116f7 for woocommerce
commit 5479dd116f7b529eed42e300f3d9579a81d946d2
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Mon Nov 2 14:06:49 2026 +0200
[tests] Render the My Account addresses and settings form values in PHPUnit, and cover the coupons list table (#68853)
* test(account): Assert the formatted shipping address is the shipping address
wc_get_account_formatted_address() builds its getter from the address
type it is given, but the only test of it passed 'billing', so a getter
hardcoded to billing would have kept every assertion green while the My
Account Addresses page showed the billing address in both columns.
Add a customer whose billing and shipping addresses share no values and
assert each address type formats its own name, street, city and
postcode, then render myaccount/my-address.php and assert each column
carries the address of its heading.
Refs TESTOPS-288
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(onboarding): Assert the profile stores each locale's currency position
The onboarding country step copies the currency position out of the
country's locale entry, but the only test of that endpoint checked the
US locale, whose position is 'left'. A position hardcoded to 'left'
would have stayed green while every other locale lost its price format.
Add a data provider covering Afghanistan ('left_space') and Germany
('right_space') and assert the currency, both separators and the decimal
count stored for each one.
Refs TESTOPS-288
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(orders): Render the order items meta box and assert the Paid row
No PHPUnit test included the order items meta box view, so the Paid row
it renders for a paid order, with the order total and the payment date,
had no coverage at all.
Add a test that renders WC_Meta_Box_Order_Items::output() for a
completed order with a payment date and asserts the Paid row shows
wc_price() of the order total and the payment date, plus two tests that
assert the row is absent when the order has no payment date and when the
status is not one of the paid statuses.
Refs TESTOPS-288
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(settings): Assert output_fields renders stored values and checked state
The settings tests asserted persistence through save() and never
rendered the form, so a blanked value attribute or a checkbox compared
against the wrong stored value would have left the form empty or
unticked with every test green.
Add a test that stores a text, a number and a separator value and
asserts each General settings input renders it back, and a test that
asserts the WooCommerce.com checkboxes are ticked only for the setting
stored as enabled.
Refs TESTOPS-288
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(orders): Assert remove_coupon gives the coupon use back
Removing a coupon from an order in admin goes through
WC_Abstract_Order::remove_coupon(), which drops the coupon line and
calls decrease_usage_count() for the order's customer. The existing
test for that method asserts the removed-coupon hook and the empty
coupon list, and the browser title that walks the admin removal checks
the order total only. Nothing asserted the usage count, so deleting the
decrease_usage_count() call left every test green and a single-use
coupon removed from an order stayed spent.
Add a test that applies a coupon to a processing order, checks the use
is counted and the customer recorded, removes the coupon and checks the
count is back to zero and the used-by list is empty. With the
decrease_usage_count() call removed, this test alone fails of the 76 in
the class.
Refs TESTOPS-288
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(coupons): Render the coupons list table columns
WC_Admin_List_Table_Coupons had no PHPUnit test. The one check on the
coupons list is a browser page-load title that asserts the screen
heading and the "Add new coupon" button, so a column that prints the
wrong value, or is dropped from the list, fails nothing.
Add a test class that drives the table through the hooks the posts list
table fires. It asserts the column set, a restricted coupon's row (code
as the edit link, type, amount, description, product IDs, usage against
the limit, expiry date) and the placeholders of an unrestricted coupon
(infinity for the limit, a dash for description, products and expiry).
Each test fails under a mutation of the table: ignoring the usage limit
and emptying the code fail the row test, dropping the usage column
fails the column test, and printing 0 for a missing limit fails the
placeholder test.
Refs TESTOPS-288
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: Drop the onboarding currency and order Paid row checks
Both filled gaps that #68634 and #68637 left when they moved browser
titles below E2E. Those PRs are now reverted (#69053, #69054), and the
onboarding wizard and order edit specs cover these screens again, so
these checks no longer fill a gap.
The My Account addresses, coupon removal, coupons list table and
settings screen checks stay: they follow up on PRs that remain merged,
or cover behaviour no test asserted before.
Refs #68853
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore: Make the #68853 changelog entry a reviewer comment
The entry was a Type: dev body, which ships to the public changelog as
a Dev line, and it still named the two checks this PR dropped. Use a
Comment instead, since the change is test-only.
Refs #68853
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test: Drop the remove_coupon usage count test that trunk now covers
The PR added test_remove_coupon_decreases_the_coupon_usage_count
because no test asserted that WC_Abstract_Order::remove_coupon() gives
the coupon use back: deleting the decrease_usage_count() call failed
nothing.
Trunk has since rewritten that path to release usage under the
identity it was recorded with (#69632, #70055, #70110) and added five
remove_coupon tests that assert the count drops to 0 and the customer
or guest email leaves the used-by list. With the decrease_usage_count()
call commented out on the merged branch, all five fail alongside this
one, so it no longer guards anything on its own.
Drop it. The file now matches trunk and leaves the PR's diff.
Refs TESTOPS-288
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: Compare coupon list placeholders as decoded text
test_renders_placeholders_for_an_unrestricted_coupon asserted each
empty column printed the exact `–` entity. The description column
runs its placeholder through wp_kses_post(), and on WordPress
7.2-beta3 that prints the `–` character instead, so the optional
pre-release unit job failed on this test even though the screen shows
the same dash.
Decode the rendered column before comparing and expect the en dash
and infinity characters. The test passes whichever form WordPress
prints, and still fails if a column drops its placeholder.
Refs TESTOPS-288
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/testops-288-restore-admin-settings-orders-account-coverage b/plugins/woocommerce/changelog/testops-288-restore-admin-settings-orders-account-coverage
new file mode 100644
index 00000000000..e50ea0e1fe0
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-288-restore-admin-settings-orders-account-coverage
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Cover My Account addresses, settings form values and coupon admin in PHPUnit; test-only change.
+
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/account/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/account/functions.php
index 3b2ef509191..207cc260eb8 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/account/functions.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/account/functions.php
@@ -279,6 +279,120 @@ class WC_Tests_Account_Functions extends WC_Unit_Test_Case {
$customer->delete( true );
}
+ /**
+ * Test wc_get_account_formatted_address() formats the address type it is asked for.
+ *
+ * @since 11.3.0
+ */
+ public function test_wc_get_account_formatted_address_formats_the_requested_address_type() {
+ $customer = $this->create_customer_with_distinct_addresses( 'formatted-address-customer', 'formatted.address@example.com' );
+
+ $shipping = wc_get_account_formatted_address( 'shipping', $customer->get_id() );
+ $billing = wc_get_account_formatted_address( 'billing', $customer->get_id() );
+
+ $this->assertStringContainsString( 'Jane Shipping', $shipping );
+ $this->assertStringContainsString( '456 Nevergreen Terrace', $shipping );
+ $this->assertStringContainsString( 'New York', $shipping );
+ $this->assertStringContainsString( '10010', $shipping );
+ $this->assertStringNotContainsString( 'John Doe', $shipping );
+ $this->assertStringNotContainsString( '123 Evergreen Terrace', $shipping );
+ $this->assertStringNotContainsString( 'Springfield', $shipping );
+ $this->assertStringNotContainsString( '10001', $shipping );
+
+ $this->assertStringContainsString( 'John Doe', $billing );
+ $this->assertStringContainsString( '123 Evergreen Terrace', $billing );
+ $this->assertStringContainsString( 'Springfield', $billing );
+ $this->assertStringContainsString( '10001', $billing );
+ $this->assertStringNotContainsString( 'Jane Shipping', $billing );
+ $this->assertStringNotContainsString( '456 Nevergreen Terrace', $billing );
+ $this->assertStringNotContainsString( 'New York', $billing );
+ $this->assertStringNotContainsString( '10010', $billing );
+ }
+
+ /**
+ * Test the My Account addresses page renders each saved address under its own heading.
+ *
+ * @since 11.3.0
+ */
+ public function test_my_address_template_renders_each_saved_address_in_its_own_column() {
+ $customer = $this->create_customer_with_distinct_addresses( 'my-address-customer', 'my.address@example.com' );
+
+ update_option( 'woocommerce_ship_to_destination', 'shipping' );
+ update_option( 'woocommerce_ship_to_countries', '' );
+ wp_set_current_user( $customer->get_id() );
+
+ $html = wc_get_template_html( 'myaccount/my-address.php' );
+
+ $shipping_column = $this->get_rendered_address_column( $html, 'Shipping address' );
+ $billing_column = $this->get_rendered_address_column( $html, 'Billing address' );
+
+ $this->assertStringContainsString( 'Jane Shipping', $shipping_column );
+ $this->assertStringContainsString( '456 Nevergreen Terrace', $shipping_column );
+ $this->assertStringContainsString( 'New York', $shipping_column );
+ $this->assertStringContainsString( '10010', $shipping_column );
+ $this->assertStringNotContainsString( '123 Evergreen Terrace', $shipping_column );
+
+ $this->assertStringContainsString( 'John Doe', $billing_column );
+ $this->assertStringContainsString( '123 Evergreen Terrace', $billing_column );
+ $this->assertStringContainsString( 'Springfield', $billing_column );
+ $this->assertStringContainsString( '10001', $billing_column );
+ $this->assertStringNotContainsString( '456 Nevergreen Terrace', $billing_column );
+ }
+
+ /**
+ * Create a customer whose billing and shipping addresses share no values.
+ *
+ * @param string $username Username for the new customer.
+ * @param string $email Email address for the new customer.
+ * @return WC_Customer
+ */
+ private function create_customer_with_distinct_addresses( $username, $email ) {
+ $customer = new WC_Customer();
+ $customer->set_username( $username );
+ $customer->set_email( $email );
+ $customer->set_billing_first_name( 'John' );
+ $customer->set_billing_last_name( 'Doe' );
+ $customer->set_billing_address_1( '123 Evergreen Terrace' );
+ $customer->set_billing_city( 'Springfield' );
+ $customer->set_billing_state( 'IL' );
+ $customer->set_billing_postcode( '10001' );
+ $customer->set_billing_country( 'US' );
+ $customer->set_shipping_first_name( 'Jane' );
+ $customer->set_shipping_last_name( 'Shipping' );
+ $customer->set_shipping_address_1( '456 Nevergreen Terrace' );
+ $customer->set_shipping_city( 'New York' );
+ $customer->set_shipping_state( 'NY' );
+ $customer->set_shipping_postcode( '10010' );
+ $customer->set_shipping_country( 'US' );
+ $customer->save();
+
+ return $customer;
+ }
+
+ /**
+ * Get the markup of the address rendered under a My Account addresses heading.
+ *
+ * @param string $html Rendered My Account addresses markup.
+ * @param string $address_title Heading the address is rendered under.
+ * @return string
+ */
+ private function get_rendered_address_column( $html, $address_title ) {
+ $document = new DOMDocument();
+ $previous_state = libxml_use_internal_errors( true );
+ $loaded = $document->loadHTML( '<!DOCTYPE html><html><body>' . $html . '</body></html>' );
+ libxml_clear_errors();
+ libxml_use_internal_errors( $previous_state );
+
+ $this->assertTrue( $loaded, 'The My Account addresses markup should be valid enough for DOM parsing.' );
+
+ $xpath = new DOMXPath( $document );
+ $nodes = $xpath->query( "//div[contains(concat(' ', normalize-space(@class), ' '), ' woocommerce-Address ')][.//h2[normalize-space(.)='{$address_title}']]/address" );
+
+ $this->assertSame( 1, $nodes->length, "The page should render one address under the {$address_title} heading." );
+
+ return (string) $document->saveHTML( $nodes->item( 0 ) );
+ }
+
/**
* Test wc_get_account_saved_payment_methods_list().
*
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-settings-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-settings-test.php
index 7385143d2c9..04aefe1840e 100644
--- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-settings-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-settings-test.php
@@ -579,6 +579,106 @@ class WC_Admin_Settings_Test extends WC_Unit_Test_Case {
$this->assertSame( 1, $xpath->query( $radio_title . '/span[normalize-space(.)="Object ID radio"]' )->length );
}
+ /**
+ * @testdox Should render each stored General setting back into its own form field.
+ */
+ public function test_output_fields_renders_stored_general_setting_values(): void {
+ $store_address = '123 "Evergreen" & Terrace';
+
+ update_option( 'woocommerce_store_address', $store_address );
+ update_option( 'woocommerce_price_num_decimals', '3' );
+ update_option( 'woocommerce_price_thousand_sep', '.' );
+ update_option( 'woocommerce_currency_pos', 'right_space' );
+
+ $output = $this->render_settings_section( new WC_Settings_General(), '' );
+
+ $this->assertSame( $store_address, $this->get_rendered_input_attribute( $output, 'woocommerce_store_address', 'value' ) );
+ $this->assertStringContainsString( '123 "Evergreen" & Terrace', $output, 'The stored value should reach the markup escaped for an attribute.' );
+ $this->assertSame( '3', $this->get_rendered_input_attribute( $output, 'woocommerce_price_num_decimals', 'value' ) );
+ $this->assertSame( '.', $this->get_rendered_input_attribute( $output, 'woocommerce_price_thousand_sep', 'value' ) );
+ $this->assertSame( 'right_space', $this->get_selected_option( $output, 'woocommerce_currency_pos' ), 'The stored option should be the selected one.' );
+ }
+
+ /**
+ * @testdox Should tick only the checkbox settings that are stored as enabled.
+ */
+ public function test_output_fields_renders_stored_checkbox_state(): void {
+ update_option( 'woocommerce_allow_tracking', 'yes' );
+ update_option( 'woocommerce_show_marketplace_suggestions', 'no' );
+
+ $output = $this->render_settings_section( new WC_Settings_Advanced(), 'woocommerce_com' );
+
+ $this->assertSame( 'checked', $this->get_rendered_input_attribute( $output, 'woocommerce_allow_tracking', 'checked' ), 'The enabled setting should render a ticked checkbox.' );
+ $this->assertNull( $this->get_rendered_input_attribute( $output, 'woocommerce_show_marketplace_suggestions', 'checked' ), 'The disabled setting should render an unticked checkbox.' );
+ }
+
+ /**
+ * Render the fields of a settings section.
+ *
+ * @param WC_Settings_Page $settings_page Settings page holding the section.
+ * @param string $section_id Section to render, an empty string for the default section.
+ * @return string
+ */
+ private function render_settings_section( WC_Settings_Page $settings_page, string $section_id ): string {
+ ob_start();
+ try {
+ WC_Admin_Settings::output_fields( $settings_page->get_settings_for_section( $section_id ) );
+ $output = (string) ob_get_contents();
+ } finally {
+ ob_end_clean();
+ }
+
+ return $output;
+ }
+
+ /**
+ * Get an attribute of a rendered input, or null when the attribute is absent.
+ *
+ * @param string $html Rendered settings markup.
+ * @param string $input_id ID of the input to read.
+ * @param string $attribute Attribute to read.
+ * @return string|bool|null
+ */
+ private function get_rendered_input_attribute( string $html, string $input_id, string $attribute ) {
+ $tags = new WP_HTML_Tag_Processor( $html );
+
+ while ( $tags->next_tag( array( 'tag_name' => 'INPUT' ) ) ) {
+ if ( $input_id === $tags->get_attribute( 'id' ) ) {
+ return $tags->get_attribute( $attribute );
+ }
+ }
+
+ $this->fail( "The settings form should render an input with the ID {$input_id}." );
+ }
+
+ /**
+ * Get the value of the selected option of a rendered select, or null when no option is selected.
+ *
+ * @param string $html Rendered settings markup.
+ * @param string $select_id ID of the select to read.
+ * @return string|null
+ */
+ private function get_selected_option( string $html, string $select_id ): ?string {
+ $tags = new WP_HTML_Tag_Processor( $html );
+
+ while ( $tags->next_tag( array( 'tag_name' => 'SELECT' ) ) ) {
+ if ( $select_id !== $tags->get_attribute( 'id' ) ) {
+ continue;
+ }
+
+ $selected = null;
+ while ( null === $selected && $tags->next_tag() && 'OPTION' === $tags->get_tag() ) {
+ if ( null !== $tags->get_attribute( 'selected' ) ) {
+ $selected = $tags->get_attribute( 'value' );
+ }
+ }
+
+ return $selected;
+ }
+
+ $this->fail( "The settings form should render a select with the ID {$select_id}." );
+ }
+
/**
* Prepare globals used by WC_Admin_Settings::save().
*
diff --git a/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-coupons-test.php b/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-coupons-test.php
new file mode 100644
index 00000000000..6981f2c70cc
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-coupons-test.php
@@ -0,0 +1,124 @@
+<?php
+/**
+ * Unit tests for the coupons admin list table.
+ *
+ * @package WooCommerce\Tests\Admin
+ */
+
+declare( strict_types = 1 );
+
+require_once WC_ABSPATH . 'includes/admin/list-tables/class-wc-admin-list-table-coupons.php';
+
+/**
+ * WC_Admin_List_Table_Coupons tests.
+ *
+ * @package WooCommerce\Tests\Admin
+ */
+class WC_Admin_List_Table_Coupons_Test extends WC_Unit_Test_Case {
+
+ /**
+ * The System Under Test.
+ *
+ * @var WC_Admin_List_Table_Coupons
+ */
+ private $sut;
+
+ /**
+ * Set up test fixtures.
+ */
+ public function setUp(): void {
+ parent::setUp();
+ wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
+ $this->sut = new WC_Admin_List_Table_Coupons();
+ }
+
+ /**
+ * @testdox The coupons list shows the code, type, amount, description, products, usage and expiry columns.
+ */
+ public function test_defines_the_coupon_columns(): void {
+ $columns = apply_filters( 'manage_shop_coupon_posts_columns', array( 'cb' => '<input type="checkbox" />' ) );
+
+ $this->assertSame(
+ array( 'cb', 'coupon_code', 'type', 'amount', 'description', 'products', 'usage', 'expiry_date' ),
+ array_keys( $columns ),
+ 'The coupons list should replace the default post columns with the coupon ones.'
+ );
+ }
+
+ /**
+ * @testdox A coupon row renders its code as the edit link, its type, amount, description, product IDs, usage against the limit and expiry date.
+ */
+ public function test_renders_a_coupon_row(): void {
+ $product = WC_Helper_Product::create_simple_product();
+ $coupon = WC_Helper_Coupon::create_coupon( 'list-table-coupon' );
+ $coupon->set_discount_type( 'percent' );
+ $coupon->set_amount( 15 );
+ $coupon->set_description( 'Spring sale' );
+ $coupon->set_product_ids( array( $product->get_id() ) );
+ $coupon->set_usage_limit( 5 );
+ $coupon->set_usage_count( 2 );
+ $coupon->set_date_expires( '2030-03-04' );
+ $coupon->save();
+
+ $code_column = $this->render_column( 'coupon_code', $coupon->get_id() );
+ $this->assertStringContainsString( '>list-table-coupon</a>', $code_column, 'The code column should print the coupon code as the link text.' );
+ $this->assertStringContainsString( 'post=' . $coupon->get_id(), $code_column, 'The code column should link to the coupon edit screen.' );
+
+ $this->assertSame( 'Percentage discount', $this->render_column( 'type', $coupon->get_id() ) );
+ $this->assertSame( '15', $this->render_column( 'amount', $coupon->get_id() ) );
+ $this->assertSame( 'Spring sale', $this->render_column( 'description', $coupon->get_id() ) );
+ $this->assertSame( (string) $product->get_id(), $this->render_column( 'products', $coupon->get_id() ) );
+ $this->assertSame( '2 / 5', $this->render_column( 'usage', $coupon->get_id() ) );
+ $this->assertSame( 'March 4, 2030', $this->render_column( 'expiry_date', $coupon->get_id() ) );
+ }
+
+ /**
+ * @testdox A coupon with no limit, description, products or expiry date renders an infinity sign for the limit and a dash for the rest.
+ */
+ public function test_renders_placeholders_for_an_unrestricted_coupon(): void {
+ $coupon = WC_Helper_Coupon::create_coupon( 'unrestricted-coupon' );
+ $coupon->set_description( '' );
+ $coupon->set_product_ids( array() );
+ $coupon->set_usage_limit( 0 );
+ $coupon->set_date_expires( null );
+ $coupon->save();
+
+ $this->assertSame( "0 / \u{221E}", $this->render_column_text( 'usage', $coupon->get_id() ) );
+ $this->assertSame( "\u{2013}", $this->render_column_text( 'description', $coupon->get_id() ) );
+ $this->assertSame( "\u{2013}", $this->render_column_text( 'products', $coupon->get_id() ) );
+ $this->assertSame( "\u{2013}", $this->render_column_text( 'expiry_date', $coupon->get_id() ) );
+ }
+
+ /**
+ * Render one column of a coupon row with HTML entities decoded.
+ *
+ * The description column goes through wp_kses_post(), which prints `–` as `–` from WordPress 7.2, so compare decoded text.
+ *
+ * @param string $column Column name.
+ * @param int $coupon_id Coupon ID.
+ * @return string
+ */
+ private function render_column_text( string $column, int $coupon_id ): string {
+ return html_entity_decode( $this->render_column( $column, $coupon_id ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
+ }
+
+ /**
+ * Render one column of a coupon row through the hook the posts list table fires.
+ *
+ * @param string $column Column name.
+ * @param int $coupon_id Coupon ID.
+ * @return string
+ */
+ private function render_column( string $column, int $coupon_id ): string {
+ $GLOBALS['post'] = get_post( $coupon_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+
+ ob_start();
+ try {
+ do_action( 'manage_shop_coupon_posts_custom_column', $column, $coupon_id );
+ } finally {
+ $output = ob_get_clean();
+ }
+
+ return trim( $output );
+ }
+}