Skip to content

Commit f0c6398

Browse files
authored
fix(e2e): repair new-post spec and add Playwright E2E job to CI (#1873)
* fix(e2e): use editor helpers and dismiss welcome dialog in new-post spec The editor-new-post E2E spec was failing on develop because: - Gutenberg's first-visit welcome dialog blocked the title locator. - The publish flow is now two-step (top bar + pre-publish panel). - The obsolete meta-box-loader response wait timed out (no meta boxes registered, so that request never fires). - The Stream plugin is deactivated by the shared setup, so post-publish was not being logged. Refactor to use the official @wordpress/e2e-test-utils-playwright Editor helpers (setContent, publishPost), reactivate Stream in beforeAll, and deactivate it again in afterAll to keep the network spec's preconditions. Closes #1872 * ci: add Playwright E2E job to lint-and-test workflow Runs the Playwright E2E suite against the Dockerized WordPress stack after the lint job succeeds. The job: - maps stream.wpenv.net to localhost so the existing hardcoded test URLs resolve; - installs Playwright's Chromium with system deps; - builds the plugin so the mounted build/ directory contains the current assets; - starts the Docker stack, waits for MySQL and HTTP readiness, then runs wp core multisite-install; - runs npm run test-e2e; - uploads the playwright-report directory as a workflow artifact on both success and failure for easier triage. * ci: fix MySQL wait, bump login-action, opt actions into Node 24, set timeouts - Use mariadb-admin instead of mysqladmin in the readiness probe; the mariadb:11.4.2 image only ships the mariadb-admin binary. - Bump docker/login-action from v2 to v3 to drop a Node 20 runtime. - Set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true at the workflow level so the remaining v4 JavaScript actions execute under Node 24 ahead of the June 2026 forced switch. - Add timeout-minutes to both jobs (lint 10, e2e 12) so a hung step fails fast instead of consuming the default 6h limit. * ci: add diagnostics to the WordPress HTTP wait step Print the HTTP status on each attempt and dump 'docker compose ps' plus recent wordpress container logs on timeout so the next CI failure gives us something actionable. * ci: install Composer deps in the E2E job so wp-cli is available The WordPress container's PATH includes the project's vendor/bin directory, where wp-cli/wp-cli-bundle installs the 'wp' binary. Without composer install, 'docker compose run wordpress wp ...' fails with 'wp: not found'. * ci: probe MySQL with a real connection instead of mariadb-admin ping mariadb-admin ping returns success as soon as the server process is up, even while the server is still refusing TCP connections during init. Run an actual 'SELECT 1' as the wordpress user against the wordpress database so the wait reflects what wp-cli will see. * ci: use xwp/wait-for for MySQL and WordPress readiness probes The project already ships xwp/wait-for as a dev dependency. Replace the hand-rolled curl/docker exec loops with a single PHP one-liner that uses XWP\\Wait_For\\Tcp_Connection to verify both services are accepting TCP connections, and dump container state in a separate on-failure step. * ci: wait for MySQL from inside the wordpress container The host-mapped 3306 port becomes available before the in-network 'mysql' hostname is reachable from a freshly created sibling container. wp-cli runs inside a one-shot 'docker compose run wordpress' which connects over the docker network, so the readiness check needs to happen there too. Use 'npm run cli -- php -r ...' to run the wait-for probe in the same context wp-cli will use. * ci: run container-state dump after install/e2e steps Previously the diagnostic step sat between wait-for and install-wordpress, so it only triggered on a wait-for failure. Move it after the e2e step so any failure in install-wordpress or test-e2e also dumps state.
1 parent 7e8a23e commit f0c6398

2 files changed

Lines changed: 159 additions & 29 deletions

File tree

‎.github/workflows/lint-and-test.yml‎

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ name: Lint and Test
22

33
on: [push, workflow_call]
44

5+
env:
6+
# Opt into Node.js 24 for JavaScript actions that still ship a Node 20 runtime.
7+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
8+
59
jobs:
610

711
lint:
812
name: Lint and Test
913
runs-on: ubuntu-22.04
14+
timeout-minutes: 10
1015
permissions:
1116
contents: read
1217
packages: read
@@ -39,7 +44,7 @@ jobs:
3944
restore-keys: ${{ runner.os }}-php-
4045

4146
- name: Log in to the Container registry
42-
uses: docker/login-action@v2
47+
uses: docker/login-action@v3
4348
with:
4449
registry: ghcr.io
4550
username: ${{ github.actor }}
@@ -62,3 +67,104 @@ jobs:
6267

6368
- name: Test
6469
run: npm run test
70+
71+
e2e:
72+
name: E2E
73+
runs-on: ubuntu-22.04
74+
timeout-minutes: 12
75+
needs: lint
76+
permissions:
77+
contents: read
78+
packages: read
79+
steps:
80+
81+
- name: Checkout
82+
uses: actions/checkout@v4
83+
84+
- name: Setup Node
85+
uses: actions/setup-node@v4
86+
with:
87+
node-version-file: '.nvmrc'
88+
cache: 'npm'
89+
90+
- name: Setup PHP
91+
uses: shivammathur/setup-php@v2
92+
with:
93+
php-version: '8.1'
94+
tools: composer:v2
95+
96+
- name: Get Composer cache directory
97+
id: composer-cache
98+
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
99+
100+
- name: Cache Composer packages
101+
uses: actions/cache@v4
102+
with:
103+
path: ${{ steps.composer-cache.outputs.dir }}
104+
key: ${{ runner.os }}-php-${{ hashFiles( 'composer.lock' ) }}
105+
restore-keys: ${{ runner.os }}-php-
106+
107+
- name: Map stream.wpenv.net to localhost
108+
run: echo "127.0.0.1 stream.wpenv.net" | sudo tee -a /etc/hosts
109+
110+
- name: Log in to the Container registry
111+
uses: docker/login-action@v3
112+
with:
113+
registry: ghcr.io
114+
username: ${{ github.actor }}
115+
password: ${{ secrets.GITHUB_TOKEN }}
116+
117+
- name: Install NPM dependencies
118+
run: npm install
119+
120+
- name: Install Composer dependencies
121+
run: composer install
122+
123+
- name: Install Playwright browsers
124+
run: npx playwright install --with-deps chromium
125+
126+
- name: Build
127+
run: npm run build
128+
129+
- name: Pull custom Docker images
130+
run: docker compose pull wordpress
131+
132+
- name: Start WordPress
133+
run: npm run start
134+
135+
- name: Wait for MySQL (from inside the wordpress container)
136+
run: |
137+
npm run cli -- php -r '
138+
require "/var/www/html/wp-content/plugins/stream-src/vendor/autoload.php";
139+
( new XWP\Wait_For\Tcp_Connection( "mysql", 3306 ) )->connect( 120 );
140+
echo "MySQL is accepting connections on mysql:3306.\n";
141+
'
142+
143+
- name: Wait for WordPress (from the runner)
144+
run: |
145+
php -r '
146+
require "vendor/autoload.php";
147+
( new XWP\Wait_For\Tcp_Connection( "127.0.0.1", 80 ) )->connect( 60 );
148+
echo "WordPress is accepting connections on 127.0.0.1:80.\n";
149+
'
150+
151+
- name: Install WordPress multisite
152+
run: npm run install-wordpress
153+
154+
- name: Run E2E tests
155+
run: npm run test-e2e
156+
157+
- name: Dump container state on failure
158+
if: failure()
159+
run: |
160+
docker compose ps || true
161+
docker compose logs --tail=80 mysql || true
162+
docker compose logs --tail=80 wordpress || true
163+
164+
- name: Upload Playwright report
165+
if: ${{ !cancelled() }}
166+
uses: actions/upload-artifact@v4
167+
with:
168+
name: playwright-report
169+
path: playwright-report
170+
retention-days: 7

‎tests/e2e/editor-new-post.spec.js‎

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,85 @@
11
/**
22
* WordPress dependencies
33
*/
4-
import { test, expect } from '@wordpress/e2e-test-utils-playwright';
4+
import { test, expect, Editor } from '@wordpress/e2e-test-utils-playwright';
55

66
test.describe( 'Editor: saving a new post', () => {
7-
let page, postTitle, postId;
7+
let page, editor, postTitle, postId;
88

99
test.beforeAll( async ( { browser } ) => {
10+
test.setTimeout( 90_000 );
11+
1012
page = await browser.newPage();
13+
editor = new Editor( { page } );
1114

1215
postTitle = `Test Post ${ crypto.randomUUID() }`; // Sometimes this runs more than once within a microsecond so it's a UUID.
1316

1417
// eslint-disable-next-line no-console
1518
console.log( `New post ${ postTitle }` );
1619

17-
// Even though we're using WP's npm package, it's more straightforward to do it this way, at least for me in this environment.
20+
// The shared setup deactivates Stream. Reactivate it so the publish action is logged.
21+
await page.goto( 'http://stream.wpenv.net/wp-admin/network/plugins.php' );
22+
const activateLink = page.getByLabel( 'Network Activate Stream' );
23+
if ( await activateLink.isVisible().catch( () => false ) ) {
24+
await activateLink.click();
25+
await page.waitForURL( /plugins\.php/ );
26+
}
27+
1828
await page.goto( 'http://stream.wpenv.net/wp-admin/post-new.php' );
19-
await page.getByLabel( 'Add title' ).click();
20-
await page.getByLabel( 'Add title' ).fill( postTitle );
21-
await page.getByLabel( 'Add title' ).press( 'Tab' );
22-
await page.getByLabel( 'Empty block; start writing or' ).fill( 'I\'m a test post' );
23-
await page.getByRole( 'button', { name: 'Publish', exact: true } ).click();
29+
30+
// Wait for Gutenberg to be ready.
31+
await page.waitForFunction( () => window?.wp?.blocks && window?.wp?.data );
32+
33+
// Dismiss the welcome dialog if it appears.
34+
const welcomeClose = page.getByRole( 'dialog', { name: 'Welcome to the editor' } ).getByRole( 'button', { name: 'Close' } );
35+
if ( await welcomeClose.isVisible().catch( () => false ) ) {
36+
await welcomeClose.click();
37+
}
38+
39+
// Set the title via the data layer, and the body via the editor helper.
40+
await page.evaluate( ( title ) => {
41+
window.wp.data.dispatch( 'core/editor' ).editPost( { title } );
42+
}, postTitle );
43+
await editor.setContent( '<!-- wp:paragraph --><p>I\'m a test post</p><!-- /wp:paragraph -->' );
44+
45+
// Publish; the helper handles the pre-publish panel and waits for the published snackbar.
46+
await editor.publishPost();
2447

2548
postId = await page.locator( 'input#post_ID' ).inputValue();
2649

2750
// eslint-disable-next-line no-console
2851
console.log( `Post ID: ${ postId }` );
2952

30-
// We need to wait for both of the editor responses! The post saves to the posts table then the metadata saves to postmeta.
31-
await Promise.all( [
32-
page.waitForResponse( ( resp ) => resp.url().includes( 'meta-box-loader' ) && resp.status() === 302 ),
33-
page.waitForResponse( ( resp ) => resp.url().includes( `wp-json/wp/v2/posts/${ postId }` ) && resp.status() === 200 ),
34-
page.getByLabel( 'Editor publish' ).getByRole( 'button', { name: 'Publish', exact: true } ).click(),
35-
] );
36-
37-
// They are too much in the posts table so I'm deleting them.
38-
await page.goto( 'http://stream.wpenv.net/wp-admin/edit.php?post_type=post' );
39-
const listTable = page.getByRole( 'table', { name: 'Table ordered by' } );
40-
await expect( listTable ).toBeVisible();
53+
// Go straight to the Stream log so the assertions below can check the published row.
54+
await page.goto( 'http://stream.wpenv.net/wp-admin/admin.php?page=wp_stream' );
55+
} );
4156

42-
// Move post to trash.
43-
await listTable.getByRole( 'link', { name: `“${ postTitle }” (Edit)` } ).hover();
44-
await listTable.getByRole( 'link', { name: `Move “${ postTitle }” to the Trash` } ).click();
57+
test.afterAll( async () => {
58+
// Clean up the published post so it doesn't accumulate in the posts table.
59+
if ( postId ) {
60+
await page.goto( `http://stream.wpenv.net/wp-admin/post.php?post=${ postId }&action=trash&_wpnonce=` ).catch( () => {} );
61+
await page.goto( 'http://stream.wpenv.net/wp-admin/edit.php?post_type=post' );
62+
const trashLink = page.getByRole( 'link', { name: `Move “${ postTitle }” to the Trash` } );
63+
if ( await trashLink.isVisible().catch( () => false ) ) {
64+
await page.getByRole( 'link', { name: `“${ postTitle }” (Edit)` } ).hover();
65+
await trashLink.click();
66+
}
67+
}
4568

46-
// Ok, we're all set up, let's go to our page.
47-
await page.goto( 'http://stream.wpenv.net/wp-admin/admin.php?page=wp_stream' );
69+
// Restore the deactivated state expected by other test files.
70+
await page.goto( 'http://stream.wpenv.net/wp-admin/network/plugins.php' );
71+
const deactivateLink = page.getByLabel( 'Network Deactivate Stream' );
72+
if ( await deactivateLink.isVisible().catch( () => false ) ) {
73+
await deactivateLink.click();
74+
}
75+
await page.close();
4876
} );
4977

50-
// Do we have a published row?
5178
test( 'has published row', async () => {
52-
// Expects Stream log to have "Test Post" post published visible.
5379
await expect( page.getByText( `"${ postTitle }" post published` ) ).toBeVisible();
5480
} );
5581

56-
// We should not have an updated row. This times out which makes it fail.
5782
test( 'does not have updated row', async () => {
58-
// Expects Stream log to have "Test Post" post published visible.
5983
await expect( page.getByText( `"${ postTitle }" post updated` ) ).not.toBeVisible();
6084
} );
6185
} );

0 commit comments

Comments
 (0)