Commit 39e0982946c for woocommerce
commit 39e0982946c231b560afbba0fb2c0ed099926d4c
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Tue Sep 15 17:06:20 2026 +0300
[tests] Reduce admin smoke E2E tests from 27 to 6 (#68619)
* test(core): Reduce admin smoke E2E tests from 27 to 6
Two specs in tests/e2e/tests/basic ran twenty-seven browser titles
over the WooCommerce admin's entry surface. Twenty-four of them
opened one admin destination each, in a separate test with its own
page load, and three checked that a customer is redirected away
from the WP Dashboard.
The twenty-four destinations become four titles, one per top-level
menu, each walking its own submenu as named steps: the click, the
heading, the sentinel and its text are all kept, and each step now
also asserts that the link is visible before it is clicked.
Which requests a customer is redirected away from, and which
scripts are exempt, is decided in PHP without a browser. Move that
to class-wc-admin-test.php, which goes from one test to seven and
creates a real My Account page so the redirect target is a page
rather than the site root. Keep two browser titles for the redirect
itself: from the WP Admin home, and through the ajax query
parameter.
Consolidates the mega-branch slices:
- Slice 033: test(e2e): Consolidate admin page-load smoke checks
- Slice 090: test(admin): Consolidate dashboard access coverage
- test(e2e): Expose missing admin submenu links, which is in neither
registry and touches only the page-loads spec
Refs TESTOPS-288
Refs #68046
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(admin): Stop the WC_Admin test restoring base-class state
setUp() snapshotted $_GET and the current user and tearDown() put them
back, removed the redirect filter, restored woocommerce_myaccount_page_id
and deleted the page it had created. clean_up_global_scope() empties
$_GET before every test, tear_down() rolls back the option and the page
row, restores the hooks and resets the current user.
$_SERVER is the exception and stays: the base class only resets it when
WP_RUN_CORE_TESTS is defined, which never happens for this suite, and
these tests write SCRIPT_FILENAME.
Verified by running the whole PHPUnit suite in one process with the
change in place: 15105 tests, 58686 assertions, 56 pre-existing skips,
no failures. A leak would have surfaced in a later class.
Refs TESTOPS-288
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(admin): Do not blank $_SERVER when setUp fails before capturing it
tearDown restored $_SERVER unconditionally from a property that setUp
assigns after parent::setUp() and the WC_Admin construction. PHPUnit sets
hasMetRequirements before running the before hooks and gates the after
hooks on that flag alone, so tearDown runs even when setUp threw -- and
in that case the property still held its empty-array initialiser, so
$_SERVER was wiped for the rest of the process. Nothing puts it back:
the base class only resets $_SERVER under WP_RUN_CORE_TESTS, which
WooCommerce never defines.
The property is now nullable and the restore is skipped when the capture
never happened.
Also record why the wc-ajax provider row is there. prevent_admin_access()
never reads $_GET -- wc-ajax is handled by WC_AJAX on init, well before
admin_init -- so that row takes the same branch as the one above it
today. It earns its place by failing if someone later adds the exemption
inside the handler instead of upstream, and the comment now says so
rather than leaving it looking like duplicated coverage.
Suite unchanged at 15105 tests and 58686 assertions.
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-admin-smoke b/plugins/woocommerce/changelog/testops-288-admin-smoke
new file mode 100644
index 00000000000..432a6cfec52
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-288-admin-smoke
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Reduce admin smoke E2E tests from 27 titles to 6; PHPUnit owns the customer redirect policy and the 24 admin destinations become four menu journeys with named steps.
+
diff --git a/plugins/woocommerce/tests/e2e/tests/basic/dashboard-access.spec.ts b/plugins/woocommerce/tests/e2e/tests/basic/dashboard-access.spec.ts
index 6ba7154147a..bdd256f4e85 100644
--- a/plugins/woocommerce/tests/e2e/tests/basic/dashboard-access.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/basic/dashboard-access.spec.ts
@@ -13,7 +13,6 @@ test.describe( 'Customer-role users are blocked from accessing the WP Dashboard.
const dashboardScreens = {
'WP Admin home': 'wp-admin',
- 'WP Admin profile page': 'wp-admin/profile.php',
'WP Admin using ajax query param': 'wp-admin?wc-ajax=1',
};
diff --git a/plugins/woocommerce/tests/e2e/tests/basic/page-loads.spec.ts b/plugins/woocommerce/tests/e2e/tests/basic/page-loads.spec.ts
index 7609a9ddd7f..e9243efd8f9 100644
--- a/plugins/woocommerce/tests/e2e/tests/basic/page-loads.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/basic/page-loads.spec.ts
@@ -187,7 +187,8 @@ const wcPages = [
];
const product = getFakeProduct();
-let orderId: number;
+let productId: number | undefined;
+let orderId: number | undefined;
test.use( { storageState: ADMIN_STATE_PATH } );
@@ -202,104 +203,90 @@ test.beforeAll( async ( { restApi } ) => {
expect( response.status ).toEqual( 200 );
- // create a simple product
- await restApi
- .post( `${ WC_API_PATH }/products`, product )
- .then( ( r ) => {
- product.id = r.data.id;
- } )
- .catch( ( e ) => {
- console.error(
- `Failed to create product ${
- e.data ? JSON.stringify( e.data ) : ''
- }`
- );
- throw e;
- } );
+ const productResponse = await restApi.post(
+ `${ WC_API_PATH }/products`,
+ product
+ );
+ const createdProductId: number = productResponse.data.id;
+ productId = createdProductId;
- // create an order
- await restApi
- .post( `${ WC_API_PATH }/orders`, {
- line_items: [
- {
- product_id: product.id,
- quantity: 1,
- },
- ],
- } )
- .then( ( r ) => {
- orderId = r.data.id;
- } )
- .catch( ( e ) => {
- console.error(
- `Failed to create order ${
- e.data ? JSON.stringify( e.data ) : ''
- }`
- );
- throw e;
- } );
+ const orderResponse = await restApi.post( `${ WC_API_PATH }/orders`, {
+ line_items: [
+ {
+ product_id: createdProductId,
+ quantity: 1,
+ },
+ ],
+ } );
+ orderId = orderResponse.data.id;
} );
test.afterAll( async ( { restApi } ) => {
- await restApi
- .delete( `${ WC_API_PATH }/orders/${ orderId }`, {
- force: true,
- } )
- .catch( ( e ) => {
- console.error(
- `Failed to delete order ${
- e.data ? JSON.stringify( e.data ) : ''
- }`
- );
- throw e;
- } );
- await restApi
- .delete( `${ WC_API_PATH }/products/${ product.id }`, {
- force: true,
- } )
- .catch( ( e ) => {
- console.error(
- `Failed to delete product ${
- e.data ? JSON.stringify( e.data ) : ''
- }`
- );
- throw e;
- } );
+ const cleanupErrors: unknown[] = [];
+
+ if ( orderId !== undefined ) {
+ try {
+ await restApi.delete( `${ WC_API_PATH }/orders/${ orderId }`, {
+ force: true,
+ } );
+ } catch ( error ) {
+ cleanupErrors.push( error );
+ }
+ }
+
+ if ( productId !== undefined ) {
+ try {
+ await restApi.delete( `${ WC_API_PATH }/products/${ productId }`, {
+ force: true,
+ } );
+ } catch ( error ) {
+ cleanupErrors.push( error );
+ }
+ }
+
+ if ( cleanupErrors.length > 0 ) {
+ throw new AggregateError(
+ cleanupErrors,
+ 'Failed to clean up page-load test fixtures.'
+ );
+ }
} );
for ( const currentPage of wcPages ) {
- for ( let i = 0; i < currentPage.subpages.length; i++ ) {
- test( `can load ${ currentPage.name } > ${ currentPage.subpages[ i ].name } page`, async ( {
- page,
- } ) => {
- await page.goto( currentPage.url );
+ test( `can load ${ currentPage.name } pages`, async ( { page } ) => {
+ await page.goto( currentPage.url );
- // needs a Regexp on link name to match exact text and also match the possible counter
- // E.g. should match "Orders 3" or "Orders", but should not match "Quick Orders"
- await page
- .locator( 'li.wp-menu-open > ul.wp-submenu' )
- .getByRole( 'link', {
- name: new RegExp(
- `^${ currentPage.subpages[ i ].name }( \\d+)?$`
- ),
- } )
- .click();
+ for ( const currentSubpage of currentPage.subpages ) {
+ await test.step( currentSubpage.name, async () => {
+ // needs a Regexp on link name to match exact text and also match the possible counter
+ // E.g. should match "Orders 3" or "Orders", but should not match "Quick Orders"
+ const subpageLink = page
+ .locator( 'li.wp-menu-open > ul.wp-submenu' )
+ .getByRole( 'link', {
+ name: new RegExp(
+ `^${ currentSubpage.name }( \\d+)?$`
+ ),
+ } );
- await expect(
- page
- .getByRole( 'heading', {
- name: currentPage.subpages[ i ].heading,
- } )
- .first()
- ).toBeVisible();
+ await expect( subpageLink ).toBeVisible();
+ await subpageLink.click();
- await expect(
- page.locator( currentPage.subpages[ i ].element ).first()
- ).toBeVisible();
+ await expect(
+ page
+ .getByRole( 'heading', {
+ name: currentSubpage.heading,
+ } )
+ .first()
+ ).toBeVisible();
- await expect(
- page.locator( currentPage.subpages[ i ].element )
- ).toContainText( currentPage.subpages[ i ].text );
- } );
- }
+ await expect(
+ page.locator( currentSubpage.element ).first()
+ ).toBeVisible();
+
+ await expect(
+ page.locator( currentSubpage.element )
+ ).toContainText( currentSubpage.text );
+ } );
+ }
+ } );
}
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-test.php
index 4c5b40d2f81..74620178c8c 100644
--- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-test.php
@@ -20,19 +20,42 @@ class WC_Admin_Test extends WC_Unit_Test_Case {
private WC_Admin $sut;
/**
- * Original $_GET.
+ * Original $_SERVER, or null when setUp did not get as far as capturing it.
*
- * @var array<string,mixed>
+ * @var array<string,mixed>|null
*/
- private array $original_get = array();
+ private ?array $original_server = null;
+
+ /**
+ * The My Account page this test creates, so the redirect target is a real page.
+ *
+ * @var int
+ */
+ private int $myaccount_page_id = 0;
/**
* Set up test fixtures.
*/
public function setUp(): void {
parent::setUp();
- $this->sut = new WC_Admin();
- $this->original_get = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $this->sut = new WC_Admin();
+
+ // $_SERVER is the one request global the base class leaves alone: it only resets
+ // it for core's own suite, which never runs here.
+ $this->original_server = $_SERVER;
+
+ // Without a real My Account page, wc_get_page_permalink() falls back to the home URL and
+ // the redirect assertions cannot tell My Account from the site root.
+ $this->myaccount_page_id = self::factory()->post->create(
+ array(
+ 'post_type' => 'page',
+ 'post_status' => 'publish',
+ 'post_title' => 'My Account',
+ 'post_name' => 'my-account',
+ )
+ );
+ update_option( 'woocommerce_myaccount_page_id', $this->myaccount_page_id );
+
add_filter( 'wp_redirect', array( $this, 'intercept_redirect' ) );
}
@@ -40,9 +63,14 @@ class WC_Admin_Test extends WC_Unit_Test_Case {
* Tear down test fixtures.
*/
public function tearDown(): void {
- remove_filter( 'wp_redirect', array( $this, 'intercept_redirect' ) );
- $_GET = $this->original_get; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
- wp_set_current_user( 0 );
+ // PHPUnit runs tearDown whenever checkRequirements() passed, whether or not
+ // setUp finished, so an unconditional restore would blank $_SERVER for the
+ // rest of the process if setUp threw before the capture below it. Nothing
+ // puts it back: the base class only resets $_SERVER for core's own suite.
+ if ( null !== $this->original_server ) {
+ $_SERVER = $this->original_server;
+ }
+
parent::tearDown();
}
@@ -101,4 +129,188 @@ class WC_Admin_Test extends WC_Unit_Test_Case {
$this->assertStringContainsString( 'plugin=woocommerce-gateway-stripe', $e->getMessage() );
}
}
+
+ /**
+ * @testdox Customers are redirected from non-exempt admin scripts.
+ * @dataProvider customer_non_exempt_admin_scripts_provider
+ *
+ * @param string $script_filename Script filename.
+ * @param array<string,mixed> $get Request query parameters.
+ */
+ public function test_prevent_admin_access_redirects_customer_from_non_exempt_script( string $script_filename, array $get ): void {
+ $customer_id = self::factory()->user->create( array( 'role' => 'customer' ) );
+
+ $this->assertSame(
+ get_permalink( $this->myaccount_page_id ),
+ $this->invoke_prevent_admin_access( $customer_id, $script_filename, $get ),
+ 'Customers should be redirected to My Account from non-exempt admin scripts.'
+ );
+ }
+
+ /**
+ * Provides customer request shapes that are not exempt from admin access prevention.
+ *
+ * @return array<string,array{string,array<string,mixed>}>
+ */
+ public function customer_non_exempt_admin_scripts_provider(): array {
+ return array(
+ 'customer ordinary wp-admin index script' => array( '/var/www/html/wp-admin/index.php', array() ),
+ 'customer wp-admin profile script' => array( '/var/www/html/wp-admin/profile.php', array() ),
+ // prevent_admin_access() does not read $_GET at all -- wc-ajax requests are
+ // handled by WC_AJAX on init, long before admin_init -- so today this row
+ // takes the same branch as the one above it. It is here to fail if someone
+ // later exempts wc-ajax inside the handler rather than upstream of it.
+ 'customer ordinary script with wc-ajax query parameter' => array( '/var/www/html/wp-admin/index.php', array( 'wc-ajax' => '1' ) ),
+ );
+ }
+
+ /**
+ * @testdox Customers are not redirected from exempt or incomplete admin request shapes.
+ * @dataProvider customer_exempt_or_incomplete_admin_request_provider
+ *
+ * @param string|null $script_filename Script filename, if present.
+ */
+ public function test_prevent_admin_access_does_not_redirect_customer_from_exempt_or_incomplete_request( ?string $script_filename ): void {
+ $customer_id = self::factory()->user->create( array( 'role' => 'customer' ) );
+
+ $this->assertSame(
+ null,
+ $this->invoke_prevent_admin_access( $customer_id, $script_filename ),
+ 'Exempt or incomplete admin request shapes should not redirect customers.'
+ );
+ }
+
+ /**
+ * Provides customer request shapes that are exempt from admin access prevention.
+ *
+ * @return array<string,array{string|null}>
+ */
+ public function customer_exempt_or_incomplete_admin_request_provider(): array {
+ return array(
+ 'customer admin-post script' => array( '/var/www/html/wp-admin/admin-post.php' ),
+ 'customer admin-ajax script' => array( '/var/www/html/wp-admin/admin-ajax.php' ),
+ 'customer missing SCRIPT_FILENAME server' => array( null ),
+ );
+ }
+
+ /**
+ * @testdox Users with an admin access capability are not redirected.
+ * @dataProvider admin_access_capabilities_provider
+ *
+ * @param string $capability Capability granted to the user.
+ */
+ public function test_prevent_admin_access_does_not_redirect_user_with_admin_access_capability( string $capability ): void {
+ $user_id = self::factory()->user->create( array( 'role' => 'customer' ) );
+ $user = get_user_by( 'id', $user_id );
+ if ( ! $user instanceof WP_User ) {
+ throw new RuntimeException( 'Factory-created user could not be loaded.' );
+ }
+ $user->add_cap( $capability );
+
+ $this->assertSame(
+ null,
+ $this->invoke_prevent_admin_access( $user_id, '/var/www/html/wp-admin/index.php' ),
+ "Users granted {$capability} should not be redirected from an ordinary admin script."
+ );
+ }
+
+ /**
+ * Provides capabilities that allow access to the admin.
+ *
+ * @return array<string,array{string}>
+ */
+ public function admin_access_capabilities_provider(): array {
+ return array(
+ 'user granted edit_posts' => array( 'edit_posts' ),
+ 'user granted manage_woocommerce' => array( 'manage_woocommerce' ),
+ 'user granted view_admin_dashboard' => array( 'view_admin_dashboard' ),
+ );
+ }
+
+ /**
+ * @testdox The disable-admin-bar filter receives true and can suppress customer redirects.
+ */
+ public function test_prevent_admin_access_disable_admin_bar_filter_can_suppress_customer_redirect(): void {
+ $customer_id = self::factory()->user->create( array( 'role' => 'customer' ) );
+ $received = null;
+ $callback = static function ( $disabled ) use ( &$received ) {
+ $received = $disabled;
+ return false;
+ };
+ add_filter( 'woocommerce_disable_admin_bar', $callback );
+
+ try {
+ $this->assertSame( null, $this->invoke_prevent_admin_access( $customer_id, '/var/www/html/wp-admin/index.php' ), 'The disable-admin-bar filter should suppress the customer redirect.' );
+ $this->assertSame( true, $received, 'The disable-admin-bar filter should receive its default true value.' );
+ } finally {
+ remove_filter( 'woocommerce_disable_admin_bar', $callback );
+ }
+ }
+
+ /**
+ * @testdox The prevent-admin-access filter receives computed customer denial and can suppress it.
+ */
+ public function test_prevent_admin_access_filter_can_suppress_computed_customer_denial(): void {
+ $customer_id = self::factory()->user->create( array( 'role' => 'customer' ) );
+ $received = null;
+ $callback = static function ( $prevent_access ) use ( &$received ) {
+ $received = $prevent_access;
+ return false;
+ };
+ add_filter( 'woocommerce_prevent_admin_access', $callback );
+
+ try {
+ $this->assertSame( null, $this->invoke_prevent_admin_access( $customer_id, '/var/www/html/wp-admin/index.php' ), 'The prevent-admin-access filter should suppress the computed customer denial.' );
+ $this->assertSame( true, $received, 'The prevent-admin-access filter should receive the computed customer denial.' );
+ } finally {
+ remove_filter( 'woocommerce_prevent_admin_access', $callback );
+ }
+ }
+
+ /**
+ * @testdox The prevent-admin-access filter can force a redirect from an exempt request.
+ */
+ public function test_prevent_admin_access_filter_can_force_redirect_from_exempt_request(): void {
+ $customer_id = self::factory()->user->create( array( 'role' => 'customer' ) );
+ $received = null;
+ $callback = static function ( $prevent_access ) use ( &$received ) {
+ $received = $prevent_access;
+ return true;
+ };
+ add_filter( 'woocommerce_prevent_admin_access', $callback );
+
+ try {
+ $this->assertSame( get_permalink( $this->myaccount_page_id ), $this->invoke_prevent_admin_access( $customer_id, '/var/www/html/wp-admin/admin-ajax.php' ), 'The prevent-admin-access filter should force an exact My Account redirect from an exempt request.' );
+ $this->assertSame( false, $received, 'The prevent-admin-access filter should receive false for an exempt request.' );
+ } finally {
+ remove_filter( 'woocommerce_prevent_admin_access', $callback );
+ }
+ }
+
+ /**
+ * Invokes prevent_admin_access() with a synthetic request and returns its redirect location.
+ *
+ * @param int $user_id Current user ID.
+ * @param string|null $script_filename Script filename, if present.
+ * @param array<string,mixed> $get Request query parameters.
+ * @return string|null Redirect location, or null when no redirect occurred.
+ */
+ private function invoke_prevent_admin_access( int $user_id, ?string $script_filename, array $get = array() ): ?string {
+ wp_set_current_user( $user_id );
+ $_GET = $get; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ if ( null === $script_filename ) {
+ unset( $_SERVER['SCRIPT_FILENAME'] );
+ } else {
+ $_SERVER['SCRIPT_FILENAME'] = $script_filename;
+ }
+
+ try {
+ $this->sut->prevent_admin_access();
+ } catch ( RuntimeException $e ) {
+ return $e->getMessage();
+ }
+
+ return null;
+ }
}