From 6e38d6174d5eafd275bc9744f9fb495a179fb300 Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Mon, 21 Sep 2026 16:12:44 -0400 Subject: [PATCH 01/11] feat: run Playwright in its own service on the Microsoft image - Add a profile-gated `playwright` service using mcr.microsoft.com/playwright - Read the image tag from the target's @playwright/test version - Make `slic playwright install` a no-op, the image contains the browser - Allow SLIC_PLAYWRIGHT_VERSION and SLIC_PLAYWRIGHT_IMAGE overrides --- .env.slic | 7 +++ changelog.md | 4 ++ skills/slic/references/slic-commands.md | 3 +- slic-stack.yml | 32 +++++++++++ src/commands/playwright.php | 74 +++++++++++-------------- src/project.php | 36 ++++++++++++ src/slic.php | 12 ++++ 7 files changed, 125 insertions(+), 43 deletions(-) diff --git a/.env.slic b/.env.slic index 521e106..dea1b23 100644 --- a/.env.slic +++ b/.env.slic @@ -58,6 +58,13 @@ 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 +# The Playwright version the `playwright` command runs, as the `mcr.microsoft.com/playwright:v` image. +# Leave it unset to read the `@playwright/test` version from the target's package.json, which is what keeps the browser +# build in the image matching the library the project installed. Set it in a project's .env.slic.local to override that. +# SLIC_PLAYWRIGHT_VERSION=1.60.0 +# The full image the `playwright` command runs. Takes precedence over SLIC_PLAYWRIGHT_VERSION. +# SLIC_PLAYWRIGHT_IMAGE=mcr.microsoft.com/playwright:v1.60.0-noble + # XDebug configuration parameters, will apply to the `cli`, `wordpress` and `codeception` services. # =============================== # The IDE key used to identify connection requests coming from the services. diff --git a/changelog.md b/changelog.md index 2a7ccf8..64d1426 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,10 @@ 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). +# [Unreleased] - TBD +- Changed - `slic playwright` now runs Playwright in the `mcr.microsoft.com/playwright` image, whose tag is read from the `@playwright/test` version in the target's `package.json`. The browser comes with the image, so nothing is downloaded into the slic container and each project upgrades Playwright on its own schedule. Set `SLIC_PLAYWRIGHT_VERSION` or `SLIC_PLAYWRIGHT_IMAGE` to override the image. +- Changed - `slic playwright install` no longer installs anything, since the image already contains the browser. + # [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. diff --git a/skills/slic/references/slic-commands.md b/skills/slic/references/slic-commands.md index 25c09f0..b006537 100644 --- a/skills/slic/references/slic-commands.md +++ b/skills/slic/references/slic-commands.md @@ -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`). +Playwright runs in its own `playwright` service on the `mcr.microsoft.com/playwright` image, which already contains the browser. The image tag comes from the `@playwright/test` version in the target's `package.json`, so the target needs `@playwright/test` installed in its `node_modules`. Set `SLIC_PLAYWRIGHT_VERSION` or `SLIC_PLAYWRIGHT_IMAGE` in the target's `.env.slic.local` to override the image. `slic playwright install` is kept for existing scripts and does nothing. ```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 ``` diff --git a/slic-stack.yml b/slic-stack.yml index df00d17..0b0a3c1 100644 --- a/slic-stack.yml +++ b/slic-stack.yml @@ -140,6 +140,38 @@ services: retries: 30 shm_size: "${SLIC_CHROME_CONTAINER_SHM_SIZE:-512m}" + # Runs the Playwright CLI and the browser it drives. + # The `playwright` command sets SLIC_PLAYWRIGHT_IMAGE from the target's `package.json` before calling docker compose, + # so the browser build in the image always matches the Playwright version the project depends on. + # The suite only reaches the site over HTTP, so this service needs nothing from the `slic` container; it mounts the + # same volumes so the project sits at the same container path it does in the rest of the stack. + playwright: + image: ${SLIC_PLAYWRIGHT_IMAGE:-mcr.microsoft.com/playwright} + # The service has no long-running process and is only used through `docker compose run`. + # The profile keeps `slic up` from starting it. + profiles: + - playwright + networks: + - slic + user: "${SLIC_UID:-}:${SLIC_GID:-}" + depends_on: + wordpress: + condition: service_healthy + environment: + # The image has no home directory for the host user the container runs as. + HOME: /tmp + # The URL the suite points the browser at; the `wordpress.test` alias is on the `slic` network. + WP_URL: "${WP_URL:-http://wordpress.test}" + # If we're in CI context, then pass it through. + CI: "${CI:-}" + volumes: + # Paths are relative to the directory that contains this file, NOT the current working directory. + - ${SLIC_WP_DIR}:/var/www/html + - ${SLIC_PLUGINS_DIR}:${SLIC_WP_CONTENT_CONTAINER_DIR}/plugins + - ${SLIC_THEMES_DIR}:${SLIC_WP_CONTENT_CONTAINER_DIR}/themes + # Chromium crashes when /dev/shm is the 64m Docker default. + shm_size: "${SLIC_PLAYWRIGHT_CONTAINER_SHM_SIZE:-1g}" + slic: image: ghcr.io/stellarwp/slic-php${SLIC_PHP_VERSION}:${SLIC_VERSION} networks: diff --git a/src/commands/playwright.php b/src/commands/playwright.php index 3fc9734..d23a59c 100644 --- a/src/commands/playwright.php +++ b/src/commands/playwright.php @@ -13,7 +13,11 @@ $help = <<< HELP SUMMARY: - This command requires a use target set using the use command. + Runs Playwright commands in the stack. This command requires a use target set using the use command. + + Playwright runs in the mcr.microsoft.com/playwright image, which already contains the browser. + The image tag is read from the @playwright/test version in the target's package.json. + Set SLIC_PLAYWRIGHT_VERSION to use a different version, or SLIC_PLAYWRIGHT_IMAGE to use a different image. USAGE: @@ -21,9 +25,6 @@ EXAMPLES: - {$cli_name} playwright install - Install Playwright dependencies in the current use target. - {$cli_name} playwright test Run all Playwright tests following the Playwright configuration in the current use target. @@ -40,48 +41,39 @@ $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 ); +if ( $playwright_args === [ 'install' ] ) { + // The browser used to be downloaded into the slic container; the Playwright image already contains it. + echo colorize( 'The playwright service image already contains the browser, there is nothing to install.' . PHP_EOL ); + + exit( 0 ); } -$status = slic_realtime()( $command ); +if ( ! getenv( 'SLIC_PLAYWRIGHT_IMAGE' ) ) { + $version = getenv( 'SLIC_PLAYWRIGHT_VERSION' ) ?: get_target_playwright_version(); -// If there is a status other than 0, we have an error. Bail. -if ( $status ) { - exit( $status ); + if ( empty( $version ) ) { + echo magenta( "Could not read the @playwright/test version from the package.json file of {$using}." . PHP_EOL ); + echo magenta( 'Add @playwright/test to its dependencies, or set SLIC_PLAYWRIGHT_VERSION.' . PHP_EOL ); + + exit( 1 ); + } + + putenv( 'SLIC_PLAYWRIGHT_IMAGE=mcr.microsoft.com/playwright:v' . ltrim( $version, 'v' ) ); } +echo colorize( 'Playwright image: ' . getenv( 'SLIC_PLAYWRIGHT_IMAGE' ) . '' . PHP_EOL ); + +setup_id(); + +$status = slic_playwright_realtime()( array_merge( [ + 'run', + '--rm', + '--workdir', + escapeshellarg( get_project_container_path() ), + 'playwright', + 'node_modules/.bin/playwright', +], $playwright_args ) ); + exit( $status ); diff --git a/src/project.php b/src/project.php index e132984..a8b40dd 100644 --- a/src/project.php +++ b/src/project.php @@ -132,6 +132,42 @@ function get_project_container_path( $target = null ) { } } +/** + * Returns the Playwright version the target depends on. + * + * The Playwright image only contains the browser build matching its own tag, and Playwright refuses to run + * when the library and the browser build differ, so the version is read from the target's own `package.json`. + * Each project upgrades Playwright on its own schedule this way, and a slic release never changes it. + * + * @param string|null $target The target to read the version from, defaults to the current one. + * + * @return string|null The version, e.g. `1.60.0`, or `null` if the target does not depend on `@playwright/test`. + */ +function get_target_playwright_version( $target = null ) { + $project_path = get_project_local_path( $target ); + + if ( empty( $project_path ) || ! is_file( $project_path . '/package.json' ) ) { + return null; + } + + $package_json = json_decode( (string) file_get_contents( $project_path . '/package.json' ), true ); + + if ( ! is_array( $package_json ) ) { + return null; + } + + foreach ( [ 'devDependencies', 'dependencies' ] as $section ) { + $version = $package_json[ $section ]['@playwright/test'] ?? null; + + // Strip any range prefix so `^1.60.0` and `1.60.0` resolve to the same image tag. + if ( is_string( $version ) && preg_match( '/(\d+\.\d+\.\d+)/', $version, $matches ) ) { + return $matches[1]; + } + } + + return null; +} + /** * Returns the .slicrc file as an array. * diff --git a/src/slic.php b/src/slic.php index b541c75..fb88a29 100644 --- a/src/slic.php +++ b/src/slic.php @@ -1042,6 +1042,18 @@ function slic_realtime() { return docker_compose_realtime( slic_stack_array() ); } +/** + * Runs a process in slic stack, with the `playwright` profile enabled, and returns the exit status. + * + * The `playwright` service is in a profile so `slic up` does not start it, which means the profile has to be + * enabled for docker compose to address the service. + * + * @return \Closure The process closure to start a real-time process using slic stack. + */ +function slic_playwright_realtime() { + return docker_compose_realtime( array_merge( slic_stack_array(), [ '--profile', 'playwright' ] ) ); +} + /** * Returns the process Closure to start a real-time process using slic stack. * From 09ed1c2a5a5bff5e3e0f8c42195f80a6ae650a89 Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Mon, 21 Sep 2026 16:24:35 -0400 Subject: [PATCH 02/11] fix: keep profiled services out of slic up slic starts each service by name, and docker compose enables a service's profile when it is named, so `slic up` started the `playwright` service. --- src/services.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/services.php b/src/services.php index 8439e8e..59ccdb5 100644 --- a/src/services.php +++ b/src/services.php @@ -61,7 +61,17 @@ function services_schema() { * @return array The services in the stack. */ function get_services() { - $services = services_schema(); + /* + * Services in a profile, like `playwright`, are only started by the commands that use them. + * Docker compose enables a service's profile when the service is named on the command line, and + * the callers of this function start each service by name, so they have to be left out here. + */ + $services = array_filter( + services_schema(), + static function ( $service ) { + return empty( $service['profiles'] ); + } + ); $services = array_keys( $services ); sort( $services ); From dff70056aa3834db0631520bc9829ab9eb5bfc12 Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Mon, 21 Sep 2026 16:29:17 -0400 Subject: [PATCH 03/11] fix: use the installed Playwright version for the image tag - Read the version from node_modules/@playwright/test/package.json - The declared range can install a newer patch than the image tag - Works with any package manager, no lockfile parsing needed --- .env.slic | 4 +-- changelog.md | 2 +- skills/slic/references/slic-commands.md | 2 +- slic-stack.yml | 5 ++-- src/commands/playwright.php | 6 ++--- src/project.php | 34 ++++++++++--------------- 6 files changed, 24 insertions(+), 29 deletions(-) diff --git a/.env.slic b/.env.slic index dea1b23..cb13d60 100644 --- a/.env.slic +++ b/.env.slic @@ -59,8 +59,8 @@ SLIC_INTERACTIVE=1 SLIC_PHP_VERSION=7.4 # The Playwright version the `playwright` command runs, as the `mcr.microsoft.com/playwright:v` image. -# Leave it unset to read the `@playwright/test` version from the target's package.json, which is what keeps the browser -# build in the image matching the library the project installed. Set it in a project's .env.slic.local to override that. +# Leave it unset to use the `@playwright/test` version installed in the target's node_modules, which is what keeps the +# browser build in the image matching the library the project installed. Set it in a project's .env.slic.local to override that. # SLIC_PLAYWRIGHT_VERSION=1.60.0 # The full image the `playwright` command runs. Takes precedence over SLIC_PLAYWRIGHT_VERSION. # SLIC_PLAYWRIGHT_IMAGE=mcr.microsoft.com/playwright:v1.60.0-noble diff --git a/changelog.md b/changelog.md index 64d1426..185c400 100644 --- a/changelog.md +++ b/changelog.md @@ -5,7 +5,7 @@ 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). # [Unreleased] - TBD -- Changed - `slic playwright` now runs Playwright in the `mcr.microsoft.com/playwright` image, whose tag is read from the `@playwright/test` version in the target's `package.json`. The browser comes with the image, so nothing is downloaded into the slic container and each project upgrades Playwright on its own schedule. Set `SLIC_PLAYWRIGHT_VERSION` or `SLIC_PLAYWRIGHT_IMAGE` to override the image. +- Changed - `slic playwright` now runs Playwright in the `mcr.microsoft.com/playwright` image, whose tag is the `@playwright/test` version installed in the target's `node_modules`. The browser comes with the image, so nothing is downloaded into the slic container and each project upgrades Playwright on its own schedule. Set `SLIC_PLAYWRIGHT_VERSION` or `SLIC_PLAYWRIGHT_IMAGE` to override the image. - Changed - `slic playwright install` no longer installs anything, since the image already contains the browser. # [2.4.2] - 2026-09-08 diff --git a/skills/slic/references/slic-commands.md b/skills/slic/references/slic-commands.md index b006537..9aa39df 100644 --- a/skills/slic/references/slic-commands.md +++ b/skills/slic/references/slic-commands.md @@ -123,7 +123,7 @@ Inside the slic shell, shorthand commands are available: Runs Playwright commands in the stack for browser-based testing. -Playwright runs in its own `playwright` service on the `mcr.microsoft.com/playwright` image, which already contains the browser. The image tag comes from the `@playwright/test` version in the target's `package.json`, so the target needs `@playwright/test` installed in its `node_modules`. Set `SLIC_PLAYWRIGHT_VERSION` or `SLIC_PLAYWRIGHT_IMAGE` in the target's `.env.slic.local` to override the image. `slic playwright install` is kept for existing scripts and does nothing. +Playwright runs in its own `playwright` service on the `mcr.microsoft.com/playwright` image, which already contains the browser. The image tag is the `@playwright/test` version installed in the target's `node_modules`, so run the project's package manager install first. Set `SLIC_PLAYWRIGHT_VERSION` or `SLIC_PLAYWRIGHT_IMAGE` in the target's `.env.slic.local` to override the image. `slic playwright install` is kept for existing scripts and does nothing. ```bash slic playwright test # run all Playwright tests diff --git a/slic-stack.yml b/slic-stack.yml index 0b0a3c1..399ba46 100644 --- a/slic-stack.yml +++ b/slic-stack.yml @@ -141,8 +141,9 @@ services: shm_size: "${SLIC_CHROME_CONTAINER_SHM_SIZE:-512m}" # Runs the Playwright CLI and the browser it drives. - # The `playwright` command sets SLIC_PLAYWRIGHT_IMAGE from the target's `package.json` before calling docker compose, - # so the browser build in the image always matches the Playwright version the project depends on. + # The `playwright` command sets SLIC_PLAYWRIGHT_IMAGE from the `@playwright/test` version installed in the target + # before calling docker compose, so the browser build in the image always matches the Playwright version the project + # installed. # The suite only reaches the site over HTTP, so this service needs nothing from the `slic` container; it mounts the # same volumes so the project sits at the same container path it does in the rest of the stack. playwright: diff --git a/src/commands/playwright.php b/src/commands/playwright.php index d23a59c..f80448f 100644 --- a/src/commands/playwright.php +++ b/src/commands/playwright.php @@ -16,7 +16,7 @@ Runs Playwright commands in the stack. This command requires a use target set using the use command. Playwright runs in the mcr.microsoft.com/playwright image, which already contains the browser. - The image tag is read from the @playwright/test version in the target's package.json. + The image tag is the @playwright/test version installed in the target's node_modules. Set SLIC_PLAYWRIGHT_VERSION to use a different version, or SLIC_PLAYWRIGHT_IMAGE to use a different image. USAGE: @@ -54,8 +54,8 @@ $version = getenv( 'SLIC_PLAYWRIGHT_VERSION' ) ?: get_target_playwright_version(); if ( empty( $version ) ) { - echo magenta( "Could not read the @playwright/test version from the package.json file of {$using}." . PHP_EOL ); - echo magenta( 'Add @playwright/test to its dependencies, or set SLIC_PLAYWRIGHT_VERSION.' . PHP_EOL ); + echo magenta( "@playwright/test is not installed in the node_modules directory of {$using}." . PHP_EOL ); + echo magenta( 'Add it to the project and run its package manager install, or set SLIC_PLAYWRIGHT_VERSION.' . PHP_EOL ); exit( 1 ); } diff --git a/src/project.php b/src/project.php index a8b40dd..2e5d207 100644 --- a/src/project.php +++ b/src/project.php @@ -133,39 +133,33 @@ function get_project_container_path( $target = null ) { } /** - * Returns the Playwright version the target depends on. + * Returns the version of `@playwright/test` installed in the target. * * The Playwright image only contains the browser build matching its own tag, and Playwright refuses to run - * when the library and the browser build differ, so the version is read from the target's own `package.json`. - * Each project upgrades Playwright on its own schedule this way, and a slic release never changes it. + * when the library and the browser build differ, so the version has to be the one actually installed. + * The range declared in the target's `package.json` is not enough: `^1.60.0` can install `1.60.1`. + * + * Every package manager that creates a `node_modules` directory writes the installed package's own + * `package.json` there, so reading it gives the exact version whichever lockfile format the project uses. + * `node_modules/.bin/playwright` is the CLI of this package, and it pins `playwright` and `playwright-core` + * to its own version, which is what the browser build depends on. * * @param string|null $target The target to read the version from, defaults to the current one. * - * @return string|null The version, e.g. `1.60.0`, or `null` if the target does not depend on `@playwright/test`. + * @return string|null The version, e.g. `1.60.0`, or `null` if `@playwright/test` is not installed in the target. */ function get_target_playwright_version( $target = null ) { $project_path = get_project_local_path( $target ); + $package_file = $project_path . '/node_modules/@playwright/test/package.json'; - if ( empty( $project_path ) || ! is_file( $project_path . '/package.json' ) ) { - return null; - } - - $package_json = json_decode( (string) file_get_contents( $project_path . '/package.json' ), true ); - - if ( ! is_array( $package_json ) ) { + if ( empty( $project_path ) || ! is_file( $package_file ) ) { return null; } - foreach ( [ 'devDependencies', 'dependencies' ] as $section ) { - $version = $package_json[ $section ]['@playwright/test'] ?? null; - - // Strip any range prefix so `^1.60.0` and `1.60.0` resolve to the same image tag. - if ( is_string( $version ) && preg_match( '/(\d+\.\d+\.\d+)/', $version, $matches ) ) { - return $matches[1]; - } - } + $package_json = json_decode( (string) file_get_contents( $package_file ), true ); + $version = is_array( $package_json ) ? ( $package_json['version'] ?? null ) : null; - return null; + return is_string( $version ) && preg_match( '/^\d+\.\d+\.\d+/', $version ) ? $version : null; } /** From 095a79fa2d7981d88268f54ddc05b1c55c39d720 Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Mon, 21 Sep 2026 20:04:54 -0400 Subject: [PATCH 04/11] fix: require an exact @playwright/test pin for the image tag - Read the version from the target's package.json again - Error when it is a range, since a range can install a version the image does not match - Error when @playwright/test is not a dependency - Works before the project's dependencies are installed --- .env.slic | 4 +- changelog.md | 2 +- skills/slic/references/slic-commands.md | 2 +- slic-stack.yml | 6 +-- src/commands/playwright.php | 22 ++++++++--- src/project.php | 52 +++++++++++++++++-------- 6 files changed, 59 insertions(+), 29 deletions(-) diff --git a/.env.slic b/.env.slic index cb13d60..62cf84b 100644 --- a/.env.slic +++ b/.env.slic @@ -59,8 +59,8 @@ SLIC_INTERACTIVE=1 SLIC_PHP_VERSION=7.4 # The Playwright version the `playwright` command runs, as the `mcr.microsoft.com/playwright:v` image. -# Leave it unset to use the `@playwright/test` version installed in the target's node_modules, which is what keeps the -# browser build in the image matching the library the project installed. Set it in a project's .env.slic.local to override that. +# Leave it unset to use the `@playwright/test` version in the target's package.json, which has to be an exact version +# such as `1.60.0` so the browser build in the image matches the library the project installs. Set it in a project's .env.slic.local to override that. # SLIC_PLAYWRIGHT_VERSION=1.60.0 # The full image the `playwright` command runs. Takes precedence over SLIC_PLAYWRIGHT_VERSION. # SLIC_PLAYWRIGHT_IMAGE=mcr.microsoft.com/playwright:v1.60.0-noble diff --git a/changelog.md b/changelog.md index 185c400..ecb3368 100644 --- a/changelog.md +++ b/changelog.md @@ -5,7 +5,7 @@ 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). # [Unreleased] - TBD -- Changed - `slic playwright` now runs Playwright in the `mcr.microsoft.com/playwright` image, whose tag is the `@playwright/test` version installed in the target's `node_modules`. The browser comes with the image, so nothing is downloaded into the slic container and each project upgrades Playwright on its own schedule. Set `SLIC_PLAYWRIGHT_VERSION` or `SLIC_PLAYWRIGHT_IMAGE` to override the image. +- Changed - `slic playwright` now runs Playwright in the `mcr.microsoft.com/playwright` image, whose tag is the `@playwright/test` version in the target's `package.json`. The version has to be an exact version such as `1.60.0`; a range such as `^1.60.0` is rejected. The browser comes with the image, so nothing is downloaded into the slic container and each project upgrades Playwright on its own schedule. Set `SLIC_PLAYWRIGHT_VERSION` or `SLIC_PLAYWRIGHT_IMAGE` to override the image. - Changed - `slic playwright install` no longer installs anything, since the image already contains the browser. # [2.4.2] - 2026-09-08 diff --git a/skills/slic/references/slic-commands.md b/skills/slic/references/slic-commands.md index 9aa39df..0ae5aea 100644 --- a/skills/slic/references/slic-commands.md +++ b/skills/slic/references/slic-commands.md @@ -123,7 +123,7 @@ Inside the slic shell, shorthand commands are available: Runs Playwright commands in the stack for browser-based testing. -Playwright runs in its own `playwright` service on the `mcr.microsoft.com/playwright` image, which already contains the browser. The image tag is the `@playwright/test` version installed in the target's `node_modules`, so run the project's package manager install first. Set `SLIC_PLAYWRIGHT_VERSION` or `SLIC_PLAYWRIGHT_IMAGE` in the target's `.env.slic.local` to override the image. `slic playwright install` is kept for existing scripts and does nothing. +Playwright runs in its own `playwright` service on the `mcr.microsoft.com/playwright` image, which already contains the browser. The image tag is the `@playwright/test` version in the target's `package.json`, which has to be pinned to an exact version such as `1.60.0`; a range such as `^1.60.0` is rejected, because it can install a version the image does not match. Set `SLIC_PLAYWRIGHT_VERSION` or `SLIC_PLAYWRIGHT_IMAGE` in the target's `.env.slic.local` to override the image. `slic playwright install` is kept for existing scripts and does nothing. ```bash slic playwright test # run all Playwright tests diff --git a/slic-stack.yml b/slic-stack.yml index 399ba46..3fdb56a 100644 --- a/slic-stack.yml +++ b/slic-stack.yml @@ -141,9 +141,9 @@ services: shm_size: "${SLIC_CHROME_CONTAINER_SHM_SIZE:-512m}" # Runs the Playwright CLI and the browser it drives. - # The `playwright` command sets SLIC_PLAYWRIGHT_IMAGE from the `@playwright/test` version installed in the target - # before calling docker compose, so the browser build in the image always matches the Playwright version the project - # installed. + # The `playwright` command sets SLIC_PLAYWRIGHT_IMAGE from the exact `@playwright/test` version pinned in the target's + # `package.json` before calling docker compose, so the browser build in the image matches the Playwright version the + # project installs. # The suite only reaches the site over HTTP, so this service needs nothing from the `slic` container; it mounts the # same volumes so the project sits at the same container path it does in the rest of the stack. playwright: diff --git a/src/commands/playwright.php b/src/commands/playwright.php index f80448f..901d9bf 100644 --- a/src/commands/playwright.php +++ b/src/commands/playwright.php @@ -16,7 +16,7 @@ Runs Playwright commands in the stack. This command requires a use target set using the use command. Playwright runs in the mcr.microsoft.com/playwright image, which already contains the browser. - The image tag is the @playwright/test version installed in the target's node_modules. + The image tag is the @playwright/test version in the target's package.json, which has to be an exact version such as 1.60.0. Set SLIC_PLAYWRIGHT_VERSION to use a different version, or SLIC_PLAYWRIGHT_IMAGE to use a different image. USAGE: @@ -51,13 +51,25 @@ } if ( ! getenv( 'SLIC_PLAYWRIGHT_IMAGE' ) ) { - $version = getenv( 'SLIC_PLAYWRIGHT_VERSION' ) ?: get_target_playwright_version(); + $version = getenv( 'SLIC_PLAYWRIGHT_VERSION' ); if ( empty( $version ) ) { - echo magenta( "@playwright/test is not installed in the node_modules directory of {$using}." . PHP_EOL ); - echo magenta( 'Add it to the project and run its package manager install, or set SLIC_PLAYWRIGHT_VERSION.' . PHP_EOL ); + $declared = get_target_playwright_dependency(); - exit( 1 ); + if ( $declared === null ) { + echo magenta( "@playwright/test is not a dependency in the package.json file of {$using}." . PHP_EOL ); + + exit( 1 ); + } + + $version = playwright_exact_version( $declared ); + + if ( $version === null ) { + echo magenta( "@playwright/test must be pinned to an exact version in the package.json file of {$using}, e.g. \"1.60.0\"; found \"{$declared}\"." . PHP_EOL ); + echo magenta( 'The Playwright image only contains the browser build for its own version, and a range can install a different version.' . PHP_EOL ); + + exit( 1 ); + } } putenv( 'SLIC_PLAYWRIGHT_IMAGE=mcr.microsoft.com/playwright:v' . ltrim( $version, 'v' ) ); diff --git a/src/project.php b/src/project.php index 2e5d207..e3b6df4 100644 --- a/src/project.php +++ b/src/project.php @@ -133,33 +133,51 @@ function get_project_container_path( $target = null ) { } /** - * Returns the version of `@playwright/test` installed in the target. - * - * The Playwright image only contains the browser build matching its own tag, and Playwright refuses to run - * when the library and the browser build differ, so the version has to be the one actually installed. - * The range declared in the target's `package.json` is not enough: `^1.60.0` can install `1.60.1`. - * - * Every package manager that creates a `node_modules` directory writes the installed package's own - * `package.json` there, so reading it gives the exact version whichever lockfile format the project uses. - * `node_modules/.bin/playwright` is the CLI of this package, and it pins `playwright` and `playwright-core` - * to its own version, which is what the browser build depends on. + * Returns the `@playwright/test` version the target declares in its `package.json`, as written. * * @param string|null $target The target to read the version from, defaults to the current one. * - * @return string|null The version, e.g. `1.60.0`, or `null` if `@playwright/test` is not installed in the target. + * @return string|null The declared version, e.g. `1.60.0` or `^1.60.0`, or `null` if the target does not declare + * `@playwright/test` as a dependency. */ -function get_target_playwright_version( $target = null ) { +function get_target_playwright_dependency( $target = null ) { $project_path = get_project_local_path( $target ); - $package_file = $project_path . '/node_modules/@playwright/test/package.json'; - if ( empty( $project_path ) || ! is_file( $package_file ) ) { + if ( empty( $project_path ) || ! is_file( $project_path . '/package.json' ) ) { return null; } - $package_json = json_decode( (string) file_get_contents( $package_file ), true ); - $version = is_array( $package_json ) ? ( $package_json['version'] ?? null ) : null; + $package_json = json_decode( (string) file_get_contents( $project_path . '/package.json' ), true ); + + if ( ! is_array( $package_json ) ) { + return null; + } + + foreach ( [ 'devDependencies', 'dependencies' ] as $section ) { + $version = $package_json[ $section ]['@playwright/test'] ?? null; + + if ( is_string( $version ) ) { + return trim( $version ); + } + } + + return null; +} - return is_string( $version ) && preg_match( '/^\d+\.\d+\.\d+/', $version ) ? $version : null; +/** + * Returns the version if a declared `@playwright/test` version is an exact version. + * + * The Playwright image only contains the browser build matching its own tag, and Playwright refuses to run + * when the library and the browser build differ. A range such as `^1.60.0` can install `1.60.1`, which the + * `v1.60.0` image does not match, so only an exact version identifies the image. Package managers install + * an exact version as written, whichever lockfile format the project uses. + * + * @param string $declared The version as declared in `package.json`. + * + * @return string|null The version, e.g. `1.60.0`, or `null` if the declared version is not an exact version. + */ +function playwright_exact_version( string $declared ) { + return preg_match( '/^\d+\.\d+\.\d+$/', $declared ) ? $declared : null; } /** From df5137826d5279b731c7760af9c6b60deeb3a737 Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Mon, 21 Sep 2026 20:28:50 -0400 Subject: [PATCH 05/11] fix: strip control characters from the rejected Playwright version The value comes from the project's package.json, so it could otherwise write terminal escape sequences into the error message. --- src/commands/playwright.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commands/playwright.php b/src/commands/playwright.php index 901d9bf..31f3fc9 100644 --- a/src/commands/playwright.php +++ b/src/commands/playwright.php @@ -65,7 +65,10 @@ $version = playwright_exact_version( $declared ); if ( $version === null ) { - echo magenta( "@playwright/test must be pinned to an exact version in the package.json file of {$using}, e.g. \"1.60.0\"; found \"{$declared}\"." . PHP_EOL ); + // The value comes from the project's package.json: strip control characters so it cannot write terminal escape sequences. + $shown = preg_replace( '/\p{Cc}/u', '', $declared ); + + echo magenta( "@playwright/test must be pinned to an exact version in the package.json file of {$using}, e.g. \"1.60.0\"; found \"{$shown}\"." . PHP_EOL ); echo magenta( 'The Playwright image only contains the browser build for its own version, and a range can install a different version.' . PHP_EOL ); exit( 1 ); From 64a36d51e563368847c247fd74409c39683ac27f Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 22 Sep 2026 14:33:07 -0600 Subject: [PATCH 06/11] Run Playwright browsers remotely while keeping tests in slic --- .env.slic | 9 +- README.md | 12 ++ changelog.md | 7 +- docs/playwright.md | 174 +++++++++++++++++++ skills/slic/references/slic-commands.md | 2 +- slic-stack.yml | 29 ++-- slic.php | 3 + src/commands/playwright.php | 59 +------ src/docker-argv.php | 216 ++++++++++++++++++++++++ src/playwright.php | 178 +++++++++++++++++++ src/process-argv.php | 134 +++++++++++++++ src/project.php | 48 ------ src/slic.php | 12 -- 13 files changed, 743 insertions(+), 140 deletions(-) create mode 100644 docs/playwright.md create mode 100644 src/docker-argv.php create mode 100644 src/playwright.php create mode 100644 src/process-argv.php diff --git a/.env.slic b/.env.slic index 62cf84b..73c5cc7 100644 --- a/.env.slic +++ b/.env.slic @@ -58,12 +58,11 @@ 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 -# The Playwright version the `playwright` command runs, as the `mcr.microsoft.com/playwright:v` image. -# Leave it unset to use the `@playwright/test` version in the target's package.json, which has to be an exact version -# such as `1.60.0` so the browser build in the image matches the library the project installs. Set it in a project's .env.slic.local to override that. -# SLIC_PLAYWRIGHT_VERSION=1.60.0 -# The full image the `playwright` command runs. Takes precedence over SLIC_PLAYWRIGHT_VERSION. +# 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. # =============================== diff --git a/README.md b/README.md index d289131..2a501b8 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 diff --git a/changelog.md b/changelog.md index ecb3368..00b1c6b 100644 --- a/changelog.md +++ b/changelog.md @@ -5,8 +5,11 @@ 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). # [Unreleased] - TBD -- Changed - `slic playwright` now runs Playwright in the `mcr.microsoft.com/playwright` image, whose tag is the `@playwright/test` version in the target's `package.json`. The version has to be an exact version such as `1.60.0`; a range such as `^1.60.0` is rejected. The browser comes with the image, so nothing is downloaded into the slic container and each project upgrades Playwright on its own schedule. Set `SLIC_PLAYWRIGHT_VERSION` or `SLIC_PLAYWRIGHT_IMAGE` to override the image. -- Changed - `slic playwright install` no longer installs anything, since the image already contains the browser. + +- 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. # [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. diff --git a/docs/playwright.md b/docs/playwright.md new file mode 100644 index 0000000..28c56db --- /dev/null +++ b/docs/playwright.md @@ -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:` 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. diff --git a/skills/slic/references/slic-commands.md b/skills/slic/references/slic-commands.md index 0ae5aea..86999a5 100644 --- a/skills/slic/references/slic-commands.md +++ b/skills/slic/references/slic-commands.md @@ -123,7 +123,7 @@ Inside the slic shell, shorthand commands are available: Runs Playwright commands in the stack for browser-based testing. -Playwright runs in its own `playwright` service on the `mcr.microsoft.com/playwright` image, which already contains the browser. The image tag is the `@playwright/test` version in the target's `package.json`, which has to be pinned to an exact version such as `1.60.0`; a range such as `^1.60.0` is rejected, because it can install a version the image does not match. Set `SLIC_PLAYWRIGHT_VERSION` or `SLIC_PLAYWRIGHT_IMAGE` in the target's `.env.slic.local` to override the image. `slic playwright install` is kept for existing scripts and does nothing. +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 test # run all Playwright tests diff --git a/slic-stack.yml b/slic-stack.yml index 3fdb56a..2841ae3 100644 --- a/slic-stack.yml +++ b/slic-stack.yml @@ -140,37 +140,30 @@ services: retries: 30 shm_size: "${SLIC_CHROME_CONTAINER_SHM_SIZE:-512m}" - # Runs the Playwright CLI and the browser it drives. - # The `playwright` command sets SLIC_PLAYWRIGHT_IMAGE from the exact `@playwright/test` version pinned in the target's - # `package.json` before calling docker compose, so the browser build in the image matches the Playwright version the - # project installs. - # The suite only reaches the site over HTTP, so this service needs nothing from the `slic` container; it mounts the - # same volumes so the project sits at the same container path it does in the rest of the stack. + # 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} - # The service has no long-running process and is only used through `docker compose run`. - # The profile keeps `slic up` from starting it. profiles: - playwright + init: true networks: - slic user: "${SLIC_UID:-}:${SLIC_GID:-}" - depends_on: - wordpress: - condition: service_healthy environment: - # The image has no home directory for the host user the container runs as. HOME: /tmp - # The URL the suite points the browser at; the `wordpress.test` alias is on the `slic` network. - WP_URL: "${WP_URL:-http://wordpress.test}" - # If we're in CI context, then pass it through. - CI: "${CI:-}" volumes: - # Paths are relative to the directory that contains this file, NOT the current working directory. + # 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 - # Chromium crashes when /dev/shm is the 64m Docker default. + 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 + start_period: 2s shm_size: "${SLIC_PLAYWRIGHT_CONTAINER_SHM_SIZE:-1g}" slic: diff --git a/slic.php b/slic.php index 098a5aa..ff2b25c 100644 --- a/slic.php +++ b/slic.php @@ -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'; diff --git a/src/commands/playwright.php b/src/commands/playwright.php index 31f3fc9..a624937 100644 --- a/src/commands/playwright.php +++ b/src/commands/playwright.php @@ -15,9 +15,10 @@ Runs Playwright commands in the stack. This command requires a use target set using the use command. - Playwright runs in the mcr.microsoft.com/playwright image, which already contains the browser. - The image tag is the @playwright/test version in the target's package.json, which has to be an exact version such as 1.60.0. - Set SLIC_PLAYWRIGHT_VERSION to use a different version, or SLIC_PLAYWRIGHT_IMAGE to use a different image. + Tests and PHP hooks run in slic; 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 SLIC_PLAYWRIGHT_IMAGE 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: @@ -41,54 +42,4 @@ $using = slic_target_or_fail(); echo light_cyan( "Using {$using}" . PHP_EOL ); -$playwright_args = $args( '...' ); - -if ( $playwright_args === [ 'install' ] ) { - // The browser used to be downloaded into the slic container; the Playwright image already contains it. - echo colorize( 'The playwright service image already contains the browser, there is nothing to install.' . PHP_EOL ); - - exit( 0 ); -} - -if ( ! getenv( 'SLIC_PLAYWRIGHT_IMAGE' ) ) { - $version = getenv( 'SLIC_PLAYWRIGHT_VERSION' ); - - if ( empty( $version ) ) { - $declared = get_target_playwright_dependency(); - - if ( $declared === null ) { - echo magenta( "@playwright/test is not a dependency in the package.json file of {$using}." . PHP_EOL ); - - exit( 1 ); - } - - $version = playwright_exact_version( $declared ); - - if ( $version === null ) { - // The value comes from the project's package.json: strip control characters so it cannot write terminal escape sequences. - $shown = preg_replace( '/\p{Cc}/u', '', $declared ); - - echo magenta( "@playwright/test must be pinned to an exact version in the package.json file of {$using}, e.g. \"1.60.0\"; found \"{$shown}\"." . PHP_EOL ); - echo magenta( 'The Playwright image only contains the browser build for its own version, and a range can install a different version.' . PHP_EOL ); - - exit( 1 ); - } - } - - putenv( 'SLIC_PLAYWRIGHT_IMAGE=mcr.microsoft.com/playwright:v' . ltrim( $version, 'v' ) ); -} - -echo colorize( 'Playwright image: ' . getenv( 'SLIC_PLAYWRIGHT_IMAGE' ) . '' . PHP_EOL ); - -setup_id(); - -$status = slic_playwright_realtime()( array_merge( [ - 'run', - '--rm', - '--workdir', - escapeshellarg( get_project_container_path() ), - 'playwright', - 'node_modules/.bin/playwright', -], $playwright_args ) ); - -exit( $status ); +exit( run_playwright( $args( '...' ) ) ); diff --git a/src/docker-argv.php b/src/docker-argv.php new file mode 100644 index 0000000..2a53dbc --- /dev/null +++ b/src/docker-argv.php @@ -0,0 +1,216 @@ + [ '-T' ], 'run' => [ '-T' ], 'logs' => [ '--no-color' ] ]; + array_splice( $arguments, 1, 0, $flags[ $subcommand ] ?? [] ); + } + + $command = array_merge( $prefix, $options, $arguments ); + + if ( ! $realtime ) { + return process_argv( $command, $environment ); + } + + // Preserve the existing Compose exec terminal workaround through stdin's descriptor. + $stdin_null = $subcommand === 'exec' && is_tty_supported(); + + return process_argv_realtime( $command, $environment, $stdin_null ); +} + +/** + * Read the configured Compose executable and optional prefix arguments. + * + * The legacy setting is a string. Split quoted words once at this boundary, without + * shell expansion. Executable paths, docker-compose and "docker --context ... compose" + * are supported. Shell operators require an executable wrapper script instead. + * + * @return string[] The executable and its prefix arguments. + */ +function docker_compose_binary_argv(): array { + $command = trim( docker_compose_bin() ); + + // An existing executable path may contain spaces without being shell-quoted. + // Only parse words when the setting is not itself a file path. + if ( is_file( $command ) ) { + return [ $command ]; + } + + return docker_compose_prefix_argv( $command, DIRECTORY_SEPARATOR === '\\' ); +} + +/** + * Split a configured command prefix, preserving platform-specific path separators. + * + * @param string $command The executable and optional quoted prefix arguments. + * @param bool $windows Whether backslashes are Windows path separators. + * + * @return string[] Literal prefix arguments, with quoting removed. + */ +function docker_compose_prefix_argv( string $command, bool $windows ): array { + // Track whether a word has started separately from its contents: quoted empty + // arguments must survive, while whitespace between words adds no arguments. + $arguments = []; + $word = ''; + $quote = null; + $started = false; + $length = strlen( $command ); + + for ( $i = 0; $i < $length; $i++ ) { + $character = $command[ $i ]; + $next = $command[ $i + 1 ] ?? ''; + $escapable = $quote === '"' ? '\\"$`' : "\\\"' \t"; + + // Preserve Windows path separators, including UNC prefixes. On POSIX, only + // consume the supported escapes; single-quoted contents stay literal below. + if ( $windows ) { + $escapable = '"'; + } + + if ( $character === '\\' && $quote !== "'" && $next !== '' && strpos( $escapable, $next ) !== false ) { + $word .= $next; + $started = true; + $i++; + + continue; + } + + if ( $quote !== null ) { + if ( $character === $quote ) { + $quote = null; + } else { + $word .= $character; + } + + continue; + } + + if ( $character === '"' || $character === "'" ) { + $quote = $character; + $started = true; + + continue; + } + + if ( strpos( " \t\r\n", $character ) !== false ) { + if ( $started ) { + $arguments[] = $word; + $word = ''; + $started = false; + } + + continue; + } + + if ( strpos( '|&;<>()`', $character ) !== false ) { + // No shell evaluates this prefix. Reject unquoted operators instead of + // silently passing a pipeline or redirection to Docker as literal arguments. + throw new \InvalidArgumentException( 'SLIC_DOCKER_COMPOSE_BIN cannot contain shell operators with the argv runner. Use an executable wrapper script.' ); + } + + $word .= $character; + $started = true; + } + + if ( $quote !== null ) { + throw new \InvalidArgumentException( 'SLIC_DOCKER_COMPOSE_BIN contains an unmatched quote.' ); + } + + if ( $started ) { + $arguments[] = $word; + } + + if ( ! $arguments || $arguments[0] === '' ) { + throw new \InvalidArgumentException( 'SLIC_DOCKER_COMPOSE_BIN requires an executable.' ); + } + + return $arguments; +} diff --git a/src/playwright.php b/src/playwright.php new file mode 100644 index 0000000..0d82c76 --- /dev/null +++ b/src/playwright.php @@ -0,0 +1,178 @@ +' . getenv( 'SLIC_PLAYWRIGHT_IMAGE' ) . ' (tests run in slic)' . PHP_EOL ); + + // Each run owns only its own server, including concurrent runs using different versions. + $name = 'slic-playwright-' . bin2hex( random_bytes( 8 ) ); + $cleaned = false; + // Both finally and shutdown can reach this callback. Remove the container once, + // including when a signal exits PHP before the finally block can run. + $cleanup = static function () use ( $name, &$cleaned ) { + if ( ! $cleaned ) { + $cleaned = true; + process_argv( [ 'docker', 'rm', '--force', $name ] ); + } + }; + register_shutdown_function( $cleanup ); + $signals = []; + $async_signals = null; + + // When PCNTL is available, handle Ctrl+C and termination signals while waiting on + // child processes. Exiting runs the registered browser cleanup and returns the + // conventional 128 + signal status. Save the existing handlers and async mode so + // the finally block can restore them when this command returns normally. + if ( function_exists( 'pcntl_async_signals' ) && function_exists( 'pcntl_signal_get_handler' ) ) { + $async_signals = pcntl_async_signals( true ); + + foreach ( [ SIGINT, SIGTERM ] as $signal ) { + $signals[ $signal ] = pcntl_signal_get_handler( $signal ); + pcntl_signal( $signal, static function ( int $received ) { + exit( 128 + $received ); + } ); + } + } + + try { + // Use the project's mounted CLI for the server as well as the client. + // Keep the container until cleanup so startup failures still have readable logs. + $status = docker_compose_argv_realtime( [ + 'run', '--detach', '--no-deps', '--name', $name, + '--workdir', get_project_container_path(), + 'playwright', 'node_modules/.bin/playwright', 'run-server', '--host', '0.0.0.0', '--port', '3000', + ], slic_stack_argv() ); + + if ( $status !== 0 ) { + return $status; + } + + if ( ! wait_for_playwright_server( $name ) ) { + echo magenta( "Playwright browser server exited or did not become ready within 30 seconds. Server logs follow:\n" ); + process_argv_realtime( [ 'docker', 'logs', $name ] ); + + return 1; + } + + return docker_compose_argv_realtime( playwright_exec_command( $arguments, 'ws://' . $name . ':3000/' ), slic_stack_argv() ); + } finally { + $cleanup(); + + foreach ( $signals as $signal => $handler ) { + pcntl_signal( $signal, $handler ); + } + + if ( $async_signals !== null ) { + pcntl_async_signals( $async_signals ); + } + } +} diff --git a/src/process-argv.php b/src/process-argv.php new file mode 100644 index 0000000..aaa4a6d --- /dev/null +++ b/src/process-argv.php @@ -0,0 +1,134 @@ + $environment Overrides for the inherited child environment. + * + * @return array{status: int, stdout: string} The exit status and unmodified stdout. + */ +function process_argv( array $arguments, array $environment = [] ): array { + // A file cannot fill a pipe buffer while the child is running, including on Windows. + $output = tmpfile(); + + if ( $output === false ) { + throw new \RuntimeException( 'Could not create a process output buffer.' ); + } + + try { + // Captured commands get EOF on stdin and leave errors visible on stderr. + // Rewind after the child exits to read stdout from the beginning of the file. + $status = run_process_argv( $arguments, [ [ 'file', process_null_device(), 'r' ], $output, STDERR ], $environment ); + rewind( $output ); + $text = stream_get_contents( $output ); + } finally { + fclose( $output ); + } + + return [ 'status' => $status, 'stdout' => $text ]; +} + +/** + * Run raw arguments with live output and interruptible child supervision. + * + * @param string[] $arguments Executable followed by literal, unquoted arguments. + * @param array $environment Overrides for the inherited child environment. + * @param bool $stdin_null Whether to close input instead of inheriting the terminal. + * + * @return int The child exit status. + */ +function process_argv_realtime( array $arguments, array $environment = [], bool $stdin_null = false ): int { + setup_terminal(); + echo PHP_EOL; + $input = $stdin_null ? [ 'file', process_null_device(), 'r' ] : STDIN; + + return run_process_argv( $arguments, [ $input, STDOUT, STDERR ], $environment ); +} + +/** + * Return the platform's null device for descriptor-based input redirection. + * + * @return string The null device path. + */ +function process_null_device(): string { + return DIRECTORY_SEPARATOR === '\\' ? 'NUL' : '/dev/null'; +} + +/** + * Execute an argument vector directly, without parsing or escaping a shell command. + * + * Polling allows PHP signal handlers to run while the child is active. Shutdown + * terminates the local child; remote processes started by Docker have their own lifecycle. + * + * @param string[] $arguments Executable followed by literal, unquoted arguments. + * @param array $descriptors The proc_open descriptors for stdin, stdout and stderr. + * @param array $environment Overrides for the inherited child environment. + * + * @return int The exit status, or 128 plus the terminating signal. + */ +function run_process_argv( array $arguments, array $descriptors, array $environment = [] ): int { + if ( ! $arguments || ! isset( $arguments[0] ) || $arguments[0] === '' ) { + throw new \InvalidArgumentException( 'A process requires an executable.' ); + } + + foreach ( $arguments as $argument ) { + if ( ! is_string( $argument ) || strpos( $argument, "\0" ) !== false ) { + throw new \InvalidArgumentException( 'Process arguments must be strings without null bytes.' ); + } + } + + debug( 'Executing arguments: ' . json_encode( $arguments ) . PHP_EOL ); + // An explicit environment replaces inheritance in proc_open, so merge overrides + // with the current environment to retain PATH and other caller settings. + // Passing an argument array preserves boundaries without a shell command string. + $env = $environment ? array_replace( getenv(), $environment ) : null; + $child = proc_open( array_values( $arguments ), $descriptors, $pipes, null, $env ); + + if ( ! is_resource( $child ) ) { + return 1; + } + + // exit() in a signal handler bypasses finally. Keep a shutdown fallback, with a + // reference so normal completion can clear the handle and make this a no-op. + register_shutdown_function( static function () use ( &$child ) { + if ( is_resource( $child ) ) { + proc_terminate( $child ); + } + } ); + + try { + // Poll instead of blocking in proc_close so PHP can dispatch signal handlers + // promptly. The short sleep avoids busy-waiting while the child is running. + do { + $state = proc_get_status( $child ); + + if ( $state === false ) { + throw new \RuntimeException( 'Could not read the child process status.' ); + } + + if ( $state['running'] ) { + usleep( 20000 ); + } + } while ( $state['running'] ); + + $closed = proc_close( $child ); + $child = null; + + // Older PHP versions may return -1 from proc_close after proc_get_status has + // collected the exit code. Prefer that saved code, then fall back to the close + // result or the conventional status for a process terminated by a signal. + return $state['exitcode'] >= 0 ? $state['exitcode'] : ( $closed >= 0 ? $closed : 128 + $state['termsig'] ); + } finally { + if ( is_resource( $child ) ) { + proc_terminate( $child ); + proc_close( $child ); + $child = null; + } + } +} diff --git a/src/project.php b/src/project.php index e3b6df4..e132984 100644 --- a/src/project.php +++ b/src/project.php @@ -132,54 +132,6 @@ function get_project_container_path( $target = null ) { } } -/** - * Returns the `@playwright/test` version the target declares in its `package.json`, as written. - * - * @param string|null $target The target to read the version from, defaults to the current one. - * - * @return string|null The declared version, e.g. `1.60.0` or `^1.60.0`, or `null` if the target does not declare - * `@playwright/test` as a dependency. - */ -function get_target_playwright_dependency( $target = null ) { - $project_path = get_project_local_path( $target ); - - if ( empty( $project_path ) || ! is_file( $project_path . '/package.json' ) ) { - return null; - } - - $package_json = json_decode( (string) file_get_contents( $project_path . '/package.json' ), true ); - - if ( ! is_array( $package_json ) ) { - return null; - } - - foreach ( [ 'devDependencies', 'dependencies' ] as $section ) { - $version = $package_json[ $section ]['@playwright/test'] ?? null; - - if ( is_string( $version ) ) { - return trim( $version ); - } - } - - return null; -} - -/** - * Returns the version if a declared `@playwright/test` version is an exact version. - * - * The Playwright image only contains the browser build matching its own tag, and Playwright refuses to run - * when the library and the browser build differ. A range such as `^1.60.0` can install `1.60.1`, which the - * `v1.60.0` image does not match, so only an exact version identifies the image. Package managers install - * an exact version as written, whichever lockfile format the project uses. - * - * @param string $declared The version as declared in `package.json`. - * - * @return string|null The version, e.g. `1.60.0`, or `null` if the declared version is not an exact version. - */ -function playwright_exact_version( string $declared ) { - return preg_match( '/^\d+\.\d+\.\d+$/', $declared ) ? $declared : null; -} - /** * Returns the .slicrc file as an array. * diff --git a/src/slic.php b/src/slic.php index fb88a29..b541c75 100644 --- a/src/slic.php +++ b/src/slic.php @@ -1042,18 +1042,6 @@ function slic_realtime() { return docker_compose_realtime( slic_stack_array() ); } -/** - * Runs a process in slic stack, with the `playwright` profile enabled, and returns the exit status. - * - * The `playwright` service is in a profile so `slic up` does not start it, which means the profile has to be - * enabled for docker compose to address the service. - * - * @return \Closure The process closure to start a real-time process using slic stack. - */ -function slic_playwright_realtime() { - return docker_compose_realtime( array_merge( slic_stack_array(), [ '--profile', 'playwright' ] ) ); -} - /** * Returns the process Closure to start a real-time process using slic stack. * From 833a0525f39103b964da9859aedb856da0004bc7 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Tue, 22 Sep 2026 14:52:07 -0600 Subject: [PATCH 07/11] Preserve Docker context when managing Playwright browsers --- src/docker-argv.php | 22 ++++++++++++++++++++++ src/playwright.php | 6 +++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/docker-argv.php b/src/docker-argv.php index 2a53dbc..a903697 100644 --- a/src/docker-argv.php +++ b/src/docker-argv.php @@ -127,6 +127,28 @@ function docker_compose_binary_argv(): array { return docker_compose_prefix_argv( $command, DIRECTORY_SEPARATOR === '\\' ); } +/** + * Use the same Docker executable and global options for container management. + * + * @return string[] The Docker executable and its prefix arguments. + */ +function docker_binary_argv(): array { + $command = docker_compose_binary_argv(); + + // For "docker --context remote compose", remove only the Compose subcommand. + // Inspect, logs and removal must address the daemon that created the container. + if ( count( $command ) > 1 && end( $command ) === 'compose' ) { + array_pop( $command ); + + return $command; + } + + // Standalone docker-compose and Compose-only wrappers cannot run Docker commands. + // Use Docker with the inherited environment, including DOCKER_HOST/DOCKER_CONTEXT. + // Wrappers must share those settings; options hidden inside a script cannot be inferred. + return [ 'docker' ]; +} + /** * Split a configured command prefix, preserving platform-specific path separators. * diff --git a/src/playwright.php b/src/playwright.php index 0d82c76..f617ccb 100644 --- a/src/playwright.php +++ b/src/playwright.php @@ -38,7 +38,7 @@ function wait_for_playwright_server( string $name, int $timeout = 30 ): bool { do { // The Docker template returns the health status while running, or "exited" // after the server stops. A running container alone does not mean it is ready. - $state = process_argv( [ 'docker', 'inspect', '--format', '{{if .State.Running}}{{.State.Health.Status}}{{else}}exited{{end}}', $name ] ); + $state = process_argv( array_merge( docker_binary_argv(), [ 'inspect', '--format', '{{if .State.Running}}{{.State.Health.Status}}{{else}}exited{{end}}', $name ] ) ); $health = trim( $state['stdout'] ); if ( $state['status'] !== 0 || in_array( $health, [ 'unhealthy', 'exited' ], true ) ) { @@ -121,7 +121,7 @@ function run_playwright( array $arguments ): int { $cleanup = static function () use ( $name, &$cleaned ) { if ( ! $cleaned ) { $cleaned = true; - process_argv( [ 'docker', 'rm', '--force', $name ] ); + process_argv( array_merge( docker_binary_argv(), [ 'rm', '--force', $name ] ) ); } }; register_shutdown_function( $cleanup ); @@ -158,7 +158,7 @@ function run_playwright( array $arguments ): int { if ( ! wait_for_playwright_server( $name ) ) { echo magenta( "Playwright browser server exited or did not become ready within 30 seconds. Server logs follow:\n" ); - process_argv_realtime( [ 'docker', 'logs', $name ] ); + process_argv_realtime( array_merge( docker_binary_argv(), [ 'logs', $name ] ) ); return 1; } From 8a6dd666a320fbe2f51345c2b291da9ec23f1647 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 25 Sep 2026 11:03:12 -0600 Subject: [PATCH 08/11] Prepare 2.5.0 release for testing --- changelog.md | 5 ++++- slic.php | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 00b1c6b..215c321 100644 --- a/changelog.md +++ b/changelog.md @@ -4,12 +4,15 @@ 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). -# [Unreleased] - TBD +# [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. diff --git a/slic.php b/slic.php index ff2b25c..fc05577 100644 --- a/slic.php +++ b/slic.php @@ -57,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 ) ) ) { From 0d21fa8dcb22d9b8218e7c691bd14b5f90f92af1 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 25 Sep 2026 12:08:59 -0600 Subject: [PATCH 09/11] Check Playwright browser readiness without a startup grace period --- slic-stack.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/slic-stack.yml b/slic-stack.yml index 2841ae3..b73de75 100644 --- a/slic-stack.yml +++ b/slic-stack.yml @@ -163,7 +163,6 @@ services: interval: 1s timeout: 2s retries: 20 - start_period: 2s shm_size: "${SLIC_PLAYWRIGHT_CONTAINER_SHM_SIZE:-1g}" slic: From 92406cb3b1dc09f751c8ba0f69cef9223c89f95b Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 25 Sep 2026 12:24:57 -0600 Subject: [PATCH 10/11] Check Redis and Selenium readiness without startup grace periods --- slic-stack.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/slic-stack.yml b/slic-stack.yml index b73de75..22539bb 100644 --- a/slic-stack.yml +++ b/slic-stack.yml @@ -36,7 +36,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 @@ -134,7 +133,6 @@ 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 From 9ff64edd1ffb92450f05c8718618a5537d981fc0 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Fri, 25 Sep 2026 12:33:09 -0600 Subject: [PATCH 11/11] Check database and WordPress readiness without startup grace periods --- slic-stack.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/slic-stack.yml b/slic-stack.yml index 22539bb..40b98c8 100644 --- a/slic-stack.yml +++ b/slic-stack.yml @@ -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 @@ -116,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