Commit 9a99124b08e for woocommerce

commit 9a99124b08e7d9074cbcf0c57da0cebacb6b37df
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Tue Sep 15 19:43:18 2026 +0300

    [tests] Demote the auto-generated shipping-class slug test to PHPUnit (#68641)

    * test(shipping): Move generated shipping-class slugs below E2E

    The shipping classes admin spec runs two browser titles from one
    parametrized data set: adding a class with an explicit slug, and
    adding one with a blank slug so the server generates it. Both drive
    the same admin screen through the same steps; the only difference
    between them is whether the slug field is filled.

    What the second title actually proves is a server decision - that
    woocommerce_shipping_classes_save_changes fills a blank slug from
    the name, persists it, and returns it. That does not need a browser.
    class-wc-ajax-test.php gains
    test_shipping_classes_save_changes_generates_slug, which posts the
    save-changes request with a blank slug and asserts the created term,
    the generated slug, the description, and that the response lists the
    new class exactly once.

    The explicit-slug title stays, and it exercises the same admin
    screen and the same rendering the removed one did. What is no longer
    asserted is the rendered result for the blank-slug case
    specifically: that the generated slug appears in the admin table.
    The retained title still asserts that for a slug the test supplies.

    class-wc-ajax-test.php is shared with another batch. This commit
    takes trunk's copy and adds only the one method its own mega-branch
    commit adds; the other four methods in that file's migration delta
    ship separately in #68637. Trunk's copy already has the do_ajax
    helper the new method uses, so it stands alone.

    Two defects in the migrated test are fixed rather than carried. It
    asserted the slug of a term it had just fetched by that slug, which
    cannot fail; that assertion is replaced by one on the response row's
    name, which nothing checked. And WC_Shipping::get_shipping_classes()
    memoizes into a public property that nothing resets, so a warm cache
    would omit the new term from the response and fail the count assertion
    for an unrelated reason, while a cache left warm would show a deleted
    class to later tests in the same process; the test now clears it before
    the request and again in its teardown. The assertion count is unchanged
    at eight.

    The spec also drops an eslint-disable for playwright/valid-title.
    That directive is already unused on trunk - it reports itself as
    unused - so removing it removes a warning rather than adding one,
    and it refers to the parametrized data row this commit changes.

    Consolidates one mega-branch commit, subject verbatim:
    - test(shipping): Move generated slugs below E2E

    Refs TESTOPS-288
    Refs #68046

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    * test(php): Trim the shipping-class test cleanup to state the base leaks

    `WP_Ajax_UnitTestCase::tear_down()` already clears `$_POST` and the
    current user, `clean_up_global_scope()` clears `$_REQUEST` before the
    next test, and the transaction rollback removes the term, so restoring
    any of those by hand did nothing.

    Two things do survive the base lifecycle and stay: `WC_Shipping`
    memoizes the shipping class list on a public property, and the handler
    assigns `$current_tab` and `$current_section` before firing
    `woocommerce_update_options`.

    Refs TESTOPS-288

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/testops-288-shipping-classes b/plugins/woocommerce/changelog/testops-288-shipping-classes
new file mode 100644
index 00000000000..999d5c3f9d9
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-288-shipping-classes
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Move the auto-generated shipping-class slug check from E2E to PHPUnit.
+
diff --git a/plugins/woocommerce/tests/e2e/tests/shipping/shipping-classes.spec.ts b/plugins/woocommerce/tests/e2e/tests/shipping/shipping-classes.spec.ts
index 8a81b16b2e8..5b876808750 100644
--- a/plugins/woocommerce/tests/e2e/tests/shipping/shipping-classes.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/shipping/shipping-classes.spec.ts
@@ -20,13 +20,6 @@ const uq = faker.string.alphanumeric( 5 );
 		description: `Small items that don't cost much to ship ${ uq }`,
 		testTitle: 'can add a shipping class with an unique slug',
 	},
-	{
-		name: `Poster Pack ${ uq }`,
-		slug: '',
-		expectedSlug: `poster-pack-${ uq }`,
-		description: `Posters, stickers, and other flat items ${ uq }`,
-		testTitle: 'can add a shipping class with an auto-generated slug',
-	},
 ].forEach( ( { testTitle, name, slug, expectedSlug, description } ) => {
 	const test = baseTest.extend( {
 		storageState: ADMIN_STATE_PATH,
@@ -48,7 +41,6 @@ const uq = faker.string.alphanumeric( 5 );
 		},
 	} );

-	// eslint-disable-next-line playwright/valid-title -- Title comes from the parametrized test data row.
 	test( testTitle, { tag: [ tags.SERVICES ] }, async ( { page } ) => {
 		await page.goto(
 			'wp-admin/admin.php?page=wc-settings&tab=shipping&section=classes'
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
index 52e47c2c948..80d9edef239 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
@@ -141,6 +141,84 @@ class WC_AJAX_Test extends \WP_Ajax_UnitTestCase {
 		$this->assertEquals( $response['data']['message'], 'There was an error generating your API Key.' );
 	}

+	/**
+	 * @testdox Saving a new shipping class with a blank slug generates and returns its persisted slug.
+	 */
+	public function test_shipping_classes_save_changes_generates_slug(): void {
+		$had_current_tab          = array_key_exists( 'current_tab', $GLOBALS );
+		$had_current_section      = array_key_exists( 'current_section', $GLOBALS );
+		$original_current_tab     = $GLOBALS['current_tab'] ?? null;
+		$original_current_section = $GLOBALS['current_section'] ?? null;
+		$name                     = 'Poster Pack ' . wp_unique_id();
+		$expected_slug            = sanitize_title( $name );
+		$description              = 'Posters, stickers, and other flat items.';
+
+		try {
+			$this->_setRole( 'administrator' );
+			$_POST    = array(
+				'wc_shipping_classes_nonce' => wp_create_nonce( 'wc_shipping_classes_nonce' ),
+				'changes'                   => array(
+					'new-row' => array(
+						'newRow'      => true,
+						'name'        => $name,
+						'slug'        => '',
+						'description' => $description,
+					),
+				),
+			);
+			$_REQUEST = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Test installs the real nonce immediately above.
+
+			// WC_Shipping::get_shipping_classes() memoizes into a public property that nothing
+			// resets, so a warm cache would omit the new term from the response.
+			WC_Shipping::instance()->shipping_classes = array();
+
+			$response = $this->do_ajax( 'woocommerce_shipping_classes_save_changes' );
+			$this->assertTrue( $response['success'] ?? false, 'The registered AJAX action should report success.' );
+
+			$term = get_term_by( 'slug', $expected_slug, 'product_shipping_class' );
+			$this->assertInstanceOf( WP_Term::class, $term, 'The blank-slug request should create a shipping-class term.' );
+			if ( ! $term instanceof WP_Term ) {
+				throw new RuntimeException( 'The shipping-class term could not be reloaded.' );
+			}
+			$term_id = $term->term_id;
+
+			$this->assertSame( $name, $term->name );
+			$this->assertSame( $description, $term->description );
+
+			$response_rows = $response['data']['shipping_classes'] ?? array();
+			$matching_rows = array_values(
+				array_filter(
+					$response_rows,
+					static fn ( array $row ): bool => (int) ( $row['term_id'] ?? 0 ) === $term_id
+				)
+			);
+			$this->assertCount( 1, $matching_rows, 'The AJAX response should contain the new shipping class exactly once.' );
+			$this->assertSame( $expected_slug, $matching_rows[0]['slug'] );
+			$this->assertSame( $name, $matching_rows[0]['name'] );
+			$this->assertSame( $description, $matching_rows[0]['description'] );
+		} finally {
+			// Only state the base lifecycle does not own is restored here.
+			// `WP_Ajax_UnitTestCase::tear_down()` clears `$_POST` and the current
+			// user, `clean_up_global_scope()` clears `$_REQUEST` before the next
+			// test, and the transaction rollback removes the term. What survives
+			// is the shipping class list, which `WC_Shipping` memoizes on a public
+			// property, and the two settings globals the handler assigns before it
+			// fires `woocommerce_update_options`.
+			WC_Shipping::instance()->shipping_classes = array();
+
+			if ( $had_current_tab ) {
+				$GLOBALS['current_tab'] = $original_current_tab;
+			} else {
+				unset( $GLOBALS['current_tab'] );
+			}
+			if ( $had_current_section ) {
+				$GLOBALS['current_section'] = $original_current_section;
+			} else {
+				unset( $GLOBALS['current_section'] );
+			}
+		}
+	}
+
 	/**
 	 * Test to verify that term color is saved in AJAX calls, but only for terms belonging to a visual attribute.
 	 *