From 3ef424e9b16d7299953a0ff58895f031c4bbb297 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 8 Jul 2026 16:24:30 +1000 Subject: [PATCH 1/8] [#205] Added selenium-less headless Chrome support for JavaScript steps. Made the JavaScript-dependent traits driver-agnostic so '@javascript' scenarios run through 'dmore/chrome-mink-driver' over the Chrome DevTools Protocol as well as Selenium2. Added a 'chrome_headless' Behat profile and a 'chromedp/headless-shell' service, wired two CI matrix legs that run the suite through the new driver, and documented the setup. Claude-Session: https://claude.ai/code/session_012WhCKmn7GbhNCQgfXmWqXC --- .github/workflows/test.yml | 27 ++++++- README.md | 28 +++++++ behat.yml | 12 +++ composer.json | 1 + docker-compose.yml | 8 ++ src/AccessibilityTrait.php | 5 +- src/CookieTrait.php | 12 +++ src/DropzoneTrait.php | 1 + src/FileDownloadTrait.php | 15 +++- src/KeyboardTrait.php | 73 ++++++++++++++----- src/ResponsiveTrait.php | 14 ---- tests/behat/bootstrap/FeatureContextTrait.php | 12 +++ tests/behat/features/keyboard.feature | 4 +- 13 files changed, 172 insertions(+), 40 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ff10acaf..04d70db3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -116,6 +116,18 @@ jobs: - php_version: '8.4' drupal_version: '11' deps: 'lowest' + # Selenium-less driver legs: run the same suite through headless + # Chrome over the DevTools Protocol to prove the steps are driver + # portable. The browser driver is Drupal-version independent, so a + # single normal-deps leg per Drupal version is enough. + - php_version: '8.3' + drupal_version: '10' + deps: 'normal' + driver: 'chrome_headless' + - php_version: '8.3' + drupal_version: '11' + deps: 'normal' + driver: 'chrome_headless' container: image: drevops/ci-runner:26.6.0@sha256:190027056cac7b7ce28c29a1e5494779ebadc4ee8e95d468b75bc56e7be5aabd @@ -160,25 +172,32 @@ jobs: ahoy --version - name: Run Unit tests with coverage - if: matrix.php_version == '8.3' && matrix.drupal_version == '11' && matrix.deps == 'normal' + if: "matrix.driver != 'chrome_headless' && matrix.php_version == '8.3' && matrix.drupal_version == '11' && matrix.deps == 'normal'" run: ahoy test-unit-coverage continue-on-error: ${{ vars.CI_TEST_IGNORE_FAILURE == '1' }} - name: Run Unit tests - if: "!(matrix.php_version == '8.3' && matrix.drupal_version == '11' && matrix.deps == 'normal')" + if: "matrix.driver != 'chrome_headless' && !(matrix.php_version == '8.3' && matrix.drupal_version == '11' && matrix.deps == 'normal')" run: ahoy test-unit continue-on-error: ${{ vars.CI_TEST_IGNORE_FAILURE == '1' }} - name: Run BDD tests with coverage - if: matrix.php_version == '8.3' && matrix.drupal_version == '11' && matrix.deps == 'normal' + if: "matrix.driver != 'chrome_headless' && matrix.php_version == '8.3' && matrix.drupal_version == '11' && matrix.deps == 'normal'" run: ahoy test-bdd-coverage continue-on-error: ${{ vars.CI_TEST_IGNORE_FAILURE == '1' }} - name: Run BDD tests - if: "!(matrix.php_version == '8.3' && matrix.drupal_version == '11' && matrix.deps == 'normal')" + if: "matrix.driver != 'chrome_headless' && !(matrix.php_version == '8.3' && matrix.drupal_version == '11' && matrix.deps == 'normal')" run: ahoy test-bdd continue-on-error: ${{ vars.CI_TEST_IGNORE_FAILURE == '1' }} + # Selenium-less leg: the same BDD suite driven through headless Chrome + # over the DevTools Protocol, proving the steps work without Selenium. + - name: Run BDD tests with the chrome_headless driver + if: "matrix.driver == 'chrome_headless'" + run: ahoy test-bdd -- -p chrome_headless + continue-on-error: ${{ vars.CI_TEST_IGNORE_FAILURE == '1' }} + - name: Process test logs and artifacts if: always() run: | diff --git a/README.md b/README.md index 7b7f1519..cc48b548 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,34 @@ class FeatureContext extends DrupalContext { Ensure that your [`behat.yml`](behat.yml) has all the required extensions enabled. +### JavaScript drivers + +Steps that require a real browser (used by scenarios tagged `@javascript`) are +driver agnostic: they work with a Selenium/WebDriver driver and with +selenium-less drivers that talk to Chrome directly over the Chrome DevTools +Protocol. Both are exercised by this library's own CI. + +To run `@javascript` scenarios without a Selenium server, add +[`dmore/behat-chrome-extension`](https://gitlab.com/behat-chrome/behat-chrome-extension) +(which pulls in `dmore/chrome-mink-driver`) and point it at a headless Chrome: + +```yaml +default: + extensions: + DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension: ~ + Behat\MinkExtension: + browser_name: chrome + javascript_session: chrome + chrome: + api_url: 'http://chrome:9222' +``` + +Any image that exposes a DevTools endpoint works (for example +[`chromedp/headless-shell`](https://hub.docker.com/r/chromedp/headless-shell)). +For local visual debugging you can override `api_url` at runtime via Behat's +`BEHAT_PARAMS` environment variable - for instance to drive a headed Chrome on +your host - as long as that browser can reach your site's `base_url`. + ### Exceptions This library uses [Mink exception classes](https://mink.behat.org/en/latest/) diff --git a/behat.yml b/behat.yml index 1ca05fd7..8b2dafa4 100644 --- a/behat.yml +++ b/behat.yml @@ -93,3 +93,15 @@ default: target: '%paths.base%/.logs/coverage/behat/cobertura.xml' php: target: '%paths.base%/.logs/coverage/behat/phpcov.php' + +# Selenium-less profile: drives headless Chrome directly over the DevTools +# Protocol (no Selenium server). Run with "behat -p chrome_headless". Inherits +# all configuration from the "default" profile and only swaps the JavaScript +# session to the "chrome" driver (the session name registered by ChromeExtension). +chrome_headless: + extensions: + DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension: ~ + Drupal\MinkExtension: + javascript_session: chrome + chrome: + api_url: 'http://chrome_headless:9222' diff --git a/composer.json b/composer.json index 19b241ff..318816a6 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "php": ">=8.2", "behat/behat": "^3.14", "behat/mink": ">=1.11", + "dmore/behat-chrome-extension": "^1.4", "drupal/drupal-extension": "^6.0", "lullabot/mink-selenium2-driver": "^1.7.4", "softcreatr/jsonpath": "^0.10 || ^1.0" diff --git a/docker-compose.yml b/docker-compose.yml index 691857af..3f5896f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -85,6 +85,14 @@ services: depends_on: - cli + chrome_headless: + image: chromedp/headless-shell:150.0.7871.47 + shm_size: '1gb' + expose: + - "9222" + depends_on: + - cli + # Helper container to wait for services to become available. wait_dependencies: image: drevops/docker-wait-for-dependencies:26.1.0 diff --git a/src/AccessibilityTrait.php b/src/AccessibilityTrait.php index 6c89d76d..3289c311 100644 --- a/src/AccessibilityTrait.php +++ b/src/AccessibilityTrait.php @@ -465,7 +465,10 @@ protected function accessibilityRunEngine(string $rules): array { )); $session->wait(30000, 'window.__accessibilityResults !== null'); - $results = $session->evaluateScript('return window.__accessibilityResults;'); + // Serialize to a JSON string in the browser rather than returning the raw + // object: the result graph is large and nested, and some drivers (e.g. + // chrome-mink) cannot walk every property when marshalling a live object. + $results = json_decode((string) $session->evaluateScript('return JSON.stringify(window.__accessibilityResults);'), TRUE); if (!is_array($results)) { throw new \RuntimeException('Accessibility engine did not return results.'); diff --git a/src/CookieTrait.php b/src/CookieTrait.php index ef611e2e..c4fb603a 100644 --- a/src/CookieTrait.php +++ b/src/CookieTrait.php @@ -271,6 +271,18 @@ protected function cookieGetAll(): array { }); } + // CDP-based drivers like the Chrome (chrome-mink) driver. + elseif (method_exists($driver, 'getCookies')) { + $cookies = []; + foreach ($driver->getCookies() as $cookie) { + $cookies[] = [ + 'name' => $cookie['name'], + 'value' => rawurldecode((string) $cookie['value']), + 'secure' => $cookie['secure'], + ]; + } + } + // BrowserKit-based drivers like GoutteDriver. elseif (method_exists($driver, 'getClient')) { /** @var \Symfony\Component\BrowserKit\CookieJar $cookie_jar */ diff --git a/src/DropzoneTrait.php b/src/DropzoneTrait.php index 462838ba..7df33102 100644 --- a/src/DropzoneTrait.php +++ b/src/DropzoneTrait.php @@ -83,6 +83,7 @@ public function dropzoneDropFiles(string $selector, TableNode $paths): void { var input = document.createElement('input'); input.type = 'file'; input.id = ids[i]; + input.name = ids[i]; input.style.position = 'fixed'; input.style.left = '-9999px'; input.style.opacity = '0'; diff --git a/src/FileDownloadTrait.php b/src/FileDownloadTrait.php index 11ce9c72..fc55e8d6 100644 --- a/src/FileDownloadTrait.php +++ b/src/FileDownloadTrait.php @@ -12,7 +12,6 @@ use Behat\Hook\BeforeScenario; use Behat\Gherkin\Node\PyStringNode; use Behat\Gherkin\Node\TableNode; -use Behat\Mink\Driver\Selenium2Driver; use Behat\Mink\Element\NodeElement; use Behat\Mink\Exception\ElementNotFoundException; use Behat\Mink\Exception\ExpectationException; @@ -87,12 +86,24 @@ public function fileDownloadFrom(string $url): void { /** @var \Behat\Mink\Driver\CoreDriver $driver */ $driver = $this->getSession()->getDriver(); - if ($driver instanceof Selenium2Driver) { + + // WebDriver-based drivers like Selenium2Driver. + if (method_exists($driver, 'getWebDriverSession')) { $cookies = $driver->getWebDriverSession()->getAllCookies(); foreach ($cookies as $cookie) { $cookie_list[] = $cookie['name'] . '=' . $cookie['value']; } } + + // CDP-based drivers like the Chrome (chrome-mink) driver. + elseif (method_exists($driver, 'getCookies')) { + // @phpstan-ignore-next-line + foreach ($driver->getCookies() as $cookie) { + $cookie_list[] = $cookie['name'] . '=' . $cookie['value']; + } + } + + // BrowserKit-based drivers like GoutteDriver. else { /** @var \Behat\Mink\Driver\BrowserKitDriver $driver */ // @phpstan-ignore-next-line diff --git a/src/KeyboardTrait.php b/src/KeyboardTrait.php index fd743e39..9aef0e3f 100644 --- a/src/KeyboardTrait.php +++ b/src/KeyboardTrait.php @@ -5,6 +5,7 @@ namespace DrevOps\BehatSteps; use Behat\Step\When; +use Behat\Mink\Driver\BrowserKitDriver; use Behat\Mink\Driver\Selenium2Driver; use Behat\Mink\Exception\ExpectationException; use Behat\Mink\Exception\UnsupportedDriverActionException; @@ -92,8 +93,11 @@ public function keyboardPressKeysOnElement(string $keys, ?string $selector): voi public function keyboardPressKeyOnElementSingle(string $char, ?string $selector): void { $driver = $this->getSession()->getDriver(); - if (!$driver instanceof Selenium2Driver) { - throw new UnsupportedDriverActionException('Method can be used only with Selenium2 driver', $driver); + // Keyboard interaction needs a JavaScript-capable driver: Selenium2 uses + // the bundled Syn library, chrome-mink uses native DevTools key events. + // BrowserKit-based (non-JavaScript) drivers cannot dispatch key events. + if ($driver instanceof BrowserKitDriver) { + throw new UnsupportedDriverActionException('Keyboard interaction is only supported by JavaScript drivers (Selenium2 or Chrome).', $driver); } $keys = [ @@ -227,22 +231,57 @@ function getPathTo(element) { */ protected function keyboardTriggerKey(string $xpath, string $key): void { $driver = $this->getSession()->getDriver(); - // @codeCoverageIgnoreStart - if (!$driver instanceof Selenium2Driver) { - throw new UnsupportedDriverActionException('Method can be used only with Selenium2 driver', $driver); + + // Selenium2 driver: reuse the bundled Syn library via reflection to inject + // synthetic events and execute JS on the element. + if ($driver instanceof Selenium2Driver) { + $reflector = new \ReflectionClass($driver); + $with_syn_reflection = $reflector->getMethod('withSyn'); + $execute_js_on_xpath_reflection = $reflector->getMethod('executeJsOnXpath'); + $with_syn_result = $with_syn_reflection->invoke($driver); + + $execute_js_on_xpath_reflection->invokeArgs($with_syn_result, [ + $xpath, + sprintf("syn.key({{ELEMENT}}, '%s');", $key), + ]); + + return; + } + + // CDP-based drivers like the Chrome (chrome-mink) driver: dispatch native + // DevTools key events. Special keys are sent as a keycode down/up pair so + // their default action (focus move, submit, etc.) fires; printable + // characters are sent as a single character so their text is inserted. + $keycodes = [ + "\b" => 8, + "\t" => 9, + "\r" => 13, + 'shift' => 16, + 'ctrl' => 17, + 'alt' => 18, + 'pause' => 19, + 'break' => 19, + 'caps' => 20, + 'escape' => 27, + 'page-up' => 33, + 'page-down' => 34, + 'end' => 35, + 'home' => 36, + 'left' => 37, + 'up' => 38, + 'right' => 39, + 'down' => 40, + 'insert' => 45, + 'delete' => 46, + ]; + + if (isset($keycodes[$key])) { + $driver->keyDown($xpath, $keycodes[$key]); + $driver->keyUp($xpath, $keycodes[$key]); + } + else { + $driver->keyPress($xpath, $key); } - // @codeCoverageIgnoreEnd - // Use reflection to re-use Syn library injection and execution of JS on - // element. - $reflector = new \ReflectionClass($driver); - $with_syn_reflection = $reflector->getMethod('withSyn'); - $execute_js_on_xpath_reflection = $reflector->getMethod('executeJsOnXpath'); - $with_syn_result = $with_syn_reflection->invoke($driver); - - $execute_js_on_xpath_reflection->invokeArgs($with_syn_result, [ - $xpath, - sprintf("syn.key({{ELEMENT}}, '%s');", $key), - ]); } } diff --git a/src/ResponsiveTrait.php b/src/ResponsiveTrait.php index 9c2e8e51..9681261b 100644 --- a/src/ResponsiveTrait.php +++ b/src/ResponsiveTrait.php @@ -11,7 +11,6 @@ use Behat\Gherkin\Node\TableNode; use Behat\Hook\BeforeScenario; use Behat\Hook\BeforeStep; -use Behat\Mink\Driver\Selenium2Driver; /** * Test responsive layouts with viewport control. @@ -340,17 +339,10 @@ protected function responsiveExtractDimensions(string $dimensions, ?string $name * Array with 'width' and 'height' keys. */ protected function responsiveGetCurrentDimensions(): array { - $driver = $this->getSession()->getDriver(); - // Default dimensions if unable to determine current size. $default_width = 1280; $default_height = 800; - // @codeCoverageIgnoreStart - if (!$driver instanceof Selenium2Driver) { - return ['width' => $default_width, 'height' => $default_height]; - } - // @codeCoverageIgnoreEnd try { $width = $this->getSession()->evaluateScript('return window.innerWidth;'); $height = $this->getSession()->evaluateScript('return window.innerHeight;'); @@ -376,12 +368,6 @@ protected function responsiveGetCurrentDimensions(): array { * The height in pixels. */ protected function responsiveResize(int $width, int $height): void { - $driver = $this->getSession()->getDriver(); - - if (!$driver instanceof Selenium2Driver) { - return; - } - try { // Ensure session is started before resizing. if (!$this->getSession()->isStarted()) { diff --git a/tests/behat/bootstrap/FeatureContextTrait.php b/tests/behat/bootstrap/FeatureContextTrait.php index a1587fad..5e6588bb 100644 --- a/tests/behat/bootstrap/FeatureContextTrait.php +++ b/tests/behat/bootstrap/FeatureContextTrait.php @@ -372,6 +372,13 @@ protected function testGetAllCookies(): array { $cookie_list[$cookie['name']] = $cookie['value']; } } + + // CDP-based drivers like the Chrome (chrome-mink) driver. + elseif (method_exists($driver, 'getCookies')) { + foreach ($driver->getCookies() as $cookie) { + $cookie_list[$cookie['name']] = rawurldecode((string) $cookie['value']); + } + } else { /** @var \Behat\Mink\Driver\BrowserKitDriver $driver */ // @phpstan-ignore-next-line @@ -405,6 +412,11 @@ public function testSetCookie(string $name, string $value): void { $cookie = new Cookie($name, rawurlencode($value)); $cookie_jar->set($cookie); } + + // CDP-based drivers like the Chrome (chrome-mink) driver. + if (method_exists($driver, 'getCookies')) { + $driver->setCookie($name, $value); + } } /** diff --git a/tests/behat/features/keyboard.feature b/tests/behat/features/keyboard.feature index 1efd05f8..5046438e 100644 --- a/tests/behat/features/keyboard.feature +++ b/tests/behat/features/keyboard.feature @@ -25,7 +25,7 @@ Feature: Check that KeyboardTrait works Then the "input1" field should contain "hello" @trait:KeyboardTrait - Scenario: Assert that negative assertion for "When I press the keys :keys on the element :selector" step throws an exception for using with a driver other than Selenium2Driver + Scenario: Assert that negative assertion for "When I press the keys :keys on the element :selector" step throws an exception for using with a non-JavaScript driver Given some behat configuration And scenario steps: """ @@ -36,7 +36,7 @@ Feature: Check that KeyboardTrait works When I run "behat --no-colors" Then it should fail with a "Behat\Mink\Exception\UnsupportedDriverActionException" exception: """ - Method can be used only with Selenium2 driver + Keyboard interaction is only supported by JavaScript drivers (Selenium2 or Chrome). """ @api @javascript From 2e8f3341d21579d346c0bdda9259acf5c3e2d2ed Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 8 Jul 2026 16:53:09 +1000 Subject: [PATCH 2/8] [#205] Included the driver in the CI test job name to distinguish the selenium-less legs. Claude-Session: https://claude.ai/code/session_012WhCKmn7GbhNCQgfXmWqXC --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 04d70db3..41c443ae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: continue-on-error: ${{ vars.CI_LINT_DOCS_IGNORE_FAILURE == '1' }} test: - name: Test PHP ${{ matrix.php_version }}, Drupal ${{ matrix.drupal_version }}, Deps ${{ matrix.deps }} + name: Test PHP ${{ matrix.php_version }}, Drupal ${{ matrix.drupal_version }}, Deps ${{ matrix.deps }}${{ matrix.driver && format(', Driver {0}', matrix.driver) || '' }} runs-on: ubuntu-latest strategy: From 65b9209bf3fd0a29058b659bf38d451d06f26392 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 8 Jul 2026 17:11:11 +1000 Subject: [PATCH 3/8] [#205] Made CI test artifact names unique per driver to avoid upload collisions. Claude-Session: https://claude.ai/code/session_012WhCKmn7GbhNCQgfXmWqXC --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 41c443ae..a0fe8760 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -211,7 +211,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: - name: test-artifacts-php${{ matrix.php_version }}-drupal${{ matrix.drupal_version }}-${{ matrix.deps }} + name: test-artifacts-php${{ matrix.php_version }}-drupal${{ matrix.drupal_version }}-${{ matrix.deps }}${{ matrix.driver && format('-{0}', matrix.driver) || '' }} path: .logs include-hidden-files: true retention-days: 30 From 2664a69753801263bc565080aecce8599b601905 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 8 Jul 2026 17:15:47 +1000 Subject: [PATCH 4/8] [#205] Ran the chrome_headless leg with coverage so selenium-less branches are measured. Claude-Session: https://claude.ai/code/session_012WhCKmn7GbhNCQgfXmWqXC --- .github/workflows/test.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a0fe8760..ff59378e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -193,8 +193,16 @@ jobs: # Selenium-less leg: the same BDD suite driven through headless Chrome # over the DevTools Protocol, proving the steps work without Selenium. + # The Drupal 11 leg runs with coverage so the driver-specific branches + # (reachable only through chrome-mink) are measured and merged into the + # Codecov report alongside the Selenium2 coverage. + - name: Run BDD tests with coverage with the chrome_headless driver + if: "matrix.driver == 'chrome_headless' && matrix.drupal_version == '11'" + run: ahoy test-bdd-coverage -- -p chrome_headless + continue-on-error: ${{ vars.CI_TEST_IGNORE_FAILURE == '1' }} + - name: Run BDD tests with the chrome_headless driver - if: "matrix.driver == 'chrome_headless'" + if: "matrix.driver == 'chrome_headless' && matrix.drupal_version != '11'" run: ahoy test-bdd -- -p chrome_headless continue-on-error: ${{ vars.CI_TEST_IGNORE_FAILURE == '1' }} From fcb526dc8ea47c13da47f66de530b45aa58a7258 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Thu, 9 Jul 2026 12:05:29 +1000 Subject: [PATCH 5/8] Re-triggered CI to clear a flaky check. From 9161afa53eeb568e0c1e9da65f915c8f2c94316e Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Thu, 9 Jul 2026 12:23:19 +1000 Subject: [PATCH 6/8] [#205] Waited for the chrome_headless CDP endpoint to be ready before its BDD steps. Claude-Session: https://claude.ai/code/session_012WhCKmn7GbhNCQgfXmWqXC --- .github/workflows/test.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ff59378e..33e73e85 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -191,6 +191,23 @@ jobs: run: ahoy test-bdd continue-on-error: ${{ vars.CI_TEST_IGNORE_FAILURE == '1' }} + # Wait for the headless Chrome DevTools endpoint to accept connections + # before driving it: depends_on only guarantees the container started, + # not that Chrome is listening on the remote debugging port yet. + - name: Wait for the chrome_headless CDP endpoint + if: "matrix.driver == 'chrome_headless'" + run: | + for i in $(seq 1 30); do + if docker compose exec -T cli curl -sSf -m 2 http://chrome_headless:9222/json/version >/dev/null 2>&1; then + echo "Chrome DevTools endpoint is ready." + exit 0 + fi + echo "Waiting for Chrome DevTools endpoint ($i/30)..." + sleep 2 + done + echo "Chrome DevTools endpoint did not become ready in time." >&2 + exit 1 + # Selenium-less leg: the same BDD suite driven through headless Chrome # over the DevTools Protocol, proving the steps work without Selenium. # The Drupal 11 leg runs with coverage so the driver-specific branches From ed3e949ac65d728bc1d2158b3c9d4e0aad1927bf Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Thu, 9 Jul 2026 12:43:18 +1000 Subject: [PATCH 7/8] [#205] Fixed the CDP readiness check to treat the DNS-rebinding response as ready. Claude-Session: https://claude.ai/code/session_012WhCKmn7GbhNCQgfXmWqXC --- .github/workflows/test.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33e73e85..1df58e74 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -193,12 +193,15 @@ jobs: # Wait for the headless Chrome DevTools endpoint to accept connections # before driving it: depends_on only guarantees the container started, - # not that Chrome is listening on the remote debugging port yet. + # not that Chrome is listening on the remote debugging port yet. Any HTTP + # response means the server is up: a raw request is answered with a + # DNS-rebinding 500 (the driver resolves the host to an IP to pass that + # check), so -f is intentionally omitted and any response counts as ready. - name: Wait for the chrome_headless CDP endpoint if: "matrix.driver == 'chrome_headless'" run: | for i in $(seq 1 30); do - if docker compose exec -T cli curl -sSf -m 2 http://chrome_headless:9222/json/version >/dev/null 2>&1; then + if docker compose exec -T cli curl -sS -m 2 http://chrome_headless:9222/json/version >/dev/null 2>&1; then echo "Chrome DevTools endpoint is ready." exit 0 fi From 07cec06815724dcc91b5db4de7b2a7dc44c71ee4 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Thu, 9 Jul 2026 13:21:54 +1000 Subject: [PATCH 8/8] [#205] Waited for the chrome_headless port through wait_dependencies instead of a CI step. Claude-Session: https://claude.ai/code/session_012WhCKmn7GbhNCQgfXmWqXC --- .github/workflows/test.yml | 20 -------------------- docker-compose.yml | 3 ++- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1df58e74..ff59378e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -191,26 +191,6 @@ jobs: run: ahoy test-bdd continue-on-error: ${{ vars.CI_TEST_IGNORE_FAILURE == '1' }} - # Wait for the headless Chrome DevTools endpoint to accept connections - # before driving it: depends_on only guarantees the container started, - # not that Chrome is listening on the remote debugging port yet. Any HTTP - # response means the server is up: a raw request is answered with a - # DNS-rebinding 500 (the driver resolves the host to an IP to pass that - # check), so -f is intentionally omitted and any response counts as ready. - - name: Wait for the chrome_headless CDP endpoint - if: "matrix.driver == 'chrome_headless'" - run: | - for i in $(seq 1 30); do - if docker compose exec -T cli curl -sS -m 2 http://chrome_headless:9222/json/version >/dev/null 2>&1; then - echo "Chrome DevTools endpoint is ready." - exit 0 - fi - echo "Waiting for Chrome DevTools endpoint ($i/30)..." - sleep 2 - done - echo "Chrome DevTools endpoint did not become ready in time." >&2 - exit 1 - # Selenium-less leg: the same BDD suite driven through headless Chrome # over the DevTools Protocol, proving the steps work without Selenium. # The Drupal 11 leg runs with coverage so the driver-specific branches diff --git a/docker-compose.yml b/docker-compose.yml index 3f5896f1..024b4e01 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -99,7 +99,8 @@ services: depends_on: - cli - mariadb - command: mariadb:3306 + - chrome_headless + command: mariadb:3306 chrome_headless:9222 networks: ### Use external networks locally. Automatically removed in CI. amazeeio-network: ### Automatically removed in CI.