Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 12 additions & 27 deletions .github/workflows/playwright-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ jobs:
contents: read
pull-requests: write

strategy:
fail-fast: false
matrix:
php: ['8.3', '8.4']
wordpress: ['6.8', '6.9', 'latest']

name: Test PHP ${{ matrix.php }} / WP ${{ matrix.wordpress }}

steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -33,48 +41,25 @@ jobs:
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium

- name: Start WordPress environment
run: npm run wp-env start

- name: Wait for WordPress to be ready
run: |
timeout 60 bash -c 'until curl -sSf http://localhost:8889 > /dev/null 2>&1; do sleep 2; done' || echo "WordPress may not be fully ready"
sleep 5

- name: Debug - Check WordPress status
run: |
curl -I http://localhost:8889 || echo "WordPress not responding"
docker ps

- name: Run Playwright tests
id: tests
continue-on-error: true
run: npm run test:e2e
env:
CI: true

- name: Stop WordPress environment
if: always()
run: npm run wp-env stop
WP_PLAYGROUND_PHP: ${{ matrix.php }}
WP_PLAYGROUND_WP: ${{ matrix.wordpress }}
WP_PLAYGROUND_PORT: 9400

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
name: playwright-report-php${{ matrix.php }}-wp${{ matrix.wordpress }}
path: test-results/
retention-days: 30
if-no-files-found: ignore

- name: Upload test videos and traces
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results
path: test-results/
retention-days: 7
if-no-files-found: ignore

- name: Comment PR with test results
uses: daun/playwright-report-comment@v3
if: always() && github.event_name == 'pull_request'
Expand Down
9 changes: 4 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ HM Query Loop is a WordPress plugin that extends the core Query Loop block with
- `npm run format` - Format all files

### Testing
- `npm run wp-env start` - Start WordPress test environment (ports 8888 dev, 8889 tests)
- `npm run test:e2e` - Run Playwright end-to-end tests
- `npm run test:e2e` - Run Playwright end-to-end tests (Playground starts and stops automatically)
- `npm run test:e2e:debug` - Run tests in debug mode
- `npm run test:e2e:watch` - Run tests in watch mode (reruns on changes)
- `npm run wp-env stop` - Stop WordPress environment
- `npm run test:e2e:watch` - Run tests in watch mode with UI
- `npm run playground:start` - Start Playground for manual testing at localhost (auto-detects port)

**Important**: Always run `npm run build` before running tests, as tests run against built assets.

Expand Down Expand Up @@ -114,7 +113,7 @@ The plugin provides a PHP API for registering custom query presets that can be s

## Testing Environment

Tests use `@wordpress/env` with WordPress 6.9, configured in `.wp-env.json`. The environment includes TwentyTwentyFour and TwentyTwentyFive themes, and the Advanced Query Loop plugin. Tests run on port 8889 and use Playwright with `@wordpress/e2e-test-utils-playwright`.
Tests use WordPress Playground via `@wp-playground/cli`, configured in `blueprint.json`. The environment includes TwentyTwentyFive theme, the Advanced Query Loop plugin, and 25 seeded test posts. Playground starts automatically when running tests (port derived from cwd hash, default ~9400) and shuts down via `global-teardown.js`. Uses Playwright with `@wordpress/e2e-test-utils-playwright`.

## Important Implementation Notes

Expand Down
71 changes: 71 additions & 0 deletions blueprint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"$schema": "https://playground.wordpress.net/blueprint-schema.json",
"landingPage": "/wp-admin/",
"preferredVersions": {
"php": "8.3",
"wp": "6.9"
},
"features": {
"networking": true
},
"extraLibraries": ["wp-cli"],
"steps": [
{
"step": "login"
},
{
"step": "installTheme",
"themeData": {
"resource": "wordpress.org/themes",
"slug": "twentytwentyfive"
},
"options": {
"activate": true
}
},
{
"step": "installPlugin",
"pluginData": {
"resource": "wordpress.org/plugins",
"slug": "advanced-query-loop"
},
"options": {
"activate": true
}
},
{
"step": "defineWpConfigConsts",
"consts": {
"WP_DEBUG": true,
"WP_DEBUG_LOG": true,
"SCRIPT_DEBUG": true
}
},
{
"step": "wp-cli",
"command": "wp plugin activate hm-query-loop"
},
{
"step": "mkdir",
"path": "/wordpress/wp-content/mu-plugins"
},
{
"step": "cp",
"fromPath": "/wordpress/wp-content/plugins/hm-query-loop/tests/mu-plugins/force-elasticpress-available.php",
"toPath": "/wordpress/wp-content/mu-plugins/force-elasticpress-available.php"
},
{
"step": "cp",
"fromPath": "/wordpress/wp-content/plugins/hm-query-loop/tests/mu-plugins/test-query-presets.php",
"toPath": "/wordpress/wp-content/mu-plugins/test-query-presets.php"
},
{
"step": "runPHP",
"code": "<?php require '/wordpress/wp-load.php'; for ( $i = 1; $i <= 25; $i++ ) { wp_insert_post( [ 'post_title' => 'Post ' . $i, 'post_name' => 'post-' . $i, 'post_status' => 'publish', 'post_type' => 'post', 'post_author' => 1, ] ); } echo 'Created 25 test posts.';"
},
{
"step": "wp-cli",
"command": "wp rewrite flush"
}
]
}
104 changes: 104 additions & 0 deletions global-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* eslint-disable no-console */
const { runCLI } = require( '@wp-playground/cli' );
const { chromium } = require( '@playwright/test' );
const fs = require( 'node:fs' );
const path = require( 'node:path' );
const crypto = require( 'node:crypto' );

/**
* Deterministic port from cwd hash so each git worktree gets its own
* Playground instance. Override with WP_PLAYGROUND_PORT (e.g., in CI).
* Range: 9400–9499.
*/
function resolvePort() {
if ( process.env.WP_PLAYGROUND_PORT ) {
return Number( process.env.WP_PLAYGROUND_PORT );
}
const hash = crypto
.createHash( 'sha1' )
.update( process.cwd() )
.digest();
return 9400 + ( hash.readUInt16BE( 0 ) % 100 );
}

module.exports = async () => {
if ( process.env.WP_BASE_URL ) return;

const blueprintPath = process.env.WP_BLUEPRINT_PATH
? path.resolve( process.env.WP_BLUEPRINT_PATH )
: path.resolve( process.cwd(), 'blueprint.json' );
const blueprint = JSON.parse( fs.readFileSync( blueprintPath, 'utf8' ) );
const port = resolvePort();

console.log( `Starting WordPress Playground on port ${ port }...` );

const cli = await runCLI( {
command: 'server',
port,
php: process.env.WP_PLAYGROUND_PHP || undefined,
wp: process.env.WP_PLAYGROUND_WP || undefined,
mount: [
{
hostPath: process.cwd(),
vfsPath: '/wordpress/wp-content/plugins/hm-query-loop',
},
],
blueprint,
} );

// runCLI returns { playground, server } — derive URL from the server socket.
const addr = cli.server.address();
const serverUrl = `http://127.0.0.1:${ addr.port }`;

process.env.WP_BASE_URL = serverUrl;
globalThis.__wpPlayground = cli;

console.log( `Playground running at ${ serverUrl }` );

// Log in and save authentication state for all tests.
const browser = await chromium.launch();
const page = await browser.newPage( {
baseURL: serverUrl,
extraHTTPHeaders: { 'Accept-Encoding': 'identity' },
} );

await page.goto( `${ serverUrl }/wp-login.php` );
await page.fill( '#user_login', 'admin' );
await page.fill( '#user_pass', 'password' );
await page.click( '#wp-submit' );

try {
await Promise.race( [
page.waitForURL( '**/wp-admin/', { timeout: 15000 } ),
page.waitForSelector( '#wpadminbar', { timeout: 15000 } ),
] );
} catch ( error ) {
console.error( 'Failed to log in to WordPress Playground' );
await browser.close();
throw error;
}

// Disable editor modals globally.
await page.evaluate( () =>
// eslint-disable-next-line no-undef
localStorage.setItem(
'WP_PREFERENCES_USER_1',
JSON.stringify( {
core: {
isComplementaryAreaVisible: true,
enableChoosePatternModal: false,
},
'core/edit-post': {
welcomeGuide: false,
fullscreenMode: false,
},
} )
)
);

fs.mkdirSync( '.auth', { recursive: true } );
await page.context().storageState( { path: '.auth/wordpress.json' } );
await browser.close();

console.log( 'WordPress authentication state saved.' );
};
9 changes: 9 additions & 0 deletions global-teardown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = async () => {
const cli = globalThis.__wpPlayground;
if ( ! cli ) return;
if ( typeof cli[ Symbol.asyncDispose ] === 'function' ) {
await cli[ Symbol.asyncDispose ]();
} else {
await cli.server?.close();
}
};
Loading
Loading