Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function main(array $options = []): void {
require_once $base_path . '/tests/behat/bootstrap/FeatureContextTrait.php';
require_once $base_path . '/tests/behat/bootstrap/FeatureContext.php';

$info = extract_info(FeatureContext::class, [FeatureContextTrait::class, 'HelperTrait'], $base_path);
$info = extract_info(FeatureContext::class, [FeatureContextTrait::class, 'HelperTrait', 'EntityFixtureTrait'], $base_path);

$errors = validate($info);

Expand Down
19 changes: 19 additions & 0 deletions src/Drupal/ContentTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use Behat\Gherkin\Node\TableNode;
use Behat\Mink\Exception\ExpectationException;
use DrevOps\BehatSteps\HelperTrait;
use Drupal\DrupalExtension\Hook\Attribute\BeforeNodeCreate;
use Drupal\DrupalExtension\Hook\Scope\BeforeNodeCreateScope;
use Drupal\node\Entity\Node;
use Drupal\node\NodeAccessControlHandlerInterface;
use Drupal\node\NodeInterface;
Expand All @@ -24,6 +26,7 @@
*/
trait ContentTrait {

use EntityFixtureTrait;
use HelperTrait;

/**
Expand Down Expand Up @@ -303,6 +306,22 @@ public function contentAssertNotExistsWithTitle(string $content_type, string $ti
}
}

/**
* Expand fixture file paths for file/image fields on nodes.
*
* Rewrites bare fixture filenames (e.g. 'document.pdf') on 'file' and
* 'image' field types to absolute paths under the Mink 'files_path' so
* drupal-driver's FileHandler can read and upload them during node
* creation. Without this, scenarios with file fields on nodes have to
* pre-create managed files explicitly via FileTrait.
*
* Backed by 'EntityFixtureTrait::entityFixtureExpand()'.
*/
#[BeforeNodeCreate]
public function contentBeforeNodeCreate(BeforeNodeCreateScope $scope): void {
$this->entityFixtureExpand('node', $scope->getStub());
}

/**
* Load multiple nodes with specified type and conditions.
*
Expand Down
126 changes: 126 additions & 0 deletions src/Drupal/EntityFixtureTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

declare(strict_types=1);

namespace DrevOps\BehatSteps\Drupal;

use Drupal\Driver\DrupalDriverInterface;
use Drupal\Driver\Entity\EntityStubInterface;

/**
* Internal helper for fixture path expansion on entity stubs.
*
* Shared by Drupal entity creation traits (e.g. ContentTrait, MediaTrait)
* that need to rewrite bare fixture filenames on 'file' and 'image' fields
* to absolute fixture paths before drupal-driver's FileHandler reads them.
*
* Requires a Drupal context: the consumer must expose 'getMinkParameter()'
* and 'getDriver()' (e.g. via MinkContext / RawDrupalContext) and Drupal
* must be bootstrapped at call time.
*
* This is an internal trait and should not be used directly in step
* definitions.
*/
trait EntityFixtureTrait {

/**
* Expand fixture file paths for file/image fields on an entity stub.
*
* Rewrites bare fixture filenames (e.g. 'document.pdf') on 'file' and
* 'image' field types to absolute paths under the Mink 'files_path' so
* drupal-driver's FileHandler can read and upload them during entity
* creation. Skips expansion when a managed file with the same basename
* already exists in public:// or private://, so existing files take
* precedence and behaviour stays backward compatible.
*
* @param string $entity_type
* The entity type machine name (e.g. 'node', 'media').
* @param \Drupal\Driver\Entity\EntityStubInterface $stub
* The entity stub mutated in place.
*/
protected function entityFixtureExpand(string $entity_type, EntityStubInterface $stub): void {
$files_path = $this->getMinkParameter('files_path');

if (empty($files_path)) {
return;
}

$resolved_files_path = realpath((string) $files_path);

if ($resolved_files_path === FALSE || !is_dir($resolved_files_path)) {
return;
}

$fixture_path = rtrim($resolved_files_path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;

$driver = $this->getDriver();

if (!$driver instanceof DrupalDriverInterface) {
return;
}

$field_types = $driver->getCore()->getEntityFieldTypes($entity_type);

foreach ($stub->getValues() as $name => $value) {
if (empty($field_types[$name]) || ($field_types[$name] !== 'image' && $field_types[$name] !== 'file')) {
continue;
}

$basename = is_array($value) ? ($value[0] ?? NULL) : $value;

if (!is_string($basename) || $basename === '') {
continue;
}

if (str_contains($basename, '/') || str_contains($basename, '\\') || $basename !== basename($basename)) {
continue;
}

if ($this->entityFixtureManagedFileExists($basename)) {
continue;
}

if (!is_file($fixture_path . $basename)) {
continue;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (is_array($value)) {
$value[0] = $fixture_path . $basename;
$stub->setValue($name, $value);
}
else {
$stub->setValue($name, $fixture_path . $basename);
}
}
}
Comment on lines +41 to +95
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 🏗️ Heavy lift

Split entityFixtureExpand() into smaller helpers to reduce risk.

This method currently handles path resolution, driver checks, field filtering, basename validation, managed-file lookup, and value mutation in one block. Please extract at least basename normalization/validation and stub-value rewrite into dedicated helpers to lower cyclomatic/NPath complexity and make future changes safer.

🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 41-95: The method entityFixtureExpand() has a Cyclomatic Complexity of 18. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


[warning] 41-95: The method entityFixtureExpand() has an NPath complexity of 9228. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


[error] 41-95: The parameter $entity_type is not named in camelCase. (undefined)

(CamelCaseParameterName)


[error] 91-93: The method entityFixtureExpand uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Drupal/EntityFixtureTrait.php` around lines 41 - 95, The
entityFixtureExpand method is doing multiple responsibilities (path resolution,
driver checks, field filtering, basename validation, and stub value mutation);
split it by extracting at least two helper methods: one like
normalizeAndValidateBasename(string|array $value): ?string to encapsulate the
basename extraction/validation logic currently using basename(), str_contains
checks and array handling, and another like rewriteStubValue(EntityStubInterface
$stub, string $fieldName, string $filePath, $originalValue): void to encapsulate
the logic that sets $stub->setValue($name, ...) for both scalar and array cases;
update entityFixtureExpand to call getMinkParameter/getDriver and
entityFixtureManagedFileExists as before but delegate basename handling to
normalizeAndValidateBasename and the mutation to rewriteStubValue so the loop
only coordinates checks and delegates specific behavior to these new helpers
(refer to symbols entityFixtureExpand, normalizeAndValidateBasename,
rewriteStubValue, entityFixtureManagedFileExists, getMinkParameter, getDriver,
and EntityStubInterface::setValue/getValues).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not adopting. PHPMD is not part of this project's enforced lint pipeline (see phpstan.neon and composer.json), so the cyclomatic/NPath thresholds it cites are not a project standard. The pre-refactor MediaTrait::mediaExpandEntityFieldsFixtures() carried a similar shape and accepted the same trade-off. The current method uses a flat guard/continue pattern that mirrors the existing style across this codebase (see MediaTrait::mediaExpandEntityFields(), FileTrait::fileCreateEntity(), etc.) and reads top-to-bottom as a series of preconditions followed by a single rewrite. Splitting basename normalization and stub mutation into helpers would push two-line operations behind two new symbols without changing what the code does, and would obscure the linear precondition flow that makes it auditable today.

The entity_type snake_case is intentional and is the convention this library uses for local variables and method arguments (see CLAUDE.md).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AlexSkrypnyk, understood — I'll defer to the project's established conventions. The flat guard/continue pattern and snake_case parameter names are clearly intentional, and PHPMD thresholds aren't meaningful here since the tool isn't part of the enforced pipeline.

(=^・ω・^=)∫


✏️ Learnings added
Learnt from: AlexSkrypnyk
Repo: drevops/behat-steps PR: 634
File: src/Drupal/EntityFixtureTrait.php:41-95
Timestamp: 2026-05-12T07:07:50.984Z
Learning: In drevops/behat-steps, PHPMD is NOT part of the enforced lint/static-analysis pipeline. The project uses PHPStan (phpstan.neon) and custom rules defined in composer.json. PHPMD cyclomatic/NPath complexity warnings should not be raised as review concerns in this repository.

Learnt from: AlexSkrypnyk
Repo: drevops/behat-steps PR: 634
File: src/Drupal/EntityFixtureTrait.php:41-95
Timestamp: 2026-05-12T07:07:50.984Z
Learning: In drevops/behat-steps, snake_case is the intentional convention for method parameter names and local variables (e.g. `$entity_type`, `$files_path`), as documented in CLAUDE.md. Do not flag snake_case parameters as violations of camelCase naming conventions in this project.

Learnt from: AlexSkrypnyk
Repo: drevops/behat-steps PR: 634
File: src/Drupal/EntityFixtureTrait.php:41-95
Timestamp: 2026-05-12T07:07:50.984Z
Learning: In drevops/behat-steps, the preferred coding style for multi-condition validation methods is a flat guard/continue (early-return) pattern — a linear series of precondition checks followed by a single mutation. This is established across MediaTrait, FileTrait, and similar traits. Do not suggest splitting such methods into multiple smaller helpers, as it obscures the auditable top-to-bottom precondition flow that the project intentionally maintains.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: AlexSkrypnyk
Repo: drevops/behat-steps PR: 560
File: src/ResponsiveTrait.php:173-173
Timestamp: 2026-04-09T08:32:59.254Z
Learning: In this project (drevops/behat-steps), Behat hook methods annotated with attributes like #[BeforeStep], #[BeforeScenario] (and similar Behat hook attributes) must keep their required scope parameter in the method signature (e.g., `BeforeStepScope $scope`, `BeforeScenarioScope $scope`) as required by the Behat framework contract. Even if the parameter is not referenced inside the method body, do not rename it to an unused placeholder like `$_scope` or otherwise change its type/name—treat it as structurally required. PHPMD `UnusedFormalParameter` warnings on these specific hook-method scope parameters are expected false positives and should be ignored.

Learnt from: AlexSkrypnyk
Repo: drevops/behat-steps PR: 623
File: STEPS.md:2647-2650
Timestamp: 2026-04-10T08:13:52.837Z
Learning: In drevops/behat-steps, docs generation (docs.php when converting docblock code/endcode blocks into STEPS.md) strips `/*` sequences from examples. When writing glob/wildcard path patterns inside trait docblocks, avoid a literal slash-star `/*`—use patterns with a trailing `*` but no preceding slash. For example, write `/news*` instead of `/news/*`, so the glob survives generation and renders correctly in STEPS.md.


/**
* Check whether a managed file with the given basename already exists.
*
* Mirrors drupal-driver FileHandler::resolveExistingFile() for bare
* basenames so callers do not pre-empt the driver's own lookup.
*
* @param string $basename
* Candidate basename (no path separators).
*
* @return bool
* TRUE when a managed file exists at public://basename or
* private://basename.
*/
protected function entityFixtureManagedFileExists(string $basename): bool {
if (str_contains($basename, '/') || str_contains($basename, '\\')) {
return FALSE;
}

$storage = \Drupal::entityTypeManager()->getStorage('file');

foreach (['public', 'private'] as $scheme) {
if ($storage->loadByProperties(['uri' => $scheme . '://' . $basename])) {
return TRUE;
}
}

return FALSE;
}

}
43 changes: 4 additions & 39 deletions src/Drupal/MediaTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
*/
trait MediaTrait {

use EntityFixtureTrait;
use HelperTrait;

/**
Expand Down Expand Up @@ -360,49 +361,13 @@ protected function mediaExpandEntityFields(EntityStub $stub): void {
/**
* Expand entity fields with fixture values.
*
* Backed by 'EntityFixtureTrait::entityFixtureExpand()'.
*
* @param \Drupal\Driver\Entity\EntityStub $stub
* The entity stub.
*/
protected function mediaExpandEntityFieldsFixtures(EntityStub $stub): void {
if (!empty($this->getMinkParameter('files_path'))) {
$fixture_path = rtrim((string) realpath($this->getMinkParameter('files_path')), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
}

// @codeCoverageIgnoreStart
if (empty($fixture_path) || !is_dir($fixture_path)) {
throw new \RuntimeException('Fixture files path is not set or does not exist. Check that the "files_path" parameter is set for Mink.');
}
// @codeCoverageIgnoreEnd
$fields = $stub->getValues();

$driver = $this->getDriver();
if (!$driver instanceof DrupalDriverInterface) {
// @codeCoverageIgnoreStart
throw new \RuntimeException(sprintf('The active Drupal driver "%s" does not support content operations required for media field expansion.', $driver::class));
// @codeCoverageIgnoreEnd
}

$field_types = $driver->getCore()->getEntityFieldTypes('media');

foreach ($fields as $name => $value) {
if (!str_contains((string) $name, 'field_')) {
continue;
}

if (!empty($field_types[$name]) && ($field_types[$name] == 'image' || $field_types[$name] == 'file')) {
if (is_array($value)) {
if (!empty($value[0]) && is_file($fixture_path . $value[0])) {
$value[0] = $fixture_path . $value[0];
$stub->setValue($name, $value);
}
}
// @codeCoverageIgnoreStart
elseif (is_file($fixture_path . $value)) {
$stub->setValue($name, $fixture_path . $value);
}
// @codeCoverageIgnoreEnd
}
}
$this->entityFixtureExpand('media', $stub);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions tests/behat/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use DrevOps\BehatSteps\Drupal\DraggableviewsTrait;
use DrevOps\BehatSteps\Drupal\EckTrait;
use DrevOps\BehatSteps\Drupal\EmailTrait;
use DrevOps\BehatSteps\Drupal\EntityFixtureTrait;
use DrevOps\BehatSteps\Drupal\FileTrait;
use DrevOps\BehatSteps\Drupal\MediaTrait;
use DrevOps\BehatSteps\Drupal\MenuTrait;
Expand Down Expand Up @@ -67,6 +68,7 @@ class FeatureContext extends DrupalContext {
use EckTrait;
use ElementTrait;
use EmailTrait;
use EntityFixtureTrait;
use FieldTrait;
use FileDownloadTrait;
use IframeTrait;
Expand Down
19 changes: 19 additions & 0 deletions tests/behat/features/drupal_content.feature
Original file line number Diff line number Diff line change
Expand Up @@ -352,3 +352,22 @@ Feature: Check that ContentTrait works
"""
Unable to find "page" content with title "[TEST] Non-existing".
"""

@api
Scenario: Assert file field on node resolves bare fixture filename without explicit managed file
Given the following article content:
| title | field_file |
| [TEST] Fixture file | text.txt |
And I am logged in as a user with the "administrator" role
When I visit the "article" content edit page with the title "[TEST] Fixture file"
Then I should see "[TEST] Fixture file"
And the response should contain ".txt"

@api
Scenario: Assert image field on node resolves bare fixture filename without explicit managed file
Given the following article content:
| title | field_image |
| [TEST] Fixture image | image.png |
And I am logged in as a user with the "administrator" role
When I visit the "article" content edit page with the title "[TEST] Fixture image"
Then I should see "[TEST] Fixture image"
Comment on lines +367 to +373
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Strengthen the image-fixture scenario with a field-specific assertion.

This scenario currently proves node creation, but not necessarily that field_image was populated from image.png. Add an assertion tied to the uploaded image value.

Suggested tweak
   `@api`
   Scenario: Assert image field on node resolves bare fixture filename without explicit managed file
     Given the following article content:
       | title                 | field_image |
       | [TEST] Fixture image  | image.png   |
     And I am logged in as a user with the "administrator" role
     When I visit the "article" content edit page with the title "[TEST] Fixture image"
     Then I should see "[TEST] Fixture image"
+    And the response should contain "image.png"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Scenario: Assert image field on node resolves bare fixture filename without explicit managed file
Given the following article content:
| title | field_image |
| [TEST] Fixture image | image.png |
And I am logged in as a user with the "administrator" role
When I visit the "article" content edit page with the title "[TEST] Fixture image"
Then I should see "[TEST] Fixture image"
Scenario: Assert image field on node resolves bare fixture filename without explicit managed file
Given the following article content:
| title | field_image |
| [TEST] Fixture image | image.png |
And I am logged in as a user with the "administrator" role
When I visit the "article" content edit page with the title "[TEST] Fixture image"
Then I should see "[TEST] Fixture image"
And the response should contain "image.png"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/behat/features/drupal_content.feature` around lines 367 - 373, Add a
field-specific assertion to the "Assert image field on node resolves bare
fixture filename without explicit managed file" scenario so it verifies the
image was actually populated: after visiting the article edit page for "[TEST]
Fixture image" add a step that asserts the field_image contains the uploaded
file (for example, a step that checks for an <img> whose src contains
"image.png" or a textual file label "image.png") so the scenario ensures
field_image was set from image.png rather than only proving node creation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not adopting. The drupal-driver FileHandler::uploadAndSave() rewrites the destination URI to public:// + uniqid() + .{ext}, so the original basename image.png is not preserved in the saved file. Asserting the response should contain "image.png" would fail because the edit page renders the file with its stored filename (e.g. 5fa3c41e89af2.png), not the source fixture filename. The existing title assertion is enough to prove the article was created with the hook in effect; if the upload had failed the hook would have thrown and node creation would have aborted before the scenario reached the assertion step.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AlexSkrypnyk, understood — that's a clear and well-reasoned explanation. Since FileHandler::uploadAndSave() replaces the original basename with a uniqid()-based name, asserting the source filename in the response would be unreliable. The title assertion serving as an indirect proof of the hook's effect (a failed upload throws before node creation completes) makes sense for this scenario.

( ˘ ω˘ *)


✏️ Learnings added
Learnt from: AlexSkrypnyk
Repo: drevops/behat-steps PR: 634
File: tests/behat/features/drupal_content.feature:367-373
Timestamp: 2026-05-12T04:47:00.917Z
Learning: In drevops/behat-steps Behat feature files for ContentTrait/MediaTrait image field fixture scenarios, do NOT assert the original fixture filename (e.g. `image.png`) in the response after node/media creation. The drupal-driver `FileHandler::uploadAndSave()` rewrites the destination URI to `public://` + `uniqid()` + `.{ext}`, so the original basename is not preserved in the stored file. The edit page renders the stored (uniqid-based) filename, not the source fixture name. A title assertion is sufficient proof the BeforeNodeCreate hook worked correctly — a failed upload would throw before node creation completes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: AlexSkrypnyk
Repo: drevops/behat-steps PR: 555
File: tests/behat/features/metatag.feature:66-69
Timestamp: 2026-04-08T06:59:35.371Z
Learning: In Behat feature files under tests/behat/features/**/*.feature, for scenarios tagged with `trait:*`, prefer triple double quotes (`"""`) for PyStrings unless the step definitions invoked inside those scenarios actually accept PyString arguments. Only use triple single quotes (`'''`) when you need to embed a PyString literal inside the outer PyString (to avoid nesting `"""` within `"""`). If the inner steps are simple one-liners and do not take PyString arguments, `"""` is sufficient.

Learnt from: AlexSkrypnyk
Repo: drevops/behat-steps PR: 555
File: tests/behat/features/metatag.feature:52-60
Timestamp: 2026-04-08T07:00:10.945Z
Learning: In Behat feature files under tests/behat/features/**/*.feature, follow the repo tagging convention: positive scenarios that exercise static fixtures (e.g., visiting prebuilt HTML like tests/behat/fixtures/*.html, with no Drupal API usage) should be intentionally left untagged. Use tags only for integration/negative behavior or trait-scoped scenarios (e.g., api for Drupal/API interactions, and trait:TraitName for scenarios that belong to a specific trait), as in element.feature and metatag.feature. If a scenario uses only static fixture HTML and no Drupal API, it should not be tagged with api or trait.

Learnt from: AlexSkrypnyk
Repo: drevops/behat-steps PR: 592
File: tests/behat/features/table.feature:6-7
Timestamp: 2026-04-09T12:24:16.774Z
Learning: In drevops/behat-steps Behat feature files under tests/behat/features/**/*.feature, use `trait:TraitName` only for negative/failure scenarios that run behat-within-behat via `BehatCliContext` (scenarios that invoke `behat --no-colors` to assert expected failure output). For positive “works as expected” scenarios that run directly against the Drupal site, use only the `api` tag and intentionally do not add `trait:TraitName`. When reviewing, flag suggestions to add `trait:TraitName` to positive scenarios, since it should be absent by design and this convention should be consistent across trait-related feature files (e.g., element.feature, drupal_content.feature, table.feature).

Learnt from: AlexSkrypnyk
Repo: drevops/behat-steps PR: 592
File: tests/behat/features/table.feature:38-40
Timestamp: 2026-04-09T12:25:02.540Z
Learning: In tests/behat/features/**/*.feature, when you have a negative scenario under `trait:*` and you nest step definitions inside a `scenario steps tagged with "api"`, you may use DrupalContext content-creation steps such as `page content:` (and similar steps like `landing_page content:`). These nested content-creation steps should be available and work correctly because BehatCliContext generates a feature file tagged with `api`, which enables the full DrupalContext (including `createNodes()`). Therefore, do not apply the guideline "avoid custom steps in negative tests" to these content-creation steps when `scenario steps tagged with "api"` is used.

Learnt from: AlexSkrypnyk
Repo: drevops/behat-steps PR: 598
File: tests/behat/features/xml.feature:613-616
Timestamp: 2026-04-09T15:06:13.608Z
Learning: In drevops/behat-steps feature files under tests/behat/features/**/*.feature, for positive scenarios that exercise debug/print steps (e.g., printing the last XML response), do not add assertions that require specific output content (avoid adding steps like “Then I should see …”). It is sufficient for these scenarios to verify the step runs successfully without throwing errors; adding output-content checks would couple the test to fixture content.

Learnt from: AlexSkrypnyk
Repo: drevops/behat-steps PR: 621
File: tests/behat/features/drupal_state.feature:17-25
Timestamp: 2026-04-10T07:58:00.186Z
Learning: In `drevops/behat-steps` Gherkin `.feature` files, don’t request separate JSON object (`{"a":1}`) scenarios when the JSON object needs to appear inside a double-quoted step argument (e.g., `Given ... "{"a":1}"`). Gherkin cannot represent escaped double quotes inside quoted step text, so JSON object literals can’t be expressed reliably that way. Prefer the existing indirect coverage strategy (e.g., JSON array scenarios that hit the same `json_decode()` code path and thus cover the `stateNormaliseValue()` JSON normalization branches) rather than suggesting additional JSON object scenarios purely for direct representation.