Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
09908d1
Renamed 'normalise()' to 'normalize()' across the field handlers.
AlexSkrypnyk Sep 21, 2026
541a8ac
Renamed 'filesUnmanagedUris' to 'fileUnmanagedUris' for the trait pre…
AlexSkrypnyk Sep 21, 2026
3e15b3e
Converted plain method arguments in the Behat layer to snake_case.
AlexSkrypnyk Sep 21, 2026
c38ae0b
Dropped the redundant 'mail' prefix from two protected 'Core' helpers.
AlexSkrypnyk Sep 21, 2026
1c452f8
Widened the 'DocsTest' helpers from private to protected.
AlexSkrypnyk Sep 21, 2026
75b5ae4
Moved the 'DocsTest' data providers next to the tests they feed.
AlexSkrypnyk Sep 21, 2026
44eaa62
Converted promoted properties on test doubles to camelCase.
AlexSkrypnyk Sep 21, 2026
b2011bb
Used Drupal's 'ContainerBuilder' in the cron methods test.
AlexSkrypnyk Sep 21, 2026
7951596
Referenced the context classes with '::class' in the Behat configurat…
AlexSkrypnyk Sep 21, 2026
86b59ec
Prefixed the wrapped upstream class aliases with 'Upstream'.
AlexSkrypnyk Sep 21, 2026
54dcd10
Renamed the driver name local to 'name' in 'DriverListener'.
AlexSkrypnyk Sep 21, 2026
1f6df53
Converted the embedded JavaScript locals to camelCase.
AlexSkrypnyk Sep 21, 2026
e078996
Expanded the 'l' local to 'line' and prefixed global classes in the s…
AlexSkrypnyk Sep 21, 2026
3ca2f33
Named helper parameters after the step placeholders they carry.
AlexSkrypnyk Sep 21, 2026
af01ca5
Made the 'in_array()' checks over string lists strict.
AlexSkrypnyk Sep 21, 2026
eafdc0d
Pointed '@phpstan-require-extends' at the class that declares the mem…
AlexSkrypnyk Sep 21, 2026
332c4a1
Composed the namespace-local 'HelperTrait' in the Drupal step traits.
AlexSkrypnyk Sep 21, 2026
2ad3046
Prefixed the harness-only tag with 'test-' and tidied the harness trait.
AlexSkrypnyk Sep 21, 2026
2a9c0e9
Sorted the 'use' imports by fully-qualified name.
AlexSkrypnyk Sep 21, 2026
21b87fc
Regenerated 'HELPERS.md' for the renamed helper parameters.
AlexSkrypnyk Sep 21, 2026
ed17337
Converged the source comments onto the technical register.
AlexSkrypnyk Sep 21, 2026
4554dfe
Aligned the thrown exception types with the documented table.
AlexSkrypnyk Sep 21, 2026
2bd9b9a
Added the missing Drupal bootstrap and contrib module guards.
AlexSkrypnyk Sep 21, 2026
f1f2c60
Re-parented the driver exception base onto 'RuntimeException'.
AlexSkrypnyk Sep 21, 2026
cf4cafe
Regenerated 'HELPERS.md' for the new 'blockGetByLabel()' helper.
AlexSkrypnyk Sep 21, 2026
d87b50f
Widened the coverage scope to the files 'phpunit.xml' already includes.
AlexSkrypnyk Sep 21, 2026
74470ae
Covered the error paths the convergence pass left untested.
AlexSkrypnyk Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions HELPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
| Class | Helpers | Description |
| --- | --- | --- |
| [Drupal\BigPipeTrait](#drupalbigpipetrait) | 2 | Wait for Drupal BigPipe placeholders to be replaced on JavaScript scenarios. |
| [Drupal\BlockTrait](#drupalblocktrait) | 1 | Manage Drupal blocks. |
| [Drupal\BlockTrait](#drupalblocktrait) | 2 | Manage Drupal blocks. |
| [Drupal\CacheTrait](#drupalcachetrait) | 1 | Invalidate Drupal caches and run cron from within a scenario. |
| [Drupal\ConfigTrait](#drupalconfigtrait) | 2 | Assert and set stored Drupal configuration values with automatic revert. |
| [Drupal\ContentBlockTrait](#drupalcontentblocktrait) | 2 | Manage Drupal content blocks. |
Expand Down Expand Up @@ -674,7 +674,7 @@ Assert that a set of meta tags is present and non-empty
</details>

<details>
<summary><code>public function metatagFindMeta(string $name): ?NodeElement</code></summary>
<summary><code>public function metatagFindMeta(string $meta_name): ?NodeElement</code></summary>

<br/>
Find a meta tag by its "name" or "property" attribute
Expand All @@ -701,7 +701,7 @@ Get the hreflang alternates present on the current page
</details>

<details>
<summary><code>public function metatagGetMetaContent(string $name): ?string</code></summary>
<summary><code>public function metatagGetMetaContent(string $meta_name): ?string</code></summary>

<br/>
Get the content of a meta tag by its "name" or "property" attribute
Expand Down Expand Up @@ -893,7 +893,7 @@ Generates a 'Random::name()' string with original case preserved
<summary><code>public function randomGenerateString(int $length): string</code></summary>

<br/>
Generates a lowercase string - the default for unknown shape requests
Generates a lowercase string, the default token type
<br/><br/>

</details>
Expand Down Expand Up @@ -974,7 +974,7 @@ Get all available breakpoints
</details>

<details>
<summary><code>public function responsiveGetBreakpoint(string $name): string</code></summary>
<summary><code>public function responsiveGetBreakpoint(string $breakpoint): string</code></summary>

<br/>
Get breakpoint dimensions by name
Expand Down Expand Up @@ -1058,7 +1058,7 @@ Find a table element by CSS selector
</details>

<details>
<summary><code>public function tableFindRowByText(string $text): ?NodeElement</code></summary>
<summary><code>public function tableFindRowByText(string $row_text): ?NodeElement</code></summary>

<br/>
Find a table row containing the given text
Expand Down Expand Up @@ -1225,6 +1225,15 @@ Wait until no BigPipe placeholder markers remain in the DOM

> Manage Drupal blocks.

<details>
<summary><code>public function blockGetByLabel(string $label): Block</code></summary>

<br/>
Load a block by its label or fail
<br/><br/>

</details>

<details>
<summary><code>public function blockLoadByLabel(string $label): ?Block</code></summary>

Expand Down Expand Up @@ -1493,7 +1502,7 @@ Create a single media item
</details>

<details>
<summary><code>public function mediaLoadMultiple(string $type, array $conditions = []): array</code></summary>
<summary><code>public function mediaLoadMultiple(string $media_type, array $conditions = []): array</code></summary>

<br/>
Load multiple media entities with specified type and conditions
Expand Down Expand Up @@ -1583,7 +1592,7 @@ Check if a module's code is present
> Manage Drupal paragraphs entities with structured field data.

<details>
<summary><code>public function paragraphsAttachFromStubToEntity(ContentEntityInterface $parent_entity, string $parent_field_name, string $paragraph_bundle, EntityStub $stub, bool $save_entity = TRUE): ParagraphInterface</code></summary>
<summary><code>public function paragraphsAttachFromStubToEntity(ContentEntityInterface $parent_entity, string $parent_field, string $paragraph_type, EntityStub $stub, bool $save_entity = TRUE): ParagraphInterface</code></summary>

<br/>
Create a paragraphs item from a stub and attach it to an entity
Expand Down
48 changes: 43 additions & 5 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -595,19 +595,57 @@ If your project catches an exception from one of these steps, update the type:
| `Drupal\ModuleTrait` (all `Then` steps) | `\Exception` | `AssertionException` |
| `Drupal\StateTrait` (all `Then` steps) | `\Exception` | `AssertionException` |
| `Drupal\RedirectTrait` (`the following redirects should (not) exist:`) | `\Exception` | `AssertionException` |
| `MetatagTrait` (all `Then` steps) | `\Exception` | `ExpectationException` |
| `MetatagTrait` (all `Then` steps) | `\Exception` | `ExpectationException`; `ElementNotFoundException` when the meta tag itself is missing; `\RuntimeException` when an hreflang alternate page returns an HTTP error |
| `XmlTrait` (`the response should be in XML format`) | `\RuntimeException` | `ExpectationException` |
| `FieldTrait` (`the option ... should (not) exist within the select element ...`) | `\InvalidArgumentException` | `ElementNotFoundException` for a missing select, `ExpectationException` for the option |
| `FieldTrait` (`the option ... should (not) exist within the select element ...`) | `\InvalidArgumentException` | `ElementNotFoundException` for a missing select or a missing option, `ExpectationException` for an option that exists but should not |
| `Drupal\CacheTrait` (`the page cache for the path(s) ... is empty`) | `\InvalidArgumentException` | `\RuntimeException` |
| `KeyboardTrait` (`I press the key(s) ...`) | `\InvalidArgumentException` | `\RuntimeException` |

3 failure messages changed along with their type:
| `TableTrait` (any table step, when the table or the row is missing) | `ExpectationException` | `ElementNotFoundException` |
| `ModalTrait` (`I close the modal`, `I click on ... in the modal`, `the modal should (not) contain ...`, when the close button, the content element or the target element is missing) | `ExpectationException` | `ElementNotFoundException` |
| `FieldTrait` (`I unselect ... from ...` and `the option ... should not be selected within the select element ...`, when the option is missing; `I fill in the multi-value field ...`, when an input row is missing) | `ExpectationException` | `ElementNotFoundException` |
| `XmlTrait` (every `the XML element ...` and `the XML attribute ... on element ...` step, when the element is missing) | `ExpectationException` | `ElementNotFoundException` |
| `JsonTrait` (an invalid JSONPath expression, an invalid regular expression, a count that is not an integer, a schema that is not JSON) | `ExpectationException` | `\RuntimeException` |
| `TableTrait` (`the table ... should be sorted by ... in ... order`, with a direction other than `ascending` or `descending`) | `ExpectationException` | `\RuntimeException` |
| `ElementTrait` (`... with the index ...`, with an index below 1; `... pinned to the top of the viewport within ... pixels`, with a negative tolerance) | `ExpectationException` | `\RuntimeException` |
| `Drupal\EmailTrait` (`I follow link number ...`, with a link number that is not a positive integer) | `ExpectationException` | `\RuntimeException` |
| `FieldTrait` (`I fill in the WYSIWYG field ...`, when the field has no `id` attribute) | `ExpectationException` | `\RuntimeException` |
| `XmlTrait` (`I print last XML response`, when the document cannot be serialised) | `ExpectationException` | `\RuntimeException` |
| `KeyboardTrait` (`I press the key(s) ...` without an element, when nothing has focus) | `ExpectationException` | `\RuntimeException` |
| `Drupal\BlockTrait` (every `Given the block ...` step, when the block does not exist) | `ExpectationException` | `\RuntimeException` |
| `WaitTrait` (`I wait for AJAX to finish` and `I wait for ... second(s) for AJAX to finish`, without a JavaScript driver) | `\RuntimeException` | `UnsupportedDriverActionException` |
| `FieldTrait` (`I fill in the multi-value field ...`, without a JavaScript driver) | `\RuntimeException` | `UnsupportedDriverActionException` |

14 failure messages changed along with their type:

| Step | Was | Now |
| --- | --- | --- |
| `the response should be in XML format` | `Failed to load XML. Errors: ...` | `The response is not valid XML: ...` |
| `the option :option should exist within the select element :selector` | `Element "..." is not found.` / `Option "..." is not found in select "...".` | `Select with id\|name\|label "..." not found.` / `The option "..." was not found in the select "..." on the page ....` |
| `the option :option should exist within the select element :selector` | `Element "..." is not found.` / `Option "..." is not found in select "...".` | `Select with id\|name\|label "..." not found.` / `Option in the select "..." with value\|text "..." not found.` |
| `the option :option should not exist within the select element :selector` | `Element "..." is not found.` / `Option "..." is found in select "...", but should not.` | `Select with id\|name\|label "..." not found.` / `The option "..." was found in the select "..." on the page ..., but should not exist.` |
| `I unselect :option from :selector` | `The option "..." was not found in the select "...".` | `Option in the select "..." with value\|text "..." not found.` |
| `the option :option should not be selected within the select element :selector` | `The option "..." was not found in the select "..." on the page ....` | `Option in the select "..." with value\|text "..." not found.` |
| `I fill in the multi-value field :field with the following values:` | `Could not locate input row N for multi-value field "...".` | `Input row of the multi-value field "..." with index "N" not found.` |
| every `the table ...` step, when the table is missing | `Table with selector "..." not found.` | `Table matching css "..." not found.` |
| every `... the row ...` step, when the row is missing | `Table row containing text "..." not found.` | `Table row with text "..." not found.` |
| `I close the modal` | `The modal close button was not found.` | `Modal close button matching css "..." not found.` |
| `I click on :selector in the modal` | `The element "..." was not found in the modal.` | `Element in the modal with css\|id\|name\|title\|alt\|value\|text "..." not found.` |
| `the modal should (not) contain :text` | `The modal content element was not found.` | `Modal content element matching css "..." not found.` |
| every `the XML element ...` and `the XML attribute ... on element ...` step, when the element is missing | `The XML element "..." was not found.` | `XML element matching xpath "..." not found.` |
| `the meta tag should exist with the following attributes:` | `Meta tag with specified attributes was not found: {...}.` | `Meta tag with attributes "{...}" not found.` |
| `the :meta_name meta tag should not contain any HTML tags` | `Meta tag with name or property "..." not found.` | `Meta tag with name\|property "..." not found.` |

The same rule now covers the driver layer and the Behat managers, which used to throw `\InvalidArgumentException` and plain `\Exception` for an invalid argument or an unmet prerequisite. If your project calls the driver or a manager directly and catches on the type, update it:

| Class | Was | Now |
| --- | --- | --- |
| `Driver\Core\Core` (an unknown entity type, bundle, vocabulary, user, language, severity or handler class) | `\InvalidArgumentException` / `\Exception` | `\RuntimeException` |
| `Driver\Core\Field\*Handler` (a malformed field value, an unreadable file, a missing referenced entity) | `\InvalidArgumentException` / `\Exception` | `\RuntimeException` |
| `Behat\Manager\DriverManager::getDriver()` and `setDefaultDriverName()` | `\InvalidArgumentException` | `\RuntimeException` |
| `Behat\Manager\UserManager::getUser()` | `\InvalidArgumentException` | `\RuntimeException` |
| `Behat\Selector\RegionSelector::translateToXPath()` | `\InvalidArgumentException` | `\RuntimeException` |
| `Driver\Exception\CreationAliasResolutionException` | extends `\InvalidArgumentException` | extends `Driver\Exception\Exception` |

`CreationAliasResolutionException` is no longer a `\LogicException`, so a `catch (\InvalidArgumentException)` or `catch (\LogicException)` no longer catches it; catch the class itself.

Behat reports every one of these as a failed step either way, so a scenario that simply runs to a failure behaves the same. Only code that catches a specific type, or asserts on the message text, needs changing.

Expand Down
61 changes: 30 additions & 31 deletions STEPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,9 +536,9 @@ Then a cookie with a name containing "user" and a value containing "guest" shoul
>
> The trait is opt-in: `use` it in the context and it is active with no further
> configuration. Every field is individually toggleable by overriding its
> `diagnosticsGetShow*()` method to return FALSE, and each value source
> degrades gracefully to nothing when the driver cannot provide it - a failed
> step is never turned into a different failure by this trait.
> `diagnosticsGetShow*()` method to return FALSE. Each value source degrades
> to nothing when the driver cannot provide it, so a failed step is never
> turned into a different failure.
> <br/><br/>
> Skip processing with tags: `@behat-steps-skip:DiagnosticsTrait`.
> <br/><br/>
Expand All @@ -565,13 +565,12 @@ Then a cookie with a name containing "user" and a value containing "guest" shoul
> - Works on any element that handles native `drop` events (Dropzone.js,
> custom drop targets, framework widgets).
> <br/><br/>
> Why this exists: Mink's `attachFile` writes each file to a hidden
> `<input type="file">` sequentially, so file A finishes uploading before
> file B starts. Real users release multiple files together, which fires a
> single `drop` event whose `dataTransfer.files` contains all of them and
> triggers concurrent uploads. Race conditions in dedup maps, status
> indicators, error handlers, and server-side queues only reproduce under
> the multi-file path - this trait reproduces it.
> Mink's `attachFile` writes each file to a hidden `<input type="file">`
> sequentially, so file A finishes uploading before file B starts. Real users
> release multiple files together, which fires a single `drop` event whose
> `dataTransfer.files` contains all of them and triggers concurrent uploads.
> Race conditions in dedup maps, status indicators, error handlers and
> server-side queues reproduce only under the multi-file path.
> <br/><br/>
> `@javascript`-only: requires a headless browser session.

Expand Down Expand Up @@ -2379,8 +2378,8 @@ Then the link "Return to site content" should not be an absolute link
> resolve identically. Keys are unique across groups, so the group a key was
> declared in does not take part in the lookup.
> <br/><br/>
> Resolution keys off the token's own braces rather than the placeholder name,
> so one map covers every step taking a string without the step opting in.
> The transform matches the token's braces rather than a placeholder name, so
> one map covers every string argument without the step opting in.
> <br/><br/>
> Operates on Gherkin text alone: no Mink session and no driver, so the trait
> works in any suite.
Expand Down Expand Up @@ -2608,7 +2607,7 @@ Then the following warning messages should not exist:
[Source](src/Steps/Generic/MetatagTrait.php), [Example](tests/behat/features/metatag.feature)

> Assert `<meta>` tags and head/SEO markup in page markup.
> - Assert presence and content of meta tags with proper attribute handling.
> - Assert presence and content of meta tags.
> - Verify meta tag content is free of HTML markup.
> - Assert canonical URL, robots directives and indexability.
> - Assert hreflang alternates are valid and reciprocal.
Expand Down Expand Up @@ -3920,8 +3919,8 @@ Then the link "Delete" should not exist in the row "Article title"
> every step that navigates or submits.
> <br/><br/>
> Mink's own AJAX wait watches `jQuery.active` alone, while Drupal renders many
> updates through `Drupal.ajax`, so an assertion following a click can read the
> page before the update lands. The wait here watches both.
> updates through `Drupal.ajax`. An assertion following a click can read the
> page before the update applies, so the wait here watches both.
> <br/><br/>
> Skip the automatic waits with tag: `@behat-steps-skip:WaitTrait`.

Expand Down Expand Up @@ -4446,29 +4445,29 @@ When I wait for the batch job to finish
> <br/><br/>
> Drupal BigPipe streams parts of a page in after the initial response and
> replaces its `<span data-big-pipe-placeholder-id="...">` markers with the
> real markup using JavaScript. Assertions that run before those replacements
> land intermittently fail with "element not found". When this trait is
> included, every `@javascript` scenario waits - before each step - until no
> BigPipe placeholder markers remain in the DOM, removing that race without an
> explicit step.
> real markup using JavaScript. An assertion that runs before those
> replacements complete fails intermittently with "element not found".
> <br/><br/>
> The wait is best-effort: on timeout the step still runs, so a genuinely stuck
> placeholder surfaces as the real assertion failure rather than being masked
> here.
> With this trait included, every `@javascript` scenario waits before each
> step until no BigPipe placeholder marker remains in the DOM, which removes
> that race without an explicit step.
> <br/><br/>
> A driver that runs no JavaScript never replaces those placeholders and does
> not follow the `http-equiv=refresh` fallback either, so an authenticated-user
> assertion silently misses whatever BigPipe deferred. Tag such a scenario
> `@bigpipe` and the `big_pipe_nojs` cookie is set for it, which makes Drupal
> render the page in full server-side.
> The wait is best-effort: on timeout the step still runs, so a placeholder
> that is never replaced fails the following assertion rather than the wait.
> <br/><br/>
> A driver that runs no JavaScript never replaces those placeholders, and does
> not follow the `http-equiv=refresh` fallback either. An authenticated-user
> assertion on such a driver silently misses whatever BigPipe deferred. A
> scenario tagged `@bigpipe` gets the `big_pipe_nojs` cookie, which makes
> Drupal render the page in full server-side.
> <br/><br/>
> Skip processing with tag: `@behat-steps-skip:BigPipeTrait`.
> <br/><br/>
> Special tags:
> - `@bigpipe` - render server-side on a driver without JavaScript.
>
> Override `bigPipeGetWaitTimeout()` (or set `$bigPipeWaitTimeout`) in your
> `FeatureContext` to change the maximum wait.
> Override `bigPipeGetWaitTimeout()` (or set `$bigPipeWaitTimeout`) in the
> consuming `FeatureContext` to change the maximum wait.


## Drupal\BlockTrait
Expand Down Expand Up @@ -7058,7 +7057,7 @@ Then the taxonomy term "Apple" from the vocabulary "Fruits" should not exist

> Control system time in tests using Drupal state overrides.
> <br/><br/>
> IMPORTANT: This trait requires your application to use a mockable time
> This trait requires the consuming application to use a mockable time
> service that checks Drupal state for time overrides.
> <br/><br/>
> Example implementation:
Expand Down
4 changes: 2 additions & 2 deletions behat.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
/**
* Reference configuration setting every option this package accepts.
*
* The suite in this repository runs from 'behat.php', which takes precedence,
* so Behat never loads this file here.
* The suite in this repository runs from 'behat.php'. That file takes
* precedence, so Behat never loads this one here.
*/

declare(strict_types=1);
Expand Down
8 changes: 4 additions & 4 deletions behat.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@

$suite = (new Suite('default'))
->withPaths('%paths.base%/tests/behat/features')
->addContext('FeatureContext')
->addContext('BehatCliContext')
->addContext(FeatureContext::class)
->addContext(BehatCliContext::class)
->addContext(MinkContext::class)
->addContext(ScreenshotContext::class)
->addContext(PhpServerContext::class, [
Expand Down Expand Up @@ -65,8 +65,8 @@
'drupal' => ['drupal_root' => 'web'],
'drush' => [
'root' => 'web',
// Drush resolves a request URI for every command, and without one it
// refuses to bootstrap.
// Drush resolves a request URI for every command and does not
// bootstrap without one.
'global_options' => '--uri=http://nginx:8080',
],
'selectors' => [
Expand Down
Loading
Loading