Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .env.slic
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ SLIC_INTERACTIVE=1
# The PHP version to run in the slic container. Only use single dot notation, e.g. 7.4, not 7.4.35
SLIC_PHP_VERSION=7.4

# Playwright tests and PHP hooks run in slic; browser fixtures use a separate server.
# The browser image is selected from the installed Playwright CLI, not package.json's range.
# Override the browser image. It must include browsers matching the installed CLI and Node.js.
# SLIC_PLAYWRIGHT_IMAGE=mcr.microsoft.com/playwright:v1.60.0-noble
# SLIC_PLAYWRIGHT_CONTAINER_SHM_SIZE=1g

# XDebug configuration parameters, will apply to the `cli`, `wordpress` and `codeception` services.
# ===============================
# The IDE key used to identify connection requests coming from the services.
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The slic (**S**tellarWP **L**ocal **I**nteractive **C**ontainers) CLI command pr
* [Preparing your project](#preparing-your-project)
* [Adding tests](#adding-tests)
* [Running tests](#running-tests)
* [Running Playwright tests](#running-playwright-tests)
* [Advanced topics](#advanced-topics)
* [Defaults for your project with `slic.json`](/docs/slicjson.md)
* [Managing PHP Versions](#managing-php-versions)
Expand Down Expand Up @@ -252,6 +253,17 @@ slic shell
> cr wpunit
```

### Running Playwright tests

For projects with a Playwright suite, prepare your WordPress test site and run:

```bash
slic playwright test
```

See the [Playwright guide](/docs/playwright.md) for dependency installation, test
examples, and how to adapt existing browser setup hooks to work with Slic.

## Advanced topics

### Managing PHP Versions
Expand Down
10 changes: 10 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

# [2.5.0] - TBD

- Breaking Change - Playwright suites that launch browsers directly must use built-in fixtures or explicitly connect to the remote browser. Browser-facing `localhost` URLs and `download.path()` calls may also need changes. See the [migration guidance](docs/playwright.md#update-setup-code-that-launches-a-browser-directly) before upgrading.

- Added - Internal process helpers in [src/process-argv.php](src/process-argv.php) for executing commands as arrays of literal, unquoted arguments. Use `process_argv()` to capture stdout and receive a `status`/`stdout` result array, or `process_argv_realtime()` to stream output and receive an exit status. These helpers preserve argument boundaries without shell expansion and support interruptible child-process execution.
- Added - Internal Compose helpers in [src/docker-argv.php](src/docker-argv.php): `docker_compose_argv()` and `docker_compose_argv_realtime()` accept command arguments followed by optional Compose options, applying Slic's environment and terminal settings. Use these helpers for new commands built from raw arguments, and `slic_stack_argv()` for stack file options. Pass values without shell quoting or escaping; existing helpers retain their current calling conventions.
- Changed - `slic playwright test` keeps tests and PHP hooks in the slic container and connects browser fixtures to a temporary server in the Microsoft Playwright image. The image follows the installed CLI version, so dependency ranges are supported. Each invocation owns and cleans up its browser container.
- Changed - `slic playwright install`, including `install chromium --with-deps`, no longer installs browsers or system packages. See [the Playwright guide](docs/playwright.md) for usage examples and help adapting existing suites.
- Fixed - Playwright browser readiness checks, logs, and cleanup preserve the configured Docker executable and context when using a Docker Compose command prefix.

# [2.4.2] - 2026-09-08
- Fixed - The PHP 7.3, 7.4 and 8.0 images install system packages from the Debian archive, so they can be built and `slic playwright install` works again now that Debian 11 has left LTS.

Expand Down
174 changes: 174 additions & 0 deletions docs/playwright.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Running Playwright tests

Slic runs your tests with the Playwright version installed in your project. Browsers
run in a separate container with their dependencies already installed, so you can
skip `playwright install`. Your tests and setup hooks still have access to PHP,
WP-CLI, and Composer inside Slic.

## Get started

Prepare your WordPress test site as usual, then install your project's dependencies
and run the tests:

```bash
slic npm ci
slic playwright test
```

You can pass the usual test filters:

```bash
slic playwright test tests/e2e/login.spec.ts
slic playwright test --grep 'can log in'
```

Keep your dependency lockfile committed and use `npm ci` in CI. Slic chooses the
browser image to match your installed Playwright version; you don't need to pin a
second version in Slic. The first run may take longer while Docker pulls the image.

Existing preparation scripts can keep `slic playwright install chromium --with-deps`.
Slic skips that installation because the browsers are already available.

## Use Playwright's built-in fixtures

Use the `page`, `context`, or `browser` fixtures in your tests. They connect to the
browser automatically, and Playwright manages their cleanup.

> [!WARNING]
> Avoid calling `chromium.launch()` directly in tests or setup hooks. It launches a
> local browser and does not use Slic's remote browser connection. Use Playwright's
> built-in fixtures, or [update your existing setup code](#update-setup-code-that-launches-a-browser-directly)
> to connect explicitly. The same applies to `firefox.launch()` and `webkit.launch()`.

Set the WordPress URL in `playwright.config.ts`:

```ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
use: {
baseURL: 'http://wordpress.test',
},
});
```

Then use relative URLs and Playwright's locators and assertions:

```ts
import { expect, test } from '@playwright/test';

test('shows the WordPress login form', async ({ page }) => {
await page.goto('/wp-login.php');

await expect(page.getByLabel('Username or Email Address')).toBeVisible();
await expect(page.getByRole('button', { name: 'Log In', exact: true })).toBeVisible();
});
```

Use `http://wordpress.test` for WordPress inside Slic. From the browser,
`localhost` refers to the browser's own container. If your tests start another
web server inside Slic, bind it to `0.0.0.0` and use `http://slic:<port>` as its
browser-facing URL.

## Keep PHP and WP-CLI setup in your hooks

Your hooks can continue calling PHP, WP-CLI, and Composer. For example, this hook
checks that a plugin is active before running the tests:

```ts
import { execFileSync } from 'node:child_process';
import { test } from '@playwright/test';

test.beforeAll(() => {
execFileSync('wp', ['plugin', 'is-active', 'my-plugin'], {
stdio: 'inherit',
});
});
```

Replace `my-plugin` with your plugin's slug. Pass arguments as an array with
`execFileSync` so values containing spaces or shell characters remain intact.
A failed command fails the hook, and `stdio: 'inherit'` shows its output in the test log.

If hooks reset or import a shared database, avoid running those tests in parallel.
Separate browser sessions still use the same WordPress database.

## Update setup code that launches a browser directly

For new browser-based setup, prefer a
[Playwright setup project](https://playwright.dev/docs/test-global-setup-teardown#option-1-project-dependencies).
It can use the same built-in fixtures as your tests, with automatic browser cleanup
and setup results in the test report.

If you already use `chromium.launch()` in a global setup file, change that call to
connect when Slic supplies an endpoint:

```ts
import { chromium } from '@playwright/test';

export default async function globalSetup() {
const endpoint = process.env.PW_TEST_CONNECT_WS_ENDPOINT;
const browser = endpoint
? await chromium.connect(endpoint)
: await chromium.launch();

try {
const context = await browser.newContext({
baseURL: process.env.WP_BASE_URL ?? 'http://wordpress.test',
});
const page = await context.newPage();

await page.goto('/wp-login.php');
// Add your project's login or other browser setup here.
} finally {
await browser.close();
}
}
```

`PW_TEST_CONNECT_WS_ENDPOINT` is Playwright's own environment variable. Slic sets
it automatically to the browser server's WebSocket address. You don't need to add
it to your `.env` file. The fallback lets the same setup run outside Slic when local
browsers are installed.

Apply the same pattern to direct `firefox.launch()` or `webkit.launch()` calls.
`launchPersistentContext()` has no equivalent in this connection pattern; code
that depends on a persistent browser profile needs a separate migration.

## Save downloads to the test output directory

Use `download.saveAs()` to copy a download to your test output. Register the download
listener before clicking, so you don't miss the event:

```ts
import { test } from '@playwright/test';

test('exports a report', async ({ page }, testInfo) => {
// Replace this route and button name with your project's export screen.
await page.goto('/reports');

const downloadPromise = page.waitForEvent('download');

await page.getByRole('button', { name: 'Export report' }).click();

const download = await downloadPromise;

await download.saveAs(testInfo.outputPath('report.csv'));
});
```

`testInfo.outputPath()` keeps files separate for each test. Avoid `download.path()`,
which is unavailable with remote browsers. See Playwright's
[download guide](https://playwright.dev/docs/downloads) for more examples.

## Before switching an existing suite

Check for direct browser launches, browser-facing `localhost` URLs, and
`download.path()` calls, then run the suite against your prepared WordPress site.
If you rely on custom browser executables, Chrome/Edge channels, or browser launch
arguments, verify those separately; the remote browser may not support your settings.

This prototype supports `slic playwright test`. Headed and UI debugging have not
been validated, and commands such as `codegen`, `open`, and `screenshot` don't use
the remote browser connection. Your Playwright package must also support the Node.js
version provided by Slic.
3 changes: 1 addition & 2 deletions skills/slic/references/slic-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,9 @@ Inside the slic shell, shorthand commands are available:

Runs Playwright commands in the stack for browser-based testing.

> **Note:** Available since slic 2.x. Requires Playwright to be installed in the target project (`slic playwright install`).
Tests and PHP hooks run in the `slic` service. Browser fixtures connect to a temporary server in `mcr.microsoft.com/playwright`, selected from the installed Playwright CLI version. Dependency ranges in `package.json` are supported. `SLIC_PLAYWRIGHT_IMAGE` can override the browser image; its browsers must match the installed CLI. `slic playwright install` (including `install chromium --with-deps`) is unnecessary and exits successfully. Explicit `browserType.launch()` calls still launch locally. See [the remote browser guide](../../../docs/playwright.md) for compatibility details.

```bash
slic playwright install # install Playwright + Chromium
slic playwright test # run all Playwright tests
slic playwright test tests/e2e/my-test.spec.ts # run a specific test file
```
Expand Down
29 changes: 25 additions & 4 deletions slic-stack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ services:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
healthcheck:
test: ${SLIC_DB_HEALTHCHECK:-healthcheck.sh --connect --innodb_initialized}
start_period: 5s
interval: 1s
timeout: 3s
retries: 30
Expand All @@ -36,7 +35,6 @@ services:
- "${SLIC_REDIS_LOCALHOST_PORT:-8379}:6379"
healthcheck: # It should reply PONG to PING
test: redis-cli ping | grep PONG
start_period: 2s
interval: 1s
timeout: 3s
retries: 30
Expand Down Expand Up @@ -117,7 +115,6 @@ services:
- ./containers/wordpress/php.ini:/usr/local/etc/php/conf.d/zz-docker.ini
healthcheck: # Apache service should be running correctly.
test: service apache2 status
start_period: 5s
interval: 1s
timeout: 3s
retries: 30
Expand All @@ -134,12 +131,36 @@ services:
condition: service_healthy
healthcheck: # It should reply with a 200 status code to a request to the status endpoint.
test: curl -f http://localhost:4444/wd/hub/status
start_period: 5s
interval: 1s
timeout: 3s
retries: 30
shm_size: "${SLIC_CHROME_CONTAINER_SHM_SIZE:-512m}"

# Browser server only. Test code and PHP hooks still execute in slic.
# The command selects the image from the installed CLI and starts a uniquely named
# one-off container. No ports are published on the host.
playwright:
image: ${SLIC_PLAYWRIGHT_IMAGE:-mcr.microsoft.com/playwright}
profiles:
- playwright
init: true
networks:
- slic
user: "${SLIC_UID:-}:${SLIC_GID:-}"
environment:
HOME: /tmp
volumes:
# Reuse the installed CLI to run the server, with no npm install at startup.
- ${SLIC_WP_DIR}:/var/www/html
- ${SLIC_PLUGINS_DIR}:${SLIC_WP_CONTENT_CONTAINER_DIR}/plugins
- ${SLIC_THEMES_DIR}:${SLIC_WP_CONTENT_CONTAINER_DIR}/themes
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/', r => {r.resume(); process.exit(r.statusCode === 200 ? 0 : 1)}).on('error', () => process.exit(1))"]
interval: 1s
timeout: 2s
retries: 20
shm_size: "${SLIC_PLAYWRIGHT_CONTAINER_SHM_SIZE:-1g}"

slic:
image: ghcr.io/stellarwp/slic-php${SLIC_PHP_VERSION}:${SLIC_VERSION}
networks:
Expand Down
5 changes: 4 additions & 1 deletion slic.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
require_once __DIR__ . '/src/scaffold.php';
require_once __DIR__ . '/src/slic.php';
require_once __DIR__ . '/src/docker.php';
require_once __DIR__ . '/src/process-argv.php';
require_once __DIR__ . '/src/docker-argv.php';
require_once __DIR__ . '/src/playwright.php';
require_once __DIR__ . '/src/notify.php';
require_once __DIR__ . '/src/plugins.php';
require_once __DIR__ . '/src/themes.php';
Expand Down Expand Up @@ -54,7 +57,7 @@
] );

$cli_name = 'slic';
const CLI_VERSION = '2.4.2';
const CLI_VERSION = '2.5.0';

// If the run-time option `-q`, for "quiet", is specified, then do not print the header.
if ( in_array( '-q', $argv, true ) || ( in_array( 'exec', $argv, true ) && ! in_array( 'help', $argv, true ) ) ) {
Expand Down
56 changes: 7 additions & 49 deletions src/commands/playwright.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@
$help = <<< HELP
SUMMARY:

This command requires a use target set using the <light_cyan>use</light_cyan> command.
Runs Playwright commands in the stack. This command requires a use target set using the <light_cyan>use</light_cyan> command.

Tests and PHP hooks run in <light_cyan>slic</light_cyan>; browser fixtures connect to a temporary Playwright server.
The browser image matches the installed Playwright CLI version. Dependency ranges in package.json are supported.
Set <light_cyan>SLIC_PLAYWRIGHT_IMAGE</light_cyan> to override the browser image; its browsers must match the installed CLI.
Browser installation is unnecessary. Explicit browser.launch() calls still launch locally; see docs/playwright.md.

USAGE:

<yellow>{$cli_name} playwright [...<commands>]</yellow>

EXAMPLES:

<light_cyan>{$cli_name} playwright install</light_cyan>
Install Playwright dependencies in the current <light_cyan>use</light_cyan> target.

<light_cyan>{$cli_name} playwright test</light_cyan>
Run all Playwright tests following the Playwright configuration in the current <light_cyan>use</light_cyan> target.

Expand All @@ -40,48 +42,4 @@
$using = slic_target_or_fail();
echo light_cyan( "Using {$using}" . PHP_EOL );

ensure_service_running( 'slic' );

setup_id();
$playwright_args = $args( '...' );
$is_install_command = $playwright_args[0] === 'install';

if ( $is_install_command ) {
// Install commands will need to run as root.
$user = '0:0';
} else {
// Other commands will run as the current user.
$user = sprintf( '"%s:%s"', getenv( 'SLIC_UID' ), getenv( 'SLIC_GID' ) );
}

if ( $playwright_args === ['install'] ) {
// It's exactly the `playwright install` command and nothing more.
$command = [
'exec',
'--user',
'0:0',
'--workdir',
escapeshellarg( get_project_container_path() ),
'slic',
'node_modules/.bin/playwright install chromium --with-deps',
];
} else {
$command = array_merge( [
'exec',
'--user',
$user,
'--workdir',
escapeshellarg( get_project_container_path() ),
'slic',
'node_modules/.bin/playwright',
], $playwright_args );
}

$status = slic_realtime()( $command );

// If there is a status other than 0, we have an error. Bail.
if ( $status ) {
exit( $status );
}

exit( $status );
exit( run_playwright( $args( '...' ) ) );
Loading
Loading