Skip to main content

Programmatic Usage of Playground CLI

The Playground CLI can also be controlled programmatically from your JavaScript/TypeScript code using the runCLI function. This gives you direct access to all CLI functionalities within your code, which is useful for automating end-to-end tests. The options you pass to runCLI map directly to the CLI flags.

Running a WordPress instance with a specific version

Using the runCLI function, you can specify options like the PHP and WordPress versions. In the example below, we request PHP 8.3, the latest version of WordPress, and to be automatically logged in. All supported arguments are defined in the RunCLIArgs type.

import { runCLI } from "@wp-playground/cli";

const cliServer = await runCLI({
command: 'server',
php: '8.3',
wp: 'latest',
login: true,
});

Run the code above using your preferred TypeScript runtime, e.g. tsx:

npx tsx my-script.ts

Setting a blueprint

You can provide a blueprint in two ways: either as an object literal directly passed to the blueprint property, or as a string containing the path to an external .json file.

import { runCLI, RunCLIServer } from "@wp-playground/cli";

const cliServer: RunCLIServer = await runCLI({
command: 'server',
wp: 'latest',
blueprint: {
steps: [
{
"step": "setSiteOptions",
"options": {
"blogname": "Blueprint Title",
"blogdescription": "A great blog description"
}
}
],
},
});

For full type-safety when defining your blueprint object, you can import and use the BlueprintDeclaration type from the @wp-playground/blueprints package:

import type { BlueprintDeclaration } from '@wp-playground/blueprints';

const myBlueprint: BlueprintDeclaration = {
landingPage: "/wp-admin/",
steps: [
{
"step": "installTheme",
"themeData": {
"resource": "wordpress.org/themes",
"slug": "twentytwentyone"
},
"options": {
"activate": true
}
}
]
};

Mounting a plugin programmatically

You can mount local directories programmatically using runCLI. The options mount and mount-before-install are available. The hostPath property expects a path to a directory on your local machine. This path should be relative to where your script is being executed.

import { runCLI } from "@wp-playground/cli";

const cliServer = await runCLI({
command: 'server',
login: true,
'mount-before-install': [
{
hostPath: './[my-plugin-local-path]',
vfsPath: '/wordpress/wp-content/plugins/my-plugin',
},
],
});

Combining mounts with blueprints

You can combine mounting parts of the project with blueprints, for example:

import { runCLI, RunCLIServer } from "@wp-playground/cli";

const cliServer: RunCLIServer = await runCLI({
command: 'server',
php: '8.3',
wp: 'latest',
login: true,
mount: [
{
"hostPath": "./plugin/",
"vfsPath": "/wordpress/wp-content/plugins/playwright-test"
}
],
blueprint: {
steps: [
{
"step": "activatePlugin",
"pluginPath": "/wordpress/wp-content/plugins/playwright-test/plugin-playwright.php"
}
]
}
});

Automated testing

Integration testing with Vitest

The programmatic API is excellent for automated testing. Here's a complete example using Vitest:

import { describe, test, expect, afterEach } from 'vitest';
import { runCLI, RunCLIServer } from "@wp-playground/cli";

describe('My Plugin Tests', () => {
const cliServer: RunCLIServer;

afterEach(async () => {
if (cliServer) {
// RunCLIServer exposes Symbol.asyncDispose as its public async cleanup API.
await cliServer[Symbol.asyncDispose]();
}
});

test('plugin activates successfully', async () => {
cliServer = await runCLI({
command: 'server',
mount: [
{
hostPath: './my-plugin',
vfsPath: '/wordpress/wp-content/plugins/my-plugin'
}
],
blueprint: {
steps: [
{
step: 'activatePlugin',
pluginPath: '/wordpress/wp-content/plugins/my-plugin/plugin.php'
}
]
}
});

const homeUrl = new URL('/', cliServer.serverUrl);
const response = await fetch(homeUrl);

expect(response.status).toBe(200);
const html = await response.text();
expect(html).toContain('My Plugin');
});

test('plugin settings page loads', async () => {
cliServer = await runCLI({
command: 'server',
login: true, // Auto-login as admin
mount: [
{
hostPath: './my-plugin',
vfsPath: '/wordpress/wp-content/plugins/my-plugin'
}
],
blueprint: {
steps: [
{
step: 'activatePlugin',
pluginPath: '/wordpress/wp-content/plugins/my-plugin/plugin.php'
}
]
}
});

const settingsUrl = new URL(
'/wp-admin/options-general.php?page=my-plugin',
cliServer.serverUrl
);
const response = await fetch(settingsUrl);

// Note: A plain `fetch` call does not send the admin session cookie set by `login: true`,
// so this request is typically redirected to the login page instead of returning 200.
expect(response.status).toBe(302);
});
});

Testing with different WordPress/PHP versions

test('plugin works with WordPress 6.4 and PHP 8.3', async () => {
cliServer = await runCLI({
command: 'server',
php: '8.3',
wp: '6.4',
mount: [
{
hostPath: './my-plugin',
vfsPath: '/wordpress/wp-content/plugins/my-plugin'
}
],
blueprint: {
steps: [
{
step: 'activatePlugin',
pluginPath: '/wordpress/wp-content/plugins/my-plugin/plugin.php'
}
]
}
});

const homeUrl = new URL('/', cliServer.serverUrl);
const response = await fetch(homeUrl);

expect(response.status).toBe(200);
});

Advanced configuration

Skip WordPress and SQLite setup

When you only need to test PHP code without WordPress, you can skip the setup for faster testing:

import { runCLI } from "@wp-playground/cli";

const cliServer = await runCLI({
command: 'server',
php: '8.3',
wordpressInstallMode: 'do-not-attempt-installing',
skipSqliteSetup: true,
});

// Test PHP version
await cliServer.playground.writeFile(
'/wordpress/version.php',
'<?php echo phpversion(); ?>'
);

const versionUrl = new URL('/version.php', cliServer.serverUrl);
const response = await fetch(versionUrl);
const version = await response.text();
console.log('PHP Version:', version); // Outputs: 8.3.x

Error handling

import { runCLI } from "@wp-playground/cli";

try {
const cliServer = await runCLI({
command: 'server',
debug: true, // Enable PHP error logging.
});

// Your test code here

} catch (error) {
console.error('Server failed to start:', error);
}