Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
15bcd1d
Switch wp-env CI jobs from Docker to WordPress Playground runtime
youknowriad Feb 13, 2026
29772f6
Temporarily enable debug verbosity for Playground runtime
youknowriad Feb 13, 2026
0f25f85
Remove redundant gutenberg plugin mapping from .wp-env.test.json
youknowriad Feb 13, 2026
1342752
Fix Playground runtime siteurl mismatch with --site-url flag
youknowriad Feb 16, 2026
fe20224
wp-env Playground: match Docker runtime defaults (site title, permali…
youknowriad Feb 25, 2026
eddd8cd
wp-env Playground: fix permalink structure and perf test timeout
youknowriad Feb 25, 2026
1fbfb4d
wp-env Playground: add diagnostics to E2E CI
youknowriad Feb 25, 2026
5b11923
Fix Playground rewrite rules and improve CI diagnostics
youknowriad Feb 25, 2026
a13331c
Scale Playwright timeouts for Playground WASM runtime
youknowriad Feb 26, 2026
06f6ca2
Increase timeout multiplier and fix hard-coded test timeouts
youknowriad Feb 26, 2026
bd8f220
Fix hard-coded timeouts in navigation block e2e tests
youknowriad Feb 26, 2026
0cdc4f2
Fix e2e test timing issues for Playground WASM runtime
youknowriad Feb 26, 2026
420bb08
Increase hard-coded timeouts in collaboration, fit-text, and widget t…
youknowriad Feb 27, 2026
5c11b24
Wait for preferences store before setting preferences
youknowriad Feb 27, 2026
d8db6df
Fix publishPost race condition with publish panel rendering
youknowriad Feb 27, 2026
b2473b1
Fix saveSiteEditorEntities race condition and collaboration timeouts
youknowriad Feb 27, 2026
37b196c
Fix typewriter caret position tolerance for Playground WASM runtime
youknowriad Feb 27, 2026
b4ab210
Fix publishPost regression: wait for panel content instead of region
youknowriad Feb 28, 2026
f43e6a9
Fix saveSiteEditorEntities panel wait and increase typewriter tolerance
youknowriad Feb 28, 2026
31a8486
Fix saveSiteEditorEntities: handle direct save without entities panel
youknowriad Feb 28, 2026
87ac646
Fix .or() strict mode violation in saveSiteEditorEntities and publish…
youknowriad Feb 28, 2026
fdf51f3
Fix publishPost: handle direct save without publish panel in Playground
youknowriad Feb 28, 2026
9895193
Fix publishPost entity save → publish sequence
youknowriad Feb 28, 2026
f8e7e3c
Fix Playground start to stop stale processes and wait for port release
youknowriad Feb 28, 2026
4220986
Force fresh WordPress installation on every Playground start
youknowriad Feb 28, 2026
b15f9d4
Check REST API readiness before declaring Playground environment ready
youknowriad Feb 28, 2026
1aaf450
Remove --experimental-multi-worker to fix Playground REST API errors
youknowriad Feb 28, 2026
7c95f85
Add stabilization phase to Playground readiness check
youknowriad Feb 28, 2026
e5a717c
Revert JSON body validation in Playground readiness check
youknowriad Feb 28, 2026
7fe1f28
Re-enable --experimental-multi-worker with stabilization guard
youknowriad Feb 28, 2026
76e5f97
Increase performance test timeout to 90 minutes
youknowriad Feb 28, 2026
ca79f8e
Skip known Playground-incompatible E2E tests
youknowriad Mar 1, 2026
e053304
Add more Playground-incompatible E2E test skips
youknowriad Mar 1, 2026
d650654
Fix grepInvert patterns to use test() titles instead of test.step()
youknowriad Mar 1, 2026
04fe86e
Skip site editor tests that fail/flake on Playground
youknowriad Mar 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 108 additions & 1 deletion .github/workflows/end2end-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,106 @@ jobs:

- name: Install WordPress and start the server
run: |
npm run wp-env-test start
npm run wp-env-test start -- --runtime=playground

- name: Playground diagnostics
if: ${{ matrix.part == 1 }}
continue-on-error: true
run: |
# Use ?rest_route= format (works regardless of permalink structure)
REST_BASE="http://localhost:8889/?rest_route"

echo "=== WordPress REST API root ==="
curl -s "${REST_BASE}=/" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(f'Site: {d.get(\"name\", \"?\")}')
print(f'URL: {d.get(\"url\", \"?\")}')
print(f'Namespaces: {len(d.get(\"namespaces\", []))}')
print(f'Authentication: {list(d.get(\"authentication\", {}).keys())}')
routes = d.get('routes', {})
print(f'Total routes: {len(routes)}')
has_comments = '/wp/v2/comments' in routes
print(f'Has /wp/v2/comments: {has_comments}')
if has_comments:
methods = routes['/wp/v2/comments'].get('methods', [])
print(f' Methods: {methods}')
" 2>/dev/null || echo "Failed to query REST API"

echo ""
echo "=== Link header (REST API discovery) ==="
curl -sI http://localhost:8889/ 2>/dev/null | grep -i "^link:" || echo "No Link header found"

echo ""
echo "=== WordPress version ==="
curl -s "${REST_BASE}=/wp/v2/settings" \
-H "Authorization: Basic $(echo -n admin:password | base64)" 2>/dev/null | \
python3 -c "import sys,json;d=json.load(sys.stdin);print(f'Title: {d.get(\"title\",\"?\")}')" 2>/dev/null || echo "Failed"

echo ""
echo "=== Test: Create a post and note comment ==="
COOKIE_JAR=$(mktemp)
curl -s -c "$COOKIE_JAR" -d "log=admin&pwd=password&wp-submit=Log+In&redirect_to=%2Fwp-admin%2F&testcookie=1" \
"http://localhost:8889/wp-login.php" -o /dev/null
NONCE=$(curl -s -b "$COOKIE_JAR" "http://localhost:8889/wp-admin/admin-ajax.php?action=rest-nonce" 2>/dev/null)
echo "REST nonce: ${NONCE:0:5}..."

POST_RESPONSE=$(curl -s -b "$COOKIE_JAR" -H "X-WP-Nonce: $NONCE" \
-H "Content-Type: application/json" \
-d '{"title":"Diagnostic Test","content":"Test content","status":"draft"}' \
"${REST_BASE}=/wp/v2/posts")
POST_ID=$(echo "$POST_RESPONSE" | python3 -c "import sys,json;print(json.load(sys.stdin).get('id','ERROR'))" 2>/dev/null)
echo "Created post ID: $POST_ID"

if [ "$POST_ID" != "ERROR" ] && [ -n "$POST_ID" ]; then
NOTE_RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -b "$COOKIE_JAR" -H "X-WP-Nonce: $NONCE" \
-H "Content-Type: application/json" \
-d "{\"post\":$POST_ID,\"content\":\"Test note\",\"type\":\"note\",\"status\":\"approve\"}" \
"${REST_BASE}=/wp/v2/comments")
HTTP_STATUS=$(echo "$NOTE_RESPONSE" | grep "HTTP_STATUS:" | cut -d: -f2)
BODY=$(echo "$NOTE_RESPONSE" | grep -v "HTTP_STATUS:")
echo "Note creation HTTP status: $HTTP_STATUS"
echo "Note creation response: $(echo "$BODY" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
if 'id' in d: print(f'SUCCESS - Note ID: {d[\"id\"]}, Type: {d.get(\"type\",\"?\")}, Status: {d.get(\"status\",\"?\")}')
elif 'code' in d: print(f'ERROR - Code: {d[\"code\"]}, Message: {d[\"message\"]}')
else: print(json.dumps(d, indent=2)[:500])
except: print(sys.stdin.read()[:500])
" 2>/dev/null)"

echo ""
echo "=== Test: Server-side block rendering ==="
SSR_RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -b "$COOKIE_JAR" -H "X-WP-Nonce: $NONCE" \
"${REST_BASE}=/wp/v2/block-renderer/core/archives&context=edit&post_id=$POST_ID")
SSR_STATUS=$(echo "$SSR_RESPONSE" | grep "HTTP_STATUS:" | cut -d: -f2)
SSR_BODY=$(echo "$SSR_RESPONSE" | grep -v "HTTP_STATUS:")
echo "Block renderer HTTP status: $SSR_STATUS"
echo "Block renderer response: $(echo "$SSR_BODY" | head -c 500)"
else
echo "Skipping note/SSR tests (post creation failed)"
echo "Post creation response: $(echo "$POST_RESPONSE" | head -c 500)"
fi
rm -f "$COOKIE_JAR"

echo ""
echo "=== wp-env work directory ==="
find "$HOME/.wp-env" -maxdepth 2 -type f -name "*.log" 2>/dev/null || echo "No log files found in $HOME/.wp-env"
ls -la "$HOME/.wp-env/" 2>/dev/null || echo "$HOME/.wp-env does not exist"

echo ""
echo "=== Playground log (last 100 lines) ==="
find "$HOME/.wp-env" -name "playground.log" -exec tail -100 {} \; 2>/dev/null || echo "No playground log found"

- name: Run the tests
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
SHARD_PART: ${{ matrix.part }}
SHARD_TOTAL: ${{ matrix.totalParts }}
# Playground (WASM PHP) is ~3x slower than Docker.
# Scale up Playwright timeouts to avoid false negatives.
TIMEOUT_MULTIPLIER: 3
run: |
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e -- --shard="${SHARD_PART}/${SHARD_TOTAL}"

Expand All @@ -76,6 +169,20 @@ jobs:
path: flaky-tests
if-no-files-found: ignore

- name: Collect Playground logs
if: ${{ !cancelled() }}
run: |
mkdir -p playground-logs
find "$HOME/.wp-env" -name "playground.log" -exec cp {} playground-logs/ \; 2>/dev/null || true

- name: Archive Playground logs
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
if: ${{ !cancelled() }}
with:
name: playground-logs--${{ matrix.part }}
path: playground-logs
if-no-files-found: ignore

merge-artifacts:
name: Merge Artifacts
if: ${{ !cancelled() }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/performance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ permissions: {}

jobs:
performance:
timeout-minutes: 60
timeout-minutes: 90
name: Run performance tests
runs-on: 'ubuntu-24.04'
permissions:
Expand Down
1 change: 0 additions & 1 deletion .wp-env.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"SCRIPT_DEBUG": false
},
"mappings": {
"wp-content/plugins/gutenberg": ".",
"wp-content/mu-plugins": "./packages/e2e-tests/mu-plugins",
"wp-content/plugins/gutenberg-test-plugins": "./packages/e2e-tests/plugins",
"wp-content/themes/gutenberg-test-themes": "./test/gutenberg-test-themes",
Expand Down
47 changes: 23 additions & 24 deletions bin/plugin/commands/performance.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,36 +400,32 @@ async function runPerformanceTests( branches, options ) {
wpEnvConfigPath,
JSON.stringify(
{
testsEnvironment: false,
port: 8889,
config: {
WP_DEBUG: false,
SCRIPT_DEBUG: false,
},
core: wpZipUrl || 'WordPress/WordPress',
plugins: [ buildDir ],
themes: [ path.join( testRunnerDir, 'test/emptytheme' ) ],
env: {
tests: {
mappings: {
'wp-content/mu-plugins': path.join(
testRunnerDir,
'packages/e2e-tests/mu-plugins'
),
'wp-content/plugins/gutenberg-test-plugins':
path.join(
testRunnerDir,
'packages/e2e-tests/plugins'
),
'wp-content/themes/gutenberg-test-themes':
path.join(
testRunnerDir,
'test/gutenberg-test-themes'
),
'wp-content/themes/gutenberg-test-themes/twentytwentyone':
'https://downloads.wordpress.org/theme/twentytwentyone.1.7.zip',
'wp-content/themes/gutenberg-test-themes/twentytwentythree':
'https://downloads.wordpress.org/theme/twentytwentythree.1.0.zip',
},
},
mappings: {
'wp-content/mu-plugins': path.join(
testRunnerDir,
'packages/e2e-tests/mu-plugins'
),
'wp-content/plugins/gutenberg-test-plugins': path.join(
testRunnerDir,
'packages/e2e-tests/plugins'
),
'wp-content/themes/gutenberg-test-themes': path.join(
testRunnerDir,
'test/gutenberg-test-themes'
),
'wp-content/themes/gutenberg-test-themes/twentytwentyone':
'https://downloads.wordpress.org/theme/twentytwentyone.1.7.zip',
'wp-content/themes/gutenberg-test-themes/twentytwentythree':
'https://downloads.wordpress.org/theme/twentytwentythree.1.0.zip',
},
},
null,
Expand Down Expand Up @@ -479,7 +475,10 @@ async function runPerformanceTests( branches, options ) {
const envDir = branchDirs[ branch ];

logAtIndent( 3, 'Starting environment' );
await runShellScript( `${ wpEnvPath } start`, envDir );
await runShellScript(
`${ wpEnvPath } start --runtime=playground`,
envDir
);

logAtIndent( 3, 'Running tests' );
await runTestSuite( testSuite, testRunnerDir, runKey );
Expand Down
73 changes: 49 additions & 24 deletions packages/e2e-test-utils-playwright/src/editor/publish-post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,66 @@ import type { Editor } from './index';
* @param this
*/
export async function publishPost( this: Editor ) {
const editorTopBar = this.page.getByRole( 'region', {
name: 'Editor top bar',
} );
// Wait for the top bar to render before checking button visibility.
await editorTopBar.waitFor();

// If we have changes in other entities, the label is `Save` instead of `Publish`.
const saveButton = this.page
.getByRole( 'region', { name: 'Editor top bar' } )
.getByRole( 'button', { name: 'Save', exact: true } );
const publishButton = this.page
.getByRole( 'region', { name: 'Editor top bar' } )
.getByRole( 'button', { name: 'Publish', exact: true } );
const saveButton = editorTopBar.getByRole( 'button', {
name: 'Save',
exact: true,
} );
const publishButton = editorTopBar.getByRole( 'button', {
name: 'Publish',
exact: true,
} );
const buttonToClick = ( await saveButton.isVisible() )
? saveButton
: publishButton;
await buttonToClick.click();

const entitiesSaveButton = this.page
.getByRole( 'region', { name: 'Editor publish' } )
.getByRole( 'button', { name: 'Save', exact: true } );
const isEntitiesSavePanelVisible = await entitiesSaveButton.isVisible();
const publishRegion = this.page.getByRole( 'region', {
name: 'Editor publish',
} );

// Save any entities.
if ( isEntitiesSavePanelVisible ) {
// Handle saving entities.
await entitiesSaveButton.click();
}
const entitiesSaveButton = publishRegion.getByRole( 'button', {
name: 'Save',
exact: true,
} );

// Handle saving just the post.
await this.page
.getByRole( 'region', {
name: 'Editor publish',
} )
.getByRole( 'button', { name: 'Publish', exact: true } )
.click();
const publishConfirmButton = publishRegion.getByRole( 'button', {
name: 'Publish',
exact: true,
} );

await this.page
// In some runtimes (e.g. Playground WASM), the publish panel may not
// appear at all — the post is saved directly. Watch for either the
// panel content or the success notice.
const successNotice = this.page
.getByRole( 'button', { name: 'Dismiss this notice' } )
.filter( { hasText: 'published' } )
.filter( { hasText: 'published' } );

await entitiesSaveButton
.or( publishConfirmButton )
.or( successNotice )
.first()
.waitFor();

// Save any entities if the entities panel appeared.
if ( await entitiesSaveButton.isVisible() ) {
await entitiesSaveButton.click();
// After entity save, the publish confirmation may appear next.
await publishConfirmButton.or( successNotice ).first().waitFor();
}

// Click the publish confirmation button if visible.
if ( await publishConfirmButton.isVisible() ) {
await publishConfirmButton.click();
}

await successNotice.waitFor();
const postId = new URL( this.page.url() ).searchParams.get( 'post' );

return typeof postId === 'string' ? parseInt( postId, 10 ) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ export async function setPreferences(
context: PreferencesContext,
preferences: Record< string, any >
) {
await this.page.waitForFunction( () => window?.wp?.data );
await this.page.waitForFunction(
() =>
window?.wp?.data &&
window.wp.data.dispatch( 'core/preferences' ) !== null
);

await this.page.evaluate(
async ( props ) => {
Expand Down
44 changes: 30 additions & 14 deletions packages/e2e-test-utils-playwright/src/editor/site-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export async function saveSiteEditorEntities(
const editorTopBar = this.page.getByRole( 'region', {
name: 'Editor top bar',
} );
// Wait for the top bar region to be ready before checking button state.
await editorTopBar.waitFor();

// If we have changes in a single entity which can be published the label is `Publish`.
const saveButton = editorTopBar.getByRole( 'button', {
Expand All @@ -30,26 +32,40 @@ export async function saveSiteEditorEntities(
const publishButton = editorTopBar.getByRole( 'button', {
name: 'Publish',
} );
const publishButtonIsVisible = ! ( await saveButton.isVisible() );
const saveButtonIsVisible = await saveButton.isVisible();
// First Save button in the top bar.
const buttonToClick = publishButtonIsVisible ? publishButton : saveButton;
const buttonToClick = saveButtonIsVisible ? saveButton : publishButton;
await buttonToClick.click();

if ( ! options.isOnlyCurrentEntityDirty ) {
// Second Save button in the entities panel.
await this.page
.getByRole( 'region', {
name: /(Editor publish|Save panel)/,
} )
.getByRole( 'button', { name: 'Save', exact: true } )
.click();
}
// The text in the notice can be different based on the edited entity, whether
// we are saving multiple entities and whether we publish or update. So for now,
// we locate it based on the last part.
await this.page
const successNotice = this.page
.getByRole( 'button', { name: 'Dismiss this notice' } )
.getByText( /(updated|published)\./ )
.first()
.waitFor();
.first();

if ( ! options.isOnlyCurrentEntityDirty ) {
// Wait for the entities panel Save button to appear.
// In some runtimes (e.g. Playground WASM), the top-bar Save button
// may directly save without showing the entities panel, so also
// watch for the success notice as a fallback.
const entitiesPanel = this.page.getByRole( 'region', {
name: /(Editor publish|Save panel)/,
} );
const entitiesSaveButton = entitiesPanel.getByRole( 'button', {
name: 'Save',
exact: true,
} );

// Wait for either the entities Save button or the success notice.
// Use .first() to avoid strict mode violation when both match.
await entitiesSaveButton.or( successNotice ).first().waitFor();

if ( await entitiesSaveButton.isVisible() ) {
await entitiesSaveButton.click();
}
}

await successNotice.waitFor();
}
Loading
Loading