From e3ccc023646cf2c13463dd9e56667e6558ec449e Mon Sep 17 00:00:00 2001 From: anamwp Date: Fri, 28 Aug 2026 10:41:24 +0600 Subject: [PATCH 1/5] Build/Test Tools: Fix `install.test.js` leaving behind `wp_e2e_` tables that break subsequent test runs. `afterEach` reverted `wp-config.php`'s table prefix but never dropped the `wp_e2e_*` tables the test's own install wizard creates, so any run after the first found a pre-existing install and never reached the installer. Also retries the initial navigation itself, since a single `page.goto('/')` right after the config swap can occasionally observe a stale config on some hosts. See https://core.trac.wordpress.org/ticket/65982 --- tests/e2e/specs/install.test.js | 40 ++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/tests/e2e/specs/install.test.js b/tests/e2e/specs/install.test.js index cc237b81452a2..2df1dd1155c1e 100644 --- a/tests/e2e/specs/install.test.js +++ b/tests/e2e/specs/install.test.js @@ -2,6 +2,7 @@ * External dependencies */ import { writeFileSync, readFileSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; import { join } from 'node:path'; /** @@ -29,15 +30,42 @@ test.describe( 'WordPress installation process', () => { test.afterEach( async () => { writeFileSync( wpConfig, wpConfigOriginal ); + + // The test completes a full install under the `wp_e2e_` prefix. Drop those + // tables, otherwise the next run finds a pre-existing install and never + // reaches the installation screen it's meant to be testing. + const tables = [ + 'commentmeta', 'comments', 'links', 'options', 'postmeta', 'posts', + 'term_relationships', 'term_taxonomy', 'termmeta', 'terms', 'usermeta', 'users', + ]; + const dropTablesPhp = tables + .map( ( table ) => `global $wpdb; $wpdb->query( "DROP TABLE IF EXISTS wp_e2e_${ table }" );` ) + .join( ' ' ); + + execFileSync( + process.execPath, + [ + join( process.cwd(), 'tools/local-env/scripts/docker.js' ), + 'exec', + '--user', + 'wp_php', + 'cli', + 'wp', + 'eval', + dropTablesPhp, + ], + { stdio: 'inherit' } + ); } ); test( 'should install WordPress with pre-existing database credentials', async ( { page } ) => { - await page.goto( '/' ); - - await expect( - page, - 'should redirect to the installation page' - ).toHaveURL( /wp-admin\/install\.php$/ ); + // The config file was just rewritten on the host; retry the navigation + // (not just the URL check) since the container's view of the file can + // lag behind the write by a request or two. + await expect( async () => { + await page.goto( '/' ); + expect( page.url() ).toMatch( /wp-admin\/install\.php$/ ); + }, 'should redirect to the installation page' ).toPass( { timeout: 10_000 } ); await expect( page.getByText( /WordPress database error/ ), From f6debd6e15e0c7f38fdf8ac6afbd4a121a9b628b Mon Sep 17 00:00:00 2001 From: anamwp Date: Sat, 29 Aug 2026 16:45:34 +0600 Subject: [PATCH 2/5] Build/Test Tools: Extract the e2e test table prefix into a single constant. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review feedback on the Trac ticket, avoid repeating the `wp_e2e_` literal in both the config rewrite and the cleanup query — use one `TEST_TABLE_PREFIX` constant instead so the two can't drift out of sync. See https://core.trac.wordpress.org/ticket/65982 --- tests/e2e/specs/install.test.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/e2e/specs/install.test.js b/tests/e2e/specs/install.test.js index 2df1dd1155c1e..9e996a28e212e 100644 --- a/tests/e2e/specs/install.test.js +++ b/tests/e2e/specs/install.test.js @@ -12,6 +12,11 @@ import { test, expect } from '@wordpress/e2e-test-utils-playwright'; let wpConfigOriginal; +// The prefix used to trick WP into "not installed" mode. Kept as a single +// constant since it has to stay in sync between the config rewrite and the +// cleanup query below. +const TEST_TABLE_PREFIX = 'wp_e2e_'; + test.describe( 'WordPress installation process', () => { const wpConfig = join( process.cwd(), @@ -24,22 +29,22 @@ test.describe( 'WordPress installation process', () => { // Changing the table prefix tricks WP into new install mode. writeFileSync( wpConfig, - wpConfigOriginal.replace( `$table_prefix = 'wp_';`, `$table_prefix = 'wp_e2e_';` ) + wpConfigOriginal.replace( `$table_prefix = 'wp_';`, `$table_prefix = '${ TEST_TABLE_PREFIX }';` ) ); } ); test.afterEach( async () => { writeFileSync( wpConfig, wpConfigOriginal ); - // The test completes a full install under the `wp_e2e_` prefix. Drop those - // tables, otherwise the next run finds a pre-existing install and never - // reaches the installation screen it's meant to be testing. + // The test completes a full install under the `TEST_TABLE_PREFIX`. Drop + // those tables, otherwise the next run finds a pre-existing install and + // never reaches the installation screen it's meant to be testing. const tables = [ 'commentmeta', 'comments', 'links', 'options', 'postmeta', 'posts', 'term_relationships', 'term_taxonomy', 'termmeta', 'terms', 'usermeta', 'users', ]; const dropTablesPhp = tables - .map( ( table ) => `global $wpdb; $wpdb->query( "DROP TABLE IF EXISTS wp_e2e_${ table }" );` ) + .map( ( table ) => `global $wpdb; $wpdb->query( "DROP TABLE IF EXISTS ${ TEST_TABLE_PREFIX }${ table }" );` ) .join( ' ' ); execFileSync( From b02645fdcb9557deac544846e0a2ab358dfe2422 Mon Sep 17 00:00:00 2001 From: anamwp Date: Thu, 3 Sep 2026 17:42:09 +0600 Subject: [PATCH 3/5] Build/Test Tools: Clean up e2e-test tables before the run too, and fail loudly if cleanup fails. Per review feedback on the Trac ticket: - Extract the `wp_e2e_*` table cleanup into a `dropE2eTables()` helper, used from both `beforeEach` and `afterEach` instead of duplicating it. - Run it before the prefix swap as well as after. Previously, stale tables left behind by an interrupted prior run caused this test to fail once and only self-heal on the next run, once `afterEach` caught up. - Check each `DROP TABLE`'s result and call `WP_CLI::error()` on failure. `wp eval` exits 0 even when a `$wpdb->query()` call inside it returns false, so a failed cleanup was previously invisible and could leave a broken install in place while still reporting green. See https://core.trac.wordpress.org/ticket/65982 --- tests/e2e/specs/install.test.js | 74 ++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/tests/e2e/specs/install.test.js b/tests/e2e/specs/install.test.js index 9e996a28e212e..2bbf58e519804 100644 --- a/tests/e2e/specs/install.test.js +++ b/tests/e2e/specs/install.test.js @@ -17,6 +17,49 @@ let wpConfigOriginal; // cleanup query below. const TEST_TABLE_PREFIX = 'wp_e2e_'; +const TEST_TABLES = [ + 'commentmeta', 'comments', 'links', 'options', 'postmeta', 'posts', + 'term_relationships', 'term_taxonomy', 'termmeta', 'terms', 'usermeta', 'users', +]; + +/** + * Drops any tables left over under TEST_TABLE_PREFIX. + * + * Called both before the prefix swap (in case a previous run crashed and + * left stale tables behind — otherwise this run fails once and only + * "self-heals" on the next run) and after it (so the next run starts clean). + * + * `wp eval` exits 0 even when a `$wpdb->query()` call inside it returns + * false, so each DROP's result is checked explicitly and reported via + * `WP_CLI::error()`, which does set a non-zero exit code — otherwise a + * failed cleanup would silently report as a passing test. + */ +function dropE2eTables() { + const dropTablesPhp = TEST_TABLES + .map( ( table ) => ` + $result = $wpdb->query( "DROP TABLE IF EXISTS ${ TEST_TABLE_PREFIX }${ table }" ); + if ( false === $result ) { + WP_CLI::error( "Failed to drop ${ TEST_TABLE_PREFIX }${ table }: {$wpdb->last_error}" ); + } + ` ) + .join( '' ); + + execFileSync( + process.execPath, + [ + join( process.cwd(), 'tools/local-env/scripts/docker.js' ), + 'exec', + '--user', + 'wp_php', + 'cli', + 'wp', + 'eval', + `global $wpdb; ${ dropTablesPhp }`, + ], + { stdio: 'inherit' } + ); +} + test.describe( 'WordPress installation process', () => { const wpConfig = join( process.cwd(), @@ -25,6 +68,8 @@ test.describe( 'WordPress installation process', () => { test.beforeEach( async () => { + dropE2eTables(); + wpConfigOriginal = readFileSync( wpConfig, 'utf-8' ); // Changing the table prefix tricks WP into new install mode. writeFileSync( @@ -36,31 +81,10 @@ test.describe( 'WordPress installation process', () => { test.afterEach( async () => { writeFileSync( wpConfig, wpConfigOriginal ); - // The test completes a full install under the `TEST_TABLE_PREFIX`. Drop - // those tables, otherwise the next run finds a pre-existing install and - // never reaches the installation screen it's meant to be testing. - const tables = [ - 'commentmeta', 'comments', 'links', 'options', 'postmeta', 'posts', - 'term_relationships', 'term_taxonomy', 'termmeta', 'terms', 'usermeta', 'users', - ]; - const dropTablesPhp = tables - .map( ( table ) => `global $wpdb; $wpdb->query( "DROP TABLE IF EXISTS ${ TEST_TABLE_PREFIX }${ table }" );` ) - .join( ' ' ); - - execFileSync( - process.execPath, - [ - join( process.cwd(), 'tools/local-env/scripts/docker.js' ), - 'exec', - '--user', - 'wp_php', - 'cli', - 'wp', - 'eval', - dropTablesPhp, - ], - { stdio: 'inherit' } - ); + // The test completes a full install under TEST_TABLE_PREFIX. Drop + // those tables, otherwise the next run finds a pre-existing install + // and never reaches the installation screen it's meant to be testing. + dropE2eTables(); } ); test( 'should install WordPress with pre-existing database credentials', async ( { page } ) => { From 344fd9e83b021f60d9dc9060a1f1378a5e9fb50c Mon Sep 17 00:00:00 2001 From: anamwp Date: Thu, 3 Sep 2026 17:56:33 +0600 Subject: [PATCH 4/5] Build/Test Tools: Avoid masking a cleanup failure with a second crash in afterEach. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while manually testing the previous commit's fail-loudly behavior: if the pre-test dropE2eTables() call in beforeEach throws, wpConfigOriginal was never assigned, so Playwright's unconditional afterEach then crashed with `TypeError: ... Received undefined` trying to restore wp-config.php from it — masking the real cleanup error behind a confusing second one. Read wpConfigOriginal before the cleanup call instead, since that read is side-effect-free and can't itself be the thing that's failing. See https://core.trac.wordpress.org/ticket/65982 --- tests/e2e/specs/install.test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/e2e/specs/install.test.js b/tests/e2e/specs/install.test.js index 2bbf58e519804..7c95bd0f244d4 100644 --- a/tests/e2e/specs/install.test.js +++ b/tests/e2e/specs/install.test.js @@ -68,9 +68,14 @@ test.describe( 'WordPress installation process', () => { test.beforeEach( async () => { + // Read this before the cleanup call below, which can throw. Otherwise, + // if it does, `afterEach` runs anyway (Playwright always runs it) and + // crashes trying to restore `wp-config.php` from an unset variable, + // masking the real cleanup error behind a confusing second one. + wpConfigOriginal = readFileSync( wpConfig, 'utf-8' ); + dropE2eTables(); - wpConfigOriginal = readFileSync( wpConfig, 'utf-8' ); // Changing the table prefix tricks WP into new install mode. writeFileSync( wpConfig, From c49c4c89b1700c65ead640a420f81ed6f4dfcfbb Mon Sep 17 00:00:00 2001 From: anamwp Date: Thu, 3 Sep 2026 22:53:04 +0600 Subject: [PATCH 5/5] Build/Test Tools: Fail loudly if wp-config.php is already stranded on the test prefix. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review feedback: beforeEach's dropE2eTables() call closed the leftover- tables failure mode, but opened a subtler one. If a run is killed after the prefix rewrite but before afterEach restores it, wp-config.php is left on TEST_TABLE_PREFIX. On the next run, `.replace()` finds no `$table_prefix = 'wp_';` to match and silently no-ops, dropE2eTables() has already cleared the tables, so the site looks freshly uninstalled under the already- stranded prefix — the test passes while the checkout stays broken. Before this PR the same state failed loudly, since the leftover tables blocked the redirect; the new cleanup made it silent instead. Now throws explicitly when the replace is a no-op. wpConfigOriginal is already read before this point, so afterEach still restores cleanly rather than crashing a second time. CI is unaffected, since env:install regenerates wp-config.php via `wp config create --force` on every run — this only matters for a local checkout that survives between runs. See https://core.trac.wordpress.org/ticket/65982 --- tests/e2e/specs/install.test.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/e2e/specs/install.test.js b/tests/e2e/specs/install.test.js index 7c95bd0f244d4..754e2b7562be6 100644 --- a/tests/e2e/specs/install.test.js +++ b/tests/e2e/specs/install.test.js @@ -77,10 +77,24 @@ test.describe( 'WordPress installation process', () => { dropE2eTables(); // Changing the table prefix tricks WP into new install mode. - writeFileSync( - wpConfig, - wpConfigOriginal.replace( `$table_prefix = 'wp_';`, `$table_prefix = '${ TEST_TABLE_PREFIX }';` ) + const wpConfigPatched = wpConfigOriginal.replace( + `$table_prefix = 'wp_';`, + `$table_prefix = '${ TEST_TABLE_PREFIX }';` ); + + // A prior run killed after this rewrite but before `afterEach` restores + // it leaves wp-config.php stranded on TEST_TABLE_PREFIX. `.replace()` + // then silently no-ops instead of throwing, and since the tables were + // just cleared above, the site looks freshly uninstalled under the + // already-stranded prefix — the test would pass while leaving the + // checkout stuck. Fail loudly here instead. + if ( wpConfigPatched === wpConfigOriginal ) { + throw new Error( + `wp-config.php does not contain the default table prefix. An interrupted run may have left it on '${ TEST_TABLE_PREFIX }'.` + ); + } + + writeFileSync( wpConfig, wpConfigPatched ); } ); test.afterEach( async () => {